简介面向STM32嵌入式开发者提供一套基于HAL库的三重ADC同步采集波形工程示例重点解决多路模拟信号同时采样时的时钟配置、同步触发、通道切换以及数据非阻塞读取等实际问题适用于传感器阵列、音频分析、电机驱动等需要高精度同步采集的应用场景。压缩包内共122个文件以C语言源文件和配套头文件为主同时包含CubeMX配置文件、Keil工程文件、十六进制固件以及文本说明完整展示基于固件库的标准工程结构整体仅1.08MB轻量易用。目前已有376人学习。工程代码逻辑清晰涵盖ADC分频时钟、转换分辨率、序列器与扫描模式、同步采样触发等关键设置并演示了使用中断和直接存储器访问两种方式启动转换、执行校准以及配置多重采集模式的操作数据处理部分还给出了缓冲存储与滤波算法思路便于直接理解三重ADC完整工作流程。读者可对照该工程快速掌握多ADC协同采样实现方法并复用或移植到自身信号采集产品中具有较好的参考价值。 先把话放前面这篇内容是我自己实际折腾“STM32HAL”三重ADC采集波形时攒下来的完整记录包括配置思路、代码逻辑、上位机联调方法还有一串踩过的坑。如果你正准备用STM32做高速波形采集或者正被单ADC采样率卡住这篇文章应该能帮你省不少时间。我尽量把“为什么这么做”也写清楚而不是只丢一堆配置截图。1. 单ADC的采样率天花板波形采集为什么需要三重ADC不少朋友第一次接触STM32的ADC第一反应是“12位分辨率够用了”。等真拿它去采波形才意识到问题不在位数而在采样率。假设你要观察一路100kHz的正弦波按奈奎斯特理论至少要200kHz采样率但那是“能看到频率”的下限想从波形上看出幅度变化、削波失真、毛刺每个周期至少要有三五十个采样点也就是采样率要拉到3MHz到5MHz这个量级。单ADC能不能做到我们来算一笔账。STM32的ADC完成一次规则组转换需要的周期数是“采样周期 12.5个固定周期”。以常见的F407为例ADC时钟上限是36MHz配置采样周期为3个ADCCLK时单通道采样率大约就是f_s 36MHz / (3 12.5) ≈ 2.32Msps2.32MHz看着不低但对100kHz信号来说一个周期只能采到23个点波形在屏幕上看起来就像锯齿一样细节全丢了。F103更惨ADC时钟上限14MHz单通道算下来也就0.86Msps采个20kHz音频波形都勉强。这时候就该动“三重ADC”的念头了STM32系列里不少中高端型号F4、F3等内部其实集成了三个独立的ADC外设可以把它们组合起来让三个ADC交替着采样等效采样率直接乘以3。这就是三重ADC模式的核心价值也是在不动外部高速ADC芯片的前提下用MCU自带的资源硬生生把波形采集能力往上顶一截的实用做法。2. 三重ADC交错采样的工作机理一次触发怎么变成3个采样点三重ADC模式全称叫“Triple Interleaved Mode”翻译过来是三重交错采样。它和普通“多ADC同步采样”有本质区别同步采样是三个ADC同时去采同一个信号得到的是三个“同时刻”的样本常用于冗余或三相电压电流监测而交错采样是三个ADC依次错开一点时间采样相当于把采样时刻细分了。硬件上怎么实现错开核心在于ADC的启动触发和采样周期配置。三个ADC由同一个定时器触发事件启动但你可以给它们设置不同的采样时间或者利用硬件内部的通道间延迟让ADC1、ADC2、ADC3分别在相邻的时隙里完成采样。以F4系列为例开启三重交错模式后ADC1的DMA会输出一个32位的数据字里面打包了三个ADC的结果分别是高16位、低16位的不同组合。读取时按顺序拼起来就能得到一串采样时刻均匀分布的波形数据。我打个比方你马上明白普通单ADC采样就是你一个人每隔1秒拍一张照片三重交错是三个人各拿一部相机你负责0秒、0.33秒、0.66秒三个时刻另一个人负责0.11秒、0.44秒、0.77秒第三个人负责0.22秒、0.55秒、0.88秒。三个人把照片按时间顺序摆到一起你得到的时间分辨率是原来的3倍。这个机制带来的收益可以从公式上看等效采样率 ≈ 3 × ADCCLK / (采样周期 12.5)。F407在ADC时钟36MHz、采样周期3个ADCCLK时等效采样率能到约6.96Msps。同样是采100kHz正弦波一个周期能采到近70个点波形画出来就圆润多了也足够观察大部分失真细节。有一点需要特别注意三重ADC模式并不是所有STM32都支持。F1系列最多只有双ADC交错模式F4和F3系列才支持完整的三重模式。做项目前先翻对应型号的参考手册在“Multi ADC mode”章节里确认一下省得CubeMX里配到一半发现型号不支持白忙一场。3. CubeMX配置详解时钟、触发源与DMA的最佳组合配置工具用STM32CubeMX版本18以上的都行生成的代码框架基于HAL库。我先说整体思路再给关键选项这样你遇到选项差异时能自己判断。时钟树部分先把系统主频配好以F407为例典型配置是HSE 8MHz外部晶振PLL倍频到168MHzAPB2总线时钟84MHz。ADC的时钟源选择PCLK2分频注意ADC时钟不能超过芯片上限。F407的上限是36MHz所以84MHz分频4得到21MHz是稳妥的选择分频2虽然得到42MHz但超了规格不建议用。F303等高速ADC型号上限更高可以放宽选择。ADC参数配置部分ADC1、ADC2、ADC3都要使能并且配置成完全一样的参数。建议这样设置Channel选择同一个外部输入引脚对应的ADC通道三个ADC各选一路硬件上把这三个引脚短接后一起接到被测信号上Continuous ConversionDisable不能开连续转换Scan ConversionDisable因为是采集单通道不是扫描多通道Trigger选择External Timer Trigger比如 TIM1 的TRGO或TIM2的TRGOSampling Time从3个ADCCLK开始试如果信号源阻抗高再往上调Multi ADC ModeType选Triple Interleaved这个是整个配置的关键DMA部分也很关键。开启ADC1所关联的DMA请求实际由ADC1的DMA通道搬运三个ADC的所有数据传输方向设为Peripheral To Memory模式设为Circular循环模式也就是DMA buffer满了之后自动从头开始继续搬运避免频繁打断CPU。数据宽度必须特别留心因为是三重交错模式DMA传输的每个单元是32位所以Peripheral数据宽度和Memory数据宽度都要配置为Word字即32位。定时器触发源我习惯用TIM1或TIM2。以TIM2为例把TIM2的时钟配好预分频和自动重装值按你想要的采样率来算。采样率的基本思路假设TIM2输入时钟84MHz预分频84得到1MHz计数频率自动重装值N决定了触发间隔那么触发频率就是1MHz / N。有效采样率等于触发频率乘以3例如希望等效采样率6MHz那就要让定时器触发频率为2MHz即计数时钟2MHz自动重装值1。这里补充说明实际采样率还受ADC转换时间上限约束把定时器配得比ADC能承受的触发频率更快没有意义反而会让触发堆积。配置完生成代码进到代码工程里还有几件事要做。第一件启动三重ADC模式用HAL库函数HAL_ADCEx_MultiModeStart_DMA这个函数在stm32f4xx_hal_adc_ex.c里。第二件启动之前先做ADC校准代码里会对每个ADC调用HAL_ADCEx_Calibration_Start。第三件DMA中断要开我们需要在半传输和全传输完成两个时点处理数据。4. 代码实现初始化、DMA搬运与三路数据重组直接上一份可用的代码框架以F407和TIM2触发为例。初始化部分CubeMX已经生成了MX_ADC1_Init、MX_ADC2_Init、MX_ADC3_Init、MX_TIM2_Initmain函数里调用即可。额外的操作是我在用户代码区补的uint32_t adc_raw_buffer[ADC_BUFFER_SIZE]; // 单个32位字含3路结果 uint16_t waveform_buffer[ADC_BUFFER_SIZE * 3]; // 重组后的波形序列 void StartWaveformAcquisition(void) { // 1. 校准三个ADC HAL_ADCEx_Calibration_Start(hadc1); HAL_ADCEx_Calibration_Start(hadc2); HAL_ADCEx_Calibration_Start(hadc3); // 2. 启动定时器触发 HAL_TIM_Base_Start(htim2); // 3. 启动多ADC模式DMA传输 HAL_ADCEx_MultiModeStart_DMA(hadc1, adc_raw_buffer, ADC_BUFFER_SIZE); }DMA传输长度这里要特别说明HAL_ADCEx_MultiModeStart_DMA的第三个参数是“每次DMA搬运的32位字个数”。假设你最终想要一帧波形有768个点那么ADC_BUFFER_SIZE应该设为256因为每256个32位字里包含了3×256768个采样结果。搬运完成后的数据重组我用DMA的半传输和全传输中断回调来触发这样可以利用双缓冲区域CPU在处理上一半数据的同时DMA继续往下一半写整个链路不间断。uint8_t dma_flag 0; void HAL_ADCEx_InjectedConvCpltCallback(ADC_HandleTypeDef *hadc) {} void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc-Instance ADC1) { dma_flag 1; } } void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc-Instance ADC1) { dma_flag 2; } }主循环里看到flag之后把对应的半区数据重组void RebuildWaveform(uint32_t *raw, uint16_t *wave, uint16_t raw_len) { for (uint16_t i 0; i raw_len; i) { // 具体位域按芯片参考手册CDR寄存器的定义调整 wave[i * 3 0] (uint16_t)(raw[i] 0xFFFF); wave[i * 3 1] (uint16_t)((raw[i] 16) 0xFFFF); // 第三个ADC的数据位置需要查手册确认 } }不同的芯片具体位域可能不同所以注释里我强调要查参考手册确认。真实调试时我建议直接给三个ADC的输入引脚接不同的直流电平比如ADC1接1V、ADC2接2V、ADC3接3V跑一次DMA看数据落在哪个位段用实测结果来验证映射关系比盯着寄存器手册更快也更直观。另一个注意点是数据位宽。三个ADC都是12位但打包到32位字里时各自占的位段可能是16位高4位是0。如果直接把这些数据送给上位机波形会带有直流偏置。可以在重组时减去一个零点偏移推荐在信号为0时采一帧静态数据算平均值作为每个通道的零点基准后续采集都减去该基准值这样上位机看到的波形才整洁。5. 波形上行串口协议选型与更新率理论值采集到的波形数据在MCU内部反复分析当然可以但大多数人还是想把它弄到电脑上看我最常用也最推荐的做法是串口VOFA上位机。VOFA支持一种叫JustFloat的协议传输格式非常简单可靠每个数据点都是一个4字节的float一帧发送完毕后追加4个固定字节的帧尾上位机收到帧尾就知道当前帧结束然后重新解析。MCU这端的发送逻辑我一般建一个uint8_t tx_buf[4]用一个联合体做float和字节数组的转换union { float f; uint8_t b[4]; } float_conv; void SendFloatViaUart(float value) { float_conv.f value; uint8_t temp[4] { float_conv.b[0], float_conv.b[1], float_conv.b[2], float_conv.b[3] }; HAL_UART_Transmit_DMA(huart1, temp, 4); }STM32小端模式下float的低字节在前VOFA默认也按小端解析所以直接把字节顺序发出去就行。这里有个现实瓶颈必须提前算清楚串口的带宽决定了波形更新率的上限。假设你每帧发送256个float数据点每帧总数据量是256×441028字节。波特率115200时每秒约可发送11520字节换算下来大概每秒钟只能刷新11帧波特率拉到460800大约是44帧/秒如果USB转串口芯片支持上到921600可以到87帧/秒。对一般的波形观察来说44帧/秒已经基本流畅了。实际操作中还有一个更省带宽的办法把ADC的12位数据截取高8位打包成一个字节发送而不发float。这样256个点只占256字节115200波特率下也能刷到40帧以上。缺点是上位机看到的是0到255的整数需要换算成实际电压。我自己的习惯是调试早期用float便于快速看电压值定型后用8位打包换取流畅度。如果你不想用现成的VOFA自己写一个简单Python上位机也一样核心就是一个串口读取循环加matplotlib动态绘图。不过自己写要注意一点串口读数据时不能保证一次读取正好是一帧需要自己维护字节缓冲并搜索帧尾0x00 0x00 0x80 0x7F麻烦一些。VOFA把这些都处理好了我建议优先用现成的。6. 实测现场几种典型配置的波形效果与坑位清单理论说得再漂亮最终还是得看示波器波形和上位机实际显示效果。我实测了三种配置都是F407核心板信号源用DDS输出50kHz正弦波三个ADC输入短接后经一级运放跟随接到信号源。第一种配置ADC时钟21MHz、采样周期3TIM2触发到等效采样率约4.06Msps。上位机显示50kHz波形时每个周期约81个采样点波形肉眼看起来很平滑幅度一致性不错FFT显示主频旁边没有明显杂散。第二种配置把采样周期从3提到8等效采样率掉到约3.06Msps。每个周期61个点波形依然能看但高次谐波处噪声略有减少。这个现象其实有讲究采样周期短时ADC内部采样电容充电时间不够信号源阻抗较大会导致采样值偏低出现“幅度下降”的假象。所以波形对幅度精度要求高时优先保证采样电容充满只要关注高频波形轮廓时才用短采样周期拉高采样率。第三种配置我把运放跟随去掉DDS输出直接接三个ADC引脚波形边缘立刻出现大量毛刺幅度也有几个LSB的跳动。原因很直接三个ADC的采样电容并联到同一路输入相当于一个额外的容性负载信号源驱动能力不足时电压建立不上来。这解释了为什么三重ADC对模拟前端的要求比单ADC高——不是芯片设计有问题而是你的信号源得多抗一点负载。电平转换和阻抗匹配是这类项目不能省的一环。接着排几个我真实踩过的坑按出现频率排序。第一个坑DMA缓冲长度填错导致波形错乱。有人把256误填成768以为“三个ADC所以乘3”结果DMA超界数据还没取完就进入了错误的缓冲区地址。记住HAL函数里的Length一直是32位字的个数不是总点数。第二个坑开了连续转换模式。三重ADC模式下如果同时使能了连续转换定时器触发就没意义了ADC会自己不停地转换采样率失去控制上位机看到的波形频率和实际完全对不上。CubeMX里务必把Continuous Conversion设为Disable让一切节奏交给定时器。第三个坑不校准直接采集。ADC的偏移误差和增益误差在不校准时可能带来几十个LSB的输出偏差波形整体抬升或下移。校准函数一定要在启动DMA之前调用而且要确保ADC时钟稳定后再校准。第四个坑中断回调里做耗时处理。有人图省事直接在HAL_ADC_ConvCpltCallback里把768个点的波形通过串口发出去。实测下来在6Msps采样率下你这个回调执行完之前DMA已经把新数据覆盖进来了最终看到的波形是前后帧混在一起的残影。正确做法是回调里只置标志位数据处理和发送全部放在主循环中做或者用DMA的双缓冲配合乒乓处理。第五个坑忘了确认型号是否支持三重模式。我身边真有朋友拿着F103C8T6配了半天最后发现F1连三重模式的选项都没有。出发前先看参考手册真的能省下半天时间。讲到这里这套“STM32HAL三重ADC采集波形”的完整链路已经全部打通了。我再多啰嗦一句个人体会三重模式的价值不只是采样率翻三倍它真正救急的地方在于当你手头只有一块普通MCU却要临时观察一路中等频率的模拟信号时不需要立刻上外部高速ADC芯片靠现有资源就能把事办了。当然它不是万能的超过10MHz的信号或者对多通道同步要求非常苛刻的场景依然得老老实实上专用ADC和FPGA。但就日常嵌入式调试而言这套方案的可复制性、性价比和上手难度都控制得相当好值得一试。本文还有配套的精品资源点击获取