STM32F407双ADC交替采样+DMA,轻松实现等效采样率翻倍
2026/9/24 12:05:40 网站建设 项目流程

1. 别再只用单ADC了:双ADC交替采样到底解决了什么问题

我之前做一块板子要采200kHz左右的正弦波做FFT,手里的STM32F407单ADC无论怎么调参数,波形总是差一口气。F407的单路ADC理论峰值是2.4Msps,听着够用,但采样保持时间稍微加一点,或者想留点余量做滤波,很快就不行了。后来我索性把片内两个ADC拿出来做交替采样,配合DMA搬运,等效采样率直接翻倍,波形质量明显改善。

如果你也遇到类似的问题——测中高频信号时单ADC速度捉襟见肘、想用F407做音频采集或振动分析、或者想彻底搞懂CubeMX里那堆ADC和DMA选项,这篇内容就是按我实际调通过的流程给你写的。整个过程不涉及复杂的外设库,都是HAL库加CubeMX生成工程,新手照做也能跑起来。

1.1 单ADC的采样率天花板到底卡在哪里

STM32F407内部是逐次逼近型ADC,转换12位结果有固定的转换周期,不是想调多快就调多快。手册上写得很清楚,ADC时钟最高36MHz,而我们常用配置下APB2是84MHz,所以要给ADC单独分频。如果用12位分辨率,单次转换需要12 + SMP个ADC时钟周期,SMP是采样保持时间,可以配置成3、15、28、56、84、112、144、480等档位。

最常用的高速配置是SMP=3,ADC时钟21MHz,那么单次转换时间是:

单ADC采样率 = fADC / (12 + SMP) = 21MHz / 15 = 1.4Msps

注意,这里1.4Msps是实际速度,不是手册标题里那个2.4Msps。2.4Msps得在ADC时钟36MHz、SMP=3的情况下才是36MHz/15=2.4Msps,而36MHz要求APB2至少72MHz再用2分频,配置上更紧张,实际工程很少这么跑极限。

单ADC只有1.4Msps,想采样一个100kHz的波形,理论上每个周期还能采14个点,勉强够看;一旦信号到200kHz,每个周期只剩7个点,FFT出来的频谱已经惨不忍睹了。这就是我决定上双ADC交替采样的直接原因。

1.2 交替采样、同步采样、独立采样要分清

很多人一听“双ADC”就以为把两个ADC都打开、两个DMA都启动就算完事,其实完全不是一回事。F407里两个ADC的组合方式有几种,方向完全不同:

  • 独立模式:ADC1采A通道,ADC2采B通道,互不干扰,速度没有任何提升。
  • 同步采样模式:两个ADC在同一个触发边沿同时开始转换,分别采A和B通道,适合需要严格同步的两路信号采集,但单路采样率依旧不变。
  • 交替采样模式:两个ADC轮流采同一个通道,ADC1采第N个点,ADC2采第N+1个点,最后通过DMA按顺序搬进内存,等效采样率翻倍。

打个比方:一个人记账速度跟不上,就安排两个人轮流记,一个记单数、一个记双数,最后的账本还是一个连续序列。但两个人的“笔迹”不可能完全一样,反映到ADC上就是两个通道之间存在微小的增益和偏移差异,采出来的相邻点会有规律性毛刺。这个坑我在后面实测部分会专门讲。

2. CubeMX里把双ADC、DMA和时钟一次配对

2.1 先定ADC时钟:所有速度的地基

打开STM32CubeMX,芯片选STM32F407VET6或者你手里的具体型号,进入RCC配置外部晶振HSE,然后到Clock Configuration里把SYSCLK配到168MHz,APB2总线频率84MHz。这一步不处理好,后面ADC分频出来全是奇怪的数字。

接下来打开ADC1和ADC2两个外设,在Parameter Settings里找到ADC Clock Prescaler,选4分频,这样ADC时钟就是84MHz/4=21MHz。很多教程为了追求速度直接选2分频,得到42MHz,已经超过36MHz上限了。短时间可能看不出问题,但长时间运行转换结果会漂,尤其在高温环境下,我强烈不建议这么做。

