DMA—直接存储区访问
DMA 简介
DMA(Direct Memory Access)—直接存储器存取,它的主要功能是用来搬数据,但是不需要占用 CPU 。
数据传输支持从外设到存储器或者存储器到存储器,这里的存储器可以是 SRAM 或者是 FLASH。
DMA 控制器包含了 DMA1 和 DMA2,其中 DMA1 有 7 个通道, DMA2 有 5 个通道。
注意的是 DMA2 只存在于大容量的单片机中。
DMA 功能框图
DMA 请求
通过 DMA 来传输数据,必须先给 DMA 控制器发送 DMA 请求,DMA 收到请求信号之后,控制器会给外设一个应答信号,当外设应答后且 DMA 控制器收到应答信号之后,就会启动 DMA 的传输,直到传输完毕。
不同的 DMA 控制器的通道对应着不同的外设请求,具体见 DMA 请求映像表。
其中 ADC3、 SDIO 和 TIM8 的 DMA 请求只在大容量产品中存在,这个在具体项目时要注意。
通道
DMA 具有 12 个独立可编程的通道,其中 DMA1 有 7 个通道, DMA2 有 5 个通道,每个通道对应不同的外设的 DMA 请求。
虽然每个通道可以接收多个外设的请求,但是同一时间只能接收一个,不能同时接收多个。
仲裁器
仲裁器管理 DMA 通道请求分为两个阶段。
第一阶段属于软件阶段:有 4 个等级:非常高、高、中和低四个优先级。
第二阶段属于硬件阶段:两个或以上的 DMA 通道请求设置的优先级一样,编号越低优先权越高,DMA1 控制器拥有高于 DMA2 控制器的优先级。
DMA 数据配置
方向有三个:从外设到存储器,从存储器到外设,从存储器到存储器。
外设到存储器
ADC 采集为例,DMA 外设寄存器的地址对应的就是 ADC数据寄存器的地址, DMA 存储器的地址就是我们自定义的变量(用来接收存储 AD 采集的数据)的地址。方向我们设置外设为源地址。
存储器到外设
串口向电脑端发送数据为例,DMA 外设寄存器的地址对应的就是串口数据寄存器的地址, DMA 存储器的地址就是我们自定义的变量(相当于一个缓冲区,用来存储通过串口发送到电脑的数据)的地址。方向我们设置外设为目标地址。
存储器到存储器
内部 FLASH 向内部 SRAM 复制数据为例。DMA 外设寄存器的地址对应的就是内部 FLASH(我们这里把内部 FALSH 当作一个外设来看)的地址, DMA存储器的地址就是我们自定义的变量(相当于一个缓冲区,用来存储来自内部 FLASH 的数据)的地址。方向我们设置外设(即内部 FLASH)为源地址。
传输数据的单位
数据传输正确,源和目标地址存储的数据宽度还必须一致。
串口向电脑发送数据为例,我们可以一次性给电脑发送很多数据,具体多少由 DMA_CNDTR配置,这是一个 32 位的寄存器,一次最多只能传输 65535 个数据。串口数据寄存器是 8 位的,所以我们定义的要发送的数据也必须是 8 位。在 DMA 控制器的控制下,还必须正确设置两边数据指针的增量模式。要发送的数据很多,每发送完一个,那么存储器的地址指针就应该加 1,而串口数据寄存器只有一个,那么外设的地址指针就固定不变。
传输完成标志
每个 DMA 通道在 DMA 传输过半、传输完成和传输错误时都会有相应的标志位。
传输完成还分两种模式,是一次传输还是循环传输。
一次传输:传输一次之后就停止。
循环传输:一次传输完成之后又恢复第一次传输时的配置循环传输,不断的重复。
DMA 初始化结构体详解
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;
1) DMA_PeripheralBaseAddr:外设地址,设定 DMA_CPAR 寄存器的值;一般设置为外设的数据寄存器地址,如果是存储器到存储器模式则设置为其中一个存储器地址。
2) DMA_Memory0BaseAddr:存储器地址,一般设置为我们自定义存储区的首地址。
3) DMA_DIR:传输方向选择,可选外设到存储器、存储器到外设。这里并没有存储器到存储器的方向选择,当使用存储器到存储器时,只需要把其中一个存储器当作外设使用即可。
4) DMA_BufferSize:设定待传输数据数目。
5) DMA_PeripheralInc:如果配置为 DMA_PeripheralInc_Enable,使能外设地址自动递增功能,一般外设都是只有一个数据寄存器,所以一般不会使能该位。
6) DMA_MemoryInc:如果配置为 DMA_MemoryInc_Enable,使能存储器地址自动递增功能,,我们自定义的存储区一般都是存放多个数据的,所以要使能存储器地址自动递增功能。
7) DMA_PeripheralDataSize:外设数据宽度,可选字节 (8 位)、半字 (16 位) 和字 (32 位)。
8) DMA_MemoryDataSize:存储器数据宽度,可选字节 (8 位)、半字 (16 位) 和字 (32 位),当外设和存储器之间传数据时,两边的数据宽度应该设置为一致大小。
9) DMA_Mode: DMA 传输模式选择,可选一次传输或者循环传输,ADC 采集是持续循环进行的,所以使用循环传输模式。
10) DMA_Priority:软件设置通道的优先级,有 4 个可选优先级分别为非常高、高、中和低, DMA 通道优先级只有在多个 DMA 通道同时使用时才有意义,如果是单个通道,优先级可以随便设置。
11) DMA_M2M:存储器到存储器模式,使用存储器到存储器时用到,可启动存储器到存储器模式。
DMA 存储器到存储器模式实验
编程要点
1) 使能 DMA 时钟;
2) 配置 DMA 数据参数;
3) 使能 DMA,进行传输;
4) 等待传输完成,并对源数据和目标地址数据进行比较。
dma.c
#include "./dma/dma.h"
//定Source_Data组作为DMA传输数据源
//const关键字将Source_Data组变量定义为常量类型,表示数据存储在内部的FLASH中
const uint32_t Source_Data[BUFFER_SIZE]= {
0x1,0x2,0x3,0x4,
0x5,0x6,0x7,0x8,
0x9,0xA,0xB,0xC,
0xD,0xE,0xF,0x10,
0x20,0x30,0x40,0x50,
0x60,0x70,0x80,0x90,
0xA0,0xB0,0xC0,0xD0,
0xE0,0xF0,0x100,0x200};
//定义DMA传输目标存储器
//存储在内部的SRAM中
uint32_t Distination_Data[BUFFER_SIZE];
void DMA_Config(void)
{
DMA_InitTypeDef DMA_InitStruct;
// 开启DMA时钟
RCC_AHBPeriphClockCmd(DMA_CLOCK,ENABLE);
// 源数据地址
DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)Source_Data;
// 目标地址
DMA_InitStruct.DMA_MemoryBaseAddr = (uint32_t)Distination_Data;
// 方向:外设到存储器(这里的外设是内部的FLASH)
DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralSRC;
// 传输大小
DMA_InitStruct.DMA_BufferSize = BUFFER_SIZE;
// 外设(内部的FLASH)地址递增
DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Enable;
// 内存地址递增
DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable;
// 外设数据单位
DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Word;
// 内存数据单位
DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_Word;
// DMA模式,一次或者循环模式
DMA_InitStruct.DMA_Mode = DMA_Mode_Normal;
// 优先级:高
DMA_InitStruct.DMA_Priority = DMA_Priority_High;
// 使能内存到内存的传输
DMA_InitStruct.DMA_M2M = DMA_M2M_Enable;
// 配置DMA通道
DMA_Init(DMA_CHANNEL,&DMA_InitStruct);
//清除DMA数据流传输完成标志位
DMA_ClearFlag(DMA_FLAG_TC);
// 使能DMA
DMA_Cmd(DMA_CHANNEL,ENABLE);
}
//判断两个数据是否相等
uint8_t DataCmp(const uint32_t *pData1,uint32_t *pData2,uint16_t DataLen)
{
while(DataLen--)
{
if(*pData1 != *pData2)
{
return 0;
}
pData1++;
pData2++;
}
return 1;
}
dma.h
#ifndef _DMA_H
#define _DMA_H
#include "stm32f10x.h"
// 当使用存储器到存储器模式时候,通道可以随便选,没有硬性的规定
#define DMA_CHANNEL DMA1_Channel1
#define DMA_CLOCK RCC_AHBPeriph_DMA1
// 传输完成标志
#define DMA_FLAG_TC DMA1_FLAG_TC1
// 要发送的数据大小
#define BUFFER_SIZE 32
void DMA_Config(void);
uint8_t DataCmp(const uint32_t *pData1,uint32_t *pData2,uint16_t DataLen);
#endif
main.c
#include "stm32f10x.h"
#include "led.h"
#include "dma/dma.h"
#include "delay/delay.h"
extern const uint32_t Source_Data[BUFFER_SIZE];
extern uint32_t Distination_Data[BUFFER_SIZE];
int main(void)
{
uint8_t transferstate; //传输状态
LED_Init(); //LED初始化
DMA_Config(); //DMA初始化
LED_Config(LED_B,ON);
Delay_ms(1000);
LED_Config(LED_B,OFF);
while(DMA_GetFlagStatus(DMA_FLAG_TC) == RESET); //等待DMA传输完成
transferstate = DataCmp(Source_Data,Distination_Data,32); //比较源数据和传输完成的目的数据
if(transferstate == 1)
LED_Config(LED_G,ON);
else
LED_Config(LED_R,ON);
while(1)
{
}
}
DMA 存储器到外设模式实验
编程要点
1) 配置 USART 通信功能;
2) 设置串口 DMA 工作参数;
3) 使能 DMA;
4) DMA 传输同时 CPU 可以运行其他任务。
dam.c
#include "./dma/dma.h"
//定Source_Data组作为DMA传输数据源
uint8_t Source_Data[BUFFER_SIZE];
void DMA_Config(void)
{
DMA_InitTypeDef DMA_InitStruct;
// 开启DMA时钟
RCC_AHBPeriphClockCmd(DMA_CLOCK,ENABLE);
// 源数据地址
DMA_InitStruct.DMA_PeripheralBaseAddr = USART_DR_ADDRESS;
// 目标地址
DMA_InitStruct.DMA_MemoryBaseAddr = (uint32_t)Source_Data;
// 方向:外设到存储器(这里的外设是内部的FLASH)
DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralDST;
// 传输大小
DMA_InitStruct.DMA_BufferSize = BUFFER_SIZE;
// 外设(内部的FLASH)地址递增
DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
// 内存地址递增
DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable;
// 外设数据单位
DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
// 内存数据单位
DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
// DMA模式,一次或者循环模式
DMA_InitStruct.DMA_Mode = DMA_Mode_Normal;
// 优先级:高
DMA_InitStruct.DMA_Priority = DMA_Priority_High;
// 使能内存到内存的传输
DMA_InitStruct.DMA_M2M = DMA_M2M_Disable;
// 配置DMA通道
DMA_Init(DMA_CHANNEL,&DMA_InitStruct);
//清除DMA数据流传输完成标志位
DMA_ClearFlag(DMA_FLAG_TC);
// 使能DMA
DMA_Cmd(DMA_CHANNEL,ENABLE);
}
//串口初始化
void usart_init(void)
{
//GPIO及USART结构体
GPIO_InitTypeDef GPIO_InitStruct;
USART_InitTypeDef USART_InitStruct;
//使能GPIO时钟和USART时钟
USART_GPIO_APBxClkCmd(USART_GPIO_CLK,ENABLE);
USART_APBxClkCmd(USART_CLK,ENABLE);
//GPIO结构体配置及初始化
//TX的GPIO
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStruct.GPIO_Pin = USART_TX_GPIO_PIN;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(USART_TX_GPIO_PORT,&GPIO_InitStruct);
//USART结构体配置
USART_InitStruct.USART_BaudRate = USART_BAUDRATE;
USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStruct.USART_Mode = USART_Mode_Tx;
USART_InitStruct.USART_Parity = USART_Parity_No;
USART_InitStruct.USART_StopBits = USART_StopBits_1;
USART_InitStruct.USART_WordLength = USART_WordLength_8b;
//初始化USART
USART_Init(USARTx,&USART_InitStruct);
//使能USART
USART_Cmd(USARTx,ENABLE);
/* USART1 向 DMA发出TX请求 */
USART_DMACmd(USARTx, USART_DMAReq_Tx, ENABLE);
}
//发送一个字节数据
void usart_sendbyte(USART_TypeDef* pUSARTx, uint8_t data)
{
//发送一个字节数据
USART_SendData(pUSARTx,data);
//等待发送数据寄存器为空
while(USART_GetFlagStatus(pUSARTx,USART_FLAG_TXE) == RESET);
}
//发送一个数组数据
void usart_sendarr(USART_TypeDef* pUSARTx, uint8_t *arr, uint8_t len)
{
uint8_t i;
for(i = 0; i < len; i++)
{
//发送一个字节数据
usart_sendbyte(pUSARTx,*arr++);
}
//等待发送完成
while(USART_GetFlagStatus(pUSARTx,USART_FLAG_TC) == RESET);
}
//发送字符串数据
void usart_sendstr(USART_TypeDef* pUSARTx, char *str)
{
do
{
//发送一个字节数据
usart_sendbyte(pUSARTx,*str++);
}while((*str) != '\0');
//等待发送完成
while(USART_GetFlagStatus(pUSARTx,USART_FLAG_TC) == RESET);
}
//重定向c库函数printf到串口,重定向后可使用printf、putchar函数
int fputc(int ch, FILE *f)
{
//发送一个字节数据
USART_SendData(USARTx,(uint8_t)ch);
//等待发送数据寄存器为空
while(USART_GetFlagStatus(USARTx,USART_FLAG_TXE) == RESET);
return ch;
}
//重定向c库函数scanf到串口,重写向后可使用scanf、getchar等函数
int fgetc(FILE *f)
{
//等待接受数据寄存器为空
while(USART_GetFlagStatus(USARTx,USART_FLAG_RXNE) == RESET);
return USART_ReceiveData(USARTx);
}
dma.h
#ifndef _DMA_H
#define _DMA_H
#include "stm32f10x.h"
#include <stdio.h>
#define USARTx USART1
#define USART_CLK RCC_APB2Periph_USART1
#define USART_APBxClkCmd RCC_APB2PeriphClockCmd
#define USART_BAUDRATE 115200
// USART GPIO 引脚宏定义
#define USART_GPIO_CLK (RCC_APB2Periph_GPIOA)
#define USART_GPIO_APBxClkCmd RCC_APB2PeriphClockCmd
#define USART_TX_GPIO_PORT GPIOA
#define USART_TX_GPIO_PIN GPIO_Pin_10
// 当使用存储器到存储器模式时候,通道可以随便选,没有硬性的规定
#define DMA_CHANNEL DMA1_Channel4
#define DMA_CLOCK RCC_AHBPeriph_DMA1
// 传输完成标志
#define DMA_FLAG_TC DMA1_FLAG_TC4
// 要发送的数据大小
#define BUFFER_SIZE 5000
外设寄存器地址
#define USART_DR_ADDRESS (USART1_BASE+0x04)
void DMA_Config(void);
void usart_init(void);
#endif
main.c
#include "stm32f10x.h"
#include "led.h"
#include "dma/dma.h"
#include "delay/delay.h"
extern uint8_t Source_Data[BUFFER_SIZE];
int main(void)
{
uint16_t i;
LED_Init();
usart_init();
DMA_Config();
//初始化要发送的数据
for(i=0; i<BUFFER_SIZE; i++)
{
Source_Data[i] = 'x';
}
while(1)
{
//DMA串口传输数据的同时灯在闪烁
LED_Toggle(LED_G);
Delay_ms(1000);
}
}
DMA 外设到存储器模式实验
编程要点
1) 配置 ADC功能;
2) 设置ADC的DMA 工作参数;
3) 使能 DMA;
4) DMA 传输同时 CPU 可以运行其他任务。
dma.c
#include "./dma/dma.h"
__IO uint16_t ADC_ConvertedValue;
//ADC的DAM模式初始化
void ADCx_DMA_Config(void)
{
DMA_InitTypeDef DMA_InitStruct;
//打开DMA时钟
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);
//复位DMA控制器
DMA_DeInit(ADC_DMA_CHANNEL);
//ADC_DAM结构体配置
//外设及地址:ADC数据寄存器地址
DMA_InitStruct.DMA_PeripheralBaseAddr = ( uint32_t ) ( & ( ADC_x->DR ) );
//存储器地址,实际上就是一个内部SRAM的变量
DMA_InitStruct.DMA_MemoryBaseAddr = (uint32_t)&ADC_ConvertedValue;
//数据传输方向(数据源来自外设)
DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralSRC;
//缓冲区的大小为1(数据传输的数目),缓冲区的大小应该改等于存储器大小()
DMA_InitStruct.DMA_BufferSize = 1;
//外设寄存器只有一个,所以地址不用自增
DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
//存储器地址固定,所以也不用自增
DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Disable;
//外设数据大小为半字,即两个字节
DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
//存储器数据大小也为半字节,跟外设数据的大小一样
DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
//循环传输模式
DMA_InitStruct.DMA_Mode = DMA_Mode_Circular;
//DAM传输通道优先级为高,当使用一个DMA通道是,优先级没有影响
DMA_InitStruct.DMA_Priority = DMA_Priority_High;
//外设存储器(即禁止存储器到存储器)
DMA_InitStruct.DMA_M2M = DMA_M2M_Disable;
//初始化DMA
DMA_Init(ADC_DMA_CHANNEL, &DMA_InitStruct);
//使能DMA通道
DMA_Cmd(ADC_DMA_CHANNEL,ENABLE);
}
//ADC的GPIO初始化
void ADCx_GPIO_Config(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
//打开GPIO时钟
ADC_GPIO_APBxClock_FUN(ADC_GPIO_CLK,ENABLE);
//GPIO结构体配置
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AIN;
GPIO_InitStruct.GPIO_Pin = ADC_PIN;
//初始化GPIO
GPIO_Init(ADC_PORT,&GPIO_InitStruct);
}
//ADC模式初始化
void ADCx_MODE_Config(void)
{
ADC_InitTypeDef ADC_InitStruct;
//打开 ADC 时钟
ADC_APBxClock_FUN(ADC_CLK,ENABLE);
// ADC 模式配置
// 只使用一个ADC,属于独立模式
ADC_InitStruct.ADC_Mode = ADC_Mode_Independent;
// 禁止扫描模式,多通道才要,单通道不需要
ADC_InitStruct.ADC_ScanConvMode = DISABLE;
// 连续转换模式
ADC_InitStruct.ADC_ContinuousConvMode = ENABLE;
// 不用外部触发转换,软件开启即可
ADC_InitStruct.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
// 转换结果右对齐
ADC_InitStruct.ADC_DataAlign = ADC_DataAlign_Right;
// 转换通道1个
ADC_InitStruct.ADC_NbrOfChannel = 1;
// 初始化ADC
ADC_Init(ADC_x,&ADC_InitStruct);
// 配置ADC时钟为PCLK2的8分频,即9MHz
RCC_ADCCLKConfig(RCC_PCLK2_Div8);
// 配置 ADC 通道转换顺序和采样时间
ADC_RegularChannelConfig(ADC_x, ADC_CHANNEL, 1, ADC_SampleTime_55Cycles5);
//ADC_DAM初始化
ADCx_DMA_Config();
//使能ADC的DAM请求
ADC_DMACmd(ADC_x,ENABLE);
// 开启ADC ,并开始转换
ADC_Cmd(ADC_x,ENABLE);
// 初始化ADC 校准寄存器
ADC_ResetCalibration(ADC_x);
// 等待校准寄存器初始化完成
while(ADC_GetResetCalibrationStatus(ADC_x));
// ADC开始校准
ADC_StartCalibration(ADC_x);
// 等待校准寄存器初始化完成
while(ADC_GetResetCalibrationStatus(ADC_x));
// 由于没有采用外部触发,所以使用软件触发ADC转换
ADC_SoftwareStartConvCmd(ADC_x,ENABLE);
}
//ADC初始化
void ADCx_Init(void)
{
ADCx_GPIO_Config();
ADCx_MODE_Config();
}
dma.h
#ifndef _ADC_H
#define _ADC_H
#include "stm32f10x.h"
// ADC 编号选择 使用ADC2
#define ADC_APBxClock_FUN RCC_APB2PeriphClockCmd
#define ADC_x ADC1
#define ADC_CLK RCC_APB2Periph_ADC1
// ADC GPIO宏定义
// 注意:用作ADC采集的IO必须没有复用,否则采集电压会有影响
#define ADC_GPIO_APBxClock_FUN RCC_APB2PeriphClockCmd
#define ADC_GPIO_CLK RCC_APB2Periph_GPIOC
#define ADC_PORT GPIOC
#define ADC_PIN GPIO_Pin_1
// ADC 通道宏定义
#define ADC_CHANNEL ADC_Channel_11
#define ADC_DMA_CHANNEL DMA1_Channel1
void ADCx_Init(void);
#endif
main.c
#include "stm32f10x.h"
#include "led.h"
#include "./usart/usart.h"
#include "./dma/dma.h"
#include "./delay/delay.h"
extern __IO uint16_t ADC_ConvertedValue;
//采集到的真时电压
float ADC_ConvertedValue_Reality;
int main(void)
{
usart_init();
ADCx_Init();
printf("这是一个adc采集电压的实验(DMA通道读取)!!!");
while(1)
{
//计算采集的真实电压
ADC_ConvertedValue_Reality = (float)ADC_ConvertedValue/4096*3.3;
printf("ADC采集到的值ADC_ConvertedValue:%d\r\n",ADC_ConvertedValue);
printf("ADC采集到的实际电压值ADC_ConvertedValue_Reality:%.2fV\r\n",ADC_ConvertedValue_Reality);
Delay_ms(500);
}
}