STM32外设DA实战-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
数组。这里设置 amplitude
为 DAC_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 持续输出流畅的正弦波信号了!