分辨率选12位,Scan Conversion Mode保持Disable,因为我们只采集一个通道;Continuous Conversion Mode选Enable,这样ADC会一直转,不用手动触发;Discontinuous Conversion Mode保持Disable。采样时间这里可以选3 Cycles先跑通流程,后面再看信号源内阻决定要不要加大。

需要注意,双ADC交替采样时,两个ADC的采样时间必须一致,否则交替的半个周期会错位。CubeMX在多ADC模式下一般会把从设备的参数跟主设备同步,但生成代码后最好再检查一次ADC2的SMP值。

2.2 双ADC交替模式与DMA的配置细节

在CubeMX里配置好ADC1后,进入Mode配置,不要选默认的Independent Mode,要往下拉找到Dual ADC interleaved mode。不同版本的CubeMX叫法可能略有差异,但关键字就是interleaved,对应中文意思是交替。有的版本里还会有Dual regular simultaneous mode,那个是同步采样,别选错。

选完交替模式后,ADC2会自动变成从模式,大部分参数由主ADC决定,不需要重复配置。主ADC是ADC1,也就是说后面启动DMA也是从ADC1这边启动,这是很多新手迷糊的地方:为什么我配了ADC2却找不到ADC2的DMA请求?因为在双ADC交替模式下,两个ADC共享一个DMA请求,由master统一发出。

接着到DMA Settings标签页,给ADC1添加一条DMA请求。关键参数就三个:

  • Mode选Circular,循环模式,缓冲区满了自动回到开头继续搬运,适合连续采集。
  • Data Width选Half Word,因为ADC结果是16位,如果选成Word,四个字节里只有两个字节有效,数据会乱成一团。
  • Memory Increment选Enable,Direction从Peripheral To Memory。

DMA传输中断建议打开,在NVIC Settings里勾选DMA Interrupt。实际使用中我通常会同时开半传输中断和全传输中断,这样上半段数据在处理的时候,DMA还在往后半段写,这就是所谓的双缓冲思想,不用额外申请复杂的数据结构。

2.3 生成代码前多检查一遍这三个地方

CubeMX生成代码很快,但配置错了后面调试更痛苦。我每次生成前都会花一分钟检查三处,你可以直接当成检查清单用。

第一,确认ADC1的Mode确实是Dual ADC interleaved mode,不是simultaneous。这两个选项在菜单里挨得很近,一个提升采样率,一个只是同步采样,选错的后果是波形看不出明显提升,但你又说不清哪里不对。

第二,确认DMA的Data Width是Half Word,并且关联的是ADC1而不是其他外设。如果之前项目里用过串口DMA,很容易在添加时选错请求源,生成后一运行,ADC缓冲区一动不动。

第三,检查ADC2没有单独配置一套DMA或中断。多模式下从设备的DMA请求是多余的,如果两个ADC都配了DMA,实际搬运时会出现重复触发,缓冲区被莫名覆盖。

确认没问题后,点击Generate Code生成工程。如果你在Project Manager里把每个外设单独生成一个.c/.h文件,后面想改ADC参数时会清爽很多,不然所有初始化都堆在main.c里,找起来很费劲。

3. 代码里真正干活的部分:DMA搬运、数据拼接与处理

3.1 声明缓冲区和双ADC启动方式

生成完工程后,打开main.c,你会在外设初始化部分看到MX_ADC1_Init和MX_ADC2_Init两个函数。在main函数外面声明一个全局缓冲区,用来接收DMA搬运过来的ADC数据。

#define ADC_BUFFER_SIZE 4096 __IO uint16_t adc_buffer[ADC_BUFFER_SIZE];

缓冲区大小选4096,是因为双ADC交替模式下,每个采样点会对应两个16位结果。DMA搬运的4096个半字,实际对应2048次双ADC采样。如果你打算做1024点FFT,缓冲区设2048就够了,不会浪费内存。

主循环开始前,调用双ADC多模式的启动函数:

HAL_ADCEx_MultiModeStart_DMA(&hadc1, (uint32_t *)adc_buffer, ADC_BUFFER_SIZE);

注意几个容易错的地方:第一个参数传主ADC句柄,也就是hadc1,不是hadc2;第二个参数是缓冲区地址,前面一定要加类型转换;第三个参数是半字个数,不是采样点数,别被函数名里的Start_DMA误导。

启动后DMA就开始自动搬运,我们只需要在中断回调里做标记:

