更多交流欢迎关注作者抖音号:81849645041
目标
了解DMA 的工作原理,通过配置 STM32F407 芯片的DMA,实现串口+ DMA数据收发。
原理
基于USART的数据通讯中采用中断方式可以在接收到信息或需要发送数据时产生中断,在中断服务程序中完成数据的接收与发送,但是中断方式的CPU使用率要高。在简单的系统中,使用中断方式确实是一种好方法。但是在复杂的系统中,处理器需要处理串口通信,多个传感器数据的采集及处理,牵扯到多个中断的优先级分配问题。为了保证数据发送与接收的可靠性,需要把USART的中断优先级设计较高,但是系统可能还有其他的需要更高优先级的中断,必须保证其定时的准确,这样就有可能造成串行通讯的中断不能及时响应,从而造成数据丢失。 为了保证串行通讯的数据及时可靠的接收,同时兼顾其它任务不受影响,采用了基于DMA和中断方式相结合的USART串行通信方式。
DMA,全称:Direct Memory Access,即直接存储器访问。DMA传输将数据从一个地址空间复制到另一个地址空间。当CPU初始化这个动作,传输动作本身是由DMA控制器来实现和完成的。DMA传输方式无需CPU直接控制传输,也没有中断处理方式那样保留现场和恢复现场过程,通过硬件为RAM和IO设备开辟一条直接传输数据的通道,使得CPU的效率大大提高。DMA最主要的作用是为CPU减小负担。
STM32F4xx 系列的 DMA功能齐全,工作模式众多,适合不同编程环境要求。STM32F4xx 系列的 DMA 支持外设到存储器传输、存储器到外设传输和存储器到存储器传输三种传输模式。这里的外设一般指外设的数据寄存器,比如 ADC、SPI、I2C、DCMI等等外设的数据寄存器,存储器一般是指片内 SRAM、外部存储器、片内 Flash 等等。
外设到存储器传输就是把外设数据寄存器内容转移到指定的内存空间。比如进行 ADC采集时我们可以利用 DMA 传输把 AD 转换数据转移到我们定义的存储区中,这样对于多通道采集、采样频率高、连续输出数据的 AD采集是非常高效的处理方法。
存储区到外设传输就是把特定存储区内容转移至外设的数据寄存器中,这种多用于外设的发送通信。
存储器到存储器传输就是把一个指定的存储区内容拷贝到另一个存储区空间。功能类似于 C语言内存拷贝函数 memcpy,利用 DMA传输可以达到更高的传输效率,特别是DMA传输是不占用 CPU的,可以节省很多 CPU资源。
STM32F4xx 系列的 DMA可以实现外设寄存器与存储器之间或者存储器与存储器之间传输三种模式,这要得益于 DMA 控制器是采样 AHB主总线的,可以控制 AHB总线矩阵来启动 AHB事务DMA控制框图如下所示:
- 外设通道选择
STM32F4xx 系列资源丰富,具有两个 DMA 控制器,同时外设繁多,为实现正常传输,DMA需要通道选择控制。每个 DMA控制器具有 8个数据流,每个数据流对应 8个外设请求。在实现 DMA 传输之前,DMA控制器会通过 DMA数据流 x 配置寄存器 DMA_SxCR(x为 0~7,对应 8 个 DMA 数据流)的 CHSEL[2:0]位选择对应的通道作为该数据流的目标外设。
外设通道选择要解决的主要问题是决定哪一个外设作为该数据流的源地址或者目标地址。
DMA1请求映射如下表所示:
DMA2请求映射如下表所示:
- 仲裁器
一个 DMA控制器对应 8个数据流,数据流包含要传输数据的源地址、目标地址、数据等等信息。如果我们需要同时使用同一个 DMA 控制器(DMA1 或 DMA2)多个外设请求时,那必然需要同时使用多个数据流,那究竟哪一个数据流具有优先传输的权利呢?这就需要仲裁器来管理判断了。
仲裁器管理数据流方法分为两个阶段。第一阶段属于软件阶段,我们在配置数据流时可以通过寄存器设定它的优先级别,具体配置 DMA_SxCR寄存器 PL[1:0]位,可以设置为非常高、高、中和低四个级别。第二阶段属于硬件阶段,如果两个或以上数据流软件设置优先级一样,则他们优先级取决于数据流编号,编号越低越具有优先权,比如数据流 2 优先级高于数据流 3。
- :FIFO
每个数据流都独立拥有四级 32 位 FIFO(先进先出存储器缓冲区)。DMA传输具有 FIFO模式和直接模式。
直接模式在每个外设请求都立即启动对存储器传输。在直接模式下,如果 DMA配置为存储器和外设之间传输,那么 DMA会将一个数据存放在 FIFO 内,如果外设启动 DMA 传输请求就可以马上将数据传输过去。
FIFO模式下FIFO 用于在源数据传输到目标地址之前临时存放这些数据。可以通过 DMA 数据流x FIFO 控制寄存器 DMA_SxFCR 的 FTH[1:0]位来控制 FIFO 的阈值,分别为 1/4、1/2、3/4和满。如果数据存储量达到阈值级别时,FIFO 内容将传输到目标中。
FIFO 对于要求源地址和目标地址数据宽度不同时非常有用,比如源数据是源源不断的字节数据,而目标地址要求输出字宽度的数据,即在实现数据传输时同时把原来 4 个 8位字节的数据拼凑成一个 32位字数据。此时使用 FIFO 功能先把数据缓存起来,分别根据需要输出数据。
- :存储器端口、⑤:外设端口
DMA控制器实现双 AHB主接口,更好利用总线矩阵和并行传输。DMA控制器通过存储器端口和外设端口与存储器和外设进行数据传输,关系如下图。DMA控制器的功能是快速转移内存数据,需要一个连接至源数据地址的端口和一个连接至目标地址的端口。
DMA2(DMA 控制器 2)的存储器端口和外设端口都是连接到 AHB 总线矩阵,可以使用AHB 总线矩阵功能。DMA2 存储器和外设端口可以访问相关的内存地址,包括有内部Flash、内部 SRAM、AHB1 外设、AHB2外设、APB2 外设和外部存储器空间。
DMA1 的存储器端口相比 DMA2 的要减少 AHB2 外设的访问权,同时 DMA1外设端口是没有连接至总线矩阵的,只有连接到 APB1外设,所以 DMA1 不能实现存储器到存储器传输。
- 编程端口
AHB从器件编程端口是连接置AHB2外设的。AHB2外设在使用DMA传输时需要相关控制信号。
DMA寄存器包括:DMA低中断状态寄存器(DMA_LISR)、DMA高中断状态寄存器(DMA_HISR)、DMA低中断标志清除寄存器(DMA_LIFCR)、DMA高中断标志清除寄存器(DMA_HIFCR)、DMA流配置寄存器(DMA_SxCR)、DMA流数据个数寄存器(DMA_SxNDTR)、DMA流外设地址寄存器(DMA_SxPAR)、DMA流内存0地址寄存器(DMA_SxM0AR)、DMA流内存1地址寄存器(DMA_SxM1AR)、DMA流FIFO控制寄存器(DMA_SxM1AR)。
准备
MDK5 开发环境。
STM32F4xx HAL库。
STM32F407 开发板。
STM32F4xx 参考手册。
STM32F407 开发板电路原理图。
串口调试助手软件。
USB转TTL模块。
步骤
- 定义串口DMA发送和接收的结构体初始化变量。
DMA_HandleTypeDef UART1RxDMA_Handler; // 串口接收DMA句柄
DMA_HandleTypeDef UART1TxDMA_Handler; // 串口发送DMA句柄
- 定义UART_DMA_Init()函数,在函数中实现串口DMA的初始化。
第一步:使能DMA2时钟。
第二步:分别配置UART1RxDMA_Handler和UART1TxDMA_Handler结构体变量中的参数,然后将两个结构体变量传入到HAL_DMA_Init函数中,以便初始化串口DMA。
第三步:使能DMA2数据流7(串口DMA发送)中断。
// 串口DMA初始化
void UART_DMA_Init(void)
{
__HAL_RCC_DMA2_CLK_ENABLE(); // 使能DMA2时钟
// 接收DMA配置
UART1RxDMA_Handler.Instance = DMA2_Stream5; // 数据流选择
UART1RxDMA_Handler.Init.Channel = DMA_CHANNEL_4; // 通道选择
UART1RxDMA_Handler.Init.Direction = DMA_PERIPH_TO_MEMORY; // 外设到存储器
UART1RxDMA_Handler.Init.FIFOMode = DMA_FIFOMODE_DISABLE; // FIFO不使能
UART1RxDMA_Handler.Init.FIFOThreshold = DMA_FIFO_THRESHOLD_FULL; // FIFO阈值
UART1RxDMA_Handler.Init.MemBurst = DMA_MBURST_SINGLE; // 内存突发传输配置
UART1RxDMA_Handler.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE; // 存储器数据长度
UART1RxDMA_Handler.Init.MemInc = DMA_MINC_ENABLE; // 内存寄存器地址是否自增
UART1RxDMA_Handler.Init.Mode = DMA_CIRCULAR; // 循环模式
UART1RxDMA_Handler.Init.PeriphBurst = DMA_PBURST_SINGLE; // 外设突发传输配置
UART1RxDMA_Handler.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE; // 外设数据长度
UART1RxDMA_Handler.Init.PeriphInc = DMA_PINC_DISABLE; // 外设地址非自增
UART1RxDMA_Handler.Init.Priority = DMA_PRIORITY_MEDIUM; // 中等优先级
HAL_DMA_Init(&UART1RxDMA_Handler);
// 发送DMA配置
UART1TxDMA_Handler.Instance = DMA2_Stream7; // 数据流选择
UART1TxDMA_Handler.Init.Channel = DMA_CHANNEL_4; // 通道选择
UART1TxDMA_Handler.Init.Direction = DMA_MEMORY_TO_PERIPH; // 外设到存储器
UART1TxDMA_Handler.Init.FIFOMode = DMA_FIFOMODE_DISABLE; // FIFO不使能
UART1TxDMA_Handler.Init.FIFOThreshold = DMA_FIFO_THRESHOLD_FULL; // FIFO阈值
UART1TxDMA_Handler.Init.MemBurst = DMA_MBURST_SINGLE; // 内存突发传输配置
UART1TxDMA_Handler.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE; // 存储器数据长度
UART1TxDMA_Handler.Init.MemInc = DMA_MINC_ENABLE; // 内存寄存器地址是否自增
UART1TxDMA_Handler.Init.Mode = DMA_NORMAL; // 正常模式
UART1TxDMA_Handler.Init.PeriphBurst = DMA_PBURST_SINGLE; // 外设突发传输配置
UART1TxDMA_Handler.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE; // 外设数据长度
UART1TxDMA_Handler.Init.PeriphInc = DMA_PINC_DISABLE; // 外设地址非自增
UART1TxDMA_Handler.Init.Priority = DMA_PRIORITY_MEDIUM; // 中等优先级
HAL_DMA_Init(&UART1TxDMA_Handler);
HAL_NVIC_SetPriority(DMA2_Stream7_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(DMA2_Stream7_IRQn);
}
- 定义DMA2数据流7(串口DMA发送)中断服务函数,在函数中调用HAL_DMA_IRQHandler处理DMA中断请求。
// 缓冲区 -> Tx 中断服务函数
void DMA2_Stream7_IRQHandler(void)
{
HAL_DMA_IRQHandler(&UART1TxDMA_Handler);
}
- 定义串口DMA接收数据函数。在函数中调用HAL_UART_Receive_DMA实现使用DMA从串口接收数据。
// 串口DMA接收数据
void UART_DMA_Receive(uint8_t *pData, uint16_t Size)
{
HAL_UART_Receive_DMA(&UART1_Handler, pData, Size);
}
- 定义串口DMA发送数据函数。在函数中首先调用HAL_DMA_Start_IT开启串口DMA发送完成中断,并设置发送数据缓冲区地址、外设寄存器地址以及发送数据个数,然后调用SET_BIT配置串口DMA发送标志位(DMAT)。
extern uint8_t Rx_buffer[RXBUFFERSIZE]; // 发送数据缓冲区
// 串口DMA发送数据
void UART_DMA_Transmit(UART_HandleTypeDef *huart)
{
HAL_DMA_Start_IT(UART1_Handler.hdmatx, (uint32_t)Rx_buffer, (uint32_t)&UART1_Handler.Instance->DR, RXBUFFERSIZE);
SET_BIT(UART1_Handler.Instance->CR3, USART_CR3_DMAT);
}
- 修改串口初始化函数,将串口DMA发送和接收初始化句柄传入串口初始化结构体变量中(串口初始化代码请参考源码)。
// 初始化串口函数
void UART_Init(void)
{
UART1_Handler.Instance = USART1; // 串口1
UART1_Handler.Init.BaudRate = 115200; // 波特率
UART1_Handler.Init.HwFlowCtl = UART_HWCONTROL_NONE; // 无硬件流控
UART1_Handler.Init.Mode = UART_MODE_TX_RX; // 收发模式
UART1_Handler.Init.Parity = UART_PARITY_NONE; // 无奇偶校验
UART1_Handler.Init.StopBits = UART_STOPBITS_1; // 一个停止位
UART1_Handler.Init.WordLength = UART_WORDLENGTH_8B; // 字长为8位格式
UART1_Handler.hdmarx = &UART1RxDMA_Handler; // 传入接收DMA句柄
UART1_Handler.hdmatx = &UART1TxDMA_Handler; // 传入发送DMA句柄
HAL_UART_Init(&UART1_Handler); // 初始化串口
}
- 在main.c文件中调用函数实现串口DMA数据收发。
第一步:声明串口初始化句柄方便下面函数调用,并定义接收数据缓冲区。
第二步:初始化系统时钟、DMA及串口。
第三步:在循环中调用UART_DMA_Transmit和HAL_Delay函数,实现每隔一秒发送一组收到的数据。
extern UART_HandleTypeDef UART1_Handler; // UART 句柄
uint8_t Rx_buffer[RXBUFFERSIZE]; // 串口接收数据缓冲区
int main(void)
{
CLOCLK_Init(); // 配置系统时钟为168M
UART_DMA_Init(); // 串口DMA初始化
UART_Init(); // 串口初始化
UART_DMA_Receive(Rx_buffer, RXBUFFERSIZE); // 使用DMA接收数据
while(1)
{
HAL_Delay(1000); // 延时1s
UART_DMA_Transmit(&UART1_Handler); // 发送数据
}
}
现象
版权声明:本文为CSDN博主「Mr.奚」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/xhj1021/article/details/123027937
暂无评论