FreeRTOS任务状态详解:运行、就绪、阻塞、挂起与删除

FreeRTOS多任务管理 FreeRTOS通过任务机制实现多任务调度,将系统功能分割为独立且无限循环的任务函数。任务采用抢占式调度机制,高优先级任务可打断低优先级任务。任务有四种状态:就绪态、运行态、阻塞态和挂起态,通过状态转换实现任务切换。系统启动时会创建最低优先级的空闲任务。动态创建任务需定义堆内存空间,通过xTaskCreate()函数实现,需指定任务函数、名称、堆栈大小、优先级等参数。任务控制块指针(任务句柄)用于操作任务。创建的任务处于就绪态,可参与系统调度。 阅读详情

1. 任务状态的本质:嵌入式并发执行的底层逻辑

在裸机开发中,我们习惯于将所有功能逻辑塞进一个无限循环( while(1) )中。这种结构看似简单,实则暗藏危机。当两个看似独立的 LED 控制需求同时出现——红灯以 500ms 周期闪烁、绿灯以“亮 200ms → 灭 500ms → 重复”模式运行时,问题立刻浮现:如何让 CPU 在同一物理时间线上,为两个不同节奏的逻辑提供服务?

答案不是魔法,而是对“时间”与“状态”的显式建模。你不得不引入多个全局标志位( red_toggle_flag , green_step_counter , last_red_tick , last_green_tick ),并在主循环中反复轮询当前系统滴答(通常来自 SysTick 或定时器中断)与各任务上次执行时间的差值。一旦差值达到预设阈值,就执行对应动作,并立即更新该任务的时间戳。这本质上是在用软件模拟一套最原始的调度器。

这种模式的问题不在于它不能工作,而在于它不可扩展。每增加一个任务,标志位数量呈线性增长,状态机复杂度呈指数级上升。一个典型的物联网终端可能同时承载传感器采集(需考虑采样周期、数据校验、缓存管理)、按键消抖与事件分发、LCD 刷新(含帧缓冲与刷新率控制)、串口协议解析(需处理帧头、长度域、校验和、粘包/拆包)、以及调试日志输出(需控制波特率与缓冲区溢出)。若将这些全部揉进一个 while(1) ,代码将迅速沦为难以维护的“意大利面”,任何一处修改都可能引发连锁反应。

FreeRTOS 的核心价值,正在于它将这套手工编排的时间与状态管理,从应用层彻底剥离,下沉为内核级的抽象。它没有创造新的物理 CPU,而是通过精确的上下文切换(Context Switch),让单个 CPU 核心在多个任务栈之间高速跳转,制造出“并行执行”的确定性幻觉。而理解这一幻觉如何被构造,起点便是透彻掌握 任务状态(Task State) ——这是 FreeRTOS 调度器进行决策的唯一依据。

2. FreeRTOS 任务状态模型:五种状态的工程含义

FreeRTOS 定义了五种标准任务状态,它们并非理论概念,而是直接映射到内核调度器的数据结构与行为逻辑中。每一个状态都对应着任务在系统资源占用、CPU 时间分配以及内核干预程度上的明确语义。

2.1 运行态(Running)

这是唯一一个“正在真实占用 CPU 执行指令”的状态。在任意时刻,对于单核 MCU(如 STM32F1/F4,或 ESP32 的单个 PRO/CORE), 有且仅有一个任务处于 Running 态 。该任务的程序计数器(PC)、堆栈指针(SP)等寄存器上下文,正被加载到 CPU 的物理寄存器中。其他所有任务,无论其逻辑多么“紧急”,在此刻都处于等待状态。

关键点在于:Running 态是瞬时的、动态的。FreeRTOS 内核通过 SysTick 中断(或可配置的其他定时器中断)定期触发一次调度器(Scheduler)。在每次 SysTick 中断服务函数(ISR)的末尾,调度器会检查:是否有更高优先级的就绪任务?或者当前运行任务是否已主动放弃 CPU(如调用 vTaskDelay() )?如果条件满足,内核会强制保存当前 Running 任务的全部寄存器上下文(即“压栈”),然后从目标任务的栈中恢复其上下文(即“出栈”),最后执行一条 BX (ARM Cortex-M)或 ERET (RISC-V)指令,将 CPU 控制权无缝移交。整个过程耗时极短(通常在微秒级),用户感知不到中断开销。

2.2 就绪态(Ready)

就绪态是任务能被调度器选中执行的“预备队”。一个任务处于 Ready 态,意味着:
- 它已被成功创建( xTaskCreate() 返回 pdPASS );
- 它的优先级高于当前 Running 任务(或等于但启用了时间片轮转);
- 它没有被任何同步机制(如队列、信号量、互斥量)所阻塞;
- 它没有主动调用延时或进入挂起。

所有 Ready 态的任务,被内核组织在一个按优先级排序的链表数组中( pxReadyTasksLists[] )。每个数组索引对应一个优先级,索引下的链表存放所有该优先级的就绪任务。当调度器需要选择下一个任务时,它只需从最高非空优先级链表的头部取出第一个任务即可,时间复杂度为 O(1)。这就是为什么在 FreeRTOS 中, 任务优先级是一个静态的、编译期决定的整数( uxPriority ),而非一个运行时可变的权重值 ——它直接决定了任务在就绪列表中的存储位置,是调度效率的基石。

2.3 阻塞态(Blocked)

阻塞态是任务主动让出 CPU 并等待某个外部事件发生的“休眠”状态。这是实现高效并发的核心机制。一个任务进入 Blocked 态,绝非因为“出错”或“卡死”,而是因为它明确地表达了“我现在无法继续,除非 X 发生”。

最常见的阻塞原因包括:
- 等待队列数据 : xQueueReceive(xQueue, &buffer, portMAX_DELAY) 。任务将自身加入该队列的 xTasksWaitingToReceive 链表,并进入 Blocked 态。只有当另一个任务或中断向该队列写入数据后,队列的 xTaskRemoveFromEventList() 函数才会将其移出 Blocked 态,放入 Ready 态。
- 等待信号量/互斥量 : xSemaphoreTake(xSemaphore, portMAX_DELAY) 。原理同上,任务被挂起在信号量的等待列表中,直到有任务释放( xSemaphoreGive() )该信号量。
- 等待事件组位 : xEventGroupWaitBits(xEventGroup, BIT_0, pdTRUE, pdFALSE, portMAX_DELAY) 。
- 等待定时器到期 : vTaskDelay(100 / portTICK_PERIOD_MS) 。这实际上是将任务挂起在内核的 xTimerList 上,由 SysTick 中断负责在指定滴答数后将其唤醒。

