1、基本概念
全称是Direct Memory Access,中文意思为直接存储器访问。DMA可用于实现外设与存储器之间或者存储器与存储器之间数据传输的高效性。之所以称为高效,是因为DMA传输数据移动过程无需CPU直接操作,这样节省的 CPU 资源就可供其它操作使用。从硬件层面来理解,DMA就好像是RAM与I/O设备间数据传输的通路,外设与存储器之间或者存储器与存储器之间可以直接在这条通路上进行数据传输。这里说的外设一般指外设的数据寄存器,比如ADC、 SPI、 I2C等外设的数据寄存器,存储器一般是指片内SRAM、外部存储器、片内 Flash 等。
利用DMA获取数据的流程:
① UART控制器读取外设发送的数据
② DMA硬件上自动读取UART数据寄存器的数据
③ DMA硬件上自动将获取的数据搬移到内存中
④ UART控制器给CPU发送中断信号通知CPU数据读取完毕
⑤ CPU可以访问内存中的数据
利用DMA发送数据的流程
① CPU将数据写入指定的内存中
② DMA硬件上自动从指定的内存中获取要发送的数据
③ DMA硬件上自动将数据搬移到数据寄存器中
④ UART控制器硬件上自动将数据发送出去
⑤ DMA给CPU发送中断信号通知数据发送完毕
结论:CPU无需频繁的访问数据寄存器,CPU只关心内存
STM32F103有2个DMA控制器,分别是DMA1和DMA2
- DMA1 有 7 个通道
- DMA2 有 5个通道
- 每个通道专门用来管理来自于一个或多个外设对存储器访问的请求
- 在同一个DMA模块上,多个请求间的优先权可以通过软件编程设置(共有四级:很高、高、中等和低),如果2个请求有相同的软件优先级,则较低编号的通道比较高编号的通道有较高的优先权。
通道2优先于通道4
2、使用DMA实现串口的传输
2.1 使用dma发送数据
在system目录下,新建DMA目录,打开keil工程,添加stm32f10x_dma.c,新建dma.c和dma.h文件
编辑dma.h
#ifndef __DMA_H_
#define __DMA_H_
#include "stm32f10x.h"
// 定义发送缓冲区的长度
#define UART1DMA_TXBUFF_SIZE 2048
typedef struct {
u8 UART1DMA_TxBuff[UART1DMA_TXBUFF_SIZE]; // 发送缓存区
int UART1DMA_TxCounter; // 记录要发送的数据下标
int Tx_Flag; // 发送完成标志
} UART1DMA_TX_DATA;
extern UART1DMA_TX_DATA UART1DMA_Tx_Data;
// 函数声明
void UART1_DMA_Init();// 初始化函数
void UART1_DMA_Tx_Test(void);// 发送测试函数
#endif
编辑dma.c
#include "dma.h"
#include "stdio.h"
// 定义发送缓冲区的数据,并初始化
UART1DMA_TX_DATA UART1DMA_Tx_Data ={{0},0,0};
void UART1_DMA_Init(){// 初始化函数
// 打开DMA1的时钟
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);
// 配置DMA1的通道四
DMA_InitTypeDef DMA_Config;
DMA_Config.DMA_DIR = DMA_DIR_PeripheralDST;// 外设作为数据传输的目的地
DMA_Config.DMA_PeripheralBaseAddr=(u32) &(USART1->DR);// 外设地址
DMA_Config.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; // 数据宽度为8位
DMA_Config.DMA_PeripheralInc = DMA_PeripheralInc_Disable;// 外设地址寄存器不变
DMA_Config.DMA_MemoryBaseAddr = (u32)(UART1DMA_Tx_Data.UART1DMA_TxBuff);// 内存地址
DMA_Config.DMA_MemoryInc = DMA_MemoryInc_Enable;// 内存地址递增
DMA_Config.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; // 数据宽度为8位
DMA_Config.DMA_Mode = DMA_Mode_Normal;// DMA_Mode_Normal 正常缓存模式 DMA_Mode_Circular 循环缓存模式
// 在普通模式在,当一次DMA数据传输完后,需要先关闭DMA,然后重新指定要发送的数据长度,然后在开启DMA 在循环模式中,当传输一次后,重新接着传送,永不停息
DMA_Config.DMA_BufferSize = UART1DMA_TXBUFF_SIZE;// 内存缓冲区的大小
DMA_Config.DMA_Priority = DMA_Priority_Medium;// DMA通道拥有中优先级
DMA_Config.DMA_M2M = DMA_M2M_Disable;// 内存之间的拷贝禁用
// 将发送缓冲区的数据搬送到串口1的DR寄存器中
// 利用DMA1的通道4
DMA_Init(DMA1_Channel4,&DMA_Config);
}
void UART1_DMA_Tx_Test(void){// 发送测试函数
// 初始化内存缓冲区
for(int i=0;i<UART1DMA_TXBUFF_SIZE;i++){
UART1DMA_Tx_Data.UART1DMA_TxBuff[i]='A'+(i%24);
}
UART1DMA_Tx_Data.Tx_Flag = 0;
// 2、关闭DMA1通道4
DMA_Cmd(DMA1_Channel4,DISABLE);
// 3、配置串口1支持DMA发送
USART_DMACmd(USART1,USART_DMAReq_Tx,ENABLE);
// 4、指定要发送的数据长度
DMA_SetCurrDataCounter(DMA1_Channel4,UART1DMA_TXBUFF_SIZE);
// 5、打开通道4,一旦打开了通道4,数据将从内存-> 寄存器->TX
DMA_Cmd(DMA1_Channel4,ENABLE);
// 6、循环等待,判断数据是否发送完毕
while(1){
if(DMA_GetFlagStatus(DMA1_FLAG_TC4)!=RESET){
// 发送完成
DMA_ClearFlag(DMA1_FLAG_TC4);// 清除发送完成标志flag
printf("\n DMA1 发送完成 \n");
break;
}
}
}
当我们需要使用中断的方式发送DMA时可以进行如下配置,当dma发送完成时,由dma触发中断,通知cpu已经发送完成
编辑dma.c中的UART1_DMA_Init
void UART1_DMA_Init(){// 初始化函数
...
// 利用DMA1的通道4
DMA_Init(DMA1_Channel4,&DMA_Config);
DMA_ITConfig(DMA1_Channel4,DMA_IT_TC,ENABLE);
NVIC_InitTypeDef NVIC_Config;
NVIC_Config.NVIC_IRQChannel = DMA1_Channel4_IRQn;
NVIC_Config.NVIC_IRQChannelCmd = ENABLE;
NVIC_Config.NVIC_IRQChannelPreemptionPriority = 2;
NVIC_Config.NVIC_IRQChannelSubPriority=2;
NVIC_Init(&NVIC_Config);
}
// 中断函数
void DMA1_Channel4_IRQHandler(void){
// 判断是否是DMA1通道4触发的中断
if(DMA_GetITStatus(DMA1_IT_TC4)==SET){
// 清除中断到来位
DMA_ClearITPendingBit(DMA1_IT_TC4);
UART1DMA_Tx_Data.Tx_Flag = 1;
// 关闭DMA1的通道
DMA_Cmd(DMA1_Channel4,DISABLE);
}
}
void UART1_DMA_Tx_Test(void){// 发送测试函数
// 初始化内存缓冲区
for(int i=0;i<UART1DMA_TXBUFF_SIZE;i++){
UART1DMA_Tx_Data.UART1DMA_TxBuff[i]='A'+(i%24);
}
UART1DMA_Tx_Data.Tx_Flag = 0;
// 2、关闭DMA1通道4
DMA_Cmd(DMA1_Channel4,DISABLE);
// 3、配置串口1支持DMA发送
USART_DMACmd(USART1,USART_DMAReq_Tx,ENABLE);
// 4、指定要发送的数据长度
DMA_SetCurrDataCounter(DMA1_Channel4,UART1DMA_TXBUFF_SIZE);
// 5、打开通道4,一旦打开了通道4,数据将从内存-> 寄存器->TX
DMA_Cmd(DMA1_Channel4,ENABLE);
// 6、循环等待,判断数据是否发送完毕
while(1){
if(UART1DMA_Tx_Data.Tx_Flag == 1){
printf("\n DMA1 中断发送完成 \n");
UART1DMA_Tx_Data.Tx_Flag=0;
break;
}
}
}
2.2 使用dma接收数据
如果数据接收完成,串口触发中断给cpu核 USART ->DR接收 -> 接收缓存区
编辑uart.c,完成对串口的配置,这里使用总线空闲中断,在两次接收数据之间,当总线空闲时触发的中断,配置串口支持DMA接收
void UART_Init(void){ // 初始化函数
// 打开USER的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
// TX PA9
GPIO_InitTypeDef gpio_init;
gpio_init.GPIO_Mode=GPIO_Mode_AF_PP; // 推挽复用输出
gpio_init.GPIO_Pin=GPIO_Pin_9;
gpio_init.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&gpio_init);
// RX PA10
gpio_init.GPIO_Mode=GPIO_Mode_IN_FLOATING; //配置为浮空输入或者上拉输入
gpio_init.GPIO_Pin=GPIO_Pin_10;
GPIO_Init(GPIOA,&gpio_init);
// 配置uart
USART_InitTypeDef usart_init;
usart_init.USART_BaudRate=115200; // 波特率
usart_init.USART_Mode=USART_Mode_Rx|USART_Mode_Tx;
usart_init.USART_WordLength=USART_WordLength_8b;// 数据位长度
usart_init.USART_StopBits=USART_StopBits_1; // 停止位
usart_init.USART_Parity=USART_Parity_No;// 不校验
usart_init.USART_HardwareFlowControl=USART_HardwareFlowControl_None;// 硬件流控制
USART_Init(USART1,&usart_init);
// 配置总线空闲中断
USART_ITConfig(USART1,USART_IT_IDLE,ENABLE);
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority =2;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
// 使能
USART_Cmd(USART1,ENABLE);
// 配置串口1支持DMA接收
USART_DMACmd(USART1,USART_DMAReq_Rx,ENABLE);
}
编辑中断函数,完成对串口总线空闲中断的功能函数
void USART1_IRQHandler(void){
// 接收到一帧数据时触发
if(USART_GetITStatus(USART1,USART_IT_IDLE) != RESET){
// 清除中断到来位
USART1->SR;
USART1->DR;
// 读取数据到接收缓冲区 "led on \r\n\0"
UART1DMA_Rx_Data.UART1DMA_RxCounter = UART1DMA_RXBUFF_SIZE - DMA_GetCurrDataCounter(DMA1_Channel5);// 数据长度总数 - 当前通道空闲数
UART1DMA_Rx_Data.UART1DMA_RxBuff[UART1DMA_Rx_Data.UART1DMA_RxCounter-2]='\0';
UART1DMA_Rx_Data.Rx_Flag = 1; // 接收完成标志位
// 关闭DMA1通道五,设置长度,打开DMA1的通道5
DMA_Cmd(DMA1_Channel5,DISABLE);
DMA_SetCurrDataCounter(DMA1_Channel5,UART1DMA_RXBUFF_SIZE);
DMA_Cmd(DMA1_Channel5,ENABLE);
}
}
编辑dma.h
#ifndef __DMA_H_
#define __DMA_H_
#include "stm32f10x.h"
// 定义接收缓冲区的长度
#define UART1DMA_RXBUFF_SIZE 2048
typedef struct {
u8 UART1DMA_RxBuff[UART1DMA_RXBUFF_SIZE]; // 接收缓存区
int UART1DMA_RxCounter; // 记录接收的数据下标
int Rx_Flag; // 发送完成标志
} UART1DMA_RX_DATA;
extern UART1DMA_RX_DATA UART1DMA_Rx_Data;
void UART1_DMA_RX_Init(void);
#endif
编辑dma.c
// 定义接收缓冲区的数据,并初始化
UART1DMA_RX_DATA UART1DMA_Rx_Data ={{0},0,0};
void UART1_DMA_RX_Init(){
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);
// 配置DMA1通道5
DMA_InitTypeDef DMA_Config;
DMA_Config.DMA_BufferSize = UART1DMA_RXBUFF_SIZE;
DMA_Config.DMA_DIR = DMA_DIR_PeripheralSRC;
DMA_Config.DMA_M2M = DMA_M2M_Disable;
DMA_Config.DMA_MemoryBaseAddr = (u32) UART1DMA_Rx_Data.UART1DMA_RxBuff;
DMA_Config.DMA_MemoryDataSize=DMA_MemoryDataSize_Byte;
DMA_Config.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_Config.DMA_Mode = DMA_Mode_Normal;
DMA_Config.DMA_PeripheralBaseAddr = (u32)&(USART1->DR);
DMA_Config.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
DMA_Config.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_Config.DMA_Priority = DMA_Priority_Medium;
// 将串口1的DR寄存器的数据搬到UART1DMA_RxBuff数组中
DMA_Init(DMA1_Channel5,&DMA_Config);
// 启动dma1通道5的功能
DMA_Cmd(DMA1_Channel5,ENABLE);
}