嵌入式Linux驱动开发必修课:内核定时器原理、API详解与LED闪烁项目实战
前言
定时器是嵌入式开发中最常用的功能之一,用于实现定时触发、周期性任务、超时处理等核心场景。无论是用户态的应用,还是内核态的驱动,都离不开定时器的支持。本文基于I.MX6ULL开发板,从Linux内核时间管理基础入手,详解内核定时器API的使用,结合LED闪烁实战,手把手编写可控制周期的定时器驱动,附完整驱动代码、测试APP及运行演示。通过本文,您不仅能掌握内核定时器的基本用法,还能学会如何构建一个可动态调节周期的、具备实际工程价值的驱动程序。
一、核心基础:Linux时间管理与内核定时器
1.1 内核时间管理简介
与FreeRTOS、RT-Thread等实时操作系统类似,Linux内核的运行同样需要一个精准且稳定的系统时钟源来驱动。这个时钟源为内核的调度、时间片计算、超时判断等所有与时间相关的行为提供了基础。
对于Cortex-A7内核,其内部集成了通用的定时器,具体硬件实现细节(如PIT(周期性中断定时器)或GPTP)通常由芯片厂商(如NXP)进行封装,驱动开发者无需深究底层寄存器配置,只需理解内核提供的抽象层即可。
系统通过硬件定时器产生的周期性中断来“计时”,这个中断的周期性频率就是 系统节拍率(tick rate) ,单位为赫兹(Hz)。每个“滴答”(tick),内核都会进行一次时间更新和处理。
1.1.1、系统节拍率(HZ)配置
系统节拍率并不是一个固定值,而是可以在编译Linux内核时,根据项目需求进行灵活配置的。配置路径通常在内核源码根目录下执行 make menuconfig,然后导航至:
Kernel Features → Timer frequency
选中“ Timer frequency ”条目后,打开配置选项,如下图 所示:

可供选择的频率值通常包括:100 Hz(默认值)、200 Hz、250 Hz、300 Hz、500 Hz、1000 Hz。一旦选定,配置工具会自动在内核的 .config 文件中生成 CONFIG_HZ 宏定义,如下图所示:

