【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
APIAPI声明解析
msecs_to_jiffieslong msecs_to_jiffies(const unsigned int m)ms  转 节拍数
usecs_to_jiffieslong usecs_to_jiffies(const unsigned int u)us  转 节拍数
nsecs_to_jiffiesunsigned 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;
}