STM32H7 零 CPU 干预波形生成:MDMA 与 DAC 的深度结合

引言

在嵌入式信号发生器中,DAC 常用于输出模拟波形。传统方法中,CPU 需在每个采样点将数据写入 DAC,或使用 DMA 但受限于通道数量与灵活性。STM32H7 系列凭借其强大的 MDMA 控制器,支持内存到外设的高速搬运,且可链接多个 DMA 请求,实现复杂的数据流控制。本文将展示如何利用 MDMA 与 DAC、定时器协同,生成任意波形(如正弦波、自定义序列),全程无需 CPU 干预,大幅降低处理器负载。

原理剖析

1. 系统架构与数据流

  • DAC:STM32H7 内置 12 位 DAC,支持双通道,可配置为独立或同步模式。数据写入 DAC->DHR12R1 寄存器后,经转换输出模拟电压。
  • MDMA:不同于普通 DMA,MDMA 支持内存到内存、内存到外设的搬运,且具备 64 位带宽、多块传输和链表功能。其关键特性是可由外设触发(如定时器事件),并支持 4x4 字节的突发传输,适合高速数据流。
  • 定时器:作为采样时钟,定时器溢出事件触发 MDMA 搬运一个样本,从而控制波形频率。

数据流路径:内存(波形表) -> MDMA -> DAC->DHR12R1 -> 模拟输出。MDMA 每次传输后,DAC 自动转换,无需 CPU 介入。

2. MDMA 与 DAC 的握手机制

MDMA 可通过硬件请求线连接到 DAC 的 DMA 请求(DAC1_CH1_DMA_REQ)。当 DAC 的 DMA 使能且数据寄存器为空时,产生请求,MDMA 响应并搬运下一个数据。为精确控制采样率,通常使用定时器触发 DAC 转换,而非 DAC 自身连续转换。配置为:

  • 定时器 PWM 输出或更新事件,连接到 DAC 的触发输入(如 TIM6_TRGO)。
  • DAC 触发模式设为定时器事件,每次事件产生时,DAC 从 DHR 寄存器锁存数据并开始转换,同时发出 DMA 请求。
  • MDMA 在收到请求后,从内存读取下一个样本并写入 DHR,准备下一次转换。

此方式下,CPU 仅需初始化,后续波形生成完全自动化。

配置步骤(以 STM32H743 为例)

使用 STM32CubeMX 进行初始化,以下为关键配置:

1. 时钟配置

  • 将系统时钟配置为 400MHz(H743 最高),APB2 定时器时钟 100MHz,DAC 时钟源选择 APB2。

2. DAC 配置

  • 启用 DAC1 通道1,输出缓冲使能(提高驱动能力)。
  • 触发方式选择 Timer 6 Trigger Out event(需先配置 TIM6)。
  • DMA 设置:添加 DMA 请求,选择 MDMA 作为控制器,方向 Memory-to-Peripheral,外设地址固定为 DAC1_DHR12R1,内存地址为波形表数组。
  • 数据宽度:外设半字(16位),内存半字,因为 DAC 数据为 12 位右对齐。

3. 定时器 TIM6 配置

  • 时钟源内部,预分频器设置以得到所需采样频率。例如,若 APB2 定时器时钟 100MHz,目标采样率 100kHz,则预分频 1000-1,自动重载 1-1(即每 1 个计数周期触发)。
  • 选择 Update Event 作为触发输出(TRGO)。

4. MDMA 配置(CubeMX 中)

  • 在 DMA 设置中,选择 MDMA 通道,配置如下:
    • 请求:DAC1_CH1
    • 方向:内存到外设
    • 优先级高
    • 数据宽度:半字
    • 突发:单次(或 4 拍,视性能)
    • 内存地址递增,外设地址固定
  • 启用 MDMA 的中断(可选,用于传输完成回调)。

代码实现

1. 波形表生成

在 main.c 中定义波形表,例如 256 点正弦波:

#define WAVE_SIZE 256
uint16_t wave_table[WAVE_SIZE];

void generate_sine_wave(void) {
    for (int i = 0; i < WAVE_SIZE; i++) {
        // 12位 DAC,0-4095,中心点 2048
        wave_table[i] = (uint16_t)(2048 + 2047 * sin(2 * M_PI * i / WAVE_SIZE));
    }
}

2. MDMA 配置代码(CubeMX 生成后补充)

CubeMX 会生成基础代码,但 MDMA 的链表或循环模式需手动调整。以下为关键部分:

// 初始化 MDMA 句柄
MDMA_HandleTypeDef hmdma_dac1_ch1;

