1. 项目概述

MCP23017 是 Microchip 推出的一款高性能、16 位 I²C 总线 GPIO 扩展器,内置可编程中断输出、可配置上拉/下拉电阻、极性反转及输入滤波等功能。其双端口(PORT A 和 PORT B)结构、独立方向寄存器与输出锁存器设计,使其在资源受限的微控制器系统中成为扩展数字 I/O 的工业级首选方案。本项目“MCP23017 library for Attiny85”是一个专为 ATtiny85 微控制器定制的轻量级驱动库,旨在克服该芯片仅具备 5 个通用 I/O 引脚(其中 PB3/PB4 兼作 USI 接口)的硬件限制,通过标准 I²C 协议实现对 MCP23017 的完整功能控制。

该库处于开发阶段(UNSTABLE / in development),但已实现核心外设抽象层,覆盖初始化、引脚模式配置、电平读写、中断使能等关键操作。其设计严格遵循嵌入式底层开发的工程化原则:零动态内存分配、无浮点运算依赖、最小化 ROM/RAM 占用,并深度适配 ATtiny85 的 USI(Universal Serial Interface)模块——这是该芯片唯一支持 I²C 主机通信的硬件外设。库不依赖 Arduino Wire 库(因其在 ATtiny85 上不可用或功能不全),而是直接操作 USI 寄存器,确保时序精确性与系统确定性。

本技术文档面向硬件工程师与嵌入式开发者,将从硬件接口原理、寄存器级协议解析、库 API 详解、典型应用代码剖析到实际工程陷阱规避,系统性地展开说明。所有内容均基于提供的 README 示例与 ATtiny85/MCP23017 官方数据手册(DS21919F, DS25862C)推导,不引入任何未验证的虚构特性。

2. 硬件接口与通信原理

2.1 ATtiny85 USI 模块与 I²C 主机实现

ATtiny85 不具备专用 I²C(TWI)外设,其 USI 模块通过软件时序配合硬件移位寄存器,可模拟 I²C 主机行为。USI 包含三个核心寄存器:

  • USIDR (USI Data Register):8 位移位寄存器,用于发送/接收字节。
  • USIBR (USI Buffer Register):提供双缓冲,避免数据覆盖。
  • USISR (USI Status Register):包含计数器溢出标志(USIOIF)、位计数器(USICNT[3:0])及状态标志。

I²C 通信由 USI 的外部时钟源(SCL)触发。库通过配置 USICR 寄存器启用 USI 中断(USISIE)与计数器溢出中断(USIOIE),并在中断服务程序(ISR)中完成 START/STOP 条件生成、地址/数据字节传输及 ACK/NACK 处理。关键时序参数如下(基于 1MHz 系统时钟):

信号 最小时间 最大时间 库实现策略
SCL 高电平 4.0 μs 由 USI 计数器周期固定
SCL 低电平 4.7 μs 同上
SDA 建立时间 250 ns 在 SCL 下降沿前写入 USIDR
SDA 保持时间 300 ns SCL 上升沿后保持至少 300ns
START 建立时间 4.7 μs SCL 高时 SDA 由高→低

库通过精确的 NOP 指令插入与 USI 计数器预分频,确保所有时序满足 MCP23017 的 I²C 规范(标准模式 100kHz)。此纯寄存器级实现,避免了 Arduino Wire 库在 ATtiny85 上因缺少硬件 TWI 而导致的严重时序偏差与通信失败问题。

2.2 MCP23017 地址配置与寄存器映射

MCP23017 的 7 位 I²C 从机地址由硬件引脚 A0 , A1 , A2 决定,基础地址为 0x20 0b0100000 ),最终地址计算公式为:

I2C_Address = 0x20 | (A2 << 2) | (A1 << 1) | A0

例如, A2=A1=A0=0 时地址为 0x20 A2=1, A1=0, A0=1 时为 0x25 。库默认使用 0x20 ,可通过修改 MCP23017.h 中的 MCP23017_ADDRESS 宏重定义。

其内部寄存器采用 BANK=0 模式(默认),关键寄存器映射如下(地址为 8 位格式):

