STM32用CD74HC4067扩展16路ADC:完整驱动与调试指南

发布时间:2026/9/8 18:04:40
STM32用CD74HC4067扩展16路ADC:完整驱动与调试指南 刚把一块板子上的 ADC 通道数从 3 路扩到 16 路用的就是 CD74HC4067。这片子是个 16 通道模拟多路复用器价格便宜、引脚好焊很多做数据采集的朋友都用过。不过实际调下来发现扩展通道只是第一步后面跟着的两个调试坑才是真正耗时间的地方。这篇笔记就把完整思路、接线方式、驱动代码和踩坑过程都整理出来给同样在折腾多通道采集的人一个参考。先说清楚 4067 能做什么。它本质上就是一个单刀十六掷模拟开关通过 4 根地址线S0-S3的二进制组合把公共端COM接通到 16 个通道C0-C15中的某一个。对于 STM32 这类 MCU 来说片内 ADC 通常只有 1 个转换器加若干通道引脚当你需要采集的模拟量超过可用引脚数时用 4067 扩展是最直接的办法。它不占用额外的 ADC 外设只消耗 4 个 GPIO 用于选通代价是每个通道需要轮流采样采样率会打折扣但对多数慢变信号温度、湿度、电位器、电池电压完全够用。1. 方案选型思路1.1 为什么不用多片 ADC 或者外部 ADC 芯片在设计多通道采集时很多人第一反应是换一颗自带 16 路 ADC 的 MCU或者加一块 ADS1115 之类的独立 ADC 芯片。这两个方案我都试过各有各的问题。换 MCU 意味着重新画板、重新写驱动项目周期直接拉长而且很多场景下主控型号是客户指定或者供应链锁定的不能随便改。外接独立 ADC 芯片的话通信接口通常是 I2C 或 SPII2C 速率低多通道高速轮询时会卡脖子SPI 虽然快但占用引脚也多还需要额外的参考电压和时钟配置软件复杂度上来了。用 4067 的好处是几乎不挑 MCU任何有 GPIO 和 ADC 的芯片都能用驱动逻辑就是「切通道-延时-读 ADC」三步循环代码量少、逻辑直观、排查问题也容易。实际代价是采样速度下降这个后面专门讲。1.2 CD74HC4067 关键参数解读CD74HC4067 是 TI 的经典型号有个兄弟叫 CD4051 是 8 通道的CD4067 则是 16 通道版本。核心参数有几个导通电阻Ron典型值 70Ω 左右供电电压 4.5V 时约 70Ω3V 供电时会高一些这个值决定了它和信号源内阻的分压关系直接影响采样精度。开关切换时间Ton/Toff在纳秒到微秒级别手册里给的传播延迟很小但实际电路中受负载电容影响通道切换后到信号稳定需要一定时间这个就是调试坑的来源。通道间串扰Crosstalk在高频信号下会明显但对直流或低频模拟量影响可以忽略。还有一个容易忽略的输入信号电压范围必须控制在 VCC 和 GND 之间不能超过电源轨否则内部 ESD 保护二极管会导通轻则测量异常重则损坏芯片。1.3 和 74HC4051/74HC4052 的对比选择如果只需要 8 路用 CD4051 就够了引脚更少控制更简单。需要 16 路就选 4067它把 4 根地址线全部用上一根 COM 线接入 ADC。还有一种双通道 4:1 的 74HC4052适合差分配对采集比如同时采电压和电流。我当时选 4067 还有一个原因它的通道开关是双向的既能当多路复用器用也能反过来当信号分配器用。比如一个 DAC 输出要分到多个负载或者一个 PWM 信号要轮流驱动多个设备都能拿它顶一下。虽然是模拟开关但数字信号通过也完全没问题只是要注意信号频率和导通电阻的参数限制。2. 硬件设计要点2.1 引脚连接与典型电路先给出一份我实际用过的连接清单MCU 用的 STM32F103ADC 引脚选 PA1ADC1_IN1四个地址脚用 PB0-PB3。CD74HC4067 引脚功能表引脚号名称功能1S3地址选择位3最高位2S2地址选择位23S1地址选择位14S0地址选择位0最低位5E使能端低电平有效6-9C15-C12通道15-1210-13C11-C8通道11-814-17C7-C4通道7-418-21C3-C0通道3-022COM公共端接ADC输入23VEE负电源/地24VCC正电源2V-6V接线时我把 E 引脚直接接地让它永远处于使能状态只用地址线控制通道。VEE 接 GNDVCC 接 3.3V和 STM32 同电源域。COM 引脚到 PA1 之间的走线尽量短中间不要过孔避免引入额外电容和噪声。每个通道的输入信号进来后我在通道引脚和 GND 之间并了一个 100nF 的电容做硬件滤波。这个电容同时也会让通道切换后的建立时间变长后面计算延时的时候要考虑进去。2.2 电源去耦和布局细节4067 的 VCC 和 GND 之间必须加 100nF 去耦电容位置尽量靠近芯片引脚。如果你在采集的是高精度模拟信号建议再加一个 10μF 的钽电容做低频去耦防止电源纹波通过开关串到信号路径上。地线处理是很多人的盲区。4067 的地和 ADC 的参考地必须单点连接尤其是当信号源和 MCU 不在同一块板子上时。如果地电位有差异轻则采样值飘重则出现固定偏移。我这次是单板内采集没有这个问题但如果你接的是外部传感器模块地线一定要单独拉一根粗线。2.3 信号范围和防护建议前面提过 4067 的输入不能超过 VCC。实际项目中如果传感器输出可能过压比如工业现场 24V 信号误接入一次就能烧掉芯片。稳妥起见每个通道入口可以串联一个几百欧到几 kΩ 的电阻限流再反并联两个二极管一个到 VCC一个到 GND做钳位保护。如果信号本身就比较干净这一步可以省但很多工程师没意识到 4067 比 ADC 引脚更脆弱因为它的 ESD 等级并不高。3. 软件驱动实现3.1 地址线 GPIO 初始化与通道切换函数软件部分我直接说结论不要想复杂了一个简单的 switch 语句就够。下面是我实际在用的代码HAL 库风格。// 地址线引脚定义 #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_GPIO_PORT GPIOB // 切换通道ch: 0-15 void MUX_SelectChannel(uint8_t ch) { if (ch 15) ch 15; HAL_GPIO_WritePin(MUX_GPIO_PORT, MUX_S0_PIN, (ch 0x01) ? GPIO_PIN_SET : GPIO_PIN_RESET); HAL_GPIO_WritePin(MUX_GPIO_PORT, MUX_S1_PIN, (ch 0x02) ? GPIO_PIN_SET : GPIO_PIN_RESET); HAL_GPIO_WritePin(MUX_GPIO_PORT, MUX_S2_PIN, (ch 0x04) ? GPIO_PIN_SET : GPIO_PIN_RESET); HAL_GPIO_WritePin(MUX_GPIO_PORT, MUX_S3_PIN, (ch 0x08) ? GPIO_PIN_SET : GPIO_PIN_RESET); }这段代码没有用任何技巧就是把通道号拆成 4 个 bit 写到 4 个引脚上。为什么不用查表法因为 4 根线的组合总共就 16 种switch 或者位运算都足够快查表反而把代码搞长。3.2 ADC 初始化配置ADC 这边用单通道 DMA 模式配合定时器触发固定频率采样。这样 CPU 可以不用一直等着转换完成DMA 自动把结果搬进数组效率高很多。void ADC1_Init(void) { ADC_HandleTypeDef hadc1; hadc1.Instance ADC1; hadc1.Init.ScanConvMode DISABLE; hadc1.Init.ContinuousConvMode DISABLE; hadc1.Init.DiscontinuousConvMode DISABLE; hadc1.Init.ExternalTrigConv ADC_SOFTWARE_START; hadc1.Init.DataAlign ADC_DATAALIGN_RIGHT; hadc1.Init.NbrOfConversion 1; HAL_ADC_Init(hadc1); }采样时间我会设置得比较保守选 ADC_SAMPLETIME_55CYCLES_5 或者更长的 239.5 周期。原因很简单经过 4067 的开关后信号源阻抗变高如果采样时间短ADC 内部的采样电容充不满读数必然偏低且不稳定。这个细节是很多人忽略的后面坑二还会再讲。3.3 轮询采样完整流程采集 16 路数据的流程就是一个 for 循环uint16_t adc_values[16]; void Sample_All_Channels(void) { for (uint8_t ch 0; ch 16; ch) { MUX_SelectChannel(ch); Delay_Us(50); // 关键延时等待信号稳定 HAL_ADC_Start(hadc1); HAL_ADC_PollForConversion(hadc1, 10); adc_values[ch] HAL_ADC_GetValue(hadc1); HAL_ADC_Stop(hadc1); } }这个代码看起来简单到像个 demo但实际上手就会发现问题如果那个 Delay_Us(50) 去掉或者延时不够采出来的数据会在通道之间串扰。比如第 0 通道接的是 0V第 1 通道接的是 3.3V你切到第 0 通道立刻采样读到的可能是 0.5V 甚至 1V过几十微秒后才慢慢掉到 0V。所以我强烈建议第一次调试时把延时代码单独留出来用示波器去实测信号稳定时间然后再定延时值不要拍脑袋写 5μs 或者 100μs。3.4 平滑滤波处理对慢变信号来说单次采样直接拿去用还是太草率。我加了一个最简单的滑动平均滤波每个通道连续采 8 次去掉最大最小值再取平均。16 个通道全扫一遍需要的时间 16 × 8 × (切换延时 采样转换时间)以 50μs 切换延时算大概在 10ms 左右对温度、水位这类信号完全没有压力。uint16_t Read_Channel_Filtered(uint8_t ch) { uint32_t sum 0; uint16_t min 0xFFFF, max 0; for (uint8_t i 0; i 8; i) { MUX_SelectChannel(ch); Delay_Us(50); HAL_ADC_Start(hadc1); HAL_ADC_PollForConversion(hadc1, 10); uint16_t val HAL_ADC_GetValue(hadc1); HAL_ADC_Stop(hadc1); sum val; if (val min) min val; if (val max) max val; } return (uint16_t)((sum - min - max) / 6); }这种滤波逻辑虽然简单但对 DCDC 纹波、工频干扰这类周期性噪声有不错的抑制作用。缺点是实时性下降如果要快速响应可以考虑用中值滤波加轻量级低通滤波的组合效果会更好但代码写起来会费点功夫。4. 实际调试中的两个坑4.1 坑一通道切换后采样值互相串扰这个问题是我最开始没意识到的。现象是0 通道接 0V1 通道接 3.3V2 通道接 1.65V。轮询采样时第 0 通道读数偶尔跳到 2.8V第 2 通道读数跟第 1 通道几乎一样。把通道顺序反过来现象也跟着反过来总之像是「后面通道的信号跑到了前面通道」。一开始怀疑是代码 bug比如 GPIO 配置错误、通道号没写对。用示波器直接看 COM 引脚的波形才发现地址线切过去之后COM 引脚上的电压并不是立刻变成目标通道的电压而是要经过一段充放电过程才能稳定。原因有两个第一4067 的内部开关导通后下一级电路ADC 引脚、PCB 走线、去耦电容有一个等效电容上一通道电容充上的电荷需要时间泄放。这个「记忆效应」在通道源阻抗高的时候特别明显。第二地址线 S0-S3 本身也有跳变时间如果 ADC 在主控 GPIO 刚切换完的几个微秒内就开始采样此时开关还没完全稳定。解决办法很直接就是在切换通道和启动 ADC 之间加足够长的延时。我当时把延时从 1μs 拉到 50μs串扰现象彻底消失。具体延时多长跟你的负载电容和源阻抗有关我用的是经验值通道间电压差超过 2V 时延时至少 20倍开关切换时间。实际项目里 50μs 是个比较安全的中间值。4.2 坑二源阻抗高时 ADC 读数整体偏低串扰问题解决后又出现一个新问题某一路信号源是电压跟随器输出内阻只有几十欧姆读数是准确的但另一路信号是从 100kΩ 电位器中间抽头取的电压理论计算应该 1.65V实际读出来只有 1.4V 左右。排查过程先用万用表量电位器抽头对地电压确实是 1.65V说明信号源没问题。接着量 COM 引脚电压发现也是 1.65V 左右信号已经到开关输出了。然后量 STM32 的 PA1 引脚读数变成 1.4V。问题出在信号经过 4067 之后发生了分压。4067 的导通电阻 Ron 在 3.3V 供电时大概有 80Ω 到 120Ω。电位器的等效输出电阻在电位器转到中间位置时是 25kΩ两个 50kΩ 并联因为我用的是 100kΩ 电位器。两者串联分压ADC 输入端的电压就变成 1.65V × 100kΩ / (100kΩ 120Ω)算下来损耗约 1.98mV这个损耗不大。那为什么实测差了 250mV 呢真正的元凶是 STM32 ADC 的采样电容。STM32F103 的内部采样电容约 8pF采样时间设置在 55.5 个周期ADC 时钟 12MHz 时约为 4.6μs。从 25kΩ 源阻抗给 8pF 电容充电充到接近满值的时间常数是 25kΩ × 8pF 200ns理论上 4.6μs 足够充满。但别忘了还有 4067 的导通电阻 120Ω加在一起是 25kΩ 120Ω时间常数几乎没变所以问题不在这。我又看了示波器波形才发现电位器抽头不是纯电阻分压它和 4067 通道引脚上的 100nF 电容组成了一阶 RC 低通滤波时间常数 25kΩ × 100nF 2.5ms。之前加的去耦电容虽然把噪声滤掉了但也把信号的建立时间拖慢了。ADC 在切换后 50μs 就采样此时电容电压才充到目标值的 1 - e^(-50μs / 2500μs) ≈ 2%也就是电容上电压还远没到达 1.65V读出来的当然偏低。这里我犯了个低级错误为了滤除噪声给每个通道加了 100nF 电容但是这个电容对高阻信号源来说太大了。在不改硬件的前提下我把延时从 50μs 增加到 10ms读数终于正常了。但代价是采样速率大幅下降16 路每轮要 160ms已经有点尴尬。后来我重新评估了硬件设计把每个通道的去耦电容从 100nF 改成了 1nF。高频噪声照样被滤掉而时间常数变成 25kΩ × 1nF 25μs50μs 的延时就能稳定到 0.86精度可接受。如果你已经有成品板子不能改硬件那就老老实实加延时或者软件滤波别想着只靠调软件参数就能同时兼顾噪声抑制和采样速度。说实话这个坑花了我接近半天时间因为不拆电容看不到波形一直以为是代码问题。后来用示波器看 COM 引脚和 PA1 引脚的波形对比通道切换瞬间的变化才意识到是 RC 建立时间的问题。这也让我养成了习惯凡是多路复用扩展 ADC抓波形永远比猜代码快。4.3 坑后的参数速查表这两个坑本质上都和通道切换后的信号建立时间有关这里整理一个速查表方便大家估算延时信号源阻抗通道电容建立到99.9%所需时间建议延时100Ω100nF69μs100μs1kΩ100nF690μs1ms10kΩ100nF6.9ms10ms25kΩ1nF172μs200μs100kΩ1nF690μs1ms实际写代码的时候我会把延时值设成表格里的 1.5 倍左右留出余量。这样虽然牺牲了一点采样率但换来的是数据稳定项目进度不会卡在最难查的鬼畜 bug 上。5. 精度提升与调试方法5.1 提高多路采集精度的几个措施如果你对精度有要求以下几个措施建议按顺序安排第一重新选择源阻抗。凡是接到 4067 的信号最好是经过运放缓冲后以低阻抗输出。运放输出阻抗一般在几十欧姆以下配合 4067 的 120Ω 导通电阻总的源阻抗也不会超过 200Ω这时候即使通道有 100nF 电容建立时间也就几十微秒延时压力很小。第二ADC 参考电压用外部基准。STM32 内部 VREF 精度一般如果追求 12 位有效位数里的最后几位还是外接一个 REF3030 或者类似的高精度基准比较靠谱。4067 不会影响参考电压精度但 ADC 的参考稳了整个采集链路的底噪就低。第三每个通道单独校准增益误差。4067 不同通道的导通电阻有差异手册上给的是典型值实际能做到的一致性大概在 ±10Ω 以内。对 12 位 ADC 来说这个误差在信号源阻抗低时几乎可以忽略但如果信号源阻抗高每个通道测同一电压可能会差十几个 LSB这时就要做逐通道校准系数表。5.2 调试多路 ADC 的工具和方法调试这类型系统一个靠谱的示波器必不可少。我没有用逻辑分析仪因为关键波形是模拟量逻辑分析仪看不出来。示波器探头直接点在三个地方4067 的 COM 引脚看切换瞬间信号有没有弹跳和过冲STM32 的 ADC 输入引脚看信号是否已经稳定到最终值通道信号源输出端确认源信号本身有没有被 4067 内部漏电影响。另外建议写一个简单的上位机串口小工具把 16 路采样值按行打印出来一眼就能看出哪一路异常。我在串口助手里用表格格式输出比如CH0: 2048 CH1: 4095 CH2: 0 ... CH0: 2049 CH1: 4095 CH2: 0 ...这样连续刷新几屏就能看到数据飘不飘、串不串比单独打印一路有效率得多。5.3 4067 的采样率极限最后说说 4067 到底能跑多快。理论速率取决于 4067 的开关时间和 ADC 采样时间但实际工程里往往被 RC 建立时间限制。如果信号源阻抗低于 1kΩ通道电容控制在 1nF 以下切换延时 10μs 就能达到 0.1% 精度单通道采样加转换共约 20μs16 路全扫也就 320μs对应约 3k 次/秒的等效采样率。这个速度对温度、电池电压、模拟电位器这类信号绰绰有余。如果你的项目需要更高的采样率建议换用专门的模拟多路复用器比如 ADG706 或 TMUX 系列的开关并优化 layout或者直接用带多路采样保持的 ADC 芯片。4067 定位就是低速大通道数的场景别拿它硬扛音频采样否则只能得到一堆噪音。6. 一些补充经验个人体会比较深的一点是用 4067 这类模拟开关最容易踩的坑其实是「你以为它是数字芯片的可能性不大但它模拟特性就是很麻烦」。比如导通电阻、漏电流、电荷注入、通道间串扰这些参数在数字电路里完全没有概念从纯逻辑视角去写驱动必然会在物理层翻车。我的建议是上手之前先把手册里的模拟参数表格翻一遍重点关注 Ron、Ton/Toff、Cds漏极-源极电容这几个值再结合自己的信号源阻抗估算建立时间提前在代码里预留可调延时参数后面调试会省很多事。在软件结构上我也吸取了这次教训不要把延时值写死在切换函数里。建议做成一个全局变量供调试时在线调整。比如volatile uint16_t mux_settle_time_us 50; // 调试时可以临时改大这不仅方便你在真机上快速试参数也能在量产前根据实际元器件批次微调整不会因为换了一批 4067Ron 略有差异就重新编译固件。当然量产时这个变量应该通过校准流程写入参数区。我这次项目最后 16 路采样全部稳定每路读数跳动不超过 3 个 LSB12 位模式下延时值定在 50μs整体扫描周期约 5ms。对于慢变信号的采集需求来说这个方案性价比确实很高。希望这篇笔记能帮你在做多通道采集时少走点弯路。