STM32外设DAC实战指南:使用DMA输出正弦波

STM32外设DA实战-DAC + DMA 输出正弦波模板

  • 一,方法思路
  • 二,CubeMX配置
  • 三,代码实现
  • 1,生成正弦波查找表
  • 2,代码实现
  • 一,方法思路

    DAC 的一个常见应用是产生任意波形,比如平滑的正弦波。如果让 CPU 频繁计算正弦值并手动更新 DAC 输出,会非常耗费 CPU 资源且难以保证输出频率的精确和稳定。这时,再次请出我们的老朋友:定时器 和 DMA。

    思路与 ADC 的定时器触发采样类似,但方向相反:
    1,生成波形查找表 (Lookup Table – LUT): 在内存中预先计算并存储一个完整周期的正弦波对应的离散数字值(例如 100 个点),形成一个数组。
    2,定时器作为"节拍器": 配置一个定时器(如 TIM6 或 TIM7,它们通常有连接到 DAC 的触发输出)以固定的频率产生触发信号 (TRGO)。这个频率决定了输出正弦波的频率。
    3,DAC 听从"节拍器"指挥: 配置 DAC,使其由定时器的 TRGO 事件触发转换。
    4,DMA 自动"喂数据": 配置 DMA 通道,在每次接收到定时器触发信号后,自动从内存中的正弦波查找表里取出下一个样本点,写入 DAC 的数据保持寄存器 (DHR)。DMA 设置为循环模式,当读取完查找表的最后一个点后,自动回到开头继续读取,从而循环输出正弦波。
    5,CPU “袖手旁观”: 一旦初始化完成,整个波形输出过程完全由 定时器 + DAC + DMA 硬件自动完成,CPU 基本无需干预。

    类比: 你预先把一首歌的乐谱 (正弦波查找表) 交给一个自动翻页机 (DMA)。然后设置一个节拍器 (Timer) 控制一个演奏机器人 (DAC)。节拍器每响一次,自动翻页机就把乐谱的下一个音符喂给机器人,机器人立刻演奏出来。整个过程自动化进行,你只需要在开始时启动它们。

    二,CubeMX配置

    选择DAC的输出通道OUT1,将DMA配置为循环,16位模式

    在参数设置中,选择定时器6事件溢出

    配置定时器6

    配置完成DAC输出,可以看到PA4引脚是DAC输出引脚,输出模拟信号

    现在DAC可以输出模拟信号了,我们如何采集呢?
    前面我们用ADC1的通道10采集滑动变阻器的模拟电压,这里我们可以打开ADC1多通道模式(再打开通道4,采集DAC输出的模拟信号)

    下面IN4通道变红,说明ADC1的输入4和DAC的输出4冲突,这个引脚已经给DAC使用了,不能再同时给ADC使用了


    既然打开了ADC的多通道,我们就可以使能多通道扫描,这样ADC就会循环扫描我们选择的通道

    使能的ADC的多通道,就要对每个通道进行配置:首先将循环的通道改为2,然后就会自动跳出需要设置的2个通道

    对于ADC的DMA配置

    用同一个ADC读取两个通道数据到同一个数组buffer,我们只需读取偶数位就能得到第二个通道的数据

    配置总结
    1,配置定时器 (如 TIM6):
    1.1启用定时器,设置时钟源。
    1.2计算并设置 Prescaler (PSC) 和 Period (ARR) 以获得所需的采样点输出频率 (注意:这不是最终的正弦波频率)。
    重要关系: 正弦波频率 = 定时器触发频率 / 每个周期的采样点数
    例如,要输出 1kHz 的正弦波,且查找表有 100 个点 (SINE_SAMPLES = 100),则定时器的触发频率需要是 1kHz * 100 = 100kHz。 你需要根据你的系统时钟计算出能产生 100kHz 触发频率的 PSC 和 ARR 组合。
    1.3将 Trigger Output (TRGO) 设置为 “Update Event”。
    2,配置 DAC:
    2.1启用 DAC 通道 (如 Channel 1)。
    2.2设置 Output Buffer 为 Enable (通常推荐)。
    2.3设置 Trigger 为触发 DAC 的那个定时器的 TRGO 事件,例如 “Timer 6 Trigger Out event”。
    3,配置 DMA (在 DAC 的 DMA Settings 页):
    3.1为 DAC 通道添加 DMA 请求 (Add DMA Request),选择一个 DMA 通道。
    3.2设置 Direction 为 Memory to Peripheral (数据从内存流向外设)。
    3.3设置 Mode 为 Circular (循环读取查找表)。
    3.4设置 Peripheral 和 Memory 的 Data Width:
    Peripheral 通常是 Half Word (16位),因为 DAC 数据寄存器通常只需要写入 12 位或 8 位。
    Memory 通常也设置为 Half Word (16位),以匹配我们 uint16_t SineWave[] 数组的元素大小。
    3.5确保 Memory 地址是递增的 (Increment Address: Memory)。
    3.6Peripheral 地址不递增 (Increment Address: Peripheral – Disabled)。
    4,NVIC 配置: 对于纯 DAC 输出,通常不需要启用 DAC 或 DMA 的中断。

    DMA 数据宽度说明: 注意这里与 ADC 的 DMA 配置不同。因为 DAC 数据寄存器通常只需要写入有效数据位(如 12 位),并且我们的查找表是 uint16_t 类型,所以 DMA 的外设和内存宽度都设置为 Half Word (16位) 是最自然、最高效的配置。

    三,代码实现

    1,生成正弦波查找表

    首先,我们需要用代码生成包含正弦波数据的数组。以下代码来自 adc_app.c

    // --- 全局变量 --- 
    #define SINE_SAMPLES 100    // 一个周期内的采样点数
    #define DAC_MAX_VALUE 4095 // 12 位 DAC 的最大数字值 (2^12 - 1)
    
    uint16_t SineWave[SINE_SAMPLES]; // 存储正弦波数据的数组
    
    // --- 生成正弦波数据的函数 ---
    /**
     * @brief 生成正弦波查找表
     * @param buffer: 存储波形数据的缓冲区指针
     * @param samples: 一个周期内的采样点数
     * @param amplitude: 正弦波的峰值幅度 (相对于中心值)
     * @param phase_shift: 相位偏移 (弧度)
     * @retval None
     */
    void Generate_Sine_Wave(uint16_t* buffer, uint32_t samples, uint16_t amplitude, float phase_shift)
    {
      // 计算每个采样点之间的角度步进 (2*PI / samples)
      float step = 2.0f * 3.14159f / samples; 
      
      for(uint32_t i = 0; i < samples; i++)
      {
        // 计算当前点的正弦值 (-1.0 到 1.0)
        float sine_value = sinf(i * step + phase_shift); // 使用 sinf 提高效率
    
        // 将正弦值映射到 DAC 的输出范围 (0 - 4095)
        // 1. 将 (-1.0 ~ 1.0) 映射到 (-amplitude ~ +amplitude)
        // 2. 加上中心值 (DAC_MAX_VALUE / 2),将范围平移到 (Center-amp ~ Center+amp)
        buffer[i] = (uint16_t)((sine_value * amplitude) + (DAC_MAX_VALUE / 2.0f));
        
        // 确保值在有效范围内 (钳位)
        if (buffer[i] > DAC_MAX_VALUE) buffer[i] = DAC_MAX_VALUE;
        // 由于浮点计算精度问题,理论上不需要检查下限,但加上更健壮
        // else if (buffer[i] < 0) buffer[i] = 0; 
      }
    }
    

    逻辑分解:

    1,参数定义: samples 决定了波形的平滑度(点数越多越平滑),amplitude 控制了波形的峰值(相对于中心值),phase_shift 可以调整波形的起始相位。
    2,计算步进: step 计算出每个采样点对应的角度增量。
    3,循环计算: 遍历所有采样点。
    使用 sinf() 函数 (单精度浮点正弦,通常比 sin() 快) 计算当前点的正弦值 (-1.0 到 1.0)。
    映射与平移: 这是关键。将 sine_value 乘以 amplitude 得到幅度缩放后的值。然后加上 DAC_MAX_VALUE / 2.0f (中心值,大约是 2047.5),将波形整体向上平移,使其中心对准 DAC 输出范围的中点。最终结果被转换为 uint16_t
    钳位 (Clamping) (可选但推荐): 由于浮点计算可能存在微小误差,最好检查计算结果是否超出 DAC 的有效范围 (0 ~ 4095),如果超出则强制限制在边界值。

    2,代码实现

    完成配置后,只需要在代码中调用生成函数和启动函数即可:

    // --- 初始化函数 (在 main 函数或外设初始化后调用) ---
    void dac_sin_init(void)
    {
        // 1. 生成正弦波查找表数据
        //     amplitude = DAC_MAX_VALUE / 2 产生最大幅度的波形 (0-4095)
        Generate_Sine_Wave(SineWave, SINE_SAMPLES, DAC_MAX_VALUE / 2, 0.0f);
        
        // 2. 启动触发 DAC 的定时器 (例如 TIM6)
        HAL_TIM_Base_Start(&htim6); // htim6 是 TIM6 的句柄
        
        // 3. 启动 DAC 通道并通过 DMA 输出查找表数据
        //    hdac: DAC 句柄
        //    DAC_CHANNEL_1: 要使用的 DAC 通道
        //    (uint32_t *)SineWave: 查找表起始地址 (HAL 库常需 uint32_t*)
        //    SINE_SAMPLES: 查找表中的点数 (DMA 传输单元数)
        //    DAC_ALIGN_12B_R: 数据对齐方式 (12 位右对齐)
        HAL_DAC_Start_DMA(&hdac, DAC_CHANNEL_1, (uint32_t *)SineWave, SINE_SAMPLES, DAC_ALIGN_12B_R);
    }
    
    // --- 无需后台处理任务 --- 
    // 一旦 dac_sin_init 调用完成,硬件会自动循环输出波形
    // adc_task() 中可以移除 dac 相关的处理
    

    逻辑分解:

    1,Generate_Sine_Wave(…): 调用我们之前定义的函数,填充 SineWave 数组。这里设置 amplitudeDAC_MAX_VALUE / 2,使得生成的波形能覆盖 DAC 的整个输出范围 (近似 0V 到 Vref)。
    2,HAL_TIM_Base_Start(&htim6);: 启动作为触发源的定时器。定时器会按照预设频率开始产生 TRGO 信号。
    3,HAL_DAC_Start_DMA(…): 这是启动 DAC 输出的关键。它会:
    3.1启用指定的 DAC 通道 (DAC_CHANNEL_1)。
    3.2配置并启动 DMA 通道,使其源地址指向 SineWave 数组的开头,目标地址指向 DAC 的数据寄存器。
    3.3DMA 会在每次接收到定时器触发信号时,从 SineWave 数组读取一个 uint16_t 值(因为配置为 Half Word),根据指定的对齐方式 (DAC_ALIGN_12B_R – 12 位右对齐) 写入 DAC 数据寄存器。
    3.4由于 DMA 设置为循环模式,读取完 SINE_SAMPLES 个点后会自动回到数组开头,无限循环。

    之后,无需 CPU 干预(不用循环遍历,只放在while循环之前初始化一次),DAC 就会在定时器的精确控制下,通过 DMA 持续输出流畅的正弦波信号了!

    作者:百里东风

    物联沃分享整理
    物联沃-IOTWORD物联网 » STM32外设DAC实战指南:使用DMA输出正弦波

    发表回复