嵌入式项目里但凡采集通道一多ADC 引脚不够用是迟早的事。我这阵子调试一块板子需要同时挂十几个传感器和电位器做参数监测MCU 自带的 ADC 通道就 8 路去掉被占用的还剩 5 路能用怎么都绕不过去。最后选了 CD74HC4067 来做多路复用扩展一路 ADC 变 16 路问题直接解决。这篇笔记就专门记一下用 CD74HC4067 扩展 16 路 ADC 采集的完整思路外加调试过程中踩到的两个特别容易翻车的坑。这芯片不是什么新鲜东西但越是常见的器件用起来越容易在一些不起眼的细节上栽跟头。文章里我不会只给电路图和代码更重要的是把选型逻辑、时序控制、精度影响这些真正影响项目成败的东西讲清楚。不管你是刚开始学 STM32 的新手还是已经在做多通道采集的老手这篇笔记都值得花几分钟看完尤其那两个坑能帮你省下半天抓耳挠腮的时间。1. 整体设计思路与方案选型1.1 为什么不是换 MCU也不是加 ADC 芯片遇到 ADC 通道不够通常有三条路换更多 ADC 引脚的 MCU、外挂独立 ADC 芯片、用多路复用开关扩展。我最后选了第三种但这里把四条路都捋一遍方便你根据自己的项目情况判断。换更高端的 MCU 是最省心的办法但代价也最直接——芯片单价上涨、PCB 可能要重新画、原有代码要迁移。如果项目只是定型的板子临时加几路采集这个成本不太划算。外挂独立 ADC 芯片比如 ADS1115、ADS1256 或者更专业的 24 位芯片适合对采样精度和速率有高要求的场景但价格贵、驱动复杂、占用通信接口有时候 SPI 或 I2C 总线已经被其他外设占满反而引入新的资源冲突。CD74HC4067 这类模拟多路复用器的思路完全不一样——它不转换信号只做开关。一个 COM 公共端接 MCU 的 ADC 输入16 个通道分别接外部传感器通过 4 根控制线( S0~S3编码选通某一路就相当于用 4 个 GPIO 换 16 路模拟输入。成本几毛钱一片无源器件外围基本为零唯一的花费是控制代码里多做一组地址切换逻辑。整体算下来是最贴合扩通道但不动核心板需求的方案。1.2 CD74HC4067 的核心参数与选型依据CD74HC4067 在同类器件里属于各方面都足够均衡的选择。导通电阻 Ron 典型值在几十欧姆量级信号源内阻不高时压降可以忽略。通道漏电流很小微安级别不会对被采信号造成明显影响。支持双向导通既能做多路采集也能做多路输出灵活性高。工作电压范围 2V~10V轻松覆盖 3.3V 和 5V 两种常见系统电压。有对比才有选择。CD4051 是 8 通道的16 路需要两颗74HC4051 虽然也很流行但同样只有 8 路。如果想更低导通电阻可以看 ADG1606性能更好但价格也上去一个数量级。对于大多数传感器采集场景CD74HC4067 的 60~70 欧姆 Ron 完全够用前提是信号源的输出阻抗足够低。如果传感器是高阻抗输出比如几十千欧以上的电位器分压电路就需要额外处理这个留到后面调试坑详细说。2. 硬件连接与信号完整性设计2.1 管脚功能与典型接线CD74HC4067 是 24 脚封装关键管脚不多功能划分很清晰管脚功能说明S0 ~ S3通道选择地址线4 位二进制编码0000~1111 对应 X0~X15X0 ~ X1516 路模拟输入/输出接外部传感器信号COM公共端接 MCU 的 ADC 输入引脚EN / INH使能端 / 禁止端低电平使能高电平关闭所有通道VCC、GND电源与地建议加 100nF 去耦电容接线看起来简单但有几个细节必须抠。首先是 COM 端这一根线会同时连接 MCU 的 ADC 输入引脚和 4067 的 COM中间不要串联任何电阻否则串阻和 4067 导通电阻直接分压给 ADC 采样结果带来系统性误差。其次是 EN 端如果单片机上电瞬间 GPIO 状态不稳定建议通过 10kΩ 下拉电阻直接接地保证 4067 在所有 GPIO 初始化完成之前保持使能但通道不确定的状态不会导致误采。这里有个微妙的问题——EN 接地会让通道在浮空状态下导通如果外部传感器输出阻抗很高反而可能引入不确定通路所以最稳妥的做法不是下拉 EN而是上拉到 VCC默认禁用等代码里明确完成 GPIO 初始化后再拉低使能。这个我在第 5 节调试坑里会再展开。2.2 电源去耦与地线布局CD74HC4067 内部是 CMOS 开关结构工作时会有瞬态电流切换如果电源引脚旁边不放去耦电容PCB 走线上的电感会产生毛刺这些毛刺会通过衬底串到模拟开关通道上最终体现在 ADC 采样值上就是莫名其妙的抖动。常规做法是在 VCC 和 GND 之间放一个 100nF 陶瓷电容位置尽量靠近芯片管脚。如果系统里还有其他数字电路在同时切换最好再加一个 10μF 钽电容做低频滤波。地线布局上有一个很容易被忽视的点CD74HC4067 的 GND 和 MCU 的模拟参考地必须处于同一个地平面不能隔着长的走线绕一圈再回来。原因是 ADC 采样时参考电压是 MCU 的 VREF 对 GND 的差值如果 4067 的 GND 电位和 MCU 的 AGND 之间有压差哪怕是毫伏级对于参考电压为 3.3V、12 位分辨率1 LSB ≈ 0.8mV的系统来说已经足以造成几位 ADC 码值的偏移。换句话说地线的干净比信号线上的走线短更重要。2.3 信号源阻抗与采样保持时间的关系这是整个设计里最容易被低估的问题。ADC 采样不是瞬间完成的它内部有一个采样电容需要在采样窗口时间内把电压充到目标值附近。如果信号源阻抗过高采样电容充电时间常数变大在固定的采样周期内电压充不满ADC 读到的值就会低于实际值。CD74HC4067 的 Ron 只有几十欧姆这个不是主要问题。真正的问题是外部传感器自身的输出阻抗。有些传感器是专门驱动低阻抗负载的比如运放输出、稳压源这种直接接 4067 没问题。但还有很多常见的场景比如用电阻分压取得的电压信号电位器、热敏电阻分压等等效输出阻抗就是分压电阻的并联值可能是 10kΩ、甚至 100kΩ 量级。这种情况下信号源阻抗、4067 导通电阻、以及 MCU 内部采样电容三者串联充电时间常数可能超过 ADC 的采样窗口。解决办法有三个按优先级排列在传感器输出和 4067 输入之间加一级运放缓冲电压跟随器大幅度降低输出阻抗。如果传感器本身是低阻抗的保证 4067 选通后延时足够长再触发 ADC 采样。在 MCU 初始化时把 ADC 采样周期配置到最大比如 STM32 的采样周期可配置为 239.5 个 ADC 时钟周期提高采样保持时间。我自己的实践是能加运放就加运放省事的话也可以先用大采样周期顶住但修不了高阻抗信号的根源问题。3. 软件驱动实现与关键代码解析3.1 GPIO 通道选通控制思路软件部分的核心就一句话通过 4 根 GPIO 输出不同的电平组合选中 4067 的对应通道然后让 MCU 的 ADC 采集 COM 引脚的电压。听起来很简单但工程上对 GPIO 的操作要讲究稳定性。CD74HC4067 的选通逻辑是纯数字电平控制S0~S3 是高电平有效每个通道对应一个 4 位二进制地址。真值表如下通道S3S2S1S0X00000X10001X20010X30011...............X151111用 GPIO 直接驱动即可但要注意一点如果你用的 MCU 某些引脚在复位瞬间有短暂的高电平脉冲而 EN 端又被拉低使能状态就有可能在传感器上瞬间施加一个不确定的电压。所以代码里要先把所有通道选择引脚初始化为输出低电平再去控制 EN 引脚。换句话说初始化的顺序很关键先保证 S0~S3 都有确定的低电平输出再使能 4067。3.2 STM32 HAL 库驱动代码示例下面给出一个基于 STM32 HAL 库的示例用 GPIO 模拟 4067 通道切换然后调用 ADC 读取一次转换结果。代码层面做了两层封装底层是通道切换函数上层是切通道 采样 返回的完整流程。#include main.h // 假设硬件连接 // S0 - PA0, S1 - PA1, S2 - PA2, S3 - PA3 // COM - PA4 (ADC1_IN4) // EN - PA5低电平使能 #define MUX_S0_Pin GPIO_PIN_0 #define MUX_S1_Pin GPIO_PIN_1 #define MUX_S2_Pin GPIO_PIN_2 #define MUX_S3_Pin GPIO_PIN_3 #define MUX_EN_Pin GPIO_PIN_5 #define MUX_GPIO_Port GPIOA // 设置 4067 的通道0~15 void MUX_SetChannel(uint8_t channel) { channel 0x0F; // 只取低 4 位防止越界 HAL_GPIO_WritePin(MUX_GPIO_Port, MUX_S0_Pin, (channel 0x01) ? GPIO_PIN_SET : GPIO_PIN_RESET); HAL_GPIO_WritePin(MUX_GPIO_Port, MUX_S1_Pin, (channel 0x02) ? GPIO_PIN_SET : GPIO_PIN_RESET); HAL_GPIO_WritePin(MUX_GPIO_Port, MUX_S2_Pin, (channel 0x04) ? GPIO_PIN_SET : GPIO_PIN_RESET); HAL_GPIO_WritePin(MUX_GPIO_Port, MUX_S3_Pin, (channel 0x08) ? GPIO_PIN_SET : GPIO_PIN_RESET); } // 使能 / 禁止 4067 void MUX_SetEnabled(uint8_t enable) { // EN 低电平使能 HAL_GPIO_WritePin(MUX_GPIO_Port, MUX_EN_Pin, enable ? GPIO_PIN_RESET : GPIO_PIN_SET); } // 初始化 void MUX_Init(void) { // 先让所有选择引脚输出低电平再使能 4067 HAL_GPIO_WritePin(MUX_GPIO_Port, MUX_S0_Pin | MUX_S1_Pin | MUX_S2_Pin | MUX_S3_Pin, GPIO_PIN_RESET); HAL_GPIO_WritePin(MUX_GPIO_Port, MUX_EN_Pin, GPIO_PIN_SET); // 上电默认禁止通道 MUX_SetChannel(0); MUX_SetEnabled(1); // 确认所有选择引脚稳定后再打开使能 } // 读取指定通道的 ADC 值 uint16_t MUX_ReadChannel(ADC_HandleTypeDef *hadc, uint8_t channel) { MUX_SetChannel(channel); HAL_Delay(1); // 等待通道切换稳定具体延时可以优化后面会讲 HAL_ADC_Start(hadc); HAL_ADC_PollForConversion(hadc, 10); // 等待转换完成超时 10ms uint16_t adc_val HAL_ADC_GetValue(hadc); HAL_ADC_Stop(hadc); return adc_val; }这段代码里有个关键细节值得多说一句HAL_Delay(1)看起来像是简单的延时实际上它在选通新的通道之后给了信号链路足够的时间建立稳态电压。这个延时到底需要多长取决于信号源阻抗和系统噪声特性不是一个固定的万能值。在实际项目中我会用示波器观察 COM 端的波形调整这个延时到波形完全稳定后再采样。典型的做法是先写 1ms然后看采样值的随机抖动如果抖动明显就逐步加大到 2ms、5ms 甚至 10ms。如果采样速度要求高这个延时也可以从HAL_Delay 固定等死改成DMA 定时器触发轮询但原理不变——保证通道切换后的建立时间足够。3.3 批量扫描与数据处理模式实际项目里不会只采一路就完事通常要按顺序扫完 16 路然后做相应的处理。最简单的做法就是循环调用MUX_ReadChannel但这样做有两个问题一是每路都重新启动 ADC浪费周期二是每次切换后都需要重新等待稳定整体扫描速度会被拖慢。如果对采样速度没有太高要求比如慢速传感信号0.5Hz 变化率上面的轮询方式完全够用。但如果要快速扫描多路建议进一步优化一次性选通一个通道等稳定后连续采样多次再做平均值然后切下一路。这种选通→采样多次→平均→切换的模式既能保证信号建立时间不被浪费又能通过多次采样均值滤波掉随机噪声。#define SAMPLE_COUNT 8 // 每个通道连续采样次数 uint16_t MUX_ReadChannelAvg(ADC_HandleTypeDef *hadc, uint8_t channel) { uint32_t sum 0; MUX_SetChannel(channel); delay_us(200); // 用微秒级延时快速稳定 for (int i 0; i SAMPLE_COUNT; i) { HAL_ADC_Start(hadc); HAL_ADC_PollForConversion(hadc, 10); sum HAL_ADC_GetValue(hadc); HAL_ADC_Stop(hadc); } return (uint16_t)(sum / SAMPLE_COUNT); }这里的均值滤波属于最基础的时域滤波在 16 路轮询场景下非常重要因为 4067 这种模拟开关切换瞬间产生的毛刺会被 ADC 捕捉到如果不做处理最后的数值可能在某一路和下一路之间来回跳动。更高级的处理方式是滑动滤波或中值滤波但那是后话按需选择即可。4. 实操过程与完整调试记录4.1 硬件焊接与初步验证我这次用的是 STM32F103 开发板加一片 CD74HC4067 模块模块上自带电平转换和滤波电容省了不少麻烦。接线方式就是典型的 4 根地址线 1 根使能线 1 根公共输出线加上电源三条线总共 9 根线。上电之后的第一个验证步骤不是写复杂代码而是用一个可调电压源或电位器分压接到 X0然后用万用表或者直接看 ADC 读数验证选通逻辑是否正确。具体做法是在代码里固定选择 X0然后调整电压源输出从 0V 到 3.3V看 ADC 读数是否线性变化。如果读数不是预期的线性关系优先检查 S0~S3 的接线顺序是不是反了——这个错误在实际操作中特别常见因为面包板或杜邦线很容易插错一位导致通道映射错乱。验证 X0 正常之后再测 X1、X2、X15 各一路确认 4 根地址线的每一位都真实有效。这里有个小技巧不要从头到尾把 16 路都测一遍浪费时间只要测 X00000、X10001、X20010、X40100、X81000、X151111这六路就能覆盖 S0~S3 每一位的高电平和低电平组合基本可以确认所有地址线的正确性。4.2 空载通道噪声测试通道全部接好传感器之前先做一次空载测试也就是所有输入引脚都没接传感器只把 COM 接到 ADC 引脚上。理论上 4067 的通道开关在没有外部信号时输入引脚是浮空的ADC 读数会随机漂移。这个漂移本身不代表设计有问题但它能反映电路的噪声底。实测下来我的板子空载时有几路 ADC 读数在 200~80012 位满幅 4095之间随机跳这是一个信号。如果所有通道都稳定在一个固定值附近反而不太正常——因为完全浮空的输入就该有噪声。但如果噪声幅度太大比如占满整个量程就要查电源和地的问题了。我这次实际情况是有几路浮空电压跟着相邻通道走比如说 X0 接了 2VX1 浮空X1 居然能读到 1.6V 左右这个现象直接引出了后面要讲的第二个调试坑。4.3 选定采样参数与精度标定硬件连接和基本通道测试通过后开始做精度标定。这里有个关键选型ADC 的采样时钟和采样周期怎么配。STM32F103 的 ADC 时钟最高 14MHz我的项目里设置为 12MHz采样周期选 71.5 个周期适合中等阻抗信号实测对大部分传感器输出已经够用。如果信号源阻抗偏高比如 20kΩ 以上就需要把采样周期跳到最大 239.5 个周期代价是单次采样时间从十几微秒增加到几十微秒但对于 16 路轮询的场景完全可接受。精度验证方法很简单用一个精确的电压源或者高精度万用表校准过的电压分别加到几个通道上对比 ADC 读数和万用表读数。我测了三组电压0.5V、1.5V、2.5V计算每路的 ADC 换算值和万用表读数之间的误差。最终结果在满量程范围内误差约 ±15mV折合 12 位 LSB 大约 5~6 个码值主要来源是 4067 的导通电阻引起的微小分压和参考电压精度。这个精度在电池电压监测、室温检测这类场景完全够用但如果你做的是高精度测量仪器就需要搭配精密参考源和校准补偿算法了。5. 两个调试坑的完整复盘5.1 坑一高阻抗信号源在 4067 上产生系统性压降先说现象我把一个 10kΩ 电位器接到 X0旋到中间位置万用表量电位器输出是 1.65V但 ADC 读回来只有 1.58V差了大约 70mV。换到另一个电位器发现误差随着阻值变大而增大20kΩ 电位器误差到了 130mV 左右。一开始我以为是 4067 的导通电阻导致的压降但算了一笔账4067 的 Ron 大概 60Ω1.65V 的源电压在 60Ω 上要产生 70mV 压降电流需要 1.16mA。而 MCU 的 ADC 输入阻抗通常是兆欧级别的10kΩ 电位器分压中点等效阻抗是 5kΩ通过 60Ω 导通电阻后给 ADC 采样电容充电稳态误差应该在毫伏级别不至于到 70mV。真正的问题是 ADC 采样保持电容和外部等效电容之间的电荷再分配效应。电位器分压中点的等效输出电阻约 5kΩ4067 导通电阻 60ΩMCU 采样电容通常为几 pF 到十几 pF。ADC 开始采样时采样开关闭合内部采样电容与外部导电路径上的寄生电容并联电荷重新分配导致采样电压下降。如果采样时间不够长采样电容就没有充分充电回到外部电压的真实值。解决这个问题有两个方向。软件方案增大 ADC 采样周期到最大239.5 个时钟周期实测误差可以缩小到 15mV 以内因为采样窗口拉长给了采样电容更长的充电时间。硬件方案在每个 4067 通道上并联一个 100nF 电容到 GND作为信号保持电容让 ADC 在采样时从电容中取电荷而不是直接从高阻抗源取。这两个方案可以叠加用信号质量会更好。强烈建议如果板子上空间允许每个通道并联一个 100nF 到 GND这个电容能让 4067 的通道瞬态响应好很多。5.2 坑二通道切换瞬间的串扰导致读到隔壁通道的电压第二个坑比第一个更难排查现象也更有迷惑性。扫完 16 路之后我发现有几路 ADC 读到的值和实际传感器电压对不上甚至有一路换了传感器之后读数不变。一开始怀疑硬件接触不良重新焊了接口没解决又怀疑代码逻辑有问题单步调试了一个下午才找到根因。根因是 4067 在切换通道的瞬间通道间的寄生电容会耦合。特别是从高电压通道切到低电压通道时ADC 采样电容还保持着上一个通道的电压电荷如果切换后立刻开始采样采样电容上的残留电压没有放干净导致新的采样结果偏高。这个现象在相邻通道电压差很大时特别明显。比如 X0 接 3.3VX1 接 0V从 X0 切到 X1 后立即采样X1 的读数可能是 0.3V 到 0.4V而不是真正的 0V。排查方法用示波器同时观察 COM 引脚和地址线 S0 的波形能清楚地看到地址线跳变后COM 引脚电压需要一定时间才能稳定到新的目标值。这个稳定时间取决于外部传感器阻抗和 4067 导通电阻组成的 RC 时间常数在 10kΩ 电位器场景下可能需要 200~500μs 才能完全稳定。解决办法有几个层面。最简单的就是在MUX_SetChannel和 ADC 启动之间加一个稳妥的延时——但注意不是随便 delay 就行最好是在切换地址后先把 4067 的 EN 引脚拉高禁止所有通道等地址线稳定后再拉低使能。这个操作相当于先断开再开关彻底避免通道切换瞬间的串扰过渡态。实测下来先禁止、再切换地址、再延 100μs、再使能能有效消除残留电荷干扰。其实这一步的原理和数字电路里的先脱开再切换再接入完全一样但很多人意识不到模拟开关也需要这种操作。改良后的代码流程uint16_t MUX_ReadChannelClean(ADC_HandleTypeDef *hadc, uint8_t channel) { // 1. 禁止所有通道让 COM 悬空 MUX_SetEnabled(0); // 2. 切换地址线 MUX_SetChannel(channel); // 3. 等 100~200μs让地址线和内部开关稳定 delay_us(150); // 4. 重新使能目标通道 MUX_SetEnabled(1); // 5. 再等信号稳定后开始采样 delay_us(200); HAL_ADC_Start(hadc); HAL_ADC_PollForConversion(hadc, 10); uint16_t adc_val HAL_ADC_GetValue(hadc); HAL_ADC_Stop(hadc); return adc_val; }这个坑的核心教训是模拟开关不是理想开关切换的过渡态不能忽略。如果你在切换后立即采样读到的数据大概率是脏的。用禁用→切换→使能→稳定→采样这个流程基本可以绕开这个问题。实际调试中这个坑浪费了我将近一个下午希望看到这篇文章的朋友少走这个弯路。5.3 常见问题与排查速查表最后把这段调试过程中容易反复出现的几个问题整理成速查表方便你在现场快速定位问题现象可能原因检查方法解决方法所有通道读数都偏高或偏低信号源阻抗过高采样时间不足用示波器看 COM 引脚波形增大 ADC 采样周期通道对地加 100nF 电容相邻通道互相串扰通道切换后未等稳定就采样从 X0(3.3V) 切到 X1(0V) 观察读数切换后延时 200μs 以上再采样某个通道读数固定不变地址线虚接或 GPIO 初始化失败万用表量 S0~S3 电压排查接线和 GPIO 初始化顺序所有读数有规律跳动4067 电源去耦不良示波器量 VCC 纹波加 100nF 10μF 去耦电容采样值偶尔出现极大偏差采样期间发生 GPIO 中断导致时序错乱逻辑分析仪看采样时序采样前屏蔽中断用 DMA 方式采集6. 经验总结与可扩展方向用 CD74HC4067 扩展 ADC 通道这件事本质上就是在硬件成本和软件复杂性之间做权衡。一片几毛钱的芯片加上几行 GPIO 控制逻辑能解决 16 路模拟输入的需求在绝大多数低速采样场景下都是性价比很高的方案。但前提是你要对模拟开关的导通电阻、采样建立时间、通道隔离度这些参数有清醒的认知不能再把它当成理想导线来看待。根据我个人的实际体会再强调三个容易被忽视的操作心得。第一PCB 布局时 4067 尽量靠近 MCU 的 ADC 引脚COM 走线越短越好这条线是整个信号链路里最容易捡到噪声的地方。第二如果项目后续可能会扩展更多通道建议在软件层面做一个 mux_device 结构体把通道映射、延时参数、采样次数封装起来将来换用其他型号的多路复用器或者增加级联时上层代码改动会很小。第三所有传感器的公共地不要跟着信号线绕一圈回到 4067 的 GND要直接连回电源地否则地环路会把数字噪声灌进模拟通路。这个方案本身还有不少可以玩的空间。比如两片 4067 级联可以扩展到 32 路或者用 EN 端配合多块板卡做通道分组这些都属于在这个基础上的自然演进。下一篇调试笔记我打算记录一下基于这套 16 路采集系统做 PID 温控时的整定过程到时候会涉及到怎么在快速采样和数字滤波之间找到平衡。如果这篇笔记里的哪个环节写得不够清楚或者你在实际调试中遇到了其他奇怪的坑欢迎交流。