阻塞态的关键工程意义在于: 它完全解耦了任务的逻辑与时间 。红灯任务无需关心“现在系统过了多少毫秒”,它只需说“我需要每 500ms 执行一次翻转”,然后调用 vTaskDelay(500 / portTICK_PERIOD_MS) 。内核会精确地在 500ms 后将它唤醒,期间 CPU 可以毫无负担地去执行其他就绪任务。这彻底消除了轮询(Polling)带来的 CPU 空转与功耗浪费。

2.4 挂起态(Suspended)

挂起态是一种“硬性暂停”,由内核 API 显式控制,与事件无关。调用 vTaskSuspend(xTaskHandle) 会立即将指定任务从其当前状态(无论是 Ready、Blocked 还是 Running)强制转移到 Suspended 态。处于 Suspended 态的任务:
- 不会被调度器选中执行;
- 不会响应任何事件(即使它正在等待的队列收到了数据,它也不会被唤醒);
- 其所有内部计时器(如 vTaskDelay 的剩余时间)将被冻结。

挂起态的主要用途是 调试与系统维护 。例如,在进行固件 OTA 升级前,可以安全地挂起所有非关键任务(如 LED 控制、用户界面),只保留通信与升级任务,确保升级过程不受干扰。又或者,在调试时,为了隔离某个疑似有问题的任务,可以临时将其挂起,观察系统其余部分的行为。与之对应的是 xTaskResume() ,用于恢复挂起任务。

需要特别注意: vTaskSuspend(NULL) 会挂起调用者自身,这将导致一个死锁——因为被挂起的任务永远无法再执行 xTaskResume() 来唤醒自己。这是一个典型的初学者陷阱。

2.5 删除态(Deleted)

删除态不是一个可被调度器观察到的“活跃”状态,而是任务生命周期的终点。当一个任务调用 vTaskDelete(NULL) 或另一个任务调用 vTaskDelete(xTaskHandle) 时,该任务的 TCB(Task Control Block)和栈内存并不会被立即释放。相反,它们被放入一个特殊的“延迟删除”列表( xTasksWaitingTermination )。真正的内存回收工作,由一个名为 prvIdleTask() 的空闲任务(Idle Task)在系统空闲时完成。

这个设计至关重要。它避免了在中断服务程序(ISR)或高优先级任务中,因调用 vTaskDelete() 而触发复杂的内存管理操作(如 pvPortMalloc() 的临界区保护),从而保证了实时性。 prvIdleTask() 是一个最低优先级( tskIDLE_PRIORITY = 0 )的系统内置任务,它的唯一职责就是:当系统无其他更高优先级任务可运行时,它便获得 CPU 时间,此时它会遍历 xTasksWaitingTermination 列表,安全地释放所有待删除任务的资源。

因此,一个被 vTaskDelete() 的任务,其 TCB 和栈空间在 prvIdleTask() 执行前依然存在。这意味着,如果你在删除任务后,又尝试通过其句柄( xTaskHandle )去操作它(如 vTaskSuspend() ),结果将是未定义的(Undefined Behavior),很可能导致系统崩溃。这是另一个必须牢记的工程实践。

3. 状态转换的驱动引擎:SysTick 与调度器

理解了五种状态,下一步是理解它们如何流动。FreeRTOS 的状态转换并非随机发生,而是严格受控于两个核心引擎: SysTick 定时器 与 调度器(Scheduler) 。

3.1 SysTick:心跳与时间片的物理来源

在 ARM Cortex-M 架构的 MCU(如 STM32)上,SysTick 是一个 24 位递减计数器,专为操作系统提供精确的系统节拍(System Tick)。FreeRTOS 的 configTICK_RATE_HZ 配置项(通常默认为 1000 Hz,即 1ms 一滴答)直接决定了 SysTick 的重装载值( LOAD 寄存器)。

每当 SysTick 计数器递减至 0,它会自动重载 LOAD 值并产生一个中断。这个中断服务函数( xPortSysTickHandler() )是 FreeRTOS 的“心脏起搏器”。它的核心工作流程如下:

void xPortSysTickHandler( void )
{
    /* 1. 清除 SysTick 中断挂起标志 */
    portCLEAR_INTERRUPT_MASK_FROM_ISR();

    /* 2. 通知内核:一个滴答过去了 */
    xTaskIncrementTick();

    /* 3. 检查是否有更高优先级任务就绪,需要进行上下文切换 */
    if( xYieldPending != pdFALSE )
    {
        vTaskSwitchContext();
    }
}

其中, xTaskIncrementTick() 是状态转换的总开关。它会:
- 递增全局滴答计数器 xTickCount ;
- 遍历所有处于 Blocked 态并设置了超时时间的任务,检查其剩余等待时间是否已归零。若已归零,则将其从 Blocked 态移入 Ready 态;
- 更新软件定时器( xTimerList )的状态,触发到期的定时器回调函数。

vTaskSwitchContext() 则是上下文切换的执行者。它会:
- 保存当前 Running 任务的全部 CPU 寄存器(R0-R12, LR, PC, xPSR, PSP/MSP)到其任务栈中;
- 从就绪列表中选取下一个最高优先级的 Ready 任务;
- 从该任务的栈中恢复其寄存器上下文;
- 执行 BX 指令,将 CPU 控制权交出。

3.2 任务创建:从“不存在”到“就绪”的初始化

任务的生命周期始于 xTaskCreate() 。这个 API 的调用,远不止是分配一块内存那么简单,它是一次完整的“任务注册”过程:

BaseType_t xTaskCreate(
    TaskFunction_t pxTaskCode,      // 任务函数指针,即任务的入口
    const char * const pcName,      // 任务名称,仅用于调试,存储在 TCB 中
    const uint16_t usStackDepth,    // 栈深度(单位:字,非字节!)
    void * const pvParameters,      // 传递给任务函数的参数
    UBaseType_t uxPriority,         // 任务优先级(0 为最低)
    TaskHandle_t * const pxCreatedTask // 输出:任务句柄,用于后续操作
);

xTaskCreate() 的内部执行步骤如下:
1. 内存分配 :调用 pvPortMalloc() 为任务的栈( usStackDepth * sizeof(StackType_t) )和 TCB(一个固定大小的结构体)分别分配内存。
2. TCB 初始化 :填充 TCB 的各个字段,包括:
- pxTopOfStack : 指向栈顶的指针,用于上下文切换时的压栈/出栈;
- pcTaskName : 存储任务名称;
- uxPriority : 设置初始优先级;
- pxStack : 指向任务栈的基地址;
- eTaskState : 初始化为 eReady ;
- xStateListItem : 将任务插入 pxReadyTasksLists[uxPriority] 链表。
3. 栈初始化 :在任务栈上预先“压入”一组初始寄存器值。最关键的是 pxTopOfStack 处的值,它被设置为任务函数 pxTaskCode 的地址; LR (链接寄存器)被设置为 prvTaskExitError (一个错误处理函数,当任务函数意外返回时调用); R0 被设置为 pvParameters 。这样,当该任务第一次被调度执行时, BX 指令会跳转到 pxTaskCode ,并且 R0 中已经包含了传入的参数。

因此,一个任务在 xTaskCreate() 成功返回后,其状态立即变为 eReady ,并被放置在对应优先级的就绪列表中,静待调度器的召唤。

3.3 主动让出: vTaskDelay() 与状态跃迁

vTaskDelay() 是最常用的状态转换 API。它的签名是 void vTaskDelay( const TickType_t xTicksToDelay ) ,参数 xTicksToDelay 表示希望任务挂起多少个系统滴答。

其内部逻辑清晰而精妙:
1. 如果 xTicksToDelay 为 0,函数直接返回,不改变任务状态(这是一种常见的优化,用于“让出本次时间片”)。
2. 否则,它会计算任务的唤醒时间点: xTimeToWake = xTickCount + xTicksToDelay 。
3. 接着,它将当前任务的 TCB 从就绪列表中移除,并将其 xStateListItem 插入到内核的 xDelayedTaskList1 (或 xDelayedTaskList2 ,用于双缓冲,避免临界区锁定)中,按 xTimeToWake 的大小进行排序。
4. 最后,它调用 taskYIELD() ,主动触发一次上下文切换,将 CPU 让给其他就绪任务。

从此刻起,该任务的状态便从 eReady 变为 eBlocked 。它将一直停留在 xDelayedTaskList 中,直到 xTaskIncrementTick() 在某个 SysTick 中断中发现 xTimeToWake <= xTickCount ,才将其移回就绪列表。

4. 实战剖析:双 LED 任务的 FreeRTOS 实现

让我们回归视频开头的红灯/绿灯需求,用 FreeRTOS 的思维重写。这不再是两个纠缠的标志位,而是两个彼此绝缘、各司其职的独立实体。

4.1 红灯任务:纯粹的周期性执行

// 定义红灯 GPIO(以 STM32 HAL 库为例)
#define RED_LED_GPIO_PORT GPIOA
#define RED_LED_GPIO_PIN  GPIO_PIN_5

void vRedLedTask( void *pvParameters )
{
    // 初始化 GPIO(此操作通常应在 main() 中完成,此处仅为逻辑完整)
    __HAL_RCC_GPIOA_CLK_ENABLE();
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    GPIO_InitStruct.Pin = RED_LED_GPIO_PIN;
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
    HAL_GPIO_Init(RED_LED_GPIO_PORT, &GPIO_InitStruct);

    // 初始状态:LED 熄灭
    HAL_GPIO_WritePin(RED_LED_GPIO_PORT, RED_LED_GPIO_PIN, GPIO_PIN_SET);

    for( ;; ) // 无限循环,FreeRTOS 会管理其生命周期
    {
        // 翻转 LED 状态
        HAL_GPIO_TogglePin(RED_LED_GPIO_PORT, RED_LED_GPIO_PIN);

        // 主动阻塞 500ms,进入 Blocked 态
        // portTICK_PERIOD_MS 是每个滴答的毫秒数(例如 configTICK_RATE_HZ=1000 时,值为 1)
        vTaskDelay(500 / portTICK_PERIOD_MS);
    }
}

工程解读 :
- 此任务的全部逻辑,就是“翻转一次,睡 500ms,再翻转”。它对系统时间一无所知,也不需要记录任何状态变量。
- vTaskDelay() 是其唯一的“时间感知”API。内核保证了 500ms 的精度(误差仅限于 SysTick 中断的 jitter,通常在微秒级)。
- 任务代码简洁、可读性强,且完全独立。你可以将其复制到任何支持 FreeRTOS 的项目中,只需修改 GPIO 定义即可复用。

4.2 绿灯任务:状态机与阻塞的完美结合

绿灯的“亮 200ms → 灭 500ms → 重复”模式,天然就是一个有限状态机(FSM)。在裸机中,你需要用 switch-case 和 static 变量来维护状态。而在 FreeRTOS 中, 阻塞本身就是状态转换的驱动力 。

// 定义绿灯 GPIO
#define GREEN_LED_GPIO_PORT GPIOB
#define GREEN_LED_GPIO_PIN  GPIO_PIN_0

