Day 3:优先级与抢占

3.1 实验目标

理解 FreeRTOS 的抢占式调度同优先级时间片轮转。通过修改任务优先级和故意去掉 osDelay 观察"饿死"现象。

3.2 代码修改

在 Day 2 双任务基础上,新增 Task_C 控制 LED2(PA7),并调整优先级:

freertos.c Init 区域

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
const osThreadAttr_t attr_a = {
    .name = "task_a",
    .priority = osPriorityNormal,        /* 串口打印任务 */
    .stack_size = 512 * 4
};
const osThreadAttr_t attr_b = {
    .name = "task_b",
    .priority = osPriorityNormal,        /* LED1 心跳 */
    .stack_size = 512 * 4
};
const osThreadAttr_t attr_c = {
    .name = "task_c",
    .priority = osPriorityAboveNormal,   /* LED2 高优先级 */
    .stack_size = 512 * 4
};

_task_a_id = osThreadNew(_task_a_handler, NULL, &attr_a);
_task_b_id = osThreadNew(_task_b_handler, NULL, &attr_b);
_task_c_id = osThreadNew(_task_c_handler, NULL, &attr_c);

Task_C 实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
static void _task_c_handler(void *argument)
{
    (void)argument;

    for (;;)
    {
        HAL_GPIO_TogglePin(LED2_GPIO_Port, LED2_Pin);
        osDelay(200);
    }
}

3.3 观察到的关键现象

现象一:正常情况(都有 osDelay)

  • Task_A(Normal):每 500ms 串口打印
  • Task_B(Normal):每 1000ms LED1 闪烁
  • Task_C(AboveNormal):每 200ms LED2 闪烁
  • 三者互不干扰,同时运行

现象二:Task_C 同优先级 + 去掉 osDelay(忙等)

临时去掉 Task_C 的 osDelay(200)

1
2
3
4
5
for (;;)
{
    HAL_GPIO_TogglePin(LED2_GPIO_Port, LED2_Pin);
    /* 没有 osDelay */
}

观察结果

  • LED2 看起来"常亮"(实际翻转频率太高,肉眼无法分辨)
  • 但 Task_A 和 Task_B 仍然正常运行!

原因:FreeRTOS 默认开启 Time Slicing(时间片轮转)。同优先级(或即使 Task_C 是 AboveNormal,但实验发现如果把它改回 Normal)任务之间,调度器每个 tick(1ms)会强制切换,即使任务没有主动阻塞。所以 Task_C 每 1ms 被踢下去,Task_A/B 能分到 CPU。

现象三:Task_C 高优先级(AboveNormal)+ 去掉 osDelay

观察结果

  • LED2 常亮
  • LED1 停止闪烁,串口停止输出

原因:Task_C 优先级 AboveNormal > Task_A/B 的 Normal。Task_C 没有 osDelay,永远处于就绪/运行态。调度器每次检查就绪任务,它都是优先级最高的,独占 CPU。低优先级任务永远轮不到——这就是饿死(Starvation)

3.4 核心概念总结

场景结果原因
同优先级 + 有 osDelay正常轮流执行主动阻塞释放 CPU
同优先级 + 无 osDelay不会饿死,但翻转极快时间片轮转强制每 tick 切换
高优先级 + 有 osDelay正常执行,不阻塞低优先级阻塞期间 CPU 释放给低优先级
高优先级 + 无 osDelay饿死低优先级调度器永远选最高优先级就绪任务

重要结论osDelay() 是任务"让出 CPU"的灵魂。没有它,高优先级任务会饿死低优先级任务;同优先级下虽然有时间片轮转保护,但也会浪费 CPU 做无意义的空转。


Day 4:任务挂起与恢复

4.1 实验目标

通过串口命令控制任务的暂停和恢复。新增 Task_CMD 作为命令解析器。

4.2 代码实现

freertos.c Init 区域

1
2
3
4
5
6
const osThreadAttr_t attr_cmd = {
    .name = "task_cmd",
    .priority = osPriorityNormal,
    .stack_size = 512 * 4
};
_task_cmd_id = osThreadNew(_task_cmd_handler, NULL, &attr_cmd);

Task_CMD 实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
static void _task_cmd_handler(void *argument)
{
    (void)argument;
    uint8_t rx_byte = 0;    /* 必须初始化,否则超时后值不确定 */

    for (;;)
    {
        if (HAL_UART_Receive(&huart1, &rx_byte, 1, 100) == HAL_OK)
        {
            if (rx_byte == 's')
            {
                osThreadSuspend(_task_b_id);   /* 暂停 LED1 */
            }
            else if (rx_byte == 'r')
            {
                osThreadResume(_task_b_id);    /* 恢复 LED1 */
            }
        }
        osDelay(10);
    }
}

4.3 踩坑记录

坑 1:HAL_UART_Receive 超时后判断未接收到的数据

错误写法

