【Linux驱动】内核定时器控制 LED 闪烁
Linux 提供了定时器,当超过预定时间时,就会触发回调函数,此外,Linux内核还提供了短延时函数,比如微秒、纳秒、毫秒延时函数。
一、内核定时器API
1、节拍数
内核定时器是通过节拍数来计时的,节拍数与时间存在关联性,在 include/asm-generic/param.h 中定义了一秒会产生多少次节拍数。由图中可知,当前系统一秒产生的节拍数为 100 次。
知道了节拍数的概念,接下来就不得不提一下全局变量 jiffies 了,jiffies 定义在文件 linux/jiffies.h 中,用于记录系统从启动以来的系统节拍数,类似于时间戳。由此可知定时器延时的基本原理,假设当前 jiffies = 1000,我们要延时2s(200次节拍),当 jiffies = 1000 + 200 时,说明定时的时间到了,此时就会执行相应的回调函数。
注意:jiffies既然是变量,那必然存在溢出的风险,溢出以后会 jiffies 重新从 0 开始计数
2、定时器数据结构
Linux内核提供了 struct timer_list 类型来表示定时器,timer_list 定义在文件 linux/timer.h 中。
struct timer_list {
struct list_head entry;
unsigned long expires; // 超时时间(单位: 节拍数)
struct tvec_base *base;
void (*function)(unsigned long); // 回调函数
unsigned long data; // 回调函数的参数
int slack;
};
超时时间:
超时时间其实是一个时间点,表示时间点的不是秒或者毫秒,而是触发回调的 jiffies 变量的值。
超时时间点 = jiffies + <delay>
- jiffies:可以表示起始时间点
- <delay>:延时节拍数,为了方便,Linux提供了将时间转换成节拍数的API
API | API声明 | 解析 |
msecs_to_jiffies | long msecs_to_jiffies(const unsigned int m) | ms 转 节拍数 |
usecs_to_jiffies | long usecs_to_jiffies(const unsigned int u) | us 转 节拍数 |
nsecs_to_jiffies | unsigned long nsecs_to_jiffies(u64 n) | ns 转 节拍数 |
回调函数:
当 jiffies 到达目标节拍数时,就会触发回调函数
3、初始化定时器
定义了一个 timer_list 类型变量后必须使用 init_timer 进行初始化。init_timer 是一个宏,这里为了方便介绍以函数形式展现:
void init_timer(struct timer_list *timer);
4、注册定时器
add_timer 函数用于向 Linux 内核注册定时器,使用 add_timer 函数向内核注册定时器以后, 定时器就会开始运行。定时器不能多次注册
void add_timer(struct timer_list *timer);
5、删除定时器
删除定时器可以使用 del_timer 或者 del_timer_sync
- del_timer:需要等待定时器退出处理函数才能调用 del_timer
- del_timer_sync:del_timer的同步版,会自动等待定时器退出处理函数,然后执行删除操作
/**
* @return 返回 0,表示定时器尚未启动;返回 1,表示定时器已启动
*/
int del_timer(struct timer_list * timer);
int del_timer_sync(struct timer_list *timer);
6、修改定时器的超时时长
mod_timer 函数用于修改定时值,即延迟多长时间后执行回调。如果定时器处于尚未启动的状态,mod_timer 函数会启动定时器。
/**
* @param timer: 要操作的定时器
* @param expires: 超时时间(下一次执行回调函数的时间)
* @return 返回 0,表示定时器尚未启动;返回 1,表示定时器已启动
*/
int mod_timer(struct timer_list *timer, unsigned long expires);
二、内核定时器控制LED
这里重点介绍定时器相关内容,设备注册、驱动节点创建、设备树等内容暂不一一列举
1、创建定时器并初始化
在自定义的驱动结构体中声明一个定时器
struct chrdev_t
{
dev_t devid; /* 设备号 */
uint32_t major; /* 主设备号 */
uint32_t minor; /* 次设备号 */
struct cdev dev; /* 字符设备 */
struct class* class; /* 设备节点所属类 */
struct device* driver_node; /* 驱动文件节点 */
struct timer_list timer; /* 内核定时器 */
uint32_t next_jiffies; /* 下一次定时器启动的jiffies节拍数 */
struct device_node* gpioNode; /* 设备树节点 */
uint32_t gpioNum; /* gpio 引脚编号 */
uint8_t status; /* LED 的状态 */
};
static struct chrdev_t chrdev;
在驱动入口函数中初始化定时器
// 回调函数
void timer_callback(unsigned long arg)
{
struct chrdev_t* timerDev = (struct chrdev_t*)arg;
printk("LED状态: %d\n", timerDev->status);
// 反转 LED 状态
timerDev->status = !timerDev->status;
// 重新启动定时器
mod_timer(&timerDev->timer, timerDev->next_jiffies);
}
// 驱动入口函数
static int __init kerneltimer_init(void)
{
// ...
/* 初始化定时器 */
init_timer(&chrdev.timer);
chrdev.timer.function = timer_callback;
chrdev.timer.data = (unsigned long)&chrdev;
// ...
}
// 驱动出口函数
static void __exit kerneltimer_exit(void)
{
// ...
/* 删除定时器 */
del_timer_sync(&chrdev.timer);
}
2、ioctl 驱动操作函数实现
应用层推荐使用 ioctl 接口来传递延时时间,对应的内核驱动接口便是 unlocked_ioctl
static struct file_operations chrdev_fops = {
.owner = THIS_MODULE,
.open = chrdev_open,
.unlocked_ioctl = chrdev_ioctl, // 对应应用层的 ioctl 接口
.release = chrdev_release,
};
我们可以在 chrdev_ioctl 中启动定时器,这里我们使用 mod_timer,虽然 add_timer 可以启动定时器,但无法对同一个定时器注册多次。
// 超时时间 = 定时器启动时间点jiffies + 延时时间 delay
#define timer_delay(delay) jiffies + msecs_to_jiffies(delay)
static int chrdev_open(struct inode *pinode, struct file *pfile)
{
printk("open timer\n");
pfile->private_data = &chrdev;
return 0;
}
static long chrdev_ioctl(struct file * pfile, unsigned int cmd, unsigned long arg)
{
struct chrdev_t* timerDev = pfile->private_data;
// 使用 arg 来传递定时器的延时时间
unsigned long delayms = arg;
mod_timer(&timerDev->timer, timer_delay(delayms));
return 0;
}