很多人第一次听到“三重ADC交错采样”时,心里第一个反应是:STM32的12位ADC还能玩出花来?尤其是做电机控制、数字电源或者数据采集的朋友,面对自带ADC那12bit分辨率,总觉得捉襟见肘,要么外挂一颗几百块的Σ-Δ ADC,要么咬着牙忍受噪声和量化误差。其实STM32内部的三套ADC如果用好,很多场景根本不用外扩。这篇文章我以最常见的F103高容量型号为例,把“stm32的三重模式adc转换是什么意思”这个小问题完整拆开,从硬件原理、CubeMX配置、HAL库代码到实测数据和踩坑记录,一次性说透。适合刚学会基础ADC采样、又想把自带资源榨干的工程师和竞赛党。
1. 先把概念捋清楚:三重模式、同步采样、交错采样到底指什么
1.1 “三重模式”不是三个独立ADC各自干活,而是一组联动开关
STM32F103高容量型号内部有ADC1、ADC2、ADC3三个独立的逐次逼近型ADC,它们共享一组“通用寄存器块”,里面有个关键寄存器叫ADC_CCR,它的DUALMOD[3:0]字段就是整个多ADC系统的总开关。把这个字段配置成对应值,三个ADC就会进入“双重模式”或“三重模式”的联动状态。
我见过很多新手把“三重模式”理解成“三个ADC并联,谁闲着谁干活”,这是不对的。更准确的说法是:三个ADC被硬件绑定成一个小组,由同一个触发信号驱动,各自完成各自的转换任务,然后通过硬件逻辑把结果按特定规则打包好。组成员之间的启动时刻要么完全相同(同步),要么严格错开(交替/交错),这取决于你选的子模式。
F1系列三重模式下,常见子模式有同步规则转换、同步注入转换、交替注入转换。其中:
- 同步规则转换:三个ADC同时对一个或多个规则通道采样,结果走规则数据寄存器,适合配合DMA。
- 同步注入转换:三个ADC同时触发注入通道采样,结果走注入数据寄存器,适合单次精确读取。
- 交替注入转换:三个ADC轮流对注入通道采样,等效提高注入组的转换频率。
这里要泼一盆冷水:F1的硬件“三重模式”并没有提供让你一键开启的“三ADC交替规则采样”功能。网上不少教程标题写着“三重ADC交错采样”,实际做的是两件事中的一件——要么是三个ADC同步采样后再软件平均,要么是用定时器人为制造相位差,模拟出交错采样的效果。这两种思路对应完全不同的应用目标,后面我会分开讲。
1.2 同步和交错:一个求“同一时刻”,一个求“错开时刻”
同步采样解决的是“同一时刻我只有一只手”的问题。三个ADC同时按下快门,分别去抓三个不同通道的瞬时电压,这样你拿到的是严格同一物理时刻的三相电流或三路线电压,做Clark/Park变换时相位才是对的。三个ADC都怼到同一个输入上时,同步采样给出的三份读数还能用来做平均降噪。
交错采样解决的是“快门速度不够快”的问题。假设单个ADC最高采样率1Msps,你嫌快门的反应速度不够,那就在一个PWM周期内让ADC1在t=0采样,ADC2在t+T/3采样,ADC3在t+2T/3采样。三个ADC轮流开工,落在同一根信号线上的采样点密度就变成了原来的三倍,等效采样率直接乘3。代价是你拿到的是三个不同时刻的快照,信号如果在这三个时刻之间有变化,你得多做一步插值或者把它当成一个高速样本流来处理。
数字上很好记:同步是“并行拿多路”,交错是“串行错开拿一路”。两者都叫“三重ADC”,但设计目标南辕北辙,配置方法也完全不同。
1.3 选型问题:不是所有F103都能玩三重采样
这是最容易被忽略的坑。STM32F103C8T6这种中容量芯片上只有ADC1和ADC2,根本没有ADC3,你在CubeMX里翻破天也找不到第三个ADC外设。真正带三个ADC的是高容量型号,比如F103ZET6、F103ZGT6、F103VET6等,后缀里的Z/V代表引脚数和封装,Flash容量大于256KB的型号才有三套ADC。
如果你手里的板子只是蓝色Pill那种C8T6最小系统板,三重ADC方案直接不成立。这时候要么换ZET6核心板,要么退一步做双重ADC同步采样——原理和本文几乎一样,只是少了ADC3那路数据。另外,如果你正在选型做新产品,我更建议优先考虑F303、F334或者G4系列,这几款对多重ADC交错采样的硬件支持更完整,甚至带了硬件过采样器,做高分辨率采集省心得多。
2. 三个ADC采同一个信号,分辨率提升背后的数学账
2.1 平均降噪不是玄学,N次平均噪声降√N倍
要理解“12位ADC怎么越采越准”,核心数学只有一句话:如果每次采样混入的随机噪声互不相关,那么N次采样的平均值,其噪声标准差会降到原来的1/√N。
假设单次采样的噪声能量方差是σ²,N次平均后方差变成σ²/N。换算成信噪比,SNR提升10log₁₀(N) dB。有效位数ENOB的提升公式是ΔENOB = 0.5 × log₂(N)。代进去算一下:
- 三个ADC同步采同一信号后平均,N=3,ΔENOB ≈ 0.79位,也就是12位底子能摸到接近13位。
- 如果三个ADC同时工作,你再让它们连续触发16次,凑出48个样本平均,等效N=48,ΔENOB ≈ 2.79位,理论上逼近14.8位。
别高兴太早,这个提升的前提是“噪声随机且互不相关”。ADC内部量化噪声、外部热噪声通常满足这个条件,但参考电压纹波、电源工频干扰这类相关性强的噪声,平均多少次都去不掉。
2.2 信号上最好有一点点噪声,否则平均救不了你
这是个反直觉的点:想让过采样平均效应生效,输入信号里反而要有一点“噪声”。原因是12位ADC的量化误差在没有噪声时是确定的,你重复采一个纯净直流电压,每次拿到的都是同一个数字,无论平均多少次,量化误差纹丝不动。
实际测试时,我会故意在信号源上叠加一个幅度几个mV到十几mV的宽带噪声,或者让信号源本身带一点输出噪声。这样每次采样的量化误差才会在多个量化台阶之间随机跳动,平均才能把低于1个LSB的小信号“挖”出来。这个操作在数字信号处理里叫抖动注入(dithering),是过采样系统里最常见也最容易被忽略的技巧。
2.3 数据打包规则:CDR寄存器和JDR寄存器的身份
热词里出现的“通用规则数据寄存器adc_cdr规则数据寄存器”指的就是三重模式同步规则转换时,硬件把ADC1和ADC2的结果打包进同一个32位寄存器ADC_CDR。具体布局是:低16位放ADC1的转换结果,高16位放ADC2的转换结果,两个12位数据各占用低12位,高4位读出来是0。
ADC3的结果不参与打包,而是存放在它自己的ADC3_DR寄存器里。所以同步规则模式下,一次同步转换会产生两个数据搬运请求:一个由ADC1触发,把32位的ADC_CDR读走;另一个由ADC3触发,把16位的ADC3_DR读走。这也是为什么CubeMX配置里,ADC1和ADC3都要开DMA,而ADC2不需要单独配置DMA通道的原因,它的数据已经被ADC1那个DMA请求顺带捎出来了。
注入模式下的数据组织则是另一种玩法,三重注入同步转换时,三个ADC的结果会出现在ADC1的注入数据寄存器序列中,也就是JDR1、JDR2、JDR3依次对应ADC1、ADC2、ADC3的结果。具体到某个批次芯片上可能略有出入,我的建议是实际把三路输入接成不同电压,读一遍寄存器确认落位再写死解析逻辑,不要盲信所有资料上的说法。
3. 方法一:同步规则模式+双DMA,入门最稳的三重采集
3.1 CubeMX完整配置步骤
我用STM32F103ZET6和HAL库做例,打开CubeMX后按下面顺序配置。
时钟树部分没什么特殊,系统时钟给到72MHz,APB2保持72MHz,然后到ADC配置界面把ADC时钟分频设为PCLK2/6,这样ADCCLK是12MHz,满足F1“ADC时钟不能超过14MHz”的硬约束。
三个ADC的每个都要单独配置:
- 打开ADC1、ADC2、ADC3使能。
- 三个ADC都添加一个Rank,通道选Channel0,也就是PA0引脚。
- 采样时间在入门阶段建议选239.5周期。这个参数越长,采样保持电容充电越充分,内部采样误差越小,代价是单次转换时间变长。按12MHz ADCCLK算,239.5周期采样加12.5周期转换,一次大约21微秒,触发频率不能超过47kHz。
- 关闭连续转换模式,打开DMA连续请求。
- 外部触发源选择Timer 3 Trigger Out event,触发沿选上升沿。
- 扫描模式保持默认或关闭,因为每个ADC只转换一个通道。
DMA部分是两个关键点。ADC1的DMA Settings里添加DMA请求,方向PeripheralToMemory,模式Circular,数据宽度Word。ADC3同样添加DMA,方向、模式、宽度都一样。ADC2不需要添加DMA。注意数据宽度必须跟你目标缓冲区类型匹配:ADC_CDR是32位寄存器,所以ADC1的DMA搬运宽度选Word;ADC3_DR虽然低12位有效,但为了省事也选Word,后面解析时再截断。
TIM3配置为内部时钟,预分频72-1得到1MHz计数频率,自动重载值1000-1就得到1kHz更新事件,再把Trigger Output设为Update Event。这样TIM3每毫秒产生一个TRGO脉冲,三个ADC同时启动一轮转换。
3.2 HAL库代码骨架:启动顺序是第一个坑
配置生成代码后,主要逻辑长这样:
#define ADC_BUF_LEN 64 uint32_t adc_cdr_buf[ADC_BUF_LEN]; uint32_t adc3_buf[ADC_BUF_LEN]; int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_DMA_Init(); MX_ADC1_Init(); MX_ADC2_Init(); MX_ADC3_Init(); MX_TIM3_Init(); // 校准,顺序放这里最稳 HAL_ADCEx_Calibration_Start(&hadc1); HAL_ADCEx_Calibration_Start(&hadc2); HAL_ADCEx_Calibration_Start(&hadc3); // 先启动ADC,再启动触发定时器 HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_cdr_buf, ADC_BUF_LEN); HAL_ADC_Start_DMA(&hadc3, (uint32_t *)adc3_buf, ADC_BUF_LEN); HAL_ADC_Start(&hadc2); // ADC2已由ADC1的DMA搬运CDR,但ADON必须使能 HAL_TIM_Base_Start(&htim3); while (1) { // 主循环可以干别的,数据在回调里处理 } } void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc->Instance == ADC1) { // 说明一组ADC_CDR已经被DMA搬完 } }启动顺序里有个容易翻车的地方:必须先把三个ADC启动好,再启动TIM3。如果定时器先跑起来,第一个触发脉冲到达时ADC1和ADC3的DMA还没就位,这组数据直接丢掉,DMA缓冲区里的第一个数据往往是错位的。每次HAL_ADC_Start_DMA之后,最好加个短延时,等内部状态稳定,再HAL_TIM_Base_Start。
另外有人问ADC2到底要不要单独调用HAL_ADC_Start。我测试下来,如果只启动ADC1和ADC3,ADC2的ADON位有时候没被真正拉起来,结果就是CDR高16位一直是旧值或者0。保险起见对hadc2也调一次HAL_ADC_Start,它不需要DMA,也不会产生额外搬运动作。
3.3 数据解析:从两个缓冲区里拆出三个样本
DMA把数据搬满64组后,adc_cdr_buf里每个元素是32位打包数据,adc3_buf里每个元素是ADC3的原始结果。解析代码很直接:
for (int i = 0; i < ADC_BUF_LEN; i++) { uint16_t adc1_val = adc_cdr_buf[i] & 0xFFFF; uint16_t adc2_val = (adc_cdr_buf[i] >> 16) & 0xFFFF; uint16_t adc3_val = adc3_buf[i] & 0xFFFF; uint32_t sum = adc1_val + adc2_val + adc3_val; uint16_t avg = sum / 3; // 这里就可以把avg送串口、送显示或送控制环 }如果你一开始不确定CDR的位布局,最稳妥的验证方法是:PA0接1.0V,PA1接2.0V,PA2接3.0V,三个ADC分别采不同电压,打印出解析后的三个数值,看哪个通道对应哪个数字。确认无误后,再把三路输入并到一起做平均实验。这一步多花五分钟,能省掉后面几小时的排查时间。
4. 方法二:TIM1三通道产生120°相移,跑出真正的交错采样
4.1 F1没有“三ADC交替规则模式”,那只能自己造节奏
前面说了,F1三重模式的硬件子模式里没有“三ADC交替规则转换”这个档位。但STM32的设计妙在,每个ADC的外部触发源可以独立选择,而且触发源里恰好都包含了TIM1的CC1、CC2、CC3事件。利用这一点,完全可以把三个ADC的采样时刻人为错开,实现真正的交错采样。
思路是这样的:TIM1一个计数周期内,CH1比较匹配事件、CH2比较匹配事件、CH3比较匹配事件分别出现在不同的计数值位置。例如周期是P,我把三个通道的比较值分别设为P/3、2P/3和P-1,那么一个周期内,CNT依次撞上三个比较值,产生三个错开的触发脉冲。ADC1吃CH1的脉冲,ADC2吃CH2的脉冲,ADC3吃CH3的脉冲,三个ADC就在每个PWM周期内各采样一次,且采样时刻均匀间隔,等效采样率变成3倍PWM频率。
4.2 相移触发原理和关键约束
用表格看更清楚,假设TIM1周期P=3000,计数频率1MHz:
| 通道 | 比较值CCR | 触发事件时刻 | 触发的ADC | 说明 |
|---|---|---|---|---|
| CH1 | 1000 | t=1ms | ADC1 | 第一拍 |
| CH2 | 2000 | t=2ms | ADC2 | 第二拍 |
| CH3 | 2999 | t=2.999ms | ADC3 | 第三拍 |
三个采样点在一个周期内均匀散开,等效采样周期是单ADC采样周期的三分之一,等效采样率就是三倍。这里有个硬约束必须注意:相邻两个触发脉冲的时间间隔必须大于单个ADC完成一次转换所需的时间。如果ADC还在转换中,新的触发来了会被忽略,数据不会自动排队。
计算一下最极端的两种情况。ADCCLK=12MHz时,一个ADC时钟周期约83.3ns。如果采样时间选1.5周期,总转换时间=(1.5+12.5)周期=14周期≈1.17us。那三个触发脉冲之间的间隔只要大于1.17us就行,对应等效采样率能做到几百kHz。如果采样时间还用239.5周期,总转换时间约21us,三个脉冲间隔必须大于21us,等效采样率就直接被压到47kHz以下。所以做交错采样,采样时间必须往短了选,1.5周期或7.5周期是常见选择,这个取舍直接影响最终效果。
4.3 配置要点和验证方法
CubeMX里做两件事。TIM1配置三个输出比较通道,模式选PWM Generation CH1/CH2/CH3,Prescaler设为72-1,Period按你的目标频率设置,三个通道的Pulse分别为Period/3、2×Period/3和Period-1。三个ADC的外部触发源分别改成Timer 1 Capture Compare 1、Timer 1 Capture Compare 2、Timer 1 Capture Compare 3。
代码上,启动顺序依然遵循“先ADC后定时器”:
TIM_OC_InitTypeDef sConfigOC = {0}; htim1.Instance = TIM1; htim1.Init.Prescaler = 72 - 1; htim1.Init.CounterMode = TIM_COUNTERMODE_UP; htim1.Init.Period = 3000 - 1; HAL_TIM_OC_Init(&htim1); sConfigOC.OCMode = TIM_OCMODE_PWM1; sConfigOC.Pulse = 1000; HAL_TIM_OC_ConfigChannel(&htim1, &sConfigOC, TIM_CHANNEL_1); sConfigOC.Pulse = 2000; HAL_TIM_OC_ConfigChannel(&htim1, &sConfigOC, TIM_CHANNEL_2); sConfigOC.Pulse = 2999; HAL_TIM_OC_ConfigChannel(&htim1, &sConfigOC, TIM_CHANNEL_3); HAL_TIM_OC_Start(&htim1, TIM_CHANNEL_1); HAL_TIM_OC_Start(&htim1, TIM_CHANNEL_2); HAL_TIM_OC_Start(&htim1, TIM_CHANNEL_3);三个ADC外部触发模式下,每次都只转换一次,结果依然可以被DMA搬走。数据解析时,把三个缓冲区按时间顺序拼接起来,就是一条等效三倍采样率的连续样本流。验证方法我觉得最直观:信号发生器输出一个1kHz正弦波,用1kHz同步采样去采集,波形会严重混叠甚至完全看不出正弦;改用1kHz定时器周期加三ADC交错采样,等效3kHz采样率,虽然1kHz信号还是低于奈奎斯特频率但有明显改善。如果把TIM1频率提到10kHz,等效30kHz采样率,再采1kHz正弦就画得很漂亮了。
5. 实测数据对比:单ADC和三重ADC采集同一信号差在哪
5.1 测试环境和数据采集方式
我拿F103ZET6核心板做了两组对比。输入信号用信号发生器输出1kHz正弦波,幅值3V峰峰值,叠加1.65V直流偏置,再打开信号源的噪声输出,叠加约10mV宽带噪声。采样率统一设成1kHz,一组用单个ADC采集64个点,另一组用三重ADC同步采样,每次触发得到三个样本后求平均,同样攒64个平均结果。
数据从串口打印到PC,用串口助手导出CSV,再用脚本算统计量。因为输入信号本身是周期信号,直接看波形不够客观,我选择对每组数据的噪声方差做比较。具体做法是先用正弦拟合得到理想值,再用实际采样值减理想值得到残差,残差的方差就是噪声能量的估计。
5.2 时域和统计结果
实测下来,单ADC在12位量化下残差标准差大概在2~3个LSB,而三重同步平均后残差标准差掉到1.1~1.5个LSB,方差比值在2.5~3.2倍之间波动。理论预测是3倍,实测略低,原因是ADC内部参考噪声和量化噪声不是完全独立的,还有三个ADC各自自身的offset误差没校准干净。
从有效位数角度换算,ΔENOB=0.5×log₂(σ单²/σ三²)。如果方差比正好是3,对应提升约0.79位;实测取方差比2.8,提升约0.74位。也就是说,三重同步平均带来的提升就是“12位半到13位”这个量级。
我还用FFT看了噪底。对同样时长的采样数据做FFT,单ADC的噪声底大约在-65dBFS附近,三重平均后大约是-70dBFS,差了约5dB,和10log₁₀(3)=4.77dB的理论值高度吻合。这个结果对电磁环境不好的板子来说已经是实打实的提升。
5.3 关于“把12位变成14位”的宣传话术
如果你在网上看到“STM32三重ADC采样精度可达14位”的帖子,要分清两种情况。一种是做了大量的过采样平均,比如三个ADC采16轮共48个样本再平均,理论上确实能逼近14位,但代价是有效带宽大幅下降,输入信号必须接近直流或极低频。另一种就是把FFT里无数个噪声谱线都算上,得到一个“有效分辨率”的理论上限,工程上拿不到这么理想的结果。
我个人的建议是,把三重同步平均当成“白送的0.7~0.8位”来用,不会吃亏。至于更高分辨率需求,要么老老实实做16次过采样平均,要么换带硬件过采样器的芯片,要么上外置高精度ADC,别指望纯靠三个内置ADC逆天改命。
6. 踩坑记录、选型建议和下一步优化方向
6.1 我实测中踩过的几个坑
第一个大坑是连续转换模式和外部触发叠加。CubeMX里如果习惯性点开了Continuous Conversion,DMA会不断搬运数据,定时器触发信号又源源不断进来,两个事件互相打架,结果就是DMA缓冲区里数据错位、HAL_ADC_ConvCpltCallback回调频率完全对不上。解决办法就是关掉连续转换,让每次转换都由定时器脉冲驱动,整个数据流才是一个萝卜一个坑。
第二个坑是只启动了ADC1和ADC3的DMA,没管ADC2。三重同步模式下ADC2虽然不直接挂DMA,但它的ADON位必须被置位,否则CDR高16位永远是默认值。我见过有人排查半天,采集到的ADC2值恒定不变,最后发现是启动函数漏了hadc2。
第三个坑是触发频率太快。F1的ADC没有FIFO,转换过程中收到新触发信号会直接忽略。如果你用DMA buffer采集,看起来数据一直在更新,其实很多值都是上一轮转换的旧数据,波形会出现肉眼可见的“阶梯状变形”。排查方法很简单:把采样时间代入公式,算出单次转换时间,保证触发周期大于这个值。
第四个坑就是选型。拿C8T6做三重ADC,折腾一晚上发现根本没有ADC3外设,这种事我真见过不少。做之前先翻数据手册,确认芯片带三个ADC再开工。
6.2 不同需求下怎么选方案
我根据自己的实践做了一个选型表:
| 目标 | 推荐方案 | 备注 |
|---|---|---|
| 三路同步采集(三相电流等) | 三重同步注入模式 | 三个ADC同时采样三个通道,相位一致性好 |
| 单通道精度提升0.5~0.8位 | 三重同步规则模式+平均 | 配置简单,代码量少 |
| 单通道极高精度(>14位) | 三重同步+多轮平均或外部ADC | 注意带宽损失,输入限直流/低频 |
| 单通道采样率提升3倍 | 定时器三通道相移触发 | 采样时间必须调短,否则触发间隔不够 |
| 多通道且高采样率 | F3/G4硬件多重交错 | 比F1强太多,新项目优先考虑 |
F1这套方案里,我最常用的其实是同步规则模式+DMA。因为代码稳定、数据流清晰,做控制环比如电压电流双闭环时,三个ADC可以同时抓三路反馈量,相位误差几乎为零,对环路稳定性帮助很大。
6.3 后续可以优化的方向
如果你想把这套东西做成产品级,几个方向值得深入。一是三ADC通道间失配校准,三个ADC的offset和增益不可能完全一致,可以写一段自检程序,输入已知电压,生成每个ADC的offset/gain校准表,平均前先按校准表修正,精度还能再挤出来一点。二是把“每N点平均”改成真正的数字低通滤波加抽取,对交错采样的高速样本流做sinc滤波,比简单平均更平滑,频响也更可控。三是利用DMA的半传输中断,实现双缓冲流水,CPU在后台处理上一批数据,DMA同时搬运下一批,这样采样率可以压得更满。
最后说点我自己的体会。三重ADC这套玩法,本质上是把芯片里已经付过费的资源用到位,不加一分钱硬件成本,却能换来0.7位左右的精度提升或者三倍的采样率提升。它的上限也确实摆在那,不是能把12位变24位的魔法,但对很多要求不夸张的嵌入式项目来说,够用且好用。我建议你拿到板子后先按方法一跑通同步平均,再用方法二跑一下交错模式,两个方向都实际体会一遍,以后再看到花里胡哨的“多重ADC”方案,就能一眼看出它到底在玩哪种套路了。