恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
STM32F103移植FreeRTOS全流程:从裸机到RTOS的实战指南
首页
资讯中心
/
STM32F103移植FreeRTOS全流程:从裸机到RTOS的实战指南
STM32F103移植FreeRTOS全流程:从裸机到RTOS的实战指南
发布时间:2026/10/12 17:10:02
1. 为什么要在 STM32F103 上跑 FreeRTOSSTM32F103 这颗芯片在嵌入式圈子里算是国民级的存在——Cortex-M3 内核、72MHz 主频、64KB~128KB Flash、20KB SRAM价格便宜、资料铺天盖地很多人入门嵌入式就是从它开始的。但真正做过稍微复杂一点的项目就会发现裸机写while(1)大循环一旦功能多起来代码会迅速变成一团乱麻按键扫描要定时、串口收包不能丢、LED 要闪烁、还要处理传感器数据几个任务的时序互相打架delay一多整个系统就卡死。这时候 FreeRTOS 的价值就体现出来了。它把 CPU 时间切成一个个时间片让多个任务看起来在同时运行每个任务各管一摊事通过信号量、队列、事件组来通信。STM32F103 的 SRAM 虽然只有 20KB但跑一个精简配置的 FreeRTOS 内核绰绰有余——内核本身占用大概 6KB 左右的 Flash 和 1KB 左右的 RAM剩下的资源足够你开四五个任务。这篇内容适合两类人一类是裸机已经写熟了、想往 RTOS 方向进阶的开发者另一类是移植过程中被各种报错卡住、想搞清楚每一步到底在干什么的人。我会把移植的完整链路拆开讲包括文件从哪来、每个文件干什么、配置怎么改、启动流程是什么样以及我自己踩过的几个坑。移植这件事网上教程一大堆但很多只给步骤不讲原理一旦报错就抓瞎所以我会重点补上为什么这么做。需要提前说明的是FreeRTOS 的移植本质上就是让内核知道怎么切换任务、怎么产生系统节拍而这两件事都跟具体的 CPU 内核强相关。STM32F103 是 Cortex-M3所以我们要用的是 FreeRTOS 针对 Cortex-M3 的移植层而不是 M0、M4 或者别的架构。搞清楚这一点后面选文件的时候就不会拿错。2. 移植前必须理清的三个核心概念2.1 任务切换靠的是什么硬件机制很多人移植完能跑但被问到任务是怎么切换的就答不上来。Cortex-M3 内核有两个跟 RTOS 强相关的硬件特性一个是SysTick 定时器另一个是PendSV 异常。SysTick 是一个 24 位的递减计数器内核自带所有 Cortex-M 芯片都有。FreeRTOS 用它来产生周期性的系统节拍tick比如每 1ms 触发一次中断在中断里判断有没有任务需要切换。这个节拍就是 RTOS 的心跳所有延时、超时、时间片轮转都基于它。PendSV 则是一个可挂起的系统异常优先级可以设成最低。为什么任务切换要用它而不是直接在 SysTick 中断里切因为如果在 SysTick 里直接切换万一此时正在处理另一个高优先级中断就会打乱中断嵌套的现场。把切换动作挂起到 PendSV等所有高优先级中断都处理完了再执行这样最安全。这是 Cortex-M 内核专门为 RTOS 设计的机制理解这一点你就明白为什么移植文件里要手动配置 PendSV 和 SysTick 的优先级了。2.2 移植层到底要提供哪些东西FreeRTOS 的源码结构是分层的核心代码tasks.c、queue.c、list.c 等是平台无关的所有芯片共用而跟硬件打交道的部分被抽出来放在portable目录下按编译器和架构分类。我们要做的移植就是选对portable目录下的文件并实现几个关键函数。对于 STM32F103Cortex-M3 Keil MDKARMCC 编译器需要的是portable/RVDS/ARM_CM3/port.c和portmacro.h。如果你用的是 GCC 工具链比如 STM32CubeIDE 或者 Makefile 工程那就要选portable/GCC/ARM_CM3/目录。选错编译器对应的目录编译时会报一堆汇编语法错误这是新手最容易犯的错。移植层要提供的核心内容其实就三块一是任务上下文切换的汇编代码PendSV 处理函数xPortPendSVHandler二是启动第一个任务的汇编代码prvPortStartFirstTask三是 SysTick 中断处理函数xPortSysTickHandler。这些在port.c里都已经写好了我们不需要自己写汇编只要正确配置就能用。2.3 FreeRTOSConfig.h 是移植的总开关整个移植过程中唯一需要你大量动手改的文件就是FreeRTOSConfig.h。它决定了内核裁剪成什么样、开哪些功能、用多少资源。这个文件不是 FreeRTOS 源码自带的而是由用户工程提供的——因为不同项目需求不同配置自然不一样。配置项里最关键的几个configCPU_CLOCK_HZ要填你芯片的实际主频STM32F103 通常是 72000000configTICK_RATE_HZ是系统节拍频率一般填 1000也就是 1ms 一个 tickconfigTOTAL_HEAP_SIZE是内核堆大小STM32F103 只有 20KB SRAM这个值不能给太大一般 8KB~10KB 比较稳妥configMAX_PRIORITIES是最大任务优先级数填 5~8 就够用了填太大浪费 RAM。还有一个容易忽略的configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY它决定了哪些中断可以调用 FreeRTOS 的 API。这个值跟 Cortex-M3 的中断优先级分组有关配错了会导致configASSERT断言失败程序卡死。后面我会专门讲这个坑。3. 手把手搭建工程从零到能编译3.1 源码获取与目录规划FreeRTOS 的源码可以从官网下载也可以从一些成熟的工程模板里提取。下载下来是一个压缩包解压后核心目录是FreeRTOS/Source里面有tasks.c、queue.c、list.c、timers.c、event_groups.c、croutine.c这些核心文件以及portable和include两个子目录。我的习惯是在自己的工程目录下建一个FreeRTOS文件夹然后把源码按下面的结构整理Project/ ├── FreeRTOS/ │ ├── Source/ │ │ ├── include/ (内核头文件) │ │ ├── portable/ │ │ │ ├── RVDS/ARM_CM3/ (移植层Keil用) │ │ │ └── MemMang/ (内存管理) │ │ ├── tasks.c │ │ ├── queue.c │ │ ├── list.c │ │ ├── timers.c │ │ └── event_groups.c │ └── FreeRTOSConfig.h (配置文件放这里方便管理) └── User/ ├── main.c └── stm32f10x_it.cMemMang目录下有五个heap_x.c文件从heap_1.c到heap_5.c这是 FreeRTOS 提供的五种内存管理方案。新手最常纠结选哪个我的建议是一般项目直接用heap_4.c。原因后面单独讲。3.2 往 Keil 工程里添加文件分组打开 Keil 工程在 Project 窗口右键新建两个 Group一个叫FreeRTOS_Core一个叫FreeRTOS_Port。FreeRTOS_Core里添加tasks.c、queue.c、list.c、timers.c、event_groups.c。如果你不用软件定时器和事件组timers.c和event_groups.c可以不添加能省一点 Flash。croutine.c是协程功能现在基本没人用了不用加。FreeRTOS_Port里添加port.c来自RVDS/ARM_CM3和heap_4.c来自MemMang。然后在 Options for Target 的 C/C 选项卡里把两个头文件路径加进去FreeRTOS/Source/include和FreeRTOS/Source/portable/RVDS/ARM_CM3。注意FreeRTOSConfig.h所在的目录也要加进去否则编译器找不到它。提示FreeRTOSConfig.h的搜索路径一定要加而且这个文件必须能被全局找到。很多undefined identifier configXXX的报错都是因为路径没配对。3.3 中断处理函数的归属问题这一步是移植里最容易出问题的地方。STM32 的标准库工程里stm32f10x_it.c文件里默认定义了SysTick_Handler、PendSV_Handler、SVC_Handler这三个函数。而 FreeRTOS 的port.c里也定义了对应的处理函数名字分别是xPortSysTickHandler、xPortPendSVHandler、vPortSVCHandler。如果你两个都留着链接时会报符号重复定义。解决办法有两种一是把stm32f10x_it.c里的这三个函数注释掉或者删掉二是在FreeRTOSConfig.h里用宏把 FreeRTOS 的处理函数名映射成标准库的名字#define vPortSVCHandler SVC_Handler #define xPortPendSVHandler PendSV_Handler #define xPortSysTickHandler SysTick_Handler我更推荐第二种因为这样不用动标准库文件工程结构更干净。但要注意这三个宏必须写在FreeRTOSConfig.h里而且要在包含port.c之前生效。4. FreeRTOSConfig.h 逐项拆解与配置实战4.1 时钟与节拍相关配置先看最基础的几个#define configUSE_PREEMPTION 1 #define configUSE_IDLE_HOOK 0 #define configUSE_TICK_HOOK 0 #define configCPU_CLOCK_HZ (72000000) #define configTICK_RATE_HZ ((TickType_t)1000) #define configMAX_PRIORITIES (8) #define configMINIMAL_STACK_SIZE ((unsigned short)128) #define configTOTAL_HEAP_SIZE ((size_t)(10 * 1024)) #define configMAX_TASK_NAME_LEN (16) #define configUSE_16_BIT_TICKS 0 #define configIDLE_SHOULD_YIELD 1configCPU_CLOCK_HZ填 72000000这是 STM32F103 的标准主频。这个值必须跟实际系统时钟一致否则 SysTick 的节拍周期就不准所有延时都会偏。如果你用的是 8MHz 外部晶振倍频到 72MHz那这里就是 72M如果你改了时钟树这里也要跟着改。configTICK_RATE_HZ填 1000意味着 1ms 一个 tick。这个值越大系统时间精度越高但中断开销也越大。一般项目 1000 足够对时间精度要求不高的可以降到 100能减少中断次数。configMAX_PRIORITIES填 8表示支持 0~7 共 8 个优先级数字越大优先级越高。0 是空闲任务的优先级用户任务从 1 开始用。填太大浪费 RAM每个优先级都要维护一个就绪链表8 个对大多数项目够用了。configMINIMAL_STACK_SIZE是空闲任务的栈大小单位是字word不是字节。填 128 就是 128 个字 512 字节。这个值不能太小否则空闲任务可能栈溢出。configTOTAL_HEAP_SIZE是内核堆的总大小单位字节。STM32F103 只有 20KB SRAM还要留给全局变量、栈、堆所以这里给 10KB 已经比较激进了。如果你的任务多、队列多可以适当加大但要盯着 SRAM 占用别爆。4.2 内存管理方案的选择逻辑前面提到heap_4.c这里展开讲讲为什么。FreeRTOS 的五种内存方案各有适用场景方案特点适用场景heap_1只分配不释放任务创建后不再删除的简单项目heap_2可释放但不合并碎片已废弃不推荐heap_3封装标准 malloc/free需要标准库内存管理时heap_4可释放带碎片合并通用场景最推荐heap_5支持多块不连续内存内存分散在多个区域时heap_4.c用首次适应算法分配内存释放时会把相邻的空闲块合并能有效减少碎片。对于大多数 STM32F103 项目任务和队列的创建删除都不频繁heap_4是最稳妥的选择。heap_1虽然简单但一旦你想动态删除任务就不行了。heap_3依赖标准库的malloc会引入额外的代码体积而且标准库的malloc本身不一定线程安全。注意configTOTAL_HEAP_SIZE定义的是 FreeRTOS 自己管理的堆跟标准库的堆是两回事。如果你同时用了标准库的malloc和 FreeRTOS 的pvPortMalloc要确保两者加起来不超过芯片的 SRAM。4.3 中断优先级配置的坑这是移植里最隐蔽的坑没有之一。Cortex-M3 的中断优先级寄存器是 8 位的但 STM32F103 只实现了高 4 位所以实际可用的优先级是 0~15。FreeRTOS 要求凡是调用 FreeRTOS API 的中断其优先级必须低于configMAX_SYSCALL_INTERRUPT_PRIORITY设定的阈值。配置项长这样#define configPRIO_BITS 4 #define configLIBRARY_LOWEST_INTERRUPT_PRIORITY 15 #define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 5 #define configKERNEL_INTERRUPT_PRIORITY \ (configLIBRARY_LOWEST_INTERRUPT_PRIORITY (8 - configPRIO_BITS)) #define configMAX_SYSCALL_INTERRUPT_PRIORITY \ (configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY (8 - configPRIO_BITS))这里的逻辑是configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY设为 5意味着优先级数值大于等于 5 的中断数值越大优先级越低才能调用 FreeRTOS 的FromISR系列 API。优先级 0~4 的中断是高优先级中断它们不受 FreeRTOS 临界区保护绝对不能调用任何 FreeRTOS API。为什么这么设计因为 FreeRTOS 进入临界区时会通过basepri寄存器屏蔽掉低于某个阈值的中断如果高优先级中断里调用了 FreeRTOS API就可能在内核数据结构被修改到一半时打断它导致链表错乱、系统崩溃。实际配置时你的外设中断比如串口、定时器如果要用xQueueSendFromISR这类函数优先级就要设成 5~15。用标准库的NVIC_Init配置时NVIC_IRQChannelPreemptionPriority要填对应的值。这里还涉及优先级分组的问题建议用NVIC_PriorityGroup_4也就是全部 4 位都用作抢占优先级这样逻辑最简单。5. 启动流程与第一个任务的创建5.1 从 main 到调度器启动发生了什么裸机程序的入口是main初始化完外设就进while(1)。用了 FreeRTOS 之后main里要做的是初始化硬件、创建任务、启动调度器。启动调度器之后main函数就回不来了CPU 交给 FreeRTOS 管理。int main(void) { /* 硬件初始化 */ LED_Init(); USART1_Init(115200); /* 创建任务 */ xTaskCreate(LED_Task, LED, 128, NULL, 2, NULL); xTaskCreate(USART_Task, USART, 256, NULL, 3, NULL); /* 启动调度器 */ vTaskStartScheduler(); /* 正常情况下不会执行到这里 */ while (1); }vTaskStartScheduler内部做了几件事创建空闲任务优先级 0、创建定时器任务如果开了软件定时器、初始化 SysTick、然后调用xPortStartScheduler启动第一个任务。第一个任务是通过 SVC 异常启动的vPortSVCHandler会从就绪链表里取出最高优先级的任务恢复它的上下文然后跳转执行。5.2 任务函数的标准写法任务函数必须是死循环不能返回。如果返回了FreeRTOS 会触发断言或者直接跑飞。void LED_Task(void *pvParameters) { while (1) { GPIO_SetBits(GPIOC, GPIO_Pin_13); vTaskDelay(500); /* 延时500个tick即500ms */ GPIO_ResetBits(GPIOC, GPIO_Pin_13); vTaskDelay(500); } }这里用vTaskDelay而不是裸机的delay_ms。区别在于vTaskDelay会让当前任务进入阻塞态把 CPU 让给其他任务而裸机的忙等延时会把 CPU 占死。这就是 RTOS 的核心价值——延时期间 CPU 不闲着。vTaskDelay的参数是 tick 数不是毫秒。如果configTICK_RATE_HZ是 1000那 500 个 tick 就是 500ms。如果改成 100那 500 个 tick 就是 5 秒。所以写代码时最好用pdMS_TO_TICKS(500)这个宏来转换避免节拍频率改了之后延时全乱。5.3 栈大小的估算方法xTaskCreate的第三个参数是栈深度单位是字word不是字节。一个 128 的栈就是 512 字节。栈给太小会溢出给太大浪费 RAM。怎么估我的经验是先给一个偏大的值比如 256跑起来之后用uxTaskGetStackHighWaterMark查每个任务的历史最小剩余栈空间。这个函数返回的是任务运行过程中栈剩余的最小值如果返回值接近 0说明栈快溢出了要加大如果返回值很大说明给多了可以适当减小。UBaseType_t watermark uxTaskGetStackHighWaterMark(NULL); printf(Task stack watermark: %u\r\n, watermark);这个函数在调试阶段非常有用建议每个任务都查一遍。等产品稳定了再把栈大小调到合适的值。6. 移植过程中最容易翻车的几个点6.1 编译报错 undefined symbol 的排查顺序移植时最常见的报错就是各种 undefined symbol。排查顺序我总结成一条链路第一步看报错的是哪个符号。如果是xTaskCreate、vTaskDelay这类内核函数说明tasks.c没加进工程或者头文件路径没配对。如果是xPortPendSVHandler这类移植层函数说明port.c没加或者编译器目录选错了。第二步看是不是FreeRTOSConfig.h找不到。报错通常是cannot open source input file FreeRTOSConfig.h这时候检查头文件搜索路径。第三步看是不是中断处理函数重复定义。报错是multiply defined说明stm32f10x_it.c和port.c里都定义了PendSV_Handler之类。用前面说的宏映射方法解决。第四步看是不是编译器不匹配。如果你用的是 GCC 工具链却加了RVDS目录下的port.c会报一堆汇编语法错误比如bad instruction。这时候换成GCC/ARM_CM3目录下的文件。6.2 程序卡在 configASSERT 的定位思路configASSERT是 FreeRTOS 的断言宏默认在FreeRTOSConfig.h里定义成空。调试阶段强烈建议打开它#define configASSERT(x) if ((x) 0) { taskDISABLE_INTERRUPTS(); for(;;); }一旦断言失败程序会关中断然后死循环。这时候用调试器看调用栈就能定位到是哪一行触发的断言。最常见的断言失败原因是中断优先级配错了——比如在优先级 0 的中断里调用了xQueueSendFromISRFreeRTOS 检测到就会断言失败。还有一种情况是栈溢出。FreeRTOS 有个configCHECK_FOR_STACK_OVERFLOW配置项设成 1 或 2 可以检测栈溢出。设成 2 会在任务切换时检查栈末尾的标记字节有没有被覆盖更可靠但稍慢。检测到溢出会调用vApplicationStackOverflowHook你可以在这个钩子函数里打印是哪个任务溢出了。6.3 中断里调用 API 必须用 FromISR 版本这个坑我见过太多人踩。在中断服务函数里不能调用xQueueSend必须调用xQueueSendFromISR不能调用xSemaphoreGive必须调用xSemaphoreGiveFromISR。带FromISR后缀的版本不会阻塞而且会在退出时判断是否需要触发任务切换。void USART1_IRQHandler(void) { BaseType_t xHigherPriorityTaskWoken pdFALSE; uint8_t data; if (USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET) { data USART_ReceiveData(USART1); xQueueSendFromISR(xQueue, data, xHigherPriorityTaskWoken); portYIELD_FROM_ISR(xHigherPriorityTaskWoken); } }xHigherPriorityTaskWoken这个变量很关键。如果中断唤醒了比当前任务优先级更高的任务它会被设成pdTRUE然后portYIELD_FROM_ISR会触发一次任务切换让高优先级任务立即执行。如果忘了这一步高优先级任务要等到下一个 tick 才能运行实时性就打折扣了。6.4 SRAM 不够用的取舍策略STM32F103C8T6 只有 20KB SRAM跑 FreeRTOS 之后经常捉襟见肘。如果编译报 region RAM overflowed说明 SRAM 不够了。这时候有几个方向可以优化一是减小configTOTAL_HEAP_SIZE从 10KB 降到 6KB 试试。二是减小各任务的栈大小用uxTaskGetStackHighWaterMark找到实际需求。三是关掉不用的功能比如configUSE_TIMERS设成 0 可以省掉定时器任务。四是把一些大的全局数组改成const放到 Flash 里或者用__attribute__((section(.ccmram)))放到 CCM RAM不过 F103 没有 CCM这条对 F4 系列适用。还有一个技巧把configMINIMAL_STACK_SIZE从 128 降到 64。空闲任务的栈需求其实不大64 个字256 字节通常够用。但要注意如果你在空闲钩子函数里做了复杂操作栈就要留够。7. 移植完成后的验证与性能观察7.1 用 LED 和串口做最小验证移植完别急着上复杂功能先用最简单的例子验证。我的做法是创建两个任务一个闪 LED一个通过串口每隔一秒打印一次系统运行时间。void Blink_Task(void *pvParameters) { while (1) { GPIO_SetBits(GPIOC, GPIO_Pin_13); vTaskDelay(pdMS_TO_TICKS(200)); GPIO_ResetBits(GPIOC, GPIO_Pin_13); vTaskDelay(pdMS_TO_TICKS(800)); } } void Print_Task(void *pvParameters) { TickType_t lastWake xTaskGetTickCount(); while (1) { printf(Tick: %lu\r\n, (unsigned long)xTaskGetTickCount()); vTaskDelayUntil(lastWake, pdMS_TO_TICKS(1000)); } }如果 LED 按 1 秒周期闪烁亮 200ms 灭 800ms串口每秒打印一次说明调度器工作正常。如果 LED 闪烁频率明显不对可能是configCPU_CLOCK_HZ填错了导致 tick 周期不准。vTaskDelayUntil和vTaskDelay的区别值得说一下。vTaskDelay是相对延时从调用时刻起延时指定 tick 数vTaskDelayUntil是绝对延时保证任务以固定周期执行不受任务执行时间波动影响。做周期性任务比如采样时应该用vTaskDelayUntil这样周期才稳定。7.2 查看任务运行状态FreeRTOS 提供了一个vTaskList函数可以打印所有任务的名称、状态、优先级、剩余栈空间。要使用它需要在FreeRTOSConfig.h里打开两个配置#define configUSE_TRACE_FACILITY 1 #define configUSE_STATS_FORMATTING_FUNCTIONS 1然后定义一个足够大的字符缓冲区char taskListBuffer[512]; vTaskList(taskListBuffer); printf(%s\r\n, taskListBuffer);输出大概长这样Name State Prio Stack Num LED R 2 100 1 USART B 3 200 2 IDLE R 0 60 3 Tmr Svc B 2 180 4State 列里 R 是就绪/运行B 是阻塞S 是挂起。Stack 列是剩余栈空间单位字如果某个任务这个值很小就要警惕栈溢出。这个工具在调试阶段非常实用能一眼看出哪个任务在占 CPU、哪个任务栈快满了。7.3 系统节拍精度的实测方法SysTick 的精度直接影响所有延时和超时的准确性。验证方法很简单用xTaskGetTickCount读 tick 计数跟一个外部精确时钟对比。比如让一个任务每 1000 个 tick 翻转一次 IO用示波器或者逻辑分析仪测 IO 的周期如果是标准的 1 秒说明节拍准确。如果测出来偏差较大检查两个地方一是configCPU_CLOCK_HZ是否跟实际主频一致二是SystemCoreClock变量是否被正确更新。标准库的SystemInit函数会设置SystemCoreClock但如果你用的是寄存器版本或者自己写的时钟初始化要确保这个变量跟实际频率同步。还有一个细节SysTick 的重装载值是根据configCPU_CLOCK_HZ / configTICK_RATE_HZ算出来的。72MHz 除以 1000 等于 72000这个值小于 24 位计数器的最大值 16777215所以没问题。但如果你把configTICK_RATE_HZ设得特别高比如 100000那 72000 就变成 720虽然还能用但中断太频繁会拖慢系统。8. 从能跑到跑得好的进阶思路8.1 优先级分配的经验法则任务优先级怎么分是 RTOS 应用设计的核心问题。我的原则是按实时性要求分配而不是按重要性分配。实时性要求高的任务比如电机控制、通信协议解析给高优先级实时性要求低的比如 LED 显示、日志打印给低优先级。具体到 STM32F103 这种资源有限的芯片我一般这样分优先级用途7紧急事件处理如故障保护5-6硬实时任务通信、控制3-4普通业务任务数据处理1-2低速任务显示、按键0空闲任务系统占用注意 FreeRTOS 的优先级数值越大优先级越高这跟某些 RTOS 是反的。另外如果两个任务优先级相同它们会按时间片轮转每个 tick 切换一次。时间片轮转适合两个同等重要的任务但会增加切换开销能不用就不用。8.2 队列和信号量的选择任务间通信有两种基本方式队列和信号量。队列用来传数据信号量用来同步。但实际用的时候经常混。队列的本质是一块环形缓冲区xQueueSend往尾部写xQueueReceive从头部读。如果队列满了发送方会阻塞或者超时返回如果队列空了接收方会阻塞。这个阻塞机制就是任务同步的基础。信号量分二值信号量和计数信号量。二值信号量只有 0 和 1 两个状态常用来做事件通知——中断里 give任务里 take。计数信号量可以累加常用来管理资源池比如有 5 个缓冲区就初始化一个计数为 5 的信号量用之前 take用完 give。还有一个互斥信号量Mutex专门用来保护共享资源。它跟二值信号量的区别是支持优先级继承——如果低优先级任务持有互斥量高优先级任务来抢低优先级任务会临时提升到高优先级避免优先级反转。用 I2C、SPI 这类共享总线时互斥量是标配。8.3 中断和任务的职责划分一个常见的架构问题是中断里该做多少事我的原则是中断里只做最紧急、最简短的操作剩下的交给任务。比如串口接收中断里只把数据丢进队列解析协议、处理命令这些耗时操作放到任务里做。这样做的好处是中断响应快不会因为处理复杂逻辑而阻塞其他中断。代价是需要一个队列来缓冲数据以及一个任务来消费数据。对于 STM32F103 这种 SRAM 紧张的芯片队列长度要控制好太长浪费内存太短容易丢数据。如果数据量很大可以用 DMA 空闲中断的方式DMA 负责搬运数据空闲中断负责通知任务一帧数据收完了。这样 CPU 几乎不参与数据搬运效率最高。不过 DMA 的配置相对复杂新手可以先从简单的中断 队列方案入手跑通了再优化。8.4 低功耗场景下的 tickless 模式如果项目是电池供电的FreeRTOS 的 tickless 模式能显著降低功耗。默认情况下SysTick 每 1ms 中断一次即使所有任务都在阻塞CPU 也会被周期性唤醒白白耗电。tickless 模式会在空闲时关掉 SysTick让 CPU 进入低功耗模式等到下一个任务该唤醒时再通过其他定时器唤醒。开启方法是在FreeRTOSConfig.h里设configUSE_TICKLESS_IDLE为 1并实现vPortSuppressTicksAndSleep函数或者用移植层提供的默认实现。STM32F103 支持睡眠、停止、待机三种低功耗模式tickless 通常配合停止模式使用。不过 tickless 模式会增加系统复杂度调试也更麻烦。如果项目对功耗要求不高不建议开。我个人的经验是先用默认的 tick 模式把功能跑通等产品定型了再考虑低功耗优化。9. 我踩过的几个真实坑和解决过程9.1 串口打印乱码查了半天是时钟配置有一次移植完LED 闪得正常但串口打印全是乱码。第一反应是波特率不对检查了串口初始化代码115200 没问题。又怀疑是晶振换了板子还是一样。最后用示波器测串口波形发现实际波特率大概是设定值的两倍。顺着这个线索查下去发现是SystemInit里把外部晶振当成了 8MHz但实际板子上焊的是 12MHz 晶振。时钟树算出来的系统频率是 72MHz但实际跑到了 108MHz超频了所以串口分频系数不对。改掉晶振频率宏定义之后一切正常。这个坑的教训是移植 FreeRTOS 之前先确保裸机程序本身是正常的。如果裸机的串口、定时器都不准移植 RTOS 之后问题只会更复杂。我现在的习惯是拿到新板子先用裸机点灯、串口打印、定时器中断各验证一遍确认硬件和时钟没问题再上 RTOS。9.2 任务创建失败堆不够了另一个坑是xTaskCreate返回errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY任务创建失败。查了一下是configTOTAL_HEAP_SIZE设得太小创建了三个任务之后堆就耗尽了。FreeRTOS 提供了xPortGetFreeHeapSize和xPortGetMinimumEverFreeHeapSize两个函数可以查当前剩余堆和历史最小剩余堆。我在main里创建完所有任务后打印这两个值发现剩余堆只有几百字节历史最小值几乎为 0说明堆给少了。解决办法是把configTOTAL_HEAP_SIZE从 6KB 加到 10KB同时把不用的任务栈减小。改完之后剩余堆有 3KB 左右历史最小值也有 1KB 以上比较安全。这里有个经验堆的大小要留有余量。因为除了任务栈队列、信号量、事件组这些内核对象也要从堆里分配。如果你算出来刚好够用实际跑起来可能因为碎片或者临时分配而失败。留 20%~30% 的余量比较稳妥。9.3 中断优先级配错导致系统随机死机最诡异的一次是系统跑着跑着随机死机有时候几分钟有时候几小时。用调试器抓发现死在configASSERT里但看调用栈又看不出明显问题。后来把configASSERT的断言信息打印出来发现是vPortValidateInterruptPriority触发的。这个函数会检查当前中断的优先级是否允许调用 FreeRTOS API。查了一下是定时器 3 的中断优先级配成了 2而configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY是 5优先级 2 高于 5不允许调用 FreeRTOS API。但我在定时器中断里调用了xQueueSendFromISR所以偶尔触发断言。把定时器中断优先级改成 6 之后问题消失。这个坑的隐蔽性在于它不一定每次都触发取决于中断发生的时机。如果中断发生时内核正好在临界区就会断言失败如果不在临界区可能就蒙混过关了。所以这种问题表现为随机死机很难复现。教训是所有调用 FreeRTOS API 的中断优先级数值必须大于等于configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY。配置中断的时候养成习惯先想清楚这个中断要不要调 FreeRTOS API要调的话优先级就不能太高。9.4 栈溢出导致的玄学问题还有一次是某个任务偶尔跑飞现象是串口输出乱码、变量值莫名其妙改变。查了很久没找到原因后来打开configCHECK_FOR_STACK_OVERFLOW设为 2触发了栈溢出钩子函数才发现是那个任务的栈给小了。那个任务里定义了一个比较大的局部数组加上函数调用深度实际需要的栈超过了分配的值。栈溢出之后覆盖了相邻内存区域的数据导致各种玄学现象。把栈从 128 加到 256 之后问题解决。这个坑的教训是局部变量大的任务栈要给够。特别是那些在栈上定义数组、结构体的任务一定要估算好。用uxTaskGetStackHighWaterMark定期检查比事后排查省事得多。10. 移植完成之后还能做什么移植跑通只是第一步后面还有很多可以深挖的方向。比如可以研究一下 FreeRTOS 的软件定时器它能在不占用额外硬件定时器的情况下实现多个定时任务可以研究事件组它能实现多个任务等待多个事件组合的复杂同步逻辑还可以研究流缓冲区Stream Buffer和消息缓冲区Message Buffer它们比队列更适合传输字节流。如果项目对实时性要求极高可以研究一下 FreeRTOS 的抢占式调度和时间片轮转的细节调整configUSE_TIME_SLICING和configUSE_PREEMPTION来优化响应时间。如果对内存敏感可以研究heap_5的多区域内存管理把不同速度的 RAM 分开管理。我个人在实际项目中的体会是FreeRTOS 在 STM32F103 上跑最关键的三个配置是configCPU_CLOCK_HZ、configTOTAL_HEAP_SIZE和中断优先级。这三个配对了系统基本就稳了配错了就会出现各种莫名其妙的问题。所以每次新建工程我都会先把这三个配置反复确认一遍再开始写业务代码。最后分享一个小技巧在main函数启动调度器之前把所有任务的栈大小、优先级、堆使用情况打印出来存成一份基线。以后每次改代码对比一下这份基线如果堆使用量突然增加很多或者某个任务栈剩余量骤降就能提前发现问题不用等到系统跑飞了再查。这个习惯帮我省了很多调试时间。