引言
在典型的 I2C 通信中,从机只能被动响应主机请求,若需上报状态,主机必须周期性轮询,造成总线拥堵和响应延迟。AVR 系列单片机(如 ATmega328P)的 TWI(Two-Wire Interface)模块提供了硬件地址匹配功能,可扩展出“从机主动上报”模式。本文将深入剖析其原理,并给出在 Arduino 环境下的完整实现。
TWI 硬件地址匹配原理
AVR 的 TWI 模块在硬件层面持续监听总线,并将接收到的地址字节与自身地址寄存器(TWAR)比较。当匹配成功时,硬件自动应答 ACK,并触发 TWI 中断(TWINT 置位)。关键点在于:
- 地址掩码寄存器(TWAMR):允许屏蔽地址位,使从机能响应多个地址。例如,设置 TWAMR=0xFE,则只匹配最低位,可响应两个地址。
- 通用调用地址(0x00):若启用,可被所有从机响应。
利用这些特性,我们可以设计一个“事件触发地址”:主机平时不轮询,但当从机需要上报时,它先通过 GPIO 拉低 SCL 或使用专用信号线通知主机,然后主机发送一个特定地址(如 0x55),从机通过地址匹配中断唤醒,并立即接管总线发送数据。但更优雅的做法是:从机直接利用 TWI 的“多主机”能力,在事件发生时主动发起传输。
免轮询方案设计
我们的目标是:从机在内部事件(如传感器阈值)发生时,主动向主机发送数据,而无需主机轮询。实现思路如下:
- 初始化 TWI 为从机模式,并设置一个“唤醒地址”(如 0x30)。
- 启用 TWI 地址匹配中断,但平时不响应任何地址(通过 TWAR 设置为不匹配地址)。
- 当事件发生时,从机临时修改 TWAR 为唤醒地址,并拉低 SCL 线(模拟总线占用)或直接发送 START 条件(多主机模式)。
- 主机检测到总线活动,发送唤醒地址,从机匹配后进入中断,此时从机切换为主机模式,发送数据。
但更简洁的免轮询方案是:从机在事件发生时,主动发送 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 占用。开发者可根据实际需求调整地址掩码和中断逻辑,实现更灵活的多从机通信。