简介:一份基于STM32与HAL库的三重ADC采集波形实现工程,面向需要实现多ADC同步采样的嵌入式开发者,主要解决三路ADC协同采集波形时配置复杂、时序对齐难的问题。工程代码覆盖ADC时钟、分辨率、扫描序列、同步模式与采样时间等初始化逻辑,支持单次/连续转换与扫描模式,并利用HAL库函数完成多路转换结果的非阻塞读取(中断/DMA方式),同时提供简单的数据处理思路,可将采集值存入缓冲区后进行滤波与分析,适用于多通道信号采集、波形监测、电源质量分析等场景。
压缩包共122个文件,大小1.08MB,以HAL库源码(.c、.h)为主,另有STM32CubeMX配置工程(.ioc)、Keil工程文件(.uvprojx)、启动文件(.s)与可直接烧录的hex固件等;附带的txt文档和bat脚本可作为使用参考与工程清理工具,便于整体导入与二次开发。目前已有376人学习,工程文件齐全,对初学者和中级嵌入式用户都有参考价值。
对于正在研究STM32多ADC同步采样或波形采集的开发者,工程提供了完整的可运行参考,能显著缩短三重ADC的调试周期,尤其适合有HAL库基础、希望快速掌握多ADC模式配置的开发者。
1. 为什么是三重ADC:从硬件原理到方案选型
1.1 多重ADC采样的底层逻辑
做过电机控制或者电力电子项目的朋友应该都有体会,单路ADC很多时候根本不够用。要同时采集三相电流、母线电压,再留一路做温度监控,一颗MCU里那一个ADC外设根本忙不过来。STM32之所以在高性能控制领域吃得开,很大一个原因就是它给F4、H7这类芯片集成了2到3个独立的ADC模块,可以并行工作。三个ADC同时触发、同时转换,再靠内部硬件把结果按固定顺序搬运到寄存器里,这就是所谓的“多重ADC模式”。要是单颗ADC轮询,三个通道依次采样会引入时间差,电机低速运行时这个误差会被放大,波形直接就是歪的。
用STM32CubeMX配合HAL库来做三重ADC,最大的好处是不用再去抠底层寄存器。但别高兴太早,HAL库虽然把初始化封装好了,多重ADC的联动、触发、DMA搬运这些关系还是得自己理清楚。我用的是STM32F407,三个ADC都齐全,主频168MHz,ADCCLK从APB2分频得到,一般压到42MHz以下比较稳。采集波形这种事,采样率、触发源、DMA缓冲这三样必须是同一个节奏,否则后边数据拼出来也是一团乱麻。
1.2 三重ADC与DMA组合的优势
先说结论,三重ADC加定时器触发加DMA搬运,这套组合就是为了解决两个问题:一是多通道同步采集,二是高采样率下不占用CPU。同步采集的价值在波形分析场景里特别明显,比如三相逆变器输出,你希望在同一时刻抓三路PWM对应的电压值,差一个微秒,波形相位就对不上。DMA的价值则是在连续采集模式下,ADC转换完一个数据就自动搬到内存数组里,溢出了再中断通知,CPU全程不用管。
HAL库对三重ADC的封装和单路有所不同,主要是把ADC1当作主设备,ADC2和ADC3作为从设备,配置都写在主设备的结构体里。实际配置中,ADC1设置为Master,模式选TripleADC,下面再选Simultaneous或者Interleaved,这两种模式的区别我会在后文细说。第一次用这功能容易懵,我自己也在这个配置页面上卡了一个下午,后来发现是没把ADC2和ADC3的时钟启用,CubeMX页面上看起来是灰色的,但主设备已经能引用它们了。
2. CubeMX配置过程:一步步搭出三重采集工程
2.1 时钟树与引脚分配
打开STM32CubeMX新建工程,选芯片型号之后先别急着配ADC,把时钟树搞定。三颗ADC都挂在APB2总线上,也就是72MHz这条线(F407的APB2默认最高84MHz,配置正确时可以到84MHz)。ADC本身的时钟最多42MHz,所以APB2除以2就够了。在这里容易犯的错是觉得ADC时钟越高采样率越快,于是拼命超频,结果采样值跳得离谱。ADC时钟太高会导致采样电容充电时间不足,尤其源阻抗高的时候,读数偏差大到不可接受。
引脚分配上,我用的是ADC1的通道0、ADC2的通道0、ADC3的通道0,对应引脚分别是PA0、PB0、PC0,这样接三个测试点互不干扰。要注意这三个引脚都能容忍5V输入吗?F407不是所有脚都是5V容忍的,查表确认PA0不是5V容忍脚,所以前端信号必须限制在0到3.3V。波形采集项目通常需要采集双极性信号,这时候必须加偏置电路和运放,把负半周抬到0V以上,否则ADC的输入级会直接烧掉。CubeMX里把这三个通道全勾上,ADC1的Resolution选12位,Data Alignment必须是右对齐,后面DMA搬运出来的数据才符合直觉。
2.2 DMA与触发源的参数选择
DMA配置是这项目的重头戏,三重ADC在F407上有专门的多重模式DMA通道,叫DMA2 Stream0,Channel0是ADC1的专用DMA请求通道,先把DMA2的时钟打开,然后设置Stream0工作在循环模式。注意不是所有STM32型号都有这个DMA通道,F1系列的多重ADC模式相对弱一些,F4和H7才有比较完整的实现。DMA方向一定要选PeripheralToMemory,数据宽度Peripheral和Memory都设HalfWord,因为12位ADC数据本身用16位存就够了。
触发源的选择取决于你要采什么样的波形。如果只是低频信号,用定时器触发最方便。我选了TIM1的TRGO事件作为ADC触发源,因为TIM1是高级定时器,可以配置PWM、也可以简单输出更新事件。CubeMX里在ADC1的配置页选ExternalTriggerConversionSource为Timer1TriggerOutEvent就行。为什么用定时器触发而不是软件触发?软件触发下DMA连续搬运的节奏很难精确控制,还容易被中断优先级打乱,采样率就飘了。定时器触发能把采样时刻钉死在固定的时间点,波形的横向刻度才准确。
CubeMX自动生成的初始化代码里,多重ADC模式在ADC1的Init结构体里设置Multiboot模式,但有一个坑,如果用较老版本的固件包,可能出现CubeMX界面配置了Multiboot,生成的代码却只初始化了ADC1,没把ADC2和ADC3加进来。这个我后面会详细讲怎么手工补。
3. 核心代码与实现细节:让三个ADC真正跑起来
3.1 初始化代码的关键成分
CubeMX生成完工程后,打开main.c观察MX_ADC1_Init这个函数。里面会有一个ADC_MultiModeTypeDef结构体,这是多重模式的总开关,Mode成员对应你要选的工作方式,比如ADC_MODE_SIMULT,就是三路同时采样,另一个常用的是ADC_MODE_INTERL,是交织采样模式,三颗ADC可以轮流采同一路信号,等效采样率翻三倍。这么一看就很清楚了:需要三路信号同时采,选Simultaneous;需要把一路信号采得飞快,选Interleaved。这个项目我要采集三相电路的三路波形,所以Simultaneous是正解。
需要注意的是,ADC2和ADC3的初始化函数CubeMX也会生成,但它们的内容明显少了Multiboot相关内容,只保留了最基础的ADC参数。别去改它们,让它们保持最简单最干净就行。实际初始化的执行顺序上,ADC1要放在最后调用,因为它是主设备,只有主设备的Multiboot配置完成后,从设备才能被正确联动。如果你发现ADC2和ADC3读回的数据一直是0,先怀疑初始化顺序,再考虑硬件接线问题。
校准也是个容易忽略的步骤。F407的ADC在初始化之后、开始转换之前,官方手册要求做一个校准动作,否则增益误差会比较明显。HAL库提供了HAL_ADCEx_Calibration_Start这个函数,在ADC启动之前调用一次,等待校准完成标志,实测校准前后的波形幅值能差出1%到2%,做精密测量时这个差距不可忽略。三重模式下,三颗ADC都需要各自校准一遍。
3.2 定时器触发与DMA搬运的无缝衔接
触发这块,TIM1配置为主模式,输出TRGO事件。CubeMX里在TIM1的配置页找到TriggerOutput(TRGO)Parameters,选择Update Event,意思是定时器更新中断发生时产生一个触发信号给ADC。配置完TIM1后,给它一个预分频值和周期值,决定触发频率。举个例子,我想以120kHz的采样率采集波形,那么设置TIM1的时钟为168MHz,预分频164,Period为9,实际计算:168MHz除以165再除以10,约101.8kHz,拿频率计一测还挺准的。这里特别注意预分频和周期的界限关系,余数处理不好会有1到2Hz的偏差,虽然波形显示看不出来,做频谱分析的时候就能看到频率轴偏移。
DMA搬运的目标数组需要定义成全局变量,我的做法是定义三块uint16_t类型的数组,每个长度1024,对应三个ADC的采样序列。启用DMA后,ADC1会在定时器触发下启动一次转换,三路ADC同时采样,转换完成后数据被DMA自动按顺序存到这三个数组里。使用HAL_ADC_Start_DMA启动ADC采集,注意参数里指定的数据数组其实就是ADC1的DMA目标缓冲,但三重模式的实际行为是,主设备DMA会把ADC1、ADC2、ADC3的结果按固定顺序轮流写入这个缓冲区,所以你会发现数组里的数据是交替的:第一项是ADC1的结果,第二项是ADC2的结果,第三项是ADC3的结果,第四项又回到ADC1。这个特性一开始坑了我好久,我以为是读取顺序错了,后面看参考手册才确认。
为了不用在中断里频繁搬数据,可以在DMA传输过半和全部完成时各触发一次回调,用ADC转换完成事件或者DMA半传输中断来通知主程序处理。HAL库里面注册HAL_ADC_ConvCpltCallback这个回调函数,数据满了之后把标志位置位,主循环看到标志位就执行波形处理逻辑。半传输中断也能处理,适合做双缓冲交替提取,以缩短数据分析的延时。
3.3 波形还原与数据解析实操
拿到DMA缓冲区里的交替数据后,第一步是做个去交替拆分。假设整个缓冲区长度是3072,那么索引0、3、6、9这些是ADC1的数据,1、4、7、10是ADC2的数据,2、5、8、11是ADC3的数据。写循环的时候要注意,三重模式的数据是按固定的依次序排列的,所以拆分逻辑很直观。拆分完成后,每个通道各得1024个点,按时间顺序排列,这就是能直接画波形的原始数据。
画波形我在实际项目里用了两种方式:一种是直接把数据通过串口发到PC端,用Python的matplotlib在线绘制,适合调试;另一种是把数据存到SD卡里,事后用上位机分析。串口发送时要注意12位ADC数据范围是0到4095,需要发送16位二进制或者转成字符串再发。如果你在电脑端看到波形是阶梯状跳变,多半是因为数据宽度设置成了8位,高4位被截断了。建议调试初期直接把原始值发上来,用PC端软件把电压值算出来,波形真假一目了然。
波形还原时还有一个重要细节:数据拼接是指把三路ADC各自的原始数据序列分别用连续曲线描出来,而不是把三路数据硬串成一路。三重ADC的Simultaneous模式三路之间无相位差,所以画在同一张图里时三条曲线的起点要严格对齐,否则电流波形会出现虚假的移相,直接影响后续计算功率因数的准确性。
4. 常见问题与排查技巧实录
4.1 数据错位、通道串扰与采样率漂移的排查
从我自己的调试经历里,最常见的噩梦就是数据序列错乱。现象是你用1V接ADC1的通道0,结果串口发出来的波形去到了ADC2的数组里。遇到这个情况别急着改代码,先确认CubeMX里ADC1是否真的配置成了Master,ADC2和ADC3是否配置成了Slave。另外,三重模式下DMA缓冲数组的长度,必须设计成3的整数倍,否则拆解的时候最后一段数据必然错位。
数据串扰问题则往往出现在PCB布线阶段。三个模拟输入通道如果离得太近,或者中间没有地线隔离,相邻通道之间会通过寄生电容和互感耦合信号,尤其在信号源阻抗高时更明显。实测中我试过把PA0和PB0的飞线并排走,10kHz正弦波在另一路能看到大约20mV的串扰,重新布局布线之后降到3mV以内。软件上也可以加一点小花招,比如在通道切换后丢弃前两次转换结果,给采样电容一个稳定的时间,能稍微缓解,但根治还得靠硬件隔离。
采样率漂移的问题基本都出在定时器上。使用PWM触发时,如果占空比或频率在测试过程中被人为调节,触发点可能被拉偏,ADC的采样时刻就不再均匀。实际上做波形采集最好用固定周期的更新事件触发ADC,而不是PWM边沿触发,除非你的项目有明确的PWM同步需求,比如电机控制器中要在一个PWM周期内的特定点采样电流。这种情况下PWM触发是必须的,但要注意留出足够的死区时间,否则ADC转换还没结束,下一个触发就来了,结果就是数据被覆盖,DMA的完成中断抛得异常频繁,最终引发主程序卡死。
4.2 从异常现象定位根因的实战对照
我这里整理了一份常见问题速查表,都是实际项目中踩过的坑,不是从手册上抄的。遇到问题先对着表格排查,可以省下大量时间。
| 现象 | 可能原因 | 排查与解决 |
|---|---|---|
| 三路数据永远是0 | ADC2和ADC3时钟未启用 | 检查ADC12和ADC3的时钟使能位,确认CubeMX里已开启对应ADC的时钟 |
| 数据顺序错乱 | DMA缓冲区长度不是3的倍数 | 把DMA目标数组长度调整为3的整数倍,严格按照三重模式拆分逻辑处理 |
| 波形明显削顶 | 输入信号超过参考电压 | 确认信号在0到3.3V范围内,必要时调整前端偏置或衰减电路 |
| 波形有锯齿抖动 | ADCCLK频率过高或采样时间太短 | 查一遍ADC时钟分频和采样周期设置,把采样时间调大到15个周期以上 |
| 采样率偏低 | 定时器预分频与周期设置不当 | 计算触发频率时注意定时器时钟源频率,用逻辑分析仪实际测量TRGO波形 |
| 只有一路有数据 | 只启动了ADC1的DMA | 确认未手动关闭ADC2/ADC3的DMA使能,多检查主设备DMA配置 |
锯齿抖动这个问题,其实很多新手会归咎于电源纹波,但真相往往是ADC的采样周期不够长。内部采样开关打开后,采样电容需要通过源阻抗充电,源阻抗越大,需要的采样时间越长。比如信号源阻抗是10kΩ,采样电容约8pF,时间常数是80ns,至少得给5个时间常数也就是400ns才能稳定,而配置成1.5个周期在84MHz ADC时钟下只有不到18ns,自然采样值会有明显波动。在允许范围内把采样时间调到最大,是最简单的降噪手段。
4.3 调试三重ADC的几个独家经验
调试三重ADC有一个我认为很有用的技巧:先用一个直流电压接在三路输入上,观察拆分后的输出是不是同一个值。如果三路都显示相同的直流值,说明硬件同步、DMA搬移、数据解析这三级链路全通了;如果显示值各不相同,就按链路顺序往上查。这个方法我把叫作“静态一致性验证”,比直接上信号发生器效率高得多,因为直流情况下所有的时序问题都会表现为数值偏差,定位误差源更方便。
另一个经验是关于中断优先级的。当ADC使用DMA连续采集时,DMA中断的优先级不要比定时器中断低,否则数据填充期间CPU在处理其他中断,DMA半传输和传输完成回调会被延迟,导致分析程序拿到的永远是旧数据,波形看起来像被冻结一样。正确做法是把DMA中断优先级设得很高,或者干脆在主循环中轮询标志位,因为我这个项目的中断频率不算太高,主循环轮询完全来得及。
最后一个建议是善用调试器实时查看DMA缓冲区。在Keil或者IAR里跑起来后,把DMA目标数组添加到Watch窗口,观察它的数值是否在持续变化。如果数组冻结在某一个固定值,大部分情况下DMA根本没跑起来。此时检查DMA的外设请求是否使能,还有一个特别容易忽略的点,ADC处于连续转换模式下,DMA请求才能持续产生,如果CubeMX里不小心把连续转换关掉了,一次触发转换一次数据就停住了。
5. 扩展思路:用三重ADC做点更实际的事
三相电流采集是我最推荐的上手项目。三路ADC配上三个电流传感器,同步采样三相电流波形,在电机控制领域属于刚需。电机低速时电流波形频率低,用低速采样率就能还原,但高速时电流频率上去以后,低速采样会产生大量谐波误差,三重ADC的并行特性在这里体现得很明显。你不需要在三个采样时刻之间做插值补偿,因为三路同时触发,直接拿数据算Clark变换和Park变换,误差能控制在一个极小的范围。
另外一个有意思的思路是用三重ADC做多通道声音采集,比如做一个简单的声音定位装置。三个麦克风分别接到ADC1、ADC2、ADC3的三个通道上,同时采样三路音频信号,通过计算通道之间的时间差来定位声源方向。这种应用里,通道同步误差会成为定位精度的决定性因素。三重ADC的硬件同步比靠软件轮流采样精确得多,实测下来时间差分辨率能稳定在几十纳秒量级,这在普通MCU方案里几乎不可能做到。
在跑这个项目的时候我还有个体会,这跟裸机代码和RTOS的关系不大,核心还是在配置的三级联动上。三重ADC加DMA这套东西本身不复杂,但其配置链路横跨了时钟、引脚、定时器、ADC、DMA五个外设,任何一个环节漏了配置,最终表现都是波形错误。养成用固定模板搭工程的习惯,能省掉大量重复试错的时间。我自己的模板是从CubeMX生成后,再补一份针对三重ADC的数据拆解工具函数库,之后每个项目直接复用,稳定性都不错。
本文还有配套的精品资源,点击获取