11-DMA数据传输.md 8.8 KB

DMA 数据传输

DMA 概念

DMA(Direct Memory Access,直接存储器访问)是一种外设,可以在不需要 CPU 参与的情况下,在存储器与外设之间、存储器与存储器之间自动转运数据。

DMA 的优势

  • 解放 CPU:数据转运由 DMA 硬件自动完成,CPU 可执行其他任务
  • 高效率:硬件级数据传输,速度远快于 CPU 逐字节搬运
  • 支持循环模式:周期性自动转运数据

通道与优先级

STM32F103 有 DMA1(7 通道)和 DMA2(5 通道)。每个通道对应不同的外设请求。

DMA 优先级分为 4 级:Low < Medium < High < VeryHigh。当多个通道同时请求时,高优先级通道优先响应。

DMA 模块结构

DMA 模块框图 上图:STM32 DMA 模块框图(来源:STM32入门教程 PPT 第102页)

DMA 内部结构 上图:DMA 内部结构(源地址、目标地址、传输计数器)(来源:STM32入门教程 PPT 第103页)

MEM-to-MEM 数据转运

原理

将源内存区域的数据搬运到目标内存区域。需要使能 DMA_M2M(存储器到存储器模式),此时 DMA 从软件请求触发转为连续工作。

驱动代码 MyDMA.c(标准库)

void MyDMA_Init(uint32_t AddrA, uint32_t AddrB, uint16_t Size)
{
    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);

    DMA_InitStructure.DMA_PeripheralBaseAddr = AddrA;       // 源地址
    DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
    DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Enable;  // 地址自增
    DMA_InitStructure.DMA_MemoryBaseAddr = AddrB;           // 目标地址
    DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
    DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
    DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;      // 外设到存储器方向
    DMA_InitStructure.DMA_BufferSize = Size;                // 转运次数
    DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;           // 正常模式
    DMA_InitStructure.DMA_M2M = DMA_M2M_Enable;             // 存储器到存储器
    DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;
    DMA_Init(DMA1_Channel1, &DMA_InitStructure);

    DMA_Cmd(DMA1_Channel1, DISABLE);  // 先失能,由 Transfer 函数启动
}

void MyDMA_Transfer(void)
{
    DMA_Cmd(DMA1_Channel1, DISABLE);                    // 失能以写入传输计数器
    DMA_SetCurrDataCounter(DMA1_Channel1, MyDMA_Size);  // 设置转运次数
    DMA_Cmd(DMA1_Channel1, ENABLE);                     // 使能启动

    while (DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET);  // 等待传输完成
    DMA_ClearFlag(DMA1_FLAG_TC1);
}

MEM-to-MEM 阻塞转运 HAL

DMA_HandleTypeDef hdma_m2m;

void MyDMA_Init_HAL(uint32_t AddrA, uint32_t AddrB, uint16_t Size)
{
    __HAL_RCC_DMA1_CLK_ENABLE();

    hdma_m2m.Instance                 = DMA1_Channel1;
    hdma_m2m.Init.Direction           = DMA_MEMORY_TO_MEMORY;
    hdma_m2m.Init.PeriphInc           = DMA_PINC_ENABLE;   // 源地址自增
    hdma_m2m.Init.MemInc              = DMA_MINC_ENABLE;   // 目标地址自增
    hdma_m2m.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
    hdma_m2m.Init.MemDataAlignment    = DMA_MDATAALIGN_BYTE;
    hdma_m2m.Init.Mode                = DMA_NORMAL;
    hdma_m2m.Init.Priority            = DMA_PRIORITY_MEDIUM;
    HAL_DMA_Init(&hdma_m2m);
}

void MyDMA_Transfer_HAL(void)
{
    HAL_DMA_Start(&hdma_m2m, (uint32_t)DataA, (uint32_t)DataB, 4);
    HAL_DMA_PollForTransfer(&hdma_m2m, HAL_DMA_FULL_TRANSFER, HAL_MAX_DELAY);
}

MEM-to-MEM 中断转运(HAL)

// 全局回调
void HAL_DMA_XferCpltCallback(DMA_HandleTypeDef *hdma)
{
    // 传输完成 — 设置标志位供主循环查询
    TransferDone = 1;
}

void MyDMA_Transfer_IT_HAL(void)
{
    HAL_DMA_Start_IT(&hdma_m2m, (uint32_t)DataA, (uint32_t)DataB, 4);
}

int main(void)
{
    HAL_Init();
    MyDMA_Init_HAL((uint32_t)DataA, (uint32_t)DataB, 4);

    HAL_NVIC_SetPriority(DMA1_Channel1_IRQn, 0, 0);
    HAL_NVIC_EnableIRQ(DMA1_Channel1_IRQn);

    while (1)
    {
        if (TransferDone)
        {
            TransferDone = 0;
            // 处理转运完成后的数据
        }
    }
}

主函数

uint8_t DataA[] = {0x01, 0x02, 0x03, 0x04};
uint8_t DataB[] = {0, 0, 0, 0};

