# 引言 在嵌入式信号生成领域,STM32 的 DAC(数模转换器)配合 DMA(直接内存访问)是实现高效、连续波形输出的黄金组合。然而,当追求高精度(如 12 位分辨率)和低失真时,开发者常遇到两个棘手问题:**缓冲区分割不当导致波形不连续**,以及 **DMA 传输时序抖动影响输出稳定性**。本文将从原理出发,结合实战代码,分享解决这些问题的系统性技巧。 # 一、DAC+DMA 工作原理与挑战 ## 1.1 基本架构 STM32 的 DAC 模块支持定时器触发,DMA 可在触发后自动从内存搬运数据到 DAC 数据寄存器(DHRx),无需 CPU 干预。典型配置: - **定时器**:产生固定频率的触发事件(如 TIM6 更新事件)。 - **DMA**:在触发事件后,将波形数据表逐点传输至 DAC。 - **DAC**:转换数据并输出模拟电压。 ## 1.2 核心挑战 - **缓冲区分割**:若使用单缓冲区,DMA 传输完一轮后需重新配置,期间输出中断,产生波形毛刺。 - **时序抖动**:DMA 中断处理、内存访问冲突或缓存未刷新,导致相邻采样点间隔不均匀,引起频谱杂散。 # 二、缓冲区分割策略:环形与双缓冲 ## 2.1 环形缓冲区(Circular Mode) DMA 支持循环模式(Circular),当传输计数归零后自动重载初始地址,实现无缝循环。这是最简单的方式,但存在隐患:**当 DMA 正在读取缓冲区尾部时,若 CPU 同时更新缓冲区头部,可能造成数据撕裂**。 **适用场景**:波形数据固定不变(如标准正弦波)。 ## 2.2 双缓冲区(Double Buffer Mode) 针对动态波形(如频率可调、实时合成),推荐使用双缓冲区。DMA 在传输完当前缓冲区后,自动切换至另一缓冲区,同时触发中断,CPU 可在中断中更新已完成的缓冲区。 **优势**: - 消除数据竞争,保证数据一致性。 - 中断处理时间充裕,不阻塞波形输出。 **实现要点**: - 配置 DMA 为双缓冲模式(需支持该特性的 DMA 控制器,如 STM32F4 系列)。 - 使用 `DMA_SetCurrentTarget` 或类似 API 切换目标。 # 三、消除时序抖动的关键技巧 ## 3.1 合理配置 DMA 中断优先级 DMA 传输完成中断应设置为**高优先级**(如抢占优先级 1),确保及时切换缓冲区。但需避免与更关键的中断(如系统滴答)冲突。建议: - 将 DMA 中断设为抢占优先级 1,子优先级 0。 - 在中断服务函数中仅做快速操作(如切换标志位),避免耗时处理。 ## 3.2 双缓冲切换同步 使用 DMA 的传输完成中断(TC)来触发缓冲区切换。在中断中,通过检查当前活动缓冲区索引,更新另一个缓冲区数据。关键点: - 在中断中先读取 DMA 的当前目标寄存器(如 `DMA_GetCurrentMemoryTarget`),确定哪个缓冲区正在被使用。 - 更新另一个缓冲区时,确保写入操作在 DMA 读取之前完成,避免撕裂。 ## 3.3 缓存一致性处理(适用于带 Cache 的 MCU) 若使用 STM32H7 等带 D-Cache 的型号,DMA 访问内存时可能绕过 Cache,导致 CPU 写入的数据未同步。必须: - 在更新缓冲区前,调用 `SCB_CleanDCache_by_Addr` 将脏数据写回内存。 - 在 DMA 读取前,调用 `SCB_InvalidateDCache_by_Addr` 使 Cache 失效,确保 DMA 看到最新数据。 ## 3.4 避免内存访问冲突 DMA 与 CPU 同时访问同一总线时可能产生等待周期,导致抖动。建议: - 将波形缓冲区放置在 **SRAM1**(若 MCU 有多个 SRAM),并配置 DMA 使用独立的存储器接口。 - 使用 `__ALIGN(32)` 对齐缓冲区,提高访问效率。 # 四、完整代码示例(以 STM32F407 为例) 以下代码演示使用 TIM6 触发 DAC,DMA 双缓冲模式生成 1kHz 正弦波,采样率 100kHz。 ```c // 波形缓冲区定义(双缓冲) #define SAMPLE_NUM 100 uint16_t buf1[SAMPLE_NUM] __ALIGN(32); uint16_t buf2[SAMPLE_NUM] __ALIGN(32); // 初始化正弦波数据 void GenerateSineWave(uint16_t* buf, uint16_t amplitude, uint16_t offset) { for (int i = 0; i < SAMPLE_NUM; i++) { float angle = 2.0f * 3.14159f * i / SAMPLE_NUM; buf[i] = (uint16_t)(offset + amplitude * (sinf(angle) + 1.0f) / 2.0f); } } // DMA 中断服务函数(在 stm32f4xx_it.c 中) void DMA1_Stream5_IRQHandler(void) { if (DMA_GetITStatus(DMA1_Stream5, DMA_IT_TCIF5)) { DMA_ClearITPendingBit(DMA1_Stream5, DMA_IT_TCIF5); // 判断当前 DMA 正在使用哪个缓冲区 uint32_t currentTarget = DMA_GetCurrentMemoryTarget(DMA1_Stream5); if (currentTarget == DMA_Memory_0) { // 正在使用 buf1,则更新 buf2 GenerateSineWave(buf2, 2048, 2048); // 示例:重新生成波形(可改为动态频率) } else { GenerateSineWave(buf1, 2048, 2048); } } } // 初始化 DAC、DMA、定时器 void DAC_DMA_Init(void) { // 1. 使能时钟 RCC_APB1PeriphClockCmd(RCC_APB1Periph_DAC | RCC_APB1Periph_TIM6, ENABLE); RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA1 | RCC_AHB1Periph_GPIOA, ENABLE); // 2. 配置 PA4 为 DAC 输出 GPIO_InitTypeDef GPIO_InitStruct; GPIO_InitStruct.Pin = GPIO_Pin_4; GPIO_InitStruct.Mode = GPIO_Mode_AN; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_Init(GPIOA, &GPIO_InitStruct); // 3. 配置 DAC 通道1,定时器触发 DAC_InitTypeDef DAC_InitStruct; DAC_InitStruct.DAC_Trigger = DAC_Trigger_T6_TRGO; DAC_InitStruct.DAC_OutputBuffer = DAC_OutputBuffer_Enable; DAC_Init(DAC_Channel_1, &DAC_InitStruct); DAC_Cmd(DAC_Channel_1, ENABLE); // 4. 配置 DMA1 Stream5 为双缓冲模式 DMA_InitTypeDef DMA_InitStruct; DMA_InitStruct.DMA_Channel = DMA_Channel_7; // DAC1 映射到 Stream5 Channel7 DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)&DAC->DHR12R1; DMA_InitStruct.DMA_Memory0BaseAddr = (uint32_t)buf1; DMA_InitStruct.DMA_Memory1BaseAddr = (uint32_t)buf2; DMA_InitStruct.DMA_DIR = DMA_DIR_MemoryToPeripheral; DMA_InitStruct.DMA_BufferSize = SAMPLE_NUM; DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable; DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable; DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; DMA_InitStruct.DMA_Mode = DMA_Mode_Circular; // 循环模式,但配合双缓冲 DMA_InitStruct.DMA_Priority = DMA_Priority_High; DMA_InitStruct.DMA_FIFOMode = DMA_FIFOMode_Disable; DMA_Init(DMA1_Stream5, &DMA_InitStruct); // 使能双缓冲模式 DMA_DoubleBufferModeConfig(DMA1_Stream5, (uint32_t)buf2, DMA_Memory_1); DMA_DoubleBufferModeCmd(DMA1_Stream5, ENABLE); // 5. 使能 DMA 传输完成中断 DMA_ITConfig(DMA1_Stream5, DMA_IT_TC, ENABLE); NVIC_InitTypeDef NVIC_InitStruct; NVIC_InitStruct.NVIC_IRQChannel = DMA1_Stream5_IRQn; NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0; NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStruct); // 6. 配置定时器 TIM6 触发频率 100kHz TIM_TimeBaseInitTypeDef TIM_InitStruct; TIM_InitStruct.TIM_Prescaler = 84 - 1; // 84MHz/84 = 1MHz TIM_InitStruct.TIM_CounterMode = TIM_CounterMode_Up; TIM_InitStruct.TIM_Period = 10 - 1; // 1MHz/10 = 100kHz TIM_TimeBaseInit(TIM6, &TIM_InitStruct); TIM_SelectOutputTrigger(TIM6, TIM_TRGO_Update); // 7. 启动 DMA、定时器 DMA_Cmd(DMA1_Stream5, ENABLE); TIM_Cmd(TIM6, ENABLE); // 预填充两个缓冲区 GenerateSineWave(buf1, 2048, 2048); GenerateSineWave(buf2, 2048, 2048); } ``` **代码说明**: - 使用双缓冲模式,DMA 自动在 buf1 和 buf2 间切换。 - 在中断中根据当前目标更新另一个缓冲区,实现动态波形更新。 - 定时器触发频率决定采样率,此处为 100kHz。 # 五、注意事项与调试建议 - **缓冲区大小**:应满足奈奎斯特采样定理,且为 2 的幂次方以优化 DMA 传输。 - **中断服务函数**:避免调用 `sinf` 等浮点函数,可预计算或查表,否则会阻塞中断导致抖动。 - **DMA 模式**:双缓冲必须配合循环模式,否则传输一轮后停止。 - **调试技巧**:使用逻辑分析仪观察 DAC 输出,对比频谱分析,若出现杂散,优先检查缓存一致性和中断优先级。 - **功耗考虑**:若不需要动态更新,可关闭双缓冲中断,减少功耗。 # 结语 通过合理的缓冲区分割(双缓冲)和时序抖动消除技巧(中断优先级、缓存同步、内存对齐),STM32 的 DAC+DMA 能输出高精度、低抖动的任意波形。本文提供的代码框架可直接应用于信号发生器、音频播放等场景。实际项目中,还需根据具体 MCU 型号调整寄存器配置,但核心思路通用。希望这些技巧能助你在嵌入式波形生成领域游刃有余。