简介面向STM32F407的HAL库开发者这份资源提供了双DAC信号发生器与双ADC采集的完整参考实现特别适合需要多路模拟输出和多点同步采样的工业控制、音频波形生成及传感器监测等场景。资源共311个文件以C源码、H头文件、Keil工程文件.uvprojx以及编译生成的hex/axf镜像为主附带CubeMX的.ioc配置便于重新生成工程或按需裁剪压缩包整体仅16.75MB。已有614人学习可用于快速理解双通道DAC/ADC在HAL库下的初始化、时钟与触发模式设置以及双ADC同时采集两路信号、双DAC分别或同步输出波形的实现思路。内容覆盖常见波形生成如正弦波、ADC转换完成中断、DMA与定时器联动、多通道数据读取逻辑并补充了map、lst等编译过程文件方便跟踪构建细节工程结构清晰代码注释详细既适合入门者对照学习也可作为F407开发板上二次开发的基础模板。 做这个项目的起因很简单我想用STM32F407内部自带的DAC做一路信号发生器但又不想只做单通道还想同时用ADC把输出实时采回来验证波形质量。所以就有了这个“双DAC信号发生器 双ADC同步采集”的方案。STM32F407的HAL库生态相对成熟CubeMX配置外设也比标准库直观得多整件事做下来最大的感受是难点不在HAL API本身而在外设之间的触发关系、DMA数据流怎么串起来以及双ADC同步模式下的数据对齐方式。这个项目适合两类人参考一是想用F407的DAC输出任意波形正弦波、方波、三角波甚至自定义任意波的开发者二是做信号激励加采集回读的闭环测试场景比如传感器模拟、电机编码器信号仿真、音频信号注入等。文章里我会把CubeMX的配置逐项拆开把DAC的触发链路、DMA搬运、双ADC同步采样这些核心逻辑讲清楚同时附上我实际调试时踩过的坑。1. 方案选型为什么是“双DAC 双ADC”而不是外接模块1.1 项目需求拆解这个项目的本质是一个微型信号源加一个回读验证系统。信号发生器部分我用F407内部DAC输出两路独立波形两路可以是同频率同幅值、可以带相位差也可以是两路频率完全不同的波形采集部分我用两个ADC通道同时采样用于验证DAC输出是否和预期一致。为什么需要双通道单通道信号发生器最大的局限是没法输出正交信号、差分信号或者两路关联波形。比如你要模拟一个旋转变压器或者光电编码器的正余弦输出就需要两路相位差90度、频率完全一致的正弦波。另一个例子是测试音频功放左右声道需要独立的测试信号用双DAC就能一次搞定。ADC回读的价值在于形成闭环。DAC输出波形之后如果不经过任何外部分析仪器单靠肉眼没法判断波形是否失真。用ADC把输出采回来可以在代码里做简单的FFT分析或者幅值统计快速判断DAC的线性度、输出噪声和带载能力。这套方案配合上位机基本上能当一个简易的实验室信号源加示波器使用。1.2 为什么用HAL库以及HAL库的适用边界这几年我新开的F4项目基本都切到HAL库了主要原因是CubeMX的图形化配置能把外设的时钟树、DMA请求映射、中断优先级这些琐碎内容自动生成好。特别是像DAC和ADC这种涉及多外设联动定时器触发、DMA搬运的场景手写标准库代码时很容易漏掉某个寄存器的配置位用CubeMX能规避大部分低级错误。但HAL库也并非没有缺点。它对寄存器操作做了很厚的封装代码执行效率比直接操作寄存器低一些而且如果项目里大量依赖中断和DMA回调一定要清楚HAL库回调的执行上下文。比如DAC和ADC的DMA传输完成回调都叫HAL_xxx_ConvCpltCallback如果两个外设同时用DMA完成中断需要在同一个回调里区分句柄来源否则程序跑飞了都不知道问题在哪。所以这套方案里配置用CubeMX生成业务逻辑自己写中断回调里只做标志位和数据搬运不做耗时运算——这是用HAL库写项目最稳妥的开发方式。2. 硬件资源盘点与核心原理定时器触发、DMA搬运、双ADC同步采样2.1 F407的DAC外设到底能做什么STM32F407内部集成了一个DAC模块包含两个独立的输出通道DAC_OUT1对应PA4引脚DAC_OUT2对应PA5引脚。每个通道都是12位分辨率支持右对齐或左对齐写入输出范围默认在0到VREF之间。注意F407的DAC输出电压上限取决于VREF的实际接法开发板上VREF通常直接连3.3V电源所以输出范围是0到3.3V如果你的板子VREF接了外部基准源则输出上限会跟随基准电压。双DAC里的“双”字很关键。两个通道虽然共享同一个DAC外设寄存器基地址但各自有独立的DHR数据保持寄存器和DOR数据输出寄存器所以可以完全独立地配置成不同的波形、不同的幅值、不同的触发方式。两个通道也可以同时触发转换实现两路波形时间对齐输出。很多人忽略的一点是DAC的带载能力。F407内部DAC模块只是一个电压输出级驱动能力非常弱直接接到低阻抗负载上电压会被拉低波形产生明显失真。我给这套方案定的规则是DAC输出端先用电压跟随器运放缓冲再接负载或者至少确保下一级输入阻抗在几十千欧以上。2.2 定时器触发是信号发生器的灵魂DAC本身不会“自动”输出一个连续波形。你要让它输出可变的模拟电平得不停地往DHR寄存器里写数据。如果在主循环里用delay来控制写入间隔输出波形的时间精度完全取决于中断优先级和代码执行时间频率稍微高一点波形就乱了。正确的做法是让定时器以固定频率触发DAC转换。配置好定时器的自动重装值ARR和预分频值PSC之后定时器的更新事件会以时钟精确的周期产生DAC再把这个更新事件当作触发信号进行一次DAC转换。这样DAC的输出电平刷新频率就是严格受控的波形频率自然也就稳定了。F407里几乎所有的定时器都能触发DAC我选的是TIM6因为它是基础定时器没有输入捕获、输出比较这些额外功能专门用来做定时触发非常干净不容易和其他PWM输出功能冲突。信号发生器输出波形的最终频率等于DAC触发频率除以波形表的点数。假设你用一个256点的正弦波表定时器触发频率是100kHz那么输出正弦波的基频就是100kHz除以256约等于390.6Hz。这个关系式是所有DDS直接数字合成类信号发生器的核心。2.3 DMA把CPU从搬运数据中解放出来光有定时器触发还不够。如果每个DAC触发周期都要CPU去写一次DHR寄存器那么在256点正弦波表、100kHz刷新率的情况下CPU每秒要执行256万次写入操作基本上什么都别干了。DMA的作用就是接管这个搬运过程。我把波形表放在内存数组里配置DMA在每次DAC触发时自动把数组里的一个数据搬运到DAC的DHR寄存器。DMA配置成循环模式搬运完整个波形表之后自动从头开始这样CPU只需要在启动时做一次配置之后的波形输出完全不占用CPU。HAL库里有一个“DMA句柄与DAC句柄关联”的概念实际就是CubeMX自动生成的HAL_DAC_MspInit函数里把DAC句柄的DMA_Handle字段指向对应的DMA通道句柄。只要CubeMX里把DAC的DMA请求勾选上这个关联关系会自动建立不需要自己写额外的关联代码。2.4 双ADC同步采样两只眼睛同时看ADC部分我的需求是同时采集两路信号并且要求两路采集时刻严格对齐。如果分别用两个ADC做独立采集即使触发源相同由于各自转换的启动延迟不一样采样时间点也会有微小偏差。相比之下F407的ADC1和ADC2支持一个“双ADC同步规则模式”Dual regular simultaneous mode一个ADC做主机另一个做从机主机启动转换时从机同步启动两路采样的时间点偏差被硬件控制在几个时钟周期以内。这个模式在CubeMX里的配置方式比较隐蔽不是在ADC1和ADC2里分别勾选模式而是在ADC1的模式设置里选择“Dual regular simultaneous mode only”然后把ADC2配置为从机。两个ADC可以各自选择不同的采样通道比如ADC1采集DAC1输出的PA4引脚ADC2采集DAC2输出的PA5引脚。转换结果通过DMA搬运到内存数据排布是ADC1和ADC2各占一个半字组成一个32位数据。3. CubeMX配置实操从时钟树到DMA链路3.1 时钟树配置要点STM32F407的时钟树在CubeMX里看起来复杂但真正需要关心的就三件事系统主频、APB1分频、APB2分频。我用的是外部8MHz晶振通过PLL倍频到168MHz系统主频APB1分频系数是4得到42MHz总线时钟APB2分频系数是2得到84MHz总线时钟。DAC外设挂在APB1总线上它的输入时钟就是42MHz。定时器TIM6也挂在APB1上且因为APB1预分频系数不为1定时器时钟自动倍频到84MHz。这里有个容易混淆的点定时器TIM6的时钟是84MHz不是42MHzCubeMX里在TIM6配置页能看到时钟频率已经自动算好。DAC的触发频率计算、分频系数设定都以84MHz为基准。ADC外设挂在APB2总线上输入时钟84MHz。F407的ADC最高时钟限制是36MHz所以必须在CubeMX里把ADC时钟分频设为4分频或6分频我这边用的4分频ADC时钟21MHz采样速度足够精度也有保障。3.2 DAC DMA Timer的串联配置DAC与定时器、DMA的关联在CubeMX里操作顺序很重要。打开DAC配置页面后先勾选OUT1和OUT2两个通道。Output Buffer这个选项建议保持默认使能内部缓冲可以增强一定的驱动能力如果后续发现波形需要更低输出阻抗可以再在外部加运放。然后切到DMA Settings页签分别添加DAC_CH1和DAC_CH2的DMA请求。F407的DAC1通道1对应DMA1 Stream5DAC1通道2对应DMA1 Stream6这个映射是芯片固定的CubeMX会自动分配不需要手动指定。DMA模式选择Circular循环模式这样波形表搬完自动重头开始。接着配置TIM6。Parameter Settings里Prescaler填分频系数减1Counter Period填自动重装值减1具体数值依据目标触发频率算。TIM6不需要开中断它的作用只是产生更新事件触发DAC转换。最后在DAC的Trigger设置里把两个通道的触发源都选为Tim6 Update Event。这里如果只改了一个通道另一个通道不触发输出就是个固定直流这种隐蔽错误排查起来特别费时间。3.3 双ADC同步采集的配置细节ADC部分我在CubeMX里的配置步骤如下把ADC1的Mode选为“Dual regular simultaneous mode only”此刻ADC2会自动进入从机模式。在ADC1的Configuration页签里打开DMA请求添加一个DMA Stream数据宽度是Half Word方向外设到内存循环模式这样采集的数据会持续刷新到内存数组里。ADC2只需要单独配置它的采样通道和采样时间不需要再配置DMA因为双ADC同步模式的DMA是由主机统一管理的。采样通道这里我还加了一个关键的联动把ADC的触发源设置为定时器触发。因为DAC输出波形是连续变化的如果ADC在主循环里随机时刻启动采样采到的电压点相位不固定没法还原完整波形。我把ADC的触发设置为TIM2的更新事件让ADC以固定频率采样这样采集到的点就是等时间间隔的方便后续在代码里做波形还原和频率分析。采样时间方面ADC1和ADC2的采样时间都改到84个时钟周期这样虽然单个通道采样速度慢了但等效输入阻抗更高对外部信号源的负载效应更小采集到的电压值也更接近真实值。4. 代码实现波形表生成、DMA输出与双ADC数据读取4.1 在代码里生成正弦波和方波表波形表我用运行时计算的方式生成放在main函数初始化阶段。代码如下#define WAVE_POINTS 256 #define DAC_MAX_VALUE 4095 uint16_t dac1_wave[WAVE_POINTS]; uint16_t dac2_wave[WAVE_POINTS]; void Waveform_GenerateSine(uint16_t *buf, uint32_t points) { for(uint32_t i 0; i points; i) { float angle 2.0f * 3.1415926f * i / points; float val (sinf(angle) 1.0f) / 2.0f; // 映射到0~1 buf[i] (uint16_t)(val * DAC_MAX_VALUE 0.5f); } } void Waveform_GenerateSquare(uint16_t *buf, uint32_t points) { for(uint32_t i 0; i points; i) { buf[i] (i points / 2) ? DAC_MAX_VALUE : 0; } }DAC的数据寄存器是12位所以波形表使用uint16_t数组取值范围0到4095。正弦波表通过把sin结果从-1到1映射到0到4095实现完整摆幅输出。映射时注意加上0.5做四舍五入避免浮点截断误差积累成波形毛刺。方波表更简单前一半点数输出满幅后一半点数输出零电平。如果想输出占空比可调的方波只需要修改分界线比如第points乘以0.3的位置。生成三角波、锯齿波也是同样套路在循环里按线性公式填充即可。4.2 双DAC的HAL库启动流程初始化完波形表后启动双通道DAC输出关键代码非常短HAL_DAC_Start_DMA(hdac1, DAC_CHANNEL_1, (uint32_t *)dac1_wave, WAVE_POINTS, DAC_ALIGN_12B_R); HAL_DAC_Start_DMA(hdac1, DAC_CHANNEL_2, (uint32_t *)dac2_wave, WAVE_POINTS, DAC_ALIGN_12B_R); HAL_TIM_Base_Start(htim6);启动顺序有讲究先启动DAC的DMA传输再启动定时器。如果反了定时器已经开始产生触发事件但DMA还没来得及准备数据会导致波形输出的前几个点是垃圾数据。DAC双通道共用一个DAC外设所以两个通道都用hdac1句柄用第二个参数区分是哪一个通道。DAC_ALIGN_12B_R对应12位右对齐模式。波形表里的值低12位有效直接写入DHR12R1或者DHR12R2寄存器。如果想实时改波形频率不用停止DAC只需要调整TIM6的ARR值即可。我在实际测试中把ARR值改小一倍输出频率立刻翻倍观察波形完全没有毛刺或瞬态跳变证明这个机制是可靠的。4.3 双ADC同步采集的启动与数据解析ADC的启动代码如下uint32_t adc_buf[2]; HAL_ADCEx_MultiModeStart_DMA(hadc1, (uint32_t *)adc_buf, 1); HAL_TIM_Base_Start(htim2);注意双ADC模式和单ADC模式启动函数不同不是HAL_ADC_Start_DMA而是HAL_ADCEx_MultiModeStart_DMA。第三个参数是DMA缓冲区的长度我这里是1个32位字长因为每次触发采集ADC1一个转换结果、ADC2一个转换结果打包成一个32位数据。数据解析时要注意F407双ADC同步模式的数据排布32位数据的高16位是ADC1的结果低16位是ADC2的结果。我之前就是在这点上翻过车一开始想当然地以为低16位是第一个ADC的数据结果两个通道数值对调了。DMA传输完成回调里拿到数据后可以换算成实际电压值float adc1_voltage adc_buf[0] 16; float adc2_voltage adc_buf[0] 0xFFFF; adc1_voltage adc1_voltage * 3.3f / 4095.0f; adc2_voltage adc2_voltage * 3.3f / 4095.0f;12位ADC结果换算成电压乘以3.3再除以4095。这个换算关系在波形回读验证时很有用比如DAC输出满幅正弦波ADC采集回来的峰值电压应该接近3.3V如果明显偏低说明DAC输出阻抗影响了ADC采样结果或者链路上有额外负载。5. 联调实测两路正弦波输出ADC回读验证5.1 目标波形指标设定我给自己设定的目标指标是参数目标值计算方式DAC1输出波形1kHz正弦波TIM6频率100kHz / 256点DAC2输出波形1kHz正弦波相位滞后90度波形表起始点偏移64ADC采样率100kHzTIM2触发频率采样点数单次采集256点用于还原1kHz正弦波TIM6触发频率设定为100kHz的具体计算过程如下TIM6时钟84MHz目标100kHz分频系数就是840。写成寄存器值PSC0ARR839这样更新事件频率为84MHz除以840正好是100kHz。DAC2相位滞后90度不需要配置任何寄存器只需要在生成波形表的时候DAC2的初始相位从数组下标的四分之一处开始即可for(uint32_t i 0; i WAVE_POINTS; i) { uint32_t idx (i WAVE_POINTS / 4) % WAVE_POINTS; dac2_wave[i] dac1_wave[idx]; }这种方法比用两个不同的三角函数公式生成效率高得多而且能保证两路波形除了相位之外完全一致。5.2 实测波形效果把程序烧进开发板用ADC回读DAC1输出波形还原出来是一条干净的正弦波峰峰值大约3.28V直流偏置约1.65V。对比理论值DAC满幅输出3.3V实际因为有源器件的导通内阻和非理想特性电压低了一点点在可接受范围内。DAC2回读波形和DAC1的相位差是四分之一周期对应90度相位偏移。两条曲线同时显示在串口波形工具上正交关系一目了然。然后我用同样的配置把TIM6频率从100kHz提到1MHz波形表点数不变输出频率从1kHz提高到10kHz。ADC这边同步调整采样率到1MHz回读波形依然能正确还原只是串口传输256点数据的时间变长了实时性有所下降但不影响验证。5.3 频率调节和波形切换调节输出频率的核心是改TIM6的ARR值。我在串口命令里加了一个简单的频率设定功能上位机下发频率数值后代码计算对应的ARR值并更新uint32_t target_freq 1000; // 从串口收到的目标频率 uint32_t arr (84000000U / target_freq) - 1U; __HAL_TIM_SET_AUTORELOAD(htim6, arr);这里需要注意更新ARR值的操作是立即生效的但定时器计数到新ARR值并产生更新事件需要等到下一个周期。如果你的波形频率切换很频繁建议在更新ARR值之后清除一次更新中断标志确保后续的触发事件是基于新频率的。6. 实际调试中的四大拦路虎6.1 波形固定在某一个电压值怎么调都不变化这个问题几乎可以锁定为DAC触发源没有配置成功。DAC虽然启动了但没有定时器触发它它就维持在上一次写入的值不再更新。排查顺序先看TIM6有没有在运行HAL_TIM_Base_Start有没有执行再看DAC配置里Trigger是否选了TIM6 Update Event最后检查DMA有没有启动用调试器看DMA的NDTR寄存器数值有没有在减少。如果NDTR数值始终不变说明DMA没有在搬运数据优先检查DMA通道映射是否正确。6.2 输出波形频率和计算值对不上最常见的原因是PSC和ARR的换算关系搞错了。定时器更新频率公式是更新频率 定时器时钟 / (PSC 1) / (ARR 1)很多人把CubeMX里的Prescaler和Counter Period输入值直接当成PSC和ARR实际上CubeMX显示的是分频系数自动在寄存器里减了1。如果你把CubeMX显示值再减1填进去频率就会偏高。这种情况我建议直接在代码里用__HAL_TIM_SET_PRESCALER和__HAL_TIM_SET_AUTORELOAD动态赋值用调试器观察实际频率慢慢逼到目标值。6.3 双ADC同步模式数据错位如果你的ADC1和ADC2通道数据在显示时发生了交换不要怀疑硬件就是数据解析的位序搞反了。F407双ADC同步模式ADC1结果在高16位ADC2结果在低16位按这个规则解析一次数值应该就正常了。另外注意DMA缓冲区是32位宽度不要设成uint16_t数组否则DMA搬运会错位。6.4 DAC输出端波形毛刺严重我测量DAC输出时发现波形上有明显的高频毛刺尤其是在波形跳变沿附近。原因有两方面一是DAC输出快速变化时产生了电荷注入需要小的低通滤波二是VREF电源噪声耦合到了DAC输出上。解决方案也很直接在DAC输出引脚和地之间加一个10nF电容做滤波在VREF引脚和地之间加一个100nF去耦电容双管齐下之后毛刺明显减少。如果后续要做更高精度的输出建议外接独立的基准电压源芯片给VREF供电把数字电源噪声和模拟基准彻底隔离。现象可能原因排查方法DAC输出固定电平定时器未启动、DMA未搬运查看定时器运行状态和DMA计数寄存器频率偏高PSC/ARR换算错误按公式重算用计数器核对双ADC数据对调数据位序解析错误高16位为ADC1低16位为ADC2波形毛刺去耦电容缺失输出加10nFVREF加100nF7. 写在最后的经验总结这个项目做完之后我最大的感受是F407这套DACADCDMA定时器的组合本质上就是一套数字信号处理的最小闭环系统。DAC负责把数字世界映射到模拟世界ADC负责把模拟世界采样回数字世界定时器和DMA是连接两者的时间轴和搬运工。搞明白这条链路之后往里去加自定义波形、加数字滤波、加FFT分析都是顺手的事。另外一个小经验是开发这类外设联动的项目不要一上来就追求复杂功能先把最简单的单通道DAC定频输出跑通再扩展到双通道同步输出ADC回读同理先单通道单点采集再做同步双通道。每扩展一步就验证一步排查问题的范围会被大幅缩小。最后的扩展建议目前的波形表是固定生成在内存里的如果你需要输出任意波形可以把串口或者SD卡读到的数据填进dac1_wave数组双ADC采集的数据也支持后续接入FFT通过SPI或者串口上传到上位机做频谱分析。这个项目的上限远比标题看起来的要高。本文还有配套的精品资源点击获取