volatile uint8_t adc_dma_done = 0; void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc->Instance == ADC1) { adc_dma_done = 1; } }

如果你的HAL库版本比较新,回调函数名可能是HAL_ADCEx_MultiModeConvCpltCallback,原理一样。主循环里查到adc_dma_done置1,就说明一批新数据已经准备好了。

3.2 把DMA里的交错数据还原成波形

双ADC交替采样时,缓冲区里的数据顺序是:adc_buffer[0]来自ADC1,adc_buffer[1]来自ADC2,adc_buffer[2]又来自ADC1,adc_buffer[3]来自ADC2,以此类推。也就是说,偶数下标是ADC1的结果,奇数下标是ADC2的结果。

从使用的角度看,如果你想直接对整段数据做FFT或者波形显示,其实不需要把奇偶拆开,因为这串数据本来就是等间隔的连续序列,时间间隔是单个ADC间隔的一半。你直接遍历整个缓冲区,按电压公式换算就行:

float voltage = (float)adc_buffer[i] * 3.3f / 4095.0f;

这里分母是4095不是4096,12位ADC满量程编码是4095,很多人第一次写会写成4096,结果整体偏高一丁点,虽然不大,但强迫症受不了。

如果为了调试,想分别观察ADC1和ADC2的波形,可以写一个简单循环把奇偶分开。我建议第一次跑通后一定要做这一步,因为接下来要判断两个ADC的一致性,只有确认了奇偶通道都正常,后面才敢放心用整段数据去算FFT。

实际采样间隔可以这样算:ADC时钟21MHz、SMP=3时,单ADC采样间隔0.714us,双ADC交替后的相邻点间隔是0.357us,等效采样率约2.8Msps。假设你采了一个100kHz正弦波,一个周期能采28个点,已经比单ADC的14点好太多。

3.3 顺手做的去直流、归一化和滤波

采集完原始数据后,很多场景都需要做后处理,常见的三个操作是去直流、归一化和滤波,我在这里给出最朴素的实现方式,够用且好理解。

去直流最简单的做法是先求整段数据的平均值,再把每个点减去平均值:

