Day 1:环境搭建与工程创建

1.1 CubeMX 配置清单

新建工程,选择 STM32F407VET6,逐项配置:

配置项设置备注
RCC → HSECrystal/Ceramic Resonator外部 8MHz 晶振
SYS → DebugSerial Wire必须选,否则只能烧录一次
SYS → Timebase SourceTIM6把 SysTick 留给 FreeRTOS
Clock → HCLK168 MHz输入 168 回车,自动配 PLL
USART1Asynchronous, 115200PA9/PA10 默认
GPIOPA6 = Output选择一个 LED信号 输出引脚
FREERTOS → InterfaceCMSIS_V2V2 API 更完整
FREERTOS → Heapheap_4通用,支持动态分配释放
FREERTOS → Total Heap20480 (20KB)F407 有 192KB SRAM,充裕
Project ManagerToolchain = STM32CubeIDE在Cube上开发

Project Manager 额外勾选

  • Generate peripheral initialization as a pair of .c/.h files
  • Keep User Code when re-generating

1.2 代码生成失败排查

点击 Project → Generate Code 时,如果窗口闪一下消失、代码没生成,检查:

  1. 先按 Ctrl+S 保存 .ioc 文件(最常见原因)
  2. 工程路径不能有中文或空格
  3. 编码问题:右键工程 → Properties → Resource → Text file encoding → 改为 UTF-8
  4. 固件包缺失:如果提示 “necessary firmware package is missing”,需要下载 STM32CubeF4 固件包。可以离线从 ST 官网下载 zip,解压到 C:\Users\xxx\STM32Cube\Repository\STM32Cube_FW_F4_V1.28.3

1.3 验证硬件

生成代码后,先在 main.cwhile(1) 里加裸机测试:

1
2
HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin);
HAL_Delay(500);

编译烧录,确认 LED 闪烁。然后删掉这两行,准备上 FreeRTOS。


Day 2:第一个 FreeRTOS 多任务程序

2.1 核心代码

freertos.c 的三个 USER CODE 区域添加代码:

① Includes 区域

1
2
3
#include "usart.h"
#include "gpio.h"
#include <string.h>

② Variables 区域

1
2
3
4
static void _task_a_handler(void *argument);
static void _task_b_handler(void *argument);
static osThreadId_t _task_a_id = NULL;
static osThreadId_t _task_b_id = NULL;

③ Init 区域MX_FREERTOS_Init 函数内):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
const osThreadAttr_t attr_a = {
    .name = "task_a",
    .priority = osPriorityNormal,
    .stack_size = 512 * 4  // 512 words = 2048 bytes
};
const osThreadAttr_t attr_b = {
    .name = "task_b",
    .priority = osPriorityNormal,
    .stack_size = 512 * 4
};

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

if (_task_a_id == NULL || _task_b_id == NULL) {
    Error_Handler();
}

④ Application 区域(文件最底部):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
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(500);  // 500 ticks = 500ms
    }
}

static void _task_b_handler(void *argument)
{
    (void)argument;
    for (;;) {
        HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin);
        osDelay(1000);
    }
}

2.2 确认 main.c 代码

确认 CubeMX 生成的 main.c 里已经包含了初始化调用代码,否则 FreeRTOS 将不会启动。 一般来说,生成代码时是会自己生成好初始化代码的,如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
int main(void)
{
    HAL_Init();
    SystemClock_Config();

    MX_GPIO_Init();
    MX_USART1_UART_Init();
    MX_FREERTOS_Init();
    osKernelStart();        // 启动调度器,这行之后不会返回

    while (1) { }
}

同时 main.c 要包含头文件

1
2
3
4
#include "gpio.h"
#include "usart.h"
#include "freertos.h"
#include "cmsis_os.h"

2.3 学习概念

2.3.1 芯片启动后, main() 里发生了什么?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
int main(void)
{
    HAL_Init();                    // ① 复位外设、初始化HAL
    SystemClock_Config();          // ② 配置168MHz时钟
    MX_GPIO_Init();                // ③ 初始化PA6为输出(LED)
    MX_USART1_UART_Init();         // ④ 初始化串口115200
    
    MX_FREERTOS_Init();            // ⑤ 创建任务(关键!)
    osKernelStart();               // ⑥ 启动调度器(关键!)
    
    while (1) { }                  // ⑦ 永远不会执行到这里
}

osKernelStart() 启动调度器后,调度器通过抢占式优先级调度管理 CPU。任何时刻,它只执行就绪态中优先级最高的任务。同优先级任务之间采用时间片轮转。

