news 2026/9/7 8:08:36

CD74HC4067扩展STM32 ADC通道:16路采样实战与避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
CD74HC4067扩展STM32 ADC通道:16路采样实战与避坑指南

如果你们项目里也遇到过单片机片上ADC通道不够用、又不想上一颗独立ADC芯片的情况,那CD74HC4067应该是一个绕不开的器件。简单说,它是一颗16通道模拟多路复用器,可以把单片机原本只有1路或几路的ADC扩展成16路,让一个采样通道轮询读取多个传感器信号。这次调试笔记就围绕这颗4067展开,记录我实际搭建16路ADC采集的完整思路、硬件接线、软件逻辑,以及最后差点把我劝退的两个坑。

先说结论:CD74HC4067用好了是扩展ADC通道的利器,用不好就是数据乱跳、通道串扰的背锅侠。这篇文章适合正在做多通道传感器采集、数据采集板或者监控系统的嵌入式工程师,尤其是手头MCU刚好ADC资源紧张的朋友,可以直接参考这里的接法和代码逻辑,少走弯路。

1. 方案选型思考:为什么最终选了CD74HC4067

1.1 片内ADC资源不够时的几条出路

做嵌入式项目,经常会碰到“ADC通道不够用”的情况。常见的出路有这么几条:换一颗ADC通道更多的MCU,外接独立ADC芯片,或者用模拟多路复用器扩展。三条路各有适用场景,但从成本、改动量和开发周期来看,用模拟多路复用器往往是最省事的选择。

换MCU的问题在于牵一发动全身。你可能只是缺两路ADC,但换一颗芯片意味着引脚兼容性、外围电路、Bootloader、底层库全都要重新适配,硬件改动量大,软件上能直接复用的代码也少了。独立ADC芯片(比如ADS1115、MCP3561这类)精度高、通道也多,但通信协议(I2C/SPI)和驱动代码是额外的工作量,而且价格通常是几块钱到几十块钱,在小批量产品里成本压力不小。

这时候CD74HC4067这种8块钱以内的模拟开关阵列就有优势了。它本质上就是一堆可以用数字信号控制的单刀双掷开关,把一路ADC输入分时切换到16路模拟信号上,MCU侧只需要额外占用4个GPIO口来控制通道选择,就能实现16路模拟量的采集,改动成本极低。

1.2 CD74HC4067的核心特性与优势

这颗芯片的原理并不复杂,内部是16个模拟开关,通过S0~S3四个选择引脚的电平组合来决定公共引脚(COM,常见标号是Z)和16个输入输出引脚(X0~X15)中的哪一路导通。导通电阻典型值大概在70Ω左右,通道切换时间在几十纳秒级别,对低速ADC采样来说完全够用。

选它还有一个重要原因:支持双向导通。也就是说它不仅能把模拟信号从Xx侧切换到COM侧给ADC采样,也能把COM侧的信号反向输出到任意一路Xx,做多路DAC输出或者多路IO扩展也可以。虽然这次项目只用了ADC采样方向,但余量留在这里,后续产品扩展功能时不用改板子。

另外CD74HC4067的工作电压范围是2V~12V,跟3.3V的单片机系统可以直接匹配,不需要额外电平转换。模拟信号的输入范围理论上也能到电源轨附近,只要信号在供电范围内都能正常传输,这对于直接采集0~3.3V的传感器输出信号来说很友好。

2. 硬件电路设计与接线详析

2.1 引脚功能与真值表对照

拿到CD74HC4067,第一件事就是确认引脚定义和通道选择逻辑。芯片常见封装是SOIC-24或者TSSOP-24,引脚分布有规律,但不同厂家的丝印脚位排序可能略有差异,一定以数据手册为准。

通道选择的核心逻辑是四位二进制,S0是最低位,S3是最高位。比如要让X0通道导通,S3~S0就应该是0000;要让X7导通,就是0111;要让X15导通,就是1111。实际开发时,这四个控制脚直接接MCU的任意GPIO,软件里把GPIO拉高或拉低就能切换通道。

我在实际接线上采用了一组8位并排的排针来引出S0~S3和公共脚,方便调试时用杜邦线飞线。不过建议产品化阶段还是画进PCB,飞线在低速模拟采样时问题不大,但高速信号和复杂电磁环境下,飞线带来的寄生参数会影响信号质量。

2.2 信号输入端的RC滤波处理

