专职搬运工DMA(STM32F103ZE)


DMA介绍

直接存储器存取(direct memory access,DMA)用来提供在外设和存储器之间或者存储器和存储器之间的高速数据传输。无须CPU干预,数据可以通过DMA快速地移动,这就节省了CPU的资源来做其他操作。

2个DMA控制器有12个通道(DMA1有7个通道,DMA2有5个通道),每个通道专门用来管理来自于一个或多个外设对存储器访问的请求。还有一个仲裁器来协调各个DMA请求的优先权。

DMA控制器和Cortex™-M3核心共享系统数据总线,执行直接存储器数据传输。当CPU和DMA同时访问相同的目标(RAM或外设)时,DMA请求会暂停CPU访问系统总线达若干个周期,总线仲裁器执行循环调度,以保证CPU至少可以得到一半的系统总线(存储器或外设)带宽。

要注意的是DMA2只存在于大容量产品和互联型产品中。

DMA框图

image-20241206225250686

DMA请求

如果外设要想通过DMA来传输数据,必须先给DMA控制器发送DMA请求,DMA控制器收到请求信号之后,控制器会给外设一个应答信号,当外设得到控制器的应答信号后,外设会立即释放它的请求。

DMA有DMA1和DMA2两个控制器,DMA1有7个通道,DMA2有5个通道,不同DMA控制器的通道对应着不同的外设请求,这决定了我们在软件编程上该怎么设置。

image-20241206225359841

DMA通道

DMA具有12个独立可编程的通道,其中 DMA1有7个通道,DMA2有5个通道,每个通道对应不同的外设的DMA请求。虽然每个通道可以接收多个外设的请求,但是同一时间只能接收一个,不能同时接收多个。

仲裁器

当发生多个DMA通道请求时,就意味着有先后响应处理的顺序问题,这个就由仲裁器管理。仲裁器管理DMA通道请求分为两个阶段。

第一阶段属于软件阶段,可以在DMA_CCRx寄存器中设置,有4个等级:非常高、高、中和低四个优先级。

第二阶段属于硬件阶段,如果两个或以上的DMA通道请求设置的优先级一样,则他们优先级取决于通道编号,编号越低优先权越高,比如通道 1 高于通道 2。

在大容量产品和互联型产品中,DMA1控制器拥有高于DMA2控制器的优先级。

传输方向

存储器到外设,外设到存储器,存储器到存储器。这里的存储器指的是ROM和RAM。注意DMA没有办法把数据从RAM传输到ROM(flash)。

实验-ROM到RAM

需求

使用寄存器操作把ROM中的数据通过DMA传输到RAM,然后把数据通过printf发送到串口验证是否正确。

DMA传输不涉及外设,所以通道随便选。我们选DMA1的1通道。

寄存器分析

RCC时钟

image-20241206230728038

image-20241206230802590

传输方向

image-20241206231014436

[!NOTE]

可以将ROM当做外设

image-20241206231212616

源/目标的基地址

image-20241206232727151

image-20241206232751481

传输数据的宽度

image-20241206231527504

外设和内存地址是否自增

image-20241206231649649

传输数据的长度

image-20241206232018096

传输中断使能

image-20241206232112172

通道传输使能

image-20241206232831121

中断状态® & 中断标志清除(w)

image-20241206233352731

image-20241206233342185

示例代码

dma1.h

#ifndef __DMA1_H__
#define __DMA1_H__

#include "stm32f10x.h"

void DMA1_Init(void);

void DMA1_Transmit(uint32_t srcAddr, uint32_t destAddr, uint16_t byteSize);

void DMA1_CH1_TransmitCompleteCallback(void);

#endif /* __DMA1_H__ */

dma1.c

#include "dma1.h"
#include "logger.h"

