Linux字符设备驱动硬件操作接口之read接口
回顾应用程序read函数
ssize_t read(int fd, void *buf, size_t count);
- 功能:从硬件读取数据放到用户缓冲区
- 参数:
fd:设备文件描述符,它是字符设备文件代理
buf:传递用户缓冲区的首地址。
count:传递要读取的字节数。
返回值:返回实际读取的字节数。
对于底层驱动的read函数接口
ssize_t (*read)(struct file *file, char __user *buf, size_t count, loff_t *ppos);
- 调用关系:应用read->C库read->软中断->内核sys_read->驱动read。
- 接口功能:从硬件读取数据给应用程序起到桥梁的作用,连接应用和硬件。
- 参数:
file:文件指针,跟应用read的第一个参数有关。
buf:此指针变量用“__user”修饰,所以必须保存的是用户缓存区首地址。所以将来底层驱动的read接口可以从硬件读取的数据通过buf放到用户缓冲区,
(*buf = 250,此操作极其不安全),两种情况:
1、应用程序故意传递非法地址,例如read(fd, NULL,0);
2、用户缓冲区对应的虚拟内存映射无效,造成地址非法访问。
驱动read接口要想通过buf来操作用户缓冲区必须利用内核提供的内存拷贝函数:
int copy_to_user(void __user *to, void *from, int n);
功能:将内核缓冲区的数据拷贝到用户缓冲区。
to:目的地址,用户缓冲区首地址。
from:源地址,内核缓冲区首地址。
n:要拷贝的字节数。
count:传递要读取的数据字节数,等于应用read的第三个参数。
ppos:记录上一次的读位置,如果驱动read采用连续多次读取。
1、先获取上一次read的读位置,unsigned long pos = *pos;
2、假设这次read又读取了100字节,底层驱动read返回之前记得更新读位置,*ppos = pos + 100;
注意:如果读一次搞定无需关注ppos。
案例:编写LED字符设备驱动,不仅仅实现开关某个灯,还能获取灯的开关状态。
- 实验步骤同上。
- led_test.c
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> //声明描述led操作信息的数据结构 struct led_event { int cmd;//开灯命令,1-开灯,0-关灯。 int index; //灯编号 }; //声明描述led等状态的数据结构 struct led_state{ int index;//灯编号。 int state;//灯状态,1-关灯,0-开灯。 }; int main() { int fd; struct led_event led;//分配用户缓冲区,保存灯的操作信息。 struct led_state ledst;//分配用户缓冲区,保存灯的状态。 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); //将LED操作信息发生给驱动,开关灯。 write(fd, &led, sizeof(led)); //获取灯的状态 //为ledst 赋值。 ledst.index = strtol(argv[2], NULL, 0); read(fd, &ledst, sizeof(ledst)); //打印状态 printf("%d led state is %s \n", ledst.index, ledst.state? "close":"open"); 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_event { int cmd;//开灯命令,1-开灯,0-关灯。 int index; //灯编号 }; //声明描述led等状态的数据结构 struct led_state{ int index;//灯编号。 int state;//灯状态,1-关灯,0-开灯。 }; //声明描述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 } //读硬件接口 static ssize_t led_read(struct file *file, const char __user *buf, size_t count, loff_t *ppos) { //1、分配内核缓冲区,暂存数据 struct led_state kledst; //2.先从用户缓存区拷贝数据到内核缓冲区 copy_from_user(&kledst, buf, sizeof(kledst)); kledst.state = gpio_get_value(led_info[kledst.index-1].gpio); //将内核缓存区中的有效数据拷贝到用户缓冲区 copy_to_user(buf, &kledst, sizeof(kledst)); return count; } //定义初始化LED硬件操作接口对象 static struct file_operations led_fops = { .write = led_write//向硬件写入数据 .read = led_read;//从硬件读取数据 } //定义设备号对象 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");
字符设备驱动硬件操作接口之ioctl
学习掌握ioctl系统调用函数
int ioctl(int fd, int request, …);
- 函数功能:
应用程序利用此函数可以向硬件设备发生控制命令略带写write意味。
应用程序利用此函数可以和硬件进行读或者写操作,简直侵略read/write的地位。
参数:
fd:设备文件描述符。
cmd:向硬件设备发送的控制命令扩展成任何命令都可以,命令又驱动工程师自行定义。
将来驱动程序可以利用第三个参数可以读写用户缓冲区。
返回值:成功返回0,失败返回-1.
对应底层驱动的ioctl接口:
struct file_operations { long (*unlocked_ioctl)(struct file *file , unsigned int cmd, unsigned long buf); }
- 调用关系:应用程序ioctl->C库的ioctl->软中断->内核sys_ioctl->驱动ioctl接口。
- 接口功能:
驱动可以向硬件设备发送控制命令,略带写write意味。
驱动还可以和硬件进行读写操作。
- 参数:
file:文件指针,与fd有关。
cmd:保存应用程序传递的控制命令
buf:如果应用要和硬件进行读写操作,buf一般保存用户缓冲区的首地址(当然也可以保存一些普通的变量),所以将来底层驱动的ioctl接口可以通过buf对用户缓冲区进行读写访问,但是使用时注意数据类型的强转换。
具体使用buf时使用copy_from_user(&kdata, (int *)buf, 4);
例如:ioctl(fd, LED_ON);//仅仅向设备发送开灯命令。
ioctl(fd, MMA8653_WRITE, &data);//除了发送命令还要写入数据。
返回值:成功返回0,失败返回-1。
案例:编写LED字符设备驱动,实现开关灯、开关某个灯。
下位机测试:
cd /home/drivers insmod led_drv.ko cat /proc/devices mknod /dev/myled c 244 0 ./led_test on ./led_test off ./led_test on 1 ./led_test off 1
代码实现:实现开关灯
- led_test.c
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> //自定义两个命令 #define LED_ON 0x100001 #define LED_OFF 0x100002 //声明描述led操作信息的数据结构 struct led_event { int cmd;//开灯命令,1-开灯,0-关灯。 int index; //灯编号 }; //声明描述led等状态的数据结构 struct led_state{ int index;//灯编号。 int state;//灯状态,1-关灯,0-开灯。 }; int main() { int fd; struct led_event led;//分配用户缓冲区,保存灯的操作信息。 struct led_state ledst;//分配用户缓冲区,保存灯的状态。 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")) ioctl(fd, LED_ON); else if(!strcmp(argv[1], "off")) ioctl(fd, LED_OFF); close(fd); return 0; }
- led_drv.c
//自定义两个命令 #define LED_ON 0x100001 #define LED_OFF 0x100002 //控制操作接口 static long led_ioctl(struct file *file, unsigned long cmd, unsigned long buf) { int i; switch(cmd){ case LED_ON: for(i = 0; i < ARRAY_SIZE(led_info); i++){ gpio_set_value(led_info[i].gpio, 0); printk("%s : turn on %d led. \n", __func__, i+1); } break; case LED_OFF: for(i = 0; i < ARRAY_SIZE(led_info); i++){ gpio_set_value(led_info[i].gpio, 1); printk("%s : turn off %d led. \n", __func__, i+1); } break; default: printk("command is invalid\n"); return -1; } retrun 0; } //部分代码: static struct file_operations led_fops = { .unlocked_ioctl = led_ioctl };
代码实现:实现开关某个灯
• led_test.c #include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> //自定义两个命令 #define LED_ON 0x100001 #define LED_OFF 0x100002 //声明描述led操作信息的数据结构 struct led_event { int cmd;//开灯命令,1-开灯,0-关灯。 int index; //灯编号 }; //声明描述led等状态的数据结构 struct led_state{ int index;//灯编号。 int state;//灯状态,1-关灯,0-开灯。 }; int main() { int fd; int index; struct led_event led;//分配用户缓冲区,保存灯的操作信息。 struct led_state ledst;//分配用户缓冲区,保存灯的状态。 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; //获取灯编号。 index = strtoul(argv[2], NULL, 0); if(!strcmp(argv[1], "on")) ioctl(fd, LED_ON, &index);//三个参数 else if(!strcmp(argv[1], "off")) ioctl(fd, LED_OFF, &index); close(fd); return 0; }
- led_drv.c
//自定义两个命令 #define LED_ON 0x100001 #define LED_OFF 0x100002 //控制操作接口 static long led_ioctl(struct file *file, unsigned long cmd, unsigned long buf) { int i; //分配内核缓冲区 int kindex; copy_from_user(&kindex, (int *)buf, sizeof(kindex)); switch(cmd){ case LED_ON: gpio_set_value(led_info[kindex - 1].gpio, 0); printk("%s : turn on %d led. \n", __func__, kindex); break; case LED_OFF: gpio_set_value(led_info[kindex - 1].gpio, 1); printk("%s : turn off %d led. \n", __func__, kindex); break; default: printk("command is invalid\n"); return -1; } retrun 0; } //部分代码: static struct file_operations led_fops = { .unlocked_ioctl = led_ioctl };
了解两个数据结构:struct inode和struct file
struct inode{ umode_t i_mode; unsigned short i_opflags; uid_t i_uid; gid_t i_gid; unsigned int i_flags; struct posix_acl *i_acl; struct posix_acl *i_default_acl; const struct inode_operations *i_op; struct super_block *i_sb; struct address_spacc *i_mapping; void *i_security; .... dev_t i_rdev;//如果此文件是设备文件,i_rdev保存对应的设备号。 struct cdev *i_cdev;//指向设备文件对应的字符设备驱动对象。 .... };
- 功能:描述一个文件的物理信息(权限,用户和组、大小、日期)
- 生命周期:每当创建一个文件时(touch,mknod,echo,vim等),Linux内核就会为此文件定义初始化一个inode对象来描述新文件的物理属性信息,每当删除文件(rm),内核就会删除之前创建的inode对象。
- 一个文件仅有唯一一个inode对象。
- 结论:硬件操作接口open/release中的第一个形参inode指针指向内核创建的Inode对象,将来驱动程序可以利用inode来获取文件的物理信息,例如:设备号
int led_open(struct inode *inode, ...) { printk("主设备号 %d, 次设备号 %d\n", MAJOR(inode->i_rdev, MINOR(indoe->i_rdev)); return 0; }
结构体file
struct file{ struct file_operations *f_op; }
- 功能:描述一个文件被成功打开open之后的属性。
- 生命周期:打开文件open成功,内核就会创建一个file对象来描述文件打开后的属性,当close文件后,内核就会销毁对应的file对象。
- 注意:一个文件可以有多个file对象。
- 成员:
- f_op:指向字符设备对象操作结构体。
- 拔高:了解内核实现原理:应用通过系统调用号找到内核函数sys_open,sys_open创建一个file对象,然后将之前通过inode找到的字符设备对象中的操作接口对象的地址给了file.f_op,最终sys_open返回一个fd,并且内核将fd和创建file建立亲戚关系。
- 将来其余系统调用函数:read/write/ioctl/close等访问都是通过fd找到相应的执行函数。找到驱动的流程:
- read(fd)->找到file->可以访问file.f_op->访问驱动函数->xxx_read
案例:编写LED字符设备驱动,利用ioctl开关灯,此时将四个LED作为四个硬件个体。
分析思路:
- 四个LED灯物理特性一致,所以只需要一个驱动。
- 一个驱动主设备号一个
- 四个硬件四个次设备号:0/1/2/3
- 四个硬件设备文件四个:myled0,myled1,myled2,myled3.
- 四个设备文件四个inode:inode0, inode1, inode2, inode3
- 如果应用程序将四个灯全部打开:fd设备文件描述符:file0, file1, file2, file3
- 终极对应关系:
- fd0->file0->inode0->i_rdev0->主设备号A,次设备号0。
- fd1->file1->inode1->i_rdev1->主设备号A,次设备号1。
- fd2->file2->inode2->i_rdev2->主设备号A,次设备号2。
- fd3->file3->inode3->i_rdev3->主设备号A,次设备号3。
- cdev对象:一个led_cdev。
- 操作接口对象:一个led_fops,操作接口共用。
- 问:共用的操作接口函数如区分某个硬件设备呢?
- 答:通过文件描述符下的设备文件。
long led_ioctl(struct file *file, cmd, buf) { struct inode *inode = file->f_path.dentry->d_inode; int minor = MINOR(inode->i_rdev); led_info(minor); }
代码实现:
- led_test.c
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> //自定义两个命令 #define LED_ON 0x100001 #define LED_OFF 0x100002 //声明描述led操作信息的数据结构 struct led_event { int cmd;//开灯命令,1-开灯,0-关灯。 int index; //灯编号 }; //声明描述led等状态的数据结构 struct led_state{ int index;//灯编号。 int state;//灯状态,1-关灯,0-开灯。 }; int main() { int fd0, fd1, fd2, fd3; int index; struct led_event led;//分配用户缓冲区,保存灯的操作信息。 struct led_state ledst;//分配用户缓冲区,保存灯的状态。 if(argc != 3){ printf("usage: %s <on|off> <1|2|3|4> \n", argv[0]); return -1; } fd0 = open("/dev/myled0", O_RDWR); if(fd0 < 0) return -1; fd1 = open("/dev/myled1", O_RDWR); if(fd1 < 0) return -1; fd2 = open("/dev/myled2", O_RDWR); if(fd2 < 0) return -1; fd3 = open("/dev/myled3", O_RDWR); if(fd3 < 0) return -1; while(1){ ioctl(fd0, LED_ON); sleep(1); ioctl(fd1, LED_ON); sleep(1); ioctl(fd2, LED_ON); sleep(1); ioctl(fd3, LED_ON); sleep(1); ioctl(fd0, LED_OFF); sleep(1); ioctl(fd1, LED_OFF); sleep(1); ioctl(fd2, LED_OFF); sleep(1); ioctl(fd3, LED_OFF); sleep(1); } close(fd0); close(fd1); close(fd2); close(fd3); return 0; }
- led_drv.c
//自定义两个命令 #define LED_ON 0x100001 #define LED_OFF 0x100002 //控制操作接口 static long led_ioctl(struct file *file, unsigned long cmd, unsigned long buf) { int i; //通过file找到对应inode struct inode *inode = file->f_path.dentry->d_inode; int minor = MINOR(inode->i_rdev); //分配内核缓冲区 switch(cmd){ case LED_ON: gpio_set_value(led_info[minor].gpio, 0); printk("%s : turn on %d led. \n", __func__, minor + 1); break; case LED_OFF: gpio_set_value(led_info[minor].gpio, 1); printk("%s : turn off %d led. \n", __func__, minor + 1); break; default: printk("command is invalid\n"); return -1; } retrun 0; } //定义初始化LED硬件操作接口对象 //共用 static struct file_operations led_fops = { .unlocked_ioctl = led_ioctl; }; //定义设备号对象 static dev_t dev; //定义字符设备对象 static struct cdev led_cdev; static int led_init(void) { int i; 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); } //申请设备号 //次设备号0,1,2,3 alloc_chrdev_region(&dev, 0, 4, "myled"); cdev_init(&led_cdev, &led_fops); cdev_add(&led_cdev, dev, 4); return 0; } static void led_exit(void) { int i; cdev_del(&led_cdev); unregister_chrdev_region(dev, 4); for(i = 0; i < ARRAY_SIZE(led_info); i++){ gpio_direction_output(led_info[i].gpio, 1); gpio_free(led_info[i].gpio); } } module_init(led_init); module_exit(led_exit); MODULE_LINCESE("GPL");
字符设备文件自动创建只需要三个保证+四个函数即可完成
- 保证根文件系统rootfs必要脚本文件rcS中添加以下两句话:
- mount -a:就是为了执行fstab文件。
- echo /sbin/mdev > /proc/sys/kernel/hotplug
- 说明:表面看是向文件hotplug写入字符串“/sbin/mdev”,本质是将来驱动要创建设备文件时,驱动自动解析hotplug文件,找到/sbin/mdev,并且执行此命令mdev,让mdev命令来帮驱动创建设备文件。
- 保证根文件系统rootfs必须有mdev命令,执行在下位机执行:which is mdev查看是否存在即可。
- 保证根文件系统rootfs必要配置文件fstab中必须有以下两句话:
- proc /proc proc defaults 0 0
- sysfs /sys sysfs defaults 0 0
- 结论:将proc虚拟文件系统挂接到/proc目录下,将sysfs虚拟文件系统挂接到/sys目录下。
四个函数:给mdev提供参数数据。
- class_create
- device_create
- device_destroy
- class_destroy
struct class *cls; //创建一个设备类指针,类似苹果上长一个嫩芽。 //cls指向创建的设备对象。 cls = class_create(THIS_MODULE, "tarena1"); //正式创建设备文件,本质是将来调用mdev来创建设备文件。 //dev:设备文件的设备号,myled:设备文件名 device_create(cls, NULL, dev, NULL, "myled"); //删除设备文件 device_destroy(cls, dev); //删除设备对象。 class_destroy(cls);
案例:
led_drv.c
//自定义两个命令 #define LED_ON 0x100001 #define LED_OFF 0x100002 //控制操作接口 static long led_ioctl(struct file *file, unsigned long cmd, unsigned long buf) { int i; //通过file找到对应inode struct inode *inode = file->f_path.dentry->d_inode; int minor = MINOR(inode->i_rdev); //分配内核缓冲区 switch(cmd){ case LED_ON: gpio_set_value(led_info[minor].gpio, 0); printk("%s : turn on %d led. \n", __func__, minor + 1); break; case LED_OFF: gpio_set_value(led_info[minor].gpio, 1); printk("%s : turn off %d led. \n", __func__, minor + 1); break; default: printk("command is invalid\n"); return -1; } retrun 0; } //定义初始化LED硬件操作接口对象 //共用 static struct file_operations led_fops = { .unlocked_ioctl = led_ioctl; }; //定义设备号对象 static dev_t dev; //定义字符设备对象 static struct cdev led_cdev; struct class *cls; //创建一个设备类指针,类似苹果上长一个 static int led_init(void) { int i; 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); } //申请设备号 //次设备号0,1,2,3 alloc_chrdev_region(&dev, 0, 4, "myled"); cdev_init(&led_cdev, &led_fops); cdev_add(&led_cdev, dev, 4); //5.创建设备类对象 cls = class_create(THIS_MODULE, "tarena1"); //6、自动创建设备文件 device_create(cls, NULL, \ MKDEV(MAJOR(dev), 0), NULL, "myled0"); device_create(cls, NULL, \ MKDEV(MAJOR(dev), 1), NULL, "myled1"); device_create(cls, NULL, \ MKDEV(MAJOR(dev), 2), NULL, "myled2"); device_create(cls, NULL, \ MKDEV(MAJOR(dev), 3), NULL, "myled3"); return 0; } static void led_exit(void) { int i; cdev_del(&led_cdev); unregister_chrdev_region(dev, 4); for(i = 0; i < ARRAY_SIZE(led_info); i++){ gpio_direction_output(led_info[i].gpio, 1); gpio_free(led_info[i].gpio); } //删除设备文件, device_destroy(cls, \ MKDEV(MAJOR(dev), 0), 0); device_destroy(cls, \ MKDEV(MAJOR(dev), 1), 1); device_destroy(cls, \ MKDEV(MAJOR(dev), 2), 2); device_destroy(cls, \ MKDEV(MAJOR(dev), 3), 3); //删除设备类对象 class_destroy(cls); } module_init(led_init); module_exit(led_exit); MODULE_LINCESE("GPL");