STM32 ADC通道不够用?用CD74HC4067模拟多路开关扩展16路采样 用了两年多STM32和GD32最烦的一件事就是ADC通道不够用。尤其做些多路传感器采集的小项目动不动就是八个十个采样点MCU自带ADC根本接不过来。加外置ADC芯片吧成本高还得写IIC或者SPI驱动换大封装MCU吧成本也扛不住。后来在项目里用CD74HC4067做通道扩展一块几毛钱的芯片就能把16路模拟信号并到同一路ADC上折腾完发现整套方案非常稳。这期调试笔记就把完整的接线、驱动逻辑、还有我在实测里踩过的两个大坑一并写清楚。1. 为什么选CD74HC4067做ADC扩展1.1 项目里的实际需求我手头这个项目是一台小型环境监测装置一个板上就需要同时采集8路温度NTC热敏电阻分压、4路0-3.3V的模拟电压信号、2路光敏电阻信号再加上电池电压监测一共超过了15路模拟量。用的主控MCU是STM32F103C8T6它只有10个ADC输入通道典型应用下还得留几个做别的功能怎么排都排不满足需求。当时有两条路可走一是换成STM32F103ZET6这种大封装芯片ADC通道倒是多了但整个PCB板子都要重新设计成本直接翻倍二是加一颗ADS1115这类16位外置ADCIIC接口读起来倒是简单但一颗芯片都要七八块钱多路同步采集还得忙等转换结果实际体验并不好。最后选定了CD74HC4067这种模拟多路复用器来做扩展核心思路就是“用数字引脚换模拟通道”MCU只需要给4067的4个通道选择引脚S0-S3输出不同的高低电平组合就能把16路输入中任意一路接到唯一的公共端上再由MCU的单一ADC引脚完成采样。一块芯片不到两块钱能省出一个大工程量级的通道资源属于性价比极高的方案。1.2 CD74HC4067和CD4051、ADG726怎么选很多人第一反应是CD4051因为它在学校实验里特别常见。CD4051是8选1的模拟开关想扩展成16路得用两片级联控制逻辑复杂还得用额外的引脚做片选。CD74HC4067是16选1单芯片方案4个引脚就能管16路布线省一半扫描逻辑也更直观。有人会问为什么不用ADG726这类低导通电阻的芯片。ADG726的导通电阻确实低典型值4欧姆左右但价格是4067的十倍以上。CD74HC4067的导通电阻虽然高一些工作电压3.3V下典型值70欧姆左右但接NTC分压电路或者电压信号源这类高阻抗信号完全够用。只要信号源的输出阻抗别太高建议低于10k欧姆ADC输入的电压误差是可以算清楚并修正的。我的建议是实验室原型验证、小批量产品、采集速率要求不高的场景闭眼选CD74HC4067就行。需要用高速开关做同步解调或者信号带宽要求超过几十kHz的场合再按需升级成低导通电阻型号。2. 硬件接线与布线细节2.1 引脚功能梳理CD74HC4067引脚逻辑非常清晰一根电源脚、一根地线、一根使能脚、四个通道选择脚、十六个输入脚外加一个公共输出脚。3.3V供电时芯片工作状态完全稳定。虽然数据手册上写着可以3V到16V宽电压供电但我实测下来3.3V下导通电阻和切换速度都能接受直接和STM32共用一组电源最省事。关键引脚关系如下S0pin10、S1pin11、S2pin14、S3pin13通道选择地址线组合出0x0-0xF共16个地址。COMMONpin1公共输入/输出端接MCU的ADC输入引脚。Epin15使能端低电平有效必须接GND才能正常工作。VEEpin7负电源输入处理单极性0-3.3V信号时直接接地即可。I0-I15pin2-5、pin9、pin12、pin6、pin7旁边那一排16路模拟输入通道。注意CD74HC4067的VEE是否接地是一个关键点。理论上VEE可以接负电压处理双极性信号但单片机场景下用不到负压VEE直接连GND即可。如果你把VEE悬空芯片会工作在异常状态各个通道之间会出现明显的串扰这是很多新手会上当的地方。2.2 布线时的三个习惯第一个习惯是模拟地和数字地单点相连。大批量采集板布线时ADC信号回路、分压电阻网络、通道开关的地线如果全部混在一起会导致同一个通道在不同时刻读出来值都有跳动。我在这个项目里把4067下方的区域划为模拟地孤岛分别接回主电源地平面实测采样值稳定性明显提升。第二个习惯是公共端走线要短。CD74HC4067的COMMON端直接连着MCU的ADC引脚这段走线越短越好最好控制在2cm以内。因为这段信号本身是高阻抗的走线一长更容易耦合到板子上的开关噪声和PWM辐射噪声直接影响采样精度。第三个习惯是未用通道必须做固定电平处理不能悬空。这个问题不只在PCB设计阶段存在面包板搭电路同样需要注意。16个通道不是每个都用那些悬空的输入引脚在不同电压条件下会表现出类似天线的作用强行耦合周围电场通过开关漏电流渗入到正在采集的通道上。最稳妥的办法是把未用的通道直接接GND或者预留一个10k欧姆电阻的焊盘位置调试时可以很灵活地决定接GND还是接一个固定电平。3. 软件驱动与采样逻辑实现3.1 通道切换的核心逻辑CD74HC4067并没有任何复杂的通信协议本质上就是给S0-S3输出对应的二进制地址选中哪一条输入通道芯片就把这一路的信号“接通”到公共端上。ADC采集前先做地址切换等待一段时间保证信号稳定再启动ADC采样。写代码时我习惯用直接寄存器操作来控制这几个引脚比HAL库里一次一次的GPIO_WritePin更干脆。这里以STM32的HAL库为例地址引脚总共占用PA0-PA3四个引脚// 选择CD74HC4067通道ch 取值范围 0~15 void Mux_SelectChannel(uint8_t ch) { ch 0x0F; HAL_GPIO_WritePin(MUX_S0_GPIO_Port, MUX_S0_Pin, (ch 0x01) ? GPIO_PIN_SET : GPIO_PIN_RESET); HAL_GPIO_WritePin(MUX_S1_GPIO_Port, MUX_S1_Pin, (ch 0x02) ? GPIO_PIN_SET : GPIO_PIN_RESET); HAL_GPIO_WritePin(MUX_S2_GPIO_Port, MUX_S2_Pin, (ch 0x04) ? GPIO_PIN_SET : GPIO_PIN_RESET); HAL_GPIO_WritePin(MUX_S3_GPIO_Port, MUX_S3_Pin, (ch 0x08) ? GPIO_PIN_SET : GPIO_PIN_RESET); }这块代码的逻辑非常直白把通道号0-15看成四位二进制数每一位控制一个引脚的电平。实际运行中MCU还需要等待一段引脚电平稳定时间通常我给50到200微秒的延时然后才去读ADC。延时本质上是为了等电平转换完成和模拟电压建立稳定CD74HC4067的切换速度非常快数据手册典型值几十纳秒级别但这只是芯片内部通道切换的时间后面连接的传感器信号源和走线电容会让电压“缓一缓”才能稳定。3.2 完整读取16路电压值通道选择和ADC采样协调工作主流程写成一个封装函数最顺手uint16_t Mux_ReadChannel(uint8_t ch) { Mux_SelectChannel(ch); delay_us(100); // 等通道切换稳定 HAL_ADC_Start(hadc1); HAL_ADC_PollForConversion(hadc1, 10); uint16_t val HAL_ADC_GetValue(hadc1); HAL_ADC_Stop(hadc1); return val; } // 依次读取16路 void Mux_ScanAllChannels(uint16_t *buf, uint8_t len) { for (uint8_t i 0; i len; i) { buf[i] Mux_ReadChannel(i); } }好多人在这个函数里犯过一个错误在for循环里不停重新初始化ADC导致程序卡死或者采样速度极慢。ADC的转换时钟、采样周期、通道配置在系统初始化时一次性配置好实际运行中只需要调用ADC转换和读取结果的接口即可。上面那段代码每次只做启动、等待、取值三个动作不重复初始化。关于ADC参数配置有一点要特别注意ADC的采样周期至少要设置到239.5个周期以上。多路开关导通会引入额外的RC充放电时间快速采样周期如1.5-7.5个周期对低阻抗源才比较保险而传感器分压网络的等效阻抗通常都在几千欧姆以上采样太快会导致采样电容充不满采出来的值普遍偏小。我实测过用71.5周期时误差在1%以内用7.5周期时误差最大能到5%因此直接选最慢的239.5周期保证电压获取准确。4. 两个差点让人砸键盘的调试坑4.1 坑一切换通道后首采值残留旧通道数据这个问题出现得非常隐蔽。一开始我写循环扫描代码每条通道要读两次才得到正确值而每次都只有第一次读到的值是错的。本来怀疑是硬件接触不良拿示波器去量公共端波形却看到通道切换后波形已经从旧值跳到了新值看起来完全正常。问题其实出在HAL_ADC_GetValue()调用之前。MCU有两个ADC模块我这里用的是ADC1的规则组。在多路复用器通道切换后立刻触发ADC转换一开始以为100us的延时足够但实测在接通瞬间仍有较大毛刺。进一步分析后发现问题根源有两层第一层是4067内部的开关结构存在导通电阻和寄生电容相当于一个低通滤波器。当输入信号变化时公共端电压是按RC曲线趋于稳定的。受信号源内阻、走线寄生电容影响,实际稳定时间远高于预期。第二层是ADC内部的采样保持电容同样需要充电时间。MCU的ADC采样原理是“采样-保持-量化”,采样阶段时内部电容要和外部信号源完成电荷交换。如果外部等效阻抗太高,一个很短得采样周期根本不够电容充满,采到的电压部分是上一通道的残留电荷。知道了原因,处理办法就很简单:通道切换后加一个可配置的延时(具体值取决于信号源阻抗,100us到1ms之间通常都能覆盖绝大多数场景),同时在软件上丢弃切换后的第一次采样值,第二次读取才算有效。代码调整为:uint16_t Mux_ReadChannel(uint8_t ch) { Mux_SelectChannel(ch); delay_us(500); // 延长稳定时间 // 丢弃第一次采样 (void)SingleAdcRead(); delay_us(10); // 正式采样 return SingleAdcRead(); }实际用这个版本的函数16路通道的采集一致性肉眼可见地变好。不同通道之间的数值再也不会出现互相“串门”的情况。这个经验本质就是模拟系统中切换信号源后必须给RC回路留够建立时间——单一参数延时把问题解决干净了。4.2 坑二未用通道悬空导致的数据漂移第二个坑更隐蔽排查时花了一下午。我在焊接第一版测试板的时候只焊了6路通道对应的分压电阻网络剩下10个通道的输入引脚直接悬空。按道理说只要程序不选中那些悬空引脚它们就不应该影响正常采集。但实测结果完全反直觉我采集0号通道时用手靠近1号通道对应的引脚附近0号通道的采样值立刻发生波动。这就是典型的悬空引脚串扰。CD74HC4067本质上是一排模拟开关所有输入引脚和公共端之间都存在寄生电容。虽然未选通的开关处于关断状态但引脚上的感应电荷还是可以通过寄生电容耦合到公共端特别是信号源阻抗高的时候这种耦合效应会放大到肉眼可辨的数值漂移。解决起来倒是简单两板斧第一把没有接传感器的通道输入引脚全部接到GND让它们固定在一个稳定电平上芯片内部寄生电容自然没有波动电荷可传。第二如果有些通道确实需要悬浮待用比如后期要外接插头记得在软件层面对采样值做限幅滤波比如两次采样差值超过50就取平均值剔除跳变数据。这第二个坑让我意识到一个原则模拟通道扩展芯片的“未用通道”必须先处理不能光在程序里不调用就完事。硬件上多花两个接地焊盘的成本比软件上追着飘数调半天要划算得多。5. 采样稳定性优化与外部校准5.1 软件滤波组合拳硬件层面的坑填完之后通道扩展方案基本就能正常工作了但原始ADC值依然有一定程度的噪声波动。这种波动主要来自电源纹波、走线上的电磁干扰以及4067本身导通电阻的微小变化。解决手段一般有三种中值滤波连续采样5次去掉最大值最小值求中间三个值的平均适合抑制偶发毛刺。滑动平均滤波维护一个长度为N的环形采样队列每次采完新数据算整个队列的平均值。N越大越平滑但响应速度变慢建议取8也就是调用8次取均值噪声抑制效果不错且实时性可控。限幅滤波判断本次采样值和上次采样值的差值如果超过设定阈值比如ADC值50就舍去本次值用于剔除明显的异常跳变。我用的组合方案是限幅滤波在前去掉粗大误差紧跟一次8点滑动平均抑制随机噪声最后再用中值滤波去除剩余毛刺。这套组合在STM32F103上消耗的CPU时间在几百微秒级别完全不影响系统主循环的其他工作。以12位ADC0-4095为例直接原始读取时同一通道同一电压下数据的标准偏差在8到15个LSB之间加上这套滤波组合后标准偏差可以压到2到3个LSB以内。换算成电压误差就是不到2.5mV对这个项目来说完全够用了。5.2 外部ADC三点校准思路CD74HC4067的导通电阻不是不变的线性值它随输入电压和供电电压的变化会有微小改变这会导致通道扩展后每个通道的实际增益都有一丁点偏差。对精度要求不那么苛刻的场景完全可以忽略因为导通电阻的偏差折算成电压误差通常不超过10mV级别。如果做的是仪表类产品离散性就必须被校准掉。常见做法是三点校准法。选三个标准电压点比如0.5V、1.8V、3.0V用高精度万用表实测进入CD74HC4067输入引脚前的实际电压同时记录下MCU采样得到的原始ADC值。对每个通道建立一条“实际电压 k × ADC值 b”的直线方程得到一对和偏移参数。理论上一组两点数据就可以标定一条直线但多取一个中间点能检查通道响应的线性度。如果三个点的拟合残差很大说明这个通道在某个电压区间存在明显非线性就得回到硬件上检查分压电阻是否虚焊或者传感器型号是否匹配。实际操作时先在程序里给每个通道开一个校准参数结构体typedef struct { float k; // 增益 float b; // 偏移 } CalibParam; float GetRealVoltage(uint8_t ch, uint16_t raw) { return calibParams[ch].k * raw calibParams[ch].b; }校准是离线完成的工厂生产时可以利用精密电压源自动扫三个电压点把对应的k、b值写进Flash。使用时的思路是把“读取ADC”和“换算工程值”变成两步在工程值换算这里插入通道系数即可。这正好契合标题里提到的外部ADC校准场景实测经过三点校准后每个通道的满量程误差可以控制在0.3%以内。6. 常见问题与排查速查表调试过程中肯定会遇到一些重复出现的问题我把最典型的几条整理一下方便后面排查。现象可能原因排查和解决所有通道读数都偏大或偏小参考电压不准、ADC采样周期太短、信号源阻抗过高用万用表测MCU参考电压把采样周期调到239.5周期降低信号源输出阻抗读取速度和实际扫描周期不匹配切换通道后延时太长或太短先用示波器量波形确定电压建立时间再设置延时以200us起步逐步微调某个特定通道读数一直为0该通道引脚虚焊、S0-S3地址线某一位控制异常、外部输入直接短路到地万用表量引脚通断查S0-S3对应的GPIO配置检查传感器接线相邻通道之间明显串扰VEE悬空、未用通道悬空、布线间距太近VEE确认接地把未用通道直接接GND模拟走线包地处理数据波动大且无规律电源纹波大、ADC参考噪声、滤波未做好电源端加10410uF去耦软件组合滤波换到某个通道后整个采集卡死ADC在切换通道后未正确停止或重新开始检查HAL_ADC_Stop调用时序确保转换完成后进入空闲状态7. 再多提一嘴的调试心得整个项目跑下来CD74HC4067最大的优势在于把硬件设计成本压得非常低、驱动逻辑又足够简单。尤其是对STM32这种ADC资源不富裕的芯片用这颗芯片扩展出来的16个通道实际精度完全能覆盖传感器采集应用的需求。我给还在用4067做项目的朋友提三个建议一是硬件上永远记住“未用通道接地”这个习惯能帮你躲掉后期一大堆软文调试时间二是软件上不要吝啬那几百微秒的通道切换延时宁可多等一会儿也不能让电荷残留影响数据真实性实际运行响应大家都是毫秒级扫描一遍16通道差10微秒的延时根本不影响总吞吐三是尽快建立一套以通道号为索引的滤波和校准框架把代码写成模块多个项目复用起来会特别顺手。这套方案后续还能扩展的方向也不少比如把16通道分成几组不同量程的通道通过配置不同的外部分压比实现宽量程采集或者把两个4067并联用8个引脚扩展出32通道只要软件层的索引抽象做到位扩展成本几乎为零。实际测试中这套组合运行了一个多月没有出现任何卡死或数据异常作为长期方案完全经得住考验。