高/低节拍率的优缺点及选择建议
选择节拍率本质上是在时间精度和系统开销之间进行权衡。
| 特性 | 高节拍率(如1000 Hz) | 低节拍率(如100 Hz) |
|---|---|---|
| 时间精度 | 高(1 ms),定时器超时时间误差小。 | 低(10 ms),定时器误差较大。 |
| 中断频率 | 高,每秒1000次中断。 | 低,每秒100次中断。 |
| 系统负担 | 相对较高,因为中断服务程序执行更频繁,会增加CPU上下文切换的开销。 | 相对较低,CPU有更多时间处理任务。 |
| 适用场景 | 对时间要求严格的场景,如音频处理、高精度测量、工业控制等。 | 对时间不敏感的通用场景,如大部分物联网设备、简单的用户交互产品。 |
| 功耗影响 | 较高,频繁唤醒CPU会增加功耗。 | 较低,有助于降低系统功耗。 |
专家建议: 对于现代高性能处理器(如Cortex-A7),1000 Hz带来的额外开销微乎其微,可以忽略不计。因此,除非你的项目对功耗有极致要求,或者运行在性能较弱的处理器上,否则选择1000 Hz可以获得更好的时间响应。本博客全程使用默认100 Hz节拍率,以满足基础教学和开发需求。
1.1.2、核心变量jiffies
Linux 内核使用一个全局变量 jiffies 来记录系统从启动以来的系统节拍数。可以把它想象成一个不断累加的计数器。系统启动的时候会将 jiffies 初始化为 0。jiffies 定义在文件 include/linux/jiffies.h 中,其定义形式如下:
extern u64 __jiffy_data jiffies_64; extern unsigned long volatile __jiffy_data jiffies;
这里巧妙地定义了一个32位(jiffies)和64位(jiffies_64)的版本。在日常驱动开发中,我们直接使用32位的 jiffies 变量即可。jiffies_64 主要用于防止在运行时间极长的系统上出现64位计数器溢出的问题,由内核内部维护。
关键换算公式:
系统运行时间(秒) = jiffies / HZ
例如,当 HZ=100 时,jiffies 的值每增加100,就代表时间过去了1秒。
1.1.3、jiffies绕回处理与转换函数
由于32位的 jiffies 是一个有限长度的变量,其最大值约为42.9亿(2^32 -1)。当 HZ=1000 时,约49.7天后就会溢出并重新从0开始计数。这种现象称为 “绕回”(wrap around) 。如果直接使用 if (jiffies > timeout) 这样的判断,在发生绕回时就会得到错误的结果。
为了解决这个问题,Linux内核提供了一组专用的宏来处理绕回情况,确保时间比较的正确性。同时,内核也提供了便捷的函数用于 jiffies 和毫秒/微秒/纳秒之间的相互转换,开发者无需手动进行乘除运算,提高了代码的可读性和可移植性。
绕回处理函数(常用)
| 函数 | 描述 | 使用示例 |
|---|---|---|
| time_after(a, b) | 当时间 a 在时间 b 之后时返回真,正确处理绕回。 | if (time_after(jiffies, deadline)) { /* 超时了 */ } |
| time_before(a, b) | 当时间 a 在时间 b 之前时返回真,正确处理绕回。 | if (time_before(jiffies, deadline)) { /* 还没超时 */ } |
| time_after_eq(a, b) | 当时间 a 在时间 b 之后或相等时返回真。 | |
| time_before_eq(a, b) | 当时间 a 在时间 b 之前或相等时返回真。 |
time_after(unkown, known) | unkown(通常为jiffies)超过known时,返回真 |
time_before(unkown, known) | unkown未超过known时,返回真 |
time_after_eq/ time_before_eq | 同上,增加“等于”判断 |
时间转换函数(常用)
| 函数 | 描述 | 方向 |
|---|---|---|
| jiffies_to_msecs(j) | 将 jiffies 转换为毫秒 (ms)。 | jiffies -> ms |
| jiffies_to_usecs(j) | 将 jiffies 转换为微秒 (µs)。 | jiffies -> µs |
| msecs_to_jiffies(m) | 将毫秒 (ms) 转换为 jiffies。最常用 | ms -> jiffies |
| usecs_to_jiffies(u) | 将微秒 (µs) 转换为 jiffies。 | µs -> jiffies |
| timespec_to_jiffies(ts) | 将 timespec 结构体转换为 jiffies。 | |
| jiffies_to_timespec(j, ts) | 将 jiffies 转换为 timespec 结构体。 |
jiffies_to_msecs(j) | 将jiffies转换为毫秒 |
msecs_to_jiffies(m) | 将毫秒转换为jiffies(最常用) |
jiffies_to_usecs(j)/nsecs_to_jiffies(n) | jiffies与微秒/纳秒互转 |
重要提示:msecs_to_jiffies() 等转换函数返回的最小值是1。这意味着即使请求的超时时间小于一个tick(例如在HZ=100时请求1ms),它也会保证至少等待一个tick。这是为了确保定时器能够被正确调度。
1.2、内核定时器简介
Linux内核定时器是一种基于软件实现的定时机制,它完全依赖于上面提到的 jiffies 和系统时钟中断。使用内核定时器,我们无需去配置硬件定时器的寄存器,只需要设置一个超时时间(以jiffies为单位)和一个定时处理函数,当超时时间到达后,内核会在软中断上下文中自动执行这个处理函数。
⚠️ 关键注意: 内核定时器本质上是单次触发(one-shot) 的。也就是说,定时器超时并执行完处理函数后,就会自动关闭。如果你需要实现周期性定时任务(如让LED以固定频率闪烁),必须在定时处理函数的末尾,使用 mod_timer 函数重新开启这个定时器。
1.2.1、定时器结构体timer_list
内核定时器使用 struct timer_list 结构体来描述,该结构体定义在 include/linux/timer.h 中。对于驱动开发者而言,主要关注以下几个核心成员(其他成员由内核内部管理):
struct timer_list {
unsigned long expires; /* 超时时间,单位:节拍数(jiffies) */
void (*function)(unsigned long); /* 超时处理函数 */
unsigned long data; /* 传递给处理函数的参数 */
};- expires:这是一个无符号长整型变量,指定了定时器的超时时刻,其单位是节拍数。例如,如果我们想定义一个周期为2秒的定时器,那么它的超时时刻就是当前时刻加上2秒对应的节拍数,即 jiffies + (2 * HZ)。
- function:这是一个函数指针,指向定时器超时后要执行的处理函数。这是我们编写驱动程序时需要实现的核心逻辑,比如在这里翻转LED的电平。
定义好 timer_list 结构体变量后,还需要通过一系列内核提供的API函数来初始化和管理它,这些函数如下:
| API函数 | 功能描述 | 关键说明 |
|---|---|---|
| init_timer(timer) | 初始化 timer_list 结构体。 | 在使用其他定时器函数之前,必须先调用此函数进行初始化。 |
| add_timer(timer) | 向内核注册一个定时器,并启动它。 | 调用此函数后,定时器开始运行,在 expires 指定的时刻触发。 |
| del_timer(timer) | 删除一个已经激活的定时器。 | 用于在模块卸载或不再需要定时器时,将其从内核的定时器链表中移除。 |
| del_timer_sync(timer) | 删除一个已经激活的定时器,并确保在其处理函数执行完毕后返回。 | 在多核处理器上,建议使用此函数代替 del_timer,以避免竞态条件。 |
| mod_timer(timer, expires) | 修改一个已经激活的定时器的超时时间,并重新启动它。 | 这是实现周期性定时器的关键函数。它也常用于在定时器超时前,延长或提前其触发时间。 |
init_timer(timer) | 初始化定时器 | 定义timer后必须先初始化 |
add_timer(timer) | 注册并启动定时器 | 启动后开始计时 |
del_timer(timer) | 删除定时器 | 多处理器需注意同步 |
del_timer_sync(timer) | 同步删除定时器 | 不可用于中断上下文 |
mod_timer(timer, expires) | 修改超时时间,未激活则启动 | 周期性定时核心函数 |
1.2.2、内核定时器使用流程
一个典型的内核定时器使用流程如下伪代码所示:
// 1. 定义定时器和设备结构体(通常结合设备驱动)
struct timer_list timer;
// 2. 定时处理函数(超时后执行)
void timer_func(unsigned long arg) {
// 业务逻辑(如LED翻转)
...
// 3. 周期性定时:重新设置超时时间并启动
mod_timer(&timer, jiffies + msecs_to_jiffies(1000)); // 1秒周期
}
// 4. 初始化定时器(通常在驱动入口)
init_timer(&timer);
timer.function = timer_func; // 绑定处理函数
timer.expires = jiffies + msecs_to_jiffies(1000); // 初始超时1秒
timer.data = (unsigned long)&dev; // 传递设备结构体参数
add_timer(&timer); // 启动定时器
// 5. 退出时删除定时器(驱动出口)
del_timer_sync(&timer);1.3、Linux内核短延时函数
在驱动开发中,我们经常需要非常短暂的延时,例如在操作硬件寄存器后等待其稳定,或者产生简单的脉冲信号。Linux内核提供了一系列精密的短延时函数,这些函数是忙等待(busy-wait)的,不会让出CPU,因此只适用于很短的时间。
- ndelay(unsigned long nsecs):纳秒级延时。精度最高,但开销也相对较大,适用于极短时间的等待。
- udelay(unsigned long usecs):微秒级延时。最常用的函数,适用于大多数需要短延时的场景,如I2C、SPI等协议的时序模拟。
- mdelay(unsigned long msecs):毫秒级延时。对于毫秒级的延时,也可以使用此函数,但更推荐使用内核定时器,因为 mdelay 会阻塞当前线程。
专家建议: 对于超过几个毫秒的延时,绝对不要使用 mdelay,因为它会浪费大量的CPU时间。这种情况下,应该始终使用内核定时器或等待队列等机制,让出CPU给其他有需要的任务。
二、硬件原理分析(看过之前博客的可以忽略)
本实验的核心目标是通过设置一个内核定时器来实现LED灯的周期性闪烁。LED灯作为最基础的输出设备,其硬件原理非常简单,非常适合作为驱动实验的载体。实验板上的LED原理图如下图所示:

