1.0 前言与概述
最近在编写软件实现的 I2C 和 SPI 协议库,需要稳定的 1us 延时用作心跳函数。在 FreeRTOS 下编写 us 延时逻辑不太一样,故在文章中作记录。
与裸机实现 us 延时方法相近,使用滴答定时器通过比较数值即可实现延时。
在 RTOS 环境下实现思路如下:
- 计算指定延时时长所需要的计数器的值
- 关闭系统任务调度 - 防止延时器件被任务调度打断影响计数精度
- 获取当前重装载值
- 获取开始延时寄存器里面的计数值
- 不断获取当前计数器的值
- 如果当前计数器值大于设定的计数器的值,退出延时
- 释放系统任务调度
2.0 实现代码
void delay_us(uint32_t us) {
volatile uint32_t tcnt = 0;
uint32_t reload = SysTick->LOAD;
uint32_t ticks = us * (SystemCoreClock / 1000000);
vTaskSuspendAll(); // 阻止 OS 调度
volatile uint32_t told = SysTick->VAL;
while(1) {
volatile uint32_t tnow = SysTick->VAL;
if(tnow != told) {
if(tnow<told) {
tcnt += told - tnow;
}else {
tcnt += reload - tnow + told;
}
told = tnow;
if(tcnt >= ticks) {
break;
}
}
}
xTaskResumeAll(); // 恢复 OS 调度
}
3.0 实现过程分析
3.1 初始化部分
uint32_t tcnt = 0;
:用于累计已经经过的时间uint32_t reload = SysTick->LOAD;
:获取 SysTick 的重装载值,这是定时器的最大值uint32_t ticks = us * (SystemCoreClock / 1000000);
:计算需要延时的 SysTick 的值
3.2 暂停任务调度
通过 FreeRTOS 提供的 vTaskSupendAll();
函数,暂停操作系统的任务调度,防止在延时期间发生任务切换,影响延时精度。
3.3 获取初始计数器值
uint32_t told = SysTick->VAL;
:获取 SysTick 定时器当前计数值
3.4 主循环
主循环中会一直循环,直到时间累计到目标值 ticks
3.4.1 获取当前计数器值
uint32_t tnow = SysTick->VAL;
:获取当前 SysTick 计数器的值
3.4.2 判断数值是否发生变化
if(tnow != told)
:如果 tnow 和 told 的值不等,则 SysTick 的值发生了变化,更新累计时间 tcnt
3.4.3 计算时间差
if(tnow < told)
:如果 tnow 小于 told,则说明计数器并未溢出,累加差值 tcnt = told - tnow
;否则说明计数器发生了溢出,reload - tnow
得到 tnow 到 0 的剩余时间,再减去 told 计算出 reload 到 told 的时间。
3.4.4 更新旧计数值
told = tnow
更新 told 的值,便于下次比较
3.4.5 判断是否达到目标延时
如果累计时间 tcnt 大于或等于 ticks 则达到了目标延时时长,退出循环。
3.4.6 恢复任务调度
FreeRTOS 提供的 xTaskResumeAll();
函数可以恢复操作系统的任务调度
4.0 上述代码存在的问题
上述的代码中,可以轻松的实现 us 级别的延时功能,但是存在一些问题;程序中为了保证延时的精度,在延时函数的核心功能代码中关闭了系统的任务调度。这会对其它线程的任务运行产生干扰。
因为在系统的任务调度被暂停后,所有的任务切换都会被禁止,相当于回到了裸机状态,直到调度器被恢复。
这种行为可能会对其它线程中要求实时性的任务产生负面影响,尤其是多任务系统。
4.1 另一种实现 us 延时的方法
通过 STM32 的硬件外设 TIM 定时器可以实现无需关闭任务调度实现 us 延时。
4.2 实现代码
void tim_delay_us (uint32_t us) {
uint32_t start = __HAL_TIM_GET_COUNTER(&htim2); // 获取当前计数值
uint32_t elapsed = 0;
while (elapsed < us) {
uint32_t now = __HAL_TIM_GET_COUNTER(&htim2); // 获取当前计数值
if (now >= start) {
elapsed = now - start; // 正常情况
} else {
elapsed = 0xFFFFFFFF - start + now; // 处理溢出
}
}
}
通过硬件定时器的实现可在不占用调度器的情况下实现同样的延时效果。
END 参考与声明
[参考文章]
本文部分内容和代码由 Ai 辅助生成
评论区(暂无评论)