资讯中心

STM32F103C8T6从零移植FreeRTOS:标准库工程搭建与多任务实战

📅 2026/10/4 22:19:35
STM32F103C8T6从零移植FreeRTOS:标准库工程搭建与多任务实战
1. 为什么这块十几块钱的小板子值得折腾RTOSSTM32F103C8T6最小系统板大概是国内嵌入式圈子里保有量最大的开发板之一没有之一。淘宝上十几块钱就能买到一块板载8MHz晶振、32.768kHz RTC晶振、复位电路、BOOT跳线、SWD调试口核心参数是72MHz主频、64KB Flash、20KB SRAM、Cortex-M3内核。很多人拿它点个灯、跑个串口就吃灰了但说实话这块板子的性能跑一个轻量级RTOS绰绰有余。问题在于大部分教程要么直接甩一个现成工程让你编译下载要么一上来就讲FreeRTOS内核源码中间怎么从零搭出一个能跑RTOS的工程这一步是断层的。我自己带过几个新人发现他们卡住的地方往往不是RTOS本身而是标准库工程怎么建、FreeRTOS源码怎么加进Keil、中断向量表怎么处理、堆内存怎么分配这些脏活。这篇内容就是把这个断层补上。从Keil MDK新建工程开始到标准库配置、FreeRTOS源码移植、第一个多任务程序跑通每一步的来龙去脉都讲清楚。适合有C语言基础、用过STM32但没接触过RTOS的朋友也适合之前移植失败想搞清楚哪里出问题的人。整个过程不需要任何额外的下载器一块ST-Link或者DAPLink就够了。注意本文基于STM32标准外设库Standard Peripheral Library和FreeRTOS V10.x版本不涉及HAL库和CubeMX的自动生成流程。原因后面会讲。2. 动手之前先把这几个概念理清楚2.1 标准库和HAL库到底选哪个现在ST主推的是HAL库加CubeMX的组合图形化配置确实方便。但如果你要理解RTOS的移植过程标准库反而是更好的选择。原因很简单标准库的代码层次浅启动文件、中断向量表、时钟配置这些东西都是明摆着的你能看清楚每一层在干什么。HAL库封装太厚CubeMX一键生成之后很多人根本不知道FreeRTOS的SysTick是怎么被接管的。另外从实际项目角度说F103C8T6这种资源紧张的芯片标准库编译出来的代码体积通常比HAL库小10%到20%对于64KB Flash来说这个差距不算小。当然如果你已经习惯了CubeMX的工作流用HAL库移植FreeRTOS也完全可行只是本文的步骤需要相应调整。2.2 FreeRTOS的源码结构长什么样下载下来的FreeRTOS源码包从官网或GitHub获取里面东西很多但真正需要放进工程的只有两部分FreeRTOS/Source/ 目录下的核心文件tasks.c、queue.c、list.c、timers.c、event_groups.c、stream_buffer.c、croutine.c。其中croutine.c是协程功能一般不用可以不添加。FreeRTOS/Source/portable/ 目录下的移植层这里要分两个子目录选。编译器相关选RVDS/ARM_CM3Keil用的是ARMCC编译器RVDS目录就是给ARMCC用的内存管理选MemMang目录下的heap_1.c到heap_5.c之一。还有FreeRTOS/Source/include/目录下的所有头文件以及FreeRTOSConfig.h这个配置文件——这个文件不在源码包里需要自己从Demo目录里找一个改或者手写。2.3 移植的本质是什么说穿了FreeRTOS移植到Cortex-M3上核心就干三件事第一接管SysTick。FreeRTOS需要一个周期性的时钟节拍来驱动任务调度它默认用SysTick所以你要把标准库里的SysTick_Handler换成FreeRTOS的xPortSysTickHandler。第二接管SVC和PendSV两个异常。SVC用于启动第一个任务PendSV用于任务上下文切换。这两个异常的优先级必须配置为最低否则会影响其他中断的实时性。第三提供内存堆。FreeRTOS创建任务、队列、信号量都需要动态分配内存它自带了一套简单的内存管理方案heap_1到heap_5你需要选一个并把对应的.c文件加进工程。理解这三点移植过程中遇到的大部分报错你都能自己定位。3. 从空白工程到能点灯的裸机程序3.1 Keil工程创建与文件组织打开Keil MDKProject - New uVision Project选一个空目录器件选择STMicroelectronics - STM32F103 - STM32F103C8。弹出Copy CMSIS Core Files的对话框时选是让Keil自动把CMSIS内核文件复制过来。工程建好之后我习惯按下面的结构组织目录后面加FreeRTOS的时候不会乱Project/ ├── CMSIS/ # 内核相关Keil自动生成 ├── Startup/ # 启动文件 startup_stm32f10x_md.s ├── Library/ # 标准外设库 src 和 inc ├── User/ # main.c、stm32f10x_it.c、系统时钟配置 ├── FreeRTOS/ # 后面加 │ ├── src/ │ ├── inc/ │ └── portable/ └── Output/ # 编译输出在Keil里右键Target - Manage Project Items建立对应的Group把文件添加进去。标准外设库需要添加的文件至少包括stm32f10x_rcc.c、stm32f10x_gpio.c、stm32f10x_usart.c如果用串口调试、misc.c。启动文件选startup_stm32f10x_md.s因为C8T6属于中等容量产品Medium DensityFlash 64KB。提示标准外设库可以从ST官网下载STM32F10x_StdPeriph_Lib_V3.5.0这是最后一个版本之后ST就全力推HAL了。网上有些最新标准外设库的说法其实指的就是这个V3.5.0。3.2 时钟配置与宏定义在Options for Target - C/C - Define里需要添加两个宏USE_STDPERIPH_DRIVER和STM32F10X_MD。前者告诉标准库启用外设驱动后者告诉库当前芯片是中容量产品决定了启动文件和寄存器映射。时钟配置我一般直接写一个SystemInit的替代函数放在main.c里用HSE 8MHz倍频到72MHzvoid RCC_Configuration(void) { ErrorStatus HSEStartUpStatus; RCC_DeInit(); RCC_HSEConfig(RCC_HSE_ON); HSEStartUpStatus RCC_WaitForHSEStartUp(); if(HSEStartUpStatus SUCCESS) { FLASH_PrefetchBufferCmd(FLASH_PrefetchBuffer_Enable); FLASH_SetLatency(FLASH_Latency_2); RCC_HCLKConfig(RCC_SYSCLK_Div1); // AHB 72MHz RCC_PCLK2Config(RCC_HCLK_Div1); // APB2 72MHz RCC_PCLK1Config(RCC_HCLK_Div2); // APB1 36MHz RCC_PLLConfig(RCC_PLLSource_HSE_Div1, RCC_PLLMul_9); // 8*972 RCC_PLLCmd(ENABLE); while(RCC_GetFlagStatus(RCC_FLAG_PLLRDY) RESET); RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK); while(RCC_GetSYSCLKSource() ! 0x08); } }这段代码里FLASH_Latency_2是必须的72MHz下Flash需要2个等待周期不设置的话程序跑起来会莫名其妙死机。这是很多人容易忽略的点。3.3 验证裸机程序在main.c里写一个最简单的GPIO翻转PC13接LED最小系统板上的LED通常是PC13低电平点亮int main(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_Configuration(); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_13; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOC, GPIO_InitStructure); while(1) { GPIO_SetBits(GPIOC, GPIO_Pin_13); for(volatile int i0;i500000;i); GPIO_ResetBits(GPIOC, GPIO_Pin_13); for(volatile int i0;i500000;i); } }编译下载LED闪烁就说明裸机环境没问题。这一步必须先跑通不然后面RTOS出问题你分不清是裸机配置的锅还是移植的锅。4. FreeRTOS源码加入工程的完整操作4.1 文件复制与分组把FreeRTOS源码包里的文件按下面的方式复制到工程的FreeRTOS目录源路径目标位置说明Source/*.cFreeRTOS/src/核心源文件Source/include/*.hFreeRTOS/inc/核心头文件Source/portable/RVDS/ARM_CM3/port.cFreeRTOS/portable/移植层Source/portable/RVDS/ARM_CM3/portmacro.hFreeRTOS/portable/移植层头文件Source/portable/MemMang/heap_4.cFreeRTOS/portable/内存管理heap_4.c是我推荐的选择它支持内存释放和碎片合并比heap_1只能分配不能释放和heap_2不支持碎片合并更实用。heap_5支持多块不连续内存区域F103C8T6用不上。在Keil里新建两个GroupFreeRTOS_Core和FreeRTOS_Port分别把src下的.c文件和port.c、heap_4.c加进去。头文件路径在Options for Target - C/C - Include Paths里添加FreeRTOS/inc和FreeRTOS/portable。4.2 FreeRTOSConfig.h的关键配置项这个文件是整个移植过程中最需要动脑子的地方。它不在源码包里需要自己创建。我一般从Demo目录比如Demo/CORTEX_STM32F103_Keil里拷一份过来改比从零写省事。几个必须关注的配置#define configUSE_PREEMPTION 1 // 抢占式调度 #define configUSE_IDLE_HOOK 0 // 空闲钩子不用就关 #define configUSE_TICK_HOOK 0 // 节拍钩子 #define configCPU_CLOCK_HZ (72000000) // CPU频率 #define configTICK_RATE_HZ (1000) // 节拍频率1kHz #define configMAX_PRIORITIES (5) // 最大优先级数 #define configMINIMAL_STACK_SIZE (128) // 最小任务栈 #define configTOTAL_HEAP_SIZE ((size_t)(10*1024)) // 堆大小10KB #define configMAX_TASK_NAME_LEN (16) #define configUSE_16_BIT_TICKS 0 // 32位节拍计数 #define configUSE_MUTEXES 1 #define configUSE_COUNTING_SEMAPHORES 1 #define configUSE_TRACE_FACILITY 0 #define configCHECK_FOR_STACK_OVERFLOW 2 // 栈溢出检测 #define configUSE_MALLOC_FAILED_HOOK 1 // 内存分配失败钩子configTOTAL_HEAP_SIZE设成10KB因为F103C8T6总共只有20KB SRAM还要留给主栈、全局变量。这个值不能太大否则编译能过但运行起来会HardFault。configCHECK_FOR_STACK_OVERFLOW设成2是方法二检测比方法一更可靠但稍慢调试阶段建议开着。还需要在FreeRTOSConfig.h里声明几个中断处理函数的映射#define vPortSVCHandler SVC_Handler #define xPortPendSVHandler PendSV_Handler #define xPortSysTickHandler SysTick_Handler这三行是移植的核心。它的作用是告诉FreeRTOS当SVC、PendSV、SysTick异常发生时跳转到FreeRTOS提供的处理函数而不是标准库stm32f10x_it.c里的默认处理函数。4.3 中断向量表的冲突处理标准库的stm32f10x_it.c里默认定义了SVC_Handler、PendSV_Handler、SysTick_Handler这三个函数虽然里面是空的。如果你不做处理编译时会报重复定义的错误。解决办法有两个一是把stm32f10x_it.c里这三个函数注释掉二是用宏定义的方式重命名。我推荐第一种直接注释掉最干净。注释之后FreeRTOSConfig.h里的宏定义就会生效启动文件里的向量表会指向FreeRTOS的实现。注意如果你用的是startup_stm32f10x_md.s里面的向量表用的是弱定义WEAK所以只要你在C文件里定义了同名函数就会覆盖弱定义。这就是为什么注释掉stm32f10x_it.c里的函数之后FreeRTOS的port.c里的函数能生效。5. 第一个多任务程序与验证方法5.1 创建两个闪烁任务在main.c里包含FreeRTOS.h和task.h然后写两个任务函数#include FreeRTOS.h #include task.h void vTaskLED1(void *pvParameters) { while(1) { GPIO_ResetBits(GPIOC, GPIO_Pin_13); vTaskDelay(500 / portTICK_RATE_MS); GPIO_SetBits(GPIOC, GPIO_Pin_13); vTaskDelay(500 / portTICK_RATE_MS); } } void vTaskLED2(void *pvParameters) { while(1) { GPIO_ResetBits(GPIOA, GPIO_Pin_0); vTaskDelay(200 / portTICK_RATE_MS); GPIO_SetBits(GPIOA, GPIO_Pin_0); vTaskDelay(200 / portTICK_RATE_MS); } } int main(void) { RCC_Configuration(); // GPIO初始化省略 xTaskCreate(vTaskLED1, LED1, 128, NULL, 2, NULL); xTaskCreate(vTaskLED2, LED2, 128, NULL, 1, NULL); vTaskStartScheduler(); while(1); }两个任务优先级不同LED1优先级2LED2优先级1。如果移植成功你会看到两个LED以不同频率闪烁而且互不干扰。这比单任务延时闪烁更能说明RTOS在正常工作。5.2 用串口打印任务状态光看LED闪烁不够直观加一个串口输出任务状态更有说服力。配置USART1波特率115200然后在空闲任务钩子或者一个低优先级任务里调用vTaskListvoid vTaskMonitor(void *pvParameters) { char pcWriteBuffer[256]; while(1) { vTaskList(pcWriteBuffer); printf(Task Name\tState\tPrio\tStack\tNum\r\n); printf(%s\r\n, pcWriteBuffer); vTaskDelay(2000 / portTICK_RATE_MS); } }要使用vTaskList需要在FreeRTOSConfig.h里把configUSE_TRACE_FACILITY和configUSE_STATS_FORMATTING_FUNCTIONS都设为1。串口打印出来你会看到类似这样的内容Task Name State Prio Stack Num LED1 R 2 100 3 LED2 B 1 100 4 Monitor R 1 120 2 IDLE R 0 80 1State列里R是ReadyB是Blocked。看到这个输出说明调度器、任务栈、延时机制全部正常。5.3 常见编译错误与排查移植过程中最容易遇到的几个报错我列个表方便对照报错信息原因解决Symbol SVC_Handler multiply definedstm32f10x_it.c和port.c都定义了注释掉it.c里的undefined symbol xPortSysTickHandlerFreeRTOSConfig.h里没做宏映射添加三行宏定义cannot open source input file FreeRTOS.h头文件路径没加Include Paths里加inc目录region RAM overflowed堆设太大减小configTOTAL_HEAP_SIZE编译通过但运行HardFault中断优先级配置错误检查PendSV和SysTick优先级最后一条特别说一下。Cortex-M3的PendSV和SysTick优先级必须设为最低数值最大否则在中断里调用FreeRTOS的API会导致优先级反转问题。在port.c里FreeRTOS已经自动配置了但如果你在别的地方改了NVIC优先级分组可能会覆盖掉。建议在main函数开头调用NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4)全部用抢占优先级不用子优先级。6. 移植过程中那些文档不会告诉你的事6.1 堆内存分配的坑configTOTAL_HEAP_SIZE设成10KB但实际能用的没这么多。FreeRTOS的heap_4会在堆的开头放一个链表头每个分配的内存块也有头部信息实际可用内存大概要打八折。我实测过10KB的堆创建两个128字512字节栈的任务加一个队列剩余可用堆大概在7KB左右。如果你在创建任务时返回errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY不要急着加大堆先算一下每个任务的栈深度乘以4字节加上TCB结构体大概100字节再加上队列和信号量的开销。F103C8T6的20KB SRAM堆设10KB到12KB是比较安全的范围再大就要小心主栈溢出。6.2 栈溢出检测的实际效果configCHECK_FOR_STACK_OVERFLOW设成2的时候FreeRTOS会在任务切换时检查栈指针是否越界以及栈末尾的标记字节是否被改写。一旦检测到溢出会调用vApplicationStackOverflowHook。你需要自己实现这个钩子函数否则检测到了也没反应void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName) { printf(Stack overflow in task: %s\r\n, pcTaskName); while(1); }我建议调试阶段把这个钩子写成死循环加打印这样一旦溢出立刻能定位到是哪个任务。产品阶段可以改成软复位或者记录日志。6.3 中断里调用API的注意事项FreeRTOS的中断安全API都是以FromISR结尾的比如xQueueSendFromISR、xSemaphoreGiveFromISR。在中断里调用普通版本的API轻则数据错乱重则直接HardFault。还有一个细节中断优先级。Cortex-M3里优先级数值小于configMAX_SYSCALL_INTERRUPT_PRIORITY的中断不能调用任何FreeRTOS的API包括FromISR版本。这个宏在FreeRTOSConfig.h里定义默认是5对于4位优先级的情况。也就是说优先级0到4的中断是不受FreeRTOS管理的响应速度最快但不能调用API。优先级5到15的中断可以调用FromISR API。这个机制叫中断延迟处理目的是保证高优先级中断的实时性不被RTOS的临界区影响。实际项目中像电机控制、高速ADC采样这类对实时性要求极高的中断就设成高优先级里面只做最紧急的处理然后把事件通过队列发给任务去慢慢处理。6.4 标准库和FreeRTOS的SysTick冲突标准库的SysTick_Handler默认是空的但SysTick_Config函数会配置SysTick。如果你在别的地方调用了SysTick_Config它会覆盖FreeRTOS的配置。所以移植FreeRTOS之后不要再手动调用SysTick_Config也不要调用delay_ms这类基于SysTick的延时函数。要延时就用vTaskDelay。我见过有人移植完之后发现任务调度不正常查了半天发现是某个驱动文件里调用了SysTick_Config。这种问题很隐蔽因为编译不报错运行时才出问题。7. 从能跑到好用还差哪些配置7.1 任务优先级的分配策略FreeRTOS的优先级数值越大优先级越高configMAX_PRIORITIES设成5意味着优先级0到4可用。优先级0是空闲任务不要用。实际项目中我一般这样分优先级4紧急事件处理比如故障保护优先级3实时控制任务比如PID计算优先级2数据处理任务比如协议解析优先级1人机交互、日志输出优先级0空闲任务原则是越靠近硬件的、实时性要求越高的优先级越高。但不要把所有任务都设成高优先级那样等于没有优先级。7.2 队列和信号量的基本用法任务之间通信最常用的就是队列。比如串口接收中断收到数据后通过队列发给解析任务QueueHandle_t xUartQueue; // 中断里 void USART1_IRQHandler(void) { BaseType_t xHigherPriorityTaskWoken pdFALSE; uint8_t data USART_ReceiveData(USART1); xQueueSendFromISR(xUartQueue, data, xHigherPriorityTaskWoken); portYIELD_FROM_ISR(xHigherPriorityTaskWoken); } // 任务里 void vUartTask(void *pvParameters) { uint8_t data; while(1) { if(xQueueReceive(xUartQueue, data, portMAX_DELAY) pdTRUE) { // 处理数据 } } }portYIELD_FROM_ISR这行很关键。如果中断发送数据唤醒了更高优先级的任务这个宏会触发一次上下文切换让高优先级任务立即运行。不写的话要等到下一个SysTick才会切换响应会慢一个节拍。7.3 调试技巧用GPIO测量任务执行时间想知道某个任务实际跑了多久最土但最有效的办法是用GPIO。在任务开头拉高一个引脚结尾拉低用示波器看波形宽度void vMeasureTask(void *pvParameters) { while(1) { GPIO_SetBits(GPIOB, GPIO_Pin_0); // 被测代码 GPIO_ResetBits(GPIOB, GPIO_Pin_0); vTaskDelay(10 / portTICK_RATE_MS); } }没有示波器的话用逻辑分析仪也行几十块钱的就能用。这个方法比用串口打印时间戳准确得多因为串口本身有延迟。7.4 后续可以扩展的方向工程跑通之后可以往上加的东西很多。比如把LVGL图形库移植上去用SPI接一块ST7789屏幕做一个带界面的小设备。LVGL需要配置心跳和时基正好可以用FreeRTOS的xTaskGetTickCount来提供。再比如加入FreeModbus协议栈把F103C8T6做成一个Modbus从站通过RS485和上位机通信。内存紧张是F103C8T6永远的痛。如果任务多了堆不够用可以考虑把一些不常用的功能放到外部Flash或者换用STM32F103CBT6128KB Flash20KB SRAM引脚完全兼容代码不用改。我个人在实际操作中的体会是移植FreeRTOS最难的不是技术本身而是耐心。每一步都要验证裸机点灯验证、SysTick验证、第一个任务验证、任务间通信验证一步一步来。跳过任何一步后面出问题都要花更多时间回头查。我见过太多人直接拿别人的工程改结果连SysTick被谁接管了都不知道出了问题完全无从下手。自己从零搭一遍虽然慢但后面用起来心里有底。

看完文章,想为自己的企业也做一次专业网站诊断?

尧图顾问免费为您评估现有网站,并给出建站/改版建议与报价方案。

免费获取方案