一、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。

四、波形生成:查表法输出正弦波

正弦波是基础波形,通过查表法生成查表函数:

#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库)

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输出

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的特性,为后续物联网语音播报、信号激励等应用打下基础。