九、DMA直接存储器存取

七、DMA直接存储器存取

DMA简介

DMA(Direct Memory Access)直接存储器存取

  • DMA可以提供外设和存储器或者存储器和存储器之间的高速数据传输,无须CPU干预,节省了CPU的资源

    • 外设:外设存储器,一般指外设的数据寄存器

    • 存储器:运行内存SRAM和程序存储器Flash,存储变量、数组和程序代码的地方

    其实外设寄存器也是存储器,只是STM32方便区分取得名字,所以DMA本质上都是存储器到存储器的数据转运

  • 12个独立可配置的通道: DMA1(7个通道), DMA2(5个通道)
    每个通道都支持软件触发和特定的硬件触发

    • 软件触发:如果是将数据从存储器转运到存储器,就使用软件触发,因为软件触发不讲究转换时机,只需要以最快速度转运完成

    • 特定的硬件触发:将外设中的数据转运到存储器中,需要一定的触发时机,例如AD转换完成,才需要通知DMA来转运数据。每个外设触发时走的DMA通道是固定的,所以叫特定的硬件触发

  • STM32F103C8T6的DMA资源:DMA1(7个通道)

存储器映像

image

查看内存时,地址以0x0800开头的数据基本都是编写的C语音代码

地址以0x2000开头的是程序运行过程中产生的变量等

DMA框图

image

image

注意:Flash部分是只读的,如果将Flash存储器作为转运数据的目标地址则会出错

如果需要对Flash进行写入就需要配置Flash阀门控制器

DMA基本结构

image

  • 起始地址:决定数据从哪来到哪去

  • 数据宽度:指定每次转运时的位数。

    • 字节Byte:8位
    • 半字HalfWord:16位
    • 字Word:32位
  • 地址是否自增:转运了一次数据后是否要对地址自增,自增就转运下一个地址的数据,不自增就还是转运当前地址的数据。相当于指针自增

  • 方向:是从外设转运数据到存储器还是从存储器转运到外设

  • 传输计数器:用于记录总共要转运几次数据的,是一个自减寄存器,每转运一次就减一,减到0之后就不在转运,并且转运的地址也会回到一开始的地址

  • 自动重装器:当计数器减到0后,恢复计数器的初始值。不使用自动重装时就是单次模式,使用自动重装就是循环模式

  • M2M:DMA的触发控制。当M2M为1时,软件触发;当M2M为0时硬件触发

    • 软件触发:以最快的速度连续不断的触发DMA,快速清零计数器完成转换。注意不能和自动重装器一起使用,否则会DMA会一直转运数据,停不下来。适用于存储器到存储器的转运
    • 硬件触发:与外设有关,需要一定时机来触发。例如ADC转换完成、串口收到数据、定时时间到了

DMA开始转运数据的条件

  1. DMA使能
  2. 传输计数器必须大于0(注意:写传输寄存器的值时,必须要关闭DMA再进行)
  3. 有触发源

DMA硬件请求通道

每个外设的硬件触发控制DMA都有固定对应的DMA通道

通道号越小优先级越高

image

总结的表

image

数据宽度与对齐

如果目标的数据宽度比源端的数据宽度大,那就在目标数据多出来的空位补0

如果目标的数据宽度比源端的数据宽度小,就会把多出来的高位舍弃

这种操作就和uint8_t、uint16_t、uint32_t的变量互相转换是一样的

image

DMA相关库函数

DMA_InitType

typedef struct
{
  uint32_t DMA_PeripheralBaseAddr; /*!< 指定要进行DMA转运的外设站点地址 */
  uint32_t DMA_MemoryBaseAddr;     /*!< 指定要进行DMA转运的存储器站点地址 */
  uint32_t DMA_DIR;                /*!< 指定外设站点是目标端还是发送端 */
  uint32_t DMA_BufferSize;         /*!< 传输计数器的初值,也就是要转运的次数 */
  uint32_t DMA_PeripheralInc;      /*!< DMA每转运一次外设站点的地址是否自增 */
  uint32_t DMA_MemoryInc;          /*!< DMA每转运一次存储器站点的地址是否自增 */
  uint32_t DMA_PeripheralDataSize; /*!< 外设站点的数据宽度 */
  uint32_t DMA_MemoryDataSize;     /*!< 存储器站点的数据宽度 */
  uint32_t DMA_Mode;               /*!< DMA的工作模式,是否启用自动重装器 */
  uint32_t DMA_Priority;           /*!< 指定通道的优先级 */
  uint32_t DMA_M2M;                /*!< 软件触发还是硬件触发 */
}DMA_InitTypeDef;

