行业资讯
📅 2026/9/8 14:02:31
用CD74HC4067给MCU扩展16路ADC:选型接线、软件时序与调试避坑实录
这篇是我调试笔记的第五篇主题从之前的串口、中断、电源问题换到了一个更偏模拟前端的方向——用CD74HC4067给MCU扩展16路ADC采集。做这个事儿的起因很简单板子上模拟传感器越挂越多MCU自带的ADC通道不够用了又不想为了两三个通道去换一颗更贵的芯片于是翻了半天选型手册最后定在了这片经典的16通道模拟多路复用器上。整体工程做下来通道扩展本身并不难真正让人头疼的是两个藏在细节里的调试坑一个让ADC读数像爬楼梯一样一截一截跳另一个让整组数据莫名其妙开始“撒欢”。这篇就把选型逻辑、接线要点、软件时序以及两个坑的完整排查过程全部记录下来给后面要踩同样路子的朋友做个参考。1. ADC通道不够为什么要选CD74HC4067而不是换主控1.1 我遇到的实际场景当时的手头项目是一块环境监测板需要同时采集温湿度传感器输出、光敏电阻分压、电位器位置反馈、电池电压以及几路模拟电流检测信号。粗粗一数模拟输入大概需要13路。手头这颗MCU的ADC只有8个外部通道划掉几个被复用功能占用的引脚实际可用的就6路出头。方案讨论时有人提出直接换一颗带更多ADC通道的主控但PCB已经改了两次版固件里外设初始化、引脚映射都绑死了换主控等于整个项目重来成本实在不划算。另一个被否掉的方案是外挂一颗独立的ADC芯片比如ADS1115或者ADS1256。这个方案精度确实更好但成本要高出一个数量级而且I2C或SPI通信、驱动移植、数据解析都要重新写对当前这种“几十毫秒采一次、分辨率12位就够用”的需求来说属于大炮打蚊子。于是焦点就落到了模拟多路复用器上——用数字引脚去选通多路模拟信号把多路输入复用到MCU那唯一的ADC通道上。1.2 同类芯片对比为什么是4067而不是4051/4052市面上常见的模拟开关芯片有好几款最容易被拿来对比的是CD4051、CD4052和CD74HC4067。它们的工作原理相似区别主要体现在通道数量和切换拓扑上。芯片型号通道结构控制线数量典型用途CD4051单端8通道8:13根地址线 1根使能通道数少、引脚紧张的场景CD4052差分双4通道双4:12根地址线 1根使能差分信号切换CD74HC4067单端16通道16:14根地址线 1根使能多路单端模拟信号扩展CD74HC4067的优势非常直接一片顶两片40514个GPIO就能切16路。如果选4051要扩16路就得用两片控制逻辑瞬间复杂起来还要额外处理片选互斥。CD4052则更适合处理差分信号我这里所有传感器输出都是对地的单端电压用不上差分拓扑。所以最终锁定了CD74HC4067它的内部拓扑相当于一个16路单刀单掷开关任何时刻只有一路输入和公共端COM连通其余通道全部断开。1.3 关键参数和芯片别名要看清CD74HC4067的几个关键参数直接影响后面的调试导通电阻Ron典型值几十欧姆VCC4.5V、常温下大概70Ω左右随供电电压和温度会漂。这意味着它不是一个理想开关信号经过它会有微小的压降。切换时间纳秒级逻辑上几乎可以认为“立刻切换”和后面要说的稳定时间完全不是一个数量级。输入电压范围GND到VCC之间不能超出电源轨否则内部ESD二极管会导通轻则读数异常重则损坏芯片。静态功耗微安级对电池供电的设备很友好。还有一个容易把新手绕晕的点同样的芯片在市面上会有各种前缀和别名CD74HC4067、74HC4067、CD4067BM、HCF4067基本逻辑功能都一样只是制造商不同、封装不同。买的时候注意确认工作电压范围是否覆盖你用的电源电压以及封装是SOP-16还是DIP-16别等贴片了才发现封装买错。2. 硬件接线最容易忽略的三件事EN脚、电平匹配和输入电压范围2.1 完整的接线框架CD74HC4067是SOP-16封装引脚不算密手工焊接没什么压力。核心接线如下芯片引脚功能接到哪里S0~S3通道选择地址线MCU的4个GPIO推挽输出EN使能引脚低电平有效直接接GND或接GPIO控制ZCOM公共输出端MCU的ADC输入引脚Y0~Y1516路模拟输入各路传感器信号VCC正电源3.3V或5V取决于系统设计GND地系统地必须和MCU共地这里要特别提醒如果MCU的ADC引脚只有一个那公共端Z就直接接它。如果MCU原本有几个ADC引脚是空闲的也可以把4067的Z接到其中一个剩下的ADC引脚空着或者复用成普通GPIO没必要把4067的输出再并联到多个ADC通道上——这会增加引脚负载电容反而影响稳定时间。2.2 EN引脚“默认接地”是省事还是埋雷EN引脚是低电平有效EN为低时芯片正常工作EN为高时所有通道断开、Z端呈现高阻态。很多例程直接教人把EN接地这在实际工程里简化了软件逻辑但也等于放弃了对模拟前端的硬件关断能力。我的建议是分情况对待如果系统上电瞬间MCU的GPIO还处于高阻态而你希望采样通路尽早建立稳定电平那EN直接接地没问题但如果你的设备有低功耗模式希望在睡眠时彻底切断传感器到ADC的通路、降低漏电和噪声耦合那最好把EN接到一个GPIO上睡眠前拉高唤醒后拉低。这个我在后续的低功耗版本里就改了实测睡眠电流确实有改善。还有一个容易被忽略的点如果多个4067级联扩展EN引脚可以作为片选信号用同一个GPIO配合地址线的高位去区分不同芯片。这个后面扩展思路里再细说。2.3 电平匹配问题5V供电和3.3V MCU的坑这是接线时最值得停下来想三分钟的地方。CD74HC4067的高电平输入阈值VIH大约是0.7×VCC低电平阈值VIL大约是0.3×VCC。如果你的4067供电是5V而MCU是3.3V系统那S0~S3引脚需要的最低高电平是0.7×53.5V。很多MCU的GPIO在3.3V供电下高电平只能输出到3.0V左右根本推不到3.5V这时候通道选择就会随机出错表现为切通道A结果读出来的是通道B的数据而且时好时坏非常难查。解决办法无非三种4067用和MCU一样的3.3V供电或者用5V供电但S0~S3通过电平转换芯片比如TXS0108E驱动或者干脆选HC系列的变体74HCT4067它的输入阈值兼容TTL电平3.3V高电平可以直接驱动5V供电的芯片。我这次选择的是第一种4067直接用3.3V供电简单可靠反正传感器的满量程输出电压也不超过3.3V。如果你的信号源本身是5V输出的那还要先分压再进4067这个在下面输入范围里一起说。2.4 输入电压范围不能测负压也不能测超过VCC的电压CD74HC4067的模拟输入范围被钳制在GND-0.5V到VCC0.5V之间。这个限制来自芯片内部的ESD保护二极管输入电压一旦超出这个范围二极管就会正向导通电流会灌进芯片电源轨读数会锁死在某个异常值时间长了还可能损坏芯片。我这次采购的时候还专门确认过CD74HC4067没有VEE引脚所以它不能像CD4067那样通过负电源去处理负电压信号。如果你要采集的信号本身带有负半周比如音频信号或者电流互感器的双极性输出那一定要先做电平抬升或者加偏置电路把负电压平移到GND以上再送进4067。同理传感器输出超过3.3V的也要先分压别指望多路复用器帮你做量程转换。另外VCC和GND之间记得放一个100nF的陶瓷去耦电容位置尽量靠近芯片电源引脚。模拟开关内部的逻辑翻转瞬间会有电流浪涌去耦电容能显著降低VCC上的毛刺——而VCC上的毛刺会直接影响导通电阻的稳定性进而影响采样精度。3. 软件驱动的正确节奏切换地址、等待稳定、再触发ADC3.1 地址线驱动几行GPIO代码的事情选通通道的逻辑非常简单S0~S3对应通道号的二进制位。用HAL库写出来就是void mux_select(uint8_t ch) { 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); }如果MCU引脚紧张也可以用两个74HC595移位寄存器或者一个I2C接口的GPIO扩展芯片去控制S0~S3原理完全一样只是把GPIO换成了串行控制。我用过一次I2C GPIO扩展的方案节省了3个引脚代价是每次切换通道多了一次I2C通信的耗时大概几十微秒对慢速采样场景完全够用。3.2 一次完整采样的标准流程“切到通道-立刻启动ADC转换-立刻读结果”是绝大多数新手会写的代码但也是采样数据不稳定的一大来源。正确节奏应该是切换、等待、采样三步走uint16_t adc_read_channel(uint8_t ch) { mux_select(ch); delay_ms(2); // 等待模拟开关导通、RC充放电完成 uint16_t val adc_read_single(); // 启动ADC转换并读取结果 return val; }如果你需要每个通道取多次平均那就在同一个通道上连续读多次再切下一个通道。千万不要在每个通道之间反复横跳——每切换一次就要重新等稳定效率低且数据质量还差。我建议的采集顺序是先切到通道0稳定后连读8次存起来再切到通道1稳定后连读8次这样依次扫完16路而不是“通道0读一次、通道1读一次、通道2读一次……再回到通道0”后者会让稳定等待时间全部变成无效开销。3.3 为什么要等稳定采样电容充电时间不是玄学等稳定这一步背后的物理逻辑是RC充放电。4067的导通电阻Ron大约几十欧姆再加上信号源本身的输出阻抗共同构成了一个等效串联电阻R而ADC内部的采样保持电容、PCB走线寄生电容、以及你可能在输入端额外并联的滤波电容组成了等效负载电容C。切换通道的瞬间等效电容上的初始电压还停留在上一个通道的值需要经过若干倍时间常数τR×C之后才能逼近当前通道的真实电压。电压趋近过程满足V(t) V_final × (1 - e^(-t/τ))假设你给信号源分压电阻是10kΩ4067的Ron是70ΩADC内部采样电容约10pF那τ≈10k×10p100ns看起来很快对吧但如果在信号路径上并联了一个100nF的滤波电容τ就变成了10k×100nF1ms电压需要5个τ也就是5ms才能稳定到99%以上。这就是为什么很多人沿用数据手册里的“通道建立时间”却依然采不准——他们忽略了外部电容把时间常数拉大了几个数量级。所以延时到底给多少不要照搬手册要根据你实际的信号源阻抗和外部电容来估算再用示波器实测确认。3.4 示波器实测验证稳定时间我当时搭建了一个最简单的实验用一个电位器输出1.8V固定电压接到通道0另一个电位器输出0V接到通道1然后让程序在通道0和通道1之间来回切换用示波器探头挂在4067的Z端看波形。结果非常直观——通道切换瞬间Z端电压会从一个值向另一个值爬升中间有明显斜坡斜坡持续时间取决于等效RC。我把延时从100μs逐渐加大观察到波形在2ms左右基本不再变化所以就定了2ms这个经验值。如果你的信号源阻抗很低比如运放输出稳定时间会短很多可以适当缩小延时提高轮询速率如果信号源是高阻的比如光敏管直接输出那2ms都不一定够。4. 坑一ADC读数像爬楼梯多路串扰的完整排查记录4.1 现象读数阶梯状跳变加滤波也压不住硬件和初步驱动写完以后我开始验证各通道读数。第一版程序里我偷了个懒通道切换后只给了几十微秒的延时就去触发ADC转换。结果数据一出来所有通道的读数都在来回跳动而且跳动的形式不是随机噪声而是像爬楼梯一样一截一截地往目标值逼近——如果通道之前的电压比当前通道低读数会从低往高爬反之会从高往低掉。更迷惑的是相邻通道之间的数据还会互相拖拽通道0接了高电压通道1接的是低电压通道1的读数却明显偏高。一开始我以为是ADC本身的噪声于是上了重武器把滑动平均窗口从8次加到64次又加了中值滤波跳动的幅度确实小了一点但“楼梯”的形状还在。这就说明问题不是随机噪声而是有一个确定性的偏差源在做怪——滤波只能平滑噪声却无法消除系统性误差。4.2 排查链路先怀疑采样配置再上示波器看真容排查第一步我重新检查了STM32的ADC采样时间配置。STM32F103的ADC采样周期可选从1.5周期到239.5周期默认配置往往取的是较短的采样周期。采样周期越短内部采样电容的有效充电时间越短对信号源内阻越敏感。我把采样周期从55.5周期改到239.5周期读数确实改善了一些尤其是高内阻信号源通道但“楼梯”还是没有完全消失。这一步的结论是问题确实和“采样电容充电时间不足”有关但ADC采样周期的调整范围有限不足以覆盖4067导通电阻加外部滤波电容造成的整体RC延迟。于是我把示波器探头从MCU的ADC引脚挪到了4067的Z端切换通道后立刻观察波形——真相一下就清楚了Z端的电压需要一个明显的斜坡才能到达目标值而我在斜坡还没走完的时候就触发了ADC转换读到的自然是个中间值。这个中间值如果恰逢斜坡中段就表现为阶梯状跳变。4.3 根因拆解导通电阻和负载电容联手造成的充电延迟这个坑的根因可以用一句话概括4067是模拟开关不是理想导线它自带几十欧姆的导通电阻叠加信号源输出阻抗和负载电容之后RC充放电时间被拉长到了毫秒级别。信号链路的等效模型是这样的——信号源电压通过源阻抗Rs接入4067的Y端再从Y端通过导通电阻Ron到达Z端Z端到MCU ADC引脚之间还有PCB走线寄生电容Cp以及你可能为抗共模噪声故意添加的滤波电容Cf。ADC内部还有采样保持电容Cs。整个等效电容CCpCfCs等效电阻RRsRon忽略走线电阻τR×C。当我用了1kΩ的分压电阻和100nF的滤波电容时τ≈1.07k×100nF≈107μs5τ约535μs。而我的延时只有几十微秒落后了一个数量级读数自然严重偏小。4.4 修复验证加大延时后的数据对比修复很简单把切换通道后的延时从几十微秒改到2ms重新跑同一组实验。差别非常明显测试状态通道0读数期望3300mV通道1读数期望660mV延时 50μs3050mV880mV延时 500μs3260mV710mV延时 2ms3301mV661mV延时 5ms3302mV660mV2ms延时下读数基本收敛到真实值附近再加大延时读数几乎不再变化。这也是一个判断延时是否足够的实用方法把延时逐级加大观察采样值不再发生明显变化时就是当前信号链路需要的稳定时间。这里还要多提醒一句如果你的信号源输出阻抗很高比如光敏电阻直接分压那种几十kΩ的情况光靠加大延时可能还是不够因为高阻抗源和分布电容耦合的噪声也会被放大。这种情况下要么在传感器输出端加一个运算放大器做电压跟随低阻抗输出到4067要么把滤波电容减小换取更短的充电时间。滤波器放前面还是放后面要结合信号源阻抗整体设计不能只管一头。5. 坑二悬空引脚把整组数据带偏这个坑更隐蔽5.1 现象只接了几个通道所有读数都在抖坑一解决之后系统基本能稳定工作我开始接入真实传感器。结果没过多久又出幺蛾子我只在板子上接了4路真实传感器信号其余12个通道的Y引脚处于悬空状态。理论上悬空的通道如果不被选通应该不影响其他通道才对。但实测时发现只要轮询程序在16路之间完整扫一圈所有有效通道的读数都会周期性地抖一下抖动的幅度大概有十几到几十个LSB。更诡异的是当我把某个悬空通道的引脚用导线碰一下金属外壳或者手摸一下读数抖得更加厉害。我第一反应是电源纹波问题于是换了一个高精度线性稳压电源给整块板子供电故障依旧。又怀疑是MCU的ADC参考电压不稳外接了一个基准源结果还是抖。也就是说问题出在4067这一级。5.2 排查链路逐个通道断开找到“幽灵”源头这一步我采用的是排除法。先把程序里的轮询列表改成只扫4个真实接入的通道结果所有读数都稳定了一丝不动。再把轮询列表改回16路全扫抖动立刻回来。这就说明问题确实出在悬空通道被选中的过程中。接着我做了更细的一步把轮询列表固定成“4个真实通道1个悬空通道”然后逐个切换那个悬空通道是哪个引脚观察读数变化。结果发现无论悬空的是哪个引脚只要它被选通后再切换到真实通道真实通道的读数总会在之后的一两个采样周期内出现偏大的波动。示波器挂在Z端看能看到当选通悬空通道的瞬间Z端会捕捉到一个毫无规律的高频噪声波形幅度有几毫伏到几十毫伏等切回真实通道后这个噪声会在Z端残留一小段时间正好被紧接着的ADC转换采到。5.3 根因拆解高阻悬空端和“幽灵电压”悬空引脚之所以是“幽灵”根源在于它的电位是完全不确定的。CMOS工艺的模拟开关输入引脚内部只有高阻的栅极网络没有上下拉结构所以悬空时引脚既不是稳定的0V也不是稳定的3.3V而是悬浮在一个受外界电场、空气湿度、手指触碰、PCB邻近走线耦合共同决定的随机电位上。当程序选通这个悬空通道时这个随机电位就会通过模拟开关进入Z端且由于Z端到ADC引脚之间往往有一定阻抗这个随机信号会在Z端造成扰动。更麻烦的是模拟开关的通道之间不是完全隔离的。同一芯片内部各通道之间、通道和地址线之间都存在寄生电容和衬底耦合。悬空通道上的高频随机电压会通过寄生电容串扰到相邻的被选通通道上尤其当相邻通道信号源阻抗较高时这种串扰会成倍放大。所以即使你的真实通道没有切换到悬空通道悬空通道本身的存在也会影响全局读数稳定性只是影响程度比直接选通它要小一些。5.4 修复方案硬件接地和软件跳空双管齐下这个坑的修复我分了两步。硬件上把板上所有未用的Y0~Y15引脚统一通过一个4.7kΩ电阻接到GND不能直接短接万一某一通道被外部误接过电压直接短接会把VCC经开关路径拉倒加个电阻可以限流确保它们永远有一个确定的直流电位。这里要注意连接到GND的电阻值不能太小否则信号源带载能力弱的时候选通该通道会拉低电压也不能太大否则失去“固定电位”的意义。4.7kΩ到10kΩ是个经验上比较好用的区间。软件上我维护了一个有效通道掩码表只对实际接入传感器的通道执行“切换-延时-采样”流程悬空通道根本不会进入轮询列表。这样即使硬件上没有完全处理干净软件层面也不会主动去碰那些悬空引脚相当于上了双保险。顺便再补一个经验如果你的PCB还有重排的余地尽量让4067的Y输入走线远离数字信号线尤其是PWM和高速通信线。我在第一版板子里有一条PWM风扇控制线恰好从4067下方穿过悬空通道对它的耦合非常敏感风扇转速一变采样噪声就跟着变。后来把走线拉开、中间铺了一段地铜皮隔开噪声立刻降了一个量级。6. 滤波与优化采集精度还能再往上拽一点6.1 软件滤波组合限幅、滑动平均和递推平均怎么配合两个大坑填平之后系统已经能稳定读数了但对于传感器信号来说纯硬件电路的干净是不够的软件滤波还得跟上。用对了滤波器可以让那些顽固的高频毛刺彻底消失。我最终采用的组合是“限幅滤波滑动平均”代码模板大概是这样的#define FILTER_N 8 #define AMP_LIMIT 50 // 相邻两次采样允许的最大跳变 static uint16_t last_valid 0; uint16_t filter_adc(uint16_t raw) { // 限幅滤波剔除突变异常值 if (last_valid ! 0 abs((int)raw - (int)last_valid) AMP_LIMIT) return last_valid; last_valid raw; // 滑动平均平滑随机噪声 static uint16_t buf[FILTER_N]; static uint8_t idx 0; static uint32_t sum 0; sum - buf[idx]; buf[idx] raw; sum buf[idx]; idx (idx 1) % FILTER_N; return (uint16_t)(sum / FILTER_N); }限幅滤波专门对付偶发性毛刺比如开关切换、电机启动瞬间给信号路径注入的干扰。滑动平均则对付叠加在白噪声上的随机起伏。对于慢变信号温度、光照、液位这两个组合足够用了如果你采的信号变化极快比如振动波形滑动平均窗口就不能太长否则会削掉真实动态峰谷这时候可以考虑用IIR一阶低通截止频率按实际信号频率计算。我自己在实测中还对比过滤波前后的数据未滤波时标准方差大概3~4个LSB限幅加8次滑动平均后标准方差降到1个LSB以内。对于12位ADC来说这个效果完全够用。6.2 电源去耦和参考电压精度提升的硬件侧经验软件滤波再强也不能替代硬件上的干净电源。我在这里有一个亲测有效的组合拳4067的VCC引脚放100nF陶瓷电容MCU的VDDA引脚放1μF100nF双去耦ADC参考电压引脚如果支持外部基准直接接一个高精度基准源比如TL431或REF3030不要用MCU的VCC当参考。4067的导通电阻和MCU的ADC参考电压都会随电源波动而漂移把这两路电源稳住采样值的长期漂移能改善不少。另外如果你想追求更稳定的绝对精度可以做一次逐通道校准。每个通道在出厂测试时接入一个已知电压记录实际ADC读数计算出每个通道的增益误差和偏移误差存到Flash里正常工作时做线性修正。我这次因为精度要求没那么高只做了全通道统一校准——其实也不太够因为4067不同通道之间的导通电阻还是有几个欧姆的离散性导致通道间的增益误差并不完全一致。后面如果做更严格的项目我会给每个通道单独建校准表。6.3 扩展思路级联、差分采集和更高位数的选择稳定运行一段时间后我又在这套架构上做了两个小扩展实验。第一个是级联用两片4067组合成256通道的采集系统。做法是第二片4067的Z端接到第一片4067的某个Y输入端比如Y15第一片负责“选大组”第二片负责“选小组”。地址线S0~S3并联两片的EN分别接两个GPIO做片选。这样可以在不增加ADC通道的前提下把采集通道数扩到约240路第一片Y15被占用了。代价是信号链路多了一级导通电阻稳定时间会更长采样速率进一步降低但做静态或慢变信号的巡检足够用了。第二件实验是误把差分信号送入4067结果输出完全错乱——这也验证了前面说的4067只能处理单端信号。如果你真的需要采集差分传感器比如电桥、热电偶建议换用CD74HC4052这种差分复用器或者把差分信号先通过仪表放大器转成单端再进4067。6.4 防坑自查清单最后把这次调试中踩过和见过的坑整理成一份清单每次画板或者写驱动时过一遍能省下不少调试时间通道切换后有没有给足RC稳定时间算过实际τ吗还是照抄手册信号源阻抗是不是太高高到连延时都救不回来没用的通道是悬空还是接了固定电平EN引脚是高有效还是低有效上电瞬间是什么电平4067供电电压和MCU GPIO高电平是否匹配输入信号有没有超出GND到VCC的范围VCC去耦电容放了没有离芯片够不够近PCB上模拟走线有没有和数字线靠太近ADC引脚配置成模拟输入了吗这个低级错误我见过不止一次。这套CD74HC4067的方案最终在我的板子上稳定跑了三个多月16路通道巡检一轮的时间大约是16×8×2ms≈256ms对温湿度、光照这类变化缓慢的信号完全够用。如果哪天你真的需要更快的巡检速度要么降低每通道的平均次数要么把延时微调到实测允许的下限但一定记住——稳定时间砍过头前面那一堆坑会一个不落全还给你。