Arduino引脚不够用?用MCP23017双芯片扩展24路数字I/O完整指南
做创客这些年凡是玩到一定程度的人迟早会碰上一个绕不开的问题Arduino的引脚不够用了。尤其当你从“点个灯”进化到“做一台智能小车”或者“搞一面灯墙”的时候UNO上那14个数字引脚掰着指头数都嫌少。我最近在整理一套24通道数字I/O接口就是专门解决这个痛点的。它可以接按键、接LED、接继电器、接编码器甚至接舵机控制信号而主控只占用两根I/O线I2C。这篇文章不聊虚的直接把这套接口从方案选型、硬件接线、寄存器配置到完整代码和踩坑记录全部捋一遍给想要扩展Arduino I/O能力的朋友一份能直接照抄的作业。这套东西适合谁如果你是做智能小车发现引脚不够分配或者想做一个几十路按键的输入面板又或者需要同时驱动一排LED指示灯而UNO资源吃紧那本文恰好就是为你准备的。即使你是刚接触Arduino不久的新手跟着走一遍也能把原理搞明白而且我会把每个“为什么”都讲清楚。1. 24通道数字I/O到底怎么选型三种主流方案各有各的脾气1.1 Arduino引脚为什么不够用典型场景回顾很多人第一次觉得引脚吃紧是在做智能小车的时候。电机驱动要占4个引脚PWM调速2个、方向2个超声波模块占2个舵机占1个蓝牙串口占2个再算上编码器反馈UNO那14个数字引脚基本就见底了。这时候如果你还想加几个指示灯、加个蜂鸣器就只能跟挤牙膏一样把功能挪来挪去甚至牺牲掉某些功能。另一个典型场景是灯光矩阵或者控制面板。比如做一个24路LED状态指示墙或者一个12按键操作台如果用原生引脚做UNO直接不够换Mega又觉得大材小用。更麻烦的是Arduino Uno本身只有6个引脚支持PWM输出你要是每个通道都想要可控亮度光靠原生引脚根本玩不转。所以“扩展数字I/O”这个需求本质上是把主控从“引脚资源焦虑”里解放出来让主控专注于逻辑处理把大量的开关量交给扩展芯片去管。扩展方案的关键指标就三个通道数量、方向灵活性、占用主控引脚数。1.2 方案一74HC595移位寄存器最便宜的“纯输出”扩展74HC595是很多入门教程都会提到的芯片。它本质是一个8位“串行输入、并行输出”的移位寄存器你用3根线数据、时钟、锁存就能扩展出8个输出通道芯片级联之后理论上可以无限扩展。但74HC595有个明显的限制它只能做输出不能读输入。你要接按键、接传感器信号它帮不上忙。而且它的输出本身不带锁存保护驱动感性负载继电器、电机时容易产生干扰需要额外加续流二极管和驱动电路。它在做LED灯带、数码管扫描这类纯粹输出场景里非常好用成本也极低一片几毛钱。但如果你要做的是一套“又能读按键又能控灯”的24通道接口它就不是最优解了。1.3 方案二CD74HC4067多路开关适合大量输入采集CD74HC4067是一片16通道模拟多路复用/解复用器用4根选择线S0-S3加1根公共端就能把16路信号“轮流”引到主控的同一个引脚上。它的思路和595正好相反适合用来快速轮询大量输入信号比如16路按键、16路模拟传感器。它的缺点是通道切换需要主控反复拉高拉低选择引脚轮询16路需要时间并且它同一时刻只能读取一路信号如果某些输入需要同时响应比如同时按下多个按键你得自己在软件里做轮询逻辑实时性会受点影响。另外它在数字I/O场景下也不是不能输出但做输出时相当于一个“可编程开关”没法保持多路同时输出的状态稳定。1.4 方案三MCP23017 I2C扩展双向、级联、中断最均衡MCP23017是一颗经典的双向16位I/O扩展芯片通过I2C接口和主控通信。它的最大优势在于16个I/O引脚每个都能独立配置为输入或输出方向可以随时改。每一个输入引脚都可以启用内部上拉电阻省掉外部上拉。占用主控引脚极少I2C总线就两根线SDA、SCL。支持8个不同I2C地址级联8片就是128路I/O。支持中断输出输入状态变化时不需要主控轮询引脚自动拉低通知主控来读。对于要组装一套“24通道数字I/O接口”的项目来说MCP23017几乎是为这个需求量身定做的。用两片MCP23017就能拼出32路双向I/O取其中24路使用哪怕后面需求增加还有余量。1.5 我的选型结论两块MCP23017拼24路市面上确实有一些直接用“1片MCP2301716路1片74HC5958路”凑24路的方案成本能压低不少但代码里要同时维护I2C和移位寄存器两种协议调试起来比较割裂。我的建议是直接用两片MCP23017理由有三个。第一代码风格统一。两片都是I2C设备驱动函数完全复用写起来清爽排查问题也简单。第二方向灵活。MCP23017每个引脚都能独立配置输入或输出595那8路是死板的输出实际做项目时自由度差别很大。第三中断统一。两片MCP23017的中断引脚都可以接到主控的同一个中断脚上主控一次中断响应就能同时查两片芯片的状态而595没有中断能力。下表是三套方案的直观对比方案通道数占用主控引脚输入/输出级联能力I2C/中断单路成本74HC595 x324输出3根仅输出可级联无中断约0.5元/路CD74HC4067 x232选15根共用42主要输入输出受限可级联无中断约1元/路MCP23017 x232双向取242根双向独立配置8片共128路支持中断约3-4元/路从长期做项目的角度看MCP23017贵出来的那点成本换来的是调试时间和功能灵活性非常值。2. MCP23017关键细节接线、地址、寄存器一次讲透2.1 引脚功能与接线要点MCP23017是28脚DIP或SOIC封装焊贴片时建议转接板。它的引脚可以分为几个功能区电源与地VDD11脚接3.3V或5VVSS9脚、10脚接地。I2C通信SCL12脚接主控的SCLSDA13脚接主控的SDA。地址选择A015脚、A116脚、A217脚三个引脚的电平组合决定设备地址。复位RESET18脚高电平有效平时接VDD如果要用外部复位就通过按钮接地触发。中断输出INTA19脚对应PortAINTB20脚对应PortB输入状态变化时会从高电平拉低。I/O引脚GPA0-GPA721-28脚是PortAGPB0-GPB71-8脚是PortB。接线时最容易犯的错是RESET悬空。有些模块上已经接了上拉电阻到VDD但如果你用裸芯片RESET不接导致芯片一直处于复位状态I2C怎么扫都扫不到。我习惯在RESET和VDD之间加一个10kΩ电阻让它默认高电平需要复位时再接一个按钮开关到地。另外I2C总线上一定要有上拉电阻。Arduino UNO的A4、A5引脚内部已经有上拉电阻但如果你用的主控是ESP32或者STM32板子内部不一定带合适的I2C上拉必须在SDA和SCL上分别接一个4.7kΩ电阻到VDD。这个问题在后面“扫描不到设备”那里还会重点讲。2.2 地址怎么配置A0/A1/A2决定设备IDMCP23017的设备地址由A0、A1、A2三个引脚的电平决定。三个引脚接低电平GND时地址是0x20接高电平VDD时按二进制位递增。排列规律如下A2A1A0I2C地址0000x200010x210100x220110x231000x241010x251100x261110x27如果用了两块MCP23017第一块三个地址引脚全接GND0x20第二块A0接VDD、A1/A2接GND0x21这就是最常见的配置。注意地址引脚不能悬空悬空会导致电平不确定设备地址随机漂移扫I2C总线时一会儿能扫到一会儿扫不到特别坑。如果你用的是带转接板的模块地址跳线一般已经默认拉到GND你需要根据模块丝印调整。我之前用过一款模块上写着A0/A1/A2旁边有焊盘默认是全部断开的内部下拉但有的模块默认是接地的所以拿到模块先看一眼电平别想当然。2.3 常用寄存器IODIR、GPPU、GPIO、IOCONMCP23017不复杂它就是一个寄存器集合。任何操作本质上就是往寄存器里写值或者读值。最常用的寄存器有以下几组IODIRA0x00、IODIRB0x01方向寄存器。每一位对应一个引脚1输入0输出。默认值是0xFF也就是上电后所有引脚都是输入模式。很多人把芯片接好后直接写GPIO发现输出无效就是因为忘了先把IODIR对应的位改成0。GPPUA0x0C、GPPUB0x0D上拉寄存器。对应位写1时该输入引脚启用约100kΩ的内部上拉电阻。如果按键一端接GND、另一端接引脚启用内部上拉后按键按下读到低电平松开读高电平省掉外部电阻。GPIOA0x12、GPIOB0x13数据寄存器。写这个寄存器就是输出电平读这个寄存器就是获取引脚电平。注意读GPIO时读到的不是内部锁存值而是引脚当前的真实电平状态所以输入悬空时读到的是不稳定的随机值。IOCON0x0A或0x0B配置寄存器。它控制bank模式、中断镜像、SMBus等特性。对大多数应用来说保持默认模式非bank即寄存器连续排布即可不过如果你用某些第三方库它可能默认把IOCON改掉了这时候用文档给出的寄存器地址可能对不上调试时要留意。还有一个简便写法MCP23017的I2C协议支持“寄存器连续写”如果你把IODIRA、IODIRB连续写入一次I2C事务就能设置两个方向寄存器效率更高。2.4 两块芯片搭24通道的接线与初始化两块MCP23017搭24通道接线其实很简单所有的SCL并在一起接主控SCL所有的SDA并在一起接主控SDA两块芯片的VDD、VSS都并接在一起。地址引脚按上面表格设置成不同值即可。这样从主控视角看就是两个独立的从设备。初始化时先对每一片芯片设置方向再设置输出初始值。参考示例代码如下#include Wire.h #define MCP1_ADDR 0x20 #define MCP2_ADDR 0x21 void mcpWriteReg(uint8_t addr, uint8_t reg, uint8_t value) { Wire.beginTransmission(addr); Wire.write(reg); Wire.write(value); Wire.endTransmission(); } uint8_t mcpReadReg(uint8_t addr, uint8_t reg) { Wire.beginTransmission(addr); Wire.write(reg); Wire.endTransmission(); Wire.requestFrom(addr, 1); return Wire.available() ? Wire.read() : 0; } void setup() { Wire.begin(); Serial.begin(115200); // 第一片PortA 全部输出PortB 全部输出 - 16路输出 mcpWriteReg(MCP1_ADDR, 0x00, 0x00); // IODIRA mcpWriteReg(MCP1_ADDR, 0x01, 0x00); // IODIRB // 第二片PortA 全部输入PortB 全部输入 - 8路输入24路里取8路 mcpWriteReg(MCP2_ADDR, 0x00, 0xFF); // IODIRA mcpWriteReg(MCP2_ADDR, 0x01, 0xFF); // IODIRB mcpWriteReg(MCP2_ADDR, 0x0C, 0xFF); // GPPUA启用输入上拉 mcpWriteReg(MCP2_ADDR, 0x0D, 0xFF); // GPPUB启用输入上拉 } void loop() { // 读第二片的PortA输入 uint8_t keys mcpReadReg(MCP2_ADDR, 0x12); Serial.println(keys, BIN); delay(100); }这套初始化代码就是整个24通道接口的地基。所有后续操作无非是在这个基础上把某一路写成输出高/低或者把某一路的状态读回来。3. 实战24路I/O模块完整实现按键输入LED输出3.1 项目功能设定与硬件清单光讲理论没意思我直接拿一个我自己做过的控制面板项目来演示。项目需求是这样的一个设备控制面板上面有12个按键按下某一键时对应的LED指示灯亮起同时通过串口把按键编号发送给上位机另外还有12路指示灯用来显示设备状态比如12个通道的开关状态由上位机通过串口命令控制。这个项目正好用满24路12路按键输入12路LED输出。当然我们前面已经说了两片MCP23017实际有32路剩下的8路作为备用方便日后增加功能。硬件清单如下Arduino UNO或者任何兼容板x1MCP23017模块 x2买成品模块比自己焊省心带排针那种12个轻触按键12个LED建议5mm彩灯加220Ω限流电阻面包板或洞洞板、杜邦线若干10kΩ电阻 x2I2C上拉如果模块上不带的话4.7kΩ电阻若干RESET上拉注意MCP23017每个I/O引脚输出高电平时的驱动能力有限数据手册上写的是最大25mA左右但实际使用中建议单路电流控制在10mA以内否则芯片发热明显且引脚电压会被拉低。LED直接串一个220Ω的限流电阻最稳妥红光LED压降约2V5V供电下电流约5-2/220≈13.6mA虽然比10mA略高一点短时间驱动没问题想要更保守就换330Ω电阻。3.2 接线细节与电源设计接线分成四块来看第一块是主控到第一片MCP23017。SCL接A5UNO或21ESP32SDA接A4UNO或20ESP32VDD接5V如果LED输出需要5V逻辑GND接GND地址引脚全接GND。第二块是主控到第二片MCP23017。SCL、SDA与第一片并联VDD、GND并接A0接VDDA1、A2接GND。第三块是输入部分。12个按键每一端接对应的输入引脚比如第二片MCP23017的GPA0-GPB3另一端全部接到GND。这里用的是“按键接GND”方式配合内部上拉电阻按下时引脚读低电平。如果你用“按键接VDD”那就不需要上拉但要小心外部干扰把引脚电压拉偏。第四块是输出部分。每个LED正极通过限流电阻接到输出引脚比如第一片MCP23017的GPA0-GPB7LED负极接GND。输出引脚为高电平时LED点亮。电源设计上有一个很容易被忽略的问题如果第一片和第二片的VDD都从Arduino的5V引脚取电而面板上12个LED同时点亮每路按10mA算就是120mA再加上MCP23017自身功耗和I2C上拉电阻的电流Arduino板载稳压器还能扛住UNO的稳压输出能力在500mA左右但电压会被拉低LED亮度会下降。如果通道数再往上加务必要用外部5V电源给扩展板单独供电并且把GND和Arduino连在一起形成一个共地系统。3.3 完整代码与逐段解析代码的核心就是“按键扫描 LED控制”我把完整代码贴出来然后逐段解释。这一段可以直接保存为.ino文件烧录。#include Wire.h #define MCP1_ADDR 0x20 #define MCP2_ADDR 0x21 #define IODIRA 0x00 #define IODIRB 0x01 #define GPPUA 0x0C #define GPPUB 0x0D #define GPIOA 0x12 #define GPIOB 0x13 uint8_t ledState 0x00; // 记录第一片当前输出状态低8位 uint8_t ledState2 0x00; // 记录第一片当前输出状态高8位 void mcpWriteReg(uint8_t addr, uint8_t reg, uint8_t value) { Wire.beginTransmission(addr); Wire.write(reg); Wire.write(value); Wire.endTransmission(); } uint8_t mcpReadReg(uint8_t addr, uint8_t reg) { Wire.beginTransmission(addr); Wire.write(reg); Wire.endTransmission(); Wire.requestFrom(addr, 1); return Wire.available() ? Wire.read() : 0; } void setup() { Wire.begin(); Serial.begin(115200); // 第一片MCP23017PortA和PortB全部配置为输出 mcpWriteReg(MCP1_ADDR, IODIRA, 0x00); mcpWriteReg(MCP1_ADDR, IODIRB, 0x00); mcpWriteReg(MCP1_ADDR, GPIOA, 0x00); mcpWriteReg(MCP1_ADDR, GPIOB, 0x00); // 第二片MCP23017PortA和PortB全部配置为输入并启用上拉 mcpWriteReg(MCP2_ADDR, IODIRA, 0xFF); mcpWriteReg(MCP2_ADDR, IODIRB, 0xFF); mcpWriteReg(MCP2_ADDR, GPPUA, 0xFF); mcpWriteReg(MCP2_ADDR, GPPUB, 0xFF); Serial.println(24-Channel IO Panel Ready.); } void loop() { // 1. 扫描第二片的16个按键输入 uint8_t keyA mcpReadReg(MCP2_ADDR, GPIOA); uint8_t keyB mcpReadReg(MCP2_ADDR, GPIOB); // 把两个字节拼成一个16位整数按键0-11对应bit0-11 uint16_t keys ((uint16_t)keyB 8) | keyA; // 2. 检查哪些按键是新按下的用lastKeys做边沿检测 static uint16_t lastKeys 0xFFFF; // 上拉输入默认全高按下为0 uint16_t pressed (~keys) (~lastKeys); // 新按下 当前为0且之前为1 // 3. 如果有按键按下点亮对应LED并发送串口指令 for (int i 0; i 12; i) { if (pressed (0x0001 i)) { setLED(i, 1); Serial.print(Button pressed: ); Serial.println(i); } } lastKeys keys; // 4. 处理上位机下发的串口命令格式 ON n 或 OFF n while (Serial.available() 0) { String cmd Serial.readStringUntil(\n); cmd.trim(); if (cmd.startsWith(ON )) { int idx cmd.substring(3).toInt(); if (idx 0 idx 12) { setLED(idx, 1); } } else if (cmd.startsWith(OFF )) { int idx cmd.substring(4).toInt(); if (idx 0 idx 12) { setLED(idx, 0); } } } } void setLED(int index, int onOff) { // 第一片MCP23017的GPA0-GPB7对应索引0-15 if (index 0 || index 11) return; // 本项目只用12路LED if (index 8) { // PortA if (onOff) { ledState | (0x01 index); } else { ledState ~(0x01 index); } mcpWriteReg(MCP1_ADDR, GPIOA, ledState); } else { // PortB if (onOff) { ledState2 | (0x01 (index - 8)); } else { ledState2 ~(0x01 (index - 8)); } mcpWriteReg(MCP1_ADDR, GPIOB, ledState2); } }这段代码里有几个细节值得展开说。第一按键边沿检测用了一个lastKeys变量。因为MCP23017输入引脚默认接内部上拉按键按下时读到0松开时读到1。直接用if (keys 0)判断会有问题你按住按键不松手loop每跑一遍都算一次“按下”串口会反复发数据。用边沿检测后只有“从没按下变到按下”的那个瞬间才触发一次按住不放不会重复触发。这是实际项目中非常实用的防抖外的另一层保障。第二串口处理用了readStringUntil和trim()。上位机发送“ON 3\n”这样的指令时末尾会带换行符如果不trimtoInt()会被换行符干扰导致解析失败。这是个极其常见的坑很多人上位机调了半天发现LED不动最后发现是串口解析的换行符问题。第三setLED函数里返回的不是简单的写寄存器而是先修改本地的ledState和ledState2变量。因为MCP23017的GPIO寄存器是可读可写的但如果你读回来再改某一位再写进去多一次I2C读操作而且读回来的可能是引脚实时电平如果外部电路把引脚拉低了会误判。所以我在内存里维护一份输出状态的镜像每次只对镜像做位操作再整体写回这样整个输出状态是可控的不会因为外部干扰导致部分LED状态错乱。3.4 验证与测试烧录完成后第一步先打开串口监视器看看是否打印出“24-Channel IO Panel Ready.”。如果没有说明程序没跑起来优先检查I2C接线。第二步分别按下12个按键观察串口是否按顺序打印“Button pressed: 0”到“Button pressed: 11”。如果按键编号和实际按下的按键对不上说明接线时GPA0-GPB3的顺序和你预期不一致调整逻辑映射就行不一定是硬件问题。第三步在串口监视器输入“ON 0”再输入“OFF 0”观察第0路LED是否正常亮灭。如果LED不亮但代码逻辑没问题用万用表量一下对应引脚的对地电压万用表显示高电平但LED不亮那大概率是LED接反了或者限流电阻太大了。4. 使用中常见的坑和排查技巧4.1 扫描不到I2C设备第一件事别怀疑芯片我做这个项目时第一次上电最典型的问题就是I2C扫描不到设备。如果你也遇到这种情况别急着换芯片按下面顺序排查先确认I2C地址。很多模块的A0/A1/A2默认不是全低或者有跳线帽没装上你实际扫描出来的地址可能不是0x20而是0x21、0x22等等。所以第一步永远是用I2C扫描程序把所有设备地址打出来常见的扫描代码网上很多几行就行。再看SCL和SDA接没接反。MCP23017的12脚是SCL、13脚是SDA有模块丝印标反的也有杜邦线颜色误导的。我踩过最离谱的坑是把模块的SCL接到了Arduino的SDA口上还反复检查了好几分钟。再查I2C上拉电阻。Arduino UNO的自带上拉可能偏弱尤其当你把I2C线拉得很长或者接了多从设备时。建议在SDA和SCL上各接一个4.7kΩ上拉到VDD这能解决绝大多数不稳定问题。最后查RESET引脚。前面说了RESET必须接高电平悬空会导致芯片处于复位状态。模块上一般有上拉裸芯片一定要自己处理。4.2 引脚不听话方向和上拉寄存器没配对很多人的代码逻辑没毛病但实际效果不对多半是方向寄存器的问题。MCP23017上电后所有引脚默认是输入模式你如果直接把0x12寄存器写值引脚并不会输出因为方向还没改。反过来如果你把一个输入引脚配置成了输出读它的时候读到的也是内部锁存值不是外部真实电平。我的习惯是初始化时先把所有用到的IODIR寄存器写清楚再写输出引脚初值。这样即使芯片在初始化瞬间被打断最终状态也是确定的。另外如果你用某一路做输入且外部没有上拉/下拉一定要启用GPPU寄存器否则引脚悬空时读到的值一会高一会低按键逻辑会彻底乱掉。这点和电脑上装驱动很像设备连上了但驱动不对功能就发挥不出来。MCP23017的IODIR就是它的“驱动方向”设错了引脚就“失灵”。4.3 中断线怎么接才不丢数据MCP23017支持中断但对新手来说直接轮询读取其实更稳。如果你要用中断功能有一个容易被忽略的细节INTA和INTB默认是“引脚状态变化就触发”但MCP23017中断在MCU读取INTCAP寄存器之前不会自动清除。如果你的中断服务程序里面只读GPIO寄存器就会导致中断标志一直未清除中断持续触发程序卡死。正确的处理方式是在中断服务程序里先读INTCAP寄存器再读GPIO寄存器或者直接读GPIO寄存器也能清除中断标志——但要先读INTCAP。这块细节在数据手册第1.7节有明确说明我第一次用的时候没细看中断不断触发串口输出刷屏排查了半天。另外如果你把两片MCP23017的INT引脚直接并联接到Arduino的同一个中断脚上因为MCP23017的INT输出是开漏结构导致一片芯片触发中断时另一片的中断状态也会被拉低你无法从电平判断到底是哪一片触发的。解决办法是中断服务程序里同时读两片芯片的INTFA/INTFB寄存器或者干脆给每片芯片单独接一个中断引脚。我自己的项目里是直接轮询的因为24路信号对实时性要求没那么高轮询10ms一次完全够用还能少两根接线。4.4 兼容板适配ESP32、STM32、CH32V003文章标题里特别写了“for Arduino Compatibles”所以兼容板的适配问题必须讲一讲。MCP23017本身是标准的I2C设备理论上任何支持I2C的总线都能通信但不同开发板有各自的坑。ESP32是最常见的替代主控。它的问题在于3.3V逻辑电平下MCP23017的I2C高电平阈值也能兼容芯片工作电压范围是1.8V-5.5V但如果你沿用5V给MCP23017供电I2C引脚的高电平阈值就要注意ESP32的3.3V高电平可能无法被识别为高电平。最稳妥的做法是MCP23017的VDD也接3.3V保证逻辑电平一致。另外ESP32的默认I2C引脚和UNO不一样SDA默认是21SCL默认是22别照抄UNO的A4/A5代码。STM32的I2C时序更严格早期ST的HAL库有I2C BUSY位卡死的bug如果你用STM32做主控建议把I2C时钟速度降到100kHz标准模式不要用400kHz快速模式。MCP23017支持400kHz但STM32的I2C外设实现偶尔会有边界问题降低速度最省心。CH32V003这种低价MCU目前在创客圈热度很高它的I2C也是硬件外设但要注意它没有真正的开漏引脚配置需要外部上拉才能可靠通信。这类小芯片跑24通道数字I/O完全没问题代码逻辑和Arduino一致只要把Wire库换成对应的HAL实现即可。如果你用的是Arduino IDE配合CH32V003的Arduino核心大部分代码可以直接移植。4.5 电脑端和软件侧的小毛病IDE、驱动与仿真最后聊几个虽然和MCP23017没有直接关系但新手在搭建开发环境时经常被卡住的“周边问题”。Arduino IDE安装ESP32板卡时如果你总是下载失败或者装完发现C盘空间被占掉好几个G那是因为Arduino IDE默认把板卡包下载到了用户目录。解决办法有两个一是手动下载离线安装包解压到用户目录/AppData/Local/Arduino15/packages二是修改IDE的板卡管理地址只保留国内镜像源速度会快很多。我一般直接下载离线包解压即用省去反复试错的麻烦。电脑偶尔识别不到Arduino板子尤其是UNO的CH340串口芯片在Linux系统下容易被一个叫brltty的服务抢走设备。现象是插上板子后系统日志里报类似“usbfs: interface 0 claimed by ch341 while brltty sets config #1”的错误串口列表里看不到设备。解决办法是把brltty服务停掉或者卸载掉这个盲文终端驱动然后重新插拔板子。Windows下则大概率是CH340驱动没装好需要手动指定驱动目录安装。这个问题在Windows和Linux双系统用户里尤其常见我前后踩过两次坑才反应过来。软件层面如果你手头还没有硬件又想先验证代码逻辑可以用Wokwi这类在线仿真平台在浏览器里搭一个Arduino MCP23017的虚拟电路直接跑本文的示例代码。它能模拟I2C通信按键和LED都能交互对前期验证非常有帮助。我通常先把逻辑在仿真里跑通再下单买硬件能省不少冤枉钱。写在最后这24路接口还能怎么玩第一次把24通道数字I/O接口完整跑通之后我自己最大的感受是扩展I/O这件事看似是硬件堆料实际上更大的价值在于把主控从“引脚焦虑”里解放出来让Arduino专心干它擅长的事。现在再回头看智能小车项目原先为了省引脚做的各种别扭设计全都可以推翻重来想加传感器就加传感器想加指示灯就加指示灯程序结构反而比之前更清爽。最后分享一个小经验MCP23017做输出时单片同时点亮LED的电流总和是有极限的不是每路25mA就能同时开16路25mA芯片的总功耗会超标。如果你要驱动大电流负载比如继电器、蜂鸣器、电磁锁千万别直接用MCP23017引脚直驱在外面加一个ULN2003或者三极管驱动级保护芯片的同时也提升整体可靠性。这算是用这颗芯片时最值得先补的一课。
