ARM Linux 驱动开发篇---内核定时器实验--- Ubuntu20.04

🎬 渡水无言个人主页渡水无言

专栏传送门 : 《linux专栏》《嵌入式linux驱动开发》《linux系统移植专栏》

专栏传送门 : 《freertos专栏》 《STM32 HAL库专栏》《linux裸机开发专栏

专栏传送门《产品测评专栏

⭐️流水不争先,争的是滔滔不绝

📚博主简介:第二十届中国研究生电子设计竞赛全国二等奖 |国家奖学金 | 省级三好学生

| 省级优秀毕业生获得者 | csdn新星杯TOP18 | 半导纵横专栏博主 | 211在读研究生

在这里主要分享自己学习的linux嵌入式领域知识;有分享错误或者不足的地方欢迎大佬指导,也欢迎各位大佬互相三连

目录

前言

一、核心基础:Linux时间管理与内核定时器

[1.1 内核时间管理简介](#1.1 内核时间管理简介)

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

1.1.2、核心变量jiffies

1.1.3、jiffies绕回处理与转换函数

1.2、内核定时器简介

1.2.1、定时器结构体timer_list

1.2.2、内核定时器使用流程

[1.3、Linux 内核短延时函数](#1.3、Linux 内核短延时函数)

二、硬件原理分析(看过之前博客的可以忽略)

三、实验程序编写

3.1、修改设备树文件

3.2定时器驱动程序编写(timer.c)

3.3、分段分析

[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 行))

3.4、测试APP编写

四、运行测试

[4.1、编译驱动程序和测试 APP](#4.1、编译驱动程序和测试 APP)

4.2、运行测试

总结


前言

定时器是嵌入式开发中最常用的功能之一,用于实现定时触发、周期性任务等场景。本文基于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及运行演示。

相关推荐
燃于AC之乐1 小时前
OpenClaw“小龙虾”深度解析:60天碾压Linux的AI智能体,从原理到搞定本地部署【Windows系统 + 接入飞书】
linux·人工智能·飞书·openclaw·小龙虾
进击切图仔1 小时前
Linux 下 USB 设备端口错乱问题排查与解决
linux·运维·服务器
智能物联网开发1 小时前
机器人电子皮肤系统开发:36通道柔性触觉阵列 + FPGA高速采集
fpga开发·计算机外设·嵌入式·fpga数据采集
Irissgwe1 小时前
基础I/O
java·linux·前端
洛菡夕1 小时前
Linux系统安全
linux
yangyanping201082 小时前
Linux学习四之 rm 命令详解
linux·运维·学习
济6172 小时前
工业级板载存储新选择:创世 SD NAND 实测-------产品测试专栏
嵌入式·tf卡·板载存储
艾莉丝努力练剑2 小时前
静态地址重定位与动态地址重定位:Linux操作系统的视角
java·linux·运维·服务器·c语言·开发语言·c++
gfdhy2 小时前
【Linux】服务器网络与安全核心配置|静态IP+SSH加固+防火墙,公网服务器必学实操
linux·服务器·网络·tcp/ip·算法·安全·哈希算法