void MX_MDMA_Init(void) {
    hmdma_dac1_ch1.Instance = MDMA_Channel0;
    hmdma_dac1_ch1.Init.Request = MDMA_REQUEST_DAC1_CH1;
    hmdma_dac1_ch1.Init.TransferTriggerMode = MDMA_REPEAT_BLOCK_TRANSFER; // 循环模式
    hmdma_dac1_ch1.Init.Priority = MDMA_PRIORITY_HIGH;
    hmdma_dac1_ch1.Init.Endianness = MDMA_LITTLE_ENDIAN;
    hmdma_dac1_ch1.Init.SourceInc = MDMA_SRC_INC_HALFWORD;  // 源地址递增半字
    hmdma_dac1_ch1.Init.DestinationInc = MDMA_DEST_INC_DISABLE; // 目的地址固定
    hmdma_dac1_ch1.Init.SourceDataSize = MDMA_SRC_DATASIZE_HALFWORD;
    hmdma_dac1_ch1.Init.DestDataSize = MDMA_DST_DATASIZE_HALFWORD;
    hmdma_dac1_ch1.Init.DataAlignment = MDMA_DATAALIGN_PACKEN;
    hmdma_dac1_ch1.Init.BufferTransferLength = WAVE_SIZE; // 传输块大小
    hmdma_dac1_ch1.Init.SyncMode = MDMA_SYNC_FREE;
    hmdma_dac1_ch1.Init.SyncPolarity = MDMA_SYNC_RISING;
    hmdma_dac1_ch1.Init.SyncRequest = MDMA_SYNC_DAC1_CH1; // 由 DAC 请求同步
    HAL_MDMA_Init(&hmdma_dac1_ch1);

    // 配置循环模式:使用链表,将块传输链接到自身
    MDMA_LinkNodeTypeDef linkNode;
    linkNode.NextNode = &linkNode; // 自链接实现循环
    linkNode.BlockSize = WAVE_SIZE;
    linkNode.SrcAddr = (uint32_t)wave_table;
    linkNode.DstAddr = (uint32_t)&DAC1->DHR12R1;
    linkNode.TransferConfig = 0; // 默认配置
    HAL_MDMA_ConfigLinkNode(&hmdma_dac1_ch1, &linkNode);
    HAL_MDMA_EnableLinkNode(&hmdma_dac1_ch1, &linkNode);
}

注意:CubeMX 的 MDMA 初始化可能不包含链表配置,需手动添加。实际应用中,也可使用普通模式并配合 DMA 循环模式,但 MDMA 链表更灵活。

3. 启动波形生成

int main(void) {
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_DAC1_Init();
    MX_TIM6_Init();
    MX_MDMA_Init();

    generate_sine_wave();

    // 启动 DAC 的 DMA 请求
    HAL_DAC_Start_DMA(&hdac1, DAC_CHANNEL_1, (uint32_t*)wave_table, WAVE_SIZE, DAC_ALIGN_12B_R);
    // 注意:HAL_DAC_Start_DMA 会配置普通 DMA,但这里我们使用 MDMA,因此需手动启动 MDMA 和定时器
    // 替代方案:直接启动 MDMA 和 DAC
    HAL_MDMA_Start(&hmdma_dac1_ch1, (uint32_t)wave_table, (uint32_t)&DAC1->DHR12R1, WAVE_SIZE, 0);
    HAL_DAC_Start(&hdac1, DAC_CHANNEL_1);
    HAL_TIM_Base_Start(&htim6);

    while (1) {
        // CPU 可执行其他任务,波形自动生成
    }
}

注意:HAL_DAC_Start_DMA 会占用普通 DMA,与 MDMA 冲突,因此应使用 HAL_DAC_Start 启动 DAC,并手动启动 MDMA。

注意事项

  • 数据对齐:DAC 数据寄存器为 12 位右对齐,写入时需左移 4 位或使用 16 位数据,但 DAC 会忽略高 4 位,因此直接写入 12 位值即可。
  • MDMA 与 DMA 区别:MDMA 的请求线可能不同于普通 DMA,需查阅数据手册确认 DAC 的 MDMA 请求映射。
  • 缓存一致性:若波形表位于 SRAM,且 CPU 修改后需刷新缓存(如使用 SCB_CleanDCache),否则 MDMA 可能读到旧数据。
  • 定时器频率:采样率 = 定时器时钟 / (预分频+1) / (自动重载+1),确保不超过 DAC 最大转换速率(STM32H7 可达 1MSPS)。
  • 循环模式:使用链表自链接实现循环传输,否则传输一次后停止。
  • 中断处理:若需动态改变波形,可在 MDMA 传输完成中断中修改链表地址或波形表内容,但需注意时序。

总结

通过 MDMA 与 DAC 的协同,STM32H7 可以高效生成任意波形,CPU 负载几乎为零。本文提供了从原理到实现的完整指南,开发者可在此基础上扩展多通道、动态波形更新等功能,适用于信号发生器、音频播放等场景。