目录

一. DMA简介

二. DMA结构

2.1 DMA框图

2.2 DMA请求映像

三. DMA主要特性

四. DMA相关参数

4.1 传输起始地址

4.2 传输目标地址

4.3 数据宽度

4.4 地址是否自增

4.5 传输计数器

4.6 自动重装器

4.7 触发方式

4.8 运输模式

五. 示例代码

5.1 DMA配置流程

5.2 ADC连续扫描转换+DMA

5.3 USART接收数据+DMA

一. DMA简介

直接存储器存取(DMA)用来提供在外设和存储器之间或者存储器和存储器之间的高速数据传输。无须CPU干预,数据可以通过DMA快速地移动,这就节省了CPU的资源来做其他操作。 两个DMA(对于F1系列而言),控制器有12个通道(DMA1有7个通道,DMA2有5个通道),每个通道专门用来管理来自于一个或多个外设对存储器访问的请求。还有一个仲裁器来协调各个DMA请求的优先权。DMA转运的本质都是存储器到存储器,转运外设也是转运外设里 数据寄存器 存储的数据。

二. DMA结构

2.1 DMA框图

  • 总体上看:

左半部分为主动单元,右半部分为被动单元,主动单元可以通过总线矩阵,对被动单元进行读写,除了内核CPU可以通过ICode,DCode,系统等总线访问被动单元外,每个DMA也有属于自己的DMA总线来访问被动单元。

  • 细节上看:

DMA1拥有7个通道,DMA2则包含5个通道,每个DMA控制器的通道通过时间复用共享同一条DMA总线。以DMA1为例,当多个通道同时产生DMA请求时,仲裁器将进行优先级判断,决定哪个通道先获得总线访问权限。总线矩阵中也设置了仲裁器,负责在DMA和CPU同时请求访问同一资源时进行冲突管理。通常,DMA拥有更高的总线优先级,若DMA正在访问总线,CPU的访问会被暂时暂停,以避免冲突。为了避免过度影响CPU操作,系统会合理分配总线带宽,确保CPU能够继续运行,并获得足够的带宽进行正常处理。

右上角的SRAM是易失性存储器(RAM),主要用于存储程序运行时的临时数据、变量以及外设寄存器配置等信息。DMA控制器位于AHB总线架构中,Cortex-M3核心的CPU也可以通过AHB总线访问并配置DMA。DMA控制器既是一个主动的数据传输单元,也能够作为响应外设请求的被动单元。系统中的硬件外设可以向DMA控制器发送数据传输请求,DMA接到请求后,便会启动并执行相应的数据传输工作。

注意事项:

  • 右上角的Flash是ROM(只读存储器)型存储器,如果通过总线直接访问,CPU和DMA都只能读,不能写,如果实在需要写入,要先对Flash接口控制器进行配置。
  • 硬件外设里的寄存器是否可读,是否可写,需要参考数据手册。

2.2 DMA请求映像

如图,每个硬件外设都有其对应的DMA通道:

选择硬件触发时,需要严格选择对应通道;选择软件触发时,选择任意通道均可。

三. DMA主要特性

  • 有12个独立的可配置的通道(请求),DMA1有7个通道,DMA2有5个通道。
  • 每个通道都支持软件触发和硬件触发。
  • 在同一个DMA模块上,同时产生多个请求时,可以通过软件编程设置优先权(共有四级:很高,高,中等,低),优先权设置相等时由硬件决定(请求0优于请求1)。
  • 独立数据源和目标数据区的传输宽度(字节、半字、全字),模拟打包和拆包的过程。源和目标地址必须按数据传输宽度对齐。
  • 每个通道都有三个事件标志(DMA半传输,DMA传输完成和DMA传输出错),这三个事件标志或成为一个单独的中断请求。
  • 存储器和存储器之间的传输,外设和存储器之间的传输,存储器和外设之间的传输。
  • 内存,SRAM,外设的SRAM,APB1,APB2和AHB外设均可作为访问的源和目标。
  • 可编程的数据传输数目(即一次性最大传输次数)最大为65535。

四. DMA相关参数

4.1 传输起始地址

即要运输数据所在的地址,可以是外设寄存器,Flash,SRAM。

4.2 传输目标地址

即要把数据运输到哪里,可以是外设寄存器,SRAM,但不能是Flash,因为Flash是只读存储器。

4.3 数据宽度

有以下几种常见类型:

  • 字节(Byte):8位
  • 半字(half-word):16位
  • 字(word):32位

这些是比较常见的, 在一些高性能的系统中,可能可以支持到更高的数据宽度,如64位,128位等。

以下是数据传输宽度和大小端操作表:

总结来说就是:

  • 如果把宽度大的数据转运到小的,高位就会舍弃掉。
  • 如果把宽度小的数据转运到大的,高位就会自动补零。

注意事项:

  • 数据宽度的选择需要综合考虑到外设寄存器位数,SRAM目标变量位数以及DMA位数。

例如:

如果外设的寄存器是8位的,那么它只能以8位为单位进行数据传输。这就意味着,DMA传输到这个外设时,必须使用8位宽的数据。

如果DMA控制器是8位的,它每次只能传输8位数据,即使外设支持更宽的数据宽度,DMA也只能逐个字节传输数据。

如果外设寄存器支持32位,而DMA控制器只有16位宽,则每次DMA传输时需要进行两次操作,分别传输16位数据。

4.4 地址是否自增

当使能开启后,每传输一次,地址就会自增一次,起始地址,目标地址都可以单独设置。

示例场景:DMA不断将ADC采集的值运输到数组中,起始地址ADC是固定的,目标地址数组使能地址自增。每次传输时,起始地址ADC不变,而目标地址数组会自增,确保数据依次写入数组,避免数据覆盖。当目标地址自增到数组末尾时,DMA控制器会将目标地址回绕到数组的开头,形成环形缓冲区。

如果是外设到存储器,例如ADC -> 存储器,USART -> 存储器等,我们可以采用DMA空闲中断+环形缓冲区,我们提高篇进行讲解。

4.5 传输计数器

连续传输的次数,最大不可以超过65535,因为计数器是16位的,每传输一次,就自减一次,自减到0就会停止传输。减到0后,如果有自增的地址,也会恢复到刚开始的地址。

4.6 自动重装器

对传输计数值进行重装。

4.7 触发方式

  • 硬件触发:常用于外设 -> 存储器,如:ADC采集到数据触发,USART接收到数据触发,定时器触发等。
  • 软件触发:通过api函数手动触发,DMA会已最快的速度,将计数值清零,常用于存储器 -> 存储器。

4.8 运输模式

  • 单次传输模式(Normal):传输计数器自减到0后会停止传输,需要进行手动重装,在重装之前要先失能DMA,才能重装计数器
  • 循环模式(Circuler):开启后,当计数器自减到0后,可以自动重装计数值。

注意事项:

  • 软件触发和循环模式不能同时开启,否则DMA就停不下来了,会一直占用DMA总线资源。

五. 示例代码

5.1 DMA配置流程

5.2 ADC连续扫描转换+DMA

#include "stm32f10x.h"

// 定义数组用于存储DMA传输的ADC数据
#define BUFFER_SIZE 64
uint32_t adc_buffer[BUFFER_SIZE];

// ADC和DMA初始化
void ADC_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    ADC_InitTypeDef ADC_InitStructure;
    DMA_InitTypeDef DMA_InitStructure;

    // 1. 开启ADC1时钟和DMA时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);     // 开启ADC1时钟
    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);        // 开启DMA1时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);     // 开启GPIOA时钟

    // 2. 配置GPIOA的引脚(例如:ADC通道0)
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;                // 选择GPIOA的第0引脚(ADC通道0)
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;            // 模式设置为模拟输入
    GPIO_Init(GPIOA, &GPIO_InitStructure);                    // 初始化GPIOA引脚

    // 3. 配置ADC1
    ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;        // 独立模式
    ADC_InitStructure.ADC_ScanConvMode = ENABLE;              // 启用扫描模式
    ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;        // 启用连续转换模式
    ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;  // 不使用外部触发
    ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;    // 数据右对齐
    ADC_InitStructure.ADC_NbrOfChannel = 1;                   // 设置为1个通道
    ADC_Init(ADC1, &ADC_InitStructure);                       // 初始化ADC1

    // 4. 配置ADC通道0
    ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5); // 配置通道0,采样时间55.5个时钟周期

    // 5. 配置DMA
    DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR; // DMA外设地址:ADC1数据寄存器
    DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)adc_buffer;    // DMA内存地址:数据存储数组
    DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralToMemory;          // 从外设(ADC)到内存
    DMA_InitStructure.DMA_BufferSize = BUFFER_SIZE;                 // DMA传输大小
    DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; // 外设地址不递增
    DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;         // 内存地址递增
    DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Word; // 外设数据宽度:32位
    DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Word; // 内存数据宽度:32位
    DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;                 // 设置为循环模式
    DMA_InitStructure.DMA_Priority = DMA_Priority_High;             // DMA优先级:高
    DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;                    // 禁止内存到内存模式
    DMA_Init(DMA1_Channel1, &DMA_InitStructure);                    // 初始化DMA1通道1

    // 6. 启用ADC1 DMA
    ADC_DMACmd(ADC1, ENABLE); // 启用ADC1的DMA

    // 7. 启动ADC转换
    ADC_Cmd(ADC1, ENABLE);    // 启用ADC1
    ADC_SoftwareStartConvCmd(ADC1, ENABLE); // 软件触发,开始ADC转换
}

