首页 / 资讯中心 / 文章详情

STM32F429标准库串口通信实战:从中断收发到DMA优化

STM32F429标准库串口通信实战:从中断收发到DMA优化 ★ FEATURED ARTICLE
简介这份资源面向使用STM32F429进行嵌入式开发的初学者与工程师聚焦标准库下的串口通信实现帮助读者解决GPIO复用配置、USART初始化、数据收发与错误处理等常见问题。压缩包共295个文件约17.11MB包含45个c源文件与46个h头文件覆盖stm32f4xx_tim、rcc、rtc、adc、flash、fmc、can等外设驱动另有62个o目标文件、61个crf交叉引用文件及axf、map、lst等编译产物并附带uvprojx工程、sct分散加载与bat脚本便于直接编译验证。资源围绕USART1的PA9、PA10引脚配置展开涉及波特率、数据位、停止位、校验位设置以及阻塞与中断两种收发方式、DMA传输和HAL_UART_ErrorCallback错误回调等要点可帮助读者掌握帧错误、校验错误与溢出错误的排查思路。目前已有1029人学习下载适合作为串口通信调试与项目移植的参考工程。1. 从寄存器到中断为什么标准库串口通信依然是绕不开的基本功很多人第一次在 STM32F429 上点亮 LED 之后紧接着要啃的就是串口通信。原因很直接没有串口你连程序跑没跑起来都不知道调试基本靠猜。标准库虽然这几年被 HAL 库抢了不少风头但在很多存量项目、教学场景和对执行效率有要求的场合它依然是首选。这套资源围绕 STM32F429 标准库串口通信展开覆盖从 GPIO 复用配置、USART 初始化、中断收发到环形缓冲区管理的完整链路。适合刚接触 F429 的嵌入式新手也适合从 F103 迁移过来、想搞清楚 F429 时钟树和串口差异的老手。串口通信看着简单但真要做到稳定收发不丢包里面有不少细节值得掰开揉碎讲。2. 标准库串口通信的底层逻辑与工程搭建2.1 F429 的 USART 外设和 F103 有什么本质区别STM32F429 的 USART 外设相比 F103 系列最明显的变化是时钟源和引脚复用的复杂度上了一个台阶。F103 的 USART1 挂在 APB2 上配置起来相对直接F429 的 USART1 和 USART6 挂在 APB2USART2/3 挂在 APB1但 APB1 和 APB2 的时钟频率在 F429 上可以配到很高比如 APB2 跑 90MHz、APB1 跑 45MHz这就意味着波特率计算时的分频系数跟 F103 完全不是一回事。另一个容易翻车的地方是 GPIO 复用功能。F103 上配置 USART 引脚只需要把 GPIO 模式设为复用推挽再使能外设时钟就行。F429 多了 GPIO 复用功能选择寄存器GPIO_AFRL 和 GPIO_AFRH你必须明确把对应引脚复用成 USART 功能否则引脚就是普通 IO串口死活不出波形。我见过不止一个从 F103 转过来的开发者卡在这里查了半天代码觉得没问题其实就是少配了一步 GPIO_PinAFConfig。F429 的 USART 还支持更高的波特率理论上可以到 fPCLK/8实际项目中跑到 921600 甚至更高都没问题。但高波特率下对时钟精度的要求也更高如果外部晶振选的是 8MHz 而 PLL 配置没算准高波特率下误码率会明显上升。2.2 用标准库搭一个可复用的串口工程骨架先看工程目录结构。我一般会这样组织Project/ ├── CMSIS/ # 内核相关文件 ├── FWLIB/ │ ├── inc/ # 标准库头文件 │ └── src/ # 标准库源文件 ├── User/ │ ├── main.c │ ├── stm32f4xx_conf.h │ ├── usart.c # 串口配置 │ └── usart.h └── Output/ # 编译输出标准库的移植步骤这里不展开重点看串口配置部分。下面是一个 USART1 的初始化函数波特率 1152008 位数据位1 位停止位无校验#include stm32f4xx.h #include usart.h void USART1_Init(uint32_t baudrate) { GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; /* 使能 GPIOA 和 USART1 时钟 */ RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); /* 将 PA9 和 PA10 复用为 USART1 */ GPIO_PinAFConfig(GPIOA, GPIO_PinSource9, GPIO_AF_USART1); GPIO_PinAFConfig(GPIOA, GPIO_PinSource10, GPIO_AF_USART1); /* 配置 PA9 为复用推挽输出TX */ GPIO_InitStructure.GPIO_Pin GPIO_Pin_9; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF; GPIO_InitStructure.GPIO_OType GPIO_OType_PP; GPIO_InitStructure.GPIO_PuPd GPIO_PuPd_UP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); /* 配置 PA10 为浮空输入RX */ GPIO_InitStructure.GPIO_Pin GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF; GPIO_InitStructure.GPIO_PuPd GPIO_PuPd_NOPULL; GPIO_Init(GPIOA, GPIO_InitStructure); /* USART1 参数配置 */ USART_InitStructure.USART_BaudRate baudrate; USART_InitStructure.USART_WordLength USART_WordLength_8b; USART_InitStructure.USART_StopBits USART_StopBits_1; USART_InitStructure.USART_Parity USART_Parity_No; USART_InitStructure.USART_HardwareFlowControl USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode USART_Mode_Rx | USART_Mode_Tx; USART_Init(USART1, USART_InitStructure); /* 使能接收中断 */ USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); /* 配置 NVIC */ NVIC_InitStructure.NVIC_IRQChannel USART1_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority 1; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); /* 使能 USART1 */ USART_Cmd(USART1, ENABLE); }这段代码里有几个关键点需要说明。GPIO_PinAFConfig是 F4 系列特有的复用配置函数第一个参数是 GPIO 端口第二个是引脚编号第三个是复用功能编号。PA9 和 PA10 对应 USART1 的 AF7标准库里已经用GPIO_AF_USART1这个宏定义好了。GPIO 模式这里 TX 配成复用推挽、RX 配成复用浮空这是常见做法。有人会把 RX 配成上拉输入短距离通信没问题但长距离或者有干扰的环境下上拉可能影响电平判决建议还是按手册推荐来。NVIC 的优先级分组需要在 main 函数开头设置一次比如NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2)这样抢占优先级和响应优先级各占 2 位。如果串口中断优先级设得太低在高频中断场景下可能丢数据。2.3 中断接收和环形缓冲区的配合方式直接在中断里处理数据是大忌。中断服务函数应该尽量短只做数据搬运把解析逻辑放到主循环里。下面是一个典型的环形缓冲区实现#define RX_BUFFER_SIZE 256 typedef struct { uint8_t buffer[RX_BUFFER_SIZE]; volatile uint16_t head; volatile uint16_t tail; } RingBuffer; static RingBuffer rxBuf { .head 0, .tail 0 }; /* 中断服务函数 */ void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET) { uint8_t data USART_ReceiveData(USART1); uint16_t next (rxBuf.head 1) % RX_BUFFER_SIZE; if (next ! rxBuf.tail) { /* 缓冲区未满 */ rxBuf.buffer[rxBuf.head] data; rxBuf.head next; } /* 缓冲区满则丢弃也可以在这里置溢出标志 */ USART_ClearITPendingBit(USART1, USART_IT_RXNE); } } /* 主循环中读取 */ uint16_t RingBuffer_Available(void) { return (rxBuf.head - rxBuf.tail RX_BUFFER_SIZE) % RX_BUFFER_SIZE; } uint8_t RingBuffer_Read(void) { uint8_t data rxBuf.buffer[rxBuf.tail]; rxBuf.tail (rxBuf.tail 1) % RX_BUFFER_SIZE; return data; }head 和 tail 都加了 volatile 修饰因为它们在中断和主循环中都会被访问不加的话编译器优化可能出玄学问题。缓冲区大小取 2 的幂次可以优化取模运算但 256 本身也不大直接用 % 运算可读性更好。判断缓冲区满的条件是(head 1) % SIZE tail这样会浪费一个字节的空间但换来了无锁设计的简洁性。如果对空间极度敏感可以用额外的标志位来区分满和空但代码复杂度会上升。3. 串口通信的避坑与常见问题排查3.1 现象串口助手收到乱码原因通常有三个波特率不匹配、时钟配置错误、或者 GPIO 复用没配对。先确认串口助手和代码里的波特率一致然后检查 SystemInit 里 PLL 的配置特别是 HSE 的值。如果用的是 8MHz 晶振但代码里按 25MHz 算系统时钟就是错的波特率自然也对不上。最后确认 GPIO_PinAFConfig 有没有调用这个漏了的话引脚根本不在 USART 模式。3.2 现象能发不能收或者能收不能发先看硬件接线TX 和 RX 有没有交叉接。然后检查 GPIO 模式TX 要配复用推挽RX 要配复用输入。如果用的是中断接收确认 NVIC 配置和 USART_IT_RXNE 使能有没有漏。还有一种情况是 USART_Cmd 在中断配置之前就调用了导致第一个字节丢失建议先配置完再使能 USART。3.3 现象高波特率下丢数据F429 在 115200 下一般很稳但跑到 921600 以上时如果中断优先级太低或者中断里处理时间太长就会丢字节。解决办法是提高串口中断的抢占优先级同时确保中断服务函数里只做最少的操作。另外检查 APB 时钟频率如果 APB2 只有 45MHz 而你要跑 921600分频系数会很小误差可能超过 3%这时候要么提高 APB2 时钟要么换用支持更高时钟的 USART。3.4 现象程序跑一段时间后串口无响应大概率是缓冲区溢出或者中断标志没清。检查环形缓冲区的满判断逻辑如果 head 和 tail 的类型是 uint8_t 而缓冲区大小是 256取模运算会出问题。另外确认 USART_ClearITPendingBit 有没有调用虽然读取 USART_ReceiveData 会自动清 RXNE 标志但显式清除更保险。还有一种可能是中断里发生了硬件错误比如访问了非法地址导致程序跑飞这种情况需要结合 HardFault 调试。3.5 现象用 printf 重定向后程序卡死printf 重定向到串口时如果用的是阻塞发送而串口初始化还没完成就调用了 printf程序会卡在等待 TXE 标志的死循环里。解决办法是在 printf 之前确保串口已经初始化或者在重定向函数里加超时退出机制。另外注意 microlib 和标准库的 printf 实现差异microlib 不支持浮点数打印如果需要打印浮点得用标准库或者自己实现格式化。4. 进阶技巧用 DMA 把 CPU 从串口收发里解放出来中断收发在低波特率下够用但如果你要连续接收大量数据比如接一个 GPS 模块或者和另一个 MCU 高速通信中断频繁触发会吃掉不少 CPU 时间。这时候 DMA 就是更优解。F429 的 USART1 支持 DMA 收发配置思路是接收用 DMA 循环模式发送用 DMA 正常模式。先看接收端的 DMA 配置void USART1_DMA_Rx_Init(void) { DMA_InitTypeDef DMA_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2, ENABLE); DMA_InitStructure.DMA_Channel DMA_Channel_4; DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)USART1-DR; DMA_InitStructure.DMA_Memory0BaseAddr (uint32_t)rxDmaBuffer; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralToMemory; DMA_InitStructure.DMA_BufferSize RX_DMA_BUFFER_SIZE; DMA_InitStructure.DMA_PeripheralInc DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize DMA_PeripheralDataSize_Byte; DMA_InitStructure.DMA_MemoryDataSize DMA_MemoryDataSize_Byte; DMA_InitStructure.DMA_Mode DMA_Mode_Circular; DMA_InitStructure.DMA_Priority DMA_Priority_High; DMA_InitStructure.DMA_FIFOMode DMA_FIFOMode_Disable; DMA_Init(DMA2_Stream2, DMA_InitStructure); DMA_Cmd(DMA2_Stream2, ENABLE); USART_DMACmd(USART1, USART_DMAReq_Rx, ENABLE); }这里用的是 DMA2 Stream2 Channel4对应 USART1 的接收请求。循环模式下DMA 会自动把接收到的数据依次填入 rxDmaBuffer填满后从头覆盖。主循环里通过读取 DMA_GetCurrDataCounter 来获取当前接收进度或者用空闲中断来判断一帧数据结束。空闲中断的用法是在串口初始化时使能 IDLE 中断当总线空闲时触发中断此时读取 DMA 剩余计数就能算出这一帧收了多少字节。这个方案在 Modbus、GPS 解析等场景下非常实用。发送端用 DMA 就简单多了把数据填到发送缓冲区配置 DMA 正常模式启动传输等传输完成中断里置个标志就行。注意发送缓冲区在传输完成前不能被修改否则会发出错误数据。一个容易忽略的点是 DMA 和中断的优先级配合。如果 DMA 传输完成中断和串口接收中断同时触发优先级配置不当可能导致数据竞争。我一般会把 DMA 中断优先级设得比串口中断低一级确保串口接收不会因为 DMA 中断而延迟。从那以后我每次配置串口 DMA 都会先确认三件事DMA 流和通道有没有选对、外设地址和内存地址有没有搞反、循环模式和正常模式有没有用错。这三条踩过任何一条调试起来都是血泪经验。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站