Linux 内核定时器实验

Linux 内核定时器实验Linux时间管理和内核定时器简介50.1.1内核时间管理简介学习过UCOS或FreeRTOS的同学应该知道,UCOS或FreeRTOS是需要一个硬件定时器提供系统时钟,一般使用Systick作为系统时钟源。同理,Linux要运行,也是需要一个系统时钟的,至于这个系统时钟是由哪个定时器提供的,笔者没有去研究过Linux内核。Linux内核中有大量的函数需要时间管理,比如周期性的调度程序、延时程序、对于我们驱动编写者来说最常用的定时器。硬件定时器提

大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新Idea2022.1教程亲测有效,一键激活。

Jetbrains全系列IDE使用 1年只要46元 售后保障 童叟无欺

Linux 时间管理和内核定时器简介

50.1.1 内核时间管理简介
学习过 UCOS 或 FreeRTOS 的同学应该知道, UCOS 或 FreeRTOS 是需要一个硬件定时器
提供系统时钟,一般使用 Systick 作为系统时钟源。同理, Linux 要运行,也是需要一个系统时
钟的,至于这个系统时钟是由哪个定时器提供的,笔者没有去研究过 Linux 内核。
Linux 内核中有大量的函数需要时间管理,比如周期性的调度程序、延时程序、对于我们驱
动编写者来说最常用的定时器。硬件定时器提供时钟源,时钟源的频率可以设置, 设置好以后
就周期性的产生定时中断,系统使用定时中断来计时。中断周期性产生的频率就是系统频率,
也叫做节拍率(tick rate)(有的资料也叫系统频率),比如 1000Hz, 100Hz 等等说的就是系统节拍
率。系统节拍率是可以设置的,单位是 Hz,我们在编译 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,有如下内容:
示例代码 50.1.1.1 include/asm-generic/param.h 文件代码段
6 # undef HZ
7 # define HZ CONFIG_HZ
8 # define USER_HZ 100
9 # define CLOCKS_PER_SEC (USER_HZ)7 行定义了一个宏 HZ,宏 HZ 就是 CONFIG_HZ,因此 HZ=100,我们后面编写 Linux
驱动的时候会常常用到 HZ,因为 HZ 表示一秒的节拍数,也就是频率。
大多数初学者看到系统节拍率默认为 100Hz 的时候都会有疑问,怎么这么小? 100Hz 是可
选的节拍率里面最小的。为什么不选择大一点的呢?这里就引出了一个问题:高节拍率和低节
拍率的优缺点:
①、高节拍率会提高系统时间精度,如果采用 100Hz 的节拍率,时间精度就是 10ms,采用
1000Hz 的话时间精度就是 1ms,精度提高了 10 倍。高精度时钟的好处有很多,对于那些对时
间要求严格的函数来说,能够以更高的精度运行,时间测量也更加准确。
②、高节拍率会导致中断的产生更加频繁,频繁的中断会加剧系统的负担, 1000Hz 和 100Hz
的系统节拍率相比,系统要花费 10 倍的“精力”去处理中断。中断服务函数占用处理器的时间
增加,但是现在的处理器性能都很强大,所以采用 1000Hz 的系统节拍率并不会增加太大的负
载压力。根据自己的实际情况,选择合适的系统节拍率,本教程我们全部采用默认的 100Hz 系
统节拍率。
Linux 内核使用全局变量 jiffies 来记录系统从启动以来的系统节拍数,系统启动的时候会
将 jiffies 初始化为 0, jiffies 定义在文件 include/linux/jiffies.h 中,定义如下:
示例代码 50.1.1.2 include/jiffies.h 文件代码段
76 extern u64 __jiffy_data jiffies_64;
77 extern unsigned long volatile __jiffy_data jiffies;76 行,定义了一个 64 位的 jiffies_64。
第 77 行,定义了一个 unsigned long 类型的 32 位的 jiffies。
jiffies_64 和 jiffies 其实是同一个东西, jiffies_64 用于 64 位系统,而 jiffies 用于 32 位系统。
为了兼容不同的硬件, jiffies 其实就是 jiffies_64 的低 32.
当我们访问 jiffies 的时候其实访问的是 jiffies_64 的低 32 位,使用 get_jiffies_64 这个函数
可以获取 jiffies_64 的值。在 32 位的系统上读取 jiffies 的值,在 64 位的系统上 jiffes 和 jiffies_64
表示同一个变量,因此也可以直接读取 jiffies 的值。所以不管是 32 位的系统还是 64 位系统,
都可以使用 jiffies。
前面说了 HZ 表示每秒的节拍数, jiffies 表示系统运行的 jiffies 节拍数,所以 jiffies/HZ 就
是系统运行时间,单位为秒。不管是 32 位还是 64 位的 jiffies,都有溢出的风险,溢出以后会重
新从 0 开始计数,相当于绕回来了,因此有些资料也将这个现象也叫做绕回。假如 HZ 为最大
值 1000 的时候, 32 位的 jiffies 只需要 49.7 天就发生了绕回,对于 64 为的 jiffies 来说大概需要
5.8 亿年才能绕回,因此 jiffies_64 的绕回忽略不计。处理 32 位 jiffies 的绕回显得尤为重要,
Linux 内核提供了如表 50.1.1.1 所示的几个 API 函数来处理绕回。
函数                                       描述
time_after(unkown, known)
time_before(unkown, known) unkown 通常为 jiffies, known 通常是需要对比的值。
time_after_eq(unkown, known)
time_before_eq(unkown, known)50.1.1.1 处理绕回的 API 函数
如果 unkown 超过 known 的话, time_after 函数返回真,否则返回假。如果 unkown 没有超
过 known 的话 time_before 函数返回真,否则返回假。time_after_eq 函数和 time_after 函数类似,
只是多了判断等于这个条件。同理, time_before_eq 函数和 time_before 函数也类似。比如我们
要判断某段代码执行时间有没有超时,此时就可以使用如下所示代码:
示例代码 50.1.1.3 使用 jiffies 判断超时
1 unsigned long timeout;
2 timeout = jiffies + (2 * HZ); /* 超时的时间点 */
4 /************************************* 5 具体的代码 6 ************************************/
/* 判断有没有超时 */
9 if(time_before(jiffies, timeout)) { 

1   0 /* 超时未发生 */
11 } else { 

12 /* 超时发生 */
13 }
timeout 就是超时时间点,比如我们要判断代码执行时间是不是超过了 2 秒,那么超时时间
点就是 jiffies+(2*HZ),如果 jiffies 大于 timeout 那就表示超时了,否则就是没有超时。
为了方便开发, Linux 内核提供了几个 jiffies 和 ms、 us、 ns 之间的转换函数,如表 50.1.1.2
所示:
函数                                        描述
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)50.1.1.2 jiffies 和 ms、 us、 ns 之间的转换函数
50.1.2 内核定时器简介
定时器是一个很常用的功能,需要周期性处理的工作都要用到定时器。 Linux 内核定时器
采用系统时钟来实现,并不是我们在裸机篇中讲解的 PIT 等硬件定时器。 Linux 内核定时器使
用很简单,只需要提供超时时间(相当于定时值)和定时处理函数即可,当超时时间到了以后设
置的定时处理函数就会执行,和我们使用硬件定时器的套路一样,只是使用内核定时器不需要
做一大堆的寄存器初始化工作。在使用内核定时器的时候要注意一点,内核定时器并不是周期
性运行的,超时以后就会自动关闭,因此如果想要实现周期性定时,那么就需要在定时处理函
数中重新开启定时器。 Linux 内核使用 timer_list 结构体表示内核定时器, timer_list 定义在文件
include/linux/timer.h 中,定义如下(省略掉条件编译):
示例代码 50.1.2.1 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;
};
要使用内核定时器首先要先定义一个 timer_list 变量,表示定时器, tiemr_list 结构体的
expires 成员变量表示超时时间,单位为节拍数。比如我们现在需要定义一个周期为 2 秒的定时
器,那么这个定时器的超时时间就是 jiffies+(2*HZ),因此 expires=jiffies+(2*HZ)。 function 就是
定时器超时以后的定时处理函数,我们要做的工作就放到这个函数里面,需要我们编写这个定
时处理函数。
定义好定时器以后还需要通过一系列的 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 函数前定时器已
被激活。
关于内核定时器常用的 API 函数就讲这些,内核定时器一般的使用流程如下所示:
示例代码 50.1.2.2 内核定时器使用方法演示
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 }
50.1.3 Linux 内核短延时函数
有时候我们需要在内核中实现短延时,尤其是在 Linux 驱动中。 Linux 内核提供了毫秒、微
秒和纳秒延时函数,这三个函数如表 50.1.3.1 所示:
函数 描述
void ndelay(unsigned long nsecs)
void udelay(unsigned long usecs) 纳秒、微秒和毫秒延时函数。
void mdelay(unsigned long mseces)50.1.3.1 内核短延时函数

