
简介面向意法半导体STM32嵌入式开发者一套FreeRTOS V9.00在STM32F103C8T6上的完整移植成功案例可供下载。作者专门分析了为何ZET6型号的工程直接迁移到C8T6后无法运行并针对FreeRTOSConfig.h配置头文件做了逐项中文注释把任务调度、系统时钟节拍、堆栈空间分配、中断优先级分组等核心参数讲清楚同时将LED示例改为PC13引脚烧录到C8T6核心板即可验证系统正常运行。压缩包共2000个文件大小约72.16MB其中C源文件与H头文件合计超过1800个覆盖FreeRTOS内核源码、STM32标准外设库和用户应用代码另有txt、PDF、HTML等格式的说明文档便于查阅和二次开发。目前已有1154人学习下载这套经实际验证、带注释、可直接运行的工程对于刚接触FreeRTOS的开发者或需要将工程从ZET6移植到C8T6的工程师能够显著降低环境配置和移植排错难度是学习与项目起步的高性价比资料。无论是毕设原型、竞赛准备还是产品预研都可以通过这份资料快速搭建一个可运行的FreeRTOS基础平台再根据需求裁剪功能。 做嵌入式开发的朋友应该都听说过FreeRTOS就算没实际用过大概率也在项目需求或招聘信息里见过这个词。这些年物联网设备爆发之后FreeRTOS几乎成了单片机RTOS的事实标准而STM32F103C8T6这块芯片又恰好是无数人入门STM32时摸过的第一颗料。把这两个东西放一起——在F103C8T6上移植FreeRTOS——就成了很多人的第一个RTOS实战项目。这篇东西不是官方文档搬过来的说明是我自己从裸机工程一路折腾到多任务稳定运行的全过程记录。包含源码怎么加、配置文件每一项怎么设、为什么设、跑起来之后怎么验证、以及那些让人头皮发麻的HardFault和任务卡死问题到底怎么查。适合正在学RTOS、刚拿到最小系统板想搞点东西、或者准备在项目里上FreeRTOS但对工程结构还不太熟的朋友。看完你至少能独立搭一个能用的FreeRTOS工程并且知道出问题时从哪儿下手。1. 为什么要在F103C8T6上移植FreeRTOS1.1 这颗小芯片跑RTOS到底行不行先说硬件底子。STM32F103C8T6Cortex-M3内核主频72MHzFlash 64KBRAM 20KB。很多人一听RTOS就觉得得H7、F4才跑得动其实这是个误解。FreeRTOS内核做的非常精简完整编译下来ROM占用通常只有6~12KBRAM主要消耗是任务栈和内核对象一个最小任务的栈给128字节能干不少事。在F103C8T6上跑三五个任务、做点串口处理、传感器采集、状态控制资源完全够用。我实测过一个包含空闲任务、两个业务任务的工程Flash占用大概14KB左右RAM占用含全局变量和任务栈4~6KB剩下的资源还能塞点逻辑代码。换句话说这颗几块钱的芯片跑小型RTOS应用一点都不勉强。而且F103C8T6是64脚封装、引脚兼容性极好市面上最小系统板遍地都是学习成本极低。用它做FreeRTOS移植练习摔坏了也不心疼。1.2 移植前要想清楚的事动手之前建议先搞清楚一个问题你到底是在“学RTOS”还是在“用RTOS”。这两个目标对应的路径不一样。如果纯粹为了学习我强烈建议别用CubeMX一键生成而是手动建立工程、手动添加源码、手写配置文件。这个过程虽然繁琐但你能把FreeRTOS的源码结构、任务调度入口、中断处理机制、堆栈管理逻辑全部过一遍。等这一遍走通之后再用CubeMX生成工程你会发现在配置界面里选的每一个选项都能对应到具体代码上那种感觉完全不一样。如果是为了项目交付那就别折腾直接CubeMX勾选FreeRTOS生成工程把注意力放在业务逻辑上。CubeMX生成的FreeRTOS工程经过了大量验证默认配置比较稳妥比自己手动拼的工程出问题概率低得多。我下面讲的内容两种方式都覆盖但重点偏向手动移植因为只有理解了原理碰到问题才知道怎么排。2. 移植前的准备工具链与源码2.1 需要准备的东西我这次用的环境如下供参考硬件STM32F103C8T6最小系统板蓝色那种板载LED在PC13调试器ST-Link V2编译环境Keil MDK 5.37AC5编译器固件库标准外设库V3.5也可以选HAL库后面细说FreeRTOS版本V10.4.6版本选择上FreeRTOS V10.x是目前的主流V9和V10的API基本兼容。V10之后内核加入了流缓冲区和消息缓冲区功能对实际开发很有用。别去用老掉牙的V8或更早版本那些东西在Cortex-M3上的实现也成熟但资料少、API旧没必要给自己找麻烦。2.2 FreeRTOS源码目录怎么看下载下来的FreeRTOS源码包很多人第一眼就懵目录很多。其实真正需要的只有两个地方FreeRTOS/Source/内核全部源码包括tasks.c、queue.c、list.c、timers.c、event_groups.c、stream_buffer.c以及一个include头文件目录FreeRTOS/Source/portable/移植层代码我们用的是RVDS/ARM_CM3这个目录下的port.c和portmacro.h那portable/MemMang/呢这个很关键里面有heap_1.c、heap_2.c、heap_3.c、heap_4.c、heap_5.c五个内存管理方案移植时必须选一个加进工程。我用的heap_4支持内存合并和碎片整理是实际项目里最常用的选择。heap_1最简单但只支持分配不支持释放只适合极简场景heap_2有历史遗留问题的坑heap_4是最稳妥的默认选项。还需要说明一点ARM_CM3这个目录还有一个port.c和portmacro.h的IAR版本、GCC版本。Keil下编译必须选择RVDS/ARM_CM3目录选错了会出现各种稀奇古怪的编译错误。这个坑我见过太多人踩了。2.3 用标准库还是HAL库这是个避不开的问题。我这次用的是标准库V3.5因为移植FreeRTOS本质上和硬件库无关FreeRTOS只依赖Cortex-M3内核的底层机制SVC、PendSV、SysTick也就是那些__asm和寄存器操作。标准库代码精简、直接操作寄存器适合我这类喜欢完全掌控的老派做法。如果你用HAL库注意一个关键点HAL库自带的HAL_Delay()依赖SysTick而FreeRTOS接管SysTick之后HAL_Delay()会跑飞或者时间完全不准。解决办法要么用vTaskDelay()替代要么把HAL的时基改为TIM2之类的硬件定时器。CubeMX生成工程时选FreeRTOS组件它会自动把HAL时基切到另一个定时器上这就是为什么CubeMX生成的工程里会无缘无故多出一个TIM.总的来说学习阶段推荐标准库因为更能看清FreeRTOS做了什么、没做什么。项目阶段用CubeMX生成HAL库工程效率高、不容易漏配置。3. 核心移植步骤从工程模板到任务跑起来3.1 搭建基础工程第一步不是加FreeRTOS而是先保证一个裸机工程能正常编译、下载、点灯。这一步的目的是把编译器和调试链路打通。我通常用标准库新建一个最小工程包含启动文件、系统时钟初始化、GPIO配置。跑一下LED闪烁确认最小系统板工作正常。这一步别省。很多人一上来就把FreeRTOS代码丢进工程出问题了分不清是硬件问题、工程配置问题还是RTOS本身的问题。先把地基打好后面定位问题会容易十倍。工程能跑之后需要把Flash和RAM的分配检查一遍。F103C8T6虽然有64KB Flash和20KB RAM但芯片型号后缀如果是C6Flash实际可能只有32KB不过我们讨论的C8是满64KB。在Keil的Options for Target里面Flash起始0x08000000、大小0x10000RAM起始0x20000000、大小0x5000确认这两个值没填错。3.2 往工程里添加FreeRTOS源码这一步是把FreeRTOS的文件分组加进Keil工程里。建议在工程下建一个FreeRTOS分组然后按如下清单添加内核源码tasks.c、queue.c、list.c、timers.c用不到哪个可以不加比如不用软件定时器就不加timers.c但建议先全加上移植层portable/RVDS/ARM_CM3/port.c内存管理portable/MemMang/heap_4.c头文件目录Source/include、Source/portable/RVDS/ARM_CM3还有一个极其容易漏的地方——FreeRTOS.h这个总头文件会去Include FreeRTOSConfig.h因此在C/C编译选项里的Include Path必须把存放FreeRTOSConfig.h的目录加上。如果你把配置文件放在用户目录这里就填用户目录路径。检查一下FreeRTOS.h源码里面有这么一段条件编译的逻辑它需要能看到FreeRTOSConfig.h才行得到配置。任何#include FreeRTOS.h报错找不到头文件基本都是Include Path没加全。3.3 配置FreeRTOSConfig.hFreeRTOSConfig.h是整个移植里最核心的文件。它不被包含在源码包里需要自己写或者从官方示例工程里拷贝。F103C8T6的RAM只有20KB所以内存相关的配置必须精打细算。下面是我这次用的关键配置项configUSE_PREEMPTION设为1使用抢占式调度configCPU_CLOCK_HZ设为72MHz要和实际系统时钟一致否则时间计算全错configTICK_RATE_HZ设为1000也就是1ms一个系统节拍configTOTAL_HEAP_SIZE设为(10 * 1024)给内核堆分配10KB留10KB给全局变量和任务栈configMINIMAL_STACK_SIZE设为128单位是字Word不是字节configMAX_PRIORITIES设为5够用了别设太高浪费RAMconfigUSE_TIMERS设为1使能软件定时器configUSE_IDLE_HOOK和configUSE_TICK_HOOK先设为0调试阶段不需要一个大坑是configTOTAL_HEAP_SIZE。很多人按网上帖子把堆大小设成15KB甚至20KB结果一编译链接就直接报错RAM溢出。F103C8T6的RAM就20KB加上任务栈、全局变量、堆必须统筹分配。我的建议是先把堆设为8~10KB跑起来之后再根据实际使用量调整够用就好。还有一个和调试强相关的配置configASSERT。这玩意儿默认是不定义的但我建议开发阶段务必定义。它会在参数错误、API误用、中断优先级配置不对时触发断言帮你迅速定位问题。定义方法是在FreeRTOSConfig.h里写#define configASSERT(x) if((x) 0) { taskDISABLE_INTERRUPTS(); for(;;); }这个宏的代价是每个API调用都会多一点检查开销但开发阶段的调试价值远远超过这点性能损失。等产品稳定了可以再把断言关掉。3.4 关键SVC、PendSV、SysTick中断优先级这一步是Cortex-M3上移植FreeRTOS最容易出错的地方也是很多人程序跑飞的根本原因。FreeRTOS在Cortex-M3上使用三个异常SVC用于启动第一个任务PendSV用于任务切换SysTick用于系统节拍时钟。这三个中断的优先级有硬性要求必须设置为最低优先级并且在NVIC里配置为可屏蔽。原因是FreeRTOS在临界区保护时用portSET_INTERRUPT_MASK()屏蔽中断如果SysTick或PendSV的优先级不是最低在临界区内触发这些中断时会被挂起等退出临界区再执行这会导致调度时序错乱。标准做法是在启动代码里设置如下优先级分组然后设置这三个异常优先级。如果使用CMSIS函数可以这样NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);优先级分组设为Group_4表示全部4位都用于抢占优先级子优先级为0。在这个分组下SysTick和PendSV的优先级值要设为最高数值数值越大优先级越低通常是15。SVC的优先级可以设为最高因为它只在启动时调用一次。这部分的代码放在main函数最开始且必须在创建任务之前执行。如果你用了CubeMX这就是为什么CubeMX生成的代码里有两行优先级设置的影子它分别对应HAL_NVIC_SetPriority(PendSV_IRQn, 15, 0)和HAL_NVIC_SetPriority(SysTick_IRQn, 15, 0)。手动移植时别忘了写。3.5 创建第一个任务配置文件搞定源码添加完毕接下来的逻辑就是创建任务、启动调度器。这里也有一个新手容易搞混的地方xTaskCreate只是创建任务并不会立即运行要让任务跑起来必须调用vTaskStartScheduler()启动调度器。调度器启动之后main函数里vTaskStartScheduler()后面的代码永远不会执行到。第一个任务建议写一个简单的LED闪烁任务。F103C8T6最小系统板上的LED接在PC13引脚输出低电平时点亮。任务代码如下void led_task(void *arg) { for (;;) { GPIO_ResetBits(GPIOC, GPIO_Pin_13); vTaskDelay(pdMS_TO_TICKS(500)); GPIO_SetBits(GPIOC, GPIO_Pin_13); vTaskDelay(pdMS_TO_TICKS(500)); } }注意里面的pdMS_TO_TICKS(500)这个宏把毫秒转换成系统节拍数。因为configTICK_RATE_HZ是10001ms等于1个tick所以500ms就是500个tick。如果你的tick频率改成了100那500ms就是50个tick。别再把delay直接写死成500除非你知道tick频率是多少。在main里创建任务并启动调度器int main(void) { NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4); SystemInit(); led_init(); xTaskCreate(led_task, led, 128, NULL, 1, NULL); vTaskStartScheduler(); for (;;); }注意任务栈大小128表示的是128个Word也就是512字节。栈空间分配在FreeRTOS内部不占普通全局变量空间实际是从堆里割出来的这也是为什么configTOTAL_HEAP_SIZE决定了你能创建多少个任务、每个任务能用多大栈。4. 实操代码与运行验证4.1 点灯不是目的验证调度才是第一个任务能闪灯之后很多人就觉得移植成功了。说实话这只是“Hello World”还不能证明移植完全正确。我建议接着加两个任务用不同频率闪烁两个不同的LED或者用一串调试信息验证调度行为。有次我在调试一个工程时手头只有一块带两个LED的板子。LED1以500ms周期闪烁LED2以1000ms周期闪烁。用示波器同时抓两个引脚可以看到两个方波各自稳定输出。如果其中一个波形频率不对、或者两个波形出现互相干扰说明调度器配置有问题比如tick频率不对、任务优先级设置失误。更严谨的验证方式是使用一个GPIO翻转信号观察空闲任务的运行情况。在vApplicationIdleHook里翻转一个引脚然后观察这个引脚的波形。空闲任务只在其他任务阻塞或挂起时运行所以这个引脚上的波形状态能直观反映任务调度是否健康。4.2 串口打印辅助观察任务切换我这次的测试工程里加了串口输出USART1波特率115200在每个任务里打印进入和退出的信息。这样通过电脑上的串口调试工具就能看到任务切换的序列。需要注意串口打印函数要加互斥保护否则两个任务同时打印时会出现字符交错。最简单的做法是用xSemaphoreCreateMutex()创建互斥锁打印前后加锁解锁。这也是实际项目中必须有的意识多任务访问共享资源必须加锁。实测打印序列大致如下led task, count 1 print task, count 10 led task, count 2 print task, count 11如果打印序列乱了、任务卡住不动、或者输出乱码就要用下面第5节的方法排查。4.3 内存使用怎么观察FreeRTOS提供了查看堆内存占用情况的APIxPortGetFreeHeapSize()返回当前剩余的堆大小。在调试阶段我会用串口定期打印这个值。如果剩余堆大小持续下降说明有内存泄漏通常是任务被反复创建、队列或信号量被反复申请后没有释放。产品发布前这个值应该在网上线以后的数小时内保持稳定。另外uxTaskGetStackHighWaterMark()可以获取某个任务栈的历史最低剩余空间。这个函数对栈大小的设置非常有指导意义。比如任务创建时给了128个Word的栈运行一段时间后调用这个函数得到剩余空间只有20个Word说明该任务栈紧张应该调大。反过来如果剩余100个Word说明栈给多了可以精简。我实际开发中习惯每个任务栈都先给一个保守偏大的值跑一段时间后用这个函数统计再统一调优。5. 常见问题与排错实录5.1 程序跑飞进入HardFault这是FreeRTOS移植最常见的现象没有之一。程序一启动就进入HardFault或者跑一段时间进入HardFault先别慌有几种最可能的原因第一中断优先级配置不对。如上文所说PendSV和SysTick必须是可屏蔽的最低优先级。如果你把SysTick配成了高优先级比如0或1那么一旦在临界区内触发了SysTick硬件会把异常挂起等关闭中断的临界区代码执行完才响应这个延迟会导致任务切换错乱严重时直接HardFault。第二栈溢出。中断服务程序和任务函数之间的栈切换很复杂最简单的排查办法是定义configCHECK_FOR_STACK_OVERFLOW为1或2然后实现钩子函数void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName) { for(;;); }程序跑飞前如果能进入这个钩子函数说明就是栈溢出了。第三中断服务函数里调用了FreaRTOS的API但所调用API的FromISR版本不对。在中断里绝对不能直接调用xQueueSend之类不带FromISR后缀的函数必须使用xQueueSendFromISR。这个错误编译时不会报错但运行时会随机崩溃极其难查。5.2 任务不切换、只有高优先级任务在跑如果任务1优先级为1任务2优先级为2且任务2的代码是一个死循环、没有延时和阻塞那么任务1永远不会有运行机会。这不是移植Bug而是抢占式调度的正常行为高优先级任务准备好时低优先级任务必须让路。解决办法是在每个任务的循环里都必须有阻塞操作比如vTaskDelay、xQueueReceive、xSemaphoreTake让出CPU给其他任务。还有种情况是任务运行一会儿后全卡死但硬件看门狗没有复位。这种往往是某个任务里不小心调用了一个死循环或者阻塞时间过长的函数。可以使用vTaskList()配合调试串口打印当前所有任务的状态状态显示为B是阻塞R是就绪D是删除。如果发现所有任务都停在阻塞状态那就是互相等待了典型的死锁问题。5.3 用了HAL_Delay导致系统卡死这个问题在HAL库工程里非常经典。HAL库的HAL_Delay()依赖SysTick中断而FreeRTOS启动调度器之后完全接管了SysTick并调整了它的中断回调。这时候任务里再调用HAL_Delay()轻则时间不准重则直接卡死因为SysTick的中断优先级被改了定时行为完全变了。我踩过这个坑后的解决办法是全工程搜索HAL_Delay全部替换为osDelay()或vTaskDelay()。如果某个底层驱动库内部用了HAL_Delay比如一些传感器驱动那就得把底层库的延时也换成vTaskDelay或者使用HAL_GetTick自己的时基。CubeMX生成的工程会额外把HAL时基绑定到TIM6等定时器上目的就是让HAL库的延时不依赖SysTick。手动移植时没有这个环节所以要自己注意。5.4 FreeRTOSConfig.h里的断言突然生效如果定义了configASSERT程序突然停在一个死循环里而且正好是configASSERT函数的for(;;)绝大多数是调用了非法参数。最常见的几个触发点信号量、队列句柄传了一个NULL比如队列还没创建好就被任务使用了或者API在中断里用了非FromISR版本。这时候就可以利用调试器的调用栈确认具体是哪个文件哪一行触发的断言一般一眼就能定位。还有一种可能configASSERT真的很好用以至于我在所有开发阶段都一直开着它只在最后Release版本才关掉。关掉前全工程测试一遍确认断言没有触发才去掉。这是我对接产品时坚持的做法。6. 移植经验总结与扩展建议6.1 我个人实际操作中的体会先说结论FreeRTOS在F103C8T6上跑小型多任务应用是完全可以依赖的关键是把优先级、堆大小和中断处理这三件事想清楚。这三件事不解决换更大的芯片也一样会出问题。移植过程中我最大的感悟是不要一上来就追求“移植成功”这个结果而要把每一部分都拆开弄明白。为什么中断优先级要那样设为什么任务里必须要有阻塞点为什么栈大小不是越大越好这几个问题弄明白了以后再上手任何带RTOS的开发板甚至在上Linux驱动里做类似的概念对接都会觉得顺畅很多。6.2 后面还能怎么玩这颗F103C8T6跑通FreeRTOS之后扩展空间很大。环境监测项目里可以挂DHT11温度湿度传感器和OLED显示屏——采集任务、显示任务、通信任务各干各的典型的多任务结构。用FreeRTOS的软件定时器做周期性采集配合队列把传感器数据从ISR传到任务这是非常经典、也非常值得练手的学习路径。我自己后来在这个基础上接了4G模块做MQTT上云采集任务负责读传感器通信任务负责联网发包消息通过队列传递。跑起来之后最明显的感觉是单线程裸机那种“一个模块卡住整机遭殃”的噩梦彻底结束了。所有任务都是独立的时间片逻辑模块之间用队列解耦出了问题只需要盯住出问题的那个任务就行。6.3 最后再分享一个小技巧调试RTOS任务调度时强烈建议给每个任务设置不同的优先级和延时时间并在每个任务入口打印一条带时间戳的日志。这样从一开始就能直观看到任务调度的全貌。我曾经花过整整一个晚上排查一个“任务不定期卡死”的问题最后发现不过是某个任务的栈给小了而当时的代码里没开栈溢出检测问题只能在系统跑近一个小时后才偶现。把configCHECK_FOR_STACK_OVERFLOW打开、把每个任务的栈余量打印出来的那一刻问题立刻现出原形。所以树棯永远不要觉得“能用就行”。把检测手段一层层加上去系统才真正算是可靠的。本文还有配套的精品资源点击获取