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

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

一,方法思路

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

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,代码实现

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

c 复制代码
// --- 初始化函数 (在 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 持续输出流畅的正弦波信号了!

相关推荐
智者知已应修善业1 小时前
【51单片机用数码管显示流水灯的种类是按钮控制数码管加一和流水灯】2022-6-14
c语言·经验分享·笔记·单片机·嵌入式硬件·51单片机
智商偏低8 小时前
单片机之helloworld
单片机·嵌入式硬件
青牛科技-Allen9 小时前
GC3910S:一款高性能双通道直流电机驱动芯片
stm32·单片机·嵌入式硬件·机器人·医疗器械·水泵、
森焱森11 小时前
无人机三轴稳定控制(2)____根据目标俯仰角,实现俯仰稳定化控制,计算出升降舵输出
c语言·单片机·算法·架构·无人机
白鱼不小白11 小时前
stm32 USART串口协议与外设(程序)——江协教程踩坑经验分享
stm32·单片机·嵌入式硬件
S,D11 小时前
MCU引脚的漏电流、灌电流、拉电流区别是什么
驱动开发·stm32·单片机·嵌入式硬件·mcu·物联网·硬件工程
芯岭技术14 小时前
PY32F002A单片机 低成本控制器解决方案,提供多种封装
单片机·嵌入式硬件
youmdt15 小时前
Arduino IDE ESP8266连接0.96寸SSD1306 IIC单色屏显示北京时间
单片机·嵌入式硬件
嘿·嘘15 小时前
第七章 STM32内部FLASH读写
stm32·单片机·嵌入式硬件
Meraki.Zhang15 小时前
【STM32实践篇】:I2C驱动编写
stm32·单片机·iic·驱动·i2c