1.相关概念
(1)DMA直接寄存器访问
可实现外设数据寄存器到存储器、存储器到外设数据寄存器、存储器到存储器之间的高效数据传输,无需CPU操作控制。
(2)外设与存储器
外设包括ADC、SPI、I2C、USART等等
存储器包括片内SRAM、外部存储器、片内Flash等等
(3)具体应用场合举例:
ADC采集可以利用DMA将AD转换数据转移到目标存储区,适用于多通道采集、采样频率高、连续传输的ADC采集场合;
将特定存储区的数据转移到外设数据寄存器,用于外设的对外数据传输,如存储器传输数据到串口数据寄存器,串口发送数据到PC端;
存储器到存储器的数据传输利用DMA可以达到更高的传输效率,不占用CPU,从而节约CPU资源。
2.DMA功能框图
(1)①外设通道
STM32F4系列有两个DMA控制器,每个控制器有8个数据流,每个数据流又对应8个数据通道(8个通道对应的是8个外设请求)。
DMA控制器通过DMA数据流x配置寄存器DMA_SxCR(x为0~7,对应8个DMA数据流)的CHSEL[2:0]位选择对应的通道即选择对应的外设。
DMA请求映射表如下:
每个外设请求都占用一个数据流通道,相同外设请求可以占用不同数据流通道。比如SPI3_RX请求,占用DMA1的数据流0的通道0,因此使用该请求时,需要在把DMA_S0CR寄存器的CHSEL[2:0]设置为“000”, 此时相同数据流的其他通道不被选择,处于不可用状态,比如此时不能使用数据流0的通道1即I2C1_RX请求。
(2)②仲裁器
由仲裁器判断哪个数据流优先传输
DMA_SxCR寄存器PL[1:0]位,可以设置为非常高、高、中和低四个级别;
当两个及以上数据流软件设置优先级相同时,比较数据流编号,编号小的优先级高。
(3)③FIFO
每个数据流都独立拥有四级32位FIFO(先入先出存储器缓存区,队列)。DMA传输分为FIFO模式和直接模式。
直接模式:收到外设请求会立即启动传输。
FIFO模式:在数据传输到目标地址前临时存放这些数据,通过DMA数据流xFIFO控制寄存器DMA_SxFCR的FTH[1:0]位来控制FIFO的阈值,分别为1/4、1/2、3/4和满。数据缓冲量达到阈值级别,将FIFO中的数据传输到目标地址中。
FIFO主要用于源地址和目标地址要求数据宽度不同的场合,比如源数据是源源不断的字节数据,而目标地址要求输出字宽度的数据,即在实现数据传输时同时把原来4个8位字节的数据拼凑成一个32位字数据。此时使用FIFO功能先把数据缓存起来,根据需要输出数据。
FIFO还用于突发(burst)传输。
(4)④存储器端口、⑤外设端口
DMA2(DMA控制器2)的存储器端口和外设端口都是连接到AHB总线矩阵,可以使用AHB总线矩阵功能。DMA2存储器和外设端口可以访问相关的内存地址,包括有内部Flash、内部SRAM、AHB1外设、AHB2外设、APB1、APB2外设和外部存储器空间。
DMA1的存储区端口相比DMA2的要减少AHB2外设的访问权,同时DMA1外设端口是没有连接至总线矩阵的,只有连接到APB1外设,所以DMA1不能实现存储器到存储器传输。
3.DMA传输模式
DMA2支持全部三种传输模式;DMA1只支持外设到存储器、存储器到外设两种模式。
(1)模式选择:DMA_SxCR寄存器的DIR[1:0]位,“00”外设到存储器;“10”存储器到存储器;“01”存储器到外设。
(2)传输使能:DMA_SxCR寄存器的EN位置1
(3)数据宽度:DMA_SxCR寄存器的PSIZE[1:0]和MSIZE[1:0]位分别指定外设和存储器数据宽度大小,可以指定为字节(8位)、半字(16位)和字(32位)。直接模式要求外设和寄存器数据宽度一致,该模式下DMA数据流只使用PSIZE,MSIZE被忽略。
(4)地址设置:
①外设地址:DMA_SxPAR寄存器(x为0~7)
②存储器地址:DMA_SxM0AR、DMA_SxM1AR,其中DMA_SxM1AR只用于双缓冲模式。
(5)循环模式与一次模式:一次模式时传输一次就停止传输,下一次传输需要手动控制;循环模式是传输一次后自动按照相同配置重新传输,直至被控制停止或传输错误。循环模式使能是DMA_SxCR寄存器的CIRC位。
(6)单次传输与突发传输:突发传输就是在传输阶段把速度瞬间提高,实现高速传输,在数据传输完成后恢复正常速度,有点类似达到数据块“秒传”效果。为达到这个效果突发传输过程要占用AHB总线,保证要求每个数据项在传输过程不被分割,这样一次性把数据全部传输完才释放AHB总线;而单次传输时必须通过AHB的总线仲裁多次控制才传输完成。
其中PBURST[1:0]和MBURST[1:0]位是位于DMA_SxCR寄存器中的, 用于分别设置外设和存储器不同节拍数的突发传输,对应为单次传输、4个节拍增量传输、8个节拍增量传输和16个节拍增量传输。
PINC位和MINC位是寄存器DMA_SxCR寄存器的第9和第10位,如果位被置1则在每次数据传输后数据地址指针自动递增, 其增量由PSIZE和MSIZE值决定,比如,设置PSIZE为半字大小,那么下一次传输地址将是前一次地址递增2。
突发传输与FIFO密切相关,突发传输需要结合FIFO使用,具体要求FIFO阈值一定要是内存突发传输数据量的整数倍。 FIFO阈值选择和存储器突发大小必须配合使用。
(7)直接模式与双缓冲模式
默认情况下,DMA工作在直接模式,不使能FIFO阈值级别。
①直接模式:每个外设请求都立即启动对存储器传输的单次传输。直接模式要求源地址和目标地址的数据宽度必须一致,所以只有PSIZE控制,而MSIZE值被忽略。突发传输是基于FIFO的所以直接模式不被支持。另外直接模式不能用于存储器到存储器传输。
②双缓冲模式:DMA_SxCR寄存器的DBM位置1可启动双缓冲传输模式,并自动激活循环模式。双缓冲不应用于存储器到存储器的传输。双缓冲模式下,两个存储器地址指针都有效,即DMA_SxM1AR寄存器将被激活使用。开始传输使用DMA_SxM0AR寄存器的地址指针所对应的存储区,当这个存储区数据传输完DMA控制器会自动切换至DMA_SxM1AR寄存器的地址指针所对应的另一块存储区,如果这一块也传输完成就再切换至DMA_SxM0AR寄存器的地址指针所对应的存储区,这样循环调用。
当其中一个存储区传输完成时都会把传输完成中断标志TCIF位置1,如果我们使能了DMA_SxCR寄存器的传输完成中断,则可以产生中断信号。
DMA_SxCR寄存器的CT位,当DMA控制器是在访问使用DMA_SxM0AR时CT=0,此时CPU不能访问DMA_SxM0AR,但可以向DMA_SxM1AR填充或者读取数据;当DMA控制器是在访问使用DMA_SxM1AR时CT=1,此时CPU不能访问DMA_SxM1AR,但可以向DMA_SxM0AR填充或者读取数据。另外在未使能DMA数据流传输时,可以直接写CT位,改变开始传输的目标存储区。
(8)流控制器
在传输之前设置DMA_SxNDTR寄存器为要传输数目值,DMA控制器在传输完这么多数目数据后就可以控制DMA停止传输。
DMA数据流x数据项数DMA_SxNDTR(x为0~7)寄存器用来记录当前仍需要传输数目,是一个16位数据有效寄存器,最大值为65535。编程时一般都会明确指定一个传输数量, 在完成一次数据传输后DMA_SxNDTR计数值就会自减,当达到零时就说明传输完成。
如果某些情况下在传输之前我们无法确定数据的数目,那DMA就无法自动控制传输停止了, 此时需要外设通过硬件通信向DMA控制器发送停止传输信号。只有SDIO可以发出停止传输信号,其他外设不具备此功能。
4.DMA中断
(1)达到半传输:DMA数据传输达到一半时HTIF标志位被置1,如果使能HTIE中断控制位将产生达到半传输中断;
(2)传输完成:DMA数据传输完成时TCIF标志位被置1,如果使能TCIE中断控制位将产生传输完成中断;
(3)传输错误:DMA访问总线发生错误或者在双缓冲模式下试图访问“受限”存储器地址寄存器时TEIF标志位被置1,如果使能TEIE中断控制位将产生传输错误中断;
(4)FIFO错误:发生FIFO下溢或者上溢时FEIF标志位被置1,如果使能FEIE中断控制位将产生FIFO错误中断;
(5)直接模式错误:在外设到存储器的直接模式下,因为存储器总线没得到授权,使得先前数据没有完成被传输到存储器空间上,此时DMEIF标志位被置1,如果使能DMEIE中断控制位将产生直接模式错误中断。
5.DMA初始化结构体
(1)DMA_InitTypeDef初始化结构体
typedef struct {
uint32_t Channel; //通道选择
uint32_t Direction; //传输方向
uint32_t PeriphInc; //外设递增
uint32_t MemInc; //存储器递增
uint32_t PeriphDataAlignment; //外设数据宽度
uint32_t MemDataAlignment; //存储器数据宽度
uint32_t Mode; //模式选择
uint32_t Priority; //优先级
uint32_t FIFOMode; //FIFO模式
uint32_t FIFOThreshold; //FIFO阈值
uint32_t MemBurst; //存储器突发传输
uint32_t PeriphBurst; //外设突发传输
} DMA_InitTypeDef;
①PeriphInc:如果配置为PeriphInc_Enable,即使能外设地址自动递增(设定DMA_SxCR寄存器的PINC位的值)。一般外设只有一个数据寄存器,因此一般不会使能该位。ADC3的数据寄存器地址是固定并且只有一个所以不使能外设地址递增
②MemInc:如果配置为MemInc_Enable,使能存储器地址自动递增功能(设定DMA_SxCR寄存器的MINC位的值);我们自定义的存储区一般都是存放多个数据的,所以使能存储器地址自动递增功能。之前定义了一个包含4个元素的数组用来存放数据,使能存储区地址递增功能,自动把每个通道数据存放到对应数组元素内。
③PeriphDataAlignment:外设数据宽度,可选字节(8位)、半字(16位)和字(32位),它设定DMA_SxCR寄存器的PSIZE[1:0]位的值。ADC数据寄存器只有低16位数据有效,使用半字数据宽度。
④MemDataAlignment:存储器数据宽度,可选字节(8位)、半字(16位)和字(32位),它设定DMA_SxCR寄存器的MSIZE[1:0]位的值。保存ADC转换数据也要使用半字数据宽度,这跟我们定义的数组是相对应的。
⑤Mode:DMA传输模式选择,可选一次传输或者循环传输,它设定DMA_SxCR寄存器的CIRC位的值。我们希望ADC采集是持续循环进行的,所以使用循环传输模式。
⑥FIFOMode:FIFO模式使能,如果设置为DMA_FIFOMode_Enable表示使能FIFO模式功能(设定DMA_SxFCR寄存器的DMDIS位)。ADC采集传输使用直接传输模式即可,不需要使用FIFO模式。
⑦MemBurst:存储器突发模式选择,可选单次模式、4节拍的增量突发模式、8节拍的增量突发模式或16节拍的增量突发模式,它设定DMA_SxCR寄存器的MBURST[1:0]位的值。ADC采集传输是直接模式,要求使用单次模式。
⑧PeriphBurst:外设突发模式选择,可选单次模式、4节拍的增量突发模式、8节拍的增量突发模式或16节拍的增量突发模式,它设定DMA_SxCR寄存器的PBURST[1:0]位的值。ADC采集传输是直接模式,要求使用单次模式。
(2)DMA_HandleTypeDef初始化结构体
typedef struct __DMA_HandleTypeDef {
DMA_Stream_TypeDef *Instance; //注册基地址
DMA_InitTypeDef Init; //DMA通信参数
HAL_LockTypeDef Lock; //DMA锁定对象
__IO HAL_DMA_StateTypeDef State; //DMA传输状态
void *Parent; //父类指针
void (* XferCpltCallback)( struct __DMA_HandleTypeDef * hdma);
//DMA传输完成回调函数
void (* XferHalfCpltCallback)( struct __DMA_HandleTypeDef * hdma);
//DMA传输完成一半回调函数
void (* XferM1CpltCallback)( struct __DMA_HandleTypeDef * hdma);
//Memory1 DMA传输完成回调函数
void (* XferM1HalfCpltCallback)( struct __DMA_HandleTypeDef * hdma);
//Memory1 DMA传输完成一半回调函数
void (* XferErrorCallback)( struct __DMA_HandleTypeDef * hdma);
//DMA传输错误回调函数
void (* XferAbortCallback)( struct __DMA_HandleTypeDef * hdma);
//DMA传输中止回调函数
__IO uint32_t ErrorCode; //DMA错误码
uint32_t StreamBaseAddress; //DMA数据流基地址
uint32_t StreamIndex; //DMA数据流索引
} DMA_HandleTypeDef;
①Instance: 指向DMA数据流基地址的指针,即指定使用哪个DMA数据流。可选数据流0至数据流7。 例如,我们使用模拟数字转换器ADC3规则采集4个输入通道的电压数据, 查表可知可以使用数据流0或者数据流1,这里支持两个数据流是为了避免多个通道使用时发生冲突,提供备选数据流可选。
②Init:这里包含上面介绍DMA_InitTypeDef结构体的所有参数的初始化。
③Lock:DMA锁定对象。DMA进程锁,通常都在DMA传输设置开始前锁上进程锁,设置完毕后释放进程锁。
④State:DMA传输状态。它包含六种状态,
复位状态,尚未初始化或者失能;
就绪状态,已经完成初始化,随时可以传输数据;
传输忙,DMA传输进程正在进行;
传输超时状态;
传输错误状态;
传输中止状态。
⑤*Parent:父类指针。只要将该指针指向一些ADC、UART等外设的handle类,就等于完成了继承。
⑥传输过程中的回调函数:包括传输完成,传输完成一半,传输错误,传输中止回调函数。这些回调函数中可以加入用户的处理代码。
⑦ErrorCode:DMA错误码,包含
无错误:HAL_DMA_ERROR_NONE,
传输错误:HAL_DMA_ERROR_TE,
FIFO错误:HAL_DMA_ERROR_FE,
直接模式错误:HAL_DMA_ERROR_DME,
超时错误:HAL_DMA_ERROR_TIMEOUT,
参数错误:HAL_DMA_ERROR_PARAM,
没有回调函数正在执行退出请求错误:HAL_DMA_ERROR_NO_XFER,
不支持模式错误:HAL_DMA_ERROR_NOT_SUPPORTED。
⑧StreamBaseAddress:DMA数据流基地址,用来根据定义句柄计算数据流的基地址。
⑨StreamIndex:DMA数据流索引,根据数据流的序号来确定数据流的偏移地址。
6.DMA_MemToMem 存储器到存储器传输
代码实现步骤:
①使能DMA数据流时钟并复位初始化DMA数据流;
②配置DMA数据参数;
③使能DMA数据流,进行传输;
④等待传输完成,并对源数据和目标地址数据进行比较;
存储器到存储器传输必须使用DMA2,但对数据流编号以及通道选择就没有硬性要求,可以自由选择。只能使用一次传输模式不能循环传输。
typedef enum
{
FAILED = 0,
PASSED = !FAILED
} TestStatus;
DMA_HandleTypeDef hdma_memtomem_dma2_stream0;
static const uint32_t SRC_Const_Buffer[32]= {
0x01020304,0x05060708,0x090A0B0C,0x0D0E0F10,
0x11121314,0x15161718,0x191A1B1C,0x1D1E1F20,
0x21222324,0x25262728,0x292A2B2C,0x2D2E2F30,
0x31323334,0x35363738,0x393A3B3C,0x3D3E3F40,
0x41424344,0x45464748,0x494A4B4C,0x4D4E4F50,
0x51525354,0x55565758,0x595A5B5C,0x5D5E5F60,
0x61626364,0x65666768,0x696A6B6C,0x6D6E6F70,
0x71727374,0x75767778,0x797A7B7C,0x7D7E7F80};
uint32_t DST_Buffer[32];
__IO TestStatus TransferStatus= FAILED;
int main(void)
{
HAL_StatusTypeDef har_status;
/* 复位所有外设,初始化Flash接口和系统滴答定时器 */
HAL_Init();
/* 配置系统时钟 */
SystemClock_Config();
/* 板载LED初始化 */
LED_GPIO_Init();
/* DMA初始化 */
MX_DMA_Init();
//开始DMA传输
har_status=HAL_DMA_Start(&hdma_memtomem_dma2_stream0,(uint32_t)&SRC_Const_Buffer,(uint32_t)&DST_Buffer,32);
if(har_status==HAL_OK)
{
/* 检查发送和接收的数据是否相等 */
TransferStatus = Buffercmp(SRC_Const_Buffer, DST_Buffer, 32);
/* 如果接收和发送的数据都是相同的,则通过 */
if(TransferStatus == 1)
{
LED1_ON;
}
/* 如果接收和发送的数据不同,则传输出错 */
else
{
LED2_ON;
}
}
else
{
LED3_ON;
}
/* 无限循环 */
while (1)
{
}
}
/**
* 函数功能: 判断指定长度的两个数据源是否完全相等
* 输入参数: 无
* 返 回 值: 无
* 说 明: 如果完全相等返回1,只要其中一对数据不相等返回0
*/
uint8_t Buffercmp(const uint32_t* pBuffer,
uint32_t* pBuffer1, uint16_t BufferLength)
{
/* 数据长度递减 */
while (BufferLength--) {
/* 判断两个数据源是否对应相等 */
if (*pBuffer != *pBuffer1) {
/* 对应数据源不相等马上退出函数,并返回0 */
return 0;
}
/* 递增两个数据源的地址指针 */
pBuffer++;
pBuffer1++;
}
/* 完成判断并且对应数据相对 */
return 1;
}
/**
* 函数功能: DMA配置
* 输入参数: 无
* 返 回 值: 无
* 说 明: 无
*/
void MX_DMA_Init(void)
{
/* 使能DMA控制器时钟 */
__HAL_RCC_DMA2_CLK_ENABLE();
/* 配置DMA通道工作方式 */
hdma_memtomem_dma2_stream0.Instance = DMA2_Stream0;//数据流0
hdma_memtomem_dma2_stream0.Init.Channel = DMA_CHANNEL_0;//通道0
hdma_memtomem_dma2_stream0.Init.Direction = DMA_MEMORY_TO_MEMORY;//数据传输方向:存储器->存储器
hdma_memtomem_dma2_stream0.Init.PeriphInc = DMA_PINC_ENABLE;//外设地址不变
hdma_memtomem_dma2_stream0.Init.MemInc = DMA_MINC_ENABLE;//内存地址自增
hdma_memtomem_dma2_stream0.Init.PeriphDataAlignment = DMA_PDATAALIGN_WORD;//传输数据半字16位
hdma_memtomem_dma2_stream0.Init.MemDataAlignment = DMA_MDATAALIGN_WORD;//接收数据半字16位
hdma_memtomem_dma2_stream0.Init.Mode = DMA_NORMAL;//发一次开启一次
hdma_memtomem_dma2_stream0.Init.Priority = DMA_PRIORITY_HIGH;//优先级(设置为最高优先级)
hdma_memtomem_dma2_stream0.Init.FIFOMode = DMA_FIFOMODE_DISABLE;
hdma_memtomem_dma2_stream0.Init.FIFOThreshold = DMA_FIFO_THRESHOLD_FULL;
hdma_memtomem_dma2_stream0.Init.MemBurst = DMA_MBURST_SINGLE;
hdma_memtomem_dma2_stream0.Init.PeriphBurst = DMA_PBURST_SINGLE;
HAL_DMA_Init(&hdma_memtomem_dma2_stream0);
}
7.DMA串口收发数据
//main.c
#define SEND_BUF_SIZE (8800) //发送数据长度,最好等于sizeof(TEXT_TO_SEND)+2的整数倍.
u8 SendBuff[SEND_BUF_SIZE]; //发送数据缓冲区
const u8 TEXT_TO_SEND[] = {"ALIENTEK Pandora STM32L4 IOT DMA 串口实验"};//显示出来是42 汉字1Byte,空格2Byte,字母数字1Byte 再加上\n\r 共44个Bytes
//使用了const关键字修饰,即常量类型,使得变量存储在内部flash空间上。
int main(void)
{
u8 t = 0, j, mask;
u16 i;
float pro = 0; //进度
HAL_Init();
SystemClock_Config(); //初始化系统时钟为80M
delay_init(80); //初始化延时函数 80M系统时钟
uart_init(115200); //初始化串口,波特率为115200
LED_Init(); //初始化LED
KEY_Init(); //初始化按键
LCD_Init(); //初始化LCD
MYDMA_Config(DMA1_Channel4, DMA_REQUEST_2); //初始化DMA
j = sizeof(TEXT_TO_SEND);
for(i = 0; i < SEND_BUF_SIZE; i++) //填充ASCII字符集数据 一共8800Bytes
{
if(t >= j) //加入换行符
{
if(mask)
{
SendBuff[i] = 0x0a;
t = 0;
}
else
{
SendBuff[i] = 0x0d;
mask++;
}
}
else //复制TEXT_TO_SEND语句
{
mask = 0;
SendBuff[i] = TEXT_TO_SEND[t];
t++;
}
}
while(1)
{
t = KEY_Scan(0);
if(t == KEY0_PRES) //KEY0按下
{
HAL_UART_Transmit_DMA(&UART1_Handler, (u8*)SendBuff, SEND_BUF_SIZE); //启动传输
//使能串口1的DMA发送 //等待DMA传输完成,此时我们来做另外一些事,点灯
//实际应用中,传输数据期间,可以执行另外的任务
while(1)
{ //DMA_HandleTypeDef UART1TxDMA_Handler; //DMA句柄
if(__HAL_DMA_GET_FLAG(&UART1TxDMA_Handler, DMA_FLAG_TC4)) //等待DMA2_Steam7传输完成
{
__HAL_DMA_CLEAR_FLAG(&UART1TxDMA_Handler, DMA_FLAG_TC4); //清除DMA2_Steam7传输完成标志
HAL_UART_DMAStop(&UART1_Handler); //传输完成以后关闭串口DMA
break;
}
pro = __HAL_DMA_GET_COUNTER(&UART1TxDMA_Handler); //返回当前DMA通道传输中剩余数据单元的数量
pro = 1 - pro / SEND_BUF_SIZE; //得到百分比
pro *= 100; //扩大100倍
}
}
delay_ms(10);
}
}
//dma.c
/**
* @brief DMAx的各通道配置,这里的传输形式是固定的,这点要根据不同的情况来修改
* 从存储器->外设模式/8位数据宽度/存储器增量模式
*
* @param DMA_Streamx DMA通道选择,DMA1_Channel0~7/DMA2_Channel0~7
* @param chx DMA请求选择,@ref DMA_REQUEST_0~DMA_REQUEST_7
*
* @return void
*/
void MYDMA_Config(DMA_Channel_TypeDef *DMA_Channel, u32 seq)
{
if((u32)DMA_Channel > (u32)DMA2) //得到当前stream是属于DMA2还是DMA1
{
__HAL_RCC_DMA2_CLK_ENABLE();//DMA2时钟使能
}
else
{
__HAL_RCC_DMA1_CLK_ENABLE();//DMA1时钟使能
}
__HAL_LINKDMA(&UART1_Handler, hdmatx, UART1TxDMA_Handler); //将DMA与USART1联系起来(发送DMA)
//Tx DMA配置
UART1TxDMA_Handler.Instance = DMA_Channel; //通道选择
UART1TxDMA_Handler.Init.Request = seq; //请求选择
UART1TxDMA_Handler.Init.Direction = DMA_MEMORY_TO_PERIPH; //存储器到外设
UART1TxDMA_Handler.Init.PeriphInc = DMA_PINC_DISABLE; //外设非增量模式
UART1TxDMA_Handler.Init.MemInc = DMA_MINC_ENABLE; //存储器增量模式
UART1TxDMA_Handler.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE; //外设数据长度:8位
UART1TxDMA_Handler.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE; //存储器数据长度:8位
UART1TxDMA_Handler.Init.Mode = DMA_NORMAL; //外设普通模式
UART1TxDMA_Handler.Init.Priority = DMA_PRIORITY_HIGH; //中等优先级
HAL_DMA_DeInit(&UART1TxDMA_Handler);
HAL_DMA_Init(&UART1TxDMA_Handler);
}
未完待续。。。。。。。。。。。。。。。。。。。。。