/* DMA_PeripheralBaseAddr */
外设站点地址。注意要将数据类型强转为:uint32_t

/* DMA_MemoryBaseAddr */
存储器站点地址。注意要将数据类型强转为:uint32_t

/* DMA_DIR */
DMA_DIR_PeripheralDST	// 外设站点作为目的地
DMA_DIR_PeripheralSRC	// 外设站点作为数据源

/* DMA_BufferSize */
0~65535		// 要转运的次数

/* DMA_PeripheralInc */
DMA_PeripheralInc_Enable	// 每次转运后外设站点的地址自增
DMA_PeripheralInc_Disable	// 每次转运后外设站点的地址不自增

/* DMA_MemoryInc */
DMA_MemoryInc_Enable	// 每次转运后存储器站点的地址自增
DMA_MemoryInc_Disable	// 每次转运后存储器站点的地址不自增

/* DMA_PeripheralDataSize */
DMA_PeripheralDataSize_Byte			// 外设站点的数据宽度为8位
DMA_PeripheralDataSize_HalfWord		// 外设站点的数据宽度为16位
DMA_PeripheralDataSize_Word			// 外设站点的数据宽度为32位

/* DMA_MemoryDataSize */
DMA_MemoryDataSize_Byte			// 存储器站点的数据宽度为8位
DMA_MemoryDataSize_HalfWord		// 存储器站点的数据宽度为16位
DMA_MemoryDataSize_Word			// 存储器站点的数据宽度为32位

/* DMA_Mode */
DMA_Mode_Circular	// 启用自动重装器,循环模式。注意该模式不能与软件触发一起使用
DMA_Mode_Normal		// 不启用自动重装器,单次模式

/* DMA_Priority */
DMA_Priority_VeryHigh	// 很高的优先级
DMA_Priority_High		// 高的优先级
DMA_Priority_Medium		// 一般优先级
DMA_Priority_Low		// 低的优先级

/* DMA_M2M */
DMA_M2M_Enable		// 软件触发。注意该触发方式不能与循环模式一起使用
DMA_M2M_Disable		// 硬件触发

函数

// 重置DMA配置
void DMA_DeInit(DMA_Channel_TypeDef* DMAy_Channelx);

// 使用DMA_InitTypeDef结构体初始化DMA
void DMA_Init(DMA_Channel_TypeDef* DMAy_Channelx, DMA_InitTypeDef* DMA_InitStruct);

// 使用默认值填充DMA_InitTypeDef的属性
void DMA_StructInit(DMA_InitTypeDef* DMA_InitStruct);

// 开启和关闭DMA通道
void DMA_Cmd(DMA_Channel_TypeDef* DMAy_Channelx, FunctionalState NewState);

// 启用或禁用DMA通道的中断
void DMA_ITConfig(DMA_Channel_TypeDef* DMAy_Channelx, uint32_t DMA_IT, FunctionalState NewState);

// 单独设置传输计数器的值
void DMA_SetCurrDataCounter(DMA_Channel_TypeDef* DMAy_Channelx, uint16_t DataNumber); 

// 获取传输计数器当前的值
uint16_t DMA_GetCurrDataCounter(DMA_Channel_TypeDef* DMAy_Channelx);

// 检测DMA通道中指定的标志位
FlagStatus DMA_GetFlagStatus(uint32_t DMAy_FLAG);

// 清除DMA通道中指定的标志位
void DMA_ClearFlag(uint32_t DMAy_FLAG);

// 检测DMA通道的是否发生中断
ITStatus DMA_GetITStatus(uint32_t DMAy_IT);

// 清除DMA通道的中断标志位
void DMA_ClearITPendingBit(uint32_t DMAy_IT);

案例

DMA转运存储器数据

用到的函数

// 使用DMA_InitTypeDef结构体初始化DMA
void DMA_Init(DMA_Channel_TypeDef* DMAy_Channelx, DMA_InitTypeDef* DMA_InitStruct);

// 开启和关闭DMA通道
void DMA_Cmd(DMA_Channel_TypeDef* DMAy_Channelx, FunctionalState NewState);

// 单独设置传输计数器的值
void DMA_SetCurrDataCounter(DMA_Channel_TypeDef* DMAy_Channelx, uint16_t DataNumber); 

// 检测DMA通道中指定的标志位
FlagStatus DMA_GetFlagStatus(uint32_t DMAy_FLAG);

// 清除DMA通道中指定的标志位
void DMA_ClearFlag(uint32_t DMAy_FLAG);