模拟信号进4067之前,我建议每个通道都加一级RC低通滤波。虽然这会增加BOM成本,但对采样稳定性的提升非常明显。4067内部的模拟开关在切换瞬间会产生电荷注入,这个电荷会耦合到信号源上,如果信号源内阻本身比较大,那么ADC采到的值就会出现明显抖动。

实际项目里我用的传感器输出阻抗普遍在10kΩ~100kΩ之间,所以每个通道入口放了一个100Ω电阻串联、100nF电容对地并联,截止频率约为16kHz。这个频率对大多数慢变传感器信号(温度、压力、液位)来说足够,又足以滤掉开关切换引入的高频毛刺。

如果你采样的信号本身是高频动态信号,比如振动传感器或音频信号,RC截止频率需要相应调高,甚至考虑去掉RC,改为在软件上做多次采样取均值来平滑。RC滤波和软件滤波是可以叠加的,两者一起用效果更好,后面软件部分我会讲我具体怎么处理。

2.3 公共端与ADC引脚的连接细节

CD74HC4067的公共端(Z脚)接MCU的ADC输入引脚。看似简单,但有两个细节需要注意:一是公共端到ADC引脚之间的距离尽量短,避免走线过长引入噪声;二是如果MCU的ADC输入引脚本身有内部上拉或下拉电阻(部分型号可以配置),必须把它们关掉,否则分压效应会直接影响采样精度。

我调试时曾经忽略了这个细节,MCU的ADC引脚内部的上下拉电阻没有关闭,结果测出来电压总是比实际值偏低。排查了很久,最后发现是内部下拉电阻和传感器阻抗形成分压,硬生生把信号拉低了。后来在初始化代码里把引脚设置为模拟输入模式,内部上下拉全部禁用,数据就恢复正常了。

另外一个细节是供电去耦。CD74HC4067的VCC引脚和GND之间放一个0.1μF的陶瓷电容是标配,电容尽量靠近芯片引脚放置。这颗电容的作用是给芯片内部的开关驱动电路提供瞬态电流,防止切换瞬间供电电压跌落引起的逻辑不稳定。虽然4067的功耗很低,但这种细节在批量产品里经常决定稳定性的上限。

3. 软件实现与代码逻辑解析

3.1 GPIO控制通道切换的底层实现

控制CD74HC4067的通道,本质上就是通过GPIO输出四位二进制编码。我用的是STM32F103系列,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_PORT GPIOB void MUX_SelectChannel(uint8_t ch) { if (ch > 15) return; HAL_GPIO_WritePin(MUX_PORT, MUX_S0_PIN, (ch & 0x01) ? GPIO_PIN_SET : GPIO_PIN_RESET); HAL_GPIO_WritePin(MUX_PORT, MUX_S1_PIN, (ch & 0x02) ? GPIO_PIN_SET : GPIO_PIN_RESET); HAL_GPIO_WritePin(MUX_PORT, MUX_S2_PIN, (ch & 0x04) ? GPIO_PIN_SET : GPIO_PIN_RESET); HAL_GPIO_WritePin(MUX_PORT, MUX_S3_PIN, (ch & 0x08) ? GPIO_PIN_SET : GPIO_PIN_RESET); }

这段代码乍看没什么技术含量,但有两点值得说。第一,GPIO初始化时必须把S0~S3全部配置成推挽输出模式,初始状态设为低电平。如果上电瞬间GPIO是浮空输入状态,4067的通道选择引脚电平不确定,可能在系统启动阶段就随机导通某一路,如果该路意外接地或接高压,会对后端电路造成不可预知的影响。

第二,HAL库的HAL_GPIO_WritePin函数在每次调用时会做参数校验和寄存器读写,单次调用开销不大,但切换16个通道就要执行4次,循环采集时这一笔时间开销累计起来也可观。如果对采样速度有极致要求,可以直接操作寄存器BSRR,一次性写入四个引脚的电平状态,速度能快一个数量级。

3.2 ADC轮询采集与数据合并逻辑

通道切换和ADC采样的配合,这里有一个容易被忽视的问题:切换通道后立即启动ADC转换,读到的值往往不是当前通道的真实电压,而是上一个通道的残留电压与新通道电压的混合值。这是我这次调试踩到的第二个大坑,后面会专门展开说。

我先给出最终优化过的采集逻辑。基本思路是:切换通道后,延时等待模拟开关稳定和RC滤波电容充电完成,然后连续采样多次,去掉最大值和最小值,取中间值平均。

