1. DMA介绍
1.1 什么是DMA
• DMA(Direct Memory Access,直接存储器访问)提供在外设与内存 、存储器和存储器 之间的高速数 据传输使用 。它允许不同速度的硬件装置来沟通 ,而不需要依赖于CPU,在这个时间中,CPU对于内存的工作来说就无法使用。
• 简单来说DMA 就是一个数据搬运工。
1.2 DMA的作用:代替 CPU 搬运数据,为 CPU 减负。
• 数据搬运的工作比较耗时间(对于CPU来说)。
• 数据搬运工作时效要求高(以串口为例,有数据来就要搬走,不然会被覆盖)。
• 没啥技术含量(可以把CPU 节约出来的时间处理更重要的事)。
1.3 搬运什么样的数据(外设,存储器)
• 这里的外设指的是spi、usart、iic、adc(的DR 寄存器 ) 等基于APB1 、APB2或AHB时钟的外设。
• 而这里的存储器包括 自身的闪存(flash) 或者内存(SRAM) 以及外设的存储设备 都可以作为访问地源或者目的地。
1.4 三种搬运方式:
• 外设到存储器
• 存储器到外设
• 存储器到存储器
1.4.1 存储器到存储器(例如:复制某特别大的数据 buf ),如图:

1.4.2 存储器到外设:(例如:将某数据buf写入串口 TDR 寄存器,至于这里为什么,可以看串口的框图),如图:

1.4.3 外设到存储器(例如:将串口RDR寄存器写入某数据buf),如图:

1.5 DMA框图:

• 说明:如果不用DMA的话,CPU首先会经过总线矩阵到指定的外设去搬运数据,然后在回到cpu,再经过系统矩阵到SRAM或者FLASH。
• 如果用DMA,外设发出DMA请求,DMA经过系统矩阵到指定外设搬运数据,然后到SRAM到FLASH。
• 这其中都不用经过cpu。
• 如果多个DMA请求,由仲裁器仲裁(根据软件优先级或者硬件优先级)。
2. DMA控制器
• STM32F103有两个DMA控制器,DMA1有七个通道,DMA2有两个通道。
• 但是我们使用的STM32F103C8T6只有一个DMA1。并且一个通道只能每次搬运一个外设的数据 。如果同时有多个外设的 DMA 请求,则按照优先级进行响应。
• DMA1的7个通道下图所示:

3. DMA优先级的管理:采用(软件 + 硬件)管理。
• 软件:每个通道 的优先级都可以DMA_CCRx 寄存器里面配置,有四个等级,最高级 > 高级 > 中级 > 低级。
• 硬件:如果2个请求,它们的软件优先级相同 ,则较低编号的通道比较高编号的通道有较高的优先权。 比如:如果软件优先级相同,通道2优先于通道4,如图:

4. DMA的传输方式:
• DMA_Mode_Nromal(正常模式):一次 DMA数据传输完后,停止 DMA 传送 ,也就是只传输一次。
• DMA_Mode_Circular(循环模式):当传输结束时,硬件自动 会将传输数据量寄存器进行重装 ,进行下一轮的数据传输。 也就是多次传输模式。
5. 指针递增模式:外设和存储器指针在每次传输后可以自动向后递增或保持常量。当设置为增量模式时,下一个要传输的地址将是前一个地址加上增量值。
• 源地址:如果是内存作为源地址,是无论如何都要递增的,外设就不一定,但是目的地址不一定都要递增,看实际应用需求。如图:


• 注:原地的指针一般需要自增,不然传来传去都是同一个数据,至于目标可以不增。
6. DMA数据对齐模式:
6.1 第一种,发送数据的长度与接收数据长度一样,就可以完全接收,如图:

6.2 第二种 发送数据的长度比接收数据长度短,那么接收的数据会放在接收区的低八位中并且高位补0,如果是一帧数据是8位,如图:

6.3 第三种,发送数据比接收数据的缓冲区长度长,那么那么只有低八位会被放进缓冲区(高于八位的数据全被截断),如图:

7. 实战
7.1 内存到内存
• 步骤如下
• 使能DMA时钟 __HAL_RCC_DMA1_CLK_ENABLE()
• 初始化DMA(数据从那里来,到那里去) HAL_DMA_Init()
• 准备源数组,目标数组
• 启动DMA数据传输 HAL_DMA_Start()
• 查询DMA传输状态 __HAL_DMA_GET_FLAG()
• dma.c
cpp
#include "dma.h"
#include "stdio.h"
DMA_HandleTypeDef dma_handle = {0};
void dma_init(){
__HAL_RCC_DMA1_CLK_ENABLE();//开启DMA时钟
dma_handle.Instance = DMA1_Channel1;//
dma_handle.Init.Direction = DMA_MEMORY_TO_MEMORY;//传输方向是内存到内存
dma_handle.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;//数据宽度是8位
dma_handle.Init.MemInc = DMA_MINC_ENABLE;//自增模式
dma_handle.Init.Mode = DMA_NORMAL;//传输方式正常的
dma_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;//数据宽度是8位
dma_handle.Init.PeriphInc = DMA_PINC_ENABLE;//自增模式
dma_handle.Init.Priority = DMA_PRIORITY_VERY_HIGH;//优先是设置为最高的
HAL_DMA_Init(&dma_handle);
}
void dma_send_start(uint32_t* src,uint32_t* dest,uint32_t len){
HAL_DMA_Start(&dma_handle,(uint32_t)src,(uint32_t)dest,sizeof(uint32_t)*len);
//DataLength:指定要传输数据字节的长度,以字节为单位。这个参数告诉DMA控制器需要传输多少数据。
//重点是字节的长度。
//转运数据的长度(数据量)
while(__HAL_DMA_GET_FLAG(&dma_handle,DMA_FLAG_TC1) == RESET);//判断通道1是否传输完成,如果完成了,会跳出循环
int i = 0;
for(i = 0;i < len;i++)
printf("dest[%d] = 0x%X\r\n",i,dest[i]);
}
• main.c
cpp
#include "sys.h"
#include "uart1.h"
#include "delay.h"
#include "led.h"
#include "dma.h"
#define BUF_SIZE 16
uint32_t src[BUF_SIZE] = {
0x00000000,0x11111111,0x22222222,0x33333333,
0x44444444,0x55555555,0x66666666,0x77777777,
0x88888888,0x99999999,0xAAAAAAAA,0xBBBBBBBB,
0xCCCCCCCC,0xDDDDDDDD,0xEEEEEEEE,0xFFFFFFFF};//起始地方
uint32_t dest[BUF_SIZE] = {0};//目的地
int main(void)
{
HAL_Init(); /* 初始化HAL库 */
stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
led_init(); /* LED初始化 */
uart1_init(115200);
dma_init();
printf("hello world\r\n");
dma_send_start(src,dest,BUF_SIZE);
while(1)
{
}
}
7.2 内存到外设
• 步骤如下
• 使能DMA时钟 __HAL_RCC_DMA1_CLK_ENABLE()
• 初始化DMA(数据从那里来,到那里去,外设连接) HAL_DMA_Init(),__HAL_LINKDMA() (重点)
• 准备大数据
• 使能串口DMA发送 HAL_UART_Transmit_DMA()
• 代码 dma.c
cpp
#include "dma.h"
#include "stdio.h"
DMA_HandleTypeDef dma_handle = {0};
extern UART_HandleTypeDef uart1_handle;//外界传入串口
void dma_init(){
__HAL_RCC_DMA1_CLK_ENABLE();//打开DMA时钟
dma_handle.Instance = DMA1_Channel4;//查表得出,USART_TX是在通道4
dma_handle.Init.Direction = DMA_MEMORY_TO_PERIPH;//内存到外设
dma_handle.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;//数据长度是按照字节
dma_handle.Init.MemInc = DMA_MINC_ENABLE;//指针偏移
dma_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;//数据长度是按照字节
dma_handle.Init.PeriphInc = DMA_PINC_DISABLE;//指针不偏移,让数据一直都在串口的TDR中
dma_handle.Init.Mode = DMA_NORMAL;//传输模式正常模式
dma_handle.Init.Priority = DMA_PRIORITY_VERY_HIGH;//优先级最高
HAL_DMA_Init(&dma_handle);
__HAL_LINKDMA(&uart1_handle,hdmatx,dma_handle);//让DMA连接到串口。
}
• main.c
cpp
#include "sys.h"
#include "uart1.h"
#include "delay.h"
#include "led.h"
#include "dma.h"
extern UART_HandleTypeDef uart1_handle;//外界传入串口
uint8_t send_buf[1000] = {0};
int main(void)
{
HAL_Init(); /* 初始化HAL库 */
stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
led_init(); /* LED初始化 */
uart1_init(115200);
dma_init();
int i = 0;
for(i = 0;i < 1000;i++)
send_buf[i] = 'A';//将1000个发送到串口TDR寄存器中
//printf("hello world\r\n");
HAL_UART_Transmit_DMA(&uart1_handle,send_buf,1000);//用DMA方式发送
while(1)
{
}
}
7.3 外设到内存
• 步骤如下
• 使能DMA时钟 __HAL_RCC_DMA1_CLK_ENABLE()
• 初始化DMA(数据从那里来,到那里去,外设连接) HAL_DMA_Init(),__HAL_LINKDMA()
• 使能串口DMA发送 HAL_UART_Receive_DMA
• 串口中断服务函数的逻辑编写
• 代码 dma.c中
cpp
#include "dma.h"
#include "uart1.h"
DMA_HandleTypeDef dma_handle = {0};
extern UART_HandleTypeDef uart1_handle;
extern uint8_t uart1_rx_buf[UART1_RX_BUF_SIZE];
void dma_init(){
__HAL_RCC_DMA1_CLK_ENABLE();//开启DMA时钟
dma_handle.Instance = DMA1_Channel5;//USART_RX在通道5
dma_handle.Init.Direction = DMA_PERIPH_TO_MEMORY;//外设到内存
dma_handle.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
dma_handle.Init.MemInc = DMA_MINC_ENABLE;
dma_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
dma_handle.Init.PeriphInc = DMA_PINC_DISABLE;//让数据在串口的RDR中
dma_handle.Init.Mode = DMA_NORMAL;
dma_handle.Init.Priority = DMA_PRIORITY_VERY_HIGH;
HAL_DMA_Init(&dma_handle);
__HAL_LINKDMA(&uart1_handle,hdmarx,dma_handle);
HAL_UART_Receive_DMA(&uart1_handle,uart1_rx_buf,UART1_RX_BUF_SIZE);//开启接收,用DMA,接收到应该在串口中断处理,配置好了之后就要开始接收了
}
• main.c
cpp
#include "sys.h"
#include "uart1.h"
#include "delay.h"
#include "led.h"
#include "dma.h"
#include "stdio.h"
extern UART_HandleTypeDef uart1_handle;
uint8_t send_buf[1000] = {0};
int main(void)
{
HAL_Init(); /* 初始化HAL库 */
stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
led_init(); /* LED初始化 */
uart1_init(115200);
dma_init();
printf("hello world\r\n");
while(1)
{
}
}
• uart1.c
cpp
#include "sys.h"
#include "uart1.h"
#include "string.h"
UART_HandleTypeDef uart1_handle; /* UART1句柄 */
extern DMA_HandleTypeDef dma_handle;
uint8_t uart1_rx_buf[UART1_RX_BUF_SIZE]; /* UART1接收缓冲区 */
uint16_t uart1_rx_len = 0; /* UART1接收字符长度 */
/**
* @brief 重定义fputc函数
* @note printf函数最终会通过调用fputc输出字符串到串口
*/
int fputc(int ch, FILE *f)
{
while ((USART1->SR & 0X40) == 0); /* 等待上一个字符发送完成 */
USART1->DR = (uint8_t)ch; /* 将要发送的字符 ch 写入到DR寄存器 */
return ch;
}
/**
* @brief 串口1初始化函数
* @param baudrate: 波特率, 根据自己需要设置波特率值
* @retval 无
*/
void uart1_init(uint32_t baudrate)
{
/*UART1 初始化设置*/
uart1_handle.Instance = USART1; /* USART1 */
uart1_handle.Init.BaudRate = baudrate; /* 波特率 */
uart1_handle.Init.WordLength = UART_WORDLENGTH_8B; /* 字长为8位数据格式 */
uart1_handle.Init.StopBits = UART_STOPBITS_1; /* 一个停止位 */
uart1_handle.Init.Parity = UART_PARITY_NONE; /* 无奇偶校验位 */
uart1_handle.Init.HwFlowCtl = UART_HWCONTROL_NONE; /* 无硬件流控 */
uart1_handle.Init.Mode = UART_MODE_TX_RX; /* 收发模式 */
HAL_UART_Init(&uart1_handle); /* HAL_UART_Init()会使能UART1 */
}
/**
* @brief UART底层初始化函数
* @param huart: UART句柄类型指针
* @note 此函数会被HAL_UART_Init()调用
* 完成时钟使能,引脚配置,中断配置
* @retval 无
*/
void HAL_UART_MspInit(UART_HandleTypeDef *huart)
{
GPIO_InitTypeDef gpio_init_struct;
if (huart->Instance == USART1) /* 如果是串口1,进行串口1 MSP初始化 */
{
__HAL_RCC_GPIOA_CLK_ENABLE(); /* 使能串口TX脚时钟 */
__HAL_RCC_USART1_CLK_ENABLE(); /* 使能串口时钟 */
gpio_init_struct.Pin = GPIO_PIN_9; /* 串口发送引脚号 */
gpio_init_struct.Mode = GPIO_MODE_AF_PP; /* 复用推挽输出 */
gpio_init_struct.Pull = GPIO_PULLUP; /* 上拉 */
gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH; /* IO速度设置为高速 */
HAL_GPIO_Init(GPIOA, &gpio_init_struct);
gpio_init_struct.Pin = GPIO_PIN_10; /* 串口RX脚 模式设置 */
gpio_init_struct.Mode = GPIO_MODE_AF_INPUT;
HAL_GPIO_Init(GPIOA, &gpio_init_struct); /* 串口RX脚 必须设置成输入模式 */
HAL_NVIC_EnableIRQ(USART1_IRQn); /* 使能USART1中断通道 */
HAL_NVIC_SetPriority(USART1_IRQn, 3, 3); /* 组2,最低优先级:抢占优先级3,子优先级3 */
__HAL_UART_ENABLE_IT(huart, UART_IT_RXNE); /* 使能UART1接收中断 */
__HAL_UART_ENABLE_IT(huart, UART_IT_IDLE); /* 使能UART1总线空闲中断 */
}
}
/**
* @brief UART1接收缓冲区清除
* @param 无
* @retval 无
*/
void uart1_rx_clear(void)
{
memset(uart1_rx_buf, 0, sizeof(uart1_rx_buf)); /* 清空接收缓冲区 */
uart1_rx_len = 0; /* 接收计数器清零 */
}
/**
* @brief 串口1中断服务函数
* @note 在此使用接收中断及空闲中断,实现不定长数据收发
* @param 无
* @retval 无
*/
void USART1_IRQHandler(void)
{
uint8_t receive_data = 0;
if(__HAL_UART_GET_FLAG(&uart1_handle, UART_FLAG_RXNE) != RESET){ /* 获取接收RXNE标志位是否被置位 */
if(uart1_rx_len >= sizeof(uart1_rx_buf)) /* 如果接收的字符数大于接收缓冲区大小, */
uart1_rx_len = 0; /* 则将接收计数器清零 */
HAL_UART_Receive(&uart1_handle, &receive_data, 1, 1000); /* 接收一个字符 */
uart1_rx_buf[uart1_rx_len++] = receive_data; /* 将接收到的字符保存在接收缓冲区 */
}
if (__HAL_UART_GET_FLAG(&uart1_handle, UART_FLAG_IDLE) != RESET) /* 获取接收空闲中断标志位是否被置位 */
{
// printf("recv: %s\r\n", uart1_rx_buf); /* 将接收到的数据打印出来 */
// uart1_rx_clear();
// __HAL_UART_CLEAR_IDLEFLAG(&uart1_handle); /* 清除UART总线空闲中断 */
//清除空闲中断标志位 IDLE
__HAL_UART_CLEAR_IDLEFLAG(&uart1_handle);
//停止DMA传输 防止一直接收的干扰,
HAL_UART_DMAStop(&uart1_handle);
//获取长度
//__HAL_DMA_GET_COUNTER();//这个函数可以获得剩下长度 这个函数底层是调用CNDTR这个寄存器
uart1_rx_len = UART1_RX_BUF_SIZE - __HAL_DMA_GET_COUNTER(&dma_handle);
//打印出来
printf("recv: %s recv_len: %d\r\n", uart1_rx_buf,uart1_rx_len);
//清除buf
uart1_rx_clear();
//重新打开DMA传输
HAL_UART_Receive_DMA(&uart1_handle,uart1_rx_buf,UART1_RX_BUF_SIZE);
}
}