首页
/
行业洞察
/
正文
INDUSTRY INSIGHT · 深度
CD74HC4067模拟开关扩展ADC通道:STM32多路采集时序与防干扰实战
📅 2026/9/8 6:47:30
✍️ 爱科研究院
👁 阅读 3,247
前阵子画的一块测试板功能本来不复杂就是采集十几路模拟量。结果原理图一画完问题来了手头MCU的ADC资源根本不够用——常见的SAR型ADC真正能引出到引脚的模拟输入通道也就三五个而板子上要接16路传感器信号。换MCU要重新画板改封装周期拖得太长上外挂ADC芯片吧一颗16位芯片的单价就顶得上一大把模拟开关。最后我把目光放到了CD74HC4067上一颗16通道模拟多路复用器用4个GPIO把原有ADC通道从几路扩展到16路。硬件改起来便宜简单软件也不复杂但调试过程中踩了两个挺典型的坑一个在时序上一个在悬空引脚上都花了些时间才定位清楚。这篇就把完整方案和踩坑过程记录下来。1. 先想清楚CD74HC4067补的是哪一路ADC缺口1.1 三选一换MCU、外挂ADC芯片、还是模拟开关遇到模拟输入通道不够大家第一反应通常有三种路子。第一条路是换MCU选一个带更多ADC通道的型号。这条路最直接但代价也最大得改封装、改PCB、改BOM固件里外设库全要重来一遍原本验证过的代码全部推倒重测。如果项目还在原理图阶段还好说板子已经画完再想换基本等于重新做一版时间成本根本扛不住。第二条路是外挂ADC芯片。比如ADS1115这种4通道16位ADC价格不低通信还要占用I2C或SPI软件上多一套驱动。如果只需要两三路高精度采集这条路很合理但要采16路要么选多片、要么选8通道以上的高精度芯片BOM成本和驱动复杂度都上去了。而且很多场景传感器的输出电压本来就是慢变直流信号根本用不上24位那种高规格属于杀鸡用牛刀。第三条路就是这次用的CD74HC4067。它的本质是一个16选1的模拟开关16路模拟输入4根地址线选一路把这一路接到公共端再由MCU自带的ADC去采样。MCU的ADC资源不需要增加只多占4个GPIO和一个ADC引脚。一颗芯片零售价一两块钱四颗也才几块钱比裸ADC芯片便宜太多。对于16路低频慢变信号轮流扫描这种需求方案非常契合。我当时的需求是采集12个NTC温度点加上4个电位器电压信号变化都很缓慢扫描一圈100ms完全够用所以4067成了最合理的选择。1.2 CD74HC4067的适用边界什么时候不该用它如果不看场景盲目上模拟开关后面会很难受。4067适合的是直流或低频信号比如温度、压力、电压、电位器这类信号本身不会剧烈变化轮流扫一圈不会丢掉关键信息。但有两种情况别用它。一种是采集高速动态波形比如音频、振动加速度波形、电流环动态响应。4067的通道切换虽然只有纳秒级传播延迟但切换后信号在外部阻抗和寄生电容上重新建立需要时间再加上逐次逼近ADC本身的采样保持时间整体扫描速率会受很大限制。真要对多路高速信号连续采样老老实实上多片ADC或带同步采样功能的专用芯片。另一种是精度要求非常高的场合。4067的导通电阻Ron有几十欧姆具体数值随供电电压、温度、信号电压变化虽然这个电阻不会直接吃掉直流精度ADC输入阻抗很高Ron上几乎不产生压降但Ron的非线性和温漂会映射到采样结果里。像称重传感器、热电偶毫伏级信号这类需要24位ADC的场景直接接一个模拟开关是很不明智的那应该直接用HX711、ADS1232这类专用芯片。一句话总结低速、多路、低成本4067是神器高速、高精度、同步采样别碰它。2. 四根地址线管16路接线细节与通道映射2.1 引脚功能与最小接线CD74HC4067的封装不算复杂但引脚数量多先理清楚再画板。核心引脚分三组16个模拟输入/输出引脚Y0到Y15一个是公共端Z文档里也叫COM/OUT还有4根地址选择线S0到S3加上供电VCC、GND和一个使能引脚EN。接线时最关键的一点是理解Z引脚的双向性。它既可以当输入也可以当输出但在这个应用里Z接MCU的ADC输入引脚Y0到Y15接各路传感器信号。VCC接3.3V或5V都行注意4067的模拟输入范围不能超过供电电压所以信号电压必须在0到VCC之间。EN引脚是低电平使能直接接地让它一直工作就好。S0到S3接MCU的4个GPIO注意别接到ADC引脚上普通推挽输出GPIO就行。我当时是接到PA0到PA3软件里用位操作直接控制。2.2 通道地址映射S0是最低位别接反S0到S3这4根线组合出来的二进制数就是通道号S0是最低位S3是最高位。也就是说S3S2S1S00000选中Y00001选中Y10010选中Y2以此类推1111选中Y15。S3S2S1S0选通通道0000Y00001Y10010Y20011Y31111Y15这个映射关系本身不复杂但有个很常见的坑地址线接反。如果画PCB时把S0和S3的顺序搞反或者飞线时图省事随便插就会出现通道乱序——你以为在选Y3实际选中的是Y12二进制取反再反转。排查这种问题很浪费时间因为代码看起来完全没问题真值表也对得上就是硬件顺序错了。我建议在原理图上就把S0、S1、S2、S3标注清楚画PCB时按顺序摆放飞线调试时照着顺序表一根根核对。收到芯片后可以用万用表先做个快速验证给S0到S3分别置1测公共端Z和哪个Y引脚导通确认顺序正确再写上层逻辑。2.3 除了导通电阻还要注意的电气参数4067的供电范围是2V到6V3.3V系统可以直连5V系统也行。数字输入S0到S3兼容TTL/CMOS电平和单片机GPIO直连没问题。导通电阻Ron需要关注一下不同供电电压下数值差异很大。在4.5V供电时大概40到70欧姆3.3V供电时更大一些而且Ron会随输入电压变化不是一个固定值。对高阻抗负载来说这个电阻影响很小因为后续ADC输入阻抗极高Ron上几乎不分压。但如果传感器输出阻抗本身很高比如10kΩ以上的分压电路Ron会跟它串联略微影响建立时间和精度。还有一个特性值得提CD74HC4067是break-before-make结构也就是先断后合。切换通道时它先把当前通道断开再接新通道这避免了两个输入信号瞬间短路的可能是个安全特性。但也意味着切换瞬间公共端Z会短暂处于高阻状态如果Z引脚后面没有电容维持电压电平会漂移这也是后面坑一的伏笔。3. 软件扫描16路的时序切地址、稳定时间、启动ADC的顺序不能乱3.1 最朴素的实现逐通道切换加读取有了前面的硬件基础软件逻辑其实很直白设置S0到S3对应通道号延迟一小段时间等信号稳定启动ADC转换读取结果然后切下一个通道。用STM32 HAL库实现的通道选择函数长这样// ch取值0~15对应Y0~Y15 static void CD74HC4067_SelectChannel(uint8_t ch) { HAL_GPIO_WritePin(S0_GPIO_Port, S0_Pin, (ch 0x01) ? GPIO_PIN_SET : GPIO_PIN_RESET); HAL_GPIO_WritePin(S1_GPIO_Port, S1_Pin, (ch 0x02) ? GPIO_PIN_SET : GPIO_PIN_RESET); HAL_GPIO_WritePin(S2_GPIO_Port, S2_Pin, (ch 0x04) ? GPIO_PIN_SET : GPIO_PIN_RESET); HAL_GPIO_WritePin(S3_GPIO_Port, S3_Pin, (ch 0x08) ? GPIO_PIN_SET : GPIO_PIN_RESET); }这段代码的核心就是通过位与和判断把通道号的每一位映射到对应GPIO引脚的电平状态。通道号是16以内的数低4位就是S0到S3的状态所以用位运算直接分解代码简洁不易错。然后读取一个通道的ADC值uint16_t CD74HC4067_ReadADC(uint8_t ch) { CD74HC4067_SelectChannel(ch); // 切换后等信号稳定1ms对于大多数直流信号足够 HAL_Delay(1); HAL_ADC_Start(hadc1); HAL_ADC_PollForConversion(hadc1, 10); uint16_t val HAL_ADC_GetValue(hadc1); HAL_ADC_Stop(hadc1); return val; }主循环里扫16路uint16_t adc_values[16]; void ScanAllChannels(void) { for (uint8_t ch 0; ch 16; ch) { adc_values[ch] CD74HC4067_ReadADC(ch); } }这段代码跑起来很简单16路全部扫一遍大约16毫秒对于100ms级别的巡检需求完全够用。3.2 为什么切换后要等一下再启动ADC很多第一次用模拟开关的人会问为什么切换通道后不能立刻启动ADC这不是多此一举吗答案藏在两个地方。第一4067内部的模拟开关从收到地址变化到实际导通新通道有一个短暂的传播延迟。虽然这个延迟是纳秒级的看起来微不足道但如果你在GPIO翻转后立即启动ADCADC的采样保持电路可能正好工作在开关还没完全稳定的窗口期。尤其是逐次逼近型ADC采样阶段会把当时的电压锁存到内部采样电容上如果锁存的是一个毛刺或过渡电平后面的转换过程再怎么精确都白搭。第二信号在外部阻抗链路上的建立需要时间。通道切换后公共端Z到ADC采样电容之间的等效电路是一个RC充放电回路信号源内阻加上4067的导通电阻再串联到ADC内部的采样电容。根据最大传输功耗理论要让电容上的电压充到0.1LSB以内需要大约8.3个时间常数这个数字很多人不知道。实际计算一下就有概念了如果外部信号源内阻10kΩ4067导通电阻60ΩMCU内置ADC采样电容大约8pF那么时间常数τ (1000060) × 8pF ≈ 80ns8.3τ大概0.66μs。这种情况下几微秒的等待就够了。但注意如果公共端加了一个外部滤波电容比如0.1μF那τ (1000060) × 0.1μF ≈ 1ms8.3τ就到了8.3ms。这时候如果再只用1ms延时根本不够稳定。3.3 不想死等1毫秒丢第一枪的技巧如果觉得每路都HAL_Delay(1)太慢有一个很实用的技巧切换通道后连续采样两次第一次的值直接丢弃用第二次的值。原理很简单第一次ADC转换的过程本身就给了模拟开关和RC电路足够的建立时间转换完成时信号早就稳了。第二次采样时读到的就是确定值。代价是每路多花一次转换时间STM32F1的ADC一次转换在几十微秒级别16路加起来也就增加几百微秒远比8ms的延时快。uint16_t CD74HC4067_ReadADC_Fast(uint8_t ch) { CD74HC4067_SelectChannel(ch); // 第一次转换只用来等信号稳定结果丢弃 HAL_ADC_Start(hadc1); HAL_ADC_PollForConversion(hadc1, 10); HAL_ADC_GetValue(hadc1); HAL_ADC_Stop(hadc1); // 第二次才是有效数据 HAL_ADC_Start(hadc1); HAL_ADC_PollForConversion(hadc1, 10); uint16_t val HAL_ADC_GetValue(hadc1); HAL_ADC_Stop(hadc1); return val; }这个丢第一枪的思路在处理模拟开关、多路复用器这类器件时非常通用值得记下来。不用去精确计算RC常数也不用手动调延时用软件换时间简单可靠。4. 调试坑一通道刚切完就采样读回上一路的残留值4.1 现象16路数据里总有几个通道的值不对劲第一版固件跑起来后串口输出的16通道数据给了我一个下马威大部分通道读数正常但每隔几个周期总有某个通道的数据突然偏离正常范围有时高出一大截有时又低很多。更奇怪的是这个异常值和它前一个通道的值有明显的关联——如果前一个通道接的是3.3V后一个通道接的是0.9V那后一个通道的异常读数就偏向3.3V的方向看起来像两个电压的某种混合体。我开始怀疑传感器本身有问题。单独测那个传感器读数正常把异常通道排到第一个去采也正常。这就说明问题出在前一个通道切换过来的过程里。4.2 排查过程示波器看到了过渡台阶把示波器探针夹在4067的公共端Z上手动切换通道观察波形。高阻探针下能看到一个很细微的台阶切换后公共端的电压不是瞬间跳到新值而是要经过一个短暂爬升或回落的过渡过程。这个过渡过程的形状和方向取决于前后两个通道之间的电压差还有外部电路里寄生电容的大小。如果在这个过渡过程还没结束时启动ADC采样结果自然就是一个中间值既不是前一路也不是后一路。而由于逐次逼近ADC在采样阶段会锁定电压后面怎么转换都无法修正这个错误输入。再往深里挖有两个叠加的机制一是4067先断后合的结构特性断开旧通道后公共端短暂悬空电压靠寄生电容维持容易发生漂移二是信号源内阻加导通电阻与采样电容组成的RC建立过程需要一个时间窗口才能完全稳定。所以问题本质上是采样时机过早而不是芯片本身有问题。4.3 解决办法延时加对问题就消失第一版代码里我用了HAL_Delay(1)但实际排查发现还不够——因为在公共端到ADC引脚之间我加了一个0.1μF的滤波电容导致RC时间常数被拉大了好几个数量级。前面算过10kΩ源阻抗加0.1μF电容时间常数就1ms达到12位精度需要8ms以上。1ms的延时相当于只等了72%稳定误差还有好几个LSB。把延时改成10ms之后所有通道的数据都稳了。这个值在应用中完全可接受扫描16路也就160ms对慢变信号来说绰绰有余。提示如果你的电路里加了滤波电容延时一定要按8.3倍RC时间常数去估算不要拍脑袋写1ms。这也是很多人在低速采集时觉得数据飘忽不定的隐藏原因。如果不想用死等就用前面说的丢第一枪策略。我最终把这两种方案结合绝大多数通道用丢第一枪的方式每个通道读取前先给一次无效转换作为建立时间对特别敏感的通道再额外加一个ms级延时。5. 调试坑二悬空的未用通道成了偷走精度的天线5.1 现象没接传感器的通道让正常的通道也乱了第二个坑比第一个更隐蔽。板子上16路只用了10路剩下6路芯片引脚在PCB上什么都没接。最初没当回事结果发现一个诡异的现象单独测某一路接好的传感器时ADC读数很稳定但16路全扫时同一路的读数明显变抖跳动幅度能达到几十个LSB。我把问题定位到电源上给模拟电源又加了一堆滤波电容没有改善。又怀疑是MCU内部多通道切换的串扰关掉了所有外设只跑ADC还是抖。折腾了好一阵子最后用示波器看公共端Z的波形才发现明明选中了一路干净的直流信号Z脚上却叠着一层几十kHz的高频噪声幅度还不小。5.2 根因高阻悬空引脚就是一根内置天线这层高频噪声是哪来的逐一断开输入排查时发现了关键线索当手指靠近那几路悬空的芯片引脚时噪声波形幅度立刻变化。这就说明噪声正是从那几路悬空引脚引进来的。原理说起来也不复杂。CMOS模拟开关输入引脚在没有外部连接时处于高阻状态板上那些快速翻转的数字信号PWM、SPI时钟、甚至GPIO中断线会通过PCB走线之间的寄生电容把噪声耦合到这些悬空引脚上。而芯片内部每个通道和公共端之间存在寄生电容即使未被选中悬空引脚上的噪声依然可以穿过芯片耦合到公共端Z最终进入ADC。相当于这几路没有使用的引脚变成了板上的噪声天线专门给采样点灌干扰。这也是为什么调试时只测单路正常全部通道一起跑就飘——因为那些悬空引脚平时不被关注但干扰一直在。5.3 解决办法悬空通道接地公共端加RC低通解决的办法有三层从根治到兜底排开。第一层也是最彻底的把不用的通道引脚全部接地。如果PCB还没画完就把Y0到Y15没用到的引脚直接打过孔到GND如果板子已经做好了飞线或者焊接时把悬空引脚就近用导线接到地。接地后高阻节点变成低阻固定电位天线效应基本被掐断。第二层在公共端Z到ADC输入之间加一个RC低通滤波器。我用的是100Ω串联电阻加0.1μF对地电容截止频率大约16kHz足以滤掉绝大多数数字噪声又不会明显拖慢慢变直流信号的建立。加了这层之后噪声峰值从前面的数十LSB降到了两三个LSB左右。但要注意这个电容同时会拉长RC建立时间回到坑一说的那个计算关系延时要跟着调整。第三层PCB布局时让模拟输入走线远离数字信号线。特别是PWM输出、SPI数据线这类高频翻转信号和模拟走线之间至少留出地线间隔。实在避不开需要平行走线的话尽量缩短平行长度中间插一条地线做隔离。实测改进前后数据对比悬空引脚全部接地之前正常通道的ADC读数跳动范围大约±15个LSB12位ADC参考电压3.3V时约等于±12mV接地并加RC滤波后跳动范围缩小到±2个LSB以内效果非常明显。这套组合处理下来16路数据肉眼可见地稳定了。6. 实操优化滤波、两点校准和不用傻等的扫描节奏6.1 软件滤波中值加滑动平均成本极低硬件处理完毕软件层再补两道滤波能让数据更干净。中值滤波对脉冲式干扰效果很好比如偶尔的电源毛刺、开关噪声。对于3个采样值取中值代码很短uint16_t ADC_Median3(uint16_t a, uint16_t b, uint16_t c) { uint16_t tmp; if (a b) { tmp a; a b; b tmp; } if (b c) { tmp b; b c; c tmp; } if (a b) { tmp a; a b; b tmp; } return b; }滑动平均对高斯白噪声更有效原理是维护一个固定长度的窗口每次采样后把最早的值丢掉用窗口内平均值作为滤波结果#define FILTER_N 8 static uint16_t filter_buf[FILTER_N]; static uint8_t filter_idx 0; static uint32_t filter_sum 0; uint16_t ADC_Smooth(uint16_t raw) { filter_sum - filter_buf[filter_idx]; filter_buf[filter_idx] raw; filter_sum raw; filter_idx (filter_idx 1) % FILTER_N; return (uint16_t)(filter_sum / FILTER_N); }实际使用上我习惯先用中值3滤掉毛刺再用滑动平均把残余的随机噪声压下来。两段代码加起来不到20行CPU开销几乎可以忽略。6.2 通道差异与两点校准每个通道单独算系数第2节里提到过每个通道的导通电阻存在差异走线阻抗也不同。如果应用对精度要求比较严逐通道校正是必要的。校准思路是两点校准。对每个通道分别输入一个已知的低电压V1和一个已知的高电压V2记录ADC原始值ADC1和ADC2然后算出该通道的增益系数和偏移量scale (V2 - V1) / (ADC2 - ADC1) offset V1 - scale * ADC1实际使用时每个通道的真实电压就是V_real scale * adc_raw offset这个校准公式对每个通道独立计算按通道号存一组参数。操作上我一般用精密电阻分压产生两个参考电压或者直接用一个可调稳压源分别设到1.0V和3.0V每个通道跑一遍采样记录原始值。整个过程写个上位机串口命令就能自动化半分钟测完16路。如果ADC本身的非线性比较明显可以再做三点校准在中间电压点再取一个样本用分段线性或者二次插值去逼近真实曲线。但大多数12位逐次逼近ADC在小信号范围内线性度够用两点校准已经能到很好的效果。6.3 扫描节奏优化时间片轮询别傻等如果最后对扫描周期还有要求可以优化扫描节奏不用每路都阻塞延时。思路是用一个状态机每次进入扫描函数时先处理上一路的ADC转换结果再切换下一路通道并启动转换然后立刻返回主循环干别的事。等到下一次调用扫描函数时ADC转换早就完成了只需要读取结果再切换下一路。uint8_t scan_ch 0; uint16_t adc_values[16]; uint8_t conv_started 0; void ScanADC_NonBlocking(void) { // 如果上一轮启动了转换现在读取结果 if (conv_started) { adc_values[scan_ch] HAL_ADC_GetValue(hadc1); HAL_ADC_Stop(hadc1); scan_ch (scan_ch 1) 0x0F; conv_started 0; } // 切换新通道启动转换 CD74HC4067_SelectChannel(scan_ch); HAL_ADC_Start(hadc1); conv_started 1; }只需要一个标志位记录当前有没有正在进行的转换每次调用函数都会推进通道但中间不产生任何阻塞延时。ADC转换期间主循环可以去做通信、显示、按键扫描整体效率高很多。对于STM32这类带DMA和定时器的芯片还可以用定时器触发ADC DMA搬运但那种方案更适合固定通道连续采样对模拟开关这种需要GPIO参与通道切换的场景状态机方案已经足够灵活高效。最后说点个人体会。CD74HC4067这颗芯片本身没什么技术门槛便宜、皮实、信号链路简单难的是那些容易想当然的细节。通道切换后需要时间稳定、悬空引脚会引入串扰这两个问题如果没人提醒很可能在项目后期让人抓狂。做模拟多路采集第一优先是理清时序第二优先是处理好每一个未使用的引脚做好这两点16路扩展ADC的方案就能稳稳运行很久了。
📌 标签:
工业官网
设计趋势
AI 建站
SEO
获取完整报告 →
RELATED ARTICLES
推荐阅读
2026/9/8 6:47:30
LLC电源设计与仿真全解析:从谐振原理到参数计算与调试
2026/9/8 6:42:30
UEFI与Windows启动流程全解:从固件到内核的完整链路
2026/9/8 6:42:30
不该存在的排序算法:从Bogo Sort到Stalin Sort的随机性与复杂度边界
2026/9/8 7:27:33
phpcmsv9后台模板定制实战:目录机制、改造步骤与踩坑指南
2026/9/8 7:27:33
SpringBoot+WebSocket实现实时消息推送实战指南
2026/9/8 7:27:33
VideoLineForJS:纯JS视频回放轴组件设计与海康设备对接实践
2026/9/8 7:27:33
Riffn:为AI Agent与本地模型搭建即时语音对话链路
2026/9/8 7:27:33
C盘满了怎么清理?Windows系统盘空间释放与自动化维护指南
2026/9/8 7:22:32
硬件工程师面试20个高频问题:从去耦电容到信号完整性深度解析
2026/9/8 0:02:01
中国车企再破谣言,GAC吉利零跑获欧盟安全五星
2026/9/8 0:02:01
Compose Hot Reload新增MCP服务器助AI智能体调试
2026/9/8 0:02:01
你熟悉的GoPro正在悄然改变
2026/9/8 0:43:11
超人会飞不算本事:系统稳定依赖清晰规则与边界设计
2026/9/8 1:13:27
超人VS蜘蛛侠:拆解超级IP的影响力与传播方法论
2026/9/8 2:18:22
基于CNN的调制信号识别:MATLAB实现时频图分类实战