Linux驱动开发基础 第一个字符设备驱动

实验简介

Linux 中的三大类驱动:字符设备驱动、块设备驱动和网络设备驱动。

其中字符设备驱动是占用篇幅最大的一类驱动,因为字符设备最多,从最简单的 LED 到 I2C、SPI、音频等都属于字符设备驱动的类型。

块设备和网络设备驱动要比字符设备驱动复杂,就是因为其复杂所以半导体厂商一般都给我们编写好了,大多数情况下都是直接可以使用的。

所谓的块设备驱动就是存储器设备的驱动,比如 EMMC、NAND、SD 卡和 U 盘等存储设备,因为这些存储设备的特点是以存储块为基础,因此叫做块设备。

网络设备驱动就更好理解了,就是网络驱动,不管是有线的还是无线的,都属于网络设备驱动的范畴。

本实验从 Linux 驱动开发中最基础的字符设备驱动开始,学习搭建一个 Linux 下最简单的字符设备驱动开发框架

实验原理

字符设备就是一个一个字节,按照字节流进行读写操作的设备,读写数据是分先后顺序的。比如我们最常见的 LED、按键、IIC、SPI,LCD 等等都是字符设备,这些设备的驱动就叫做字符设备驱动。

先来简单的了解一下 Linux 下的应用程序是如何调用驱动程序,Linux 应用程序对驱动程序的调用如下图所示:

8

在 Linux 中一切皆为文件,驱动加载成功以后会在“/dev”目录下生成一个相应的文件,应用程序通过对这个名为“/dev/xxx”(xxx 是具体的驱动文件名字)的文件进行相应的操作即可实现对硬件的操作。

比如现在有个叫做/dev/led 的驱动文件,此文件是 led 的驱动文件。

当用户想要通过应用程序操作控制一盏 led 灯亮起或者熄灭的时候,操作的是应用程序中的函数。然后该函数会调用库函数,譬如常见的标准库。

库函数又会调用系统,将指令陷入内核中。进入内核之后,便会找到对应的驱动程序接口,进而执行驱动程序中的内容,从而控制 led 灯。

实验目的

开发一个HelloWorld字符设备驱动,这个字符设备并没有对应的硬件,是虚拟的,里面包含读写缓冲区和一个100字节的字符缓冲区,存放有字符串"Hello World!",可以读取字符设备中的字符串,也可以向字符设备写入新的字符串(再读取)。

实验流程

代码编写

一般代码编写需要写好三类文件

  1. 驱动代码文件 将编译为驱动程序(.ko文件),之后在开发板上使用insmod xxx.ko来加载驱动
  2. 应用测试文件 将编译为可执行程序,之后在开发板上执行,测试驱动代码文件的编写是否符合要求
  3. Makefile 规定了内核与交叉编译环境,同时规定了前两个文件是如何编译的

驱动代码文件HelloWorld.c的内容

//添加头文件
#include <linux/init.h>
#include <linux/module.h>
#include <linux/ide.h>
static char readbuf[100];  // 读缓冲区
static char writebuf[100]; // 写缓冲区
static char message[100] = {"Hello World! comes from kernel."};

static int drive_major; //设备号
static struct class *HelloWorld_cls;

static int HelloWorld_open(struct inode *inode, struct file *filp) //打开函数
{
	//本DEMO无需申请资源,此处留白
	printk("-HelloWorld open-\n");
	return 0;
}
static ssize_t HelloWorld_read(struct file *filp, char __user *buf, size_t count, loff_t *fops) //用户读取,内核发送信息
{
	int flag = 0;
	memcpy(readbuf, message, sizeof(message)); //使用memcpy将内核中要发送的内容写入读缓冲区
	flag = copy_to_user(buf, readbuf, count);  //使用copy_to_user函数将读缓冲区的内容发送到用户态
	if (flag == 0)							   //返回0成功,否则失败
	{
		printk("Kernel send data success!\n");
	}
	else
	{
		printk("Kernel send data failed!\n");
	}
	printk("-HelloWorld read-\n");
	return 0;
}
static ssize_t HelloWorld_write(struct file *filp, const char __user *buf, size_t count, loff_t *fops) //用户发送,内核读取信息并打印
{
	int flag = 0;
	flag = copy_from_user(writebuf, buf, count); //使用copy_from_user读取用户态发送过来的数据
	memcpy(message, writebuf, sizeof(writebuf));
	if (flag == 0)
	{
		printk(KERN_CRIT "Kernel receive data: %s\n", writebuf);
	}
	else
	{
		printk("Kernel receive data failed!\n");
	}
	printk("-HelloWorld write-\n");
	return 0;
}
static int HelloWorld_release(struct inode *inode, struct file *filp) //释放设备
{
	//由于open函数并没有占用什么资源,因此无需释放
	printk("-HelloWorld release-\n");
	return 0;
}

// 驱动文件描述集合
static struct file_operations drive_fops = {
	.owner = THIS_MODULE,
	.open = HelloWorld_open,
	.read = HelloWorld_read,
	.write = HelloWorld_write,
	.release = HelloWorld_release,
};

