导言
首先,进行USART和DMA状态监测、记录异常状态并主动处理,是高健壮性嵌入式系统开发的核心思想之一。
这种机制看似复杂,实则能有效保障系统长期、稳定地运行:
- 提升通讯可靠性。
- 降低维护成本。
- 增强系统自恢复能力。
因此,建议你在STM32项目中实现USART和DMA的健康监测机制。
另外,STM32串口+DMA通信中引入超时保护机制的主要目的是:
- 防止通讯过程中数据不完整或缺失(数据量小于半传输阈值,导致DMA半满、满传输中断均未触发)。可能有人会问?不是有接收空闲中断吗?确实有。但是,有可能接收空闲中断因某些原因导致失效。
- 利用定时器,定期检测DMA接收缓冲区数据变化情况。比如若在一定时间内未收到数据,触发处理或重启DMA。
因此,引入超时机制(Timeout),可有效提升通信实时性与健壮性。
项目地址:https://github.com/q164129345/MCU_Develop/tree/main/stm32f103_ll_library11_usart_dma_error_check
一、监控串口异常
1.1、寄存器USART_SR
如上所示,串口的状态寄存器ISR一共有4个错误标志。其中位0的校验错误是在奇偶校验功能开启后才会触发,然而,这个功能一般不会开启。
以上这4个错误的清除方式都是先读USART_SR再读USART_DR,跟清除DILE空闲中断标志一样。所以,有了清除IDLE标志的代码后,不用单独去清除这些错误标志了。
NE、FE、ORE中断都依赖RXNE中断。 如上所示,参考手册说到NE、FE、ORE中断都依赖RXNE中断。就是说,想及时获取NE、FE、ORE中断就需要让RXNEIE置1,开启RXNE中断。但是,如果数据流量很大,开启RXNE中断会使每个字节到达时都进入中断,这可能导致中断频繁且影响系统性能。利用DMA接收数据,开启空闲中断来判断数据帧的结束。在空闲中断处理函数中,除了处理数据,还可以检查错误标志。不过,前提是必须有数据到达,才会触发空闲中断。所以,更好的方案是USART1全局中断与主轮询都要监控串口异常,记录异常并及时恢复。
1.2、代码实现(LL库)
/* LL库 */
uint16_t usart1Error = 0; // 记录串口发生的错误次数static __inline uint8_t USART1_Error_Handler(void) {// 错误处理:检查USART错误标志(ORE、NE、FE、PE)if (LL_USART_IsActiveFlag_ORE(USART1) ||LL_USART_IsActiveFlag_NE(USART1) ||LL_USART_IsActiveFlag_FE(USART1) ||LL_USART_IsActiveFlag_PE(USART1)){// 通过读SR和DR来清除错误标志volatile uint32_t tmp = USART1->SR;tmp = USART1->DR;(void)tmp;return 1;} else {return 0;}
}void USART1_IRQHandler(void)
{// 错误处理:检查USART错误标志(ORE、NE、FE、PE)if (USART1_Error_Handler()) {usart1Error++; // 记录事件} else if (LL_USART_IsActiveFlag_IDLE(USART1)) { // 空闲中断// 处理空闲中断} else { // 其他中断// 处理其他中断}
}
1.3、代码实现(寄存器方式)
uint16_t usart1Error = 0; // 记录串口发生的错误次数static __inline uint8_t USART1_Error_Handler(void) {// 寄存器方式:检查错误标志(PE、FE、NE、ORE分别位0~3)if (USART1->SR & ((1UL << 0) | (1UL << 1) | (1UL << 2) | (1UL << 3))) {// 清除错误标志volatile uint32_t tmp = USART1->SR;tmp = USART1->DR;(void)tmp;return 1;} else {return 0;}
}void USART1_IRQHandler(void)
{// 寄存器方式:检查错误标志(PE、FE、NE、ORE分别位0~3)if (USART1_Error_Handler()) {usart1Error++; // 记录事件} else if (USART1->SR & (1UL << 4)) { // 空闲中断// 处理空闲中断} else { // 其他中断// 处理其他中断}
}
为了更好地排查问题,可以在检测到错误时记录错误日志或计数,便于后续进行调试和系统优化。 全局变量usart1Error的目的就是统计错误的次数,在适当的实际将它上报给上位机。上位机有一个GUI程序,为了了解单片机系统的串口通讯情况,可以将usart1Error的计数显示在GUI上。当计数经常递增,表明串口有问题,需要去排查硬件或者线束问题等等。
根据实际需求,可以设计一些自动恢复机制。 当检测到错误时,除了清除错误标志、统计错误的次数。可以考虑重启串口、重新配置DMA通道或者进行数据重传,从而提高系统的鲁棒性。
1.4、串口重启的方法(重新初始化)
当错误次数累积太多时,可以考虑重启串口。进一步提升系统的鲁棒性。
1.4.1、代码实现(LL库)
/*** @brief 当检测到USART1异常时,重新初始化USART1及其相关DMA通道。* @note 此函数首先停止DMA1通道4(USART1_TX)和通道5(USART1_RX),确保DMA传输终止,* 然后禁用USART1及其DMA接收请求,并通过读SR和DR清除挂起的错误标志,* 经过短暂延时后调用MX_USART1_UART_Init()重新配置USART1、GPIO和DMA,* MX_USART1_UART_Init()内部已完成USART1中断的配置,无需额外使能。* @retval None*/
void USART1_Reinit(void)
{/* 禁用DMA,如果USART1启用了DMA的话 */LL_DMA_DisableChannel(DMA1, LL_DMA_CHANNEL_4); // 禁用DMA1通道4while(LL_DMA_IsEnabledChannel(DMA1, LL_DMA_CHANNEL_4)) { // 等待通道完全关闭(可以添加超时机制以避免死循环)// 空循环等待}LL_DMA_DisableChannel(DMA1, LL_DMA_CHANNEL_5); // 禁用DMA1通道5while(LL_DMA_IsEnabledChannel(DMA1, LL_DMA_CHANNEL_5)) { // 等待通道完全关闭(可以添加超时机制以避免死循环)// 空循环等待}/* USART1 */NVIC_DisableIRQ(USART1_IRQn); // 1. 禁用USART1全局中断,避免重启过程中产生新的中断干扰LL_USART_DisableDMAReq_RX(USART1); // 2. 禁用USART1的DMA接收请求(如果使用DMA接收)LL_USART_Disable(USART1); // 3. 禁用USART1// 4. 读SR和DR以清除挂起的错误标志(例如IDLE、ORE、NE、FE、PE)volatile uint32_t tmp = USART1->SR;tmp = USART1->DR;(void)tmp;for (volatile uint32_t i = 0; i < 1000; i++); // 5. 可选:短暂延时,确保USART完全关闭tx_dma_busy = 0; // 复位发送标志!!!!!!!!!!!!!!!!!!!MX_USART1_UART_Init(); // 重新初始化USART1DMA1_Channel5_Configure(); // 重新初始化DMA1通道5
}
1.4.2、代码实现(寄存器方式)
// 配置DMA1的通道4:普通模式,内存到外设(flash->USART1_TX),优先级高,存储器地址递增、数据大小都是8bit
__STATIC_INLINE void DMA1_Channel4_Configure(void) {// 开启时钟RCC->AHBENR |= (1UL << 0UL); // 开启DMA1时钟// 设置并开启全局中断NVIC_SetPriority(DMA1_Channel4_IRQn, NVIC_EncodePriority(NVIC_GetPriorityGrouping(),2, 0));NVIC_EnableIRQ(DMA1_Channel4_IRQn);// 数据传输方向DMA1_Channel4->CCR &= ~(1UL << 14UL); // 外设到存储器模式DMA1_Channel4->CCR |= (1UL << 4UL); // DIR位设置1(内存到外设)// 通道优先级DMA1_Channel4->CCR &= ~(3UL << 12UL); // 清除PL位DMA1_Channel4->CCR |= (2UL << 12UL); // PL位设置10,优先级高// 循环模式DMA1_Channel4->CCR &= ~(1UL << 5UL); // 清除CIRC位,关闭循环模式// 增量模式DMA1_Channel4->CCR &= ~(1UL << 6UL); // 外设存储地址不递增DMA1_Channel4->CCR |= (1UL << 7UL); // 存储器地址递增// 数据大小DMA1_Channel4->CCR &= ~(3UL << 8UL); // 外设数据宽度8位,清除PSIZE位,相当于00DMA1_Channel4->CCR &= ~(3UL << 10UL); // 存储器数据宽度8位,清除MSIZE位,相当于00// 中断DMA1_Channel4->CCR |= (1UL << 1UL); // 开启发送完成中断
}__STATIC_INLINE void USART1_Configure(void) {/* 1. 使能外设时钟 */// RCC->APB2ENR 寄存器控制 APB2 外设时钟RCC->APB2ENR |= (1UL << 14UL); // 使能 USART1 时钟 (位 14)RCC->APB2ENR |= (1UL << 2UL); // 使能 GPIOA 时钟 (位 2)/* 2. 配置 GPIO (PA9 - TX, PA10 - RX) */// GPIOA->CRH 寄存器控制 PA8-PA15 的模式和配置// PA9: 复用推挽输出 (模式: 10, CNF: 10)GPIOA->CRH &= ~(0xF << 4UL); // 清零 PA9 的配置位 (位 4-7)GPIOA->CRH |= (0xA << 4UL); // PA9: 10MHz 复用推挽输出 (MODE9 = 10, CNF9 = 10)// PA10: 浮空输入 (模式: 00, CNF: 01)GPIOA->CRH &= ~(0xF << 8UL); // 清零 PA10 的配置位 (位 8-11)GPIOA->CRH |= (0x4 << 8UL); // PA10: 输入模式,浮空输入 (MODE10 = 00, CNF10 = 01)// 开启USART1全局中断NVIC_SetPriority(USART1_IRQn, NVIC_EncodePriority(NVIC_GetPriorityGrouping(),0, 0)); // 优先级1(优先级越低相当于越优先)NVIC_EnableIRQ(USART1_IRQn);/* 3. 配置 USART1 参数 */// (1) 设置波特率 115200 (系统时钟 72MHz, 过采样 16)// 波特率计算: USART_BRR = fPCLK / (16 * BaudRate)// 72MHz / (16 * 115200) = 39.0625// 整数部分: 39 (0x27), 小数部分: 0.0625 * 16 = 1 (0x1)USART1->BRR = (39 << 4UL) | 1; // BRR = 0x271 (39.0625)// (2) 配置数据帧格式 (USART_CR1 和 USART_CR2)USART1->CR1 &= ~(1UL << 12UL); // M 位 = 0, 8 位数据USART1->CR2 &= ~(3UL << 12UL); // STOP 位 = 00, 1 个停止位USART1->CR1 &= ~(1UL << 10UL); // 没奇偶校验// (3) 配置传输方向 (收发双向)USART1->CR1 |= (1UL << 3UL); // TE 位 = 1, 使能发送USART1->CR1 |= (1UL << 2UL); // RE 位 = 1, 使能接收// (4) 禁用硬件流控 (USART_CR3)USART1->CR3 &= ~(3UL << 8UL); // CTSE 和 RTSE 位 = 0, 无流控// (5) 配置异步模式 (清除无关模式位)USART1->CR2 &= ~(1UL << 14UL); // LINEN 位 = 0, 禁用 LIN 模式USART1->CR2 &= ~(1UL << 11UL); // CLKEN 位 = 0, 禁用时钟输出USART1->CR3 &= ~(1UL << 5UL); // SCEN 位 = 0, 禁用智能卡模式USART1->CR3 &= ~(1UL << 1UL); // IREN 位 = 0, 禁用 IrDA 模式USART1->CR3 &= ~(1UL << 3UL); // HDSEL 位 = 0, 禁用半双工// (6) 中断USART1->CR1 |= (1UL << 4UL); // 使能USART1空闲中断 (IDLEIE, 位4)// (7) 关联DMA的接收请求USART1->CR3 |= (1UL << 6UL); // 使能USART1的DMA接收请求(DMAR,位6) // (7) 启用 USARTUSART1->CR1 |= (1UL << 13UL); // UE 位 = 1, 启用 USART
}// 配置USART1_RX的DMA1通道5
__STATIC_INLINE void DMA1_Channel5_Configure(void) {// 时钟RCC->AHBENR |= (1UL << 0UL); // 开启DMA1时钟// 开启全局中断NVIC_SetPriority(DMA1_Channel5_IRQn, NVIC_EncodePriority(NVIC_GetPriorityGrouping(),0, 0));NVIC_EnableIRQ(DMA1_Channel5_IRQn);/* 1. 禁用DMA通道5,等待其完全关闭 */DMA1_Channel5->CCR &= ~(1UL << 0); // 清除EN位while(DMA1_Channel5->CCR & 1UL); // 等待DMA通道5关闭/* 2. 配置外设地址和存储器地址 */DMA1_Channel5->CPAR = (uint32_t)&USART1->DR; // 外设地址为USART1数据寄存器DMA1_Channel5->CMAR = (uint32_t)rx_buffer; // 存储器地址为rx_bufferDMA1_Channel5->CNDTR = RX_BUFFER_SIZE; // 传输数据长度为缓冲区大小/* 3. 配置DMA1通道5 CCR寄存器- DIR = 0:外设→存储器- CIRC = 1:循环模式- PINC = 0:外设地址不自增- MINC = 1:存储器地址自增- PSIZE = 00:外设数据宽度8位- MSIZE = 00:存储器数据宽度8位- PL = 10:优先级设为高- MEM2MEM = 0:非存储器到存储器模式*/DMA1_Channel5->CCR = 0; // 清除之前的配置DMA1_Channel5->CCR |= (1UL << 5); // 使能循环模式 (CIRC,bit5)DMA1_Channel5->CCR |= (1UL << 7); // 使能存储器自增 (MINC,bit7)DMA1_Channel5->CCR |= (3UL << 12); // 设置优先级为非常高 (PL置为“11”,bit12-13)// 增加传输完成与传输过半中断DMA1_Channel5->CCR |= (1UL << 1); // 传输完成中断 (TCIE)DMA1_Channel5->CCR |= (1UL << 2); // 传输过半中断 (HTIE)/* 4. 使能DMA通道5 */DMA1_Channel5->CCR |= 1UL; // 置EN位启动通道
}/*** @brief 当检测到USART1异常时,重新初始化USART1及其相关DMA通道。* @note 此函数首先停止DMA1通道4(USART1_TX)和通道5(USART1_RX),确保DMA传输终止,* 然后禁用USART1及其DMA接收请求,并通过读SR和DR清除挂起的错误标志,* 经过短暂延时后调用MX_USART1_UART_Init()重新配置USART1、GPIO和DMA,* MX_USART1_UART_Init()内部已完成USART1中断的配置,无需额外使能。* @retval None*/
void USART1_Reinit(void)
{/* 禁用DMA,如果USART1启用了DMA的话 */DMA1_Channel4->CCR &= ~(1UL << 0); // 禁用DMA1通道4:清除CCR的EN位(位0)while (DMA1_Channel4->CCR & 1UL) { // 等待DMA1通道4完全关闭(建议增加超时处理)// 空循环等待}DMA1_Channel5->CCR &= ~(1UL << 0); // 禁用DMA1通道5:清除CCR的EN位(位0)while (DMA1_Channel5->CCR & 1UL) { // 等待DMA1通道5完全关闭(建议增加超时处理)// 空循环等待}NVIC_DisableIRQ(USART1_IRQn); // 禁用USART1全局中断,避免重启过程中产生新的中断干扰USART1->CR3 &= ~(1UL << 6); // 禁用USART1的DMA接收请求:清除CR3的DMAR位(位6)USART1->CR1 &= ~(1UL << 13); // 禁用USART1:清除CR1的UE位(位13)// 读SR和DR以清除挂起的错误标志(例如IDLE、ORE、NE、FE、PE)volatile uint32_t tmp = USART1->SR;tmp = USART1->DR;(void)tmp;for (volatile uint32_t i = 0; i < 1000; i++); // 可选:短暂延时,确保USART1完全关闭tx_dma_busy = 0; // 复位发送标志!!!!!/* 重新初始化USART1、DMA1 */USART1_Configure(); // USART1DMA1_Channel4_Configure(); // DMA1通道4DMA1_Channel5_Configure(); // DMA1通道5
}
1.4.3、测试
测试方法:当串口在频繁收发时,中途让串口重启,看看串口收发能不能恢复正常。
如上所示,一开始串口正在正常收发大量的数据。接着,设置全局变量rebootUsart1等于1后,在主循环里就会执行函数USART1_Reinit()
将串口与DMA重新初始化。可以看到,重新初始化后,串口能正常恢复收发!!
二、监控DMA异常
2.1、寄存器DMA_ISR
DMA每一个通道都只有一个错误标志TEIFx。 如上所示,例如,当TEIF5被置1时,表示DMA1通道5的传输发生了错误。
清除DMA通道的传输错误,使用寄存器DMA_IFCR。 如上所示,例如,当DMA1通道5发生传输错误之后,寄存器DMA_ISR的位19-TEIF5会被单片机系统置1。此时,通过软件将寄存器DMA_IFCR的位19-CTEIF置1,单片机系统就会清除错误。
2.2、代码实现(LL库)
/* LL库 */
uint16_t dma1Channel4Error = 0;
uint16_t dam1Channel5Error = 0;/* DMA1通道4,USART1发送 */
static __inline uint8_t DMA1_Channel4_Error_Handler(void) {// 检查通道4是否发生传输错误(TE)if (LL_DMA_IsActiveFlag_TE4(DMA1)) {// 清除传输错误标志LL_DMA_ClearFlag_TE4(DMA1);// 禁用DMA通道4,停止当前传输LL_DMA_DisableChannel(DMA1, LL_DMA_CHANNEL_4);// 清除USART1的DMA发送请求(DMAT位)LL_USART_DisableDMAReq_TX(USART1);// 清除发送标志!!tx_dma_busy = 0;return 1;} else {return 0;}
}void DMA1_Channel4_IRQHandler(void) {if (DMA1_Channel4_Error_Handler()) { // 传输错误中断dma1Channel4Error++; // 统计错误次数tx_dma_busy = 0; // 复位发送标志} else if () {// 传输完成中断处理}
}/* DMA1通道5,USART1接收 */
static __inline uint8_t DMA1_Channel5_Error_Hanlder(void) {// 检查通道5是否发生传输错误(TE)if (LL_DMA_IsActiveFlag_TE5(DMA1)) {// 清除传输错误标志LL_DMA_ClearFlag_TE5(DMA1);// 禁用DMA通道5,停止当前传输LL_DMA_DisableChannel(DMA1, LL_DMA_CHANNEL_5);// 重新设置传输长度,恢复到初始状态LL_DMA_SetDataLength(DMA1, LL_DMA_CHANNEL_5, RX_BUFFER_SIZE);// 重新使能DMA通道5,恢复接收LL_DMA_EnableChannel(DMA1, LL_DMA_CHANNEL_5);return 1;} else {return 0;}
}void DMA1_Channel5_IRQHandler(void) {if (DMA1_Channel5_Error_Hanlder()) {dam1Channel5Error++; // 统计错误次数} else if () {// 传输过半、传输完成中断处理}
}
2.3、代码实现(寄存器方式)
uint16_t dma1Channel4Error = 0;
uint16_t dam1Channel5Error = 0;/* DMA1通道4,USART1发送 */
static __inline uint8_t DMA1_Channel4_Error_Handler(void) {// 检查传输错误(TE)标志,假设TE对应位(1UL << 15)(请根据具体芯片参考手册确认)if (DMA1->ISR & (1UL << 15)) {// 清除TE错误标志DMA1->IFCR |= (1UL << 15);// 禁用DMA通道4DMA1_Channel4->CCR &= ~(1UL << 0);// 清除USART1中DMAT位USART1->CR3 &= ~(1UL << 7);// 清除发送标志!!tx_dma_busy = 0;return 1;} else {return 0;}
}void DMA1_Channel4_IRQHandler(void) {if (DMA1_Channel4_Error_Handler()) { // 传输错误中断dma1Channel4Error++; // 统计错误次数tx_dma_busy = 0; // 复位发送标志} else if () {// 传输完成中断处理}
}/* DMA1通道5,USART1接收 */
static __inline uint8_t DMA1_Channel5_Error_Hanlder(void) {// 检查传输错误(TE)标志,假设TE对应位(1UL << 19)(请确认具体位)if (DMA1->ISR & (1UL << 19)) {// 清除错误标志DMA1->IFCR |= (1UL << 19);// 禁用DMA通道5DMA1_Channel5->CCR &= ~(1UL << 0);// 重置传输计数DMA1_Channel5->CNDTR = RX_BUFFER_SIZE;// 重新使能DMA通道5DMA1_Channel5->CCR |= 1UL;return 1;} else {return 0;}
}void DMA1_Channel5_IRQHandler(void) {if (DMA1_Channel5_Error_Hanlder()) {dam1Channel5Error++; // 统计错误次数} else if () {// 传输过半、传输完成中断处理}
}
DMA传输错误,需立即清除,以免影响数据收发。 统计错误次数跟串口统计错误次数的目的是一样的。将信息传递给上位机的GUI程序,提供发生单片机系统问题的一些参考信息。
三、超时保护机制
超时保护机制,实际就是超时重启机制。 这里只简单聊一下实现思路,不提供代码,因为实现的方式太多了,需要根据项目的实际情况来修改。比如,某个项目的STM32控制板跟上位机有很频繁的串口通讯。正常情况下,每1S至少有10个消息包下发给STM32控制板。那么,此时可以在一个软件定时器(回调周期5S)里,监控消息包的数量(可以在中断回调里用一个变量来统计收到的串口消息包)。如果数量没有变化,可以认为有通讯超时异常。可以调用函数USART1_Reinit()
去重启USART、DMA等外设。如果超过20S都还没有收到任何消息,可以调用函数NVIC_SystemReset()
重启单片机系统等等。