第三讲——USART+DMA通信(步骤详细)
文章目录
前言
接前文第二讲,尾言中提到仅仅使用串口通信,会频繁进入中断,对CPU资源消耗大,于是使用DMA对数据进行“搬运”,减少对CPU的占用。
本文通信数据流如下:
发送流程:手机APP——>蓝牙——>开发板(DMA搬运)——>内存1(串口搬运)——>上位机
接收流程:上位机——>开发板(DMA搬运)——>内存2(串口搬运)——>蓝牙——>手机APP
一、DMA简介
DMA是直接内存访问(Direct Memory Access)的缩写。它是计算机系统中的一种特性,允许某些硬件组件(如硬盘或网络卡)直接访问系统的内存,而无需涉及CPU。这可以通过减少CPU在数据传输方面的工作量来提高系统的整体性能和效率。DMA通常用于需要高速数据传输的设备,如显卡和存储设备。
1.DMA工作原理
- CPU发出DMA请求:当一个设备(如硬盘或网络卡)需要从内存中读取或写入数据时,它会发送一个DMA请求给DMA控制器。
- DMA控制器接收请求:DMA控制器是一个独立的硬件组件,负责管理DMA传输。它接收到设备的DMA请求后,会协调数据传输的过程。
- CPU授权DMA传输:CPU在接收到DMA请求后,会授予DMA控制器对系统内存的访问权限。这样,DMA控制器就可以直接访问内存,而无需CPU的干预。
- 数据传输:DMA控制器开始在设备和内存之间直接传输数据。这样可以大大减少CPU的负担,提高数据传输的效率和速度。
- 完成传输:一旦数据传输完成,DMA控制器会发送一个中断信号给CPU,通知传输已经完成。CPU可以继续处理其他任务。
可以认为成DMA是连个数据端的一条路线,不需要管理员干预可以自行双方数据传输。如果对USART通信不了解的,可以去看第二讲。
二、使用CubeMX建立工程
详细的工程创建去看第一讲,这里只说明DMA配置的关键步骤。
提前说明,这里只开启DMA接收通道,为了清除对比DMA传输和USART传输。
再第二讲工程基础上,进入UART5配置页面,如图添加DMA的UART5_RX通道
配置说明
mode
Normsl:普通模式,使能后,数据只传输一遍
Circula:循环模式,使能后,传输完成后从头又重新传输,一直循环下去。
这里使用Normsl模式就好,不需要循环
Increment Address
勾选后,每传输一个设定的数据大小,地址自动加一
这里的Peripheral配置成UART5的DR寄存器,所以地址要保持不变,而memory是创建的一个全局数组,用于储存传输的数据,地址要依递加,所以勾选memory。
Data Width
数据宽度就是数据流中一个数据的大小,有字节、半字、字三种,因为USART的DR传输一次数据的大小是字节,所以都选byte
这里不使用FIFO模式,FIFO是一种缓冲区,可以在数据传输过程中缓存一定数量的数据。使用的话可以提高数据的效率和可靠性,这里不用这么复杂,先不使用。
配置好UART5的DMA后,同样配置USART1,如下:
接着进入NVIC中断管理。保证系统默认中断优先级最高,如图配置中断,这里中断不多,优先级可以不用那么注意。
配置好后,就可以生成keil工程了
三、keil工程代码编写
1.usart.c
进入usart.c可以看到在串口初始化下有DMA的初始化:
以UART5为例
LL_DMA_SetChannelSelection(DMA1, LL_DMA_STREAM_0, LL_DMA_CHANNEL_4);
用来确定DMA控制器、数据流、通道,这里在使能外设的时候就确定好了,DMA1请求映射如下:
LL_DMA_SetDataTransferDirection(DMA1, LL_DMA_STREAM_0, LL_DMA_DIRECTION_PERIPH_TO_MEMORY);
配置改数据流的传输方向,可以看到是从外设传输到内存,这里的外设就是UART5_DR,也就是蓝牙发送的数据,内存就是开辟的一个数据,用于储存数据。
LL_DMA_SetStreamPriorityLevel(DMA1, LL_DMA_STREAM_0, LL_DMA_PRIORITY_HIGH);
配置该数据流的优先级,一个DMA控制器有7个数据流,之间的优先级有四个等级可以设置,这里DMA1只开启了一个数据流,那么优先级就无所谓了。
LL_DMA_SetMode(DMA1, LL_DMA_STREAM_0, LL_DMA_MODE_NORMAL);
配置模式为普通模式,也就是不循环
LL_DMA_SetPeriphIncMode(DMA1, LL_DMA_STREAM_0, LL_DMA_PERIPH_NOINCREMENT);
LL_DMA_SetMemoryIncMode(DMA1, LL_DMA_STREAM_0, LL_DMA_MEMORY_INCREMENT);
配置外设地址不自增,内存地址自增
LL_DMA_SetPeriphSize(DMA1, LL_DMA_STREAM_0, LL_DMA_PDATAALIGN_BYTE);
LL_DMA_SetMemorySize(DMA1, LL_DMA_STREAM_0, LL_DMA_MDATAALIGN_BYTE);
外设和内存传输的单个数据大小为字节,也就是8bit
LL_DMA_DisableFifoMode(DMA1, LL_DMA_STREAM_0);
禁止FIFO
2.dma.c
DMA配置如下:
/* USER CODE BEGIN 2 */
void uart5_DMA_init(void)
{
//DMA接收配置
LL_DMA_SetPeriphAddress(DMA1, LL_DMA_STREAM_0, (uint32_t)&(UART5->DR));//设置外设地址
LL_DMA_SetMemoryAddress(DMA1, LL_DMA_STREAM_0, (uint32_t)uart5RX_buf);//设置内存地址
LL_DMA_SetDataLength(DMA1, LL_DMA_STREAM_0, UART5_RX_BUF_SIZE);//设置接受的数据长度
//串口空闲中断指的是,数据传输完成后,串口监测到一段时间内没有数据进来,则触发产生的中断信号。
LL_USART_EnableIT_IDLE(UART5);//启用串口空闲中
LL_DMA_EnableIT_TC(DMA1,LL_DMA_STREAM_0);
LL_DMA_EnableIT_HT(DMA1,LL_DMA_STREAM_0);//启用半buf中断
LL_DMA_EnableIT_TE(DMA1,LL_DMA_STREAM_0);//启用传输错误中断。
//清除中断标志位
LL_USART_ClearFlag_IDLE(UART5);
LL_DMA_ClearFlag_HT0(DMA1);
LL_DMA_ClearFlag_TC0(DMA1);
LL_DMA_ClearFlag_TE0(DMA1);
LL_USART_EnableDMAReq_RX(UART5);//启用串口DMA接收模式
LL_DMA_EnableStream(DMA1, LL_DMA_STREAM_0);//使能DMA数据流
}
void uart1_DMA_init(void)
{
//DMA接收配置
LL_DMA_SetPeriphAddress(DMA2, LL_DMA_STREAM_2, (uint32_t)&(USART1->DR));//设置外设地址
LL_DMA_SetMemoryAddress(DMA2, LL_DMA_STREAM_2, (uint32_t)uart1RX_buf);//设置内存地址
LL_DMA_SetDataLength(DMA2, LL_DMA_STREAM_2, UART1_RX_BUF_SIZE);//设置接受的数据长度
//串口空闲中断指的是,数据传输完成后,串口监测到一段时间内没有数据进来,则触发产生的中断信号。
LL_USART_EnableIT_IDLE(USART1);//启用串口空闲中
LL_DMA_EnableIT_TC(DMA2,LL_DMA_STREAM_2);//开启满buf中断
LL_DMA_EnableIT_HT(DMA2,LL_DMA_STREAM_2);//启用半buf中断
LL_DMA_EnableIT_TE(DMA2,LL_DMA_STREAM_2);//启用传输错误中断。
//清除中断标志位
LL_USART_ClearFlag_IDLE(USART1);
LL_DMA_ClearFlag_HT0(DMA2);
LL_DMA_ClearFlag_TC0(DMA2);
LL_DMA_ClearFlag_TE0(DMA2);
LL_USART_EnableDMAReq_RX(USART1);//启用串口DMA接收模式
LL_DMA_EnableStream(DMA2, LL_DMA_STREAM_2);//使能DMA数据流
}
/* USER CODE END 2 */
uart5RX_buf与uart1RX_buf为自定义的全局数组,用来储存传输的数据(数据大小最好和传输的单个数据大小保持一致),定义如下:
/* USER CODE BEGIN 0 */
uint8_t uart5RX_buf[UART5_RX_BUF_SIZE];
uint8_t uart1RX_buf[UART1_RX_BUF_SIZE];
/* USER CODE END 0 */
UART5_RX_BUF_SIZE为自定义的全局变量,用来确定一次数据流的最大传输字节,定义在dma.h中,如下:
/* USER CODE BEGIN Includes */
#define UART5_RX_BUF_SIZE 16
#define UART1_RX_BUF_SIZE 16
extern uint8_t uart5RX_buf[UART5_RX_BUF_SIZE];
extern uint8_t uart1RX_buf[UART1_RX_BUF_SIZE];
/* USER CODE END Includes */
这里开启了四个中断,在数据传输中数据线上检测到一个数据大小传输时间内没有数据传输,则进入空闲中断,可以用来表示一次数据传输结束。
半buf中断,表示当一次传输的数据流达到UART1_RX_BUF_SIZE/2是便进入中断
半buf中断,表示当一次传输的数据流达到UART1_RX_BUF_SIZE便进入中断(超过UART1_RX_BUF_SIZE也进入中断,并且会丢失超出的数据,所以根据实际情况设定UART1_RX_BUF_SIZE的大小。这里为了方便展示,就设置成16)
虽然标志位默认是清除的,但习惯开启中断后清除一下标志位
最后使能串口接收,有数据进入DR寄存器会被接收回来;使能DMA数据流,有数据被接收会走DMA通道到内存。
3.stm32f4xx_it.c
先定义一下标志位,在中断服务函数只会使能标志位,没有其他的内容,具体的操作会放在main的while循环里
/* USER CODE BEGIN TD */
#include "printf.h"
unsigned char UART5_IDLE = 0,USART1_IDLE = 0;
unsigned char DMA1_TC0 = 0,DMA1_HT0 = 0,DMA1_TE0 = 0;
unsigned char DMA2_TC2 = 0,DMA2_HT2 = 0,DMA2_TE2 = 0;
/* USER CODE END TD */
中断服务函数如下
void DMA1_Stream0_IRQHandler(void)
{
/* USER CODE BEGIN DMA1_Stream0_IRQn 0 */
if(LL_DMA_IsActiveFlag_TC0(DMA1))
{
LL_DMA_ClearFlag_TC0(DMA1);
DMA1_TC0 = 1;
}
if(LL_DMA_IsActiveFlag_HT0(DMA1))
{
LL_DMA_ClearFlag_HT0(DMA1);
DMA1_HT0 = 1;
}
if(LL_DMA_IsActiveFlag_TE0(DMA1))
{
LL_DMA_ClearFlag_TE0(DMA1);
DMA1_TE0 = 1;
}
/* USER CODE END DMA1_Stream0_IRQn 0 */
/* USER CODE BEGIN DMA1_Stream0_IRQn 1 */
/* USER CODE END DMA1_Stream0_IRQn 1 */
}
void USART1_IRQHandler(void)
{
/* USER CODE BEGIN USART1_IRQn 0 */
if(LL_USART_IsActiveFlag_IDLE(USART1))
{
LL_USART_ClearFlag_IDLE(USART1);
USART1_IDLE = 1;
}
/* USER CODE END USART1_IRQn 0 */
/* USER CODE BEGIN USART1_IRQn 1 */
/* USER CODE END USART1_IRQn 1 */
}
void UART5_IRQHandler(void)
{
/* USER CODE BEGIN UART5_IRQn 0 */
if(LL_USART_IsActiveFlag_IDLE(UART5))
{
LL_USART_ClearFlag_IDLE(UART5);
UART5_IDLE = 1;
}
/* USER CODE END UART5_IRQn 0 */
/* USER CODE BEGIN UART5_IRQn 1 */
/* USER CODE END UART5_IRQn 1 */
}
void DMA2_Stream2_IRQHandler(void)
{
/* USER CODE BEGIN DMA2_Stream2_IRQn 0 */
if(LL_DMA_IsActiveFlag_TC2(DMA2))
{
LL_DMA_ClearFlag_TC2(DMA2);
DMA2_TC2 = 1;
}
if(LL_DMA_IsActiveFlag_HT2(DMA2))
{
LL_DMA_ClearFlag_HT2(DMA2);
DMA2_HT2 = 1;
}
if(LL_DMA_IsActiveFlag_TE2(DMA2))
{
LL_DMA_ClearFlag_TE2(DMA2);
DMA2_TE2 = 1;
}
/* USER CODE END DMA2_Stream2_IRQn 0 */
/* USER CODE BEGIN DMA2_Stream2_IRQn 1 */
/* USER CODE END DMA2_Stream2_IRQn 1 */
}
进入中断后,千万别忘了清除中断标志位。
4.main.c
引用头文件
/* USER CODE BEGIN Includes */
#include "delay.h"
#include "printf.h"
#include "stm32f4xx_it.h"
#include "dma.h"
/* USER CODE END Includes */
加入初始化
uart5_DMA_init();
uart1_DMA_init();
while循环代码
while (1)
{
if(UART5_IDLE)
{
UART5_IDLE = 0;
UART5_RX_LEN = UART5_RX_BUF_SIZE - LL_DMA_GetDataLength(DMA1,LL_DMA_STREAM_0);
LL_DMA_DisableStream(DMA1, LL_DMA_STREAM_0);
printf("蓝牙数据字节数:%d\r\n",UART5_RX_LEN);
for(uart_i = 0;uart_i<UART5_RX_LEN;uart_i++)
{
LL_USART_TransmitData8(USART1,uart5RX_buf[uart_i]);
delay_us(100);
}
LL_USART_TransmitData8(USART1,0X0A);
delay_us(100);
LL_DMA_EnableStream(DMA1, LL_DMA_STREAM_0);//使能DMA数据流
}
if(USART1_IDLE)
{
LL_DMA_DisableStream(DMA2, LL_DMA_STREAM_2);
USART1_IDLE = 0;
UART1_RX_LEN = UART1_RX_BUF_SIZE - LL_DMA_GetDataLength(DMA2,LL_DMA_STREAM_2);
LL_USART_TransmitData8(UART5,0x0A);
delay_us(100);
for(uart_i = 0;uart_i<UART1_RX_LEN;uart_i++)
{
LL_USART_TransmitData8(UART5,uart1RX_buf[uart_i]);
delay_us(100);
}
LL_USART_TransmitData8(USART1,0X0A);
delay_us(100);
LL_DMA_EnableStream(DMA2, LL_DMA_STREAM_2);//使能DMA数据流
}
if(DMA1_TC0)
{
DMA1_TC0 = 0;
if(UART5_RX_LEN==UART5_RX_BUF_SIZE)
{
printf("到最大传输数据字节,可能数据丢失\r\n");
}
}
if(DMA1_HT0)
{
DMA1_HT0 = 0;
printf("发送字节数达到一半\r\n");
}
if(DMA1_TE0)
{
DMA1_TE0 = 0;
printf("DMA1数据流0传输错误\r\n");
}
if(DMA2_TC2)
{
DMA2_TC2 = 0;
if(UART5_RX_LEN==UART5_RX_BUF_SIZE)
{
}
}
if(DMA2_HT2)
{
DMA2_HT2 = 0;
}
if(DMA2_TE2)
{
DMA2_TE2 = 0;
}
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
当触发空闲中断后表明已经有一组数据进来了,这时进入if(UART5_IDLE)。
首先禁止数据流LL_DMA_DisableStream(DMA1, LL_DMA_STREAM_0);
这一步是很必要的,静止数据流会把DMA_SxCR的EN位置0,而普通模式DMA 数据流 x 数据项数寄存器 (DMA_SxNDTR)只有通过将 EN 位置“1”来重新使能数据流才会自动以先前编程的值重载。所以先置0,才能置1。不然的话地址不会回到初始值,也是接着储存并递增(当然这种方式也有应用场景,不过这里不使用)。
通过UART5_RX_LEN = UART5_RX_BUF_SIZE - LL_DMA_GetDataLength(DMA1,LL_DMA_STREAM_0);
计算发送的字节数,LL_DMA_GetDataLength函数会返回剩余的空间大小,用总空间大小UART5_RX_BUF_SIZE 减去剩余就是使用的空间大小,也就是发送的字节数
for(uart_i = 0;uart_i<UART5_RX_LEN;uart_i++)
{
LL_USART_TransmitData8(USART1,uart5RX_buf[uart_i]);
delay_us(100);
}
将UART5接收的数据依次发送到UART1,从而上位机串口助手能够显示。这里的延时是很有必要的,我们的波特率为115200,也就是发送一个比特需要1/115200s,那么发送一个字节需要8/115200=69.4us。所以延时必须大于69.4us,考虑到实际botel会低于理论值,干脆取整延时100us。
最后发送一个换行,再重新使能数据流,那么一个发送流程就完成了。
再DMA中断中给出printf说明,这里无需特别处理,对于UART5没有printf说明是因为printf重定义到USART1上,如果需要通过LL_USART_TransmitData8发送一句话挺麻烦的,就算了。
这样整个工程就结束了
四、效果展示
可以看到,数据能完美接收,到达到一般数据量(8字节)触犯半buf中断,当达到最大数据量(16字节)触发全buf中断,当超过最大数据量,依旧触发全buf中断,并且超过的数据丢失
五、工程下载
链接:https://pan.baidu.com/s/1lmkLqtB9M59j-RwMNt4Wsg?pwd=1234
提取码:1234