[TOC]

简介

直接存储器存取(DMA)用来提供在外设和存储器之间或者存储器和存储器之间的高速数据传输。无须CPU干预,数据可以通过DMA快速地移动,这就节省了CPU的资源来做其他操作。两个DMA控制器有12个通道(DMA1有7个通道,DMA2有5个通道),每个通道专门用来管理来自于一个或多个外设对存储器访问的请求。还有一个仲裁器来协调各个DMA请求的优先权。

上面来自官方文档对DMA的解释,从最后一句话,仲裁器协调各个DMA请求。可以推测出DMA各个通道在搬运数据时,并不是并行运行的,而是并发的。关于这一点下面会进一步解释

DMA控制器和Cortex™-M3核心共享系统数据总线,执行直接存储器数据传输。当CPU和DMA同时访问相同的目标(RAM或外设)时,DMA请求会暂停CPU访问系统总线达若干个周期,总线仲裁器执行循环调度,以保证CPU至少可以得到一半的系统总线(存储器或外设)带宽。

也就是在408操作系统中学习到的,当CPU和DMA都要访问一块数据的时候,DMA会优先访问,CPU等待DMA处理之后再进行处理

主要特性

在这里我们以stm32f103c8t6为例。

  • 只有一个DMA1(有7个通道)

  • 每个通道都直连对应的硬件请求,每个通道也同样支持软件触发,通过软件来配置

  • 在我们初始化DMA设置参数时,会有一个优先级的参数(共有四级: 很高、高、中等和低)。当优先级相等的时候由硬件决定(就是上文中仲裁器的仲裁方式)

  • 初始化时,同样也会设置数据源和目标数据区的传输宽度(字节,半字,全字),模拟打包拆包的过程,源地址和目的地址必须按照数据宽度对齐

    由以上这几条我们可以更好的推理出,DMA数据搬运的方式。在初始化的时候就设置了等级。也就是有优先级优先传输,那么很明显优先级不同的话,会出现通道阻塞。但是这一点CPU是感觉不到的

    如果是同优先级呢?关于传输宽度我们就可以大致猜出来,应该是并发的。因为设置了数据宽度,假设我们用USART接收数据(100Bytes),数据宽度设置为字节。那么有两个串口同时收到数据,DMA应该是一字节一字节的交替搬运也就是并发执行的。

    由此可见,高优先级抢占+同优先级并发的设计思想是很常见的设计思路

  • 支持循环的缓冲器管理

  • 每个通道都有三个事件标志(DMA半传输,DMA传输完成,DMA传输出错),这三个事件标志裸机或称为一个单独的请求中断

    在DMA的使用中,搬运数据后,为了避免出现CPU数据处理不及时,出现数据覆盖的情况,有非常经典的两个使用方法:

    1. 线性缓冲区 + 半传输中断+传输中断
    2. 双缓冲机制

    其实二者的思路是一样的,将CPU处理的数据区域和DMA搬运数据区域分割开来,避免受影响。无非区别就是关于两块缓冲区的物理地址一个相邻,一个不相邻。

    在stm32f103c8t6的实现中,是没有办法实现真正意义上的双缓冲区的机制,只能做到逻辑分割。

  • 源地址和目标地址没有什么特别的限制,我们日常开发环境基本都包含在内了

    方向: 外设 ——> 存储器 存储器——>外设 存储器——>存储器

    闪存、SRAM、外设都可以作为源地址和目标地址

  • 可编程的数据传输数目:最大为65535

image

可以看到DMA是挂载到AHP总线上的设备。所以在初始化的时候,我们应该开启AHB的时钟。

下面是我引用的江科大的的图。还是很直观的。如果看不懂的话可以去B站看一下,质量还是很高的。

image

这张图中我们还有一个计数器在上面没有讲到,其实也可以推理出来应该有这么一个东西。因为DMA转运数据我们设置数据宽度,以及也存在优先级抢占的方式。如果高优先级的通道数据搬运完了,总要让低级的通道搬运。所以这里应该有一个机制可以知道,这个通道的数据搬运结束了。这就是传输计数器,前面我们设置了数据宽度,也就是每搬运一次的数据是多少。搬运一次计数器就减1。

