4. 任务控制API:vTaskDelay()、vTaskDelayUntil()、vTaskSuspend()、vTaskResume()、vTaskDelete()
任务创建好了,跑起来了,然后呢?
你想想看,一个系统里不可能所有任务都在疯狂抢CPU。有的任务需要等一等,有的任务需要暂时歇一歇,有的任务干脆就不想干了。这时候,任务控制API就派上用场了。
我个人习惯把这几个API分成三类:延时类、挂起/恢复类、自杀类。咱们一个一个来聊。
4.1 vTaskDelay() —— 让任务「睡一会儿」
这个API最简单,也最常用。说白了就是让当前任务进入阻塞态,等一段时间后再回来。
函数原型:
void vTaskDelay( const TickType_t xTicksToDelay );
参数: xTicksToDelay —— 要延时的时钟节拍数
注意: 单位是 tick,不是毫秒!
嗯,这里要注意。很多新手上来就写 vTaskDelay(1000),以为延时1秒。结果发现任务跑得飞快或者慢得离谱。为什么?因为tick的周期是由 configTICK_RATE_HZ 决定的。
举个例子:
// 假设 configTICK_RATE_HZ = 1000,每个tick = 1ms
vTaskDelay(1000); // 延时1000ms = 1秒
// 假设 configTICK_RATE_HZ = 100,每个tick = 10ms
vTaskDelay(1000); // 延时10000ms = 10秒
我在项目中遇到过这种情况:一个同事把tick频率设成了100Hz,然后写了个 vTaskDelay(50) 想延时50ms,结果实际延时了500ms。整个系统的响应节奏全乱了。所以,一定要搞清楚你的tick配置。
小技巧: 用 pdMS_TO_TICKS() 宏来转换毫秒到tick。
vTaskDelay( pdMS_TO_TICKS(100) ); // 延时100ms,不管tick频率是多少
4.2 vTaskDelayUntil() —— 精准定时的利器
这个API比 vTaskDelay() 高级一点。它解决了一个很实际的问题:周期性任务的时间漂移。
你想想看,如果用 vTaskDelay() 做周期性任务:
void vTaskPeriodic(void *pvParameters)
{
for(;;)
{
// 做事情,耗时 5ms
vTaskDelay(pdMS_TO_TICKS(100)); // 延时100ms
}
}
这个任务实际周期是多少?100ms + 5ms = 105ms。而且随着系统负载变化,这个5ms还会变。时间长了,周期就漂了。
vTaskDelayUntil() 就是来解决这个问题的:
函数原型:
void vTaskDelayUntil( TickType_t *pxPreviousWakeTime,
const TickType_t xTimeIncrement );
参数1: pxPreviousWakeTime —— 上次唤醒时间的指针
参数2: xTimeIncrement —— 周期长度(tick数)
用法是这样的:
void vTaskPeriodicAccurate(void *pvParameters)
{
TickType_t xLastWakeTime = xTaskGetTickCount();
for(;;)
{
// 做事情,耗时 5ms
vTaskDelayUntil(&xLastWakeTime, pdMS_TO_TICKS(100));
}
}
这个API会保证:不管你的任务执行了多久,下一次唤醒的时间总是 上次唤醒时间 + 周期。说白了,它把执行时间「吃掉」了,周期是固定的。
我曾经踩过的坑: 如果任务的执行时间超过了周期长度,vTaskDelayUntil() 会立即返回,不会等待。这时候周期就乱了。所以,一定要保证任务执行时间 < 周期时间。
4.3 vTaskSuspend() 和 vTaskResume() —— 任务的「暂停键」
这两个API是成对出现的。一个让任务挂起,一个让任务恢复。
函数原型:
void vTaskSuspend( TaskHandle_t xTaskToSuspend );
void vTaskResume( TaskHandle_t xTaskToResume );
参数: 任务句柄,就是创建任务时传出来的那个句柄
用法很简单:
TaskHandle_t xTaskHandle = NULL;
void vTaskToControl(void *pvParameters)
{
for(;;)
{
// 正常工作
vTaskDelay(pdMS_TO_TICKS(100));
}
}
// 在另一个任务或中断中
vTaskSuspend(xTaskHandle); // 挂起任务
// ... 过了一段时间 ...
vTaskResume(xTaskHandle); // 恢复任务
我个人习惯用这个来做「省电模式」。比如一个传感器采集任务,平时每100ms采集一次。当系统检测到设备空闲时,就把这个任务挂起,省电。等用户操作了,再恢复。
注意: 如果挂起自己,可以传 NULL 或者用 vTaskSuspend(NULL)。但恢复自己?嗯,自己都挂起了,谁来恢复呢?所以恢复操作必须在其他任务或中断中完成。
4.4 vTaskDelete() —— 任务的「安乐死」
这个API用来删除一个任务。任务被删除后,它的栈空间和TCB(任务控制块)会被释放回堆内存。
函数原型:
void vTaskDelete( TaskHandle_t xTaskToDelete );
参数: 要删除的任务句柄,传NULL表示删除自己
用法:
void vTaskThatWillDie(void *pvParameters)
{
// 做一些初始化工作
// ...
// 任务完成了,删除自己
vTaskDelete(NULL);
}
// 或者在另一个任务中删除它
vTaskDelete(xTaskHandle);
我曾经踩过的坑: 删除任务后,如果这个任务使用了动态分配的内存(比如malloc),这些内存不会自动释放。所以,在删除任务之前,一定要确保任务已经释放了它占用的所有资源。否则,内存泄漏等着你。
还有一个细节:vTaskDelete() 不会立即回收栈空间。它会把这个任务标记为「待删除」,然后在空闲任务中真正回收。所以,如果你在中断中调用了 vTaskDelete(),要确保空闲任务有机会运行。
4.5 知识体系总览
下面这张图帮你理清这几个API的关系:
4.6 避坑指南
最后,把我这些年踩过的坑总结一下:
- vTaskDelay() 的tick单位问题 —— 永远用
pdMS_TO_TICKS()转换,别自己算 - vTaskDelayUntil() 的执行时间超周期 —— 设计时留余量,或者用双缓冲机制
- vTaskSuspend() 后忘记恢复 —— 这个任务就永远「睡死」了,调试时很难发现
- vTaskDelete() 后的内存泄漏 —— 删除前检查所有动态分配的内存是否已释放
- 在中断中调用这些API —— vTaskDelay() 和 vTaskDelayUntil() 不能在中断中用!vTaskResume() 和 vTaskDelete() 可以,但要注意优先级
嗯,任务控制API就这些了。说白了,就是让任务能「等一等」、「歇一歇」、「走人」。掌握了这些,你的FreeRTOS系统才算真正有了「控制力」。