int main(void)
{
    OLED_Init();
    MyDMA_Init((uint32_t)DataA, (uint32_t)DataB, 4);

    while (1) {
        DataA[0]++; DataA[1]++; DataA[2]++; DataA[3]++;
        // 显示转运前数据
        MyDMA_Transfer();  // 启动 DMA 转运
        // 显示转运后数据
        Delay_ms(1000);
    }
}

关键配置项:

  • DMA_DIR_PeripheralSRC:外设作为源(即使两个都是存储器,M2M 模式下也如此设置)
  • DMA_PeripheralInc / DMA_MemoryInc:使能后源/目的地址自动递增
  • DMA_M2M_Enable:使能存储器到存储器模式
  • DMA_Mode_Normal:正常模式,传输一次后停止

MEM-to-Periph(DMA+ADC)

原理

ADC 每次转换完成后自动触发 DMA 请求,DMA 将 ADC 数据寄存器(ADC1->DR)的值转运到内存数组。配合 ADC 的连续转换 + 扫描模式,实现多通道自动采集。

AD.c(DMA 版本,标准库)

DMA+ADC 循环采集(HAL)

DMA_HandleTypeDef hdma_adc;

void AD_Init_HAL(void)
{
    // GPIO, ADC 时钟使能 ...(省略,同标准库)

    __HAL_RCC_DMA1_CLK_ENABLE();

    hdma_adc.Instance                 = DMA1_Channel1;
    hdma_adc.Init.Direction           = DMA_PERIPH_TO_MEMORY;
    hdma_adc.Init.PeriphInc           = DMA_PINC_DISABLE;     // ADC 数据寄存器固定
    hdma_adc.Init.MemInc              = DMA_MINC_ENABLE;      // 内存地址递增
    hdma_adc.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;  // ADC 16位
    hdma_adc.Init.MemDataAlignment    = DMA_MDATAALIGN_HALFWORD;
    hdma_adc.Init.Mode                = DMA_CIRCULAR;          // 循环模式
    hdma_adc.Init.Priority            = DMA_PRIORITY_MEDIUM;
    HAL_DMA_Init(&hdma_adc);

    // 连接 DMA 到 ADC
    __HAL_LINKDMA(&hadc, DMA_Handle, hdma_adc);
    HAL_DMA_Start(&hdma_adc, (uint32_t)&ADC1->DR, (uint32_t)AD_Value, 4);
    HAL_ADC_Start_DMA(&hadc, (uint32_t *)AD_Value, 4);
}

AD.c(DMA 版本)

uint16_t AD_Value[4];

void AD_Init(void)
{
    // ...GPIO + ADC 时钟、引脚初始化...

    // 配置规则组 4 个通道(扫描模式)
    ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);
    ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_55Cycles5);
    ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 3, ADC_SampleTime_55Cycles5);
    ADC_RegularChannelConfig(ADC1, ADC_Channel_3, 4, ADC_SampleTime_55Cycles5);

    // ADC 初始化:连续 + 扫描 + 4 通道
    ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;  // 连续转换
    ADC_InitStructure.ADC_ScanConvMode = ENABLE;        // 扫描模式
    ADC_InitStructure.ADC_NbrOfChannel = 4;

    // DMA 初始化
    DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;
    DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; // 16位
    DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;  // 外设地址固定
    DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)AD_Value;
    DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
    DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;  // 内存地址递增
    DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
    DMA_InitStructure.DMA_BufferSize = 4;
    DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;     // 循环模式
    DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;        // 外设触发,非 M2M
    DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;
    DMA_Init(DMA1_Channel1, &DMA_InitStructure);

    DMA_Cmd(DMA1_Channel1, ENABLE);
    ADC_DMACmd(ADC1, ENABLE);      // ADC 触发 DMA 信号使能
    ADC_Cmd(ADC1, ENABLE);
    // 校准...
    ADC_SoftwareStartConvCmd(ADC1, ENABLE);  // 触发一次后连续转换
}

与纯软件多通道的区别

对比项 纯软件多通道 DMA+ADC
触发方式 每次软件触发 ADC 连续转换自动触发
数据搬运 CPU 循环读取 DR 寄存器 DMA 自动将 DR 写入数组
CPU 占用 每次转换都占用 仅初始化时配置
通道扩展 每次读取一个通道 扫描模式一次转换 4 通道
模式 正常模式 循环模式,持续采集

主函数(DMA+ADC)

int main(void)
{
    OLED_Init();
    AD_Init();                      // ADC+DMA 初始化后自动连续采集

    while (1) {
        OLED_ShowNum(1, 5, AD_Value[0], 4);  // 直接读取数组已更新的结果
        OLED_ShowNum(2, 5, AD_Value[1], 4);
        OLED_ShowNum(3, 5, AD_Value[2], 4);
        OLED_ShowNum(4, 5, AD_Value[3], 4);
        Delay_ms(100);
    }
}