1、内核时间管理
UCOS 或 FreeRTOS 是需要一个硬件定时器提供系统时钟,一般使用 Systick 作为系统时钟源。同理, Linux 要运行,也是需要一个系统时钟的。
硬件定时器提供时钟源,时钟源的频率可以设置, 设置好以后就周期性的产生定时中断,系统使用定时中断来计时。中断周期性产生的频率就是系统频率,也叫做节拍率(tick rate),比如 1000Hz, 100Hz 说的就是系统节拍率(HZ 表示一秒的节拍数)。系统节拍率是可以设置的,单位是 Hz,我们在编译 Linux 内核的时候可以通过图形化界面设置系统节拍率。
-> Kernel Features
-> Timer frequency (<choice> [=y])
- 高节拍率和低节拍率的优缺点:
①、高节拍率会提高系统时间精度,如果采用 100Hz 的节拍率,时间精度就是 10ms,采用1000Hz 的话时间精度就是 1ms,精度提高了 10 倍。高精度时钟的好处有很多,对于那些对时间要求严格的函数来说,能够以更高的精度运行,时间测量也更加准确。
②、高节拍率会导致中断的产生更加频繁,频繁的中断会加剧系统的负担, 1000Hz 和 100Hz的系统节拍率相比,系统要花费 10 倍的“精力”去处理中断。
Linux 内核使用全局变量 jiffies 来记录系统从启动以来的系统节拍数,系统启动的时候会将 jiffies 初始化为 0, jiffies 定义在文件 include/linux/jiffies.h 中
76 extern u64 __jiffy_data jiffies_64;
77 extern unsigned long volatile __jiffy_data jiffies;
jiffies_64 和 jiffies 其实是同一个东西, jiffies_64 用于 64 位系统, jiffies 其实就是 jiffies_64 的低 32 位。
- HZ 表示每秒的节拍数, jiffies 表示系统运行的 jiffies 节拍数,所以 jiffies/HZ 就是系统运行时间,单位为秒。溢出风险,溢出以后会重新从 0 开始计数,API 函数来处理绕回。
如果 unkown 超过 known 的话, time_after 函数返回真,否则返回假。如果 unkown 没有超过 known 的话 time_before 函数返回真,否则返回假。 time_after_eq 函数和 time_after 函数类似。
使用 jiffies 判断超时
1 unsigned long timeout;
2 timeout = jiffies + (2 * HZ); /* 超时的时间点 */
3
4/*************************************
5 具体的代码
6 ************************************/
7
8/* 判断有没有超时 */
9 if(time_before(jiffies, timeout)) {
10 /* 超时未发生 */
11 } else {
12 /* 超时发生 */
13 }
timeout 就是超时时间点,比如我们要判断代码执行时间是不是超过了 2 秒,那么超时时间点就是 jiffies+(2*HZ),如果 jiffies 大于 timeout 那就表示超时了,否则就是没有超时。
2、内核定时器
Linux 内核定时器只需要提供超时时间(相当于定时值)和定时处理函数即可,当超时时间到了以后设置的定时处理函数就会执行,不需要做一大堆的寄存器初始化工作。软件定时器
在使用内核定时器的时候要注意一点,内核定时器并不是周期性运行的,超时以后就会自动关闭,因此如果想要实现周期性定时,那么就需要在定时处理函数中重新开启定时器。
Linux 内核使用 timer_list 结构体表示内核定时器, timer_list 定义在文件include/linux/timer.h 中
timer_list 结构体
struct timer_list {
struct list_head entry;
unsigned long expires; /* 定时器超时时间,单位是节拍数 */
struct tvec_base *base;
void (*function)(unsigned long); /* 定时处理函数 */
unsigned long data; /* 要传递给 function 函数的参数 */
int slack;
};
API函数
定义好定时器以后还需要通过一系列的 API 函数来初始化此定时器
1、 init_timer 函数
- init_timer 函数负责初始化 timer_list 类型变量,当我们定义了一个 timer_list 变量以后一定要先用 init_timer 初始化。 init_timer 函数原型如下:
void init_timer(struct timer_list *timer)
函数参数和返回值含义如下:
timer:要初始化定时器。
返回值: 没有返回值。
2、 add_timer 函数
- add_timer 函数用于向 Linux 内核注册定时器,使用 add_timer 函数向内核注册定时器以后,定时器就会开始运行,函数原型如下:
void add_timer(struct timer_list *timer)
函数参数和返回值含义如下:
timer:要注册的定时器。
返回值: 没有返回值。
3、 del_timer 函数
- del_timer 函数用于删除一个定时器,不管定时器有没有被激活,都可以使用此函数删除。在多处理器系统上,定时器可能会在其他的处理器上运行,因此在调用 del_timer 函数删除定时器之前要先等待其他处理器的定时处理器函数退出。 del_timer 函数原型如下:
int del_timer(struct timer_list * timer)
函数参数和返回值含义如下:
timer:要删除的定时器。
返回值: 0,定时器还没被激活; 1,定时器已经激活。
4、 del_timer_sync 函数
- del_timer_sync 函数是 del_timer 函数的同步版,会等待其他处理器使用完定时器再删除,del_timer_sync 不能使用在中断上下文中。 del_timer_sync 函数原型如下所示:
int del_timer_sync(struct timer_list *timer)
函数参数和返回值含义如下:
timer:要删除的定时器。
返回值: 0,定时器还没被激活; 1,定时器已经激活。
5、 mod_timer 函数
- mod_timer 函数用于修改定时值,如果定时器还没有激活的话, mod_timer 函数会激活定时器!函数原型如下:
int mod_timer(struct timer_list *timer, unsigned long expires)
函数参数和返回值含义如下:
timer:要修改超时时间(定时值)的定时器。
expires:修改后的超时时间。
返回值: 0,调用 mod_timer 函数前定时器未被激活; 1,调用 mod_timer 函数前定时器已被激活。
1 struct timer_list timer; /* 定义定时器 */
2
3/* 定时器回调函数 */
4 void function(unsigned long arg)
5 {
6 /*
7 * 定时器处理代码
8 */
9
10 /* 如果需要定时器周期性运行的话就使用 mod_timer
11 * 函数重新设置超时值并且启动定时器。
12 */
13 mod_timer(&dev->timertest, jiffies + msecs_to_jiffies(2000));
14 }
15
16 /* 初始化函数 */
17 void init(void)
18 {
19 init_timer(&timer); /* 初始化定时器 */
20
21 timer.function = function; /* 设置定时处理函数 */
22 timer.expires=jffies + msecs_to_jiffies(2000);/* 超时时间 2 秒 */
23 timer.data = (unsigned long)&dev; /* 将设备结构体作为参数 */
24
25 add_timer(&timer); /* 启动定时器 */
26 }
27
28 /* 退出函数 */
29 void exit(void)
30 {
31 del_timer(&timer); /* 删除定时器 */
32 /* 或者使用 */
33 del_timer_sync(&timer);
34 }
3、实现
通过设置一个定时器来实现周期性的闪烁 LED 灯:
- 使用内核定时器周期性的点亮和熄灭开发板上的 LED 灯, LED 灯的闪烁周期由内核定时器来设置,测试应用程序可以控制内核定时器周期。
驱动程序编写
新建名为“12_timer”的文件夹,然后在 12_timer 文件夹里面创建 vscode 工程,工作区命名为“timer”。工程创建好以后新建 timer.c 文件。
驱动代码段:
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>
#include <linux/timer.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#include <linux/irq.h>
#define TIMER_CNT 1 /*设备号个数*/
#define TIMER_NAME "timer" /*设备名字*/
#define CLOSE_CMD (_IO(0XEF,0x1)) /*关闭定时器*/
#define OPEN_CMD (_IO(0XEF,0x2)) /*打开定时器*/
#define SETPERIOD_CMD (_IO(0XEF,0x3)) /*设置定时器周期命令*/
#define LEDOFF 0 /*关灯*/
#define LEDON 1 /*开灯*/
/*timer设备结构体*/
struct timer_dev{
dev_t devid; /*设备号*/
struct cdev cdev; /*cdev*/
struct class *class; /*lei*/
struct device *device; /*设备*/
int major; /*主设备号*/
int minor; /*次设备号*/
struct device_node * nd; /*设备节点*/
int led_gpio; /*led所使用的gpio编号*/
int timerperiod; /*定时周期,单位为ms*/
struct timer_list timer; /*定义一个定时器*/
spinlock_t lock; /*定义自旋锁*/
};
struct timer_dev timerdev; /*定义timer设备*/
/*初始化 LED 灯 IO, open 函数打开驱动的时候初始化 LED 灯所使用的 GPIO 引脚。*/
static int led_init(void)
{
int ret=0;
/*1、获取设备节点:gpioled*/
timerdev.nd=of_find_node_by_path("/gpioled");
if(timerdev.nd == NULL){
printk("gpioled node can not found!\r\n");
return -EFAULT;
}else{
printk("gpioled node has been found!\r\n");
}
/*2、获取设备树中的gpio属性,得到LED所使用的LED编号*/
timerdev.led_gpio=of_get_named_gpio(timerdev.nd,"led-gpio",0);
if(timerdev.led_gpio<0){
printk("can't get led-gpio");
return -EFAULT;
}
printk("led-gpio num= %d\r\n",timerdev.led_gpio);
/*3、申请IO*/
ret = gpio_request(timerdev.led_gpio,"led");
if(ret){
printk("Failed to request the led\r\n");
return -EFAULT;
}
/* 4、设置GPIO_IO03为输出,并且输出高电平,默认关闭LED */
ret = gpio_direction_output(timerdev.led_gpio,1);
if(ret < 0) {
printk("can't set gpio!\r\n");
}
return 0;
}
/*打开设备*/
static int timer_open(struct inode *inode, struct file *filp)
{
int ret = 0;
filp->private_data = &timerdev; /* 设置私有数据 */
timerdev.timerperiod = 1000; /*默认周期1s*/
ret=led_init(); /*初始化LED IO*/
if(ret<0){
return ret;
}
return 0;
}
/*ioctl 函数,filp : 要打开的设备文件(文件描述符),
cmd : 应用程序发送过来的命令
arg : 参数
return : 0 成功;其他 失败*/
static long timer_unlocked_ioctl(struct file *filp,unsigned int cmd,unsigned long arg)
{
struct timer_dev *dev=(struct timer_dev*)filp->private_data;
int timerperiod;
unsigned long flags;
switch(cmd){
case CLOSE_CMD: /*关闭定时器*/
del_timer_sync(&dev->timer);
break;
case OPEN_CMD: /*打开定时器*/
spin_lock_irqsave(&dev->lock,flags);
timerperiod=dev->timerperiod;
spin_unlock_irqrestore(&dev->lock,flags);
mod_timer(&dev->timer,jiffies+msecs_to_jiffies(timerperiod));
break;
case SETPERIOD_CMD: /*设置定时器周期*/
spin_lock_irqsave(&dev->lock,flags);
dev->timerperiod=arg;
spin_unlock_irqrestore(&dev->lock,flags);
mod_timer(&dev->timer,jiffies+msecs_to_jiffies(arg));
break;
default:
break;
}
return 0;
}
/* 设备操作函数 */
static struct file_operations timer_fops={
.owner=THIS_MODULE,
.open=timer_open,
.unlocked_ioctl=timer_unlocked_ioctl,
};
/*定时器回调处理函数*/
void timer_function(unsigned long arg)
{
struct timer_dev *dev = (struct timer_dev *)arg;
static int sta=1;
int timerperiod;
unsigned long flags;
sta = !sta; /* 每次都取反,实现 LED 灯反转 */
gpio_set_value(dev->led_gpio,sta);
/*重启定时器*/
spin_lock_irqsave(&dev->lock,flags);
timerperiod=dev->timerperiod;
spin_unlock_irqrestore(&dev->lock,flags);
mod_timer(&dev->timer,jiffies+msecs_to_jiffies(dev->timerperiod));
}
/*模块入口函数*/
static int __init timer_init(void)
{
/*初始化自旋锁*/
spin_lock_init(&timerdev.lock);
/*注册字符设备驱动*/
/*1、注册设备号*/
if(timerdev.major){ /*定义设备号*/
timerdev.devid=MKDEV(timerdev.major,0);
register_chrdev_region(timerdev.devid,TIMER_CNT,TIMER_NAME);
}else{ /*没有定义设备*/
alloc_chrdev_region(&timerdev.devid,0,TIMER_CNT,TIMER_NAME); /*申请设备号*/
timerdev.major=MAJOR(timerdev.devid); /* 获取分配号的主设备号 */
timerdev.minor=MINOR(timerdev.devid);
}
printk("timerdev major=%d, minor=%d\r\n",timerdev.major,timerdev.minor);
/*2、初始化cdev*/
timerdev.cdev.owner=THIS_MODULE;
cdev_init(&timerdev.cdev,&timer_fops);
/*3、添加一个cdev*/
cdev_add(&timerdev.cdev,timerdev.devid,TIMER_CNT);
/*4、创建类*/
timerdev.class = class_create(THIS_MODULE, TIMER_NAME);
if (IS_ERR(timerdev.class)) {
return PTR_ERR(timerdev.class);
}
/*5、创建设备*/
timerdev.device = device_create(timerdev.class, NULL, timerdev.devid,NULL, TIMER_NAME);
if (IS_ERR(timerdev.device)) {
return PTR_ERR(timerdev.device);
}
/*6、初始化timer,设置定时器处理函数,还未设置周期,所以不会激活定时器*/
init_timer(&timerdev.timer);
timerdev.timer.function=timer_function;
timerdev.timer.data=(unsigned long)&timerdev;
return 0;
}
/*驱动出口*/
static void __exit timer_exit(void)
{
gpio_set_value(timerdev.led_gpio,1); /* 卸载驱动的时候关闭 LED */
del_timer_sync(&timerdev.timer); /*删除timer*/
/* 注销字符设备驱动 */
cdev_del(&timerdev.cdev); /* 删除 cdev */
unregister_chrdev_region(timerdev.devid, TIMER_CNT); /*注销设备号*/
device_destroy(timerdev.class, timerdev.devid);
class_destroy(timerdev.class);
/*释放IO*/
gpio_free(timerdev.led_gpio);
}
/*注册模块入口和出口*/
module_init(timer_init);
module_exit(timer_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("bruce");
- 第 31~43行,定时器设备结构体,在 41 行定义了一个定时器成员变量 timer。
- 第 48~82行, LED 灯初始化函数,从设备树中获取 LED 灯信息,然后初始化相应的 IO。
- 第 85~96 行,函数 timer_open,对应应用程序的 open 函数,应用程序调用 open 函数打开/dev/timer 驱动文件的时候此函数就会执行。此函数设置文件私有数据为 timerdev,并且初始化定时周期默认为 1 秒,最后调用 led_init 函数初始化 LED 所使用的 IO。
- 第 103~131 行,函数 timer_unlocked_ioctl,对应应用程序的 ioctl 函数,应用程序调用 ioctl函数向驱动发送控制信息,此函数响应并执行。此函数有三个参数: filp, cmd 和 arg,其中 filp是对应的设备文件, cmd 是应用程序发送过来的命令信息, arg 是应用程序发送过来的参数,在例程中 arg 参数表示定时周期。
一共有三种命令 CLOSE_CMD, OPEN_CMD 和 SETPERIOD_CMD,这三个命令分别为关闭定时器、打开定时器、设置定时周期。这三个命令的左右如下: -
- CLOSE_CMD: 关闭定时器命令, 调用 del_timer_sync 函数关闭定时器。
-
- OPEN_CMD:打开定时器命令,调用 mod_timer 函数打开定时器,定时周期为 timerdev 的timeperiod 成员变量,定时周期默认是 1 秒。
-
- SETPERIOD_CMD:设置定时器周期命令,参数 arg 就是新的定时周期,设置 timerdev 的timeperiod 成员变量为 arg 所表示定时周期指。并且使用 mod_timer 重新打开定时器,使定时器以新的周期运行。
- 第 134~138 行,定时器驱动操作函数集 timer_fops。
- 第 141~157行,函数 timer_function,定时器服务函数,此函有一个参数 arg,在本例程中arg 参数就是 timerdev 的地址,这样通过 arg 参数就可以访问到设备结构体。当定时周期到了以后此函数就会被调用。在此函数中将 LED 灯的状态取反,实现 LED 灯闪烁的效果。因为内核定时器不是循环的定时器,执行一次以后就结束了,因此在 156行又调用了mod_timer 函数重新开启定时器。
- 第 160~ 200行,函数 timer_init,驱动入口函数。在第197行初始化定时器,设置定时器的定时处理函数为 timer_function,另外设置要传递给 timer_function 函数的参数为 timerdev的地址。在此函数中并没有调用 timer_add 函数来开启定时器,因此定时器默认是关闭的,除非应用程序发送打开命令。
- 第 204~219 行,驱动出口函数,在 206 行关闭 LED,也就是卸载驱动以后 LED 处于熄灭状态。第207 行调用 del_timer_sync 函数删除定时器,也可以使用 del_timer 函数。
测试App
实现的内容如下:
- ①、运行 APP 以后提示我们输入要测试的命令,输入 1 表示关闭定时器、输入 2 表示打开定时器,输入 3 设置定时器周期。
- ②、如果要设置定时器周期的话,需要让用户输入要设置的周期值,单位为毫秒。
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
#include "linux/ioctl.h"
/* 使用方法 : ./timertest /dev/timer 打开测试 */
/*命令值*/
#define CLOSE_CMD (_IO(0xEF,0x1)) /*关闭定时器*/
#define OPEN_CMD (_IO(0xEF,0x2)) /*打开定时器*/
#define SETPERIOD_CMD (_IO(0xEF,0x3)) /*设置定时器周期命令*/
int main(int argc, char** argv[])
{
int fd,ret;
char * filename;
unsigned int cmd;
unsigned int arg;
unsigned char str[100];
if(argc!=2){
printf("Error Usage!\r\n");
return -1;
}
filename = argv[1];
/*打开led驱动*/
fd=open(filename,O_RDWR);
if(fd<0){
printf("file %s open failed!\r\n",argv[1]);
return -1;
}
/*向/dev/led文件写入数据*/
while(1){
printf("Input CMD:");
ret=scanf("%d",&cmd);
if (ret!=1){
gets(str); /*参数输入错误、防止卡死*/
}
if(cmd==1)
cmd=CLOSE_CMD;
else if (cmd==2)
cmd=OPEN_CMD;
else if(cmd==3){
cmd=SETPERIOD_CMD;
printf("Input Timer Period: ");
ret=scanf("%d",&arg);
if (ret!=1){
gets(str);
}
}
ioctl(fd,cmd,arg); /*控制定时器的打开和关闭*/
}
close(fd);/*关闭文件*/
}
- while(1)循环,让用户输入要测试的命令,然后通过第59行的 ioctl 函数发送给驱动程序。如果是设置定时器周期命令 SETPERIOD_CMD,那么 ioctl 函数的 arg 参数就是用户输入的周期值。
运行测试
编译驱动程序和测试 APP
- 将Makefile文件的 obj-m 变量的值改为 timer.o
- make命令编译成功后会生成一个名为“ timer.ko”的驱动模块文件
- 输入
arm-linux-gnueabihf-gcc timerApp.c -o timerApp
命令编译测试 timerApp.c 程序,生成 timerApp
运行测试
-
将编译出来的 timer.ko 和 timerApp 这两个文件拷贝到 rootfs/lib/modules/4.1.15目录中,重启开发板,进入到目录 lib/modules/4.1.15 中,输入如下命令加载 timer.ko 驱动模
块:insmod timer.ko
-
输入
./timerApp /dev/timer
命令终端提示输入命令 -
输入“2”,打开定时器,此时 LED 灯就会以默认的 1 秒周期开始闪烁。在输入“3”来设置定时周期,根据提示输入要设置的周期值
-
输入“500”,表示设置定时器周期值为 500ms,设置好以后 LED 灯就会以 500ms 为间隔,开始闪烁。最后可以通过输入“1”来关闭定时器
-
如果要卸载驱动的话输入如下命令即可:
rmmod timer.ko