void DMA1_Init(void) {
    // 使能AHB时钟
    RCC->AHBENR |= RCC_AHBENR_DMA1EN;

    // 配置DMA通道(使用内存到内存模式时,DMA所有通道都可以选择,这里以通道1为例)
    // 传输模式:内存到内存
    DMA1_Channel1->CCR |= DMA_CCR1_MEM2MEM;
    // 传输方向:从外设读取数据(内存到内存模式下,提供数据的内存可以当做外设)
    DMA1_Channel1->CCR &= ~DMA_CCR1_DIR;
    // 数据宽度,统一为8字节
    DMA1_Channel1->CCR &= ~DMA_CCR1_PSIZE;
    DMA1_Channel1->CCR &= ~DMA_CCR1_MSIZE;
    // 地址自动递增
    DMA1_Channel1->CCR |= DMA_CCR1_PINC;
    DMA1_Channel1->CCR |= DMA_CCR1_MINC;

    // 使能传输完成中断 Transmit Complete Interrupt Enable
    DMA1_Channel1->CCR |= DMA_CCR1_TCIE;
    NVIC_SetPriorityGrouping(3);
    NVIC_SetPriority(DMA1_Channel1_IRQn, 3);
    NVIC_EnableIRQ(DMA1_Channel1_IRQn);
}

void DMA1_Transmit(uint32_t srcAddr, uint32_t destAddr, uint16_t byteSize) {
    // 外设地址(内存到内存模式下,为内存中源数据的基地址)
    DMA1_Channel1->CPAR = srcAddr;
    // 内存地址(内存到内存模式下,为内存中存放数据的目标地址)
    DMA1_Channel1->CMAR = destAddr;
    // 指定要传输的数据数量
    DMA1_Channel1->CNDTR = byteSize;

    // 使能通道,开始传输
    DMA1_Channel1->CCR |= DMA_CCR1_EN;
    LOG_DEBUG("DMA start")
}

void DMA1_Channel1_IRQHandler(void) {
    LOG_DEBUG("DMA1_Channel1_IRQHandler")
    // 判断是否为传输完成事件对应的中断
    if (DMA1->ISR & DMA_ISR_TCIF1) {
        // 清除中断标志
        DMA1->IFCR |= DMA_IFCR_CTCIF1;
        DMA1_CH1_TransmitCompleteCallback();
        // 关闭DMA通道
        DMA1_Channel1->CCR &= ~DMA_CCR1_EN;
    }
}

__weak void DMA1_CH1_TransmitCompleteCallback(void) {

}

main.c

#include "delay.h"
#include "dma1.h"
#include "key.h"
#include "led.h"
#include "logger.h"
#include "stdio.h"
#include "stm32f10x.h"
#include "string.h"
#include "uart.h"

void uart1_received_callback(uint8_t buf[], uint8_t size) {
    uart_send_bytes(buf, size);
}

const uint8_t src[] = {0x11, 0x22, 0x33, 0x44};
uint8_t dest[4] = {0};

void DMA1_CH1_TransmitCompleteCallback(void) {
    LOG_DUMP("dest data => ", dest, 4)
}

int main() {
    uart_init();
    DMA1_Init();
    LOG_DEBUG("main start")

    LOG_DEBUG("src address => %p, dest address => %p", src, dest);
    DMA1_Transmit((uint32_t)src, (uint32_t)dest, 4);

    while (1) {
    }
}

ROM & RAM地址验证

image-20241207104428811

image-20241207104440939

HAL库实现

Cube配置

image-20241207110716672

image-20241207110804672

注册传输完成回调 & 中断方式开启DMA传输

/* USER CODE BEGIN PV */
const uint8_t src[] = {11, 22, 33, 44};
uint8_t dest[4] = {0};
/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */
void DMA_XferCpltCallback(DMA_HandleTypeDef *_hdma);
/* USER CODE END PFP */


/* USER CODE BEGIN 4 */
void DMA_XferCpltCallback(DMA_HandleTypeDef *_hdma) {
    HAL_DMA_Abort_IT(_hdma);
    for (uint8_t i = 0; i < 4; i++) {
        printf("%d\t", dest[i]);
    }
    printf("\n");
}
/* USER CODE END 4 */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_DMA_Init();
MX_USART1_UART_Init();
/* USER CODE BEGIN 2 */
HAL_DMA_RegisterCallback(&hdma_memtomem_dma1_channel1,
                         HAL_DMA_XFER_CPLT_CB_ID, DMA_XferCpltCallback);
