
1. 既然要量电压先搞懂ADC到底在干什么1.1 逐次逼近ADC的工作方式STM32C562做ADC电压采集很多初学者第一步就打开CubeMX点两下然后调HAL_ADC_GetValue拿到一个0到4095的数字就不知道下一步怎么办了。这里面的关键问题在于如果不理解逐次逼近ADC的工作方式后面遇到的采样值跳变、电压不准、通道错位你根本无从下手。逐次逼近型ADC也就是常说的SAR ADC核心思路是“用二分法猜电压”。内部有一个比较器一个DAC一个逻辑控制器。启动转换后先假设输入电压等于最高参考电压的一半把这个值送给DAC再和外部输入电压比较。如果输入比假设值大就把当前位保留为1如果小就清零。然后继续猜下一位一直猜到最低位。12位ADC就是猜12次每次都能把范围缩小一半。这里有个容易忽略的点SAR ADC在真正“猜”之前还有一个采样阶段。采样开关闭合外部信号给内部采样电容充电采样时间必须足够长电容电压才能跟上输入信号。如果信号源的输出阻抗太大或者采样周期设置太短电容还没充满就进入比较阶段后边猜出来的数字就偏低。这就是很多人常说的adc建立时间问题。很多情况下我们用STM32C562采集电位器电压时电位器阻值达到几十千欧这时候如果采样周期选得太短读到的值会明显低于实际值。1.2 STM32C562的ADC外设特点STM32C562内部的高精度ADC是逐次逼近结构支持单端输入、多通道扫描可以通过DMA请求把结果批量搬到内存。这类外设的配置思路在STM32全系列里都是相通的差别主要在通道编号、引脚复用、时钟分频和校准寄存器。所以我在实际项目里通常以参考手册的ADC章节为准再配合CubeMX生成的初始化代码去核对。C562的ADC一般会带以下几种资源外部通道采集外部模拟信号内部通道采集VREFINT内部参考电压、温度传感器和Vbat。VREFINT特别实用后面讲电压校准时会用它反推实际的VREF电平。片内温度传感器虽然精度一般但做温度趋势监测足够了。另外一个值得注意的功能是自动校准。ADC启动前最好做一次校准用来消除内部电容失配和输入偏移带来的误差。这个校准在HAL库里一句话就能调用很多人的代码里漏掉了它结果零点漂移了几十个LSB还在那边纠结硬件电路。2. CubeMX配置与HAL库初始化2.1 最小硬件连接我调试STM32C562 ADC电压采集时最常用的测试电路是一个10k欧姆电位器两端分别接3.3V和GND中间滑动端接到ADC输入引脚。这样拧电位器就能在0到3.3V范围内得到一个连续变化的模拟电压非常适合验证软件逻辑。这里有个实际经验电位器阻值如果是几十千欧建议在ADC引脚对地加一个100nF电容组成简单的RC低通滤波。这个电容能滤掉一部分空间耦合进来的高频噪声也能补偿一部分采样瞬间的电荷冲击。但电容也不能加得太大否则信号建立时间会被拉长遇到快速变化的电压时会造成滞后。如果采集的是传感器输出还要先确认传感器输出阻抗。STM32C562的ADC内部采样电容大概在几皮法量级等效输入阻抗不是无穷大信号源输出阻抗过高会直接影响采集精度。稳妥的做法是在电路上用一个运放做电压跟随用低输出阻抗去驱动ADC输入。2.2 在STM32CubeMX里把ADC配起来CubeMX的配置路径很直观在Pinout视图中找到对应的ADC通道设置为Analog模式然后在左侧Analog栏里打开ADC1配置。分辨率直接选12位这是大多数应用最常用的模式。扫描模式这里要注意如果只采一个通道就把扫描模式关掉如果后面要多通道采集再打开。连续转换模式也分情况用DMA持续采样时最好打开用定时器触发单次转换时可以关掉。采样周期的选择是新手容易忽略的关键项。CubeMX里下拉列表会列出好几个档位比如1.5、7.5、13.5、28.5、41.5、71.5、239.5等采样周期数。我自己的习惯是如果信号源输出阻抗不高、信号本身比较干净选中档采样周期就够用如果信号源内阻较大或者噪声明显选较长的档位会更稳。千万别一上来冲着“高速采样”选最短采样周期否则采集精度会打折扣。时钟分频也要注意。ADC时钟太高时逐次逼近比较器工作时间不足精度下降。CubeMX里配置ADC的时钟分频时可以打开Clock Configuration视图确认ADC时钟频率。通常把ADC时钟压到14MHz左右是一个稳妥的选择既能保证转换速度也能保证精度。2.3 生成的HAL代码怎么看CubeMX生成完后打开adc.c文件里面的MX_ADC1_Init函数就是所有初始化逻辑。我建议不要只看不思考要把这个函数和参考手册的寄存器对应起来。初始化过程中有几个关键步骤首先是配置ADC_InitTypeDef里的分辨率、扫描模式、连续转换模式、数据对齐方式、外部触发方式。然后是配置ADC_ChannelConfTypeDef这里有Rank、Channel和SamplingTime。Rank表示扫描顺序中的位置Channel表示哪个通道SamplingTime就是前面说的采样周期。校准代码出现在初始化之后主循环之前。HAL库自带HAL_ADCEx_Calibration_Start函数需要在ADC启动前执行。如果我看到有人采集不准第一反应就是看有没有调用这个函数。很多时候加一行校准代码零点误差能明显改善。接下来是启动转换和读取结果的三种方式。轮询方式用HAL_ADC_Start、HAL_ADC_PollForConversion、HAL_ADC_GetValue中断方式用HAL_ADC_Start_IT在回调函数里收数据DMA方式用HAL_ADC_Start_DMA数据直接放在内存数组里。单通道、简单测试用轮询没问题但实际工程里推荐DMA。3. 把原始值变成真正电压换算与校准3.1 最简单的线性换算为什么不准拿到原始ADC值后最常见的换算是电压值 ADC原始值 * VREF / 4096如果VREF正好等于3.3V那这个公式算出来的数值在大多数情况下看着像那么回事。但这个公式隐含了一个假设参考电压稳定且精确等于3.3V。实际板子上VREF引脚可能直接连到了LDO输出而LDO本身有2%到3%的误差负载变化时还有纹波和压降。这时候你的电压值其实是跟着电源在漂的。另外ADC自身的零点误差、增益误差和温漂也会影响结果。逐次逼近ADC虽然线性度不错但绝对精度并不算高。如果想用手里的STM32C562做一个相对准确的电压表不能只靠公式硬算需要做校准。3.2 用内部参考电压VREFINT校准STM32C562内部有一个相对稳定的参考电压通常叫做VREFINT典型值在1.2V左右。它的绝对值存在片内的校准寄存器里芯片出厂时会写入实际值。校准原理很简单让ADC读一下VREFINT通道得到一个原始值vrefint_raw。因为VREFINT本身是固定电压所以有VREFINT典型值 vrefint_raw * VREF实际值 / 4096等号右边是ADC的采样公式。整理一下就能反推当前VREF实际值VREF实际值 VREFINT典型值 * 4096 / vrefint_raw有了这个实际VREF值再去换算外部通道的电压就准多了。这也就是我经常说的“先量一下尺子长度再用这把尺子去量东西”。具体代码可以这样做#define VREFINT_TYPICAL_MV (1200.0f) float get_actual_vref_mv(void) { uint16_t vrefint_raw 0; if (HAL_ADC_Start(hadc1) ! HAL_OK) { Error_Handler(); } if (HAL_ADC_PollForConversion(hadc1, 100) ! HAL_OK) { Error_Handler(); } vrefint_raw HAL_ADC_GetValue(hadc1); HAL_ADC_Stop(hadc1); if (vrefint_raw 0) { return 3300.0f; } return VREFINT_TYPICAL_MV * 4096.0f / vrefint_raw; } float get_adc_voltage_mv(uint16_t raw, float vref_mv) { return (float)raw * vref_mv / 4096.0f; }注意VREFINT典型值要从具体型号的数据手册里查我这里写的是很多STM32通用的1200mVC562到底是不是这个值建议你打开对应数据手册确认。另外VREFINT通道是内部通道不需要外部硬件直接配置成对应的内部通道并启动转换就行。3.3 外部ADC三点校准思路如果你做的项目对电压精度要求到0.1%级别只靠芯片内部VREFINT做些补偿可能还不够。这时候可以引入外部标准电压源对整条ADC测量链路做多点校准。这个方法不仅适用于STM32C562也适用于HX711、ADS1232、MCP3561等外部ADC甚至可以用于那些24bit高精度ADC的线性度校正。所谓三点校准就是分别用两个标准电压源和GND测量点拿到三个点的实际ADC原始值然后建立实际电压和原始值之间的关系。如果ADC线性度足够好两点校准就能解决增益误差和零点误差增加第三点可以校正一部分非线性。校准步骤并不复杂。第一点是零电压把ADC输入接地记录原始值offset第二点是接近满量程的标准电压比如2.500V记录原始值gain_raw第三点放中间值比如1.250V记录mid_raw。然后根据三点偏差在软件里做分段线性插值。两点校准的增益系数可以这样算增益系数 (标准电压2 - 标准电压1) / (原始值2 - 原始值1)实际电压 (当前原始值 - 原始值1) * 增益系数 标准电压1三点校准则是在这个基础上判断当前原始值落在哪个区间再用对应区间的增益系数计算。这个方法很灵活哪怕你用的外部ADC不是高精度的也能把整条链路的系统误差纠正掉。4. 多通道、DMA与连续采样4.1 为什么单次阻塞不够用很多初学代码长这样HAL_ADC_Start(hadc1); HAL_ADC_PollForConversion(hadc1, 100); uint16_t value HAL_ADC_GetValue(hadc1); HAL_ADC_Stop(hadc1);这段代码在单通道、低速场景确实能用。但一旦要采集四路电压还要反馈给控制逻辑再逐个轮询等待转换完成CPU会被白白占用。更麻烦的是如果需要等间隔采样主循环里其他任务稍微一忙采样间隔就会抖直接影响信号分析结果。实际工程里我更喜欢DMA方式。ADC转换完成后由DMA自动把结果搬到内存数组CPU根本不用管中间过程。等DMA的传输完成中断触发后主程序直接取数组里的数据去处理就行。4.2 多通道DMA正确配置在CubeMX里做多通道ADC的关键是扫描顺序和DMA缓冲区大小。配置多通道时需要在Configuration面板里给每个通道设置Rank。Rank1对应DMA缓冲区里的第一个数据Rank2对应第二个数据。我调试时遇到过通道数据错位的问题后来排查发现就是Rank配置顺序和程序里取数据的顺序不一致。DMA配置需要注意数据长度。如果配置了4个通道连续转换一轮会产生4个数据。DMA的缓冲区大小最好是通道数的整数倍比如4个通道就开一个长度为64的数组这样DMA会连续存下16轮转换结果再用半传输完成中断或传输完成中断去读取。代码可以这样写#define ADC_CH_NUM 4 #define ADC_BUF_LEN (ADC_CH_NUM * 16) uint16_t adc_buf[ADC_BUF_LEN]; void start_multi_channel_adc(void) { HAL_ADC_Start_DMA(hadc1, (uint32_t *)adc_buf, ADC_BUF_LEN); }这样配置后DMA会自动把4个通道的数据循环填入数组。主循环里可以根据自己写的索引解析各个通道的值。注意如果开启了连续转换DMA会一直搬运数据不要用“在缓冲区里只采一次”的思路去理解它。4.3 采样周期与转换速率权衡很多人搞不清ADC采样周期到底会影响什么。ADC完成一次转换需要的时间是转换时间 (采样周期数 转换周期数) / ADC时钟频率对于12位逐次逼近ADC转换周期数一般是12.5个ADC时钟周期。如果ADC时钟是14MHz采样周期选41.5个周期一次转换就是(41.5 12.5) / 14MHz约等于3.86微秒。理论上每秒可以完成约26万次转换。但注意多通道扫描时每个通道都要消耗一次转换时间所以4个通道每轮转换时间大约15微秒也就是每秒约6.6万轮。在实际项目里我做的是32点滑动平均加10Hz采样速率这种情况下ADC转换速度根本不是瓶颈。反而应该把采样周期调长一点换取更好的精度。如果你要采集的是音频或者快速变化的信号那才需要追求短采样周期和高转换速率。4.4 代码多通道DMA采集配合前面讲的配置我习惯在主循环里每100毫秒读取一次当前最新数据。因为DMA是连续搬运的所以数组里的数据时刻在更新。在DMA传输完成中断里把当前的半缓冲区标志记录下来再在后台做数据处理。一个比较简单可靠的方案是void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc-Instance ADC1) { // 这里解析最新一轮数据 // 例如前4个数组元素是4个通道的最新值 channel0_value adc_buf[0]; channel1_value adc_buf[1]; channel2_value adc_buf[2]; channel3_value adc_buf[3]; } }当然这种直接读数组的方式在极端情况下可能和DMA搬运存在竞争但STM32CPU读取32位或16位内存对齐数据时通常不会出现撕裂问题。如果对数据一致性要求高可以用双缓冲方案或者用DMA的半传输和全传输中断分别处理两段数据区。这个思路在触摸采样和无刷电机相电流采样里很常见。5. 软件滤波与信号质量问题5.1 典型干扰与信噪比ADC采集到的信号不可能像理论电压那样干净。最常见的噪声源有三个电源纹波、数字电路开关噪声、引脚间的串扰。STM32C562运行时GPIO翻转、通信接口收发数据都会在芯片内部和PCB走线上产生瞬态电流这些电流最终会在电源平面上形成噪声并被ADC采样到。遇到ADC值跳变时我一般先用示波器看引脚上的实际波形如果波形本身就有毛刺那就别指望软件滤波能完全解决。从ADC信噪比角度看12位ADC的理论动态范围大约74dB但实际应用里受噪声影响有效位数往往达不到12位。提高信噪比的路径有两条一是降低噪声二是软件滤波。降低噪声靠硬件软件滤波解决的是随机噪声和工频干扰。5.2 C语言ADC滤波函数我常用的滤波函数有三种滑动平均滤波、中位值滤波、限幅消抖滤波。滑动平均滤波适合数据变化较为平缓的电压采集。维护一个环形缓冲区每来一个新值替换掉最旧的值然后求平均。代码实现很简单#define FILTER_SIZE 32 static uint16_t filter_buf[FILTER_SIZE]; static uint8_t filter_index 0; static uint32_t filter_sum 0; uint16_t adc_sliding_average(uint16_t new_value) { filter_sum - filter_buf[filter_index]; filter_buf[filter_index] new_value; filter_sum new_value; filter_index (filter_index 1) % FILTER_SIZE; return (uint16_t)(filter_sum / FILTER_SIZE); }中位值滤波对脉冲干扰特别有效。连续采样5到7次排序后取中间值。缺点是排序需要一点时间但对低速采集完全够用。更推荐的做法是中位值平均滤波先去掉几个最大值和最小值再把剩余值求平均兼顾抗脉冲和抑制随机噪声。限幅消抖滤波的原理是如果当前值和上一次值的差值超过设定阈值就认定是干扰丢弃这次数据。这个方法适合需要保护执行机构的场景比如温度控制里突然跳变10度大概率是干扰。5.3 硬件层面的实用手段软件滤波再强也不能替代基本的硬件处理。我给STM32C562做ADC电压采集时硬件上会注意几点。第一模拟电源和数字电源尽量分开如果芯片有独立的VDDA引脚一定要在VDDA和GND之间加足够的去耦电容通常0.1uF和1uF并联。第二输入走线尽量短远离PWM输出和通信线实在避不开就用地线包围起来。第三VREF引脚必须干净参考电压是整个ADC的“尺子”它一旦晃动所有通道都会不准。有的板子为了省成本把VREF直接和3.3V电源连在一起这时候再用内部VREFINT校准能补救一部分误差。如果项目对精度要求高最好单独用基准电压芯片给VREF供电比如常见的2.5V或3.0V基准源。6. 实际踩坑记录与排查6.1 读到的ADC值一直是0或者满量程这类问题排查起来先分两个方向配置问题和电路问题。配置方面先检查CubeMX里有没有把引脚设置成Analog模式。如果引脚默认是GPIO输出或者复用功能ADC通道根本采集不到正常电压。再检查ADC通道配置和你实际接线的引脚是否一致很多人改板子换了引脚代码里还保留原来的通道号结果读数固定不变。电路方面如果ADC值固定为0大概率是输入被拉到GND了看看是不是板子上有焊锡桥接或者引脚配置成了输出模式把电平拉低。如果ADC值固定为4095先测一下引脚对地电压如果引脚确实有电压但ADC读满量程很可能是参考电压配置不对比如选择了2.5V基准但外部输入是3.3V或者通道配置成了内部参考通道。6.2 多通道数据错位多通道采集最经典的问题就是“通道数据串了”。明明采集的是通道1处理出来的数据却像是通道2的。这个问题的根源通常是Rank配置和DMA缓冲区索引对不上。比如你在CubeMX里把通道7配置成了Rank1通道1配置成了Rank2但程序里却用old_channel_value[0]去接收通道1的数据那自然全错。还有一种情况是开启扫描模式后DMA缓冲区长度小于通道数。这样会发生缓冲区溢出覆盖得到的数据时序完全乱掉。多通道下DMA的BufferSize一定要是通道数的倍数并且根据你的需求设置一个合适的安全余量。6.3 采样值跳动大采样值跳动大先看是不是电信号本身就有纹波。用示波器看输入引脚如果已经有毛刺那就不是软件算法能完全解决的。先把100nF滤波电容加上把采样周期调长再用中位值平均滤波通常能处理掉大部分问题。另一个容易被忽略的是ADC时钟频率过高。如果ADC时钟设置得太高片内开关电容建立不充分会出现采样值整体偏大且抖动严重。这时把ADC时钟降一档采样周期调大一档很多问题就消失了。还有一个经验如果ADC值只在某个固定区间反复跳比如总是在400到420之间来回跳可能是参考电压有周期性纹波优先检查电源去耦电容。6.4 什么时候该外扩高精度ADCSTM32C562的内部ADC做常规电压采集、电池检测、电位器采样完全够用。但如果项目是做电子秤、热电偶或高精度传感器需要到16bit甚至24bit分辨率那就别再纠结内部ADC的极限性能直接外扩ADC芯片更省心。常见的外部ADC有HX711、ADS1232、MCP3561等。HX711适合应变片桥式传感器ADS1232是低噪声桥式传感器ADCMCP3561则支持SPI接口采样速率和可配置性更强。外扩ADC时前面的校准思路照样适用而且高分辨率ADC对参考电压和PCB布局更敏感电源处理必须比内部ADC更细致。我在实际项目里通常先列一个精度预算表把传感器误差、参考电压误差、ADC量化误差和软件滤波误差全部加起来看最终能否满足系统要求。如果差得不多通过三点校准就能解决如果内部ADC的有效位数差得太远才考虑外扩方案。这样才能把成本花在刀刃上。最后再说一点个人体会。ADC电压采集做到后面真正的难题不是初始化而是把原始值变成可信电压值的全过程。校准做得越主动后面系统联调时就越省心。ST官方在应用笔记里也强调ADC的精度受参考源、采样时间、布局走线影响很大。这些坑我在STM32C562上踩过也在别的MCU上踩过分享出来就是希望大家少走弯路。你拿到板子之后先别急着看数据先按上面的步骤检查一下参考电压和校准再开始采集基本能一路顺下来。