void vGreenLedTask( void *pvParameters )
{
    __HAL_RCC_GPIOB_CLK_ENABLE();
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    GPIO_InitStruct.Pin = GREEN_LED_GPIO_PIN;
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
    HAL_GPIO_Init(GREEN_LED_GPIO_PORT, &GPIO_InitStruct);

    // 初始状态:LED 熄灭
    HAL_GPIO_WritePin(GREEN_LED_GPIO_PORT, GREEN_LED_GPIO_PIN, GPIO_PIN_SET);

    // 状态变量:记录当前是第几次“亮-灭”循环
    uint8_t ucBlinkCycle = 0;

    for( ;; )
    {
        // 状态 1:点亮绿灯,持续 200ms
        HAL_GPIO_WritePin(GREEN_LED_GPIO_PORT, GREEN_LED_GPIO_PIN, GPIO_PIN_RESET);
        vTaskDelay(200 / portTICK_PERIOD_MS);

        // 状态 2:熄灭绿灯,持续 500ms
        HAL_GPIO_WritePin(GREEN_LED_GPIO_PORT, GREEN_LED_GPIO_PIN, GPIO_PIN_SET);
        vTaskDelay(500 / portTICK_PERIOD_MS);

        // 更新循环计数器
        ucBlinkCycle++;
        if( ucBlinkCycle >= 5 )
        {
            // 已完成 5 次,进入“长灭”阶段
            HAL_GPIO_WritePin(GREEN_LED_GPIO_PORT, GREEN_LED_GPIO_PIN, GPIO_PIN_SET);
            vTaskDelay(500 / portTICK_PERIOD_MS); // 熄灭 500ms

            // 重置计数器,开始下一轮
            ucBlinkCycle = 0;
        }
    }
}

工程解读 :
- 任务的“状态”不再需要复杂的 switch-case 和 static 变量来维护。每一次 vTaskDelay() 的调用,都标志着一个状态的结束和下一个状态的开始。
- 整个逻辑流是线性的、自解释的。阅读代码,你能清晰地看到“亮→灭→判断→重置→再亮”的完整脉络。
- ucBlinkCycle 是一个局部变量,其生命周期与任务本身绑定。由于任务是独立的,这个变量不会与其他任务产生任何耦合。

4.3 主函数:任务的“注册中心”

main() 函数的角色,从裸机时代的“逻辑中枢”,转变为 FreeRTOS 的“任务注册中心”和“调度启动器”。

int main(void)
{
    // 1. 硬件初始化(时钟、外设等)
    HAL_Init();
    SystemClock_Config(); // 配置系统时钟(如 72MHz)

    // 2. 创建任务
    xTaskCreate(
        vRedLedTask,       // 任务函数
        "Red LED",         // 任务名
        configMINIMAL_STACK_SIZE, // 栈大小,最小栈足够用于简单 GPIO 操作
        NULL,              // 无参数
        tskIDLE_PRIORITY + 2, // 优先级,比空闲任务高 2 级
        NULL               // 不需要任务句柄
    );

    xTaskCreate(
        vGreenLedTask,
        "Green LED",
        configMINIMAL_STACK_SIZE,
        NULL,
        tskIDLE_PRIORITY + 1, // 优先级略低于红灯任务
        NULL
    );

    // 3. 启动调度器!
    // 此函数永不返回。它会启动 SysTick,开始第一个上下文切换。
    vTaskStartScheduler();

    // 如果执行到这里,说明调度器启动失败(通常是内存不足)
    // 此处应添加错误处理,例如点亮一个错误 LED
    for( ;; );
}

关键点 :
- vTaskStartScheduler() 是 FreeRTOS 的“点火开关”。调用它之后, main() 函数的栈帧将被丢弃,CPU 完全交由 FreeRTOS 内核管理。 main() 中的 for(;;) 只是一个安全兜底。
- 任务的优先级设置( tskIDLE_PRIORITY + 1/2 )决定了它们的执行顺序。在本例中,红灯任务优先级更高,意味着当两个任务同时就绪时,红灯会先得到 CPU。但这并不影响绿灯的精确时序,因为 vTaskDelay() 的阻塞是绝对的。

5. 深度实践:理解状态转换的调试技巧

在实际开发中,仅仅知道状态定义是不够的。你需要工具来观测它们,才能诊断并发问题。

5.1 使用 uxTaskGetSystemState()

这是最强大的运行时状态快照 API。它能获取系统中所有任务的详细信息,包括其当前状态、栈使用量、优先级等。

void vPrintTaskStatus( void )
{
    // 定义一个足够大的数组来存储所有任务的状态
    TaskStatus_t *pxTaskStatusArray;
    volatile UBaseType_t uxNumberOfTasks;
    uint32_t ulTotalRunTime;

    // 获取任务总数
    uxNumberOfTasks = uxTaskGetNumberOfTasks();

    // 分配内存(通常在 RAM 中)
    pxTaskStatusArray = pvPortMalloc( uxNumberOfTasks * sizeof( TaskStatus_t ) );

    if( pxTaskStatusArray != NULL )
    {
        // 获取系统状态快照
        uxNumberOfTasks = uxTaskGetSystemState(
            pxTaskStatusArray,          // 输出数组
            uxNumberOfTasks,            // 数组大小
            &ulTotalRunTime             // 总运行时间(需启用 configGENERATE_RUN_TIME_STATS)
        );

        // 打印每个任务的状态
        for( int i = 0; i < (int) uxNumberOfTasks; i++ )
        {
            printf("Task: %s | State: ", pxTaskStatusArray[i].pcTaskName);
            switch( pxTaskStatusArray[i].eCurrentState )
            {
                case eRunning:   printf("RUNNING"); break;
                case eReady:     printf("READY"); break;
                case eBlocked:   printf("BLOCKED"); break;
                case eSuspended: printf("SUSPENDED"); break;
                case eDeleted:   printf("DELETED"); break;
                default:         printf("UNKNOWN"); break;
            }
            printf(" | Priority: %d | Stack High Water Mark: %d\r\n",
                   pxTaskStatusArray[i].uxCurrentPriority,
                   pxTaskStatusArray[i].usStackHighWaterMark);
        }

        vPortFree( pxTaskStatusArray );
    }
}

将 vPrintTaskStatus() 放入一个低优先级的调试任务中,或通过串口命令触发,你就能实时看到所有任务的“健康状况”。栈的“高水位标记”(High Water Mark)尤其重要,它告诉你该任务历史上消耗的最大栈空间。如果这个值接近你为任务分配的栈大小,就预示着栈溢出的风险,必须扩容。

5.2 利用 IDE 的 RTOS 视图

现代 IDE(如 STM32CubeIDE、ESP-IDF 的 VS Code 插件)通常集成了 FreeRTOS 的可视化插件。当你连接调试器(如 ST-Link)并暂停程序时,这些插件能自动解析内存中的 TCB 和就绪列表,以图形化方式展示:
- 所有任务的名称、状态、优先级、栈使用率;
- 当前哪个任务处于 Running 态;
- 哪些任务被阻塞在哪个队列/信号量上。

