本文最后更新于 2025-01-10T22:20:33+08:00
说明:
本文档由DuRuofu撰写,由DuRuofu负责解释及执行。
修订历史:
文档名称
版本
作者
时间
备注
STM32串口进阶
v1.0.0
DuRuofu
2024-02-12
已同步至博客
STM32串口进阶 一、前言 我们使用基本的串口接收中断配合回调函数,已经可以完成很多功能。详见STM32串口使用(HAL库) 一文。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 #include "uart_it_config.h" void UART_IT_Init (void ) { UART3_Init(); }void HAL_UART_RxCpltCallback (UART_HandleTypeDef *huart) { if (huart->Instance == USART1) { } else if (huart->Instance == USART2) { } else if (huart->Instance == USART3) { UART3_RxCpltCallback(huart); } }
我们使用uart_it_config
模块代理全局的串口接收数据非空中断HAL_UART_RxCpltCallback
,然后在内部实现各个串口的逻辑,以UART3
为例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 #include "uart_3.h" #define RXBUFFERSIZE_3 256 #define UART_HANDLE huart3 uint8_t RxBuffer_3[RXBUFFERSIZE_3]; uint8_t Uart_RxBuffer_3; uint8_t Uart_Rx_Cnt_3 = 0 ; void UART3_Init (void ) { __HAL_UART_ENABLE_IT(&UART_HANDLE, UART_IT_RXNE); HAL_UART_Receive_IT(&UART_HANDLE, &Uart_RxBuffer_3, 1 ); }void UART3_RxCpltCallback (UART_HandleTypeDef *huart) { UNUSED(huart); if (Uart_Rx_Cnt_3 >= 255 ) { Uart_Rx_Cnt_3 = 0 ; memset (RxBuffer_3, 0x00 , sizeof (RxBuffer_3)); HAL_UART_Transmit(&UART_HANDLE, (uint8_t *)"数据溢出" , 10 , 0xFFFF ); } else { RxBuffer_3[Uart_Rx_Cnt_3++] = Uart_RxBuffer_3; if ((RxBuffer_3[Uart_Rx_Cnt_3 - 1 ] == 0x0A ) && (RxBuffer_3[Uart_Rx_Cnt_3 - 2 ] == 0x0D )) { HAL_UART_Transmit(&UART_HANDLE, (uint8_t *)&RxBuffer_3, Uart_Rx_Cnt_3, 0xFFFF ); while (HAL_UART_GetState(&UART_HANDLE) == HAL_UART_STATE_BUSY_TX); Uart_Rx_Cnt_3 = 0 ; memset (RxBuffer_3, 0x00 , sizeof (RxBuffer_3)); } } HAL_UART_Receive_IT(&UART_HANDLE, (uint8_t *)&Uart_RxBuffer_3, 1 ); }int fputc (int ch, FILE *f) { HAL_UART_Transmit(&UART_HANDLE, (uint8_t *)&ch, 1 , 0xffff ); return ch; }
UART3_RxCpltCallback
函数通过判断接收计数器Uart_Rx_Cnt_3
的值是否达到上限255,来检测是否发生了溢出。如果没有溢出,将接收到的数据存储到接收缓冲区RxBuffer_3
中,并进行结束位的判断。这里只给出了单字符结束位的判断,即判断收到的字符是否是0x0D和0x0A(对应回车换行)。如果满足结束位的条件,则将接收到的信息通过串口发送出去,并等待发送结束。最后,重新使能串口接收中断,以便继续接收后续的数据。
这样就可以实现结尾为\r\n
,的不定长数据接收,但 并不是真正的接收不定长数据,只能算是”伪不定长”(但实际上也能满足业务需求)。
二、串口单字节连续接收数据+DMA 上面这样一个字节一个字节的处理,频繁中断会占用cpu时间,我们可以引入DMA代替cpu搬运数据。
为串口添加DMA通道:
为UART_TX,UART_RX分别创建DMA通道。
其余的变化不大,只需要再前言的代码基础上将HAL_UART_Receive_IT
改为HAL_UART_Receive_DMA
将HAL_UART_Transmit
改为HAL_UART_Transmit_DMA
,
串口DMA方式接收函数:HAL_UART_Receive_DMA
函数原型
HAL_StatusTypeDef HAL_UART_Receive_DMA(UART_Handle TypeDef *huart, uint 8_t *pData, uint 16_t Size)
功能描述
在DMA方式下接收一定数量的数据
入口参数1
huart:串口句柄的地址
入口参数
pData:待接收数据的首地址
入口参数3
Size:待接收数据的个数
返回值
HAL状态值:HAL_OK表示发送成功;HAL_ERROR表示参数错误;HAL_BUSY表示串口被占用;
注意事项
1. 该函数将启动DMA方式的串口数据接收2. 完成指定数量的数据接收后,可以触发DMA中断,在中断中将调用接收中断回调函数HAL_UART_ExCpltCallback进行后续处理3. 该函数由用户调用户调用
串口DMA方式发送函数:HAL_UART_Transmit_DMA
函数原型
HAL_StatusTypeDef HAL_UART_Transmit_DMA(UART_Handle TypeDef *huart, uint 8_t *pData, uint 16_t Size)
功能描述
在DMA方式下发送一定数量的数据
入口参数1
huart:串口句柄的地址
入口参数
pData:待发送数据的首地址
入口参数3
Size:待发送数据的个数
返回值
HAL状态值:HAL_OK表示发送成功;HAL_ERROR表示参数错误;HAL_BUSY表示串口被占用;
注意事项
1. 该函数将启动DMA方式的串口数据发送2. 完成指定数量的数据发送后,可以触发DMA中断,在中断中将调用发送中断回调函数HAL_UART_TxCpltCallback进行后续处理3. 该函数由用户调用户调用
这里有一点值得注意:
1 2 3 4 HAL_UART_Transmit_DMA(&UART_HANDLE, (uint8_t *)&RxBuffer_3, Uart_Rx_Cnt_3, 0xFFFF ); while (HAL_UART_GetState(&UART_HANDLE) == HAL_UART_STATE_BUSY_TX);
修改为DMA后,这里判断发送结束的函数将失去作用,因此要去掉这句。
还有就是STM32以DMA方式实现printf函数和普通中断方式不一样。 实现printf函数主要就是要改写fputc函数
1 int fputc (int ch, FILE *f)
但是fputc函数每次只能发送一个字节,如果我们把fputc函数直接改成:
1 2 3 4 5 int fputc (int ch, FILE *f) { HAL_UART_Transmit_DMA(&PrintUartHandle, (uint8_t *)&ch, 1 ); return (ch); }
那么至少存在两个问题: 1、DMA每次都发送一个字节,效率比较低。 2、频繁调用fputc,可能DMA上一次的数据还没有发送完,导致这次发送失败。 例如:
1 printf ("HelloWorld\r\n" );
printf先发送H,调用fputc函数,此时DMA开始工作。由之前的分析可知,对于9600bps来说,需要1ms才能把字符H发送完成。在这1ms之内elloWolrd\r\n都会调用fputc函数,但由于DMA还没有发送完成,会导致其他的字符发送失败。最终成功发出去的只有第一个字符H。
如果想用调用DMA,就要想其他的办法。
简单起见,我们自定义一个使用DMA的打印函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 #define UART_TX_BUF_SIZE 256 uint8_t UartTxBuf[UART_TX_BUF_SIZE];void Usart3DmaPrintf (const char *format,...) { uint16_t len; va_list args; va_start(args,format); len = vsnprintf((char *)UartTxBuf,sizeof (UartTxBuf)+1 ,(char *)format,args); va_end(args); HAL_UART_Transmit_DMA(&huart3, UartTxBuf, len); }
在函数开始时,使用 va_start 宏初始化了一个 va_list args 变量,以便访问可变数量的参数。
然后,使用 vsnprintf 函数将格式化后的字符串写入到 UartTxBuf 缓冲区中,并返回生成的字符串长度 len。vsnprintf 函数的第一个参数是目标缓冲区的地址,第二个参数是缓冲区大小,第三个参数是格式化字符串,最后一个参数是一个 va_list 变量,用于获取可变数量的参数列表。
在调用 vsnprintf 函数之后,使用 va_end 宏清理 va_list 变量,并结束对可变数量参数的访问。
最后,使用 HAL 库函数 HAL_UART_Transmit_DMA 发送数据。该函数的参数包括串口句柄 huart3、待发送数据的缓冲区 UartTxBuf,以及数据长度 len(注意:在代码中,len 实际上比 UartTxBuf 的大小小 1,因为 vsnprintf 已经将字符串末尾的 ‘\0’ 字符计算在内了)。
修改后的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 #include "uart_3.h" #define RXBUFFERSIZE_3 256 #define UART_HANDLE huart3 uint8_t RxBuffer_3[RXBUFFERSIZE_3];uint8_t Uart_RxBuffer_3; uint8_t Uart_Rx_Cnt_3 = 0 ; #define UART_TX_BUF_SIZE 256 uint8_t UartTxBuf[UART_TX_BUF_SIZE];void UART3_Init (void ) { __HAL_UART_ENABLE_IT(&UART_HANDLE, UART_IT_RXNE); HAL_UART_Receive_DMA(&UART_HANDLE, &Uart_RxBuffer_3, 1 ); }void UART3_RxCpltCallback (UART_HandleTypeDef *huart) { UNUSED(huart); if (Uart_Rx_Cnt_3 >= 255 ) { Uart_Rx_Cnt_3 = 0 ; memset (RxBuffer_3, 0x00 , sizeof (RxBuffer_3)); Usart3DmaPrintf("数据溢出" ); } else { RxBuffer_3[Uart_Rx_Cnt_3++] = Uart_RxBuffer_3; if ((RxBuffer_3[Uart_Rx_Cnt_3 - 1 ] == 0x0A ) && (RxBuffer_3[Uart_Rx_Cnt_3 - 2 ] == 0x0D )) { char str[256 ]; sprintf (str, "%s" , RxBuffer_3); Usart3DmaPrintf(str); Uart_Rx_Cnt_3 = 0 ; memset (RxBuffer_3, 0x00 , sizeof (RxBuffer_3)); } } HAL_UART_Receive_DMA(&UART_HANDLE, (uint8_t *)&Uart_RxBuffer_3, 1 ); }void Usart3DmaPrintf (const char *format,...) { uint16_t len; va_list args; va_start(args,format); len = vsnprintf((char *)UartTxBuf,sizeof (UartTxBuf),(char *)format,args); va_end(args); HAL_UART_Transmit_DMA(&huart3, UartTxBuf, len); }
值得注意的是,发送部分使用了自己封装的 Usart3DmaPrintf(str);
函数。
效果如下:
使用DMA进一步减轻了CPU的负担,但是这样依旧只能一个字节一个字节接收。不是真正的不定长数据接收。
三、串口空闲中断接收不定长数据 为了完成接收真正的不定长数据,我们可以使用串口空闲中断,在一帧数据接收完再进行处理,而不是上面这样一个字节一个字节的处理。
空闲中断是串口RX总线上在一个字节的时间内没有再接收到数据的时候发生的。
对于HAL库,使用空闲中断接收,只需要在上面的代码基础上将HAL_UART_Receive_DMA
改为HAL_UARTEx_ReceiveToIdle_DMA
当然,对于空闲中断也可以不使用DMA,这里我们直接使用DMA。
如何将串口接收寄存器非空回调更改为:HAL_UARTEx_RxEventCallback
串口空闲中断回调函数
对几个串口的空闲中断进行代理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 void HAL_UARTEx_RxEventCallback (UART_HandleTypeDef *huart, uint16_t Size) { if (huart->Instance == USART1) { } else if (huart->Instance == USART2) { } else if (huart->Instance == USART3) { UARTx3_RxEventCallback(huart,Size); } }
在串口模块自行实现逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 void UARTx3_RxEventCallback (UART_HandleTypeDef *huart, uint16_t Size) { UNUSED(huart); UNUSED(Size); HAL_UART_Transmit_DMA(&UART_HANDLE, RxBuffer_3,Size ); HAL_UARTEx_ReceiveToIdle_DMA(&UART_HANDLE, RxBuffer_3, RXBUFFERSIZE_3); }
这样就真正实现了任意长度数据接收,并且没有任何格式要求了。
值得注意的是,DMA的传输过半中断也会触发HAL_UARTEx_RxEventCallback回调函数,在目前的例子里是不需要的,反而会造成问题。所以我们要手动关闭DMA的传输过半中断。
在开启接收中断的同时关闭DMA的传输过半中断。
1 2 HAL_UARTEx_ReceiveToIdle_DMA(&UART_HANDLE, RxBuffer_3, RXBUFFERSIZE_3); __HAL_DMA_DISABLE_IT(&hdma_usart3_rx, DMA_IT_HT);
如果报错需要在模块前面添加:extern DMA_HandleTypeDef hdma_usart3_rx;
这样就完成了串口模块的进阶。
参考链接
https://blog.csdn.net/soledade/article/details/129030082
https://zhuanlan.zhihu.com/p/622278829
https://blog.csdn.net/youmeichifan/article/details/103133537
https://www.bilibili.com/video/BV1do4y1F7wt/?spm_id_from=333.1007.top_right_bar_window_history.content.click&vd_source=ef5a0ab0106372751602034cdd9ab98e