DMA直接存储器存取
参考视频:[8-1] DMA直接存储器存取_哔哩哔哩_bilibili
DMA简介
DMA(Direct Memory Access)直接存储器存取
DMA可以提供外设和存储器或者存储器和存储器之间的高速数据传输,无须CPU干预,节省了CPU的资源
12个独立可配置的通道:DMA1(7个通道),DMA2(5个通道)
每个通道都支持软件触发和特定的硬件触发
存储器到存储器的转运:一般使用软件触发
外设到存储器的数据转运:一般使用硬件触发
STM32F103C8T6 DMA资源:DMA1(7个通道)
存储器映像
- ROM是只读存储器,是一种非易失性、掉电不丢失的存储器。
- RAM是随机存储器,是一种易失性、掉电丢失的存储器。
DMA框图
Cortex-M3内核,包含了CPU和内核外设;其余的都可以看成存储器。
Flash是主闪存,SRAM是运行内存。各个外设可以看成是寄存器,也是一种SRAM存储器。
寄存器可以看成是一种特殊的存储器:
-
一方面,CPU可以对寄存器进行读写。
-
另一方面,寄存器的每一位背后,都连接了一根导线,这些导线可以用于控制外设电路的状态,比如置高低电平、导通和断开开关、切换数据选择器等等。
所以寄存器是连接软件和硬件的桥梁。软件读写寄存器,就当于在控制硬件的执行。
总线矩阵的左端,是主动单元,也就是拥有存储器的访问权;右端的是被动单元,它们的存储器只能被左边的主动单元读写。
DCode总线是专门访问Flash的,系统总线是访问其他东西的。
由于DMA要转运数据,DMA也必须要有访问的主动权。
DMA1、DMA2、以太网MAC都有一条DMA总线。
DMA1有七条通道,DMA2有五条通道,各个通道可以分别设置它们转运数据的源地址和目的地址。
仲裁器存在的原因:虽然有多个通道可以独立转运数据,但是最终DMA总线只有一条,所以所有的通道都只能分时复用这一条DMA总线,如果产生了冲突,那就会由仲裁器,根据通道的优先级来决定谁先用谁后用。
另外,在总线矩阵这里也有仲裁器,如果DMA和CPU都要访问同一个目标,那么DMA就会暂停CPU的访问,以防止冲突,不过总线仲裁器,仍然会保证CPU得到一半的总线带宽,保证CPU也会正常的工作。
AHB从设备,也就是DMA自身的寄存器。因为DMA作为一个外设,它自己也会有相应的配置寄存器。AHB从设备连接在了总线右边的AHB总线上。
所以DMA既是总线矩阵的主动单元,可以读写各种存储器,也是AHB总线上的被动单元。
DMA请求,也就是DMA触发,线路右边的触发源是各个外设,所以DMA请求就是DMA的硬件触发源,比如ADC转换完成,串口接收到数据。需要触发DMA转运数据的时候,就会通过这条线路,向DMA发出硬件触发信号,之后DMA就可以执行数据转运的工作了。
DMA基本结构
方向参数是用来控制从外设到存储器,还是从存储器到外设。另外还有一种转运方式,就是存储器到存储器。由于Flash是只读的,所以只能进行从SRAM到SRAM,或者Flash到SRAM。
外设和存储器两个站点,都有三个参数:
起始地址:有外设端的起始地址,存储器端的起始地址。这两个参数决定了数据是从哪里来到哪里去的。
数据宽度:指定一次转运要按多大的数据宽度来进行,可以选择字节、半字和字。字节是8位,半字是16位,字是32位。
地址是否自增:指定一次转运完成后,下一次转运,是不是要把地址移动到下一个位置去。
传输计数器:这个是用来指定总共需要转运几次,是一个自减计数器。当减到0之后,之前自增的地址,也会恢复到起始地址的位置。
自动重装器:传输计数器减到0之后,是否要自动恢复到最初的值。不重装就是单次模式,重装就是循环模式。
触发,就是决定DMA需要在什么时机进行转运的。触发源有硬件触发和软件触发,具体选择哪个由M2M来决定。
给M2M位1时,DMA就会选择软件触发,软件触发是指以最快的速度,连续不断地触发DMA,争取早日把传输计数器清零,完成这一轮的转换(这里的软件触发和之前的ADC和外部中断的软件触发不太一样)。软件触发和循环模式不能同时用。软件触发一般是存储器到存储器的转运。
硬件触发源可以选择ADC、串口、定时器等等。硬件触发源的转运一般都是与外设有关的转运,这些转运需要一定的时机,比如ADC转换完成、串口收到数据、定时时间到等等,在硬件达到这些时机时,传一个信号过来,来触发DMA进行转运。
开关控制,DMA_Cmd函数,使能之后就可以进行DMA转运了。
DMA进行转运的几个条件:
1. 开关控制,DMA_Cmd必须使能。
2. 传输计数器必须大于0。
3. 触发源,必须有触发信号。
触发一次,转运一次,传输计数器自减一次,当传输计数器等于0,且没有自动重装时,这时无论是否触发,DMA都不会再进行DMA转运了。此时就需要DMA_Cmd,给DISABLE,关闭DMA,再为传输计数器写一个大于0的数,再DMA_Cmd,给ENABLE,开启DMA,DMA才能继续工作。注意一下,写传输计数器时,必须要先关闭DMA,再进行。
DMA1请求映像
DMA1的七个通道,每个通道都有一个数据选择器,可以选择硬件触发或者软件触发。EN是开关控制;M2M位是数据选择器的控制位,用于选择是硬件触发还是软件触发。
外设请求信号那里,每个通道的硬件触发源都是不同的,如果需要用ADC1来触发的话,就必须选择通道1,如果需要定时器2的更新事件来触发的话,就必须选择通道2。如果用软件触发,就可以任意选择通道了。
通道1,选择哪个硬件触发源是由对应的外设是否开启了DMA输出来决定的,比如要使用DMA1,那会有个库函数ADC_DMACmd,必须使用这个库函数开启ADC1的这一路输出,它才有效。如果都开启了,理论上3个硬件都可以触发。
之后七个通道进入仲裁器,进行优先级判断,最终产生内部的DMA1请求。
数据宽度与对齐
此部分是为了解决外设寄存器和存储器的数据宽度不一致问题。
当目标宽度比源端宽度大,就在目标数据前面多出来的空位补0。
当目标宽度比源端宽度小,把源端多出来的高位舍弃掉。
数据转运+DMA
首先是外设站点和存储器站点的起始地址、数据宽度、地址是否自增。
在这个例子里,外设地址显然应该填DataA数组的首地址。存储器地址给DataB数组的首地址。
数据宽度,两个数组的类型都是uint8_t,所以数据宽度都是按8位的字节传输。
由下图可以看出,两边地址都应该自增。
方向参数是外设站点转移到存储器站点。
显然要转运7次,所以传输计数器给7,自动重装暂时不需要。
这里选择软件触发,因为是存储器到存储器的转运,不需要等待硬件时机。
最后调用DMA_Cmd,给DMA使能。
ADC扫描模式+DMA
触发一次后,七个通道依次进行AD转换,然后转换结果都放到ADC_DR数据寄存器里面,在每个单独的通道转换完成后,进行一次DMA数据转运,并且目的地址进行自增。
所以DMA的配置:
外设地址写入ADC_DR这个寄存器的地址。存储器的地址,可以在SRAM中定义一个数组ADValue,然后把ADValue的地址当作存储器的地址。
数据宽度,因为ADC_DR和SRAM数组,都是uint16_t的数据,所以数据的宽度都是16位的半字传输。
地址是否自增:显然外设地址不自增,存储器地址自增。
传输方向是外设站点到存储器站点。
传输计数器,通道有7个,所以计数7次,计数器是否自动重装,可以看ADC的配置,如果ADC是单次扫描,那DMA的传输计数器可以不自动重装,转换一轮就停止,如果ADC是连续扫描,那么DMA就可以使用自动重装。在ADC启动下一轮的转换时,DMA也启动下一轮的转运。
触发选择:ADC_DR的值是在ADC单个通道转换完成后才会有效,所以DMA转运的时机,需要和ADC单个通道转换完成同步。所以DMA的触发要选择ADC的硬件触发。ADC扫描模式,在每个单独的通道转换完成后,没有任何标志位,也不会触发中断,但是它会产生DMA请求,去触发DMA转运。
DMA.h中的函数
// 恢复缺省配置
void DMA_DeInit(DMA_Channel_TypeDef* DMAy_Channelx);
// 初始化
void DMA_Init(DMA_Channel_TypeDef* DMAy_Channelx, DMA_InitTypeDef* DMA_InitStruct);
// 结构体初始化
void DMA_StructInit(DMA_InitTypeDef* DMA_InitStruct);
// 使能
void DMA_Cmd(DMA_Channel_TypeDef* DMAy_Channelx, FunctionalState NewState);
// 中断输出使能
void DMA_ITConfig(DMA_Channel_TypeDef* DMAy_Channelx, uint32_t DMA_IT, FunctionalState NewState);
// DMA设置当前数据寄存器
void DMA_SetCurrDataCounter(DMA_Channel_TypeDef* DMAy_Channelx, uint16_t DataNumber);
// 给传输计数器写入数据,之后DMA获取当前数据寄存器
uint16_t DMA_GetCurrDataCounter(DMA_Channel_TypeDef* DMAy_Channelx);
// 获取标志位状态
FlagStatus DMA_GetFlagStatus(uint32_t DMAy_FLAG);
// 清除标志位
void DMA_ClearFlag(uint32_t DMAy_FLAG);
// 获取中断状态
ITStatus DMA_GetITStatus(uint32_t DMAy_IT);
// 清除中断挂起位
void DMA_ClearITPendingBit(uint32_t DMAy_IT);
DMA数据转运
- RCC开启DMA的时钟
- 调用DMA_Init,初始化各个参数
- 进行开关控制,DMA_Cmd
接线图:
MyDMA.c
#include "stm32f10x.h" // Device headeruint16_t MyDMA_Size;void MyDMA_Init(uint32_t AddrA, uint32_t AddrB, uint16_t Size)
{MyDMA_Size = Size;RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);DMA_InitTypeDef DMA_InitStructure;// 外设站点DMA_InitStructure.DMA_PeripheralBaseAddr = AddrA;DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Enable;// 存储器DMA_InitStructure.DMA_MemoryBaseAddr = AddrB;DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;// 传输方向DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;// 缓存区大小,传输计数器DMA_InitStructure.DMA_BufferSize = Size;// 传输模式,是否使用自动重装DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;// 是否是存储器到存储器,选择硬件触发还是软件触发DMA_InitStructure.DMA_M2M = DMA_M2M_Enable;// 优先级DMA_InitStructure.DMA_Priority = DMA_Priority_VeryHigh;DMA_Init(DMA1_Channel1, &DMA_InitStructure);DMA_Cmd(DMA1_Channel1, DISABLE);
}void MyDMA_Transfer(void)
{DMA_Cmd(DMA1_Channel1, DISABLE);DMA_SetCurrDataCounter(DMA1_Channel1, MyDMA_Size);DMA_Cmd(DMA1_Channel1, ENABLE);while (DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET);DMA_ClearFlag(DMA1_FLAG_TC1);
}
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "MyDMA.h"uint8_t DataA[] = {0x01, 0x02, 0x03, 0x04};
uint8_t DataB[] = {0, 0, 0, 0};int main(void)
{OLED_Init();MyDMA_Init((uint32_t)DataA, (uint32_t)DataB, 4);OLED_ShowString(1, 1, "DataA");OLED_ShowString(3, 1, "DataB");OLED_ShowHexNum(1, 8, (uint32_t)DataA, 8);OLED_ShowHexNum(3, 8, (uint32_t)DataB, 8);while(1){DataA[0] ++;DataA[1] ++;DataA[2] ++;DataA[3] ++;OLED_ShowHexNum(2, 1, DataA[0], 2);OLED_ShowHexNum(2, 4, DataA[1], 2);OLED_ShowHexNum(2, 7, DataA[2], 2);OLED_ShowHexNum(2, 10, DataA[3], 2);OLED_ShowHexNum(4, 1, DataB[0], 2);OLED_ShowHexNum(4, 4, DataB[1], 2);OLED_ShowHexNum(4, 7, DataB[2], 2);OLED_ShowHexNum(4, 10, DataB[3], 2);Delay_ms(1000);MyDMA_Transfer();OLED_ShowHexNum(2, 1, DataA[0], 2);OLED_ShowHexNum(2, 4, DataA[1], 2);OLED_ShowHexNum(2, 7, DataA[2], 2);OLED_ShowHexNum(2, 10, DataA[3], 2);OLED_ShowHexNum(4, 1, DataB[0], 2);OLED_ShowHexNum(4, 4, DataB[1], 2);OLED_ShowHexNum(4, 7, DataB[2], 2);OLED_ShowHexNum(4, 10, DataB[3], 2);Delay_ms(1000);}
}
DMA+AD多通道
接线图:
AD.c
#include "stm32f10x.h" // Device headeruint16_t AD_Value[4];void AD_Init(void)
{RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);RCC_ADCCLKConfig(RCC_PCLK2_Div6);GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOA, &GPIO_InitStructure);ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_55Cycles5);ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 3, ADC_SampleTime_55Cycles5);ADC_RegularChannelConfig(ADC1, ADC_Channel_3, 4, ADC_SampleTime_55Cycles5);ADC_InitTypeDef ADC_InitStructure;ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; // 非连续转换模式ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; // 对齐方式ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; // 触发方式ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;ADC_InitStructure.ADC_NbrOfChannel = 4; // 通道数目ADC_InitStructure.ADC_ScanConvMode = DISABLE; // 扫描转换模式ADC_Init(ADC1, &ADC_InitStructure);DMA_InitTypeDef DMA_InitStructure;// 外设站点DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;// 存储器DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)AD_Value;DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;// 传输方向DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;// 缓存区大小,传输计数器DMA_InitStructure.DMA_BufferSize = 4;// 传输模式,是否使用自动重装DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;// 是否是存储器到存储器,选择硬件触发还是软件触发DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;// 优先级DMA_InitStructure.DMA_Priority = DMA_Priority_VeryHigh;DMA_Init(DMA1_Channel1, &DMA_InitStructure);DMA_Cmd(DMA1_Channel1, ENABLE);ADC_DMACmd(ADC1, ENABLE);ADC_Cmd(ADC1, ENABLE);// 调用复位校准ADC_ResetCalibration(ADC1);// 等待复位校准完成,该函数是返回复位校准的状态,所以要等待复位校准完成的话,要加一个while循环while (ADC_GetResetCalibrationStatus(ADC1) == SET) ;// 开始校准ADC_StartCalibration(ADC1);// 等待校准完成while (ADC_GetCalibrationStatus(ADC1) == SET);
}// 首先软件触发,然后等待转换完成,也就是等待EOC标志位置1,最后读取ADC数据寄存器
void AD_GetValue(void)
{DMA_Cmd(DMA1_Channel1, DISABLE);DMA_SetCurrDataCounter(DMA1_Channel1, 4);DMA_Cmd(DMA1_Channel1, ENABLE);ADC_SoftwareStartConvCmd(ADC1, ENABLE);while (DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET);DMA_ClearFlag(DMA1_FLAG_TC1);
}
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "AD.h"int main(void)
{OLED_Init();AD_Init();OLED_ShowString(1, 1, "AD0:");OLED_ShowString(2, 1, "AD1:");OLED_ShowString(3, 1, "AD2:");OLED_ShowString(4, 1, "AD3:");while(1){AD_GetValue();OLED_ShowNum(1, 5, AD_Value[0], 4);OLED_ShowNum(2, 5, AD_Value[1], 4);OLED_ShowNum(3, 5, AD_Value[2], 4);OLED_ShowNum(4, 5, AD_Value[3], 4);Delay_ms(100);}
}