2.3.2 什么是“任务”?

任务本质上就是一个无限循环的C函数,但比普通函数多一个概念:它会堵塞。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
static void _task_a_handler(void *argument)
{
    (void)argument;    // 消除编译器警告:参数不用
    
    for (;;) {         // 无限循环,任务永不退出
        // 执行任务
        HAL_UART_Transmit(...);
        
        // 阻塞自己,让出CPU
        osDelay(500);  // 关键代码,没有它,其他任务将永远不会执行到
    }
}

为什么 osDelay() 是关键代码?

因为FreeRTOS是 抢占式调度 。调度器只看一件事:当前哪个就绪任务的优先级最高。但如果最高优先级的任务目前在 osDelay(), 那它就会变成“阻塞态”(Blocked),调度器就会跳过它,去找下一个就绪的高优先级任务。

FreeRTOS默认开启 Time Slicing(时间片轮转), 同优先级之间,调度器默认每个tick(默认1ms)会强制切换一次,不能论任务是否主动阻塞。

如果没有osDelay,那么任务永远处于“运行态”或者“就绪态”,调度器每次检查它时它都在就绪,CPU被它独占了,那其它任务将永远没有出头之日了,这就是饿死(Starvation)

2.3.3 任务状态

任务一共有五个状态,分别是就绪运行阻塞挂起删除

状态英文定义怎么进入怎么退出
就绪Ready可以运行,等调度器选我创建、阻塞到期、挂起恢复被调度器选中执行
运行Running正在 CPU 上执行调度器选中被抢占(回就绪)、主动阻塞、被挂起
阻塞Blocked我在等某件事(时间/事件)osDelay()、信号量等待、队列接收延时到期、事件到达
挂起Suspended我被外部强制暂停了osThreadSuspend()osThreadResume()
删除Deleted我死了,等空闲任务收尸vTaskDelete()空闲任务清理内存后彻底消失

2.3.3 创建任务时,那些参数是什么意思?

1
2
3
4
5
const osThreadAttr_t attr_a = {
	.name = "task_a",                // 调试时看到的名字
	.priority = osPriorityNormal,    // 优先级,数字越大优先级越高
	.stack_size = 512 * 4            // 栈大小
}

栈大小为什么是 512 * 4 ?

FreeRTOS 中,栈的单位是 word(字) , ARM Cortex-M 是 32 位架构, 1 word = 4 bytes。所以 512 * 4 也就是 512 个字, 2048个字节。 这个栈是任务的私有栈,保存着:

  • 局部变量
  • 函数调用时的返回地址
  • 中断发生时自动保存的寄存器 如果栈设的太小,任务里函数调用层太深,或者局部函数太大,就会发生栈溢出,触发HardFault

2.3.4 优先级数值(FreeRTOS 内部)

CMSIS-RTOS V2 封装了一层,但底层 FreeRTOS 的优先级是数字:

  • osPriorityIdle = 1

  • osPriorityLow = 8

  • osPriorityBelowNormal = 16

  • osPriorityNormal = 24

  • osPriorityAboveNormal = 32

  • osPriorityHigh = 40

  • osPriorityRealtime = 48

  • osPriorityError = 错误

数字越大,优先级越高osPriorityAboveNormal (32) > osPriorityNormal (24),所以调度器优先执行 AboveNormal 的任务。

2.4 调试器配置

我用的国产 J-Link OB Mini,CubeIDE 默认的 GDB 配置会报 localhost:2331 超时。

排查步骤

  1. 确认接线:J-Link CLK→板子 SWCLK,DIO→SWDIO,GND→GND,3.3V→3.3V。(后面发现就是接线问题…我买的板子丝印有点问题)
  2. 设备管理器确认识别到 J-Link 设备
  3. 安装 SEGGER J-Link Software Pack
  4. 用 J-Link Commander 测试连接:connectSTM32F407VESWD4000
  5. 如果报 Failed to initialize DAP,尝试降速到 speed 100

下载方式

  • 方式一:J-Flash 直接下载 hex
  • 方式二:CubeIDE Run Configurations 里调试探头选 SEGGER J-LINK,手动启动 J-Link GDB Server 后再点 Debug

2.5 验证现象

烧录后:

  • 串口助手(115200-8-1-N)每 500ms 收到 TaskA: Running
  • LED(PA6)以 1Hz 频率闪烁
  • 两个现象同时出现,说明调度器正常工作了