ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

嵌入式工程师成长之路(十六)之DMA

2021-06-18 16:33:01  阅读:217  来源: 互联网

标签:DMA 工程师 嵌入式 传输 DMA1 InitStructure 外设 通道


stm32F1DMA详解

1、问题:什么是DMA?
回答:DMA,全称为:Direct Memory Access,即直接存储器访问
简而言之,DMA就是将一个内存里的数据搬运到另一个内存里,此过程无需CPU直接控制输出
详细介绍:
DMA,全称为:Direct Memory Access,即直接存储器访问,DMA 传输将数据从一个地址空间复制到另外一个地址空间。当 CPU 初始化这个传输动作,传输动作本身是由DMA 控制器 来实行和完成。典型的例子就是移动一个外部内存的区块到芯片内部更快的内存区。像是这样的操作并没有让处理器工作拖延,反而可以被重新排程去处理其他的工作。DMA 传输对于高效能嵌入式系统算法和网络是很重要的。DMA 传输方式无需 CPU 直接控制传输,也没有中断处理方式那样保留现场和恢复现场的过程,通过硬件为 RAM 与 I/O 设备开辟一条直接传送数据的通路,能使 CPU 的效率大为提高。STM32 最多有 2 个 DMA 控制器(DMA2 仅存在大容量产品中),DMA1 有 7 个通道。DMA2 有 5个通道。每个通道专门用来管理来自于一个或多个外设对存储器访问的请求。还有一个仲裁起来协调各个 DMA 请求的优先权。
2、STM32DMA特性:
●每个通道都直接连接专用的硬件 DMA 请求,每个通道都同样支持软件触发。这些功能通过软件来配置。
●在七个请求间的优先权可以通过软件编程设置(共有四级:很高、高、中等和低),假如在相等优先权时由硬件决定(请求 0 优先于请求 1,依此类推) 。 ●独立的源和目标数据区的传输宽度(字节、半字、全字),模拟打包和拆包的过程。源和
目标地址必须按数据传输宽度对齐。
●支持循环的缓冲器管理
●每个通道都有 3 个事件标志(DMA 半传输,DMA 传输完成和 DMA 传输出错),这 3 个事件标志逻辑或成为一个单独的中断请求。
●存储器和存储器间的传输
●外设和存储器,存储器和外设的传输
●闪存、SRAM、外设的 SRAM、APB1 APB2 和 AHB 外设均可作为访问的源和目标。
●可编程的数据传输数目:最大为 65536
3、DMA框图:
在这里插入图片描述
4、DMA1控制器:
在这里插入图片描述
通道 1 的几个 DMA1 请求(ADC1、TIM2_CH3、TIM4_CH1),这几个是通过逻辑或到通道 1 的,这样我们在同一时间,就只能使用其中的一个。其他通道也是类似的。
5、各个通道的DMA2请求表:
在这里插入图片描述
6、DMA处理:
在这里插入图片描述
7、DMA相关寄存器
(1) DMA 中断状态寄存器(DMA_ISR)
在这里插入图片描述
如果开启了 DMA_ISR 中这些中断,在达到条件后就会跳到中断服务函数里面去,即使没开启,我们也可以通过查询这些位来获得当前 DMA 传输的状态。这里我们常用的是 TCIFx,即通道 DMA 传输完成与否的标志。注意此寄存器为只读寄存器,所以在这些位被置位之后,只能通过其他的操作来清除。
(2) DMA 中断标志清除寄存器(DMA_IFCR)
在这里插入图片描述
DMA_IFCR 的各位就是用来清除 DMA_ISR 的对应位的,通过写 0 清除。在 DMA_ISR 被置位后,必须通过向该位寄存器对应的位写入 0 来清除。
(3)DMA 通道 x 配置寄存器(DMA_CCRx)
该寄存器控制着 DMA 的很多相关信息,包括数据宽度、外设及存储器的宽度、通道优先级、增量模式、传输方向、中断允许、使能等都是通过该寄存器来设置的。所以 DMA_CCRx 是 DMA 传输的核心控制寄存器。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
(4)DMA 通道 x 传输数据量寄存器(DMA_CNDTRx)
这个寄存器控制 DMA 通道 x 的每次传输所要传输的数据量。其设置范围为 0~65535。并且该寄存器的值会随着传输的进行而减少,当该寄存器的值为 0 的时候就代表此次数据传输已经全部发送完成了。所以可以通过这个寄存器的值来知道当前 DMA 传输的进度。
在这里插入图片描述
(5) DMA 通道 x 的外设地址寄存器(DMA_CPARx)
该寄存器用来存储 STM32 外设的地址,比如我们使用串口 1,那么该寄存器必须写入 0x40013804(其实就是&USART1_DR)。如果使用其他外设,就修改成相应外设的地址就行了。
(6)DMA 通道 x 的存储器地址寄存器(DMA_CMARx)
该寄存器和 DMA_CPARx 差不多,但是是用来放存储器的地址的。比如我们使用 SendBuf[5200]数组来做存储器,那么我们在DMA_CMARx 中写入&SendBuff 就可以了。
8、DMA中断
在这里插入图片描述
9、DMA通道配置过程
在这里插入图片描述
10、DMA配置参数
●通道
●优先级
●数据传输方向
●存储器/外设 数据宽度
●存储器/外设 地址是否增量
●循环模式
●数据传输量
11、DMA函数配置
库函数下 DMA1 通道 4 的配置步骤:
1)使能 DMA 时钟
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); //使能 DMA 时钟
2)初始化 DMA 通道 4 参数
DMA 通道配置参数种类比较繁多,包括内存地址,外设地址,传输数据长度,数据宽度,通道优先级等等。这些参数的配置在库函数中都是在函数 DMA_Init 中完成,函数定义:

void DMA_Init(DMA_Channel_TypeDef* DMAy_Channelx, DMA_InitTypeDef* DMA_InitStruct)

DMA_InitTypeDef 结构体的定义:

typedef struct
{
 uint32_t DMA_PeripheralBaseAddr;
uint32_t DMA_MemoryBaseAddr; 
 uint32_t DMA_DIR; 
 uint32_t DMA_BufferSize; 
 uint32_t DMA_PeripheralInc; 
 uint32_t DMA_MemoryInc; 
 uint32_t DMA_PeripheralDataSize;
 uint32_t DMA_MemoryDataSize; 
 uint32_t DMA_Mode; 
 uint32_t DMA_Priority; 
 uint32_t DMA_M2M; 
}DMA_InitTypeDef;

第一个参数 DMA_PeripheralBaseAddr 用来设置 DMA 传输的外设基地址,比如要进行串口DMA 传输,那么外设基地址为串口接受发送数据存储器 USART1->DR 的地址,表示方法为&USART1->DR。
第二个参数DMA_MemoryBaseAddr为内存基地址,也就是我们存放DMA传输数据的内存地址。
第三个参数 DMA_DIR 设置数据传输方向,决定是从外设读取数据到内存还送从内存读取数据发送到外设,也就是外设是源地还是目的地,这里我们设置为从内存读取数据发送到串口,所以外设自然就是目的地了,所以选择值为DMA_DIR_PeripheralDST。
第四个参数 DMA_BufferSize 设置一次传输数据量的大小
第五个参数 DMA_PeripheralInc 设置传输数据的时候外设地址是不变还是递增。如果设置为递增,那么下一次传输的时候地址加 1,如果是一直往固定外设地址&USART1->DR发送数据,地址不递增,值为DMA_PeripheralInc_Disable;
第六个参 数 DMA_MemoryInc 设置传输数据时候内存地址是否递增。这个参数 和DMA_PeripheralInc 意思接近,只不过针对的是内存。如果是将内存中连续存储单元的数据发送到串口,毫无疑问内存地址是需要递增的,所以值为 DMA_MemoryInc_Enable。
第七个参数 DMA_PeripheralDataSize 用来设置外设的数据长度是为字节传输(8bits),半字传输 (16bits) 还 是 字 传 输 (32bits) ,如果是 8 位 字 节 传 输 ,值 设 置 为DMA_PeripheralDataSize_Byte。
第八个参数 DMA_MemoryDataSize 是用来设置内存的数据长度,和第七个参数意思接近,如果设置为字节传输,值 设 置 为_MemoryDataSize_Byte。
第九个参数 DMA_Mode 用来设置 DMA 模式是否循环采集,也就是说,比如我们要从内存中采集 64 个字节发送到串口,如果设置为重复采集,那么它会在 64 个字节采集完成之后继续从内存的第一个地址采集,如此循环。如果设置为一次连续采集完成之后不循环。设置值为 DMA_Mode_Normal。
第十个参数是设置 DMA 通道的优先级,有低,中,高,超高三种模式,如果设置优先级别为中级,值为DMA_Priority_Medium。如果要开启多个通道,那么这个值就非常有意义。
第 十 一 个 参 数 DMA_M2M 设 置 是 否 是 存 储 器 到 存 储 器 模 式 传 输,可以选择DMA_M2M_Disable。
实例代码:

DMA_InitTypeDef DMA_InitStructure;
DMA_InitStructure.DMA_PeripheralBaseAddr = &USART1->DR; //DMA 外设 ADC 基地址
DMA_InitStructure.DMA_MemoryBaseAddr = cmar; //DMA 内存基地址
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; //从内存读取发送到外设
DMA_InitStructure.DMA_BufferSize = 64; //DMA 通道的 DMA 缓存的大小
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 通道 x 拥有中优先级
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; //非内存到内存传输
DMA_Init(DMA_CHx, &DMA_InitStructure); //根据指定的参数初始化

3)使能串口 DMA 发送
进行 DMA 配置之后,我们就要开启串口的 DMA 发送功能,使用的函数是:
USART_DMACmd(USART1,USART_DMAReq_Tx,ENABLE);
如果是要使能串口 DMA 接受,那么第二个参数修改为 USART_DMAReq_Rx 即可。
4)使能 DMA1 通道 4,启动传输。
使能串口 DMA 发送之后,我们接着就要使能 DMA 传输通道:
DMA_Cmd(DMA_CHx, ENABLE);
通过以上 3 步设置,我们就可以启动一次 USART1 的 DMA 传输了。
5)查询 DMA 传输状态
在 DMA 传输过程中,我们要查询 DMA 传输通道的状态,使用的函数是:
FlagStatus DMA_GetFlagStatus(uint32_t DMAy_FLAG)
比如我们要查询 DMA 通道 4 传输是否完成,方法是:
DMA_GetFlagStatus(DMA2_FLAG_TC4);
这里还有一个比较重要的函数就是获取当前剩余数据量大小的函数:
uint16_t DMA_GetCurrDataCounter(DMA_Channel_TypeDef* DMAy_Channelx)
比如我们要获取 DMA 通道 4 还有多少个数据没有传输,方法是:
DMA_GetCurrDataCounter(DMA1_Channel4);
12、程序:
dma.c

#include "dma.h"
DMA_InitTypeDef DMA_InitStructure;
u16 DMA1_MEM_LEN;//保存 DMA 每次数据传送的长度 
//DMA1 的各通道配置
//这里的传输形式是固定的,这点要根据不同的情况来修改
//从存储器->外设模式/8 位数据宽度/存储器增量模式
//DMA_CHx:DMA 通道 CHx
//cpar:外设地址
//cmar:存储器地址
//cndtr:数据传输量
void MYDMA_Config(DMA_Channel_TypeDef* DMA_CHx,u32 cpar,u32 cmar,u16 cndtr)
{
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); //使能 DMA 时钟
DMA_DeInit(DMA_CHx); //将 DMA 的通道 1 寄存器重设为缺省值
DMA1_MEM_LEN=cndtr;
DMA_InitStructure.DMA_PeripheralBaseAddr = cpar; //DMA 外设 ADC 基地址
DMA_InitStructure.DMA_MemoryBaseAddr = cmar; //DMA 内存基地址
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; //数据传输方向内存到外设
DMA_InitStructure.DMA_BufferSize = cndtr; //DMA 通道的 DMA 缓存的大小
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; //DM 通道拥有中优先级
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; //非内存到内存传输
DMA_Init(DMA_CHx, &DMA_InitStructure); //初始化 DMA 的通道
} 
//开启一次 DMA 传输
void MYDMA_Enable(DMA_Channel_TypeDef*DMA_CHx)
{ 
DMA_Cmd(DMA_CHx, DISABLE ); //关闭 USART1 TX DMA1 所指示的通道 
DMA_SetCurrDataCounter(DMA1_Channel4,DMA1_MEM_LEN);//设置 DMA 缓存的大小
DMA_Cmd(DMA_CHx, ENABLE); //使能 USART1 TX DMA1 所指示的通道
}

