## 一、STM32 DAC概述 STM32的DAC(数模转换器)可将数字量转换为模拟电压信号,相比PWM+滤波方案,DAC输出波形更平滑、无需外部滤波电路。STM32绝大多数系列(如F1、F4、L4、H7)都集成至少1个DAC,通常包含两个独立通道(如PA4、PA5),支持三角波生成、噪声生成以及DMA请求,非常适合波形合成和音频播放。 ## 二、DAC工作原理与关键参数 DAC模块将12位数字值映射为模拟电压,输出关系为: - **Vout = VREF+ × (DAC_DOR / 4096)** 其中DAC_DOR为输出数据寄存器值,VREF+为参考电压(通常接3.3V)。若开启输出缓冲,则驱动能力增强,但最小输出电压可能受限;关闭缓冲则输出阻抗较高,适合外接高阻负载或运放。 核心寄存器: - **DAC_CR**:控制使能、缓冲、DMA请求等; - **DAC_DHRx**:数据保持寄存器(8/12位,左对齐/右对齐); - **DAC_DORx**:输出数据寄存器,转换结果由此直接驱动输出。 实际开发多使用HAL库,无需直接操作寄存器,但理解原理有助于定位问题。 ## 三、硬件连接与CubeMX配置 以STM32F407为例,使用DAC通道1输出至PA4引脚。硬件连接:PA4接到示波器探头或ADC采样端,若驱动扬声器需通过功放/运放。 ### CubeMX配置步骤 1. 设置PA4为DAC_OUT1模式; 2. 使能DAC通道1,开启输出缓冲; 3. 添加定时器(如TIM6)作为DMA触发源,配置更新事件产生TRGO信号; 4. 配置DAC DMA通道,选择循环模式,数据宽度为半字(16位),内存地址递增; 5. 生成项目代码。 若希望精确控制采样率,定时器触发周期决定每个波形点的时间间隔。比如正弦波表256点,采样率48kHz,则定时器更新频率应为48kHz,即每个点间隔约20.8μs。 ## 四、波形生成:查表法输出正弦波 正弦波是基础波形,通过查表法生成查表函数: ```c #define SINE_POINTS 256 uint16_t sine_wave[SINE_POINTS]; void Generate_Sine_Wave(void) { for (int i = 0; i < SINE_POINTS; i++) { // 以3.3V为参考电压,满量程12位 float v = 3.3f * (1.0f + sinf(2 * 3.14159265f * i / SINE_POINTS)) / 2.0f; sine_wave[i] = (uint16_t)(v * 4095.0f / 3.3f); } } ``` 此表产生0~3.3V偏置为1.65V的正弦波,峰值1.65V。若需不同幅值,修改v的计算即可。 ## 五、完整代码示例:定时器触发DMA循环输出 ### 1. DAC与DMA初始化(HAL库) ```c DAC_HandleTypeDef hdac; DMA_HandleTypeDef hdma_dac; TIM_HandleTypeDef htim6; static void MX_DAC_Init(void) { hdac.Instance = DAC; HAL_DAC_Init(&hdac); DAC_ChannelConfTypeDef sConfig = {0}; sConfig.DAC_SampleAndHold = DAC_SAMPLEANDHOLD_DISABLE; sConfig.DAC_Trigger = DAC_TRIGGER_T6_TRGO; sConfig.DAC_OutputBuffer = DAC_OUTPUTBUFFER_ENABLE; HAL_DAC_ConfigChannel(&hdac, &sConfig, DAC_CHANNEL_1); } static void MX_DMA_Init(void) { __HAL_RCC_DMA1_CLK_ENABLE(); hdma_dac.Instance = DMA1_Stream5; hdma_dac.Init.Channel = DMA_CHANNEL_7; hdma_dac.Init.Direction = DMA_MEMORY_TO_PERIPH; hdma_dac.Init.PeriphInc = DMA_PINC_DISABLE; hdma_dac.Init.MemInc = DMA_MINC_ENABLE; hdma_dac.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD; hdma_dac.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD; hdma_dac.Init.Mode = DMA_CIRCULAR; hdma_dac.Init.Priority = DMA_PRIORITY_HIGH; HAL_DMA_Init(&hdma_dac); __HAL_LINKDMA(&hdac, DMA_Handle1, hdma_dac); } static void MX_TIM6_Init(void) { htim6.Instance = TIM6; htim6.Init.Prescaler = 0; htim6.Init.Period = 420; // 84MHz / (0+1) / (420+1) ≈ 200kHz? 调整为实际采样率 HAL_TIM_Base_Init(&htim6); TIM_MasterConfigTypeDef sMasterConfig = {0}; sMasterConfig.MasterOutputTrigger = TIM_TRGO_UPDATE; HAL_TIMEx_MasterConfigSynchronization(&htim6, &sMasterConfig); } ``` ### 2. 主函数启动DAC输出 ```c int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_DMA_Init(); MX_TIM6_Init(); MX_DAC_Init(); Generate_Sine_Wave(); // 启动定时器更新中断,并开启DAC通道触发 HAL_TIM_Base_Start(&htim6); HAL_DAC_Start_DMA(&hdac, DAC_CHANNEL_1, (uint32_t*)sine_wave, SINE_POINTS, DAC_ALIGN_12B_R); while (1) { // 可在此修改波形或采样率 } } ``` 上述代码配置TIM6周期为420,假设DAC时钟84MHz,则更新频率为200kHz,实际采样率200kHz。若需48kHz,设置Period = (84MHz / 48kHz) - 1 ≈ 1750。注意DMA循环模式无需中断即可持续输出。 ## 六、播放音频的扩展思路 若要播放WAV音频,可将音频数据转为12位无符号格式,用DMA循环输出,配合定时器触发实现固定采样率。建议使用缓冲区,双缓冲(Half Transfer/Transfer Complete中断)动态填充数据,避免音频中断。 ## 七、注意事项 - **参考电压**:VREF+必须稳定,否则输出波形幅度漂移,建议测量实际电压并修正计算公式。 - **输出缓冲**:开启缓冲时,DAC输出接近轨到轨但最小电压可能为0.2V左右;关闭缓冲可趋近0V,但负载能力弱,需外接高阻或运放。 - **DMA与定时器配置**:确保DMA通道与DAC请求匹配(不同芯片映射不同,如F4使用DMA1 Stream5 Channel7),且定时器TRGO选择正确。 - **波形表长度与采样率**:表长度和采样率决定输出频率,公式为:**f_out = fs / N**(如fs=200kHz, N=256 → 781Hz)。调整两者可改变频率。 - **数据对齐格式**:DMA传输数据为16位,对应DAC的12位右对齐(DAC_ALIGN_12B_R),务必保证内存和DMA宽度一致。 - **功耗与发热**:高采样率持续输出会增加CPU空闲但DMA占用总线,注意电源稳定性。 ## 八、总结 本文从STM32 DAC原理出发,讲解了通过定时器触发DMA查表输出正弦波的方法。该方案可扩展至任意周期波形和音频播放,核心在于理解DMA+定时器+DAC的协同工作。建议读者用示波器观察实际输出,调整缓冲开关和波形表,深入体会DAC的特性,为后续物联网语音播报、信号激励等应用打下基础。