TWI接口实战用Arduino模拟I2C从机设备附完整代码在物联网和嵌入式系统的世界里设备间的“对话”是构建智能场景的基石。想象一下你手头有一个小巧的Arduino开发板需要让它扮演一个温湿度传感器的角色向一个更强大的主控制器比如树莓派或另一块Arduino汇报数据。这种主从协作的模式其背后往往依赖于一种经典、简洁且高效的通信协议——TWI也就是我们常说的I2C。对于许多开发者尤其是刚接触嵌入式通信的朋友配置一个I2C主机去读取现成的传感器模块如BMP280、MPU6050已经是家常便饭Arduino的Wire库让这一切变得轻而易举。然而当你需要让你的Arduino设备“伪装”成一个传感器主动响应来自其他主机的查询时情况就变得有趣且更具挑战性了。这不仅仅是调用几个库函数那么简单它要求你深入理解TWI协议的状态机、中断处理以及寄存器级别的操作逻辑。本文将带你从零开始用一块最常见的Arduino Uno基于ATmega328P微控制器实现一个功能完整的TWI从机设备。我们将绕过高级库的封装直接操作硬件寄存器编写中断服务程序并构建一个可以稳定响应主机请求的从机框架。无论你是想为自定义传感器创建协议还是需要在多设备系统中模拟一个虚拟外设这篇实战指南都将为你提供清晰的路径和可直接运行的代码。1. 理解TWI/I2C从机模式的核心机制在动手写代码之前我们必须先厘清TWI从机设备是如何“思考”和“响应”的。与主机主动发起通信不同从机始终处于一种被动的监听状态它的行为完全由总线上的事件起始条件、地址匹配、数据收发驱动。1.1 TWI状态机从机的“行为准则”ATmega328P的TWI硬件模块本质上是一个复杂的状态机。它会在每一个关键事件如接收到起始条件、地址字节、数据字节或停止条件后设置中断标志TWINT并更新状态寄存器TWSTA。我们的软件任务就是根据TWSTA的值判断当前处于哪个状态并执行相应的操作然后清除TWINT标志以允许硬件继续运行。注意TWINT标志的清除非常关键。清除它并非简单地写0而是通过向TWCR控制寄存器写入一个特定的值通常包含TWINT位为1来实现。这是一个“以写1来清除”的标志位。对于从机而言其生命周期通常围绕以下几个核心状态展开地址识别阶段从机初始化后会持续监听总线。当主机发送起始条件S并紧随一个7位地址加读写位R/W时TWI硬件会将其与自身预设的从机地址进行比较。如果匹配且从机已使能应答AA位为1硬件会自动在第九个时钟脉冲拉低SDA线发出应答ACK并进入相应的状态0x60或0x80分别对应从机接收和从机发送模式。数据交换阶段地址匹配后从机便进入数据收发循环。在从机接收模式SR下每收到一个数据字节状态寄存器会更新如0x80我们需要从TWDR寄存器读取数据并决定是否发送ACK。在从机发送模式ST下当主机请求数据时状态会变为0xA8我们需要将待发送的数据写入TWDR寄存器。通信终止当主机发送停止条件P时通信结束从机回到空闲监听状态。此外如果从机在发送最后一个字节后收到主机的NACK也意味着主机不再需要数据通信同样终止。为了更直观地理解从机在典型数据请求流程中的状态变迁我们可以参考下面的简化状态图从机初始化 (AA1) | v 监听总线等待地址匹配 | v [主机发送: S 地址 R] | v 状态: 0xA8 (从机发送模式地址已识别ACK已发送) |-- 软件将数据写入TWDR清除TWINT v [主机接收数据字节并回复ACK] | v 状态: 0xB8 (数据已发送收到ACK) |-- 软件准备下一个数据如有清除TWINT v ... (循环直到主机发送NACK或停止条件) | v 状态: 0xC0 (数据已发送收到NACK) - 通信结束回到监听 或 状态: 0xA0 (收到停止/重复起始条件) - 通信结束回到监听1.2 关键寄存器速览直接操作寄存器是本次实战的精髓。ATmega328P上与TWI相关的几个主要寄存器如下寄存器名称地址主要功能描述TWBR0xB8TWI比特率寄存器。与预分频器共同决定SCL时钟频率。在从机模式下此寄存器通常不影响从机功能因为时钟由主机提供。TWCR0xBCTWI控制寄存器。核心控制单元包含中断使能、应答控制、启动/停止条件生成等位。TWSR0xB9TWI状态寄存器。高5位TWS7:3表示状态码这是驱动状态机的关键。低3位用于预分频设置。TWDR0xBBTWI数据寄存器。在发送时写入待发送的数据在接收时读取接收到的数据。TWAR0xBATWI从机地址寄存器。高7位TWAR6:0用于设置从机地址最低位TWGCE用于使能通用呼叫地址0x00识别。其中TWCR寄存器的几个位需要我们特别关注TWINT (位7): TWI中断标志。当TWI硬件完成当前操作并等待应用程序响应时此位由硬件置1。软件通过向该位写1来清除它清除后TWI硬件才会继续工作。TWEA (位6): TWI应答使能。置1时在地址匹配或数据接收成功后硬件会自动发出ACK。当从机不希望接收更多数据时可将其清零随后主机将收到NACK。TWSTA (位5) / TWSTO (位4): 分别用于生成起始和停止条件。在从机模式下我们通常不主动设置它们。TWEN (位2): TWI使能位。必须置1才能启用TWI硬件功能。理解了这些机制我们就有了搭建从机代码框架的理论基础。接下来我们将进入实际的开发环境设置环节。2. 搭建Arduino开发环境与基础工程虽然我们使用Arduino平台但为了进行寄存器级编程我们将主要利用AVR-GCC编译器提供的底层头文件和语法这能让我们获得对硬件最直接的控制权。2.1 创建工程与核心头文件打开Arduino IDE创建一个新的空白项目。我们首先需要包含必要的AVR头文件并定义一些宏来简化寄存器访问和从机地址。#include avr/io.h #include avr/interrupt.h // 定义TWI相关寄存器针对ATmega328P // 这些定义通常已包含在avr/io.h中此处为清晰列出 // #define TWBR _SFR_MEM8(0xB8) // #define TWSR _SFR_MEM8(0xB9) // #define TWAR _SFR_MEM8(0xBA) // #define TWDR _SFR_MEM8(0xBB) // #define TWCR _SFR_MEM8(0xBC) // 定义我们的从机地址 (7位格式左对齐) // 例如地址0x50 (二进制 1010000) #define MY_SLAVE_ADDRESS (0x50 1) // TWAR要求地址左移一位 // 定义TWI状态码 (从机相关部分) // 从机接收器模式 #define TWI_SRX_ADR_ACK 0x60 // 自身地址W已接收ACK已返回 #define TWI_SRX_ADR_ACK_M_ARB_LOST 0x68 // 仲裁丢失后自身地址W已接收ACK已返回 #define TWI_SRX_GEN_ACK 0x70 // 通用呼叫地址已接收ACK已返回 #define TWI_SRX_GEN_ACK_M_ARB_LOST 0x78 // 仲裁丢失后通用呼叫地址已接收ACK已返回 #define TWI_SRX_DATA_ACK 0x80 // 数据字节已接收ACK已返回 #define TWI_SRX_DATA_NACK 0x88 // 数据字节已接收NACK已返回 #define TWI_SRX_STOP_RESTART 0xA0 // 收到STOP或重复START条件 // 从机发送器模式 #define TWI_STX_ADR_ACK 0xA8 // 自身地址R已接收ACK已返回 #define TWI_STX_ADR_ACK_M_ARB_LOST 0xB0 // 仲裁丢失后自身地址R已接收ACK已返回 #define TWI_STX_DATA_ACK 0xB8 // 数据字节已发送收到ACK #define TWI_STX_DATA_NACK 0xC0 // 数据字节已发送收到NACK #define TWI_STX_DATA_ACK_LAST 0xC8 // 最后一个数据字节已发送AA0收到ACK // 全局变量用于存储接收到的数据和待发送的数据 volatile uint8_t twi_received_data 0; volatile uint8_t twi_data_to_send 0xAA; // 示例数据 volatile bool new_data_received false;2.2 初始化TWI为从机模式在setup()函数中我们需要完成TWI硬件的初始化。这个过程主要包括设置从机地址、使能TWI模块、使能应答以及开启全局中断。void setup() { // 初始化串口用于调试输出 Serial.begin(115200); Serial.println(TWI Slave Device Initializing...); // 1. 设置从机地址 // TWAR的高7位是地址最低位TWGCE是通用呼叫识别使能 TWAR MY_SLAVE_ADDRESS; // 例如 0xA0 (0x50 1) // 如果希望响应通用呼叫地址(0x00)则设置 TWAR | (1 TWGCE); // 2. 设置比特率可选从机模式下主要适应主机时钟 // TWSR的低3位是预分频位清零设置为预分频1 TWSR ~((1TWPS1) | (1TWPS0)); // TWBR值影响主机模式从机模式下可设为一个值这里设为72在16MHz下SCL约100kHz TWBR 72; // 3. 使能TWI使能应答使能TWI中断清除TWINT标志位 // TWCR (1TWEN) | (1TWIE) | (1TWEA) | (1TWINT); // 注意初始化时先不使能中断(TWIE)等配置完再开启 TWCR (1TWEN) | (1TWEA); // 通过写1清除TWINT标志启动TWI逻辑 TWCR | (1TWINT); // 4. 使能全局中断 sei(); Serial.println(TWI Slave Ready. Address: 0x); Serial.println(MY_SLAVE_ADDRESS 1, HEX); // 打印7位地址 }这里有几个关键点从机地址我们设定的地址是0x507位写入TWAR时需要左移一位所以实际写入的是0xA0。TWCR初始化(1TWEN)启用TWI模块(1TWEA)使能应答这样在地址匹配和成功接收数据后会自动回复ACK(1TWINT)通过写1来清除中断标志这步操作会“启动”TWI接口使其开始监听总线。中断我们在初始化时没有立即开启TWI中断(TWIE)而是在TWCR设置好之后再开启全局中断sei()。更常见的做法是在TWI中断服务程序(ISR)稳定后再开启TWIE或者在ISR内根据状态谨慎处理避免嵌套中断带来的复杂性。我们将在完整的ISR中处理TWIE。初始化完成后我们的Arduino就已经在I2C总线上“挂载”好了地址是0x50静静地等待主机的召唤。3. 编写TWI中断服务程序ISR中断服务程序是整个从机逻辑的核心。它需要快速、准确地响应各种TWI状态并执行相应的数据读写操作。由于ISR中不宜进行耗时操作如大量计算或串口打印我们通常只做最必要的寄存器操作和标志位设置复杂的数据处理留给主循环。3.1 ISR框架与状态分发首先我们定义一个全局的twi_state变量来跟踪状态尽管可以从TWSR读取但保存一下有时更方便然后编写ISR的主体框架。// 可选定义一个状态变量便于调试跟踪 volatile uint8_t twi_state 0; ISR(TWI_vect) { // 读取当前状态码屏蔽预分频位 twi_state TWSR 0xF8; // 根据状态码进行分发处理 switch (twi_state) { // --- 从机接收器模式 (主机写数据到从机) --- case TWI_SRX_ADR_ACK: // 自身地址W已接收ACK已自动发出 // 准备接收数据 // 确保TWEA保持为1以继续接收后续数据 TWCR (1TWEN) | (1TWIE) | (1TWEA) | (1TWINT); break; case TWI_SRX_DATA_ACK: // 数据字节已接收ACK已自动发出 twi_received_data TWDR; // 读取接收到的数据 new_data_received true; // 设置标志通知主循环 // 继续使能应答准备接收下一个字节 TWCR (1TWEN) | (1TWIE) | (1TWEA) | (1TWINT); break; case TWI_SRX_DATA_NACK: // 数据字节已接收但从机回复了NACK通常因为TWEA0 // 这表示从机不想再接收数据 twi_received_data TWDR; new_data_received true; // 可以在此处将TWEA置1恢复监听或保持TWEA0 TWCR (1TWEN) | (1TWIE) | (1TWEA) | (1TWINT); break; case TWI_SRX_STOP_RESTART: // 收到STOP或重复START条件一次传输结束 // 复位到可寻址的从机监听状态 TWCR (1TWEN) | (1TWIE) | (1TWEA) | (1TWINT); break; // --- 从机发送器模式 (主机从从机读数据) --- case TWI_STX_ADR_ACK: // 自身地址R已接收ACK已自动发出 // 需要加载第一个要发送的数据到TWDR TWDR twi_data_to_send; // 发送示例数据 // 使能应答继续发送 TWCR (1TWEN) | (1TWIE) | (1TWEA) | (1TWINT); break; case TWI_STX_DATA_ACK: // 数据字节已发送且主机回复了ACK请求更多数据 // 准备下一个要发送的数据 // 这里可以更新twi_data_to_send例如从传感器读取新值 // twi_data_to_send read_sensor(); TWDR twi_data_to_send; // 保持TWEA1继续发送下一个字节 TWCR (1TWEN) | (1TWIE) | (1TWEA) | (1TWINT); break; case TWI_STX_DATA_NACK: // 数据字节已发送但主机回复了NACK停止请求数据 // 或者在发送最后一个字节前我们主动将TWEA清零也会进入此状态 // 传输结束复位到监听状态 TWCR (1TWEN) | (1TWIE) | (1TWEA) | (1TWINT); break; // --- 其他状态处理错误或未实现 --- default: // 遇到未预期的状态执行恢复操作 // 发送NACK然后生成STOP条件在从机模式下谨慎使用STOP // 更安全的做法是复位TWI接口 TWCR 0; // 关闭TWI delayMicroseconds(10); TWCR (1TWEN) | (1TWIE) | (1TWEA) | (1TWINT); // 重新启用 break; } }这个ISR处理了从机模式下的几个主要状态。它的核心逻辑是读取状态从TWSR获取当前状态码。分支处理根据状态码执行对应的操作如读取TWDR、写入TWDR、设置TWCR。清除中断并继续在每一个分支的最后都必须重新配置TWCR其中必须包含(1TWINT)来清除中断标志并设置好TWEN、TWIE、TWEA等控制位以便TWI硬件继续进行下一步操作。3.2 处理数据缓冲区与主从协同上面的示例使用了一个简单的单字节变量twi_data_to_send和twi_received_data。在实际应用中我们可能需要一个缓冲区来存储要发送的多字节数据例如一个完整的传感器读数包含温度、湿度等多个字节或者解析接收到的多字节命令。同时ISR与主循环(loop)之间的通信需要通过** volatile **变量和标志位来实现以确保数据同步和避免竞争条件。下面我们扩展一下数据处理的例子// 定义数据缓冲区 #define TX_BUFFER_SIZE 4 #define RX_BUFFER_SIZE 8 volatile uint8_t tx_buffer[TX_BUFFER_SIZE] {0x01, 0x02, 0x03, 0x04}; volatile uint8_t rx_buffer[RX_BUFFER_SIZE]; volatile uint8_t tx_index 0; volatile uint8_t rx_index 0; volatile bool data_ready_for_tx true; volatile bool rx_complete false; // 在ISR的TWI_STX_ADR_ACK和TWI_STX_DATA_ACK分支中修改 case TWI_STX_ADR_ACK: // 地址匹配开始发送。重置发送索引。 tx_index 0; if (tx_index TX_BUFFER_SIZE) { TWDR tx_buffer[tx_index]; } else { // 缓冲区无数据发送默认值或错误码 TWDR 0xFF; } TWCR (1TWEN) | (1TWIE) | (1TWEA) | (1TWINT); break; case TWI_STX_DATA_ACK: // 主机请求更多数据 if (tx_index TX_BUFFER_SIZE) { TWDR tx_buffer[tx_index]; } else { // 所有数据已发送完下一个字节发送完后将回复NACK // 可以发送一个结束符或者将TWEA清零 TWDR 0xFF; // 结束符 // 如果想在发送完此字节后停止可以在发送前将TWEA清零 // 但更常见的做法是让主机发NACK来停止 } TWCR (1TWEN) | (1TWIE) | (1TWEA) | (1TWINT); break; // 在ISR的TWI_SRX_DATA_ACK分支中修改 case TWI_SRX_DATA_ACK: if (rx_index RX_BUFFER_SIZE) { rx_buffer[rx_index] TWDR; } else { // 缓冲区溢出可以忽略数据或采取错误处理 } // 通知主循环有新数据部分 // 更完整的做法是在收到STOP条件(TWI_SRX_STOP_RESTART)后再标志完成 TWCR (1TWEN) | (1TWIE) | (1TWEA) | (1TWINT); break; case TWI_SRX_STOP_RESTART: // 一次完整的写传输结束 rx_complete true; // 设置完成标志 rx_index 0; // 为下一次接收准备或根据协议决定 TWCR (1TWEN) | (1TWIE) | (1TWEA) | (1TWINT); break;在主循环中我们可以检查rx_complete或new_data_received标志然后处理接收到的数据或者更新tx_buffer中的待发送数据。void loop() { // 处理接收完成的数据包 if (rx_complete) { noInterrupts(); // 临时关闭中断安全访问共享缓冲区 uint8_t len rx_index; uint8_t temp_buf[RX_BUFFER_SIZE]; for (uint8_t i0; ilen; i) { temp_buf[i] rx_buffer[i]; } rx_complete false; rx_index 0; interrupts(); // 重新开启中断 // 处理temp_buf中的数据... Serial.print(Received Command: ); for (uint8_t i0; ilen; i) { Serial.print(temp_buf[i], HEX); Serial.print( ); } Serial.println(); // 示例如果收到0x01则更新发送缓冲区为模拟传感器数据 if (len 0 temp_buf[0] 0x01) { // 模拟读取传感器 tx_buffer[0] 0x5A; // 温度高字节 tx_buffer[1] 0x1F; // 温度低字节 tx_buffer[2] 0x40; // 湿度高字节 tx_buffer[3] 0x00; // 湿度低字节 data_ready_for_tx true; } } // 其他主循环任务... delay(100); }通过这样的设计我们就实现了一个能够稳定处理多字节读写、具备基本命令解析能力的TWI从机设备。主机可以通过I2C总线向地址0x50写入命令字节如0x01从机接收到后会在下一次主机发起读请求时返回模拟的传感器数据。4. 完整代码示例与实战调试将以上所有部分整合并添加一些必要的注释和调试信息我们就得到了一个完整的、可运行的Arduino TWI从机示例。这个示例模拟了一个简单的“智能寄存器”设备主机可以写入一个寄存器地址然后读取该地址对应的值。4.1 完整代码清单/** * Arduino TWI (I2C) Slave Device Demo * 从机地址: 0x50 (7-bit) * 功能 * 1. 主机写入一个字节寄存器地址 0x00 或 0x01 * 2. 主机读取一个或两个字节对应寄存器的值 */ #include avr/io.h #include avr/interrupt.h // 从机地址定义 #define SLAVE_ADDR 0x50 #define TWI_SLAVE_ADDR ((SLAVE_ADDR 1) 0xFE) // 左移一位清空R/W位 // TWI状态码定义 (从机部分) #define TWI_SRX_ADR_ACK 0x60 #define TWI_SRX_DATA_ACK 0x80 #define TWI_SRX_DATA_NACK 0x88 #define TWI_SRX_STOP_RESTART 0xA0 #define TWI_STX_ADR_ACK 0xA8 #define TWI_STX_DATA_ACK 0xB8 #define TWI_STX_DATA_NACK 0xC0 // 全局变量 volatile uint8_t twi_state; volatile uint8_t register_address 0; // 主机请求的寄存器地址 volatile uint8_t tx_data[2] {0xAB, 0xCD}; // 模拟的寄存器数据 volatile bool address_received false; volatile bool read_request_pending false; void setup() { Serial.begin(115200); Serial.println( TWI Slave Demo Start ); // 初始化TWI从机 TWAR TWI_SLAVE_ADDR; // 设置自身地址 TWSR 0x00; // 预分频器 1 TWBR 72; // SCL ~ 100kHz 16MHz // 使能TWI使能应答清除中断标志以启动 TWCR (1 TWEN) | (1 TWEA) | (1 TWINT); // 使能TWI中断 TWCR | (1 TWIE); // 使能全局中断 sei(); Serial.print(Slave Address: 0x); Serial.println(SLAVE_ADDR, HEX); Serial.println(Waiting for commands...); } void loop() { // 主循环可以处理其他任务如更新tx_data // 例如可以定期从传感器读取真实数据更新到tx_data // 此处仅作演示每秒输出一次状态 static unsigned long last_print 0; if (millis() - last_print 1000) { last_print millis(); Serial.print(State: 0x); Serial.print(twi_state, HEX); Serial.print(, RegAddr: 0x); Serial.print(register_address, HEX); Serial.print(, TxData: 0x); Serial.print(tx_data[0], HEX); Serial.print( 0x); Serial.println(tx_data[1], HEX); } delay(10); } // TWI中断服务程序 ISR(TWI_vect) { twi_state TWSR 0xF8; // 获取状态码屏蔽低3位 switch (twi_state) { // --- 从机接收模式 (主机写) --- case TWI_SRX_ADR_ACK: // 地址匹配写ACK已发送。准备接收数据寄存器地址。 address_received false; read_request_pending false; TWCR (1 TWEN) | (1 TWIE) | (1 TWEA) | (1 TWINT); break; case TWI_SRX_DATA_ACK: // 收到数据字节假定为寄存器地址ACK已发送。 register_address TWDR; // 保存寄存器地址 address_received true; // 这里可以判断地址是否有效无效则后续可发送NACK TWCR (1 TWEN) | (1 TWIE) | (1 TWEA) | (1 TWINT); break; case TWI_SRX_STOP_RESTART: // 收到STOP或重复START。一次写传输结束。 // 如果之前收到了有效的寄存器地址可以标记有待处理的读请求。 if (address_received) { read_request_pending true; address_received false; } TWCR (1 TWEN) | (1 TWIE) | (1 TWEA) | (1 TWINT); break; // --- 从机发送模式 (主机读) --- case TWI_STX_ADR_ACK: // 地址匹配读ACK已发送。需要发送第一个数据字节。 // 根据之前存储的register_address决定发送哪个数据 if (register_address 0x00) { TWDR tx_data[0]; } else if (register_address 0x01) { TWDR tx_data[1]; } else { TWDR 0xFF; // 无效地址返回错误码 } read_request_pending false; TWCR (1 TWEN) | (1 TWIE) | (1 TWEA) | (1 TWINT); break; case TWI_STX_DATA_ACK: // 上一个数据字节已发送且收到ACK主机请求更多数据。 // 本例中我们只定义了两个单字节寄存器所以第二个字节发送固定值或结束符。 // 发送一个结束字节后在下一次主机请求前我们可以将TWEA清零使其回复NACK。 TWDR 0xEE; // 发送结束标记 // 保持TWEA1如果主机继续读会收到0xEE并回复ACK/NACK TWCR (1 TWEN) | (1 TWIE) | (1 TWEA) | (1 TWINT); break; case TWI_STX_DATA_NACK: // 数据字节已发送收到NACK主机停止读取。 // 或者我们主动发送最后一个字节前将TWEA清零也会进入此状态。 // 传输结束回到监听状态。 TWCR (1 TWEN) | (1 TWIE) | (1 TWEA) | (1 TWINT); break; // --- 其他/错误状态 --- default: // 遇到未处理状态进行复位恢复 TWCR 0; // 关闭TWI TWCR (1 TWEN) | (1 TWIE) | (1 TWEA) | (1 TWINT); break; } }4.2 使用Arduino作为主机进行测试为了验证从机代码我们可以使用另一块Arduino或任何I2C主机设备进行测试。下面是一个简单的Arduino主机测试脚本使用标准的Wire库。// TWI Slave Tester (Master Code) #include Wire.h #define SLAVE_ADDR 0x50 void setup() { Wire.begin(); // 作为主机无需地址 Serial.begin(115200); Serial.println(I2C Master Tester); } void loop() { // 测试1向从机寄存器0x00写入任意值例如0x5A然后读取该寄存器 Serial.println(\n--- Test Write then Read Register 0x00 ---); Wire.beginTransmission(SLAVE_ADDR); Wire.write(0x00); // 寄存器地址 // Wire.write(0x5A); // 如果要写入数据可以取消注释。本例从机忽略写入的数据值。 byte error Wire.endTransmission(); if (error 0) { Serial.println(Write address OK.); } else { Serial.print(Write error: ); Serial.println(error); } delay(10); // 给从机一点处理时间 Wire.requestFrom(SLAVE_ADDR, 2); // 请求2个字节 Serial.print(Data received: ); while (Wire.available()) { byte c Wire.read(); Serial.print(0x); Serial.print(c, HEX); Serial.print( ); } Serial.println(); delay(2000); // 等待2秒 // 测试2读取寄存器0x01 Serial.println(\n--- Test Read Register 0x01 ---); Wire.beginTransmission(SLAVE_ADDR); Wire.write(0x01); // 寄存器地址 0x01 error Wire.endTransmission(); if (error 0) { Serial.println(Write address OK.); } delay(10); Wire.requestFrom(SLAVE_ADDR, 1); // 请求1个字节 Serial.print(Data received: ); while (Wire.available()) { byte c Wire.read(); Serial.print(0x); Serial.print(c, HEX); } Serial.println(); delay(5000); // 等待5秒进行下一轮测试 }将主机代码上传到另一块Arduino连接好SDAA4和SCLA5线并共地。打开两个串口监视器你应该能看到从机打印出自己的状态而主机则能成功写入寄存器地址并读取到对应的数据0xAB或0xCD。4.3 调试技巧与常见问题在调试TWI从机时逻辑分析仪或示波器是极其有用的工具可以直观地看到总线上的起始、停止、地址、数据和ACK/NACK信号。如果缺乏硬件工具串口打印结合状态码分析是主要手段。常见问题与排查从机无响应检查地址确认主机使用的7位地址与从机设置的MY_SLAVE_ADDRESS一致。Wire库使用7位地址而TWAR寄存器需要左移一位后的地址。检查接线确保SDA、SCL线连接正确并已上拉电阻通常4.7kΩ到10kΩ。Arduino内部有弱上拉但在长导线或多设备时外部上拉电阻更可靠。检查初始化确认TWCR在初始化时包含了(1TWEN) | (1TWEA) | (1TWINT)并且最后执行了sei()开启全局中断。通信不稳定数据错误状态码分析在ISR中通过串口打印twi_state注意在ISR中直接使用Serial.print可能不稳定最好设置标志位在主循环中打印。对照数据手册的状态码表看是否进入了预期状态。时序问题确保ISR执行时间足够短。如果ISR中做了太多事情可能导致无法及时响应下一个TWI中断造成超时或数据丢失。ACK/NACK处理仔细检查在每个状态下TWEA位的设置。例如在发送完最后一个数据字节后如果希望主机停止读取可以在发送该字节前将TWEA清零这样主机收到数据后会回复NACK从机进入TWI_STX_DATA_NACK状态。多主机仲裁与时钟同步在复杂的多主机系统中从机代码通常不需要处理仲裁丢失状态码0x68,0x78,0xB0因为那是主机需要考虑的问题。但一个设计健壮的从机可以处理这些状态通常的处理方式是简单地复位到监听状态TWCR (1TWEN) | (1TWIE) | (1TWEA) | (1TWINT);。通过这个完整的实战项目你不仅获得了一个可用的Arduino TWI从机代码模板更重要的是深入理解了I2C/TWI协议在从机端的运作机理。你可以在此基础上进行扩展例如实现更复杂的寄存器映射、支持更长的数据包、或者将真实的传感器数据如ADC读数填充到发送缓冲区中从而创造出真正有用的自定义I2C从机设备。