资讯动态

STM32CubeMX配置HAL库串口通信:从基础到DMA空闲中断

发布时间:2026/10/19 17:44:00 来源:尧图企业网站定制
简介基于STM32F103C8T6与HAL库、STM32CubeMX的UART串口通信完整例程包主要面向嵌入式初学者、单片机爱好者以及需要快速搭建串口收发功能的开发者。压缩包共146个文件大小4.31MB源码以c/h文件为主同时包含ioc工程配置、uvprojx工程、hex烧录文件、axf/map/lst调试信息以及htm说明文档覆盖从项目生成到编译下载的完整链路目录结构清晰便于对照学习。已有6056人学习下载热度较好。例程围绕CubeMX的时钟与串口引脚配置、HAL库UART初始化、阻塞式发送与中断接收回调处理展开并额外提供NV_USART模块方便将重要数据结合非易失性存储保存描述中还提到传输错误检查、接收超时、DMA搬运等调试与优化思路对实际项目开发有直接参考价值。下载后可在现有工程模板上直接修改波特率、引脚和中断方式快速验证串口通信适合入门STM32串口通信、设备间数据交互及底层驱动调试。1. 为什么用CubeMX配HAL库做串口从最省事的点接到中断收发做嵌入式开发的人大多有过这种体验手里一块STM32F103C8T6最小系统板想通过串口跟电脑通信结果不是乱码就是数据收不全。这个资源把CubeMX的图形配置和HAL库的UART收发代码打包在一起照着做就能拿到一套能直接复用的串口Demo。它解决的不只是“点灯式”的发送还讲了中断接收、回调函数、以及数据丢失的排查方法。适合嵌入式入门者、HAL库新手也适合需要快速移植串口模块的工程师。整个工程从引脚配置到收发代码逻辑是完整的每一处参数都可以按自己的需求改。2. CubeMX配置UART1引脚、时钟和参数面板上的四个关键项2.1 引脚分配与时钟树别让APB2总线时钟拖慢波特率打开CubeMX选好芯片型号后第一件事是配置时钟。很多教程只教你点几下鼠标却不解释为什么。以STM32F103C8T6为例USART1挂在APB2总线上USART2和USART3挂在APB1上。APB2的默认最高频率是72MHz而APB1是36MHz。波特率计算公式是波特率 时钟频率 / (16 * USARTDIV)。USART1的时钟源来自APB2如果你在CubeMX里把APB2的分频系数设成了2那么USART1的输入时钟就会变成36MHz在相同USARTDIV下波特率就会减半。用115200时接收端按72MHz的时钟去采样就会因为位周期变长而产生误码。所以在Clock Configuration页面先把HCLK调到72MHz然后检查APB1 Prescaler为2APB2 Prescaler为1。这样系统时钟72MHzAPB1 36MHzAPB2 72MHz。对USART1来说时钟就是72MHz误差为零。引脚分配方面USART1默认的发送引脚是PA9接收引脚是PA10。在Pinout页面中用鼠标点击PA9选择USART1_TX点击PA10选择USART1_RX。如果你的板子把这两个引脚复用成了其他功能也可以用CubeMX的Alternate Function Mapping重新映射比如映射到PB6/PB7但F103C8T6的引脚不多绝大多数情况默认PA9/PA10就够了。注意如果同时使能了I2C或SPI的引脚可能发生冲突CubeMX会直接标记为红色这时你需要调整。时钟配置生成的代码会是这样的void SystemClock_Config(void) { RCC_OscInitTypeDef RCC_OscInitStruct {0}; RCC_ClkInitTypeDef RCC_ClkInitStruct {0}; RCC_OscInitStruct.OscillatorType RCC_OSCILLATORTYPE_HSE; RCC_OscInitStruct.HSEState RCC_HSE_ON; RCC_OscInitStruct.HSEPredivValue RCC_HSE_PREDIV_DIV1; RCC_OscInitStruct.PLL.PLLState RCC_PLL_ON; RCC_OscInitStruct.PLL.PLLSource RCC_PLLSOURCE_HSE; RCC_OscInitStruct.PLL.PLLMUL RCC_PLL_MUL9; if (HAL_RCC_OscConfig(RCC_OscInitStruct) ! HAL_OK) { Error_Handler(); } RCC_ClkInitStruct.ClockType RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2; RCC_ClkInitStruct.SYSCLKSource RCC_SYSCLKSOURCE_PLLCLK; RCC_ClkInitStruct.AHBCLKDivider RCC_SYSCLK_DIV1; RCC_ClkInitStruct.APB1CLKDivider RCC_HCLK_DIV2; RCC_ClkInitStruct.APB2CLKDivider RCC_HCLK_DIV1; if (HAL_RCC_ClockConfig(RCC_ClkInitStruct, FLASH_LATENCY_2) ! HAL_OK) { Error_Handler(); } }这段代码的含义是使用外部晶振HSEPLL 9倍频得到72MHz系统时钟AHB不分频APB1二分频得到36MHzAPB2不分频保持72MHz。FLASH_LATENCY_2表示Flash等待周期为2因为72MHz下必须设置为2否则程序运行会出错。PLL_MUL9适合外部晶振为8MHz的情况。如果你的板子上晶振是外部无源就用这个配置。如果用的是内部RC振荡器HSE部分换成HSI但内部RC精度不高串口高速通信容易丢数据所以板子上有8MHz晶振时优先用HSE。如果你买的板子没有焊晶振那就只能用HSI然后注意波特率偏差可能在2%以上115200时会有可感知的误码。2.2 参数面板波特率、字长、停止位与校验位的取舍在CubeMX中点击USART1右侧参数面板出现。这里最重要的四个参数是Baud Rate、Word Length、Parity和Stop Bits。我自己的默认组合是115200、8位数据、无校验、1位停止位也就是常说的8N1。这个组合兼容性最好几乎所有的USB转串口模块和串口助手都认它。波特率的选择要结合对端设备。GPS模块通常9600蓝牙模块常见38400或115200WiFi模块多用115200少数工业仪表是4800。选波特率没有绝对标准但有一个原则能低则低只有在速率不够时才提高。因为波特率越高位周期越短对线路寄生电容和时钟误差越敏感。调试时用115200很舒服但如果线材较长或现场有干扰降到9600能解决很多莫名丢包。Word Length决定一帧数据的有效位数可以是8或9。如果开启ParityWord Length必须包含校验位。例如8位数据加偶校验Word Length要选9。如果选8那么数据位会少一位通信直接对不上。我们平时调试关掉校验选8位因为很多廉价USB转串口线对校验位的处理并不严格容易出奇怪问题。Stop Bits选1位就够2位停止位一般用于老式设备或低速同步场景。如果你发现对方设备参数里明确要求2停止位才需要改。这里有个细节CubeMX里修改停止位后生成的代码会从STOPBITS_1变成STOPBITS_2但波特率不变。另外这个面板里有Overrun Detection溢出检测选项。默认是使能的作用是在接收数据时如果CPU没有及时读走数据硬件会置溢出错误标志。HAL库在溢出错误后需要你清除错误并重新启动接收否则它一直不工作。很多人遇到“收了几帧就再也不收”的情况往往就是这个原因。这个选项可以保持使能但必须在错误回调中恢复接收。我们会在避坑章节具体说。参数确认后在Project Manager的Code Generator页面我习惯勾选“Generate peripheral initialization as a pair of .c/.h files per peripheral”这样每个外设单独生成uart.c和uart.h而不是全部堆在main.c。这样代码结构清晰找问题也快。生成的UART初始化函数如下static 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(); } }这里的OverSampling是16倍过采样。USART1最高可以到72MHz/164.5M波特率但实际不建议超过921600。HwFlowCtl是硬件流控要用RTS/CTS引脚一般调试不需要保持NONE。这段代码里的每个字段都可以在前面参数面板中找到读代码时对照界面就不会晕。另外Mode选择里有一个Async模式和Sync模式。我们常用的就是Asynchronous异步模式。如果选成Synchronous会多出SCLK时钟引脚通讯方式变成同步需要外部时钟线大多数人用不到。还有半双工模式Single Wire只占用一根数据线用于RS485这类总线。这里用异步就好。然后在Configuration旁边的NVIC Settings中必须勾选“USART1 global interrupt”使能否则HAL_UART_Receive_IT不会触发中断回调。这个选项很多人会漏掉配置完参数后一定要点开NVIC Settings看一眼。2.3 硬件连接与电平匹配TX/RX交叉是常识也是坑软件配置完之后剩下就是硬件接线。最小系统板的USART1_TX引脚是PA9RX是PA10。接到USB转串口模块时需要把PA9接到模块的RXPA10接到模块的TX交叉连接。这是我见过最多的翻车点多数人会下意识把同名的TX和TX接在一起结果数据永远出不来。其次要共地模块的GND和板子的GND必须连在一起否则电平没有参考点通信随时可能错乱。电平匹配是另一个坑。STM32F103的I/O是3.3V逻辑很多USB转串口模块是5V逻辑。如果模块的输出是5V直接接到PA10上长期使用可能损伤引脚。正规的模块会做电平转换比如带输入输出电平指示的或者使用3.3V供电的CP2102N/CH340E等。如果你手里只有一个5V的模块最安全的方案是在RX线上串一个1K电阻把电流限制住再配合内部钳位二极管勉强能用但不推荐长期用。串口线本身也会出问题。常见的是老款USB转串口芯片在系统上驱动不稳定设备管理器里显示黄色感叹号。这时换一根常见方案的线最省事。还有的是USB延长线太长导致供电不足模块指示灯亮但通信失败。用串口助手时如果发送了数据但开发板没反应先用回环法测试串口线把模块的TX和RX短接自发自收如果能在串口助手看到自己发的数据说明模块和驱动没问题问题在目标板接线或代码。这个方法能快速定位问题。这一章内容比较多主要是把配置阶段的坑点铺开为后面的代码和排错打底。记住一个现象配置好了之后生成的代码本身不会错错的往往是时钟源、引脚映射和外部接线这三件事。3. HAL_UART_Receive_IT与发送函数中断收发的代码骨架3.1 用回调函数HAL_UART_RxCpltCallback处理不定长数据HAL库的中断接收机制不是“收到数据就自动存到缓冲区”而是要先调用一次HAL_UART_Receive_IT函数告诉HAL库你希望接收多少个字节。比如你调用HAL_UART_Receive_IT(huart1, buffer, 1)那么每收到一个字节就会触发一次回调。这是最基础的做法但效率不高因为每收一个字节都要重新调用一次。在数据量小、波特率低的场合完全够用。在实际项目中我一般先约定一帧数据的长度比如8个字节再用HAL_UART_Receive_IT(huart1, rx_buf, 8)去接收。当8个字节全部到达后HAL_UART_RxCpltCallback被调用在这里处理数据然后再重新调用一次HAL_UART_Receive_IT继续接收。这种固定长度的方式比较适合通信协议固定、帧长不变的场景比如传感器模块每帧固定输出8字节。回调函数的标准写法uint8_t rx_buf[8] {0}; uint8_t rx_flag 0; void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART1) { rx_flag 1; // 标记接收到完整一帧 } }这个回调函数的触发条件是之前调用的HAL_UART_Receive_IT收到了指定数量的字节。函数里通过huart-Instance判断是哪个串口因为多个串口共用同一个回调入口。rx_flag是主循环里检查的标记处理完数据后要手动清除。注意回调函数是在中断上下文中执行的不要在回调函数里做耗时操作比如printf打印、延时、内存分配等。正确做法是置个标志位把数据处理放到主循环里。如果你要处理不定长数据固定长度的方式就不够用了。这时要么用DMA加空闲中断要么在接收完一个字节后把接收到的字节拼接起来。更简单的做法是每次调用HAL_UART_Receive_IT(huart1, temp, 1)接收一个字节然后在回调函数里把temp保存到自己的数组并判断是否满足帧结束条件。代码框架uint8_t temp 0; uint8_t cmd_buf[64] {0}; uint8_t idx 0; void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART1) { cmd_buf[idx] temp; if (temp \n || idx sizeof(cmd_buf) - 1) { rx_flag 1; idx 0; } HAL_UART_Receive_IT(huart1, temp, 1); } }这段代码先定义一个临时变量temp每次接收一个字节在回调里存储到cmd_buf。如果收到换行符或者缓冲区快满就置位rx_flag。注意在回调里重新调用HAL_UART_Receive_IT是为了让串口持续接收。如果不重新调用之后串口就不再接收数据了这是HAL库最容易踩的坑之一。这种逐字节接收的方式适合数据长度不定的场景但缺点是频繁进入中断波特率高时CPU负担大比如115200下10字节/ms每个字节触发一次中断CPU占用很高。所以做产品级代码时我更倾向于用DMA空闲中断后面会讲。提示HAL_UART_Receive_IT的长度参数决定了每次回调触发需要积累多少个字节。不要把这里的长度理解成缓冲区大小而是“目标接收字节数”。3.2 发送数据的三种方式阻塞、中断和DMA的适用边界HAL库发送数据有三种调用方式。第一种是HAL_UART_Transmit阻塞发送。调用后函数会一直等待直到所有字节都移出数据寄存器才返回。比如发送16字节时在9600波特率下大约需要16/960*1.1约18ms这个时间内CPU被占用。适合调试时打印日志或者发送短小的查询指令。第二种是HAL_UART_Transmit_IT中断发送。调用后立即返回数据在串口中断中逐字节发送发送完成会回调HAL_UART_TxCpltCallback。适合发长数据但需要保证发送缓冲区在发送完成前不能被修改。比如你发一个全局数组紧接着又往数组里写新数据新内容可能被当成串口数据发出去产生莫名其妙的帧错乱。第三种是HAL_UART_Transmit_DMADMA发送。调用后返回硬件DMA负责把数据移位到串口发送寄存器发送完成后触发回调。适合大批量数据比如音频流或固件升级。CPU基本不用参与但缓冲区不能是局部变量因为DMA是硬件异步搬运函数返回后栈内存就失效了。常见做法是把发送缓冲区定义为static或全局。发送代码示例char msg[] Hello STM32\r\n; HAL_UART_Transmit(huart1, (uint8_t *)msg, strlen(msg), 100);这里的100是超时时间单位是ms。如果串口发送队列堵塞超过100ms函数会返回超时错误。实际项目中我把超时时间设成1000。但要注意阻塞发送里这个超时是“轮询等待”的实现HAL库在空闲时进入等待每毫秒检查一次状态超时后返回HAL_TIMEOUT。如果你设成HAL_MAX_DELAY那就会永远等下去。在某些低功耗场景恶意阻塞可能导致系统进入休眠时串口没发完。中断发送的写法uint8_t tx_data[32] {0}; HAL_UART_Transmit_IT(huart1, tx_data, sizeof(tx_data));调用后不需要等待你可以继续干别的事。但如果你紧接着修改tx_data的内容可能发生数据错乱因为发送还没完成。所以要么等发送完成回调要么用双缓冲区。这个坑在慢波特率下尤其明显比如9600波特率下发送32字节需要约30ms这期间CPU已经跑了好几万条指令。DMA发送的缓冲区常见的错误是void send_data(size_t len) { uint8_t buf[64]; HAL_UART_Transmit_DMA(huart1, buf, len); // 危险函数返回后buf失效 }正确做法是把buf改成静态数组。我一般还会在发送前加一份拷贝防止数据在DMA期间被修改。3.3 中断优先级与NVIC一个被忽略的丢数据来源CubeMX中使能USART1中断后NVIC设置页面会出现USART1 global interrupt的优先级选项。默认的抢占优先级是0子优先级是0。对于串口接收中断建议抢占优先级不要设得太高因为每个字节都会触发中断如果优先级高于系统滴答定时器可能会影响系统时钟。反过来如果系统里其他中断频繁打断串口接收而你没有及时读走数据就可能发生溢出丢包。某开发者曾把USART1中断的抢占优先级设为最高然后又跑了一段软件定时器中断串口数据频繁丢失。原因是软件定时器中断被串口抢占但串口中断里又重新调用了HAL_UART_Receive_IT这段代码虽然短多次叠加后导致定时器任务超时。后来把串口中断优先级降到5问题就消失了。所以优先级需要根据系统负载权衡一般把串口优先级设为中等偏上即可。HAL库生成的NVIC初始化代码HAL_NVIC_SetPriority(USART1_IRQn, 5, 0); HAL_NVIC_EnableIRQ(USART1_IRQn);STM32F103内核的优先级位共4位CubeMX中可以通过“Priority Group”设置分组模式。默认是Group 4即4位全部用于抢占优先级子优先级固定为0。所以你看到的优先级范围是0到15数值越小优先级越高。如果配置成Group 2那么高两位是抢占优先级低两位是子优先级。这个分组如果和操作系统配置不一致会导致中断无法正确嵌套。如果你的工程没跑系统保持默认即可。另外中断服务函数不需要你手动编写逻辑。CubeMX生成的stm32f1xx_it.c中已经包含了USART1_IRQHandler里面调用HAL_UART_IRQHandlerHAL库会自己判断是接收、发送还是错误中断并调用相应的回调函数。网上一些老代码教你手动操作SR寄存器清除标志在HAL库下属于多余操作而且可能和HAL内部状态冲突。所以直接用生成的就好。4. 从CubeMX生成代码到点灯验证一份能跑的串口回环Demo4.1 生成代码后必须补的那几行变量与初始化在CubeMX中完成配置点击生成代码后工程结构是main.c、stm32f1xx_it.c等。你需要手动添加的主要是接收缓冲区和回调函数。以最小工程为例我们在main.c的私有变量区定义一个接收缓冲区然后在main函数里初始化串口后调用一次HAL_UART_Receive_IT启动第一次接收。代码/* Private variables */ UART_HandleTypeDef huart1; uint8_t rx_buf[16] {0}; uint8_t rx_complete_flag 0; int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_USART1_UART_Init(); HAL_UART_Receive_IT(huart1, (uint8_t *)rx_buf, 16); while (1) { if (rx_complete_flag) { // 把收到的数据原样发回去形成回环 HAL_UART_Transmit(huart1, rx_buf, 16, 1000); rx_complete_flag 0; HAL_UART_Receive_IT(huart1, rx_buf, 16); } } }注意这里rx_buf大小是16所以必须收满16字节才会触发一次回调。如果你在串口助手里发送“hello”只有5个字节那永远不会触发回调这是初学者最常见的问题。解决办法是按实际帧长度定义缓冲区或者改用单字节接收自定义结束符方式。回环测试时发送长度为16字节的固定数据包即可验证通路。回调函数定义在哪里它可以放在main.c的末尾也可以单独建一个文件。HAL库在stm32f1xx_hal_uart.c里有弱定义你定义的强定义会覆盖它。注意回调函数里不要调用HAL_UART_Transmit这种阻塞函数因为你现在就在中断上下文里再发送会阻塞等待而且嵌套调用容易出问题。正确做法是置标志位在主循环中发送。说到Error_Handler默认生成的代码在出现错误时会进入死循环。调试时可以在Error_Handler里加一个闪烁LED或者在串口里输出错误代码。实际工程中可以使用HAL_UART_ErrorCallback来捕获串口错误避免程序卡死。4.2 主循环里怎么组织接收标记与发送触发主循环的典型结构是轮询标志位、处理数据、重新启动接收。这里有个容易犯的错重新调用HAL_UART_Receive_IT在什么位置如果放在处理完数据之后那在处理期间中断就无法接收新数据可能造成数据覆盖。如果放在处理之前缓冲区可能在处理中被新数据覆盖。细想一下HAL_UART_Receive_IT只是启动接收数据到达后是中断硬件往缓冲区里写的。如果你在处理旧数据的同时启动了新接收那么新数据会写入同一个缓冲区当下一次回调触发时旧数据可能还没被处理完。所以正常的顺序是收到完整一帧后不再调用Receive_IT直到主循环处理完当前帧再调用一次开启下一帧接收。这样可以避免缓冲区竞争。一个更稳妥的框架是使用双缓冲区。定义rx_buf1和rx_buf2每帧交替使用。在处理rx_buf1时开启对rx_buf2的接收。这种乒乓缓冲在数据量大的时候很有用但普通Demo不需要。作为工程师我通常先固定帧长用单缓冲区加标志位确认调试成功后再升级到DMA空闲中断。在验证回环时注意串口助手的设置波特率1152008数据位1停止位无校验。发送时如果你用的是字符串模式并且勾选了“自动追加回车换行”那么实际发送的数据会多出\r\n。如果你固定接收16字节就会因为多了两个字节导致对不上。所以调试时要么关闭追加要么发送HEX格式固定长度。4.3 用LED指示收发状态肉眼看到的才算数串口通信这种不可见的逻辑最好加一个可视化指示。在GPIO配置里我们把PC13设为输出这个引脚上板载LED通常低电平点亮。收到数据时翻转LED发送完成时再翻一次。这样你不需要看串口助手的日志就知道收发是否在跑。示例代码void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART1) { rx_complete_flag 1; HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin); } }主循环里发送后也翻转一次。如果LED只亮一次不再动说明接收根本没触发如果LED快速闪烁但串口没有数据说明你收到的数据都是垃圾或者正在处理错误。这个经验在调试无线模块时尤其有用因为你不知道干扰还是链路问题先把串口链路用LED确认了再排查后面。5. 串口通信避坑指南乱码、丢数据、卡死的排查路径5.1 现象一收到乱码波特率明明没错现象串口助手打印出来的内容是乱码但频率不高偶尔有一两个字符错误。原因最常见的是时钟树配置错误。如果USART1使用的时钟不是72MHz而是36MHz甚至8MHz那么计算出的波特率与真实波特率存在偏差。比如在115200波特率下如果时钟差一倍接收方会在每个位周期采样点偏离产生误判。另一个常见原因是USB转串口模块质量差或者接线过长。解决先用CubeMX重新确认时钟配置。把HCLK设为72MHzAPB2时钟不分频APB1二分频。然后在代码里通过串口发送一个固定字符串用逻辑分析仪测量实际波形或者换一个已知正常的串口模块交叉验证。还要检查串口助手的波特率是否与代码一致。有时候串口助手有两个波特率设置比如数据位和停止位旁边有个“流控”选项也影响通信。关掉所有流控选项。5.2 现象二偶尔丢一包数据回调没触发现象发送多帧数据有时第一帧正常第二帧就丢失或者数据是错乱的。原因这是典型的接收缓冲区被覆盖。当你调用HAL_UART_Receive_IT接收固定长度数据时如果对方发送的数据不连续每帧之间间隔很短主循环还没来得及处理上一帧下一帧已经把缓冲区写满并触发回调。但实际上HAL库的Overrun检测机制在检测到接收溢出时会停止接收这时回调不会触发而是进入HAL_UART_ErrorCallback。很多人没实现这个错误回调所以看起来就是“数据丢了”。解决实现HAL_UART_ErrorCallback并在里面清除错误标志后重新启动接收。代码片段void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART1) { __HAL_UART_CLEAR_OREFLAG(huart1); HAL_UART_Receive_IT(huart1, rx_buf, 16); } }__HAL_UART_CLEAR_OREFLAG是清溢出标志的宏。注意HAL库版本差异老版本可能叫__HAL_UART_CLEAR_FLAG。错误回调重新启动接收后至少不会卡死。但要真正解决丢包还是得优化上层协议比如增加帧序号和重发机制或者接收端改用DMA方式来降低CPU响应压力。注意清除溢出标志后重新启动接收前最好先确认中断接收没有在其他地方同时启动避免重入。5.3 现象三程序死在HAL_UART_Receive_IT里现象调用HAL_UART_Receive_IT后程序进入死循环或卡死在Error_Handler。原因一种情况是串口外设没有正确使能时钟或引脚冲突导致初始化失败。另一种情况是你在中断回调里调用了可能阻塞的函数比如HAL_UART_Transmit而发送缓冲区恰好有数据发送过程又被当前中断打断造成死锁。还有一种是接收长度大于缓冲区实际大小导致数组越界破坏了内存。解决先检查初始化函数是否返回HAL_OK。在Error_Handler里加个串口打印或LED闪烁别让Bug静默。然后审查回调函数把数据处理都放到主循环中断里只置标志。接收缓冲区大小与HAL_UART_Receive_IT的length参数严格一致别用sizeof(指针)这种写法。另外某些HAL库版本在UART中断中反复调用Receive_IT会导致重入问题可以在错误回调里加一个静态标志如果还忙就忽略这一次。5.4 现象四发送中文乱码接收英文正常现象发送ASCII字符一切正常发送中文字符串就变成乱码。原因这个和串口本身无关是编码问题。在常见的IDE环境中如果源文件编码是GB2312串口助手以UTF-8方式接收就会显示乱码。反过来如果源文件是UTF-8而串口助手按GBK解码同样乱码。还有的IDE会在编译时根据编译器选项把字符串编码转换导致代码文件中看着是中文Flash里存的却是另一种编码。解决调试时统一使用十六进制或纯ASCII别用中文。如果必须发中文确认代码源文件编码和串口助手的解码编码一致。最省心的方法是输出英文或拼音反正协议里中文也不是必须的。另外有的串口助手有“字符集选择”下拉框切换成GB2312或UTF-8看效果。5.5 现象五热插拔或上电顺序导致通信失败现象板子上电后第一次发送没反应重新给串口模块断电再上电又正常了。原因这个多半是USB转串口模块和开发板的电源时序问题。如果开发板先上电串口模块后上电模块的TX引脚在初始化瞬间可能输出高低电平抖动被单片机当成一帧数据的开始导致接收状态错乱。反过来也可能。还有的是因为模块的DTR/RTS引脚在打开串口时产生电平跳变接到开发板的复位电路上导致单片机被意外复位。解决上电顺序固定为“先接串口模块再给单片机供电”或者反过来找到一个稳定的顺序。如果使用的是带DTR/RTS控制的模块在串口助手里把“DTR/RTS”相关的自动复位功能关掉。或者干脆在电路上把DTR/RTS悬空只接GND、TX、RX三根线。这个现象在调试配置了串口下载的板子上特别常见因为硬件设计上会把DTR连到复位脚。6. 进阶把收发改成带空闲中断的DMA环形缓冲6.1 为什么需要IDLE中断不定长帧的可靠接收前面说到的固定长度接收有一个麻烦如果通信双方不约定帧长度就无法确定何时“接收完成”。很多自定义协议是用帧头长度字段的方式也有的干脆以换行符作为结尾。最稳妥的办法是利用串口的空闲中断IDLE。当串口在一帧数据后出现总线空闲硬件会触发IDLE中断此时你就能知道“这一帧已经收完了”。IDLE中断配合DMA可以自动接收数据到缓冲区空闲时回调CPU只在帧结束时处理数据效率很高。STM32F103的USART1接收DMA通道是DMA1 Channel5使用外设到内存模式字节数设为最大可能长度。配置步骤CubeMX中在串口配置里使能USART1中断然后在DMA Settings中添加USART1_RX的DMA通道模式Normal。注意DMA模式不要选Circular循环模式否则计数器回绕后难以准确计算接收长度。Normal模式配合IDLE中断是最常用的组合。6.2 空闲中断DMA的完整实现与重启流程这里给出一个可用的代码骨架。在CubeMX里配置DMA后把生成的代码补充为#define RX_BUF_SIZE 128 uint8_t dma_rx_buf[RX_BUF_SIZE]; volatile uint16_t rx_len 0; volatile uint8_t idle_flag 0; void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { // 如果用DMA正常接收完成缓冲区满这里可以作为备用 } void USART1_IRQHandler(void) { HAL_UART_IRQHandler(huart1); if (__HAL_UART_GET_FLAG(huart1, UART_FLAG_IDLE) ! RESET) { __HAL_UART_CLEAR_IDLEFLAG(huart1); // 停止DMA计算接收长度 HAL_UART_DMAStop(huart1); rx_len RX_BUF_SIZE - __HAL_DMA_GET_COUNTER(hdma_usart1_rx); idle_flag 1; // 在这里可以处理数据也可以设置标志交给主循环 UART1_Restart(); } }这段代码在USART1的中断服务函数中检查空闲标志。当总线空闲DMA已经接收完一帧数据此时通过DMA剩余计数器rx_len RX_BUF_SIZE - __HAL_DMA_GET_COUNTER(...)算出实际接收字节数。然后置idle_flag通知主循环并调用UART1_Restart重新准备下一次接收。UART1_Restart函数实现void UART1_Restart(void) { __HAL_UART_CLEAR_OREFLAG(huart1); __HAL_UART_CLEAR_IDLEFLAG(huart1); HAL_UART_Receive_DMA(huart1, dma_rx_buf, RX_BUF_SIZE); }需要注意的是HAL_UART_DMAStop会关闭DMA和串口接收所以重启时必须重新调用HAL_UART_Receive_DMA来恢复。清除溢出标志和IDLE标志的顺序要放在启动DMA之前否则残留标志可能导致立即触发错误中断。另外HAL_UART_IRQHandler内部对错误和正常中断的处理不会和你手动检查IDLE冲突因为IDLE标志不是接收完成标志HAL库默认不会清除它。我们手动清除是安全的。使用DMA接收时缓冲区的大小要留有余量。如果你预计最大帧长度是64字节建议缓冲区设为128甚至256防止一帧数据超过缓冲区大小导致DMA溢出或覆盖其他内存。另外DMA接收缓冲区必须对齐到4字节边界否则在某些编译器或高优化等级下DMA访问可能出错。可以在定义数组时用__attribute__((aligned(4)))。这个方案的验证方法连接串口助手在发送区填入一帧数据但不限制长度点击发送。观察LED或打印信息确认接收长度等于实际发送长度。然后再连续发送多帧中间间隔几十毫秒确认每帧都能正确捕获。如果帧与帧间隔太短比如小于一个字符的传输时间IDLE中断不会触发这时需要协议上增加帧间隔的约束。从那以后我每次写串口接收代码都强制走一遍这个流程先固定帧长跑通再换DMA加IDLE最后验证多帧间隔下的稳定性。这个习惯帮我省下了很多现场调试的时间。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