好的,阿杰,我为你整理了一份基于 正点原子 第四期 “Linux 驱动开发”课程中 第10.1节 “Linux 内核定时器实验” 的学习笔记:涵盖实验背景、时间管理、内核定时器 API、驱动实现流程、测试方式,以及一些常见问题与分析。你可以把它作为复习材料或整理为你自己的笔记文档。
如果你需要“完整代码+Makefile+设备树修改”我也可以帮你整理。
一、实验背景与目标
在该实验中,目标是:
- 学习 Linux 内核 的时间管理机制(例如节拍数 HZ 、jiffies 等)以及短延时函数。
- 掌握内核定时器(
timer_list)的使用流程:初始化 → 添加 → 删除/修改。 - 在驱动中利用定时器实现一个周期性任务:比如 周期性闪烁 LED 灯。
- 通过
ioctl接口控制定时器的打开、关闭及周期设置。
该实验所用的硬件平台为正点原子的 I.MX6ULL/Alpha 开发板。(CSDN博客)
二、Linux 时间管理简介
2.1 系统节拍(HZ)与 jiffies
- 在 Linux 内核中,有一个全局变量
jiffies(或jiffies_64)用于记录系统启动以来的节拍数。(CSDN博客) - 宏
HZ表示每秒 “节拍数”(tick rate),例如默认情况下可能为 100、200、250、300、500、1000 Hz。(CSDN博客)- 如果 HZ = 100,意味着每 10 ms 一个节拍;如果 HZ = 1000,则每 1 ms 一个节拍。
jiffies / HZ可以大致转换为系统运行的秒数。- 为了安全做时间比较(考虑回绕的问题),内核提供了
time_before(),time_after()等宏。(CSDN博客)
2.2 短延时函数
内核还提供将毫秒、微秒、纳秒转换为 jiffies 的函数,如 msecs_to_jiffies(), usecs_to_jiffies() 等。(CSDN博客)
三、内核定时器简介
3.1 定时器结构
- 定时器在 Linux 内核中由
struct timer_list表示,其定义在include/linux/timer.h。(CSDN博客) - 结构中关键成员包括:
expires(超时时间,单位为 jiffies)、function(超时后调用的函数)、data(传给回调函数的参数)等。
3.2 常用 API
init_timer(struct timer_list *timer):初始化定时器。add_timer(struct timer_list *timer):将定时器加入系统,开始计时。del_timer(struct timer_list *timer)和del_timer_sync(struct timer_list *timer):删除定时器,后者在 SMP 或中断上下文更安全。mod_timer(struct timer_list *timer, unsigned long expires):修改定时器的超时时间(也可用来重启周期定时器)。(CSDN博客)
3.3 实现周期性定时器的注意
- 内核定时器 不是自动周期的:当超时触发函数执行后,定时器不会自动再次激活。
- 要做周期性行为,需在定时器回调函数中再次调用
mod_timer()以设置下一次超时。(CSDN博客)
四、实验驱动程序流程(基于 “周期闪烁 LED” 示例)
下面是实验中驱动的大致实现步骤(按笔记整理):
4.1 硬件与设备树准备
- 使用开发板上的一个 LED 灯,将其作为定时器回调里控制的对象。
- 在设备树中配置 LED 的 GPIO 节点(在笔记中提到以 45.4.1 小节为参考)以便驱动使用。(CSDN博客)
4.2 驱动源码结构
- 在
12_timer文件夹中创建timer.c。(CSDN博客) - 定义一个
timerdev设备结构体,包含struct timer_list timer;、定时周期timeperiod、LED 控制信息、互斥或自旋锁等。 - 在驱动的
open()函数中(如timer_open())设置设备私有数据filp->private_data = &timerdev,并初始化timeperiod(例如默认 1 秒) & LED IO。(CSDN博客) - 在
unlocked_ioctl()中实现三条指令:CLOSE_CMD:关闭定时器—调用del_timer_sync(&timerdev.timer)。OPEN_CMD:启动定时器—调用mod_timer(&timerdev.timer, jiffies + msecs_to_jiffies(timeperiod))。SETPERIOD_CMD:设置新周期(如 arg 为毫秒数),更新timerdev.timeperiod = arg,然后调用mod_timer()启动。(CSDN博客)
- 定时器回调函数
timer_function(unsigned long arg):其中arg是timerdev的地址。回调里:翻转 LED 状态;若要保持周期性,再调用mod_timer()设置下一次超时为jiffies + msecs_to_jiffies(timeperiod)。(CSDN博客) - 在模块
init函数中初始化定时器:如init_timer(&timerdev.timer);设置timerdev.timer.function = timer_function;设置timerdev.timer.data = (unsigned long)&timerdev;**注意:**此处通常不立即add_timer(),等待用户通过 ioctl 开启。(CSDN博客) - 在模块
exit函数中删除定时器:del_timer_sync(&timerdev.timer)并关闭 LED。(CSDN博客)
4.3 用户空间测试程序
- 编写一个简单的用户程序
timerApp.c:打开设备/dev/timer;提示用户输入命令:1=关闭、2=打开、3=设置周期(单位 ms)。如果选择3,则再输入新周期值。然后通过ioctl()向驱动发送命令。(CSDN博客)
4.4 测试流程
- 编译驱动,加载模块。
- 运行用户程序:输入 “2” 打开定时器 → LED 每默认周期(1 秒)闪烁。
- 输入 “3” → 输入例如 “500” → 设置周期为 500 ms → LED 每 0.5 秒闪烁。
- 输入 “1” → 关闭定时器 → LED 停止闪烁。
- 卸载驱动模块。
五、关键代码片段(摘录 &改编)
这里摘录关键部分以便你理解(已略作改编):
static struct timer_device {
struct timer_list timer;
unsigned long timeperiod; /* 毫秒 */
struct led_device *led; /* LED 控制结构 */
spinlock_t lock;
} timerdev;
/* 定时器回调 */
static void timer_function(unsigned long arg)
{
struct timer_device *dev = (struct timer_device *)arg;
/* 翻转 LED 状态 */
led_toggle(dev->led);
/* 若周期大于0,则重新设置定时器 */
mod_timer(&dev->timer,
jiffies + msecs_to_jiffies(dev->timeperiod));
}
static int timer_open(struct inode *inode, struct file *filp)
{
filp->private_data = &timerdev;
timerdev.timeperiod = 1000; /* 默认 1 秒 */
return led_init(&timerdev.led);
}
static long timer_unlocked_ioctl(struct file *filp,
unsigned int cmd,
unsigned long arg)
{
struct timer_device *dev = filp->private_data;
switch(cmd) {
case IO_STOP_TIMER:
del_timer_sync(&dev->timer);
break;
case IO_START_TIMER:
mod_timer(&dev->timer,
jiffies + msecs_to_jiffies(dev->timeperiod));
break;
case IO_MODD_TIMER:
dev->timeperiod = arg;
mod_timer(&dev->timer,
jiffies + msecs_to_jiffies(dev->timeperiod));
break;
default:
return -EINVAL;
}
return 0;
}
static int __init timer_init_module(void)
{
init_timer(&timerdev.timer);
timerdev.timer.function = timer_function;
timerdev.timer.data = (unsigned long)&timerdev;
/* 此处不立即 add_timer,等待用户启动 */
return misc_register(&timerdev_misc);
}
static void __exit timer_cleanup_module(void)
{
del_timer_sync(&timerdev.timer);
led_exit(timerdev.led);
misc_deregister(&timerdev_misc);
}
你在实现时可能还会看到 setup_timer() 等新接口(在较新内核版本里建议使用 timer_setup())。不过本实验以较传统方式展示。
六、常见问题与注意事项
- 为什么要用
del_timer_sync()而非del_timer()?
因为在 SMP 或中断上下文中,可能定时器的回调正在运行或即将运行。del_timer_sync()会等待正在运行的回调完成,保证安全卸载。 (CSDN博客) - 为什么回调函数里要再次调用
mod_timer()?
因为内核定时器默认是一次性的。若要循环执行,就必须在回调里“自我续约”。 - 为什么使用
ioctl而不是write/read?
因为设置定时器周期、启动/停止定时器属于“控制命令”,而不是简单的数据读写。使用ioctl更合适。 (cnblogs.com) - 如何避免
jiffies溢出(回绕)带来的时间比较问题?
应使用内核提供的time_after(),time_before()宏,而不是直接用>、<。 (CSDN博客) - 周期数较小(如 ms 级别)是否适合用定时器?
在驱动中,如果要求超高精度(如几十微秒或纳秒),可能需要使用高精度定时器 (hrtimer) 或硬件定时器,而不是普通timer_list。
七、为何这个实验重要?
- 驱动开发中,很多任务必须 延时/定时执行(如 LED 闪烁、数据采样、通信心跳)。掌握内核定时器是基础。
- 理解除 timer_list 结构与 API,有助于你在编写驱动模块时合理布局定时任务、避免竞态/死锁。
- 通过用户空间控制驱动(如
ioctl命令),培养你做“驱动 ↔ 应用”交互的思路。 - 加深你对 Linux 时间管理机制(节拍、jiffies) 的理解,这对于性能分析、延时计算助益很大。
发表回复