接线图

image

示例代码

#include "stm32f10x.h"                  // Device header
#include "OLED.h"
#include "Delay.h"

/**
  * @brief  初始化DMA1的通道1.软件触发、不使用自动重装器,单次模式、外设端作为目标端、使用通道1
  * @param  Peripheral: 外设端的地址
  * @param  Memory: 存储器端的地址
  * @param  BufferSize: 转运的次数,写入传输计数器的初始值
  * @retval 无
  */
void MyDMA_Init(uint32_t Peripheral, uint32_t Memory, uint8_t BufferSize)
{
	// 开启属于AHB的MDA1的时钟
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
	
	// 初始化DMA1的通道1。因为是软件触发,所以DMA的通道可以随便选
	DMA_InitTypeDef DMA_InitStruct;
	DMA_InitStruct.DMA_MemoryBaseAddr = Memory;								// 外设端的地址
	DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;			// 外设端的数据宽度:8位
	DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable;					// 外设端每转运一次地址自增一次
	DMA_InitStruct.DMA_PeripheralBaseAddr = Peripheral;						// 存储器端的地址
	DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;	// 存储器端的数据宽度:8位
	DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Enable;			// 存储器端每转运一次地址自增一次
	DMA_InitStruct.DMA_BufferSize = BufferSize;								// 转运次数
	DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralDST;							// 外设端作为目的端
	DMA_InitStruct.DMA_M2M = DMA_M2M_Enable;								// 软件触发
	DMA_InitStruct.DMA_Mode = DMA_Mode_Normal;								// 不开启自动重装器,单次模式
	DMA_InitStruct.DMA_Priority = DMA_Priority_Medium;						// DMA优先级
	DMA_Init(DMA1_Channel1, &DMA_InitStruct);								// 配置到DMA1的通道1
}

/**
  * @brief  根据DMA初始化的配置开始转运数据
  * @param  无
  * @retval 无
  */
void MyDMA_Transfer()
{
	// 开启DMA,开始转运数据
	DMA_Cmd(DMA1_Channel1, ENABLE);
	// 检测DMA通道的标志位。TC1表示通道1的转运完成标志位,返回SET说明数据全部转运完成
	while(DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET);
	// 清除转运完成标志位
	DMA_ClearFlag(DMA1_FLAG_TC1);
	// DMA失能
	DMA_Cmd(DMA1_Channel1, DISABLE);
	// 重新写入传输计数器的值。注意,必须在DMA失能后再写入该寄存器
	DMA_SetCurrDataCounter(DMA1_Channel1, 4);
}

int main()
{
	uint8_t DataA[4] = {0x01, 0x02, 0x03, 0x04};
	uint8_t DataB[4] = {0, 0, 0, 0};
	
	// 初始化DMA1的通道1
	MyDMA_Init((uint32_t)DataB, (uint32_t)DataA, 4);
	
	OLED_Init();
	while(1)
	{
		// 显示DataA和DataB
		for(uint8_t i = 0;i < 4;i++)
		{
			OLED_ShowString(i + 1, 1, "A[ ]:");
			OLED_ShowNum(i + 1, 3, i, 1);
			OLED_ShowNum(i + 1, 6, DataA[i], 2);
			
			OLED_ShowString(i + 1, 9, "B[ ]:");
			OLED_ShowNum(i + 1, 11, i, 1);
			OLED_ShowNum(i + 1, 14, DataB[i], 2);
		}
		
		Delay_s(1);
		
		// DataA中的所有数据自增
		for(uint8_t i = 0;i < 4;i++)
		{
			DataA[i]++;
		}
		
		for(uint8_t i = 0;i < 4;i++)
		{
			OLED_ShowString(i + 1, 1, "A[ ]:");
			OLED_ShowNum(i + 1, 3, i, 1);
			OLED_ShowNum(i + 1, 6, DataA[i], 2);
			
			OLED_ShowString(i + 1, 9, "B[ ]:");
			OLED_ShowNum(i + 1, 11, i, 1);
			OLED_ShowNum(i + 1, 14, DataB[i], 2);
		}
		
		Delay_s(1);
		
		// DMA转运数据
		MyDMA_Transfer();
	}
}

DMA与ADC多通道的扫描模式配合

接线图

image

示例代码

#include "stm32f10x.h"                  // Device header
#include "OLED.h"
#include "Delay.h"

