好的,阿杰,我为你整理了一份基于 正点原子 第四期 “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):其中 argtimerdev 的地址。回调里:翻转 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())。不过本实验以较传统方式展示。


六、常见问题与注意事项

  1. 为什么要用 del_timer_sync() 而非 del_timer()
    因为在 SMP 或中断上下文中,可能定时器的回调正在运行或即将运行。 del_timer_sync() 会等待正在运行的回调完成,保证安全卸载。 (CSDN博客)
  2. 为什么回调函数里要再次调用 mod_timer()
    因为内核定时器默认是一次性的。若要循环执行,就必须在回调里“自我续约”。
  3. 为什么使用 ioctl 而不是 writeread
    因为设置定时器周期、启动/停止定时器属于“控制命令”,而不是简单的数据读写。使用 ioctl 更合适。 (cnblogs.com)
  4. 如何避免 jiffies 溢出(回绕)带来的时间比较问题?
    应使用内核提供的 time_after(), time_before() 宏,而不是直接用 ><。 (CSDN博客)
  5. 周期数较小(如 ms 级别)是否适合用定时器?
    在驱动中,如果要求超高精度(如几十微秒或纳秒),可能需要使用高精度定时器 (hrtimer) 或硬件定时器,而不是普通 timer_list

七、为何这个实验重要?

  • 驱动开发中,很多任务必须 延时/定时执行(如 LED 闪烁、数据采样、通信心跳)。掌握内核定时器是基础。
  • 理解除 timer_list 结构与 API,有助于你在编写驱动模块时合理布局定时任务、避免竞态/死锁。
  • 通过用户空间控制驱动(如 ioctl 命令),培养你做“驱动 ↔ 应用”交互的思路。
  • 加深你对 Linux 时间管理机制(节拍、jiffies) 的理解,这对于性能分析、延时计算助益很大。