# 利用 STM32 的 DAC + DMA 生成高精度任意波形时,如何规避缓冲区切换时的毛刺 ## 一、问题背景与毛刺根源 在嵌入式信号发生器、音频合成等应用中,STM32 的 DAC(数模转换器)配合 DMA(直接内存访问)是生成连续波形的经典方案。DMA 负责将内存中的波形数据(如正弦表)自动搬运到 DAC 数据寄存器,无需 CPU 干预,从而保证波形速率。 然而,当波形数据量较大(如高采样率、长周期波形)时,通常需要将数据分成多个缓冲区(Buffer),DMA 在缓冲区之间切换。若切换时机不当,DAC 会输出一个不连续的电压值,形成毛刺(Glitch),严重影响波形质量。 **毛刺产生的核心原因:** - DMA 传输完成中断(TC)触发时,CPU 需要重新配置 DMA 缓冲区地址,若此时 DAC 仍在输出当前缓冲区最后一个数据,而新缓冲区地址尚未生效,DAC 会短暂输出未知电平。 - 缓冲区切换时,若新缓冲区首地址的数据与旧缓冲区尾地址的数据不连续(相位跳变),也会产生电压跳变。 - 系统时钟抖动或中断响应延迟,导致切换时刻不确定,进一步加剧毛刺。 ## 二、规避策略原理 针对上述原因,业界常用三种策略,可单独或组合使用: ### 1. 双缓冲(Double Buffer)无缝切换 利用 STM32 的 DMA 双缓冲模式(如 DMA1 的 Stream 支持双缓冲),硬件自动在两个缓冲区之间交替传输,无需 CPU 干预。当 DMA 正在使用缓冲区 A 时,CPU 可预先填充缓冲区 B;当 A 传输完成,DMA 自动切换到 B,同时触发中断,CPU 再填充 A。由于切换由硬件完成,且缓冲区地址切换发生在 DMA 内部,DAC 输出不会中断,从而消除毛刺。 ### 2. DAC 输出保持与定时器同步 在 DMA 传输完成中断中,先禁用 DAC 输出(或保持输出),待新缓冲区地址配置完成后,再重新启用。但禁用/启用 DAC 本身会引入短暂延迟,因此更推荐使用定时器触发 DAC 转换,并让 DMA 在定时器更新事件时同步切换缓冲区,确保切换发生在 DAC 输出采样点之间。 ### 3. DMA 中断中的相位补偿 若无法使用双缓冲,可在 DMA 传输完成中断中,通过软件计算新缓冲区首地址与旧缓冲区尾地址的相位差,并调整新缓冲区数据,使输出连续。该方法需要额外计算,但灵活性高。 ## 三、配置步骤(以 STM32F407 为例) 以下步骤基于 STM32CubeMX 生成初始化代码,再手动添加关键配置。 ### 1. 配置 DAC 和 DMA - 在 CubeMX 中启用 DAC1 的 Output1,模式为“Connected to external pin”,触发方式选择“Timer 6 Trigger Out”或“Software Trigger”(若用定时器同步)。 - 启用 DMA1 Stream5(DAC1 通道1)或相应 Stream,方向为“Memory-to-Peripheral”,模式为“Circular”(循环模式),数据宽度为 Half Word(16位)。 - 若使用双缓冲,在 DMA 设置中启用“Double Buffer”模式,并分配两个缓冲区地址。 ### 2. 配置定时器触发 - 启用 TIM6,设置预分频和自动重载值,以确定采样率(如 1kHz 采样率,则更新频率为 1kHz)。 - 在 TIM6 配置中,选择“Update Event”作为触发输出,并连接到 DAC。 ### 3. 编写缓冲区管理代码 - 定义两个缓冲区 `bufA` 和 `bufB`,每个缓冲区包含 N 个 16 位数据点。 - 在 DMA 传输完成中断回调中,填充未使用的缓冲区。 ## 四、完整代码示例 以下代码演示了双缓冲模式下的无缝切换,并配合定时器触发。 ```c // 定义缓冲区大小和波形数据 #define BUF_SIZE 256 uint16_t bufA[BUF_SIZE]; uint16_t bufB[BUF_SIZE]; volatile uint8_t buf_index = 0; // 当前使用的缓冲区索引 // 正弦波生成函数 void generate_sine(uint16_t *buf, uint16_t size, uint32_t phase) { for (uint16_t i = 0; i < size; i++) { buf[i] = (uint16_t)(2048 + 2047 * sin(2 * M_PI * (i + phase) / size)); } } // DMA 传输完成中断回调(在 stm32f4xx_it.c 中调用) void HAL_DAC_ConvCpltCallbackCh1(DAC_HandleTypeDef *hdac) { // 切换缓冲区索引 buf_index ^= 1; // 填充下一个缓冲区(此处为演示,实际可预生成) if (buf_index == 0) { generate_sine(bufA, BUF_SIZE, 0); // 注意:实际应使用相位累加 } else { generate_sine(bufB, BUF_SIZE, 0); } } int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_DAC_Init(); MX_DMA_Init(); MX_TIM6_Init(); // 初始化缓冲区 generate_sine(bufA, BUF_SIZE, 0); generate_sine(bufB, BUF_SIZE, 0); // 配置 DMA 双缓冲 HAL_DAC_Start_DMA(&hdac, DAC_CHANNEL_1, (uint32_t*)bufA, (uint32_t*)bufB, BUF_SIZE, DAC_ALIGN_12B_R); // 启动定时器触发 HAL_TIM_Base_Start(&htim6); while (1) { // 主循环可做其他任务 } } ``` **关键点说明:** - `HAL_DAC_Start_DMA` 的第三个和第四个参数分别是两个缓冲区地址,DMA 会自动交替传输。 - 在回调中,我们填充刚使用完的缓冲区(即 `buf_index` 切换后的新缓冲区),但注意回调触发时,DMA 已经切换到另一个缓冲区,因此填充的是当前未使用的缓冲区,不会冲突。 - 若要生成连续相位波形,需在回调中维护一个全局相位变量,并基于该相位生成数据,避免每次从 0 开始导致相位跳变。 ## 五、注意事项 - **缓冲区大小与采样率匹配**:缓冲区大小应足够大,以容纳至少一个完整波形周期,否则波形不连续。 - **中断优先级**:DMA 中断优先级应设置为最高(或足够高),确保回调及时执行,避免缓冲区未填充导致 DMA 读取空数据。 - **内存对齐**:缓冲区地址建议按 4 字节对齐,以提高 DMA 访问效率。 - **相位连续性**:若生成非周期波形,需在回调中计算相位累加,确保数据连续。 - **双缓冲模式限制**:并非所有 STM32 系列都支持 DMA 双缓冲,需查阅参考手册。若不支持,可采用“乒乓缓冲”软件实现,但需注意切换时机。 - **硬件滤波**:在 DAC 输出端添加低通滤波器(如 RC 滤波),可进一步抑制残余毛刺。 ## 六、总结 通过合理利用 STM32 的 DMA 双缓冲模式,并配合定时器触发,可以彻底消除缓冲区切换时的毛刺,实现高精度任意波形输出。本文提供的原理和代码示例,可帮助开发者快速落地。在实际项目中,还需根据具体芯片型号和需求调整配置,但核心思路一致。