Linux内核字符设备驱动的实现过程
Linux内核描述字符设备驱动的硬件操作接口数据结构
struct file_operations{ open, close, read, write, };
Linux内核描述字符设备驱动的数据结构
struct cdev{ const struct file_operations *ops;//硬件操作接口结构对象 dev_t dev;//保存申请的设备号 unsigned int count;//次设备号的个数。 ... };
配套函数
void cdev_init(struct cdev *cdev, struct file_operations *fops);
- 功能:给字符设备驱动对象添加硬件操作接口
cdev_add(struct cdev *p, dev_t dev, unsigned count);
- 功能:向内核的大数组注册一个字符设备驱动对象。
cdev_del(struct cdev *p);
- 功能:从内核大数组中删除字符设备对象。
总结:编写一个字符设备驱动的编程步骤
根据用户需求先定义初始化硬件操作接口对象
struct file_operations A={ .opern = xxx_open, .close = xxx_close, ...... }
然后定义初始化字符设备驱动对象
struct cdev B;//定义 cdev_init(&B, &A);//初始化
向内核注册字符设备对象
cdev_add(&B, 申请号的设备号,次设备号的个数);
- 至此,内核就有了一个真实的字符设备驱动存在于内存中,静静等待着应用程序利用系统调用函数来访问驱动中的各个接口函数。
在适当的地方卸载字符设备驱动对象
cdev_del(&B);
根据用户需求编写各个接口函数
int xxx_open() { //打开设备 } ...
案例:编写LED字符设备驱动,实现打开设备开灯,关闭设备关灯。
操作流程:
- 上位机执行:
mkdir /opt/drivers/day03/1.0 -p cd /opt/drivers/day03/1.0 vim led_drv.c //驱动程序 vim led_test.c //应用程序 vim Makefile make arm... gcc -o led_test led_test.c cp led_drv.ko led_test /opt/rootfs/home/drivers
- 下位机测试:
cd /home/drivers insmod led_drv.ko //调用入口函数 cat /proc/devices //查看申请到的主设备号 mknod /dev/myled c 主设备号 0 //创建设备文件,代表LED0 ./led_test //open device fail 测试失败 cd /home/drivers insmod led_drv.ko //调用入口函数 cat /proc/devices //查看申请到的主设备号 character devices://当前系统支持的字符设备,主设备号和设备名称 1 mem 5 /dev/tty 5 /dev/console 5 /dev/ptmx ... 244 myled //LED驱动申请到的主设备号就是244,设备名称为myled ... mknod /dev/myled c 主设备号 0 //创建设备文件,它代表LED设备。 ./led_test //再次执行
代码编写:
- led_drv.c
#include <linux/init.h> #include <linux/module.h> #include <linux/gpio.h> #inlcude <mach/platform.h> #include <linux/cdev.h> // struct cdev #include <linux/fs.h> //struct file_operations //声明描述LED硬件信息的数据结构 struct led_resource{ char *name; //名称 int gpio; //gpio编号 }; //定义初始化四个LED灯的硬件信息对象 static struct led_resource led_info[] = { { .name = "LED1", .gpio = PAD_GPIO_C+12 }, { .name = "LED2", .gpio = PAD_GPIO_C+17 }, { .name = "LED3", .gpio = PAD_GPIO_C+11 }, { .name = "LED4", .gpio = PAD_GPIO_B+26 } }; //打开设备接口: //执行成功返回0,失败返回负值 static int led_open(struct inode *, struct file *) { //开灯 int i; for(i = 0; i <ARRAY_SIZE(led_info); i++) { gpio_set_value(led_info[i].gpio, 0); printk("%s:打开第%d个灯.\n", __func__, i+1); } return 0;//执行成功返回0 } //关闭设备接口: static int led_close(struct inode *, struct file *) { //关灯 int i; for(i = 0; i <ARRAY_SIZE(led_info); i++) { gpio_set_value(led_info[i].gpio, 1); printk("%s:关闭第%d个灯.\n", __func__, i+1); } return 0;//执行成功返回0 } //定义初始化硬件操作接口对象 static struct file_operations led_fops = { .open = led_open, //打开设备 .release = led_close //关闭设备 }; //定义字符设备对象 static struct cdev led_cdev; //定义设备号对象 static dev_t dev; static int led_init(void) { int i; //1、申请GPIO资源,配置为输出,输出1(省电) for(i = 0; i < ARRAY_SIZE(led_info); i++){ gpio_request(led_info[i].gpio, led_info[i].name); gpio_direction_output(led_info[i].gpio, 1); } //2、申请设备号 alloc_chrdev_region(&dev, 0, 1, "myled"); printk("major: %d, minor: %d\n", MAJOR(dev), MINOR(dev)); //3、初始化字符设备对象,添加硬件操作接口 cdev_init(&led_cdev, &led_fops); //4、向内核的大数组中注册字符设备对象 //至此,内核就有了一个真实的字符设备驱动,等待应用调用。 cdev_add(&led_cdev, dev, 1); return 0; } static void led_exit(void) { //1、从内核大数组中卸载字符设备对象 cdev_del(&led_cdev); //2、释放设备号 unregister_chrdev_region(dev, 1); //3、输出1,释放GPIO资源。 for(i = 0; i < ARRAY_SIZE(led_info); i++){ gpio_set_value(led_info[i].gpio, 1); gpio_free(led_info[i].gpio); } } module_init(led_init); module_exit(led_exit); MODULE_LICENSE("GPL");
- led_test.c
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int main() { int fd; //应用open->软中断->内核sys_open->驱动led_open fd = open("/dev/myled", O_RDWR); if(fd < 0){ printf("open led device failed.\n"); return -1; } sleep(3); //应用close->软中断->内核sys_close->驱动led_close close(fd); return 0; }
- Makefile
obj-m += led_drv.o all: make -C /opt/kernel SUBDIRS=$(PWD) modules clean: make -C /opt/kernel SUBDIRS=$(PWD) clean
总结编写字符设备驱动的详细步骤
- 先搭建驱动框架:
头文件
入口函数
出口函数
此时先不要写入口和出口
- 各种该:
该声明的声明
该定义的定义
该初始化的初始化
先搞硬件后搞软件【变量】
- 填充入口和出口
先写注释
后塞代码【体力活】
- 最后编写各个接口函数
Linux字符设备驱动硬件操作接口之write接口
- 明确:Linux系统缓冲区其实就是内存,分两类:
用户缓冲区:分配的内存在用户3G虚拟内存上。
内核缓冲区:分配的内存在内核1G虚拟内存上。
回顾write系统调用函数
ssize_t write(int fd, const void *buf, size_t count);
- 功能:向硬件设备写入数据
- fd:设备文件描述符,fd代表的就是硬件,fd代表的就是
- buf:传递要写入的数据所在数据所在用户缓冲区的首地址。
- count:传递要写入的数据大小。
- 返回值:返回实际写入的字节数。
- write(fd, &cmd, sizeof(cmd)); //向硬件写入数据1
对应的底层驱动的write接口
struct file_operations { int (*open)(struct inode *, struct file *); int (*release)(struct inode *, struct file *); ssize_t (*write) (struct file *, const char __user *buf, size_t count, loff_t *ppos); }
注意:
- 底层驱动的open,release两个接口可以不用初始化(.open = led_open…),应用程序调用open,close永远返回成功,扩展内核的sys_open代码:
int sys_open(...) { if(驱动的open接口是否为NULL) return fd > 0 // 永远成功 else xxx->ops->open();//调用驱动的open接口 }
如果用户对open/close没有要求,底层驱动open/close可以不用初始化!
write接口和应用write函数的调用关系:
- 应用write->C库的write->软中断->内核的sys_write->驱动write接口->应用write返回。
- write接口的功能:向硬件设备写入数据
本质就是一个桥梁:连接用户和硬件,也就是用户数据->底层驱动write->硬件
ssize_t (*write) (struct file *, const char __user *buf, size_t count, loff_t *ppos);
- 参数说明:
file:文件指针,暂时用不着。
buf:此指针变量用“__user”修饰,说明此指针变量保存的地址一定是用户缓冲区的首地址,例如buf = &cmd.
所以底层驱动的write接口可以通过buf指针来获取用户缓冲区的数据,底层驱动write接口获取数据的代码无脑的写。
count:传递要写入的字节数,等于write的第三个参数。
ppos:保存上一次的写位置,开始值为0。
如果要记录位置,编程步骤:
1、先获取上一次的写位置:unsigned long pos = *ppos;
2、假设这次write又写了100字节,底层驱动write返回之前记得要更新写位置:*ppos = pos +100;
3、注意:应用于连续多次write操作,如果一次性write完,无需关注此参数。
注意:此种写法极其危险,两种危险情况
- 如果应用write这么写:
write(fd, NULL, 0); //直接空指针的非法访问
- 如果&cmd用户虚拟地址和物理地址的映射没有建立,会造成地址非法访问。
int copy_from_user(void *to, const void __user *from, int n);
- 所以:底层驱动要想通过buf指针来获取用户缓存区要写入的数据,必须利用内核来提供的内存拷贝函数来实现用户缓存区和内核缓冲区的数据拷贝此函数会帮你检查地址是否有效:
- 功能:拷贝用户缓冲区的数据到内核缓存区。
- 参数:
to:内核缓冲区的首地址,目的地址。
from:用户缓冲区的首地址,源地址。
n:要拷贝的字节数。
案例:编写LED字符设备驱动,实现向设备写1开灯,写0关灯。
- led_test.c
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int main() { int fd; int cmd = 0; if(argc != 2){ printf("usage : %s <on|off>\n", argv[0]); return -1; } fd = open("/dev/myled", O_RDWR); if(fd < 0){ printf("open led device failed.\n); return -1; } if(!strcmp(argv[1], "on")) { cmd = 1; } else if(!strcmp(argv[1], "off")) { cmd = 0; } //向设备驱动文件写cmd write(fd, &cmd, sizeof(cmd)); close(fd); return 0; }
- led_drv.c
#include <linux/init.h> #include <linux/module.h> #include <linux/fs.h> #include <linux/cdev.h> #include <linux/gpio.h> #include <mach/platform.h> //声明描述LED硬件信息数据结构 struct led_resource{ char *name; int gpio; }; //初始化LED硬件信息对象 static struct led_resource led_info[] = { { .name = "LED1", .gpio = PAD_GPIO_C + 12 }, { .name = "LED2", .gpio = PAD_GPIO_C + 17 }, { .name = "LED3", .gpio = PAD_GPIO_C + 11 }, { .name = "LED4", .gpio = PAD_GPIO_B + 26 } }; //向硬件设备写入数据接口 //参数对应关系 //write的fd <->led_write file; write的buf <-> led_write buf; static ssize_t led_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos) { int i; //分配内核缓冲区,暂存从用户缓冲区获取的数据 int kcmd; //拷贝用户缓冲区的数据到内核缓冲区中 //kcmd = *(int *)buf; <---危险操作,使用操作函数 copy_from_user(&cmd, buf, sizeof(kcmd)); //操作硬件 for(i = 0; i< ARRAY_SIZE(led_info); i++) { gpio_set_value(led_info[i].gpio, !kcmd); printk("%s: %s 第 %d 个灯", __func, kcmd ? "开":"关", i+1); } return count } //定义初始化LED硬件操作接口对象 static struct file_operations led_fops = { .write = led_write//向硬件写入数据 } //定义设备号对象 static dev_t dev; //定义字符设备对象 static struct cdev led_cdev; static int led_init(void) { int i; //申请GPIO资源,配置为输出,输出1 for(i = 0; i < ARRAY_SIZE(led_info); i++) { gpio_request(led_info[i].gpio, led_info[i].name); gpio_direction_output(led_info[i].gpio, 1); } //申请设备号. alloc_chrdev_region(&dev, 0, 1, "myled"); //初始化字符设备对象,添加操作接口 cdev_init(&led_cdev, &led_fops); //向内核注册字符设备对象。 cdev_add(&led_cdev, dev, 1); return 0; } static void led_exit(void) { //从内核卸载字符设备对象 cdev_del(&led_cdev); //释放设备号 unregister_chrdev_region(dev, 1); //释放GPIO资源,输出1 for(i = 0; i < ARRAY_SIZE(led_info); i++) { gpio_set_value(led_info[i].gpio, 1); gpio_free(led_info[i].gpio); } } module_init(led_init); module_exit(led_exit); MODULE_LICENSE("GPL");
- led_test.c
#include <fcntl.h> //声明描述LED操作信息的数据结构 struct led_event { int cmd;//开关灯命令,1-开灯,0-关灯。 int index;//灯编号。1、2、3、4 }; int main() { int fd; struct led_event led; if(argc != 3){ printf("usage: %s <on|off> <1|2|3|4> \n", argv[0]); return -1; } fd = open("/dev/myled", O_RDWR); if(fd < 0) return -1; if(!strcmp(argv[1], "on")) led.cmd = 1; else if(!strcmp(argv[1], "off")) led.cmd = 0; led.index = strtoul(argv[2], NULL, 0); write(fd, &led, sizeof(led)); close(fd); return 0; }