#define ADC_SAMPLE_TIMES 5 uint16_t MUX_ReadChannel(uint8_t ch, ADC_HandleTypeDef* hadc) { uint32_t sum = 0; uint16_t val[ADC_SAMPLE_TIMES]; uint16_t min_val, max_val; MUX_SelectChannel(ch); HAL_Delay(1); // 等通道稳定,这个延时很重要 for (int i = 0; i < ADC_SAMPLE_TIMES; i++) { HAL_ADC_Start(hadc); HAL_ADC_PollForConversion(hadc, 10); val[i] = HAL_ADC_GetValue(hadc); HAL_ADC_Stop(hadc); HAL_Delay(1); } min_val = val[0]; max_val = val[0]; sum = 0; for (int i = 0; i < ADC_SAMPLE_TIMES; i++) { if (val[i] < min_val) min_val = val[i]; if (val[i] > max_val) max_val = val[i]; sum += val[i]; } // 去掉最大最小值后取平均 sum = sum - min_val - max_val; return (uint16_t)(sum / (ADC_SAMPLE_TIMES - 2)); }

这里我特意在通道切换后加了1ms的延时。很多人会觉得这个延时太奢侈了,毕竟4067的切换时间只有几十纳秒。但别忘了,RC滤波电容的充电需要时间,传感器源阻抗和4067导通电阻、RC电阻串联起来会形成一个时间常数,只有等电容电压充到接近信号真实电压,ADC采到的数据才有意义。

以100Ω电阻加100nF电容为例,时间常数是10μs,理论上等50μs就能充到99%以上。但传感器的源阻抗如果是10kΩ,串联进电路后时间常数直接变成1ms级别,所以1ms延时是保险的做法。如果你的传感器源阻抗更大,这个延时还要相应加长。

3.3 单次采集改成周期轮询的主循环框架

多通道采集一般都是在主循环或者定时器中断里做轮询。我用的方案是定时器每100ms触发一次软件标志位,主循环检测到标志位后遍历16个通道,依次采集数据并存入全局数组,供显示或者通信模块使用。

#define TOTAL_CHANNELS 16 uint16_t adc_values[TOTAL_CHANNELS]; volatile uint8_t adc_flag = 0; void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM2) { adc_flag = 1; } } void ADC_ScanTask(void) { if (adc_flag) { adc_flag = 0; for (int ch = 0; ch < TOTAL_CHANNELS; ch++) { adc_values[ch] = MUX_ReadChannel(ch, &hadc1); } } }

这个框架的好处是采集节拍固定,数据更新周期可控,不会因为某个通道异常而阻塞整个系统。16个通道每个通道采样5次、每次间隔1ms,一轮采集下来大概需要80ms~100ms,正好和100ms的定时器周期匹配。如果后续单个通道的滤波次数增加,可以把定时器周期相应调大,避免采集任务还没跑完下一轮标志位又来了。

4. 两个调试大坑实录

4.1 坑一:通道选择引脚悬空,采样值随机跳变

这个坑是我在这次项目里遇到的最诡异的问题。现象是:上电后,固定让X0通道接一个1.5V的稳定电压,理论上ADC采到的数值应该稳定在某个区间,但实际数据显示一会儿是X0的值,一会儿是X2、X5、X8的值,完全没有规律,像是被幽灵切换了通道。

当时的第一反应是4067芯片坏了,换了一颗新芯片,问题依旧。然后用万用表量S0~S3的电压,发现这四个引脚的电平竟然在0V和3.3V之间缓慢漂移。再回头查代码,GPIO初始化明明已经配置成推挽输出了,为什么还会出现高阻态?

问题出在初始化顺序上。我调用MUX_GPIO_Init()函数初始化GPIO的同时,也初始化了ADC和DMA,而ADC的初始化函数里有一段代码会重新配置GPIO,间接把S0~S3引脚的模式改成了模拟输入。模拟输入模式下引脚呈高阻态,S0~S3电平由外部浮空决定,4067的通道选择逻辑就乱了,表现为随机切换到不同通道。

知道了原因就好办了,把GPIO初始化的顺序调整到ADC初始化之后,确保4067的控制引脚始终处于推挽输出状态。另外在硬件上我也做了兜底:四个通道选择引脚各自接了一个10kΩ的下拉电阻到地。这样即使代码里出现异常导致引脚变成输入模式,4067也会默认选中X0通道,至少不会出现随机乱跳的情况。