50.3 实验程序编写

本实验对应的例程路径为: 开发板光盘-> 2、 Linux 驱动例程-> 12_timer。
本章实验我们使用内核定时器周期性的点亮和熄灭开发板上的 LED 灯, LED 灯的闪烁周
期由内核定时器来设置,测试应用程序可以控制内核定时器周期。
50.3.1 修改设备树文件
本章实验使用到了 LED 灯, LED 灯的设备树节点信息使用 45.4.1 小节创建的即可。
50.3.2 定时器驱动程序编写
新建名为“ 12_timer”的文件夹,然后在 12_timer 文件夹里面创建 vscode 工程,工作区命
名为“ timer”。工程创建好以后新建 timer.c 文件.38~50 行,定时器设备结构体,在 48 行定义了一个定时器成员变量 timer。
第 60~82 行, LED 灯初始化函数,从设备树中获取 LED 灯信息,然后初始化相应的 IO。
第 91~102 行,函数 timer_open,对应应用程序的 open 函数,应用程序调用 open 函数打开
/dev/timer 驱动文件的时候此函数就会执行。此函数设置文件私有数据为 timerdev,并且初始化
定时周期默认为 1 秒,最后调用 led_init 函数初始化 LED 所使用的 IO。
第 111~137 行,函数 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 重新打开定时器,使定时器
以新的周期运行。
第 140~144 行,定时器驱动操作函数集 timer_fops。
第 147~162 行,函数 timer_function,定时器服务函数,此函有一个参数 arg,在本例程中
arg 参数就是 timerdev 的地址,这样通过 arg 参数就可以访问到设备结构体。当定时周期到了以
后此函数就会被调用。在此函数中将 LED 灯的状态取反,实现 LED 灯闪烁的效果。因为内核
定时器不是循环的定时器,执行一次以后就结束了,因此在 161 行又调用了 mod_timer 函数重
新开启定时器。
第 169~209 行,函数 timer_init,驱动入口函数。在第 205~207 行初始化定时器,设置定时
器的定时处理函数为 timer_function,另外设置要传递给 timer_function 函数的参数为 timerdev。
在此函数中并没有调用 timer_add 函数来开启定时器,因此定时器默认是关闭的,除非应用程序
发送打开命令。
第 216~231 行,驱动出口函数,在 219 行关闭 LED,也就是卸载驱动以后 LED 处于熄灭
状态。第 220 行调用 del_timer_sync 函数删除定时器,也可以使用 del_timer 函数。
50.3.3 编写测试 APP
测试 APP 我们要实现的内容如下:
①、运行 APP 以后提示我们输入要测试的命令,输入 1 表示关闭定时器、输入 2 表示打开
定时器,输入 3 设置定时器周期。
②、如果要设置定时器周期的话,需要让用户输入要设置的周期值,单位为毫秒。
新建名为 timerApp.c 的文件.22~24 行,命令值。
第 53~73 行, while(1)循环,让用户输入要测试的命令,然后通过第 72 行的 ioctl 函数发送
给驱动程序。如果是设置定时器周期命令 SETPERIOD_CMD,那么 ioctl 函数的 arg 参数就是用
户输入的周期值。

