解决在 FreeRTOS 运行时无法进行精确微秒与毫秒延时问题
目录
1. 问题背景
在 FreeRTOS 运行环境下,通常使用 vTaskDelay()
或 vTaskDelayUntil()
进行任务延时。但这些函数的延时精度是 Tick 级,默认最小单位为 1 Tick(比如 1ms),无法满足 精确到微秒级 的延时需求。
对于某些应用,例如 传感器通信(DHT11)、软件定时、信号采集,我们需要更高精度的微秒级或毫秒级延时。
2. 常见延时方法及其局限性
方法 1:vTaskDelay()(毫秒级)
vTaskDelay(pdMS_TO_TICKS(10)); // 延时 10ms
✅ 优点: 使用 FreeRTOS 任务调度,不阻塞 CPU。
❌ 缺点: 受 Tick 时间粒度限制,无法实现微秒级精确延时。
方法 2:for 循环软件延时(不可取)
void delay_us(uint32_t us) {
for (volatile uint32_t i = 0; i < us * 10; i++); // 假设 SystemCoreClock 72MHz
}
❌ 缺点:
- 受 编译优化 影响,延时时间不准确。
- 占用 CPU,无法执行其他任务。
- 不同频率 MCU 需要调整延时系数,不通用。
方法 3:SysTick 计数(受限于 FreeRTOS)
- FreeRTOS 会占用 SysTick,无法直接使用
SysTick->VAL
进行延时。 - 需使用 DWT(数据观察点和跟踪)计数器 实现高精度延时。
3. 解决方案:使用 DWT 计数器
DWT(Data Watchpoint and Trace)是 Cortex-M 内核中的调试组件,其中的 DWT->CYCCNT
计数器每个 CPU 时钟周期递增 1。我们可以利用它实现 高精度的微秒级延时。
在 SystemCoreClock = 72MHz
时:
DWT->CYCCNT
每 1 个周期增加 1。
1 微秒(us) = 72 个时钟周期
。
通过 DWT->CYCCNT
计算 us
级别的时间。
默认情况下,DWT 计数器是关闭的,我们需要手动启用它。
3.1 启用 DWT
DWT 计数器默认是关闭的,我们需要手动启用:
void DWT_Init(void) {
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; // 使能 DWT 计数器
DWT->CYCCNT = 0; // 计数器清零
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; // 使能 CYCCNT 计数功能
}
3.2 实现微秒延时
void delay_us(uint32_t us) {
uint32_t start = DWT->CYCCNT; // 记录起始计数值
uint32_t ticks = us * (SystemCoreClock / 1000000); // 计算需要的周期数
while ((DWT->CYCCNT - start) < ticks); // 等待计时完成
}
说明:
SystemCoreClock / 1000000
计算 1 微秒所需的 CPU 周期数。DWT->CYCCNT
不会受到 FreeRTOS 调度影响,确保高精度计时。
3.3 实现毫秒延时
void delay_ms(uint32_t ms) {
while (ms--) delay_us(1000);
}
将这个代码进行与RTOS结合进阶一下:
static u8 fac_us = 0; // us延时倍乘数
static u16 fac_ms = 0; // ms延时倍乘数,在FreeRTOS下,代表每个节拍的ms数
// 延时nms
void delay_ms(u32 nms)
{
if (xTaskGetSchedulerState() != taskSCHEDULER_NOT_STARTED) // 系统已经运行
{
if (nms >= fac_ms) // 延时的时间大于OS的最少时间周期
{
vTaskDelay(nms / fac_ms); // FreeRTOS延时
}
nms %= fac_ms; // OS已经无法提供这么小的延时了,采用普通方式延时
}
delay_us((u32)(nms * 1000)); // 普通方式延时
}
3.4 实现不会引起任务调度的毫秒延时
static u8 fac_us = 0; // us延时倍乘数
static u16 fac_ms = 0; // ms延时倍乘数,在FreeRTOS下,代表每个节拍的ms数
// 延时nms,不会引起任务调度
void delay_xms(u32 nms)
{
u32 ticks;
u32 told, tnow, tcnt = 0;
u32 reload = SysTick->LOAD; // LOAD的值
ticks = nms * fac_us * 1000; // 需要的节拍数 (nms * 1000 * fac_us)
told = SysTick->VAL; // 刚进入时的计数器值
while (1) {
tnow = SysTick->VAL;
if (tnow != told) {
if (tnow < told) {
tcnt += told - tnow; // SYSTICK是递减计数器
} else {
tcnt += reload - tnow + told;
}
told = tnow;
if (tcnt >= ticks) {
break; // 时间超过/等于要延迟的时间,则退出
}
}
}
}
4. 在 FreeRTOS 中正确使用 DWT 延时
由于 FreeRTOS 任务调度不允许长时间阻塞,我们可以在 非实时任务(如数据采集、I/O 操作)中使用 delay_us()
,但对于任务调度仍然推荐 vTaskDelay()
进行 非阻塞式等待。
示例:在任务中读取 DHT11 传感器
void DHT11_Task(void *pvParameters) {
uint8_t temp, humi;
while (1) {
if (DHT11_Read_Data(&temp, &humi) == 0) {
printf("Temp: %d, Humi: %d\n", temp, humi);
}
vTaskDelay(pdMS_TO_TICKS(1000)); // 每 1 秒读取一次
}
}
- DHT11 传感器 需要
delay_us()
来正确读取数据。 - 任务本身仍使用
vTaskDelay()
进行定时,避免 CPU 资源浪费。
5. 总结
✅ 问题: FreeRTOS 中 vTaskDelay()
仅支持 Tick 级延时,无法实现微秒级精确延时。
✅ 解决方案: 启用 DWT->CYCCNT
计数器,实现 高精度微秒级延时。
✅ 最佳实践:
- 短时间延时(<1ms) → 使用
delay_us()
。 - 长时间延时(≥1ms) → 使用
vTaskDelay()
,减少 CPU 占用。 - 任务中使用高精度延时 → 结合 FreeRTOS 任务调度,避免长时间阻塞 CPU。
通过 DWT 计数器,我们可以在 FreeRTOS 下实现 精确微秒级延时,满足高精度计时需求!🚀