这个经历给我一个很重要的教训:多个外设共用GPIO时,初始化顺序和引脚复用配置的优先级必须梳理清楚。特别是在HAL库这种高度封装的环境里,函数调用顺序产生的影响远比你想象的隐蔽。

4.2 坑二:通道切换后立即读取ADC,前几次数据明显异常

第二个坑是采样数据稳定性问题。现象是:每个通道采集到的数据在初始几次总是偏高或偏低,等到第五六次采样后才逐渐稳定。比如X1通道接的是一个稳定的2.0V电压,理论上采样值应该在2480左右(12位ADC,3.3V参考电压),但实际前两次采到2300甚至2200,后面才慢慢回到2480附近。

这个问题本质是RC滤波电路建立时间不够。4067切换到新通道后,公共端到ADC引脚之间的寄生电容、外部RC滤波电容、传感器源阻抗、4067导通电阻共同组成了一阶RC电路,电容电压从原来的电平过渡到新的信号电平需要时间。模拟开关切换虽然只需要几十纳秒,但电容充放电的过程却要持续几百微秒甚至毫秒级。

我的解决方法是双管齐下。软件上,通道切换后增加延时,再连续采集多次,丢弃前两次数据,只保留后面几次用于计算平均。硬件上,把RC滤波电阻从100Ω降到10Ω,降低时间常数,加快建立过程。两者配合之下,每个通道只需要两三次采样就能进入稳定状态。

补充一个更精细的做法:如果项目对采样速度和稳定性都有要求,可以在切换通道之前先把ADC引脚配置为普通GPIO输出低电平,保持几十微秒,把通道上的电容电荷放掉,然后再切换回模拟输入模式开始采样。这个“预放电”技巧在处理多通道快速切换时很有效,适合做实操级优化。

5. 数据稳定性优化与滤波策略补充

5.1 一阶低通滤波在软件端的实现

硬件RC滤波是模拟域的平滑手段,但光靠它还不够。如果被测信号本身带有工频干扰或者传感器固有噪声,ADC采样值会呈现明显的随机波动。软件上做数字滤波是最直接的补充手段。

我用的是一阶低通滤波算法,公式是Y(n) = α * X(n) + (1-α) * Y(n-1),其中X(n)是当前采样值,Y(n-1)是上一次滤波输出值,α是滤波系数。这个滤波器的响应速度和平滑程度由α决定,α越大响应越快但平滑越弱,α越小响应越慢但平滑越强。

static float adc_filtered[TOTAL_CHANNELS]; float ADC_LowPassFilter(uint8_t ch, uint16_t new_value, float alpha) { adc_filtered[ch] = alpha * new_value + (1.0f - alpha) * adc_filtered[ch]; return adc_filtered[ch]; }

对慢变传感器信号,我一般取α=0.2,也就是新的采样值只占20%权重,历史值占80%。这样既能有效抑制随机噪声,又不会让响应滞后到影响系统控制性能。如果你需要更快的响应,α可以调到0.3~0.4,但相应的数据波动也会更大。

5.2 中位值平均滤波的组合策略

一阶低通滤波不擅长对付脉冲干扰,比如静电放电、继电器吸合瞬间产生的尖峰噪声。这种干扰可能让单次采样值远离真实值,而一阶低通滤波会把尖峰逐步“消化”掉,造成一段时间内的数据失真。

对付这种场景,我会在MUX_ReadChannel函数里先做中位值平均滤波,再做一阶低通平滑。中位值平均滤波的思路是连续采样N次,去掉最大值和最小值,剩下N-2个值取平均。这个算法能有效剔除脉冲干扰,同时又保留了平均算法对随机噪声的抑制能力。

两段滤波串联后,16路通道的数据质量提升非常明显。用示波器对比滤波前后的波形,噪声峰峰值从原来的±30mV降到了±5mV以内,对后续的控制逻辑完全够用。

5.3 采样周期与系统实时性的平衡

加了滤波算法后,每个通道的采样耗时变得更长,这会导致整个轮询周期拉长。我在实际调试中发现,16通道全量采集加双重滤波,一轮耗时约150ms,对应每秒只能刷新6.6次。对于温度、压力、液位这种慢变量足够,但如果你有需要高速响应的通道,就必须区别对待。

我的建议是对通道分组管理。高速响应的通道(比如过流保护信号)跳过通道延时,直接连续采样两次取平均值,不做过多的滤波处理;低速通道则维持完整的中位值平均加低通滤波。这样既保证关键信号的实时性,又不牺牲整体数据的稳定性。

