基于循环查询/中断/DMA的串口通信
引言:
串口协议和RS-232标准,以及RS232电平与TTL电平的区别;了解"USB/TTL转232"模块(以CH340芯片模块为例)的工作原理。 使用HAL库(或标准库)方式,设置USART1 波特率为115200,1位停止位,无校验位
以上的内容在上一篇文章中已经描述,在本文中就不过多描写。
本文主要以DMA实现串口通信为主,以循环查询和中断实现串口通信为辅
╭ ( `∀´ )╯ ╰ ( ’ ’ )╮
文章目录
一、DMA介绍
什么是DMA
官方一点的表达:DMA,全称为:Direct Memory Access,即直接存储器访问。直接存储器存取( DMA )用来提供在外设和存储器之间或者存储器和存储器之间的高速数据传输。无须 CPU 干预,数据可以通过 DMA 快速地移动,这就节省了 CPU 的资源来做其他操作。典型的例子就是移动一个外部内存的区块到芯片内部更快的内存区。像是这样的操作并没有让处理器工作拖延,反而可以被重新排程去处理其他的工作。DMA 传输对于高效能嵌入式系统算法和网络是很重要的。DMA 传输方式无需 CPU 直接控制传输,也没有中断处理方式那样保留现场和恢复现场的过程,通过硬件为 RAM 与 I/O 设备开辟一条直接传送数据的通路, 能使 CPU 的效率大为提高。
不太官方的理解:DMA只是个搬运工,帮助老板(CPU)搬运东西(数据),可以帮老板(CPU)把东西(数据)从家搬运到家门外(存储器→外设);可以帮老板(CPU)把到东西(数据)从家门外搬运到家里(外设→存储器);还可以帮老板(CPU)把东西(数据)从家里卧室1搬到卧室2(存储器→存储器)。
STM32上的DMA资源
STM32 最多有 2 个 DMA 控制器( DMA2 仅存在大容量产品中),12个独立的可配置的通道(请求), DMA1 有 7 个通道。DMA2 有 5 个通道。每个通道专门用来管理来自于一个或多个外设对存储器访问的请求。还有一个仲裁器来协调各个 DMA 请求的优先权。
- 小容量产品是指闪存存储器容量在16K至32K字节之间的微控制器。
- 中容量产品是指闪存存储器容量在64K至128K字节之间的微控制器。
- 大容量产品是指闪存存储器容量在256K至512K字节之间的微控制器。
- 互联型产品是指STM32F105xx和STM32F107xx微控制器。
DMA主要特征
- 每个通道都直接连接专用的硬件 DMA 请求,每个通道都同样支持软件触发。这些功能通过软件来配置。
- 在同一个 DMA 模块上,多个请求间的优先权可以通过软件编程设置(共有四级:很高、高、中等和低),优先权设置相等时由硬件决定(请求 0 优先于请求 1 ,依此类推,可以参考STM32数据手册)。
- 独立的源和目标数据区的传输宽度(字节、半字、全字),模拟打包和拆包的过程。源和目标地址必须按数据传输宽度对齐。
- 支持循环的缓冲器管理(会把原来的数据覆盖)。
- 每个通道都有 3 个事件标志(DMA 半传输, DMA 传输完成和 DMA 传输出错),这 3 个事件标志逻辑或成为一个单独的中断请求。
- 存储器和存储器间的传输(仅 DMA2 可以)。
- 外设和存储器、存储器和外设之间的传输。
- 闪存、SRAM 、外设的 SRAM 、APB1 、APB2 和 AHB 外设均可作为访问的源和目标。
- 可编程的数据传输数目:最大为65535(216-1)。
二、基本函数介绍
循环查询实现使用的函数
HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout)
功能:
HAL_UART_Transmit函数是一个用于通过UART发送数据的函数,它可以发送指定长度的数据到UART外设中。
在使用HAL库进行UART编程时,可以使用HAL_UART_Transmit函数来发送数据。该函数会将指定的数据缓冲区中的数据通过UART发送出去,并在数据发送完成后返回。
参数:
-
UART_HandleTypeDef *huart UATR的别名 如 : UART_HandleTypeDef huart1; 别名就是huart1
-
*pData 需要发送的数据
-
Size 发送的字节数
-
Timeout 最大发送时间,发送数据超过该时间退出发送
-
举例:
HAL_UART_Transmit(&huart1, (uint8_t *)ZZX, 3, 0xffff); //串口发送三个字节数据,最大传输时间0xffff
HAL_StatusTypeDef HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout)
功能:
HAL_UART_Receive函数是一个用于通过UART接收数据的函数,它可以接收指定长度的数据到指定的缓冲区中。
在使用HAL库进行UART编程时,可以使用HAL_UART_Receive函数来接收数据。该函数会从UART外设中接收数据,并将接收到的数据存储到指定的数据缓冲区中。
HAL_UART_Receive函数会使用轮询方式来接收数据,即在接收到指定长度的数据前会一直等待。
参数:
-
UART_HandleTypeDef *huart UATR的别名 如 : UART_HandleTypeDef huart1; 别名就是huart1
-
*pData 需要发送的数据
-
Size 发送的字节数
-
Timeout 最大发送时间,发送数据超过该时间退出发送
-
举例:
HAL_UART_Receive(&huart3, receiveBuffer, 5, 100); //串口接收五个字节数据,存储在receiveBuffer中,最大传输时间100
中断实现使用的函数
HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
功能:串口中断接收,以中断方式接收指定长度数据。
大致过程是,设置数据存放位置,接收数据长度,然后使能串口接收中断。接收到数据时,会触发串口中断。
再然后,串口中断函数处理,直到接收到指定长度数据,而后关闭中断,进入中断接收回调函数,不再触发接收中断。(只触发一次中断)
因此在中断函数的最后推荐再重新开启对应的中断。
参数:
-
UART_HandleTypeDef *huart UATR的别名 如 : UART_HandleTypeDef huart1; 别名就是huart1
-
*pData 接收到的数据存放地址
-
Size 接收的字节数
-
举例:
HAL_UART_Receive_IT(&huart1,(uint8_t *)&value,1); //中断接收一个字符,存储到value中
__weak void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
功能:HAL_UART_RxCpltCallback 是一个回调函数,用于在使用 HAL 库进行串口接收时处理接收完成事件。当使用 HAL_UART_Receive_IT 函数启动串口接收并且接收到指定数量的数据后,HAL 库会自动调用 HAL_UART_RxCpltCallback 函数。
参数:
- UART_HandleTypeDef *huart UATR的别名 如 : UART_HandleTypeDef huart1; 别名就是huart1
DMA函数
HAL_StatusTypeDef HAL_UARTEx_ReceiveToIdle_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
功能:
HAL_UARTEx_ReceiveToIdle_DMA函数是一个使用DMA(直接内存访问)接收数据的函数,用于通过UART接收数据并将数据存储到指定的缓冲区中,直到接收到空闲状态。
在使用HAL库进行UART编程时,通常使用HAL_UART_Receive_DMA函数来接收数据。但是,HAL_UART_Receive_DMA函数会一直接收数据直到指定的数据长度被接收完毕,或者发生错误。而HAL_UARTEx_ReceiveToIdle_DMA函数则会在接收到空闲状态时自动停止接收数据。
当UART接收到数据时,DMA控制器会自动将数据从UART的数据寄存器复制到指定的数据缓冲区中,直到接收到空闲状态。一旦接收到空闲状态,HAL_UARTEx_ReceiveToIdle_DMA函数会自动停止DMA传输,并触发一个DMA传输完成中断。
参数:
-
UART_HandleTypeDef *huart UATR的别名 如 : UART_HandleTypeDef huart1; 别名就是huart1
-
*pData 接收到的数据存放地址
-
Size 接收的字节数
-
举例:
HAL_UARTEx_ReceiveToIdle_DMA(&huart1, receiveBuffer, 5); //开启中断,接收数据存储到receiveBuffer中
HAL_StatusTypeDef HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size)
功能:函数会将指定的数据缓冲区中的数据通过UART发送出去,发送的长度由指定的数据长度参数确定。在函数执行期间,DMA控制器会自动将数据从内存复制到UART的数据寄存器中,并且在数据发送完成后会触发一个DMA传输完成中断。
参数:
-
UART_HandleTypeDef *huart UATR的别名 如 : UART_HandleTypeDef huart1; 别名就是huart1
-
*pData 接收到的数据存放地址
-
Size 接收的字节数
-
举例:
HAL_UART_Transmit_DMA(&huart1, sendBuffer, strlen(sendBuffer)); // 用于 DMA发送数据sendBuffer
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
功能:
HAL_UARTEx_RxEventCallback是一个回调函数,用于处理UART接收事件的回调函数。
在使用HAL库进行UART编程时,当接收到UART数据时,会触发一个接收事件。可以通过HAL_UARTEx_RxEventCallback函数来处理这个接收事件。当接收事件发生时,HAL库会自动调用该回调函数,并将相应的参数传递给该函数。
HAL_UARTEx_RxEventCallback函数可以根据接收事件的类型来执行不同的操作。例如,可以在该函数中读取接收到的数据,并进行相应的处理,比如解析数据、存储数据或者触发其他事件等。
使用HAL_UARTEx_RxEventCallback函数可以实现对UART接收事件的自定义处理,提高UART通信的灵活性和可扩展性。
参数:
- UART_HandleTypeDef *huart UATR的别名 如 : UART_HandleTypeDef huart1; 别名就是huart1
- Size 接收的字节数
三、工程建立
CubeMX配置
基本的配置在我之前的文章中有,这里我们直接跳到对应串口通信的配置
选择对应的芯片端口使用
USART3 的相关配置
-
模式配置
-
-
Asynchronous:全双工异步通信
Disable: 只通过RX和TX实现基本的串口通信,不需要使用Hardware Flow Control(硬件流控)
-
通信协议相关数据配置
Parameter Settings
**Baud Rate:**波特率。 通过波特率寄存器(USART_BRR)设置。
**Word Length:**字长。 数据位可选8位或9位 ,通过控制寄存器1(USART_CR1)中的M位设置。
**Parity:**奇偶校验选择。 校验位可选无校验(None)、偶校验(Even)、奇校验(Odd)。通过控制寄存器1(USART_CR1)中的PCE位和PS位设置。
**Stop Bits:**停止位。 停止位可选1位、2位。通过控制寄存器2(USART_CR2)中的STOP位设置。
**Data Direction:**数据方向。 可选收发(Receive and Transmit)、只接收(Receive Only)、只发送(Transmit Only)。通过控制寄存器1(USART_CR1)中的TE和RE位设置。
USART2 的相关配置
-
模式配置
-
-
Asynchronous:全双工异步通信
Disable: 只通过RX和TX实现基本的串口通信,不需要使用Hardware Flow Control(硬件流控)
-
通信协议相关数据配置
Parameter Settings
**Baud Rate:**波特率。 通过波特率寄存器(USART_BRR)设置。
**Word Length:**字长。 数据位可选8位或9位 ,通过控制寄存器1(USART_CR1)中的M位设置。
**Parity:**奇偶校验选择。 校验位可选无校验(None)、偶校验(Even)、奇校验(Odd)。通过控制寄存器1(USART_CR1)中的PCE位和PS位设置。
**Stop Bits:**停止位。 停止位可选1位、2位。通过控制寄存器2(USART_CR2)中的STOP位设置。
**Data Direction:**数据方向。 可选收发(Receive and Transmit)、只接收(Receive Only)、只发送(Transmit Only)。通过控制寄存器1(USART_CR1)中的TE和RE位设置。
-
开启对应的中断
-
USART1 的相关配置
-
模式配置
-
-
Asynchronous:全双工异步通信
Disable: 只通过RX和TX实现基本的串口通信,不需要使用Hardware Flow Control(硬件流控)
-
通信协议相关数据配置
Parameter Settings
**Baud Rate:**波特率。 通过波特率寄存器(USART_BRR)设置。
**Word Length:**字长。 数据位可选8位或9位 ,通过控制寄存器1(USART_CR1)中的M位设置。
**Parity:**奇偶校验选择。 校验位可选无校验(None)、偶校验(Even)、奇校验(Odd)。通过控制寄存器1(USART_CR1)中的PCE位和PS位设置。
**Stop Bits:**停止位。 停止位可选1位、2位。通过控制寄存器2(USART_CR2)中的STOP位设置。
**Data Direction:**数据方向。 可选收发(Receive and Transmit)、只接收(Receive Only)、只发送(Transmit Only)。通过控制寄存器1(USART_CR1)中的TE和RE位设置。
- DMA配置
- 开启对应中断
函数撰写
printf重定向
说到串口,肯定为了调试程序,使用HAL_UART_Transmit发送字符串很不方便,经常会用printf()函数输出一些调试信息;
如果使用到串口的时候,可以在 usart.c 里面进行串口重定向
/* USER CODE BEGIN 0 */
// printf重定向代码,修改其底层fputc
#if 1
#include <stdio.h>
/* 告知连接器不从C库链接使用半主机的函数 */
#pragma import(__use_no_semihosting)
/* 定义 _sys_exit() 以避免使用半主机模式 */
void _sys_exit(int x)
{
x = x;
}
/* 标准库需要的支持类型 */
struct __FILE
{
int handle;
};
FILE __stdout;
/* */
int fputc(int ch, FILE *stream)
{
/* 堵塞判断串口是否发送完成 */
/* 不同芯片的串口标志位不一定相同! */
while ((USART1->SR & 0X40) == 0)
;
/* 串口发送完成,将该字符发送 */
USART1->DR = (uint8_t)ch;
return ch;
}
#endif
/* USER CODE END 0 */
示例:
/* 串口重定向,一般用printf()比较多 */
printf("hello lu shi jun \r\n");
HAL_Delay(500);
循环查询串口通信函数
数据初始化
/* USER CODE BEGIN 0 */
uint8_t receiveBuffer[5] = "";
uint8_t *sendBuffer = "Hello Windows!!!\r\n";
uint8_t flag = 1;
/* USER CODE END 0 */
循环查询 main.c 函数
while (1)
{
if (flag) // 标志为1 开始发送 否则停止发送
{
// 发送数据
HAL_UART_Transmit(&huart1, sendBuffer, strlen(sendBuffer));
}
HAL_Delay(1000);
// 循环查询方式;
HAL_UART_Receive(&huart3, receiveBuffer, 5, 100); // 接收数据
// 判断是否符合条件
if (strcmp(receiveBuffer, "start") == 0)
{
flag = 1;
}
else if (strcmp(receiveBuffer, "stop1") == 0)
{
flag = 0;
}
}
中断方式串口通信函数
数据初始化
/* USER CODE BEGIN 0 */
uint8_t receiveBuffer[5] = "";
uint8_t *sendBuffer = "Hello Windows!!!\r\n";
uint8_t flag = 1;
/* USER CODE END 0 */
中断实现串口通信main.c函数
/* USER CODE BEGIN 2 */
// 用于中断实现 开启 (中断方式)
HAL_UART_Receive_IT(&huart2, receiveBuffer, 5);
/* USER CODE END 2 */
while (1)
{
if (flag) // 标志为1 开始发送 否则停止发送
{
// 发送数据
HAL_UART_Transmit(&huart1, sendBuffer, strlen(sendBuffer));
}
HAL_Delay(1000);
// 判断是否符合条件
if (strcmp(receiveBuffer, "start") == 0)
{
flag = 1;
}
else if (strcmp(receiveBuffer, "stop1") == 0)
{
flag = 0;
}
}
中断回调函数
// 中断方式
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart == &huart2)
{
HAL_UART_Transmit(&huart2, receiveBuffer, strlen(receiveBuffer), 100);
if (strcmp(receiveBuffer, "start") == 0)
{
flag = 1;
}
else if (strcmp(receiveBuffer, "stop1") == 0)
{
flag = 0;
}
// 再次开启
HAL_UART_Receive_IT(&huart2, receiveBuffer, 5);
}
}
DMA实现串口通信函数
数据初始化
/* USER CODE BEGIN 0 */
uint8_t receiveBuffer[5] = "";
uint8_t *sendBuffer = "Hello Windows!!!\r\n";
uint8_t flag = 1;
/* USER CODE END 0 */
DMA实现串口通信main.c函数
/* USER CODE BEGIN 2 */
// 用于DMA实现 开启 (DMA方式)
HAL_UARTEx_ReceiveToIdle_DMA(&huart1, receiveBuffer, 5);
/* USER CODE END 2 */
while (1)
{
if (flag) // 标志为1 开始发送 否则停止发送
{
// 发送数据
HAL_UART_Transmit_DMA(&huart1, sendBuffer, strlen(sendBuffer)); // 用于 DMA
}
HAL_Delay(1000);
// 判断是否符合条件
if (strcmp(receiveBuffer, "start") == 0)
{
flag = 1;
}
else if (strcmp(receiveBuffer, "stop1") == 0)
{
flag = 0;
}
}
中断回调函数
// DMA方式
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
if (huart == &huart1)
{
HAL_UART_Transmit_DMA(&huart1, receiveBuffer, strlen(receiveBuffer));
}
if (strcmp(receiveBuffer, "start") == 0)
flag = 1;
else if (strcmp(receiveBuffer, "stop!") == 0)
flag = 0;
HAL_UARTEx_ReceiveToIdle_DMA(&huart1, receiveBuffer, 5); // 再次中断
}
四、效果展示
第一个现象:
Video1:
循环实现
第二个现象:
Video2:
中断实现
第三个现象:
Video3:
DMA实现
KEIL 波形观察
计算波特率
五、总结
串口通信主要有三种模式:查询模式、中断模式、DMA模式。
- 查询模式:
- 主机CPU通过循环查询串口状态寄存器来检查是否有数据接收或发送完成。
- CPU需要不断轮询,占用较多CPU资源。通信效率较低。
- 中断模式:
- 串口有数据接收或发送完成时,会产生中断请求信号。
- CPU接收到中断后,会暂停当前任务,执行中断服务程序来处理串口通信。
- 相比查询模式,中断模式下CPU无需循环查询,节约CPU资源。通信效率较高。
- DMA模式:
- 串口接收或发送数据时,由DMA直接从内存缓冲区读取或写入数据,无需CPU参与。
- CPU仅负责配置DMA,然后继续执行其他任务。
- DMA模式下CPU几乎不参与数据传输,最大限度提高通信效率。
最后感谢大佬友情链接:
- https://blog.youkuaiyun.com/weixin_44524484/article/details/105671273