ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

CD74HC4067模拟开关扩展ADC通道实战:从电路到驱动的完整指南

CD74HC4067模拟开关扩展ADC通道实战:从电路到驱动的完整指南 ADC通道不够用是嵌入式开发里特别常见的事。手头一块主控板上集成ADC通道就8路可设备要采的模拟量加起来有12路换主控意味着重新画板、重新调驱动时间完全不允许外挂独立ADC芯片成本又上去了一个项目还没到量产能抠成本的阶段。我最后选了CD74HC4067这颗16选1模拟开关用4个GPIO把单片机的1路ADC硬生生扩展成16路整个硬件链和驱动代码都调通了。过程不算复杂但有两个调试坑让我印象很深一个和DMA连续采样的时序有关另一个是高阻抗信号源下的采样建立时间问题这篇文章把这些一起整理出来给正在考虑用模拟开关扩ADC的同学做参考。1. 方案选型为什么用模拟开关而不是换主控1.1 通道不够用是普遍痛点但换主控代价太高嵌入式项目里传感器数量一多MCU自带ADC最先扛不住。以常见的8位或32位MCU为例集成ADC通道数一般在6到16个之间但这些通道往往还要分给电池电压检测、按键检测、温度采集、电流采样等不同功能真正留给传感器的余地非常小。我这次的情况是主控带8个ADC通道传感器总计12路直接差4路。第一反应是换一颗ADC通道更多的主控。可查了一圈发现选型到引脚兼容的芯片基本没有一旦换了MCUPCB封装、启动代码、外设驱动、中断优先级、flash空间全都要重新评估一个成熟项目的软件框架会被打散。更别说两周后就要交样机这个方案根本不现实。第二反应是加一块独立ADC芯片比如ADS1115、ADS1256这类。它们的精度确实好但对这个项目来说有点用力过猛。项目里都是电池电压、温度、电位器这类静态或慢变信号12位分辨率就够采样率几十到几百赫兹足够外挂I2C/SPI接口的独立ADC意味着要占用新的总线资源还要处理总线地址冲突、时序握手、驱动移植工作量比想象中大不少。1.2 扩ADC的三种方案我为什么选了模拟开关对比下来核心就是三种方案换主控、外挂独立ADC、用模拟开关扩展。我直接列个表格把关键点摆出来方案成本软件改动量精度影响适用场景换高配主控高大需重写驱动无产品刚立项可选型外挂独立ADC中高中大需移植驱动高精度对精度和采样率有硬要求模拟开关扩展极低小复用原ADC驱动有导通电阻和串扰通道多、慢速信号、成本敏感模拟开关扩展的思路很简单把多路传感器的输出通过一个16选1的模拟开关接到MCU的同一个ADC引脚上通过GPIO选择当前要采哪一路。一次只导通一路相当于把ADC通道“分时复用”成多路。这个方案的优点是硬件改动极小原有ADC驱动大部分保留软件上只需要加一个通道切换函数。缺点也很明显导通电阻会影响信号切换后的建立时间需要额外处理这两点正是后面两个调试坑的根源。1.3 权衡之后CD74HC4067的适用边界结合项目里都是直流变化缓慢的信号我最终把目光锁定到CD74HC4067上。它是一颗16通道的CMOS模拟多路复用器/信号分离器单芯片就能覆盖12路需求还留了4路余量。和其它方案对比下来CD74HC4067在三个维度上很契合控制简单4根地址线S0到S3一个使能脚EN全部接GPIO电平兼容3.3V和5V。对模拟信号完全是“透明”的信号可以从公共端流向任意通道也可以反向双向模拟开关非常适合分时复用。导通电阻大约几十欧到一百多欧对低阻抗信号源来说影响不大配合高输入阻抗的ADC引脚使用误差可控。当然这颗芯片也有边界它不适合高速采样场景通道切换后需要等待信号稳定信号幅值不能超过供电电压范围高阻抗信号源直接接入会带来分压误差。但只要应用场景是“慢速、多通道、低成本”它就是非常优秀的方案。2. CD74HC4067的参数理解与电路设计细节2.1 模拟开关内部原理它怎么做到一拖十六CD74HC4067本质上就是16个CMOS传输门Transmission Gate并联在一根公共输出线上。传输门由P沟道和N沟道MOSFET并联组成控制信号为高时两个管子都导通输入和输出近似短接控制信号为低时两个管子都关断通道呈高阻态。地址译码器根据S0到S3的4位二进制组合从16个通道里选中唯一一个导通。使能脚EN低有效EN为高时所有通道全部关断这个特性在设计多片级联时非常有用把所有芯片的地址线并联再用芯片各自的EN引脚做片选就能把通道扩展到32路、48路甚至更多。理解这个内部结构对后面排查问题很有帮助。传输门导通时不是理想开关它有一个等效电阻Ron电压不同Ron还会小幅变化关断时也不是绝对断路会有极小的漏电流通常在nA量级对一般采集系统影响可忽略。真正需要关注的是导通瞬间的电荷注入效应——MOS管栅极和漏源之间存在寄生电容开关从关断变导通时会有微小电荷转移这在处理高阻抗信号源时会放大成可见的电压跳变。2.2 电路连接地址线、公共端、使能脚一个都不能接错CD74HC4067的引脚配置很直观但细节决定成败。我实际连接的时候有几个点花了不少功夫模拟信号路径上传感器输出分别接到芯片的16个输入通道引脚通常标为Y0到Y15或C0到C15公共输出端通常标为Z或COM接到MCU的ADC输入引脚。公共端的命名在不同厂商标注不一样有些叫COM有些叫Z原理都一样。我最初拿到一颗国产兼容型号引脚标注方式不太一样对着数据手册核对了两遍才放心接线。地址线S0到S3接到MCU的4个GPIOEN引脚直接接地让芯片始终处于使能状态。需要注意的是EN下拉而不是上拉因为它是低电平有效如果GPIO初始状态不确定最好在硬件上用一个10kΩ电阻把EN拉低避免上电瞬间芯片处于全关断状态导致读出全零。一个比较容易忽略的点是未使用的输入通道不能悬空。模拟开关的输入悬空时引脚电位不确定可能处于中间电平导致内部CMOS管反复翻转增大静态功耗。更糟糕的是悬空通道通过寄生电容可能把噪声耦合到公共输出线上。我处理的办法是把不用的通道全部接地或者在PCB上预留了0Ω电阻位根据实际情况决定接GND还是接信号。2.3 电源去耦和引脚保护不要偷懒CD74HC4067的供电范围是2V到6V我用的是3.3V供电刚好和主控逻辑电平同域。但模拟开关的导通电阻和电源电压强相关供电越低Ron越大。数据手册给出的Ron典型值大约在70ΩVCC4.5V时3.3V供电时可能会到100Ω以上这个数值虽然不大但在后面积累误差时不能忽略。电源引脚旁边一定要放去耦电容我用了0.1μF和10μF双电容组合靠近芯片的电源引脚放置。模拟开关内部有多个反相器和电平转换电路开关切换瞬间会产生尖峰电流没有去耦电容的话这个尖峰会顺着电源轨传到ADC的地或参考电压上导致采样值跳动。MUX模拟输入引脚靠近PCB边缘的地方我还加了一组ESD保护二极管和串联电阻防止插拔线束时静电打到芯片内部。公共端到MCU ADC引脚之间我串了一个100Ω的电阻这个电阻有两个作用一是限制异常情况下的电流二是给后面估算RC时间常数提供一个基准点方便调试时计算信号建立时间。走线尽量短粗减少电感效应模拟信号和数字控制信号分开布线避免S0到S3切换时的数字沿跳变干扰模拟通路。3. 驱动代码与采样逻辑实现3.1 最简驱动代码切通道、等稳定、读一次硬件连接确认没问题后软件部分就顺理成章了。以STM32的HAL库为例我的实现思路是先根据CD74HC4067的地址逻辑写一个通道切换函数然后每次读取一个通道时先切换、再延时、最后启动ADC单次转换。// 定义MUX地址线引脚 #define MUX_S0_PIN GPIO_PIN_0 #define MUX_S0_PORT GPIOA #define MUX_S1_PIN GPIO_PIN_1 #define MUX_S1_PORT GPIOA #define MUX_S2_PIN GPIO_PIN_2 #define MUX_S2_PORT GPIOA #define MUX_S3_PIN GPIO_PIN_3 #define MUX_S3_PORT GPIOA // 切换MUX通道 void mux_set_channel(uint8_t ch) { ch 0x0F; // 4位地址范围为0~15 HAL_GPIO_WritePin(MUX_S0_PORT, MUX_S0_PIN, (ch 0) 0x01 ? GPIO_PIN_SET : GPIO_PIN_RESET); HAL_GPIO_WritePin(MUX_S1_PORT, MUX_S1_PIN, (ch 1) 0x01 ? GPIO_PIN_SET : GPIO_PIN_RESET); HAL_GPIO_WritePin(MUX_S2_PORT, MUX_S2_PIN, (ch 2) 0x01 ? GPIO_PIN_SET : GPIO_PIN_RESET); HAL_GPIO_WritePin(MUX_S3_PORT, MUX_S3_PIN, (ch 3) 0x01 ? GPIO_PIN_SET : GPIO_PIN_RESET); }读取某个通道的ADC值时切完通道不要急着启动转换至少延时几百微秒等模拟开关导通后的信号建立过程完全结束。这个延时在后面会细讲先记住它必不可少。uint16_t adc_read_mux_channel(uint8_t ch) { uint16_t val; mux_set_channel(ch); // 1. 切换MUX通道 delay_us(500); // 2. 等待信号稳定不能省 HAL_ADC_Start(hadc1); // 3. 启动单次ADC转换 HAL_ADC_PollForConversion(hadc1, 10); // 4. 等待转换完成 val HAL_ADC_GetValue(hadc1); // 5. 读取数值 HAL_ADC_Stop(hadc1); // 6. 停止ADC return val; }对于12路传感器轮询我开了个循环每路间隔100ms采集一次保证通道之间不会互相干扰。整个逻辑非常直白核心思路就是“一个时刻只导通一个通道切换到哪个通道就读哪个通道”绝不追求连续转换模式的高速。3.2 采样时间的设置不是越快越好STM32的ADC有一个可配置的采样时间参数它决定采样阶段保持电容充电的时间。HAL库中可以直接配置采样保持周期数我这里是把它拉到最大ADC_ChannelConfTypeDef sConfig {0}; sConfig.Channel ADC_CHANNEL_0; sConfig.Rank ADC_REGULAR_RANK_1; sConfig.SamplingTime ADC_SAMPLETIME_239CYCLES_5; // 最长采样时间 HAL_ADC_ConfigChannel(hadc1, sConfig);很多人习惯用默认的几十个周期就去采这在信号源阻抗很低、直连MCU引脚时问题不大。但经过CD74HC4067后信号路径上多了模拟开关的Ron再加上PCB走线和MUX内部寄生电容ADC输入端的等效源阻抗比直连时高不少。采样电容需要更多时间才能充到和输入信号一致的电平采样时间太短的话读到的值会比真实值偏低这在第5章会仔细分析。时钟频率也要注意。ADC时钟越高单个周期越短同样的采样周期数对应的实际采样时间越短。我用的是12MHz的ADC时钟239.5个周期换算下来大约20μs这个时长对大多数传感器信号来说绰绰有余。3.3 多通道轮询与数据后处理12路数据轮询读取时我只对每路做简单的中位值滤波连续读3次取中间值作为最终结果。这个方法对付偶发的尖峰干扰很有效尤其是电磁环境不理想的工业现场。代码如下uint16_t read_filtered(uint8_t ch) { uint16_t samples[3]; uint16_t temp; for (int i 0; i 3; i) { samples[i] adc_read_mux_channel(ch); } // 简单冒泡取中位 for (int i 0; i 2; i) { for (int j i 1; j 3; j) { if (samples[j] samples[i]) { temp samples[i]; samples[i] samples[j]; samples[j] temp; } } } return samples[1]; }由于采集的是慢变信号三通道三次采样之间的时间差完全不影响最终结果。如果未来要采快速变化的信号中位值平均滤波或者滑动平均滤波会更合适但原理都是一样的牺牲一点实时性换来稳定干净的数据。4. 调试坑一DMA连续模式下切通道读到的总是旧数据4.1 现象DMA连续模式永远读到上一个通道的值第一次调通代码后我寻思既然要轮询12个通道用ADC的DMA连续模式不是更省CPU吗于是把单次转换改成了DMA采集初始化时用HAL_ADC_Start_DMA(hadc1, buffer, 1)启动MCU自动把ADC结果搬到内存每切换一次MUX通道就等DMA搬完再读。结果问题立刻冒出来了切换CD74HC4067通道后读到的数值和上一通道的数值几乎一模一样。比如通道0采集到3.2V切到通道1后读到的还是3.2V再切到通道2读到的是通道1的值。看起来像是ADC没有感知到MUX切换或者说管线里始终残留着上一次转换的结果。一开始我怀疑是MUX地址线没切换成功用万用表量GPIO输出又量MUX引脚电平全部正常。给MUX输入通道加不同的电压从芯片公共端测输出的电压也是正确的说明硬件通路没问题。那问题就只能出在软件逻辑和ADC/DMA的配合上。4.2 排查从硬件怀疑到软件定位检查了一遍代码发现一个关键问题HAL_ADC_Start_DMA启动后ADC会进入连续转换模式只要不调用HAL_ADC_Stop它就一直在转。DMA处于循环模式时每次转换结果都会自动覆盖到内存中的同一个变量或数组里。这时切换了MUX通道ADC并不知道外部信号已经变了它仍在持续转换DMA也在持续搬运。问题出在时序对齐上切换到新通道后我读取缓冲区的时机和DMA搬运的时机是错位的。ADC内部转换完成到DMA把数据写入内存之间有一个延迟我读到的缓冲区内容有可能是新通道转换之前被搬运进去的值。这个现象在DMA循环模式下尤其明显因为缓冲区数据是流水线式的上一次循环写入的旧数据还没来得及被覆盖我就把整个缓冲区读走了解析。更隐蔽的是如果连续两次切换通道的时间间隔小于DMA完整搬运一个缓冲区的周期缓冲区里会出现“新旧混合”的数据——一部分还是旧通道的结果一部分是刚开始转换的新通道结果解析出来自然一团糟。4.3 解法放弃DMA连续模式回归单次转换或者停干净再切想明白原理后我做了两个改变。第一个改变是放弃了用DMA循环模式去跟踪MUX切换的写法改成最保险的单次转换模式每次读取都走完整的“切换→等待→启动→等待完成→停止”流程。第二个改变是如果确实需要用到DMA比如想省CPU时间必须在切换MUX通道之前把ADC和DMA彻底停下来清空相关标志位切换完通道等信号稳定后再重新启动。不能图省事直接复用之前启动的DMA通道。// 切换MUX通道前的正确处理方式 HAL_ADC_Stop(hadc1); // 停止ADC连续转换 __HAL_DMA_DISABLE(hadc1.DMA_Handle); // 停止DMA搬运 __HAL_DMA_CLEAR_FLAG(hadc1.DMA_Handle, DMA_FLAG_TC | DMA_FLAG_HT | DMA_FLAG_TE); HAL_ADC_Start(hadc1); // 重新启动转换完成再读 mux_set_channel(5); // 切换到新通道 delay_us(500); // 等待稳定 HAL_ADC_Start(hadc1); HAL_ADC_PollForConversion(hadc1, 10); val HAL_ADC_GetValue(hadc1); HAL_ADC_Stop(hadc1);这套改动看起来只是加了几行停止和清标志的代码实际上是彻底改变了ADC的运行方式从“后台无脑连续跑”变成“按需单次触发”。对慢速多通道采集来说单次触发模式的控制粒度更细MUX切到哪个通道就采哪路不需要猜DMA缓冲区里的数据到底对应哪个通道。这个坑给我的教训是DMA和连续转换模式是给“固定通道、持续高速采样”的场景用的一旦信号通道本身会被动态切换再叠加DMA的异步搬运时序控制就会变得非常脆弱。与其费劲去对齐DMA缓冲区和MUX通道的对应关系不如直接用单次转换代码逻辑一目了然也更好排查问题。5. 调试坑二采样值整体偏低高阻抗信号源 采样时间不够5.1 现象万用表量MUX输出正常MCU采集却总是少一截第一个坑解决后又遇到第二个怪问题。我用一个固定3.3V电压接到CD74HC4067的某个输入通道用示波器和万用表量芯片公共输出端电压都是准确的3.3V。但MCU的ADC读回来却只有2.9V左右而且不同通道之间还有差异越是信号源输出阻抗高的通道偏差越大。拿热敏电阻分压网络举例分压网络中串联电阻是10kΩ热敏电阻阻值随温度变化分压点的输出阻抗在几百欧到十几千欧之间变化。用万用表量分压点的电压完全正常但经过MUX和ADC之后读数都有偏差。当时我一度怀疑MCU ADC坏了后来用一个电位器直接输出低阻抗到同一ADC引脚测试读数恢复正常这才确定问题出在模拟开关和ADC采样电容的配合上。5.2 分析采样电容充电过程需要时间这是硬约束把这个现象拆开看整个模拟信号链路是传感器/分压网络 → CD74HC4067的Ron → PCB走线 → MCU ADC引脚 → ADC内部的采样电容。在STM32的SAR ADC内部每次转换前都要通过内部的采样开关把采样电容接到输入引脚上让电容充电到输入电压。充电完成后开关断开ADC才进入逐次逼近的比较阶段。采样时间就是这个充电时间窗口。STM32F103的ADC采样时间可以配置为1.5、7.5、13.5、28.5、41.5、55.5、71.5、239.5个ADC时钟周期我用12MHz的ADC时钟1.5周期大约只有125ns。再算一下充电回路的等效时间常数CD74HC4067的Ron3.3V供电时按100Ω估热敏电阻分压网络等效输出阻抗取最恶劣情况10kΩADC内部采样电容STM32F103大约12pFPCB走线、MUX内部寄生电容估20到30pF总等效电阻约10.1kΩ总等效电容约40pF左右RC时间常数τ约400ns。要把采样电容充到和输入电压差小于1LSB12位ADC的1LSB约0.024%理论上需要约8个τ也就是3.2μs。如果采样时间只有125ns充电过程只进行了不到1个τ电容上的电压远远没到目标值ADC读出来的结果自然偏低。看起来偏差大小还和源阻抗有关源阻抗越高RC时间常数越大同样的采样时间下误差越大这和实际观察到的“源阻抗越大偏差越大”完全吻合。万用表和示波器输入阻抗极高几乎没有负载效应所以它们看到的电压是准确的而ADC采样电容就像一个小负载在有限时间内充不满读数就失真了。5.3 解法加大采样时间降低源阻抗必要时丢弃首样定位到原因后解决起来并不复杂我前后做了三件事第一件是STM32的ADC采样时间直接拉到最大。按照HAL库配置ADC_SAMPLETIME_239CYCLES_5配合12MHz的ADC时钟实际采样时间约20μs是之前1.5周期的160倍。改动之后固定3.3V输入采集回来的数值从2.9V恢复到了3.29V基本接近真实值。第二件是CD74HC4067的切换延时给了余量。MUX切换后公共端到ADC引脚的线上还挂着之前的电压信号源要通过Ron对线路电容充电。我在每次切换后加500μs延时给信号足够时间稳定。这个延时并不需要精确计算到微秒级因为采集的是直流缓变信号多等几百微秒完全不影响系统实时性。第三件是信号源阻抗确实很高的场景我在MUX公共输出端和ADC引脚之间加了一级运放电压跟随器。运放的输出阻抗极低可以瞬间给采样电容充电就算采样时间短也不怕。这个方案对精度要求高的场合很有效但它增加了硬件成本我只在其中一个高阻抗通道上用了一个单运放其它通道靠加大采样时间就搞定了。这里还有一个细节如果用了DMA批量读取同一通道多次第一次读到的值可能还是上一通道的残留电荷最好丢弃第一帧数据从第二个样本开始用。我之前在调试中见过这个现象首次读数跳动明显比后续均值大丢弃首样后数值立刻稳定。最后补充一句很多人习惯了ADC默认配置直接采集很少深入思考采样电容的充电时间。模拟开关扩展方案把这个隐藏在后台的约束变成了必须面对的问题弄懂RC建立过程之后后面再遇到类似的MUX场景就不会再被坑到了。6. 调试问题速查与后续优化方向6.1 常见问题速查表把这两个调试坑和平时收集的一些经验整理成一张速查表方便排查问题时对照现象可能原因检查/解决方向通道切换后读到旧值DMA循环模式与MUX切换错位改用单次转换切换前后彻底停止ADC和DMA读数整体偏低ADC采样时间过短、源阻抗偏高把采样时间调到最大或加电压跟随器缓冲某个通道读数为零S0-S3地址线配置错误用万用表量GPIO输出和MUX引脚电平对比所有通道读数乱跳EN引脚悬空、电源纹波大EN下拉接地电源加去耦电容相邻通道互相干扰悬空输入通道耦合噪声未用通道接地或接固定电位切换后电压缓慢爬升线路电容大、源阻抗高增加切换后的延时简化走线减小寄生电容MUX输出带不动ADCRon与后级电路分压加缓冲器或降低信号源阻抗6.2 几个可以继续优化的方向CD74HC4067这个方案运行稳定后我又做了一些研究和测试这里分享几个后续优化方向供需要的人参考级联扩展单颗4067是16路用两颗芯片就可以扩展到32路。方法是把两片4067的S0-S3地址线并联控制信号共用再用两个GPIO分别控制两片芯片的EN引脚通过使能不同芯片来选择通道。这种级联方式可以一直扩展下去直到MCU的GPIO不够用。差分信号切换如果传感器输出是差分信号需要两颗CD74HC4067同步切换正负端一片接正一片接负地址线并联EN也并联这样就能实现16路差分信号的复用。动态阈值告警多通道采集系统里可以对每个通道设置独立的高/低阈值采集完当前通道后立即比较超过阈值就置故障标志。相比把所有数据都传回上位机再做判断在MCU内做阈值判断实时性更高也减少了通信负担。通道自检利用MUX的16个通道中预留的几个接口可以接到已知参考电压上每次上电后先采一遍参考通道计算校准系数再对其它通道做软件校正抵消掉Ron随温度和电压的变化。这个方法对提升长期稳定性很有帮助。我在实际使用中还发现一个小技巧CD74HC4067的公共输出端到ADC引脚之间的走线尽量短如果必须走过孔建议在过孔两侧都铺地减少寄生电容耦合。另外MCU开启ADC扫描前先把MUX切到一个确定的通道比如通道0不要让ADC上电后引脚悬空。这些小细节在批量生产和长时间运行时都能避免很多莫名奇怪的软故障。写在最后这两个调试坑调完之后整套16路采集系统在我项目里稳定跑了好几个月。回头看整个过程用模拟开关扩ADC这件事本身不难难的是理解开关切换带来的时序问题和阻抗变化。CD74HC4067的硬件连接、驱动代码、RC时间常数估算这些都不是什么高深理论但在实际工程中如果不注意就会变成让你熬夜排查的“玄学问题”。我个人体会是模拟开关扩ADC本质不是“把1路当成16路用”这么简单而是要老老实实解决“开关切换后的信号建立时间”和“模拟通路阻抗”这两个核心问题。下次再让我做类似的通道扩展我还是会优先选CD74HC4067但一定会先把采样时间和切换延时算清楚这样的投入远比在调试阶段加班找Bug划算得多。
返回列表