
🎬 渡水无言 :个人主页渡水无言
❄专栏传送门 : 《linux专栏》《嵌入式linux驱动开发》《linux系统移植专栏》
❄专栏传送门 : 《freertos专栏》 《STM32 HAL库专栏》《linux裸机开发专栏》
❄专栏传送门 :《产品测评专栏》
⭐️流水不争先,争的是滔滔不绝
📚博主简介:第二十届中国研究生电子设计竞赛全国二等奖 |国家奖学金 | 省级三好学生
| 省级优秀毕业生获得者 | csdn新星杯TOP18 | 半导纵横专栏博主 | 211在读研究生
在这里主要分享自己学习的linux嵌入式领域知识;有分享错误或者不足的地方欢迎大佬指导,也欢迎各位大佬互相三连
目录
[1.1 内核时间管理简介](#1.1 内核时间管理简介)
[1.3、Linux 内核短延时函数](#1.3、Linux 内核短延时函数)
[3.3.1、设备结构体定义(33-47 行)](#3.3.1、设备结构体定义(33-47 行))
[3.3.2、LED GPIO 初始化(55-76)](#3.3.2、LED GPIO 初始化(55-76))
[3.3.3、设备打开函数(50-63 行)](#3.3.3、设备打开函数(50-63 行))
[3.3.4、ioctl 控制函数(核心)](#3.3.4、ioctl 控制函数(核心))
[3.3.5、定时器回调函数(144-159 行)](#3.3.5、定时器回调函数(144-159 行))
[3.3.6、驱动入口函数(166-206 行)](#3.3.6、驱动入口函数(166-206 行))
[4.1、编译驱动程序和测试 APP](#4.1、编译驱动程序和测试 APP)
前言
定时器是嵌入式开发中最常用的功能之一,用于实现定时触发、周期性任务等场景。本文基于I.MX6ULL开发板,从Linux内核时间管理基础入手,详解内核定时器API的使用,结合LED闪烁实战,手把手编写可控制周期的定时器驱动,附完整驱动代码、测试APP及运行演示。
一、核心基础:Linux时间管理与内核定时器
1.1 内核时间管理简介
和FreeRTOS类似,Linux运行也需要一个系统时钟源。
Cortex-A7内核常用通用定时器,具体实现无需深究。
系统通过定时中断计时,这个中断的周期性频率就是系统节拍率(tick rate),单位为Hz。
1.1.1、系统节拍率(HZ)配置
系统节拍率可在编译Linux内核时通过图形化界面配置,路径如下:
Kernel Features → Timer frequency
选中" Timer frequency ",打开以后如下图 所示:

可选值:100Hz(默认)、200Hz、250Hz、300Hz、500Hz、1000Hz,配置后会在.config文件中生成CONFIG_HZ定义,如下图所示:

高/低节拍率的优缺点
高节拍率(如1000Hz):时间精度高(1ms),适合对时间要求严格的场景,但中断频繁,会增加系统负担(现代处理器可忽略);
低节拍率(如100Hz):时间精度低(10ms),中断频率低,系统负担小,适合一般场景。
大家要根据自己的实际情况,选择合适的系统节拍率,本博客全程使用默认100Hz节拍率,满足基础开发需求。
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),日常开发直接使用jiffies即可。
关键换算:
jiffies / HZ = 系统运行时间(秒),例如HZ=100时,100个jiffies就是1秒。
1.1.3、jiffies绕回处理与转换函数
32位jiffies存在溢出风险(HZ=1000时,49.7天溢出),Linux内核提供专用函数处理绕回,同时提供jiffies与ms/us/ns的转换函数,无需手动计算:
绕回处理函数(常用)
| 函数 | 描述 |
|---|---|
| time_after(unkown, known) | unkown(通常为jiffies)超过known时,返回真 |
| time_before(unkown, known) | unkown未超过known时,返回真 |
| time_after_eq/ time_before_eq | 同上,增加"等于"判断 |
时间转换函数(常用)
| 函数 | 描述 |
|---|---|
| jiffies_to_msecs(j) | 将jiffies转换为毫秒 |
| msecs_to_jiffies(m) | 将毫秒转换为jiffies(最常用) |
| jiffies_to_usecs(j)/nsecs_to_jiffies(n) | jiffies与微秒/纳秒互转 |
1.2、内核定时器简介
Linux内核定时器基于系统时钟实现,无需配置硬件寄存器,只需设置超时时间 和定时处理函数,超时后自动执行处理函数。
⚠️ 关键注意:内核定时器不是周期性的,超时后会自动关闭,若需周期性定时,需在处理函数中重新开启定时器。
1.2.1、定时器结构体timer_list
内核定时器用timer_list结构体表示,定义在include/linux/timer.h中,核心成员如下(省略无关字段):
struct timer_list {
unsigned long expires; /* 超时时间,单位:节拍数(jiffies) */
void (*function)(unsigned long); /* 超时处理函数 */
unsigned long data; /* 传递给处理函数的参数 */
};
expires 成员变量表示超时时间,单位为节拍数。
比如我们现在需要定义一个周期为 2 秒的定时器,那么这个定时器的超时时间就是 jiffies+(2*HZ) ,因此 expires=jiffies+(2*HZ) 。
function 就是定时器超时以后的定时处理函数,我们要做的工作就放到这个函数里面,需要我们编写这个定时处理函数。
定义好定时器以后还需要通过一系列的 API 函数来初始化此定时器,这些函数如下
| API函数 | 功能描述 | 关键说明 |
|---|---|---|
| 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内核提供现成函数,无需手动实现:
ndelay(unsigned long nsecs):纳秒级延时
udelay(unsigned long usecs):微秒级延时(常用)
mdelay(unsigned long msecs):毫秒级延时
二、硬件原理分析(看过之前博客的可以忽略)
本实验使用通过设置一个定时器来实现周期性的闪烁 LED 灯,Led灯的原理图如下图所示:
从图中可以看出,LED0 接到了 GPIO_3 上,GPIO_3 就是 GPIO1_IO03,当 GPIO1_IO03 输出低电平 (0) 的时候发光二极管 LED0 就会导通点亮,当 GPIO1_IO03 输出高电平 (1) 的时候发光二极管 LED0 不会导通,因此 LED0 也就不会点亮。所以 LED0 的亮灭取决于 GPIO1_IO03 的输出电平,输出 0 就亮,输出 1 就灭。
三、实验程序编写
本期实验我们使用内核定时器周期性的点亮和熄灭开发板上的 LED 灯, LED 灯的闪烁周期由内核定时器来设置,测试应用程序可以控制内核定时器周期。
3.1、修改设备树文件
这部分可以看我之前的这篇博客的修改设备树文件小节:ARM Linux 驱动开发篇---基于 pinctrl+GPIO 子系统的 LED 驱动开发(设备树 + 驱动 + 测试全流程)-- Ubuntu20.04-CSDN博客
3.2定时器驱动程序编写( 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;
把字符设备、GPIO、定时器、自旋锁打包成一个设备结构体。
timeperiod:周期,单位 ms。
timer:内核定时器实体。
lock:自旋锁,保护周期变量。
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 }
从设备树 /gpioled 节点获取 GPIO 编号。
申请 GPIO,设置为输出,默认熄灭。
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 函数
应用程序调用 open 函数打开 /dev/timer 驱动文件的时候此函数就会执行。此函数设置文件私有数据为 timerdev。
并且初始化 定时周期默认为 1 秒,最后调用 led_init 函数初始化 LED 所使用的 IO 。
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 函数。
应用程序调用 ioctl 函数向驱动发送控制信息,此函数响应并执行。此函数有三个参数:filp , cmd 和 arg ,其中 filp 是对应的设备文件,cmd 是应用程序发送过来的命令信息, arg 是应用程序发送过来的参数(此例子里是定时周期)。
| 行号 | 代码逻辑 | 深度解析 |
|---|---|---|
| 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 }
行号 代码逻辑 深度解析 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)) 重启定时器实现周期性 timer_function为 定时器服务函数,
此函有一个参数 arg ,在本例程中 arg 参数就是 timerdev 的地址,这样通过 arg 参数就可以访问到设备结构体。当定时周期到了以后此函数就会被调用。在此函数中将 LED 灯的状态取反,实现 LED 灯闪烁的效果。
因为内核定时器不是循环的定时器,执行一次以后就结束了,因此在 161 行又调用了 mod_timer 函数重新开启定时器。
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 }
| 行号 | 代码逻辑 | 深度解析 |
|---|---|---|
| 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 我们要实现的内容如下:
①、运行 APP 以后提示我们输入要测试的命令,输入 1 表示关闭定时器、输入 2 表示打开定时器,输入 3 设置定时器周期。
②、如果要设置定时器周期的话,需要让用户输入要设置的周期值,单位为毫秒。
cpp
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) 循环,让用户输入要测试的命令,然后通过第68 行的 ioctl 函数发送给驱动程序。如果是设置定时器周期命令 SETPERIOD_CMD ,那么 ioctl 函数的 arg 参数就是用 户输入的周期值。
四、运行测试
4.1、编译驱动程序和测试 APP
编写 Makefile 文件,本次实验的 Makefile 文件和之前的led实验基本一样,只是将 obj-m 变量的值改为timer.o,Makefile 内容如下所示:
cpp
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
第 4 行,设置 obj-m 变量的值为timer.o。
输入如下命令编译出驱动模块文件:
make -j32
编译成功以后就会生成一个名为"timer.ko"的驱动模块文件。
编译测试 APP
输入如下命令编译测试timerApp.c这个测试程序:
cpp
arm-linux-gnueabihf-gcc timerApp.c -o timerApp
4.2、运行测试
将上一小节编译出来的timer.ko 和 timerApp这两个文件拷贝到 rootfs/lib/modules/4.1.15 目录中。
cpp
sudo cp timer.ko /home/duan/linux/nfs/rootfs/lib/modules/4.1.15/ -f
cpp
sudo cp timerApp /home/duan/linux/nfs/rootfs/lib/modules/4.1.15/ -f
进入到目录 lib/modules/4.1.15 中,输入如下命令加载timer.ko 驱动模块:
bash
depmod //第一次加载驱动的时候需要运行此命令
modprobe timer.ko//加载驱动
驱动加载成功以后如下命令来测试:
bash
./timerApp /dev/timer
输入上述命令以后终端提示输入命令,如下图所示:

输入" 2 ",打开定时器,此时 LED 灯就会以默认的 1 秒周期开始闪烁。在输入" 3 "来设置定时周期,根据提示输入要设置的周期值,如下图 所示:

输入" 500 ",表示设置定时器周期值为 500ms ,设置好以后 LED 灯就会以 500ms 为间隔,开始闪烁。

最后可以通过输入"1 "来关闭定时器,此时led会熄灭。

如果要卸载驱动的话输入如下命令即可:
bash
rmmod timer.ko
总结
本期博客基于I.MX6ULL开发板,从Linux内核时间管理基础入手,详解内核定时器API的使用,结合LED闪烁实战,手把手编写可控制周期的定时器驱动,附完整驱动代码、测试APP及运行演示。