####DMA(Direct Memory Access)的理解:
1.直接内存访问,(内存是主存,即ram)而非处理器读取外设数据,再赋值给内存中某个地址。
2.控制器是一种外设,可以将其视为一种能够通过一组专用总线将内部和外部存储器与每个具有DMA能力的外设连接起来的控制器。它之所以属于外设,是因为它是在处理器的编程控制下来 执行传输的。
3.DMA控制器将包括一条地址总线、一条数据总线和控制寄存器。高效率的DMA控制器将具有访问其所需要的任意资源的能力(即可以访问所有地址,运用中大多分配一个足够大的数组给它用),而无须处理器本身的介入,它必须能产生中断(收到数据报告给处理器)。最后,它必须能在控制器内部计算出地址(进行移位操作)。
4.最大的优点:不用cpu花销时间在发送,接收数据上,只需要中断一来 ,处理使用并再此接收即可。
这里举个例子:
用stm32串口发送接收数据,发送与接收的频率为60HZ,发送与接收的数据帧都是10字节的(stm32与视觉板通信差不多就这样),波特率115200,无校验位,一个停止位。
这样:
发送一个字节时间=9/115200s
发送接收一次时间=9/20/115200s
那么一秒钟花费在串口上的时间=9/20/60/115200s=93ms
(注意,发送与接收都是在中断中且一帧数据发送是连续的,即使有系统也不可省去这个时间)
那么:
大概十分之一的时间都花在了串口接收上,当然我们也许还有更多的传感器,更多的控制与计算要做,这十分之一是惊人的。而使用DMA后可能不到1ms,飞的感觉。
最近也发现一个有趣的事:大疆的遥控器的DR16接收机与处理器通信也是串口加DMA。
4.更多的底层原理不管,把DMA配置好,用好最重要,DMA核心思想还是把复杂的事交给CPU,简单的事加模块。
5.最后一句:用DMA一般数据量都较大,如果是关键数据,最好有校验。
###DMA最重要的两张图(图片摘自摘自):
这里也可以见得许多外设都支持DMA,好奇TIM4_CH1这种DMA有什么用。
###DMA串口使用流程:
其中串口初始化与普通串口无异,后面的配置需要修改的值在代码中会提到。
###DMA串口2初始化代码及说明:
这个代码你发送不超过30个字节的数据给串口二,串口二也会返回相同数据。
mian.h 主要是u8等的typedef代码,因为代码摘自一个工程,也就不提供了,自己随便写写就好。代码有错误的也欢迎指正。
usart2.h:
#ifndef USART2_H
#define USART2_H
#include "main.h" //mian.h 主要是u8等的typedef
#include "sys.h"
#include "stm32f4xx.h"
extern uint8_t SendBuffer2[40]; //DMA发送缓冲区,就是DMA发送时用的内存
extern uint8_t ReceiveBuffer2[30]; //DMA接收缓冲区,同理同上
extern void Init_Usart2(u32 Bound); //串口初始化
//DMA发送初始化
extern void Init_Dma_Tx_Config2(USART_TypeDef *Usartx,u32 Par, DMA_Stream_TypeDef *DMA_Streamx_Tx, u32 DMA_Chx,u32 Tx_Buf, u16 Tx_Size, u32 DMA_Streamx_Irqn);
//DMA接收初始化
extern void Init_Dma_Rx_Config2(USART_TypeDef *Usartx, u32 Par, DMA_Stream_TypeDef *DMA_Streamx_Rx, u32 DMA_Chx,u32 Rx_Buf, u16 Rx_Size);
//DMA发送数据函数
extern void Dma_Send_Enable2(DMA_Stream_TypeDef *DMA_Streamx_tx,u16 ndtr);
#endif
usart2.c:
/**
****************************(C) COPYRIGHT 2020 CCZU****************************
* @file usart2.c/h
* @brief 串口2,调试串口DMA初始化,DMA发送,DMA接收
*
* @note
* @history
* Version Date Author Modification
* V1.0.0 2020/1/11 BIZHI finished
*
@verbatim
==============================================================================
==============================================================================
@endverbatim
****************************(C) COPYRIGHT 2020 CCZU****************************
*/
//#include "include.h"
#include "usart2.h"
u16 rx_len2 = 0; //DMA接收数据的长度
uint8_t SendBuffer2[30]={0}; //DMA发送缓冲区
uint8_t ReceiveBuffer2[30]={0}; //DMA接收缓冲区
u16 BufferLen2 = 30; //DMA缓冲区的长度
u8 rx_flag2 = 0; //数据帧接收标志
/**************************************
@功能 串口的初始化配置
@调用关系
@输入参数 u32 Bound,eg:115200
@返回值 无
@说明 PA2 => USART2_TX,PA3 => USART2_RX
***************************************/
void Init_Usart2(u32 Bound)
{
NVIC_InitTypeDef NVIC_InitStructure; //定义中断结构体
GPIO_InitTypeDef GPIO_InitStructure; //定义IO初始化结构体
USART_InitTypeDef USART_InitStructure; //定义串口结构体
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA,ENABLE); //使能GPIOG时钟,配置这几个函数时注意前后时钟的一致性
RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2,ENABLE);//使能USART6时钟
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA1,ENABLE); //DMA2时钟使能
//GPIO引脚复用映射
GPIO_PinAFConfig(GPIOA,GPIO_PinSource2,GPIO_AF_USART2); //引脚复用映射 PA2 => USART2_TX
GPIO_PinAFConfig(GPIOA,GPIO_PinSource3,GPIO_AF_USART2); //引脚复用映射 PA3 => USART2_RX
//GPIO结构体设置
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2 | GPIO_Pin_3; //管脚指定 PA9 PA14
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF; //复用功能
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; //推挽复用输出
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; //上拉
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //速度50MHz
GPIO_Init(GPIOA, &GPIO_InitStructure); //初始化
//Usart的 NVIC 配置
USART_InitStructure.USART_BaudRate = Bound; //波特率设置,一般设置为9600
USART_InitStructure.USART_WordLength = USART_WordLength_8b; //字长为8位数据格式
USART_InitStructure.USART_StopBits = USART_StopBits_1; //一个停止位
USART_InitStructure.USART_Parity = USART_Parity_No; //无奇偶校验位
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //无硬件数据流控制
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; //收发模式
USART_Init(USART2,&USART_InitStructure); //初始化串口
//使能串口
USART_Cmd(USART2, ENABLE);
//开启串口空闲中断
USART_ITConfig(USART2, USART_IT_IDLE, ENABLE);
NVIC_InitStructure.NVIC_IRQChannel = USART2_IRQn; //串口中断通道
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=7; //抢占优先级3
NVIC_InitStructure.NVIC_IRQChannelSubPriority =0; //子优先级3
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道使能
NVIC_Init(&NVIC_InitStructure); //根据指定的参数初始化NVIC寄存器
}
/**************************************
@功能 DMA的发送初始化配置
@调用关系 无
@输入参数
@Usartx 指定的串口,如 USART1
@Par DMA外设地址,如 (u32)&USART1->DR
@DMA_Streamx_tx 代表DMA1的发送数据流,DMA1_Stream0~DMA1_Stream7,参见手册
@DMA_Chx 代表DMA的通道选择,DMA_Channel_0~DMA_Channel_7
@Tx_Buf 代表DMA的发送缓冲区
@Tx_Size 代表发送数据个数
@DMA_Streamx_Irqn DMA数据流的中断,如 DMA2_Stream7_IRQn
@返回值 无
@说明 无
***************************************/
void Init_Dma_Tx_Config2(USART_TypeDef *Usartx,u32 Par, DMA_Stream_TypeDef *DMA_Streamx_Tx,
u32 DMA_Chx,u32 Tx_Buf, u16 Tx_Size, u32 DMA_Streamx_Irqn)
{
NVIC_InitTypeDef NVIC_InitStructure; //定义中断结构体
DMA_InitTypeDef DMA_InitStructure; //定义DMA结构体
DMA_DeInit(DMA_Streamx_Tx); //将DMA的发送数据流设置为缺省值
while (DMA_GetCmdStatus(DMA_Streamx_Tx) != DISABLE) ; //等待DMA的发送数据流可配置
//DMA的结构体设置
DMA_InitStructure.DMA_Channel = DMA_Chx; //通道选择
DMA_InitStructure.DMA_PeripheralBaseAddr = Par; //DMA外设地址
DMA_InitStructure.DMA_Memory0BaseAddr = Tx_Buf; //DMA 存储器0地址
DMA_InitStructure.DMA_DIR = DMA_DIR_MemoryToPeripheral; //存储器到外设模式
DMA_InitStructure.DMA_BufferSize = Tx_Size; //数据传输量
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //外设非增量模式
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //存储器增量模式
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; //外设数据长度:8位
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; //存储器数据长度:8位
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; // 使用普通模式
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium; //中等优先级
DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable; //初始化DMA_FIFOMode
DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_Full; //FIFO 阈值选择。
DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single; //存储器突发单次传输
DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single; //外设突发单次传输
DMA_Init(DMA_Streamx_Tx, &DMA_InitStructure); //初始化DMA Stream
//DMA 中断设置
NVIC_InitStructure.NVIC_IRQChannel = DMA_Streamx_Irqn; //DMA发送数据流中断通道
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 13; //抢占优先级0
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; //子优先级0
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道使能
NVIC_Init(&NVIC_InitStructure); //根据指定的参数初始化NVIC寄存器
USART_DMACmd(Usartx,USART_DMAReq_Tx,ENABLE); //使能串口1的DMA发送
DMA_Cmd(DMA_Streamx_Tx, DISABLE); //关闭DMA传输
DMA_ITConfig(DMA_Streamx_Tx,DMA_IT_TC,ENABLE); //使能DMA的发送数据流中断
}
/**************************************
@功能 DMA的接收初始化配置
@调用关系 无
@输入参数
@Usartx 指定的串口,如 USART1
@Par DMA外设地址,如 (u32)&USART1->DR
@DMA_Streamx_rx 代表DMA1的接收数据流,DMA1_Stream0~DMA1_Stream7,参见手册
@DMA_Chx 代表DMA的通道选择,DMA_Channel_0~DMA_Channel_7
@Rx_Buf 代表DMA的接收缓冲区
@Rx_Size 代表接收数据个数
@返回值 无
@说明 无
***************************************/
void Init_Dma_Rx_Config2(USART_TypeDef *Usartx, u32 Par, DMA_Stream_TypeDef *DMA_Streamx_Rx,
u32 DMA_Chx,u32 Rx_Buf, u16 Rx_Size)
{
DMA_InitTypeDef DMA_InitStructure; //定义DMA结构体
DMA_DeInit(DMA_Streamx_Rx); //将DMA的接收数据流设置为缺省值
while (DMA_GetCmdStatus(DMA_Streamx_Rx) != DISABLE); //等待DMA的接收数据流可配置
//DMA的结构体设置
DMA_InitStructure.DMA_Channel = DMA_Chx; //通道选择
DMA_InitStructure.DMA_PeripheralBaseAddr = Par; //DMA外设地址
DMA_InitStructure.DMA_Memory0BaseAddr = Rx_Buf; //DMA 存储器0地址
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralToMemory ; //外设到存储器模式
DMA_InitStructure.DMA_BufferSize = Rx_Size; //数据传输量
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //外设非增量模式
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //存储器增量模式
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; //外设数据长度:8位
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; //存储器数据长度:8位
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; // 使用普通模式
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium; //中等优先级
DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Enable; //初始化DMA_FIFOMode
DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_Full; //FIFO 阈值选择。
DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single; //存储器突发单次传输
DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single; //外设突发单次传输
DMA_Init(DMA_Streamx_Rx, &DMA_InitStructure); //初始化DMA Stream
USART_DMACmd(Usartx,USART_DMAReq_Rx,ENABLE); //使能串口的DMA接收
DMA_Cmd(DMA_Streamx_Rx, ENABLE); //开启DMA传输
}
/**************************************
@功能 DMA的发送使能
@调用关系 无
@输入参数
@DMA_Streamx_tx 代表DMA的发送数据流,DMA1_Stream0~DMA1_Stream7 / DMA2_Stream0~DMA2_Stream7
@ndtr 代表DMA的发送数据个数
@返回值 无
@说明 无
***************************************/
void Dma_Send_Enable2(DMA_Stream_TypeDef *DMA_Streamx_tx,u16 ndtr)
{
DMA_Cmd(DMA_Streamx_tx, DISABLE); //关闭DMA传输
while (DMA_GetCmdStatus(DMA_Streamx_tx) != DISABLE); //确保DMA可以被设置
DMA_SetCurrDataCounter(DMA_Streamx_tx,ndtr); //数据传输量
DMA_Cmd(DMA_Streamx_tx, ENABLE); //开启DMA传输
}
/**************************************
@功能 DMA的发送完成中断函数
@调用关系 无
@输入参数 无
@返回值 无
@说明 无
***************************************/
void DMA1_Stream6_IRQHandler(void)
{
if(DMA_GetFlagStatus(DMA1_Stream6,DMA_FLAG_TCIF6)!=RESET) //等待DMA1_Steam6传输完成 注意修改前后的‘6’,下同
{
DMA_ClearFlag(DMA1_Stream6,DMA_FLAG_TCIF6); //清除DMA1_Steam6传输完成标志
DMA_Cmd(DMA1_Stream6,DISABLE); //关闭DMA
DMA_ClearITPendingBit(DMA1_Stream6, DMA_IT_TCIF6); //清除中断屏蔽位
}
}
/**************************************
@功能 串口2的空闲中断函数
@调用关系 无
@输入参数 无
@返回值 无
@说明 无
***************************************/
void USART2_IRQHandler(void)
{
u16 i;
if(USART_GetITStatus(USART2,USART_IT_IDLE) != RESET) //如果接到一帧数据
{
DMA_Cmd(DMA1_Stream5, DISABLE); //关闭DMA,防止处理其间有数据
//先读SR,再读DR,是为了清除IDLE中断
i = USART2->SR; //读SR寄存器
i = USART2->DR; //读DR寄存器
rx_len2 = 6+BufferLen2 - DMA_GetCurrDataCounter(DMA1_Stream5); //获得这一帧数据的长度
if(rx_len2 !=0&&rx_len2<30) //这一帧数据长度不为0
rx_flag2 = 1;
if(rx_flag2)
{
for(i=0;i<rx_len2;i++)
{
SendBuffer2[i]=ReceiveBuffer2[i];
}
Dma_Send_Enable2(DMA1_Stream6,rx_len2);//发送回去
rx_flag2 = 0;
}
for(i=0;i<rx_len2;i++) //将接收缓冲区清零
{
ReceiveBuffer2[i]=0;
}
//清除DMA2_Steam5传输完成标志
DMA_ClearFlag(DMA1_Stream5,DMA_FLAG_TCIF5 | DMA_FLAG_FEIF5 | DMA_FLAG_DMEIF5 | DMA_FLAG_TEIF5 | DMA_FLAG_HTIF5);
//注意修改前后的’5‘
DMA_SetCurrDataCounter(DMA1_Stream5, BufferLen2); //设置DMA接收单元的长度
DMA_Cmd(DMA1_Stream5, ENABLE); //打开DMA
}
}
最后:初始化写在main的while(1)前就可以实验了,我有空再把代码从工程中脱离出来,发个工程文件出来。(也许会忘记)
Init_Usart6(115200);
Init_Dma_Tx_Config(USART6, (u32)(&USART6->DR), DMA2_Stream7, DMA_Channel_5, (u32)SendBuffer, BufferLen, DMA2_Stream7_IRQn);
//DMA 接收数据流初始化
Init_Dma_Rx_Config(USART6, (u32)(&USART6->DR), DMA2_Stream1, DMA_Channel_5, (u32)ReceiveBuffer, BufferLen);
###最后的最后
DMA的printf是有意思的,这里只粘出代码:
使用printfu2(char *fmt,…)要#include "stdarg.h"与#include “stdio.h”,前者允许函数传递未定义数量个参数用的即…,后者用于支持函数vsprintf等。谢谢评论提醒。
/**************************************
@功能 调试用printfu2
@调用关系
@输入参数 eg:("aaa:%d",2)
@返回值 无
@说明 单次发送不超过30个字节
***************************************/
void printfu2(char *fmt,...)
{
va_list arg;
va_start(arg,fmt);//将...中的输入与fmt初始化到arg中
vsprintf((char*)SendBuffer2,fmt,arg);//将输出存到字符缓冲中
va_end(arg);
Dma_Send_Enable2(DMA1_Stream6,strlen((const char*)SendBuffer2));
}