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个通道)
存储器映像
查看内存时,地址以0x0800开头的数据基本都是编写的C语音代码
地址以0x2000开头的是程序运行过程中产生的变量等
DMA框图
注意:Flash部分是只读的,如果将Flash存储器作为转运数据的目标地址则会出错
如果需要对Flash进行写入就需要配置Flash阀门控制器
DMA基本结构
起始地址:决定数据从哪来到哪去
数据宽度:指定每次转运时的位数。
- 字节Byte:8位
- 半字HalfWord:16位
- 字Word:32位
地址是否自增:转运了一次数据后是否要对地址自增,自增就转运下一个地址的数据,不自增就还是转运当前地址的数据。相当于指针自增
方向:是从外设转运数据到存储器还是从存储器转运到外设
传输计数器:用于记录总共要转运几次数据的,是一个自减寄存器,每转运一次就减一,减到0之后就不在转运,并且转运的地址也会回到一开始的地址
自动重装器:当计数器减到0后,恢复计数器的初始值。不使用自动重装时就是单次模式,使用自动重装就是循环模式
M2M:DMA的触发控制。当M2M为1时,软件触发;当M2M为0时硬件触发
- 软件触发:以最快的速度连续不断的触发DMA,快速清零计数器完成转换。注意不能和自动重装器一起使用,否则会DMA会一直转运数据,停不下来。适用于存储器到存储器的转运
- 硬件触发:与外设有关,需要一定时机来触发。例如ADC转换完成、串口收到数据、定时时间到了
DMA开始转运数据的条件
- DMA使能
- 传输计数器必须大于0(注意:写传输寄存器的值时,必须要关闭DMA再进行)
- 有触发源
DMA硬件请求通道
每个外设的硬件触发控制DMA都有固定对应的DMA通道
通道号越小优先级越高
总结的表
数据宽度与对齐
如果目标的数据宽度比源端的数据宽度大,那就在目标数据多出来的空位补0
如果目标的数据宽度比源端的数据宽度小,就会把多出来的高位舍弃
这种操作就和uint8_t、uint16_t、uint32_t的变量互相转换是一样的
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);
接线图
示例代码
#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多通道的扫描模式配合
接线图
示例代码
#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);
}
}
}