1. 首页 > 服务器系统 > Linux

嵌入式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 ”条目后,打开配置选项,如下图 所示:

1.1.1、系统节拍率(HZ)配置

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

1.1.1、系统节拍率(HZ)配置_图2

高/低节拍率的优缺点及选择建议

选择节拍率本质上是在时间精度系统开销之间进行权衡。

特性高节拍率(如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-132switch 分支处理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 释放锁并恢复之前的中断状态。
110dev = filp->private_data从文件私有数据取出设备结构体指针,简化后续代码
111timerperiod 临时变量存储读取的周期值,避免自旋锁持有时间过长
112flags 自旋锁标志保存中断状态,配合 spin_lock_irqsave/restore 使用
115-117CLOSE_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返回 0IOCTL 执行成功返回 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 修改。
146dev = (struct timer_dev *)argarg 是初始化定时器时传入的 timerdev 地址(204 行),转换为结构体指针。
147static int sta = 1静态变量,保存 LED 状态(初始 1 = 熄灭),每次回调取反
151sta = !sta状态取反(1→0→1...),实现 LED 闪烁
152gpio_set_value(dev->led_gpio, sta)设置 GPIO 电平,控制 LED 亮灭
155-157自旋锁保护读 timeperiod读共享变量必须加锁,避免与 ioctl 的写操作冲突
158mod_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/ 下生成设备文件。
169spin_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-193class_create 创建类类名 = TIMER_NAME,创建设备节点的前提,类目录在 /sys/class/timer
196-199device_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

程序运行后,终端会显示交互提示,如下图所示:

4.2、运行测试

测试打开定时器:输入 2 并回车。此时,应该能看到开发板上的LED以默认的1秒为周期开始闪烁。

测试设置周期:输入 3 并回车。根据终端提示,输入新的周期值(如 500,代表500毫秒),如下图所示:
ARM Linux 驱动开发篇---内核定时器实验--- Ubuntu20.04_图3
输入完毕后回车,LED的闪烁频率会立即变为500ms间隔,如下图所示:
ARM Linux 驱动开发篇---内核定时器实验--- Ubuntu20.04_图4

测试关闭定时器:输入 1 并回车。此时LED应该停止闪烁,并保持在熄灭状态(或最后的状态)。
ARM Linux 驱动开发篇---内核定时器实验--- Ubuntu20.04_图5

测试完成后,如需卸载驱动,可以使用 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

联系我们

在线咨询:点击这里给我发消息

Q Q:2220678578