iTOP-STM32MP157开发板采用ST推出的双核cortex-A7+单核cortex-M4异构处理器,既可用Linux、又可以用于STM32单片机开发。开发板采用核心板+底板结构,主频650M、1G内存、8G存储,核心板采用工业级板对板连接器,高可靠,牢固耐用,可满足高速信号环境下使用。共240PIN,CPU功能全部引出:底板扩展接口丰富底板板载4G接口(选配)、千兆以太网、WIFI蓝牙模块HDMI、CAN、RS485、LVDS接口、温湿度传感器(选配)光环境传感器、六轴传感器、2路USB OTG、3路串口,CAMERA接口、ADC电位器、SPDIF、SDIO接口等
第六十一章 Linux内核定时器
本章导读
定时器是我们最常用到的功能,一般用来完成定时功能,本章我们就来学习一下 Linux 内核提供的定时器 API 函数,通过这些定时器 API 函数我们可以完成很多要求定时的应用。Linux内核也提供了短延时函数,比如微秒、纳秒、毫秒延时函数,本章我们就来学习一下这些和时间有关的功能。
61.1章节讲解了Linux内核时间管理的基础知识
61.2章节讲解了内核定时器简介和相关函数介绍
61.3章节以实验的形式进行Linux内核定时器实验,实现内核定时器每隔一秒打印“This is timer_function \n”。
本章内容对应视频讲解链接(在线观看):
内核定时器 → https://www.bilibili.com/video/BV1Vy4y1B7ta?p=40
程序源码在网盘资料“iTOP-STM32MP157开发板网盘资料汇总\09_嵌入式Linux开发指南(iTOP-STM32MP157)手册配套资料\驱动程序例程\19-Linux内核定时器”路径下。
61.1 Linux 内核时间管理
时间管理在内核中占有非常重要的地位。相对于事件驱动,内核中有大量的函数都是基于时间驱动的。
内核必须管理系统的运行时间以及当前的日期和时间。
周期产生的事件都是由系统定时器驱动的。系统定时器是一种可编程硬件芯片,它已固定频率产生中
断。该中断就是所谓的定时器中断,它所对应的中断处理程序负责更新系统时间,还负责执行需要周期性
运行的任务。系统定时器和时钟中断处理程序是 Linux 系统内核管理机制中的中枢。
61.1.1 内核中的时间概念
硬件为内核提供了一个系统定时器用以计算流逝的时间,系统定时器以某种频率自行触发时钟中断,该频率可以通过编程预定,称节拍率。当时钟中断发生时,内核就通过一种特殊中断处理程序对其进行处
理。内核知道连续两次时钟中断的间隔时间。这个间隔时间称为节拍(tick),内核就是靠这种已知的时钟
中断来计算墙上时间和系统运行时间。墙上时间即实际时间,内核提供了一组系统调用以获取实际日期和
实际时间。系统运行时间——自系统启动开始所经过的时间——对用户和内核都很有用,因为许多程序都必
须清楚流逝过的时间。
61.1.2 节拍率
系统定时器频率是通过静态预处理定义的,也就是 HZ,在系统启动时按照 Hz 对硬件进行设置。体系结构不同,HZ 的值也不同。内核在文件 <include/asm-generic/param.h> 中定义了 HZ 的实际值,节拍率就是 HZ,周期为 1/HZ。一般 ARM 体系结构的节拍率多数都等于 100。在编译 Linux 内核的时候可以通过图形化界面设置系统节拍率,按照如下路径打开配置界面:
-> Kernel Features
-> Timer frequency (<choice> [=y])
选中“Timer frequency”,打开以后如下图所示:
从上图可以看出可选的系统节拍率为 100Hz、200Hz、250Hz、300Hz、500Hz 和 1000Hz,默认情况下
选择 100Hz。设置好以后打开 Linux 内核源码根目录下的.config 文件,在此文件中有如下图所示定义:
上图中的 CONFIG_HZ 为 100,Linux 内核会使用 CONFIG_HZ 来设置自己的系统时钟。打开文件
include/asm-generic/param.h,有如下内容:
# undef HZ
# define HZ CONFIG_HZ
# define USER_HZ 100
# define CLOCKS_PER_SEC (USER_HZ)
第 7 行定义了一个宏 HZ,宏 HZ 就是 CONFIG_HZ,因此 HZ=100,我们后面编写 Linux 驱动的时候
会常常用到 HZ,因为 HZ 表示一秒的节拍数,也就是频率。
61.1.3 jiffies
全局变量 jiffies 用来记录自系统启动以来产生的节拍的总数。启动时,内核将该变量初始化为 0,此后,每次时钟中断处理程序都会增加该变量的值。因为一秒内时钟中断的次数等于 Hz,所以 jiffes 一秒内增加的值也就为 Hz,系统运行时间以秒为单位计算,就等于 jiffes/Hz。jiffes=seconds*HZ。jiffies 定义在文件include/linux/jiffies.h 中,定义如下:
extern u64 __jiffy_data jiffies_64;
extern unsigned long volatile __jiffy_data jiffies;
jiffies_64 和 jiffies 其实是同一个东西,jiffies_64 用于 64 位系统,而 jiffies 用于 32 位系统。
关键字 volatile 指示编译器在每次访问变量时都重新从主内存中获得,而不是通过寄存器中的变量别名访问,从而确保前面的循环能按预期的方式执行。
jiffies 变量总是无符号长整型(unsigned long),因此,在 32 位体系结构上是 32 位,在时钟频率为 100
的情况下,497 天后会溢出,如果频率是 1000,49.7 天后会溢出。
当jiffies变量的值超过它的最大存放范围后就会发生溢出,对于32位无符号长整型,最大取值为2^32-1,
在溢出前,定时器节拍计数最大为 4294967295,如果节拍数达到了最大值后还要继续增加的话,它的值会
回绕到 0。回绕会引起许多问题,下面的宏可以正确的处理节拍计数回绕的情况:
函数 |
作用 |
time_after(unkown, known) |
unkown 通常为 jiffies,known 通常是需要对比的值。 |
time_before(unkown, known) | |
time_after_eq(unkown, known) | |
time_before_eq(unkown, known) |
如果 unkown 超过 known 的话,time_after 函数返回真,否则返回假。如果 unkown 没有超过 known
的话 time_before 函数返回真,否则返回假。time_after_eq 函数和 time_after 函数类似,只是多了判断等
于这个条件。同理,time_before_eq 函数和 time_before 函数也类似。
为了方便开发,Linux 内核提供了几个 jiffies 和 ms、us、ns 之间的转换函数,如下所示:
函数 |
作用 |
int jiffies_to_msecs(const unsigned long j) |
将 jiffies 类型的参数 j 分别转换为对应的毫秒、微秒、纳秒。 |
int jiffies_to_usecs(const unsigned long j) | |
u64 jiffies_to_nsecs(const unsigned long j) | |
long msecs_to_jiffies(const unsigned int m) |
将毫秒、微秒、纳秒转换为 jiffies 类型。 |
long usecs_to_jiffies(const unsigned int u) | |
unsigned long nsecs_to_jiffies(u64 n) |
举例来说
<1>定时10ms
计算:jiffies +msecs_to_jiffies(10)
<2>定时10us
计算:jiffies +usecs_to_jiffies(10)
61.2 内核定时器简介
定时器,有时也称为动态定时器或内核定时器——是管理内核时间的基础。定时器的使用很简单。只需要执行一些初始化工作,设置一个超时时间,指定超时发生后执行的函数,然后激活定时器就可以了。指定的函数将在定时器到期时自动执行。定时器并不周期运行,它在超时后就自行销毁,这也正是这种定时器被称为动态定时器的一个原因。
Linux 内核使用 timer_list 结构体表示内核定时器,timer_list 定义在文件 include/linux/timer.h 中,定义如下:
struct timer_list {
/*
* All fields that change during normal runtime grouped to the
* same cacheline
*/
struct hlist_node entry;
unsigned long expires; /* 定时器超时时间,单位是节拍数 */
void (*function)(struct timer_list *);/* 定时处理函数 */
u32 flags;
#ifdef CONFIG_LOCKDEP
struct lockdep_map lockdep_map;
#endif
};
要使用内核定时器首先要先定义一个 timer_list 变量,表示定时器,tiemr_list 结构体的 expires 成员变量表示超时时间,单位为节拍数。比如我们现在需要定义一个周期为 2 秒的定时器,那么这个定时器的超时时间就是 jiffies+(2*HZ),因此 expires=jiffies+(2*HZ)。function 就是定时器超时以后的定时处理函数,我们要做的工作就放到这个函数里面,需要我们编写这个定时处理函数。
定义好定时器以后还需要通过一系列的 API 函数来初始化此定时器,这些函数如下:
函数 |
作用 |
void init_timer(struct timer_list *timer) |
初始化定时器 |
#define DEFINE_TIMER(_name, _function, _expires, _data) |
该宏会静态创建一个名叫 timer_name 内核定时器,并初始化其 function, expires, name 和 base 字段。 |
void add_timer(struct timer_list *timer) |
向 Linux 内核注册定时器,使用 add_timer 函数向内核注册定时器以后,定时器就会开始运行 |
int del_timer(struct timer_list * timer) |
删除一个定时器 |
int del_timer_sync(struct timer_list *timer) |
等待其他处理器使用完定时器再删除 |
int mod_timer(struct timer_list *timer,unsigned long expires) |
修改定时值,如果定时器还没有激活的话,mod_timer 函数会激活定时器 |
宏 |
#define DEFINE_TIMER(_name, _function) |
_name |
变量名 |
_function |
超时处理函数 |
功能 |
静态定义结构体变量并且初始化初始化function成员。 |
内核定时器一般的使用流程如下所示:
struct timer_list timer; /* 定义定时器 */
/* 定时器回调函数 */
void function(unsigned long arg)
{
/*
* 定时器处理代码
*/
/* 如果需要定时器周期性运行的话就使用 mod_timer
* 函数重新设置超时值并且启动定时器。
*/
mod_timer(&dev->timertest, jiffies + msecs_to_jiffies(2000));
}
/* 初始化函数 */
void init(void)
{
init_timer(&timer); /* 初始化定时器 */
timer.function = function; /* 设置定时处理函数 */
timer.expires=jffies + msecs_to_jiffies(2000);/* 超时时间 2 秒 */
timer.data = (unsigned long)&dev; /* 将设备结构体作为参数 */
add_timer(&timer); /* 启动定时器 */
}
/* 退出函数 */
void exit(void)
{
del_timer(&timer); /* 删除定时器 */
/* 或者使用 */
del_timer_sync(&timer);
}
有时候我们需要在内核中实现短延时,尤其是在 Linux 驱动中。Linux 内核提供了毫秒、微秒和纳秒
延时函数,这三个函数如下所示:
函数 |
作用 |
void ndelay(unsigned long nsecs) |
纳秒、微秒和毫秒延时函数。 |
void udelay(unsigned long usecs) | |
void mdelay(unsigned long mseces) |
61.3 实验测试
我们拷贝最简单的驱动文件helloworld.c和Makefile到Ubuntu的/home/nfs/23目录下,我们在此基础上进行修改,编写驱动代码如下所示:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/timer.h>
// 声明时间处理函数
static void timer_function(struct timer_list *data);
// 该宏会静态创建一个名叫 timer_name 内核定时器,
// 并初始化其 function, name字段。
DEFINE_TIMER(test_timer,timer_function);
/**
* @description:超时处理函数
* @param {*}
* @return {*}
*/
static void timer_function(struct timer_list *data)
{
printk(" This is timer_function \n");
/**
* @description: 修改定时值,如果定时器还没有激活的话,mod_timer 函数会激活定时器
* @param {1} *
* @return {*}
*/
mod_timer(&test_timer, jiffies + 1 * HZ);
}
static int hello_init(void)
{
printk("hello world! \n");
//初始化test_timer.expires意为超时时间
test_timer.expires = jiffies + 1 * HZ;
//定时器注册到内核里面,启动定时器
add_timer(&test_timer);
return 0;
}
static void hello_exit(void)
{
printk("gooodbye! \n");
// 删除一个定时器
del_timer(&test_timer);
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
我们将刚刚编写的驱动代码编译为驱动模块,如下图所示:
编译完加载驱动模块,如下图所示:
如上图所示,内核定时器每隔一秒打印“This is timer_function \n”。