参考资料4.1 建立 CubeMX 项目4.2 移植lwRB4.3 串口发送程序设计4.3.1 APP层 - usart_send_string4.3.2 启动发送-usart_start_tx_dma_transfer4.3.3 完成回调-HAL_UART_TxCpltCallback4.4 串口接收程序设计4.4.1 接收回调 - HAL_UARTEx_RxEventCallback4.4.2 用户线程 - usart_rx_dma_thread4.4.3 数据处理函数 - usart_rx_check
本节使用 CubeMX 图形工具配置串口收发,基于HAL驱动库,移植环形缓冲区 LwRB,实现上一节所提最优的串口收发方式(LL驱动库方式可以直接参照上一节参考资料)。
开发环境搭建和如何新建项目参考开发框架02-开发环境搭建
参考资料
- HAL 和 LL 串口驱动使用说明,在 https://www.st.com/en/embedded-software/stm32cube-mcu-mpu-packages.html 中查询到对应芯片手册。以stm32f4为例,参考【3.12 How to use HAL drivers】、【70.2.1 How to use this driver】。
- STM32Cube MCU Package中有许多HAL 和 LL 驱动使用例程(同一个系列差别不大,可以互相借鉴),可以从这里下载。借鉴UART文件夹下项目。


常见的串口收发程序存在如下问题,详细讨论返回上一节 查看。
- 未使用串口 IDEL 中断,中断字符处理,导致频繁进入中断,影响系统时序
- 未使用 DMA,CPU占用过高
- 阻塞方式导致 CPU 资源的浪费(串口硬件输出速度远低于CPU运行速度)。
- 收发过程存在负载平衡问题
比如接收数据,若突发数据量大,应用层数据处理缓慢,来不及从缓存中取出数据,会导致缓存数据覆盖丢失情况,对于发送数据,多线程服务同时发送数据时,若直接对接硬件,也会导致耦合严重,程序阻塞,处理不好也会导致数据丢失问题。以上两种都是生产和消费速率不一致导致的负载平衡问题。
接下来,让我们按步骤实现收发程序。
4.1 建立 CubeMX 项目
以 STM32F429BI 为例,说明在 STMCubeMX 中如何配置项目。
- 时钟 HSE 选择外部晶振。

- Debug 选择 Serial Wire,这里一定要配置,这篇介绍了未设置导致的芯片无法识别问题,以及解决方法。时基这里没有选择systick,而是选择了精度更高的TIM1。

- 串口(具体串口视板子情况确定)配置为异步通讯,硬件流控制未打开。

- 串口 DMA 配置中,映射串口收发对应的流(如下表格供选择时参考),并设设置对应优先级,这里设置为默认。
- 设置 DMA 方向 memory-to-peripheral
- 设置 DMA 为常规模式 normal mode
- 设置 DMA 方向 peripheral-to-memory
- 设置 DMA 为常规模式 circular mode
STM32 family | Board name | USART | STM32 TX | STM32 RX | RX DMA settings | TX DMA settings |
STM32F1xx | BluePill-F103C8 | USART1 | PA9 | PA10 | *DMA1 , Channel 5 * | ㅤ |
STM32F4xx | NUCLEO-F413ZH | USART3 | PD8 | PD9 | *DMA1 , Stream 1 , Channel 4 * | *DMA1 , Stream 3 , Channel 4 * |
STM32G0xx | NUCLEO-G071RB | USART2 | PA2 | PA3 | *DMA1 , Channel 1 * | ㅤ |
STM32G4xx | NUCLEO-G474RE | LPUART1 | PA2 | PA3 | *DMA1 , Channel 1 * | ㅤ |
STM32L4xx | NUCLEO-L432KC | USART2 | PA2 | PA15 | *DMA1 , Channel 6 , Request 2 * | ㅤ |
STM32H7xx | NUCLEO-H743ZI2* | USART3 | PD8 | PD9 | *DMA1 , Stream 0 * | *DMA1 , Stream 1 * |
STM32U5xx | NUCLEO-U575ZI-Q* | USART1 | PA9 | PA10 | *GPDMA1 , Channel 0 * | *GPDMA1 , Channel 1 * |

串口发送设置
串口接收设置
其他配置保持默认,这里没有打开 FIFO 设置。
- 添加操作系统需求,接口选择 CMSIS_V2。

- 修改使用外部告诉晶振HSE,我这里使用的是8MHz,设置目标频率为180MHz(最大),这两个参数视你的板子具体情况填写。

- 工具链选择 STM32IDE,生成在根目录下。

- 仅添加必要HAL库文件

4.2 移植lwRB
移植ringbuffer和unity
4.3 串口发送程序设计
总体过程是,应用层直接将数据发送到环形缓冲区,启动低层数据发送。发送函数解析缓冲区数据地址和长度配置DMA,开始发送。上一帧发送完成,在完成回调中继续调用发送函数,连续消费应用层存储 ringbuffer 中的数据。主要的工作状态如下图:

