手头一个项目要同时采16路模拟量,MCU自带的ADC通道不够用,换芯片又涉及硬件改动和供货周期,正好手边有几片CD74HC4067,干脆用它把单路ADC扩成16路,一路调下来确实省了不少事。这篇文章把整个调试过程、硬件接法、软件驱动思路,以及我在实际调试中踩过的两个坑完整记录下来,给正在做多路模拟采集的朋友一个参考。
CD74HC4067是一片16通道模拟多路复用器/解复用器,四根地址线加一根使能脚就能选通16个模拟通道,公共端接MCU的ADC输入即可。它解决的是"MCU ADC通道不够用"的典型场景,适合对采样速率要求不高的多路传感器采集、电池电压巡检、电位器/分压信号读取等应用。只要你对采样频率没那么敏感,一片4067只要一两块钱,性价比非常直观。
下面按我实际调试的顺序来写,从选型、硬件、软件到两个坑完整展开。
1. 为什么是CD74HC4067:三套16路采集方案的实际对比
接到需求的时候,我最初考虑过三条路:换一颗自带更多ADC通道的MCU、外挂多通道ADC芯片、用模拟开关扩展现有ADC。三条路各有各的适用场景,但在这个项目里,CD74HC4067是综合下来最省事的。
先看换MCU这条路。STM32F103这类主流芯片的ADC通道数通常在10到16个之间,但实际能用多少还要看封装。我用的LQFP48封装,ADC引脚被其他功能占掉一部分,真正能拿来采集模拟量的就那么几个通道。换一颗引脚更多、ADC通道更多的MCU,硬件要重新画,固件底层也要跟着动,采购周期也不一定等得起。为了几个模拟通道去动整块板子的核心器件,怎么看都不划算。
再看外挂多通道ADC芯片,比如ADS1115、ADS1262这类。ADS1115是16位ADC,精度确实好,但I2C接口的吞吐率有限,而且在4路ADC不够用、需要2到4颗芯片并联的时候,成本和PCB面积都上来了。ADS1262这类高精度ADC性能很强,但价格和外围电路复杂度也直接拉满。这个项目的信号本身是电池电压、电位器反馈这类对精度要求没那么极端的模拟量,12位ADC配合滤波完全够用,没必要上16位甚至24位的方案。
最后就是CD74HC4067这条路线。它的思路很简单:用模拟开关在单路ADC前面做通道切换,让ADC分时复用。一片CD74HC4067有16个模拟输入通道,四根地址线S0到S3用二进制编码选通,一个公共端SIG接MCU的ADC输入引脚,这样MCU只需要占用4个GPIO加1个ADC引脚就能采16路模拟量。
三套方案的对比我简单整理了一下:
| 方案 | 成本 | 硬件改动 | 采样精度 | 适合场景 |
|---|---|---|---|---|
| 换更高规格MCU | 较高 | 大,需改板 | 取决于MCU | 需要更多功能扩展,不只缺ADC |
| 外挂ADS1115等多通道ADC | 较高 | 中等,I2C接线 | 16位,精度高 | 对精度有明确要求,通道数不多 |
| CD74HC4067扩展 | 低,几块钱 | 小,4个GPIO | 12位,够用 | 多路低速模拟量巡检 |
当然,4067方案也有它的局限性。最直接的一点,模拟开关不是理想开关,通道之间存在导通电阻,信号经过开关会有一定衰减,后面我在坑二的排查里就跟这个特性正面撞上了。另外它是单端选通,不支持差分输入,如果信号是差分性质的,这条路就走不通。还有就是16路扫描需要时间,每一路切换后都要等信号稳定,所以整体采样速率不会太快,做音频采样这种高速场景肯定不行,但做电池电压巡检、环境监测这类需求完全够用。
2. CD74HC4067核心电气特性:手册里容易被忽略的几个参数
芯片选好了不等于直接就能用,动手之前把数据手册里的关键参数吃透,后面能少走很多弯路。我把自己觉得最重要的几个特性单独列出来,这几个参数恰恰是后面两个坑的根源。
2.1 引脚功能与真值表
CD74HC4067一共16个通道输入(CH0到CH15),一个公共端SIG,四根地址线S0到S3,一个使能端EN。通道选通逻辑非常直接:
| S3 | S2 | S1 | S0 | 选通通道 |
|---|---|---|---|---|
| 0 | 0 | 0 | 0 | CH0 |
| 0 | 0 | 0 | 1 | CH1 |
| 0 | 0 | 1 | 0 | CH2 |
| 0 | 0 | 1 | 1 | CH3 |
| ... | ... | ... | ... | ... |
| 1 | 1 | 1 | 1 | CH15 |
这里需要特别注意的是EN引脚的电平极性。CD74HC4067的EN是低电平有效,也就是说EN接地或者被拉低时芯片正常工作,EN拉高时所有通道全部关断。我第一次接触这类芯片时就吃过想当然的亏,以为是高电平使能,结果通道怎么切都是断的。如果你的板子上EN脚悬空,芯片会处于不确定状态,最好用GPIO控制EN,或者直接连到GND。
2.2 导通电阻Ron:模拟开关最关键的指标
模拟开关最核心的参数就是导通电阻Ron。CD74HC4067的Ron不是一个固定值,它随着供电电压、信号电压和温度变化。我在数据手册里看到的典型值是这样的:
| 供电VCC | 典型Ron | 最大Ron |
|---|---|---|
| 2.5V | 120Ω | 240Ω |
| 4.5V | 70Ω | 160Ω |
| 6V | 60Ω | 130Ω |
从表格可以明显看出,VCC越高,Ron越小。以3.3V供电、信号电压在1V到2V之间时,Ron大致在80到120欧姆这个区间。
这个Ron的绝对值看起来不大,但放在分压电路里就会造成实际读数偏差。比如信号源是一个100kΩ电位器分压,输出阻抗本身就比较高,此时Ron和信号源阻抗串在一起,再叠加ADC输入级的采样电容效应,问题就来了。这就是坑二的伏笔。
2.3 模拟信号电压范围
CD74HC4067的模拟信号输入范围是GND到VCC。也就是说,如果VCC接3.3V,那16个通道上输入的模拟电压绝对不能超过3.3V,也不能低于GND。超出这个范围,芯片内部的寄生二极管会导通,轻则采集值被钳位,重则损坏芯片甚至倒灌到MCU的ADC引脚。
这一点在实际项目中特别容易踩,因为很多传感器输出可能是0到5V或者0到10V的。如果你的信号源输出电压超过了4067的VCC,就需要先做电平转换或者分压,不能直接把信号怼到芯片输入上。
2.4 开关切换时间与信号建立时间
CD74HC4067的导通和关断时间在纳秒级别,但这里有个容易混淆的概念:开关切换速度快不等于信号能瞬间稳定。开关切换完成后,信号通路上的寄生电容、后级负载的输入电容、ADC采样电容都需要时间充电到目标电压。这个"建立时间"取决于信号源内阻和电容的乘积,也就是RC常数。
这个特性直接决定了坑一的成因。我在第一次调试时天真地以为,地址线切过去就可以立刻启动ADC转换,结果读出来的数据完全不对,后面会详细展开。
3. 硬件接线实操:16路通道与MCU怎么连最稳妥
这部分我把实际接线方式和走线布局经验整理出来。接线不复杂,但如果细节处理不好,后面排查起来会很头疼。
3.1 基础接线清单
以STM32F103为例,我使用的ADC输入引脚是PA4,四个GPIO分别是PB0到PB3。接线如下:
- VCC接3.3V,GND接GND,两者之间加一个100nF去耦电容,尽量靠近芯片电源引脚。
- CH0到CH15分别接16路待测模拟信号。
- SIG引脚接MCU的ADC输入引脚PA4。
- S0接PB0,S1接PB1,S2接PB2,S3接PB3。
- EN脚接PB4,方便软件控制,也可以直接接GND让芯片常使能。
接线看起来很简单,但有三个细节必须注意。
第一个细节是SIG引脚到MCU ADC引脚之间的走线尽量短。PA4和4067的SIG如果隔得太远,中间走线越长,寄生电容越大,信号建立时间越长,也会更容易受到周围数字信号的干扰。PCB条件允许的情况下,4067尽量靠近MCU放置。
第二个细节是模拟信号线要和数字控制线(S0到S3)保持距离。S0到S3上的电平跳变是非常陡峭的数字信号,如果长时间和模拟信号线平行走线,数字跳变会通过寄生电容耦合到模拟通道上,导致ADC采样值抖动。布线紧张时,至少保证模拟线和数字线不要平行走太长距离,中间用地线隔开更好。
第三个细节是未使用的通道一定要处理,不能悬空。我的板子上16路只用了12路,剩余4路我直接接到了GND。如果让未用通道悬空,引脚上的电压会漂移到一个不确定值,这会在模拟开关内部产生漏电流,从而影响公共端SIG上的电压,导致正常通道的采集值也受干扰。
3.2 关于信号源阻抗的预先评估
这一步很多人会忽略,但我建议在画板子之前就过一遍。信号源阻抗直接决定了后面的采样建立时间,也决定了用4067到底顺不顺利。
如果用电阻分压网络作为信号源,分压电阻的值不要选太大。两个10kΩ电阻分压,信号源等效内阻是两个电阻的并联,约5kΩ,这个量级配合4007的Ron还在可以接受的范围内。但如果分压电阻换成100kΩ级别,信号源等效内阻就到了50kΩ,这时候整个信号链的建立时间就会明显变慢,坑二就是从这个地方来的。
3.3 电源与参考电压的处理
这个项目对精度要求中等,ADC参考电压直接用了MCU的VDD。如果你对采集精度有更高要求,建议用外部基准电压源连接到MCU的VREF+引脚。因为ADC的转换结果实际上是输入电压和参考电压的比例值,参考电压不稳,采集值就跟着漂。
4. 软件驱动:GPIO控制通道扫描与ADC读取
硬件接好了,接下来是软件部分。整体流程不复杂:控制S0到S3的电平组合选通目标通道,等待信号稳定,然后启动ADC转换读取数值。
4.1 基础GPIO控制函数
首先实现通道选择的基础函数。这个函数接受0到15的通道号,通过位运算从通道号中提取每一位,分别写到四个GPIO引脚上:
static void mux_select_channel(uint8_t ch) { HAL_GPIO_WritePin(MUX_S0_GPIO_Port, MUX_S0_Pin, (ch >> 0) & 0x01); HAL_GPIO_WritePin(MUX_S1_GPIO_Port, MUX_S1_Pin, (ch >> 1) & 0x01); HAL_GPIO_WritePin(MUX_S2_GPIO_Port, MUX_S2_Pin, (ch >> 2) & 0x01); HAL_GPIO_WritePin(MUX_S3_GPIO_Port, MUX_S3_Pin, (ch >> 3) & 0x01); // 通道切换后等待信号稳定 delay_us(100); }这里我用宏定义把GPIO端口和引脚号做了映射,方便移植。注意最后有一个延迟,作用是等模拟开关切换完成、信号建立稳定后再进行ADC采样。
4.2 单通道读取与16路扫描
单通道读取函数的逻辑是:选通通道,启动ADC转换,等待转换完成,读取结果:
uint16_t mux_read_channel(uint8_t ch) { mux_select_channel(ch); HAL_ADC_Start(&hadc1); HAL_ADC_PollForConversion(&hadc1, 10); uint16_t val = HAL_ADC_GetValue(&hadc1); HAL_ADC_Stop(&hadc1); return val; }写到这里有个使用习惯要说明一下,我用的是HAL库的阻塞式读取,每次启动一次转换读一次值,读完就停。这套流程对低速巡检场景足够了。如果你对转换效率有要求,可以考虑用ADC的扫描模式或者DMA,但要注意DMA模式下ADC会连续采样,通道切到下一路之前要处理好时序关系,否则会读到切换过程中的中间态电压。
16路扫描就是循环调用单通道读取函数,把所有通道的值存在数组里:
#define MUX_CHANNEL_COUNT 16 uint16_t adc_values[MUX_CHANNEL_COUNT]; void mux_scan_all_channels(void) { for (uint8_t i = 0; i < MUX_CHANNEL_COUNT; i++) { adc_values[i] = mux_read_channel(i); } }4.3 采样值滤波处理
单次采样的值直接拿来用,在某些场景下会有点抖。我习惯在读取函数外面再加一道软件滤波,最简单有效的是多次采样取平均,或者用滑动窗口滤波。
uint16_t mux_read_channel_filtered(uint8_t ch, uint8_t samples) { uint32_t sum = 0; for (uint8_t i = 0; i < samples; i++) { sum += mux_read_channel(ch); } return (uint16_t)(sum / samples); }多次采样取平均会成倍增加扫描周期。采样3次取平均,16路全部扫一遍就是原来的3倍时间。对低速信号来说问题不大,但如果信号变化本来就快,就要在实时性和平滑度之间做权衡。
4.4 关于扫描周期的计算
16路全部扫一遍需要多久,这个可以估算。每路通道的耗时等于:切换延时(100us)加ADC采样加转换时间。比如STM32F103的ADC时钟12MHz,采样周期1.5周期加转换周期12.5周期,一次转换大约1.17us,再算上HAL库函数调用的开销,单通道大概120us左右。16路全扫一遍就是2ms左右,也就是500Hz的扫描频率。这个指标对慢速传感器巡检完全够用,也验证了4067方案对这类场景确实匹配。
5. 调试坑一:通道切换后立即采样,读到的居然是上一个通道的值
这个坑是第一个让我卡住的问题,现在回头看看,原因其实并不复杂,但当时的排查过程绕了不少弯路。把这个过程完整写出来,比直接给结果更有参考价值。
5.1 现象描述
板子焊好,程序写完,接上信号源开始测试。我先把CH3接了一个2.0V的稳定电压,CH5接了一个1.0V的稳定电压,扫描所有通道,结果发现CH5读出来的值明显偏高,接近CH3的2.0V。单独反复读取CH5,第一次读到的值是1.8V左右,第二次才接近1.0V真实值。
这个现象很典型:通道切换后第一次采样拿到的是上一个通道的残留电压,等待一段时间或者再采一次才正确。我一开始还以为是4067的通道切换逻辑出问题了,通道没切过去,结果用万用表量SIG引脚,发现电压实际上已经切换到位了,但ADC读出来的值却是错的。
5.2 排查链路
我把当时的排查过程按顺序记录下来,方便遇到同类问题的朋友对照:
第一步,用万用表确认S0到S3的GPIO电平是否正确。测量结果发现地址线电平组合完全正确,CH5对应的二进制0101确实已经作用到芯片地址引脚上。
第二步,用示波器观察SIG引脚的电压波形。示波器显示,地址切换之后SIG引脚上的电压确实是1.0V,而且稳定,说明4067本身切换没有问题,电压已经正确输出到了公共端。
第三步,问题收敛到了MCU的ADC采样环节。重新检查ADC初始化代码,发现采样周期配置的是1.5周期,这是STM32 ADC最短的采样周期配置。
到这里基本可以定位了:问题出在ADC内部采样电容的建立时间不够。ADC采样时,内部采样保持电容要先充电到输入电压才能开始转换,如果采样窗口太短,电容还没有完全充到目标电压就进入了保持阶段,读到的值自然偏低或者偏向上一个通道的电压值。
5.3 根因分析
模拟开关切换完成后,整个信号链路可以等效成一个RC充电回路。信号源内阻加上4067的导通电阻Ron构成了R,信号通路的寄生电容和ADC采样电容构成了C。当切换到新通道时,C要经过R充电到新电压,充电时间是RC的若干倍。如果采样窗口小于这个充电时间,采到的就是充电中间态,也就是上一个通道的残留电压。
这个项目的信号源等效内阻约5kΩ,加上4067的Ron约100Ω,总共约5.1kΩ。ADC采样电容典型值按8pF算,加上走线和芯片寄生电容约15pF,RC时间常数大约5.1kΩ乘以15pF,约76.5纳秒。看起来非常小,但ADC采样时间只有1.5个ADC时钟周期,在12MHz ADC时钟下只有125纳秒。这时候问题就来了:125纳秒的采样窗口对应大约1.6倍时间常数,采样电容大约只能充到目标电压的80%。这个偏差已经是ADC转换精度的量级了。
5.4 解决方案
解决思路有两个方向,我从实际效果来看两个都可以用。
第一个方案是加长通道切换后的等待时间。我最终在切换通道后延时100微秒,远大于前面的计算值,确保信号完全建立后再采。这个方案的好处是简单直接,代码看起来也直观,适合采样频率要求不高的场景。
第二个方案是拉长ADC采样周期。把ADC采样周期从1.5周期改成239.5周期,采样窗口显著拉长,采样电容也就能充分充电了。这个方案适合ADC在连续采样、不方便插入延时的场景。
两个方案选哪个,取决于你的整体采样频率要求。如果16路扫描周期在2ms左右还有余量,直接用延时的方案就够了。如果对扫描速度有更高要求,就试着拉长采样周期,找到精度和速度之间的平衡点。
6. 调试坑二:高内阻信号源接入后,ADC读数系统性偏低
第一个坑解决之后,我以为后面就顺了,结果换上一批高阻信号源做测试时又发现新问题。这个坑比第一个更隐蔽,也更考验对模拟电路的底层理解。
6.1 现象描述
测试时用了一个100kΩ电位器作为信号源,电位器分压后输出电压用万用表测量是1.20V,但通过4067送到ADC读出来的值换算成电压之后是1.02V,偏低了不少。一开始我以为还是第一个坑的采样时间问题,但把延时加到500微秒、采样周期拉到最大之后,读数依然是1.02V左右,没有明显改善。
这说明问题不在采样时间,而是信号源本身带不动4067加ADC这个负载。万用表的内阻是10MΩ级别,测量时几乎不从信号源抽取电流,所以测出来是真实电压。但4067的Ron和ADC采样电路加起来,会从信号源抽取一定电流,如果信号源内阻很大,这个负载效应就会把电压拉低。
6.2 排查链路
这次的排查过程比坑一更曲折,我按顺序记录:
第一步,检查参考电压和分压电阻的标称值。确认VCC是稳定的3.3V,电位器本身阻值也正常,没有焊接虚焊的问题。
第二步,怀疑4067的Ron导致分压。用20kΩ电位器替换100kΩ电位器,发现读数偏差明显减小,这个实验证实了我的猜测:问题确实跟信号源内阻相关,信号源内阻越大,读数偏差越大。
第三步,用示波器看SIG引脚在ADC采样期间的实际波形。重点观察采样保持期间SIG电压是否有下降,结果确实看到了电压塌陷的现象。
第四步,确认根因是信号源内阻、4067导通电阻和ADC输入阻抗共同构成了分压关系,把输入电压拉低了。
6.3 根因分析
要充分理解这个问题,需要把ADC采样过程再往深里拆一层。ADC在采样阶段,内部采样电容会通过采样开关接到输入引脚上,这时信号源要向这个电容充电,充电电流的路径是:信号源 -> 信号源内阻 -> 4067的Ron -> 采样开关 -> 采样电容。
在这个充电过程中,ADC的输入等效阻抗不是一个单纯的电阻,而是一个动态变化的量。采样开关闭合的瞬间,输入阻抗很低,因为采样电容刚开始充电,需要很大的电流。如果信号源内阻很高,能提供的电流很有限,采样电容就充不满,最终转换结果就会是个偏低的值。
这个问题的本质是信号源驱动能力不足。万用表这类高阻抗测量设备不会被这个效应影响,但4067的Ron再加上ADC采样时的瞬态电流需求,就让高阻信号源的缺陷暴露出来了。
6.4 解决方案
解决这个问题有几种思路,我从工程实践角度按推荐顺序给你列出来:
第一个方案是降低信号源内阻。如果用电阻分压网络做信号源,分压电阻尽量选低一点的阻值。10kΩ级别的分压电阻在这个场景下基本没有明显问题,100kΩ级别就会出现可以观察到的读数偏差。如果你受限于功耗或功耗预算必须用高阻值电阻分压,就需要用后面的方案。
第二个方案是在4067的SIG引脚后面加一级运放电压跟随器。运放输入阻抗极高,基本不向前级索取电流,输出阻抗极低,可以轻松驱动ADC采样电容。这个方案解决得很彻底,是目前工程上最稳妥的做法,代价是增加一片运放的成本和一点PCB面积。
第三个方案是软件层面补偿校准。这个方案需要对每一路信号做标定,把ADC读到的值和实际值建一个映射关系,在代码里做补偿。遇到信号源内阻变化、温度变化时补偿关系会漂移,所以我个人不推荐作为唯一手段,但作为辅助手段是没问题。
三个方案我最终选了运放跟随器,因为它的效果最直观,而且一劳永逸。调试的时候用一块LMV321单运放小板临时接上去验证,读数偏差立刻就消失了。
7. 还有一些经验:电压范围、使能脚、通道悬空等容易中招的细节
除了上面两个大坑,实际调试过程中还积累了一些零散经验。这些细节单独看都不大,但每一个都可能让采集结果变得不对劲,我集中写出来供参考。
7.1 VCC电压不要和输入信号范围对着干
4067的VCC引脚天然决定了模拟通道的电压上限。我一开始想着4067的Ron在VCC较高时更小,就把VCC接到了5V,结果发现3.3V的MCU GPIO驱动S0到S3时,高电平只有3.3V,而5V供电下的4067对逻辑高电平的要求接近3.5V,GPIO的3.3V高电平无法稳定识别,通道切换就变得不可靠了。
后来我把VCC接到3.3V,跟MCU逻辑电平对齐,这个问题直接消失。所以如果你用的是3.3V的MCU,VCC就用3.3V,不要为了降低Ron去提高VCC,逻辑兼容性比那几十欧姆的Ron重要得多。
7.2 EN使能脚一定要有确定的电平
EN悬空是板上最常见的疏漏之一。4067的EN输入如果悬空,芯片内部逻辑状态不确定,表现为通道时而导通时而不导通,排查起来非常头疼。最靠谱的做法是用一个GPIO单独控制EN,软件初始化时拉低使能;如果不想占用GPIO,直接硬件拉低到GND也是可以的。
7.3 未用通道处理要果断
未使用的通道不接任何东西,芯片内部逻辑虽然不会把悬空通道接入,但悬空的引脚会因为外部电磁干扰产生不确定电压,通过芯片内部的寄生电容和漏电流影响公共端SIG。我在板子上把未用通道直接接到GND,扫描时那些通道也会被选中,读到0值,对正常通道没有干扰。
7.4 定期巡检的项目,数据跳变先查电源和参考地
如果某个通道的采集值出现无规律的跳变,先别急着怀疑4067。我用一个旧开发板测试时出现过类似现象,排查了很久,最后发现是面包板供电线上压降太大,MCU和4067的VCC之间还有几十毫伏的波动,ADC参考电压自然也跟着抖。把供电改成独立的稳压输出后问题消失了。多路模拟采集系统对电源质量的要求比纯数字电路要高一个档次,这个心理准备要有。
7.5 4067可以双向使用,但电流限制要注意
CD74HC4067本质上是一个双向模拟开关,除了当多路选择器把多路信号汇到一路ADC,也可以反过来当多路分配器,把一路信号分到16个输出中的一个。不过双向可用的前提是信号电流要控制在芯片允许范围内,它毕竟不是功率开关,不能拿它切大电流负载。我用它做过一个简单的信号路由实验,效果正常,但始终记得控制电流在毫安级别。
CD74HC4067这个芯片本身不难用,难的是模拟电路里的那些"看不见"的电容、阻抗和建立时间。项目做完之后,我的体会是:模拟开关方案的成败,往往不在芯片本身,而在对整个信号链路阻抗和时序的理解上。只要把这两点想清楚,16路ADC扩展就是一个性价比很高的方案。