/**
  * @brief  初始化ADC1所用的DMA通道
  * @param  Peripheral: 外设端的地址
  * @param  Memory: 存储器端的地址
  * @param  BufferSize: 转运的次数,写入传输计数器的初始值
  * @retval 无
  */
void MyDMA_Init(uint32_t Peripheral, uint32_t Memory, uint8_t BufferSize)
{
	// 开启DMA1的时钟
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
	
	// 初始化DMA
	DMA_InitTypeDef DMA_InitStruct;
	DMA_InitStruct.DMA_BufferSize = BufferSize;									// 转运次数
	DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralSRC;								// 外设端为数据源
	DMA_InitStruct.DMA_M2M = DMA_M2M_Disable;									// 硬件触发
	DMA_InitStruct.DMA_MemoryBaseAddr = Memory;									// 存储端地址
	DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;			// 存储端数据宽度
	DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable;						// 存储端地址自增
	DMA_InitStruct.DMA_Mode = DMA_Mode_Circular;								// 循环模式。使用自动重装器
	DMA_InitStruct.DMA_PeripheralBaseAddr = Peripheral;							// 外设端地址
	DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;	// 外设端数据宽度
	DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable;				// 外设端地址不自增
	DMA_InitStruct.DMA_Priority = DMA_Priority_High;							// 高优先级
	DMA_Init(DMA1_Channel1, &DMA_InitStruct);
	
	// 启用DMA1的通道1。根据DMA硬件请求通道可知ADC1的DMA通道是DMA1
	DMA_Cmd(DMA1_Channel1, ENABLE); 
}

/**
  * @brief  初始化ADC1的0~3通道
  * @param  无
  * @retval 无
  */
void MyADC_Init()
{
	// 开启ADC1和GPIOA的时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	
	// 对APB2的时钟频率进行6分频后作为ADC的时钟频率
	RCC_ADCCLKConfig(RCC_PCLK2_Div6);
	
	// 初始化PA0、PA1、PA2、PA3为模拟输入
	GPIO_InitTypeDef GPIO_InitStruct;
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AIN;
	GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3;
	GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStruct);
	
	// 初始化ADC1
	ADC_InitTypeDef ADC_InitStruct;
	ADC_InitStruct.ADC_ContinuousConvMode = ENABLE;						// ADC连续转换模式
	ADC_InitStruct.ADC_DataAlign = ADC_DataAlign_Right;					// 数据右对齐
	ADC_InitStruct.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;	// 使用软件触发
	ADC_InitStruct.ADC_Mode = ADC_Mode_Independent;						// ADC1独立工作模式
	ADC_InitStruct.ADC_NbrOfChannel = 4;								// 扫描模式下每一轮转运的次数
	ADC_InitStruct.ADC_ScanConvMode = ENABLE;							// 扫描模式
	ADC_Init(ADC1, &ADC_InitStruct);
	
	// ADC1开始工作
	ADC_Cmd(ADC1, ENABLE);
	
	// 根据手册要求ADC开始工作后进行校准
	ADC_ResetCalibration(ADC1);
	while(ADC_GetResetCalibrationStatus(ADC1) == SET);
	ADC_StartCalibration(ADC1);
	while(ADC_GetCalibrationStatus(ADC1) == SET);
	
	// 软件触发ADC1的AD转换
	ADC_SoftwareStartConvCmd(ADC1, ENABLE);
	
	// 配置规则组的通道
	ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);		// 将ADC1的通道0放到规则组的序列1上
	ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_55Cycles5);		// 将ADC1的通道1放到规则组的序列2上
	ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 3, ADC_SampleTime_55Cycles5);		// 将ADC1的通道2放到规则组的序列3上
	ADC_RegularChannelConfig(ADC1, ADC_Channel_3, 4, ADC_SampleTime_55Cycles5);		// 将ADC1的通道3放到规则组的序列4上
	
	// 开启ADC1的DMA通道
	ADC_DMACmd(ADC1, ENABLE);
}

// 该数组用于存放ADC转换完成的数据,由DMA搬运。索引0对应通道0,以此类推
uint16_t AOData[4];

int main()
{
	MyADC_Init();
	// ADC规则组数据转换完成后将数据放到叫DR16位数据寄存器中,可以在手册中了解
	MyDMA_Init((uint32_t)&ADC1->DR, (uint32_t)AOData, 4);
	OLED_Init();
	while(1)
	{
		for(uint8_t i = 0; i < 4; i++)
		{
			OLED_ShowNum(i + 1, 1, AOData[i], 5);
		}
	}
}

posted @ 2024-01-29 01:52  7七柒  阅读(18)  评论(0编辑  收藏  举报