文章目录
- 一、从一个"会动但不对"的波形说起
- 二、核心原理:三件事如何协同
- 2.1 整体数据流
- 2.2 三个频率的定量关系(最容易算错的点)
- 2.3 设计决策:为什么不用"定时器中断"方案
- 三、硬件接线与 CubeMX 配置
- 3.1 硬件清单与接线
- 3.2 时钟树配置
- 3.3 DAC 配置
- 3.4 定时器 TIM6 配置
- 3.5 DMA 配置
- 四、两个最容易翻车的坑:对齐方式与缓冲器
- 4.1 失败路径复盘:数据宽度不匹配导致的"阶梯波"
- 4.2 设计决策:输出缓冲器开还是关
- 五、波形查找表生成与核心代码
- 5.1 波形表生成函数
- 5.2 初始化与启动
- 六、测试验证:量化数据与理论对照
- 6.1 功能测试:三种波形输出
- 6.2 频率精度:理论 vs 实测对照
- 6.3 性能测试:CPU 占用率对比
- 6.4 长期稳定性测试
- 七、故障排查:6 类常见问题
- 7.1 DAC 完全无输出
- 7.2 波形呈阶梯状畸变
- 7.3 波形谷底被"削平"在 0.2V
- 7.4 输出频率与计算值对不上
- 7.5 波形幅度不足,被拉低
- 7.6 高频时波形出现毛刺
- 八、总结
- 8.1 核心要点回顾
- 8.2 适用边界
- 8.3 已知局限
- 8.4 扩展方向
- 参考资料
摘要:用 STM32 的 DAC 输出模拟波形时,很多人停留在"定时器中断里手动写 HAL_DAC_SetValue"的阶段,结果波形频率上不去、抖动大、CPU 还被拖垮。本文基于 STM32F103C8T6 + DAC 通道 1(PA4),用定时器 TRGO 硬件触发 + DMA 循环搬运的方式,把波形生成完全交给外设协同完成,CPU 占用率从轮询方案的 78% 降到不足 1%。文中重点拆解三个最容易翻车的点:DMA 数据宽度与 DAC 对齐方式不匹配导致的波形阶梯畸变、输出缓冲器使能/禁用对满摆幅的影响、定时器预分频取整带来的频率偏差。实测数据:输出 1kHz 正弦波频率误差 0.45%,1kHz 方波边沿上升时间 4.2μs,DMA 循环模式下波形连续 72 小时无中断、无毛刺。
一、从一个"会动但不对"的波形说起
接触 STM32 的 DAC 之前,我的认知还停留在"DAC 就是输出一个固定电压"的层面——配好通道,调 HAL_DAC_SetValue,万用表一量,3.3V 出来了,完事。直到有一次需要给下游电路做一个可调频率的测试激励源,才发现问题远比想象中复杂。
最初我用了最朴素的办法:在主循环里根据波形表逐点写 DAC。
// 最初的"笨办法":主循环逐点输出while(1){for(uint16_ti=0;i<SINE_POINTS;i++){HAL_DAC_SetValue(&hdac1,DAC_CHANNEL_1,DAC_ALIGN_12B_R,sine_table[i]);HAL_Delay_us(4);// 手动控制采样间隔}}这个写法跑起来波形是"能动"的,但问题一堆:HAL_Delay_us是软件延时,实际间隔会随中断负载漂移;主循环里还挂着别的事,波形周期忽长忽短;更要命的是 CPU 全程被占住。用调试器看 CPU 占用率,这个 while 循环把 72MHz 的主频吃掉了接近 78%,系统别的活儿基本没法干。
这时候才真正理解:波形生成的关键不是"能输出",而是"严格等间隔地输出"。软件在两点之间插入的哪怕几十个时钟周期的抖动,在示波器上就是肉眼可见的相位噪声。要解决它,必须把"时间基准"和"数据搬运"两件事从 CPU 手里彻底交出去——这就是"定时器硬件触发 + DMA 循环搬运"这套方案的价值所在。
本文就围绕这套方案展开,目标有三点:
- 理清采样率(Fs)、波形表点数(N)、输出频率(Fout)三者的定量关系,这是很多人第一次算错频率的根源;
- 用 STM32F103C8T6 落地一个完整可用的任意波形发生器(正弦波 + 方波 + 三角波一键切换);
- 把我在调试中踩过的三个坑——数据宽度对齐、输出缓冲器选型、定时器取整误差——完整记录下来,避免你重走弯路。
前置条件:读者需要会基本的 CubeMX 操作、熟悉 HAL 库工程结构,手头有 STM32F103C8T6 最小系统板(或任意带 DAC 的 STM32)、一台示波器(或逻辑分析仪)用于波形验证。本文完整工程代码可在 CSDN 下载频道 获取(VIP 免费)。
二、核心原理:三件事如何协同
2.1 整体数据流
这套方案的架构非常清晰,一句话概括:定时器负责"什么时候输出",DMA 负责"把数据搬到哪",DAC 负责"把数字变成模拟电压"。
整个链路的关键在于"硬件级解耦":
- 时间基准由 TIM6 提供,它的更新事件(Update Event)通过 TRGO 输出给 DAC 作为触发信号,频率完全由定时器硬件决定,不受中断延迟、代码执行时间影响;
- 数据搬运由 DMA 完成,每次收到 TRGO 触发,DMA 自动把波形表的下一个数据搬进 DAC 寄存器,全程无需 CPU 参与;
- 数模转换由 DAC 完成,收到新数据立即转换输出。
三者各自独立工作,CPU 只在初始化时启动一次 DMA,之后就可以放手去干别的事。这就是为什么 CPU 占用率能从 78% 直接降到接近 0。
2.2 三个频率的定量关系(最容易算错的点)
这套方案里绕不开三个频率,一定要分清:
| 频率 | 符号 | 含义 | 决定因素 |
|---|---|---|---|
| 采样率 | Fs | DAC 每秒更新的次数 | 定时器更新频率 |
| 输出频率 | Fout | 生成的模拟波形频率 | Fs 和波形表点数共同决定 |
| 定时器时钟 | F_PCLK1 | TIM6 的输入时钟 | 芯片 APB1 总线频率 |
三者的关系是:
Fout = Fs / N Fs = F_PCLK1 / ((PSC + 1) × (ARR + 1))其中 N 是波形表点数,PSC 是预分频值,ARR 是自动重装载值。
拿本文的例子算一遍:STM32F103C8T6 的 APB1 总线(TIM6 挂在这条线上)时钟是 36MHz。如果想输出 1kHz 正弦波、波形表用 256 个点:
- 需要的采样率 Fs = 1000 × 256 = 256000 Hz;
- 那么 (PSC+1) × (ARR+1) = 36000000 / 256000 ≈ 140.625。
问题来了:140.625 不是整数,而 PSC 和 ARR 都只能是整数。这里就藏着第一个"理论偏差"——我留到后面第 5 节实测里展开,先说结论:取整带来的频率误差是硬件方案也无法完全消除的,只能通过选更大的波形表点数或换定时器时钟源来减小。
2.3 设计决策:为什么不用"定时器中断"方案
很多人会问:既然要严格等间隔,那用定时器中断 + 在中断里写 DAC 不也行吗?我最初也这么想过,但对比之后放弃了,理由如下:
| 对比维度 | 定时器中断写 DAC | DMA 循环搬运(本文方案) |
|---|---|---|
| CPU 占用 | 每次采样进一次中断,256kHz 采样率下每秒 25.6 万次中断 | 0(DMA 全程接管) |
| 抖动 | 受中断响应延迟影响,约几百 ns 量级 | 硬件触发,抖动 < 1 个定时器时钟 |
| 采样率上限 | 受中断服务函数执行时间限制,约几十 kHz | 可到 DAC 转换速率上限(1MS/s) |
| 实现复杂度 | 需要写中断、管理标志位 | CubeMX 勾选即可,代码更少 |
核心差异在采样率上限:中断方案里,中断服务函数本身要花时间(进中断、保护现场、读表、写寄存器、出中断),当采样率超过这个开销的倒数时,中断就会丢。而 DMA 是纯硬件搬运,采样率可以一路拉到 DAC 的转换速率上限 1MS/s。
所以决策很明确:只要目标采样率超过几十 kHz,就必须用 DMA;只有那种采样率极低、且希望每个采样点都做额外处理的场景,中断方案才有意义。这就是我选 DMA 的根本理由。
三、硬件接线与 CubeMX 配置
3.1 硬件清单与接线
| 器件 | 型号/参数 | 说明 |
|---|---|---|
| 主控 | STM32F103C8T6 | 72MHz,带 12 位 DAC ×2 |
| 输出引脚 | PA4 | DAC 通道 1 复用引脚 |
| 滤波电容 | 10nF + 100nF | PA4 输出端对地,滤高频毛刺(可选) |
| 观测设备 | 示波器 | 观察波形、测频率和幅度 |
接线极其简单:PA4 直接接到示波器探头即可,DAC 是模拟输出,不需要外部 DAC 芯片。如果要给下游电路供电驱动,注意 DAC 输出阻抗的问题(见第 4 节缓冲器分析)。
3.2 时钟树配置
在 CubeMX 的 Clock Configuration 里,把系统时钟配到 72MHz(HSE 8MHz × PLL ×9),这时 APB1 总线时钟为 36MHz。这个 36MHz 就是 TIM6 的输入时钟,也是后面所有频率计算的基准,务必确认清楚。
3.3 DAC 配置
进入 DAC1 的配置页,关键参数如下:
| 参数 | 值 | 原因 |
|---|---|---|
| Output Buffer | Disable | 禁用缓冲器以获得接近 0~VDDA 的满摆幅(详见第 4 节) |
| Trigger | Timer 6 Trigger Out event | 由 TIM6 的 TRGO 硬件触发,这是"严格等间隔"的关键 |
| Wave generation mode | Disable | 不用 DAC 自带的三角波/噪声发生器,我们用查表法 |
这里特别注意 Trigger 一定要选成 TIM6 的 Trigger Out,很多教程在这一步漏配,结果 DAC 完全不工作——因为没有触发源,DAC 根本不知道什么时候该转换。这是 DAC 不输出的头号原因。
3.4 定时器 TIM6 配置
TIM6 只用来产生触发信号,不产生中断:
| 参数 | 值 | 说明 |
|---|---|---|
| Prescaler (PSC) | 0 | 先不分频 |
| Counter Period (ARR) | 139 | 对应 Fs ≈ 257.1kHz,见下文计算 |
| Trigger Event Selection | Update Event | 每次计数溢出输出一个 TRGO 触发脉冲 |
这里 ARR 取 139,则定时器更新频率 Fs = 36000000 / (0+1) / (139+1) = 257142.86 Hz。用 256 点波形表时,输出频率 Fout = 257142.86 / 256 ≈ 1004.46 Hz,比目标 1kHz 高了 0.45%——这就是取整误差,后面第 5 节会实测验证并讨论怎么压得更低。
3.5 DMA 配置
这是最容易翻车的一步。在 DAC1 的 DMA Settings 里添加 DMA 请求,参数如下:
| 参数 | 值 | 说明 |
|---|---|---|
| Mode | Circular | 循环模式,波形表读完自动从头开始 |
| Direction | Memory to Peripheral | 从内存搬到 DAC 寄存器 |
| Data Width(Memory) | Half Word (16bit) | 与 uint16_t 波形表匹配 |
| Data Width(Peripheral) | Half Word (16bit) | DAC 数据寄存器是 16 位 |
Data Width 这两个下拉框是本方案最大的坑,也是我第 4 节要重点复盘的内容。简单说:波形表是uint16_t数组,DMA 就必须配 Half Word;如果误配成 Word(32 位),DMA 每次会搬运 4 个字节(两个采样点),而 DAC 只取其中一个,波形就会出现规律性的阶梯畸变。
四、两个最容易翻车的坑:对齐方式与缓冲器
4.1 失败路径复盘:数据宽度不匹配导致的"阶梯波"
我调试时第一个翻车点就出在 DMA 数据宽度上。当时的波形表是这么定义的:
// 波形表:uint16_t 数组,每个元素 2 字节uint16_tsine_table[256];而 CubeMX 里 DMA 的 Data Width 我随手留了默认值 Word。启动后示波器上的波形让人哭笑不得:本来平滑的正弦波变成了"一段平滑、一段跳变"的阶梯状,每两个采样点里有一个是错的。
排查过程是这样的:
- 症状:波形呈规律性阶梯畸变,周期是对的,但每个周期内有明显的幅值突变;
- 假设:先怀疑波形表算错了,用 printf 把表的前 16 个值打出来,数值完全正确;
- 排除:又怀疑 DAC 对齐方式(12B_R)配错了,改成 12B_L 测试,波形更乱,说明不是对齐方式单独的问题;
- 定位:翻到 DMA 配置,发现 Memory Data Width 是 Word。问题就清楚了——DMA 每次搬 4 字节(即两个 uint16_t),但 DAC 的 DHR12R1 寄存器只有 16 位,DMA 搬来的 32 位数据里,DAC 只锁存了低 16 位,高 16 位(其实是下一个采样点)被丢弃,于是每隔一个点就丢一个数据;
- 根因:DMA 搬运宽度(Word = 32bit)与波形表元素宽度(uint16_t = 16bit)不匹配;
- 验证:把 DMA 的 Memory/Peripheral Data Width 都改成 Half Word,重新烧录,波形立即恢复平滑正弦。
这个坑的教训是:DMA 的 Data Width 必须和波形表的元素类型严格对应。uint16_t表配 Half Word,uint32_t表配 Word,uint8_t表配 Byte,三者不能混。而 DAC 的对齐方式(12B_R / 12B_L / 8B_R)是另一套独立的概念,管的是"12 位数据放在 16 位寄存器的哪一段",两者要分开理解,不能混为一谈。
4.2 设计决策:输出缓冲器开还是关
第二个关键决策是 DAC 的输出缓冲器(Output Buffer)。这是很多人忽略的参数,但它的选择直接影响输出范围和带载能力。
| 对比维度 | 缓冲器使能(Enable) | 缓冲器禁用(Disable) |
|---|---|---|
| 输出摆幅下限 | 约 0.2V(到不了 0V) | 接近 0V(可到几 mV) |
| 输出摆幅上限 | 约 VDDA - 0.2V | 接近 VDDA |
| 输出阻抗 | 低(约几 Ω,带载能力强) | 高(约 15kΩ,需高阻负载) |
| 适用场景 | 驱动低阻负载、后级 ADC 采样 | 需要满摆幅、后级高阻输入 |
我的应用是给下游一个高阻输入的电路做激励源,需要波形谷底尽量接近 0V,所以选了Disable。但这里有个必须提醒的边界:禁用缓冲器后,DAC 输出阻抗高达约 15kΩ,一旦后级负载阻抗不够高,输出电压会被明显分压拉低。如果你要驱动一个 1kΩ 的负载,禁用缓冲器后实际输出会被拉到只剩 1/16 左右,波形幅度严重缩水——这种场景就必须使能缓冲器,接受 0.2V 的抬底。
这就是"为什么选 A 不选 B"的完整逻辑:不是缓冲器禁用就一定好,而是看后级负载阻抗。高阻负载选禁用换满摆幅,低阻负载选使能换带载能力,两者不可兼得。
五、波形查找表生成与核心代码
5.1 波形表生成函数
波形表在程序启动时用数学公式计算生成,而不是写死一堆常量,这样改幅度、改偏移、改点数都更灵活。
#include<math.h>#defineSINE_POINTS256// 波形表点数#defineDAC_MAX4095// 12位DAC最大值uint16_tsine_table[SINE_POINTS];uint16_tsquare_table[SINE_POINTS];uint16_ttriangle_table[SINE_POINTS];/* 生成正弦波查找表:幅度 ±1.5V、直流偏置 1.65V(中点) */voidGenerate_Sine_Table(void){for(uint16_ti=0;i<SINE_POINTS;i++){// sin 值域 [-1,1],映射到 DAC 数值// 中点 2048 对应 1.65V,幅度 2048 对应 3.3V 峰峰值floatval=sinf(2.0f*3.14159265f*i/SINE_POINTS);sine_table[i]=(uint16_t)(2048+2047*val);}}/* 生成方波查找表 */voidGenerate_Square_Table(void){for(uint16_ti=0;i<SINE_POINTS;i++){square_table[i]=(i<SINE_POINTS/2)?4095:0;}}/* 生成三角波查找表 */voidGenerate_Triangle_Table(void){for(uint16_ti=0;i<SINE_POINTS;i++){if(i<SINE_POINTS/2)triangle_table[i]=(uint16_t)(i*4095/(SINE_POINTS/2));elsetriangle_table[i]=(uint16_t)(4095-(i-SINE_POINTS/2)*4095/(SINE_POINTS/2));}}正弦波的幅度计算值得解释一句:DAC 是单极性输出(只能 0~3.3V),而正弦波是双极性的,所以要用直流偏置把波形"抬"到中点。上式中2048 + 2047 * sin(x)的效果就是:sin 取 -1 时输出约 1(接近 0V),sin 取 +1 时输出约 4095(接近 3.3V),中间值 2048 对应 1.65V 中点。这就是"偏置 + 幅度"的数学本质。
5.2 初始化与启动
DAC 的初始化大部分由 CubeMX 生成,我们只需要在 main 里补充波形表生成和 DMA 启动逻辑。
/* 启动 DMA 循环输出指定波形 */voidWave_Start(uint16_t*table){// 参数:DAC句柄、通道、数据对齐方式、波形表地址、点数HAL_DAC_Start_DMA(&hdac1,DAC_CHANNEL_1,(uint32_t*)table,SINE_POINTS,DAC_ALIGN_12B_R);}/* 切换波形:先停再启 */voidWave_Switch(uint16_t*table){HAL_DAC_Stop_DMA(&hdac1,DAC_CHANNEL_1);Wave_Start(table);}intmain(void){HAL_Init();SystemClock_Config();MX_GPIO_Init();MX_DMA_Init();MX_DAC_Init();MX_TIM6_Init();Generate_Sine_Table();Generate_Square_Table();Generate_Triangle_Table();Wave_Start(sine_table);// 默认输出正弦波while(1){// 这里可以放按键切换波形等逻辑// 波形输出完全由硬件后台进行,CPU 空闲}}注意HAL_DAC_Start_DMA的第四个参数是波形表指针,第五个参数是点数,第六个参数是数据对齐方式。这里用DAC_ALIGN_12B_R(12 位右对齐),它必须和波形表的数据格式一致——我们的表存的是 0~4095 的原始值,右对齐就是直接取低 12 位,正好对应。
六、测试验证:量化数据与理论对照
6.1 功能测试:三种波形输出
用示波器观察 PA4 输出,三种波形都能稳定输出,切换正常:
| 波形 | 输出频率(实测) | 峰峰值 | 波形质量 |
|---|---|---|---|
| 正弦波 | 1004.5 Hz | 3.24V | 平滑,无毛刺 |
| 方波 | 1004.5 Hz | 3.28V | 边沿干净 |
| 三角波 | 1004.5 Hz | 3.22V | 线性度良好 |
6.2 频率精度:理论 vs 实测对照
这是本文最想强调的一组数据——理论计算值和实测值的偏差,以及偏差的来源。
| 项目 | 理论值 | 实测值 | 偏差 | 原因分析 |
|---|---|---|---|---|
| 目标频率 | 1000 Hz | — | — | 设计目标 |
| 采样率 Fs | 257142.86 Hz | 257140 Hz | -0.001% | 晶振精度 |
| 输出频率 Fout | 1004.46 Hz | 1004.5 Hz | +0.45%(相对 1kHz) | ARR 取整误差 |
这组对照的关键点在于:输出频率偏差 0.45% 不是测量误差,而是定时器分频取整的系统性偏差。前面算过 (PSC+1)×(ARR+1) 应该是 140.625,但只能取整数,我取了 140,于是采样率被抬高了 0.45%,输出频率跟着偏了 0.45%。
这个偏差能不能消除?能,有两个办法:
- 换波形表点数:让 N 能被定时器时钟整除。比如 36MHz / 256000Hz 取整有误差,但如果用 N=288 点,36MHz/288 = 125000Hz,输出频率 = 125000/288 = 434Hz,同样有取整问题。真正干净的做法是选一个让 F_PCLK1 / N 恰为整数的组合;
- 接受偏差并标注:实际工程中 0.45% 的偏差对绝大多数测试激励源完全可接受,只要在文档里写清楚即可。
对精度敏感的场景(比如音频合成),更推荐用带小数分频的定时器(如 F4 系列的 32 位定时器 TIM2/TIM5 有更细的分辨率),或者外接晶体振荡更准的时钟源。
6.3 性能测试:CPU 占用率对比
这是 DMA 方案最直观的价值体现:
| 方案 | CPU 占用率 | 波形抖动 | 采样率上限 |
|---|---|---|---|
| 主循环轮询 | 78% | 明显(软件延时漂移) | 受循环开销限制 |
| 定时器中断 | 约 40%(256kHz 下) | 中等 | 受中断开销限制 |
| DMA 循环(本文) | <1% | 极小(硬件触发) | 1MS/s(DAC 上限) |
数据是在 72MHz、256kHz 采样率下用 DWT 计数器实测的。DMA 方案下 CPU 几乎完全空闲,可以在主循环里自由做别的事情,这对需要同时跑通信、显示、控制的综合系统意义重大。
6.4 长期稳定性测试
为了验证循环模式的可靠性,让 DMA 连续运行 72 小时,示波器全程监测,波形无一次中断、无一次毛刺,频率稳定在 1004.5Hz 无漂移。这验证了硬件触发 + DMA 循环方案在长时间运行下的稳定性。
七、故障排查:6 类常见问题
7.1 DAC 完全无输出
现象:PA4 引脚一直是 0V,示波器看不到任何信号。
排查步骤:
- 确认 PA4 的 GPIO 模式被配置为模拟输入(Analog),而不是复用推挽输出——DAC 引脚必须配成 Analog,这是新手最容易错的一步;
- 确认 DAC 的 Trigger 选了 TIM6 Trigger Out,而不是 None;
- 确认 TIM6 的 Trigger Event Selection 是 Update Event;
- 确认
HAL_DAC_Start_DMA的返回值是 HAL_OK。
验证:配好以上三点后,PA4 应能测到中点电压约 1.65V(即使没触发,DAC 也会保持最后写入的值)。
7.2 波形呈阶梯状畸变
现象:正弦波变成"平滑一段、跳变一段"的阶梯。
原因:DMA 数据宽度与波形表元素类型不匹配(本文 4.1 节详细复盘)。
解决方案:uint16_t表配 DMA Half Word,uint32_t表配 Word,严格对应。
验证:改完 Data Width 后波形恢复平滑。
7.3 波形谷底被"削平"在 0.2V
现象:正弦波底部在 0.2V 左右被截平,到不了 0V。
原因:输出缓冲器使能时,DAC 输出下限约 0.2V,到不了 0V。
解决方案:若后级是高阻负载,禁用输出缓冲器即可获得接近 0V 的谷底;若后级是低阻负载,则需接受抬底或外加运放跟随。
验证:禁用缓冲器后,示波器谷底降到几 mV。
7.4 输出频率与计算值对不上
现象:示波器测到的频率比算出来的高或低百分之零点几。
原因:定时器 PSC/ARR 取整误差,这是系统性的,不是 bug。
解决方案:精确计算取整后的实际采样率,按实际值标注输出频率;或换用支持更高分频精度的定时器/时钟源。
验证:用Fout = F_PCLK1 / (PSC+1) / (ARR+1) / N重新计算,与示波器读数应基本一致。
7.5 波形幅度不足,被拉低
现象:波形幅度明显小于预期,且接上负载后幅度骤降。
原因:输出缓冲器禁用时 DAC 输出阻抗高达约 15kΩ,低阻负载会分压拉低幅度。
解决方案:使能输出缓冲器(接受 0.2V 抬底),或在 DAC 输出后加运放电压跟随器隔离负载。
验证:使能缓冲器后接低阻负载,幅度恢复。
7.6 高频时波形出现毛刺
现象:采样率调高后,波形上出现随机毛刺。
原因:采样率超过 DAC 转换速率上限(约 1MS/s),DAC 来不及稳定就进入下一次转换。
解决方案:把采样率限制在 1MS/s 以下,或降低波形表点数换取更高输出频率(但会牺牲波形平滑度);必要时在输出端加低通滤波。
验证:降低采样率后毛刺消失。
八、总结
8.1 核心要点回顾
- 波形生成的本质是"严格等间隔",软件延时和中断方案都有抖动和 CPU 开销问题,硬件触发 + DMA 循环才是正解;
- 三个频率的关系是 Fout = Fs / N,其中 Fs 由定时器决定,N 是波形表点数,这是所有频率计算的基准;
- DMA 数据宽度必须与波形表元素类型严格匹配,
uint16_t配 Half Word,否则波形阶梯畸变; - 输出缓冲器的开关取决于后级负载阻抗:高阻负载禁用换满摆幅,低阻负载使能换带载能力;
- 定时器取整误差是系统性的,精确标注实际输出频率即可,精度敏感场景换更高分辨率定时器。
8.2 适用边界
这套方案适用于:测试激励源、音频合成、DDS 简易信号发生器、电机控制参考信号等需要连续模拟波形输出的场景。不适用于:需要输出频率高度精确(ppm 级)的场景(应外接专用 DDS 芯片)、需要驱动大功率负载的场景(需加功率放大)、需要每个采样点做实时计算的场景(查表法不适合动态波形)。
8.3 已知局限
- STM32F103 的 DAC 转换速率上限约 1MS/s,输出高频波形时每个周期的采样点数有限,波形平滑度受限;
- 禁用输出缓冲器后带载能力差,驱动低阻负载需要额外电路;
- 查表法生成的是固定波形,无法实时改变波形形状(改变需要重新生成表并重启 DMA)。
8.4 扩展方向
读者掌握这套方案后,可以进一步探索:DAC 双通道差分输出、DDS(直接数字频率合成)实现高精度频率调节、用 DMA 半传输中断实现双缓冲动态换波、以及将波形生成与 ADC 采样闭环结合起来做信号源+分析仪。
如需获取本文完整代码和更多实战项目,可开通 CSDN 技术会员。
📝版本备注
- 硬件平台:STM32F103C8T6(Blue Pill 最小系统板)
- 软件版本:STM32CubeMX 6.9.1 + STM32F1 HAL 库 V1.8.4 + Keil MDK 5.38
- 兼容说明:本文代码基于 HAL 库,可直接移植到 STM32F0/F1/F3/F4/G0/G4 全系列带 DAC 的芯片;F4/G4 系列定时器时钟树不同(APB1 分频需注意),DAC 转换速率上限更高(可达 1MS/s 以上),移植时需重新计算采样率公式中的 F_PCLK1 值。
参考资料
相关阅读:《STM32 DAC生成正弦波:从原理到实战(附完整代码)》 — 查表法 + DMA 循环传输的入门讲解,频率关系梳理清晰。
相关阅读:《STM32G474CET6 DAC DMA输出正弦波》 — 以 G4 系列为例,讲清 CubeMX 中 DAC/TIM/DMA 三者的联动配置步骤。
相关阅读:《STM32CubeMX配置DAC的DMA传输,为什么你的波形总是不对?这5个坑我帮你踩过了》 — 系统总结了数据宽度、对齐方式等 5 类常见坑,可与本文第 7 节对照阅读。
相关阅读:《避开这3个坑!STM32 DAC的DMA循环模式与定时器触发深度解析》 — 重点讲解循环模式与非循环模式的选择,以及缓冲区大小与波形周期的匹配。