寄存器名 地址(HEX) 功能说明
IODIRA 0x00 PORT A 方向寄存器:0=输出,1=输入
IODIRB 0x01 PORT B 方向寄存器
IPOLA 0x02 PORT A 输入极性反转:0=正常,1=反转(读取值取反)
IPOLB 0x03 PORT B 输入极性反转
GPINTENA 0x04 PORT A 中断使能:1=对应引脚使能中断
GPINTENB 0x05 PORT B 中断使能
DEFVALA 0x06 PORT A 默认比较值(用于中断触发条件)
DEFVALB 0x07 PORT B 默认比较值
INTCONA 0x08 PORT A 中断控制:0=对比 DEFVAL,1=对比上一状态
INTCONB 0x09 PORT B 中断控制
IOCON 0x0A 配置寄存器:BIT6=SEQOP(顺序操作),BIT5=DISSLW(Slew Rate),BIT4=HAEN(硬件地址使能)等
GPPUA 0x0C PORT A 上拉电阻使能:1=使能对应引脚上拉
GPPUB 0x0D PORT B 上拉电阻使能
INTFA 0x0E PORT A 中断标志寄存器(只读)
INTFB 0x0F PORT B 中断标志寄存器
INTCAPA 0x10 PORT A 中断捕获寄存器(只读,记录中断发生时的输入状态)
INTCAPB 0x11 PORT B 中断捕获寄存器
GPIOA 0x12 PORT A 通用 I/O 寄存器:读=输入状态,写=输出电平
GPIOB 0x13 PORT B 通用 I/O 寄存器
OLATA 0x14 PORT A 输出锁存器(读取当前输出状态)
OLATB 0x15 PORT B 输出锁存器

库通过连续两次 I²C 写操作(先写地址,再写数据)或一次写+一次读操作访问这些寄存器,所有操作均以 8 位字节为单位。

3. 核心 API 接口详解

库以面向对象方式封装, MCP23017 类提供统一接口。所有函数均为内联(inline)或静态,消除函数调用开销,符合 ATtiny85 的资源约束。

3.1 初始化与配置

void begin(uint8_t address = MCP23017_ADDRESS);
  • 功能 :初始化 USI 模块为 I²C 主机模式,并复位 MCP23017。
  • 参数 address - 7 位 I²C 从机地址,默认 0x20
  • 实现细节
    1. 配置 PB0 (SDA) 和 PB2 (SCL) 为输出,并拉高( PORTB |= (1<<PB0) | (1<<PB2) )。
    2. 设置 USICR USIWM0=1 (3-wire 模式), USICS1=1 (外部时钟), USISIE=1 (使能中断)。
    3. 发送 I²C START + 地址写命令( address<<1 | 0 )。
    4. IOCON 寄存器写入 0x00 ,禁用 SEQOP、启用 BANK=0 模式。
    5. IODIRA IODIRB 清零,设置所有引脚为输入(安全默认)。
  • 返回值 :无。失败时无错误码(需用户通过后续 digitalRead() 返回值判断通信状态)。

3.2 引脚模式配置

void pinMode(uint8_t pin, uint8_t mode);
  • 功能 :配置指定引脚的工作模式。
  • 参数
    • pin :引脚编号,0–15。 0–7 对应 PORT A (A0–A7), 8–15 对应 PORT B (B0–B7)。
    • mode :模式常量,定义于头文件:
      • MCP_INPUT ( 0x00 ):配置为输入。
      • MCP_OUTPUT ( 0x01 ):配置为输出。
      • MCP_PULLUP ( 0x02 ):配置为输入并使能内部上拉电阻。
  • 实现逻辑
    • 根据 pin 计算目标寄存器( IODIRA / IODIRB )和位掩码。
    • mode == MCP_PULLUP ,则同时写 GPPUA / GPPUB 对应位为 1
    • 使用读-修改-写(Read-Modify-Write)操作更新方向寄存器,避免影响其他引脚。
  • 示例 mcp.pinMode(15, MCP_PULLUP) → 配置 PORT B 引脚 7 (B7) 为上拉输入。

3.3 数字电平读写

void digitalWrite(uint8_t pin, uint8_t val);
uint8_t digitalRead(uint8_t pin);
  • digitalWrite 功能 :设置指定引脚输出电平。
  • digitalRead 功能 :读取指定引脚当前电平。
  • 参数
    • pin :同 pinMode ,0–15。
    • val HIGH ( 0x01 ) 或 LOW ( 0x00 )。
  • 实现细节
    • 写操作 :读取当前 GPIOA / GPIOB 值 → 清除或置位对应位 → 写回寄存器。此操作保证原子性,避免多引脚并发写入冲突。
    • 读操作 :直接读取 GPIOA / GPIOB 寄存器 → 提取对应位 → 返回 HIGH / LOW 。若引脚配置为输出,读取的是锁存器值( OLATA / OLATB );若为输入,则是真实引脚电平。
  • 关键点 digitalRead(15) 在示例中读取 B7,其值被直接用于驱动 A1,构成一个简单的输入-输出桥接逻辑。