uint32_t sum = 0; for (int i = 0; i < ADC_BUFFER_SIZE; i++) { sum += adc_buffer[i]; } int32_t avg = sum / ADC_BUFFER_SIZE; for (int i = 0; i < ADC_BUFFER_SIZE; i++) { float normalized = (float)(adc_buffer[i] - avg) * 3.3f / 4095.0f; // 这里继续你的后续处理 }

归一化一般在去直流之后做,可以除以峰值,也可以除以有效值,看你要分析什么。如果只是看波形轮廓,除以峰值比较直观;如果要算信噪比或者做频谱分析,建议除以有效值。

滤波这块,最常用的是滑动平均滤波:

#define WINDOW 5 for (int i = WINDOW / 2; i < ADC_BUFFER_SIZE - WINDOW / 2; i++) { uint32_t acc = 0; for (int j = -WINDOW / 2; j <= WINDOW / 2; j++) { acc += adc_buffer[i + j]; } filtered_out[i] = acc / WINDOW; }

但我要提醒你,滑动平均会降低有效带宽,对于本来就在追求高速采样的场景,别盲目滤波。如果信号本身干净,滤波反而把细节磨掉了。真遇到偶发尖峰,用中位值滤波往往比均值滤波更合适。

如果你想把数据发到电脑上看,最简单的办法是通过串口把原始ADC值按CSV格式打出去,再用串口示波器或者波形工具导入。这里有个经验:不要在主循环里用阻塞式HAL_UART_Transmit发送大批量数据,会拖垮采集节奏;要发就用串口DMA,或者先把要发的数据放进缓冲,等ADC一轮采集结束后再统一发送。

4. 实测记录:采样率没变快、波形乱跳怎么办

4.1 先验证通道顺序:用低频正弦波找规律

我第一次跑双ADC交替采样时,波形看起来总是不对劲,FFT出来一堆杂散,后来发现不是配置问题,而是我没搞清楚缓冲区里谁先谁后。验证方法其实很简单:信号发生器输出一个1kHz正弦波,接到ADC通道引脚,然后分别把缓冲区里的偶数下标和奇数下标单独还原成波形,在调试器里看数组。

正常情况下,两个波形应该都是平滑的正弦波,只是相位上略有差异。如果偶数位波形正常、奇数位波形有固定偏移或者幅度偏小,说明ADC1和ADC2之间的增益/失调不一致。如果奇偶波形都正常但拼接起来后锯齿感很强,大概率是两个ADC的转换结果在缓冲区里的顺序跟预期相反,可以把DMA目标地址往后移一个半字,或者软件里做奇偶交换。

这个验证步骤值十分钟,别省。高速采集的数据一旦错位,后面所有分析都白做,排查起来比重新配置还要费时间。

4.2 计算真实等效采样率而不是看手册标题

很多人以为开了双ADC交替采样就直接到4.8Msps,这是最大误区。4.8Msps是ADC时钟36MHz、SMP=3、双ADC交替的理论极限,实际工程中受分频限制、信号源内阻、PCB布局等因素影响,很难跑满。我用常见的21MHz ADC时钟做了几组计算,整理成表格给你参考:

ADC时钟采样保持SMP单ADC实际采样率双ADC交替等效采样率
21MHz3 Cycles1.40Msps2.80Msps
21MHz15 Cycles0.78Msps1.56Msps
14MHz3 Cycles0.93Msps1.87Msps
14MHz15 Cycles0.52Msps1.04Msps

如果你追求高采样率,就把SMP调到3;如果信号源输出阻抗比较高,或者前端电路驱动能力弱,采样时间太短会导致采样电容充不满,转换结果偏低,这种情况宁可把SMP调大一点,让波形更可靠。片内ADC再快,前端信号跟不上也是白搭。

4.3 常见问题速查表

这段时间我周围也有好几个朋友在调双ADC,加上自己踩过的坑,我把典型问题整理成了一张速查表,排查时直接对着看:

现象可能原因解决方法
adc_buffer全为0DMA没启动或通道未接到引脚检查HAL_ADCEx_MultiModeStart_DMA是否调用,用万用表确认引脚有信号
波形呈规律性锯齿ADC1和ADC2增益/失调不一致分别提取奇偶数据做两点校正,或确认SMP设置相同
数据每隔一个点跳变DMA数据宽度选成了Word改回Half Word
缓冲区数据被莫名覆盖ADC2也重复配置了DMA删除ADC2的DMA配置,只保留主ADC1的DMA
中断里卡死或丢数据在DMA中断回调里做了浮点运算中断里只置标志位,浮点运算放到主循环
调试器暂停后波形乱调试模式暂停导致ADC时钟停止正常现象,不要用它验证动态波形
想用Proteus仿真但找不到F407模型双ADC和DMA时序在仿真里支持不完善建议直接用开发板实测,别在仿真上耗时间

关于ADC1和ADC2不一致的校正,我用的方法是输入一个已知直流电压,分别统计两个ADC各2000个点的均值,算出各自的offset,再测另一个电压点算gain比例,最后对其中一个通道做线性修正。公式就是最简单的两点校准:

adc2_corrected = (adc2_raw - offset2) * (gain1 / gain2) + offset1

如果只是做FFT不看绝对幅度,这个校正可以简化成只修正offset,因为相邻两个通道的gain差异在多数情况下很小。

4.4 给同样在折腾双ADC的朋友几句实在话

这套方案我前前后后调了两个星期,最大的体会是:不要一上来就追求极限速度。先用1kHz正弦波跑通整个链路,确认DMA搬运正常、奇偶通道顺序正确,再一点点往上加频率,遇到波形异常就回头查配置,这样省时间得多。

还有一点很关键:F407的双ADC交替采样能把等效采样率提到Msps级别,但ADC前端电路才是真正决定系统性能的地方。我用过一次很长的杜邦线直接接信号源,结果高频波形严重畸变,还以为是双ADC配置出了问题,后来换成短引线加运放缓冲,波形立刻干净了。芯片的ADC速度再快,输入信号没准备好,后面都是白算。

如果你后续要用更高频率采集,可以考虑F407的8位或6位分辨率模式,转换周期更短,采样率还能再往上拉。但代价是分辨率下降,值不值得就看你的应用场景了。至少就我目前的振动分析和音频采集需求来说,双ADC交替采样加DMA搬运这套组合,已经是F407上性价比很高的方案了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询