简介:面向STM32G474嵌入式开发者的DAC+DMA波形发生Demo,基于NUCLEO-G474RE开发板,使用STM32CubeMX生成初始化代码、Keil5编译下载。例程通过DAC与DMA协同输出波形,并预留频率调整、波形选择、幅度调制三类接口;频率由定时器TIM6的分频寄存器PSC与重载值ARR控制,波形点数由WAVE_SIZE宏定义,修改wave[]数组即可改变波形种类和幅值,用户无需手动配置底层寄存器,即可快速验证不同波形输出。借助这三个接口,可快速搭建从正弦波到任意自定义波形的发生器原型。压缩包共1247个文件,其中C源文件676个、H头文件274个,构成完整可编译工程,另有汇编文件、文本说明、IAR与Keil链接配置、CubeMX的IOC引脚配置、HEX烧录文件以及ARM官方DSP数学库等,整体约31.31MB。目前已有380人学习下载,对想深入了解STM32G4系列DAC转换、DMA循环搬运和定时器触发同步机制的开发者,是一份结构清晰、二次开发友好的参考资料。
1. 给 G474RE 做波形发生,CPU 应该在旁边看,而不是亲自搬数据
G474RE 这类带 FPU 和硬件协处理器的 MCU,很容易让人把波形生成写成“定时中断里算一个点、写一次 DAC”。跑 170 MHz 不算错,但中断里做三角函数、查表、改地址,性能其实都耗在搬运上。DAC+DMA 的思路是把整个波形放进数组,让 DMA 按节拍把数据连续投递给 DAC 的数据保持寄存器,CPU 只负责起停和改参数。这套做法在信号源、逆变电源、超声驱动以及需要多路同步波形的场景里是常见方案。这个 DEMO 会把 G474RE 的 DAC1 通道配上 DMA 循环模式,用定时器触发输出 1 kHz 正弦波,并把每一步的参数和坑摊开讲。
2. G474RE 的 DAC 与 DMA 触发链路:先看清资源再动 CubeMX
2.1 三个 DAC 控制器,输出通道比 F1 时代宽裕得多
G474RE 不是 F1 那种“一个 DAC、几个引脚重复映射”的结构。它内部有多个 DAC 控制器,先看 CubeMX 里 DAC1、DAC2、DAC3 三个实例可见的资源,每个实例又有独立的输出通道可选。这种多控制器设计不是给你做三份同样东西,而是让电流环控制、三相逆变、超声驱动这类多路同步输出场景有更干净的寄存器隔离:每路通道有独立的 DHR 数据保持寄存器、独立的 DMA 请求和独立的触发源选择,可以单独起停,也可以由同一个定时器同时触发保持相位一致。LQFP64 封装具体能引出几路输出,要以 ST 数据手册的引脚复用表为准,DEMO 阶段只占 DAC1_OUT1 一个通道。
输出通道还有一个容易忽略的环节:缓冲。G474 的 DAC 输出可以直接接到片内运放,也可以启用 DAC 输出缓冲后直接驱动外部负载。DEMO 里把 Output Buffer 使能,输出阻抗低,示波器探头上去电压不会被拉垮。如果关掉缓冲,输出引脚对负载很敏感,接一个 10 kΩ 负载电压都会明显下跌,初学阶段没必要在这个变量上浪费时间。
2.2 DAC 的 DMA 请求机制:每次触发搬一个点,不是一次搬完一张表
12 位 DAC 的数据寄存器是 32 位对齐的 DHRxR,低 12 位有效。DMA 在这里做的事,是把内存里的 16 位半字依次搬到 DHRxR。关键问题是“什么时候搬”:由 DAC 的触发源控制。配置成 TIM 触发后,定时器更新事件到来,DAC 内部产生一个转换请求,DMA 搬一个点到 DHR,随后 DAC 把 DHR 锁存到输出寄存器 DOR,完成一次模拟输出。
把 DMA 配成 Circullar 循环模式后,传输计数回绕,表首尾相接,DAC 才能持续输出完整波形。常见误用是以为“使能了 DMA,DAC 就会全速刷表”。实际上没有触发源时,DAC 只完成一次转换,波形停在第一点。很多 demo 程序“输出不了连续波形”的问题,不是 DMA 没配好,而是 DAC 的触发源没配上。G474 的 DAC 支持多种触发来源,最可控的是定时器 TRGO,后面第 4 章专门讲频率怎么算。
2.3 CubeMX 里最容易配错的三个开关:Buffer、Trigger、Circular
在 STM32CubeMX 里把 DAC1 的 OUT1 打开,DMA Settings 里添加 DAC1_CH1 请求,真正影响能否连续输出的配置项就几个:
| 配置项 | 推荐值 | 说明 |
|---|---|---|
| Output Buffer | Enable | 直接接示波器或负载时输出能力更好;如果 DAC 输出送片内运放再放大,可以关 |
| Trigger | Timer 3 Trigger Out event | 每个触发事件请求一次 DMA 搬运,节拍来自 TIM3 的 TRGO |
| DMA Mode | Circular | 表播完自动从头部回卷,这就是 continuous requests 的实际体现 |
| Memory Increment | Enable | DMA 每次搬运后内存地址后移,否则永远只发第一个点 |
| Peripheral Increment | Disable | 外设地址固定为 DAC 的 DHRxR,不需要自增 |
| Data Width | Half Word | 12 位右对齐,DMA 搬运半字(16 位)最合适 |
提示:CubeMX 生成的 MX_DMA_Init 会放在外设初始化之前,HAL 启动 DAC 时已经能拿到初始化好的 DMA 句柄。不要手动把 DMA 通道改成 Very High 优先级,DEMO 阶段 High 足够,G474 的 DMA 请求很多,后面接 ADC 多通道采集时再一起调优先级。
3. 最小 DEMO:256 点正弦表从 DMA 循环打进 DAC1
3.1 生成查找表:别在中断里算,CPU 只做一次初始化
#include <math.h> #define LUT_SIZE 256 static uint16_t sine_lut[LUT_SIZE]; /* SRAM 静态区,DMA 可读 */ void sine_lut_init(void) { for (uint16_t i = 0; i < LUT_SIZE; i++) { float phase = 2.0f * 3.14159265358979f * i / LUT_SIZE; float v = 2048.0f + 2047.0f * sinf(phase); /* 中点 2048,幅值 2047 */ sine_lut[i] = (uint16_t)v; } }参数说明:LUT_SIZE 选 256 是习惯,不是硬性要求,后面会看到点数应该按目标频率反推。2048 是 12 位 DAC 的满码中点,幅值取 2047 而不取 2048,是因为满码是 4095,写在 2048 + 2048 * sinf 在峰顶会得到 4096,溢出回绕成 0,波形顶会被削掉。这样留 1 LSB 余量,输出不会削顶且中点正好在半幅。初始化跑一次循环,运行期 DMA 持续读这张表,CPU 不再参与。
3.2 HAL_DAC_Start_DMA 的参数逐个说明
HAL_DAC_Start_DMA(&hdac1, DAC_CHANNEL_1, (uint32_t *)sine_lut, LUT_SIZE, DAC_ALIGN_12B_R);参数逻辑说明:hdac1 是 CubeMX 生成的 DAC1 句柄;DAC_CHANNEL_1 对应 DAC1_OUT1;sine_lut 是 uint16_t 数组,强转成 uint32_t * 是因为 HAL 内部统一用 32 位指针承接,实际搬运宽度看 DMA 配置的 Half Word;LUT_SIZE 是“半字个数”而不是字节数;DAC_ALIGN_12B_R 表示 12 位右对齐,数组里元素 0 到 4095 的数值直接映射到 DAC 输出的 12 位编码。
这个函数内部做了三件事:使能 DAC 通道、配置 DMA 通道工作在循环模式、使能 DMA 的传输完成和半传输中断。所以只需要在初始化阶段生成过 LUT,调用前确保 MX_DMA_Init 已经执行过即可。停止时用对应的 HAL_DAC_Stop_DMA,再把 DAC 通道关掉:
HAL_DAC_Stop_DMA(&hdac1, DAC_CHANNEL_1); HAL_DAC_Stop(&hdac1, DAC_CHANNEL_1);停止顺序注意点:先停 DMA 搬运,再停 DAC 通道。如果先停 DAC,DHR 里残留的是最后一次 DMA 写入的数值,输出会停在某个随机电压上,不会回到中点。
3.3 没有示波器时怎么判断 DEMO 已经跑通
没有示波器也能验证链路通不通。第一个办法:万用表直流档量 DAC1_OUT1 引脚,正弦波平均值等于直流偏置对应的电压,也就是 2048 / 4096 * VREF+。VREF+ 是 3.3 V 时,读数应该在 1.65 V 左右,偏差超过 2% 就查供电。
第二个办法更直接:把正弦表临时替换成方波表,重新初始化后 DMA 循环输出,万用表能看到电压在高电平和低电平之间切换;如果测到的是稳定的中间值,说明 DMA 根本没在搬数据。
for (uint16_t i = 0; i < LUT_SIZE; i++) sine_lut[i] = (i < LUT_SIZE / 2) ? 4095 : 0;这段方波代码验证的是“DMA 有没有在跑、触发有没有生效”这一整条链路,比直接盯正弦波形更容易定位问题。方波能翻,正弦自然也能出,只是阶梯波在示波器上看起来更明显。
4. TIM3 触发与采样率换算:把频率从碰运气改成设计值
4.1 为什么不能让 DMA 全速跑
有人会把 DAC 触发源留空,指望 DMA 自己高速循环搬运,结果要么只输出一个点,要么输出频率毫无规律。原因是 DAC 的转换节奏必须由触发输入控制,没有触发源时它只是被动等待。可复现的常见做法是让 TIM3 产生更新事件,更新事件通过 TRGO 连线到 DAC 的触发输入,DAC 每个触发沿向 DMA 发出一次请求,DMA 搬一个点。这样每个样点的持续时间就由定时器固定住了,波形频率成为一个设计值,而不是总线上抢出来的随机数。
这和 PWM DMA 的思路是同一族:PWM 的 DMA 是把占空比序列刷进 TIM 的 CCR 寄存器,DAC 的 DMA 是把样点刷进 DHR 寄存器,核心都是“用定时器节拍给 DMA 编舞”。区别在于 DAC 需要把 DMA 配置成 Circular 并且把触发源接到 TRGO,PWM 则是把 TIM 本身当作输出媒介。
4.2 采样率计算公式与两组直接可抄的参数
触发频率和波形频率的换算很简单:
f_trigger = F_tclk1 / ((PSC + 1) * (ARR + 1))
f_wave = f_trigger / LUT_SIZE
目标波形频率确定后,用 LUT_SIZE * f_wave 算出需要的触发频率,反推 ARR:ARR = F_tclk1 / (LUT_SIZE * f_wave * (PSC + 1)) - 1。
| 目标波形频率 | 点数 | 触发频率 | PSC | ARR | 实际波形频率 |
|---|---|---|---|---|---|
| 1 kHz | 250 | 250 kHz | 0 | 679 | 1000.00 Hz |
| 50 Hz | 200 | 10 kHz | 169 | 99 | 50.00 Hz |
第一组故意不选 256 点:256 * 1000 = 256 kHz,170 MHz / 256 kHz = 664.06,ARR 取整后会有 0.4 Hz 误差;改成 250 点后 170 MHz / 680 = 250 kHz,完美整除。这说明波形点数不是非 2 的幂不可,DMA 传输长度没有这种约束,按频率反推点数是更工程化的做法。
CubeMX 里 TIM3 的配置:Clock Source 选 Internal Clock,Prescaler 和 Counter Period 按表填,Trigger Output (TRGO) 选 Update Event。不需要使能 TIM3 中断,触发是硬件连线,不走 CPU。启动顺序固定为先启动定时器,再启动 DMA:
HAL_TIM_Base_Start(&htim3); HAL_DAC_Start_DMA(&hdac1, DAC_CHANNEL_1, (uint32_t *)sine_lut, LUT_SIZE, DAC_ALIGN_12B_R);两者顺序不能反:先启动 DMA 后,DAC 拿到触发源之前不会搬数据,倒也无害;但先启动定时器后,DAC 还没准备好 DMA,前面几个触发沿会被浪费,不产生实际影响。统一写成先 TIM 后 DAC,逻辑上更干净。
4.3 多路波形同步:每个通道一条 DMA 请求,内存自己管
G474RE 每路 DAC 输出通道有独立的 DHR 寄存器和独立的 DMA 请求线。要同时出两路波形,就再调一次 HAL_DAC_Start_DMA,第二个参数改成 DAC_CHANNEL_2,配另一张 LUT。两个通道的 Trigger 都选 TIM3 TRGO,它们会在同一个更新沿锁存数据,输出天然同步。
带宽问题不大:单路每秒 1 MSPS、16 位,两路才 4 MB/s,AHB 总线轻松。真正的坑在 DMA 优先级。如果项目里同时有 ADC 四通道 DMA 采集,ADC 的 DMA 通道和 DAC 的 DMA 通道会争总线;实际情况中把 ADC 的 DMA 优先级调高、DAC 维持默认即可,因为 DAC 是周期性触发,丢一个点波形会缺一截,而 ADC 采集可以通过软件判超时重取一次。
多通道还有一个细节:不要试图用一条 DMA 通道同时服务 DAC1_CH1 和 DAC1_CH2。两个 DHR 寄存器地址不连续,无法靠 Peripheral Increment 自动跨越,强行拼接数据只会写错寄存器地址。老老实实每路一个 DMA 通道,G474 的 DMA1 和 DMA2 通道数足够。
5. 收尾的三个改造技巧:从 DEMO 到可交付的波形源
5.1 直流法验证 12 位 DHR 与输出电压的关系
波形不对时,先绕过 DMA,直接往 DHR 写一个固定值,把数模链路单独摘出来验证。
HAL_DAC_SetValue(&hdac1, DAC_CHANNEL_1, DAC_ALIGN_12B_R, 2048); HAL_DAC_Start(&hdac1, DAC_CHANNEL_1);这段代码没有启动 DMA,只写直流。理论输出是 VREF+ * 2048 / 4096,VREF+ 为 3.3 V 时约 1.65 V。实测偏差过大时查两处:VREF+ 与 VDDA 的供电纹波,以及 DAC 输出缓冲是否被误关。这一招能先把参考电压和 12 位编码关系校准,再回来查 DMA 链路,避免两个变量混在一起猜。
5.2 半传输中断翻转 GPIO,用频率计观察表更新节奏
DMA 循环模式下,每张表传输到一半会触发半传输中断。在回调里翻转一个 GPIO,可以直接用示波器或频率计确认 LUT 更新节奏是否和设计一致。
void HAL_DAC_ConvHalfCpltCallback(DAC_HandleTypeDef *hdac) { GPIOB->ODR ^= GPIO_PIN_0; }回调逻辑说明:LUT_SIZE 为 250、波形频率 1 kHz 时,一个完整表周期是 1 ms,半传输点每 1 ms 出现一次,GPIO 翻转频率正好等于波形频率。看到翻转频率偏离设计值,说明触发频率或点数设置有误,不用再拿探头盯 DAC 引脚上的模拟毛刺。注意回调里只做 GPIO 翻转,不要做打印或延时,半传输中断里拖太久会干扰下一个 DMA 请求。
5.3 波形表放 Flash 还是 SRAM,VREF+ 的布局取舍
LUT 作为全局静态数组默认在 SRAM,这是最省事的选择:DMA 读 SRAM 不跟 Flash 的 ART 预取抢总线,而且运行期可以改写这张表,为后续做任意波形发生器留了路。G474RE 的 SRAM 容量对几 K 长度的 LUT 完全无压力。如果表大几十 K,才考虑用 const 放到 Flash,但那时 DMA 读 Flash 会和 CPU 取指竞争总线,表现是波形出现偶发毛刺。
最后看 DAC 输出质量:DHR 编码没有错,引脚上还是有周期性噪声尖峰,八成是 VREF+ 参考电压上的高频噪声。PCB 布局要点有三个:VREF+ 到 VSSA 之间放 100 nF 加 1 uF 的低 ESR 电容,位置贴近 MCU 引脚;VREF+ 走线不要穿过数字开关区,尤其不能和定时器 PWM 输出线长距离并行;VDDA 与 VDD 之间单独加磁珠或小电感隔离,避免数字电流串进模拟电源。G474 的 ADC 和 DAC 共用参考电压时,这两处处理好,ADC 采样跳动和 DAC 毛刺会同时好转。
本文还有配套的精品资源,点击获取