计数器置0之后,如果再来数据,就不会工作了,此时就需要重置计数器。在编程中要注意这里,重置计数器的话要按照以下顺序操作:
关使能——>重装计数器——>开使能 关使能——>重装计数器——>开使能 关使能——>重装计数器——>开使能
下面一些文档中提到关于底层是怎么运作的内容,我不会全部粘贴或讲解,感兴趣的话可以去看一下手册,还是可以看懂的,篇幅不长。

DMA通道

image

这就是DMA中各个外设对应的通道。在写程序的时候,我更喜欢直接把我常用的外设用宏定义出来。这样省得每次过来看图。

实验:DMA + IDLE

在这里我们用一个比较经典的组合来作为DMA的实验。在此篇文章之前,我们使用ADC+DMA实现了自动化工作的一个效果只需要初始化两个外设就可以工作了。

在这里我们使用一个开销很小的串口实验来作为案例。这也是我项目中可以优化的一个地方。

在串口数据收发时,我们一般使用中断来进行数据接收。假设每次接收数据都是100字节。那么也就是说我们每次接收一个字节的数据都要触发一次中断,假设一次中断是1个时钟周期,那就是需要100个时钟周期进行数据处理。而使用DMA只需要在传输完成后通知CPU即可,也就是只需要一次中断。开销显而易见了。

在这里就又有一个问题,我们需要在接收数据之前需要先设置计数器。设置计数器之后才能搬运,那么这里就会出现三种情况:

  1. 数据传输结束,计数器未归0
  2. 数据传输结束,计数器刚好归0
  3. 数据传输未结束,计数器归0

第二种当然是最好的结果,不需要我们额外处理了。但是显然这是最难出现的。

第三种情况,我们在上面已经接触到了,可以通过半传输中断和传输中断,在中断中重置计数器,进而可以计数转运数据

那么第一种呢?USART的不定长数据接收如何与预先设置数据长度的DMA配合。

很简单,我们先设置的足够大的就可以了,当数据接收完成之后通过中断,通知CPU和重置计数器。

在设置USART中断的标志位中还有一个标志位就叫做IDLE

在串口通信中,数据是一个字节一个字节传输的。当一帧数据(包含起始位、数据位、校验位和停止位)传输完毕后,如果通信线路(RX引脚)继续保持高电平(即空闲状态),且空闲持续时间大于或等于一个完整字符帧的传输时间,硬件就会自动将 IDLE 标志位置 1

由此我们就完成了最后一块拼图。下面就开始准备写代码吧!

实验要求

向串口输入数据,再将输入数据打印到串口助手中

DMA.h头文件

// DMA.h 头文件
#ifndef __DMA_H
#define __DMA_H
#include "stm32f10x.h"
void Init_DMA(char *addr, uint16_t buffersize);

#endif

DAM.c源文件

#include "DMA.h"

void Init_DMA(char *addr, uint16_t buffersize){
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
	
	DMA_InitTypeDef DMA_InitStruct;
	// DMA转运数据的长度/次数
	DMA_InitStruct.DMA_BufferSize = buffersize;
	// 外设寄存器是源地址
	DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralSRC ;
	// ADC1外设到内存
	DMA_InitStruct.DMA_M2M = DMA_M2M_Disable ;
	// 内存相关设置
	DMA_InitStruct.DMA_MemoryBaseAddr = (uint32_t)addr;
	// USART接收是字节为单位
	DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
	// 进行自增
	DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable;
	
	// 单词触发
	DMA_InitStruct.DMA_Mode = DMA_Mode_Normal;
	
	// 设置外设相关
	DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR;
	// 数据宽度保持一致
	DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
	// 寄存器不自增
	DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
	
	// 设置优先级
	DMA_InitStruct.DMA_Priority = DMA_Priority_Medium;
	
	DMA_Init(DMA1_Channel5, &DMA_InitStruct);
	DMA_Cmd(DMA1_Channel5, DISABLE);
	DMA_SetCurrDataCounter(DMA1_Channel5 , 256);
	DMA_Cmd(DMA1_Channel5, ENABLE);
}

