公司动态

手动移植FreeRTOS V9.00到STM32F103:从内核原理到工程实践

📅 2026/8/19 15:27:01
手动移植FreeRTOS V9.00到STM32F103:从内核原理到工程实践
1. 项目缘起为什么还要手动移植FreeRTOS V9.00如果你手头正好有一块经典的“蓝桥杯”或“最小系统板”——STM32F103C8T6并且想给它跑一个实时操作系统RTOS你可能会发现现在网上的教程大多基于CubeMXHAL库FreeRTOS的最新版一键生成方便是方便但总感觉少了点什么。就像吃泡面虽然能饱腹但你永远不知道汤料包是怎么调出来的。这次我们不借助CubeMX的“魔法”也不依赖最新版FreeRTOS的复杂封装而是回归到相对经典的FreeRTOS V9.00和标准外设库StdPeriph_Lib进行一次彻底的手动移植。为什么是V9.00这个版本在FreeRTOS的发展史上是一个重要的稳定节点它已经包含了任务通知、流缓冲区等现代特性代码结构清晰又没有后期版本为了兼容更多架构而引入的过度抽象层非常适合学习其内核机理。而STM32F103C8T6作为ARM Cortex-M3内核的“国民MCU”其硬件资源64KB Flash20KB RAM对于运行一个裁剪过的FreeRTOS内核绰绰有余是实践嵌入式RTOS的绝佳平台。手动移植的意义在于“知其所以然”。你会清晰地知道每一个中断向量如何重定向系统节拍定时器SysTick如何被接管堆栈空间如何分配以及任务上下文切换时CPU的寄存器是如何被压栈和弹栈的。这个过程会让你对RTOS的理解从“会用API”深入到“理解机制”未来遇到任何RTOS相关的诡异问题你都能从底层找到排查方向。网上很多关于configTICK_RATE_HZ配置错误、堆栈溢出、优先级反转的问题其根源都能在移植阶段找到线索。2. 移植前的核心准备理清代码与硬件的关系手动移植本质上是在目标硬件STM32F103C8T6和操作系统内核FreeRTOS V9.00之间搭建一座桥梁。这座桥有两端一端是CPU核心Cortex-M3另一端是具体的MCU外设主要是定时器。我们的工作就是让FreeRTOS内核能在这块具体的芯片上“跑起来”。2.1 获取正确的源代码首先你需要准备两份核心代码FreeRTOS V9.00 内核源码可以从FreeRTOS官网的历史版本页面下载。解压后我们主要关注FreeRTOS/Source目录下的内容。里面包含了任务调度、队列、信号量等所有核心文件以及针对不同处理器架构的portable文件夹。STM32标准外设库对于F1系列通常使用V3.5.0版本。这个库提供了对STM32F103所有外设GPIO、USART、TIM等的寄存器级操作接口是我们初始化硬件、配置时钟的基础。2.2 理解FreeRTOS的“可移植层”FreeRTOS的精妙设计在于其分层架构。内核核心代码tasks.c,queue.c等是平台无关的。而与硬件相关的部分全部抽象在portable目录下。对于Cortex-M3内核的STM32F103我们需要重点关注portable/GCC/ARM_CM3如果你使用GCC编译器如STM32CubeIDE或Arm GNU Toolchain或portable/RVDS/ARM_CM3如果你使用Keil MDK-ARM。这个ARM_CM3文件夹里的文件就是我们要修改和适配的“桥梁”核心主要包括port.c: 包含了任务调度器启动、上下文切换、系统节拍中断服务例程等最底层的汇编/C混合代码。portmacro.h: 定义了与编译器、处理器相关的数据类型、宏和函数如portBASE_TYPE基本数据类型、portENTER_CRITICAL()进入临界区的实现。一个关键的认知FreeRTOS需要一个稳定的时基来驱动其任务调度和时间管理这个时基就是“Tick”节拍。在Cortex-M内核上通常由SysTick定时器来产生这个节拍中断。port.c中的xPortSysTickHandler函数就是SysTick的中断服务程序ISR它调用xTaskIncrementTick()和vTaskSwitchContext()是系统心跳的来源。2.3 建立你的工程骨架在你的项目目录下建议建立如下清晰的文件夹结构这能有效管理依赖避免混乱YourProject/ ├── Core/ │ ├── Inc/ // 用户头文件如 main.h, stm32f10x_conf.h │ ├── Src/ // 用户源文件如 main.c, stm32f10x_it.c中断服务程序 │ └── Startup/ // 启动文件 startup_stm32f10x_md.s对于C8T6用md中等密度 ├── Drivers/ │ ├── CMSIS/ // Cortex-M3内核抽象层包含 core_cm3.h 等 │ └── STM32F10x_StdPeriph_Driver/ // 标准外设库源码 ├── FreeRTOS/ │ ├── Source/ │ │ ├── include/ // FreeRTOS内核头文件 │ │ ├── portable/ │ │ │ └── GCC/ARM_CM3/ // 我们重点关注的移植层文件 │ │ ├── tasks.c │ │ ├── queue.c │ │ ├── list.c │ │ └── ... // 其他需要的内核文件 │ └── Demo/ // 官方Demo可参考其配置但不要直接复制 └── README.md在IDE如Keil或STM32CubeIDE中创建新工程选择设备为STM32F103C8并将上述文件夹路径添加到工程的包含路径和源文件组中。特别注意启动文件startup_stm32f10x_md.s必须正确添加它定义了中断向量表是芯片上电后执行的第一段代码。3. 核心移植步骤详解从“点灯”到“心跳”移植工作可以分解为几个关键阶段我们目标是让一个简单的LED闪烁任务在FreeRTOS调度下运行。3.1 第一步基础工程与时钟树配置在main.c中我们首先要像裸机编程一样初始化系统时钟和基本外设。STM32F103C8T6最高可运行在72MHz。使用标准库配置时钟的代码大家应该很熟悉这里的关键是确保系统时钟SystemCoreClock这个全局变量被正确设置为72000000。因为FreeRTOS的某些时间计算如vTaskDelay可能会间接依赖它虽然其内核主要依靠SysTick。// main.c 片段 #include stm32f10x.h #include FreeRTOS.h #include task.h // 简单的LED任务函数原型 static void vLEDTask(void *pvParameters); int main(void) { // 1. 初始化系统时钟HCLK 72MHz SystemInit(); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE); GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin GPIO_Pin_13; // 假设LED在PC13 GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOC, GPIO_InitStructure); // 2. 创建第一个任务 xTaskCreate(vLEDTask, LED, configMINIMAL_STACK_SIZE, NULL, tskIDLE_PRIORITY 1, NULL); // 3. 启动调度器永不返回 vTaskStartScheduler(); // 4. 如果调度器启动失败才会执行到这里 while(1); }3.2 第二步裁剪与配置FreeRTOSFreeRTOS通过一个名为FreeRTOSConfig.h的配置文件进行高度定制。这个文件需要我们自己创建并放在编译器能找到的路径通常放在Core/Inc或项目根目录。你可以从FreeRTOS官方Demo中找一个FreeRTOSConfig.h作为模板但必须根据STM32F103C8T6的资源进行大幅裁剪。以下是针对C8T620KB RAM的关键配置示例每一行配置都直接影响系统行为和资源消耗// FreeRTOSConfig.h #ifndef FREERTOS_CONFIG_H #define FREERTOS_CONFIG_H /* 1. 内核基础配置 */ #define configUSE_PREEMPTION 1 // 使用抢占式调度 #define configUSE_IDLE_HOOK 0 // 为了简化先不用空闲任务钩子 #define configUSE_TICK_HOOK 0 // 不用时间片钩子 #define configCPU_CLOCK_HZ ( ( unsigned long ) 72000000 ) // CPU主频非常重要 #define configTICK_RATE_HZ ( ( TickType_t ) 1000 ) // 系统节拍频率1ms一个Tick #define configMAX_PRIORITIES ( 5 ) // 最大优先级数不宜过多 #define configMINIMAL_STACK_SIZE ( ( unsigned short ) 128 ) // 空闲任务栈大小字 #define configTOTAL_HEAP_SIZE ( ( size_t ) ( 10 * 1024 ) ) // 堆总大小用于动态创建任务/队列约10KB /* 2. 功能裁剪节省ROM/RAM*/ #define configUSE_MUTEXES 1 // 使用互斥量 #define configUSE_RECURSIVE_MUTEXES 1 // 使用递归互斥量 #define configUSE_COUNTING_SEMAPHORES 1 // 使用计数信号量 #define configUSE_16_BIT_TICKS 0 // Cortex-M3是32位机用32位Tick计数器 #define configUSE_APPLICATION_TASK_TAG 0 // 关闭高级功能 #define configUSE_TASK_NOTIFICATIONS 1 // 开启任务通知高效的IPC方式 /* 3. 内存分配方案 */ #define configSUPPORT_DYNAMIC_ALLOCATION 1 // 使用动态内存分配heap_4.c #define configSUPPORT_STATIC_ALLOCATION 0 // 暂时不用静态分配 /* 4. 钩子函数与调试 */ #define configCHECK_FOR_STACK_OVERFLOW 2 // 栈溢出检查级别2较强检查 #define configUSE_TRACE_FACILITY 0 // 关闭可视化跟踪节省代码 #define configUSE_STATS_FORMATTING_FUNCTIONS 0 // 关闭统计格式化函数 /* 5. 与移植层相关的关键配置 */ #define configKERNEL_INTERRUPT_PRIORITY 255 // 内核中断优先级最低用8位表达时 #define configMAX_SYSCALL_INTERRUPT_PRIORITY 191 // 可调用FromISR API的最高中断优先级 /* 解释Cortex-M3优先级数值越小优先级越高。255对应最低优先级。 191是0xB0意味着优先级高于此值的中断不能调用FreeRTOS的API如xQueueSendFromISR。 这是保证内核数据一致性的关键。*/ /* 包含处理器特定的定义 */ #include stm32f10x.h /* 断言用于调试 */ extern void vAssertCalled( const char *pcFile, unsigned long ulLine ); #define configASSERT( x ) if( ( x ) 0 ) vAssertCalled( __FILE__, __LINE__ ) #endif /* FREERTOS_CONFIG_H */配置难点解析configTICK_RATE_HZ与configCPU_CLOCK_HZconfigCPU_CLOCK_HZ必须准确设置为你的系统时钟频率72MHz因为port.c中的vPortSetupTimerInterrupt()函数它配置SysTick会使用这个宏来计算重装载值。configTICK_RATE_HZ决定了系统的时间粒度。设为1000意味着vTaskDelay(1000)会延时1秒。这个值越大调度器响应越快但中断开销也越大。对于STM32F1031000Hz1ms是一个常用且稳定的选择。一个经典错误在port.c中如果configCPU_CLOCK_HZ定义错误会导致SysTick重装载值计算溢出或为0编译时可能报错#error directive: configTICK_RATE_HZ must be greater than 0或者系统运行后Tick频率完全不对。3.3 第三步修改启动文件与中断向量这是手动移植最核心、最容易出错的一步。Cortex-M3的中断向量表定义在启动文件.s汇编文件中。FreeRTOS需要接管SysTick中断系统节拍和PendSV中断上下文切换。找到并修改启动文件在Core/Startup/目录下找到startup_stm32f10x_md.s。用文本编辑器打开找到中断向量表部分通常是一系列DCD指令。重定向中断服务程序找到SysTick_Handler对应的那一行。默认它可能指向一个默认处理函数Default_Handler。将其修改为指向FreeRTOS的SysTick处理函数。关键点来了FreeRTOS在port.c中定义的SysTick ISR名字是xPortSysTickHandler而不是SysTick_Handler。我们需要在汇编层面做一次“重命名”或“弱定义覆盖”。更稳妥的做法是不修改启动文件里的向量名而是在你的工程中例如在stm32f10x_it.c里定义一个名为SysTick_Handler的函数在这个函数内部直接调用xPortSysTickHandler()。因为启动文件里DCD SysTick_Handler是固定的链接器会链接我们定义的强符号。// 在 stm32f10x_it.c 中 #include “FreeRTOS.h” #include “task.h” extern void xPortSysTickHandler(void); void SysTick_Handler(void) { xPortSysTickHandler(); }处理PendSV和SVC中断同样的方法处理PendSV_Handler和SVC_Handler。FreeRTOS用PendSV来进行低优先级的上下文切换用SVC来启动调度器。void PendSV_Handler(void) { extern void xPortPendSVHandler(void); xPortPendSVHandler(); } void SVC_Handler(void) { extern void vPortSVCHandler(void); vPortSVCHandler(); }注意vPortSVCHandler只在vTaskStartScheduler()内部被调用一次用于启动第一个任务。关闭默认中断函数确保stm32f10x_it.c中的默认弱定义SysTick_Handler,PendSV_Handler,SVC_Handler被我们上面的强定义覆盖或者被注释/删除避免冲突。3.4 第四步实现内存管理并编写测试任务FreeRTOS提供了几种堆内存管理方案heap_1.c到heap_5.c。对于初学者heap_4.c是最佳选择它支持内存释放、碎片合并功能比较全面。将FreeRTOS/Source/portable/MemMang/heap_4.c添加到你的工程中。configTOTAL_HEAP_SIZE就为这个堆分配总大小。现在回到main.c完善我们的LED任务static void vLEDTask(void *pvParameters) { const TickType_t xDelay500ms pdMS_TO_TICKS(500); // 将毫秒转换为Tick数更规范 for(;;) { GPIO_WriteBit(GPIOC, GPIO_Pin_13, (BitAction)(1 - GPIO_ReadOutputDataBit(GPIOC, GPIO_Pin_13))); // 翻转LED vTaskDelay(xDelay500ms); // 延时500ms交出CPU控制权 } }pdMS_TO_TICKS是一个宏能根据configTICK_RATE_HZ正确地将毫秒时间转换为系统Tick数这样即使你改变了Tick频率延时时间依然是准确的。4. 编译、调试与排错实战指南完成代码编写后点击编译。你几乎一定会遇到错误和警告。别慌这是手动移植的必经之路。4.1 常见编译错误与解决方案..\freertos\portable\GCC\ARM_CM3\portmacro.h(73): error: #35: #error directive: “configTICK_RATE_HZ must be greater than 0”原因portmacro.h中有一处检查configTICK_RATE_HZ的定义。这个错误通常不是因为FreeRTOSConfig.h里没定义而是因为定义的值在预处理阶段未能正确传递到该文件。排查首先确认FreeRTOSConfig.h文件确实被包含在了编译路径中。在IDE的Options for Target - C/C - Include Paths里添加其所在目录。检查FreeRTOSConfig.h中configTICK_RATE_HZ的定义语法是否正确末尾是否有分号干扰。在portmacro.h报错的位置附近可以临时添加#warning “configTICK_RATE_HZ value is X”来打印其值或者查看预处理后的文件确认宏是否展开。undefined reference tovPortSVCHandler‘,xPortPendSVHandler‘,xPortSysTickHandler‘原因链接器找不到这些函数的实现。它们定义在port.c中。排查确认port.c文件已正确添加到工程的源文件组中。确认你使用的port.c路径正确是ARM_CM3下的那个。检查函数声明是否一致。在port.c中这些函数可能被声明为extern “C”在C环境下或就是普通的C函数。确保你在stm32f10x_it.c中用extern void声明时名字完全一致大小写敏感。大量关于uint32_t等类型未定义的错误原因FreeRTOS的基础类型定义依赖于stdint.h或标准库。在FreeRTOS.h中它会包含stdint.h。排查确保你的编译器支持C99标准stdint.h是C99的一部分。在Keil中勾选Options for Target - C/C - Language/Code Generation - C99 Mode。同时确保FreeRTOS.h能正确找到stdint.h通常CMSIS核心文件core_cm3.h会提供。4.2 链接与系统启动失败排查编译通过后下载程序到开发板。如果LED不闪烁或者程序根本跑不起来可以按以下步骤排查检查堆栈大小在启动文件startup_stm32f10x_md.s开头通常有堆栈大小的定义Stack_Size EQU 0x4001KB。FreeRTOS内核和任务会使用自己的堆栈但这个初始栈用于在调用main()函数和启动调度器之前的环境初始化。如果太小可能在初始化阶段就溢出。可以适当增大例如设为0x8002KB。检查configTOTAL_HEAP_SIZE如果堆大小设置不足xTaskCreate可能因为申请不到内存而失败返回errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY。你可以在创建任务后检查返回值。对于简单的LED任务10KB10240字节是足够的。使用调试器单步跟踪在main()函数开头设置断点看能否执行到。在vTaskStartScheduler()内部设置断点。这个函数会先调用xPortStartScheduler()在port.c中它会配置SysTick并触发SVC异常。如果能执行到SVC_Handler说明调度器启动流程前半部分正常。如果卡在某个地方很可能是中断优先级配置问题。回顾FreeRTOSConfig.h中的configKERNEL_INTERRUPT_PRIORITY和configMAX_SYSCALL_INTERRUPT_PRIORITY。在Cortex-M3中优先级寄存器是8位宽但通常只使用高4位[7:4]。所以数值2550xFF和1910xB0写入优先级寄存器时实际会被硬件左移4位成为最低和次低优先级。确保你没有在其他地方如NVIC初始化将SysTick或PendSV的优先级设置为高于configMAX_SYSCALL_INTERRUPT_PRIORITY。验证SysTick是否正常工作可以在SysTick_Handler里放一个GPIO翻转语句用示波器或逻辑分析仪测量引脚看是否有1ms对应1000Hz的方波产生。这是系统的“心跳”没有心跳调度器就无法工作。4.3 进阶调试栈溢出检测当你的任务越来越复杂栈溢出是常见且难以排查的问题。FreeRTOS提供了configCHECK_FOR_STACK_OVERFLOW选项。我们之前配置为2这是最强的检测级别。原理在任务切换时和任务栈被大量使用时检查任务栈顶附近的一个特定模式“魔术字”是否被修改。如果被修改说明栈已经向下生长并覆盖了它发生了溢出。使用你需要实现一个钩子函数vApplicationStackOverflowHook。当检测到溢出时内核会调用这个函数。void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName) { (void)xTask; // 消除未使用参数警告 // 在这里记录错误比如点亮一个错误指示灯或通过串口打印任务名pcTaskName GPIO_SetBits(GPIOC, GPIO_Pin_13); // 常亮LED表示错误 for(;;); // 死循环便于捕获 }通过这个函数你可以快速定位是哪个任务栈溢出然后去调整该任务的栈大小xTaskCreate的参数。5. 从移植到应用构建一个简单的多任务系统移植成功LED开始规律闪烁这只是万里长征第一步。接下来我们构建一个包含两个任务和一个信号量通信的小系统验证移植的完整性和稳定性。5.1 创建第二个任务串口打印假设我们使用USART1PA9 TX, PA10 RX来打印信息。首先用标准库初始化串口然后创建一个任务// 在main.c中 #include “stdio.h” // 用于printf // ... 串口初始化代码略... // 重定向printf到串口1 int fputc(int ch, FILE *f) { USART_SendData(USART1, (uint8_t) ch); while (USART_GetFlagStatus(USART1, USART_FLAG_TC) RESET); return ch; } static void vPrintTask(void *pvParameters) { int count 0; const TickType_t xDelay1000ms pdMS_TO_TICKS(1000); for(;;) { printf(“FreeRTOS is running! Count: %d\n”, count); vTaskDelay(xDelay1000ms); } } // 在main函数中创建任务 xTaskCreate(vPrintTask, “Print”, 256, NULL, tskIDLE_PRIORITY 1, NULL); // 分配稍大的栈5.2 实现任务间同步二进制信号量我们让LED任务每闪烁一次500ms就释放一个信号量。打印任务等待这个信号量收到后才打印一次。这样两个任务就同步起来了。// 全局变量信号量句柄 SemaphoreHandle_t xLedSemaphore NULL; static void vLEDTask(void *pvParameters) { const TickType_t xDelay500ms pdMS_TO_TICKS(500); for(;;) { GPIO_WriteBit(GPIOC, GPIO_Pin_13, (BitAction)(1 - GPIO_ReadOutputDataBit(GPIOC, GPIO_Pin_13))); xSemaphoreGive(xLedSemaphore); // 释放信号量 vTaskDelay(xDelay500ms); } } static void vPrintTask(void *pvParameters) { int count 0; for(;;) { if(xSemaphoreTake(xLedSemaphore, portMAX_DELAY) pdTRUE) { // 无限等待信号量 printf(“LED Toggled! Print Count: %d\n”, count); } } } // 在main函数中创建信号量 xLedSemaphore xSemaphoreCreateBinary(); if(xLedSemaphore NULL) { // 信号量创建失败处理错误 } // 然后创建两个任务...5.3 观察调度器行为与性能初探运行这个程序你应该能看到LED以1Hz频率闪烁同时串口每1秒打印一次信息。你可以尝试修改任务优先级将打印任务的优先级设置得比LED任务高观察打印是否会更频繁实际上因为打印任务在等待信号量它会阻塞所以优先级影响不大这是一个很好的理解阻塞状态的机会。使用vTaskList()如果开启了configUSE_TRACE_FACILITY和configUSE_STATS_FORMATTING_FUNCTIONS可以调用vTaskList()函数需要实现一个输出函数如通过串口来获取所有任务的状态就绪、阻塞、挂起、优先级、栈高水位线等信息。这对于优化栈空间分配至关重要。测量CPU使用率FreeRTOS有一个vTaskGetRunTimeStats()函数可以粗略统计每个任务占用CPU的时间百分比。这需要配置一个比SysTick精度更高的定时器如一个通用定时器作为运行时统计的时钟源。6. 移植后的优化与深度思考系统跑起来后我们可以从工程和原理层面进行一些优化和思考。6.1 内存管理的选择与优化我们使用了heap_4.c它适用于多次创建删除任务、队列的场景。但对于资源极其紧张RAM10KB或要求确定性不允许内存分配失败的系统可以考虑heap_1.c只分配不释放。适用于系统启动后所有资源都静态确定的应用最简单碎片风险为零。heap_2.c支持释放但会产生碎片不推荐在新项目中使用。heap_3.c简单包装了标准的malloc()和free()需要编译器提供堆实现。heap_5.c允许堆内存分布在多个不连续的内存区域适用于有外部RAM或内存分区的复杂芯片。对于STM32F103C8T620KB RAM如果只运行3-5个简单任务使用heap_4并分配6-8KB堆是合理的。你可以通过xPortGetFreeHeapSize()函数在运行时监控剩余堆大小辅助判断。6.2 中断嵌套与configMAX_SYSCALL_INTERRUPT_PRIORITY的再理解这是FreeRTOS在Cortex-M上移植的精华与难点。Cortex-M3支持中断嵌套。FreeRTOS通过configMAX_SYSCALL_INTERRUPT_PRIORITY划了一条“安全线”。高于此优先级的中断为“不可屏蔽”或“极高实时性”中断。它们绝对不能调用任何以FromISR结尾的FreeRTOS API如xQueueSendFromISR也不能使用portENTER_CRITICAL()/portEXIT_CRITICAL()。因为它们会打断内核可能导致数据不一致。这类中断应尽可能短小精悍。低于或等于此优先级的中断为“内核安全”中断。它们可以安全地调用FromISRAPI与任务通信。SysTick和PendSV的优先级必须设置为最低configKERNEL_INTERRUPT_PRIORITY以确保它们不会抢占这些“内核安全”中断从而保护内核数据结构的完整性。在你的实际应用中比如一个高速ADC采样定时器中断优先级高它只设置一个标志另一个处理数据的UART接收中断优先级较低它可以调用xQueueSendFromISR将数据发送给任务处理。这种设计确保了系统的实时性和可靠性。6.3 从标准外设库到HAL库的思考本次移植基于标准库。如果你未来要迁移到STM32CubeMX和HAL库思路是一致的但细节有变启动文件CubeMX生成的启动文件.s其中断向量名称是固定的如SysTick_Handler你依然需要实现这个函数去调用xPortSysTickHandler。系统时钟初始化CubeMX在main()之前通过SystemInit()函数初始化了时钟SystemCoreClock变量会被自动设置好。你只需在FreeRTOSConfig.h中正确设置configCPU_CLOCK_HZ。HAL的时间基准HAL库需要一个时间基准HAL_Delay,HAL_GetTick。默认它可能想用SysTick。但SysTick已经被FreeRTOS占用。解决方案是在FreeRTOSConfig.h中定义configOVERRIDE_DEFAULT_TICK_CONFIGURATION为1并实现void vApplicationSetupTickInterrupt(void)在这里面用另一个硬件定时器如TIM2为HAL提供Tick。或者更简单的方法是告诉HAL不要用SysTick。在stm32f1xx_hal_conf.h中将HAL_USE_TICK的默认提供者从SysTick改为其他定时器或者直接重写HAL_GetTick()函数让其返回xTaskGetTickCount()需要处理单位转换FreeRTOS的Tick可能是1ms而HAL默认期望1ms。这需要仔细处理避免冲突。手动移植FreeRTOS V9.00到STM32F103的过程是一次对RTOS内核、Cortex-M3架构以及嵌入式系统软硬件协同的深度探索。它强迫你去理解每一个配置项的含义每一个中断向量的流向每一字节内存的用途。当你亲手点亮LED看到任务在调度器下自如切换时那种对系统掌控感的确立是任何一键生成工具都无法给予的。这份经验将成为你解决更复杂嵌入式系统问题的坚实基石。