50.4 运行测试

50.4.1 编译驱动程序和测试 APP
1、编译驱动程序
编写 Makefile 文件,本章实验的 Makefile 文件和第四十章实验基本一样,只是将 obj-m 变
量的值改为 timer.o, Makefile 内容如下所示:
示例代码 50.4.1.1 Makefile 文件
1 KERNELDIR := /home/zuozhongkai/linux/IMX6ULL/linux/temp/linux-imxrel_imx_4.1.15_2.1.0_ga_alientek
......
4 obj-m := timer.o
......
11 clean:
12 $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
第 4 行,设置 obj-m 变量的值为 timer.o。
输入如下命令编译出驱动模块文件:
make -j32
编译成功以后就会生成一个名为“ timer.ko”的驱动模块文件。
2、编译测试 APP
输入如下命令编译测试 timerApp.c 这个测试程序:
arm-linux-gnueabihf-gcc timerApp.c -o timerApp
编译成功以后就会生成 timerApp 这个应用程序。
50.4.2 运行测试
将上一小节编译出来的 timer.ko 和 timerApp 这两个文件拷贝到 rootfs/lib/modules/4.1.15 目
录中,重启开发板,进入到目录 lib/modules/4.1.15 中,输入如下命令加载 timer.ko 驱动模块:
depmod //第一次加载驱动的时候需要运行此命令
modprobe timer.ko //加载驱动
驱动加载成功以后如下命令来测试:
./timerApp /dev/timer
输入上述命令以后终端提示输入命令
输入“ 2”,打开定时器,此时 LED 灯就会以默认的 1 秒周期开始闪烁。在输入“ 3”来设
置定时周期,根据提示输入要设置的周期值.
输入“ 500”,表示设置定时器周期值为 500ms,设置好以后 LED 灯就会以 500ms 为间隔,
开始闪烁。最后可以通过输入“ 1”来关闭定时器,如果要卸载驱动的话输入如下命令即可:
rmmod timer.ko

