概念
全称是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可以访问内存中的数据
总结:CPU无需频繁的访问数据寄存器,CPU只关心内存
利用DMA发送数据的流程
- CPU将数据写入指定的内存中
- DMA硬件上自动从指定的内存中获取要发送的数据
- DMA硬件上自动将数据搬移到数据寄存器中
- UART控制器硬件上自动将数据发送出去
- DMA给CPU发送中断信号通知数据发送完毕
结论:CPU无需频繁的访问数据寄存器,CPU只关心内存
DMA的特性
- STM32F103有 2 个 DMA 控制器,分别是DMA1和DMA2
- DMA1 有 7 个通道
- DMA2 有 5个通道
- 每个通道专门用来管理来自于一个或多个外设对存储器访问的请求
在同一个DMA模块上,多个请求间的优先权可以通过软件编程设置(共有四级:很高、高、中等和低),如果2个请求有相同的软件优先级,则较低编号的通道比较高编号的通道有较高的优先权。举个例子,通道2优先于通道4
代码实现
标准库函数和结构体
typedef struct
{
uint32_t DMA_PeripheralBaseAddr;
uint32_t DMA_MemoryBaseAddr;
uint32_t DMA_DIR;
uint32_t DMA_BufferSize;
uint32_t DMA_PeripheralInc;
uint32_t DMA_MemoryInc;
uint32_t DMA_PeripheralDataSize;
uint32_t DMA_MemoryDataSize;
uint32_t DMA_Mode;
uint32_t DMA_Priority;
uint32_t DMA_M2M;
}DMA_InitTypeDef;
初始化
采用DMA发送数据完成后,使用中断发送给CPU结束信号,需要匹配中断和DMA已经NVIC控制器
void My_DMA_Init(void)
{
// 1.打开DMA1控制器时钟
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
// 2.配置DMA1通道4作为串口1发送 : 内存 -> 寄存器
DMA_InitTypeDef DMA_Config;
DMA_Config.DMA_MemoryBaseAddr = (u32)UART1DMA_TxBuff; // 内存缓冲区首地址
DMA_Config.DMA_PeripheralBaseAddr = (u32)&USART1->DR; // 寄存器首地址
DMA_Config.DMA_BufferSize = UART1DMA_TXBUFF_SIZE; // 内存缓冲区大小
DMA_Config.DMA_DIR = DMA_DIR_PeripheralDST; // 内存->寄存器
DMA_Config.DMA_PeripheralInc = DMA_PeripheralInc_Disable; // 寄存器不自增
DMA_Config.DMA_MemoryInc = DMA_MemoryInc_Enable; // 内存自增
DMA_Config.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; // 内存搬移字节为单位
DMA_Config.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; // 寄存器字节位单位
DMA_Config.DMA_Mode = DMA_Mode_Normal; // 普通模式
DMA_Config.DMA_Priority = DMA_Priority_Medium; // 中等
DMA_Config.DMA_M2M = DMA_M2M_Disable; // 禁止内存之间拷贝
DMA_Init(DMA1_Channel4, &DMA_Config);
// 3.配置DMA1通道4支持DMA_IT_TC中断
DMA_ITConfig(DMA1_Channel4, DMA_IT_TC, ENABLE);
// 4.配置NVIC支持DMA1通道4中断
NVIC_InitTypeDef NVIC_Config;
NVIC_Config.NVIC_IRQChannel = DMA1_Channel4_IRQn; // DMA1通道4中断
NVIC_Config.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_Config.NVIC_IRQChannelSubPriority = 2;
NVIC_Config.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_Config);
}
测试函数
// 串口1发送测试函数
void UART1_DMA_Tx_Test(void)
{
// 1.初始化内存缓冲区
u32 i;
for(i = 0; i < UART1DMA_TXBUFF_SIZE; i++)
UART1DMA_TxBuff[i] = 'A';
// 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.打开DMA1通道4
DMA_Cmd(DMA1_Channel4, ENABLE);
// 打开该功能后, DMA1通道4就会自动从内存缓冲区中搬移数据到串口1的DR中
// 搬移完成会触发DMA1通道4 TC中断
DMA_TcFlag = 0; // 标识数据还没发完
// 6.采用中断的方式判断数据是否搬移完毕 ??
while(1){
if(DMA_TcFlag){ //
printf("\n DMA TX SUCCESS\n");
break;
}
// 做其它业务 ...
LED0 = !LED0;
delay_ms(200);
}
}
中断处理函数
// DMA1通道4触发的中断
// 发送完成触发中断
void DMA1_Channel4_IRQHandler(void){
// 1.判断是哪个中断
if(DMA_GetITStatus(DMA1_IT_TC4) != RESET){
// 2.清除中断到来位
DMA_ClearITPendingBit(DMA1_IT_TC4);
// 3.关闭DMA1通道4
DMA_Cmd(DMA1_Channel4, DISABLE);
// 4.标志数据发送完毕
DMA_TcFlag = 1;
}
}
初始化函数和cmd函数中需要将DMA相关添加进来
init.c中
static PINIT_T init_func[] = {
LED_Init, // led灯初始化
BEEP_Init, // beep初始化
Systick_init, // 滴答定时器初始化
KEY_Init, // 按键初始化
My_EXTI_Init, // 中断初始化
UART_Init, // 串口初始化
AT24C02_Init, // AT24C02初始化
DS18B20_Init, // 温度传感器的初始化
My_DMA_Init, // DMA初始化
0
};
cmd.c中
cmd_t cmd[] = {
{"led on", LED_On},
{"led off", LED_Off},
{"beep on", BEEP_On},
{"beep off", BEEP_Off},
{"EEPROM R", AT24C02_ReadOne}, // 读取单字节
{"EEPROM W", AT24C02_WriteOne}, //写入单字节
{"EEPROM RS", AT24C02_ReadMul}, // 读取多字节
{"EEPROM WS", AT24C02_WriteMul}, // 写入多字节
{"temp", DS18B20_Test}, // 获取温度命令
{"rom", DS18B20_ReadRom}, // 读取ROM值命令
{"dma tx", UART1_DMA_Tx_Test} // DMA发送数据命令
};
实验结果
在串口工具中发送dma tx,串口工具能够显示通过内存写入到寄存器中的数据,本文中是写了1024和'A'