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的关系:

任务控制API知识体系 任务控制API 延时类 挂起/恢复类 自杀类 vTaskDelay() 相对延时 vTaskDelayUntil() 绝对延时 vTaskSuspend() 挂起任务 vTaskResume() 恢复任务 vTaskDelete() 删除任务 关键特性对比 🔹 vTaskDelay():简单延时,时间会漂移 🔹 vTaskDelayUntil():精准周期,适合定时任务 🔹 vTaskSuspend/Resume():成对使用,适合省电模式 🔹 vTaskDelete():删除任务,注意资源释放 所有API都可在任务中调用,vTaskResume()和vTaskDelete()还可在中断中调用

4.6 避坑指南

最后,把我这些年踩过的坑总结一下:

  • vTaskDelay() 的tick单位问题 —— 永远用 pdMS_TO_TICKS() 转换,别自己算
  • vTaskDelayUntil() 的执行时间超周期 —— 设计时留余量,或者用双缓冲机制
  • vTaskSuspend() 后忘记恢复 —— 这个任务就永远「睡死」了,调试时很难发现
  • vTaskDelete() 后的内存泄漏 —— 删除前检查所有动态分配的内存是否已释放
  • 在中断中调用这些API —— vTaskDelay() 和 vTaskDelayUntil() 不能在中断中用!vTaskResume() 和 vTaskDelete() 可以,但要注意优先级

嗯,任务控制API就这些了。说白了,就是让任务能「等一等」、「歇一歇」、「走人」。掌握了这些,你的FreeRTOS系统才算真正有了「控制力」。