USART1.h头文件

#ifndef __USART1_H
#define __USART1_H
#include "stm32f10x.h"
void init_USART1(void);
void USART1_SendStr(char *str);
void init_NVIC(void);
#endif

USART1.c源文件

#include "USART1.h"

#define USART1_TX GPIO_Pin_9
#define USART1_RX GPIO_Pin_10
extern volatile uint8_t flag;
void init_USART1(){
	// 开启外设时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA |RCC_APB2Periph_USART1, ENABLE);
	// 先初始化引脚
	GPIO_InitTypeDef GPIO_InitStruct;
	// 因为是USART外设使用,需要移交控制权,故使用复用推挽
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
	GPIO_InitStruct.GPIO_Pin = USART1_TX;
	GPIO_InitStruct.GPIO_Speed = GPIO_Speed_2MHz;
	GPIO_Init(GPIOA, &GPIO_InitStruct);
	// 接收采用浮空输入模式
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;
	GPIO_InitStruct.GPIO_Pin = USART1_RX;
	GPIO_Init(GPIOA, &GPIO_InitStruct);
	
	// 初始化USART外设
	USART_InitTypeDef USART_InitStruct;
	// 设置波特率
	USART_InitStruct.USART_BaudRate = 115200; 
	// 不使用硬件控制流
	USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None; 
	// 开启收发模式
	USART_InitStruct.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
	// 不设置校验位
	USART_InitStruct.USART_Parity = USART_Parity_No;
	// 1位停止位
	USART_InitStruct.USART_StopBits = USART_StopBits_1;
	// 数据长度8bit
	USART_InitStruct.USART_WordLength = USART_WordLength_8b;
	USART_Init(USART1, &USART_InitStruct);
	// 记得开使能
	USART_Cmd(USART1, ENABLE);
	// 也要开启USART使用DMA的使能
	// 这里的参数分别是(外设,发送/接收,使能)
	USART_DMACmd(USART1,USART_DMAReq_Rx, ENABLE);
}

void USART1_SendStr(char *str){
	while(*str){
		while(USART_GetFlagStatus(USART1,USART_FLAG_TXE) == RESET); 
		USART_SendData(USART1, (uint16_t)*str++);
	}
}
// 因为需要触发IDLE中断,所以配置中段相关
void init_NVIC(void){
	// 设置中断分组
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);
	NVIC_InitTypeDef NVIC_InitStruct;
	NVIC_InitStruct.NVIC_IRQChannel = USART1_IRQn;
	NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
	NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 2;
	NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;
	
	NVIC_Init(&NVIC_InitStruct);
	// 记得开启USART1的中断
	USART_ITConfig(USART1, USART_IT_IDLE, ENABLE);
}

void USART1_IRQHandler(void){
	if(USART_GetITStatus(USART1, USART_IT_IDLE) == SET){
		// 清除IDLE标志位
		volatile uint32_t temp;
		temp = USART1->SR;		// 先读取状态标志位
		temp = USART1->DR;		// 再读取数据实现清除标志位
		
		flag = 1;
		DMA_Cmd(DMA1_Channel5, DISABLE);
		DMA_SetCurrDataCounter(DMA1_Channel5 , 256);
		DMA_Cmd(DMA1_Channel5, ENABLE);
	}
	
}

main.c主程序

#include "stm32f10x.h"
#include "USART1.h"
#include "DMA.h"

#define BUFFERSIZE 256

char buffer[BUFFERSIZE];
volatile uint8_t flag = 0;

int main(){
	init_USART1();
	init_NVIC();
	Init_DMA(buffer, BUFFERSIZE);

	
	while(1){
		if(flag){
			USART1_SendStr("receive:");
			USART1_SendStr(buffer);
			flag = 0;
		}

	}
		
}

实验结果:
image

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