Toggle navigation
首页
问答
文章
积分商城
专家
专区
更多专区...
文档中心
返回主站
搜索
提问
会员
中心
登录
注册
RT-Thread一般讨论
系统优化
软定时器
rt-thread 系统优化系列(三) 之 软定时器的定时漂移误差分析
发布于 2021-08-13 18:49:42 浏览:3228
订阅该版
[tocm] # 软定时器 所谓软定时器,是由一个线程运行维护的定时器列表。由线程调用定时器回调函数。 相对硬定时器,是由中断(SysTick)维护的定时器列表,并在中断中调用定时器回调函数。 > 另外,还有一种*硬件定时器*,这个和单片机里的定时器是一个概念,由外设定时器实现定时。和 rt-thread 提供的硬定时器是两个不同概念。 对硬定时器回调函数有严格的执行时间要求,而且不能调用任何在中断中不能调用的函数。总之不能有任何不能在中断中执行的操作。 那么,软定时器呢,要求需要这么严格吗? 比如有个处理执行时间 10ms,比如在定时器中断函数里发送个等待 10ms 的消息... 无论是硬定时器还是软定时器,它们各自有一个定时器列表。列表中的定时器根据定时时间长短排序,定时时间短的在前。扫描这个列表中所有定时器,直到结尾或者出现第一个定时时间未到的定时器节点。当判断出定时时间到的时候,调用定时器回调函数。 假如,某次扫描这个列表中多于一个定时器到达定时时间,也就是需要执行两个以上的定时器回调函数。并且前一个扫描到的定时器的回调函数执行时间比较长,出现上面设想的某一种使用情况。这时候会出现什么效果? > 因为硬定时器的种种硬性要求,以下讨论只针对软定时器。 ### 从对 `rt_soft_timer_check` 的几个疑问讲起 先摆出官方的 rt_soft_timer_check 函数,实现。这个函数是来扫描定时器列表中到达定时时间的定时器,并调用定时器回调函数的。 ``` void rt_soft_timer_check(void) { rt_tick_t current_tick; struct rt_timer *t; register rt_base_t level; rt_list_t list; rt_list_init(&list); RT_DEBUG_LOG(RT_DEBUG_TIMER, ("software timer check enter\n")); /* disable interrupt */ level = rt_hw_interrupt_disable(); while (!rt_list_isempty(&rt_soft_timer_list[RT_TIMER_SKIP_LIST_LEVEL - 1])) { t = rt_list_entry(rt_soft_timer_list[RT_TIMER_SKIP_LIST_LEVEL - 1].next, struct rt_timer, row[RT_TIMER_SKIP_LIST_LEVEL - 1]); current_tick = rt_tick_get(); /* * It supposes that the new tick shall less than the half duration of * tick max. */ if ((current_tick - t->timeout_tick) < RT_TICK_MAX / 2) { RT_OBJECT_HOOK_CALL(rt_timer_enter_hook, (t)); /* remove timer from timer list firstly */ _rt_timer_remove(t); if (!(t->parent.flag & RT_TIMER_FLAG_PERIODIC)) { t->parent.flag &= ~RT_TIMER_FLAG_ACTIVATED; } /* add timer to temporary list */ rt_list_insert_after(&list, &(t->row[RT_TIMER_SKIP_LIST_LEVEL - 1])); soft_timer_status = RT_SOFT_TIMER_BUSY; /* enable interrupt */ rt_hw_interrupt_enable(level); /* call timeout function */ t->timeout_func(t->parameter); RT_OBJECT_HOOK_CALL(rt_timer_exit_hook, (t)); RT_DEBUG_LOG(RT_DEBUG_TIMER, ("current tick: %d\n", current_tick)); /* disable interrupt */ level = rt_hw_interrupt_disable(); soft_timer_status = RT_SOFT_TIMER_IDLE; /* Check whether the timer object is detached or started again */ if (rt_list_isempty(&list)) { continue; } rt_list_remove(&(t->row[RT_TIMER_SKIP_LIST_LEVEL - 1])); if ((t->parent.flag & RT_TIMER_FLAG_PERIODIC) && (t->parent.flag & RT_TIMER_FLAG_ACTIVATED)) { /* start it */ t->parent.flag &= ~RT_TIMER_FLAG_ACTIVATED; rt_timer_start(t); } } else break; /* not check anymore */ } /* enable interrupt */ rt_hw_interrupt_enable(level); RT_DEBUG_LOG(RT_DEBUG_TIMER, ("software timer check leave\n")); } ``` ### 疑问一 `rt_list_t list` 这个临时中间变量的作用是什么? - 进入 `rt_soft_timer_check` 函数后,先初始化 list 变量。 - 扫描定时器列表,当有到时的定时器,把这个定时器从软定时器列表 `rt_soft_timer_list` 移除,插入到这个 list 临时存放。 - 开中断,调用定时器回调函数,关中断 - 把 list 中存放的定时器移除 - 判断定时器的周期定时器还是一次性定时器 - 继续扫描定时器列表 list 是个局部变量,仅仅起临时存放当前这个定时器的作用。看似是一种稳妥的做法。 但是,这样做的初衷是什么? 为什么这个定时器一定要放到某个列表里? 如果从软定时器列表 `rt_soft_timer_list` 移除后,不插入任何列表会有什么影响? 因为退出 `rt_soft_timer_check` 函数后,list 列表不复存在了,应该不是退出 `rt_soft_timer_check` 函数后的需求,那么插入 list 和 从 list 取出之间有哪些情况需要我们注意,需要用一个临时列表将软定时器暂存? #### 定时器回调函数里可能发生哪些操作? - 修改定时器设置 - 停止,重启定时器 - 删除定时器 - 发生中断,在中断里执行上述三种操作 修改定时器设置,可能只涉及到定时器的定时时间间隔和定时周期特性。这两个参数设置需要定时器必须在某个列表中吗? 停止,重启定时器,必然导致修改定时器所在列表指针,这里就涉及到双向列表的操作了。 > 简短介绍一下双向列表, - rt-thread 使用的双向列表, - 每一个节点有一个 prev 和 一个 next 指针,分别指向双向列表中的前一个节点和后一个节点。 - 一个空列表 l 仅有一个不含数据的节点,此节点的 prev next 指针均指向它自己。 - 任何一个带数据的列表节点必须进行初始化,使得它的 prev next 分别指向它自己,这一点和空列表 l 完全雷同!换句话说,***任何一个双向列表节点均有作为链表的潜质***。从操作上讲,你可以定义两个定时器,然后这两个定时器之间构建一个含有两个节点的双向列表,当然,这种做法没有多少实用意义。 - 从链表中移除的节点,**必须**使得它的 prev next 指针指向它自己。 - **无论一个节点是否在某个双向链表中,或者仅仅是一个独立节点,对它进行删除操作,效果是完全一样的!** - 更多的操作详见 rtservice.c 文件中相关函数,`rt_list_init rt_list_insert_after rt_list_insert_before rt_list_remove` 等等。 停止定时器会把当前定时器从定时器列表删除,无论这个定时器有没有在某个定时器列表中,或者只是一个独立的定时器节点,删除操作的结果都是一样的,使用 list 这个临时列表可能不能保护它不被删除。 重启定时器会把它先从前一个列表中删除,然后插入软定时器列表 `rt_soft_timer_list` 。list 这个临时列表也阻止不了重启定时器操作。 **至此,可以看出,`rt_list_t list` 这个临时列表无任何存在意义** ### 疑问二 `soft_timer_status` 指示的是什么状态? 这是一个全局静态变量,它的使用也很简单,只在四个地方使用了,上面的源码函数里有两处,其它两个地方分别是初始化声明 ``` /* soft timer status */ static rt_uint8_t soft_timer_status = RT_SOFT_TIMER_IDLE; ``` 和——以下摘自 `rt_timer_start` 函数 ``` #ifdef RT_USING_TIMER_SOFT if (timer->parent.flag & RT_TIMER_FLAG_SOFT_TIMER) { /* check whether timer thread is ready */ if ((soft_timer_status == RT_SOFT_TIMER_IDLE) && ((timer_thread.stat & RT_THREAD_STAT_MASK) == RT_THREAD_SUSPEND)) { /* resume timer thread to check soft timer */ rt_thread_resume(&timer_thread); rt_schedule(); } } #endif /* RT_USING_TIMER_SOFT */ ``` 将这四个地方联系起来看,意思好像是调用定时器回调函数前修改软定时器为 busy 状态,返回回调函数后恢复为 idle 状态,而如果是在定时器回调函数里调用 `rt_timer_start` ,可以达到不进行任务调度的目的。好像是起了双保险作用,真是这样吗? 我们分析一下上面这段 `rt_timer_start` 函数片段。 首先判断定时器是不是软定时器,只有软定时器启动时才有进行任务调度的可能。 其次,判断 `soft_timer_status` 是否空闲,以及软定时器线程是否***挂起态***。 以上仨条件均满足,进行任务调度。 我们重点关注“其次”,一个定时器线程调用的定时器回调函数,这个线程会是挂起态吗?答案是肯定不是。它在运行着,肯定是 `RT_THREAD_RUNNING` 的。那么这个 `soft_timer_status` “双保险”了吗? ### 疑问三 开篇提到的假想 开篇提到了一种假想,假想软定时器回调函数占用 cpu 时间有点儿长,会产生什么影响,引起什么后果。 讨论这个问题仍然离不开 `rt_soft_timer_check` 函数工作原理,我们再梳理一下 `rt_soft_timer_check` 函数的操作。(以下分析忽略 list 以及 soft_timer_status 相关操作) - 关中断 - 扫描列表是否有节点 - 取出第一个节点 - ***获取当前系统 tick*** - 检查定时器是否定时时间到,如果到时 - 先从软定时器列表 `rt_soft_timer_list` 移除定时器。非周期定时器,取消激活态 - 开中断 - 执行定时器回调函数。(这里可能存在长时间操作) - 关中断 - 对于周期性定时器,重启定时器;非周期定时器,前面已经做了取消激活态操作。 - 继续扫描列表,取出第一个节点,***获取当前系统 tick*** ,检查定时器是否定时时间到。。。 假设 RT_TICK_PER_SECOND = 1000,有两个周期性定时器 t0 t1 ,定时间隔不同,同时启动,各自的定时器回调函数执行时间 t0 500us,t1 5 ms。 经过一段时间以后,总是可能会出现定时间隔公倍数时刻 Tn ,它们俩同时达到定时时间。 如果 t1 先被处理,那么 t1 重启的时候系统 tick 已经是 Tn + 5;t0 的重启时间有 50% 的可能是 Tn + 5, 50%的可能是 Tn + 6。 如果 t0 先被处理,t0 的重启时间有 50% 的可能是 Tn, 50%的可能是 Tn + 1;t1 重启时间是 Tn + 5 或 Tn + 6。 即便不考虑 t0 不考虑它对外的影响,也不考虑它受到的影响,仅仅分析 t1 自己对自己的影响,也可以看出来,随着时间的推移,它的定时间隔不是初始设定的 Inv ,而是 Inv + 5。 优化后的 `rt_soft_timer_check` 流程 - ***获取当前系统 tick*** - 关中断 - 扫描列表是否有节点 - 取出第一个节点 - 检查定时器是否定时时间到,如果到时 - 先从软定时器列表 `rt_soft_timer_list` 移除定时器。 - 对于周期性定时器,***重启定时器***;非周期定时器,取消激活态。 - 开中断 - 执行定时器回调函数。(这里可能存在长时间操作) - 关中断 - 继续扫描列表,取出第一个节点,检查定时器是否定时时间到。。。 其中,优化后的重启定时器不能使用原来的接口。需要使用如下原型函数接口 ``` static rt_err_t _rt_timer_start(rt_timer_t timer, rt_tick_t current_tick) ``` 第二个参数是进入 `rt_soft_timer_check` 函数,第一次关中断前获取的当前系统 `tick` 值,无论下面扫描出多少个到达时间的定时器,启动时间都是同一个 `tick` 值。 而且无论其中某个定时器回调函数执行时间有多长,或者多个回调函数累积执行时间有多长,它们的启动时间都是相同的! > 注:由此,引起的另一个弊端的,期间某个定时器定时时间到了,但是会被判定为未到,下次调用 `rt_soft_timer_check` 时才会被处理。 ### 测试样例程序 ``` static void led1_timeout(void *parameter) { rt_tick_t current_tick; int pin = rt_pin_read(LED1_PIN); rt_pin_write(LED1_PIN, !pin); current_tick = rt_tick_get(); rt_hw_us_delay(50000); } void led_tick_thread(void *parameter) { rt_timer_t led1_timer; led1_timer = rt_timer_create("ledtim1", led1_timeout, RT_NULL, 1000, RT_TIMER_FLAG_PERIODIC | RT_TIMER_FLAG_SOFT_TIMER); if (led1_timer != RT_NULL) { rt_timer_start(led1_timer); } while (1) { rt_pin_write(LED0_PIN, PIN_HIGH); rt_thread_mdelay(500); rt_pin_write(LED0_PIN, PIN_LOW); rt_thread_mdelay(500); } } ``` 作为对比,两个 led 一个用软定时器控制亮灭频率,一个用 mdelay 延时控制亮灭频率。 如果 timeout 没有延迟,两个灯一直是同步的;有延迟后,过一段时间两个灯亮灭变不同步了。 ### 总结 肯定有很多人反对说,定时器回调函数不要有长时间操作。发消息,信号,邮箱...交给其它线程操作云云。 软定时器本身就是一个线程,通过某种技术手段,在这个线程中可以完成的工作,一定要使用消息机制交给另外一下线程完成吗? 如何抉择,请君深思 > 本优化系列所有提到的更改已经提交到 gitee ,欢迎大家测试 https://gitee.com/thewon/rt_thread_repo 相关文章: [rt-thread 系统优化系列(一) 之 关中断](https://club.rt-thread.org/ask/article/2931.html) [rt-thread 系统优化系列(二) 之 线程间同步和通信对中断的影响](https://club.rt-thread.org/ask/article/2939.html) [rt-thread 系统优化系列(三) 之 软定时器](https://club.rt-thread.org/ask/article/2967.html) [ rt-thread 系统优化系列(四) 之 再谈 ipc 中的 bug](https://club.rt-thread.org/ask/article/3044.html) [rt-thread 系统优化系列(五) 之 线程销毁谜题](https://club.rt-thread.org/ask/article/3138.html) [rt-thread 系统优化系列(六) 之 让 idle 线程闲下来](https://club.rt-thread.org/ask/article/3141.html)
10
条评论
默认排序
按发布时间排序
登录
注册新账号
关于作者
出出啊
恃人不如自恃,人之为己者不如己之自为也
文章
43
回答
1517
被采纳
342
关注TA
发私信
相关文章
1
有关动态模块加载的一篇论文
2
最近的调程序总结
3
晕掉了,这么久都不见layer2的踪影啊
4
继续K9ii的历程
5
[GUI相关] FreeType 2
6
[GUI相关]嵌入式系统中文输入法的设计
7
20081101 RT-Thread开发者聚会总结
8
嵌入式系统基础
9
linux2.4.19在at91rm9200 上的寄存器设置
10
[转]基于嵌入式Linux的通用触摸屏校准程序
推荐文章
1
RT-Thread应用项目汇总
2
玩转RT-Thread系列教程
3
国产MCU移植系列教程汇总,欢迎查看!
4
机器人操作系统 (ROS2) 和 RT-Thread 通信
5
五分钟玩转RT-Thread新社区
6
【技术三千问】之《玩转ART-Pi》,看这篇就够了!干货汇总
7
关于STM32H7开发板上使用SDIO接口驱动SD卡挂载文件系统的问题总结
8
STM32的“GPU”——DMA2D实例详解
9
RT-Thread隐藏的宝藏之completion
10
【ART-PI】RT-Thread 开启RTC 与 Alarm组件
热门标签
RT-Thread Studio
串口
Env
LWIP
SPI
AT
Bootloader
Hardfault
CAN总线
FinSH
ART-Pi
USB
DMA
文件系统
RT-Thread
SCons
RT-Thread Nano
线程
MQTT
STM32
RTC
FAL
rt-smart
ESP8266
I2C_IIC
WIZnet_W5500
UART
ota在线升级
PWM
cubemx
freemodbus
flash
packages_软件包
BSP
潘多拉开发板_Pandora
定时器
ADC
GD32
flashDB
socket
中断
Debug
编译报错
msh
SFUD
keil_MDK
rt_mq_消息队列_msg_queue
MicroPython
ulog
C++_cpp
本月问答贡献
踩姑娘的小蘑菇
7
个答案
3
次被采纳
a1012112796
15
个答案
2
次被采纳
张世争
9
个答案
2
次被采纳
rv666
5
个答案
2
次被采纳
用户名由3_15位
13
个答案
1
次被采纳
本月文章贡献
程序员阿伟
9
篇文章
2
次点赞
hhart
3
篇文章
4
次点赞
大龄码农
1
篇文章
5
次点赞
RTT_逍遥
1
篇文章
2
次点赞
ThinkCode
1
篇文章
1
次点赞
回到
顶部
发布
问题
投诉
建议
回到
底部