6. 常见问题排查与速查表

调试CD74HC4067的过程中,前前后后遇到过不少问题,我把典型的几类整理成一个排查表,方便大家对照检查。

现象可能原因排查方法解决方案
采样值随机跳变控制引脚悬空或配置为模拟输入量S0~S3对地电压设置为推挽输出,加10kΩ下拉电阻
切换通道后首几次采样值不准RC建立时间不够示波器观察切换瞬间波形切换后延时1ms,丢弃前两次采样数据
所有通道读数整体偏低ADC引脚内部下拉电阻未关闭查看MCU引脚配置寄存器引脚配置为模拟输入,禁用上下拉
通道之间相互串扰信号源阻抗过高,公共端电容耦合分别测相邻通道接地时的读数降低信号源阻抗,公共端加缓冲运放
高速采样时数据抖动4067导通电阻变化,电荷注入影响用稳定电压源测试不同通道软件中位值平均滤波,硬件加RC低通
多通道间读数偏差大通道间导通电阻不一致用同一电压测试不同通道系统校准,建立通道偏差补偿表

这些排查项是实践里的高频问题,其中“所有通道读数整体偏低”这个现象最容易误判成4067的问题,实际上十有八九是MCU引脚内部上下拉或者传感器驱动能力不足导致的。

7. 复盘与踩坑启示

这次的CD74HC4067多通道ADC采集调试,从画原理图到最后稳定运行,前后花了将近一周时间。回头复盘,大部分精力其实不是花在了4067本身的使用上,而是花在了排查那些看起来“不可能有问题”的环节上。

第一点体会是芯片数据手册要反复读。CD74HC4067的导通电阻、切换时间、电荷注入这些参数,一开始觉得差不多就行,实际工程里每一个参数都会影响最终的数据质量。尤其是电荷注入这个指标,在高速切换场景下直接表现为采样毛刺,不提前了解就只能靠踩坑积累经验。

第二点是硬件设计要留足调试余地。我这次画的PCB上,每个通道的RC滤波电阻和电容都留了0603封装,方便调试时更换参数;S0~S3控制引脚预留了测试点和下拉电阻位。这些预留看起来不起眼,但真遇到问题时的价值比预想的大得多。举例来说,RC建立时间不够时,我把100Ω电阻换成10Ω只用了两分钟,如果没留调试位就只能重新打板。

第三点是软件架构要为后期优化留口子。通道延时、采样次数、滤波系数这些参数我全部做成了宏定义,而不是直接写在代码里。后续换不同响应速度的传感器时,只需要改宏的值,不需要改动核心算法逻辑,调试成本会低很多。强烈建议所有做采集系统的朋友都保持这个习惯。

这次调试笔记就写到这里,希望这棵菜的经验能帮你避掉几个坑。如果你也遇到类似的模拟开关扩通道问题,欢迎在实际调试中验证这里的建议,再和我交流。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 8:07:32

Unity 6,Unity 2022和团结引擎到底怎么选

背景Unity中国下架了unity6&#xff0c;并且在官网删除了unity6相关的信息。同时unity付费证书分为国内版本和国际版本&#xff0c;以及团结引擎。团结引擎是unity中国基于unity2022针对国内定制的游戏引擎,目前主要是增加了平台支持。官方表示会在后续逐步添加unity6的相关功能…

作者头像 李华
网站建设 2026/9/7 8:04:38

用Delphi自研一维条形码控件:从Code128原理到扫码枪实战

简介&#xff1a;这是一份面向Delphi 6开发者的条形码控件源码&#xff0c;目标是在不安装外部插件的前提下&#xff0c;为老版本IDE项目补充一维条形码生成能力。控件支持Code 39与Code 128等常见制式&#xff0c;前者可编码数字、大写字母与部分符号&#xff0c;后者则能覆盖…

作者头像 李华
网站建设 2026/9/7 8:02:25

C#上位机调用VisionPro实战:ToolBlock加载、扫码触发与九点标定

简介&#xff1a;一份面向C#机器视觉开发者的源码示例&#xff0c;演示如何调用VisionPro库实现图像圆形检测。方案基于Cognex.VisionPro_dotNET&#xff0c;通过CogFindCircleTool工具完成读取图像、设定半径范围、执行查找并显示结果&#xff0c;适用于制造质检、尺寸测量等自…

作者头像 李华