其中有两点需要注意:
- 为了保证环形缓冲区不被破坏,要对读写操作过程进行临界区保护。主要有两处可能存在风险,第一处应用层发送数据到环形缓冲区,若有多个线程同时向缓冲区添加数据,可能会导致数据错乱。第二处,是从缓冲区读出地址和长度,若上一个数据还未发送完,再次读取会导致地址和长度错误。
- 另外,使用 DMA 发送环形缓冲区数据地址必须连续(内存发送到外设,内存地址按递增处理),由于没有使用 lwrb_read 读取数据(因为DMA传输只需要获得地址和长度,环形缓冲区维护需要用户手动调整指针),因此 DMA 整个内存发送完毕后,需要手动调整 Read指针位置。
下面贴出主要接口源码,完整项目从这里获取。
4.3.1 APP层 - usart_send_string
4.3.2 启动发送-usart_start_tx_dma_transfer
4.3.3 完成回调-HAL_UART_TxCpltCallback
4.4 串口接收程序设计
串口接收综合使用串口空闲中断+DMA+中断处理线程化。其中,使用空闲中断,可以避免频繁进入字节接收中断,使用 DMA 可以降低CPU使用率,中断处理线程化遵从了中断服务例程尽可能简短原则,通过同步机制将任务转移到任务调度中处理。
首先,我们来看一下HAL库使用手册,摘自参考资料 1。
When HAL_UARTEx_ReceiveToIdle_IT() or HAL_UARTEx_ReceiveToIdle_DMA() API are called, progress of reception process is provided to application through calls of Rx Event callback (either default one HAL_UARTEx_RxEventCallback() or user registered one). As several types of events could occur (IDLE event, Half Transfer, or Transfer Complete), this function allows to retrieve the Rx Event type that has lead to Rx Event callback execution. This function is expected to be called within the user implementation of RxEvent Callback, in order to provide the accurate value
In Interrupt Mode : a). HAL_UART_RXEVENT_TC : when Reception has been completed (expected nb of data has been received) b). HAL_UART_RXEVENT_IDLE : when Idle event occurred prior reception has been completed (nb of received data is lower than expected one)
In DMA Mode : a). HAL_UART_RXEVENT_TC : when Reception has been completed (expected nb of data has been received) b). HAL_UART_RXEVENT_HT : when half of expected nb of data has been received c). HAL_UART_RXEVENT_IDLE : when Idle event occurred prior reception has been completed (nb of received data is lower than expected one). In DMA mode, RxEvent callback could be called several times; When DMA is configured in Normal Mode, HT event does not stop Reception process; When DMA is configured in Circular Mode, HT, TC or IDLE events don't stop Reception process;
上面引用文字的意思是,调用
HAL_UARTEx_ReceiveToIdle_DMA()
接收,IDEL事件,传输一半和传输完成事件都会调用 HAL_UARTEx_RxEventCallback()
。下面我们来分析一下,内部如何实现。DMA配置参考4.1 建立CubeMX项目内容。
4.4.1 接收回调 - HAL_UARTEx_RxEventCallback
首先,
stm32f4xx_it.c
中中断服务程序定义了HAL_UART_IRQHandler
和 HAL_DMA_IRQHandler
。下面分析一下,串口空闲中断,DMA HT和TC中断函数调用链。
HAL_UART_IRQHandler
:内部判断是否进入空闲中断,空闲中断中则调用HAL_UARTEx_RxEventCallback
,执行用户逻辑。
HAL_DMA_IRQHandler
:内部调用传输过半和传输完成回调函数,执行用户逻辑
hdma->XferHalfCpltCallback
和 hdma->XferCpltCallback
在 UART_Start_Receive_DMA
指定。UART_DMAReceiveCplt
中首先判断串口当前的接收模式,若为空闲中断接收,则直接使用HAL_UARTEx_RxEventCallback
回调;若不是,则在HAL_UART_RxCpltCallback
调用用户逻辑(通常为解包代码,这里主要进行回环测试,即收到什么发出什么)。综上,串口空闲、DMA HT和TC三个中断,均会
HAL_UART_RxCpltCallback
,执行用户逻辑。我们只需要重写HAL_UART_RxCpltCallback
即可,不用关注HAL_UART_RxCpltCallback
和HAL_UART_RxHalfCpltCallback
。4.4.2 用户线程 - usart_rx_dma_thread
创建初始化线程
init_thread
,新建消息队列usart_rx_dma_queue_id
,用于中断和用户处理线程 usart_rx_dma_thread
同步信息。中断接收到数据,通过消息队列通知
usart_rx_dma_thread
开始处理数据,没有数据时,线程usart_rx_dma_queue_id
处于阻塞状态(CPU让渡出去)。usart_rx_dma_thread
中调用接收数据处理函数usart_rx_check()
。中断
HAL_UART_RxCpltCallback
不执行数据处理工作,而是通过同步机制(示例使用队列,信号量等也可行)通知线程usart_rx_dma_thread
处理。4.4.3 数据处理函数 - usart_rx_check
usart_rx_check
将数据存储到缓存中,并调用usart_process_data
处理。主要有如下注意事项:- 线性缓存存储数据存在溢出风险,对于溢出场景要特殊处理。参考【 三、详谈串口讯通讯机制(以STM32为例) 3.3.2 中断组合以及Overflow异常 】获取详细说明。
- 用户会在中断(DMA HT, DMA TC, UART IDLE)和用户线程调用该函数,需要做好独占访问保护。(也可以通过架构设计避免该问题,比如中断设置为同一抢占优先级,使用同步机制,只唤醒单个线程)
- 若数据消耗速度跟不上生产速度,则DMA会发生溢出,造成数据丢失。可以通过改进架构或者提高缓存尺寸来解决该问题。本文打开DMA HT, DMA TC, UART IDLE中断,通知更加及时,并通过处理线程化,降低数据处理延迟。
接收到数据,为了测试目的,这里做简单的回环处理。
↩️ 返回 开发框架09-高效可靠串口通讯
⬅️上一节 ➡️ 下一节