// 装载入口函数
static __init int HelloWorld_init(void)
{
	printk("-------^v^-------\n");
	printk("-HelloWorld init-\n");

	//设备的申请
	//申请主设备号
	//参数1----需要的主设备号,>0静态分配, ==0自动分配
	//参数2----设备的描述 信息,体现在cat /proc/devices, 一般自定义
	//参数3----文件描述集合
	//返回值,小于0报错
	drive_major = register_chrdev(0, "HelloWorld", &drive_fops);
	if (drive_major < 0) //判断是否申请成功
	{
		printk("register chrdev faile!\n");
		return drive_major;
	}
	else
	{
		printk("register chrdev ok!\n");
	}

	//自动创建设备节点
	//创建设备的类别
	//参数1----设备的拥有者,当前模块,直接填THIS_MODULE
	//参数2----设备类别的名字,自定义
	//返回值:类别结构体指针,其实就是分配了一个结构体空间
	HelloWorld_cls = class_create(THIS_MODULE, "HelloWorld_class");
	printk("class create ok!\n");

	//创建设备
	//参数1----设备对应的类别
	//参数2----当前设备的父类,直接填NULL
	//参数3----设备节点关联的设备号
	//参数4----私有数据直接填NULL
	//参数5----设备节点的名字
	device_create(HelloWorld_cls, NULL, MKDEV(drive_major, 0), NULL, "HelloWorld_%d", 0);
	printk("device create ok!\n");

	return 0;
}

// 卸载入口函数
static __exit void HelloWorld_exit(void)
{
	// 设备的注销
	device_destroy(HelloWorld_cls, MKDEV(drive_major, 0)); //删除设备
	class_destroy(HelloWorld_cls);							//删除类
	unregister_chrdev(drive_major, "HelloWorld");			//注销主设备号
	printk("-------^v^-------\n");
	printk("-HelloWorld exit-\n");
}

//申明装载入口函数和卸载入口函数
module_init(HelloWorld_init);
module_exit(HelloWorld_exit);

//添加各类信息
MODULE_LICENSE("GPL");
MODULE_AUTHOR("msxbo");

应用测试文件HelloWorldApp.c的内容

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char *argv[])
{
	int fd, retvalue;
	char *filename="/dev/HelloWorld_0";
	char readbuf[100], writebuf[100];


	fd = open(filename, O_RDWR); //打开设备
	if (fd < 0)
	{
		printf("Can't open file %s\n", filename);
		return -1;
	}

	switch (*argv[1]) //对操作数进行解析
	{
	case 'r':
		if (argc != 2) //进行鲁棒性检查
		{
			printf("Unknow operation, use the formate: ./APPNAME /dev/DRIVENAME r to read date from kernel.\n");
			return -1;
		}
		retvalue = read(fd, readbuf, 100);
		if (retvalue < 0) //检查是否读取成功
		{
			printf("Read file %s failed!\n", filename);
		}
		else
		{
			printf("User receive data: %s\n", readbuf);
		}
		break;
	case 'w':
		if (argc != 3) //进行鲁棒性检查
		{
			printf("Unknow operation, use the formate: ./APPNAME /dev/DRIVENAME w \"USERDATE\" to write date to kernel.\n");
			return -2;
		}
		memcpy(writebuf, argv[2], strlen(argv[2])); //将内容拷贝到缓冲区
		retvalue = write(fd, writebuf, 50);			//写数据
		if (retvalue < 0)
		{
			printf("Write file %s failed!\n", filename);
		}
		else
		{
			printf("Write file success!\n");
		}
		break;
	default:
		printf("Unknow Operation: %d\n", *argv[1]);
		break;
	}

	retvalue = close(fd); //关闭设备
	if (retvalue < 0)
	{
		printf("Can't close file %s\n", filename);
		return -1;
	}

	return 0;
}

Makefile的内容

#已经编译过的内核源码路径
KERNEL_DIR = /home/uisrc/uisrc-lab-xlnx/sources/kernel

export ARCH=arm64
export CROSS_COMPILE=aarch64-linux-gnu-

#当前路径
CURRENT_DIR = $(shell pwd)

MODULE = HelloWorld
APP = HelloWorldApp

all : 
#进入并调用内核源码目录中Makefile的规则, 将当前的目录中的源码编译成模块
	make -C $(KERNEL_DIR) M=$(CURRENT_DIR) modules 
	rm -rf *.symvers *.order *.o *.mod.o *.mod.c

ifneq ($(APP), )
	$(CROSS_COMPILE)gcc $(APP).c -o $(APP)
endif

clean : 
	make -C $(KERNEL_DIR) M=$(CURRENT_DIR) clean
	rm $(APP)

#指定编译哪个文件
obj-m += $(MODULE).o

交叉编译

这三个文件都编写好后,就传输到虚拟机Linux开发环境中,然后放到一个目录下make即可

6

make完的结果如下,新生成的两个文件传输到开发板中

6.1

板上执行与测试

将生成的两个文件置于合适的目录下,给HelloWorldApp添加实行权限

chmod +x HelloWorldApp

加载驱动

insmod HelloWorld.ko

执行应用程序,可以进行读和写两种操作

读的形式为

./HelloWorldApp r

写的形式为

./HelloWorldApp w "string to write"

具体执行和测试情况如下图所示,初始字符缓冲区的内容为"Hello World! comes from kernel.",后面会随着不同写入内容而改变

7


本文章使用limfx的vscode插件快速发布