引言

在典型的 I2C 通信中,从机只能被动响应主机请求,若需上报状态,主机必须周期性轮询,造成总线拥堵和响应延迟。AVR 系列单片机(如 ATmega328P)的 TWI(Two-Wire Interface)模块提供了硬件地址匹配功能,可扩展出“从机主动上报”模式。本文将深入剖析其原理,并给出在 Arduino 环境下的完整实现。

TWI 硬件地址匹配原理

AVR 的 TWI 模块在硬件层面持续监听总线,并将接收到的地址字节与自身地址寄存器(TWAR)比较。当匹配成功时,硬件自动应答 ACK,并触发 TWI 中断(TWINT 置位)。关键点在于:

  • 地址掩码寄存器(TWAMR):允许屏蔽地址位,使从机能响应多个地址。例如,设置 TWAMR=0xFE,则只匹配最低位,可响应两个地址。
  • 通用调用地址(0x00):若启用,可被所有从机响应。

利用这些特性,我们可以设计一个“事件触发地址”:主机平时不轮询,但当从机需要上报时,它先通过 GPIO 拉低 SCL 或使用专用信号线通知主机,然后主机发送一个特定地址(如 0x55),从机通过地址匹配中断唤醒,并立即接管总线发送数据。但更优雅的做法是:从机直接利用 TWI 的“多主机”能力,在事件发生时主动发起传输。

免轮询方案设计

我们的目标是:从机在内部事件(如传感器阈值)发生时,主动向主机发送数据,而无需主机轮询。实现思路如下:

  1. 初始化 TWI 为从机模式,并设置一个“唤醒地址”(如 0x30)。
  2. 启用 TWI 地址匹配中断,但平时不响应任何地址(通过 TWAR 设置为不匹配地址)。
  3. 当事件发生时,从机临时修改 TWAR 为唤醒地址,并拉低 SCL 线(模拟总线占用)或直接发送 START 条件(多主机模式)。
  4. 主机检测到总线活动,发送唤醒地址,从机匹配后进入中断,此时从机切换为主机模式,发送数据。

但更简洁的免轮询方案是:从机在事件发生时,主动发送 START 并抢占总线(多主机模式),直接向主机地址发送数据。这需要从机支持多主机仲裁。AVR TWI 硬件支持多主机,但需谨慎处理总线冲突。

配置步骤

1. TWI 从机模式初始化

#include <avr/io.h>
#include <avr/interrupt.h>

void twi_slave_init(uint8_t addr) {
    TWAR = addr << 1;  // 设置从机地址(7位左移一位)
    TWCR = (1<<TWEA) | (1<<TWEN) | (1<<TWIE);  // 使能 ACK、TWI 和中断
}

2. 地址匹配中断处理

ISR(TWI_vect) {
    uint8_t status = TWSR & 0xF8;
    if (status == 0x60) {  // 自己的地址+写
        // 准备数据,等待主机读取
    } else if (status == 0xA8) {  // 自己的地址+读
        // 主机请求数据,发送
    }
    TWCR = (1<<TWEA) | (1<<TWEN) | (1<<TWIE) | (1<<TWINT);  // 清除中断并继续
}

3. 主动上报实现

从机在事件发生时,临时切换为主机模式发送数据:

void twi_send_data(uint8_t host_addr, uint8_t* data, uint8_t len) {
    // 切换为主机模式
    TWCR = (1<<TWINT) | (1<<TWSTA) | (1<<TWEN);  // 发送 START
    // 等待 START 完成
    while (!(TWCR & (1<<TWINT)));
    // 发送主机地址+写
    TWDR = (host_addr << 1) | 0;
    TWCR = (1<<TWINT) | (1<<TWEN);
    // 等待并检查 ACK...
    // 发送数据...
    // 发送 STOP
    TWCR = (1<<TWINT) | (1<<TWSTO) | (1<<TWEN);
}

但注意:从机主动发送时,需确保总线空闲,否则会冲突。更稳妥的方式是使用“地址匹配唤醒”方案:

  • 从机平时设置 TWAR 为不匹配地址(如 0x00),并启用中断。
  • 事件发生时,从机拉低一个 GPIO 通知主机,主机收到后发送唤醒地址(如 0x55)。
  • 从机在中断中修改 TWAR 为自身地址,并准备数据,主机随后正常读取。

完整代码示例

以下代码实现从机事件触发上报(使用 GPIO 通知+地址匹配唤醒):

#include <Arduino.h>
#include <avr/io.h>
#include <avr/interrupt.h>

#define EVENT_PIN 2
#define WAKE_ADDR 0x55
#define SELF_ADDR 0x30

volatile bool event_occurred = false;
volatile bool wake_received = false;

void setup() {
    pinMode(EVENT_PIN, OUTPUT);
    digitalWrite(EVENT_PIN, HIGH);  // 空闲高电平
    // 初始化 TWI 从机,地址设为不匹配
    TWAR = 0x00;  // 不响应任何地址
    TWCR = (1<<TWEA) | (1<<TWEN) | (1<<TWIE);
    sei();
}

void loop() {
    if (event_occurred) {
        event_occurred = false;
        digitalWrite(EVENT_PIN, LOW);  // 通知主机
        delay(10);  // 等待主机发送唤醒地址
        // 等待唤醒中断
        while (!wake_received);
        wake_received = false;
        digitalWrite(EVENT_PIN, HIGH);
        // 此时主机已知道从机地址,可正常读取数据
    }
}

ISR(TWI_vect) {
    uint8_t status = TWSR & 0xF8;
    if (status == 0x60) {  // 收到唤醒地址(写)
        // 修改 TWAR 为自身地址
        TWAR = SELF_ADDR << 1;
        wake_received = true;
    }
    TWCR = (1<<TWEA) | (1<<TWEN) | (1<<TWIE) | (1<<TWINT);
}

// 模拟事件触发
void simulate_event() {
    event_occurred = true;
}

注意事项

  • 总线仲裁:从机主动发送时,必须检测总线空闲(TWCR 中的 TWINT 和 TWSTA 状态),否则可能冲突。
  • 地址冲突:唤醒地址应避免与总线上其他设备地址冲突,建议使用 0x00-0x07 保留地址或自定义地址。
  • 中断优先级:TWI 中断可能与其他中断竞争,需合理设置优先级。
  • 时序:GPIO 通知与 TWI 操作之间需适当延时,确保主机响应。
  • 功耗:启用 TWI 中断会唤醒 MCU,若需低功耗,可在事件发生前关闭 TWI,事件后重新初始化。

总结

利用 AVR TWI 的硬件地址匹配和中断机制,我们可以设计出高效的免轮询从机上报方案。通过 GPIO 通知与地址匹配结合,既避免了总线冲突,又提高了实时性。此方案适用于传感器节点、报警系统等场景,能显著降低总线负载和主机 CPU 占用。开发者可根据实际需求调整地址掩码和中断逻辑,实现更灵活的多从机通信。