尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

STM32F103 + FreeRTOS 串口控制项目:从踩坑到重构的完整调试记录

发布时间:2026/9/26 4:17:29

资讯中心
01
ARTICLE

STM32F103 + FreeRTOS 串口控制项目:从踩坑到重构的完整调试记录

STM32F103 + FreeRTOS 串口控制项目:从踩坑到重构的完整调试记录
STM32F103 FreeRTOS 串口控制项目从踩坑到重构的完整调试记录前言最近做了一个基于 STM32F103C8T6 和 FreeRTOS 的串口控制小项目。本来以为就是串口收个字符、翻个 GPIO的简单事结果前后踩了好几个坑——从 CMSIS-RTOS 版本混用到串口接收死活不进中断再到 FreeRTOS 中断优先级导致 HardFault……这篇文章不打算写成教程而是记录整个调试过程中遇到的问题、排查思路和最终方案。希望对同样在做 STM32 FreeRTOS 项目的同学有点参考价值。1. 项目简介功能需求PC 通过 USART1 发送指令控制 LED支持0/1/2/s四种命令0/1/2 对应不同 LED 状态s 查询当前状态串口返回执行结果每隔 5 秒自动上报当前 LED 状态硬件连接外设引脚说明USART1_TXPA9串口发送USART1_RXPA10串口接收LED1PA0低电平点亮软件环境STM32CubeMX图形化配置HAL 库FreeRTOSCMSIS-RTOS V1Keil MDK5任务划分任务职责UARTTask串口命令解析LEDTaskLED 控制MonitorTask状态周期上报最开始的想法很简单串口接收字符 → 解析 → 控制 GPIO。但实际调试过程中遇到了几个比较典型的问题最后不得不重新调整了串口接收的整体架构。2. 坑一CMSIS-RTOS V1 和 V2 混用导致编译错误问题现象编译stm32f1xx_it.c时报错error: #20: identifier osSemaphoreId is undefined error: #223-D: function osSemaphoreRelease declared implicitly一开始以为只是少包含了头文件加了#include cmsis_os.h之后还是报错。排查过程仔细对比后发现CubeMX 里默认选的是CMSIS-RTOS V2但我写的代码用的全是V1 的接口CMSIS-RTOS V1CMSIS-RTOS V2osSemaphoreIdosSemaphoreId_tosMessagePut()osMessageQueuePut()osMessageGet()osMessageQueueGet()osMutexWait()osMutexAcquire()两者的函数名、类型名、参数都不完全一样混着用必然编译不过。解决方法因为项目代码已经按照 V1 写了不少全部改成 V2 工作量比较大所以选择在 CubeMX 中改回Middleware → FREERTOS → Interface → CMSIS_V1重新生成代码后在所有使用 RTOS API 的文件中确保包含#includecmsis_os.h修改后编译正常通过。小结这个问题的本质是CubeMX 配置的 RTOS 接口版本必须和代码中使用的 API 保持一致。V1 和 V2 不能混合使用否则类型和函数名都对不上。建议新建项目时就确认好版本避免后面返工。3. 坑二串口能发送但无法接收问题现象程序烧录后上电提示信息可以正常发送 ✅每 5 秒的状态上报也正常 ✅说明STM32 → PC这个方向完全没问题。但是从电脑串口助手发送012sSTM32 完全没有反应 ❌所以问题肯定出在PC → STM32方向也就是串口接收部分。排查思路硬件检查TX/RX 有没有接反PA9/PA10 对应正确吗→ 确认没问题GPIO 配置PA10 是否配置为复用推挽/输入模式→ CubeMX 生成的应该没问题中断使能USART1 全局中断是否在 NVIC 中使能→ 检查后发现已使能接收函数是否调用了接收中断相关函数→ 这就是后面要讲的重点4. 坑三ReceiveToIdle 接收方式没有正常工作最初方案最开始使用 HAL 提供的HAL_UARTEx_ReceiveToIdle_IT(huart1,rxBuffer,BUFFER_SIZE);这个函数的设计初衷是用于不定长数据接收通过检测IDLE 空闲帧来判断一帧数据结束收到数据后会进入HAL_UARTEx_RxEventCallback()回调理论上很美好适合处理变长协议。实际问题但实际调试发现HAL_UARTEx_RxEventCallback()根本没有进入。代码可以正常编译函数也调用了但接收就是没有反应。在 STM32F1 系列上HAL_UARTEx_ReceiveToIdle_IT()的实现和 F4/F7 等系列有些差异而且对 IDLE 中断的处理方式也不太一样。调试起来比较费劲。决策考虑到当前项目的协议非常简单0 / 1 / 2 / s每次只需要接收一个字符完全用不上不定长帧接收的能力。所以决定放弃 ReceiveToIdle改成最基础的HAL_UART_Receive_IT(huart1,uartRxByte,1);进行单字节中断接收。5. 坑四IDLE 缓存方式的并发问题在改成单字节接收之前我还尝试过另一种方案单字节中断 IDLE 空闲中断 缓存数组设计思路收到数据 → RX中断 → 保存到缓存数组 → index 检测到IDLE → 认为一帧结束 → 处理整帧数据这种方式本身没有问题很多串口协议比如 Modbus都会用。我的实现中的问题但我的实现里有一个典型的并发 bug接收回调中voidHAL_UART_RxCpltCallback(UART_HandleTypeDef*huart){rxBuffer[uartRxIndex]uartRxByte;HAL_UART_Receive_IT(huart1,uartRxByte,1);}IDLE 中断中if(__HAL_UART_GET_FLAG(huart1,UART_FLAG_IDLE)){__HAL_UART_CLEAR_IDLEFLAG(huart1);// 处理一帧数据processFrame(rxBuffer,uartRxIndex);uartRxIndex0;// 重置索引}问题在于uartRxIndex这个变量在两个中断里同时被修改。RX 中断里uartRxIndexIDLE 中断里uartRxIndex 0虽然这两个中断理论上不会同时触发IDLE 是在总线空闲时触发此时没有 RX但在某些边界情况下比如一帧数据的最后一个字节和 IDLE 标志几乎同时到达可能导致数据丢失索引错乱接收状态异常另外STM32F1 的 USART IDLE 标志清除也有讲究需要按照规定顺序读取SR和DR寄存器否则可能出现重复进入中断的问题。HAL 库的__HAL_UART_CLEAR_IDLEFLAG()宏在 F1 上的实现需要特别注意。结论这个方案对于当前简单协议来说过度设计了而且引入了不必要的并发风险。最终还是回到了更简单的方案。6. 最终方案单字节接收 消息队列重新分析需求当前协议0 / 1 / 2 / s 本质一个字节 一个命令既然一个字节就是一条完整命令那就完全没必要设计复杂的帧接收机制。架构设计USART接收中断 ↓ 放入队列 uartRxQueue ↓ 取出解析 UARTTask ↓ 放入队列 ledQueue ↓ 取出执行 LEDTask → 控制LED接收中断实现中断服务函数只做三件事获取接收到的数据放入消息队列开启下一次接收uint8_tuartRxByte;voidHAL_UART_RxCpltCallback(UART_HandleTypeDef*huart){if(huart-InstanceUSART1){// 将接收到的字节放入队列不做任何业务处理osMessagePut(uartRxQueueHandle,(uint32_t)uartRxByte,0);// 立即开启下一次接收HAL_UART_Receive_IT(huart1,uartRxByte,1);}}这样做的好处是中断极短只做数据搬运不做业务逻辑无共享变量数据通过队列传递不需要全局 buffer 和 index解耦接收和处理完全分离7. 坑五FreeRTOS 中断优先级问题问题现象在 UART 接收中断里调用了osMessagePut()之后系统偶尔会出现HardFault系统异常复位RTOS 调度卡死原因分析这是 FreeRTOS 的一个经典问题不是所有优先级的中断都可以调用 FreeRTOS 的内核 API。FreeRTOS 有一个配置项#defineconfigMAX_SYSCALL_INTERRUPT_PRIORITY5它的含义是优先级数值大于等于 5 的中断即优先级低于等于 5才允许调用 FreeRTOS API。注意STM32 中优先级数字越小优先级越高。Priority 0 是最高优先级。如果把 USART1 中断优先级设为 0最高然后在里面调用osMessagePut()就可能导致中断优先级高于 FreeRTOS 管理的范围内核临界区无法保护该中断数据结构被破坏 → HardFault最终配置USART1 中断优先级 Preemption Priority 5 Sub Priority 0确保 USART1 中断优先级不高于configMAX_SYSCALL_INTERRUPT_PRIORITY。小结这个问题非常容易被忽略尤其是从裸机开发转过来的同学——裸机里中断优先级随便设反正没有 RTOS 管着。但在 FreeRTOS 环境下任何调用了内核 API 的中断都必须满足优先级约束。8. 为什么设计两个消息队列整个系统用了两个消息队列可能有人会问为什么不直接在 UARTTask 里操作 GPIO队列一uartRxQueueUART中断 → uartRxQueue → UARTTask负责传递串口收到的原始数据字节。队列二ledQueueUARTTask → ledQueue → LEDTask负责传递解析后的控制命令。这样设计的好处UARTTask 不直接操作硬件它只负责协议解析把解析结果发出去每个任务职责单一接收、解析、执行完全分开结构清晰接收 → 解析 → 执行易于扩展以后如果把 LED 换成电机、舵机、继电器只需要修改 LEDTask或者叫 ActuatorTaskUARTTask 和协议解析部分完全不用动。这其实就是一种简单的分层设计思想虽然项目小但养成好的架构习惯很重要。9. UART 发送增加 Mutex 保护问题项目中有两个地方会调用串口发送任务发送内容UARTTask命令执行结果回显MonitorTask每 5 秒状态上报如果两个任务同时调用HAL_UART_Transmit()就会导致发送数据交错混乱状态机异常甚至出现死等解决方法增加一个互斥量uartTxMutex发送流程变为Task → 获取Mutex → UART发送 → 释放Mutex示例代码voiduartSendString(constchar*str){// 获取互斥量等待时间设为 osWaitForeverosMutexWait(uartTxMutexHandle,osWaitForever);// 发送数据HAL_UART_Transmit(huart1,(uint8_t*)str,strlen(str),100);// 释放互斥量osMutexRelease(uartTxMutexHandle);}这样保证同一时间只有一个任务使用 UART 发送避免数据冲突。注意HAL_UART_Transmit()是阻塞式发送如果发送时间较长会占用 Mutex 较久。对于本项目这种短数据发送完全没问题如果是大量数据发送建议改用 DMA 中断方式。10. 最终运行效果整个系统的数据流电脑发送命令 ↓ USART1 接收中断单字节 ↓ uartRxQueue消息队列 ↓ UARTTask解析命令 ↓ ledQueue消息队列 ↓ LEDTask控制 GPIO ↓ LED 状态改变同时MonitorTask 独立运行MonitorTask每5秒 ↓ 读取 LED 状态 ↓ 获取 uartTxMutex ↓ 串口发送状态 ↓ 释放 uartTxMutex目前系统可以稳定实现✅ 串口控制 LED0/1/2 命令✅ 命令执行结果回显✅ 状态查询s 命令✅ 每 5 秒自动状态上报✅ 多任务稳定运行无死锁、无崩溃11. 项目总结与反思这次调试最大的收获不是简单地把串口调通了而是对STM32 FreeRTOS 的多任务设计有了更深入的理解。几个重要的经验1. 中断里不要处理复杂业务中断应该尽量只做接收数据保存数据或放入队列通知任务复杂的协议解析、业务逻辑全部放到 Task 里处理。这样中断执行时间短不会影响其他中断的响应。2. 尽量减少共享变量之前的 IDLE 方案依赖buffer、index、length等多个全局变量多个中断和任务都在访问很容易出问题。改用 Queue 之后数据通过队列传递不需要手动管理 buffer 和 indexFreeRTOS 的队列本身是线程安全的结构更简单出 bug 的概率更低3. 架构不要过度设计当前协议只有0 / 1 / 2 / s四个字符使用单字节中断 Queue已经完全足够。如果一开始就上DMA IDLE RingBuffer 状态机不仅开发周期长而且调试难度大对于这个项目来说完全是杀鸡用牛刀。合适的才是最好的。如果以后数据量增加、协议变复杂再升级到 DMA RingBuffer 也不迟。4. FreeRTOS 中断优先级是必修课从裸机转 RTOS最容易踩的坑之一就是中断优先级。一定要记住调用了 FreeRTOS API 的中断优先级不能高于configMAX_SYSCALL_INTERRUPT_PRIORITYSTM32 中数字越小优先级越高配置中断时多留个心眼通过这个项目掌握的知识点UART 串口通信原理与 HAL 库使用STM32 中断机制与 NVIC 优先级管理FreeRTOS 任务调度与任务间通信Queue消息队列的使用场景与实现Mutex互斥量的资源保护作用中断与任务的协同设计简单的分层架构设计思想这些内容也为后续开发更复杂的嵌入式控制系统打下了基础。写在最后回头看这个项目代码量不大但踩的坑不少。每个坑背后都是一个知识点版本管理、中断机制、并发保护、RTOS 约束、架构设计……嵌入式开发就是这样很多问题不亲自踩一遍看再多教程也记不住。希望这篇调试记录能帮到正在踩坑的你。如果文章中有错误或者更好的方案欢迎在评论区交流
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。