本文包含源代码、原理图、PCB、封装库、中英文PDF等资源
您需要 登录 才可以下载或查看,没有账号?立即注册
x
前言
本文记录RT-Thread的时钟相关知识,包括时钟节拍、RT-Thread定时器工作机制以及定时器的管理方式,在定时器方面有硬件定时器和软件定时器,这里不讲硬件定时器,硬件定时器只需学习裸机时候的硬件定时器即可。后面进行实际的操作时采用STM32L475VET6,RTT&正点原子联合出品潘多拉开发板进行实验。
一、时钟节拍
任何操作系统都需要提供一个时钟节拍,以供系统处理所有和时间有关的事件,如线程的延时、线程的时间片轮转调度以及定时器超时等。时钟节拍是特定的周期性中断,这个中断可以看做是系统心跳,中断之间的时间间隔取决于不同的应用,一般是 1ms–100ms,时钟节拍率越快,系统的额外开销就越大,从系统启动开始计数的时钟节拍数称为系统时间。
1、定义时钟节拍大小
RT-Thread 中,时钟节拍的长度可以根据 RT_TICK_PER_SECOND 的定义来调整,RT_TICK_PER_SECOND 在rtconfig.h里面定义,时钟节拍的长度等于 1/RT_TICK_PER_SECOND 秒,如下是1个时钟节拍为1ms:
- #define RT_TICK_PER_SECOND 1000 //定义时钟节拍,为1000时表示1000个tick每 秒,一个tick为1ms
复制代码
2、时钟节拍的实现方式
时钟节拍由配置为中断触发模式的硬件定时器产生。
在前面讲移植RT-Thread的时候,修改board.c中,有如下函数:
- void SysTick_Handler(void)
- {
- /* enter interrupt */
- rt_interrupt_enter();
-
- rt_tick_increase();
-
- /* leave interrupt */
- rt_interrupt_leave();
- }
复制代码
当中断到来时,将调用一次rt_tick_increase();,而在中断函数中调用 rt_tick_increase() 对全局变量 rt_tick 进行自加,如下代码:
- /**
- * This function will notify kernel there is one tick passed. Normally,
- * this function is invoked by clock ISR.
- */
- void rt_tick_increase(void)
- {
- struct rt_thread *thread;
-
- /* increase the global tick */
- ++ rt_tick;
-
- /* check time slice */
- thread = rt_thread_self();
-
- -- thread->remaining_tick;
- if (thread->remaining_tick == 0)
- {
- /* change to initialized tick */
- thread->remaining_tick = thread->init_tick;
-
- /* yield */
- rt_thread_yield();
- }
-
- /* check timer */
- rt_timer_check();
- }
复制代码
可以看到全局变量 rt_tick 在每经过一个时钟节拍时,值就会加 1,rt_tick 的值表示了系统从启动开始总共经过的时钟节拍数,即系统时间。此外,每经过一个时钟节拍时,都会检查当前线程的时间片是否用完,以及是否有定时器超时。
注意:上面的中断中的 rt_timer_check() 用于检查系统硬件定时器链表,如果有定时器超时,将调用相应的超时函数。且所有定时器在定时超时后都会从定时器链表中被移除,而周期性定时器会在它再次启动时被加入定时器链表。
3、获取时钟节拍
在RT-Thread中,全局变量 rt_tick 在每经过一个时钟节拍时,值就会加 1,通过调用 rt_tick_get 会返回当前rt_tick 的值,即可以获取到当前的时钟节拍值。此接口可用于记录系统的运行时间长短,或者测量某任务运行的时间。
- /**
- * This function will return current tick from operating system startup
- *
- * @return current tick
- */
- rt_tick_t rt_tick_get(void)
- {
- /* return the global tick */
- return rt_tick;
- }
- RTM_EXPORT(rt_tick_get);
复制代码
rt_tick:当前时钟节拍值。
二、软件定时器
RT-Thread 的软件定时器提供两类定时器机制:第一类是单次触发定时器,这类定时器在启动后只会触发一次定时器事件,然后定时器自动停止。第二类是周期触发定时器,这类定时器会周期性的触发定时器事件,直到用户手动的停止,否则将永远持续执行下去。软件定时器的优先级为RT_TIMER_THREAD_PRIO。
1、开启软件定时宏
如果要开启使用软件定时器,需再rtconfig.h中打开软件定时器的宏:
- #define RT_USING_TIMER_SOFT //定义该宏可开启软件定时器,未定义则关闭
复制代码
2、软件定时器工作机制
(1)在 RT-Thread 定时器模块中维护着两个重要的全局变量:
(A)当前系统经过的 tick 时间 rt_tick(当硬件定时器中断来临时,它将加 1);
(B)定时器链表 rt_timer_list。系统新创建并激活的定时器都会按照以超时时间排序的方式插入到rt_timer_list 链表中。
(2)如下图所示,系统当前 tick 值为 20,在当前系统中已经创建并启动了三个定时器,分别是定时时间为50 个 tick 的Timer1、100 个 tick 的 Timer2 和 500 个 tick 的 Timer3,这三个定时器分别加上系统当前时间 rt_tick=20,从小到大排序链接rt_timer_list 链表中,形成如图所示的定时器链表结构。而 rt_tick 随着硬件定时器的触发一直在增长(每一次硬件定时器中断来临,rt_tick 变量会加 1),50个 tick 以后,rt_tick 从 20 增长到 70,与 Timer1 的 timeout 值相等,这时会触发与 Timer1 定时器相关
联的超时函数,同时将 Timer1 从 rt_timer_list 链表上删除。同理,100 个 tick 和 500 个 tick 过去后,与Timer2 和 Timer3 定时器相关联的超时函数会被触发,接着将 Time2 和 Timer3 定时器从 rt_timer_list链表中删除。
定时器链表示意图(来源RT-Thread编程指南) (3)如果系统当前定时器状态在 10 个 tick 以后(rt_tick=30)有一个任务新创建了一个 tick 值为 300 的Timer4 定时器,由于 Timer4 定时器的 timeout=rt_tick+300=330, 因此它将被插入到 Timer2 和 Timer3定时器中间,形成如下图所示链表结构。
定时器链表插入示意图(来源RT-Thread编程指南)
注意:所有定时器在定时超时后都会从定时器链表中被移除,而周期性定时器会在它再次启动时被加入定时器链表。
3、动态创建软件定时器函数
调用动态创建软件定时器函数接口后,内核首先从动态内存堆中分配一个定时器控制块,然后对该控制块进行基本的初始化,函数如下:
- rt_timer_t rt_timer_create(const char *name,
- void (*timeout)(void *parameter),
- void *parameter,
- rt_tick_t time,
- rt_uint8_t flag);
复制代码
(1)入口参数:
name:定时器的名称。name 定时器的名称
void (timeout) (voidparameter):定时器超时函数指针(当定时器超时时,系统会调用这个函数,即定时器超时回调函数)。parameter:定时器超时函数的入口参数(当定时器超时时,调用超时回调函数会把这个参数做为入口参数传递给超时函数)。
time:定时器的超时时间,单位是时钟节拍。
flag:定时器创建时的参数,支持的值包括单次定时、周期定时、硬件定时器、软件定时器等(可以用 “或” 关系取多个值)。
(2)返回值:
RT_NULL:创建失败(通常会由于系统内存不够用而返回 RT_NULL)
定时器的句柄:定时器创建成功时返回定时句柄。
(3)include/rtdef.h 中定义了一些定时器相关的宏,通过如下4个宏或起来后赋给flag,来指定定时器类型:
- #define RT_TIMER_FLAG_ONE_SHOT 0x0 /**< one shot timer */
- #define RT_TIMER_FLAG_PERIODIC 0x2 /**< periodic timer */
-
- #define RT_TIMER_FLAG_HARD_TIMER 0x0 /**< hard timer,the timer's callback function will be called in tick isr. */
- #define RT_TIMER_FLAG_SOFT_TIMER 0x4 /**< soft timer,the timer's callback function will be called in timer thread. */
复制代码
4、删除动态定时器函数
当不再需要动态定时器时,可以将其删除,执行如下函数之后系统会把这个定时器从 rt_timer_list 链表中删除,然后释放相应的定时器控制块占有的内存:
- rt_err_t rt_timer_delete(rt_timer_t timer);
复制代码
(1)入口参数:
timer:定时器句柄,指向要删除的定时器。
(2)返回值:
RT_EOK:删除成功(如果参数 timer 句柄是一个 RT_NULL,将会导致一个 ASSERT 断言)
5、静态创建软件定时器
静态创建软件定时器也就是《RT-Thread编程指南》里面所讲的定时器初始化,如下面函数,使用该函数接口时会初始化相应的定时器控制块,初始化相应的定时器名称,定时器超时函数:
- void rt_timer_init(rt_timer_t timer,
- const char *name,
- void (*timeout)(void *parameter),
- void *parameter,
- rt_tick_t time,
- rt_uint8_t flag);
复制代码
(1)入口参数:
timer:定时器句柄,指向要初始化的定时器控制块。
name:定时器的名称
void (timeout) (voidparameter):定时器超时函数指针(当定时器超时时,系统会调用这个函数,即定时器超时回调函数)。
parameter:定时器超时函数的入口参数(当定时器超时时,调用超时回调函数会把这个参数做为入口参数传递给超时函数)。
time:定时器的超时时间,单位是时钟节拍
flag:定时器创建时的参数,支持的值包括单次定时、周期定时、硬件定时器、软件定时器(可以用 “或” 关系取多个值)。
6、删除静态定时器函数
当不再需要静态定时器时,可将它删除,也就是《RT-Thread编程》指南里面所说的脱离定时器。脱离定时器时,系统会把定时器对象从内核对象容器中脱离,但是定时器对象所占有的内存不会被释放:
- rt_err_t rt_timer_detach(rt_timer_t timer);
复制代码
(1)入口参数:
timer:定时器句柄,指向要脱离的定时器控制块。
(2)返回值:
RT_EOK:脱离成功。
7、启动软件定时器
前面讲了定时器的创建,但创建好的定时器并不会立即开始工作,需要在调用启动定时器函数接口后才开始工作,调用定时器启动函数接口后,定时器的状态将更改为激活状态(RT_TIMER_FLAG_ACTIVATED),并按照超时顺序插入到 rt_timer_list 队列链表中,启动函数如下:
- rt_err_t rt_timer_start(rt_timer_t timer);
复制代码
(1)入口参数:
timer:定时器句柄,指向要启动的定时器控制块。
(2)返回值:
RT_EOK:启动成功。
8、停止软件定时器
启动定时器以后,若想使它停止,可调用停止函数,调用定时器停止函数接口后,定时器状态将更改为停止状态(RT_TIMER_FLAG_DEACTIVATED),并从 rt_timer_list 链表中脱离出来不参与定时器超时检查,函数如下:
- rt_err_t rt_timer_stop(rt_timer_t timer);
复制代码
(1)入口参数:
timer:定时器句柄,指向要停止的定时器控制块。
(2)返回值:
RT_EOK:成功停止定时器。
RT_ERROR:timer 已经处于停止状态。。
9、控制定时器
控制定时器函数接口可根据命令类型参数,来查看或改变定时器的设置,函数如下:
- rt_err_t rt_timer_control(rt_timer_t timer, int cmd, void *arg);
复制代码
(1)入口参数:
timer:定时器句柄,指向要停止的定时器控制块。
cmd:用于控制定时器的命令,当前支持四个命令,分别是设置定时时间,查看定时时间,设置单次触发,设置周期触发。
arg:与 cmd 相对应的控制命令参数比如,cmd 为设定超时时间时,就可以将超时时间参数通过arg 进行设定。
(2)函数参数 cmd 支持的命令:
- #define RT_TIMER_CTRL_SET_TIME 0x0 /* 设 置定 时 器 超 时 时 间 */
- #define RT_TIMER_CTRL_GET_TIME 0x1 /* 获 得定 时 器 超 时 时 间 */
- #define RT_TIMER_CTRL_SET_ONESHOT 0x2 /* 设 置定 时 器 为 单 次 定 时 器 */
- #define RT_TIMER_CTRL_SET_PERIODIC 0x3 /* 设 置定 时 器 为 周 期 型 定 时 器 */
复制代码
(3)返回值:
RT_EOK:成功
三、基于STM32的软件定时器应用示例
前面讲了很多RT-Thread定时器方面的东西,实际上,我如果用RT-Thread的定时相关接口的话,一般都是用软件定时器,硬件定时器通常我们都是自己像以前学习裸机那边去用。接下来我们来实际使用一些RT-Thread的软件定时器,使用RTT&正点原子联合出品的潘多拉开发板来实验:(1)动态创建一个软件定时器,周期执行,实现定时器超时时打印出当前获取滴答定时器的计数值以及回调函数执行次数。(2)动态创建一个线程,通过按下KEY0来启动软件定时器,按下KEY1来停止软件定时器。
1、实现代码
- #include "main.h"
- #include "board.h"
- #include "rtthread.h"
- #include "data_typedef.h"
- #include "delay.h"
- #include "led.h"
- #include "key.h"
-
- void rt_sw_timer1(void);
- void key_start(void);
-
- static rt_timer_t timer1; /* timer1句柄 */
- int g_sw_timer1_count = 0;
-
- int main(void)
- {
- rt_sw_timer1();
- key_start();
-
- return 0;
- }
-
- /**************************************************************
- 函数名称 : rt_sw_timer1_callback
- 函数功能 : 软件定时timer1回调函数
- 输入参数 : parameter:回调函数的入口参数,当定时器超时,
- 调用回调函数会把这个参数做为入口参数传递给回调函数。
- 返回值 : 无
- 备注 : 无
- **************************************************************/
- void rt_sw_timer1_callback(void *parameter)
- {
- u32 tick_num1;
-
- tick_num1 = (u32)rt_tick_get(); /* 获取滴答定时器的计数值 */
- g_sw_timer1_count++;
-
- rt_kprintf("tick_num1 = %d\r\n", tick_num1);
- rt_kprintf("enter rt_sw_timer_callback, g_sw_timer1_count = %d\r\n", g_sw_timer1_count);
- }
-
- /**************************************************************
- 函数名称 : rt_sw_timer1
- 函数功能 : 软件定时timer1动态创建函数
- 输入参数 : 无
- 返回值 : 无
- 备注 : 无
- **************************************************************/
- void rt_sw_timer1(void)
- {
- /* 动态创建软件定时器,周期执行 */
- timer1 = rt_timer_create("timer1",
- rt_sw_timer1_callback,
- RT_NULL,
- 5000, /* 周期为5000个时钟节拍 */
- RT_TIMER_FLAG_SOFT_TIMER | RT_TIMER_FLAG_PERIODIC);/* 软件定时器,周期执行 */
- }
-
- /**************************************************************
- 函数名称 : key_thread_entry
- 函数功能 : key线程入口函数
- 输入参数 : parameter:线程入口函数参数
- 返回值 : 无
- 备注 : 无
- **************************************************************/
- void key_thread_entry(void *parameter)
- {
- u8 key;
-
- while(1)
- {
- key = key_scan(0);
- if(key == KEY0_PRES)
- {
- rt_timer_start(timer1);
- rt_kprintf("RT-Thread sw timer1 satrt\r\n");
- }
- else if(key == KEY1_PRES)
- {
- rt_timer_stop(timer1);
- rt_kprintf("RT-Thread sw timer1 stop\r\n");
- }
- rt_thread_mdelay(1);
- }
-
- }
-
- /**************************************************************
- 函数名称 : key_start
- 函数功能 : 创建并启动key线程
- 输入参数 : 无
- 返回值 : 无
- 备注 : 无
- **************************************************************/
- void key_start(void)
- {
- rt_thread_t key_thread = RT_NULL;;
-
- /* 动态创建KEY线程 */
- key_thread = rt_thread_create("key",
- key_thread_entry,
- RT_NULL,
- 512, /* 线程栈大小,单位是字节 */
- RT_THREAD_PRIORITY_MAX / 2 - 5, /* 优先级 */
- 50);/* 50个时钟节拍 */
- /* 创建KEY线程成功,则启动线程 */
- if(key_thread != RT_NULL)
- {
- rt_thread_startup(key_thread);
- }
- }
复制代码
2、观察FinSH
(1)开机之后,等待超过软件定时配置的周期过后,还是没有打印出回调函数要打印的信息,说明回调函数没有被执行,为什么呢?因为我们创建软件定时器之后还没启动软件定时器,我们输入list_timer回车,可以看到timer1是处于deactivated状态。
(2)按下KEY0按键,启动软件定时器,可观察到每个5s打印一次当前tick的数值和回调函数的执行次数,将前后两次tick的数值相减大概为5006ms,再次输入list_timer,可看到timer1为activated状态。
(3)按下KEY1按键,停止软件定时器,停止打印回调函数要打印的信息,输入list_timer,可以看到timer1为deactivated状态。
四、软件定时器设计注意事项
在设计日软件定时器时,超时回调函数的要求严格:执行时间应该尽量短,执行时不应导致当前上下文挂起、等待。例如在中断上下文中执行的超时函数它不应该试图去申请动态内存、释放动态内存等,也不允许调用rt_thread_delay()等导致上下文挂起的 API 接口。
参考文献:
1、[野火®]《RT-Thread 内核实现与应用开发实战—基于STM32》
2、《RT-THREAD 编程指南》
|