1
2
3
uint8_t rx_byte;    /* 未初始化,值随机 */
HAL_UART_Receive(&huart1, &rx_byte, 1, 100);  /* 超时后 rx_byte 内容不确定 */
if (rx_byte == 's') ...  /* 可能误触发! */

正确写法

1
2
3
4
5
uint8_t rx_byte = 0;
if (HAL_UART_Receive(&huart1, &rx_byte, 1, 100) == HAL_OK)
{
    /* 只有返回 HAL_OK 才说明真的收到了数据 */
}

HAL_UART_Receive 的第四个参数是超时时间(ms)。如果 100ms 内没收到数据,返回 HAL_TIMEOUT,此时 rx_byte 内容不会被修改(但初始值随机的话就会误判)。

4.4 验证现象

  • 串口发送 's' → LED1 停止闪烁(被挂起)
  • 串口发送 'r' → LED1 恢复闪烁(被恢复)
  • Task_A 串口打印和 Task_C LED2 闪烁不受影响

Day 5:空闲任务钩子与独立看门狗(IWDG)

5.1 实验目标

利用 FreeRTOS 的空闲任务钩子(Idle Hook)喂独立看门狗(IWDG)。如果任何用户任务进入死循环(忙等),空闲任务无法执行,看门狗超时复位系统。

5.2 CubeMX 配置

  • IWDG → 勾选 Activated
  • Prescaler:/32
  • Reload Value:1000
  • 计算超时:LSI 约 32kHz,32k/32 = 1kHz,1000/1k = 约 1 秒

重新 Generate Code

5.3 代码实现

FreeRTOSConfig.h

1
#define configUSE_IDLE_HOOK    1

freertos.c Application 区域

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
void vApplicationIdleHook(void)
{
    HAL_IWDG_Refresh(&hiwdg);

    /* 调试用:确认空闲任务在运行 */
    static uint32_t cnt = 0;
    if (++cnt % 200 == 0)
    {
        HAL_GPIO_TogglePin(LED2_GPIO_Port, LED2_Pin);
    }
}

5.4 踩坑记录

坑 1:死循环测试时"看不出复位"

把 Task_A 改成死循环:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
static void _task_a_handler(void *argument)
{
    (void)argument;
    for (;;)
    {
        const char msg[] = "TaskA: Running\r\n";
        HAL_UART_Transmit(&huart1, (uint8_t *)msg, strlen(msg), 100);
        /* 去掉 osDelay */
    }
}

观察现象

  • 串口疯狂输出 TaskA: Running
  • LED1 和 LED2 看起来常亮或微亮
  • 系统似乎"没有复位"

原因:系统确实在反复复位(约每秒一次),但复位后 Task_A 立刻又开始疯狂输出,肉眼看起来像是"一直在跑"。

验证方法:在 main.cHAL_Init() 之后添加复位标志检测:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
/* USER CODE BEGIN Init */
if (__HAL_RCC_GET_FLAG(RCC_FLAG_IWDGRST))
{
    const char msg[] = "\r\n>>> IWDG RESET DETECTED! <<<\r\n";
    HAL_UART_Transmit(&huart1, (uint8_t*)msg, strlen(msg), 200);

    /* LED2 闪烁 3 次 */
    for (int i = 0; i < 6; i++)
    {
        HAL_GPIO_TogglePin(LED2_GPIO_Port, LED2_Pin);
        HAL_Delay(300);
    }
    __HAL_RCC_CLEAR_RESET_FLAGS();
}
/* USER CODE END Init */

添加后观察到:

  • 串口每隔约 1 秒打印 >>> IWDG RESET DETECTED! <<<
  • LED2 每 1 秒闪烁 3 次
  • 证实了系统在看门狗控制下反复复位

5.5 核心概念

概念说明
空闲任务FreeRTOS 自动创建的最低优先级任务(优先级 0)。当所有用户任务都阻塞时,CPU 执行它。
空闲任务钩子在空闲任务每次执行时调用的用户函数。不能阻塞,不能调用会引起任务切换的 API。
IWDG 独立看门狗使用独立低速时钟(LSI),不依赖主时钟。即使主程序跑飞,只要 LSI 还在,看门狗就会超时复位。
为什么把喂狗放在空闲任务?只要有任何用户任务在忙等(死循环),空闲任务永远轮不到,看门狗就会超时。这是检测"系统是否还能正常调度"的最简单方法。

Week 1 总结

天数内容学习目的
Day 1CubeMX 环境搭建、时钟配置、代码生成基本理解 FreeRTOS 工程的创建
Day 2双任务创建、串口+LED同时运行理解任务创建osDelay 阻塞
Day 3优先级抢占、时间片轮转、饿死实验理解抢占式调度核心机制
Day 4任务挂起/恢复、串口命令控制掌握挂起恢复机制
Day 5空闲任务钩子、IWDG看门狗、复位检测理解看门狗与调度器的关系