这比手动解析内存要直观百倍,是定位死锁(Deadlock)和优先级反转(Priority Inversion)问题的利器。

6. 经验之谈:我在项目中踩过的状态相关坑

理论再完美,也抵不过一次真实的现场故障。分享几个我在工业网关项目中因误解任务状态而付出的代价。

6.1 “永不超时”的 portMAX_DELAY

portMAX_DELAY 是一个宏,其值通常为 0xffffffffUL ,表示“无限期等待”。在 xQueueReceive() 或 xSemaphoreTake() 中使用它,意味着任务将永久阻塞,直到事件发生。

坑 :在一个 CAN 总线通信任务中,我错误地使用了 portMAX_DELAY 来等待 CAN 接收邮箱。当 CAN 总线因物理断开而停止通信时,该任务便永远卡在 Blocked 态,再也无法执行任何看门狗喂狗操作,最终导致整个系统被硬件看门狗复位。

解法 :永远为阻塞 API 设置一个合理的超时时间。即使业务逻辑上“应该永远有数据”,也要设置一个 5000 / portTICK_PERIOD_MS (5秒)这样的兜底值。在超时分支中,执行错误处理(如记录日志、重启 CAN 外设、甚至 vTaskSuspend(NULL) 自身以隔离故障)。

6.2 vTaskSuspend() 与 vTaskResume() 的配对

在调试一个 Modbus TCP 从站时,我需要临时禁用其网络任务。我调用了 vTaskSuspend(xModbusTaskHandle) ,一切正常。但后来我发现,即使调用 xTaskResume(xModbusTaskHandle) ,该任务也再未被唤醒。

根因 : xTaskResume() 必须由一个 与被挂起任务不同优先级 的任务来调用。在我的代码中, xTaskResume() 是在 xModbusTaskHandle 自身的上下文中调用的(一个回调函数里)。这违反了 FreeRTOS 的设计约束。

解法 :始终确保 vTaskSuspend() 和 vTaskResume() 由不同的任务(或中断)执行。更安全的做法是使用信号量或事件组来实现逻辑上的“使能/禁用”,而不是物理上的挂起。

6.3 空闲任务钩子(Idle Hook)的滥用

configUSE_IDLE_HOOK 允许你注册一个函数,在空闲任务执行时被调用。我曾试图在这个钩子中执行一个低优先级的 Flash 擦除操作。

坑 :Flash 擦除是耗时操作(数十毫秒),而空闲任务是最低优先级。这意味着,只要系统中有任何其他任务处于就绪态,这个擦除操作就会被无数次打断,导致擦除失败或 Flash 损坏。

解法 :空闲钩子只适合执行 极其轻量 的操作,如低功耗模式进入( __WFI() )、简单的统计(如计算 CPU 利用率)。任何耗时操作,都应创建一个专用的、具有合适优先级的任务来完成。

这些教训的核心,是时刻铭记: FreeRTOS 的任务状态,是内核与你之间的一份契约。你承诺遵守它的规则(如不滥用 portMAX_DELAY ),它则回报你以确定性、可预测性和可维护性。

ESP32 FreeRTOS任务状态全解析:从就绪态到挂起态的深度理解与应用 本文深入解析了ESP32 FreeRTOS任务状态模型,从就绪态到挂起态的转换机制及应用技巧。通过详细的代码示例和状态转换表,帮助开发者掌握任务调度核心原理,优化嵌入式系统性能。特别针对ESP32平台的双核特性和低功耗模式提供了实用建议。 阅读详情

相关推荐

移植到STM32中FreeRTOS的业务创建与删除,挂起与恢复

FreeRTOS提供了多种任务管理API函数,包括动态和静态创建任务、删除任务、挂起和恢复任务等。动态创建任务使用xTaskCreate(),由系统自动管理内存;静态创建任务使用xTaskCreateStatic(),需手动分配内存,适用于需要精细控制的场景。任务删除使用vTaskDelete(),挂起和恢复任务分别使用vTaskSuspend()和vTaskResume()。启动调度器使用vTaskStartScheduler(),只需调用一次即可。任务创建后立即进入就绪态,由调度器调度运行。动态创建任务

2402_83192880的博客 983

【FreeRTOS(三)】任务状态

freeRTOS任务的状态有四种:运行、就绪、阻塞、挂起 运行态(runnnig):当任务正在运行,此时的状态被称为运行态,即CPU的使用权被这个任务占用; 挂起态(suspended):任务被暂时停止,通过调用挂起函数(vTaskSuspend())可以把指定任务挂起,任务挂起后暂时不会运行,只有调用恢复函数(xTaskResume())才可以退出挂起状态; 阻塞态(blocked):任务在等待信号量、消息队列、事件标准组、系统延时时,被称为阻塞态,如果等待的事件到了,就会自动退出阻塞态,准备运行; 就

K3169的博客 4208

[FREERTOS] 任务的创建、删除、调度与状态

抢占式调度用更直白的话来说就是:先执行优先级高的,然后执行优先级略低的,如果正在执行优先级低的,这个时候出现了优先级高的任务,就中断优先级低的任务去执行优先级高的任务,如果优先级最高的任务堵塞(比如使用了延时函数)就执行优先级略低的那个任务,等待优先级最高的任务堵塞结束后重新执行优先级最高的任务。·xTaskCreate()---------------------------动态方式创建任务。·vTaskDeletc()---------------------------删除任务。

weixin_63303786的博客 903

2.FreeRTOS系统任务调度简介及任务状态

FreeRTOS系统任务调度简介及任务状态

zywcxz的博客 3691

一种基于改进YOLOv8并与ORB-SLAM3融合的动态环境密集点云同步定位与建图(SLAM)方法

文章提出了一种基于改进 YOLOv8 与 ORB-SLAM3 融合的动态环境稠密点云 SLAM 方法,通过引入GSConv 卷积模块和VoVGSCSP 特征融合模块优化 YOLOv8 的轻量性,结合实时目标检测与语义分割技术去除动态特征点,显著提升了动态场景下的位姿估计精度。