printf("src => %p, dest => %p\n", src, dest);
HAL_DMA_Start_IT(&hdma_memtomem_dma1_channel1, (uint32_t)src,
                 (uint32_t)dest, 4);
/* USER CODE END 2 */

/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1) {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */

实验-RAM到UART

外设特定DMA通道

image-20241207114408798

外设使能DMA模式

image-20241207114541503

示例代码

DMA1_CH4

#include "dma1.h"
#include "logger.h"

void DMA1_Init(void) {
    // 使能AHB时钟
    RCC->AHBENR |= RCC_AHBENR_DMA1EN;

    // 配置DMA通道(使用内存到内存模式时,DMA所有通道都可以选择,这里以通道1为例)
    // 传输方向:从内存读,发送到外设uart
    DMA1_Channel4->CCR |= DMA_CCR4_DIR;
    // 数据宽度,统一为8字节
    DMA1_Channel4->CCR &= ~DMA_CCR4_PSIZE;
    DMA1_Channel4->CCR &= ~DMA_CCR4_MSIZE;
    // 内存地址自增,UART缓冲区地址不自增
    DMA1_Channel4->CCR |= DMA_CCR4_MINC;
    DMA1_Channel4->CCR &= ~DMA_CCR4_PINC;

    // 如果开启循环传输模式,那么传输完成中断中不要停止DMA通道
    //DMA1_Channel4->CCR |= DMA_CCR4_CIRC;

    // 使能传输完成中断 Transmit Complete Interrupt Enable
    DMA1_Channel4->CCR |= DMA_CCR4_TCIE;
    NVIC_SetPriorityGrouping(3);
    NVIC_SetPriority(DMA1_Channel4_IRQn, 3);
    NVIC_EnableIRQ(DMA1_Channel4_IRQn);
}

void DMA1_Transmit(uint32_t srcAddr, uint32_t destAddr, uint16_t byteSize) {
    DMA1_Channel4->CMAR = srcAddr;
    DMA1_Channel4->CPAR = destAddr;
    DMA1_Channel4->CNDTR = byteSize;

    // 使能通道,开始传输
    DMA1_Channel4->CCR |= DMA_CCR4_EN;
}

void DMA1_Channel4_IRQHandler(void) {
    // 判断是否为传输完成事件对应的中断
    if (DMA1->ISR & DMA_ISR_TCIF4) {
        // 清除中断标志
        DMA1->IFCR |= DMA_IFCR_CTCIF4;
        // 关闭DMA通道
        DMA1_Channel4->CCR &= ~DMA_CCR4_EN;
    }
}

UART使能DMA发送

// 发送数据使能DMA
USART1->CR3 |= USART_CR3_DMAT;

main

#include "delay.h"
#include "dma1.h"
#include "key.h"
#include "led.h"
#include "logger.h"
#include "stdio.h"
#include "stm32f10x.h"
#include "string.h"
#include "uart.h"

void uart1_received_callback(uint8_t buf[], uint8_t size) {
    uart_send_bytes(buf, size);
}

const uint8_t src[] = {'a', 'b', 'c', 'd'};

int main() {
    uart_init();
    DMA1_Init();

    DMA1_Transmit((uint32_t)src, (uint32_t)&(USART1->DR), 4);

    while (1) {
    }
}

HAL库实现

外设下的DMA设置

image-20241207121451388

以DMA方式驱动外设

/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_DMA_Init();
MX_USART1_UART_Init();
/* USER CODE BEGIN 2 */
  uint8_t data[] = {'a', 'b', 'c', 'd', 'e'};
  HAL_UART_Transmit_DMA(&huart1, data, 5);
/* USER CODE END 2 */

/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
  /* USER CODE END WHILE */

  /* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */

源码分析

  • MX_USART1_UART_Init:DMA初始化:HAL_UART_MspInit
  • HAL_UART_Transmit_DMA:调用 HAL_DMA_Start_IT

循环传输模式

image-20241207120044568


文章作者: 安文
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 安文 !
  目录