从零开始学嵌入式之STM32——33.直接存储器访问-DMA

目录

一、DMA简介

二、DMA的硬件资源

(1)DMA1通道分配:

(2)DMA2通道分配

三、DMA优先级

四、CPU和DMA的总线调度策略

(1)总线仲裁规则

(2)周期窃取与分时复用机制

[(3)DMA 请求与应答握手流程](#(3)DMA 请求与应答握手流程)

五、数据传输

六、寄存器介绍

(1)DMA通道配置寄存器(DMA_CCRx)

(2)DMA通道传输数量寄存器(DMA_CNDTRx)

(3)DMA通道外设地址寄存器(DMA_CPARx)

(4)DMA通道存储器地址寄存器(DMA_CMARx)

(5)DMA中断状态寄存器(DMA_ISR)

(6)DMA中断标志清除寄存器(DMA_IFCR)

七、使用寄存器操作DMA功能时的配置思路

(1)封装初始化函数:

(2)封装数据传输函数

(3)寄存器操作DMA的使用示例

八、使用HAL库配置DMA的方法

1.时钟配置

2.串口基础配置

3.生成代码调用HAL库实现DMA传输数据


一、DMA简介

DMA(direct memory access)直接存储器访问,提供了一种外设和存储器之间直接高速的数据传输通道。通过DMA通道传输数据,可以无需CPU干预,从而节省CPU资源,用于更重要的工作中,提升CPU性能。

在普通的情况下,当外设需要访问内存时,需要先向CPU发送申请,由CPU代为向内存中读写数据。在整个过程中,CPU的速度很快,外设的速度则较为慢,CPU需要浪费大量的时间等待外设。

在DMA模式下,外设和内存之间的数据交换可以完全不经过CPU,这样就节省了CPU的宝贵的时间。

二、DMA的硬件资源

以STM32F10X系列芯片为例(其它系列芯片的DMA资源需要查手册确认)在大容量产品和互联型产品中,有两个DMA控制器:DMA1和DMA2。该系列的其他产品仅有一个:DMA1,本文主要介绍DMA1。其中DMA1和DMA2的通道数量不一致,具体如下:

DMA1有7个通道

DMA2有5个通道

芯片中的每个外设连接到了固定的DMA通道,由于外设数量较多,每个DMA通道上分配了多个外设,这就意味着:在一个DMA通道中,在同一时刻,仅可以有一个请求生效。具体的分配图如下:

(1)DMA1通道分配:

(2)DMA2通道分配

三、DMA优先级

STM32F10x 的每个 DMA 通道可被多个外设 DMA 请求复用,当多个 DMA 通道同时发起传输请求时,需要通过优先级仲裁决定响应顺序。
DMA 优先级分为 软件优先级硬件优先级两级仲裁机制:

  • 软件优先级 由 DMA 仲裁器统一仲裁配置,可编程设置 4 个等级:非常高、高、中等、低。软件优先级可由用户在程序中自行配置。
  • 硬件优先级 当多个 DMA 通道软件优先级设置相同时 ,自动启用硬件优先级裁决。硬件优先级由通道编号 决定:通道号越小,硬件优先级越高

四、CPU和DMA的总线调度策略

STM32F10x 采用 Cortex-M3 内核,CPU 内核与 DMA 控制器共享系统数据总线,同一时刻总线只能被一方占用,因此需要硬件总线仲裁机制进行调度。

(1)总线仲裁规则

硬件默认配置下, DMA 控制器的总线请求优先级高于 CPU。原因是外设数据流具有实时性、内部缓存容量小,若总线响应延迟,极易造成数据丢失;而 CPU 程序执行可短暂等待,不会立即丢失关键数据

(2)周期窃取与分时复用机制

系统采用 周期窃取 的总线调度方式:DMA 并不会长期独占总线,而是每次仅占用少数总线周期完成一次数据传输,传输间隙主动释放总线给 CPU 使用。通过分时复用总线资源,既保证外设 DMA 实时传输,又 避免 CPU 长期得不到总线使用权而产生 "饥饿" 现象

(3)DMA 请求与应答握手流程

外设需要以 DMA 方式收发数据时,首先向 DMA 控制器发送 DMA 请求信号 ;DMA 控制器经通道配置与总线仲裁通过后,向外设返回 DMA 应答信号,随后正式启动总线数据传输。

五、数据传输

STM32 DMA 支持三种数据传输方向:

  • 外设 → 内存外设数据通过 DMA 搬运至内部存储器。
  • 内存 → 外设内存中数据通过 DMA 发送到外设。
  • 内存 → 内存无需外设参与,DMA 实现存储器之间直接搬运,仅 DMA1 支持该模式。
    ROM 属于只读程序存储器,仅允许 ROM → RAM 的 DMA 读取;硬件上 禁止 RAM → ROM 写入操作,无法通过 DMA 向 ROM 写数据。

六、寄存器介绍

(1)DMA通道配置寄存器(DMA_CCRx)

MEM2MEM:使用存储器到存储器模式时置位该位

PL:优先级配置

MSIZE:存储器数据宽度,设置从存储器中一次获取的数据的数据大小,有8、16、32三个长度

PSIZE:外设数据宽度,设置向外设中一次发送的数据的数据大小,有8、16、32三个长度,除特殊需求,PSIZE和MSIZE应保持一致。

MINC:存储器地址自增,需要传输多个数据、且数据都在连续地址中存储时,可以使用该功能。

PINC:外设地址自增,当外设的数据连续存储在外设的内存中时,使用该功能。注意,如果外设的数据仅在同一个寄存器中不断刷新时,禁用该功能。

CIRC:循环模式,当设定了数据传输数量CNDTR后,DMA每传输一个数据,CNDTR的值会减1,直到减为零。当循环模式打开时,会再次回到初始位置重新开始传输,如果不使能循环模式,当CNDTR为0后传输停止。

DIR:数据传输方向

除此之外,还有三个中断控制位

(2)DMA通道传输数量寄存器(DMA_CNDTRx)

数据传输数量

数据传输数量为0至65535。这个寄存器只能在通道不工作(DMA_CCRx的EN=0)时写入。通道开启后该寄存器变为只读,指示剩余的待传输字节数目。寄存器内容在每次DMA传输后递减。

数据传输结束后,寄存器的内容或者变为0;或者当该通道配置为自动重加载模式时,寄存器的内容将被自动重新加载为之前配置时的数值。

当寄存器的内容为0时,无论通道是否开启,都不会发生任何数据传输。

(3)DMA通道外设地址寄存器(DMA_CPARx)

外设数据寄存器的基地址,作为数据传输的源或目标。

当PSIZE='01'(16位),不使用PA[0]位。操作自动地与半字地址对齐。

当PSIZE='10'(32位),不使用PA[1:0]位。操作自动地与字地址对齐。

(4)DMA通道存储器地址寄存器(DMA_CMARx)

存储器地址作为数据传输的源或目标。

当MSIZE='01'(16位),不使用MA[0]位。操作自动地与半字地址对齐。

当MSIZE='10'(32位),不使用MA[1:0]位。操作自动地与字地址对齐。

(5)DMA中断状态寄存器(DMA_ISR)

该寄存器的控制位分为三类:

TEIF:传输错误标志位

HTIF:传输过半标志位

TCIF:传输完成标志位

该寄存器为只读,由硬件置位,清零需要通过IFCR寄存器操作

(6)DMA中断标志清除寄存器(DMA_IFCR)

该寄存器的控制位分为三类:

CTEIF:清除传输错误标志位

CHTIF:清除传输过半标志位

CTCIF:清除传输完成标志位

七、使用寄存器操作DMA功能时的配置思路

(1)封装初始化函数:

1.开启时钟

2.DMA配置传输设置

数据传输方向配置

数据宽度配置

地址自增配置

3.配置中断,传输结束后进行处理,控制位:TCIE

4.NVIC中断配置

(2)封装数据传输函数

1.配置前先停止DMA

2.设置外设地址

3.设置目的地址

4.设置数据长度

5.使能DMA,开始传输

(3)寄存器操作DMA的使用示例

文件名:dma.c

cpp 复制代码
#include "dma.h"

void DMA1_Init(void)
{
    // 1.打开时钟模块
    RCC->AHBENR |= RCC_AHBENR_DMA1EN;

    // 2.DMA配置
    DMA1_Channel4->CCR = 0;
    // 2.1.数据宽度配置00-8位宽度
    DMA1_Channel4->CCR &= ~DMA_CCR4_MSIZE;
    DMA1_Channel4->CCR &= ~DMA_CCR4_PSIZE;
    // 2.2.地址自增配置
    DMA1_Channel4->CCR |= DMA_CCR4_MINC;
    DMA1_Channel4->CCR &= ~DMA_CCR4_PINC;   // 串口不自增
    // 2.3.数据传输方向
    DMA1_Channel4->CCR |= DMA_CCR4_DIR;    // 从存储器读

    // 3.开启DMA中断,处理传输完成后的操作
    DMA1_Channel4->CCR |= DMA_CCR4_TCIE;

    // 4.中断配置
    NVIC_SetPriorityGrouping(3);
    NVIC_SetPriority(DMA1_Channel4_IRQn, 2);
    NVIC_EnableIRQ(DMA1_Channel4_IRQn);
}

void DMA1_TO_USART1(uint32_t src, uint32_t dst, uint16_t dataLen)
{
    // 传输前关闭DMA通道
    DMA1_Channel4->CCR &= ~DMA_CCR4_EN;

    // 设置源数据地址,内存地址
    DMA1_Channel4->CMAR = src;

    // 设置目的地址,外设地址
    DMA1_Channel4->CPAR = dst;

    // 设置数据长度
    DMA1_Channel4->CNDTR = dataLen;

    // 开启DMA通道
    DMA1_Channel4->CCR |= DMA_CCR4_EN;
}

void DMA1_Channel4_IRQHandler(void)
{
    if(DMA1->ISR & DMA_ISR_TCIF4)
    {
        // 关闭DMA通道4
        /*
        在中断服务函数中,常规的操作顺序是:
        第一步:立刻关闭 / 屏蔽 能再次触发本次同类型中断的硬件使能
        第二步:清除中断标志位
        第三步:执行正常业务逻辑处理
        */
        DMA1_Channel4->CCR &= ~DMA_CCR4_EN;
        // 清除中断标志位
        DMA1->IFCR |= DMA_IFCR_CTCIF4;
    }
}

文件名:dma.h

cpp 复制代码
#ifndef __DMA_H
#define __DMA_H

#include "stm32f10x.h"
// DMA初始化
void DMA1_Init(void);
// DMA通道传输数据到UART1
void DMA1_TO_USART1(uint32_t src, uint32_t dst, uint16_t dataLen);
#endif

文件名:usart.c

cpp 复制代码
#include "usart.h"

// 1.串口初始化
void USART1_Init(void)
{
    // 1.打开时钟模块
    RCC->APB2ENR |= RCC_APB2ENR_USART1EN;
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;

    // 2.串口收发引脚配置
    /*
        GPIOA9 -TX  复用功能推挽输出;CNF-10;MODE-11
        GPIOA10-RX  浮空输入;CNF-01;MODE-00
    */
   GPIOA->CRH |= GPIO_CRH_CNF9_1;
   GPIOA->CRH &= ~GPIO_CRH_CNF9_0;
   GPIOA->CRH |= GPIO_CRH_MODE9;

   GPIOA->CRH &= ~GPIO_CRH_CNF10_1;
   GPIOA->CRH |= GPIO_CRH_CNF10_0;
   GPIOA->CRH &= ~GPIO_CRH_MODE10;

   // 3.串口参数配置,波特率115200
   USART1->BRR = 0X271;
   USART1->CR1 |= USART_CR1_UE;
   USART1->CR1 |= USART_CR1_TE;
   USART1->CR1 |= USART_CR1_RE;

   // 开启DMA发送
   USART1->CR3 |= USART_CR3_DMAT;
}

// 2.发送一字节数据
void USART1_Send_Byte(int ch)
{
    while((USART1->SR & USART_SR_TXE) == 0);   // 等待上一字节发送完成
    USART1->DR = ch;    // 发送当前字节
}

// 3.模拟实现fputc
int fputc(int ch, FILE* file)
{
    USART1_Send_Byte(ch);
    return ch;
}

文件名:usart.h

cpp 复制代码
#ifndef __USART_H
#define __USART_H

#include "stm32f10x.h"
#include <stdio.h>

// 1.串口初始化
void USART1_Init(void);

// 2.发送一字节数据
void USART1_Send_Byte(int ch);

// 3.模拟实现fputc
int fputc(int ch, FILE* file);

#endif

文件名:main.c

cpp 复制代码
#include "main.h"

uint8_t data_in_ram[4] = {"ABC"};

int main(void)
{
    USART1_Init();
    DMA1_Init();

    printf("test.\n");

    DMA1_TO_USART1((uint32_t)data_in_ram, (uint32_t)&(USART1->DR), 4);

    while(1)
    {
    }
}

八、使用HAL库配置DMA的方法

通过STM32cubemx图形化配置界面做基础配置

1.时钟配置

HSE和LSE都选择选择外部晶振

2.串口基础配置

开启串口通讯功能,选择异步通讯,其余配置默认即可。

在串口配置界面配置DMA(也可以开启串口后再DMA中配置)

在这里选择UART-TX

其余配置保持默认即可

3.生成代码调用HAL库实现DMA传输数据

cpp 复制代码
int main(void)
{
  /* USER CODE BEGIN 1 */
  // 准备要发送的数据
  uint8_t data_in_ram[4] = {"abc"};
  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_DMA_Init();
  MX_USART1_UART_Init();
  /* USER CODE BEGIN 2 */
  // 调用HAL库,通过DMA发送数据
  HAL_UART_Transmit_DMA(&huart1, data_in_ram, 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 */
}

以上是通过HAL库的实现方法,这些基础的操作全部由图形化界面配置完成,快速、稳定,在工作中,做产品开发时使用HAL库会大大提高开发效率,寄存器仅用于学习原理。

相关推荐
jllllyuz2 小时前
stm32“多串口并发采集 + 无线传输”系统实现
stm32·单片机·嵌入式硬件
LCG元2 小时前
STM32实战:基于STM32F103的简易示波器(ADC+DMA+LCD)
stm32·单片机·嵌入式硬件
小灰灰搞电子3 小时前
rt-thread UART串口使用详解
单片机·嵌入式硬件·串口
洲洲不是州州3 小时前
单片机onenet云平台的万能APP
单片机·onenet·app·嵌入式·云平台
钿驰科技3 小时前
无刷电机的驱动原理及驱动电路解析
单片机·嵌入式硬件
木木_王3 小时前
嵌入式学习 | STM32裸板驱动开发(Day01)入门学习笔记(超详细完整版|点灯实验 + 库函数代码 + 原理全解)
linux·驱动开发·笔记·stm32·学习
小锋学长生活大爆炸3 小时前
【教程】树莓派驱动 0.96 寸 SSD1315 OLED 屏幕完整指南
单片机·嵌入式硬件·嵌入式·教程·树莓派·oled·屏幕
ye150127774554 小时前
12V-24V升110V升压转换WT3207
单片机·嵌入式硬件·其他·硬件工程
yong99905 小时前
基于 STM32 的数字控制实现双向 DC-DC 电源
stm32·单片机·嵌入式硬件