从图中可以清晰地看到,LED0的阳极通过一个限流电阻连接到3.3V电源,阴极则连接到了I.MX6ULL处理器的GPIO_3引脚。查阅芯片数据手册可知,GPIO_3实际上对应的是 GPIO1_IO03。
- LED点亮条件:当 GPIO1_IO03 引脚被软件配置为输出模式,并输出低电平(0) 时,LED0的阴极电压为0V,与阳极的3.3V形成电压差,从而导通发光。
- LED熄灭条件:当 GPIO1_IO03 引脚输出高电平(1) 时,LED0的阴极电压也为3.3V,两端电压差为0,因此LED0熄灭。
所以,控制LED的亮灭,本质上就是通过驱动程序来控制 GPIO1_IO03 引脚的电平状态。
三、实验程序编写
本期实验我们将编写一个内核定时器驱动,利用它来周期性地点亮和熄灭开发板上的LED灯。驱动的核心亮点在于,LED的闪烁周期不是硬编码在驱动里的,而是可以由用户态的测试应用程序动态设置和控制。
3.1、修改设备树文件
为了驱动能够找到并控制LED对应的GPIO,我们需要在设备树中为LED创建一个节点。这部分内容属于Linux设备树的基础知识,限于篇幅,本文不再赘述。具体的修改步骤和细节,可以参见我之前的一篇博客:ARM Linux 驱动开发篇---基于 pinctrl+GPIO 子系统的 LED 驱动开发(设备树 + 驱动 + 测试全流程)-- Ubuntu20.04-CSDN博客
简单来说,我们需要在根节点 / 下添加一个名为 gpioled 的子节点,并指定其使用的GPIO引脚。
3.2 定时器驱动程序编写(timer.c)
以下是完整的驱动程序源代码,我们将它命名为 timer.c。
1 #include <linux/types.h>
2 #include <linux/kernel.h>
3 #include <linux/delay.h>
4 #include <linux/ide.h>
5 #include <linux/init.h>
6 #include <linux/module.h>
7 #include <linux/errno.h>
8 #include <linux/gpio.h>
9 #include <linux/cdev.h>
10 #include <linux/device.h>
11 #include <linux/of.h>
12 #include <linux/of_address.h>
13 #include <linux/of_gpio.h>
14 #include <linux/semaphore.h>
15 #include <linux/timer.h>
16 #include <asm/mach/map.h>
17 #include <asm/uaccess.h>
18 #include <asm/io.h>
19 /***************************************************************
20 Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
21 文件名 : timer.c
22 版本 : V1.0
23 描述 : Linux内核定时器实验
24 ***************************************************************/
25 #define TIMER_CNT 1 /* 设备号个数 */
26 #define TIMER_NAME "timer" /* 名字 */
27 #define CLOSE_CMD (_IO(0XEF, 0x1)) /* 关闭定时器 */
28 #define OPEN_CMD (_IO(0XEF, 0x2)) /* 打开定时器 */
29 #define SETPERIOD_CMD (_IO(0XEF, 0x3)) /* 设置定时器周期命令 */
30 #define LEDON 1 /* 开灯 */
31 #define LEDOFF 0 /* 关灯 */
32
33 /* timer设备结构体 */
34 struct timer_dev{
35 dev_t devid; /* 设备号 */
36 struct cdev cdev; /* cdev */
37 struct class *class; /* 类 */
38 struct device *device; /* 设备 */
39 int major; /* 主设备号 */
40 int minor; /* 次设备号 */
41 struct device_node *nd; /* 设备节点 */
42 int led_gpio; /* key所使用的GPIO编号 */
43 int timeperiod; /* 定时周期,单位为ms */
44 struct timer_list timer;/* 定义一个定时器*/
45 spinlock_t lock; /* 定义自旋锁 */
46 };
47
48 struct timer_dev timerdev; /* timer设备 */
49
50 /*
51 * @description : 初始化LED灯IO,open函数打开驱动的时候
52 * 初始化LED灯所使用的GPIO引脚。
53 * @param : 无
54 * @return : 无
55 */
56 static int led_init(void)
57 {
58 int ret = 0;
59
60 timerdev.nd = of_find_node_by_path("/gpioled");
61 if (timerdev.nd== NULL) {
62 return -EINVAL;
63 }
64
65 timerdev.led_gpio = of_get_named_gpio(timerdev.nd ,"led-gpio", 0);
66 if (timerdev.led_gpio < 0) {
67 printk("can't get led\r\n");
68 return -EINVAL;
69 }
70
71 /* 初始化led所使用的IO */
72 gpio_request(timerdev.led_gpio, "led"); /* 请求IO */
73 ret = gpio_direction_output(timerdev.led_gpio, 1);
74 if(ret < 0) {
75 printk("can't set gpio!\r\n");
76 }
77 return 0;
78 }
79
80 /*
81 * @description : 打开设备
82 * @param - inode : 传递给驱动的inode
83 * @param - filp : 设备文件,file结构体有个叫做private_data的成员变量
84 * 一般在open的时候将private_data指向设备结构体。
85 * @return : 0 成功;其他 失败
86 */
87 static int timer_open(struct inode *inode, struct file *filp)
88 {
89 int ret = 0;
90 filp->private_data = &timerdev; /* 设置私有数据 */
91
92 timerdev.timeperiod = 1000; /* 默认周期为1s */
93 ret = led_init(); /* 初始化LED IO */
94 if (ret < 0) {
95 return ret;
96 }
97
98 return 0;
99 }
100
101 /*
102 * @description : ioctl函数,
103 * @param - filp : 要打开的设备文件(文件描述符)
104 * @param - cmd : 应用程序发送过来的命令
105 * @param - arg : 参数
106 * @return : 0 成功;其他 失败
107 */
108 static long timer_unlocked_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
109 {
110 struct timer_dev *dev = (struct timer_dev *)filp->private_data;
111 int timerperiod;
112 unsigned long flags;
113
114 switch (cmd) {
115 case CLOSE_CMD: /* 关闭定时器 */
116 del_timer_sync(&dev->timer);
117 break;
118 case OPEN_CMD: /* 打开定时器 */
119 spin_lock_irqsave(&dev->lock, flags);
120 timerperiod = dev->timeperiod;
121 spin_unlock_irqrestore(&dev->lock, flags);
122 mod_timer(&dev->timer, jiffies + msecs_to_jiffies(timerperiod));
123 break;
124 case SETPERIOD_CMD: /* 设置定时器周期 */
125 spin_lock_irqsave(&dev->lock, flags);
126 dev->timeperiod = arg;
127 spin_unlock_irqrestore(&dev->lock, flags);
128 mod_timer(&dev->timer, jiffies + msecs_to_jiffies(arg));
129 break;
130 default:
131 break;
132 }
133 return 0;
134 }
135
136 /* 设备操作函数 */
137 static struct file_operations timer_fops = {
138 .owner = THIS_MODULE,
139 .open = timer_open,
140 .unlocked_ioctl = timer_unlocked_ioctl,
141 };
142
143 /* 定时器回调函数 */
144 void timer_function(unsigned long arg)
145 {
146 struct timer_dev *dev = (struct timer_dev *)arg;
147 static int sta = 1;
148 int timerperiod;
149 unsigned long flags;
150
151 sta = !sta; /* 每次都取反,实现LED灯反转 */
152 gpio_set_value(dev->led_gpio, sta);
153
154 /* 重启定时器 */
155 spin_lock_irqsave(&dev->lock, flags);
156 timerperiod = dev->timeperiod;
157 spin_unlock_irqrestore(&dev->lock, flags);
158 mod_timer(&dev->timer, jiffies + msecs_to_jiffies(dev->timeperiod));
159 }
160
161 /*
162 * @description : 驱动入口函数
163 * @param : 无
164 * @return : 无
165 */
166 static int __init timer_init(void)
167 {
168 /* 初始化自旋锁 */
169 spin_lock_init(&timerdev.lock);
170
171 /* 注册字符设备驱动 */
172 /* 1、创建设备号 */
173 if (timerdev.major) { /* 定义了设备号 */
174 timerdev.devid = MKDEV(timerdev.major, 0);
175 register_chrdev_region(timerdev.devid, TIMER_CNT, TIMER_NAME);
176 } else { /* 没有定义设备号 */
177 alloc_chrdev_region(&timerdev.devid, 0, TIMER_CNT, TIMER_NAME); /* 申请设备号 */
178 timerdev.major = MAJOR(timerdev.devid); /* 获取分配号的主设备号 */
179 timerdev.minor = MINOR(timerdev.devid); /* 获取分配号的次设备号 */
180 }
181
182 /* 2、初始化cdev */
183 timerdev.cdev.owner = THIS_MODULE;
184 cdev_init(&timerdev.cdev, &timer_fops);
185
186 /* 3、添加一个cdev */
187 cdev_add(&timerdev.cdev, timerdev.devid, TIMER_CNT);
188
189 /* 4、创建类 */
190 timerdev.class = class_create(THIS_MODULE, TIMER_NAME);
191 if (IS_ERR(timerdev.class)) {
192 return PTR_ERR(timerdev.class);
193 }
194
195 /* 5、创建设备 */
196 timerdev.device = device_create(timerdev.class, NULL, timerdev.devid, NULL, TIMER_NAME);
197 if (IS_ERR(timerdev.device)) {
198 return PTR_ERR(timerdev.device);
199 }
200
201 /* 6、初始化timer,设置定时器处理函数,还未设置周期,所有不会激活定时器 */
202 init_timer(&timerdev.timer);
203 timerdev.timer.function = timer_function;
204 timerdev.timer.data = (unsigned long)&timerdev;
205 return 0;
206 }
207
208 /*
209 * @description : 驱动出口函数
210 * @param : 无
211 * @return : 无
212 */
213 static void __exit timer_exit(void)
214 {
215
216 gpio_set_value(timerdev.led_gpio, 1); /* 卸载驱动的时候关闭LED */
217 del_timer_sync(&timerdev.timer); /* 删除timer */
218 #if 0
219 del_timer(&timerdev.tiemr);
220 #endif
221
222 /* 注销字符设备驱动 */
223 gpio_free(timerdev.led_gpio);
224 cdev_del(&timerdev.cdev);/* 删除cdev */
225 unregister_chrdev_region(timerdev.devid, TIMER_CNT); /* 注销设备号 */
226
227 device_destroy(timerdev.class, timerdev.devid);
228 class_destroy(timerdev.class);
229 }
230
231 module_init(timer_init);
232 module_exit(timer_exit);
233 MODULE_LICENSE("GPL");
234 MODULE_AUTHOR("duan");3.3、分段分析
接下来,我们对驱动代码的关键部分进行详细解析,并补充必要的专业知识。
3.3.1、设备结构体定义(33-47 行)
33 struct timer_dev{
34 dev_t devid;
35 struct cdev cdev;
36 struct class *class;
37 struct device *device;
38 int major;
39 int minor;
40 struct device_node *nd;
41 int led_gpio;
42 int timeperiod;
43 struct timer_list timer;
44 spinlock_t lock;
45 };
46
47 struct timer_dev timerdev;代码逻辑与深度解析
- timeperiod:用于保存用户设置的定时周期,单位是毫秒。这个变量会被定时器回调函数使用。
- timer:内核定时器的实体,所有的定时操作都围绕它进行。
- lock:这是一个自旋锁。它的作用是保护 timeperiod 这个共享资源。因为 timeperiod 既可能在上层 ioctl 函数(进程上下文)中被修改,也可能在定时器回调函数(软中断上下文)中被读取。为了防止并发访问导致的数据不一致,我们需要用自旋锁来保护它。注意:在定时器回调函数中,我们不能使用可能导致睡眠的锁(如信号量),而必须使用自旋锁。
3.3.2、LED GPIO 初始化(55-76行)
55 static int led_init(void)
56 {
57 int ret = 0;
58
59 timerdev.nd = of_find_node_by_path("/gpioled");
60 if (timerdev.nd == NULL) {
61 return -EINVAL;
62 }
63
64 timerdev.led_gpio = of_get_named_gpio(timerdev.nd, "led-gpio", 0);
65 if (timerdev.led_gpio < 0) {
66 printk("can't get led\r\n");
67 return -EINVAL;
68 }
69
70 gpio_request(timerdev.led_gpio, "led");
71 ret = gpio_direction_output(timerdev.led_gpio, 1);
72 if(ret < 0) {
73 printk("can't set gpio!\r\n");
74 }
75 return 0;
76 }代码逻辑与深度解析
查找设备树节点:of_find_node_by_path("/gpioled") 函数通过设备树的绝对路径查找我们之前创建的节点。
获取GPIO编号:of_get_named_gpio 函数从找到的节点中,解析 led-gpio 属性,得到对应的全局GPIO编号。
申请并配置GPIO:
- gpio_request:向内核申请对该GPIO的使用权。这是一个好习惯,可以防止其他驱动误用同一个引脚。
- gpio_direction_output:将GPIO设置为输出模式,并初始化为高电平(1),确保LED初始状态是熄灭的。
3.3.3、设备打开函数(50-63 行)
85 static int timer_open(struct inode *inode, struct file *filp)
86 {
87 int ret = 0;
88 filp->private_data = &timerdev;
89
90 timerdev.timeperiod = 1000;
91 ret = led_init();
92 if (ret < 0) {
93 return ret;
94 }
95 return 0;
96 }代码逻辑与深度解析
函数 timer_open 对应于应用程序的 open 系统调用。其主要工作包括:
1. 将设备结构体的私有数据 private_data 设置为我们在入口函数中分配的 timerdev 指针。这样,在 read、write、ioctl 等其他文件操作函数中,我们就可以通过 filp->private_data 方便地获取到我们的设备结构体。
2. 初始化定时周期 timeperiod 为默认值1000毫秒(1秒)。
3. 调用 led_init 函数完成LED引脚的初始化工作。
3.3.4、ioctl 控制函数(核心)
102 * @description : ioctl函数,
103 * @param - filp : 要打开的设备文件(文件描述符)
104 * @param - cmd : 应用程序发送过来的命令
105 * @param - arg : 参数
106 * @return : 0 成功;其他 失败
107 */
108 static long timer_unlocked_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
109 {
110 struct timer_dev *dev = (struct timer_dev *)filp->private_data;
111 int timerperiod;
112 unsigned long flags;
113
114 switch (cmd) {
115 case CLOSE_CMD: /* 关闭定时器 */
116 del_timer_sync(&dev->timer);
117 break;
118 case OPEN_CMD: /* 打开定时器 */
119 spin_lock_irqsave(&dev->lock, flags);
120 timerperiod = dev->timeperiod;
121 spin_unlock_irqrestore(&dev->lock, flags);
122 mod_timer(&dev->timer, jiffies + msecs_to_jiffies(timerperiod));
123 break;
124 case SETPERIOD_CMD: /* 设置定时器周期 */
125 spin_lock_irqsave(&dev->lock, flags);
126 dev->timeperiod = arg;
127 spin_unlock_irqrestore(&dev->lock, flags);
128 mod_timer(&dev->timer, jiffies + msecs_to_jiffies(arg));
129 break;
130 default:
131 break;
132 }
133 return 0;
134 }代码逻辑与深度解析
timer_unlocked_ioctl 是驱动与应用程序交互的核心接口。应用程序通过 ioctl 系统调用传入命令和参数,驱动在这里进行解析和执行。
| 行号范围 | 代码逻辑 | 深度解析 |
|---|---|---|
| 114-117 | 获取设备结构体和自旋锁 | 从 filp->private_data 中取出设备结构体地址。然后,spin_lock_irqsave 用于获取自旋锁并同时保存和禁用本地中断。这是最安全的获取锁方式,可以防止在锁住期间被中断处理程序打断而形成死锁。 |
| 119-132 | switch 分支处理 | CMD_CLOSE(关闭定时器):调用 del_timer_sync(&dev->timer) 安全地删除定时器。确保定时器在被删除前不会再次触发。CMD_OPEN(打开定时器):调用 mod_timer 函数,将定时器的超时时间设置为 jiffies + msecs_to_jiffies(dev->timeperiod),并启动定时器。CMD_SETPERIOD(设置周期):将用户传入的 arg 参数(周期值)赋值给 dev->timeperiod。这里使用 spin_lock 和 spin_unlock 包围,确保对共享变量 timeperiod 的修改是原子的。 |
| 134 | 解锁 | 操作完成后,调用 spin_unlock_irqrestore 释放锁并恢复之前的中断状态。 |
| 110 | dev = filp->private_data | 从文件私有数据取出设备结构体指针,简化后续代码 |
| 111 | timerperiod 临时变量 | 存储读取的周期值,避免自旋锁持有时间过长 |
| 112 | flags 自旋锁标志 | 保存中断状态,配合 spin_lock_irqsave/restore 使用 |
| 115-117 | CLOSE_CMD:del_timer_sync | 安全删除定时器,等待回调函数执行完毕。 |
| 118-123 | OPEN_CMD 逻辑: 1. 自旋锁保护读 timeperiod 2. mod_timer 启动定时器 | ,调用 mod_timer 函数打开定时器,定时周期为 timerdev 的 timeperiod 成员变量,定时周期默认是 1 秒。 |
| 124-129 | SETPERIOD_CMD 逻辑: 1. 自旋锁保护写 timeperiod 2. mod_timer 重启定时器 | 设置定时器周期命令,参数 arg 就是新的定时周期。 设置 timerdev 的 timeperiod 成员变量为 arg 所表示定时周期指。 并且使用 mod_timer 重新打开定时器,使定时器以新的周期运行。 |
| 133 | 返回 0 | IOCTL 执行成功返回 0,失败需返回对应错误码(如 -EINVAL) |
3.3.5、定时器回调函数(144-159 行)
143 /* 定时器回调函数 */
144 void timer_function(unsigned long arg)
145 {
146 struct timer_dev *dev = (struct timer_dev *)arg;
147 static int sta = 1;
148 int timerperiod;
149 unsigned long flags;
150
151 sta = !sta; /* 每次都取反,实现LED灯反转 */
152 gpio_set_value(dev->led_gpio, sta);
153
154 /* 重启定时器 */
155 spin_lock_irqsave(&dev->lock, flags);
156 timerperiod = dev->timeperiod;
157 spin_unlock_irqrestore(&dev->lock, flags);
158 mod_timer(&dev->timer, jiffies + msecs_to_jiffies(dev->timeperiod));
159 }代码逻辑与深度解析
timer_function 是定时器超时后内核自动调用的函数。它运行在软中断上下文中,因此有以下重要限制:不能睡眠,不能访问用户空间内存,不能执行可能引起阻塞的操作。
| 行号范围 | 代码逻辑 | 深度解析 |
|---|---|---|
| 144-147 | 获取设备结构体 | 定时器回调函数的参数 arg 在初始化时被设置为设备结构体的地址。这里将其强制类型转换回来,以便访问LED引脚和周期变量。 |
| 149-153 | 翻转LED状态 | 这是实现闪烁的核心逻辑。gpio_get_value 读取当前引脚电平,然后 gpio_set_value 设置相反的电平。这样就实现了一次亮灭翻转。 |
| 155-158 | 重新开启定时器 | 由于内核定时器是单次的,为了形成连续的闪烁,这里再次调用 mod_timer。它将当前定时器的超时时间重新设置为 jiffies + msecs_to_jiffies(dev->timeperiod),从而实现周期性运行。注意,在访问 dev->timeperiod 之前,需要用自旋锁保护起来,防止在读取时被 ioctl 修改。 |
| 146 | dev = (struct timer_dev *)arg | arg 是初始化定时器时传入的 timerdev 地址(204 行),转换为结构体指针。 |
| 147 | static int sta = 1 | 静态变量,保存 LED 状态(初始 1 = 熄灭),每次回调取反 |
| 151 | sta = !sta | 状态取反(1→0→1...),实现 LED 闪烁 |
| 152 | gpio_set_value(dev->led_gpio, sta) | 设置 GPIO 电平,控制 LED 亮灭 |
| 155-157 | 自旋锁保护读 timeperiod | 读共享变量必须加锁,避免与 ioctl 的写操作冲突 |
| 158 | mod_timer(&dev->timer, jiffies + msecs_to_jiffies(dev->timeperiod)) | 重启定时器实现周期性 |
3.3.6、驱动入口函数(166-206 行)
161 /*
162 * @description : 驱动入口函数
163 * @param : 无
164 * @return : 无
165 */
166 static int __init timer_init(void)
167 {
168 /* 初始化自旋锁 */
169 spin_lock_init(&timerdev.lock);
170
171 /* 注册字符设备驱动 */
172 /* 1、创建设备号 */
173 if (timerdev.major) { /* 定义了设备号 */
174 timerdev.devid = MKDEV(timerdev.major, 0);
175 register_chrdev_region(timerdev.devid, TIMER_CNT, TIMER_NAME);
176 } else { /* 没有定义设备号 */
177 alloc_chrdev_region(&timerdev.devid, 0, TIMER_CNT, TIMER_NAME); /* 申请设备号 */
178 timerdev.major = MAJOR(timerdev.devid); /* 获取分配号的主设备号 */
179 timerdev.minor = MINOR(timerdev.devid); /* 获取分配号的次设备号 */
180 }
181
182 /* 2、初始化cdev */
183 timerdev.cdev.owner = THIS_MODULE;
184 cdev_init(&timerdev.cdev, &timer_fops);
185
186 /* 3、添加一个cdev */
187 cdev_add(&timerdev.cdev, timerdev.devid, TIMER_CNT);
188
189 /* 4、创建类 */
190 timerdev.class = class_create(THIS_MODULE, TIMER_NAME);
191 if (IS_ERR(timerdev.class)) {
192 return PTR_ERR(timerdev.class);
193 }
194
195 /* 5、创建设备 */
196 timerdev.device = device_create(timerdev.class, NULL, timerdev.devid, NULL, TIMER_NAME);
197 if (IS_ERR(timerdev.device)) {
198 return PTR_ERR(timerdev.device);
199 }
200
201 /* 6、初始化timer,设置定时器处理函数,还未设置周期,所有不会激活定时器 */
202 init_timer(&timerdev.timer);
203 timerdev.timer.function = timer_function;
204 timerdev.timer.data = (unsigned long)&timerdev;
205 return 0;
206 }代码逻辑与深度解析
| 行号范围 | 代码逻辑 | 深度解析 |
|---|---|---|
| 170-185 | 分配设备结构体内存 | 使用 kzalloc 分配 struct timer_dev 大小的内存,并用0初始化。GFP_KERNEL 标志表示分配过程可以睡眠,适用于进程上下文。 |
| 186-193 | 初始化定时器和自旋锁 | 初始化定时器:init_timer(&dev->timer)。设置回调函数和数据:dev->timer.function = timer_function; dev->timer.data = (unsigned long)dev;,将设备结构体的地址作为参数传递给回调函数。初始化自旋锁:spin_lock_init(&dev->lock);。 |
| 194-206 | 注册字符设备 | 这是标准的字符设备注册流程: 1. 分配设备号:alloc_chrdev_region 动态分配主设备号和次设备号。 2. 初始化cdev:cdev_init 将 dev->cdev 与我们定义的文件操作集 timer_fops 关联起来。 3. 添加cdev到内核:cdev_add 将字符设备正式注册到系统中。 4. 创建设备类:class_create 在 /sys/class/ 下创建一个类。 5. 创建设备节点:device_create 在该类下创建一个设备,这会自动在 /dev/ 下生成设备文件。 |
| 169 | spin_lock_init(&timerdev.lock) | 初始化自旋锁(必须!否则自旋锁使用会崩溃) |
| 173-180 | 设备号分配逻辑: 1. 若指定 major,手动注册 2. 否则动态分配 | 1. 手动指定:MKDEV (major, 0) 组合主 + 次设备号,register_chrdev_region 注册 2. 动态分配(推荐):alloc_chrdev_region 自动分配,MAJOR/MINOR 提取主 / 次设备号 |
| 183-187 | 字符设备初始化:1. cdev.owner = THIS_MODULE 2. cdev_init 绑定 fops 3. cdev_add 添加到内核 | 1. cdev_init:将 file_operations 绑定到 cdev 2. cdev_add:将 cdev 注册到内核,设备号生效 |
| 190-193 | class_create 创建类 | 类名 = TIMER_NAME,创建设备节点的前提,类目录在 /sys/class/timer |
| 196-199 | device_create 创建设备节点 | 最终生成 /dev/timer 节点,应用层可直接访问 |
| 202-204 | 定时器初始化: 1. init_timer 初始化结构体 2. 绑定回调函数 3. 传递参数 | 1. function = timer_function:指定超时后执行的函数 2. data = &timerdev:传递给回调函数的参数 3. 仅初始化,不启动(启动由 ioctl OPEN_CMD 控制) |
| 205 | 返回 0 | 驱动加载成功返回 0 失败返回错误码(如 PTR_ERR (class/device)) |
3.4、测试APP编写
测试APP的目标是实现一个简单的命令行交互程序,让用户可以通过输入命令来控制驱动程序。
功能需求:
① 程序运行后,提示用户输入命令。
② 输入 1,表示关闭定时器(LED停止闪烁)。
③ 输入 2,表示打开定时器(LED开始闪烁)。
④ 输入 3,表示设置定时器周期。输入此命令后,程序会再次提示用户输入周期值(单位为毫秒)。
1 #include "stdio.h"
2 #include "unistd.h"
3 #include "sys/types.h"
4 #include "sys/stat.h"
5 #include "fcntl.h"
6 #include "stdlib.h"
7 #include "string.h"
8 #include "linux/ioctl.h"
9 /***************************************************************
10 Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
11 文件名 : timerApp.c
12 描述 : 定时器测试应用程序
13 其他 : 无
14 使用方法 :./timertest /dev/timer 打开测试App
15 ***************************************************************/
16
17 /* 命令值 */
18 #define CLOSE_CMD (_IO(0XEF, 0x1)) /* 关闭定时器 */
19 #define OPEN_CMD (_IO(0XEF, 0x2)) /* 打开定时器 */
20 #define SETPERIOD_CMD (_IO(0XEF, 0x3)) /* 设置定时器周期命令 */
21
22 /*
23 * @description : main主程序
24 * @param - argc : argv数组元素个数
25 * @param - argv : 具体参数
26 * @return : 0 成功;其他 失败
27 */
28 int main(int argc, char *argv[])
29 {
30 int fd, ret;
31 char *filename;
32 unsigned int cmd;
33 unsigned int arg;
34 unsigned char str[100];
35
36 if (argc != 2) {
37 printf("Error Usage!\r\n");
38 return -1;
39 }
40
41 filename = argv[1];
42
43 fd = open(filename, O_RDWR);
44 if (fd < 0) {
45 printf("Can't open file %s\r\n", filename);
46 return -1;
47 }
48
49 while (1) {
50 printf("Input CMD:");
51 ret = scanf("%d", &cmd);
52 if (ret != 1) { /* 参数输入错误 */
53 gets(str); /* 防止卡死 */
54 }
55
56 if(cmd == 1) /* 关闭LED灯 */
57 cmd = CLOSE_CMD;
58 else if(cmd == 2) /* 打开LED灯 */
59 cmd = OPEN_CMD;
60 else if(cmd == 3) {
61 cmd = SETPERIOD_CMD; /* 设置周期值 */
62 printf("Input Timer Period:");
63 ret = scanf("%d", &arg);
64 if (ret != 1) { /* 参数输入错误 */
65 gets(str); /* 防止卡死 */
66 }
67 }
68 ioctl(fd, cmd, arg); /* 控制定时器的打开和关闭 */
69 }
70
71 close(fd);
72 return 0;
73 }代码解析:
* 第18-20行定义了与驱动约定好的命令宏。
* 第49-69行的 while(1) 循环构成了程序的主逻辑。它使用 fgets 获取用户输入,然后根据输入的数字执行相应的 ioctl 调用。
* 当用户输入 3 时,程序会再次提示输入周期值,然后调用 ioctl(fd, SETPERIOD_CMD, period),将周期值作为 ioctl 的第三个参数传递给驱动程序。
四、运行测试
4.1、编译驱动程序和测试APP
编写Makefile来编译内核驱动。本次实验的Makefile与之前的LED实验非常相似,只需将目标文件名修改为 timer.o 即可。
KERNELDIR := /home/duan/linux/linux-imx-rel_imx_4.1.15_2.1.1_ga_alientek_v2.2 CURRENT_PATH := $(shell pwd) obj-m := timer.o build: kernel_modules kernel_modules: $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules clean: $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
在终端中执行 make 命令,即可编译出驱动模块文件:
make -j32
编译成功后,会在当前目录下生成 timer.ko 驱动文件。
编译测试APP
测试APP是一个普通的Linux用户态程序,直接用交叉编译工具链编译即可:
arm-linux-gnueabihf-gcc timerApp.c -o timerApp
编译成功后,会生成 timerApp 可执行文件。
4.2、运行测试
将编译好的 timer.ko 驱动模块和 timerApp 测试程序,通过NFS(网络文件系统)或SCP等方式,拷贝到开发板根文件系统的 /lib/modules/4.1.15/ 目录中(此目录为示例,也可放在任意目录)。
sudo cp timer.ko /home/duan/linux/nfs/rootfs/lib/modules/4.1.15/ -f
进入该目录,首先使用 depmod 命令生成模块依赖关系(虽然不是必须的,但是一个好习惯),然后使用 insmod 命令加载驱动模块:
sudo cp timerApp /home/duan/linux/nfs/rootfs/lib/modules/4.1.15/ -f
depmod //第一次加载驱动的时候需要运行此命令 modprobe timer.ko//加载驱动
驱动加载成功后,执行测试程序:
./timerApp /dev/timer
程序运行后,终端会显示交互提示,如下图所示:

