STM32之DMA

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);
    }
}
相关推荐
LS_learner2 小时前
ros2命令行工具的完整命令简介
嵌入式硬件
飞凌嵌入式2 小时前
嵌入式AI领域的主控选择
linux·arm开发·人工智能·嵌入式硬件
猫猫的小茶馆2 小时前
【Linux 驱动开发】四. 平台总线驱动
linux·c语言·arm开发·驱动开发·嵌入式硬件·mcu·物联网
promising-w2 小时前
【stm32入门教程】TIM定时中断
stm32·单片机·嵌入式硬件
代码游侠2 小时前
嵌入式开发代码实践——串口通信(UART)开发
c语言·开发语言·笔记·单片机·嵌入式硬件·重构
二十画~书生2 小时前
CH340G 驱动的多功能 USB 转串口电路
单片机·嵌入式硬件·硬件工程
阿凉07023 小时前
STM32 USB CDC虚拟串口配置
stm32·单片机·嵌入式硬件
FakeOccupational3 小时前
【电路笔记 STM32】STM32下载器完整配置流程:驱动安装+硬件连接+芯片包安装+软件测试
笔记·stm32·嵌入式硬件
fanged3 小时前
STM32(6)--HAL2(PWM/I2C/ADC)(TODO)
stm32·单片机·嵌入式硬件