STM32F103R8T6 FreeRTOS移植实战:配置、避坑与验证 简介这份资源面向嵌入式开发初学者与需要上手实时操作系统的工程师提供将FreeRTOS移植到STM32F103R8T6ARM Cortex-M3内核的完整工程。压缩包共226个文件约5.87MB以48个h头文件、39个c源文件为主辅以lst、o、d、crf等编译中间文件以及map、hex、uvprojx、uvoptx等工程与烧录产物。目录按HARDWARE、CORE、SYSTEM、FreeRTOS、USER、STM32F10x_FWLib等模块划分分别承载硬件驱动、内核启动与中断配置、系统时钟与内存管理、FreeRTOS内核源码及STM32适配、用户业务逻辑和标准外设库结构清晰便于按模块查阅。已有925人学习下载。通过该工程读者可掌握任务调度、信号量、队列等内核对象在Cortex-M3上的落地方式理解中断服务、堆栈分配与时钟配置的配合并借助现成工程快速验证LED、定时器、串口等外设任务为后续嵌入式实时系统开发打下基础。1. 拿到 STM32F103R8T6 的 FreeRTOS 移植包先别急着点编译如果你手头正好有一块 STM32F103R8T6 的核心板或者某个项目从裸机 while(1) 轮询切到 RTOS 时卡在了第一步这个移植包大概率能省你半天时间。它解决的不是“FreeRTOS 是什么”这种概念问题而是把一个能跑的工程骨架直接摆在你面前启动文件、时钟配置、FreeRTOS 内核源码、任务创建示例、中断优先级分组这些容易在移植时翻车的地方都已经搭好了。适合两类人一是刚接触 RTOS、想找个能编译通过的工程对照着理解任务调度和内存配置的新手二是从别的芯片平台切到 STM32F103R8T6、需要快速验证 FreeRTOS 能否稳定跑起来的熟手。但要注意移植包不是万能钥匙时钟树、堆栈大小、中断向量表偏移这些参数必须按你的实际板子和外设来改直接烧录大概率跑不起来。2. 拆开压缩包FreeRTOS 移植包里到底有什么2.1 目录结构与关键文件定位拿到一个 FreeRTOS 移植包第一件事不是打开 Keil 或 IAR 直接编译而是先看清楚目录里哪些是内核源码、哪些是芯片厂商的库、哪些是移植层胶水代码。常见的目录组织方式是这样的STM32F103R8T6_FreeRTOS/ ├── Libraries/ │ ├── CMSIS/ # 内核头文件与启动文件 │ └── STM32F10x_StdPeriph_Driver/ # 标准外设库 ├── FreeRTOS/ │ ├── Source/ │ │ ├── tasks.c # 任务调度核心 │ │ ├── queue.c # 队列与信号量 │ │ ├── list.c # 链表实现 │ │ ├── timers.c # 软件定时器 │ │ └── portable/ │ │ ├── RVDS/ARM_CM3/ # Cortex-M3 移植层 │ │ └── MemMang/ # 内存管理方案 │ └── include/ # FreeRTOS 配置头文件 ├── User/ │ ├── main.c # 任务创建入口 │ ├── stm32f10x_it.c # 中断服务函数 │ └── FreeRTOSConfig.h # 裁剪与参数配置 └── Project/ # 工程文件这里最容易被忽略的是FreeRTOS/portable/下的内容。Cortex-M3 的移植层通常放在RVDS/ARM_CM3或GCC/ARM_CM3目录下里面有一个port.c和portmacro.h。port.c负责实现任务上下文切换的汇编部分portmacro.h定义了临界区进出、栈增长方向等宏。如果你用的编译器是 Keil MDK选 RVDS 目录如果是 GCC 或 IAR对应目录不同选错了会在链接阶段报一堆未定义符号。内存管理方案在MemMang目录下通常有heap_1.c到heap_5.c五个文件。新手最容易在这里翻车——不是随便选一个就行后面会细说。2.2 内核源码与芯片库的版本匹配STM32F103R8T6 属于 F1 系列标准外设库的版本一般是 V3.5.0。FreeRTOS 内核版本从 V8 到 V10 都有移植包里带的可能是 V9 或 V10。版本本身不是大问题但要注意两个匹配点第一FreeRTOSConfig.h里的configUSE_PORT_OPTIMISED_TASK_SELECTION宏。如果设为 1任务切换用硬件前导零指令Cortex-M3 支持速度快但任务优先级数量受限通常 32 个。如果设为 0用软件循环查找优先级数量不限但切换稍慢。F103R8T6 的 Flash 和 RAM 都够用一般设 1 没问题。第二中断优先级分组。Cortex-M3 的 NVIC 支持 4 位优先级FreeRTOS 默认用最低 4 位中的一部分做内核中断优先级。FreeRTOSConfig.h里的configKERNEL_INTERRUPT_PRIORITY和configMAX_SYSCALL_INTERRUPT_PRIORITY必须和NVIC_PriorityGroupConfig()的设置一致。常见做法是在main()开头调用NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4)所有 4 位都用于抢占优先级然后configKERNEL_INTERRUPT_PRIORITY设为 15最低configMAX_SYSCALL_INTERRUPT_PRIORITY设为 5 或更低。这样优先级高于 5 的中断不受 FreeRTOS 临界区屏蔽适合硬实时外设。提示如果编译时报configMAX_SYSCALL_INTERRUPT_PRIORITY未定义检查FreeRTOSConfig.h是否被正确包含以及工程的头文件搜索路径是否指向了FreeRTOS/include。3. 从裸机到 RTOS移植步骤与参数配置3.1 启动文件与时钟配置的适配STM32F103R8T6 的启动文件通常是startup_stm32f10x_md.s中容量因为 R8T6 的 Flash 是 64KB属于中容量产品。如果你拿到的移植包里启动文件是startup_stm32f10x_hd.s大容量编译不会报错但中断向量表可能对不上运行时会出现中断不响应或跑飞。检查方法很简单打开启动文件看Stack_Size和Heap_Size的定义以及中断向量表的条目数。中容量和大容量的向量表前 60 个左右是相同的但后面外设中断的偏移不同。时钟配置在system_stm32f10x.c里默认是 72MHz。F103R8T6 最高主频就是 72MHz所以一般不用改。但如果你用的是外部晶振 8MHz而HSE_VALUE宏定义成了 8000000那SystemInit()会自动倍频到 72MHz。如果板子上没有晶振或者晶振频率不对系统会退到内部 8MHz RC 振荡器FreeRTOS 的configCPU_CLOCK_HZ必须同步改成 8000000否则vTaskDelay()的延时时间会差 9 倍。// system_stm32f10x.c 中的关键宏 #define SYSCLK_FREQ_72MHz 72000000 // FreeRTOSConfig.h 中必须匹配 #define configCPU_CLOCK_HZ ( ( unsigned long ) 72000000 ) #define configTICK_RATE_HZ ( ( TickType_t ) 1000 )configTICK_RATE_HZ设为 1000 表示 1ms 一个 tick这是最常用的值。设太高比如 10000会增加调度开销设太低比如 100会让vTaskDelay()的精度变差。F103R8T6 的 SysTick 是 24 位递减计数器72MHz 下最大计数值 16777215对应约 233ms所以 1ms 的 tick 完全没问题。3.2 FreeRTOSConfig.h 里必须改的五个参数FreeRTOSConfig.h是整个移植的核心配置文件参数几十个但真正影响能否跑起来的主要是下面五个参数推荐值作用改错后果configTOTAL_HEAP_SIZE10*1024系统总堆大小太小创建任务失败configMINIMAL_STACK_SIZE128空闲任务栈深度太小空闲任务溢出configCHECK_FOR_STACK_OVERFLOW2栈溢出检测设为 0 时溢出无提示configUSE_MALLOC_FAILED_HOOK1内存分配失败钩子设为 0 时失败后死机configASSERT自定义断言宏不定义时参数错误无提示configTOTAL_HEAP_SIZE是新手最容易设错的地方。F103R8T6 有 20KB RAM刨去全局变量、栈、堆留给 FreeRTOS 堆的空间通常 8KB 到 12KB。如果你创建了 3 个任务每个任务栈 256 字1KB再加上队列和信号量的开销10KB 是比较稳妥的起点。设成 20KB 编译能过但运行时一创建任务就返回失败因为链接器已经把 RAM 分配完了。// FreeRTOSConfig.h 中的关键配置 #define configTOTAL_HEAP_SIZE ( ( size_t ) ( 10 * 1024 ) ) #define configMINIMAL_STACK_SIZE ( ( unsigned short ) 128 ) #define configCHECK_FOR_STACK_OVERFLOW 2 #define configUSE_MALLOC_FAILED_HOOK 1 // 栈溢出钩子函数必须自己实现 void vApplicationStackOverflowHook( TaskHandle_t xTask, char *pcTaskName ) { ( void ) xTask; ( void ) pcTaskName; for( ;; ); // 死循环配合调试器查看哪个任务溢出 }configCHECK_FOR_STACK_OVERFLOW设为 2 时FreeRTOS 会在任务切换时检查栈指针是否越界并在栈末尾写入魔术字。如果检测到溢出会调用vApplicationStackOverflowHook。这个钩子函数必须自己实现否则链接会报错。我一般会在钩子里把任务名打印到串口或者直接点灯方便定位是哪个任务栈不够。3.3 内存管理方案选型heap_1 到 heap_5 怎么选FreeRTOS 提供了五种内存管理方案移植包里通常只带了其中一两个。选错了不会编译报错但运行时会出各种玄学问题。heap_1.c最简单只支持分配不支持释放。适合任务和队列在系统启动时一次性创建、之后不再删除的场景。优点是代码小、无碎片缺点是没法动态创建删除任务。heap_2.c支持释放但不会合并相邻空闲块。如果频繁创建删除不同大小的任务会产生碎片最终分配失败。不推荐在新项目里用。heap_3.c直接包装了标准库的malloc()和free()需要自己实现pvPortMalloc的线程安全。优点是简单缺点是标准库的堆管理和 FreeRTOS 的调度器可能冲突尤其是在中断里调用pvPortMalloc时。heap_4.c支持释放并合并相邻空闲块是最常用的方案。适合大多数需要动态创建删除任务的场景。缺点是代码比 heap_1 大一些但 F103R8T6 的 64KB Flash 完全放得下。heap_5.c在 heap_4 基础上支持多个不连续的内存区域适合外扩 SRAM 的场景。F103R8T6 没有外扩内存一般用不到。我一般会选heap_4.c然后在FreeRTOSConfig.h里把configTOTAL_HEAP_SIZE设为 10KB。如果项目里任务数量固定也可以选heap_1.c省点空间。但要注意heap_1.c下调用vTaskDelete()会直接触发断言失败因为根本没有释放逻辑。// 在 FreeRTOS/Source/portable/MemMang/ 目录下只保留一个 heap 文件 // 比如只保留 heap_4.c其他 heap_x.c 从工程中移除 // 否则链接时会报重复定义 // 创建任务的典型写法 xTaskCreate( vTask1, Task1, 128, NULL, 2, NULL ); xTaskCreate( vTask2, Task2, 256, NULL, 1, NULL ); vTaskStartScheduler(); // 启动调度器正常情况下不会返回xTaskCreate的第三个参数是栈深度单位是字4 字节不是字节。128 表示 512 字节。第四个参数是传递给任务的参数指针第五个是优先级数字越大优先级越高。最后一个参数是任务句柄不需要的话传 NULL。4. 避坑排查移植 FreeRTOS 时最容易翻车的五个地方4.1 现象编译通过但一启动就进 HardFault原因通常是中断优先级分组没设对或者configMAX_SYSCALL_INTERRUPT_PRIORITY的值和实际使用的中断优先级冲突。FreeRTOS 的port.c里会在vPortStartFirstTask()中触发 SVC 异常来启动第一个任务如果 NVIC 分组不对SVC 异常的优先级可能被设成了 0导致一进异常就嵌套 HardFault。解决方法是确保main()开头调用NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4)并且FreeRTOSConfig.h里configKERNEL_INTERRUPT_PRIORITY设为 15configMAX_SYSCALL_INTERRUPT_PRIORITY设为 5。另外检查stm32f10x_it.c里是否重复定义了SVC_Handler、PendSV_Handler、SysTick_Handler。FreeRTOS 的port.c已经实现了这三个如果芯片库的中断文件里也有链接时会报重复定义或者运行时跳到了错误的处理函数。4.2 现象任务创建成功但串口只打印一次就卡死原因多半是栈溢出。F103R8T6 的 RAM 只有 20KB如果configTOTAL_HEAP_SIZE设得太大链接器会把其他全局变量挤到栈区导致主栈和任务栈重叠。或者任务栈本身设得太小任务里调用了printf这种吃栈的函数一调用就溢出。解决方法是先把configCHECK_FOR_STACK_OVERFLOW设为 2在vApplicationStackOverflowHook里点灯或打印任务名。然后逐步增大可疑任务的栈深度每次加 64 字。另外注意printf在 Keil 里默认使用微库MicroLIB栈消耗比标准库小但如果你在任务里用了浮点格式化栈需求会急剧增加。常见做法是把printf换成sprintf到缓冲区再通过串口发送或者直接用ITM_SendChar输出。4.3 现象vTaskDelay 延时时间不对差了好几倍原因通常是configCPU_CLOCK_HZ和实际系统时钟不匹配。比如板子用的是 8MHz 晶振但SystemInit()因为HSE_VALUE定义错误没有正确倍频系统实际跑在 8MHz而configCPU_CLOCK_HZ还写着 72000000。这样vTaskDelay(1000)实际延时是 9 秒而不是 1 秒。解决方法是先用示波器或逻辑分析仪测一下 GPIO 翻转频率确认系统时钟。或者在main()里读取SystemCoreClock变量通过串口打印出来。如果发现是 8000000检查system_stm32f10x.c里的HSE_VALUE是否和板子晶振一致以及SystemInit()里的 PLL 配置是否正确。4.4 现象中断里调用 FreeRTOS API 后系统死机原因是在中断服务函数里调用了非FromISR版本的 API。比如在串口中断里调用xQueueSend()而不是xQueueSendFromISR()会导致调度器状态混乱最终死机。解决方法是所有在中断里调用的 FreeRTOS API 必须带FromISR后缀并且中断优先级不能高于configMAX_SYSCALL_INTERRUPT_PRIORITY。如果某个中断的实时性要求极高必须高于这个优先级那它里面不能调用任何 FreeRTOS API只能操作硬件寄存器。另外FromISR版本的 API 通常需要一个BaseType_t xHigherPriorityTaskWoken参数在退出中断前要用portYIELD_FROM_ISR()触发任务切换。// 串口中断中发送队列的正确写法 void USART1_IRQHandler( void ) { BaseType_t xHigherPriorityTaskWoken pdFALSE; uint8_t data USART_ReceiveData( USART1 ); xQueueSendFromISR( xUartQueue, data, xHigherPriorityTaskWoken ); portYIELD_FROM_ISR( xHigherPriorityTaskWoken ); }4.5 现象移植后编译报错 “undefined symbol vApplicationIdleHook”原因是FreeRTOSConfig.h里把configUSE_IDLE_HOOK设为了 1但没有实现vApplicationIdleHook()函数。类似的还有configUSE_TICK_HOOK、configUSE_MALLOC_FAILED_HOOK等。解决方法是把不用的钩子宏设为 0或者实现对应的空函数。我一般会保留configUSE_MALLOC_FAILED_HOOK和configCHECK_FOR_STACK_OVERFLOW因为这两个在调试阶段非常有用。空闲钩子和 tick 钩子如果不用就关掉省得链接报错。5. 进阶验证用 GPIO 翻转和串口输出确认调度器真的在跑移植完成后怎么确认 FreeRTOS 真的在调度任务而不是碰巧跑通了最直接的方法是用两个不同优先级的任务翻转 GPIO然后用示波器看波形。高优先级任务翻转 PA0低优先级任务翻转 PA1如果调度器正常PA0 的翻转频率应该明显高于 PA1因为高优先级任务会抢占低优先级任务。// 任务1高优先级翻转 PA0 void vTaskHigh( void *pvParameters ) { for( ;; ) { GPIO_SetBits( GPIOA, GPIO_Pin_0 ); GPIO_ResetBits( GPIOA, GPIO_Pin_0 ); taskYIELD(); // 主动让出 CPU让低优先级任务有机会跑 } } // 任务2低优先级翻转 PA1 void vTaskLow( void *pvParameters ) { for( ;; ) { GPIO_SetBits( GPIOA, GPIO_Pin_1 ); GPIO_ResetBits( GPIOA, GPIO_Pin_1 ); vTaskDelay( 1 ); // 延时 1ms让高优先级任务跑 } }如果 PA0 的波形频率远高于 PA1说明抢占式调度正常。如果两个波形频率差不多检查configUSE_PREEMPTION是否设为了 1。如果 PA0 完全没有波形检查任务是否创建成功以及vTaskStartScheduler()是否被调用。另一个验证方法是串口输出任务状态。FreeRTOS 提供了vTaskList()函数可以把所有任务的状态、优先级、剩余栈空间打印出来。但vTaskList()会占用不少栈空间建议单独创建一个低优先级任务来调用它并且把configUSE_TRACE_FACILITY和configUSE_STATS_FORMATTING_FUNCTIONS设为 1。// 任务状态打印任务 void vTaskStats( void *pvParameters ) { char pcWriteBuffer[ 512 ]; for( ;; ) { vTaskList( pcWriteBuffer ); printf( Task Name\tState\tPrio\tStack\tNum\n ); printf( %s\n, pcWriteBuffer ); vTaskDelay( 5000 ); } }vTaskList()输出的 Stack 列是任务剩余栈空间单位是字。如果某个任务的剩余栈长期低于 20 字说明栈快溢出了需要增大。State 列中 R 表示就绪B 表示阻塞S 表示挂起D 表示删除。如果看到某个任务一直是 R 但优先级很低说明高优先级任务可能卡在某个死循环里没让出 CPU。从那以后我每次移植完 FreeRTOS都会先跑一遍 GPIO 翻转和vTaskList()确认调度器和栈空间都正常再往上加业务代码。希望帮到你。本文还有配套的精品资源点击获取