嵌入式Linux驱动开发必修课:内核定时器原理、API详解与LED闪烁项目实战
<div id="navCategory"><h5 class="catalogue">目录</h5><ul class="first_class_ul"><li>前言</li><li>一、核心基础:Linux时间管理与内核定时器<ul class="second_class_ul"><li>1.1 内核时间管理简介</li><ul class="third_class_ul"><li>1.1.1、系统节拍率(HZ)配置</li><li>1.1.2、核心变量jiffies</li><li>1.1.3、jiffies绕回处理与转换函数</li></ul><li>1.2、内核定时器简介</li><ul class="third_class_ul"><li>1.2.1、定时器结构体timer_list</li><li>1.2.2、内核定时器使用流程</li></ul><li><strong>1.3、Linux内核短延时函数</strong></li><ul class="third_class_ul"></ul></ul></li><li>二、硬件原理分析(看过之前博客的可以忽略)<ul class="second_class_ul"></ul></li><li>三、实验程序编写<ul class="second_class_ul"><li><strong>3.1、修改设备树文件</strong></li><ul class="third_class_ul"></ul><li><strong>3.2 定时器驱动程序编写(timer.c)</strong></li><ul class="third_class_ul"></ul><li>3.3、分段分析</li><ul class="third_class_ul"><li>3.3.1、设备结构体定义(33-47 行)</li><li>3.3.2、LED GPIO 初始化(55-76行)</li><li>3.3.3、设备打开函数(50-63 行)</li><li>3.3.4、ioctl 控制函数(核心)</li><li>3.3.5、定时器回调函数(144-159 行)</li><li>3.3.6、驱动入口函数(166-206 行)</li></ul><li>3.4、测试APP编写</li><ul class="third_class_ul"></ul></ul></li><li><strong>四、运行测试</strong><ul class="second_class_ul"><li><strong>4.1、编译驱动程序和测试APP</strong></li><ul class="third_class_ul"></ul><li>4.2、运行测试</li><ul class="third_class_ul"></ul></ul></li><li>总结<ul class="second_class_ul"></ul></li></ul></div><p class="maodian"></p><h2>前言</h2><p>定时器是嵌入式开发中最常用的功能之一,用于实现定时触发、周期性任务、超时处理等核心场景。无论是用户态的应用,还是内核态的驱动,都离不开定时器的支持。本文基于I.MX6ULL开发板,从Linux内核时间管理基础入手,详解内核定时器API的使用,结合LED闪烁实战,手把手编写可控制周期的定时器驱动,附完整驱动代码、测试APP及运行演示。通过本文,您不仅能掌握内核定时器的基本用法,还能学会如何构建一个可动态调节周期的、具备实际工程价值的驱动程序。</p>
<p class="maodian"></p><h2>一、核心基础:Linux时间管理与内核定时器</h2>
<p class="maodian"></p><h3>1.1 内核时间管理简介</h3>
<p>与FreeRTOS、RT-Thread等实时操作系统类似,Linux内核的运行同样需要一个精准且稳定的系统时钟源来驱动。这个时钟源为内核的调度、时间片计算、超时判断等所有与时间相关的行为提供了基础。</p>
<p>对于Cortex-A7内核,其内部集成了通用的定时器,具体硬件实现细节(如PIT(周期性中断定时器)或GPTP)通常由芯片厂商(如NXP)进行封装,驱动开发者无需深究底层寄存器配置,只需理解内核提供的抽象层即可。</p>
<p>系统通过硬件定时器产生的周期性中断来“计时”,这个中断的周期性频率就是 <strong>系统节拍率(tick rate)</strong> ,单位为赫兹(Hz)。每个“滴答”(tick),内核都会进行一次时间更新和处理。</p>
<p class="maodian"></p><h4>1.1.1、系统节拍率(HZ)配置</h4>
<p>系统节拍率并不是一个固定值,而是可以在编译Linux内核时,根据项目需求进行灵活配置的。配置路径通常在内核源码根目录下执行 make menuconfig,然后导航至:</p>
<div class="dxycode"><pre class="brush:bash;">Kernel Features → Timer frequency</pre></div>
<p>选中“ Timer frequency ”条目后,打开配置选项,如下图 所示:</p>
<p><img alt="1.1.1、系统节拍率(HZ)配置" src="https://zhuji.jb51.net/uploads/allimg/20260316/1-2603161419223Z.png" /></p>
<p>可供选择的频率值通常包括:100 Hz(默认值)、200 Hz、250 Hz、300 Hz、500 Hz、1000 Hz。一旦选定,配置工具会自动在内核的 .config 文件中生成 CONFIG_HZ 宏定义,如下图所示:</p>
<p><img alt="1.1.1、系统节拍率(HZ)配置_图2" src="https://zhuji.jb51.net/uploads/allimg/20260316/1-260316141922c7.png" /></p>
<p><strong>高/低节拍率的优缺点及选择建议</strong></p>
<p>选择节拍率本质上是在<strong>时间精度</strong>和<strong>系统开销</strong>之间进行权衡。</p>
<table><tbody><tr><th>特性</th><th>高节拍率(如1000 Hz)</th><th>低节拍率(如100 Hz)</th></tr><tr><td><strong>时间精度</strong></td><td>高(1 ms),定时器超时时间误差小。</td><td>低(10 ms),定时器误差较大。</td></tr><tr><td><strong>中断频率</strong></td><td>高,每秒1000次中断。</td><td>低,每秒100次中断。</td></tr><tr><td><strong>系统负担</strong></td><td>相对较高,因为中断服务程序执行更频繁,会增加CPU上下文切换的开销。</td><td>相对较低,CPU有更多时间处理任务。</td></tr><tr><td><strong>适用场景</strong></td><td>对时间要求严格的场景,如音频处理、高精度测量、工业控制等。</td><td>对时间不敏感的通用场景,如大部分物联网设备、简单的用户交互产品。</td></tr><tr><td><strong>功耗影响</strong></td><td>较高,频繁唤醒CPU会增加功耗。</td><td>较低,有助于降低系统功耗。</td></tr></tbody></table>
<p><strong>专家建议:</strong> 对于现代高性能处理器(如Cortex-A7),1000 Hz带来的额外开销微乎其微,可以忽略不计。因此,除非你的项目对功耗有极致要求,或者运行在性能较弱的处理器上,否则选择1000 Hz可以获得更好的时间响应。本博客全程使用默认100 Hz节拍率,以满足基础教学和开发需求。</p>
<p class="maodian"></p><h4>1.1.2、核心变量jiffies</h4>
<p>Linux 内核使用一个全局变量 <strong>jiffies</strong> 来记录系统从启动以来的<strong>系统节拍数</strong>。可以把它想象成一个不断累加的计数器。系统启动的时候会将 jiffies 初始化为 0。jiffies 定义在文件 include/linux/jiffies.h 中,其定义形式如下:</p>
<div class="dxycode"><pre class="brush:bash;"> extern u64 __jiffy_data jiffies_64;
extern unsigned long volatile __jiffy_data jiffies;</pre></div>
<p>这里巧妙地定义了一个32位(jiffies)和64位(jiffies_64)的版本。在日常驱动开发中,我们直接使用32位的 jiffies 变量即可。jiffies_64 主要用于防止在运行时间极长的系统上出现64位计数器溢出的问题,由内核内部维护。</p>
<p><strong>关键换算公式:</strong><br />系统运行时间(秒) = jiffies / HZ<br />例如,当 HZ=100 时,jiffies 的值每增加100,就代表时间过去了1秒。</p>
<p class="maodian"></p><h4>1.1.3、jiffies绕回处理与转换函数</h4>
<p>由于32位的 jiffies 是一个有限长度的变量,其最大值约为42.9亿(2^32 -1)。当 HZ=1000 时,约49.7天后就会溢出并重新从0开始计数。这种现象称为 <strong>“绕回”(wrap around)</strong> 。如果直接使用 if (jiffies > timeout) 这样的判断,在发生绕回时就会得到错误的结果。</p>
<p>为了解决这个问题,Linux内核提供了一组专用的宏来处理绕回情况,确保时间比较的正确性。同时,内核也提供了便捷的函数用于 jiffies 和毫秒/微秒/纳秒之间的相互转换,开发者无需手动进行乘除运算,提高了代码的可读性和可移植性。</p>
<p><strong>绕回处理函数(常用)</strong></p>
<table><tbody><tr><th>函数</th><th>描述</th><th>使用示例</th></tr><tr><td>time_after(a, b)</td><td>当时间 a 在时间 b 之后时返回真,正确处理绕回。</td><td>if (time_after(jiffies, deadline)) { /* 超时了 */ }</td></tr><tr><td>time_before(a, b)</td><td>当时间 a 在时间 b 之前时返回真,正确处理绕回。</td><td>if (time_before(jiffies, deadline)) { /* 还没超时 */ }</td></tr><tr><td>time_after_eq(a, b)</td><td>当时间 a 在时间 b 之后或相等时返回真。</td><td></td></tr><tr><td>time_before_eq(a, b)</td><td>当时间 a 在时间 b 之前或相等时返回真。</td><td></td></tr></tbody></table>
<table><tbody><tr></tr><tr><td><p>time_after(unkown, known)</p></td><td><p>unkown(通常为jiffies)超过known时,返回真</p></td></tr><tr><td><p>time_before(unkown, known)</p></td><td><p>unkown未超过known时,返回真</p></td></tr><tr><td><p>time_after_eq/ time_before_eq</p></td><td><p>同上,增加“等于”判断</p></td></tr></tbody></table>
<p><strong>时间转换函数(常用)</strong></p>
<table><tbody><tr><th>函数</th><th>描述</th><th>方向</th></tr><tr><td>jiffies_to_msecs(j)</td><td>将 jiffies 转换为毫秒 (ms)。</td><td>jiffies -> ms</td></tr><tr><td>jiffies_to_usecs(j)</td><td>将 jiffies 转换为微秒 (µs)。</td><td>jiffies -> µs</td></tr><tr><td>msecs_to_jiffies(m)</td><td>将毫秒 (ms) 转换为 jiffies。<strong>最常用</strong></td><td>ms -> jiffies</td></tr><tr><td>usecs_to_jiffies(u)</td><td>将微秒 (µs) 转换为 jiffies。</td><td>µs -> jiffies</td></tr><tr><td>timespec_to_jiffies(ts)</td><td>将 timespec 结构体转换为 jiffies。</td><td></td></tr><tr><td>jiffies_to_timespec(j, ts)</td><td>将 jiffies 转换为 timespec 结构体。</td><td></td></tr></tbody></table>
<table><tbody><tr></tr><tr><td><p>jiffies_to_msecs(j)</p></td><td><p>将jiffies转换为毫秒</p></td></tr><tr><td><p>msecs_to_jiffies(m)</p></td><td><p>将毫秒转换为jiffies(最常用)</p></td></tr><tr><td><p>jiffies_to_usecs(j)/nsecs_to_jiffies(n)</p></td><td><p>jiffies与微秒/纳秒互转</p></td></tr></tbody></table>
<p><strong>重要提示:</strong>msecs_to_jiffies() 等转换函数返回的最小值是1。这意味着即使请求的超时时间小于一个tick(例如在HZ=100时请求1ms),它也会保证至少等待一个tick。这是为了确保定时器能够被正确调度。</p>
<p class="maodian"></p><h3>1.2、内核定时器简介</h3>
<p>Linux内核定时器是一种基于软件实现的定时机制,它完全依赖于上面提到的 jiffies 和系统时钟中断。使用内核定时器,我们无需去配置硬件定时器的寄存器,只需要设置一个<strong>超时时间</strong>(以jiffies为单位)和一个<strong>定时处理函数</strong>,当超时时间到达后,内核会在软中断上下文中自动执行这个处理函数。</p>
<p><strong>⚠️ 关键注意:</strong> 内核定时器本质上是<strong>单次触发(one-shot)</strong> 的。也就是说,定时器超时并执行完处理函数后,就会自动关闭。如果你需要实现周期性定时任务(如让LED以固定频率闪烁),必须在定时处理函数的末尾,使用 mod_timer 函数重新开启这个定时器。</p>
<p class="maodian"></p><h4>1.2.1、定时器结构体timer_list</h4>
<p>内核定时器使用 struct timer_list 结构体来描述,该结构体定义在 include/linux/timer.h 中。对于驱动开发者而言,主要关注以下几个核心成员(其他成员由内核内部管理):</p>
<div class="dxycode"><pre class="brush:bash;">struct timer_list {
unsigned long expires; /* 超时时间,单位:节拍数(jiffies) */
void (*function)(unsigned long); /* 超时处理函数 */
unsigned long data; /* 传递给处理函数的参数 */
};</pre></div>
<ul><li><strong>expires</strong>:这是一个无符号长整型变量,指定了定时器的<strong>超时时刻</strong>,其单位是节拍数。例如,如果我们想定义一个周期为2秒的定时器,那么它的超时时刻就是当前时刻加上2秒对应的节拍数,即 jiffies + (2 * HZ)。</li><li><strong>function</strong>:这是一个函数指针,指向定时器超时后要执行的处理函数。这是我们编写驱动程序时需要实现的核心逻辑,比如在这里翻转LED的电平。</li></ul>
<p>定义好 timer_list 结构体变量后,还需要通过一系列内核提供的API函数来初始化和管理它,这些函数如下:</p>
<table><tbody><tr><th>API函数</th><th>功能描述</th><th>关键说明</th></tr><tr><td>init_timer(timer)</td><td>初始化 timer_list 结构体。</td><td>在使用其他定时器函数之前,必须先调用此函数进行初始化。</td></tr><tr><td>add_timer(timer)</td><td>向内核注册一个定时器,并启动它。</td><td>调用此函数后,定时器开始运行,在 expires 指定的时刻触发。</td></tr><tr><td>del_timer(timer)</td><td>删除一个<strong>已经激活</strong>的定时器。</td><td>用于在模块卸载或不再需要定时器时,将其从内核的定时器链表中移除。</td></tr><tr><td>del_timer_sync(timer)</td><td>删除一个已经激活的定时器,并<strong>确保在其处理函数执行完毕后返回</strong>。</td><td>在多核处理器上,建议使用此函数代替 del_timer,以避免竞态条件。</td></tr><tr><td>mod_timer(timer, expires)</td><td>修改一个<strong>已经激活</strong>的定时器的超时时间,并重新启动它。</td><td>这是实现周期性定时器的关键函数。它也常用于在定时器超时前,延长或提前其触发时间。</td></tr></tbody></table>
<table><tbody><tr></tr><tr><td><p>init_timer(timer)</p></td><td><p>初始化定时器</p></td><td><p>定义timer后必须先初始化</p></td></tr><tr><td><p>add_timer(timer)</p></td><td><p>注册并启动定时器</p></td><td><p>启动后开始计时</p></td></tr><tr><td><p>del_timer(timer)</p></td><td><p>删除定时器</p></td><td><p>多处理器需注意同步</p></td></tr><tr><td><p>del_timer_sync(timer)</p></td><td><p>同步删除定时器</p></td><td><p>不可用于中断上下文</p></td></tr><tr><td><p>mod_timer(timer, expires)</p></td><td><p>修改超时时间,未激活则启动</p></td><td><p>周期性定时核心函数</p></td></tr></tbody></table>
<p class="maodian"></p><h4>1.2.2、内核定时器使用流程</h4>
<p>一个典型的内核定时器使用流程如下伪代码所示:</p>
<div class="dxycode"><pre class="brush:bash;">// 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);</pre></div>
<p class="maodian"></p><h3><strong>1.3、Linux内核短延时函数</strong></h3>
<p>在驱动开发中,我们经常需要非常短暂的延时,例如在操作硬件寄存器后等待其稳定,或者产生简单的脉冲信号。Linux内核提供了一系列精密的短延时函数,这些函数是忙等待(busy-wait)的,不会让出CPU,因此只适用于很短的时间。</p>
<ul><li><strong>ndelay(unsigned long nsecs)</strong>:纳秒级延时。精度最高,但开销也相对较大,适用于极短时间的等待。</li><li><strong>udelay(unsigned long usecs)</strong>:微秒级延时。<strong>最常用</strong>的函数,适用于大多数需要短延时的场景,如I2C、SPI等协议的时序模拟。</li><li><strong>mdelay(unsigned long msecs)</strong>:毫秒级延时。对于毫秒级的延时,也可以使用此函数,但更推荐使用内核定时器,因为 mdelay 会阻塞当前线程。</li></ul>
<p><strong>专家建议:</strong> 对于超过几个毫秒的延时,绝对不要使用 mdelay,因为它会浪费大量的CPU时间。这种情况下,应该始终使用内核定时器或等待队列等机制,让出CPU给其他有需要的任务。</p>
<p class="maodian"></p><h2>二、硬件原理分析(看过之前博客的可以忽略)</h2>
<p>本实验的核心目标是通过设置一个内核定时器来实现LED灯的周期性闪烁。LED灯作为最基础的输出设备,其硬件原理非常简单,非常适合作为驱动实验的载体。实验板上的LED原理图如下图所示:</p>
<p><img alt="硬件原理分析(看过之前博客的可以忽略)" src="https://zhuji.jb51.net/uploads/allimg/20260316/1-260316141923345.png" /></p>
<p>从图中可以清晰地看到,LED0的阳极通过一个限流电阻连接到3.3V电源,阴极则连接到了I.MX6ULL处理器的GPIO_3引脚。查阅芯片数据手册可知,GPIO_3实际上对应的是 <strong>GPIO1_IO03</strong>。</p>
<ul><li><strong>LED点亮条件</strong>:当 GPIO1_IO03 引脚被软件配置为输出模式,并输出<strong>低电平(0)</strong> 时,LED0的阴极电压为0V,与阳极的3.3V形成电压差,从而导通发光。</li><li><strong>LED熄灭条件</strong>:当 GPIO1_IO03 引脚输出<strong>高电平(1)</strong> 时,LED0的阴极电压也为3.3V,两端电压差为0,因此LED0熄灭。</li></ul>
<p>所以,控制LED的亮灭,本质上就是通过驱动程序来控制 GPIO1_IO03 引脚的电平状态。</p>
<p class="maodian"></p><h2>三、实验程序编写</h2>
<p>本期实验我们将编写一个内核定时器驱动,利用它来周期性地点亮和熄灭开发板上的LED灯。驱动的核心亮点在于,LED的闪烁周期不是硬编码在驱动里的,而是可以由用户态的测试应用程序动态设置和控制。</p>
<p class="maodian"></p><h3><strong>3.1、修改设备树文件</strong></h3>
<p>为了驱动能够找到并控制LED对应的GPIO,我们需要在设备树中为LED创建一个节点。这部分内容属于Linux设备树的基础知识,限于篇幅,本文不再赘述。具体的修改步骤和细节,可以参见我之前的一篇博客:ARM Linux 驱动开发篇---基于 pinctrl+GPIO 子系统的 LED 驱动开发(设备树 + 驱动 + 测试全流程)-- Ubuntu20.04-CSDN博客</p>
<p>简单来说,我们需要在根节点 / 下添加一个名为 gpioled 的子节点,并指定其使用的GPIO引脚。</p>
<p class="maodian"></p><h3><strong>3.2 定时器驱动程序编写(timer.c)</strong></h3>
<p>以下是完整的驱动程序源代码,我们将它命名为 timer.c。</p>
<div class="dxycode"><pre class="brush:bash;">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");</pre></div>
<p class="maodian"></p><h3>3.3、分段分析</h3>
<p>接下来,我们对驱动代码的关键部分进行详细解析,并补充必要的专业知识。</p>
<p class="maodian"></p><h4>3.3.1、设备结构体定义(33-47 行)</h4>
<div class="dxycode"><pre class="brush:bash;"> 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;</pre></div>
<p><strong>代码逻辑与深度解析</strong></p>
<ul><li><strong>timeperiod</strong>:用于保存用户设置的定时周期,单位是毫秒。这个变量会被定时器回调函数使用。</li><li><strong>timer</strong>:内核定时器的实体,所有的定时操作都围绕它进行。</li><li><strong>lock</strong>:这是一个自旋锁。它的作用是保护 timeperiod 这个共享资源。因为 timeperiod 既可能在上层 ioctl 函数(进程上下文)中被修改,也可能在定时器回调函数(软中断上下文)中被读取。为了防止并发访问导致的数据不一致,我们需要用自旋锁来保护它。<strong>注意</strong>:在定时器回调函数中,我们不能使用可能导致睡眠的锁(如信号量),而必须使用自旋锁。</li></ul>
<p class="maodian"></p><h4>3.3.2、LED GPIO 初始化(55-76行)</h4>
<div class="dxycode"><pre class="brush:bash;"> 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 }</pre></div>
<p><strong>代码逻辑与深度解析</strong></p>
<p><strong>查找设备树节点</strong>:of_find_node_by_path("/gpioled") 函数通过设备树的绝对路径查找我们之前创建的节点。</p>
<p><strong>获取GPIO编号</strong>:of_get_named_gpio 函数从找到的节点中,解析 led-gpio 属性,得到对应的全局GPIO编号。</p>
<p><strong>申请并配置GPIO</strong>:</p>
<ul><li>gpio_request:向内核申请对该GPIO的使用权。这是一个好习惯,可以防止其他驱动误用同一个引脚。</li><li>gpio_direction_output:将GPIO设置为输出模式,并初始化为高电平(1),确保LED初始状态是熄灭的。</li></ul>
<p class="maodian"></p><h4>3.3.3、设备打开函数(50-63 行)</h4>
<div class="dxycode"><pre class="brush:bash;"> 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 }</pre></div>
<p><strong>代码逻辑与深度解析</strong></p>
<p>函数 timer_open 对应于应用程序的 open 系统调用。其主要工作包括:<br />1. 将设备结构体的私有数据 private_data 设置为我们在入口函数中分配的 timerdev 指针。这样,在 read、write、ioctl 等其他文件操作函数中,我们就可以通过 filp->private_data 方便地获取到我们的设备结构体。<br />2. 初始化定时周期 timeperiod 为默认值1000毫秒(1秒)。<br />3. 调用 led_init 函数完成LED引脚的初始化工作。</p>
<p class="maodian"></p><h4>3.3.4、ioctl 控制函数(核心)</h4>
<div class="dxycode"><pre class="brush:bash;">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 }</pre></div>
<p><strong>代码逻辑与深度解析</strong></p>
<p>timer_unlocked_ioctl 是驱动与应用程序交互的核心接口。应用程序通过 ioctl 系统调用传入命令和参数,驱动在这里进行解析和执行。</p>
<table><tbody><tr><th>行号范围</th><th>代码逻辑</th><th>深度解析</th></tr><tr><td>114-117</td><td>获取设备结构体和自旋锁</td><td>从 filp->private_data 中取出设备结构体地址。然后,spin_lock_irqsave 用于获取自旋锁并同时保存和禁用本地中断。这是最安全的获取锁方式,可以防止在锁住期间被中断处理程序打断而形成死锁。</td></tr><tr><td>119-132</td><td>switch 分支处理</td><td><strong>CMD_CLOSE(关闭定时器)</strong>:调用 del_timer_sync(&dev->timer) 安全地删除定时器。确保定时器在被删除前不会再次触发。<strong>CMD_OPEN(打开定时器)</strong>:调用 mod_timer 函数,将定时器的超时时间设置为 jiffies + msecs_to_jiffies(dev->timeperiod),并启动定时器。<strong>CMD_SETPERIOD(设置周期)</strong>:将用户传入的 arg 参数(周期值)赋值给 dev->timeperiod。这里使用 spin_lock 和 spin_unlock 包围,确保对共享变量 timeperiod 的修改是原子的。</td></tr><tr><td>134</td><td>解锁</td><td>操作完成后,调用 spin_unlock_irqrestore 释放锁并恢复之前的中断状态。</td></tr></tbody></table>
<table><tbody><tr></tr><tr><td>110</td><td>dev = filp->private_data</td><td>从文件私有数据取出设备结构体指针,简化后续代码</td></tr><tr><td>111</td><td>timerperiod 临时变量</td><td>存储读取的周期值,避免自旋锁持有时间过长</td></tr><tr><td>112</td><td>flags 自旋锁标志</td><td>保存中断状态,配合 spin_lock_irqsave/restore 使用</td></tr><tr><td>115-117</td><td>CLOSE_CMD:del_timer_sync</td><td>安全删除定时器,等待回调函数执行完毕。</td></tr><tr><td>118-123</td><td><p>OPEN_CMD 逻辑:</p>
<p>1. 自旋锁保护读 timeperiod</p>
<p>2. mod_timer 启动定时器</p></td><td>,调用 mod_timer 函数打开定时器,定时周期为 timerdev 的 timeperiod 成员变量,定时周期默认是 1 秒。</td></tr><tr><td>124-129</td><td><p>SETPERIOD_CMD 逻辑:</p>
<p>1. 自旋锁保护写 timeperiod</p>
<p>2. mod_timer 重启定时器</p></td><td>设置定时器周期命令,参数 arg 就是新的定时周期。 设置 timerdev 的 timeperiod 成员变量为 arg 所表示定时周期指。 并且使用 mod_timer 重新打开定时器,使定时器以新的周期运行。</td></tr><tr><td>133</td><td>返回 0</td><td>IOCTL 执行成功返回 0,失败需返回对应错误码(如 -EINVAL)</td></tr></tbody></table>
<p class="maodian"></p><h4>3.3.5、定时器回调函数(144-159 行)</h4>
<div class="dxycode"><pre class="brush:bash;">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 }</pre></div>
<p><strong>代码逻辑与深度解析</strong></p>
<p>timer_function 是定时器超时后内核自动调用的函数。它运行在<strong>软中断上下文</strong>中,因此有以下重要限制:<strong>不能睡眠,不能访问用户空间内存,不能执行可能引起阻塞的操作。</strong></p>
<table><tbody><tr><th>行号范围</th><th>代码逻辑</th><th>深度解析</th></tr><tr><td>144-147</td><td>获取设备结构体</td><td>定时器回调函数的参数 arg 在初始化时被设置为设备结构体的地址。这里将其强制类型转换回来,以便访问LED引脚和周期变量。</td></tr><tr><td>149-153</td><td>翻转LED状态</td><td>这是实现闪烁的核心逻辑。gpio_get_value 读取当前引脚电平,然后 gpio_set_value 设置相反的电平。这样就实现了一次亮灭翻转。</td></tr><tr><td>155-158</td><td><strong>重新开启定时器</strong></td><td>由于内核定时器是单次的,为了形成连续的闪烁,这里再次调用 mod_timer。它将当前定时器的超时时间重新设置为 jiffies + msecs_to_jiffies(dev->timeperiod),从而实现周期性运行。注意,在访问 dev->timeperiod 之前,需要用自旋锁保护起来,防止在读取时被 ioctl 修改。</td></tr></tbody></table>
<table><tbody><tr></tr><tr><td>146</td><td>dev = (struct timer_dev *)arg</td><td>arg 是初始化定时器时传入的 timerdev 地址(204 行),转换为结构体指针。</td></tr><tr><td>147</td><td>static int sta = 1</td><td>静态变量,保存 LED 状态(初始 1 = 熄灭),每次回调取反</td></tr><tr><td>151</td><td>sta = !sta</td><td>状态取反(1→0→1...),实现 LED 闪烁</td></tr><tr><td>152</td><td>gpio_set_value(dev->led_gpio, sta)</td><td>设置 GPIO 电平,控制 LED 亮灭</td></tr><tr><td>155-157</td><td>自旋锁保护读 timeperiod</td><td>读共享变量必须加锁,避免与 ioctl 的写操作冲突</td></tr><tr><td>158</td><td>mod_timer(&dev->timer, jiffies + msecs_to_jiffies(dev->timeperiod))</td><td><strong>重启定时器实现周期性</strong></td></tr></tbody></table>
<p class="maodian"></p><h4>3.3.6、驱动入口函数(166-206 行)</h4>
<div class="dxycode"><pre class="brush:bash;">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 }</pre></div>
<p><strong>代码逻辑与深度解析</strong></p>
<table><tbody><tr><th>行号范围</th><th>代码逻辑</th><th>深度解析</th></tr><tr><td>170-185</td><td>分配设备结构体内存</td><td>使用 kzalloc 分配 struct timer_dev 大小的内存,并用0初始化。GFP_KERNEL 标志表示分配过程可以睡眠,适用于进程上下文。</td></tr><tr><td>186-193</td><td>初始化定时器和自旋锁</td><td><strong>初始化定时器</strong>:init_timer(&dev->timer)。<strong>设置回调函数和数据</strong>:dev->timer.function = timer_function; dev->timer.data = (unsigned long)dev;,将设备结构体的地址作为参数传递给回调函数。<strong>初始化自旋锁</strong>:spin_lock_init(&dev->lock);。</td></tr><tr><td>194-206</td><td>注册字符设备</td><td>这是标准的字符设备注册流程:<br />1. <strong>分配设备号</strong>:alloc_chrdev_region 动态分配主设备号和次设备号。<br />2. <strong>初始化cdev</strong>:cdev_init 将 dev->cdev 与我们定义的文件操作集 timer_fops 关联起来。<br />3. <strong>添加cdev到内核</strong>:cdev_add 将字符设备正式注册到系统中。<br />4. <strong>创建设备类</strong>:class_create 在 /sys/class/ 下创建一个类。<br />5. <strong>创建设备节点</strong>:device_create 在该类下创建一个设备,这会自动在 /dev/ 下生成设备文件。</td></tr></tbody></table>
<table><tbody><tr></tr><tr><td>169</td><td>spin_lock_init(&timerdev.lock)</td><td>初始化自旋锁(必须!否则自旋锁使用会崩溃)</td></tr><tr><td>173-180</td><td><p>设备号分配逻辑:</p>
<p>1. 若指定 major,手动注册</p>
<p>2. 否则动态分配</p></td><td><p>1. 手动指定:MKDEV (major, 0) 组合主 + 次设备号,register_chrdev_region 注册</p>
<p>2. 动态分配(推荐):alloc_chrdev_region 自动分配,MAJOR/MINOR 提取主 / 次设备号</p></td></tr><tr><td>183-187</td><td><p>字符设备初始化:1. cdev.owner = THIS_MODULE</p>
<p>2. cdev_init 绑定 fops</p>
<p>3. cdev_add 添加到内核</p></td><td><p>1. cdev_init:将 file_operations 绑定到 cdev</p>
<p>2. cdev_add:将 cdev 注册到内核,设备号生效</p></td></tr><tr><td>190-193</td><td>class_create 创建类</td><td>类名 = TIMER_NAME,创建设备节点的前提,类目录在 /sys/class/timer</td></tr><tr><td>196-199</td><td>device_create 创建设备节点</td><td>最终生成 /dev/timer 节点,应用层可直接访问</td></tr><tr><td>202-204</td><td><p>定时器初始化:</p>
<p>1. init_timer 初始化结构体</p>
<p>2. 绑定回调函数</p>
<p>3. 传递参数</p></td><td><p>1. function = timer_function:指定超时后执行的函数</p>
<p>2. data = &timerdev:传递给回调函数的参数</p>
<p>3. 仅初始化,不启动(启动由 ioctl OPEN_CMD 控制)</p></td></tr><tr><td>205</td><td>返回 0</td><td><p>驱动加载成功返回 0</p>
<p>失败返回错误码(如 PTR_ERR (class/device))</p></td></tr></tbody></table>
<p class="maodian"></p><h3>3.4、测试APP编写</h3>
<p>测试APP的目标是实现一个简单的命令行交互程序,让用户可以通过输入命令来控制驱动程序。</p>
<p><strong>功能需求:</strong><br />① 程序运行后,提示用户输入命令。<br />② 输入 1,表示关闭定时器(LED停止闪烁)。<br />③ 输入 2,表示打开定时器(LED开始闪烁)。<br />④ 输入 3,表示设置定时器周期。输入此命令后,程序会再次提示用户输入周期值(单位为毫秒)。</p>
<div class="dxycode"><pre class="brush:bash;">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;
35
36 if (argc != 2) {
37 printf("Error Usage!\r\n");
38 return -1;
39 }
40
41 filename = argv;
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 }</pre></div>
<p><strong>代码解析:</strong><br />* 第18-20行定义了与驱动约定好的命令宏。<br />* 第49-69行的 while(1) 循环构成了程序的主逻辑。它使用 fgets 获取用户输入,然后根据输入的数字执行相应的 ioctl 调用。<br />* 当用户输入 3 时,程序会再次提示输入周期值,然后调用 ioctl(fd, SETPERIOD_CMD, period),将周期值作为 ioctl 的第三个参数传递给驱动程序。</p>
<p class="maodian"></p><h2><strong>四、运行测试</strong></h2>
<p class="maodian"></p><h3><strong>4.1、编译驱动程序和测试APP</strong></h3>
<p>编写Makefile来编译内核驱动。本次实验的Makefile与之前的LED实验非常相似,只需将目标文件名修改为 timer.o 即可。</p>
<div class="dxycode"><pre class="brush:bash;">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
</pre></div>
<p>在终端中执行 make 命令,即可编译出驱动模块文件:</p>
<div class="dxycode"><pre class="brush:bash;">make -j32</pre></div>
<p>编译成功后,会在当前目录下生成 timer.ko 驱动文件。</p>
<p><strong>编译测试APP</strong></p>
<p>测试APP是一个普通的Linux用户态程序,直接用交叉编译工具链编译即可:</p>
<div class="dxycode"><pre class="brush:bash;">arm-linux-gnueabihf-gcc timerApp.c -otimerApp</pre></div>
<p>编译成功后,会生成 timerApp 可执行文件。</p>
<p class="maodian"></p><h3>4.2、运行测试</h3>
<p>将编译好的 timer.ko 驱动模块和 timerApp 测试程序,通过NFS(网络文件系统)或SCP等方式,拷贝到开发板根文件系统的 /lib/modules/4.1.15/ 目录中(此目录为示例,也可放在任意目录)。</p>
<div class="dxycode"><pre class="brush:bash;">sudo cp timer.ko /home/duan/linux/nfs/rootfs/lib/modules/4.1.15/ -f</pre></div>
<p>进入该目录,首先使用 depmod 命令生成模块依赖关系(虽然不是必须的,但是一个好习惯),然后使用 insmod 命令加载驱动模块:</p>
<div class="dxycode"><pre class="brush:bash;">sudo cptimerApp /home/duan/linux/nfs/rootfs/lib/modules/4.1.15/ -f</pre></div>
<div class="dxycode"><pre class="brush:bash;">depmod //第一次加载驱动的时候需要运行此命令
modprobe timer.ko//加载驱动</pre></div>
<p>驱动加载成功后,执行测试程序:</p>
<div class="dxycode"><pre class="brush:bash;">./timerApp /dev/timer</pre></div>
<p>程序运行后,终端会显示交互提示,如下图所示:</p>
<p><img alt="4.2、运行测试" src="https://zhuji.jb51.net/uploads/allimg/20260316/1-260316141923W3.png" /></p>
<p><strong>测试打开定时器</strong>:输入 2 并回车。此时,应该能看到开发板上的LED以默认的1秒为周期开始闪烁。</p>
<p><strong>测试设置周期</strong>:输入 3 并回车。根据终端提示,输入新的周期值(如 500,代表500毫秒),如下图所示:<br /><img alt="ARM Linux 驱动开发篇---内核定时器实验--- Ubuntu20.04_图3" src="https://zhuji.jb51.net/uploads/allimg/20260316/1-260316141923b4.png" /><br />输入完毕后回车,LED的闪烁频率会立即变为500ms间隔,如下图所示:<br /><img alt="ARM Linux 驱动开发篇---内核定时器实验--- Ubuntu20.04_图4" src="https://zhuji.jb51.net/uploads/allimg/20260316/1-2603161419244T.png" /></p>
<p><strong>测试关闭定时器</strong>:输入 1 并回车。此时LED应该停止闪烁,并保持在熄灭状态(或最后的状态)。<br /><img alt="ARM Linux 驱动开发篇---内核定时器实验--- Ubuntu20.04_图5" src="https://zhuji.jb51.net/uploads/allimg/20260316/1-260316141924160.png" /></p>
<p>测试完成后,如需卸载驱动,可以使用 rmmod 命令:</p>
<div class="dxycode"><pre class="brush:bash;">rmmod timer.ko</pre></div>
<p><strong>常见问题排查:</strong><br />* <strong>insmod 失败</strong>:检查内核版本是否与驱动编译时使用的内核源码版本一致。检查设备树节点名称和属性是否正确。<br />* <strong>LED不闪烁</strong>:检查GPIO引脚是否正确。使用 cat /sys/kernel/debug/gpio 查看GPIO状态,确认引脚是否被正确申请和设置为输出。检查定时器回调函数是否被调用,可以在回调函数中添加 printk 打印信息进行验证。</p>
<p class="maodian"></p><h2>总结</h2>
<p>本期博客基于I.MX6ULL开发板,从Linux内核时间管理基础入手,详解内核定时器API的使用,结合LED闪烁实战,手把手编写可控制周期的定时器驱动,附完整驱动代码、测试APP及运行演示。</p>
<p><strong>未来趋势与展望:</strong><br />随着硬件性能和实时性要求的提高,Linux内核也引入了更先进的定时器机制——<strong>高精度定时器(hrtimer)</strong>。hrtimer不依赖于周期性的系统节拍,而是利用硬件的高精度定时器,可以实现纳秒级的定时精度,并且支持更多灵活的触发模式(如绝对时间、相对时间)。对于需要极高时间精度的应用(如多媒体同步、工业以太网等),学习和使用hrtimer将是下一步的进阶方向。但无论如何,理解传统的基于 jiffies 的定时器,仍然是掌握Linux内核时间管理概念的基石。希望本文能为您后续的深入学习打下坚实的基础。</p>
頁:
[1]