STM32 DAC+DMA 高精度任意波形生成:缓冲区切换毛刺的规避策略
👁 1 阅读 · 2026-08-27 · 嵌入式
在STM32上利用DAC+DMA生成高精度任意波形时,缓冲区切换(如双缓冲模式)常因时序不一致或配置不当引入输出毛刺,影响波形质量。本文深入分析毛刺产生的根源(DMA传输间隙、DAC更新时序、缓存一致性问题),并给出三种实用规避方案:基于双缓冲的预加载策略、利用定时器触发同步、以及DMA循环模式的乒乓切换优化。通过原理讲解、配置步骤和完整代码示例,帮助开发者彻底解决波形切换时的瞬态干扰,提升信号生成精度。
# 利用 STM32 的 DAC + DMA 生成高精度任意波形时,如何规避缓冲区切换的毛刺
## 1. 问题背景与毛刺根源
在嵌入式系统中,使用 STM32 的 DAC(数模转换器)配合 DMA(直接内存访问)生成任意波形(如正弦波、任意函数信号)是常见需求。DMA 负责将内存中的波形数据连续搬运到 DAC 数据寄存器,实现零 CPU 干预的波形输出。然而,当波形数据需要更新(例如切换频率、幅度或波形类型)时,若直接修改缓冲区或切换 DMA 目标地址,往往会在输出端产生短暂的毛刺(Glitch),表现为电压尖峰或异常跳变。
毛刺产生的核心原因有三:
- **DMA 传输间隙**:当 DMA 完成一个缓冲区传输并切换到下一个缓冲区时,存在短暂的停止-重启过程,若此时 DAC 仍在采样,就会因数据缺失产生输出跳变。
- **DAC 更新时序**:DAC 输出寄存器的更新通常发生在 DMA 写入数据寄存器的下一个时钟沿,若写入时机与 DAC 转换周期不同步,可能导致部分数据被覆盖或延迟。
- **缓存一致性问题**:在带缓存(如 Cortex-M7 的 D-Cache)的 MCU 上,若 DMA 读取的缓冲区被 CPU 修改,而缓存未刷新,DMA 可能读到旧数据,造成波形不连续。
## 2. 规避策略概览
针对上述根源,业界常用以下三种策略,可单独或组合使用:
- **策略一:双缓冲 + 预加载**(软件层面)——使用两个缓冲区,在 DMA 正在使用缓冲区 A 时,CPU 预先填充缓冲区 B,并通过 DMA 的传输完成中断切换,确保切换时数据已就绪。
- **策略二:定时器触发同步**(硬件层面)——利用定时器产生更新事件,触发 DAC 转换,使 DMA 传输与 DAC 采样严格同步,消除异步写入导致的毛刺。
- **策略三:DMA 循环模式 + 乒乓切换**(优化层面)——使用 DMA 的循环模式,通过修改内存地址实现无缝切换,避免 DMA 重启。
下面以 STM32F407(Cortex-M4,无 D-Cache)为例,重点讲解策略一和策略二的结合实现,因为这是最通用且效果显著的方法。
## 3. 原理详解:双缓冲与定时器同步
### 3.1 双缓冲预加载原理
双缓冲模式使用两个大小相同的缓冲区(Buffer0 和 Buffer1)。DMA 先传输 Buffer0,传输完成后触发中断,在中断服务函数(ISR)中切换 DMA 的目标地址到 Buffer1,同时 CPU 可以开始填充 Buffer0 的新数据。关键点在于:**切换动作必须在 DMA 完全停止后执行**,否则会导致数据错位。但 DMA 停止和重启的间隙会产生毛刺,因此需要结合定时器同步来掩盖这个间隙。
### 3.2 定时器触发同步原理
STM32 的 DAC 支持外部触发转换(如定时器 TRGO 事件)。配置定时器(如 TIM6)工作在 PWM 模式,产生固定频率的更新事件,作为 DAC 的触发源。这样,DAC 只在定时器触发时采样一次,DMA 则在触发前准备好数据。当 DMA 切换缓冲区时,即使有短暂停止,只要在下一个触发沿到来前完成切换,输出就不会中断。
## 4. 配置步骤(以 STM32CubeMX + HAL 库为例)
### 4.1 基本配置
1. **DAC 配置**:启用 DAC 输出通道(如 PA4),选择触发方式为“定时器触发”,触发源选择 TIM6_TRGO。
2. **定时器配置**:TIM6 时钟源设为内部时钟,预分频和自动重载值决定触发频率(即波形采样率)。设置 TIM6 的 TRGO 事件为“更新事件”。
3. **DMA 配置**:添加 DMA 流(如 DMA1_Stream5),方向为内存到外设,外设地址固定为 DAC 数据寄存器,内存地址为缓冲区首地址,模式为循环模式(或普通模式,但需在中断中切换)。数据宽度为半字(16位,因为 DAC 为 12 位)。
### 4.2 双缓冲实现
在代码中定义两个缓冲区,并启用 DMA 传输完成中断。
```c
// 缓冲区定义(示例:256点正弦波)
#define WAVE_SIZE 256
uint16_t buf0[WAVE_SIZE];
uint16_t buf1[WAVE_SIZE];
volatile uint8_t current_buf = 0; // 当前 DMA 使用的缓冲区索引
// 初始化 DMA 使用 buf0
HAL_DAC_Start_DMA(&hdac, DAC_CHANNEL_1, (uint32_t*)buf0, WAVE_SIZE, DAC_ALIGN_12B_R);
```
### 4.3 中断处理与切换
在 DMA 传输完成中断回调中,切换缓冲区并准备新数据。
```c
void HAL_DAC_ConvCpltCallbackCh1(DAC_HandleTypeDef *hdac)
{
// 切换 DMA 目标缓冲区
if (current_buf == 0) {
HAL_DAC_Stop_DMA(&hdac, DAC_CHANNEL_1); // 停止 DMA(注意:此操作会立即停止传输)
// 填充 buf1 的新波形数据(此处为示例,实际可调用波形生成函数)
generate_waveform(buf1, WAVE_SIZE, new_freq);
HAL_DAC_Start_DMA(&hdac, DAC_CHANNEL_1, (uint32_t*)buf1, WAVE_SIZE, DAC_ALIGN_12B_R);
current_buf = 1;
} else {
HAL_DAC_Stop_DMA(&hdac, DAC_CHANNEL_1);
generate_waveform(buf0, WAVE_SIZE, new_freq);
HAL_DAC_Start_DMA(&hdac, DAC_CHANNEL_1, (uint32_t*)buf0, WAVE_SIZE, DAC_ALIGN_12B_R);
current_buf = 0;
}
}
```
**注意**:上述代码中 `HAL_DAC_Stop_DMA` 和 `HAL_DAC_Start_DMA` 会停止和重启 DMA,这期间 DAC 可能无数据可转换,但得益于定时器触发,只要在下一个触发沿前完成重启,输出不会出现毛刺。实际测试中,这个间隙通常为几微秒,而定时器周期(如 10kHz 采样率)为 100 微秒,因此足够安全。
## 5. 完整代码示例(基于 HAL)
以下是一个完整的初始化函数,展示了如何配置 DAC、TIM6 和 DMA。
```c
void MX_DAC1_Init(void)
{
DAC_ChannelConfTypeDef sConfig = {0};
hdac.Instance = DAC;
HAL_DAC_Init(&hdac);
sConfig.DAC_SampleAndHold = DAC_SAMPLEANDHOLD_DISABLE;
sConfig.DAC_Trigger = DAC_TRIGGER_T6_TRGO; // 定时器6触发
sConfig.DAC_OutputBuffer = DAC_OUTPUTBUFFER_ENABLE;
HAL_DAC_ConfigChannel(&hdac, &sConfig, DAC_CHANNEL_1);
}
void MX_TIM6_Init(void)
{
TIM_MasterConfigTypeDef sMasterConfig = {0};
htim6.Instance = TIM6;
htim6.Init.Prescaler = 84-1; // 84MHz/84 = 1MHz
htim6.Init.CounterMode = TIM_COUNTERMODE_UP;
htim6.Init.Period = 100-1; // 1MHz/100 = 10kHz 采样率
HAL_TIM_Base_Init(&htim6);
sMasterConfig.MasterOutputTrigger = TIM_TRGO_UPDATE;
sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
HAL_TIMEx_MasterConfigSynchronization(&htim6, &sMasterConfig);
}
void MX_DMA_Init(void)
{
hdma_dac1.Instance = DMA1_Stream5;
hdma_dac1.Init.Channel = DMA_CHANNEL_7;
hdma_dac1.Init.Direction = DMA_MEMORY_TO_PERIPH;
hdma_dac1.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_dac1.Init.MemInc = DMA_MINC_ENABLE;
hdma_dac1.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
hdma_dac1.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
hdma_dac1.Init.Mode = DMA_NORMAL; // 使用普通模式,在中断中切换
hdma_dac1.Init.Priority = DMA_PRIORITY_HIGH;
HAL_DMA_Init(&hdma_dac1);
__HAL_LINKDMA(&hdac, DMA_Handle1, hdma_dac1);
HAL_NVIC_SetPriority(DMA1_Stream5_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(DMA1_Stream5_IRQn);
}
// 启动波形生成
void start_waveform_generation(void)
{
generate_waveform(buf0, WAVE_SIZE, 1000); // 初始频率 1kHz
HAL_TIM_Base_Start(&htim6);
HAL_DAC_Start_DMA(&hdac, DAC_CHANNEL_1, (uint32_t*)buf0, WAVE_SIZE, DAC_ALIGN_12B_R);
}
```
## 6. 注意事项与进阶优化
- **缓冲区大小与采样率匹配**:缓冲区大小应足够大,以容纳至少一个完整波形周期,否则波形会不连续。
- **DMA 停止/重启的时序**:在 ISR 中执行 `HAL_DAC_Stop_DMA` 时,确保 DAC 已停止转换(可查询状态),否则可能丢失最后一个数据。
- **缓存刷新(针对 Cortex-M7)**:若使用 STM32H7 等带 D-Cache 的芯片,在 CPU 修改缓冲区后,需调用 `SCB_CleanDCache_by_Addr` 刷新缓存,否则 DMA 可能读到旧数据。
- **使用循环模式 + 乒乓切换**:更高级的做法是使用 DMA 循环模式,通过修改 `NDTR` 寄存器和内存地址实现无缝切换,避免停止/重启。但实现复杂,建议先用双缓冲方案。
- **触发频率限制**:定时器触发频率不能超过 DAC 的最大转换速率(如 1Msps),否则数据会丢失。
## 7. 总结
通过双缓冲预加载和定时器触发同步,可以有效规避 STM32 DAC+DMA 生成任意波形时的缓冲区切换毛刺。核心思想是:**让 DMA 的切换发生在 DAC 非采样时刻**。本文提供的代码示例可直接应用于 STM32F4 系列,对于其他系列只需调整时钟和引脚配置。在实际项目中,建议先用示波器观察毛刺,再逐步调整定时器周期和缓冲区大小,以达到最佳波形质量。