// DMA传输完成中断处理
void DMA1_Channel1_IRQHandler(void)
{
    if (DMA_GetITStatus(DMA1_IT_TC1))  // 如果传输完成
    {
        // 清除中断标志
        DMA_ClearITPendingBit(DMA1_IT_TC1);
        
        // 这里可以处理传输完成后的数据,数据已经存储在adc_buffer中
    }
}

// 主程序
int main(void)
{
    // 系统初始化
    SystemInit();
    
    // 初始化ADC和DMA
    ADC_Init();

    // 启用DMA中断
    NVIC_EnableIRQ(DMA1_Channel1_IRQn); // 启用DMA1通道1中断

    // 主循环
    while (1)
    {
        // 在主循环中可以执行其他任务
        // DMA会在后台自动传输数据到adc_buffer数组
    }
}

5.3 USART接收数据+DMA

#include "stm32f10x.h"

// 定义缓冲区用于接收数据
#define BUFFER_SIZE 128
uint8_t rx_buffer[BUFFER_SIZE];

// USART和DMA初始化
void USART_Init_DMA(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    USART_InitTypeDef USART_InitStructure;
    DMA_InitTypeDef DMA_InitStructure;

    // 1. 开启USART1、DMA和GPIO的时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);    // 开启USART1时钟
    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);        // 开启DMA1时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);     // 开启GPIOA时钟

    // 2. 配置GPIOA的引脚(例如:USART1的RX引脚)
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;               // 选择GPIOA的第10引脚(USART1_RX)
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;    // 模式设置为浮动输入
    GPIO_Init(GPIOA, &GPIO_InitStructure);                    // 初始化GPIOA引脚

    // 3. 配置USART1
    USART_InitStructure.USART_BaudRate = 9600;                // 设置波特率为9600
    USART_InitStructure.USART_WordLength = USART_WordLength_8b;  // 8位数据位
    USART_InitStructure.USART_StopBits = USART_StopBits_1;        // 1个停止位
    USART_InitStructure.USART_Parity = USART_Parity_No;           // 无奇偶校验
    USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; // 不使用硬件流控制
    USART_InitStructure.USART_Mode = USART_Mode_Rx;               // 只使能接收模式
    USART_Init(USART1, &USART_InitStructure);                    // 初始化USART1

    // 4. 配置DMA
    DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR;  // DMA外设地址:USART1数据寄存器
    DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)rx_buffer;        // DMA内存地址:接收数据的缓冲区
    DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralToMemory;            // 从外设到内存
    DMA_InitStructure.DMA_BufferSize = BUFFER_SIZE;                    // 设置DMA传输大小
    DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;   // 外设地址不递增
    DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;            // 内存地址递增
    DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; // 外设数据宽度:8位
    DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;    // 内存数据宽度:8位
    DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;                    // 设置为循环模式
    DMA_InitStructure.DMA_Priority = DMA_Priority_High;                // DMA优先级:高
    DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;                       // 禁止内存到内存模式
    DMA_Init(DMA1_Channel5, &DMA_InitStructure);                       // 初始化DMA1通道5(用于USART1_RX)

    // 5. 启用USART1的DMA接收
    USART_DMACmd(USART1, USART_DMAReq_Rx, ENABLE);   // 启用USART1的DMA接收请求

    // 6. 启用USART1
    USART_Cmd(USART1, ENABLE);                      // 启用USART1

    // 7. 启用DMA1通道5
    DMA_Cmd(DMA1_Channel5, ENABLE);                 // 启用DMA1通道5(启动数据接收)
}

// DMA传输完成中断处理
void DMA1_Channel5_IRQHandler(void)
{
    if (DMA_GetITStatus(DMA1_IT_TC5))  // 如果DMA传输完成
    {
        // 清除中断标志
        DMA_ClearITPendingBit(DMA1_IT_TC5);
        
        // 在此处可以处理接收到的数据,数据已经存储在rx_buffer中
    }
}

// 主程序
int main(void)
{
    // 系统初始化
    SystemInit();

    // 初始化USART和DMA
    USART_Init_DMA();

    // 启用DMA中断
    NVIC_EnableIRQ(DMA1_Channel5_IRQn); // 启用DMA1通道5中断

    // 主循环
    while (1)
    {
        // 在主循环中可以执行其他任务
        // 数据会自动传输到rx_buffer数组,DMA会在后台自动接收数据
    }
}

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