拓冰建站拓冰建站
首页 / 资讯中心 / 正文

STM32 DAC实战:从原理到波形生成,打通数字与模拟世界

1. 从数字到模拟为什么STM32的DAC值得你花时间如果你玩过STM32大概率已经用它的ADC把模拟世界的电压信号变成数字量在代码里分析处理了。但反过来呢当你的代码算出一个结果如何让它去驱动一个真实的物理世界设备比如让一个扬声器发出特定频率的声音让一个电机平滑地转动或者让一个LED的亮度缓缓变化。这时候你就需要数模转换器DAC出场了。STM32内部集成的DAC对于很多嵌入式应用来说是一个被低估的“瑞士军刀”。它不像GPIO点灯那样直观也不像PWM调光那样常见但它的价值在于“精确”和“连续”。PWM输出的是方波通过调节占空比来等效模拟电压但其本质还是数字开关在驱动一些对噪声敏感的高精度模拟器件如运放、某些传感器基准源或需要生成纯净波形如正弦波、音频时就显得力不从心。而DAC输出的是真正连续变化的电压值直接从芯片引脚上给出一个0到3.3V或参考电压范围内的任意电压。我最初接触DAC是为了做一个简易的信号发生器需要输出一个频率可调的正弦波。用PWM加低通滤波的方案波形毛刺多频率和幅度调整起来非常麻烦。换上STM32的DAC后代码里直接计算正弦表并定时更新DAC的输出寄存器一个干净漂亮的正弦波就在示波器上显示出来了那种“所见即所得”的精准控制感是数字方案很难比拟的。这个实验不仅让我搞懂了DAC的配置更让我理解了数字系统与模拟世界接口的核心思想。所以无论你是想制作一个简易音频播放器、控制一个线性电源的电压、构建一个自动化测试的激励源还是仅仅为了更深入地理解MCU的模拟外设STM32的DAC实验都是一个绝佳的切入点。它连接了你的算法逻辑与真实的物理效应是嵌入式开发中从“虚拟”走向“实体”的关键一步。2. 核心原理STM32 DAC是如何工作的在动手写代码之前我们得先搞清楚STM32内部的DAC模块到底是怎么把一堆二进制数字变成引脚上的电压的。这有助于我们理解后续配置中的各种参数以及如何避免一些常见的性能陷阱。2.1 DAC的基本架构与数据通路STM32的DAC模块核心是一个“数字到模拟转换器”但其周围配套的“基础设施”决定了它的易用性和灵活性。以常见的STM32F1/F4系列为例其DAC模块主要包含以下几个关键部分DAC数据寄存器 (DAC_DHRx)这是我们的代码与DAC硬件交互的主要窗口。我们想输出什么电压值就换算成数字量写入这个寄存器。它有多种对齐格式右对齐12位、左对齐12位、8位右对齐以适应不同精度的需求。DAC数据保持寄存器这是一个内部的缓冲器。当我们写入DAC_DHRx时数据并不会立刻进入DAC的核心转换电路而是先暂存在这里。这种设计可以防止在转换过程中数据被意外修改保证输出电压的稳定。DAC转换器核心这是实现数模转换的模拟电路部分通常采用电阻网络如R-2R梯形网络或电容阵列结构。它将保持寄存器中的数字量转换成对应的模拟电压。这个转换需要一定的时间即建立时间Settling Time。输出缓冲器这是一个运算放大器构成的缓冲级。它的作用有两个一是增强DAC的输出驱动能力使其能够直接驱动一定的外部负载如10kΩ到50kΩ的电阻二是提供低输出阻抗减少外部电路对DAC输出电压的影响。需要注意的是这个缓冲器是可以被软件关闭的。关闭后DAC的输出阻抗会变高驱动能力变弱但可以改善建立时间和功耗在驱动高阻抗负载如运放的同相输入端时可以考虑关闭。数据通路是这样的CPU或DMA将数据写入DAC_DHRx- 数据被锁存到数据保持寄存器- 在指定的触发事件或软件触发到来时保持寄存器的数据被加载到DAC转换器核心- 转换器开始工作经过一个建立时间后模拟电压稳定出现在输出引脚上并经过输出缓冲器后输出到芯片外部。理解这个通路非常重要。比如当你发现输出电压更新不及时就要考虑是数据写入慢了还是触发事件频率不对或者是输出缓冲器驱动能力不足导致外部负载影响了响应速度。2.2 触发机制谁来决定DAC何时更新DAC不会自动、连续地转换数据寄存器里的值。它需要一个“开始转换”的信号这就是触发。STM32的DAC支持多种触发源软件触发通过设置控制寄存器中的TENx位为0然后向SWTRIG位写1来手动触发一次转换。这种方式最简单但时序完全由软件控制难以保证精确的周期性。定时器触发这是最常用、最强大的方式。你可以将一个定时器如TIM2, TIM4, TIM6, TIM7等的更新事件UEV连接到DAC的触发输入。这样DAC的更新频率就由定时器的溢出频率精确控制。例如定时器每1ms溢出一次DAC就会每1ms更新一次输出电压。这对于生成任意波形正弦波、三角波至关重要。外部引脚触发某些型号支持通过外部EXTI线如EXTI9触发。适用于需要与外部事件同步的场景。关键点在于触发事件发生时DAC会将DAC_DHRx寄存器中的数据加载到其内部的保持寄存器中并启动一次新的数模转换。因此你必须在下次触发到来之前将新的数据写入DAC_DHRx。如果使用定时器触发通常我们会配合DMA在定时器中断或DMA传输完成中断中准备下一批数据或者直接让DMA自动从内存搬运数据到DAC_DHRx从而实现不间断的波形输出。2.3 分辨率与参考电压你的输出精度天花板DAC的性能有两个硬性指标分辨率和参考电压。分辨率STM32的DAC通常是12位的。这意味着DAC_DHRx寄存器可以存放0到40952^12 - 1之间的数字。这个数字我们称为DigitalValue。参考电压 (Vref)这是DAC转换的基准。DAC输出的模拟电压Vout由公式决定Vout (DigitalValue / 4095) * Vref对于大多数STM32Vref引脚通常与VDDA模拟电源相连典型值为3.3V。因此当DigitalValue为4095时Vout约为3.3V为0时Vout为0V。有些型号有独立的Vref引脚可以接入更精准、更稳定的基准电压源如2.5V的REF系列芯片从而获得更高精度的输出电压。一个常见的误解认为12位DAC的最小电压步进LSB是3.3V / 4095 ≈ 0.806mV。理论上是的但实际上由于DAC的积分非线性INL、微分非线性DNL误差以及参考电压的噪声实际精度会打折扣。在要求不高的场合我们可以按此计算在高精度应用中必须查阅芯片数据手册的电气特性章节了解其实际精度指标并做好PCB的模拟部分布局布线如将VDDA、VSSA与数字电源分开增加滤波电容。3. 硬件连接与工程准备理论清楚了我们开始动手。一个完整的DAC实验硬件连接是基础它直接决定了信号的质量。3.1 最小系统与引脚确认首先你需要一块STM32开发板如STM32F103C8T6核心板、STM32F407 Discovery等。找到芯片数据手册或开发板原理图确认DAC输出引脚。以STM32F103C8T6为例DAC通道1输出PA4DAC通道2输出PA5这两个引脚是复用功能除了作为DAC输出还可以作为普通GPIO或ADC输入。在我们的实验中需要将其配置为DAC模拟输出模式。硬件连接清单STM32开发板及USB数据线用于供电和程序下载。示波器这是观察DAC输出波形最直观、最必要的工具。如果没有示波器也可以用万用表测量直流电压但无法观察动态波形。杜邦线若干。可选一个运算放大器如LM358、电阻、电容用于搭建一个简单的放大或滤波电路验证DAC驱动外部电路的能力。3.2 开发环境与工程创建这里以使用最广泛的STM32CubeIDE基于HAL库为例进行说明。如果你习惯标准库或LL库原理相通只是函数名称和配置方式略有差异。新建工程打开STM32CubeIDE选择对应的芯片型号如STM32F103C8Tx。图形化配置 (CubeMX)引脚配置在Pinout Configuration标签页找到PA4引脚。点击它在弹出的功能选择菜单中选择ADC1_IN4等等这里有个坑对于DAC输出正确的选择是DAC_OUT1。同样将PA5配置为DAC_OUT2。配置成功后引脚会变成绿色。DAC模块配置在左侧分类中找到Analog-DAC1。勾选Channel 1和/或Channel 2。Output Buffer选择Enable。初次实验建议开启缓冲驱动能力强。后续可以尝试关闭观察波形变化。Trigger选择None。我们先从最简单的“无触发”模式开始即DAC输出由软件直接控制写入数据后立即更新实际上仍有内部延迟。后续生成波形时再改为Timer 6 Trigger Out event之类的定时器触发。Wave generation mode选择None。波形生成模式是DAC的一个高级功能可以自动生成噪声波或三角波我们先不用。为后续实验准备定时器配置如果我们计划用定时器触发DAC生成波形可以提前配置一个定时器。例如配置TIM6一个基本定时器常专用于DAC触发。在Timers-TIM6中使能它。设置Prescaler预分频器和Counter Period自动重装载值ARR以得到所需的触发频率。例如如果系统时钟是72MHz预分频器设为71ARR设为999则定时器更新频率为72MHz / (711) / (9991) 1000Hz即每秒触发DAC 1000次。最关键的一步在Trigger Output (TRGO) Parameters部分将Master Mode Selection设置为Update Event。这样TIM6的每次更新事件就会在其TRGO引脚上产生一个触发信号这个信号可以被DAC捕获。回到DAC配置将DAC的Trigger改为Timer 6 Trigger Out event。这样DAC的更新就与TIM6的更新同步了。可选DMA配置如果要输出一段连续的波形数据如一个正弦波表为了不占用CPU可以配置DMA。在DAC1的DMA Settings标签页点击Add选择DAC1Stream通常为固定的如DMA1_Stream5 for DAC1 on F4方向为Memory-To-Peripheral模式为Circular循环模式。这样DMA会自动循环地将内存中的波形数据搬运到DAC数据寄存器。生成代码配置完成后点击Project - Generate Code。CubeIDE会自动生成初始化代码包括MX_DAC_Init()和MX_TIM6_Init()等函数。4. 基础实验输出固定电压与三角波我们从最简单的开始验证DAC的基本功能。4.1 实验一软件控制输出固定电压这个实验的目标是通过软件写入一个数字值让PA4引脚输出一个固定的直流电压并用万用表测量验证。核心代码main.c中的while循环之前// DAC已经由CubeMX初始化好了缓冲开启无触发模式。 // 我们直接使用HAL库函数设置输出电压。 uint16_t dac_value 2048; // 12位数据中间值 2048 / 4095 * 3.3V ≈ 1.65V if (HAL_DAC_SetValue(hdac1, DAC_CHANNEL_1, DAC_ALIGN_12B_R, dac_value) ! HAL_OK) { Error_Handler(); } // 对于无触发模式设置值后需要启动DAC转换对于有些HAL版本SetValue已包含启动 if (HAL_DAC_Start(hdac1, DAC_CHANNEL_1) ! HAL_OK) { Error_Handler(); }将程序下载到开发板用万用表测量PA4或连接了PA4的排针与GND之间的电压。你应该能测量到大约1.65V的电压。尝试修改dac_value为0、1024、4095分别测量电压应该接近0V、0.825V、3.3V。注意实测电压可能与计算值有微小偏差这主要是参考电压VDDA并非精确的3.300V以及DAC自身误差导致的。这是正常现象。4.2 实验二生成一个周期性的三角波现在我们来点动态的。不使用定时器触发而是在主循环里不断修改DAC值模拟一个三角波。这种方法简单但波形频率不稳定受循环内其他代码影响。核心思路定义一个变量dac_value和一个方向标志direction递增或递减。在主循环while(1)中根据方向增加或减少dac_value。每次变化后调用HAL_DAC_SetValue更新输出。加入一个简单的HAL_Delay来控制更新速度但这很不精确。核心代码片段uint16_t dac_value 0; int8_t direction 1; // 1表示增加-1表示减少 HAL_DAC_Start(hdac1, DAC_CHANNEL_1); while (1) { // 更新DAC值 if (HAL_DAC_SetValue(hdac1, DAC_CHANNEL_1, DAC_ALIGN_12B_R, dac_value) ! HAL_OK) { Error_Handler(); } // 更新下一个值 dac_value direction; if (dac_value 4095) { dac_value 4095; direction -1; } else if (dac_value 0) { dac_value 0; direction 1; } // 延迟控制频率粗略 HAL_Delay(1); // 每1ms更新一次理论三角波周期约为 4096*2ms ≈ 8.2秒 }用示波器探头连接到PA4你应该能看到一个阶梯状的三角波。因为每次更新电压后电压会保持1ms所以波形是阶梯状的而非光滑直线。同时由于HAL_Delay本身精度和循环内其他潜在开销波形的周期和占空比可能不精确。5. 进阶实战定时器触发与DMA输出精确波形要产生稳定、高频、光滑的波形必须将DAC的更新从不可控的主循环中解放出来交给硬件定时器和DMA。5.1 配置定时器触发按照第3.2节的说明在CubeMX中配置TIM6并让DAC1通道1使用TIM6的更新事件作为触发源。生成代码后在main函数中启动定时器。关键代码// 启动定时器它将周期性地产生触发信号 if (HAL_TIM_Base_Start(htim6) ! HAL_OK) { Error_Handler(); } // 启动DAC并指定其触发源 if (HAL_DAC_Start(hdac1, DAC_CHANNEL_1) ! HAL_OK) { Error_Handler(); } // 注意此时DAC不会立即输出它等待第一次定时器触发此时如果你还没有给DAC数据寄存器写入数据DAC输出可能为0或不确定。DAC会在每次定时器触发时将当前DAC_DHRx寄存器中的值转换输出。所以我们需要在触发之前就把数据准备好。5.2 配合DMA输出正弦波这是DAC应用的经典场景。我们预计算一个正弦波表数组存放在内存中然后让DMA在定时器的每次触发时自动将数组中的下一个值搬运到DAC的数据寄存器。步骤详解生成正弦波表#define SINE_WAVE_TABLE_SIZE 100 // 一个周期的点数 uint16_t sineWaveTable[SINE_WAVE_TABLE_SIZE]; void generate_sine_wave_table(void) { for (int i 0; i SINE_WAVE_TABLE_SIZE; i) { // 计算正弦值范围[-1, 1] float sine_value sinf(2 * M_PI * i / SINE_WAVE_TABLE_SIZE); // 将[-1,1]映射到[0, 4095]并偏移到中间值附近避免负电压单电源供电 // 例如输出以1.65V为中心幅值1V的正弦波: Vout 1.65 1.0 * sin(...) // DigitalValue (Vout / Vref) * 4095 // 假设Vref3.3V则中心值对应 1.65/3.3*4095 ≈ 2048 // 幅值1V对应 1.0/3.3*4095 ≈ 1241 float voltage 1.65f 1.0f * sine_value; // 电压值 if (voltage 0) voltage 0; if (voltage 3.3) voltage 3.3; sineWaveTable[i] (uint16_t)((voltage / 3.3f) * 4095); } }在main函数初始化部分调用这个函数。配置并启动DMA 在CubeMX中已为DAC1添加了DMA流。生成代码后我们需要手动启动DMA传输。// 启动DAC的DMA传输 if (HAL_DAC_Start_DMA(hdac1, DAC_CHANNEL_1, (uint32_t*)sineWaveTable, SINE_WAVE_TABLE_SIZE, DAC_ALIGN_12B_R) ! HAL_OK) { Error_Handler(); } // 启动定时器 if (HAL_TIM_Base_Start(htim6) ! HAL_OK) { Error_Handler(); }HAL_DAC_Start_DMA函数会做几件事配置DMA将sineWaveTable的地址作为源地址DAC的数据寄存器地址作为目标地址设置数据长度和对齐方式并启动DMA循环传输。计算与验证波形频率定时器触发频率 (Ft)由TIM6的配置决定。例如系统时钟72MHz预分频器71ARR为999则Ft 72MHz / (711) / (9991) 1000 Hz。DAC更新频率等于定时器触发频率即每秒更新1000次。输出正弦波频率 (Fout)Fout Ft / SINE_WAVE_TABLE_SIZE 1000 Hz / 100 10 Hz。 所以我们将在示波器上看到一个10Hz的正弦波。要改变输出频率可以调整定时器的分频和重载值改变Ft或者调整正弦波表的点数改变一个周期的采样数。示波器实测将探头连接到PA4你应该能看到一个非常光滑、稳定的10Hz正弦波。尝试改变SINE_WAVE_TABLE_SIZE或定时器参数观察波形频率的变化。你会发现通过这种方法可以轻松生成数百Hz甚至更高频率的波形受限于DAC的建立时间和DMA速度。5.3 动态更新波形双缓冲技巧上面的例子是静态波形表。如果我们想实时改变波形比如改变频率、幅度甚至切换成方波、三角波怎么办直接在DMA传输过程中修改sineWaveTable数组是危险的可能导致输出波形出现毛刺或断裂。一个实用的技巧是使用双缓冲Ping-Pong Buffer。原理是准备两个一样大的缓冲区BufferA和BufferB。当DMA正在从BufferA中读取数据并发送给DAC时CPU可以安全地准备下一帧数据到BufferB。当DMA完成BufferA的传输后会产生一个传输完成中断或半传输完成中断在中断服务程序里我们迅速将DMA的目标地址切换到BufferB同时CPU开始填充BufferA。如此循环。HAL库的DMA循环模式本身并不直接支持双缓冲但我们可以利用DMA的“半传输完成中断”HT和“传输完成中断”TC来模拟。或者更直接的方法是使用更底层的寄存器操作或LL库来配置双缓冲模式。对于STM32F4等系列DMA控制器本身支持双缓冲DMA_CR寄存器的DBM位。简化实现思路利用半传输和传输完成中断使能DMA流的“半传输中断”和“传输完成中断”。在CubeMX生成的stm32f4xx_it.c中找到对应的DMA中断服务函数如DMA1_Stream5_IRQHandler。在中断函数中判断中断标志void DMA1_Stream5_IRQHandler(void) { if(__HAL_DMA_GET_FLAG(hdma_dac1, DMA_FLAG_HTIF1_5)) { // 半传输完成 // 此时前半部分Buffer前半已传输完后半部分正在传输 // CPU可以安全地修改前半部分缓冲区数据 __HAL_DMA_CLEAR_FLAG(hdma_dac1, DMA_FLAG_HTIF1_5); } if(__HAL_DMA_GET_FLAG(hdma_dac1, DMA_FLAG_TCIF1_5)) { // 传输完成 // 此时整个缓冲区Buffer已传输完DMA将回到开头重新开始 // CPU可以安全地修改整个缓冲区数据但时间较短 __HAL_DMA_CLEAR_FLAG(hdma_dac1, DMA_FLAG_TCIF1_5); } // ... 调用HAL库的中断处理函数 }这种方法对CPU中断响应时间要求较高如果波形数据点很多、更新频率很快CPU可能会来不及准备新数据。对于复杂的实时波形生成需要考虑使用更大的缓冲区、降低波形更新率或者使用更强大的芯片如带DAC的DSP或高性能MCU。6. 性能优化与常见问题排查在实际项目中使用DAC可能会遇到各种问题。下面分享一些我踩过的坑和优化经验。6.1 输出噪声与纹波抑制即使代码正确用示波器观察DAC输出也可能看到一些你不想要的“毛刺”或高频噪声。电源噪声这是最主要的噪声来源。DAC的参考电压Vref通常连着VDDA如果不够干净会直接反映在输出上。对策确保模拟电源VDDA和数字电源VDD之间使用磁珠或0Ω电阻隔离。在VDDA和VSSA引脚附近尽可能靠近芯片放置一个10uF的钽电容和一个0.1uF的陶瓷电容进行滤波。如果板子有独立的Vref引脚强烈建议连接一个低噪声、高精度的基准电压源芯片。数字开关噪声当MCU内部高速数字电路如CPU、总线、其他外设工作时会产生高频噪声通过电源或地平面耦合到敏感的模拟部分。对策良好的PCB布局布线是关键。将模拟部分DAC输出、运放、滤波电路集中放置并尽量远离数字噪声源如晶振、开关电源、高速数据线。使用独立的模拟地平面并通过单点与数字地连接。输出缓冲器的影响开启输出缓冲器可以提供更强的驱动能力但缓冲器本身会引入一定的噪声和失调电压。关闭缓冲器可以获得更纯净的输出但驱动能力骤降只能驱动高阻抗负载。对策根据负载情况选择。驱动运放同相输入端高阻抗可以关闭缓冲器。驱动直接接负载如耳机、低阻值电阻必须开启。可以用示波器对比开关缓冲器时的输出噪声选择更优方案。软件引起的毛刺如果在DAC转换过程中两次触发之间错误地写入了数据寄存器会导致输出出现一个短暂的错误电压。对策确保数据更新时序正确。使用DMA是避免此问题的最佳方法。如果使用中断更新确保在DAC触发事件发生前足够早的时间完成数据写入。6.2 建立时间与输出带宽限制DAC不是理想器件从数字量写入到输出电压稳定到目标值误差范围内需要时间这就是建立时间。STM32 DAC的建立时间在数据手册中有明确给出通常在几微秒量级与负载有关。影响这决定了DAC能够输出的最高信号频率。根据奈奎斯特采样定理输出信号的最高频率分量必须小于采样频率即DAC更新频率的一半。但实际上由于建立时间的限制即使更新频率很高DAC也无法完美重建高频信号输出波形会变得“圆滑”高频分量衰减。实测建议当你试图输出一个很高频率的正弦波比如更新频率1MHz输出10kHz正弦波发现波形幅度下降或失真时很可能是受到了DAC建立时间和输出缓冲器带宽的限制。此时需要降低期望的输出频率或者查阅芯片手册确认在当前配置下的最大可达到的无杂散动态范围SFDR和带宽。6.3 调试技巧与示波器使用无输出或输出为0检查引脚配置首先确认CubeMX中引脚是否已正确设置为DAC_OUTx。用万用表测量引脚对地电阻如果被配置为推挽输出且输出低电阻会很低。检查DAC和定时器是否启动确保调用了HAL_DAC_Start()和HAL_TIM_Base_Start()。检查触发模式如果使用了定时器触发但定时器没启动DAC会一直等待触发没有输出。可以先切换回软件触发或无触发模式验证DAC基本功能。检查数据写入使用调试器在HAL_DAC_SetValue函数后设置断点查看传入的dac_value是否正确以及函数返回值是否为HAL_OK。输出波形频率不对计算定时器频率仔细核对系统时钟、定时器预分频器PSC和自动重载值ARR的计算。记住定时器频率 系统时钟 / (PSC1) / (ARR1)。检查DMA传输大小确认HAL_DAC_Start_DMA中传入的数据长度Length是否正确。它应该等于波形表的点数而不是字节数。示波器测量直接用示波器测量DAC输出波形的周期反推频率。同时可以测量定时器触发引脚如果有引出或另一个GPIO在定时器更新中断中翻转的频率来验证定时器是否按预期工作。波形有台阶或毛刺台阶这是正常的因为DAC输出是离散的。提高一个周期内的采样点数波形表大小台阶会变细经过输出缓冲器和外部电路的低通滤波后波形会更光滑。周期性毛刺可能与电源噪声同步检查电源质量。也可能与CPU活动周期同步尝试在DAC输出期间关闭其他高优先级中断或外设。随机毛刺检查PCB布局是否存在信号串扰。确保模拟地线干净。通过以上这些步骤你应该能够驾驭STM32的DAC让它成为你项目中连接数字与模拟世界的可靠桥梁。从输出一个稳定的直流电压到生成复杂的任意波形DAC的应用场景非常广泛。关键在于理解其工作原理合理配置触发和DMA并注意模拟电路设计的细节。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门