参考文献

【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.3.pdf

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/158894.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)


相关推荐

  • 登录Exchange 2013 OWA或ECP “HTTP 500内部服务器错误”的解决办法[通俗易懂]

    登录Exchange 2013 OWA或ECP “HTTP 500内部服务器错误”的解决办法[通俗易懂] 1. 安装好ExchangeServer2013后,打开IE访问https://mail.contoso.com/ecp或https://mail.contoso.com/owa,输入用户名密码登陆后提示“HTTP 500 内部服务器错误”,如下图1  2. 解决方法:打开ExchangeManagementShell,运行以下命令禁用邮箱后再启用邮箱,如下图2Di…

  • 重启IBMP750小型机之后:telnet登陆相当缓慢以及xmanger登陆工具无法打开图形界面的解决…

    重启IBMP750小型机之后:telnet登陆相当缓慢以及xmanger登陆工具无法打开图形界面的解决…shutdown-Fr重启IMB小机之后,使用xmanger工具telnet小机,可以打开telent,但是大约40多秒之后,才能登陆到机器,重启之前,telnet打开就能登陆。[@more@]原因是:该台小型机配置了DNS服…

  • Altium Designer 入门教程

    Altium Designer 入门教程注:使用了引用语法但不是引用:以下内容有部分来源于网络、博客等等,结尾会给出参考链接。(๑•ั็ω•็ั๑)希望大家可以自觉的在转载、转发时著名出处。(๑•.•๑)预防侵权,支持原创,支持开源,从你我做起。= ̄ω ̄=放在开始如果您喜欢我的文章,拜托点赞+收藏+关注,博主会根据大家喜好来推出相关系列文章~更多精彩内容也可以访问我的博客Aelous-BLog/***Copyright(C),2019-2020,xudongpo.cn*Author:许东坡*Email.

  • clion激活码_在线激活[通俗易懂]

    (clion激活码)最近有小伙伴私信我,问我这边有没有免费的intellijIdea的激活码,然后我将全栈君台教程分享给他了。激活成功之后他一直表示感谢,哈哈~IntelliJ2021最新激活注册码,破解教程可免费永久激活,亲测有效,下面是详细链接哦~https://javaforall.cn/100143.html…

  • lvs常用命令

    lvs常用命令lvs命令(具体参数的意思可以参看其他文章)1,查看配置ipvsadm-ln2,清空所有配置ipvsadm-Clistener添加:ipvsadm-A-t192.168.11.60:80-srr修改:ipvsadm-E-t192.168.11.60:80-swrr删除:ipvsadm-D-t192.168.11.60:80realserv…

  • 贴片电阻查询_贴片电阻的封装是什么

    贴片电阻查询_贴片电阻的封装是什么随着新技术的不断发展,目前电阻的种类有很多种,常见的有:薄膜和厚膜电阻(贴片电阻)、金属膜电阻、碳膜电阻、绕线电阻等。其中,贴片电阻器又可分为低阻值贴片电阻器,贴片电阻器阵列,贴片网络电阻器等。贴片电阻器的封装和尺寸的关系(长,宽,高)0201封装电阻对应的尺寸大小为(0.6,0.3,0.23),0402封装电阻对应的尺寸大小为(1.0,0.5,0.3),0603封装电阻对应的尺寸大小为(1.6,0.8,0.4),0805封装电阻对应的尺寸大小为(2.0,1.25,0.5),1206封装电阻对应的尺寸.

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号