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 负载几乎为零。本文提供了从原理到实现的完整指南,开发者可在此基础上扩展多通道、动态波形更新等功能,适用于信号发生器、音频播放等场景。