3.4 高级功能接口(开发中)

根据库的 UNSTABLE 状态及典型 MCP23017 应用需求,以下接口虽未在示例中出现,但属于该库合理且必要的扩展方向,其实现逻辑可基于现有寄存器操作推导:

// 使能/禁用单个引脚中断
void enableInterrupt(uint8_t pin, bool enable);

// 读取中断标志(清除中断条件)
uint8_t getInterruptFlags();

// 读取中断捕获寄存器(获取中断发生瞬间的输入快照)
uint16_t getInterruptCapture();
  • enableInterrupt :写 GPINTENA / GPINTENB 对应位。需注意,MCP23017 的中断引脚(INTA/INTB)需外接至 ATtiny85 的 INT0(PB2)或 PCINT(任意引脚),库本身不处理中断服务,仅配置使能。
  • getInterruptFlags :读 INTFA / INTFB ,返回 16 位标志字,bit0–7 对应 PORT A/B 的中断状态。
  • getInterruptCapture :读 INTCAPA / INTCAPB ,组合为 16 位值,反映中断触发时刻各引脚的真实电平。

4. 示例代码深度解析

提供的示例代码是一个典型的“乒乓”控制演示,其核心逻辑在于利用 MCP23017 的两个端口构建一个闭环反馈系统。

#include <Arduino.h>
#include <MCP23017.h>
MCP23017 mcp;

void setup() {
  mcp.begin(); // 初始化 USI 与 MCP23017
  for (int i = 0; i < 16; i++) {
    mcp.pinMode(i, MCP_OUTPUT); // 将全部 16 个引脚设为输出
  }
  pinMode(3, OUTPUT); // ATtiny85 的 PB3 设为输出(驱动 LED 或其他负载)
  mcp.pinMode(15, MCP_PULLUP); // 将 MCP23017 的 B7 (pin 15) 设为上拉输入
}

void loop() {
  mcp.digitalWrite(1, mcp.digitalRead(15)); // 将 B7 的输入状态复制到 A1
  mcp.digitalWrite(8, LOW);  // PORT B pin 0 (B0) 输出低电平
  mcp.digitalWrite(0, HIGH); // PORT A pin 0 (A0) 输出高电平
  delay(100);

  mcp.digitalWrite(8, HIGH); // B0 输出高电平
  mcp.digitalWrite(0, LOW);  // A0 输出低电平
  digitalWrite(3, LOW);      // ATtiny85 PB3 输出低电平
  delay(100);
}

4.1 电路连接推演

