一、什么是DMA?
DMA,全称为: Direct Memory Access,即直接存储器访问。 DMA 传输方式无需 CPU 直接
控制传输,也没有中断处理方式那样保留现场和恢复现场的过程,通过硬件为 RAM 与 I/O 设备
开辟一条直接传送数据的通路, 能使 CPU 的效率大为提高。
STM32F4 最多有 2 个 DMA 控制器(DMA1 和 DMA2), 共 16 个数据流(每个控制器 8个), 每一个 DMA 控制器都用于管理一个或多个外设的存储器访问请求。每个数据流总共可以有多达 8个通道(或称请求)。每个数据流通道都有一个仲裁器,用于处理 DMA 请求间的优先级。
即:DMA是一个协助CPU搬运数据的助手,可以大大减小CPU在搬运数据工作的工作量
DMA 控制器执行直接存储器传输:因为采用 AHB 主总线,它可以控制 AHB 总线矩阵来
启动 AHB 事务。它可以执行下列事务:
1, 外设到存储器的传输
3, 存储器到外设的传输
3, 存储器到存储器的传输
这里特别注意一下,存储器到存储器需要外设接口可以访问存储器,而仅 DMA2 的外设接口可以访问存储器,所以仅 DMA2 控制器支持存储器到存储器的传输, DMA1 不支持。
二、DMA如何使用?
1)使能 DMA2 时钟,并等待数据流可配置。
DMA 的时钟使能是通过 AHB1ENR 寄存器来控制的,这里我们要先使能时钟,才可以配置 DMA
相关寄存器。 HAL 库方法为:
__HAL_RCC_DMA2_CLK_ENABLE();//DMA2 时钟使能
__HAL_RCC_DMA1_CLK_ENABLE();//DMA1 时钟使能
2) 初始化 DMA2 数据流7 ,包括配置通道,外设地址,存储器地址,传输数据量等。
DMA 的某个数据流各种配置参数初始化是通过 HAL_DMA_Init 函数实现的,该函数声明为:
HAL_StatusTypeDef HAL_DMA_Init(DMA_HandleTypeDef *hdma);
该函数只有一个 DMA_HandleTypeDef 结构体指针类型入口参数,结构体定义为:
typedef struct __DMA_HandleTypeDef
{
DMA_Stream_TypeDef *Instance; /*!< Register base address */
DMA_InitTypeDef Init; /*!< DMA communication parameters */
HAL_LockTypeDef Lock; /*!< DMA locking object */
__IO HAL_DMA_StateTypeDef State; /*!< DMA transfer state */
void *Parent; /*!< Parent object state */
void (* XferCpltCallback)( struct __DMA_HandleTypeDef * hdma); /*!< DMA transfer complete callback */
void (* XferHalfCpltCallback)( struct __DMA_HandleTypeDef * hdma); /*!< DMA Half transfer complete callback */
void (* XferM1CpltCallback)( struct __DMA_HandleTypeDef * hdma); /*!< DMA transfer complete Memory1 callback */
void (* XferM1HalfCpltCallback)( struct __DMA_HandleTypeDef * hdma); /*!< DMA transfer Half complete Memory1 callback */
void (* XferErrorCallback)( struct __DMA_HandleTypeDef * hdma); /*!< DMA transfer error callback */
void (* XferAbortCallback)( struct __DMA_HandleTypeDef * hdma); /*!< DMA transfer Abort callback */
__IO uint32_t ErrorCode; /*!< DMA Error code */
uint32_t StreamBaseAddress; /*!< DMA Stream Base Address */
uint32_t StreamIndex; /*!< DMA Stream Index */
}DMA_HandleTypeDef;
成员变量 Instance 是用来设置寄存器基地址,例如要设置为 DMA2 的数据流 7,那么取值
为 DMA2_Stream7。
成员变量 Parent 是 HAL 库处理中间变量,用来指向 DMA 通道外设句柄。
成员变量 XferCpltCallback(传输完成回调函数) , XferHalfCpltCallback(半传输完成
回调函数) , XferM1CpltCallback(Memory1 传输完成回调函数)和 XferErrorCallback(传输
错误回调函数)是四个函数指针,用来指向回调函数入口地址。
成员变量 StreamBaseAddress 和 StreamIndex 是数据流基地址和索引号,这个是 HAL 库处
理的时候会自动计算,用户无需设置。
其他成员变量 HAL 库处理过程状态标识变量,这里就不做过多讲解。接下来我们着重看看
成员变量 Init,它是 DMA_InitTypeDef 结构体类型,该结构体定义为:
typedef struct
{
uint32_t Channel; //通道,例如: DMA_CHANNEL_4
uint32_t Direction;//传输方向,例如存储器到外设 DMA_MEMORY_TO_PERIPH
uint32_t PeriphInc;//外设(非)增量模式,非增量模式 DMA_PINC_DISABLE
uint32_t MemInc;//存储器(非)增量模式,增量模式 DMA_MINC_ENABLE
uint32_t PeriphDataAlignment; //外设数据大小: 8/16/32 位。
uint32_t MemDataAlignment; //存储器数据大小: 8/16/32 位。
uint32_t Mode;//模式:外设流控模式/循环模式/普通模式
uint32_t Priority; //DMA 优先级:低/中/高/非常高
uint32_t FIFOMode;//FIFO 模式开启或者禁止
uint32_t FIFOThreshold; //FIFO 阈值选择:
uint32_t MemBurst; //存储器突发模式:单次/4 个节拍/8 个节拍/16 个节拍
uint32_t PeriphBurst; //外设突发模式:单次/4 个节拍/8 个节拍/16 个节拍
}DMA_InitTypeDef;
该结构体成员变量非常多,但是每个成员变量配置的基本都是 DMA_SxCR 寄存器和DMA_SxFCR 寄存器的相应位。我们把结构体各个成员变量的含义都通过注释的方式列出来了。
例如本实验我们要用到 DMA2_Stream7 的 DMA_CHANNEL_4,把内存中数组的值发送到串口外设发送寄存器 DR,所以方向为存储器到外设 DMA_MEMORY_TO_PERIPH,一个一个字节发送,需要数字索引自动增加,所以是存储器增量模式 DMA_MINC_ENABLE,存储器和外设的字宽都是字节 8 位。
具体配置如下:
DMA_HandleTypeDef UART1TxDMA_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.PeriphInc=DMA_PINC_DISABLE; //外设非增量模式
UART1TxDMA_Handler.Init.MemInc=DMA_MINC_ENABLE; //存储器增量模式
UART1TxDMA_Handler.Init.PeriphDataAlignment=DMA_PDATAALIGN_BYTE;//外设: 8 位
UART1TxDMA_Handler.Init.MemDataAlignment=DMA_MDATAALIGN_BYTE; //存储器: 8 位
UART1TxDMA_Handler.Init.Mode=DMA_NORMAL; //普通模式
UART1TxDMA_Handler.Init.Priority=DMA_PRIORITY_MEDIUM; //中等优先级
UART1TxDMA_Handler.Init.FIFOMode=DMA_FIFOMODE_DISABLE;
UART1TxDMA_Handler.Init.FIFOThreshold=DMA_FIFO_THRESHOLD_FULL;
UART1TxDMA_Handler.Init.MemBurst=DMA_MBURST_SINGLE; //存储器突发单次传输
UART1TxDMA_Handler.Init.PeriphBurst=DMA_PBURST_SINGLE; //外设突发单次传输
这里注意, HAL 库为了处理各类外设的 DMA 请求,在调用相关函数之前,需要调用
一个宏定义标识符,来连接 DMA 和外设句柄。例如要使用串口 DMA 发送,所以方式为:
__HAL_LINKDMA(&UART1_Handler,hdmatx,UART1TxDMA_Handler);
UART1_Handler 是串口初始化句柄,我们在 usart.c 中定义过了(具体见上次串口通信文章)。UART1TxDMA_Handler是 DMA 初始化句柄。 hdmatx 是外设句柄结构体的成员变量, 在这里实际就是 UART1_Handler 的成员变量。 在 HAL 库中,任何一个可以使用 DMA 的外设,它的初始化结构体句柄都会有一个DMA_HandleTypeDef 指针类型的成员变量,是 HAL 库用来做相关指向的。 Hdmatx 就是DMA_HandleTypeDef 结构体指针类型。
这句话的含义就是把UART1_Handler 句柄的成员变 量 hdmatx和DMA句柄
UART1TxDMA_Handler 连接起来,是纯软件处理,没有任何硬件操作。
3)使能串口 1 的 DMA 发送
串口 1 的 DMA 发送实际是串口控制寄存器 CR3 的位 7 来控制的,在 HAL 库中,多次操作该
寄存器来使能串口 DMA 发送,但是它并没有提供一个独立的使能函数,所以这里我们可以通过
直接操作寄存器方式来实现:
USART1->CR3 |= USART_CR3_DMAT;//使能串口 1 的 DMA 发送
HAL 库还提供了对串口的 DMA 发送的停止,暂停,继续等操作函数:
HAL_StatusTypeDef HAL_UART_DMAStop(UART_HandleTypeDef *huart); //停止
HAL_StatusTypeDef HAL_UART_DMAPause(UART_HandleTypeDef *huart); //暂停
HAL_StatusTypeDef HAL_UART_DMAResume(UART_HandleTypeDef *huart);//恢复
4)使能 DMA2 数据流 7,启动传输
使能 DMA 数据流的函数为:
HAL_StatusTypeDef HAL_DMA_Start(DMA_HandleTypeDef *hdma, uint32_t SrcAddress,uint32_t DstAddress, uint32_t DataLength);
这个函数比较好理解,第一个参数是 DMA 句柄,第二个是传输源地址,第三个是传输目标
地址,第四个是传输的数据长度。
通过以上 4 步设置,我们就可以启动一次 USART1 的 DMA 传输了。
5)查询 DMA 传输状态
在 DMA 传输过程中,我们要查询 DMA 传输通道的状态,使用的方法是:
__HAL_DMA_GET_FLAG(&UART1TxDMA_Handler,DMA_FLAG_TCIF3_7);
获取当前传输剩余数据量:
__HAL_DMA_GET_COUNTER(&UART1TxDMA_Handler);
同样,我们也可以设置对应的 DMA 数据流传输的数据量大小,函数为
__HAL_DMA_SET_COUNTER(&UART1TxDMA_Handler,1000);
6) DMA 中断使用方法
DMA 中断对于每个流都有一个中断服务函数,比如 DMA2_Stream7 的中断服务函数为
DMA2_Stream7_IRQHandler。同样, HAL 库也提供了一个通用的 DMA 中断处理函数
HAL_DMA_IRQHandler,在该函数内部,会对 DMA 传输状态进行分析,然后调用相应的中断
处理回调函数:
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart);//发送完成回调函数
void HAL_UART_TxHalfCpltCallback(UART_HandleTypeDef *huart);/发送一半回调函数
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart);//接收完成回调函数
void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart);//接收一半回调函数
void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart);//传输出错回调函数
对于串口 DMA 开启,使能数据流,启动传输,这些步骤,如果使用了中断,可以直接调
用 HAL 库函数 HAL_USART_Transmit_DMA,该函数声明如下:
HAL_StatusTypeDef HAL_USART_Transmit_DMA(USART_HandleTypeDef *husart,uint8_t *pTxData, uint16_t Size);
三、代码验证
需要的硬件资源(集成在开发板上):
1) 指示灯 DS0
2) KEY0 按键
3) 串口
4) TFTLCD 模块
5) DMA
我们将利用外部按键 KEY0 来控制 DMA 的传送,每按一次 KEY0, DMA 就传送一次数据到
USART1,然后在 TFTLCD 模块上显示进度等信息。 DS0 用来做为程序运行的指示灯。
需要注意 P6 口的 RXD 和 TXD 是否和 PA9 和 PA10 连接上,如果没有,请先连接。
代码:
dma.h
DMA_HandleTypeDef UART1TxDMA_Handler; //DMA句柄
//DMAx的各通道配置
//这里的传输形式是固定的,这点要根据不同的情况来修改
//从存储器->外设模式/8位数据宽度/存储器增量模式
//DMA_Streamx:DMA数据流,DMA1_Stream0~7/DMA2_Stream0~7
//chx:DMA通道选择,@ref DMA_channel DMA_CHANNEL_0~DMA_CHANNEL_7
void MYDMA_Config(DMA_Stream_TypeDef *DMA_Streamx,u32 chx)
{
if((u32)DMA_Streamx>(u32)DMA2)//得到当前stream是属于DMA2还是DMA1
{
__HAL_RCC_DMA2_CLK_ENABLE();//DMA2时钟使能
}else
{
__HAL_RCC_DMA1_CLK_ENABLE();//DMA1时钟使能
}
__HAL_LINKDMA(&UART1_Handler,hdmatx,UART1TxDMA_Handler); //将DMA与USART1联系起来(发送DMA)
//Tx DMA配置
UART1TxDMA_Handler.Instance=DMA_Streamx; //数据流选择
UART1TxDMA_Handler.Init.Channel=chx; //通道选择
UART1TxDMA_Handler.Init.Direction=DMA_MEMORY_TO_PERIPH; //存储器到外设
UART1TxDMA_Handler.Init.PeriphInc=DMA_PINC_DISABLE; //外设非增量模式
UART1TxDMA_Handler.Init.MemInc=DMA_MINC_ENABLE; //存储器增量模式
UART1TxDMA_Handler.Init.PeriphDataAlignment=DMA_PDATAALIGN_BYTE; //外设数据长度:8位
UART1TxDMA_Handler.Init.MemDataAlignment=DMA_MDATAALIGN_BYTE; //存储器数据长度:8位
UART1TxDMA_Handler.Init.Mode=DMA_NORMAL; //外设普通模式
UART1TxDMA_Handler.Init.Priority=DMA_PRIORITY_MEDIUM; //中等优先级
UART1TxDMA_Handler.Init.FIFOMode=DMA_FIFOMODE_DISABLE;
UART1TxDMA_Handler.Init.FIFOThreshold=DMA_FIFO_THRESHOLD_FULL;
UART1TxDMA_Handler.Init.MemBurst=DMA_MBURST_SINGLE; //存储器突发单次传输
UART1TxDMA_Handler.Init.PeriphBurst=DMA_PBURST_SINGLE; //外设突发单次传输
HAL_DMA_DeInit(&UART1TxDMA_Handler);
HAL_DMA_Init(&UART1TxDMA_Handler);
}
//开启一次DMA传输
//huart:串口句柄
//pData:传输的数据指针
//Size:传输的数据量
void MYDMA_USART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
{
HAL_DMA_Start(huart->hdmatx, (u32)pData, (uint32_t)&huart->Instance->DR, Size);//开启DMA传输
huart->Instance->CR3 |= USART_CR3_DMAT;//使能串口DMA发送
}
该部分代码仅仅 2 个函数, MYDMA_Config 函数,基本上就是按照我们上面介绍的步骤来初
始化 DMA 的, 该函数是一个通用的 DMA 配置函数, DMA1/DMA2 的所有通道,都可以利用该函数配置,不过有些固定参数可能要适当修改(比如位宽,传输方向等)。 该函数在外部只能修改DMA 及数据流编号、 通道号、 外设地址、 存储器地址(SxM0AR)传输数据量等几个参数, 更多的其他设置只能在该函数内部修改。
dma.h
extern DMA_HandleTypeDef UART1TxDMA_Handler; //DMA句柄
void MYDMA_Config(DMA_Stream_TypeDef *DMA_Streamx,u32 chx);
void MYDMA_USART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
main.h
#define SEND_BUF_SIZE 8200 //发送数据长度,最好等于sizeof(TEXT_TO_SEND)+2的整数倍.
u8 SendBuff[SEND_BUF_SIZE]; //发送数据缓冲区
const u8 TEXT_TO_SEND[]={"ALIENTEK Explorer STM32F4 DMA 串口实验"};
int main(void)
{
u16 i;
u8 t=0;
u8 j,mask=0;
float pro=0;
HAL_Init(); //初始化HAL库
Stm32_Clock_Init(336,8,2,7); //设置时钟,168Mhz
delay_init(168); //初始化延时函数
uart_init(115200); //初始化USART
usmart_dev.init(84); //初始化USMART
LED_Init(); //初始化LED
KEY_Init(); //初始化KEY
LCD_Init(); //初始化LCD
MYDMA_Config(DMA2_Stream7,DMA_CHANNEL_4);//初始化DMA
POINT_COLOR=RED;
LCD_ShowString(30,50,200,16,16,"Explorer STM32F4");
LCD_ShowString(30,70,200,16,16,"DMA TEST");
LCD_ShowString(30,90,200,16,16,"ATOM@ALIENTEK");
LCD_ShowString(30,110,200,16,16,"2017/4/13");
LCD_ShowString(30,130,200,16,16,"KEY0:Start");
POINT_COLOR=BLUE;//设置字体为蓝色
//显示提示信息
j=sizeof(TEXT_TO_SEND);
for(i=0;i<SEND_BUF_SIZE;i++)//填充ASCII字符集数据
{
if(t>=j)//加入换行符
{
if(mask)
{
SendBuff[i]=0x0a;
t=0;
}else
{
SendBuff[i]=0x0d;
mask++;
}
}else//复制TEXT_TO_SEND语句
{
mask=0;
SendBuff[i]=TEXT_TO_SEND[t];
t++;
}
}
POINT_COLOR=BLUE;//设置字体为蓝色
i=0;
while(1)
{
t=KEY_Scan(0);
if(t==KEY0_PRES) //KEY0按下
{
printf("\r\nDMA DATA:\r\n");
LCD_ShowString(30,150,200,16,16,"Start Transimit....");
LCD_ShowString(30,170,200,16,16," %") ; //显示百分号
HAL_UART_Transmit_DMA(&UART1_Handler,SendBuff,SEND_BUF_SIZE);//启动传输
//使能串口1的DMA发送 //等待DMA传输完成,此时我们来做另外一些事,点灯
//实际应用中,传输数据期间,可以执行另外的任务
while(1)
{
if(__HAL_DMA_GET_FLAG(&UART1TxDMA_Handler,DMA_FLAG_TCIF3_7))//等待DMA2_Steam7传输完成
{
__HAL_DMA_CLEAR_FLAG(&UART1TxDMA_Handler,DMA_FLAG_TCIF3_7);//清除DMA2_Steam7传输完成标志
HAL_UART_DMAStop(&UART1_Handler); //传输完成以后关闭串口DMA
break;
}
pro=__HAL_DMA_GET_COUNTER(&UART1TxDMA_Handler);//得到当前还剩余多少个数据
pro=1-pro/SEND_BUF_SIZE; //得到百分比
pro*=100; //扩大100倍
LCD_ShowNum(30,170,pro,3,16);
}
LCD_ShowNum(30,170,100,3,16);//显示100%
LCD_ShowString(30,150,200,16,16,"Transimit Finished!");//提示传送完成
}
i++;
delay_ms(10);
if(i==20)
{
LED0=!LED0;//提示系统正在运行
i=0;
}
}
}
main 函数的流程大致是:先初始化内存 SendBuff 的值,然后通过 KEY0 开启串口 DMA 发送,
在发送过程中,通过__HAL_DMA_GET_COUNTER()函数获取当前还剩余的数据量来计算传输百分比,
最后在传输结束之后清除相应标志位,提示已经传输完成。这里还有一点要注意,因为是使用
的串口 1 DMA 发送,所以代码中使用 HAL_UART_Transmit_DMA 函数开启串口的 DMA 发送:
至此, DMA 串口传输的软件设计就完成了。
四、结果