2401_87345388的博客 1940

ESP32 DMX512与RDM协议栈开发指南

DMX512-A是专业灯光控制领域广泛采用的串行通信标准,基于RS-485物理层实现确定性单向数据传输;RDM(Remote Device Management)则在其基础上扩展双向、可寻址、可配置的智能管理能力,满足现代舞台设备远程固件升级、自动发现与参数配置需求。二者共用同一总线但时序与帧结构严格隔离,对硬件定时精度、中断响应延迟及电气隔离提出极高要求。在嵌入式实践中,ESP32凭借Wi-Fi/蓝牙连接能力与丰富外设资源,成为构建低成本智能DMX节点的理想平台。esp_dmx协议栈正是面向该场景的高性能

weixin_34945060的博客 316

【FreeRTOS篇】任务的状态

任务的状态

m0_66492811的博客 751

51单片机通过计时器实现倒计时

51单片机通过计时器操作两位8段阳极数码管,实现倒计时

一份耕耘,一分收获 3万+

FreeRTOS系统介绍——任务

FreeRTOS系统 任务介绍

sky的博客 1851

避坑指南:将大华官方Java SDK无缝迁移到SpringBoot 2.x项目的完整流程

本文详细介绍了如何将大华官方Java SDK无缝迁移到SpringBoot 2.x项目,重点解决内存管理、连接池设计和异步处理等核心问题。通过设备连接池化、异步非阻塞接口改造和内存管理优化,实现刷卡、刷脸等认证功能的高效整合,提升Web服务的稳定性和性能。

weixin_42557830的博客 417

ic后端学习笔记-floorplan

floorplan是布图规划,规划包括定义芯片的大小,定义芯片内各种模块、hard macros(硬核。比如RAM,ROM及其他IP模块等)、IO等的大小和位置,有时候还需要提前定义一些placement blockages,groups/regions等等,是后端设计的第一步,也是后续所有P&R工作的基础。floorplan与placement的区别是。

tfrofc的博客 5831

FreeRTOS任务状态

在FreeRTOS中,任务状态是一个重要的概念,它决定了每个任务当前所处的状态以及调度器如何管理任务的执行。

L1643319918的博客 1397

深信服技术认证“SCCA-C”划重点:信服云托管云

该认证包括云数据中心基础、云计算基础、云计算关键技术、云计算交付和运维体系、云计算技术商业应用、信服云托管云、深信服超融合HCI、深信服桌面云aDesk、深信服分布式存储EDS、深信服应用交付AD。学习并获得SCCA-C认证后,可以具备深信服产品功能原理与实践,能够根据业务需求,协助完成云产品方案落地与实施;能够根据业务需求,完成深信服云产品的部署和管理的基本工作。为帮助大家更加系统化地学习云计算知识,高效通过云计算工程师认证,深信服特推出“SCCA-C认证备考秘笈”,共十期内容。

sangfor_edu的博客 613

FreeRTOS任务状态切换(就绪、挂起、运行、删除、恢复、延时、阻塞)-小结

