
简介基于STM32F407 HAL库的双DAC信号发生器与双ADC采集工程适合嵌入式开发者学习DAC波形生成、ADC多通道采样及HAL库外设配置。资源包共311个文件包含.c/.h源码、Keil工程文件(.uvprojx/.uvoptx/.ioc)、编译输出(.axf/.hex/.o)以及调试映射文件(.map/.lst)整体约16.75MB可直接导入Keil进行二次开发与学习。目前已有614人学习使用。代码覆盖时钟、DAC、ADC及DMA初始化并提供正弦波生成与多路采集例程同时附带HAL库底层驱动文件与工程配置文件便于理解外设初始化流程和中断/DMA协同机制。通过该项目可掌握F407双DAC同时输出不同波形、双ADC同步采集等核心技能适合工业控制、信号处理等场景。整体结构完整、注释清晰是入门与进阶STM32信号链开发的实用参考。 最近用STM32F407的HAL库做了一套双DAC信号发生器加双ADC同步采集的小工具用途很直接调试电路板时我需要同时给外部设备输出两路模拟激励信号正弦、三角、方波都行并且在同一时刻把系统里的两路模拟反馈信号采回来。以前这种活得搬台式信号发生器加示波器现在一块F407开发板全干了还能把DAC输出和ADC采集做到严格同步省了不少事。如果你也在折腾信号发生器、传感器数据采集或者想搞个小型的频率响应测试装置这篇经验应该能帮你少走不少弯路。整套东西没有用什么特别复杂的算法核心就是“定时器触发DAC输出、同一触发源同步启动ADC采样再用DMA把波形数据和采集数据分别搬走”。F407的资源刚好覆盖这个需求自带双DAC通道、多个ADCHAL库把底层寄存器操作包了一层配合CubeMX做初始化非常快。下面我把整个设计思路、硬件注意点、关键代码和踩过的坑一条条讲清楚。1. 项目定位与整体设计思路1.1 这个项目解决什么问题STM32F407自带两个12位DAC通道1和通道2可以独立输出也可以同时由定时器事件触发更新数据。这意味着我不需要外挂DAC芯片就能产生两路同步的模拟波形。同时F407有3个ADC其中ADC1和ADC2支持一种特殊的“双ADC同步模式”一个触发信号到来时两个ADC同时启动转换转换结果被合并到一个32位寄存器里一次DMA搬运就能把两路数据取走。这个特性对“激励-响应”类测量特别有价值。我做的这套系统典型用途是用DAC1输出正弦波作为激励信号DAC2输出另一路相位可调的波形比如给电机驱动板模拟两路正交编码器信号。ADC1采DAC1的输出电压回读校准ADC2采外部反馈信号两路数据在时间上严格对齐后续算幅度比、相位差都很方便。在闭环调试场景里DAC输出一个扫频信号ADC同步采集系统响应整理一下就能画出简单的频率响应曲线。这相当于把“信号源双通道采集”塞进了一块MCU里。对于做控制、传感器调理、电源调试的人来说是非常趁手的一个功能组合。1.2 为什么选F407加HAL库选F407不光是看中双DAC和多ADC还因为它的定时器系统足够灵活。简单定时器TIM6虽然功能少但有一个“TRGO”事件输出能同时作为DAC触发源和ADC触发源这正好满足同步需求。如果选一些小封装的芯片很多时候要么只有一个DAC要么DAC和ADC的触发链路绑不到一起方案就得绕很大弯。HAL库方面说实话它有不少啰嗦的地方但在这个场景下优势很明显CubeMX图形化配置DAC、ADC、DMA、TIM6的调用关系生成的初始化代码相当规范我只需要在业务逻辑里维护波形表和缓冲切换。比起对着寄存器手册反复翻参考手册HAL库能帮我把精力放在波形质量和同步精度上。当然这也要求我对HAL库的几个API调用逻辑有概念比如DMA句柄和DAC句柄怎么关联、双ADC模式下应该用哪个回调函数这些后面会详细说。2. 硬件方案与核心原理2.1 DAC输出级与负载匹配F407的DAC是12位电压输出型参考电压VREF接3.3V时理论分辨率是3.3V/4096约0.8mV一个LSB。但要注意DAC输出引脚内部自带一个输出缓冲放大器这个缓冲能让DAC直接驱动一部分负载优点是输出电压稳定缺点是靠近电源轨的时候线性度会变差实际可用输出范围大概在0.2V到VREF-0.2V之间。换句话说你让它输出3.3V满幅它可能最早在3.1V左右就开始拉不动了。所以我的做法是DAC引脚出来先经过一级运放电压跟随器比如LM358或者MCP6002这类单电源轨到轨运放再接负载。这样有两个好处一是把DAC内部缓冲从重负载中解放出来改善近轨线性二是出线的驱动能力大幅提高接个10k甚至1k负载都没问题。如果你要输出双极性信号比如-1.65V到1.65V还需要再加一级以1.65V为参考的偏置电路和反相放大器这块属于扩展内容基础版先把0~3.3V单极性输出做稳。2.2 ADC输入级与采样电阻匹配ADC采集端同样有讲究。F407的ADC是逐次逼近型SAR内部有一个采样电容采样瞬间会从信号源抽取一部分电荷。如果信号源输出阻抗高采样电容在极短的采样窗口内充不满采集到的值就会偏小而且信号频率越高偏差越明显。解决思路有两条一是把ADC的采样时间配置长一点CubeMX里可以选比如84个ADC时钟周期给电容足够的充电时间二是在ADC引脚前加一级RC低通比如串联一个几百欧的电阻再接一个100nF左右的电容到地。这个RC一方面滤掉高频噪声另一方面相当于一个“电荷池”采样瞬间可以由电容顶上去保证信号稳定。如果信号源内阻本身就很高更稳妥的方案是再接一级运放缓冲。这个细节很多教程不提但实际调试时ADC读数漂或者高频衰减十有八九是这个原因。2.3 定时器触发同步链路这个项目里DAC输出和ADC采集能保持同步靠的是TIM6的TRGO事件。原理一句话TIM6是个基础定时器它的更新事件可以映射到TRGO引脚片内信号DAC和ADC都允许把这个TRGO作为外部触发源。当定时器计数溢出时TRGO产生一个脉冲DAC转换一次从DMA缓冲区取一个数ADC1/ADC2同步启动一次规则转换。我用的是双ADC同步模式硬件上由ADC1作为主机ADC2作为从机同一个TRGO到来时两片ADC同时转换各自的通道。转换完成后ADC1的结果存在共享的32位数据寄存器CDR的低16位ADC2的结果在高16位再由DMA一次性搬运到内存。这种模式最漂亮的地方在于不需要软件干预两路ADC的采样时刻在硬件上就是同时的时间戳天然对齐非常适合做双通道相关分析。3. CubeMX配置与HAL库代码实现3.1 时钟树与外设初始化CubeMX里我先把时钟树调到F407最常用的配置外部8MHz晶振PLL锁相到168MHz系统主频AHB总线168MHzAPB1分频得到42MHzAPB2 84MHz。这里有个关键点定时器时钟不是直接等于总线时钟APB1预分频系数不是1时定时器时钟会翻倍所以TIM6的实际计数时钟是84MHz不是42MHz。DAC挂在APB1上工作在42MHzADC挂在APB2上工作在84MHz。这个关系决定了后面的频率计算搞错了波形频率会差一倍。外设配置上DAC1的两个通道都要开启触发源选择Timer 6 Trigger Out event并且为两个通道分别配置DMA请求。这里的DMA不是乱选需要对应F407的DMA请求映射表DAC_CH1对应DMA1_Stream5DAC_CH2对应DMA1_Stream6。传输方向是存储器到外设Memory to Peripheral数据宽度半字DAC数据寄存器是16位内存地址递增、外设地址固定模式选择循环模式Circular这样波形表可以自动往复搬运。ADC配置时我选择ADC1作为MasterADC2作为Slave工作模式选“Dual regular simultaneous mode”即双规则同步模式。ADC1使能一个通道ADC2使能另一个通道按你的引脚需求选触发源都选Timer 6 Trigger Out event。DMA方面双ADC模式下只需要给ADC1配一个DMA通道DMA2_Stream0方向是外设到内存数据宽度32位循环模式。注意这个32位宽度因为搬运的是合并后的CDR寄存器不是16位的单个ADC结果。3.2 波形表生成与查表法信号发生器输出的波形是怎么来的我用的是查表法。在内存里放一个波形数据表DMA按固定的节奏把这个表里的数据逐个送到DAC数据寄存器DAC就输出对应的电压台阶连起来就形成波形。MCU不需要在中断里实时算正弦而是提前算好存着。以720点的正弦表为例用Python或者MATLAB生成一段C语言数组import math TABLE_SIZE 720 for i in range(TABLE_SIZE): val int(2047.5 2047.5 * math.sin(2 * math.pi * i / TABLE_SIZE)) print(val, end, ) if (i 1) % 12 0: print()这段代码生成的值范围是0到4095正好对应12位DAC。中间值2048对应1.65V中点峰值对应3.3V附近。方波、三角波、锯齿波同理改一下计算逻辑就行。我实际在RAM里同时放了正弦表、三角表和方波表运行时通过一个变量切换DMA的源地址就能快速切换波形。为什么不用C语言的arm_sin_f32现算因为DAC更新频率动不动几十k甚至上百kHz每更新一次就算一次sinCPU占用率高得吓人而且浮点运算时间抖动还会让波形边沿不整齐。查表法只做内存读取DMA通道自己搬运CPU完全空闲。3.3 DAC双通道DMA输出初始化代码生成后启动DAC输出只需要三行核心调用HAL_DAC_Start_DMA(hdac1, DAC_CHANNEL_1, (uint32_t *)waveformSin, TABLE_SIZE, DAC_ALIGN_12B_R); HAL_DAC_Start_DMA(hdac1, DAC_CHANNEL_2, (uint32_t *)waveformTri, TABLE_SIZE, DAC_ALIGN_12B_R); HAL_TIM_Base_Start(htim6);这里有个容易忽视的细节DAC的DMA请求是在定时器触发事件到来、DAC完成一次转换时产生的所以先启动DAC_DMA再启动定时器顺序不能反。如果先启动定时器DAC的DMA通道还没就绪触发事件来了可能直接导致DMA underrun错误。DAC_ALIGN_12B_R表示12位右对齐常用的一种对齐方式告诉HAL库数据是12位有效还是16位全有效别选错了选错了输出波形会整体偏移或者缺失高位。另外DAC的DMA启动后波形是“循环播放”的所以两个通道各自使用的波形表长度必须是能整除当前触发频率的整数否则会出现波形跳变。比如100kHz触发率配720点表正弦频率就是138.889Hz这个关系下面算。3.4 双ADC同步采集与结果提取双ADC同步模式的启动代码长这样HAL_ADCEx_MultiModeStart_DMA(hadc1, (uint32_t *)adcDualBuf, ADC_BUF_LEN);这里HAL库的函数名里带Ex和MultiMode和单ADC的HAL_ADC_Start_DMA完全不一样。参数里第一个是主机句柄也就是ADC1的句柄第二个是要搬运到的目标地址第三个是传输次数。由于DMA每搬运一次就是32位CDR寄存器整体搬运所以缓冲区应该声明成uint32_t数组。双ADC同步模式的数据排列要特别注意我用了一个简单的提取函数uint32_t cdr adcDualBuf[i]; uint16_t adc1_raw cdr 0xFFFF; uint16_t adc2_raw (cdr 16) 0xFFFF; float volt1 adc1_raw * 3.3f / 4095.0f; float volt2 adc2_raw * 3.3f / 4095.0f;CDR寄存器的低16位是主机ADC1的结果高16位是从机ADC2的结果。一开始我就是把顺序搞反了导致CH1和CH2数据对调排查了半天。记住“低主人、高从人”这个口诀就好。为了进一步降低CPU负担我给ADC的DMA开启了双缓冲模式。简单说就是准备两块同样大小的缓冲区DMA先填A填满后自动切到B同时触发完成中断主程序在中断里处理A的数据。这样ADC采集可以不间断数据也可以按帧处理是持续采样场景比较标准的做法。3.5 触发频率与预期波形频率计算TIM6的计数时钟是84MHz触发频率的计算公式是f_TRGO 84MHz / (PSC 1) / (ARR 1)我调试时常用的一组参数是PSC41ARR19这样就是一个脉冲10微秒触发频率100kHz。如果波形表有720个点那么DAC输出正弦波的频率就是f_signal 100000 / 720 ≈ 138.889HzADC的同步采样率也是100kSPS由于双ADC同步一次触发得到两路样本整套系统的数据时间都对齐在10微秒的网格上。想要更高频率的波形要么减小PSC/ARR提高触发率要么缩短波形表点数。但触发率不能无限往上提受三个因素限制DAC转换时间满载时大约1us左右、DMA搬运带宽和ADC采样时间设置。F407在168MHz下面我把触发率跑到500kHz到1MHz问题不大但要留足余量否则会出现DAC的DMA underrun或者ADC采样不准。4. 实测波形与调试心得4.1 实测输出质量我用示波器探了DAC1经过运放跟随后的输出正弦波在138.9Hz、1kHz、20kHz几个频率下波形都还算平滑。12位分辨率在这个频率下肉眼基本看不出台阶除非把示波器打在非常窄的时间档才能看到DAC的量化阶梯。实测下来波形幅度准确性不错0到3.3V范围内和万用表测出来的直流值能对上误差在几个毫伏量级主要来源是参考电压精度和运放失调。用ADC回读DAC输出时采样数据还原出来的波形和原始正弦表高度重合这说明同步链路工作正常。方波输出时边沿没有理想数字方波那么陡会有几十到几百纳秒的爬升时间毕竟DAC输出本质上是模拟信号经过运放不可能是斜坡瞬变。如果非要极速沿的方波建议用比较器或者直接上DDS芯片方案DAC这边做到这个程度已经可以了。4.2 布线与电源细节模拟性能最怕的是电源噪声和地弹。我的板子上VREF专门用了一个低噪声LDO供电模拟地和数字地在主电源入口单点汇合DAC和ADC相关的走线尽量短。顶层走模拟信号底层铺完整地平面。这些看似老生常谈但在信号发生器这种场景里直接影响波形毛刺和ADC读数跳动。还有一个容易被忽略的点TIM6本身不需要开中断它只作为触发源工作。如果你在主程序里开了HAL_TIM_Base_Start_IT反而会频繁进入定时器中断增加系统负载。我只需要HAL_TIM_Base_Start就够了波形搬运和ADC转换全走DMACPU几乎不参与。5. 踩坑记录与排查表格5.1 实际遇到的五个坑第一个坑是DAC完全没有输出。检查了很久最后发现CubeMX里DAC的触发源虽然选了Timer 6 Trigger Out event但DAC通道的TEN位没被置位。HAL库的HAL_DAC_Start_DMA会处理好这一步前提是你在CubeMX里把DAC触发正确配置并且初始化代码中确实把DAC的硬件触发功能展开。建议配置完CubeMX后看一眼生成的MX_DAC1_Init函数确认hdac1.Init.Trigger是DAC_TRIGGER_T6_TRGO而不是DAC_TRIGGER_NONE。第二个坑是频率差了一倍。罪魁祸首就是前文说的APB1定时器时钟倍频问题。我一开始按42MHz算TIM6频率算出1kHz的配置示波器实测却是2kHz。查到参考手册才发现预分频系数不为1时定时器时钟是总线时钟的两倍也就是84MHz。记住CubeMX生成的时钟树里能直接看到Timer Clock别用总线时钟直接算。第三个坑是双ADC数据错位这个前面提过CDR高低16位的顺序问题。这个坑很隐蔽如果没有用两个不同的输入电压去验证很容易把两路数据对调还浑然不觉。我的习惯是先在CH1接一个1V固定电压CH2接一个2V通过数值直接验证对应关系。第四个坑是DMA中断不触发。双ADC模式下HAL库的回调函数名是HAL_ADCEx_MultiModeConvCpltCallback不是普通的HAL_ADC_ConvCpltCallback。很多人改了前者但忘了在NVIC里使能DMA2_Stream0的中断或者中断优先级配得和其他外设冲突导致回调不执行。我当时是中断优先级设置和串口中断一样结果两个都没跑顺。第五个坑是ADC输入电压超过参考轨。ADC输入范围超过3.3V后读数会一直满量程4095甚至可能损伤引脚。后来我在ADC输入前加了一对背靠背的钳位二极管再配上RC这个问题就再没出现过。5.2 常见问题速查表现象可能原因解决办法DAC无输出DAC触发源没配置TEN位未置位确认DAC Trigger为Timer 6 TRGO确认调用了HAL_DAC_Start_DMA输出频率翻倍/减半定时器时钟算错APB1倍频没考虑用CubeMX中的Timer Clock值计算而不是APB1总线频率DAC输出波形有台阶/毛刺VREF去耦不足、DAC缓冲带载过重VREF引脚加10uF100nF去耦输出加运放跟随器ADC采样值跳动输入阻抗高、采样时间不足加RC低通或运放跟随采样时间调到28或84个ADC时钟周期两路ADC数据对调CDR高低16位顺序搞反低16位是ADC1高16位是ADC2互换掩码DMA回调不执行回调用错函数名NVIC未开用HAL_ADCEx_MultiModeConvCpltCallback开启DMA通道NVIC高频率下波形截断触发频率超过DAC转换能力DMA underrun降低触发率减少波形表点数调高DMA优先级方波沿不陡DAC输出带宽有限换比较器电路或用PWM滤波方案这套组合做完之后我才真正体会到“片上信号发生器加同步采集”有多顺手。以前调试闭环控制需要一边看激励波形一边看反馈波形还得靠示波器双通道手工对齐现在程序里天然就是同步的数据拿过来就是时间对齐的。后面如果还想升级可以考虑把波形表做成可下载格式通过串口从PC端上传任意波形再结合双ADC做一个简易的阻抗分析仪。不过那些都是后话了先把这套双DAC双ADC的底子打好后面一切好说。本文还有配套的精品资源点击获取