ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

STM32F103 SPL集成FreeRTOS v202112.00实战指南与移植详解

STM32F103 SPL集成FreeRTOS v202112.00实战指南与移植详解 1. 项目缘起为什么要在SPL上集成FreeRTOS如果你手头有一块经典的STM32F103C8T6“蓝桥杯”开发板或者是从某个旧项目中翻出来的STM32F10x系列芯片想在上面跑一个实时操作系统那么你大概率会遇到一个选择是直接用HAL库配合CubeMX一键生成还是回头去折腾那个“古老”的标准外设库SPL我最近就遇到了这样一个需求一个老客户的老产品基于STM32F103代码全是SPL V3.6.2写的现在需要增加一个复杂的后台数据采集和通信任务裸机轮询已经捉襟见肘必须上RTOS。客户明确要求为了保持代码的稳定性和可维护性不希望大动干戈地迁移到HAL库就在现有的SPL框架上集成FreeRTOS。这个需求听起来简单但做起来坑不少。STM32CubeMX和HAL库已经把FreeRTOS的集成做得非常傻瓜化但SPL作为更底层的库需要你手动处理很多细节。网上能找到的教程要么是基于HAL的要么是FreeRTOS版本太老要么就是步骤跳跃缺了关键配置。特别是当你使用最新的FreeRTOS v202112.00版本这是FreeRTOS LTS长期支持版本的一个重要更新时很多旧的移植方法已经不适用了。我花了几天时间踩遍了能踩的坑终于把FreeRTOS v202112.00稳定地跑在了STM32F10x的SPL V3.6.2环境上。这篇文章我就把这个从零开始、手把手的过程连同所有的配置细节、编译错误解决方案和调试心得完整地记录下来。2. 环境准备与工程骨架搭建在开始动手之前我们需要把所有的原材料和工具准备好。这个阶段看似简单但工具链和源码版本的匹配是后续一切工作的基础一步错步步错。2.1 工具链与源码获取首先明确我们的核心组件MCU: STM32F10x系列以最常见的STM32F103C8T6为例。固件库: STM32F10x Standard Peripheral Library V3.6.2。这是ST官方为F10x系列提供的标准外设库相比HAL更接近寄存器代码量小在资源受限的老项目中很常见。RTOS: FreeRTOS v202112.00。从FreeRTOS官网或GitHub仓库下载。务必确认版本号v202112.00在任务通知、流缓冲区等方面有重要改进且是LTS版本。开发环境: Keil MDK-ARM我用的V5.36。也可以选择IAR或GCC ARM但本文以Keil为例因为它在国内STM32开发者中普及率最高。确保你的Keil安装了对应的STM32F10x Device Family Pack。我的工程目录结构是这样规划的My_FreeRTOS_SPL_Project/ ├── CMSIS/ # Cortex-M3核心支持文件来自SPL包 ├── STM32F10x_StdPeriph_Driver/ # SPL外设驱动源码 ├── FreeRTOS/ # FreeRTOS v202112.00源码 │ ├── Source/ │ │ ├── include/ # FreeRTOS头文件 │ │ ├── portable/ │ │ │ └── RVDS/ARM_CM3/ # 针对Cortex-M3的Keil移植层 │ │ └── ... # 其他.c文件tasks.c, queue.c等 │ └── Demo/ # 官方Demo我们只参考不复制 ├── User/ │ ├── main.c │ ├── stm32f10x_it.c # 中断服务程序 │ ├── system_stm32f10x.c │ └── ... # 其他用户文件 ├── MDK-ARM/ # Keil工程文件 └── ReadMe.txt注意FreeRTOS的portable文件夹下有很多编译器对应的移植层。对于Keil MDK和ARMCC编译器我们必须使用RVDS/ARM_CM3这个目录下的port.c和portmacro.h文件。这是第一个关键点选错了会导致编译出一堆底层汇编错误。2.2 创建Keil工程与基础配置在Keil中新建一个工程选择你的具体芯片型号例如STM32F103C8。然后将上述目录中的文件添加到工程对应的分组中CMSIS分组添加system_stm32f10x.c和core_cm3.c后者通常在Keil安装目录下也可从SPL包找。StdPeriph_Driver分组添加你需要用到的外设驱动.c文件如misc.cNVIC配置、gpio.c、usart.c等。切记misc.c必须添加它包含了NVIC的配置函数与FreeRTOS的中断管理密切相关。FreeRTOS/Source分组添加核心文件tasks.c,queue.c,list.c,timers.c。如果你需要任务通知、流缓冲区等新特性还需添加event_groups.c,stream_buffer.c等。FreeRTOS/Portable分组添加MemMang文件夹下的内存管理实现如heap_4.c这是最通用稳定的以及RVDS/ARM_CM3下的port.c。User分组添加你的main.c,stm32f10x_it.c,system_stm32f10x.c。接下来是关键的工程配置点击魔术棒按钮Target: 根据你的芯片选择正确的ROM/RAM地址和大小。对于STM32F103C8T6通常是ROM: 0x8000000 Size: 64K, RAM: 0x20000000 Size: 20K。C/C:Define: 这里必须输入两个重要的宏定义USE_STDPERIPH_DRIVER告诉编译器使用标准外设库STM32F10X_MD定义芯片密度MD对应中等容量F103C8HD对应大容量F103ZE等。这个必须和你的芯片匹配否则编译SPL时会报错Include Paths: 添加所有头文件路径。务必包含../User../CMSIS../STM32F10x_StdPeriph_Driver/inc../FreeRTOS/Source/include../FreeRTOS/Source/portable/RVDS/ARM_CM3../FreeRTOS/Source/portable/MemMang因为heap_4.c需要包含portable.hDebug: 配置你的调试器如ST-Link。Utilities: 设置正确的Flash编程算法。完成这些一个不含FreeRTOS的SPL基础工程应该能编译通过。这是我们的“地基”必须打牢。3. FreeRTOS内核移植与关键文件修改地基打好现在开始盖RTOS这层楼。移植的核心是让FreeRTOS接管系统的时钟节拍SysTick和PendSV中断并管理好中断优先级。3.1 修改FreeRTOSConfig.h配置文件FreeRTOSConfig.h是FreeRTOS的“大脑”所有配置都在这里。你可以从FreeRTOS Demo目录里找一个STM32F103的参考但不要直接复制需要根据我们的SPL环境进行定制。我在User文件夹下新建了这个文件内容核心如下#ifndef FREERTOS_CONFIG_H #define FREERTOS_CONFIG_H #include stm32f10x.h // 必须包含以使用STM32的寄存器定义 #define configUSE_PREEMPTION 1 // 使用抢占式调度 #define configUSE_IDLE_HOOK 0 // 空闲任务钩子调试用可开 #define configUSE_TICK_HOOK 0 // 时钟节拍钩子 #define configTICK_RATE_HZ ( ( TickType_t ) 1000 ) // 系统节拍1kHz (1ms) #define configCPU_CLOCK_HZ ( SystemCoreClock ) // 系统主频在system_stm32f10x.c中定义 #define configMAX_PRIORITIES ( 5 ) // 最大任务优先级对于CM3不要超过32实际用不了这么多 #define configMINIMAL_STACK_SIZE ( ( unsigned short ) 128 ) // 空闲任务栈大小 #define configTOTAL_HEAP_SIZE ( ( size_t ) ( 10 * 1024 ) ) // 堆总大小根据芯片RAM调整F103C8约20K分配10K给RTOS #define configMAX_TASK_NAME_LEN ( 16 ) #define configUSE_16_BIT_TICKS 0 // 必须为0使用32位Tick计数器 #define configIDLE_SHOULD_YIELD 1 #define configUSE_MUTEXES 1 // 使用互斥量 #define configUSE_RECURSIVE_MUTEXES 1 #define configUSE_COUNTING_SEMAPHORES 1 #define configUSE_ALTERNATIVE_API 0 // 废弃API必须为0 #define configQUEUE_REGISTRY_SIZE 10 #define configUSE_QUEUE_SETS 0 #define configUSE_TIME_SLICING 1 // 启用时间片轮转 #define configCHECK_FOR_STACK_OVERFLOW 2 // 栈溢出检测方法2较严格 #define configUSE_MALLOC_FAILED_HOOK 1 // 内存分配失败钩子调试利器 #define configUSE_APPLICATION_TASK_TAG 0 #define configUSE_NEWLIB_REENTRANT 0 // 如果不用newlib设为0 #define configSUPPORT_DYNAMIC_ALLOCATION 1 // 使用动态内存分配 #define configSUPPORT_STATIC_ALLOCATION 0 // 本例全部动态如需静态需修改 /* 中断优先级配置 - Cortex-M3内核特定这是重中之重 */ #define configKERNEL_INTERRUPT_PRIORITY 255 // 内核中断优先级 (最低)对应抢占优先级15 #define configMAX_SYSCALL_INTERRUPT_PRIORITY 191 // 可调用FromISR API的最高中断优先级对应抢占优先级11 /* 解释STM32使用4位优先级数值越小优先级越高。FreeRTOS要求SysTick和PendSV为最低优先级。 我们将优先级位拆分为抢占优先级和子优先级。这里配置的是“数值”不是“优先级位”。 255 (0xFF) 对应所有4位都为1即抢占优先级15最低。 191 (0xBF) 对应二进制1011即抢占优先级11。所有优先级数值高于此即抢占优先级12的中断不能调用FreeRTOS的FromISR API。*/ #define configENABLE_BACKWARD_COMPATIBILITY 0 #define configUSE_PORT_OPTIMISED_TASK_SELECTION 1 // 使用硬件计算就绪任务最高优先级CM3支持 /* 硬件特定 */ #define configPRIO_BITS 4 // STM32F10x使用4位优先级 #define configLIBRARY_LOWEST_INTERRUPT_PRIORITY 15 // 最低中断优先级抢占优先级 #define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 5 // 可调用系统API的最高中断优先级抢占优先级通常设为5高于此值的中断不会被RTOS延迟 /* 断言开发阶段打开 */ #define configASSERT( x ) if( ( x ) 0 ) { taskDISABLE_INTERRUPTS(); for( ;; ); } #include stm32f10x_it.h // 包含我们的中断头文件确保声明一致 #endif /* FREERTOS_CONFIG_H */这个配置文件里最需要理解的是configKERNEL_INTERRUPT_PRIORITY和configMAX_SYSCALL_INTERRUPT_PRIORITY。它们定义了FreeRTOS管理的中断范围。任何优先级数值低于configMAX_SYSCALL_INTERRUPT_PRIORITY即优先级更高的中断不会被RTOS延迟但也不能调用xQueueSendFromISR这类API。优先级数值在两者之间的中断可以安全调用FromISR API。SysTick和PendSV的优先级必须设为configKERNEL_INTERRUPT_PRIORITY最低。3.2 修改系统时钟与中断向量表FreeRTOS需要SysTick作为其心跳时钟。在SPL中我们通常用SysTick_Config(SystemCoreClock / 1000)来配置1ms中断。但FreeRTOS的移植层port.c里已经实现了vPortSetupTimerInterrupt()函数来配置SysTick。因此我们需要注释掉或删除在main()函数或system_stm32f10x.c中可能存在的SysTick配置避免冲突。关键修改在stm32f10x_it.c中断服务程序文件删除或注释掉原有的SysTick_Handler函数。因为FreeRTOS在port.c里提供了自己的xPortSysTickHandler它会在port.c中被映射到SysTick_Handler通过#define vPortSVCHandler SVC_Handler这类宏具体看port.c开头。我们需要确保不重复定义。同样处理PendSV_Handler和SVC_Handler。FreeRTOS需要接管这两个异常用于上下文切换和启动第一个任务。修改其他中断对于你希望使用FreeRTOS FromISR API的中断如USART接收中断其中断优先级抢占优先级部分必须设置为高于configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY数值上小于等于5。例如设置为4。并且在中断服务函数中需要使用portEND_SWITCHING_ISR()或portYIELD_FROM_ISR()来触发任务切换。一个USART1中断服务程序的修改示例// stm32f10x_it.c void USART1_IRQHandler(void) { BaseType_t xHigherPriorityTaskWoken pdFALSE; // 初始化为pdFALSE if(USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET) { uint8_t ch USART_ReceiveData(USART1); // 假设我们有一个队列xQueueUartRx if(xQueueUartRx ! NULL) { // 发送到队列FromISR版本 xQueueSendFromISR(xQueueUartRx, ch, xHigherPriorityTaskWoken); } USART_ClearITPendingBit(USART1, USART_IT_RXNE); } // 如果有更高优先级任务被唤醒需要进行上下文切换 portEND_SWITCHING_ISR(xHigherPriorityTaskWoken); }3.3 解决编译错误configTICK_T 错误这是移植FreeRTOS v202112.00到较旧编译器如ARMCC 5时的一个经典错误。错误信息通常出现在portmacro.h中提示#error directive: configTICK_T。根因分析FreeRTOS v202112.00为了增强类型安全引入了一个新的配置项configTICK_TYPE_WIDTH_IN_BITS用于定义TickType_t的位宽16或32。同时它期望在FreeRTOSConfig.h中通过configTICK_T来定义TickType_t的类型。但为了向后兼容它提供了一套复杂的宏逻辑。当使用ARM Compiler 5Keil MDK默认时如果FreeRTOSConfig.h中没有明确定义configTICK_T且没有正确定义configUSE_16_BIT_TICKS编译器就会走到一个抛出该错误的分支。解决方案在FreeRTOSConfig.h中添加明确的类型定义绕过其自动检测逻辑。添加以下代码/* 针对ARM Compiler 5 (Keil) 解决 configTICK_T 错误 */ #if defined(__CC_ARM) || defined(__ARMCC_VERSION) #define configTICK_T TickType_t #ifndef configUSE_16_BIT_TICKS #define configUSE_16_BIT_TICKS 0 // 强制使用32位tick #endif #endif同时确保configUSE_16_BIT_TICKS已定义为0如上文配置所示。这样修改后portmacro.h中的错误检查分支就会被跳过编译得以继续。4. 创建任务与系统启动内核配置好了中断也接管了现在让我们创建两个简单的任务来验证系统是否跑起来了。在main.c中我们不再有while(1)超级循环取而代之的是RTOS的任务和调度器。#include stm32f10x.h #include FreeRTOS.h #include task.h #include queue.h #include usart.h // 假设你已初始化USART用于打印 /* 任务函数原型 */ static void vTaskLedBlink(void *pvParameters); static void vTaskPrintHello(void *pvParameters); /* 全局句柄 */ TaskHandle_t xTaskLedHandle NULL; TaskHandle_t xTaskPrintHandle NULL; int main(void) { /* 初始化系统时钟HSE 8MHz - PLL 72MHz */ SystemInit(); /* 初始化外设例如GPIO、USART等 */ GPIO_Configuration(); // 你的GPIO初始化函数 USART1_Configuration(115200); // 串口初始化用于调试输出 printf(System Start...\r\n); /* 创建任务 */ xTaskCreate(vTaskLedBlink, /* 任务函数指针 */ TaskLed, /* 任务名调试用 */ 128, /* 栈深度以字为单位STM32是32位即128*4512字节 */ NULL, /* 传递给任务的参数 */ 2, /* 优先级数值越大优先级越高 */ xTaskLedHandle); /* 任务句柄 */ xTaskCreate(vTaskPrintHello, TaskPrint, 256, /* 这个任务用了printf栈需要大一些 */ NULL, 1, /* 优先级比LED任务低 */ xTaskPrintHandle); /* 启动调度器从此RTOS接管控制权 */ vTaskStartScheduler(); /* 正常情况下vTaskStartScheduler()不会返回。 如果返回了说明内存不足或调度器启动失败。 */ for(;;) { // 死循环通常点亮一个错误指示灯 } } /* LED闪烁任务 */ static void vTaskLedBlink(void *pvParameters) { (void)pvParameters; // 消除未使用参数警告 const TickType_t xDelay500ms pdMS_TO_TICKS(500); // 将毫秒转换为系统节拍数 for(;;) { GPIO_SetBits(GPIOC, GPIO_Pin_13); // 假设LED接在PC13蓝桥板 vTaskDelay(xDelay500ms); GPIO_ResetBits(GPIOC, GPIO_Pin_13); vTaskDelay(xDelay500ms); } } /* 打印任务 */ static void vTaskPrintHello(void *pvParameters) { (void)pvParameters; const TickType_t xDelay1000ms pdMS_TO_TICKS(1000); int count 0; for(;;) { printf(Hello from FreeRTOS! Count: %d\r\n, count); vTaskDelay(xDelay1000ms); } } /* 重定向printf到串口1 (ARMCC) */ #ifdef __CC_ARM int fputc(int ch, FILE *f) { USART_SendData(USART1, (uint8_t)ch); while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); return ch; } #endif关键点解析vTaskStartScheduler(): 这是启动FreeRTOS调度器的函数。它会创建空闲任务Idle Task如果使能了软件定时器还会创建定时器服务任务然后启动SysTick定时器并开始调度最高优先级的就绪任务。这个函数调用后不会返回除非内存堆初始化失败。栈深度xTaskCreate的第三个参数是栈深度单位是字Word。对于32位的Cortex-M3一个字是4字节。vTaskPrintHello任务因为调用了printf函数调用层级较深局部变量可能较多所以分配了256字即1KB的栈空间。这是一个经验值实际开发中需要根据任务复杂度调整并利用configCHECK_FOR_STACK_OVERFLOW功能检测溢出。pdMS_TO_TICKS(): 这是一个FreeRTOS提供的宏用于将毫秒时间转换为系统节拍数。它根据configTICK_RATE_HZ自动计算。务必使用这个宏而不是硬编码数字这样当你想修改系统时钟频率时所有延时都会自动适应。vTaskDelay(): 这是相对延时函数调用该函数的任务会进入阻塞状态让出CPU给其他就绪任务。这是RTOS多任务协作的基础。编译、下载到开发板你应该能看到LED以1Hz频率闪烁同时串口每秒打印一次信息。恭喜FreeRTOS已经在你的SPL工程上成功跑起来了5. 外设驱动与RTOS的协同工作在裸机SPL程序中我们通常直接在中断或主循环里操作外设。但在RTOS环境下我们需要考虑资源共享、任务同步和中断安全。5.1 串口打印的线程安全上面的例子中两个任务都可能调用printf虽然只有vTaskPrintHello调用了而printf底层调用的fputc是非线程安全的。如果多个任务同时调用printf输出会交错混乱。解决方法是为串口发送创建一个互斥量Mutex或使用一个专用的打印任务。方案一使用互斥量/* main.c 中定义互斥量 */ SemaphoreHandle_t xPrintMutex; /* 在main函数创建任务前创建互斥量 */ xPrintMutex xSemaphoreCreateMutex(); if(xPrintMutex NULL) { printf(Mutex creation failed!\r\n); } /* 定义一个线程安全的打印函数 */ void safe_printf(const char *format, ...) { va_list args; va_start(args, format); if(xSemaphoreTake(xPrintMutex, portMAX_DELAY) pdTRUE) { vprintf(format, args); // 使用vprintf处理变参 xSemaphoreGive(xPrintMutex); } va_end(args); }然后在任务中用safe_printf替代printf。互斥量确保同一时刻只有一个任务能执行打印操作。方案二使用队列和专用打印任务更优创建一个高优先级的专用打印任务和一个队列。其他任务将需要打印的字符串发送到这个队列由打印任务统一取出并发送到串口。这完全解耦了业务逻辑和输出更符合RTOS的设计思想且不会阻塞高优先级任务太久只是发送到队列很快。5.2 中断服务程序与FromISR API如前所述在中断中与任务通信必须使用FromISR结尾的API如xQueueSendFromISR,xSemaphoreGiveFromISR,xTaskResumeFromISR等。这些API是专门为中断上下文设计的它们不会进行可能导致阻塞的操作如等待互斥量。一个常见的模式是在中断中接收到数据通过队列发送给一个处理任务或者给出一个信号量通知一个任务去处理。// 在某个硬件定时器中断中给出信号量 void TIM2_IRQHandler(void) { BaseType_t xHigherPriorityTaskWoken pdFALSE; if(TIM_GetITStatus(TIM2, TIM_IT_Update) ! RESET) { // 给出信号量通知处理任务 xSemaphoreGiveFromISR(xTimSemaphore, xHigherPriorityTaskWoken); TIM_ClearITPendingBit(TIM2, TIM_IT_Update); } portEND_SWITCHING_ISR(xHigherPriorityTaskWoken); }处理任务则在另一个地方等待这个信号量void vTaskTimProcess(void *pvParameters) { for(;;) { // 等待信号量无限期等待 if(xSemaphoreTake(xTimSemaphore, portMAX_DELAY) pdTRUE) { // 执行定时处理工作 do_something(); } } }5.3 低功耗与空闲任务在RTOS中当所有任务都处于阻塞状态等待延时、信号量、队列等时调度器会运行空闲任务Idle Task。我们可以利用空闲任务钩子函数Idle Hook来实现芯片的低功耗。在FreeRTOSConfig.h中使能configUSE_IDLE_HOOK并实现vApplicationIdleHook函数。// 在main.c或单独文件中实现 void vApplicationIdleHook(void) { /* 在此处进入低功耗模式。 注意进入低功耗前必须确保SysTick能唤醒CPU。 对于STM32可以使用WFI指令。*/ __WFI(); // 等待中断进入睡眠模式 }重要提示使用低功耗模式需要仔细考虑所有可能唤醒CPU的中断源并确保FreeRTOS的SysTick中断能正确地将CPU从睡眠中唤醒以进行任务调度。在某些深度睡眠模式下可能需要特殊的处理。6. 调试技巧与常见问题排查即使一切配置看起来都正确实际运行中也可能遇到各种奇怪的问题。这里分享几个我踩过的坑和调试方法。6.1 栈溢出检测栈溢出是RTOS开发中最常见也最隐蔽的问题之一。FreeRTOS提供了两种检测方法通过configCHECK_FOR_STACK_OVERFLOW配置方法1在任务切换时检查栈指针是否指向了任务栈范围之外的一个已知模式区域。开销小但不能检测到栈内所有区域的溢出。方法2在任务切换时检查任务创建时栈的“水印”通常是用特定值填充的是否被修改。开销稍大但更可靠。我强烈推荐在开发阶段使用方法2。一旦检测到溢出FreeRTOS会调用vApplicationStackOverflowHook函数。你需要实现这个函数通常在里面打印错误信息或让一个LED疯狂闪烁。void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName) { (void)xTask; printf(!!! STACK OVERFLOW in task: %s !!!\r\n, pcTaskName); // 死循环或重启系统 for(;;); }通过这个钩子你可以快速定位是哪个任务栈开小了。6.2 使用Keil的RTX插件进行可视化调试Keil MDK自带一个叫做Event Recorder的工具但它对FreeRTOS的支持不如其自家的RTX。不过我们仍然可以通过串口打印任务状态来调试。FreeRTOS提供了vTaskList()和vTaskGetRunTimeStats()等函数可以获取任务列表和运行时间统计。但这些函数会消耗较多资源和时间建议仅在调试时使用。一个更轻量级的方法是在调试器中查看pxCurrentTCB变量当前任务控制块指针以及就绪任务链表pxReadyTasksLists[]。这需要你对FreeRTOS内核数据结构比较熟悉。6.3 HardFault_Handler 定位在RTOS环境下HardFault硬件错误更难定位因为错误可能发生在任何任务或中断中。当发生HardFault时首先检查栈配置是不是某个任务栈溢出破坏了其他内存中断优先级有没有中断的优先级设置在了configMAX_SYSCALL_INTERRUPT_PRIORITY以上即数值更小优先级更高并且这个中断调用了FreeRTOS的API这是绝对禁止的。内存访问是不是有任务或中断访问了非法内存地址如空指针对齐问题Cortex-M3要求某些访问必须按字对齐非对齐访问可能触发HardFault。可以在stm32f10x_it.c中编写一个详细的HardFault_Handler读取相关的寄存器如SCB-CFSR,SCB-HFSR,SCB-MMFAR,SCB-BFAR等并通过串口打印出来帮助定位错误原因。6.4 系统卡死或运行异常如果系统启动后直接卡死或者任务调度看起来不正常检查SysTick中断确认SysTick中断是否成功触发。可以在xPortSysTickHandler即实际的SysTick_Handler里设置一个断点或翻转一个GPIO看是否每1ms执行一次。检查PendSV优先级PendSV中断必须设置为最低优先级。在port.c的vPortSetupTimerInterrupt函数或prvPortStartFirstTask汇编函数中会设置PendSV的优先级。确保它没有被其他地方修改。检查堆大小configTOTAL_HEAP_SIZE是否设置得太小如果堆初始化失败vTaskStartScheduler会直接返回。可以在pvPortMalloc失败钩子函数vApplicationMallocFailedHook中添加调试信息。关闭编译器优化在调试初期尝试将Keil的优化等级设置为-O0排除因优化导致的异常。7. 从裸机思维到RTOS思维的转变最后我想分享一点比技术配置更重要的东西思维模式的转变。把FreeRTOS成功移植到SPL工程上只是万里长征第一步。真正用好RTOS需要从裸机的“顺序执行中断”思维转变为“多任务并发资源同步”的思维。在裸机程序里你关心的是“什么时候该做什么”。在RTOS程序里你更关心的是“哪些事情可以独立并发地做”以及“它们之间如何安全地通信与同步”。你会开始思考这个功能应该拆分成几个独立的任务任务之间的优先级关系应该是怎样的注意防止优先级反转它们共享哪些资源变量、外设需要用互斥量、信号量还是队列来保护中断服务程序应该只做最紧急的事如读取数据然后把处理工作交给一个任务去完成。例如一个简单的数据采集系统可以拆分为Task_Sensor中优先级周期性读取传感器数据放入队列。Task_Process高优先级从队列取出数据进行滤波、计算等处理将结果放入另一个队列或全局变量。Task_Display低优先级从全局变量获取处理结果刷新显示。Task_Comm中优先级通过串口或网络发送数据。每个任务都是一个独立的for(;;)循环通过vTaskDelay,xQueueReceive,xSemaphoreTake等函数主动让出CPU。整个系统看起来是“同时”运行的实际上是由调度器根据优先级和状态在快速切换。这种思维转变需要时间和实践。我的建议是从一个简单的、功能清晰的项目开始大胆地拆分任务即使一开始觉得“杀鸡用牛刀”。在踩过几次资源冲突、优先级设置不当的坑之后你会对RTOS的精髓有更深的理解。当你再回头看那些冗长的裸机超级循环时你会庆幸自己走上了RTOS这条路。
返回列表