1. 项目概述当你在鼓捣一个基于微控制器的项目时比如一个智能花盆、一个桌面小摆件或者一个复杂的机器人最常遇到的瓶颈之一就是GPIO通用输入输出引脚不够用。主控芯片的引脚数量是固定的但你的创意和需求往往是无限的——想多接几个传感器、多控制几个LED灯带、再加几个按钮引脚很快就捉襟见肘了。这时候I/O扩展芯片就成了你的“救星”。它们就像给你的微控制器增加了一个或多个“外挂”的GPIO端口让你能连接更多的外部设备。在众多I/O扩展方案中Microchip的MCP230088位和MCP2301716位因其简单、可靠和广泛的支持度成为了社区和产品中的常客。它们通过I2C总线与主控通信这是一种只需要两根线数据线SDA和时钟线SCL就能连接多个设备的协议极大地简化了硬件布线。而CircuitPython作为一款对初学者极其友好的嵌入式Python实现让操控这些硬件变得像写普通Python脚本一样直观。本文的核心就是带你从零开始在CircuitPython环境下把MCP23008或MCP23017这颗“外挂芯片”用起来。我会详细拆解从硬件连接到软件配置的每一个步骤解释背后的原理并分享我在实际项目中积累的调试技巧和避坑经验。无论你是刚接触硬件的编程新手还是想寻找一个稳定I/O扩展方案的老手这篇文章都能给你提供一份可直接“抄作业”的实战指南。2. 芯片选型与硬件设计解析2.1 MCP23008 vs MCP23017如何选择面对MCP23008和MCP23017第一个问题就是选哪个。这不仅仅是8个引脚和16个引脚的简单区别选择背后需要考虑项目规模、成本、布线复杂度和功耗。MCP23008提供8个可配置的GPIO引脚。它的优势在于封装更小常见的为18引脚PDIP或SOIC价格通常略低对于只需要少量额外引脚的项目来说它是最经济、最紧凑的选择。例如你的项目只需要多接一个4x4矩阵键盘需要8个引脚或者几个状态指示灯和按钮MCP23008就足够了。它的I2C地址配置也相对简单。MCP23017则提供了16个GPIO引脚分为两个端口GPIOA和GPIOB各8位。除了数量翻倍它还有一些增强功能比如可配置的中断输出引脚INTA和INTB这在需要实时响应输入状态变化如按键按下的应用中非常有用可以避免主控不断轮询polling引脚状态节省CPU资源。如果你需要驱动一个两位的7段数码管约需16个引脚、连接多个传感器和执行器或者希望用中断来提高效率MCP23017是更合适的选择。注意虽然MCP23017功能更强但其封装通常是28引脚占用PCB面积更大布线也稍复杂。在空间受限的项目中需要权衡利弊。2.2 I2C总线原理与硬件连接要点要让芯片工作首先得正确连接。I2C总线看似简单两根线但细节决定成败。1. 电源与地线Vdd Vss这是最基本也是最容易出错的地方。必须确保扩展芯片和主控板如树莓派Pico、ESP32使用共地GND连接在一起。电源电压也需要匹配。MCP230xx系列通常支持2.7V-5.5V的宽电压但为了稳定最好让它的Vdd和主控板的I/O电压一致。如果主控是3.3V系统就给芯片接3.3V如果是5V系统就接5V。混用可能导致通信失败或损坏芯片。2. I2C信号线SDA SCL与上拉电阻这是整个连接的核心也是新手最容易忽略的部分。I2C总线是“开源漏极”结构这意味着芯片内部的驱动电路只能将总线拉低到GND而不能主动拉高到Vcc。因此总线必须依靠外部电阻拉到高电平这个电阻就是上拉电阻。为什么必须加上拉电阻没有上拉电阻SDA和SCL线在空闲时处于“悬空”状态电平不确定极易受到干扰导致I2C通信完全无法启动或时好时坏。电阻值怎么选常用值是4.7kΩ或10kΩ。阻值太小电流过大浪费电能且可能超过芯片驱动能力阻值太大上升沿太慢在高速通信时可能导致时序错误。对于大多数嵌入式应用标准模式100kHz或快速模式400kHz4.7kΩ是一个兼顾速度和可靠性的通用值。怎么接在SDA和SCL两条线上分别接一个4.7kΩ电阻到正电源3.3V或5V。注意整个I2C总线上只需要一组上拉电阻无论你接了一个还是多个从设备。如果你在开发板上如某些Arduino、树莓派已经找到了标有I2C的引脚并且板上可能已经集成了上拉电阻那么外接时可以省略。但为了保险起见尤其是在面包板搭建的原型上自己加上总是没错的。3. 地址配置引脚A0, A1, A2I2C总线允许多个设备靠地址区分。MCP23008/17的这三个引脚决定了它的7位I2C设备地址。通过将它们连接到GND逻辑0或Vdd逻辑1可以设置不同的地址。默认情况下全部接地MCP23008的地址是0x20MCP23017也是0x20实际上MCP23017的基地址是0x20A0/A1/A2的组合决定最终地址。这意味着如果你总线上只有一个该型号芯片最简单的方法就是把A0, A1, A2全部接地。如果你需要连接多个同型号芯片就必须为它们设置不同的地址例如将第二个芯片的A0接Vdd地址就变成了0x21。4. 复位引脚RESET这是一个低电平有效的复位引脚。通常直接连接到Vdd高电平使其保持工作状态。你也可以将它连接到一个GPIO上通过程序在需要时进行硬件复位这在调试复杂状态时很有用。2.3 典型连接图与实物搭建建议根据上述要点一个连接Feather M0或其他3.3V CircuitPython板与MCP23017的完整接线如下电源Feather 3.3V - MCP23017 Vdd (引脚9)地线Feather GND - MCP23017 Vss (引脚10)I2C总线Feather SCL - MCP23017 SCL (引脚12)Feather SDA - MCP23017 SDA (引脚13)在SCL和SDA线上分别接一个4.7kΩ电阻到3.3V。地址配置MCP23017 A0 (引脚15), A1 (引脚16), A2 (引脚17) 全部连接到GND设置地址为0x20。复位MCP23017 RESET (引脚18) 连接到3.3V。对于MCP23008引脚定义不同但逻辑一致请务必参照其数据手册的引脚图。实操心得在面包板上搭建时强烈建议使用不同颜色的杜邦线区分电源红色、地线黑色、信号线黄色/绿色。这能极大减少接错线的概率。连接完成后务必用万用表通断档或电压档快速检查一下Vdd对GND是否为3.3VSDA/SCL对GND在空闲时是否也被拉到了接近3.3V证明上拉电阻生效3. CircuitPython环境配置与库安装硬件连接妥当后下一步就是让软件“认识”这块新芯片。CircuitPython的魅力在于你可以像安装Python库一样轻松添加硬件驱动。3.1 确保CircuitPython固件为最新版本首先访问 CircuitPython官方网站 找到你的开发板型号下载最新的.uf2固件文件。将开发板通过USB连接到电脑它通常会显示为一个名为CIRCUITPY的可移动磁盘。将下载的.uf2文件拖入这个磁盘板子会自动重启并更新固件。保持固件最新能确保最好的兼容性和最多的功能支持。3.2 安装Adafruit CircuitPython库包Adafruit为几乎所有他们销售或支持的传感器、执行器编写了高质量的CircuitPython库并打包成一个压缩文件。你需要从中提取我们所需的库。访问 Adafruit CircuitPython Library Bundle 发布页面 。下载对应你CircuitPython版本的压缩包通常选择最新的adafruit-circuitpython-bundle-py-*.zip。解压这个ZIP文件你会看到里面有很多文件夹和.mpy文件。打开你的CIRCUITPY磁盘找到或创建一个名为lib的文件夹。从解压的库包中找到以下两个文件/文件夹并将其复制到CIRCUITPY磁盘的lib文件夹内adafruit_mcp230xx.mpyadafruit_bus_device(这是一个文件夹)注意对于某些存储空间极小非Express系列的开发板如果lib文件夹放不下整个adafruit_bus_device文件夹你可能需要只复制其中必要的.mpy文件。但通常对于M0/M4系列直接复制整个文件夹是最稳妥的。3.3 验证安装与连接REPL库文件复制完成后断开并重新连接USB线或者按一下板子的复位键。接下来我们需要通过“串行REPL”来与板子交互并测试。使用串口终端工具如Mu编辑器、Thonny或者命令行下的screen(Mac/Linux) 、putty(Windows)。选择正确的串口设备在设备管理器中查看COM端口号。连接后按一下键盘上的CtrlC这会中断任何正在运行的程序。然后按CtrlD软复位你将看到CircuitPython的欢迎信息并出现提示符。这说明你已经进入了交互式Python环境可以逐行执行代码了。4. 软件驱动与核心API详解环境准备好后我们来深入看看如何用代码操控这颗芯片。Adafruit的库设计得非常巧妙它让MCP230xx的引脚对象看起来和CircuitPython原生的digitalio.DigitalInOut对象几乎一模一样大大降低了学习成本。4.1 初始化I2C总线与芯片对象一切操作始于初始化I2C总线和创建芯片对象。import board import busio from adafruit_mcp230xx.mcp23017 import MCP23017 # 如果是MCP23017 # 或者 from adafruit_mcp230xx.mcp23008 import MCP23008 # 如果是MCP23008 # 初始化I2C总线使用板子默认的SCL和SDA引脚 i2c busio.I2C(board.SCL, board.SDA) # 创建MCP23017实例使用默认地址0x20 mcp MCP23017(i2c) # 如果地址引脚有配置例如A0接高电平则地址变为0x21 # mcp MCP23017(i2c, address0x21)代码解析busio.I2C(board.SCL, board.SDA)这行代码初始化了硬件I2C控制器并指定使用开发板上标为SCL和SDA的引脚。对于大多数板子这是固定的。MCP23017(i2c)创建了一个代表整个芯片的对象。库会通过I2C总线自动与芯片通信完成初始化。address参数是可选的只有当你的地址配置引脚A0,A1,A2没有全部接地时才需要指定。4.2 引脚对象化与方向控制芯片对象mcp本身不直接操作引脚你需要通过get_pin(pin_number)方法来获取单个引脚的代理对象。# 获取芯片上的第0号引脚对于MCP23017是GPIOA0 led_pin mcp.get_pin(0) # 获取芯片上的第8号引脚对于MCP23017是GPIOB0 button_pin mcp.get_pin(8)获取到的led_pin和button_pin对象其类型和行为与原生digitalio.DigitalInOut高度一致。设置引脚方向是操作的第一步import digitalio # 将0号引脚设置为输出模式 led_pin.direction digitalio.Direction.OUTPUT # 将8号引脚设置为输入模式 button_pin.direction digitalio.Direction.INPUT方向Direction这个属性决定了电流的“流向”。OUTPUT意味着微控制器通过芯片主动控制该引脚输出高电平Vdd或低电平GND用来驱动LED、继电器等。INPUT意味着微控制器通过芯片读取该引脚从外部感受到的电压水平用于读取按钮、开关等传感器的状态。4.3 输出控制与输入读取设置好方向后就可以进行具体的读写操作了。输出控制# 将输出引脚设置为高电平3.3V或5V led_pin.value True # 或者 led_pin.value 1 # 将输出引脚设置为低电平0VGND led_pin.value False # 或者 led_pin.value 0通过交替设置True和False并加上time.sleep()延时就能轻松实现LED闪烁等效果。输入读取# 读取输入引脚的当前状态 current_state button_pin.value if current_state: print(引脚为高电平) else: print(引脚为低电平)value属性在输入模式下是只读的它反映了引脚当前的逻辑电平。4.4 内部上拉电阻配置机械按钮、开关等输入设备在未按下时引脚是“悬空”的电平不确定读取的值会随机跳动。为了解决这个问题我们需要一个“上拉电阻”将引脚在空闲时稳定地拉到高电平。当按钮按下引脚接地电平被拉低从而产生一个清晰的下降沿信号。幸运的是MCP230xx芯片内部集成了可软件控制的上拉电阻这省去了外接物理电阻的麻烦。# 在将引脚设置为输入后启用内部上拉电阻 button_pin.pull digitalio.Pull.UP启用上拉后当按钮未按下button_pin.value读取为True按下按钮连接到GND后读取为False。重要限制MCP230xx芯片只支持内部上拉电阻不支持内部下拉电阻。如果你需要下拉电阻让引脚空闲时为低电平触发时变高则必须在外部连接一个物理电阻通常10kΩ从引脚到GND。5. 完整项目实战智能按钮控制LED理论讲得再多不如动手做一个项目。我们来构建一个经典电路用一个连接到MCP23017输入引脚的按钮来控制另一个连接到MCP23017输出引脚的LED。同时我们还会通过REPL打印状态并加入简单的防抖处理。5.1 硬件连接清单主控任意CircuitPython开发板如RP2040, ESP32-S3等I/O扩展MCP23017芯片输入部分1个轻触开关按钮1个10kΩ外部上拉电阻可选我们使用内部上拉。输出部分1个LED1个220Ω限流电阻。其他4.7kΩ I2C上拉电阻x2面包板杜邦线若干。连接示意图按章节2.3连接主控、MCP23017和I2C上拉电阻。按钮连接将按钮的一端连接到MCP23017的GPIOB0对应get_pin(8)另一端连接到GND。LED连接将LED的阳极长脚通过一个220Ω电阻连接到MCP23017的GPIOA0对应get_pin(0)。将LED的阴极短脚连接到GND。提示220Ω电阻用于限制流过LED的电流防止其烧毁。阻值可根据LED规格和电源电压调整通常在220Ω-1kΩ之间。5.2 软件代码实现将以下代码保存到你的CIRCUITPY磁盘的根目录并命名为code.py。CircuitPython会自动运行这个文件。# SPDX-FileCopyrightText: 2024 Your Name # SPDX-License-Identifier: MIT MCP23017 按钮控制LED示例 按下按钮LED亮起松开按钮LED熄灭。 包含软件防抖功能。 import time import board import busio import digitalio from adafruit_mcp230xx.mcp23017 import MCP23017 # 初始化I2C总线 i2c busio.I2C(board.SCL, board.SDA) # 初始化MCP23017地址默认为0x20 mcp MCP23017(i2c) # 定义引脚 # GPIOA0 作为输出控制LED led_pin mcp.get_pin(0) # GPIOB0 作为输入连接按钮 button_pin mcp.get_pin(8) # 配置LED引脚为输出初始状态为低熄灭 led_pin.switch_to_output(valueFalse) # 另一种等价写法 # led_pin.direction digitalio.Direction.OUTPUT # led_pin.value False # 配置按钮引脚为输入并启用内部上拉电阻 button_pin.direction digitalio.Direction.INPUT button_pin.pull digitalio.Pull.UP # 防抖相关变量 debounce_delay 0.05 # 50毫秒防抖时间 last_button_state button_pin.value # 上一次读取的按钮状态 last_debounce_time 0 # 上次状态变化的时间 current_button_state last_button_state # 当前稳定的按钮状态 print(MCP23017 按钮控制LED程序已启动) print(按下按钮LED亮松开按钮LED灭。) while True: # 读取按钮的原始状态 reading button_pin.value current_time time.monotonic() # 防抖逻辑如果读取到的状态与上次稳定状态不同则记录时间点 if reading ! last_button_state: last_debounce_time current_time # 如果距离上次状态变化已经过了防抖时间 if (current_time - last_debounce_time) debounce_delay: # 并且当前读取的状态与当前稳定状态不同 if reading ! current_button_state: current_button_state reading # 更新稳定状态 # 根据稳定状态控制LED if current_button_state False: # 按钮被按下上拉模式下为低电平 led_pin.value True print(按钮按下LED亮) else: # 按钮被释放 led_pin.value False print(按钮释放LED灭) # 更新上一次读取的原始状态用于下一轮比较 last_button_state reading # 短暂延时降低CPU占用率 time.sleep(0.01)5.3 代码逐行解析与防抖原理初始化与引脚配置前半部分与之前示例一致创建了led_pin和button_pin对象并正确设置了方向和上拉。防抖Debounce这是处理机械开关的关键。按钮在按下或弹起的瞬间金属触点会发生物理抖动导致在几毫秒内电平快速变化多次。如果不处理程序会误判为多次按下。debounce_delay 0.05设置一个50ms的防抖时间认为短于此时间的变化是抖动忽略。last_button_state记录上一轮循环读取到的原始电平。last_debounce_time记录上次电平发生变化的时间点。current_button_state经过防抖处理后认定的“稳定”状态。防抖逻辑循环每次循环读取按钮原始电平reading。如果reading与last_button_state不同说明电平变化了可能是抖动也可能是真按立即记录下当前时间last_debounce_time。然后判断如果从上次变化时间点到现在已经过去了超过debounce_delay50ms并且当前读取的reading与我们认为的稳定状态current_button_state不同那么我们就认为这是一个“有效的”、“稳定的”状态变化。此时才更新current_button_state并执行相应的动作开/关LED。最后更新last_button_state为本次的reading用于下一次比较。控制与输出根据稳定的按钮状态current_button_state来控制LED的亮灭并在串口打印信息。运行这个程序按下按钮LED应该会稳定地亮起并在串口监视器中看到“按钮按下LED亮”的信息没有误触发。这就是一个完整的、具有工业实用性的I/O扩展应用。6. 高级应用与性能优化掌握了基础操作后我们可以探索一些更高级的用法和优化技巧让你的项目更可靠、更高效。6.1 使用MCP23017的中断功能轮询不断读取引脚状态是一种简单但低效的方式会占用CPU时间。MCP23017支持硬件中断当输入引脚状态变化时它会通过专用的INTA或INTB引脚通知主控主控只在收到中断时才去处理大大提高了效率。硬件连接需要将MCP23017的INTA或INTB引脚连接到主控板的一个支持外部中断的输入引脚上。软件配置概念性代码Adafruit库对中断的支持可能需要直接操作寄存器 库的当前版本可能未直接封装高级中断API。通常需要直接通过I2C读写芯片的配置寄存器来启用中断。主要步骤包括配置GPINTEN寄存器启用特定引脚的中断。配置DEFVAL和INTCON寄存器设置中断触发条件与默认值不同时触发或引脚变化时触发。配置IOCON寄存器设置中断输出引脚的模式开漏、有效电平等。在主控端将连接INTA的GPIO配置为输入并设置中断服务程序IRQ。当中断触发时主控读取INTF中断标志和INTCAP中断捕获寄存器来确定是哪个引脚引起的中断并清除标志。由于涉及底层寄存器操作代码较为复杂。一个更通用的替代方案是如果你使用的MCU如ESP32有足够的GPIO可以将MCP23017的多个输入引脚通过逻辑门合并后接到一个中断引脚或者在主控端使用“引脚变化中断”功能更强大的芯片。6.2 批量操作与性能提升当你需要同时设置或读取多个引脚时逐位操作效率较低。MCP23017允许你一次性读写整个8位端口GPIOA或GPIOB。# 假设mcp是MCP23017对象 # 一次性设置GPIOA端口所有8个引脚的值 # 例如设置二进制 0b01010101 (0x55)即A0,A2,A4,A6为高A1,A3,A5,A7为低 mcp.gpio 0x55 # 注意这是设置所有16个引脚A和B端口的值具体取决于库实现 # 更常见的通过端口对象操作 # 设置整个GPIOA端口为输出并赋初值 for i in range(8): mcp.get_pin(i).direction digitalio.Direction.OUTPUT # 然后可以通过直接写寄存器来快速设置整个端口但Adafruit库可能未直接暴露此方法。实际上Adafruit的adafruit_mcp230xx库为了保持与DigitalInOutAPI的一致性主要面向单引脚操作。对于需要极致速度的批量操作你可能需要直接使用busio.I2C的write_byte_data等方法来操作芯片的GPIOA、GPIOB、OLATA、OLATB等寄存器。这需要对芯片数据手册有深入了解。6.3 驱动能力与外部电路设计MCP230xx每个引脚的输出驱动能力是有限的典型值25mA。这不足以直接驱动大功率器件如电机、继电器线圈或大功率LED。驱动LED必须串联限流电阻如220Ω-1kΩ。驱动继电器或电机必须使用三极管如NPN型2N2222或MOSFET或者专用的电机驱动芯片如L293D、TB6612作为开关。MCP230xx的引脚仅用于控制这些开关元件的基极或栅极。输入保护如果输入引脚可能接触到外部高压或静电应考虑串联一个1kΩ电阻并并联一个TVS二极管或稳压二极管到地进行限流和钳位保护。7. 故障排查与常见问题实录即使按照指南操作也难免会遇到问题。这里汇总了我调试MCP230xx时踩过的坑和解决方法。7.1 I2C通信失败这是最常见的问题表现为代码初始化时卡在busio.I2C()或MCP23017(i2c)或者提示找不到设备。问题现象可能原因排查步骤与解决方案初始化卡住或报错1. I2C上拉电阻缺失或阻值不对。用万用表测量SDA/SCL线对地电压空闲时应为电源电压如3.3V。若无电压或电压很低检查并补上4.7kΩ上拉电阻。2. 电源问题。测量MCP230xx的Vdd引脚电压是否为稳定的3.3V/5V与主控共地了吗3. 接线错误或接触不良。仔细对照引脚图检查SDA、SCL是否接反用万用表通断档检查每条线是否连通。面包板接触不良很常见试着压紧芯片和导线。4. I2C地址错误。确认A0,A1,A2的接法。如果全部接地地址是0x20。如果接了Vdd地址需要相应改变。可以用I2C扫描工具检查。5. 芯片损坏。更换一个芯片试试。静电或电源反接可能导致损坏。能初始化但读写不稳定1. 总线干扰或过长。I2C总线不宜过长一般50cm。确保SDA/SCL线远离电源等噪声源。可以尝试降低I2C速度在busio.I2C()中指定frequency100000。2. 多个主设备冲突。确保总线上只有一个I2C主设备你的主控板。使用I2C扫描工具在REPL中运行以下代码可以列出总线上所有发现的设备地址这是极其有用的诊断工具。import board import busio i2c busio.I2C(board.SCL, board.SDA) while not i2c.try_lock(): pass try: print(I2C addresses found:, [hex(addr) for addr in i2c.scan()]) finally: i2c.unlock()如果扫描结果中包含0x20或其他你设置的地址说明通信链路基本正常。7.2 引脚操作无反应通信正常但设置输出或读取输入没效果。问题现象可能原因排查步骤与解决方案输出引脚无电压变化1. 引脚方向未设置为OUTPUT。检查代码确认执行了pin.direction digitalio.Direction.OUTPUT。2. 外部电路负载过重。测量输出引脚在设置valueTrue时的电压。如果远低于Vdd可能是驱动的LED等器件电流过大超过芯片驱动能力。增加限流电阻或改用三极管驱动。3. 引脚冲突。确保没有其他电路将该引脚短路到地或电源。输入引脚读取值不变1. 引脚方向未设置为INPUT。确认代码中为digitalio.Direction.INPUT。2. 上拉电阻未启用对于开关输入。对于按钮等需要上拉的场景必须设置pin.pull digitalio.Pull.UP。3. 外部信号电平不匹配。输入信号的电压必须在GND到Vdd之间。超过Vdd会损坏芯片低于GND可能导致读取错误。4. 开关接触不良或接线错误。用万用表检查按钮按下和松开时引脚对地的电阻/电压变化。7.3 软件与库相关问题问题解决方案ImportError: no module named adafruit_mcp230xx库未正确安装。确保adafruit_mcp230xx.mpy文件在CIRCUITPY磁盘的lib文件夹内。AttributeError: MCP23017 object has no attribute get_pin库版本可能不兼容。确保你从最新的Adafruit库包中获取了.mpy文件。程序运行一次后卡死检查代码中是否有死循环或I2C操作未正确处理。确保主循环中有time.sleep()或pass避免过度占用CPU导致看门狗复位。同时使用多个I2C设备冲突确保每个I2C从设备都有唯一的地址。MCP230xx通过A0,A1,A2设置地址最多可在同一总线上挂8个MCP23008或8个MCP23017。调试是一个系统性的过程。我的习惯是先电源再通信后功能。确保供电稳定且正确用I2C扫描确认通信链路最后再测试具体的输入输出功能。使用逻辑分析仪或示波器观察I2C波形是解决复杂时序问题的终极武器。对于大多数爱好者一个几块钱的USB逻辑分析仪配合PulseView软件就能极大地提升调试效率。