Linux驱动开发九.内核定时器使用——1.定时器底层驱动
定时器和中断是我们最常用到的功能,在前面裸机开发的时候我们通过EPIT、GPT等定时器实现定时功能,那种算硬件定时器。今天我们来看下如何在Linux内核里实现软件定时功能的。
Linux内核时间管理
Linux系统在运行的时候有大量的函数需要时间管理,比如调度程序、延时程序等。并且我们在驱动开发的时候很要用各种定时器。硬件系统在系统运行时会周期性产生中断,系统使用定时中断来提供时钟源。时钟源的频率可以设置,设置好以后就会产生定时中断,系统通过这个定时中断来定时。中断周期产生的频率叫做系统频率(节拍率tick rate)。系统频率在编译内核的时候可以根据需求设置(设置路径为Kernel Features--->Timer frequency)
默认情况下系统节拍率是100HZ。如果选择较高的节拍率会提高系统时间精度,但是会导致中断产生的频繁,会产生不必要的开销。所以我们需要根据我们的实际需求,选择合适的节拍率。节拍率在内核中提供了个宏供我们使用:HZ
系统节拍数
Linux内核中有个全局变量jiffies,用来记录系统自启动以来的运行的节拍数,系统在启动时会将jiffies初始化为0,变量会根据节拍率自增。jiffies的定义在内核文件include/linux/jiffies.h中
extern u64 __jiffy_data jiffies_64; extern unsigned long volatile __jiffy_data jiffies;
可以从数据类型看出来,其中第一个是一个64位的整形变量,第二个是个32为的长整型变量。所以第一个是供64位系统使用的,并且为了兼容不同的硬件,jiffies是jiffies_64的低32位。数据结构是这样的
当我们访问变量jiffies的时候,其实是访问了jiffies_64的低32位。所以不管是32位的系统还是64位的系统,我们都可以使用jiffies来获取值。
jiffies绕回
我们使用了一个全局变量来描述系统运行的时间,既然这个变量是个整形数据,那么必然存在一个溢出的可能性,如果系统频率是最大值1000Hz,那么每一秒jiffies就会增加1000。32位的unsigned数据最大值为0xFFFFFFFF=4294967295,那么48.7天这个jiffies就溢出了。这个现象叫绕回。但是对于64位系统来说,这个值就足够我们使用了(粗略算一下有584942417355年,快6亿年怎么都够用了),当绕回现象发生后,jiffies的值会重新回到0。
Linux内核给我们提供了一组函数来做时间比较,比如一个任务的执行周期是10s,那么就可以把任务开始的jiffies和10s对应的的jiffies值相加和当前的jiffies比较,如果发生绕回,那么当前的jiffies由于回到0重新计算值会远小于计算出的值。这时候做相应的处理就行了。内核给我们提供的API函数如下
函数 | 描述 |
time_after(unkown, known) | 两个参数都为jiffies值,一般情况下 unkown 通常为当前系统jiffies,known 通常是需要对比的值。 |
time_before(unkown, known) | |
time_after_eq(unkown, known) | |
time_before_eq(unkown, known) |
比如第一个time_after,如果unknown的值超过known,返回值久违真,否则为假,内核中给了对应的函数说明
并且上面几个函数可以用来判定是否超过指定时长,用来判定是否超时。在计算known的值时应该是指定的时间*系统节拍率。比如我们需要程序在10S内结束,就可以这样使用
unsigned long time_out = jiffies+10*HZ if(time_after(jiffies,timeout)){ /*超时发生*/ } else{ /*超时未发生*/ }
为了方便我们使用,Linux内核还为我们提供了一组函数用来将jiffies和ms、us和ns之间相互转化
函数 | 描述 |
int jiffies_to_msecs(const unsigned long j) | 将jiffies值换算为对应的毫秒值 |
int jiffies_to_usecs(const unsigned long j) | 将jiffies值换算为对应的微秒值 |
u64 jiffies_to_nsecs(const unsigned long j) | 将jiffies值换算为对应的纳秒值 |
long msecs_to_jiffies(const unsigned int m) | 将指定的毫秒值换算为jiffies值 |
long usecs_to_jiffies(const unsigned int u) | 将指定的微秒值换算为jiffies值 |
unsigned long nsecs_to_jiffies(u64 n) | 将指定的纳秒值换算为jiffies值 |
内核定时器
知道了Linux的内核时间管理,我们就可以使用超时来实现定时功能了。当然这样操作起来比较麻烦,所以Linux为我们提供了一个软件定时器。和裸机的PIT等硬件定时器不同的是,软件定时器不用设置一堆寄存器然后初始化,只要提供一个超时时间还有定时器需要运行的函数就可以了。
Linux在内核中通过一个结构体timer_list来描述一个内核定时器。这个定时器在include/linux/timer.h中
struct timer_list { /* * All fields that change during normal runtime grouped to the * same cacheline */ struct list_head entry; unsigned long expires; struct tvec_base *base; void (*function)(unsigned long); unsigned long data; int slack; #ifdef CONFIG_TIMER_STATS int start_pid; void *start_site; char start_comm[16]; #endif #ifdef CONFIG_LOCKDEP struct lockdep_map lockdep_map; #endif };
我们主要用到的成员就是加粗的几个
expiers,就是超时时间,但注意这个参数的单位是节拍数。需要在使用的过程中将指定的时间换算成对应的节拍数。
function是定时器的回调函数。注意是个指针,
data,function绑定函数的参数
常用API
timer.h文件中还给我们定义了一些API来对定时器进行初始化或设置
extern void add_timer_on(struct timer_list *timer, int cpu); extern int del_timer(struct timer_list * timer); extern int mod_timer(struct timer_list *timer, unsigned long expires); extern int mod_timer_pending(struct timer_list *timer, unsigned long expires); extern int mod_timer_pinned(struct timer_list *timer, unsigned long expires);
下面一个个看下
init_timer
定时器初始化,当我们声明了一个定时器timer_list变量后要通过该函数初始化
struct timer_list timer; init_timer(&timer);
函数无返回值,直接调用即可
add_timer
add_timer用于向Linux内核中注册一个新的定时器,该定时器一旦被注册,定时器就会开始运行。从这个函数范例开始,里面的timer都是在上一个过程声明的定时器变量。
add_timer(&timer);
函数无返回值。
del_timer
del_timer用于删除指定的定时器,不管该定时器是否被激活,都可以被删除。所以在调用该函数是要等待定时器处理的函数执行完毕,特别是多核处理器。
ret = del_timer(&timer);
函数返回值为int,1时表示定时器已经激活,0时表示定时器还没被激活
del_timer_sync
del_timer_sync是del_timer的同步版,会等待所有处理器将定时器处理的函数都是用完成后再删除,要注意的是该函数不能用中断上下文中。返回值也和del_timer一样。
mod_timer
用于修改定时器的定时值,如果定时器还未被激活,该函数会激活定时器
unsigned long time_out; ret = mod_timer(&timer,time_out);
参数time_out为修改后的定时时间,返回值为1时表示在调用该函数前定时器未被激活,1时表示调用前被激活。由于Linux内核定时器只运行一次而不是周期运行的,所以当定时器需要周期运行时该函数会经常被使用。
下面结合代码来演示下这个定时器是怎么使用的
驱动构成
下面结合部分代码看一下内核定时器是怎么使用的,定时器对蜂鸣器进行操作,实现定时通断效果
设备结构体
首先,要对设备结构体进行修改
1 struct new_dev 2 { 3 dev_t dev_id; 4 int major; 5 int minor; 6 struct class *class; 7 struct device *device; 8 struct cdev cdev; 9 struct device_node *dev_nd; 10 int gpio; 11 struct timer_list timer; //定时器 12 atomic_t timer_per; //定时器周期 13 };
第11行的timer就是定义的定时器结构体
第12行的timer_per是定时器的周期,注意这里用了个原子整形变量,因为考虑到可能存在数据读写冲突,这里一定要把数据保护起来防止发生竞争现象。
定时器初始化
定时器初始化在设备注册绑定的函数内(设备初始化)。
1 init_timer(&new_dev.timer); //定时器初始化 2 atomic_set(&new_dev.timer_per,value); //读取原子变量获取定时周期值(单位:ms) 3 new_dev.timer.expires = jiffies + msecs_to_jiffies(value); //设置定时周期 4 new_dev.timer.function = timer_func; //绑定定时回调函数 5 new_dev.timer.data = (unsigned long)&new_dev; //回调函数参数 6 add_timer(&new_dev.timer); //添加并启动定时器
这里要注意一点,代码第6行后定时器会直接运行,因为我们回调函数是实现外设的控制,这句代码一定要放到外设初始化后面(对这个过程来说就是GPIO子系统初始化)。
其次就是我们将定时器周期设置的单位为毫秒,在整个设备初始化的过程开始将原子变量timer_per初始化设置为500,在这里需要重新读取一下值并保存到变量value中。然后计算实际周期对应的jiffies(这里我有个疑问,就是设置expires和绑定function之间要不要不要放太多代码,要不是占用周期过长可能会影响定时周期精度)。还有要注意的地方是这个周期不是外设运行的周期(高电平翻转总时长),而是定时器的溢出周期,设置为500ms时意思是到500ms后执行回调函数,如果只是翻转输出电平的话实际输出周期就成1s了。
在第5行我们传递参数时,由于要求传递的参数形式为长整形,所以使用了强制类型转换,在回调函数中使用的时候要将其恢复回来。
回调函数
回调函数使用时要注意传递参数的形式转换
1 timer_func(unsigned long arg){ 2 static int stat = 1; 3 int value = 0; 4 struct new_dev *dev = (struct new_dev*)arg; 5 stat = !stat; 6 gpio_set_value(dev->gpio,stat); 7 value = atomic_read(&dev->timer_per); 8 mod_timer(&dev->timer,jiffies + msecs_to_jiffies(value)); 9 }
在上面初始化定时器的代码的第5行,我们将参数强制转换为long整形数据,所以这里使用时要再恢复成new_dev结构体。
还有就是第8行,因为在前面说过,Linux内核中定时器只运行1次,所以如果需要重复循环定时的话需要使用mod_timer将定时器重新激活。其他就没什么说的了。
最后就是放出来最终代码
/** * @file timer.c * @author your name (you@domain.com) * @brief 定时器驱动测试程序 * @version 0.1 * @date 2022-07-14 * * @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 文件操作集合 * */ static const struct file_operations key_fops = { .owner = THIS_MODULE, .open = new_dev_open, }; /** * @brief 外设设备GPIO系统初始化 * * @param dev 设备结构体 * @return int */ 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; } /** * @brief Construct a new timer func object * 定时器回调函数 * @param arg */ 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)); } /** * @brief 设备初始化 * * @return int */ 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");
编译完成后加载ko模块,可以发现挂载完成后蜂鸣器开始定时工作。
现在只是有了最基础的驱动,后面一张我们来做一个APP程序来调用这个驱动。