在STM32F103上进行FreeRTOS移植和配置(STM32CubeIDE)
个人博客:blogs.wurp.top
一. 创建新工程 (以STM32CubeIDE为例)
-
核心组件:
- 硬件: STM32F103开发板 (如常见的"Blue Pill"板)
- 开发环境:
- 选项1: Keil MDK-ARM (uVision) + STM32F1xx Device Family Pack (DFP)
- 选项2: STM32CubeIDE (推荐,集成度更高)
- 软件:
- FreeRTOS源码 (从官网或STM32CubeF1库获取)
- STM32标准外设库 (StdPeriph) 或 HAL库 (推荐使用CubeIDE自带的HAL)
-
打开STM32CubeIDE。
-
File -> New -> STM32 Project
。 -
在
Target Selection
窗口:- 在
Part Number
搜索框输入STM32F103C8
(或你的具体型号,如F103RC)。 - 选择正确的型号 (例如
STM32F103C8Tx
)。 - 点击
Next
。
- 在
-
输入项目名称 (如
FreeRTOS_Demo
),选择项目位置。 -
点击
Finish
。IDE会自动生成基于HAL库的初始化代码框架。
二. 启用并配置FreeRTOS
- 在
Project Explorer
中双击打开*.ioc
文件 (CubeMX配置文件)。 - 转到
Middleware
选项卡。 - 在左侧列表中选择
FREERTOS
。 - 在
Mode
下拉菜单中,选择Interface
为CMSIS_V2
(这是FreeRTOS的CMSIS-RTOS API v2封装层,标准化且易用)。 - 配置内核参数 (关键步骤!):
CONFIG
子选项卡:Kernel settings
:TICK_RATE_HZ
: 系统时钟节拍频率。通常设置为1000
(1ms)。注意: 这个值必须与SysTick
中断频率匹配(在Clock Configuration
中设置,通常HCLK / 1000)。MAX_PRIORITIES
: 最大任务优先级数。默认56
通常足够,资源紧张时可减小(如7
)。MINIMAL_STACK_SIZE
: 最小任务栈大小(字为单位,1字=4字节)。默认128
字(512字节)。重要: 根据任务复杂度调整!简单任务可能够用,复杂任务(大量局部变量、函数调用深)需要增加(如256
或512
字)。栈溢出是常见错误源!TOTAL_HEAP_SIZE
: FreeRTOS堆总大小(字节)。这是关键资源! STM32F103C8只有20KB RAM,需谨慎分配。初始值10240
(10KB)是常用起点。观察后续应用使用情况调整(使用xPortGetFreeHeapSize()
或uxTaskGetStackHighWaterMark()
监控)。Memory management scheme
: 内存分配方案。推荐heap_4
(碎片管理较好)。heap_1
或heap_2
更简单但碎片严重。
Include parameters
: 按需启用API(如vTaskDelayUntil()
,Task notifications
等),默认通常够用。
Tasks and Queues
子选项卡 (可选):可以在这里图形化创建初始任务(设置名称、优先级、栈大小、入口函数等)。对于学习,建议在代码中手动创建。Timers and Semaphores
子选项卡 (可选):图形化创建软件定时器、信号量、互斥量、队列等。同样建议在代码中学习创建。
- 配置时钟 (SysTick):
- 转到
Clock Configuration
选项卡。 - 确保
HCLK
(AHB总线时钟) 是你期望的主频 (如72MHz)。 - 确认
SysTick
的时钟源是HCLK
(通常默认是)。 - 关键: 确保
SysTick
中断频率等于你在FreeRTOS中设置的TICK_RATE_HZ
。FreeRTOS依赖SysTick作为时基。例如,HCLK=72MHz,TICK_RATE_HZ=1000,则SysTick Reload Value应设置为72000
(72,000,000 / 1000 = 72,000)。
- 转到
- 保存配置: 点击
File -> Save
或工具栏的保存图标。CubeIDE会自动生成FreeRTOS配置代码和初始化代码。
三. 理解自动生成的FreeRTOS相关代码
保存.ioc
后,CubeIDE会自动更新工程:
Core/Src/freertos.c
: 包含MX_FREERTOS_Init()
函数。如果你在CubeMX中图形化创建了任务/信号量等,它们的创建代码会在这里生成。这是FreeRTOS对象的初始化入口。Core/Inc/FreeRTOSConfig.h
: 极其重要! 这是FreeRTOS的主要配置文件。它定义了:- 你在CubeMX中设置的所有参数 (
configTICK_RATE_HZ
,configTOTAL_HEAP_SIZE
,configMINIMAL_STACK_SIZE
,configMAX_PRIORITIES
,configUSE_PREEMPTION
等)。 - 硬件相关设置 (如
configCPU_CLOCK_HZ
,configSYSTICK_CLOCK_HZ
)。 - 启用的API函数 (
configUSE_MUTEXES
,configUSE_RECURSIVE_MUTEXES
,configUSE_COUNTING_SEMAPHORES
,configUSE_QUEUES
等)。 - 内存对齐 (
configTOTAL_HEAP_SIZE
)。 - 钩子函数 (
configUSE_IDLE_HOOK
,configUSE_TICK_HOOK
等)。 - 移植层定义:
configKERNEL_INTERRUPT_PRIORITY
(通常设置为15
,即最低优先级,确保FreeRTOS API调用不会屏蔽其他中断),configMAX_SYSCALL_INTERRUPT_PRIORITY
(通常设置为5
,高于此优先级的中断不会被FreeRTOS管理,也不能调用FreeRTOS API)。STM32F103的优先级数值越小优先级越高 (0最高, 15最低)。
- 你在CubeMX中设置的所有参数 (
Middlewares/Third_Party/FreeRTOS/Source/
: 存放FreeRTOS内核源码 (tasks.c
,queue.c
,list.c
,timers.c
等) 和内存管理方案 (heap_x.c
)。Middlewares/Third_Party/FreeRTOS/Source/portable/[Compiler]/ARM_CM3/
: 包含针对Cortex-M3架构和特定编译器 (如GCC, Keil) 的移植层代码。最关键的文件是port.c
(实现任务切换、SysTick中断处理、PendSV中断处理等) 和portmacro.h
(定义数据类型、关键宏、内联汇编等)。
四. 编写应用程序代码 (创建任务)
通常在Core/Src/main.c
的main()
函数中或MX_FREERTOS_Init()
之后创建任务。
示例:创建两个简单任务 (LED闪烁 & 串口打印)
/* Private includes ----------------------------------------------------------*/
#include "FreeRTOS.h"
#include "task.h"
#include "main.h"
#include "gpio.h" // 确保包含HAL GPIO头文件
#include "usart.h" // 如果使用串口/* Private function prototypes -----------------------------------------------*/
void vTaskLED(void *pvParameters);
void vTaskPrint(void *pvParameters);int main(void) {HAL_Init();SystemClock_Config(); // CubeIDE自动生成MX_GPIO_Init(); // CubeIDE自动生成MX_USART1_UART_Init(); // 如果使用串口,CubeIDE自动生成MX_FREERTOS_Init(); // **重要!** 初始化FreeRTOS对象(任务/队列等,如果有图形化创建)和内核。这个函数会调用vTaskStartScheduler()。/* 如果MX_FREERTOS_Init()里没有启动调度器,或者你想在它之后创建任务,需要手动调用vTaskStartScheduler() */// vTaskStartScheduler();/* 正常情况下,vTaskStartScheduler() 永远不会返回 */while (1);
}/* 任务1:闪烁LED */
void vTaskLED(void *pvParameters) {(void)pvParameters; // 防止未使用参数警告const TickType_t xDelay500ms = pdMS_TO_TICKS(500); // 将毫秒转换为节拍数for (;;) {HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13); // 假设LED在PC13vTaskDelay(xDelay500ms); // 阻塞延时500ms,让出CPU}
}/* 任务2:串口打印 */
void vTaskPrint(void *pvParameters) {(void)pvParameters;const TickType_t xDelay1000ms = pdMS_TO_TICKS(1000);char msg[] = "Hello from FreeRTOS!\r\n";for (;;) {HAL_UART_Transmit(&huart1, (uint8_t*)msg, strlen(msg), HAL_MAX_DELAY); // 假设使用USART1vTaskDelay(xDelay1000ms); // 阻塞延时1秒}
}/* 在合适的地方创建任务 (例如在MX_FREERTOS_Init()之前或在MX_FREERTOS_Init()里面) */
void MX_FREERTOS_Init(void) {/* 创建LED任务 */if (xTaskCreate(vTaskLED, /* 任务函数指针 */"LED Task", /* 任务名称 (字符串) */128, /* 栈深度 (单位:字,4字节/字) - 根据需求调整! */NULL, /* 传递给任务的参数 */2, /* 任务优先级 (0是最低,configMAX_PRIORITIES-1是最高) */NULL /* 任务句柄 (用于后续引用任务,可设为NULL) */) != pdPASS) {/* 任务创建失败!处理错误 (例如循环或挂起) */Error_Handler();}/* 创建打印任务 */if (xTaskCreate(vTaskPrint,"Print Task",256, /* 串口任务可能需要稍大一点的栈 */NULL,1, /* 优先级低于LED任务 */NULL) != pdPASS) {Error_Handler();}/* FreeRTOS内核启动 */vTaskStartScheduler(); // **核心!启动任务调度器,永不返回(除非出错)**
}
五. 编译与下载
- 点击STM32CubeIDE工具栏上的
Build
(锤子图标) 编译项目。 - 连接开发板。
- 点击
Debug
(虫子图标) 或Run
(播放图标) 将程序下载到目标板并启动调试/运行。
六. 调试与监控
- 调试器 (ST-Link, J-Link): 单步执行、设置断点、查看变量、寄存器、内存、调用栈。FreeRTOS感知调试器能显示任务列表、队列、信号量状态。
- 串口输出: 通过
vTaskPrint
任务输出状态信息。 - LED指示: 观察
vTaskLED
任务的行为。 - FreeRTOS API 监控:
uxTaskGetStackHighWaterMark(TaskHandle_t xTask)
: 获取任务栈的历史最小剩余空间(高水位线),强烈推荐用于检测栈是否设置过小。值接近0表示危险。xPortGetFreeHeapSize()
: 获取当前剩余堆大小,监控内存使用和碎片。vTaskList(char *pcWriteBuffer)
: (需要启用configUSE_TRACE_FACILITY
和configUSE_STATS_FORMATTING_FUNCTIONS
) 将任务状态信息格式化为字符串输出(任务名、状态、优先级、栈高水位线)。非常有用!
七. 关键注意事项与常见问题
-
栈大小 (
Stack Size
):- 这是最常见的问题!任务栈溢出会导致难以预测的崩溃(覆盖其他内存)。
- 初始值
MINIMAL_STACK_SIZE
通常不够用于实际任务。 - 使用
uxTaskGetStackHighWaterMark()
在任务运行时监控栈使用。目标是在任务生命周期内保持高水位线有合理的余量(例如>100字节)。 - 复杂的函数调用、大的局部变量数组、递归都会消耗大量栈空间。
- 如果使用
printf
等库函数,它们内部使用的栈可能很大。
-
堆大小 (
TOTAL_HEAP_SIZE
):- STM32F103 RAM有限 (20KB for C8, 64KB for RC)。
- 分配的堆需要容纳:所有任务栈 + 任务控制块 (TCB) + 动态创建的对象 (队列、信号量、软件定时器、任务通知、动态分配内存)。
- 监控
xPortGetFreeHeapSize()
。如果堆耗尽,创建对象会失败。 - 在资源紧张的F103上,尽量静态分配 (在CubeMX中图形化创建或使用
xTaskCreateStatic()
)。
-
中断优先级 (
configMAX_SYSCALL_INTERRUPT_PRIORITY
):- 理解: FreeRTOS需要管理一些中断以进行任务切换(主要是PendSV)和提供API(如
xQueueSendFromISR
)。 configMAX_SYSCALL_INTERRUPT_PRIORITY
定义了一个中断优先级阈值。- 高于此阈值的中断:不能被FreeRTOS延迟,绝对禁止调用任何可能导致上下文切换的FreeRTOS API (如
xQueueSend
,xSemaphoreGive
,vTaskDelay
等)。只能调用带FromISR
后缀的API (如xQueueSendFromISR
,xSemaphoreGiveFromISR
)。 - 低于或等于此阈值的中断:可以被FreeRTOS延迟,可以调用FreeRTOS API(包括带
FromISR
和不带FromISR
的,但在ISR中应始终使用FromISR
版本)。
- 高于此阈值的中断:不能被FreeRTOS延迟,绝对禁止调用任何可能导致上下文切换的FreeRTOS API (如
- STM32F103设置: 通常设
configMAX_SYSCALL_INTERRUPT_PRIORITY = 5
(数值优先级),configKERNEL_INTERRUPT_PRIORITY = 15
(最低)。确保你的关键硬件中断(如电机控制、高速ADC)优先级高于configMAX_SYSCALL_INTERRUPT_PRIORITY
(数值上更小,如0-4),并且这些中断的处理函数只使用FromISR
API或不调用任何FreeRTOS API。
- 理解: FreeRTOS需要管理一些中断以进行任务切换(主要是PendSV)和提供API(如
-
SysTick配置:
- 必须保证在CubeMX的
Clock Configuration
中计算的SysTick重装载值 (Reload Value) 产生的实际中断频率精确等于FreeRTOSConfig.h
中的configTICK_RATE_HZ
。不匹配会导致时间相关函数 (vTaskDelay
,xQueueReceive
with timeout) 时间基准错误。
- 必须保证在CubeMX的
-
启动调度器 (
vTaskStartScheduler()
):- 这个函数调用后,FreeRTOS接管控制权,开始调度任务。它通常不会返回。
- 如果它返回了,通常意味着:
- 没有创建任何任务 (
configMINIMAL_STACK_SIZE
太小导致Idle任务创建失败?)。 - 堆空间不足,无法创建Idle任务或Timer服务任务。
- 移植层配置错误。
- 没有创建任何任务 (
-
阻塞 (
Blocking
):- 任务函数中必须包含能让出CPU的阻塞调用,如
vTaskDelay()
,xQueueReceive()
,xSemaphoreTake()
等。否则高优先级任务会独占CPU,导致低优先级任务永远无法运行。 - Idle任务 (优先级0) 是系统自动创建的最低优先级任务,当所有其他任务阻塞时运行。
- 任务函数中必须包含能让出CPU的阻塞调用,如
-
HAL库与FreeRTOS:
- HAL库本身不是线程安全的。如果多个任务访问同一个外设 (如UART),必须使用FreeRTOS同步原语 (互斥锁
xSemaphoreCreateMutex()
, 队列) 进行保护,防止并发访问导致数据损坏。 - HAL延时 (
HAL_Delay()
) 基于SysTick,但SysTick已被FreeRTOS接管。在任务中禁止使用HAL_Delay()
!必须用vTaskDelay()
替代。在中断服务程序 (ISR) 中绝对禁止使用任何阻塞延时。
- HAL库本身不是线程安全的。如果多个任务访问同一个外设 (如UART),必须使用FreeRTOS同步原语 (互斥锁
八. 总结关键步骤:
- 创建工程 & 配置时钟: 使用CubeIDE创建项目并正确配置系统时钟、外设时钟。
- 启用FreeRTOS: 在Middleware中选择FreeRTOS (CMSIS_V2)。
- 配置内核参数: 仔细设置
TICK_RATE_HZ
,TOTAL_HEAP_SIZE
,MINIMAL_STACK_SIZE
,MAX_PRIORITIES
, 内存方案。 - 配置中断优先级: 设置
configKERNEL_INTERRUPT_PRIORITY
和configMAX_SYSCALL_INTERRUPT_PRIORITY
(STM32F103常用15和5)。 - 验证SysTick: 确保SysTick中断频率匹配
TICK_RATE_HZ
。 - 编写任务函数: 实现任务逻辑,包含阻塞调用。
- 创建任务: 在
main()
或MX_FREERTOS_Init()
中调用xTaskCreate()
。 - 启动调度器: 调用
vTaskStartScheduler()
。 - 编译下载调试: 监控栈和堆的使用。
- 添加同步机制: 根据需要使用队列、信号量、互斥量保护共享资源。