RT-Thread第4课,听听 RT-Thread 的心跳,再学习一下基于心跳的软件定时器使用。
前言
学习RTOS,肯定接触到软件定时器,学会软件定时器的使用能够使得我们摆脱硬件定时器在某些地方的局限性,而软件定时器的实现,又是基于系统的时钟节拍,本文除了了解 RT-Thread 软件定时器API,学会使用 RT-Thread 软件定时器,还需要先了解下 RT-Thread 时钟管理相关知识。
本 RT-Thread 专栏记录的开发环境:
RT-Thread记录(一、RT-Thread 版本、RT-Thread Studio开发环境 及 配合CubeMX开发快速上手)
RT-Thread记录(二、RT-Thread内核启动流程 — 启动文件和源码分析
RT-Thread 内核篇系列博文链接:
RT-Thread记录(三、RT-Thread 线程操作函数及线程管理与FreeRTOS的比较)
一、RT-Thread 时钟节拍
1.1 时钟节拍的概念
时钟节拍 (OS Tick)是系统心跳!任何操作系统都需要提供一个时钟节拍,以供系统处理所有和时间有关的事件。
操作系统中最小的时间单位是时钟节拍,时钟节拍是特定的周期性中断,内核在时钟节拍到的时候进行上下文切换。
RT-Thread 中,时钟节拍的长度可以根据 RT_TICK_PER_SECOND
的定义来调整,等于1/RT_TICK_PER_SECOND
秒,在我们测试的STM32F上,默认的时钟节拍为1ms,如下:
在上一节创建线程的时候最后一个参数是时间节拍数,比如设置为50,那么线程的时间片就是50ms。
另外,rtconfig.h
中有 RT-Thread 内核配置,线程通讯配置,组件配置,shell配置,设备驱动配置等等的宏定义配置。
RT_TICK_PER_SECOND
是可以修改的,比如我们修改成100。时钟节拍就是10ms。
1.2 时钟节拍实现原理
那么时间节拍是如何实现的?
前面说过:时钟节拍是特定的周期性中断,这个中断一般由MCU硬件定时器决定,就是系统的时钟节拍的产生还是基于MCU的硬件定时器!
对于我们使用的 Corex-M
芯片来说,就是由滴答定时器Systick 来实现系统时钟节拍的。
既然与滴答定时器Systick有关系,那么我们可以通过工程代码来看一看,如图:
在滴答定时器的中断处理函数中,我们可以看到如下操作:
上图代码中的 全局变量,rt_tick
的值表示了系统从启动开始总共经过的时钟节拍数,即系统时间。rt_tick 在每经过一个时钟节拍时,值就会加 1。
到这里又有一个问题了,STM32中滴答定时器Systick不是固定的吗?
所以我们这里就要说说 RT_TICK_PER_SECOND
改变的是什么,这个宏定义是如何影响系统节拍的。
我们找到drv_common.c
文件中的rt_hw_systick_init
函数,如下图:
上图就是 RT-Thread 初始化配置启动 MCU 滴答定时器的函数,里面的配置用到了我们的宏定义RT_TICK_PER_SECOND
,所以宏定义的改变可以直接改变 Systick 的频率,直接使得系统的时钟节拍不同。
1.3 时钟节拍示例
在上文我们说到,全局变量 rt_tick
表示了系统从启动开始总共经过的时钟节拍数, RT-Thread 给我们提供了一个函数rt_tick_get
来查看当前的时钟节拍值:
/*
返回值:rt_tick 当前时钟节拍值
*/
rt_tick_t rt_tick_get(void);
为了巩固一下上面的内容,我们来简单的做个测试,因为测试比较简单,我就直接上图:
当RT_TICK_PER_SECOND
为1000的时候,就表示我们设置系统节拍为 1ms,那么 tick 的值就是 1ms 加一次,所以延时 1000ms 以后,是增加1000。
当RT_TICK_PER_SECOND
为100 的时候,就表示我们设置系统节拍为 10ms,那么 tick 的值就是 10ms 加一次,所以延时 1000ms 以后,是增加100。
二、RT-Thread 软件定时器
2.1 软件定时器基本介绍
RT-Thread 操作系统提供软件实现的定时器,以时钟节拍(OS Tick)的时间长度为单位,即定时数值必须是 OS Tick 的整数倍。
定时器分为 单次触发定时器,周期触发定时器;
定时器不管使用和理解都是比较简单的,对于软件定时的的一些注意事项,我在介绍FreeRTOS软件定时器的时候写过,可以比较参考一下,如下:
与 FreeRTOS 不同的是,根据超时函数执行时所处的上下文环境,RT-Thread 的定时器可以分为 HARD_TIMER 模式与 SOFT_TIMER 模式:
- HARD_TIMER 模式
HARD_TIMER 模式的定时器超时函数在中断上下文环境中执行,RT-Thread 定时器默认的方式是 HARD_TIMER 模式,即定时器超时后,超时函数是在系统时钟中断的上下文环境中运行的。
简单来说就是要把 HARD_TIMER 模式的回调函数当成 中断函数处理,快进快出。
- SOFT_TIMER 模式
SOFT_TIMER 模式可配置,通过宏定义 RT_USING_TIMER_SOFT
来决定是否启用该模式。该模式被启用后,系统会在初始化时创建一个 timer 线程,然后 SOFT_TIMER 模式的定时器超时函数在都会在 timer 线程的上下文环境中执行。可以在初始化 / 创建定时器时使用参数RT_TIMER_FLAG_SOFT_TIMER
来指定设置 SOFT_TIMER 模式。
个人的习惯是,应用中还是定义 RT_USING_TIMER_SOFT
,然后使用 SOFT_TIMER 模式,个人感觉这样才更“像”软件定时器。
最后要给个建议,实际应用,不管是 HARD_TIMER 模式,还是 SOFT_TIMER 模式,在超时函数中都要做到快进快出,不要有延时挂起等操作。
2.2 系统定时器初始化
在 RT-Thread 使用中,往往都会定义RT_USING_TIMER_SOFT
,使用软件定时器并且启动 SOFT_TIMER 模式 ,该模式被启用后,系统会在初始化时创建一个 timer
线程,用来对软件定时器经常管理,那么我们就通过源码来看看 RT-Thread 到底是如何操作的。
通过 《RT-Thread记录(二、RT-Thread内核启动流程 — 启动文件和源码分析)》学习,我们可以找到rtthread_startup
函数:
先来看看第一个rt_system_timer_init
:
接下来看看第二个函数rt_system_timer_thread_init
:
我们继续进入 timer
线程的入口函数,来看看 timer
线程具体做了什么事情,这里我们就通过放源码,看注释来分析一下:
/* system timer thread entry */
static void rt_thread_timer_entry(void *parameter)
{
rt_tick_t next_timeout;
while (1)
{
/*
get the next timeout tick
获取下一次超时时间
得到软件定时器链表上的下一个定时器的超时时间点
*/
next_timeout = rt_timer_list_next_timeout(rt_soft_timer_list);
/*
如果超过范围,表示没有软件定时器,
则挂起当前线程,继续线程调度
*/
if (next_timeout == RT_TICK_MAX)
{
/* no software timer exist, suspend self. */
rt_thread_suspend(rt_thread_self());
rt_schedule();
}
else
{
rt_tick_t current_tick;
/*
get current tick
获取当前时间点
*/
current_tick = rt_tick_get();
/*
离下个中断时间点还差些时候
*/
if ((next_timeout - current_tick) < RT_TICK_MAX / 2)
{
/* get the delta timeout tick */
next_timeout = next_timeout - current_tick;//计算还差多长时间
rt_thread_delay(next_timeout);//休眠差的这段时间
}
}
/*
check software timer
检查是否该产生超时事件
*/
rt_soft_timer_check();
}
}
#endif
如果要继续往下面分析,就得继续分析rt_soft_timer_check();
的实现源码了,这里我们就不继续分析下去,因为到目前为止,我们对于 RT-Thread 系统定时器的初始化过程已经有了一个全面的认识,对于我们理解定时器有了很大的帮助,但是喜欢研究的小伙伴可以继续往下面分析,分析源码是理解一个系统最直接最有效的方式!
2.3 定时器工作机制
和线程控制块一样,内核对于定时器的管理是通过这个定时器控制块结构体,里面包括 RT-Thread 软件定时器的所有的“属性”,对这些属性的查看,修改就可以对实现对这个软件定时器的管理控制。
/**
* timer structure
*/
struct rt_timer
{
/**< inherit from rt_object */
struct rt_object parent;
/* 定时器链表节点 */
rt_list_t row[RT_TIMER_SKIP_LIST_LEVEL];
/**< timeout function 定时器超时调用的函数 */
void (*timeout_func)(void *parameter);
/**< timeout function's parameter 超时函数的参数*/
void *parameter;
/** < timer timeout tick 定时器初始超时节拍数 */
rt_tick_t init_tick;
/**< timeout tick 定时器实际超时时的节拍数*/
rt_tick_t timeout_tick;
};
typedef struct rt_timer *rt_timer_t;
定时器控制块由 struct rt_timer 结构体定义并形成定时器内核对象,再链接到内核对象容器中进行管理,list 成员则用于把一个激活的(已经启动的)定时器链接到 rt_timer_list 链表中。
对于定时器的工作机制,在 RT-Thread 的介绍已经很详细了,我这里用官方一张图表示一下:
对于定时器的工作机制,实际上对我们使用定时器并没有直接的帮助(因为定时器的使用真的很简单),但是他能够让我们更深的理解定时器。往往这些更深的理解在我们遇到问题的时候,对解决问题起着至关重要的作用。
2.4 us 延时函数
使用过STM32 HAL 库的小伙伴都知道,HAL库是没有us延时的,在 FreeRTOS 中,也是没有us延时函数的。但是我们在进行一些总线操作的时候,比如软件 I2C 通讯,不得不用到 us 延时函数。
现在好了,在使用 RT-Thread 的时候,系统直接给了我们一个 us延时函数,如下:
/**
* This function will delay for some us.
*
* @param us the delay time of us
*/
void rt_hw_us_delay(rt_uint32_t us)
{
rt_uint32_t start, now, delta, reload, us_tick;
start = SysTick->VAL;
reload = SysTick->LOAD;
/* 获得延时经过的 tick 数 */
us_tick = SystemCoreClock / 1000000UL;
do {
now = SysTick->VAL; // 获得当前时间
delta = start > now ? start - now : reload + start - now;
} while(delta < us_tick * us);
}
注意,这个函数只能支持低于 1 OS Tick 的延时。比如我们默认的设置,1OS Tick 为 1ms,那么这个函数的参数 us 必须小于 1000!
2.5 软件定时器 or 硬件定时器?
那么在实际应用中,是使用软件定时器 还是硬件定时器?我在另外一篇博文有过说明,如下图:
用我们常用的 STM32系列芯片打个比方,也要看什么系列的芯片,首先STM32的定时器其实已经足够多,不管是M0全系列,还是M3M4全系列,至少也有4个(有错误请指出),他的硬件定时器足够多的情况下,你可以不用软件定时器,对于一些系列信号,RAM比较小的情况,有的小于10KB,那么你在跑 RTOS 的时候都得特别注意内存大小,这个时候如果对内存管理,代码优化不是很了解的情况下,我是不建议用的。
但是一些特殊的应用场合,软件定时器还是比硬件定时器优势明显,因为可以随意更改延时时间。同时使用软件定时器的代码移植起来更快。
三、 RT-Thread 软件定时器操作函数
上文的基础知识,说来说去也有一大堆了,到了说说怎么使用的时候了。
(快点!快点!理论都快睡着了! 用起来!展示起来!)
3.1 动态创建和删除定时器
动态创建定时器(函数介绍看注释,以下函数介绍类似):
/*
参数的含义:
1、name 定时器的名称
2、void (timeout) (void parameter) 定时器超时函数指针(当定时器超时时,系统会调用这个函数)
3、parameter 定时器超时函数的入口参数(当定时器超时时,调用超时回调函数会把这个参数做为入口参数传递给超时函数)
4、time 定时器的超时时间,单位是时钟节拍
5、flag 定时器创建时的参数,支持的值包括单次定时、周期定时、硬件定时器、软件定时器等(可以用 “或” 关系取多个值)
返回值:
RT_NULL 创建失败(通常会由于系统内存不够用而返回 RT_NULL)
定时器的句柄 定时器创建成功
*/
rt_timer_t rt_timer_create(const char *name,
void (*timeout)(void *parameter),
void *parameter,
rt_tick_t time,
rt_uint8_t flag)
上面的函数,其中第5个参数 flag
,有2组值可以填写(对于静态创建定时器也是如此),如下图:
上面的2组宏定义可以以 “或” 逻辑的方式赋给 flag
,比如:
动态删除定时器:
/*
参数:
timer 定时器句柄,指向要删除的定时器控制块
返回值:
RT_EOK 删除成功
*/
rt_err_t rt_timer_delete(rt_timer_t timer);
3.2 静态创建和删除定时器
和线程一样,官方的用语是 初始化 和 脱离 定时器(理由在我上一篇博文有分析)。
静态创建定时器:
/*
参数的含义:
1、timer 定时器句柄,指向要初始化的定时器控制块,用户创建的定时器控制块结构体
2、name 定时器的名称
3、void (timeout) (void parameter) 定时器超时函数指针(当定时器超时时,系统会调用这个函数)
4、parameter 定时器超时函数的入口参数(当定时器超时时,调用超时回调函数会把这个参数做为入口参数传递给超时函数)
5、time 定时器的超时时间,单位是时钟节拍
6、flag 定时器创建时的参数,支持的值包括单次定时、周期定时、硬件定时器、软件定时器等(可以用 “或” 关系取多个值)
返回值:
无返回值
*/
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)
静态删除定时器:
/**
参数:
timer 定时器句柄,指向要删除的定时器控制块
返回值:
RT_EOK 删除成功
*/
rt_err_t rt_timer_detach(rt_timer_t timer)
3.3 启动和停止定时器
启动定时器:
当定时器被创建或者初始化以后,并不会被立即启动,必须在调用启动定时器函数接口后,才开始工作,启动定时器函数接口如下:
/**
参数:
timer 定时器句柄,指向要启动的定时器控制块
返回值:
RT_EOK 启动成功
*/
rt_err_t rt_timer_start(rt_timer_t timer)
停止定时器:
/**
参数:
timer 定时器句柄,指向要启动的定时器控制块
返回值:
RT_EOK 成功停止定时器
- RT_ERROR timer 已经处于停止状态
*/
rt_err_t rt_timer_stop(rt_timer_t timer)
调用定时器停止函数接口后,定时器状态将更改为停止状态,并从 rt_timer_list 链表中脱离出来不参与定时器超时检查。
当一个周期性定时器超时时,也可以调用这个函数接口停止这个定时器本身。
3.4 定时器控制函数
RT-Thread 提供了定时器控制函数接口,以获取或设置更多定时器的信息。
/**
参数的含义:
1、timer 定时器句柄,指向要控制的定时器控制块
2、cmd
用于控制定时器的命令,当前支持5个命令,
分别是设置定时时间,查看定时时间,设置单次触发,设置周期触发,查看状态
3、arg
与 cmd 相对应的控制命令参数
比如,cmd 为设定超时时间时,就可以将超时时间参数通过 arg 进行设定
返回值:
RT_EOK 成功
*/
rt_err_t rt_timer_control(rt_timer_t timer, int cmd, void *arg)
在官网介绍,上面函数的第二个参数只有 4 个命令,但是实际上我通过自己的工程查看得知现在的版本已经有5个命令了,如下:
四、定时器使用示例
定时器的使用还是比较简单的,我们这里还是直接通过截图说明的方式讲解下示例。
4.1 动态创建定时器示例
4.2 静态创建定时器示例
结语
本文的内容其实还是比较简单的,在使用示例,我们只展示了定时器的创建方法和使用效果,实际引用中已经能够满足大部分场合的需求。小伙伴可以自己新建线程尝试通过定时器的控制函数控制某个定时器,以便更加熟悉 RT-Thread 软件定时器的使用。
还是希望懂的朋友看完能够多多指教!不懂的朋友看完能懂!(看完还是不懂说明博主没有讲到位,这是问题,博主得改!= =!)
谢谢!