测试打开定时器:输入 2 并回车。此时,应该能看到开发板上的LED以默认的1秒为周期开始闪烁。
测试设置周期:输入 3 并回车。根据终端提示,输入新的周期值(如 500,代表500毫秒),如下图所示:
输入完毕后回车,LED的闪烁频率会立即变为500ms间隔,如下图所示:
测试关闭定时器:输入 1 并回车。此时LED应该停止闪烁,并保持在熄灭状态(或最后的状态)。
测试完成后,如需卸载驱动,可以使用 rmmod 命令:
rmmod timer.ko
常见问题排查:
* insmod 失败:检查内核版本是否与驱动编译时使用的内核源码版本一致。检查设备树节点名称和属性是否正确。
* LED不闪烁:检查GPIO引脚是否正确。使用 cat /sys/kernel/debug/gpio 查看GPIO状态,确认引脚是否被正确申请和设置为输出。检查定时器回调函数是否被调用,可以在回调函数中添加 printk 打印信息进行验证。
总结
本期博客基于I.MX6ULL开发板,从Linux内核时间管理基础入手,详解内核定时器API的使用,结合LED闪烁实战,手把手编写可控制周期的定时器驱动,附完整驱动代码、测试APP及运行演示。
未来趋势与展望:
随着硬件性能和实时性要求的提高,Linux内核也引入了更先进的定时器机制——高精度定时器(hrtimer)。hrtimer不依赖于周期性的系统节拍,而是利用硬件的高精度定时器,可以实现纳秒级的定时精度,并且支持更多灵活的触发模式(如绝对时间、相对时间)。对于需要极高时间精度的应用(如多媒体同步、工业以太网等),学习和使用hrtimer将是下一步的进阶方向。但无论如何,理解传统的基于 jiffies 的定时器,仍然是掌握Linux内核时间管理概念的基石。希望本文能为您后续的深入学习打下坚实的基础。
本文由主机测评网发布,不代表主机测评网立场,转载联系作者并注明出处:https:///linux/9606.html
