AVR TWI寄存器级编程与CRC内存扫描实战指南
1. 项目概述:深入AVR TWI总线与CRC扫描的寄存器级操作
如果你正在捣鼓一块基于AVR单片机的板子,需要连接多个传感器或扩展芯片,比如AT24Cxx系列EEPROM、DS1307时钟芯片,或者像PCA9555这样的I/O扩展器,那么你大概率绕不开TWI(Two-Wire Interface)总线。这其实就是我们常说的I²C总线在Atmel/Microchip AVR世界里的名字。表面上,用Arduino的Wire库几行代码就能读写数据,但当你需要实现更复杂的多主机仲裁、高速模式,或者像我最近做的一个项目——需要通过TWI总线对远端设备的一片内存区域进行完整性校验(CRC扫描)时,仅仅停留在库函数层面就远远不够了。你会遇到时序不稳定、从机无响应、校验结果飘忽不定等一系列玄学问题。
这时候,就必须深入到寄存器层面,去理解主机(Master)如何发起传输,客户端(Slave,这里指作为从机的AVR或外设)如何响应,以及如何利用CRC(循环冗余校验)机制来确保大块内存数据传输的绝对可靠。这不仅仅是配置几个寄存器位那么简单,它关乎你对总线状态机、中断时序和错误处理机制的深刻理解。本文将从一个实际的内存扫描需求出发,拆解AVR TWI主机与客户端的核心寄存器,并详细阐述如何构建一个可靠的、基于CRC的远程内存验证机制。无论你是想优化现有TWI通信的稳定性,还是需要实现类似的诊断功能,这些寄存器级的操作细节和避坑经验都将为你提供直接的参考。
2. TWI总线基础与寄存器框架解析
2.1 TWI协议精要与AVR实现特点
TWI/I²C是一种同步、半双工、多主多从的串行总线。它依靠两根线:串行数据线(SDA)和串行时钟线(SCL)。所有通信都由主机产生的时钟驱动,每个设备都有唯一的7位或10位地址。协议本身包括起始条件(START)、重复起始条件(Repeated START)、地址帧(含读写位)、数据帧和停止条件(STOP)。
AVR单片机(如ATmega328P, ATmega2560等)将TWI控制器集成在外设内部,它本质上是一个高度自动化的状态机。我们程序员需要做的,就是通过配置几个关键的寄存器,来设置总线速度、自身地址(如果作为从机),然后通过读写数据寄存器来触发状态机的运转,并通过状态寄存器来了解当前通信进行到哪一步了。这种“寄存器驱动状态机”的模式,是高效、可靠使用TWI的关键,它避免了纯软件模拟时序可能带来的不精确和CPU占用率高的问题。
2.2 TWI相关寄存器全景图
在AVR中,TWI功能主要围绕以下几个寄存器展开,它们是整个通信的操控面板:
- TWBR (TWI Bit Rate Register): 比特率寄存器。它与预分频器共同决定SCL时钟频率。计算公式为:
SCL频率 = CPU时钟频率 / (16 + 2 * TWBR * 预分频因子)。其中预分频因子由TWSR寄存器的低两位(TWPS1:0)设置,可为1, 4, 16, 64。这是调整通信速率的首要入口。 - TWSR (TWI Status Register): 状态寄存器。其高5位(TWS7:3)反映了TWI硬件状态机的当前状态,这是所有决策的依据。低2位(TWPS1:0)是上述的预分频位。任何状态判断都必须基于这高5位。
- TWAR (TWI (Slave) Address Register): 从机地址寄存器。当本机被配置为从机时,其高7位存放自身的7位地址。最低位(TWGCE)用于决定是否响应广播呼叫地址(0x00)。
- TWDR (TWI Data Register): 数据寄存器。在发送模式下,你要写入的数据就放在这里;在接收模式下,从总线上读取到的数据也存放在这里。它是在主机和客户端之间流动的信息载体。
- TWCR (TWI Control Register): 控制寄存器。这是整个TWI模块的“总开关”和“动作触发器”,每一个位都至关重要:
TWINT(TWI Interrupt Flag): 中断标志位。硬件在完成一个操作(如发送START、收到ACK、发送完一个字节等)后会自动置1。我们的核心编程模式就是:等待TWINT置1 -> 读取TWSR判断状态 -> 根据状态执行相应操作(如写TWDR) -> 写TWCR触发下一个动作(同时清除TWINT)。TWEA(TWI Enable Acknowledge Bit): 使能应答位。置1时,本机在作为接收器时会发出ACK信号;清0则发出NACK信号。这在主机读取最后一个字节时非常有用。TWSTA(TWI START Condition Bit): 起始条件位。置1将尝试发起一个START或Repeated START条件。TWSTO(TWI STOP Condition Bit): 停止条件位。置1将在总线上产生一个STOP条件。注意:TWSTO在操作完成后由硬件自动清除,而TWSTA需要软件清除。TWWC(TWI Write Collision Flag): 写冲突标志。在TWINT为低时写入TWDR会被置位,提示写入无效。TWEN(TWI Enable Bit): TWI使能位。必须置1才能启用TWI硬件模块。TWIE(TWI Interrupt Enable Bit): TWI中断使能位。如果使用中断模式,需要将此位置1。
关键理解:
TWINT标志是理解AVR TWI编程的钥匙。它并非由中断控制器设置,而是TWI硬件本身在完成一个总线操作周期后设置的。即使你不使用全局中断,也需要以查询的方式检查这个位。清除它的方法不是直接写0,而是通过向TWCR写入一个包含TWINT=1、TWEN=1以及其他控制位(如TWSTA)的特定值来实现。这个操作同时清除了标志并启动了下一个总线操作。
3. 主机模式寄存器操作流程详解
让我们以一个具体的任务为例:主机需要向一个地址为0x50的EEPROM(写模式地址0xA0)的0x0100位置写入一个字节数据0x55,然后读回验证。我们将拆解每一步的寄存器操作。
3.1 初始化与启动传输
首先,初始化TWI为主机模式,设置合适的速率。假设CPU主频为16MHz,目标SCL为100kHz,预分频设为1。
// 设置预分频为1 (TWSR的低两位) TWSR = 0x00; // TWPS1=0, TWPS0=0 // 计算并设置TWBR。公式: TWBR = ((F_CPU / SCL) - 16) / (2 * 预分频) // ((16000000 / 100000) - 16) / 2 = (160 - 16) / 2 = 72 TWBR = 72; // 使能TWI模块 TWCR = (1 << TWEN);发起一次传输,总是以发送START条件开始。
// 发送START条件 TWCR = (1 << TWINT) | (1 << TWSTA) | (1 << TWEN); // 等待START条件发送完毕(TWINT置位) while (!(TWCR & (1 << TWINT))); // 检查状态寄存器,确认START已成功发送 // 成功发送START后,TWSR状态码应为0x08 (START condition transmitted) if ((TWSR & 0xF8) != 0x08) { // 错误处理:可能是总线被占用等 handleTWIError(); return; }这里,TWCR = (1 << TWINT) | (1 << TWSTA) | (1 << TWEN);这个操作是关键。写入TWINT=1是为了清除之前可能存在的标志并启动操作,TWSTA=1是命令硬件产生START条件,TWEN=1是保持模块使能。执行后,硬件开始操作总线,完成后将TWINT再次置1。
3.2 发送从机地址与读写位
START成功后,接下来发送7位从机地址和读写位。我们要写入EEPROM,所以是写操作(R/W位为0)。地址0x50左移一位后为0xA0。
// 装载SLA+W (0xA0) 到数据寄存器 TWDR = 0xA0; // 0x50 << 1 | 0 // 触发发送动作,清除TWINT标志以启动地址帧的发送 TWCR = (1 << TWINT) | (1 << TWEN); while (!(TWCR & (1 << TWINT))); // 检查状态:成功发送SLA+W并收到ACK,状态码应为0x18 if ((TWSR & 0xF8) != 0x18) { // 错误处理:从机无应答(可能地址错误、设备未就绪) handleTWIError(); return; }状态码0x18 (MT_SLA_ACK) 表示主机已成功发送“从机地址+写”帧,并且从机回复了应答(ACK)。这是通信建立的关键标志。
3.3 发送内存地址(16位)
许多存储器件需要先发送要访问的内部地址。对于16位地址的EEPROM,我们需要发送两个地址字节,高位在前。
// 发送内存地址高字节 (0x01) TWDR = 0x01; TWCR = (1 << TWINT) | (1 << TWEN); while (!(TWCR & (1 << TWINT))); // 状态应为0x28 (MT_DATA_ACK): 数据字节已发送,收到ACK if ((TWSR & 0xF8) != 0x28) { handleTWIError(); return; } // 发送内存地址低字节 (0x00) TWDR = 0x00; TWCR = (1 << TWINT) | (1 << TWEN); while (!(TWCR & (1 << TWINT))); if ((TWSR & 0xF8) != 0x28) { // 同样是0x28状态 handleTWIError(); return; }3.4 发送数据与停止条件
现在发送要写入的数据字节。
// 发送数据字节 (0x55) TWDR = 0x55; TWCR = (1 << TWINT) | (1 << TWEN); while (!(TWCR & (1 << TWINT))); if ((TWSR & 0xF8) != 0x28) { handleTWIError(); return; }数据发送成功后,我们需要产生一个STOP条件来结束本次写入周期。对于EEPROM,STOP信号是触发内部写周期的关键。
// 发送STOP条件 TWCR = (1 << TWINT) | (1 << TWSTO) | (1 << TWEN); // 注意:此处不需要等待TWINT置位!TWSTO操作完成后硬件自动清除该位。 // 但必须等待一小段时间,确保STOP条件在总线上完成。一个简单的延时即可。 _delay_us(10); // 简短延时至此,一个完整的TWI主机写操作流程完成。读操作类似,但需要在发送地址和内存地址后,发送一个Repeated START条件,然后发送SLA+R(读地址),并切换为主机接收模式。
4. 客户端(从机)模式寄存器配置要点
当你的AVR需要作为从机被访问时(例如,作为另一个主机的传感器数据接口),配置重心就转向了TWAR和中断处理。
4.1 从机地址配置与使能
假设我们希望AVR响应地址0x42。
// 设置自身7位从机地址为0x42,并禁用广播呼叫 TWAR = (0x42 << 1); // TWAR[7:1] = 地址, TWAR[0] (TWGCE) = 0 // 使能TWI模块,并使能TWI中断及应答 TWCR = (1 << TWEN) | (1 << TWIE) | (1 << TWEA);TWEA=1使得从机在接收到自己的地址或数据后(如果被寻址)会自动发出ACK。TWIE=1允许TWI中断,这样当总线上有针对本机的活动时,会触发TWI中断服务程序(ISR)。
4.2 从机中断服务程序框架
在TWI的ISR中,你需要读取TWSR来判断具体发生了什么事件。
ISR(TWI_vect) { uint8_t status = TWSR & 0xF8; // 屏蔽预分频位 switch(status) { case 0x60: // 0x60: SLA+W received, ACK returned (自身被主机寻址,写模式) // 主机将要发送数据过来。准备接收。 // 确保TWEA保持为1,以便接收数据时继续应答 TWCR |= (1 << TWEA); break; case 0x80: // 0x80: Data received, ACK returned (在从机接收模式下收到数据字节) g_rx_data = TWDR; // 从TWDR读取收到的数据 // 根据你的协议处理g_rx_data... // 继续使能应答,准备接收下一个字节 TWCR |= (1 << TWEA); break; case 0xA8: // 0xA8: SLA+R received, ACK returned (自身被主机寻址,读模式) // 主机请求数据。将要发送的数据装入TWDR。 TWDR = g_tx_data; // g_tx_data是你要发送的数据 // 使能应答,并启动发送 TWCR |= (1 << TWEA); break; case 0xB8: // 0xB8: Data transmitted, ACK received (在从机发送模式下,数据已发送并收到ACK) // 主机收到了上一个字节并应答。准备下一个要发送的数据(如果有)。 g_tx_data = prepare_next_byte(); TWDR = g_tx_data; TWCR |= (1 << TWEA); break; case 0xC0: // 0xC0: Data transmitted, NACK received (数据已发送,但收到NACK) case 0xC8: // 0xC8: Last data transmitted, ACK received (最后字节已发送,收到ACK) // 传输结束或主机停止读取。可以做一些清理工作。 // 通常重新使能应答,等待下一次被寻址。 TWCR |= (1 << TWEA); break; default: // 其他状态或错误状态处理 // 例如,收到STOP条件(0xA0)等 // 一种常见的错误恢复是发送一个STOP条件(如果本机可作为主机)或重新初始化 TWCR = (1 << TWINT) | (1 << TWEN) | (1 << TWEA); // 重置状态,重新使能 break; } // 关键一步:清除TWINT标志,释放TWI硬件以响应下一个总线事件。 // 通过写TWCR(其中TWINT位为1)来清除它。这里我们保持其他控制位不变。 TWCR |= (1 << TWINT); }这个ISR框架展示了从机如何响应主机的读写请求。核心是根据状态码分支处理,并在最后清除TWINT标志。
5. CRC校验原理与在内存扫描中的应用
5.1 CRC校验的核心价值与算法选择
CRC校验是一种强大的检错技术,常用于检测数据传输或存储过程中产生的错误。在通过TWI进行远程内存扫描的场景下,其价值尤为突出:我们不需要将整片内存数据全部读回本地比对,只需主机计算一个初始CRC值,然后命令从机基于其本地内存数据计算CRC并返回结果,双方比对即可。这极大地节省了总线带宽和验证时间。
CRC算法有很多变体(CRC-8, CRC-16-CCITT, CRC-32等)。选择时需权衡检错能力、计算复杂度和结果长度。对于单片机内存校验(通常几KB到几十KB),CRC-16是一个很好的平衡点。例如,CRC-16-CCITT(多项式0x1021,初始值0xFFFF)被广泛用于通信协议中,其硬件实现简单,软件查表法也很快。
5.2 软件CRC计算与校验流程
假设我们选择CRC-16-CCITT。一个高效的软件实现是查表法。
// 生成CRC-16/CCITT-FALSE的查找表 (多项式0x1021,初始值0xFFFF) const uint16_t crc16_table[256] = { 0x0000, 0x1021, 0x2042, 0x3063, // ... 此处省略256个表项 }; uint16_t calculate_crc16(const uint8_t *data, uint16_t length) { uint16_t crc = 0xFFFF; // 初始值 while (length--) { crc = (crc << 8) ^ crc16_table[((crc >> 8) ^ *data) & 0xFF]; data++; } return crc; }基于TWI的CRC内存扫描流程如下:
- 主机端计算期望CRC:主机已知(或从其他途径获得)从机内存中的原始数据(例如,固件镜像)。主机使用相同的CRC算法,预先计算这段内存数据的CRC值,作为
expected_crc。 - 主机发起扫描命令:主机通过TWI向从机发送一个自定义命令帧,该帧包含:操作码(如
CMD_CRC_SCAN)、起始地址、数据长度。这需要你定义一套简单的应用层协议。 - 从机计算实际CRC:从机收到命令后,在其本地内存(可能是Flash或EEPROM)中,从指定地址开始,读取指定长度的数据,并用相同的CRC算法进行计算,得到
calculated_crc。 - 从机返回结果:从机将计算得到的
calculated_crc(两个字节)通过TWI返回给主机。 - 主机比对与判断:主机比较收到的
calculated_crc与自己预先计算的expected_crc。如果一致,则认为内存数据完整;否则,认为数据存在错误。
5.3 整合TWI与CRC的扫描协议设计
一个简单的协议帧设计示例:
- 主机发送(写操作):
- 字节1: 从机地址 + W
- 字节2: 命令字节 (例如 0xC3 代表CRC扫描)
- 字节3: 起始地址高字节
- 字节4: 起始地址低字节
- 字节5: 数据长度高字节
- 字节6: 数据长度低字节
- STOP条件
- 主机接收(读操作):
- Repeated START
- 字节1: 从机地址 + R
- 字节2: 从机返回的CRC值高字节 (主机读取,发送ACK)
- 字节3: 从机返回的CRC值低字节 (主机读取,发送NACK,表示这是最后一个字节)
- STOP条件
从机端的命令解析和CRC计算需要在TWI从机中断服务程序中实现,根据收到的命令字节跳转到不同的处理函数。
6. 实战:构建完整的TWI CRC内存扫描机制
6.1 系统架构与模块划分
我们将系统分为三层:
- TWI底层驱动层:提供
twi_master_init(),twi_master_start(),twi_master_write_byte(),twi_master_read_byte_ack(),twi_master_read_byte_nack(),twi_master_stop()等基本函数。这些函数封装了第3章所述的寄存器操作,并包含基本的状态检查。 - CRC算法层:提供
crc16_update()或crc16_calculate()函数,供主机和从机调用。 - 应用协议层:
- 主机端:提供
memory_crc_scan(uint8_t slave_addr, uint16_t start_addr, uint16_t length)函数,内部组合TWI命令发送、数据接收和CRC比对逻辑。 - 从机端:在TWI ISR中扩展命令处理分支。当收到
CMD_CRC_SCAN时,从指定内存区域读取数据,调用CRC算法计算,并将结果存入发送缓冲区,等待主机读取。
- 主机端:提供
6.2 主机端扫描函数实现示例
uint8_t memory_crc_scan(uint8_t slave_addr, uint16_t start_addr, uint16_t length, uint16_t expected_crc) { uint8_t twi_status; uint16_t received_crc; // 1. 发送START twi_status = twi_master_start(); if (twi_status != TW_START) return TWI_ERROR_START; // 2. 发送从机地址+W twi_status = twi_master_write_byte((slave_addr << 1) | TW_WRITE); if (twi_status != TW_MT_SLA_ACK) return TWI_ERROR_MT_SLA_ACK; // 3. 发送命令字节 twi_status = twi_master_write_byte(CMD_CRC_SCAN); if (twi_status != TW_MT_DATA_ACK) return TWI_ERROR_MT_DATA_ACK; // 4. 发送起始地址(16位,高位在前) twi_status = twi_master_write_byte((uint8_t)(start_addr >> 8)); if (twi_status != TW_MT_DATA_ACK) return TWI_ERROR_MT_DATA_ACK; twi_status = twi_master_write_byte((uint8_t)(start_addr & 0xFF)); if (twi_status != TW_MT_DATA_ACK) return TWI_ERROR_MT_DATA_ACK; // 5. 发送数据长度(16位,高位在前) twi_status = twi_master_write_byte((uint8_t)(length >> 8)); if (twi_status != TW_MT_DATA_ACK) return TWI_ERROR_MT_DATA_ACK; twi_status = twi_master_write_byte((uint8_t)(length & 0xFF)); if (twi_status != TW_MT_DATA_ACK) return TWI_ERROR_MT_DATA_ACK; // 6. 发送Repeated START,准备读取结果 twi_status = twi_master_repeated_start(); if (twi_status != TW_REP_START) return TWI_ERROR_REP_START; // 7. 发送从机地址+R twi_status = twi_master_write_byte((slave_addr << 1) | TW_READ); if (twi_status != TW_MR_SLA_ACK) return TWI_ERROR_MR_SLA_ACK; // 8. 读取CRC高字节(发送ACK) received_crc = twi_master_read_byte_ack() << 8; // 9. 读取CRC低字节(发送NACK,结束读取) received_crc |= twi_master_read_byte_nack(); // 10. 发送STOP条件 twi_master_stop(); // 11. 比对CRC if (received_crc == expected_crc) { return TWI_SCAN_SUCCESS; } else { return TWI_SCAN_CRC_MISMATCH; } }6.3 从机端命令处理增强
在从机的TWI ISR中,需要增加对CMD_CRC_SCAN的处理。
// 全局变量,用于在ISR和主程序间传递命令参数(需考虑volatile和临界区保护) volatile uint16_t crc_scan_addr; volatile uint16_t crc_scan_len; volatile uint8_t crc_scan_state = 0; // 0:空闲,1:等待地址高字节,2:等待地址低字节... ISR(TWI_vect) { uint8_t status = TWSR & 0xF8; static uint8_t cmd; // 存储接收到的命令 switch(status) { case 0x60: // SLA+W received // 复位参数接收状态 crc_scan_state = 0; TWCR |= (1 << TWEA); // 使能ACK,准备接收数据 break; case 0x80: // Data received in Slave Rx mode uint8_t rx_data = TWDR; switch(crc_scan_state) { case 0: cmd = rx_data; // 第一个数据字节是命令 if (cmd == CMD_CRC_SCAN) { crc_scan_state = 1; // 下一个字节是地址高字节 } else { // 其他命令处理... } break; case 1: crc_scan_addr = (uint16_t)rx_data << 8; crc_scan_state = 2; break; case 2: crc_scan_addr |= rx_data; crc_scan_state = 3; break; case 3: crc_scan_len = (uint16_t)rx_data << 8; crc_scan_state = 4; break; case 4: crc_scan_len |= rx_data; // 所有参数接收完毕!可以触发一个标志,让主循环去执行耗时的CRC计算。 // 注意:ISR中不宜进行大量计算或内存访问。 crc_scan_complete_flag = 1; crc_scan_state = 0; break; default: break; } TWCR |= (1 << TWEA); // 继续ACK,接收下一个参数字节 break; case 0xA8: // SLA+R received (主机要读数据了) if (cmd == CMD_CRC_SCAN && crc_scan_complete_flag) { // 假设主循环已经计算好crc_result TWDR = (uint8_t)(crc_result >> 8); // 发送CRC高字节 } else { TWDR = 0xFF; // 或发送默认/错误值 } TWCR |= (1 << TWEA); // 使能ACK,启动发送 break; case 0xB8: // Data transmitted, ACK received if (cmd == CMD_CRC_SCAN) { // 发送CRC低字节 TWDR = (uint8_t)(crc_result & 0xFF); TWCR |= (1 << TWEA); // 发送完后,可以清除命令标志 cmd = 0xFF; } break; // ... 其他状态处理 } TWCR |= (1 << TWINT); // 清除中断标志 }在主循环中,检查crc_scan_complete_flag,如果置位,则执行CRC计算,将结果存入crc_result,并清除标志。这样将耗时的计算从ISR中剥离,保证了中断响应速度。
7. 调试技巧、常见问题与性能优化
7.1 调试技巧与问题排查
- 状态码是生命线:任何TWI操作后,都必须检查
TWSR & 0xF8的状态码。准备一个状态码对照表,遇到非预期状态立刻进入错误处理。最常见的错误状态是0x38(仲裁丢失)和0x00(总线错误,可能由于START/STOP条件不完整)。 - 逻辑分析仪是神器:如果通信异常,一个支持I²C解码的逻辑分析仪(如Saleae)能直观地显示SDA和SCL线上的每一位、每一个START/STOP、地址和数据,帮你快速定位是主机问题还是从机问题,是ACK缺失还是时序不对。
- 上拉电阻不能省:TWI总线是开漏输出,必须接上拉电阻(通常4.7kΩ到10kΩ)。不接或阻值太大会导致信号上升沿缓慢,在高波特率下容易出错。
- 从机无应答(NACK):如果主机发送地址后收到NACK(状态码
0x20或0x48),检查:- 从机地址是否正确(7位地址 vs 8位地址+读写位)。
- 从机设备是否上电、初始化完成。
- 总线连接是否正常。
- 从机是否处于忙状态(如EEPROM正在内部写入)。
- CRC校验失败:如果CRC比对失败,按以下步骤排查:
- 算法一致性:确保主机和从机使用完全相同的CRC算法(多项式、初始值、输入输出是否反转)。
- 数据范围:确保双方计算CRC的内存起始地址和长度完全一致。
- 数据传输错误:CRC本身能检错,但需排除是TWI传输过程中单字节出错导致的计算输入错误。可以尝试先进行简单的数据回读测试,验证TWI通信本身的可靠性。
- 从机内存访问:确保从机在计算CRC时,访问的是正确的物理内存区域,且该区域在计算期间内容没有变化(如被中断修改)。
7.2 性能优化考量
- 中断 vs 轮询:对于主机,简单的单次读写用轮询
while(!(TWCR & (1<<TWINT)))足够。但对于从机,或者主机需要处理复杂、多步骤的协议时,使用中断可以解放CPU。注意:TWI中断向量只有一个,需要在ISR中根据状态码处理所有主机/从机、发送/接收事件。 - 时钟速率优化:在总线电容允许的情况下,适当提高SCL频率可以显著提升吞吐量。计算公式见2.2节。注意从机设备支持的最高速率。
- CRC计算优化:对于需要快速扫描的大内存,软件查表法(LUT)比直接计算法快一个数量级。如果AVR支持硬件CRC外设(部分新型号有),应优先使用硬件CRC,速度极快且不占用CPU。
- 协议优化:对于内存扫描,可以设计更复杂的协议,例如支持分块CRC(将大内存分成多个块,分别计算和校验,便于定位错误块),或者支持在从机端缓存CRC结果,主机多次查询。
7.3 一个真实的“坑”:TWINT标志的清除
这是我早期调试时踩过的一个大坑。我最初错误地认为直接写TWCR &= ~(1<<TWINT);可以清除中断标志。实际上,AVR的TWI模块设计是:向TWINT位写1来清除它。更准确地说,是向TWCR寄存器写入一个值,其中TWINT位为1,TWEN位为1,以及其他需要的控制位(如TWSTA)。这个写入操作本身会清零TWINT标志(硬件检测到从0到1的跳变),并同时根据其他位的设置启动下一个TWI操作。所以,TWCR = (1 << TWINT) | (1 << TWEN) | (1 << TWSTA);这行代码,既清除了之前的TWINT,又发出了一个新的START条件。理解这个“写1清0”的机制,是避免TWI程序卡死的关键。
通过将TWI寄存器操作与CRC校验机制深度结合,我们构建了一个强大、可靠的远程内存完整性验证工具。这套方法不仅适用于AVR,其思想也可以迁移到其他具有硬件I²C模块的MCU上。关键在于吃透状态机,严谨地处理每一个状态,并设计好容错和恢复机制。当你能够熟练地在寄存器层面驾驭TWI时,你会发现面对各种复杂的I²C设备时,底气都会足很多。
