STM32 DAC实战指南:从基础配置到DMA任意波形输出
发布时间:2026/8/28 7:01:11 作者:尧图编辑部 阅读量:1,286

1. 项目概述从数字到模拟的桥梁玩STM32的兄弟不知道你有没有过这样的经历用PWM调个LED亮度或者驱动个舵机感觉还行但一遇到需要真正平滑、精确的模拟电压输出时比如做个简易信号发生器、控制一个线性电源的基准或者驱动一个高保真音频的DAC芯片PWM那套就显得力不从心了。这时候STM32片内集成的DAC数模转换器就该登场了。今天聊的就是基于STM32F103C8T6这类“Mini”核心板把DAC功能彻底玩转的实验笔记。这不是一份照搬手册的说明书而是我踩过坑、调通代码后把核心原理、寄存器操作、HAL库用法以及那些容易忽略的细节揉碎了分享给你。简单说DAC就是把你程序里那个冰冷的数字值比如0-4095转换成实实在在的、连续可调的模拟电压比如0-3.3V。STM32的DAC模块用起来其实很优雅它不像ADC那样受外部噪声影响大输出相对稳定。核心就两件事一是配置让DAC硬件电路工作起来二是写入数据告诉它你要输出多少电压。听起来简单但怎么配置最稳怎么输出波形怎么结合DMA和定时器玩出花样这就是咱们接下来要深挖的。无论你是刚接触STM32想搞懂片上外设还是正在做需要模拟输出的项目这篇笔记里的实操细节和避坑指南都能直接拿来用。2. DAC模块核心原理与STM32实现拆解2.1 DAC工作的底层逻辑不仅仅是写一个值很多人觉得DAC就是往数据寄存器里写个数电压就出来了。这没错但了解背后的机制才能用好它尤其在要求精度和速度的时候。STM32的DAC本质上是一个“数字电位计输出缓冲器”的组合。首先DAC的核心是电阻网络常见的有R-2R梯形网络。当你写入一个12位数字量假设是12位分辨率时芯片内部的一堆模拟开关会根据这个数字量把不同的电阻连接到参考电压或地上从而在输出点产生一个对应的分压。这个分压是离散的但由于电阻网络的精细设计其台阶可以非常小对于3.3V参考12位分辨率下每一步约0.8mV在人眼或许多电路看来就是连续的。STM32的DAC模块提供了两个输出通道DAC1, DAC2对应PA4和PA5引脚但需注意复用。每个通道都包含数字接口负责接收来自内核直接写寄存器、DMA或定时器触发器的数据。数据保持寄存器DHRx这是我们软件直接操作的寄存器。写入的数据先放在这里。数据输出寄存器DORx这才是真正驱动DAC电阻网络的数据寄存器。DHRx到DORx的传输可以立即发生也可以在特定触发事件如定时器更新发生时同步发生这对于生成同步波形至关重要。模拟输出缓冲器这是一个运算放大器用于增强DAC的带载能力降低输出阻抗。你可以选择启用或禁用它。启用时输出驱动能力强但输出电压范围被限制在大概0.2V到Vref-0.2V之间禁用时输出阻抗高但电压范围可以接近电源轨0-Vref适合外接高速运放。关键一点是参考电压Vref。对于STM32F103DAC的参考电压通常与ADC共享即连接到VDDA模拟电源通常与VCC同为3.3V。这意味着DAC的输出电压范围是0到VDDA。你的数字量如0-4095线性映射到这个电压范围。所以计算输出电压的公式很简单Vout (DORx / 4095) * Vref对于12位右对齐数据。如果你的VDDA不稳输出就会飘这是高精度应用首先要解决的问题。2.2 STM32F103 DAC外设特性与配置要点以最经典的STM32F103C8T6为例它有一个DAC模块包含两个独立的通道。每个通道支持8位/12位模式数据格式可以是右对齐、左对齐仅12位或8位右对齐。配置时以下几个寄存器是关键DAC_CR控制寄存器这是大脑。你需要在这里使能DAC通道ENx位、使能输出缓冲BOFFx位、选择触发源TSELx位、选择波形生成模式WAVEx位等。一个常见的坑是如果你选择使用定时器触发TSELx不为000那么必须同时使能触发TENx位设为1否则数据不会从DHRx传输到DORx也就没有输出。DAC_DHR12Rx12位右对齐数据保持寄存器我们最常用的数据寄存器。向这里写数据如果触发使能且发生了触发事件或者触发被禁用TENx0数据就会在APB1时钟的一个周期后传到DORx并输出。DAC_SWTRIGR软件触发寄存器当触发源选择为软件触发TSELx111时向SWTRIGx位写1可以产生一个触发事件手动将DHRx的数据搬移到DORx。在HAL库中这些底层操作被封装成了清晰的API。初始化通过HAL_DAC_Init()和HAL_DAC_ConfigChannel()完成设置触发、波形等。写入数据则用HAL_DAC_SetValue()它帮你处理数据对齐。读取当前输出值用HAL_DAC_GetValue()。但HAL库的便利性背后理解上述寄存器逻辑才能在你遇到输出不对、波形不同步等问题时快速定位。注意引脚复用与模拟配置。DAC输出引脚PA4/PA5默认是GPIO功能。在初始化DAC前必须通过HAL_DAC_MspInit()回调函数或在CubeMX中配置将这些引脚设置为模拟模式Analog。如果配置为推挽输出等其他模式DAC输出会被阻断或者导致引脚冲突。这是新手最容易疏忽的一点症状就是量不到电压或者电压不对。3. 基础实验输出固定电压与三角波3.1 环境搭建与CubeMX配置我们以STM32F103C8T6核心板使用STM32CubeIDE和HAL库为例。首先打开CubeMX选择你的芯片型号。引脚配置在Pinout视图找到PA4DAC_OUT1和PA5DAC_OUT2。点击它们选择“Analog”模式。CubeMX会自动将引脚功能设置为DAC。DAC模块配置在左侧“Analog”分类下点击“DAC”。勾选“OUT1 Configuration”或“OUT2 Configuration”来使能通道。Output Buffer默认Enable。如果你的负载很轻且需要满幅输出接近0V和3.3V可以Disable以获得更宽的电压范围但记得外部要接运放做缓冲。Trigger选择“None”表示不使用硬件触发数据写入后立即生效。选择“Timer X”则可以由定时器自动触发转换用于波形生成。Wave generation选择“None”是普通模式。这里我们可以先选“Triangle”来体验硬件生成三角波的功能。Data Format选择“12-bit right alignment”最常用。时钟配置确保APB1总线时钟DAC挂载在APB1上已正确开启。通常系统时钟配置好后会自动设置。生成代码配置好工程名、路径和工具链后生成代码。3.2 输出固定电压代码实现与精度验证在生成的工程中进入main.c的main()函数在初始化部分之后MX_DAC_Init()被调用后我们就可以控制DAC了。// 1. 简单的固定电压输出 (输出约1.65V) uint32_t dac_value 2048; // 12位最大值4095对应3.3V 2048对应约1.65V HAL_DAC_SetValue(hdac, DAC_CHANNEL_1, DAC_ALIGN_12B_R, dac_value); HAL_DAC_Start(hdac, DAC_CHANNEL_1); // 启动DAC转换输出这段代码会将DAC通道1的输出固定在约1.65V。用万用表测量PA4引脚应该能看到一个稳定的直流电压。你可以改变dac_value从0到4095观察电压从0V线性增长到约3.3V。实操心得测量技巧测量时万用表表笔最好直接点在芯片引脚上而不是开发板的排针以减少接触电阻和干扰。如果开发板有模拟地AGND测试点就用它作为参考地。精度验证你会发现当dac_value0时输出电压可能不是0.000V而是几毫伏到几十毫伏零点误差当dac_value4095时也可能达不到3.300V满量程误差。这是DAC的固有误差数据手册里用“偏移误差”、“增益误差”来描述。对于一般应用可以忽略高精度应用则需要校准。负载影响如果输出缓冲使能DAC可以驱动轻负载如5kΩ。如果直接驱动一个LED需串联限流电阻你会发现电压会被拉低因为输出电流能力有限数据手册有最大输出电流参数。驱动低阻抗负载必须外接运放。3.3 硬件生成三角波利用DAC内置波形发生器STM32的DAC有一个很酷的功能内置波形发生器可以自动生成三角波无需CPU持续干预。我们在CubeMX里已经将“Wave generation”设为了“Triangle”。// 2. 配置并启动硬件三角波 HAL_DACEx_TriangleWaveGenerate(hdac, DAC_CHANNEL_1, DAC_TRIANGLEAMPLITUDE_4095); HAL_DAC_SetValue(hdac, DAC_CHANNEL_1, DAC_ALIGN_12B_R, 0); // 设置三角波基线最小值 HAL_DAC_Start(hdac, DAC_CHANNEL_1);DAC_TRIANGLEAMPLITUDE_4095表示三角波的幅度为4095个LSB即满量程。三角波会从你设置的基线值这里为0开始上升到基线幅度再下降回基线如此循环。频率由什么决定它由你选择的触发源频率决定。如果触发源是“None”软件触发则你需要不断调用HAL_DACEx_TriangleWaveGenerate来步进这没有意义。因此硬件波形生成必须配合定时器触发。配置定时器触发三角波回到CubeMX将DAC的“Trigger”改为“Timer 6”举例。配置一个定时器如TIM6作为基本定时器设置其预分频器和自动重载值ARR以产生所需的触发频率。三角波的频率F_triangle F_trigger / (2 * amplitude * 2)。对于4095幅度F_triangle ≈ F_trigger / 16380。如果你想生成100Hz的三角波触发频率需设为1.638MHz。定时器时钟若为72MHz则需计算合适的预分频器和ARR值。在代码中启动定时器然后像上面一样配置并启动DAC三角波。用示波器观察PA4就能看到一个非常平滑的三角波。它的优势是CPU占用率极低波形由硬件自动生成精度和稳定性极高。4. 进阶应用DMA定时器实现任意波形输出硬件三角波虽好但只能生成一种波形。更通用的方法是利用DMA将一段代表波形数据的数组自动搬运到DAC的数据寄存器再由定时器定时触发DAC更新。这样就能输出任意形状的波形比如正弦波、方波、自定义波形等。4.1 系统架构与配置思路这个方案的流程是定时器溢出事件 - 触发DAC转换请求 - DAC向DMA发送数据请求 - DMA从内存数组搬运一个数据到DAC_DHR - DAC输出对应电压。如此循环形成连续波形。CubeMX关键配置步骤DAC使能通道1触发源选择“Timer 6 Trigger Out event”。输出缓冲根据需要选择。定时器TIM6配置为基本定时器。计算波形采样率。例如要输出1kHz的正弦波一个周期至少需要几十个点如100个点那么定时器触发频率即DAC更新率应为 1kHz * 100 100kHz。根据APB1时钟72MHz计算预分频器PSC和自动重载值ARR。DMA在DAC配置页添加DMA请求。方向为“Memory To Peripheral”数据宽度都选“Word”半字也可但对齐要注意。模式选择“Circular”循环模式这样DMA在传输完数组后会自动从头开始实现连续输出。生成代码。4.2 生成正弦波数据表与代码实现首先我们需要在内存中准备一个正弦波的数据数组。数组长度决定了一个周期波形的“细腻度”。// 生成一个周期的正弦波数据表 (12位 满量程4095) #define SINE_WAVE_LENGTH 100 // 一个周期100个点 uint32_t sine_wave[SINE_WAVE_LENGTH]; void generate_sine_wave(void) { for (int i 0; i SINE_WAVE_LENGTH; i) { // 计算正弦值范围[-1, 1] float sine_value sinf(2 * M_PI * i / SINE_WAVE_LENGTH); // 偏移到[0, 1]然后缩放到[0, 4095] sine_wave[i] (uint32_t)((sine_value 1.0f) / 2.0f * 4095.0f); } }在main()函数中// 初始化 generate_sine_wave(); HAL_DAC_Start_DMA(hdac, DAC_CHANNEL_1, (uint32_t*)sine_wave, SINE_WAVE_LENGTH, DAC_ALIGN_12B_R); HAL_TIM_Base_Start(htim6); // 启动定时器开始触发HAL_DAC_Start_DMA函数会启动DMA将sine_wave数组中的数据循环不断地送往DAC。定时器TIM6以固定的频率如100kHz触发DACDAC每次被触发就通过DMA获取下一个数据并输出。这样在PA4引脚上就能观察到连续的正弦波。输出波形频率计算波形频率 定时器触发频率 / 波形数据表长度例如定时器配置为100kHz正弦波表长度为100则输出正弦波频率为 100kHz / 100 1kHz。4.3 动态更新波形与双通道同步更高级的玩法是动态更新DMA传输的目标数组。例如你可以通过串口接收新的波形数据然后在不停止输出的情况下安全地更新sine_wave数组。需要注意的是在DMA循环传输过程中直接写数组可能导致波形撕裂一部分旧数据一部分新数据。一个稳妥的方法是使用双缓冲区准备两个数组BufferA和BufferBDMA当前传输BufferA时CPU向BufferB填充新数据填充完成后通过DMA提供的API如HAL_DAC_Stop_DMA后重新配置地址启动或更优雅地使用DMA双缓冲模式切换传输目标到BufferB。对于双通道同步输出例如立体声音频STM32的DAC可以配置为使用同一个定时器触发两个通道。在CubeMX中将两个DAC通道的触发源都设置为同一个定时器如TIM6。然后使用HAL_DAC_Start_DMA分别启动两个通道的DMA传输填充不同的数据数组。由于触发源相同两个通道的更新将严格同步这对于需要相位关系的应用至关重要。5. 常见问题、调试技巧与精度优化5.1 典型问题排查速查表现象可能原因排查步骤与解决方案无电压输出1. DAC未使能。2. 引脚未配置为模拟模式。3. 输出缓冲使能且负载过重/短路。4. 未调用HAL_DAC_Start()。1. 检查hdac.Instance-CR寄存器的ENx位是否为1。2. 检查GPIO配置确保为ANALOG。3. 断开负载测量或尝试禁用输出缓冲需外接运放。4. 确认启动函数被正确调用。输出电压值不对1. 参考电压VDDA不准确。2. 数据对齐格式错误。3. 写入的数据寄存器不对如该写DHR却写了DOR。4. 触发模式配置错误。1. 用万用表测量VDDA引脚电压确保稳定在3.3V。高精度应用建议使用外部基准源。2. 检查HAL_DAC_SetValue或写入寄存器时的对齐参数必须与配置一致。3. 使用HAL库API可避免此问题。4. 若使用触发检查TENx位是否使能触发事件是否发生。波形输出不稳定、有毛刺1. 电源噪声。2. DMA传输速率过快CPU或总线带宽不足。3. 数组数据计算有误或越界。4. 未使用定时器触发而是用软件循环写入时序不精确。1. 在VDDA和VSSA引脚就近接滤波电容如10uF0.1uF。2. 降低定时器触发频率或检查是否有更高优先级中断打断了DMA。3. 调试查看数组内容确保数据在0-4095范围内。4. 务必使用“定时器触发DMA”方案以获得稳定波形。DMA传输不循环或中途停止1. DMA未配置为循环模式Circular。2. DMA传输完成中断中处理不当。3. 数组地址或长度配置错误。1. 在CubeMX中确认DMA模式为“Circular”。2. 如果启用了传输完成中断确保在其中没有进行可能导致DMA停止的操作。3. 检查HAL_DAC_Start_DMA的参数地址和长度是否正确。5.2 精度优化实战建议电源是根基DAC的精度直接依赖于参考电压Vref即VDDA的质量。对于核心板VCC通常来自USB或LDO噪声较大。如果项目对精度要求高10位强烈建议使用独立的、低噪声的LDO为VDDA引脚供电。在VDDA和VSSA引脚上尽可能靠近芯片放置高质量的滤波电容典型值为一个10μF的钽电容并联一个0.1μF的陶瓷电容。将VDDA与数字VCC隔离使用磁珠或0Ω电阻连接并在隔离后的模拟侧再加强滤波。布局与接地模拟部分DAC输出、运放电路的走线应远离数字高速信号线如时钟、PWM。采用星型单点接地将模拟地AGND和数字地DGND在芯片下方或电源入口处单点连接。输出缓冲与运放片内输出缓冲虽然方便但其带宽、压摆率和输出电流有限。对于驱动低阻抗负载或需要高速、高精度的场合禁用片内缓冲外接一个精密运算放大器如OPA365, ADA4899-1等构成电压跟随器或放大电路。这能显著改善动态性能。软件校准虽然STM32的DAC出厂有一定精度但通过两点校准可以进一步提高。方法测量DAC输出为0码和4095码时的实际电压值V0, Vfs。建立线性方程实际电压 (原始码值 * (Vfs - V0) / 4095) V0。在输出前将目标电压按此方程的逆运算转换为需要写入的码值。可以将校准系数存储在Flash中。调试时示波器是你的最佳伙伴。除了看波形形状更要关注电源轨上的噪声用示波器AC耦合档位观察。逻辑分析仪则可以帮助你验证定时器触发信号和DMA请求是否按预期发生。当你把这些细节都做到位STM32的DAC就能从一个简单的数模转换模块变身为你项目中稳定可靠的模拟信号源。