一直关注我嵌入式调试笔记的朋友应该知道,我平时做项目喜欢把一些不常见但很实用的小技巧记录下来。这一篇想聊聊最近在项目里做多路模拟量采集时的一个方案——用 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~C15:16路模拟输入通道,这是被选择的信号输入端。
- COM(公共端):内部通过开关连接到被选中的通道,外部接到MCU的ADC引脚。
- S0~S3:4根通道选择地址线,二进制组合对应16个通道。
- EN(使能):低电平有效。EN接低电平时芯片正常工作,接高电平时所有开关断开,COM端呈现高阻态。如果不控制使能,直接接地即可。
- VCC和GND:电源引脚,HC系列建议3.3V~5V。
地址线和通道的对应关系我直接用一张表列一下,方便对照:
| S3 | S2 | S1 | S0 | 选通通道 |
|---|---|---|---|---|
| 0 | 0 | 0 | 0 | C0 |
| 0 | 0 | 0 | 1 | C1 |
| 0 | 0 | 1 | 0 | C2 |
| 0 | 0 | 1 | 1 | C3 |
| 0 | 1 | 0 | 0 | C4 |
| 0 | 1 | 0 | 1 | C5 |
| 0 | 1 | 1 | 0 | C6 |
| 0 | 1 | 1 | 1 | C7 |
| 1 | 0 | 0 | 0 | C8 |
| ... | ... | ... | ... | ... |
| 1 | 1 | 1 | 1 | C15 |
接线的时候有几个很重要的细节:
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之间(严格说是VCC+0.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.3V,GND接GND,VCC旁边并联0.1uF和10uF电容到GND。
- S0~S3分别接到MCU的PA0~PA3(这4个引脚我配置为通用推挽输出)。
- COM引脚接到MCU的PA4,这个引脚配置为模拟输入,复用为ADC1的通道4。
- EN引脚直接接地。
- 16路信号依次接到C0~C15。
之所以让S0~S3用PA0~PA3,COM用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位ADC,3.3V参考电压,理论上每路应该读到1241左右(1.000/3.3*4095)。实测结果我记录一下:
| 通道 | 读数 | 误差 |
|---|---|---|
| C0 | 1240 | -1 |
| C1 | 1241 | 0 |
| C5 | 1242 | +1 |
| C7 | 1240 | -1 |
| C10 | 1239 | -2 |
| C15 | 1241 | 0 |
整体表现还可以,最大误差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供电范围 | 分压/钳位/电平移位后再进4067 |
5. 后续扩展与改进空间
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这个芯片便宜、简单、稳定,值得经常备几片在抽屉里。希望这篇笔记里的内容对你有所帮助,如果你看完也有类似的调试经历,欢迎多在评论区交流,大家一起把坑填平。