该部分代码仅仅 2 个函数,MYDMA_Config 函数,基本上就是按照我们上面介绍的步骤来初始化 DMA 的,该函数在外部只能修改通道、源地址、目标地址和传输数据量等几个参数,更多的其他设置只能在该函数内部修改。MYDMA_Enable 函数就是设置 DMA 缓存大小并且使能DMA 通道。
main.c

#define SEND_BUF_SIZE 8200
u8 SendBuff[SEND_BUF_SIZE]; //发送数据缓冲区
const u8 TEXT_TO_SEND[]={"ALIENTEK ELITE STM32F1 DMA 串口实验"};
int main(void)
{
u16 i;
u8 t=0;
u8 j,mask=0;
float pro=0;//进度
delay_init(); //延时函数初始化 
 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//设置中断优先级分组为组 2 
uart_init(115200); //串口初始化为 115200
LED_Init(); //初始化与 LED 连接的硬件接口
LCD_Init(); //初始化 LCD 
KEY_Init(); //按键初始化
MYDMA_Config(DMA1_Channel4,(u32)&USART1->DR,
(u32)SendBuff,SEND_BUF_SIZE); 
POINT_COLOR=RED;//设置字体为红色
LCD_ShowString(30,50,200,16,16,"ELITE STM32");
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,"2015/1/15");
LCD_ShowString(30,130,200,16,16,"KEY0:Start");
//显示提示信息
j=sizeof(TEXT_TO_SEND); 
for(i=0;i<SEND_BUF_SIZE;i++)//填充数据到 SendBuff
{
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 按下
{
LCD_ShowString(30,150,200,16,16,"Start Transimit....");
LCD_ShowString(30,170,200,16,16," %");//显示百分号
 printf("\r\nDMA DATA:\r\n"); 
 USART_DMACmd(USART1,USART_DMAReq_Tx,ENABLE);
//使能串口 1 的 DMA 发送 
MYDMA_Enable(DMA1_Channel4);//开始一次 DMA 传输! 
 //等待 DMA 传输完成,此时我们来做另外一些事,点灯
 //实际应用中,传输数据期间,可以执行另外的任务
 while(1)
 {
if(DMA_GetFlagStatus(DMA1_FLAG_TC4)!=RESET)//判断通道 4 传输完成
{
DMA_ClearFlag(DMA1_FLAG_TC4);//清除通道 4 传输完成标志
break; 
 }
pro=DMA_GetCurrDataCounter(DMA1_Channel4);//得到当前剩余数据量
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 发送,在发送过程中,通过 DMA_GetCurrDataCounter()函数获取当前还剩余的数据量来计算传输百分比,最后在传输结束之后清除相应标志位,提示已经传输完成。这里还有一点要注意,因为是使用的串口 1 DMA 发送,所以代码中使用 USART_DMACmd 函数开启串口的 DMA 发送:
USART_DMACmd(USART1,USART_DMAReq_Tx,ENABLE); //使能串口 1 的 DMA 发送

标签:DMA,工程师,嵌入式,传输,DMA1,InitStructure,外设,通道
来源: https://blog.csdn.net/lostlll/article/details/118020917

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有