简介:基于STM32F103C8T6(HAL库+STM32CubeMX)的UART串口通信数据收发完整工程,面向嵌入式初学者、STM32入门者及需要快速搭建串口通信原型的开发者,重点解决CubeMX外设配置、HAL库驱动调用和收发验证等问题。压缩包共146个文件,包含47个h头文件、20个c源文件、62个编译中间文件(o/d/crf)、1个ioc配置工程、1个hex可执行文件以及其他辅助调试内容,约4.31MB,目录对应Keil工程结构,便于直接打开查看或二次修改。工程覆盖系统时钟(HSE+PLL至72MHz)、USART引脚映射与波特率设置、HAL_UART_Transmit/HAL_UART_Receive收发调用,并保留NV_USART相关实现及map/axf等构建产物,方便对照初始化流程、调试排错和进一步扩展。已有6056人浏览学习,适合直接在开发板上烧录验证,也可作为串口通信项目的基础模板复用,对掌握嵌入式通信基础和理解HAL库抽象层均有实际参考价值。
1. 串口通信调不通的痛点:这套基于STM32F103C8T6的资源能做什么
调试单片机板子时最怕撞上的场景:串口助手波特率反复调了三次,发一个字节过去,回显是满屏乱码,甚至完全没反应。这套资源就是用STM32CubeMX加HAL库,把STM32F103C8T6的UART串口通信数据收发从头到尾跑通,解压后按步骤烧录,回显、中断接收、数据互发都能在最小系统板上落地。
项目核心解决的是“串口为什么不通”和“收发到底怎么组织”这两件事:CubeMX完成引脚和时钟的图形化配置,生成的HAL库代码处理寄存器细节,开发者只需要在用户代码区写发送和接收逻辑。它适合刚接触STM32的嵌入式初学者,也适合备赛、做上位机联调时需要一个能直接改的串口基座工程的开发者。
拿到手里,第一件事不是看代码,而是先读工程里的.ioc文件和串口初始化逻辑,确认它用的是哪一个USART、挂在哪个时钟树上。下面按这个顺序拆。
2. 这份串口资源到底做了什么:从板卡选型到工程目录拆解
2.1 为什么是STM32F103C8T6加HAL库加CubeMX这三件套
STM32F103C8T6是一颗Cortex-M3内核的单片机,主频最高72MHz,片上64KB Flash和20KB RAM,集成三个USART。做串口通信这类基础外设验证,它成本低、资料多、烧录方式灵活,用一块蓝板加一个USB转TTL模块就能开始干活。
HAL库在项目里的价值是把底层寄存器访问封装成结构体操作。想改波特率,改huart1.Init.BaudRate;想切换收发模式,改huart1.Init.Mode。相比直接操作寄存器,HAL库的代码更接近“填表”,对第一次接触STM32的人来说,出错时看API名就能猜出大概。CubeMX负责生成初始化代码,包括时钟树、GPIO复用和中断配置,生成的工程直接拖进Keil或者STM32CubeIDE就能编译。
这套组合也有代价。HAL库会引入大量中间层,最终编译出的固件体积比标准库大。F103C8T6只有64KB Flash,只做串口收发时完全没有压力,如果后面要塞进复杂的协议栈、显示屏驱动加多个传感器算法,就要注意Flash占用。打算深入做产品化的场景下,我一般会建议把HAL库裁剪或者局部改用LL库,但作为学习资源和二次开发基座,HAL库的易读性更重要。
另外要留意C8T6的中容量定位。CubeMX生成工程时如果目标芯片选错成高容量型号,代码同样能编译,但链接脚本的Flash布局不同,烧录后可能出现启动异常。这类资源拿到手后,我第一件事是看芯片顶面丝印,确认后缀是C8T6再开始配工程。
2.2 解压后先看哪里:CubeMX工程的目录结构与文件分工
资源解压后是CubeMX生成的标准工程布局。先找到.ioc文件,它是整个工程的“配置快照”,双击打开能回到图形化界面重新配置外设;重新生成代码时,CubeMX只覆盖自动生成区域,手工写在USER CODE BEGIN和USER CODE END之间的代码会保留,这是二次开发时最重要的一个约定,千万不要把代码写到这两个标记之外。
stm32f103c8t6_uart/ ├── stm32f103c8t6_uart.ioc // CubeMX 配置文件,双击可重新生成工程 ├── Core/ │ ├── Inc/ │ │ ├── main.h // 主头文件,存放全局声明 │ │ └── usart.h // USART1 句柄声明 │ └── Src/ │ ├── main.c // 主循环、发送/接收逻辑入口 │ ├── usart.c // MX_USART1_UART_Init 初始化 │ ├── gpio.c // 引脚时钟和模式配置 │ └── stm32f1xx_hal_msp.c // 外设底层的时钟、GPIO、NVIC 配置 ├── Drivers/ │ ├── CMSIS/ │ └── STM32F1xx_HAL_Driver/ // HAL 库源码 ├── MDK-ARM/ // Keil 工程,.uvprojx 从这里打开 │ └── stm32f103c8t6_uart.uvprojx └── README.md // 资源自述和运行说明这个目录结构里,真正需要改的是Core/Src/main.c和usart.c。usart.c里是CubeMX生成的MX_USART1_UART_Init(),配置项用结构体赋值完成;main.c里是用户逻辑,包括初始化后的发送语句、主循环轮询和中断回调。stm32f1xx_hal_msp.c比较容易被忽略,它承担外设底层初始化,比如开启USART1时钟、配置PA9/PA10为复用推挽输出、设置NVIC中断优先级,很多“初始化成功但串口不动”的问题最后都出在这里。
usart.h里声明了外部句柄extern UART_HandleTypeDef huart1;,main.c和用户文件都能直接引用它。HAL_UART_HandleTypeDef内部包含Instance、Init、gState、RxState这些字段,调试时可以在Keil的Watch窗口直接看gState状态值,判断外设当前是否处于空闲或忙碌状态。
提示:CubeMX重新生成代码时,只会保留
USER CODE BEGIN与USER CODE END之间的内容,业务代码务必放在这两个标记之间。
2.3 串口资源选址:USART1的引脚、总线时钟与中断关系
F103C8T6的USART1默认映射到PA9和PA10,PA9是TX发送引脚,PA10是RX接收引脚。USART1挂在APB2总线上,APB2的最高频率和系统主频相同,都是72MHz,这也是为什么配置完时钟树后,串口波特率计算没有额外分频误差的主要原因。
USART1的中断入口是USART1_IRQn。CubeMX在NVIC Settings里使能USART1 global interrupt后,会在stm32f1xx_it.c中生成USART1_IRQHandler,由它调用HAL库的HAL_UART_IRQHandler(huart)完成中断分发。中断触发条件包括RXNE接收寄存器非空、TXE发送寄存器空、ORE溢出错误等,HAL库会把这些事件分类,再回调到用户可重写的弱函数里。
波特率由USARTDIV分频值决定,F1的USART1可以用72MHz作为输入时钟。115200对应的分频值是72MHz除以115200,HAL库会自动算好分频值写入BRR寄存器。如果把USART1误配到APB1的36MHz时钟上,同一个波特率设置算出的分频值差一倍,通信自然对不上。这也是排查乱码时先问“你的USART挂在哪个总线”的原因。
如果是USART2或USART3,对应引脚、挂载总线、中断向量都不一样。改资源里的通信口时,要同时改.ioc里的引脚分配、时钟树里对应总线的分频系数和NVIC配置,只改代码不改引脚映射是跑不起来的。现在这份资源只用了USART1,理解了这一套对应关系,后面换串口只是重复同样的配置流程。
3. 核心模块拆解:从CubeMX配置到中断收发的完整链路
3.1 CubeMX配置阶段:时钟树、USART1和NVIC的填表过程
先看CubeMX侧需要确认的配置项。RCC里启用HSE外部高速时钟,选择Crystal/Ceramic Resonator,外部晶振按8MHz算;时钟树里PLL Source选HSE,把PLL倍频系数调到9,得到72MHz的系统主频;APB1 Prescaler选/2让APB1保持在36MHz,APB2 Prescaler保持/1,这样USART1能拿到完整的72MHz时钟。
USART1的配置页选择Asynchronous异步模式,引脚自动分配PA9的USART1_TX和PA10的USART1_RX。常用参数是波特率115200、8位数据长度、无校验、1位停止位、无硬件流控,这些参数要和上位机串口助手完全一致,差一位停止位或者勾上了流控,就会出现能发不能收或者乱码的怪问题。NVIC设置里使能USART1全局中断,这是后面做中断接收的前提。
CubeMX生成的初始化代码长这样:
void MX_USART1_UART_Init(void) { huart1.Instance = USART1; huart1.Init.BaudRate = 115200; huart1.Init.WordLength = UART_WORDLENGTH_8B; huart1.Init.StopBits = UART_STOPBITS_1; huart1.Init.Parity = UART_PARITY_NONE; huart1.Init.Mode = UART_MODE_TX_RX; huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE; huart1.Init.OverSampling = UART_OVERSAMPLING_16; if (HAL_UART_Init(&huart1) != HAL_OK) { Error_Handler(); } }BaudRate、WordLength、StopBits、Parity这几个字段直接决定物理层格式。上位机串口助手里的设置要和这里一一对应,比如这里UART_PARITY_NONE,上位机就必须关掉校验。HwFlowCtl默认关闭硬件流控,如果开了RTS/CTS,而USB转TTL模块不支持硬件流控,数据发送会一直等待握手信号,看起来就像串口卡死。
底层初始化在HAL_UART_MspInit里完成,包括使能时钟、配置GPIO复用、设置中断优先级:
void HAL_UART_MspInit(UART_HandleTypeDef *huart) { GPIO_InitTypeDef GPIO_InitStruct = {0}; if (huart->Instance == USART1) { __HAL_RCC_USART1_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitStruct.Pin = GPIO_PIN_9 | GPIO_PIN_10; GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; // 复用推挽,TX/RX 都按复用模式配置 GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); HAL_NVIC_SetPriority(USART1_IRQn, 0, 0); HAL_NVIC_EnableIRQ(USART1_IRQn); } }PA9和PA10都配置成GPIO_MODE_AF_PP复用推挽输出模式,这句话很反直觉,有人把RX引脚配成输入模式导致收不到数据。事实上在F1系列里,USART的TX和RX引脚都要配置为复用推挽,硬件的收发方向由USART外设自己控制,不需要手动配置输入输出方向。HAL_NVIC_SetPriority(USART1_IRQn, 0, 0)设置抢占优先级0,子优先级0,如果后面加了更多中断,要注意优先级分组方式保持一致。
3.2 发送逻辑:阻塞发送与printf重定向的两种写法
发送数据最直接的方式是HAL提供的HAL_UART_Transmit,属于阻塞式发送,发送期间CPU一直等发送完成。初始化完成后发一串欢迎信息:
uint8_t tx_buf[] = "STM32 UART Ready\r\n"; HAL_UART_Transmit(&huart1, tx_buf, sizeof(tx_buf) - 1, 1000);参数里第一个是串口句柄,第二个是待发送数据指针,第三个是发送长度,第四个是超时时间。这里用sizeof(tx_buf) - 1是为了把结尾的\0去掉,字符串数组在定义时自动多出了一个结束符,串口发送时不需要发\0。超时时间单位是毫秒,1000表示最多等1秒,如果1秒内没发完就返回HAL_TIMEOUT,主程序可以用返回值判断通信是否异常。
但实际调试时更常用printf往串口打数据。HAL库没有自带printf支持,需要自己做fputc重定向,把标准输出重定向到USART1:
#include "stdio.h" int fputc(int ch, FILE *f) { HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF); return ch; }在Keil里使用MicroLIB或者在STM32CubeIDE里启用newlib-nano,printf就能直接通过串口输出。这个重定向文件里如果同时存在多个串口,fputc里可以按调试需要切换句柄,但同一时刻只能有一个目标。重定向之后,printf("count = %d\r\n", count);就成了嵌入式里最常用的日志手段,比LED闪烁和仿真器断点都要直观。
3.3 接收逻辑:单字节中断接收的接力写法
接收侧不能在主循环里调用HAL_UART_Receive死等,那样CPU会在阻塞中空转,其他任务全部卡死。惯用且好用的做法是用HAL_UART_Receive_IT启动一次中断接收,每收到一个字节触发一次回调,回调处理完之后再次调用HAL_UART_Receive_IT启动下一轮接收,形成接力。
先把接收缓冲和标志位定义在文件顶部:
uint8_t rx_byte = 0; // 每轮中断接收的单个字节 uint8_t rx_flag = 0; // 收到新数据的标志位初始化时启动第一次接收:
HAL_UART_Receive_IT(&huart1, &rx_byte, 1);HAL_UART_Receive_IT的第二个参数是接收缓冲区地址,第三个参数是期望接收的字节数。传1表示每接收完1个字节就触发一次HAL_UART_RxCpltCallback回调。使用时要特别注意:这个函数只是启动接收,不是轮询接收,如果上一次接收还没完成,重复调用会返回HAL_BUSY。
然后在用户代码区重写回调函数:
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart->Instance == USART1) { rx_flag = 1; // 通知主循环有新数据 HAL_UART_Receive_IT(&huart1, &rx_byte, 1); // 立刻接力下一轮接收 } }回调里只做两件事:置标志位、重新启动下一次接收。主循环通过轮询rx_flag来处理数据,比如原样回显:
while (1) { if (rx_flag) { rx_flag = 0; HAL_UART_Transmit(&huart1, &rx_byte, 1, 100); } }这里容易踩的坑点在于回调执行上下文是中断。回调里不要写耗时太长的操作,更不要在里面调用printf或者做大数据拷贝,中断处理时间过久会延迟下一次中断响应,高速收发时会出现丢字节。重启动接收的调用必须放在回调末尾,如果在主循环里重新调用HAL_UART_Receive_IT,中间会存在一段空窗期,这期间到达的字节会直接丢失。
3.4 一条数据从进入到返回的完整时序
把上面几段拼起来,一条完整的数据链路是这样的:上位机通过USB转TTL模块把字节送到PA10引脚,USART外设移位寄存器逐位接收,置起RXNE标志;中断控制器响应USART1全局中断,进入USART1_IRQHandler;HAL库的HAL_UART_IRQHandler识别事件类型是RXNE,调用UART_Receive_IT把接收寄存器里的字节拷贝到rx_byte,然后触发HAL_UART_RxCpltCallback;回调里置rx_flag并立刻启动下一轮接收;主循环检测到rx_flag后,调用HAL_UART_Transmit把同一个字节写进发送移位寄存器,数据从PA9发出,回到上位机串口助手的接收框里。
这条链路里每一环都有检查点。收不到数据,先量PA10引脚有没有电平翻转,再看RXNE标志有没有置起,然后确认NVIC是否使能,最后看回调有没有被调用。按这个顺序排查,能把问题范围一步步缩小,不会在一个错误的环节上反复试。
4. 串口联调避坑指南:乱码、丢帧与卡死的排查记录
4.1 现象一:波特率调了一遍又一遍,收到的还是乱码
现象是上位机发送hello,接收框显示h?f或者无规律乱码,波特率从9600试到115200甚至460800都一个样。如果每次都排除波特率设置问题,真正原因往往在时钟配置上。
原因是串口波特率由USART时钟和分频系数决定。CubeMX里HSE如果填了8MHz但实际板子焊接的是16MHz晶振,或者HSE根本没起振导致系统跑在HSI 8MHz,那么USART1的波特率计算全部偏掉。串口助手看到的现象就是“无论选哪个波特率都对不上”,因为误差已经不是简单换挡能解决的。
解决方式是回到CubeMX时钟树页面,确认HSE输入频率与硬件一致,确认PLL输出是72MHz。更快的判断方法是先看串口调试助手能不能收到引导信息,如果上电后连STM32 UART Ready都是乱的,优先怀疑系统时钟而不是波特率设置。从某次项目调试开始,我每次拿到新板子都会先开一个GPIO翻转用示波器量实际主频,确认时钟正常再碰串口。
排查时还可以在调试器里直接看huart1.Init.BaudRate的当前值和CubeMX配置的是否一致,排除代码被其他版本覆盖的情况。如果时钟和波特率都对,换个USB转TTL模块交叉验证,有些模块的晶振精度太差,高位波特率下同样会乱码。
4.2 现象二:能发不能收,HAL_UART_Receive_IT只触发一次
现象是复位后第一帧数据能收到,之后无论怎么发都没有新回显,重新上电又恢复一次。这是HAL_UART_Receive_IT用得最典型的翻车点。
原因是HAL_UART_Receive_IT是一次性接收函数,完成指定长度后会恢复到空闲状态,不再接收新数据。直接在初始化时调用一次,后续没有在回调里重新调用,第二次字节到达时接收流程没有启动,数据自然全部丢弃。另一种情况是接收的过程中又重复调用HAL_UART_Receive_IT,函数发现上次接收还没完成,返回HAL_BUSY且启动失败。
解决方式是在HAL_UART_RxCpltCallback回调末尾重新调用HAL_UART_Receive_IT,让接收流程形成持续接力。同时可以在调用前检查huart1.gState,有必要时打印一次启动结果:
if (HAL_UART_Receive_IT(&huart1, &rx_byte, 1) != HAL_OK) { printf("receive start failed\r\n"); }如果高速收发场景下发现回调频繁触发但数据还是丢,需要检查是否发生了ORE溢出错误。可以重写HAL_UART_ErrorCallback,在里面打印huart->ErrorCode,如果看到HAL_UART_ERROR_ORE,说明上一字节还没处理完下一字节就来了,这时要提高中断优先级或者改DMA接收。
从那以后,我每次写串口中断接收都会检查一遍“回调里有没有再次启动”,这个习惯避免了很多从能收到不能收的反复。
4.3 现象三:上位机一发数据,板子就像被复位了一样
现象是串口助手一勾选“发送新行”,板子就重启,或者第一次数据正常,第二次带换行符的数据把整个程序打乱。如果用的是常见的USB转TTL模块,这是DTR/RTS信号在捣乱。
原因是不少USB转TTL模块会引出一路DTR和RTS信号,它们可以控制目标板的复位引脚和BOOT0引脚。串口助手在“发送新行”时,如果勾上了DTR/RTS联动或“打开串口时复位”,就会在信号线上产生电平跳变,等于给板子按了复位键。
解决方式简单:在串口助手的DTR/RTS选项里全部取消勾选,或者用只接了TXD、RXD、GND三根线的模块。也注意不要在USB转TTL的RTS引脚上接了复位电路还反复调试硬件流控。出现“一发送板子就重启”这种玄学现象时,先看信号线,再看代码,大部分时候是物理连线和助手的辅助信号配置问题。
更隐蔽的一种情况是,发送新行里的换行符是\r\n,而板子程序只按\n做帧结束处理,\r被当成正常数据收进缓冲。这时收进来的内容里会残留一个回车符,字符串比较永远失败。解决方式是做帧解析时主动过滤\r,或者上位机端只发送自定义帧格式,不带多余控制字符。
4.4 现象四:程序卡死在HAL_UART_Transmit里
现象是高波特率下连续发送一段较长数据,程序停住不动,调试器暂停时看到当前PC指针停在HAL_UART_Transmit内部,等了很久也不返回。
原因是处理逻辑里把大块数据的发送放在中断回调中执行。中断上下文里发长数据,发送期间其他中断被阻塞,而USART的后续中断可能和发送互相等待,最终让程序陷入类似死锁的等待。HAL_UART_Transmit的阻塞发送机制内部会等待TXE标志,超时设置得再长也解决不了优先级倒挂的问题。
解决方式是遵循中断回调快进快出的原则。回调里只置标志位,把发送放到主循环处理;如果确实需要在中断里边收边发,使用HAL_UART_Transmit_IT中断发送,发送完成后会有HAL_UART_TxCpltCallback回调。发送大量日志数据时,也可以先把数据拼到缓冲区,再通过DMA搬运,完全不占CPU。
另外要注意中断优先级分组。如果工程里同时用了定时器中断和串口中断,且分组方式用错了抢占优先级和子优先级的位数分配,串口中断可能被其他高频中断持续抢占,表现为串口时通时断。出现这种问题先统一检查HAL_NVIC_SetPriorityGrouping的设置,确保所有中断的优先级定义在同一个分组体系下。
5. 环境搭建与运行验证:从解压到回显成功
5.1 开发环境准备:Keil MDK和F1器件支持包
这套资源通常是用MDK-ARM工程形式提供的,所以本机要装Keil MDK5并安装对应的STM32F1系列器件支持包。打开工程前建议先把整个压缩包解压到纯英文路径下,不要放在带中文或空格的目录里,MDK有时会在特殊字符路径下编译出错,报错信息又很长很绕。
双击.uvprojx打开工程后,在Options for Target里的Device页面确认芯片型号显示的是STM32F103C8Tx。如果配置里选成了高容量型号,链接器会按更大Flash的空间布局分配地址,实际烧录到C8T6上会出现启动异常。第一次编译如果报缺头文件,多半是支持包版本和工程使用的HAL库版本不一致,重新安装当前版本的STM32F1xx_DFP即可。
编译时遇到warning不用太紧张,但要看一眼。常见的unused variable警告通常不影响运行,而declaration follows execution statement警告在旧版C标准下可能掩盖真正的问题,建议把C标准切到gnu11再编译。工程能零错误零警告通过,再往下做烧录验证。
5.2 接线与烧录:交叉接线和下载方式选择
串口通信的接线遵循“TX对RX、RX对TX”的交叉原则。板子的USART1_TX在PA9,它要接USB转TTL模块的RXD;板子的USART1_RX在PA10,它要接USB转TTL模块的TXD。最重要的还有一条GND必须和电脑侧共地,不共地时信号没有基准电位,现象是时通时断、乱码、甚至完全无反应。
| 板子引脚 | 功能 | USB转TTL模块 |
|---|---|---|
| PA9 | USART1_TX | RXD |
| PA10 | USART1_RX | TXD |
| GND | 电源地 | GND |
烧录方式有两条路。用ST-Link做SWD下载时,连接SWDIO、SWCLK、GND、3V3四根线,BOOT0接地,Keil里下载器选ST-Link即可。用串口ISP下载时,BOOT0接3V3,BOOT1接地,用STM32CubeProgrammer选择UART方式连接,注意串口ISP模式下PA9/PA10被占用,下载完成后要把BOOT0拉回低电平并手动复位一次,程序才会从Flash启动运行。
很多初次上手的人把板子连好线,Keil也显示下载成功,程序却完全不跑,原因常常是BOOT0还挂在3V3上,上电后MCU进了ISP引导程序而不是用户程序。烧录成功和程序运行是两个概念,复位后观察串口有没有打印上电信息是最快的验证方式。
5.3 串口助手联调:回显验证和关键参数
打开串口助手,选择正确的COM口,波特率115200,数据位8,停止位1,无校验,关闭DTR/RTS。这些参数必须和上一章MX_USART1_UART_Init里的配置完全一致,尤其是停止位,上位机设置成2位停止位不会报错,但接收时序对不上,长数据时会出现偶发丢字节。
向板子发送任意字符串,预期收到相同的回显内容,同时板载用户LED同步翻转一次。我习惯用一个固定格式做验证:先发hello,再发1234567890,最后发控制字符。如果字符串回显正常,说明字节级收发的物理链路已经打通;如果只回显一部分,再回到第四章检查是否漏了重新启动接收。
调试时建议用文本模式和HEX模式互相验证。文本模式适合看可读字符串,HEX模式适合确认实际字节值,比如发送0x41,文本模式显示A,HEX模式显示41 0A,两种模式交叉观察能更快定位是上位机转义问题还是板子解析问题。部分串口助手支持显示时间戳,这个功能在分析粘包和间隔时间时很管用。
5.4 如何判断资源里的工程真的跑起来了
串口回显通过、LED动作正常,只说明收发链路本身是通的。更严谨的验证是往上位机周期发送递增计数,观察程序是否长时间稳定运行:
while (1) { printf("count = %d\r\n", count++); HAL_Delay(500); }如果计数能连续跑到几千甚至几万不卡顿,说明HAL库的中断和主循环协作正常。接着在上位机连续快速发送数据,观察有没有漏字节,这一步能检验发送缓冲区和接收接力的健壮性。整套走完之后,再把工程里的发送函数改成自己的业务帧格式,二次开发就可以从完整验证过的底板上起步了。
6. 二次开发改造思路:从回显到自定义帧协议解析
基础回显验证之后,下一步通常是让串口不只回显,而是能解析命令。改成帧协议接收时,我一般会在回调里维护一个字节累积缓冲,以\n作为一帧结束标志,收到结束符后把帧交给解析函数:
#define FRAME_BUF_SIZE 64 static uint8_t frame_buf[FRAME_BUF_SIZE]; static uint8_t frame_index = 0; static uint8_t frame_ready = 0; void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart->Instance == USART1) { if (rx_byte == '\n') { frame_buf[frame_index] = '\0'; frame_ready = 1; frame_index = 0; } else if (frame_index < FRAME_BUF_SIZE - 1) { frame_buf[frame_index++] = rx_byte; } HAL_UART_Receive_IT(&huart1, &rx_byte, 1); } }主循环里检测到frame_ready后,对frame_buf做字符串匹配,比如“LED=1”点灯、“LED=0”灭灯。这种简单状态机的好处是只依赖单字节中断,不引入DMA,逻辑都看得见,适合小数据量控制命令。注意处理\r残留,上位机发送\r\n时,\r会被收进缓冲,比较前先做一次过滤。
如果通信数据量大、帧长不定,再往前一步是换成DMA加空闲中断的接收方式。CubeMX里给USART1添加DMA接收通道,NVIC里使能USART1全局中断,启动用HAL_UARTEx_ReceiveToIdle_DMA,数据接收完成或总线空闲时触发HAL_UARTEx_RxEventCallback。改动后先用串口助手连续发多帧数据验证分帧边界,确认不会出现大数据包互相粘连。
验证方法很简单:用串口助手依次发送协议规定的命令帧,观察板子回显和IO动作是否一一对应;再故意发送一帧残缺数据和超长数据,确认状态机能自动复位而不是卡死。改完后我每次都会强制自己走一遍“时钟—引脚—波特率—共地”四连查,确认新加的中断和DMA没有和串口中断抢优先级,再去写业务逻辑。这个习惯帮我少翻了很多次车,希望帮到你。
本文还有配套的精品资源,点击获取