嵌入式驱动程序学习第 1 周 - 定时器和延迟功能
前言
这篇博客一起学习定时器,定时器是最常用到的功能之一,其最大的作用之一就是提供了延时函数。
嵌入式驱动学习专栏将详细记录博主学习驱动的详细过程,未来预计四个月将高强度更新本专栏,喜欢的可以关注本博主并订阅本专栏,一起讨论一起学习。现在关注就是老粉啦!
行文目录
- 前言
- 1. Linux内核定时器介绍
- 1.1 定时器介绍
- 1.2 超时时间计算
- 2. 内核定时器使用
- 2.1 内核定时器的API函数
- 2.2 内核定时器的使用过程
- 2.2 内核定时器的使用案例
- 3. 内核的延迟机制
- 3.1 对比jiffies的函数
- 3.2 忙等延时
- 3.2.1 短延时
- 3.2.2 长延时
- 3.3 睡眠延时
- 3.3.1 sleep类延时函数
- 3.3.2 schedule类延时函数
- 3.3.3 sleep_on类延时函数
- 参考资料
1. Linux内核定时器介绍
1.1 定时器介绍
Linux内核定时器采用系统时钟,而非像单片机中使用PIT
等硬件定时器。其使用只需要提供超时时间与定时处理函数即可,当超时时间到了以后设置的定时函数就会执行。
不同于之前的单片机中的定时器,内核定时器并非周期性运行的,而是超时后会关闭,因此想周期性实现定时的话,就需要在定时处理函数中重新开启定时器。
1.2 超时时间计算
Linux内核使用了timer_list
结构体表示内核定时器,该结构体在include/linux/timer.h
中,其如下所示:
struct timer_list {
struct list_head entry;
unsigned long expires; // 定时器超时时间,单位是节拍数
struct tvec_base *base;
void (*function)(unsigned long); // 定时处理函数
unsigned long data; // 要传递给 function 函数的参数
int slack;
};
使用内核定时器需要先定义一个timer_list
变量。
其中的expires
成员变量表示超时时间,单位为节拍数。假设现在需要一个周期为2s的定时器,那么定时器的超时时间为jiffies+(2*Hz)
,因此expires
就为该值。其中jiffies
是系统运行的节拍数,jiffies/Hz
即系统运行时间,单位为s。
结构体中的function
为定时器超时后的定时处理函数。
2. 内核定时器使用
2.1 内核定时器的API函数
内核定时器的使用最关键的就是设置超时时间和定时处理函数,剩下步骤和其他一样,都需要初始化与删除等操作,具体的API如下所示:
/*
* @description: 初始化timer_list类型变量
* @param-timer: 要初始化的定时器
* @return : 无
*/
void init_timer(struct timer_list *timer);
/*
* @description: 向Linux内核注册定时器,注册完后定时器就会开始运行
* @param-timer: 要注册的定时器
* @return : 无
*/
void add_timer(struct timer_list *timer);
不管定时器有没有被激活,都可以用del_timer()
函数删除。在多处理器系统上,定时器可能会在其他的处理器上运行,因此在调用此函数之前要先等待其他处理器的定时处理器函数退出。因此可以使用其同步版——del_timer_sync()
/*
* @description: 删除一个定时器
* @param-timer: 要删除的定时器
* @return : 无
*/
int del_timer(struct timer_list *timer);
/*
* @description: del_timer函数的同步版,会等其他处理器使用完定时器再删除
* @param-timer: 要删除的定时器
* @return : 0, 定时器还没被激活;1, 定时器已经激活
*/
int del_timer_sync(struct timer_list *timer);
/*
* @description : 用于修改定时值,如果定时器还没有激活的话,mod_timer会激活定时器
* @param-timer : 要修改超时时间的定时器
* @param-expires: 修改后的超时时间
* @return : 0,调用mod_timer 函数前定时器未激活;1,调用前定时器已被激活
*/
int mod_timer(struct timer_list *timer, unsigned long expires);
2.2 内核定时器的使用过程
内核定时器的一般使用流程如下所示:
struct timer_list timer;
void function(unsigned long arg)
{
// 定时器处理代码
// 如果要周期性运行就用mod_timer
mod_timer(&dev->timertest, jiffies+msecs_to_jiffies(2000));
}
void init(void)
{
init_timer(&timer);
timer.function = function;
timer.expires = jffies + msecs_to_jiffies(2000);
timer.data = (unsigned long)&dev;
add_timer(&timer);
}
void exit(void)
{
del_timer(&timer); // 删除定时器
del_timer_sync(&timer); // 同步版本
}
2.2 内核定时器的使用案例
先在设备结构体中加入定时器变量和自旋锁,自旋锁用来保护超时时间。
struct timer_dev {
dev_t devid;
struct cdev cdev;
struct class *class;
struct device *device;
int major;
int minor;
struct device_node *nd;
int led_gpio;
int timerperiod; // 定时周期,单位为ms
struct timer_list timer; // 定时器
spinlock_t lock; // 自旋锁,保护超时时间
};
编写函数timer_unlocked_ioctl(),对应应用程序的ioctl函数,应用程序调用ioctl函数向驱动发送控制信号,次函数相应并执行
static long timer_unlocked_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
struct timer_dev *dev = (struct timer_dev *)filp->private_data;
int timerperiod;
unsigned long flags;
switch (cmd)
{
// 关闭定时器
case CLOSE_CMD:
del_timer_sync(&dev->timer);
break;
// 打开定时器
case OPEN_CMD:
spin_lock_irqsave(&dev->lock, flags); // 加锁保护超时时间
timerperiod = dev->timerperiod;
spin_unlock_irqrestore(&dev->lock, flags);
mod_timer(&dev->timer, jiffies + msecs_to_jiffies(timerperiod)); // 设置定时器
break;
// 设置定时器周期
case SETPERIOD_CMD:
spin_lock_irqsave(&dev->lock, flags); // 加锁保护超时时间
dev->timerperiod = arg;
spin_unlock_irqrestore(&dev->lock, flags);
mod_timer(&dev->timer, jiffies + msecs_to_jiffies(arg));
break;
default:
break;
}
return 0;
}
static struct file_operations timer_fops = {
.owner = THIS_MODULE,
.open = timer_open,
.unlocked_ioctl = timer_unlocked_ioctl,
};
定时器的超时函数,在最后重新设置超时时间,实现定时器的周期性。
void timer_function(unsigned long arg)
{
struct timer_dev *dev = (struct timer_dev *)arg;
static int sta = 1;
int timerperiod;
unsigned long flags;
sta = !sta;
gpio_set_value(dev->led_gpio, sta);
spin_lock_irqsave(&dev->lock, flags);
timerperiod = dev->timerperiod;
spin_unlock_irqrestore(&dev->lock, flags);
mod_timer(&dev->timer, jiffies + msecs_to_jiffies(dev->timerperiod));
}
最后在__init()函数中初始化定时器并设置超时函数
static int __init timer_init(void)
{
// 初始化自旋锁
spin_lock_init(&timerdev.lock);
// 驱动代码
// 初始化定时器并设置超时函数
init_timer(&timerdev.timer);
timerdev.timer.function = timer_function;
timerdev.timer.data = (unsigned long)&timerdev;
return 0;
}
3. 内核的延迟机制
内核中涉及的延时主要有两种实现方式:忙等待或者睡眠等待。前者阻塞程序,在延时时间到达前一直占用CPU;后者则是将进程挂起(置进程于睡眠态并释放CPU资源)。所以前者一般用在毫秒以内的精确延时,后者用于延时时间在毫秒以上的长延时。
3.1 对比jiffies的函数
linux内核中提供了以下几个函数用来对比jiffies和设置的值之间是否相等。
time_after(unkown, known);
time_before(unkown, known);
time_after_eq(unkown, known);
time_before_eq(unkown, known);
unknown
为jiffies
,known
为需要对比的值,如果unknown
超过known
,time_after
返回真,否则返回假;如果unknown
没有超过known
,time_before
返回真,否则返回假。后面的time_after_eq与time_before_eq类似,只是增加了相等的判断。
3.2 忙等延时
3.2.1 短延时
Linux内核提供了毫秒,微秒和纳秒延时函数。这些实现方式均是忙等待短延时。
void ndelay(unsigned long nsecs); // 纳秒
void udelay(unsigned long usecs); // 微秒
void mdelay(unsigned long msecs); // 毫秒
其本质类似于以下代码:
void delay(unsigned int time)
{
while (time--);
}
3.2.2 长延时
利用jiffies
和time_before()
实现延时100个jiffies和2s
/*延迟 100 个 jiffies*/
unsigned long delay = jiffies + 100;
while (time_before(jiffies, delay));
/*再延迟 2s*/
unsigned long delay = jiffies + 2*HZ;
while (time_before(jiffies, delay));
3.3 睡眠延时
3.3.1 sleep类延时函数
下述函数将使得调用它的进程睡眠参数指定的时间,受系统 HZ 和进程调度的影响,msleep()类似函数的精度是有限的。msleep()
、ssleep()
不能被打断,而msleep_interruptible()
则可以被打断。
void msleep(unsigned int millisecs);
unsigned long msleep_interruptible(unsigned int millisecs);
void ssleep(unsigned int seconds);
3.3.2 schedule类延时函数
schedule_timeout()
可以使当前任务睡眠指定的jiffies
之后重新被调度执行,它的实现原理是向系统添加一个定时器,在定时器处理函数中唤醒参数对应的进程。上一小节的sleep
类函数的底层实现也是调用它实现的:
signed long schedule_timeout_interruptible(signed long timeout);
signed long schedule_timeout_uninterruptible(signed long timeout)
3.3.3 sleep_on类延时函数
函数可以将当前进程添加到等待队列中,从而在等待队列上睡眠。当超时发生时,进程将被唤醒(后者可以在超时前被打断):
sleep_on_timeout(wait_queue_head_t *q, unsigned long timeout);
interruptible_sleep_on_timeout(wait_queue_head_t*q, unsigned long timeout);
参考资料
[1] 【正点原子】I.MX6U嵌入式Linux驱区动开发指南 第五十章
[2] Linux内核延时机制