
1. 为什么我要用50块钱做一套ADC采集实战先说结论这套东西我前后做了三版第一版烧了一块芯片第二版数据跳得没法看第三版才稳定下来。总物料成本控制在50元以内核心是一块STM32F103C8T6最小系统板配合CubeMX生成工程重点解决的是ADC采集里最容易被忽略的两个问题——输入缓冲和参考电压稳定性。很多人入门嵌入式第一步点灯第二步串口打印第三步就开始调ADC。但真正把ADC采出来的数据用到项目里你会发现事情没那么简单。传感器输出阻抗高一点采出来的值就飘电源纹波大一点12位ADC的后几位基本是噪声DMA配置稍微不对数据就错位。这些问题在教程里往往一笔带过但在实际项目里每一个都能让你调一整天。这篇内容适合谁看如果你刚学完GPIO和串口想找一个能真正跑通、能复现、能理解背后原理的ADC实战项目那这套方案就是给你准备的。如果你已经做过ADC采集但数据一直不稳定也可以看看我在缓冲设计和参考电压处理上的做法。整套方案不依赖昂贵的开发板不需要示波器也能调50块钱的物料清单我放在下面。物料型号/规格参考单价主控板STM32F103C8T6最小系统板12元下载器ST-Link V215元电位器10kΩ多圈精密电位器3元电容0.1uF、10uF各若干2元电阻1kΩ、10kΩ若干2元面包板杜邦线半尺寸面包板8元传感器光敏电阻或NTC热敏电阻5元USB线Micro USB3元合计大约50元。如果你手头已经有ST-Link和面包板实际支出会更低。这套配置的好处是所有元件都是通用件坏了随时能换不会因为一块专用开发板停产就卡住。2. 整体设计思路与方案选型2.1 为什么选STM32F103C8T6而不是其他芯片STM32F103C8T6这颗芯片在嵌入式入门圈子里几乎是绕不开的。72MHz主频、64KB Flash、20KB RAM、12位ADC、DMA支持这些参数放在今天看不算亮眼但它的资料丰富度和工具链成熟度是其他同价位芯片比不了的。CubeMX对它的支持非常完整HAL库的ADC驱动也足够稳定。我对比过几款替代方案。ESP32的ADC线性度一般而且WiFi功能对纯ADC采集来说是干扰源GD32虽然引脚兼容但ADC的校准逻辑和STM32有差异新手容易踩坑合泰、松翰这类芯片更偏向消费电子开发环境不够友好。所以最终锁定F103C8T6不是因为它最强而是因为它最适合入门阶段把注意力放在ADC本身而不是折腾工具链。2.2 ADC采集的核心难点在哪里很多人以为ADC就是“读一个寄存器得到一个数字”。实际不是。ADC采集的精度受四个因素影响参考电压的稳定性、输入信号的阻抗匹配、采样保持电路的建立时间、数字部分的噪声耦合。参考电压如果直接取MCU的3.3V供电那电源上的纹波会直接反映到采集结果里。F103的VDDA和VDD虽然内部有连接但如果你用USB供电电脑USB口的电压波动能到几十毫伏12位ADC在3.3V量程下的分辨率是0.8mV这意味着电源波动直接吃掉了几十个LSB。输入阻抗的问题更隐蔽。F103的ADC输入等效为一个采样电容加开关采样时间不够或者信号源内阻太大采样电容充不满读出来的值就偏低。数据手册里给的公式是采样时间 ≥ (R_source R_ADC) × C_ADC × ln(2^12)。R_ADC大约1kΩC_ADC约8pF如果你用10kΩ的电位器直接接ADC引脚采样时间至少要设到几十个周期才够。2.3 缓冲设计的两种思路缓冲设计是这套方案的核心。我试过两种做法运放电压跟随器和RC低通滤波加阻抗变换。运放跟随器效果最好但需要额外一颗运放芯片比如LM358或者轨到轨的MCP6001成本增加两三块钱而且需要双电源或者单电源偏置对新手来说接线复杂度上去了。我第一版就是用LM358做的跟随器结果单电源供电下输出摆幅不够靠近0V和3.3V的区域非线性严重。第二版改用RC低通加直接采集在电位器输出和ADC引脚之间串一个1kΩ电阻再对地接一个0.1uF电容。这个RC网络的时间常数是100微秒截止频率约1.6kHz对于大多数慢变信号足够用。关键是这个1kΩ电阻把信号源阻抗“隔离”了一部分配合ADC内部采样电容建立时间明显改善。实测下来同样的电位器加RC之后跳动从±15个LSB降到±3个LSB。注意RC滤波的电阻不能太大否则采样时间要设得很长。1kΩ到10kΩ之间比较合适电容用0.1uF的陶瓷电容不要用电解电容电解电容的等效串联电阻和漏电流会影响精度。3. CubeMX配置与ADC参数详解3.1 时钟树配置与ADC时钟来源CubeMX里第一步是配时钟。F103的ADC时钟来自APB2经过分频最大不能超过14MHz。系统时钟72MHz时APB2也是72MHzADC预分频器选6分频得到12MHz这是最常用的配置。如果你选4分频ADC时钟18MHz超过手册上限采集结果会明显变差这个坑我踩过数据跳得莫名其妙查了半天才发现是时钟超了。在Clock Configuration页面确认ADC Prescaler是/6然后ADC的输入时钟显示12MHz。这个12MHz决定了采样时间的单位一个ADC时钟周期约83纳秒。3.2 ADC模式选择独立模式还是扫描模式这套方案只用了一个ADC通道所以选Independent模式不开启Scan。如果你后面要扩展多通道可以开Scan加DMA但单通道先用最简单的配置把链路跑通。关键参数在Parameter Settings里Resolution12位不用改Data AlignmentRight alignment右对齐读出来的值直接就是0到4095Scan Conversion ModeDisabledContinuous Conversion ModeEnabled这样ADC转换完自动开始下一次不需要软件反复触发Discontinuous Conversion ModeDisabledExternal Trigger Conversion SourceRegular Conversion launched by software先用软件触发后面可以改成定时器触发采样时间我设的是71.5个周期这是F103可选的最长采样时间之一。为什么设这么长因为我的信号源经过RC滤波后等效阻抗大约1kΩ加上ADC内部1kΩ总阻抗2kΩ采样电容8pF时间常数16纳秒理论上1个周期就够。但实际PCB或者面包板上会有寄生电容和分布参数采样时间留足余量能显著改善稳定性。71.5个周期在12MHz下约6微秒对于慢变信号完全够用。3.3 DMA配置与数据搬运虽然单通道可以用轮询读但我建议直接上DMA。原因有两个一是DMA搬运不占CPU你可以同时做其他事情二是DMA能保证数据在转换完成后立刻被取走不会因为CPU延迟导致数据被覆盖。CubeMX里在DMA Settings页添加一个DMA请求ADC1对应DMA1 Channel1。模式选Circular这样DMA会自动循环填充缓冲区。数据宽度选Half Word因为ADC是12位用16位变量存刚好。缓冲区大小我设的是64也就是一个64个元素的uint16_t数组。这个大小不是随便定的。太小了你读的时候容易读到正在被DMA写的区域太大了数据处理延迟高。64个样本在12MHz ADC时钟、71.5周期采样时间下大约每6微秒一个样本64个样本约384微秒刷新一轮。对于慢变信号这个刷新率足够同时CPU有充足时间处理。提示DMA的Circular模式和Normal模式的区别在于Circular模式下DMA搬完一轮自动回到起点继续搬不需要软件重新使能。ADC连续转换加DMA Circular是整个链路最省心的组合。4. 硬件电路搭建与缓冲设计实操4.1 最小系统板的供电处理F103C8T6最小系统板通常通过Micro USB或者ST-Link供电。如果你用ST-Link的3.3V输出供电要注意ST-Link的3.3V输出电流有限大概100mA左右带最小系统板加几个LED没问题但如果你外接传感器或者显示屏最好单独供电。我在VDDA和VSSA之间并了一个10uF的钽电容和一个0.1uF的陶瓷电容。VDDA是ADC的模拟供电引脚虽然最小系统板上通常已经接了滤波电容但如果你用的是廉价板子这个电容可能省掉了。加这两个电容之后ADC读数的底噪明显降低。实测不加电容时输入端短接到地读数在0到5之间跳加了之后读数稳定在0到1之间。4.2 电位器分压电路与RC缓冲电位器接法很简单两端分别接3.3V和GND中间抽头接ADC输入。但直接接和加RC接效果差别很大。直接接的时候电位器抽头的输出阻抗最大可以到2.5kΩ10kΩ电位器旋到中间时上下各5kΩ并联。这个阻抗对ADC来说偏高采样电容充电时间不够读数会偏低且跳动。加RC缓冲的接法电位器抽头先串一个1kΩ电阻然后对地接一个0.1uF电容电容上的电压再接到ADC引脚。这个1kΩ电阻和0.1uF电容组成低通滤波截止频率约1.6kHz。同时电容的低阻抗特性为ADC采样电容提供了瞬时充电电流等效信号源阻抗大幅降低。实测数据对比接法读数跳动范围LSB线性度误差直接接±15约2%加RC缓冲±3约0.5%这个改善是肉眼可见的。如果你用光敏电阻或者热敏电阻它们的阻抗更高有的能到几十kΩ不加缓冲根本没法用。4.3 参考电压的处理F103的参考电压默认是VDDA也就是3.3V。如果你对精度要求高可以外接一个REF3033之类的基准芯片输出3.3V基准成本大概五六块钱。但50元预算下我用了一个折中方案在VDDA引脚附近加一个TL431做的简易基准或者直接用一颗低压差稳压器把5V稳到3.3V给VDDA单独供电。实际测试中如果用USB供电VDDA的波动大约在3.28V到3.32V之间对应12位ADC的满量程误差约0.6%。这个误差对于大多数入门项目够用了。如果你要做电池电压检测或者精密测量那还是建议上外部基准。注意F103的VREF和VDDA在芯片内部是分开的引脚但最小系统板上通常把VREF和VDDA连在一起了。如果你要外接基准需要把VREF的连线断开单独接基准输出。这个操作需要一点焊接功底。5. 代码实现与数据处理5.1 CubeMX生成的ADC初始化代码解读CubeMX生成的MX_ADC1_Init()函数里关键配置都在hadc1.Init结构体里。我挑几个容易出问题的地方说。hadc1.Init.ScanConvMode ADC_SCAN_DISABLE;单通道必须关扫描开了扫描但只配了一个通道行为不确定。hadc1.Init.ContinuousConvMode ENABLE;连续转换模式ADC一次转换结束后自动开始下一次。配合DMA Circular整个采集链路自动运行。hadc1.Init.DiscontinuousConvMode DISABLE;这个模式用于多通道分组转换单通道用不上。hadc1.Init.ExternalTrigConv ADC_SOFTWARE_START;软件触发在main函数里调用HAL_ADC_Start_DMA()之后ADC就开始连续转换。hadc1.Init.DataAlign ADC_DATAALIGN_RIGHT;右对齐读出来的值直接是0到4095。hadc1.Init.NbrOfConversion 1;转换序列长度单通道就是1。5.2 DMA缓冲区设计与数据读取DMA缓冲区我定义了一个64元素的数组#define ADC_BUFFER_SIZE 64 uint16_t adc_buffer[ADC_BUFFER_SIZE];在main函数里启动ADC和DMAHAL_ADC_Start_DMA(hadc1, (uint32_t*)adc_buffer, ADC_BUFFER_SIZE);这行代码执行后ADC开始连续转换每转换完一个数据DMA自动把它搬到adc_buffer里搬满64个之后自动回到起点继续搬。整个过程不需要CPU干预。读取数据的时候我建议不要直接读adc_buffer的某一个元素因为DMA可能在任意时刻更新它。稳妥的做法是读两次或者用一个中间变量uint32_t adc_sum 0; for (int i 0; i ADC_BUFFER_SIZE; i) { adc_sum adc_buffer[i]; } uint16_t adc_avg adc_sum / ADC_BUFFER_SIZE;这样得到的是64个样本的平均值能有效抑制随机噪声。如果你需要更快的响应可以只取最近16个样本平均或者用滑动平均滤波。5.3 从原始值到物理量的转换ADC读出来的是0到4095的整数要转换成电压或者物理量需要做两步计算。第一步转电压float voltage adc_avg * 3.3f / 4095.0f;这里用4095而不是4096因为12位ADC的满量程是4095。用4096会引入一个LSB的固定误差。第二步根据传感器特性转物理量。比如光敏电阻它的阻值和光照强度是对数关系你需要先算出电阻值再查表或者用公式转换。NTC热敏电阻可以用Steinhart-Hart公式float resistance 10000.0f * voltage / (3.3f - voltage); float temperature 1.0f / (1.0f/298.15f log(resistance/10000.0f)/3950.0f) - 273.15f;这个公式里的3950是NTC的B值不同型号不一样用之前要查手册。提示浮点运算在F103上没有硬件FPU是软件模拟的速度较慢。如果你对实时性要求高可以把公式里的常数预先算好用定点数运算代替。但对于慢变信号浮点完全够用。6. 常见问题与排查技巧实录6.1 ADC读数跳动大怎么办这是最常见的问题。排查顺序如下第一检查参考电压。用万用表量VDDA引脚对GND的电压如果波动超过10mV先解决电源问题。加电容、换LDO、或者单独给VDDA供电。第二检查信号源阻抗。如果你直接接电位器或者热敏电阻先加RC缓冲。1kΩ加0.1uF是最小配置效果立竿见影。第三检查采样时间。CubeMX里把采样时间设到71.5周期或者239.5周期看看跳动是否改善。如果改善明显说明之前采样时间不够。第四检查DMA配置。如果DMA没开Circular搬完一轮就停了你读到的可能是旧数据。确认DMA模式是Circular。第五检查地线。面包板上的地线如果太长太细数字部分的开关噪声会耦合到模拟地。把ADC相关的地线单独走最后单点接到主地。6.2 读数始终为0或者4095读数卡在0或者4095通常是硬件连接问题。先量ADC输入引脚的电压如果电压正常但读数不对检查CubeMX里的通道配置是否和实际引脚一致。F103C8T6的ADC1_IN0对应PA0IN1对应PA1以此类推。如果你接的是PA0但CubeMX里配的是IN1读出来就是错的。还有一种可能是ADC没有启动。确认HAL_ADC_Start_DMA()被调用了并且返回值是HAL_OK。如果返回HAL_ERROR可能是DMA配置冲突或者时钟没使能。6.3 DMA数据错位DMA数据错位表现为缓冲区里的数据顺序乱了或者一半是旧数据一半是新数据。原因通常是DMA缓冲区和ADC转换速率不匹配。如果你在DMA搬送过程中读取缓冲区可能读到半新半旧的数据。解决办法是使用双缓冲或者读两次取一致值。HAL库支持DMA双缓冲模式但配置稍复杂。简单做法是读两次如果两次结果差异小于某个阈值就认为数据有效。6.4 常见问题速查表现象可能原因排查方法解决措施读数跳动±15LSB信号源阻抗高加RC缓冲1kΩ0.1uF读数卡0或4095通道配置错误核对引脚和CubeMX配置改通道号数据错位DMA读取冲突读两次比较用双缓冲或平均线性度差参考电压不稳量VDDA电压加LDO或基准采样值偏低采样时间不足增大采样周期设71.5或239.5周期DMA不搬运DMA未使能查HAL返回值重新配置DMA6.5 几个我踩过的坑第一个坑CubeMX里ADC的GPIO模式要选Analog不能选Input。选错了读出来一直是0。这个在GPIO配置页面容易忽略。第二个坑如果你用ST-Link供电同时USB也插着两个电源会打架。虽然一般不会烧但ADC读数会异常。只保留一个供电来源。第三个坑面包板的接触电阻。有些廉价面包板用久了插孔接触不良ADC读数会随振动变化。如果你怎么调都不稳换一块面包板试试。第四个坑HAL库的HAL_ADC_Start_DMA()在连续转换模式下只需要调用一次。如果你在while循环里反复调用DMA会反复重启数据反而乱。7. 从这套方案延伸出去还能做什么这套ADC采集框架跑通之后你可以往上叠很多东西。比如加一个OLED显示屏把电压值实时显示出来加一个SD卡模块把数据存成CSV文件加一个蓝牙模块把数据传到手机上看曲线。如果你想做电池电压监测把电位器换成电池分压电路就行注意分压后的电压不要超过3.3V。如果你想做音频采集把采样率提上去F103的ADC在12MHz时钟下最快能做到1微秒一个样本也就是1MSPS但DMA缓冲区和数据处理要相应调整。我个人的体会是ADC采集这件事硬件上的功夫比软件多。软件配置再对硬件上少一个电容、多一段地线数据就是不稳。反过来硬件做扎实了软件用最朴素的轮询都能读出漂亮的数据。这套50元的方案核心价值不在于省了多少钱而在于它把ADC采集里最关键的几个环节都暴露出来了让你能亲手摸到每一个影响精度的因素。