前言
在上一篇文章中,我们学会了将耗时工作从中断顶半部推迟到底半部处理,并利用 tasklet 和工作队列实现了软件消抖。这些机制都是"一次性"延迟执行:一次中断触发一次处理。然而,很多场景需要周期性地执行某个动作,例如让 LED 以固定频率闪烁、定期轮询传感器状态、内核心跳检测等。
Linux 内核提供了内核定时器(timer_list)来完成这类周期性任务。本文将以 GPIO LED 为例,编写一个可以设置闪烁频率的字符设备驱动,并借此介绍定时器的核心 API、使用方法以及注意事项。同时,我们会简要说明轮询驱动的概念------无需外部中断,仅靠定时器反复查询硬件状态即可实现设备监控。
读完本文你将掌握:
timer_list的初始化、启动与删除mod_timer实现周期触发的技巧- 自旋锁保护共享数据(定时器回调上下文与进程上下文并发)
- 通过设备节点动态调整定时周期
- 高精度定时器
hrtimer简介

一、内核定时器简介
1.1 内核定时器(timer_list)
struct timer_list 是 Linux 内核提供的一种低精度定时器,基于jiffies(内核节拍数)工作,典型分辨率为 1~10 毫秒(取决于 HZ 配置)。它的回调函数在软中断上下文中执行,因此不能睡眠,也不能调用可能导致阻塞的函数。
核心 API:
| 函数 | 作用 |
|---|---|
timer_setup(timer, callback, flags) |
初始化定时器并绑定回调函数(新内核推荐) |
mod_timer(timer, expires) |
修改定时器的到期时间,并激活定时器。常用于实现周期触发 |
add_timer(timer) |
向内核添加一个已经设置了 expires 的定时器 |
del_timer(timer) |
删除定时器,若已失效则返回 0,否则返回 1 |
del_timer_sync(timer) |
等待定时器处理函数执行完毕后删除(用于卸载模块) |
旧版内核使用
init_timer()和timer->data、timer->function,新内核已废弃,推荐使用timer_setup。
1.2 实现周期触发的常用方法
在定时器回调函数中再次调用 mod_timer,即可形成"一次性触发→处理→重新设定下次触发"的循环,实现周期性任务。退出循环时,只需不再调用 mod_timer 或调用 del_timer 即可。
二、设计思路
本文沿用第四篇文章中定义的 gpioled 设备树节点,使用 i.MX6ULL 的 GPIO5_IO03 控制 LED(低电平有效)。驱动将以字符设备框架呈现:
- 设备节点
/dev/ledblink,支持读写。 - 默认闪烁周期为 500ms(即 1Hz),加载后 LED 开始闪烁。
- write 操作 :用户可写入新的周期值(毫秒),例如
echo 200 > /dev/ledblink将闪烁周期设为 200ms。 - read 操作:返回当前设置的周期值。
- 定时器回调函数在软中断中运行,翻转 GPIO 电平并重新设定定时器。
- 使用自旋锁保护共享变量(周期值和 LED 状态),因为回调在软中断上下文,write 在进程上下文,需防止竞态。
三、设备树修改
沿用之前的 LED 设备树节点(无需改动)。确保板级设备树中存在以下内容:
dts
/ {
gpioled {
compatible = "yourname,gpioled";
gpios = <&gpio5 3 GPIO_ACTIVE_LOW>;
status = "okay";
};
};
如果你的 LED 连接在其他 GPIO,请相应修改。
四、驱动代码实现
新建文件 led_blink_timer.c,完整代码如下:
c
/*
* led_blink_timer.c
* 基于内核定时器的 LED 闪烁驱动。
* 使用 platform_driver 获取 GPIO,timer_list 实现周期性翻转。
* 设备节点 /dev/ledblink:支持读写,写入毫秒值改变闪烁周期。
* 作者:[你的ID]
* 适配内核:Linux 5.x (4.x 亦可)
* 参考开发板:i.MX6ULL
*/
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>
#include <linux/platform_device.h>
#include <linux/gpio/consumer.h>
#include <linux/timer.h>
#include <linux/spinlock.h>
#include <linux/jiffies.h>
#include <linux/of.h>
#define DEVICE_NAME "ledblink"
#define CLASS_NAME "ledblink_class"
#define DEFAULT_PERIOD_MS 500 /* 默认闪烁周期 500ms */
static dev_t dev_num;
static struct cdev my_cdev;
static struct class *my_class;
static struct device *my_device;
static struct gpio_desc *led_gpio; /* LED GPIO 描述符 */
static struct timer_list blink_timer; /* 内核定时器 */
static int led_state; /* 当前LED逻辑状态(0/1) */
static int period_ms = DEFAULT_PERIOD_MS; /* 闪烁周期(毫秒) */
/* 自旋锁保护 led_state 和 period_ms */
static DEFINE_SPINLOCK(lock);
/* 定时器回调函数(软中断上下文) */
static void blink_timer_callback(struct timer_list *t)
{
unsigned long flags;
spin_lock_irqsave(&lock, flags);
/* 翻转 LED 逻辑状态 */
led_state = !led_state;
gpiod_set_value(led_gpio, led_state);
/* 重新设定定时器,实现周期触发 */
mod_timer(&blink_timer, jiffies + msecs_to_jiffies(period_ms));
spin_unlock_irqrestore(&lock, flags);
pr_info("ledblink: LED %s (period=%dms)\n",
led_state ? "ON" : "OFF", period_ms);
}
/* 打开设备 */
static int ledblink_open(struct inode *inode, struct file *file)
{
pr_info("ledblink: device opened\n");
return 0;
}
/* 关闭设备 */
static int ledblink_release(struct inode *inode, struct file *file)
{
pr_info("ledblink: device closed\n");
return 0;
}
/* 读取当前闪烁周期(返回字符串,单位毫秒) */
static ssize_t ledblink_read(struct file *file, char __user *buf,
size_t count, loff_t *f_pos)
{
char kbuf[16];
int len;
unsigned long flags;
int current_period;
spin_lock_irqsave(&lock, flags);
current_period = period_ms;
spin_unlock_irqrestore(&lock, flags);
len = snprintf(kbuf, sizeof(kbuf), "%d\n", current_period);
if (*f_pos >= len)
return 0;
if (copy_to_user(buf, kbuf, len))
return -EFAULT;
*f_pos += len;
return len;
}
/* 写入新的闪烁周期(单位毫秒) */
static ssize_t ledblink_write(struct file *file, const char __user *buf,
size_t count, loff_t *f_pos)
{
char kbuf[16] = {0};
unsigned long new_period;
int ret;
unsigned long flags;
if (count > 15)
count = 15;
if (copy_from_user(kbuf, buf, count))
return -EFAULT;
ret = kstrtoul(kbuf, 0, &new_period);
if (ret < 0)
return -EINVAL;
if (new_period == 0)
return -EINVAL; /* 不允许周期为0,否则定时器频繁触发 */
spin_lock_irqsave(&lock, flags);
period_ms = new_period;
/* 更新定时器,使新周期立即生效 */
mod_timer(&blink_timer, jiffies + msecs_to_jiffies(period_ms));
spin_unlock_irqrestore(&lock, flags);
pr_info("ledblink: period set to %ld ms\n", new_period);
return count;
}
static struct file_operations ledblink_fops = {
.owner = THIS_MODULE,
.open = ledblink_open,
.release = ledblink_release,
.read = ledblink_read,
.write = ledblink_write,
};
/* ---------------- platform_driver 部分 ---------------- */
static int ledblink_probe(struct platform_device *pdev)
{
int ret;
struct device *dev = &pdev->dev;
pr_info("ledblink: probe called\n");
/* 1. 获取 GPIO 描述符,初始化为低电平(LED灭) */
led_gpio = gpiod_get(dev, NULL, GPIOD_OUT_LOW);
if (IS_ERR(led_gpio)) {
pr_err("ledblink: failed to get gpio\n");
return PTR_ERR(led_gpio);
}
/* 2. 分配设备号 */
ret = alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME);
if (ret < 0) {
pr_err("ledblink: alloc_chrdev_region failed\n");
goto err_alloc;
}
/* 3. 初始化 cdev */
cdev_init(&my_cdev, &ledblink_fops);
my_cdev.owner = THIS_MODULE;
ret = cdev_add(&my_cdev, dev_num, 1);
if (ret) {
pr_err("ledblink: cdev_add failed\n");
goto err_cdev_add;
}
/* 4. 创建 class */
my_class = class_create(THIS_MODULE, CLASS_NAME);
if (IS_ERR(my_class)) {
pr_err("ledblink: class_create failed\n");
ret = PTR_ERR(my_class);
goto err_class_create;
}
/* 5. 创建设备节点 */
my_device = device_create(my_class, dev, dev_num, NULL, DEVICE_NAME);
if (IS_ERR(my_device)) {
pr_err("ledblink: device_create failed\n");
ret = PTR_ERR(my_device);
goto err_device_create;
}
/* 6. 初始化内核定时器 */
timer_setup(&blink_timer, blink_timer_callback, 0);
/* 启动定时器:第一次触发在 period_ms 毫秒后 */
mod_timer(&blink_timer, jiffies + msecs_to_jiffies(period_ms));
pr_info("ledblink: /dev/%s created, default period=%d ms\n",
DEVICE_NAME, period_ms);
return 0;
err_device_create:
class_destroy(my_class);
err_class_create:
cdev_del(&my_cdev);
err_cdev_add:
unregister_chrdev_region(dev_num, 1);
err_alloc:
gpiod_put(led_gpio);
return ret;
}
static int ledblink_remove(struct platform_device *pdev)
{
del_timer_sync(&blink_timer); /* 安全删除定时器 */
device_destroy(my_class, dev_num);
class_destroy(my_class);
cdev_del(&my_cdev);
unregister_chrdev_region(dev_num, 1);
/* 关闭 LED */
gpiod_set_value(led_gpio, 0);
gpiod_put(led_gpio);
pr_info("ledblink: module unloaded\n");
return 0;
}
/* 设备树匹配表 */
static const struct of_device_id ledblink_of_match[] = {
{ .compatible = "yourname,gpioled" },
{ }
};
MODULE_DEVICE_TABLE(of, ledblink_of_match);
static struct platform_driver ledblink_driver = {
.probe = ledblink_probe,
.remove = ledblink_remove,
.driver = {
.name = "ledblink",
.owner = THIS_MODULE,
.of_match_table = ledblink_of_match,
},
};
module_platform_driver(ledblink_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("LED blink driver using kernel timer");
MODULE_VERSION("1.0");
代码核心说明:
- timer_setup :将
blink_timer与回调函数绑定,第三个参数flags通常为 0。 - mod_timer :重新设定定时器的
expires并激活。在回调中再次调用mod_timer,实现周期性。 - 自旋锁 :保护
led_state和period_ms,因为定时器回调(软中断)与write(进程上下文)可能并发访问。 - led_state 翻转 :在回调中用
led_state = !led_state并调用gpiod_set_value,实现亮灭交替。 - 默认周期 :
DEFAULT_PERIOD_MS为 500ms,用户可通过设备节点修改。 - 错误处理 :probe 中采用 goto 链式回滚;remove 中调用
del_timer_sync确保定时器不再运行。
五、Makefile
makefile
# Makefile for led_blink_timer
KERNEL_DIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
obj-m := led_blink_timer.o
all:
make -C $(KERNEL_DIR) M=$(PWD) modules
clean:
make -C $(KERNEL_DIR) M=$(PWD) clean
交叉编译时设置 ARCH 和 CROSS_COMPILE。
六、测试与验证
6.1 确认设备树生效
开发板启动后,检查 GPIO LED 节点(/proc/device-tree/gpioled)是否存在。
6.2 加载驱动
bash
insmod led_blink_timer.ko
dmesg | tail
# ledblink: probe called
# ledblink: /dev/ledblink created, default period=500 ms
此时 LED 应开始以 1Hz 频率闪烁(亮 500ms,灭 500ms)。
6.3 查看设备节点并赋权
bash
ls -l /dev/ledblink
chmod 666 /dev/ledblink
6.4 读取当前周期
bash
cat /dev/ledblink
# 输出 500
6.5 动态修改闪烁周期
bash
echo 200 > /dev/ledblink # 改为 200ms,闪烁明显加快
echo 1000 > /dev/ledblink # 改为 1s
观察 LED 闪烁速度变化。
6.6 关闭 LED 并停止闪烁
写入 0 是无效的(驱动返回 -EINVAL),因为周期为 0 没有意义。若需停止闪烁,可直接卸载模块:
bash
rmmod led_blink_timer
LED 会熄灭,设备节点消失。
七、高精度定时器(hrtimer)简介
timer_list 基于 jiffies,精度受 HZ 限制(通常为 1~10ms)。对于需要微秒甚至纳秒级精度的场景(如音频、精密控制),内核提供了高精度定时器(hrtimer)。
hrtimer 基于 ktime_t 时间值,提供了比 jiffies 更精确的定时服务。其基本用法与 timer_list 类似,但能实现纳秒级的定时触发。当内核配置了 CONFIG_HIGH_RES_TIMERS 时,hrtimer 会自动使用高精度时钟源。对于普通的 LED 闪烁,timer_list 已经足够,但若需要精确定时(如产生特定 PWM 波形),可以考虑 hrtimer。其常用 API 包括:
hrtimer_init(&timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL)hrtimer_start(&timer, ktime_set(0, 500000000), HRTIMER_MODE_REL)(500ms)hrtimer_cancel(&timer)
读者可自行查阅内核源码中的 samples/timers/ 示例。
八、轮询驱动的概念
本文实现的 LED 闪烁是由定时器主动翻转 GPIO,这属于典型的定时器驱动输出 。而另一类常见用法是轮询驱动输入:在没有硬件中断(或中断不可用)的情况下,使用定时器周期性地读取 GPIO 或其他寄存器的值,从而实现按键检测、传感器数据采集等。其优点是不依赖中断控制器,实现简单;缺点是占用 CPU 资源,功耗较高。在实际嵌入式系统中,如果能使用中断,应优先选择中断方式(如上一篇的输入子系统按键驱动),轮询通常用于中断资源紧张的极低成本场景。
九、总结与下篇预告
本文利用内核定时器 timer_list 实现了一个 LED 闪烁驱动,并可通过设备节点动态调整闪烁频率。我们再次实践了平台驱动、GPIO 控制和自旋锁保护共享数据的并发编程技巧。
下篇预告 :至此,我们已经掌握了字符设备驱动开发的核心技术。下一篇文章将是本系列的收官之作------我们将把这些知识点整合起来,实现一个综合性实例(例如结合 I2C 传感器和 LED 的多功能驱动),并探讨 Linux 内核驱动开发的学习路径与进阶方向。敬请期待!
如果本文对你有帮助,欢迎点赞、收藏、关注。有任何技术疑问,欢迎在评论区留言交流!