1. 任务的状态切换 FreeRTOS中任务的状态可分为:未创建态、就绪态、运行态、挂起态、延时态五种状态。 下图总结了一个任务可能出现的任务转换流程: a: 调用xTaskCreate()函数将新建一个任务,新建的任务会加入到就绪列表,若新建的任务的优先级足够高,调度器会立即将CPU资源分配给他,使它进入运行态。 b: 调度器检查就绪列表中优先级高的任务,并将CPU资源分配给它,使他进入运行态。 c: 运行态的任务可能创建/恢复了新的更高优先级的任务,或者因其操作了某事件(如发送了一个更高优

物联网老王的博客,公众号同名 1万+

Aip1640、TM1640驱动程序

本文介绍了AIP1640/TM1640驱动芯片的编程实现,主要包括硬件接口定义和通信协议函数。通过宏定义配置时钟(CLK)和数据线(DIO)的GPIO控制,实现了I2C协议的启动、停止、字节发送等基本操作。核心函数AIP1640_Disp_Drive()完成显示驱动流程:先发送0x40命令设置地址自增模式,再发送0xC0起始地址和16字节显示数据,最后设置亮度参数。文中还包含5微秒延时函数、显示数据缓冲区定义,以及LED和数码管(SMG)的位映射注释说明。该代码适用于控制LED数码管显示,支持亮度调节功能。

zqx的博客 1402

【即插即用模块】注意力篇 | ECCV 2024 | DPB:高低频动态分离+双提示融合,PSNR直冲40+!

本文提出了一种基于频率提示的图像恢复方法FPro,通过动态滤波器分离输入特征的高低频分量,并采用双提示块(DPB)进行频域专属提示生成与调制。DPB包含低频提示调制器(LPM)和高频提示调制器(HPM),分别处理不同频段信号,通过生成提示组件并融合解码器特征来优化恢复效果。实验表明,该方法在U型结构中表现优异,尤其适用于分割、底层视觉和多模态任务。模块通过动态无监督频率分离、频域专属提示生成和适配性融合机制,有效解决了传统方法对频域信息关注不足的问题,显著提升了图像恢复质量。

主要更新底层视觉(去噪、超分等)相关的科研内容,形式为【论文精读】+【论文复现】 147

FreeRTOS中任务状态的转换,任务挂起,任务删除

任务创建的时候默认优先级是22,这里需要再额外介绍一下各种任务的优先级的相关知识。介绍一下这个杀死任务的函数时如何实现的(主要是了解一下,可以帮我们来更好的进行)其实时不能自杀的,自杀就是将当前的这个任务放置到了我们的杀死的任务队列中来。任务函数中不能直接return,要想销毁的话必须手动调用一个函数,通过空闲任务来杀死这个任务的pcb,这个空闲任务的优先级是0。任务转换图(这个是重点,面试的时候经常问)空闲任务就是在检查这些任务列表。剑哥说这里面这段描述非常重要。任务创建时候的结构体。

cyz322649的博客 1635

VC中sqlite的使用

在VC中对sqlite的使用,实现数据库大批数据的快速插入,读取数据库中的数据在ListCtrl中显示

FreeRTOS任务状态机制:就绪、运行、阻塞与挂起深度解析

在嵌入式实时系统中,任务状态是操作系统实现确定性调度与资源高效分配的核心抽象。其本质是一组影响CPU时间片分配权的语义化标志位,遵循严格的有限状态机模型。FreeRTOS通过eReady(就绪态)、eRunning(运行态)、eBlocked(阻塞态)和eSuspended(挂起态)四大状态,构建起事件驱动、低功耗、高响应的多任务运行基础。这种状态机制不仅支撑vTaskDelay等API的协作式让权,更直接决定了任务能否参与调度、是否响应中断、以及如何协同硬件外设。理解状态流转逻辑,是掌握FreeRTOS实

motor的博客 1011

Statistics and Machine Learning Toolbox

Statistics and Machine Learning Toolbox(带书签),MATLAB统计学习工具

深度学习工具包 DeepLearningToolbox

深度学习工具包Deprecation notice.-----This toolbox is outdated and no longer maintained.There are much better tools available for deep learning than this toolbox, e.g. [Theano](http://deeplearning.net/software/theano/), [torch](http://torch.ch/) or [tensorflow](http://www.tensorflow.org/)I would suggest you use one of the tools mentioned above rather than use this toolbox.Best, Rasmus.DeepLearnToolbox================A Matlab toolbox for Deep Learning.Deep Learning is a new subfield of machine learning that focuses on learning deep hierarchical models of data.It is inspired by the human brain's apparent deep (layered, hierarchical) architecture.A good overview of the theory of Deep Learning theory is[Learning Deep Architectures for AI](http://www.iro.umontreal.ca/~bengioy/papers/ftml_book.pdf)For a more informal introduction, see the following videos by Geoffrey Hinton and Andrew Ng.* [The Next Generation of Neural Networks](http://www.youtube.com/watch?v=AyzOUbkUf3M) (Hinton, 2007)* [Recent Developments in Deep Learning](http://www.youtube.com/watch?v=VdIURAu1-aU) (Hinton, 2010)* [Unsupervised Feature Learning and Deep Learning](http://www.youtube.com/watch?v=ZmNOAtZIgIk) (Ng, 2011)If you use this toolbox in your research please cite [Prediction as a candidate for learning deep hierarchical models of data](http://www2.imm.dtu.dk/pubdb/views/publication_details.php?id=6284)```@MASTERSTHESIS\{IMM2012-06284, author = "R. B. Palm", title = "Prediction as a candidate for learning deep hierarchical models of data", year = "2012",}```Contact: rasmusbergpalm at gmail dot comDirectories included in the toolbox-----------------------------------`NN/` - A library for Feedforward Backpropagation Neural Networks`CNN/` - A library for Convolutional Neural Networks`DBN/` - A library for Deep Belief Networks`SAE/` - A library for Stacked Auto-Encoders`CAE/` - A library for Convolutional Auto-Encoders`util/` - Utility functions used by the libraries`data/` - Data used by the examples`tests/` - unit tests to verify toolbox is workingFor references on each library check REFS.mdSetup-----1. Download.2. addpath(genpath('DeepLearnToolbox'));Example: Deep Belief Network---------------------```matlabfunction test_example_DBNload mnist_uint8;train_x = double(train_x) / 255;test_x = double(test_x) / 255;train_y = double(train_y);test_y = double(test_y);%% ex1 train a 100 hidden unit RBM and visualize its weightsrand('state',0)dbn.sizes = [100];opts.numepochs = 1;opts.batchsize = 100;opts.momentum = 0;opts.alpha = 1;dbn = dbnsetup(dbn, train_x, opts);dbn = dbntrain(dbn, train_x, opts);figure; visualize(dbn.rbm{1}.W'); % Visualize the RBM weights%% ex2 train a 100-100 hidden unit DBN and use its weights to initialize a NNrand('state',0)%train dbndbn.sizes = [100 100];opts.numepochs = 1;opts.batchsize = 100;opts.momentum = 0;opts.alpha = 1;dbn = dbnsetup(dbn, train_x, opts);dbn = dbntrain(dbn, train_x, opts);%unfold dbn to nnnn = dbnunfoldtonn(dbn, 10);nn.activation_function = 'sigm';%train nnopts.numepochs = 1;opts.batchsize = 100;nn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.10, 'Too big error');```Example: Stacked Auto-Encoders---------------------```matlabfunction test_example_SAEload mnist_uint8;train_x = double(train_x)/255;test_x = double(test_x)/255;train_y = double(train_y);test_y = double(test_y);%% ex1 train a 100 hidden unit SDAE and use it to initialize a FFNN% Setup and train a stacked denoising autoencoder (SDAE)rand('state',0)sae = saesetup([784 100]);sae.ae{1}.activation_function = 'sigm';sae.ae{1}.learningRate = 1;sae.ae{1}.inputZeroMaskedFraction = 0.5;opts.numepochs = 1;opts.batchsize = 100;sae = saetrain(sae, train_x, opts);visualize(sae.ae{1}.W{1}(:,2:end)')% Use the SDAE to initialize a FFNNnn = nnsetup([784 100 10]);nn.activation_function = 'sigm';nn.learningRate = 1;nn.W{1} = sae.ae{1}.W{1};% Train the FFNNopts.numepochs = 1;opts.batchsize = 100;nn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.16, 'Too big error');```Example: Convolutional Neural Nets---------------------```matlabfunction test_example_CNNload mnist_uint8;train_x = double(reshape(train_x',28,28,60000))/255;test_x = double(reshape(test_x',28,28,10000))/255;train_y = double(train_y');test_y = double(test_y');%% ex1 Train a 6c-2s-12c-2s Convolutional neural network %will run 1 epoch in about 200 second and get around 11% error. %With 100 epochs you'll get around 1.2% errorrand('state',0)cnn.layers = { struct('type', 'i') %input layer struct('type', 'c', 'outputmaps', 6, 'kernelsize', 5) %convolution layer struct('type', 's', 'scale', 2) %sub sampling layer struct('type', 'c', 'outputmaps', 12, 'kernelsize', 5) %convolution layer struct('type', 's', 'scale', 2) %subsampling layer};cnn = cnnsetup(cnn, train_x, train_y);opts.alpha = 1;opts.batchsize = 50;opts.numepochs = 1;cnn = cnntrain(cnn, train_x, train_y, opts);[er, bad] = cnntest(cnn, test_x, test_y);%plot mean squared errorfigure; plot(cnn.rL);assert(er<0.12, 'Too big error');```Example: Neural Networks---------------------```matlabfunction test_example_NNload mnist_uint8;train_x = double(train_x) / 255;test_x = double(test_x) / 255;train_y = double(train_y);test_y = double(test_y);% normalize[train_x, mu, sigma] = zscore(train_x);test_x = normalize(test_x, mu, sigma);%% ex1 vanilla neural netrand('state',0)nn = nnsetup([784 100 10]);opts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samples[nn, L] = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.08, 'Too big error');%% ex2 neural net with L2 weight decayrand('state',0)nn = nnsetup([784 100 10]);nn.weightPenaltyL2 = 1e-4; % L2 weight decayopts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex3 neural net with dropoutrand('state',0)nn = nnsetup([784 100 10]);nn.dropoutFraction = 0.5; % Dropout fraction opts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex4 neural net with sigmoid activation functionrand('state',0)nn = nnsetup([784 100 10]);nn.activation_function = 'sigm'; % Sigmoid activation functionnn.learningRate = 1; % Sigm require a lower learning rateopts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex5 plotting functionalityrand('state',0)nn = nnsetup([784 20 10]);opts.numepochs = 5; % Number of full sweeps through datann.output = 'softmax'; % use softmax outputopts.batchsize = 1000; % Take a mean gradient step over this many samplesopts.plot = 1; % enable plottingnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex6 neural net with sigmoid activation and plotting of validation and training error% split training data into training and validation datavx = train_x(1:10000,:);tx = train_x(10001:end,:);vy = train_y(1:10000,:);ty = train_y(10001:end,:);rand('state',0)nn = nnsetup([784 20 10]); nn.output = 'softmax'; % use softmax outputopts.numepochs = 5; % Number of full sweeps through dataopts.batchsize = 1000; % Take a mean gradient step over this many samplesopts.plot = 1; % enable plottingnn = nntrain(nn, tx, ty, opts, vx, vy); % nntrain takes validation set as last two arguments (optionally)[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');```[![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/rasmusbergpalm/deeplearntoolbox/trend.png)](https://bitdeli.com/free "Bitdeli Badge")

FreeRTOS任务的运行状态和核心原理

FreeRTOS的任务(Task) 是其调度的基本单位,也是构建整个应用程序的基石。本文将从核心概念到设计实践,系统性地详细介绍。

mftang的博客 1688

FreeRTOS任务管理(创建、删除、挂起、恢复)

3、在FreeRTOS中,创建任务所需要的内存需要在空闲任务中释放,如果用户在FreeRTOS中调用了这个函数的话,一定要让空闲任务有执行的机会,否则这部分内存无法释放,另外,创建的这个任务在使用中申请了动态内存,这个内存不会因任务被删除而释放,这一点要注意,一定要在删除任务前将内存释放。每个任务在自己的环境中运行,在任何时刻,只有一个任务得到运行,FreeRTOS调度器决定运行哪个任务,调度器会不断的启动,停止每个任务,宏观上看每个任务都在同时运行。

2301_77866762的博客 1753

FreeRTOS的任务详解

详细讲解freeRTOS的任务

qq_44318582的博客 4214

FreeRTOS 任务的挂起与恢复 任务状态查询

本篇文章详细解析了 FreeRTOS 任务挂起与恢复机制,包括 vTaskSuspend()、vTaskResume()、xTaskResumeFromISR() 以及调度器挂起 vTaskSuspendAll() 和 xTaskResumeAll() 的应用场景。结合 STM32 平台,讲解了如何配置 FreeRTOSConfig.h 以启用相关 API,并详细分析了 vTaskList() 任务状态监视的输出结果。此外,文中提供了 完整代码示例,展示了一个小实验来掌握任务调度控制,帮助开发者更深入理解

weixin_52342399的博客 1373

【STM32之FreeRTOS(三)】任务的调度与状态

任务调度就是调度器使用相关的调度算法来决定当前需要执行的哪个任务。FreeRTOS中开启任务调度的函数是,但在 CubeMX 中被封装为。

ArsenLupin的博客 2938

FreeRTOS任务状态机制与调度原理详解

在嵌入式实时系统中,任务状态是理解多任务并发行为的核心抽象。它源于操作系统对CPU时间资源的结构化管理,通过运行态、就绪态、阻塞态等确定性状态迁移,实现任务间的非抢占或抢占式切换。其底层依赖SysTick中断驱动的时间片轮转与优先级调度算法,技术价值在于保障响应确定性、降低功耗并提升资源复用率。典型应用场景包括传感器数据采集、通信协议栈处理及低功耗物联网终端。本文深入解析FreeRTOS中eRunning、eBlocked等五种任务状态的语义差异、流转条件及调试验证方法,尤其聚焦vTaskDelay与HAL

motor的博客 906

FreeRTOS任务管理、任务状态、任务优先级

任务状态 运行态 任务分为运行态和非运行态两种,在处理器运行过程中,任一时刻只能有一个任务处于运行态,非运行态又包含很多子态。 当任务处于非运行态时,任务处于休眠态(dormant),它的状态被保存,以便下一次调度程序决定它应该进入运行态。当一个任务恢复执行时,它从最后一次离开运行态之前将要执行的指令开始执行。 FreeRTOS调度器是唯一可以切换任务状态的实体。 非运行态 阻塞态 等待事件的任务处于阻塞态,任务会因等待两种不同的事件进入阻塞态 时间相关事件-此类事件要么是一个延时的相对时间,要么是一个

Do Always Learing的博客 2305

【FreeRTOS】FreeRTOS 任务管理

FreeRTOS 任务管理梳理,总结

qq_34142812的博客 1181
上一篇: 为什么嵌入式开发需要RTOS:从裸机到FreeRTOS的工程跃迁
下一篇: STM32驱动L298N控制直流电机:PWM原理与H桥实践
cuda7parallel
博客等级 码龄1天 0粉丝 1273原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值