在上面一张我们在一个基础IO外设上加上了定时器功能,但是在设备驱动挂载完成后直接就运行定时器了,这样肯定是不行的。一定是需要一个APP程序和底层驱动进行交互。APP起码具备的功能有启动、停止定时器,修改定时器工作周期的功能。
在前面所有的APP中我们主要用了file_operations结构体中的open、read、write和realease。其实write就可以满足我们的需求,但是今天我们通过一个新的操作来实现数据的交互——ioctrl
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
ioctl和write用法有些区别,用man来打印一下看看
上面的是write的介绍,下面是ioctl的介绍
具体的区别我暂时还没搞太明白,但是很明显两个函数的参数是不一样的,write是通过指针指向要修改的地方,有个参数是指针类型。而ioctl是两个int类型的参数。
compat_ioctl 函数与 unlocked_ioctl 函数功能一样,区别在于在 64 位系统上,32 位的应用程序调用将会使用此函数。在 32 位的系统上运行 32 位的应用程序调用的是unlocked_ioctl。因为我们的I.MX6UL运行的是32位的程序,所以我们要使用的就是unlock_ioctl。
/** * @brief 文件操作集合 * */ static const struct file_operations key_fops = { .owner = THIS_MODULE, .open = new_dev_open, .unlocked_ioctl = new_dev_ioctl, };
unlock_ioctl函数
先看下函数的参数
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
我们可以查到ioctl的引用一直到include/uapi/asm-generic/ioctl.h这个文件中有下面的介绍
/* ioctl command encoding: 32 bits total, command in lower 16 bits, * size of the parameter structure in the lower 14 bits of the * upper 16 bits. * Encoding the size of the parameter structure in the ioctl request * is useful for catching programs compiled with old versions * and to avoid overwriting user space outside the user buffer area. * The highest 2 bits are reserved for indicating the ``access mode''. * NOTE: This limits the max parameter size to 16kB -1 ! */ /* * The following is for compatibility across the various Linux * platforms. The generic ioctl numbering scheme doesn't really enforce * a type field. De facto, however, the top 8 bits of the lower 16 * bits are indeed used as a type field, so we might just as well make * this explicit here. Please be sure to use the decoding macros * below from now on. */
第一个参数不用说了,就是文件句柄,第二个参数是命令,这个命令是有固定格式的,我们后面再讲。最后的参数可以用来传一个长整型的参数。既然第二个参数是命令,我们就可以使用switch语句来进行处理。内核中整个函数结果可以简化成这样的
static long new_dev_ioctl(struct file *file,unsigned int cmd,unsigned long arg) { switch (cmd) { case cmd1: //do case1 break; case cmd2: //do case2 break; case cmd3: //do case3 break; } }
我们只需要根据不同的cmd对应的值去做相应的工作就行了。
cmd结构
虽然命令的数据类型是个int类型,但是这个数据时有形式要求的。这个在Documentation/ioctl/路径下的ioctl-number.txt里有详细的说明。我们可以在里面看一下内核里使用这个ioctl写的驱动样式.
随便在内核中搜索一下ioctl的驱动文件,可以发现下面的案例
1 #define CMD_COREB_START _IO('b', 0) 2 #define CMD_COREB_STOP _IO('b', 1) 3 #define CMD_COREB_RESET _IO('b', 2) 4 5 static long 6 coreb_ioctl(struct file *file, unsigned int cmd, unsigned long arg) 7 { 8 int ret = 0; 9 10 switch (cmd) { 11 case CMD_COREB_START: 12 bfin_write_SYSCR(bfin_read_SYSCR() & ~0x0020); 13 break; 14 case CMD_COREB_STOP: 15 bfin_write_SYSCR(bfin_read_SYSCR() | 0x0020); 16 bfin_write_SICB_SYSCR(bfin_read_SICB_SYSCR() | 0x0080); 17 break; 18 case CMD_COREB_RESET: 19 bfin_write_SICB_SYSCR(bfin_read_SICB_SYSCR() | 0x0080); 20 break; 21 default: 22 ret = -EINVAL; 23 break; 24 } 25 26 CSYNC(); 27 28 return ret; 29 }
在上面的驱动代码中,就是通过switch来实现cmd的处理的,注意cmd是开始定义的宏,这个宏是调用了一个_IO的函数。所以也就是说这个cmd的格式是有要求的。
cmd命令格式
Linux内核在使用ioctl函数来进行文件读写操作时,是对cmd参数有指定要求的。在内核文档ioctl/ioctl-decoding.txt里面有对其进行的详细说明
整个cmd包含了4个部分,包括了幻数、 序数、传输方向和数据大小,有些介绍在这里我们不再做详细的说明。总之我们要构造一个cmd是很麻烦的事情,还好,Linux在include/uapi/asm-generic/ioctl.h里为我们提供了一个API生成这个cmd,我们只需要传输相应的参数就可以了
1 #ifndef _UAPI_ASM_GENERIC_IOCTL_H 2 #define _UAPI_ASM_GENERIC_IOCTL_H 3 4 /* ioctl command encoding: 32 bits total, command in lower 16 bits, 5 * size of the parameter structure in the lower 14 bits of the 6 * upper 16 bits. 7 * Encoding the size of the parameter structure in the ioctl request 8 * is useful for catching programs compiled with old versions 9 * and to avoid overwriting user space outside the user buffer area. 10 * The highest 2 bits are reserved for indicating the ``access mode''. 11 * NOTE: This limits the max parameter size to 16kB -1 ! 12 */ 13 14 /* 15 * The following is for compatibility across the various Linux 16 * platforms. The generic ioctl numbering scheme doesn't really enforce 17 * a type field. De facto, however, the top 8 bits of the lower 16 18 * bits are indeed used as a type field, so we might just as well make 19 * this explicit here. Please be sure to use the decoding macros 20 * below from now on. 21 */ 22 #define _IOC_NRBITS 8 23 #define _IOC_TYPEBITS 8 24 25 /* 26 * Let any architecture override either of the following before 27 * including this file. 28 */ 29 30 #ifndef _IOC_SIZEBITS 31 # define _IOC_SIZEBITS 14 32 #endif 33 34 #ifndef _IOC_DIRBITS 35 # define _IOC_DIRBITS 2 36 #endif 37 38 #define _IOC_NRMASK ((1 << _IOC_NRBITS)-1) 39 #define _IOC_TYPEMASK ((1 << _IOC_TYPEBITS)-1) 40 #define _IOC_SIZEMASK ((1 << _IOC_SIZEBITS)-1) 41 #define _IOC_DIRMASK ((1 << _IOC_DIRBITS)-1) 42 43 #define _IOC_NRSHIFT 0 44 #define _IOC_TYPESHIFT (_IOC_NRSHIFT+_IOC_NRBITS) 45 #define _IOC_SIZESHIFT (_IOC_TYPESHIFT+_IOC_TYPEBITS) 46 #define _IOC_DIRSHIFT (_IOC_SIZESHIFT+_IOC_SIZEBITS) 47 48 /* 49 * Direction bits, which any architecture can choose to override 50 * before including this file. 51 */ 52 53 #ifndef _IOC_NONE 54 # define _IOC_NONE 0U 55 #endif 56 57 #ifndef _IOC_WRITE 58 # define _IOC_WRITE 1U 59 #endif 60 61 #ifndef _IOC_READ 62 # define _IOC_READ 2U 63 #endif 64 65 #define _IOC(dir,type,nr,size) \ 66 (((dir) << _IOC_DIRSHIFT) | \ 67 ((type) << _IOC_TYPESHIFT) | \ 68 ((nr) << _IOC_NRSHIFT) | \ 69 ((size) << _IOC_SIZESHIFT)) 70 71 #ifndef __KERNEL__ 72 #define _IOC_TYPECHECK(t) (sizeof(t)) 73 #endif 74 75 /* used to create numbers */ 76 #define _IO(type,nr) _IOC(_IOC_NONE,(type),(nr),0) 77 #define _IOR(type,nr,size) _IOC(_IOC_READ,(type),(nr),(_IOC_TYPECHECK(size))) 78 #define _IOW(type,nr,size) _IOC(_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size))) 79 #define _IOWR(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size))) 80 #define _IOR_BAD(type,nr,size) _IOC(_IOC_READ,(type),(nr),sizeof(size)) 81 #define _IOW_BAD(type,nr,size) _IOC(_IOC_WRITE,(type),(nr),sizeof(size)) 82 #define _IOWR_BAD(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),sizeof(size)) 83 84 /* used to decode ioctl numbers.. */ 85 #define _IOC_DIR(nr) (((nr) >> _IOC_DIRSHIFT) & _IOC_DIRMASK) 86 #define _IOC_TYPE(nr) (((nr) >> _IOC_TYPESHIFT) & _IOC_TYPEMASK) 87 #define _IOC_NR(nr) (((nr) >> _IOC_NRSHIFT) & _IOC_NRMASK) 88 #define _IOC_SIZE(nr) (((nr) >> _IOC_SIZESHIFT) & _IOC_SIZEMASK) 89 90 /* ...and for the drivers/sound files... */ 91 92 #define IOC_IN (_IOC_WRITE << _IOC_DIRSHIFT) 93 #define IOC_OUT (_IOC_READ << _IOC_DIRSHIFT) 94 #define IOC_INOUT ((_IOC_WRITE|_IOC_READ) << _IOC_DIRSHIFT) 95 #define IOCSIZE_MASK (_IOC_SIZEMASK << _IOC_SIZESHIFT) 96 #define IOCSIZE_SHIFT (_IOC_SIZESHIFT) 97 98 #endif /* _UAPI_ASM_GENERIC_IOCTL_H */
我们主要使用的就是滴85~88行几组宏,说白了就是将0或1左右移动最后获取到指定的cmd。
#define _IO(type,nr) //没有参数的命令 #define _IOR(type,nr,size) //从驱动中读取数据 #define _IOW(type,nr,size) //向驱动中写入数据 #define _IOWR(type,nr,size) //双向数据传输
其中参数type就是幻数、nr是序号,size是大小,幻数也是个int点数据,可以在内核文档Documentation/ioctl/ioctl-number.txt里已经给出了参考值,可以根据需求从里面查询。
所以我们可以把我们需要对命令按照需求声明出来。我们的APP和驱动交互主要用来实现3个功能,启动、关闭定时器及修改定时器工作频率,打开和关闭是不用传递额外的参数的,而设置周期要写入个周期,就要用_IOW了。幻数使用的0xEF
#define CMD_CLOSE _IO(0xEF,1) //cmd值为1,关闭定时器 #define CMD_OPEN _IO(0xEF,2) //cmd值为2,启动定时器 #define CMD_PERIOD _IOW(0xEF,3,int) //cmd值为3,修改定时器工作频率
前两个不需要传参数,就用来_IO,不用指定count,设置频率涉及到应用程序向内核写数据,就用个_IOW,参数int意思是我们要传递个int类型的数据,所以长度就是int对应的长度值。可以看出来,使用Linux提供的方法来构建cmd就简单多了,我们只需要使用定义好的宏就可以了。
驱动修改
下面我们就可以根据ioctl的样式修改前面的驱动。主要就是unlocked_ioctl绑定的函数构建
static long new_dev_ioctl(struct file *file,unsigned int cmd,unsigned long arg) { int ret = 0; int value = 0; struct new_dev *dev = file->private_data; switch (cmd) { case CMD_CLOSE: del_timer_sync(&dev->timer); gpio_set_value(dev->gpio,1); break; case CMD_OPEN: value = atomic_read(&dev->timer_per); mod_timer(&dev->timer,jiffies+msecs_to_jiffies(value)); break; case CMD_PERIOD: ret = copy_from_user(&value,(int *)arg,sizeof(int)); //arg是应用传递给驱动的周期值数据首地址,长度为4个字节 if(ret<0){ return -EFAULT; } atomic_set(&dev->timer_per,value); mod_timer(&dev->timer,jiffies+msecs_to_jiffies(value)); break; } }
驱动没什么可讲的,主要就是一个switch结构,根据APP通过ioctl操作传进来的cmd进行操作。要注意点就是第修改周期时使用了copy_from_user函数,注意传递函数时和APP里指针取值的对应。还有就是如果我们的定时器如果在运行中,是要不停读取timer_per这个变量值来设置定时器expires的,如果我们在没有停止定时器时去设置周期值,很有可能发生竞争,所以在一开始设计程序结构的时候,这个变量采用了原子变量,避免了竞争的发生。所以我前面说过,并发和竞争一定要在写代码前先想好,要不是改起来就很麻烦了!
下面吧整个驱动的代码放出来
/** * @file timer.c * @author your name (you@domain.com) * @brief 定时器测试驱动程序 * @version 0.1 * @date 2022-07-16 * * @copyright Copyright (c) 2022 * */ #include <linux/module.h> #include <linux/kernel.h> #include <linux/init.h> #include <linux/fs.h> #include <linux/uaccess.h> #include <linux/io.h> #include <linux/types.h> #include <linux/cdev.h> #include <linux/device.h> #include <linux/of.h> #include <linux/of_address.h> #include <linux/of_irq.h> #include <linux/gpio.h> #include <linux/of_gpio.h> #include <linux/ioctl.h> #define DEVICE_CNT 1 #define DEVICE_NAME "time" #define CMD_CLOSE _IO(0xEF,1) #define CMD_OPEN _IO(0xEF,2) #define CMD_PERIOD _IOW(0xEF,3,int) struct new_dev { dev_t dev_id; int major; int minor; struct class *class; struct device *device; struct cdev cdev; struct device_node *dev_nd; int gpio; struct timer_list timer; //定时器 atomic_t timer_per; //定时器周期 }; struct new_dev new_dev; static int new_dev_open(struct inode *inode, struct file *filp) { filp->private_data = &new_dev; /* 设置私有数据 */ return 0; } /** * @brief 文件io操作 * * @param file * @param cmd * @param arg * @return long */ static long new_dev_ioctl(struct file *file,unsigned int cmd,unsigned long arg) { int ret = 0; int value = 0; struct new_dev *dev = file->private_data; switch (cmd) { case CMD_CLOSE: del_timer_sync(&dev->timer); gpio_set_value(dev->gpio,1); break; case CMD_OPEN: value = atomic_read(&dev->timer_per); mod_timer(&dev->timer,jiffies+msecs_to_jiffies(value)); break; case CMD_PERIOD: ret = copy_from_user(&value,(int *)arg,sizeof(int)); //arg是应用传递给驱动的周期值数据首地址,长度为4个字节 if(ret<0){ return -EFAULT; } atomic_set(&dev->timer_per,value); mod_timer(&dev->timer,jiffies+msecs_to_jiffies(value)); break; } } /** * @brief 文件操作集合 * */ static const struct file_operations key_fops = { .owner = THIS_MODULE, .open = new_dev_open, // .release = new_dev_release, .unlocked_ioctl = new_dev_ioctl, }; //gpio设备初始化 int beep_init(struct new_dev *dev) { int ret = 0 ; //从设备树搜索设备节点 dev->dev_nd = of_find_node_by_path("/beep"); if(dev->dev_nd == NULL){ printk("no device found\r\n"); ret = -EINVAL; goto fail_nd; } //获取beep对应GPIO dev->gpio = of_get_named_gpio(dev->dev_nd,"beep-gpios",0); printk("beep_gpio=%d\r\n",dev->gpio); if(dev->gpio < 0){ printk("no GPIO found!\r\n"); ret = -EINVAL; //errno-base.h中定义的异常数值到34,这里从100开始使用防止冲突 goto fail_gpio; } //请求GPIO ret = gpio_request(dev->gpio,"beep-gpio"); if(ret){ printk("gpio request err\r\n"); ret = -EBUSY; goto fail_request;} ret = gpio_direction_output(dev->gpio,1); if(ret < 0){ ret = -EINVAL; goto fail_gpioset; } return 0; fail_gpioset: gpio_free(dev->gpio); fail_request: fail_gpio: fail_nd: return ret; } timer_func(unsigned long arg){ static int stat = 1; int value = 0; struct new_dev *dev = (struct new_dev*)arg; stat = !stat; gpio_set_value(dev->gpio,stat); value = atomic_read(&dev->timer_per); mod_timer(&dev->timer,jiffies + msecs_to_jiffies(value)); } static int __init timer_init(void){ int ret = 0; unsigned int value = 500; //申请设备号 new_dev.major = 0; if(new_dev.major){ //手动指定设备号,使用指定的设备号 new_dev.dev_id = MKDEV(new_dev.major,0); ret = register_chrdev_region(new_dev.dev_id,DEVICE_CNT,DEVICE_NAME); } else{ //设备号未指定,申请设备号 ret = alloc_chrdev_region(&new_dev.dev_id,0,DEVICE_CNT,DEVICE_NAME); new_dev.major = MAJOR(new_dev.dev_id); new_dev.minor = MINOR(new_dev.dev_id); } printk("dev id geted!\r\n"); if(ret<0){ //设备号申请异常,跳转至异常处理 goto faile_devid; } //字符设备cdev初始化 new_dev.cdev.owner = THIS_MODULE; cdev_init(&new_dev.cdev,&key_fops); //文件操作集合映射 ret = cdev_add(&new_dev.cdev,new_dev.dev_id,DEVICE_CNT); if(ret<0){ //cdev初始化异常,跳转至异常处理 goto fail_cdev; } printk("chr dev inited!\r\n"); //自动创建设备节点 new_dev.class = class_create(THIS_MODULE,DEVICE_NAME); if(IS_ERR(new_dev.class)){ //class创建异常处理 printk("class err!\r\n"); ret = PTR_ERR(new_dev.class); goto fail_class; } printk("dev class created\r\n"); new_dev.device = device_create(new_dev.class,NULL,new_dev.dev_id,NULL,DEVICE_NAME); if(IS_ERR(new_dev.device)){ //设备创建异常处理 printk("device err!\r\n"); ret = PTR_ERR(new_dev.device); goto fail_device; } printk("device created!\r\n"); //gpio外设初始化 ret = beep_init(&new_dev); if(ret<0){ printk("gpio init err\r\n"); goto fail_gpioinit; } //定时器初始化 init_timer(&new_dev.timer); atomic_set(&new_dev.timer_per,value); new_dev.timer.expires = jiffies + msecs_to_jiffies(value); new_dev.timer.function = timer_func; new_dev.timer.data = (unsigned long)&new_dev; add_timer(&new_dev.timer); return ret; fail_gpioinit: fail_device: //device创建失败,意味着class创建成功,应该将class销毁 printk("device create err,class destroyed\r\n"); class_destroy(new_dev.class); fail_class: //类创建失败,意味着设备应该已经创建成功,此刻应将其释放掉 printk("class create err,cdev del\r\n"); cdev_del(&new_dev.cdev); fail_cdev: //cdev初始化异常,意味着设备号已经申请完成,应将其释放 printk("cdev init err,chrdev register\r\n"); unregister_chrdev_region(new_dev.dev_id,DEVICE_CNT); faile_devid: //设备号申请异常,由于是第一步操作,不需要进行其他处理 printk("dev id err\r\n"); return ret; } static void __exit timer_exit(void) { gpio_set_value(new_dev.gpio,1); del_timer(&new_dev.timer); cdev_del(&new_dev.cdev); unregister_chrdev_region(new_dev.dev_id,DEVICE_CNT); device_destroy(new_dev.class,new_dev.dev_id); class_destroy(new_dev.class); gpio_free(new_dev.gpio); } module_init(timer_init); module_exit(timer_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("ZeqiZ");
应用APP
先把应用APP的代码放出来
/** * @file timerAPP.c * @author your name (you@domain.com) * @brief 定时器APP测试程序 * @version 0.1 * @date 2022-07-16 * * @copyright Copyright (c) 2022 * */ #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <sys/ioctl.h> #define CMD_CLOSE _IO(0xEF,1) #define CMD_OPEN _IO(0xEF,2) #define CMD_PERIOD _IOW(0xEF,3,int) /** * @brief * * @param argc //参数个数 * @param argv //参数 * @return int */ int main(int argc,char *argv[]) { char *filename; //文件名 filename = argv[1]; //文件名为命令行后第二个参数(索引值为1) int value = 0; int ret = 0; //初始化操作返回值 int f = 0; //初始化文件句柄 unsigned int cmd; unsigned int arg; unsigned char str[100]; f = open(filename, O_RDWR); //打开文件 if(f < 0){ printf("file open error\r\n"); return -1; } while(1){ printf("Input CMD:"); ret = scanf("%d",&cmd); //cmd对应 if(ret !=1){ gets(str);//防止卡死 } if(cmd == 1){ //cmd值为1,关闭定时器 ioctl(f,CMD_CLOSE,&arg); } else if(cmd == 2){ //cmd值为2,启动定时器 ioctl(f,CMD_OPEN,&arg); } else if(cmd == 3){ //cmd值为3,设置定时器周期 printf("Input Timer Period="); ret = scanf("%d",&arg); if(ret !=1){ gets(str); } ioctl(f,CMD_PERIOD,&arg); } } close(f); //关闭文件 return 0; }
程序运行以后,先打开驱动文件,然后进入while循环等待键盘输入cmd的值。当输入1时,定时器关闭,输入2时定时器重新启动,输入3时从键盘获取新的周期值