
一直关注我嵌入式调试笔记的朋友应该知道我平时做项目喜欢把一些不常见但很实用的小技巧记录下来。这一篇想聊聊最近在项目里做多路模拟量采集时的一个方案——用 CD74HC4067 把MCU原本不够用的ADC通道扩展到16路整个过程不算复杂但调试中踩了两个坑还挺典型的值得记录一下。如果你手头的MCU ADC通道不够、又不想直接换大封装芯片或者只是临时需要多路模拟量采集那么CD74HC4067这类模拟多路开关基本是最省钱也最灵活的方案。这篇笔记我会把原理、接线、驱动逻辑以及我实际调试中遇到的两个坑一并讲清楚希望能帮你少走点弯路。1. 内容整体设计与思路拆解1.1 为什么要用CD74HC4067扩展ADC通道先说说我的实际场景。这次用的主控是STM32F103系列它虽然有多个ADC、每个ADC有十几个通道但引脚资源被其他功能占掉之后真正能留给模拟采集的通道并不多。而我的项目需要同时采集16路模拟信号包括电位器电压、分压电阻网络输出、传感器模拟输出等ADC通道明显不够。直接换一颗ADC通道更多的MCU成本、开发周期、PCB改动都不划算。外挂一颗独立的ADC芯片比如ADS1115、ADS1256这些通信协议和驱动又要重新写而且对于慢速模拟信号来说有点杀鸡用牛刀。这时候用模拟多路开关是最务实的思路既然MCU只有一个ADC转换器那就在ADC前面加一个“多选一”的开关分时把16路信号轮流送到ADC引脚上。CD74HC4067是TI原仙童的一颗16通道模拟多路复用器/信号分离器英文叫 Analog Multiplexer/Demultiplexer。它的核心逻辑很简单通过4根地址线S0~S3的16种组合从16路输入C0~C15中选通一路接到公共端COM然后由MCU的ADC去采样公共端。用这个方案有几个优势成本低一片CD74HC4067在电商平台几毛钱到一两块钱比换主控或者加ADC芯片便宜太多。电路简单不需要额外的供电复杂设计3.3V或5V都能工作外围几乎不用加器件。功耗低静态功耗极低适合电池供电产品。控制占用少只需要4个GPIO作为地址线加上1个ADC输入引脚总共5个引脚就换来了16路模拟输入。1.2 这个方案的适用边界和选型考量CD74HC4067不是万能的。它适用于信号频率低、内阻不太高、精度要求不是极端的场合。如果信号频率很高比如音频、振动信号或者要求通道间完全隔离、不能有串扰那模拟开关方案就不合适了得像示波器前端那样每个通道独立缓冲放大或者直接用多路同步采样的ADC。我这次采集的信号基本都是直流或者极低频变化传感器输出、电位器分压信号变化速率远低于开关切换速率所以用4051/4067这一类多路开关绰绰有余。选型上还有几个细节可以注意HC系列 vs CD4000系列CD4067是CD4000系列的工作电压范围宽3V~18V但导通电阻较大典型几百欧。CD74HC4067是高速CMOS版本工作电压2V~6V导通电阻小典型几十欧切换速度快很多。如果MCU是3.3V供电建议直接用CD74HC4067导通电阻小对ADC采样更友好。导通电阻的影响模拟开关导通时是有电阻的通常几十欧姆。这个电阻对高阻抗信号源影响比较大因为会和信号源内阻一起分压造成采样值偏低。如果你的信号源内阻很大比如几十千欧以上最好在信号进入4067之前做一个电压跟随缓冲。我这次信号源基本都是低内阻输出直接接也没问题。通道数量如果只需要8路可以选CD74HC4051如果需要差分输入可以看CD74HC4052/4053。4067是最经典的16路单端版本适合我这种需求。2. 核心细节解析与实操要点2.1 引脚功能与硬件接线详解CD74HC4067的封装一般是SOIC-24或者TSSOP-24也有DIP-24的老式封装。引脚功能分布为C0~C1516路模拟输入通道这是被选择的信号输入端。COM公共端内部通过开关连接到被选中的通道外部接到MCU的ADC引脚。S0~S34根通道选择地址线二进制组合对应16个通道。EN使能低电平有效。EN接低电平时芯片正常工作接高电平时所有开关断开COM端呈现高阻态。如果不控制使能直接接地即可。VCC和GND电源引脚HC系列建议3.3V~5V。地址线和通道的对应关系我直接用一张表列一下方便对照S3S2S1S0选通通道0000C00001C10010C20011C30100C40101C50110C60111C71000C8...............1111C15接线的时候有几个很重要的细节S0~S3不要悬空CMOS输入引脚最怕悬空悬空会导致输入电平不确定芯片可能随机选通某个通道而且会增大静态功耗。我的做法是MCU的4个GPIO直接连过去GPIO初始化时先全部配置为推挽输出。如果你用的是按键拨码这些手动控制方式那必须接上下拉电阻。COM引脚到ADC的走线尽量短COM引脚输出的信号会被ADC采样走线太长容易引入噪声。我的板子上把4067放在距离MCU ADC引脚比较近的位置两引脚间直接连了一根短走线没有经过接插件转接。EN引脚直接拉低以我这次的需求不需要用EN做通道隔离所以在PCB上直接把这脚接地了。如果你的应用需要多个4067并行扩展比如用2片做32路那EN引脚可以用MCU的GPIO来控制片选。电源去耦VCC引脚旁边加一个0.1uF陶瓷电容到GND这点很多人会忽略。虽然HC系列静态功耗低但开关瞬间会有电流毛刺不加去耦容易在模拟信号上耦合出噪声。我习惯再并联一个10uF电解电容做低频滤波。2.2 信号调理的前置注意事项把16路信号直接接到4067的C0~C15之前要确认每路信号的电压范围都在芯片允许范围之内。CD74HC4067允许的模拟信号范围是VCC到GND之间严格说是VCC0.5V到GND-0.5V超出这个范围会对内部的ESD保护二极管构成正向导通轻则信号被钳位、采样错误重则损坏芯片。实际采集中我遇到过一路信号来自运放输出运放供电是±5V输出范围达到-3V~3V直接接4067肯定不行。解决方案是先用电阻分压加二极管钳位把信号搬到0~3.3V范围内或者干脆用运放做电平移位。这一点信号链设计的时候就要想清楚别等焊好板子再改。另一个注意点是信号源的输出阻抗。4067的导通电阻虽然只有几十欧但如果信号源内阻是几十千欧那么信号在开关导通电阻上会产生分压。举例来说如果信号源内阻是100kΩ开关导通电阻是50Ω那么开关导通后ADC看到的电压只是原来的99.95%虽然误差很小但如果后续信号还要经过放大器这个误差会被放大。更糟糕的情况是ADC采样时会有电荷注入高内阻信号源无法及时补充采样电容的电荷导致采样值偏低甚至跳动。如果信号源内阻大于10kΩ强烈建议加一级运放缓冲我这次有一路传感器输出内阻20kΩ加了一个双运放做跟随之后数据明显稳定很多。2.3 电源与地平面布局的细节PCB布局上模拟信号、数字控制信号和电源之间的隔离是模拟采集系统常见的老大难问题。4067正好横跨在模拟域和数字域之间C0~C15是模拟信号S0~S3是数字控制信号。我的经验是4067放置在模拟区域和数字区域的交界处模拟输入走线从模拟区域进来数字控制线从数字区域过来。模拟信号走线尽量短并且不要和数字信号线平行走线过长。如果PCB层数允许最好让模拟走线在参考地平面的完整区域上走。每个模拟输入引脚可以加一个小电容到地典型值1nF~10nF视信号频率而定这个小电容能滤除高频噪声但也会降低信号带宽直流信号测量无所谓如果信号有几十kHz以上变化要谨慎。3. 实操过程与核心环节实现3.1 硬件连接实例我这次的硬件连接大概是这样的以STM32F103为例4067的VCC接3.3VGND接GNDVCC旁边并联0.1uF和10uF电容到GND。S0~S3分别接到MCU的PA0~PA3这4个引脚我配置为通用推挽输出。COM引脚接到MCU的PA4这个引脚配置为模拟输入复用为ADC1的通道4。EN引脚直接接地。16路信号依次接到C0~C15。之所以让S0~S3用PA0~PA3COM用PA4是因为这些引脚在同一个GPIO端口操作时可以用一个字节的位操作同时改变4个地址引脚效率高、时序一致性好。3.2 驱动逻辑的软件实现软件逻辑并不复杂核心就是写通道地址 - 等待开关稳定 - 触发ADC采样 - 读取结果 - 切换下一个通道。下面是我实际用的初始化代码HAL库环境// 初始化S0-S3四个GPIO为推挽输出 void MUX_Init(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitStruct.Pin GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); // 初始化PA4为模拟输入 GPIO_InitStruct.Pin GPIO_PIN_4; GPIO_InitStruct.Mode GPIO_MODE_ANALOG; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); } // 切换4067通道channel范围0~15 void MUX_SelectChannel(uint8_t channel) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, (channel 0x01) ? GPIO_PIN_SET : GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, (channel 0x02) ? GPIO_PIN_SET : GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, (channel 0x04) ? GPIO_PIN_SET : GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_3, (channel 0x08) ? GPIO_PIN_SET : GPIO_PIN_RESET); }然后ADC的采集我用了HAL库的单通道阻塞式读取uint16_t MUX_ReadChannel(uint8_t channel) { uint16_t adc_val 0; MUX_SelectChannel(channel); delay_us(100); // 等待开关切换稳定后面会详细讲 HAL_ADC_Start(hadc1); HAL_ADC_PollForConversion(hadc1, 10); adc_val HAL_ADC_GetValue(hadc1); HAL_ADC_Stop(hadc1); return adc_val; }3.3 多路循环采样的调度策略实际项目中当然不是单路读一下就行而是需要16路轮询采集形成一个“扫描周期”。我的做法是写一个轮询函数uint16_t adc_results[16]; void MUX_ScanAllChannels(void) { for (uint8_t i 0; i 16; i) { adc_results[i] MUX_ReadChannel(i); } }看起来很简单但这里有个实时性问题16个通道每个通道切换后等待100us加上ADC采样转换时间我这颗STM32F103的ADC时钟12MHz采样周期设为13.5个时钟转换约1us整个一轮扫描大约需要1.6ms左右。也就是说系统的模拟量刷新率大约是600Hz左右。对于温度、压力、电位器这类变化缓慢的物理量600Hz完全够了。但如果你要采集的是几十赫兹甚至上百赫兹的交流信号这个刷新率就不行了。这时候有两个优化方向减少等待时间把通道切换后的等待时间压缩到开关稳定所需的最小值其实几十微秒就够了后面我会讲。用DMA定时器触发让ADC周期性地自动采样同时用定时器输出PWM来控制4067的地址切换做到“切换通道-延时-采样”硬件化完全不占CPU。这个方案更复杂但刷新率可以提升到几千赫兹。我这次在方案里也预留了优化空间S0~S3是从MCU直接控制的如果要升级到定时器触发方案只要把S0~S3引到定时器的PWM输出脚即可不需要重新画PCB。3.4 实测数据与稳定性观察焊好板子之后我先用直流稳压电源输出一个1.000V的电压依次接到C0~C15看每一路读回来的值是否一致。用12位ADC3.3V参考电压理论上每路应该读到1241左右1.000/3.3*4095。实测结果我记录一下通道读数误差C01240-1C112410C512421C71240-1C101239-2C1512410整体表现还可以最大误差2个LSB在12位ADC的量化误差范围内。但如果信号内阻较大或者布线较长误差会明显增加。所以4067方案的精髓在于“信号在进入开关之前必须足够强壮”——低内阻、干净、无偏置。4. 常见问题与排查技巧实录4.1 调试坑一通道切换后不等待采集值严重跳变这是我遇到的第一个坑也是最容易踩的。第一次写驱动的时候我只想着快速读完16路所以切换完通道后马上就触发ADC采样结果发现某些通道的值跳动剧烈尤其是相邻通道之间还会互相“串扰”。排查过程其实挺快的我先是换了一个很稳定的直流电压接到C0上其他通道悬空然后循环采样。结果发现C0的采集值会随着上一次扫描的最后一个通道不同而变化。比如从C15切换到C0时C0第一次读到的值明显偏低切换后立即读数跳动幅度有时达到30~50个LSB。原因分析4067是机械开关的半导体等价物通道切换后COM引脚的电压并不是瞬间就跳到目标值的它需要一定时间来完成对线路寄生电容和ADC采样电容的充放电。尤其是在信号源内阻较大的情况下充电时间常数RC变大稳定时间会更长。如果不等信号稳定就采样采集到的就是“半路上”的电压自然不准。这个问题网上很多方案都用delay_ms(1)这种粗暴方式解决实际上并不需要等那么久。4067的开关切换时间手册上说是几十纳秒真正拖慢稳定速度的是外部电路的时间常数。我实测下来对于几kΩ级别的信号源切换后等30~100us就足够稳定了。最终我统一采用100us留足裕量。代码在MUX_ReadChannel里加了delay_us(100)问题就消失了。提示如果你用的是裸机无操作系统的环境延时用DWT计数器或者SysTick实现比较准确。如果用了RTOS要确保这个延时不阻塞高优先级任务太久否则建议用信号量或者任务调度的方式来处理。4.2 调试坑二未选通的悬空引脚导致通道间串扰第二个坑比第一个隐蔽多了。某天我测试的时候发现当C1接入一个缓慢变化的三角波信号、其他通道悬空时C0上竟然能看到微弱的三角波“漏”过来虽然幅度不大大概0.1V级别但确实是存在的。我一开始怀疑是4067内部的漏电流但仔细看了手册HC系列的断开通道泄漏电流只有纳安级别根本不可能在外部产生0.1V的影响。后来用示波器测C0引脚才发现真正的串扰路径不在4067内部而在外部走线——C0引脚旁边悬空了很长一段走线这段走线相当于一根天线通过电磁耦合接收到了相邻通道的信号。排查到这里问题的根源就很清楚了未选通的通道引脚如果悬空就相当于把噪声天线直接接到了芯片内部。虽然这些引脚对应的开关是断开的但悬空的引脚和COM引脚之间还是存在寄生电容耦合高频信号就能“越过”断开的开关串扰到COM上。处理办法有两招硬件上给每一路输入加一个下拉电阻10kΩ~100kΩ到地。这样未选通的通道被固定到确定的电平不会悬空接收噪声。同时下拉电阻也给信号提供了一个确定的直流回路采样稳定性会更好。注意下拉电阻不能太小否则会和信号源内阻分压。软件上如果某个通道根本没有接信号比如16路只用了10路就把它当成普通悬空通道处理即可。如果信号源本身有输出阻抗且不允许接下拉电阻那可以在Cx引脚和地之间只加一个小电容1nF同样能吸收耦合过来的高频噪声。我最终的方案是在每一路输入引脚上放了10kΩ下拉电阻同时在PCB布局上尽量缩短了悬空走线长度。改完再测串扰现象基本消失示波器上看C0引脚完全干净。4.3 其他容易忽视的硬伤排查除了上面两个坑调试中我还遇到过几个平时不留意就很容易踩的问题一并整理在这里参考电压不稳定导致整体读数漂移如果你的MCU ADC参考电压直接用LDO输出而系统里又有大电流负载比如电机、射频模块参考电压会被拉低ADC读数就会整体偏大。解决方法是参考电压用独立的基准源或者至少加一个大的去耦电容。我这次直接用3.3V系统电源做参考其实有点冒险还好负载不大数字还算稳定。采样保持电容充放电不足导致数值偏低STM32的ADC内部有采样保持电容如果信号源阻抗太高采样时间内电容充不满读出的值就会偏低。除了在4067之前加运放缓冲还可以增大ADC的采样周期STM32的SMPR寄存器里可以配置我用的是ADC_SAMPLETIME_13CYCLES_5如果信号源阻抗再高一点会改到239.5周期。GPIO配置错误导致通道选择不对S0~S3接的是PA0~PA3如果初始化的时候把某个引脚误配置成模拟输入这个脚就输出不了高电平导致地址码不对选中的通道跟预期不符。排查这种问题很简单用万用表量一下4067的S引脚电压和代码里写的地址对照一下就知道谁出错了。4067供电电压和信号电压不匹配如果你用3.3V给4067供电但有一路输入信号是5V那么这个5V信号会被内部保护二极管钳位采样值最高只能到3.3V左右而且长时间工作还可能损坏芯片。要么保证所有输入信号都在供电范围内要么用5V给4067供电但要注意COM引脚输出的电压不能超过MCU ADC引脚的耐压。4.4 常见问题速查表我把这次调试碰到的典型问题整理成一个速查表方便大家对照排查现象可能原因解决办法所有通道读数整体偏低信号源内阻过高、4067导通电阻分压、ADC采样时间不足增加运放缓冲、加大ADC采样周期某一通道读数跳变严重该通道信号源阻抗高、走线过长、接触不良检查接线和焊点、加缓冲运放、缩短走线通道间互相串扰未选通引脚悬空、走线耦合加下拉电阻、缩短悬空走线、加小电容到地切换后第一次读数不准通道切换后未等待稳定切换后延时100us以上再采样读数整体偏高或偏低并跟随电源波动参考电压不稳优化电源去耦、改用独立基准源通道选不对、总是固定读某一路GPIO配置错误、S引脚接触不良检查GPIO输出电平、万用表验证S引脚电压高电压通道读数被钳位输入信号超出4067供电范围分压/钳位/电平移位后再进40675. 后续扩展与改进空间5.1 如果信号更多怎么办单片CD74HC4067支持16路但如果你需要32路、48路甚至更多有几种扩展方式用多片4067每一片的COM引脚分别接到MCU的不同ADC通道。这种方法最简单但需要MCU有足够的ADC通道。用多片4067每一片的COM引脚通过一个4053三路二选一可以用作二选一开关再汇合到同一路ADC。相当于先做分组选通再做最终选通形成两级开关树。这种方案控制逻辑复杂一点但可以省MCU的ADC引脚。如果信号是差分的可以用CD74HC4052这类多路差分开关它能同时切换信号的正负端。5.2 提高采样精度的小建议虽然4067方案的目标是“够用就好”但如果想让数据更漂亮一点可以尝试软件上做多次采样取平均。我一般每路连续采4次或8次去掉最大最小值后取平均这个滤波操作能显著降低随机噪声。采样值加一个简单的滑动滤波或一阶低通滤波。比如filtered filtered (new_val - filtered) * alpha;alpha取0.1~0.3效果很平滑。如果项目对温度漂移敏感可以在板上留一个热敏电阻通道实时测温做软件补偿。5.3 从调试到量产要考虑的问题如果你只是做个原型或者课程设计上面的内容基本够用了。但如果要进入量产阶段还有几个点需要考虑4067不同批次的导通电阻会有差异如果你的产品要求每一路都校准那么出厂时要逐个通道做软件校准把每路偏移存在Flash里。PCB板材、布局的一致性会影响通道间的一致性量产时建议每块板出厂都跑一遍自检程序读一下基准电压源的采样值确认ADC链路正常。4067的工作温度范围是-40°C~85°C如果你做的是工业级产品要注意温度引起的漂移。6. 最后分享一点个人体会这次用CD74HC4067扩展16路ADC采集整体来说是一次很典型的嵌入式模拟链路调试经历。方案本身不复杂原理也不深奥但“能用”和“好用”之间差的往往就是那些调试中才能发现的细节。比如通道切换后的稳定时间、悬空引脚引入的串扰、信号源阻抗对精度的影响这些东西数据手册上不会按你的电路算好只能靠实际测量一点一点抠出来。我个人的习惯是手里常备一个稳压电源和一个高精度万用表调试模拟前端时用稳压电源输出一个很干净、很稳定的电压接到每一路去检测一致性。如果某一路读数不对先量信号源输出再量4067的C引脚再量COM引脚一级一级缩小范围很快就能定位问题。另外强烈建议在代码里把“单通道采样”封装成独立函数这样不管以后是轮询16路还是根据业务需求动态选择某一路都不用改底层逻辑使用和维护都很方便。调试的时候也可以用串口把通道号和ADC值打印出来配合上位机波形显示数据变化一目了然。多路模拟采集这个需求在嵌入式项目里实在太常见了4067这个芯片便宜、简单、稳定值得经常备几片在抽屉里。希望这篇笔记里的内容对你有所帮助如果你看完也有类似的调试经历欢迎多在评论区交流大家一起把坑填平。