
手里这个项目已经调了两天MCU 自带的 ADC 通道只有 8 路外围传感器加起来 12 路模拟量怎么排都排不够。换 MCU交期和成本都不允许。最后方案定为用 CD74HC4067 把一路 ADC 扩展成 16 路。这个芯片本身不贵接法也简单但真正调起来才发现模拟开关这个东西并不是把线接通那么简单。这篇笔记就把扩展方案的完整用法记下来重点写我踩过的两个坑一个是切换通道后采样值张冠李戴一个是所有通道电压系统性偏低排查链路和最终解决办法都会写清楚。如果你也在做多路模拟量采集或者正准备用 CD74HC4067 这类模拟多路复用器这篇应该能帮你少走不少弯路。1. 什么情况下才会想到用 CD74HC40671.1 换了三版 PCB 之后我决定不换 MCU这个项目的核心是一台小型环境监测设备需要采集温湿度、气压、光照、风速、风向、PM2.5、CO2 等多路传感器的模拟输出。有些传感器直接输出 0~3.3V 模拟电压有些是 4~20mA 电流环还有的是电阻分压输出。前期设计时预留了 8 路 ADC 输入当时觉得够用结果实际接传感器的时候发现光温湿度变送器就占了 4 路再加上其他传感器12 路往上一排8 路 ADC 直接爆了。当时摆在面前的有三个选择。第一个是换更大封装的 MCU比如从 64 脚换到 100 脚ADC 通道数确实多了但 PCB 要重新画、固件要重新移植、原来的样品全部浪费交期完全不允许。第二个是外挂一颗多通道 ADC 芯片比如 ADS1115、ADS1220 这类精度确实好但价格摆在那里一颗芯片十几块甚至几十块BOM 成本直接上去一截。第三个就是朋友推荐的方案用 CD74HC4067 模拟多路复用器一颗芯片几毛钱PCB 改一小块就行软件上写一个通道切换逻辑本质上是用时间换空间——一次只能采集一路但可以通过快速切换把所有路扫一遍。我做了一个粗略的对比表方案成本改动量精度采集速度换高引脚 MCU差异不大重新画板移固件不变不变外挂多通道 ADC高加 I2C/SPI 通信层通常更高同步采样CD74HC4067 扩展低改 GPIOADC 逻辑受模拟开关影响分时采样最后选了 CD74HC4067。事实证明这个选择在成本和时间上确实值得但代价是调试过程中踩了两个不踩不知道、一踩吓一跳的坑。下面先讲清楚这个芯片到底是什么、能做什么、不能做什么然后再进入具体的调试记录。1.2 4067 能做什么不能做什么CD74HC4067 本质上是一个 16 通道模拟多路复用器/解复用器。它内部是 16 个模拟开关公共端是一个 COM 引脚另外有 16 个通道引脚 C0~C15还有 4 个地址选择引脚 S0~S3。S0~S3 组合出 16 种状态每一种状态把对应的通道引脚和 COM 引脚之间的模拟开关导通于是 COM 引脚上就能看到那一路的模拟电压。它的基本参数值得记一下工作电压范围2V~6V典型 3.3V 或 5V导通电阻 Ron几十欧姆量级特定条件下典型约 70Ω 上下开关切换时间纳秒级但后面会说到电路实际的建立时间远远不止纳秒EN 引脚低电平有效EN 接 GND 时正常工作双向导通模拟信号既可以从 Cx 进 COM 出也可以反向拿它来扩展 ADC 采集是它的典型应用之一但它有几个明显的边界条件第一它是分时的。任意时刻只能导通一个通道16 路信号不可能同时被采集。如果你需要的是多路信号的同步采样——比如三相电流的波形分析——那 4067 就不合适应该用多通道同步采样 ADC。第二它不能处理超出电源轨的电压。模拟输入电压必须在 VCC 和 GND 之间否则芯片内部的 ESD 保护二极管会导通轻则测量不准重则损坏芯片。像我这次有一路 4~20mA 电流环信号先经过采样电阻变成电压再把电压范围分压到 0~3.3V 以内才能进 4067。第三它是有导通电阻的。这里先埋一个伏笔这是后面第二个坑的根源。2. 硬件接线和基础驱动半小时焊好注意这几点2.1 4067 的关键引脚和连接策略CD74HC4067 是 SOIC-24 或者 SSOP-24 封装也有 DIP-24 的焊起来不算难。我的连接方式如下VCC 接 3.3VGND 接系统地S0、S1、S2、S3 分别接 MCU 的 4 个 GPIO这 4 根线用于通道选择EN 引脚直接接地因为板子上只用了一片 4067不需要多片级联COM 引脚接 MCU 的 ADC 输入引脚C0~C15 分别接各路模拟信号VCC 引脚旁边放了一颗 100nF 去耦电容这个是标配这里有几个接线细节值得单独说。第一个是 S0~S3 的位权顺序。S0 是 LSBS3 是 MSB也就是说通道号 0~15 对应二进制地址 S3S2S1S0。如果把 S0 和 S3 接反了通道 1 会变成通道 8等真正调试的时候发现通道号和实际传感器对不上你会怀疑人生。最好一上来就用文字标注清楚别只靠颜色区分杜邦线。第二个是 COM 引脚到 MCU ADC 引脚之间的走线要尽量短。这个引脚上传输的是模拟信号走线太长容易耦合噪声。另外COM 到 ADC 之间的寄生电容会影响信号建立时间这也是坑一的根源之一。第三个是未使用的通道引脚必须处理。如果某个通道悬空它对应的模拟开关虽然没导通但引脚上的浮空电压可能通过内部漏电流耦合到 COM 引脚上造成串扰。我这次把不用的 C12~C15 直接接到 GND实测串扰明显减小。第四个是 GPIO 的模式设置。控制 S0~S3 的 GPIO 必须设置为推挽输出模式不能是开漏。如果在开漏模式下没有外部上拉电平状态不稳定通道地址就会乱跳。2.2 第一版测试驱动先让 16 个通道能切通接通硬件之后我先写了一个最简单的测试程序目的只有一个验证 16 个通道能不能通过 GPIO 地址切换全部切通。基于 STM32 HAL 库的代码如下// 假设 S0~S3 接在 GPIOB 的 PIN0~PIN3EN 接 PIN4 #define MUX_EN_PIN GPIO_PIN_4 #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 void MUX_SetChannel(uint8_t ch) { // 使能 4067EN 低电平有效 HAL_GPIO_WritePin(GPIOB, MUX_EN_PIN, GPIO_PIN_RESET); // 设置 S0~S3 HAL_GPIO_WritePin(GPIOB, MUX_S0_PIN, (ch 0x01) ? GPIO_PIN_SET : GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOB, MUX_S1_PIN, (ch 0x02) ? GPIO_PIN_SET : GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOB, MUX_S2_PIN, (ch 0x04) ? GPIO_PIN_SET : GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOB, MUX_S3_PIN, (ch 0x08) ? GPIO_PIN_SET : GPIO_PIN_RESET); } uint16_t MUX_ReadChannel(uint8_t ch) { uint32_t sum 0; uint8_t i; MUX_SetChannel(ch); for (i 0; i 5; i) { HAL_ADC_Start(hadc1); HAL_ADC_PollForConversion(hadc1, 10); sum HAL_ADC_GetValue(hadc1); } return sum / 5; }这个代码很简单但注意一点通道切换和 ADC 采样之间没有加任何延时。我当时天真地以为芯片切换时间才几个纳秒GPIO 写完之后立刻启动 ADC 采样应该没问题。结果正是这个没问题的想当然让我踩了第一个坑。3. 坑一切换通道后读到的电压还是上一路的3.1 现象和第一轮排查测试的时候我在 C0 上接了 1.0V 的稳定电压C1 上接了 3.0V 的稳定电压然后用串口打印轮流采集 C0 和 C1 的结果。理想输出应该是 1.0V、3.0V、1.0V、3.0V这样交替实际输出却总是 C0 采完 C1 之后C1 的第一次采样值仍旧读成 1.2V 左右要过一小段时间才慢慢爬升到 3.0V。这个现象非常经典。从 C0 切到 C1 的时候ADC 读到的还是接近 C0 的电压。如果只是第一次读错那可能是软件时序问题但它不是突变到 3.0V而是有一个爬升过程这就奇怪了。第一轮排查我先怀疑自己的 GPIO 配置有问题。在调试工具里打了一堆日志确认 S0~S3 的电平状态确实按照通道号正确变化了GPIO 输出没有毛病。第二轮排查怀疑是不是 ADC 配置里采样时间太短。我把 STM32 的采样周期从默认的 1.5 周期加到 55.5 周期情况有好转但不明显。C1 的第一次采样值从 1.2V 变成了 1.8V还是没有直接读对。这说明问题不在 ADC 采样时间本身或者说采样时间的增加只是间接缓解了问题没有命中根因。第三轮排查我拿万用表去量 4067 的 C1 引脚电压是标准的 3.0V再量 COM 引脚发现切换完成瞬间 COM 上的电压也接近 3.0V。万用表看到的是稳态值说明静态电平是对的问题出现在动态过程中。3.2 示波器抓到真凶模拟开关的建立时间直到我拿示波器同时测量两个点——一个是 MCU 的 ADC 输入引脚一个是 GPIO 上 S0~S3 的切换瞬间——才看清楚问题。示波器显示GPIO 切换到 C1 的地址之后ADC 引脚上的电压并不是立刻跳到 3.0V而是从原来的 1.0V 开始以一条指数曲线缓慢上升大约经过 20 多微秒才基本稳定到 3.0V。如果这时候 ADC 已经开始采样采到的自然是半路值。为什么会有这个建立过程这里要拆开两层原因来说。第一层CD74HC4067 的模拟开关内部和引脚上存在寄生电容COM 引脚到 ADC 输入引脚之间的 PCB 走线也有寄生电容。当开关从 C0 切换到 C1 时这些电容上还存着 C0 那一路的电荷。开关导通后信号源需要给这些电容充放电让电压从旧值过渡到新值。如果信号源内阻比较大充电电流小这个过渡时间就会变长。第二层ADC 内部还有一个采样电容。STM32 的 ADC 在采样阶段会通过一个内部开关把采样电容接入输入引脚采样电容上同样存着上一次的电压。这就相当于给旧电压又加了一个储存器要让这个电容充到新电压需要的时间更长。ADC 手册上给的采样周期就是这个电容充电所需的时间但它假设的是输入信号源内阻很低的情况。一旦前面串了 4067 的导通电阻和线路电阻充电时间常数变大原本的采样周期就不够用了。3.3 治标和治本延时采样与采样策略调整找到根因之后解决办法就很清晰了。我总结成三层策略从最简单到最彻底第一层切换通道后加延时。在MUX_SetChannel()之后延时几十到几百微秒再启动 ADC 采样。这个延时的长度取决于信号源内阻和线路电容。我实测下来信号源内阻 1kΩ 左右时延时 100μs 足够内阻 10kΩ 时需要加到 500μs 甚至 1ms。如果不知道自己的信号源内阻就用示波器实测 ADC 引脚上的建立时间然后留 2 倍以上余量。第二层丢弃切换后的前几次采样值。哪怕加了延时第一次采样也可能受残留电荷影响干脆先采几次丢掉只取后面的值。我在代码里做了 5 次采样前 2 次丢弃后 3 次取平均效果很稳定。第三层用定时器触发 ADC把通道切换和采样时序交给硬件管理。这个方式更适合周期性扫描所有通道的场景。基本思路是每次定时器中断里先切换通道等待一个固定的稳定时间后再触发 ADC 转换。如果配合 DMA可以把连续多次采样的结果直接搬到内存缓冲区CPU 只在全部完成后统一处理效率高很多。我最终用的是第二层方案。原因也很简单这个项目里 16 路模拟量本来就属于慢变信号温度、湿度、气压这些传感器的响应时间都是秒级几百微秒的采样延时完全不敏感。用最少的代码改动解决最核心的问题才是调试该有的思路。4. 坑二所有通道读数系统性偏低4.1 标准电压源都救不了的精度问题坑一解决之后16 路通道都能切通采样值也稳定了。但我很快发现另一个问题所有通道的读数都比预期偏低。我用一块精度 0.1% 的电压源给 C0 输入 2.500V程序读回来只有 2.31V 左右差了大几十毫伏。一开始我怀疑是 ADC 的参考电压不准。直接拿电压源接 MCU 的 ADC 引脚测量读数是 2.498V精准得很。这说明 MCU 这一侧没有问题问题出在从 C0 到 COM 再到 ADC 的这一条链路上。我又拿万用表量了 C0 引脚和 COM 引脚的电压。C0 是 2.500V而 COM 引脚上只有 2.42V 左右。对问题在这里——信号经过 4067 之后电压真的降低了。模拟开关不是一根理想的导线它内部是有导通电阻的。这个电阻和信号源的内阻串联分压把一部分电压吃掉了。4.2 导通电阻与采样电容之间的时间常数账CD74HC4067 的导通电阻大概在几十欧到一百多欧之间受供电电压和工作温度影响。当我用低内阻的电压源直接输入时电压源内阻可以忽略不计那 70Ω 左右的 Ron 上分到的电压应该很小为什么实测掉了 80mV这里就要把两个因素合起来算了。第一个因素是稳态分压。如果信号源内阻是 2kΩ导通电阻是 70Ω那么负载端开路时分压比例是 70/(200070)大约掉了 3.4%。换算到 2.5V 上就是 85mV。这和实测值非常接近。我的信号源确实不是理想电压源它本身设计成带有一定输出阻抗用来模拟真实传感器。真实世界里很多传感器也是高阻输出比如光敏电阻、热敏电阻分压电路内阻动辄几kΩ到几十kΩ接上 4067 之后电压跌落会非常明显。第二个因素是动态时间常数。ADC 采样阶段等效为一个大电容接在输入引脚上充电回路是信号源内阻 4067 导通电阻 走线电阻。STM32 的 ADC 采样时间即使配到最长239.5 个 ADC 时钟周期在 12MHz ADC 时钟下也只有大约 20μs。如果信号源内阻是 10kΩ加上 70Ω 的 Ron总充电电阻约 10.07kΩADC 采样电容按 10pF 估算时间常数 τRC≈0.1μs20μs 采样时间看似足够。但如果信号源内阻是 100kΩτ 就是 1μs5 个 τ 需要 5μs看起来也够。真正的问题出现在信号源内阻更大、或者用快速采样配置的时候。我这次的核心矛盾还是在稳态分压上。换一个更直观的说法4067 内部的导通电阻就是一根细水管传感器信号就是水流ADC 就是要接水的水桶。水管本身细水桶又有容量水压电压一部分就损失在细水管上了。要减少损失要么把水管换粗要么别让水桶直接接在水龙头上。4.3 加一级运放跟随器一劳永逸的方案最彻底的解决办法是在 COM 引脚和 MCU 的 ADC 输入引脚之间加一级运放电压跟随器缓冲器。电压跟随器的特点是输入阻抗极高、输出阻抗极低。4067 的导通电阻再大它面对的负载是一个几乎不取电流的运放输入端因此 Ron 上几乎没有电流流过也就基本没有电压降落。运放的输出端再去驱动 ADC 采样电容由于输出阻抗只有几欧甚至更低采样电容充得快ADC 采样时间也更容易满足。我选的是 MCP6004四路轨到轨运放单电源供电 3.3V输入和输出都能到电源轨价格便宜很契合这种低速模拟信号的缓冲需求。贵一点的方案还有 OPA333 这类自动调零运放精度更高但对于这个项目来说 MCP6004 已经足够。接法如下4067 的 COM 引脚接运放的同相输入端运放输出端直接接 MCU ADC 引脚运放的反相输入端和输出端短接形成跟随器。运放供电用 3.3V去耦电容别落下。加上运放之后实测 2.500V 输入ADC 读回来 2.493V剩下的误差主要来自 ADC 参考电压本身的精度已经可以接受了。值得一提的是加运放后坑一的建立时间问题也一并改善。因为运放输出的低阻抗能迅速给 ADC 采样电容充电信号源内阻的影响被隔离掉了。4.4 如果不想改硬件软件上还能怎么补救如果条件限制不能加运放软件上还有几个补救办法理解它们的原理很重要第一个办法是增大 ADC 采样时间。把采样周期从几周期配到最大的几百周期增加采样电容的充电时间能在一定程度上缓解动态时间常数导致的跌落。但对稳态分压无效因为分压是电阻网络的性质等再久也不会变。第二个办法是做线性校准。既然信号源内阻相对固定、4067 的导通电阻相对固定那么每个通道的电压跌落比例基本是固定的。我可以用标准电压源逐通道标定得到每个通道的校准系数然后在软件里做线性修正。这个方法的问题是导通电阻会随温度、电压变化校准系数做不到完全恒定。实测下来如果只做单点校准误差可以压到 1% 以内要做到更高精度就必须加硬件缓冲。第三个办法是提高后级输入阻抗。如果 MCU 内部 ADC 支持可以尝试把 ADC 配置为高输入阻抗模式但大多数 MCU 没有这个选项。用外部手段就是加缓冲又绕回运放方案了。我的建议是做原型验证的时候可以先用软件标定顶着如果项目要量产直接把运放加上。成本多几块钱省掉的调试时间和售后问题算下来非常划算。5. 把 16 路采集做稳滤波、扫描策略和选型边界5.1 扫描 16 路时的采样节奏和滤波函数通道切换和电压跌落的问题解决之后我把 16 路通道全部接上传感器开始测整体扫描效果。此时面临一个新的实际问题16 路轮询一遍数据稳定性如何保证传感器信号本身往往带有噪声尤其是工业现场50Hz 工频干扰、开关电源纹波、电机启停产生的尖峰都会耦合到模拟信号上。4067 本身是模拟开关它不会放大噪声但也不会滤除噪声。如果直接拿单次采样值做控制逻辑数据跳变得厉害。我引入了两层滤波。第一层是硬件滤波每个通道引脚对地并联一个 100nF 电容。这个电容有两个作用一是滤除高频噪声二是存储电荷——配合欠采样策略信号建立时间内的电压变化能更平滑。但注意电容不能太大否则建立时间成倍增加和坑一的矛盾会加剧。第二层是软件滤波我用的是滑动平均加中值复合滤波。过程是连续采样 5 次去掉最大值和最小值对剩下的 3 次取平均。这种方法对随机噪声抑制效果好对大脉冲干扰也有极强的抵抗能力而且代码只有十来行执行时间可控。软件滤波的关键参数是采样次数和平均策略。次数太少效果差次数太多响应慢。对于秒级变化的模拟量一次扫描 16 路总共才几百次 ADC 转换耗时完全可以接受。这个滤波函数我放在这里有需要的可以直接抄// 简单防脉冲滑动平均滤波对 5 次采样去极值后求平均 uint16_t ADC_Filter(uint8_t ch) { uint16_t buf[5]; uint16_t tmp; uint8_t i, j; for (i 0; i 5; i) { buf[i] MUX_ReadChannel(ch); } // 冒泡排序取中间三个 for (i 0; i 4; i) { for (j 0; j 4 - i; j) { if (buf[j] buf[j 1]) { tmp buf[j]; buf[j] buf[j 1]; buf[j 1] tmp; } } } return (uint16_t)((buf[1] buf[2] buf[3]) / 3); }如果噪声不是脉冲型而是高频随机噪声一阶低通滤波指数加权滑动平均对平滑波形更友好代码也更轻量// 一阶低通滤波alpha 取 0~1越小越平滑但响应越慢 uint16_t ADC_LowPass(uint16_t new_val, uint16_t last_val, uint8_t alpha) { return (uint16_t)((uint32_t)last_val * (256 - alpha) (uint32_t)new_val * alpha) 8; }另一个容易忽略的问题是扫描节奏。16 路逐一切换扫描如果代码在某个通道的滤波过程中被中断打断或者两个通道之间的间隔时间不均匀采到的数据可能会有周期性跳变。我的做法是给整个 16 路扫描过程加一个固定周期用一个 1ms 的定时器 tick每 tick 只处理一路16 个 tick 完成一轮扫描。这样每个通道的实际采样间隔完全一致后续做数据融合时省了很多麻烦。5.2 那些不该用 CD74HC4067 的场合用了一段时间之后我对 4067 的适用范围也有了一些自己的判断。它不是万能的下面这几类场景我明确不推荐用第一类是高阻抗信号源。前文已经算过信号源内阻到几十 kΩ 以后导通电阻分压造成的误差已经不能忽视了。哪怕是加了运放缓冲也不如直接把信号先放大再切换。第二类是微弱信号采集。如果你要采集的是微伏到毫伏级别的信号比如热电偶、应变桥的输出4067 的导通电阻变化和通道间串扰会成为误差的主要来源。这类场景应该用专门的模拟前端或者每路独立的仪表放大器。第三类是高精度同步采样。比如三相电压电流分析、多个传感器同一时刻的状态快照4067 本身就做不到同步采集只能用多通道同步 ADC。第四类是信号频率较高的场合。4067 的开关速度虽然快但走线和寄生参数带来的建立时间限制会拖慢实际的有效采样率。超过几十 kHz 的信号经过它之后很容易畸变。我这次处理的都是响应时间秒级的慢变环境信号4067 完全够用如果你要采的是动态信号建议先评估建立时间再决定。另外还有一种特殊情况多片 4067 并联扩展通道数。EN 引脚在这时候就有用了——每片 4067 的 EN 由 GPIO 控制只有当前需要的片子被使能其他片子的通道全部断开这样可以用一个 COM 引脚扩展出 32、48、64 路。但要特别注意未使能片子的漏电流多片并联时 COM 引脚上的漏电流会累加精度要求高的话比较头疼。5.3 除了 4067 还能用什么从扩展方案对比看选型逻辑我在选型阶段还对比过其他几种模拟开关和多路 ADC 方案这里把结论整理出来方便大家做决策。方案单颗成本通道数精度影响适合场景CD74HC4067低16有 Ron 分压低速多路模拟量采集74HC4051更低8同上少量通道、极简电路多片 4067 级联中32漏电累加通道数量要求极大ADS1115/ADS1220高4~8精度高、内置 MUX高精度低速采集24bit ADCHX711/ADS1232/MCP3561较高1~4极高称重、桥式传感器如果你的采集目标是高精度称重这类应用HX711、ADS1232、MCP3561 这类 24 位 ADC 确实更合适它们不仅分辨率高还内置了可编程增益放大器和稳定的模拟前端精度不是 4067 这种方案能比的。4067 的优势在于通道多、成本低、接入简单适合精度要求不高、通道数量不少的场景。选型从来不是越贵越好而是刚好够用。再补充一个实际项目中的经验4067 和 MCU 的 ADC 参考电压最好来自同一个电源轨。如果 4067 用 5V 供电而 MCU 的 ADC 参考是 3.3V模拟信号摆幅超过 3.3V 会被 ADC 削顶而 4067 内部的导通特性和漏电流也会发生变化。我这次的板子统一用 3.3V避免了很多奇怪的精度问题。6. 两个坑背后的同一套排查思路回头看这两个坑其实本质上是同一个问题的两面模拟开关的模型不是理想的短路它有电阻、有电容、有过渡过程。坑一的问题是过渡过程导致的时间维度误差坑二的问题是导通电阻导致的稳态维度误差。处理模拟开关扩展方案只要把这两笔账算清楚大部分问题都能提前规避。调试过程中给我最大帮助的是示波器。写代码之前先量硬件这是嵌入式调试的老规矩。第一次遇到坑一时如果我没有拿示波器去看 ADC 引脚上的波形而是对着代码猜来猜去估计一天都找不到根因。示波器上那条缓慢爬升的指数曲线比任何日志都直观。类似的遇到电压偏低的问题用万用表从信号源头逐级往后量量到哪一级电压掉了问题就锁定在哪一级。实际操作中我的体会是模拟前端这种东西能在电路图上解决的尽量不要靠软件硬扛。加一颗运放、加一颗电容成本几毛钱换来的是稳定性和可维护性。软件校准和滤波能补一部分精度但补不了原理性的缺陷。最后再分享一个小建议如果你准备在自己的板子上用 4067先在洞洞板上搭一个最小验证电路把 C0 和 C1 分别接上两个已知电压用串口把切换采样值打出来运行一个晚上看稳定性。这一步半小时就能做完却能省下后面整个联调阶段的时间。