根据代码逻辑,可反推出最小硬件连接方案:

  • ATtiny85 PB0 (SDA) MCP23017 SDA (上拉至 VCC,4.7kΩ)
  • ATtiny85 PB2 (SCL) MCP23017 SCL (上拉至 VCC,4.7kΩ)
  • MCP23017 A0/A1/A2 → 接地(设定地址 0x20
  • MCP23017 RESET → 接 VCC(高电平有效,不复位)
  • MCP23017 INTA/INTB → 悬空(示例未使用中断)
  • MCP23017 B7 (pin 15) → 可接按钮开关至 GND(按下时读取为 LOW ,释放时 HIGH
  • MCP23017 A1 (pin 1) → 连接至 B7 的同一按钮(形成反馈环),或作为独立输出指示灯
  • MCP23017 B0 (pin 8) A0 (pin 0) → 可分别驱动两个 LED(阴极接地,阳极经限流电阻接引脚)
  • ATtiny85 PB3 (pin 3) → 驱动第三个 LED

4.2 时序与状态分析

loop() 函数每 200ms 完成一个完整周期,分为两个 100ms 阶段:

  • 阶段一(t=0–100ms)
    • mcp.digitalRead(15) 读取 B7 状态(假设按钮释放,为 HIGH )→ mcp.digitalWrite(1, HIGH) 将 A1 置高。
    • mcp.digitalWrite(8, LOW) 将 B0 置低 → B0 LED 熄灭。
    • mcp.digitalWrite(0, HIGH) 将 A0 置高 → A0 LED 点亮。
  • 阶段二(t=100–200ms)
    • mcp.digitalWrite(8, HIGH) 将 B0 置高 → B0 LED 点亮。
    • mcp.digitalWrite(0, LOW) 将 A0 置低 → A0 LED 熄灭。
    • digitalWrite(3, LOW) 将 ATtiny85 PB3 置低 → PB3 LED 点亮。

整个过程实现了 A0 与 B0 的互补闪烁,A1 实时镜像 B7 的输入状态,PB3 在第二阶段同步点亮。 delay(100) 的精度依赖于 millis() _delay_ms() 的实现;在 ATtiny85 上,若未启用 millis() (通常需要 Timer0),库可能依赖 util/delay.h _delay_ms() ,其精度由 F_CPU 定义决定。

5. 工程实践要点与常见问题

5.1 硬件设计注意事项

  • 上拉电阻 :I²C 总线必须有上拉电阻。推荐值: 4.7kΩ (标准模式,100kHz)。过小(如 1kΩ )会增加功耗并可能导致 ATtiny85 驱动能力不足;过大(如 10kΩ )会降低上升沿速度,引发通信错误。
  • 电源去耦 :在 MCP23017 的 VDD 和 VSS 引脚间放置 0.1μF 陶瓷电容,紧邻芯片引脚,抑制高频噪声。
  • 地址冲突 :若系统中存在多个 MCP23017,必须确保 A0–A2 组合产生唯一地址。库不提供地址扫描功能,需用户手动配置。
  • 电平匹配 :ATtiny85 工作电压为 1.8–5.5V,MCP23017 为 1.8–5.5V,二者可直接连接,无需电平转换。

5.2 软件集成与调试技巧

  • 编译环境 :使用 ATTinyCore arduino-tiny 核心。在 Arduino IDE 中选择 Board: ATtiny25/45/85 ,Processor: ATtiny85 ,Clock: 1 MHz (internal)
  • 调试通信 :当 mcp.begin() digitalRead() 返回异常值(如恒为 0 255 ),首先检查:
    1. 物理连接(SDA/SCL 是否接反?上拉电阻是否焊接?)
    2. 地址是否正确(用逻辑分析仪抓包,确认发出的地址字节)。
    3. begin() 函数中 USI 初始化是否成功(可临时添加 LED 闪烁指示)。
  • 性能优化 :库已为 ATtiny85 优化,但若需更高吞吐量,可考虑:
    • 将频繁访问的寄存器(如 GPIOA / GPIOB )缓存至 RAM,减少 I²C 事务次数。
    • 使用批量读写(如一次读取整个 PORT)替代单引脚操作。

5.3 与 FreeRTOS 的集成(扩展场景)

尽管示例未涉及 RTOS,但在更复杂的系统中,可将 MCP23017 驱动封装为 FreeRTOS 任务:

// 创建一个专用 I/O 任务
void io_task(void *pvParameters) {
  mcp.begin();
  mcp.pinMode(15, MCP_PULLUP);
  QueueHandle_t xQueue = xQueueCreate(10, sizeof(uint16_t));

  while(1) {
    uint16_t state = (mcp.digitalRead(15) << 1) | mcp.digitalRead(0);
    xQueueSend(xQueue, &state, portMAX_DELAY);
    vTaskDelay(pdMS_TO_TICKS(10));
  }
}
// 在其他任务中接收状态
uint16_t received;
if(xQueueReceive(xQueue, &received, portMAX_DELAY) == pdPASS) {
  // 处理 received 的 bit0 (A0) 和 bit1 (B7)
}

此模式将 I/O 操作与业务逻辑解耦,提升系统响应性与可维护性。

6. 源码结构与可移植性分析

库的源码结构极为精简,典型布局如下:

MCP23017/
├── MCP23017.h     // 类声明、宏定义、函数原型
├── MCP23017.cpp   // USI I²C 实现、寄存器读写、pinMode/digitalWrite 等
└── USI_TWI.h/.c   // (可选)独立的 USI I²C 驱动,提高复用性
  • 可移植性路径
    • 至其他 AVR :只需修改 USI 相关寄存器定义(如 ATmega328P 使用 TWI,需替换为 TWDR / TWSR 操作)。
    • 至 ARM Cortex-M :替换 MCP23017.cpp 中的 I²C 通信部分为 HAL_I2C_Master_Transmit/Receive。
    • 至 ESP32 :可直接使用 Wire.h ,大幅简化代码。
  • 关键移植点 :所有与硬件交互的代码(USI 初始化、START/STOP 生成、字节传输)均集中于少数函数中,隔离度高,便于维护。

该库的价值不仅在于其当前功能,更在于它提供了一个在极端资源约束下,如何通过寄存器级编程实现可靠外设通信的范本。对于任何需要在 8 位 MCU 上扩展 I/O 的项目,此库的架构与实现思路都具有直接的参考价值。

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