LPC21xx/22xx I2C从机发送模式状态机编程实战指南
1. 项目概述:深入LPC21xx/22xx的I2C从机发送模式
在嵌入式开发中,I2C总线因其简洁的两线制(SDA数据线和SCL时钟线)和灵活的多主多从架构,成为了连接微控制器与各类传感器、存储器、IO扩展芯片的首选协议之一。然而,对于许多开发者而言,I2C从机设备的编程,尤其是作为发送方(Slave Transmitter)的角色,往往比主机编程更具挑战性。这并非因为协议本身复杂,而是因为从机需要被动响应主机的请求,其行为完全由主机发起的时序和状态驱动,这就要求开发者必须深入理解硬件状态机的每一个跳转逻辑。
NXP的LPC21xx和LPC22xx系列ARM7微控制器,以其经典和稳定的I2C硬件接口而闻名。其I2C模块实现了一个完整的状态机,通过状态寄存器I2STAT的26个有效状态码,精确地反映了总线上的每一个关键时刻。本文将聚焦于其中最核心但也最容易出错的从机发送模式(Slave Transmitter Mode),结合官方手册UM10114的原始资料,为你拆解其工作原理、状态机流程,并提供一个可直接“抄作业”的、基于状态服务例程的编程实战指南。无论你是正在调试一个I2C接口的EEPROM模拟器,还是为一个自定义传感器编写固件,理解并掌握这套状态机编程模型,都将是你摆脱调试困境、实现稳定通信的关键。
2. I2C从机发送模式的核心机制与状态机解析
要驾驭LPC21xx/22xx的I2C从机发送,绝不能把它当作一个简单的“发送数据”函数来对待。你必须建立起一个清晰的认知:你的代码是一个事件驱动的状态处理器,而硬件I2STAT寄存器就是那个告诉你“现在发生了什么事件”的信使。
2.1 从机发送模式的基本流程
在从机发送模式下,你的设备(从机)需要等待主机(Master)的“点名”来发送数据。整个流程可以类比为一次课堂提问:老师(主机)喊出某个学生的学号(从机地址)并说“请回答”(R/W位为1,表示读),被点名的学生(从机)才能起立发言(发送数据)。
具体到总线时序上,过程如下:
- 等待寻址:从机初始化
I2ADR(自身地址寄存器)和I2CON(控制寄存器)后,便持续监听总线。 - 地址匹配:主机发送起始条件(S),紧接着发送7位从机地址和1位方向位。当方向位为‘1’(读)时,硬件识别到自己的地址,便会自动回复一个ACK(应答),并立即将串行中断标志
SI置位,同时在I2STAT寄存器中存入一个特定的状态码(对于从机发送模式,第一个状态码通常是0xA8)。 - 进入中断服务:CPU响应I2C中断,你的代码需要读取
I2STAT的值,并跳转到对应的状态服务例程。 - 状态驱动数据发送:在
0xA8状态,你需要将第一个要发送的数据字节写入I2DAT数据寄存器,并清除SI标志以释放总线。硬件会自动将这个字节发送出去。 - 持续应答与发送:主机在接收到每个字节后,会回复一个ACK。每发送完一个字节并收到ACK,硬件都会再次产生中断,状态变为
0xB8。在此状态下,你需要判断是否还有后续数据:如果有,则装载下一个字节并清除SI;如果这是最后一个字节,你需要在清除SI前,将I2CON中的AA(断言应答)位清零。 - 结束传输:当主机收到最后一个字节后,可以选择发送NACK(非应答)或直接发送停止条件(P)。如果主机发送NACK,从机会进入
0xC0状态;如果主机直接发送停止条件,从机会进入0xA0状态。在这些状态下,从机通常需要重新置位AA位,准备下一次被寻址。
2.2 关键寄存器与位域详解
理解状态机的前提是吃透几个关键寄存器:
I2CON (I2C Control Register) - 控制寄存器
I2EN(Bit 6): I2C接口使能位。必须置1才能操作I2C模块。STA(Bit 5): 起始标志位。在主机模式下,置1以产生起始条件;在从机模式下,用于在仲裁丢失后尝试重发起始条件。STO(Bit 4): 停止标志位。在主机模式下,置1以产生停止条件;在从机模式下无效;在总线错误(0x00状态)时,置1用于恢复总线。SI(Bit 3): 串行中断标志位。当I2C状态改变(如地址匹配、数据收发完成、仲裁丢失等)时由硬件置1。软件必须通过向I2CONCLR寄存器的bit3写1来清除它,这是状态机向前推进的关键操作。AA(Bit 2): 断言应答位。这是从机模式下的灵魂位。AA=1: 从机将在地址匹配或成功接收数据字节后,在下一次ACK周期内输出低电平(发送ACK)。AA=0: 从机将在地址匹配或成功接收数据字节后,在下一次ACK周期内释放SDA线(输出高电平,即发送NACK)。在从机发送模式下,当你要发送最后一个数据字节时,必须在装载该字节到I2DAT后、清除SI前,将AA清零。这样,主机在收到最后一个字节后回复的ACK(实际上从机不会应答这个ACK)会触发状态0xC8,或者主机直接回复NACK触发状态0xC0,从而正确结束发送。
I2STAT (I2C Status Register) - 状态寄存器这是一个只读寄存器,包含了最近一次I2C中断时的状态代码。其高5位是状态码,低3位无用。手册中定义的26个有效状态码(
0x08,0x10,0x18,0x20,0x28,0x30,0x38,0x40,0x48,0x50,0x58,0x60,0x68,0x70,0x78,0x80,0x88,0x90,0x98,0xA0,0xA8,0xB0,0xB8,0xC0,0xC8)就是你的代码进行分支判断的唯一依据。此外,还有两个特殊代码0xF8(无状态信息,SI=0)和0x00(总线错误)。I2DAT (I2C Data Register) - 数据寄存器可读可写。当要发送数据时,将数据写入此寄存器;当接收到数据时,从此寄存器读取。重要原则:只有在状态服务例程明确指示“Load data byte”时,才能向
I2DAT写入数据;同样,只有在指示“Read data byte”时,才从中读取数据。胡乱读写I2DAT是导致通信混乱的常见原因。
2.3 从机发送模式状态转移图精讲
手册中的状态表(Table 189)是权威指南,但将其转化为状态转移图更容易理解。以下是核心状态的跳转逻辑:
- 起始状态
0xA8: “Own SLA+R has been received; ACK has been returned.” 这是从机发送模式的入口。你的代码必须在此状态将第一个数据字节写入I2DAT,然后清除SI。同时,你需要根据本次传输要发送的字节总数,决定是否保持AA=1(还有更多数据)或清零AA(这是最后一个字节)。通常首次进入时AA保持为1。 - 数据已发送状态
0xB8: “Data byte in I2DAT has been transmitted; ACK has been received.” 这是最常进入的状态,表示上一个字节发送成功且主机回复了ACK。此时你需要:- 如果还有数据要发送 (
AA=1),则将下一个字节写入I2DAT,然后清除SI。 - 如果刚才发送的是最后一个字节(即你在上一个状态或更早之前已将
AA清零),那么主机回复的这个ACK会导致硬件进入0xC8状态,而不是0xB8。所以,在0xB8状态,你通常总是装载新数据。
- 如果还有数据要发送 (
- 最后一个字节发送后状态
0xC8: “Last data byte in I2DAT has been transmitted (AA = 0); ACK has been received.” 这个状态仅在AA=0时发送最后一个字节后才会出现。此时数据已发送完毕,主机回复了ACK。你的操作通常是:不操作I2DAT,重新置位AA(为下一次寻址做准备),然后清除SI。从机将切换回“未寻址”模式。 - 非应答状态
0xC0: “Data byte in I2DAT has been transmitted; NOT ACK has been received.” 这表示主机在收到数据字节后回复了NACK,通常意味着主机希望终止接收。你的操作与0xC8类似:不操作I2DAT,重新置位AA,清除SI。 - 停止或重复起始条件状态
0xA0: “A STOP condition or repeated START condition has been received while still addressed as SLV/REC or SLV/TRX.” 如果主机在传输过程中直接发送了停止条件(P)或重复起始条件(S),则会进入此状态。这也标志着本次传输结束,你需要重新置位AA,清除SI。 - 仲裁丢失状态
0xB0: “Arbitration lost in SLA+R/W as master; Own SLA+R has been received, ACK has been returned.” 这是一个特殊场景:你的设备原本处于主机模式并参与总线仲裁,但仲裁失败,同时发现自己被另一个主机寻址为从机发送者。此时的处理与0xA8状态完全相同——装载第一个数据字节。
关键心得:
AA位的管理是从机发送编程的重中之重。一个经典的策略是维护一个发送数据计数器。在0xA8或0xB8状态,发送一个字节后计数器减1。当计数器减为0(即下一个要发送的是最后一个字节)时,在装载该最后一个字节到I2DAT后,紧接着将AA位清零,然后再去清除SI标志。这个顺序不能错。
3. 状态服务例程的实战编程与代码实现
理解了原理,我们来看如何用代码实现。以下将以一个具体的例子展开:假设我们的LPC21xx从机设备地址为0x50,需要实现一个简单的“读版本号”功能。当主机读取时,从机发送两个字节:0x01和0x23。
3.1 初始化流程详解
初始化不仅仅是配置寄存器,更是为整个状态机运行搭建舞台。
// 定义I2C相关寄存器地址(以LPC2103为例,具体请查阅数据手册) #define I2CONSET (*((volatile unsigned char *)0xE001C000)) #define I2CONCLR (*((volatile unsigned char *)0xE001C018)) #define I2STAT (*((volatile unsigned char *)0xE001C004)) #define I2DAT (*((volatile unsigned char *)0xE001C008)) #define I2ADR (*((volatile unsigned char *)0xE001C00C)) #define I2SCLH (*((volatile unsigned short *)0xE001C010)) // SCL高电平周期 #define I2SCLL (*((volatile unsigned short *)0xE001C014)) // SCL低电平周期 // 定义控制位 #define I2EN_BIT (1 << 6) #define STA_BIT (1 << 5) #define STO_BIT (1 << 4) #define SI_BIT (1 << 3) #define AA_BIT (1 << 2) // 应用变量 unsigned char i2c_tx_buffer[2] = {0x01, 0x23}; // 待发送数据 unsigned char i2c_tx_index = 0; // 发送缓冲区索引 unsigned char i2c_tx_count = 2; // 待发送字节总数 void I2C_Slave_Init(void) { // 1. 设置自身从机地址。bit0是GC(General Call)位,0表示忽略全局呼叫。 // 假设我们的7位地址是0x50,写入I2ADR时需要左移一位,即0xA0。 I2ADR = 0xA0; // 0x50 << 1 // 2. 设置I2C时钟频率(主模式时有效,从模式忽略,但建议设置) // 假设PCLK = 15MHz,目标I2C速率=100kHz。 // I2SCLH = I2SCLL = (PCLK / (2 * I2C_CLK)) - 1 // 计算: (15,000,000 / (2 * 100,000)) - 1 = 75 - 1 = 74 I2SCLH = 74; I2SCLL = 74; // 3. 使能I2C中断(假设已配置好VIC向量中断控制器) // VICIntEnable |= (1 << 9); // 使能I2C中断,中断号需查手册 // 4. 关键步骤:使能I2C模块并置位AA位,进入被寻址的从机模式 // 向I2CONSET写入I2EN_BIT | AA_BIT,即0x44。 I2CONSET = I2EN_BIT | AA_BIT; // 注意:这里只SET,不清除任何位。确保SI标志初始为0。 // 初始化应用变量 i2c_tx_index = 0; i2c_tx_count = sizeof(i2c_tx_buffer); }初始化要点解析:
- 地址设置:
I2ADR存储的是8位值,其格式是[7:1]为7位从机地址,[0]位为GC(全局呼叫)使能位。因此7位地址0x50需要左移一位变成0xA0再写入。 - 时钟设置:
I2SCLH和I2SCLL决定了主机模式下的SCL时钟频率。虽然在纯从机模式下,时钟由主机提供,这两个寄存器不影响从机时序,但良好的编程习惯是将其设置为一个合理的值,以防万一设备需要切换到主机模式。 - 使能顺序:先配置地址和时钟,最后再使能模块(
I2EN)和应答(AA)。一旦AA置位,硬件立即开始监听总线。确保其他配置在此之前已完成。
3.2 I2C中断服务例程(ISR)框架
中断服务例程是状态机处理的核心。它需要快速读取状态码,并跳转到对应的处理函数。
void I2C_IRQHandler(void) __irq { unsigned char status; // 1. 读取当前状态码。I2STAT的高5位是状态,低3位保留。 status = I2STAT & 0xF8; // 屏蔽低3位,获取标准状态码 // 2. 根据状态码分支处理 switch (status) { // ------- 从机发送模式相关状态 ------- case 0xA8: // 自身从机地址+读已被接收,ACK已回复 i2c_state_A8_handler(); break; case 0xB0: // 仲裁丢失后,自身从机地址+读已被接收,ACK已回复 i2c_state_B0_handler(); break; case 0xB8: // 数据字节已发送,ACK已收到 i2c_state_B8_handler(); break; case 0xC0: // 数据字节已发送,NACK已收到 case 0xC8: // 最后一个数据字节已发送(AA=0),ACK已收到 i2c_state_C0_C8_handler(status); break; case 0xA0: // 接收到停止或重复起始条件 i2c_state_A0_handler(); break; // ------- 其他可能用到的状态(示例) ------- case 0x60: // 自身从机地址+写已被接收,进入从机接收模式 i2c_state_60_handler(); break; case 0x00: // 总线错误 i2c_state_00_handler(); break; case 0xF8: // 无状态信息,SI=0,通常直接退出 default: // 意外状态,进行错误恢复,例如强制产生停止条件并复位状态 I2CONSET = STO_BIT; I2CONCLR = SI_BIT; // 重新使能从机模式 I2CONCLR = AA_BIT | I2EN_BIT; // 先清除 I2CONSET = AA_BIT | I2EN_BIT; // 再置位 break; } // 3. 清除VIC中的中断标志(根据具体MCU的VIC操作) // VICVectAddr = 0x00; // 写0到向量地址寄存器以清除中断 }3.3 从机发送模式核心状态处理函数实现
现在,我们实现上述switch-case中调用的几个核心处理函数。
// 状态 0xA8 处理函数:发送第一个数据字节 void i2c_state_A8_handler(void) { // 这是发送过程的起点。检查是否有数据要发送。 if (i2c_tx_count > 0) { // 1. 装载第一个数据字节到I2DAT I2DAT = i2c_tx_buffer[i2c_tx_index++]; i2c_tx_count--; // 2. 判断是否为最后一个字节 if (i2c_tx_count == 0) { // 这是最后一个字节,发送完后应让主机结束传输。 // 在装载数据后,清除AA位,这样主机回复ACK后我们会进入0xC8状态。 I2CONCLR = AA_BIT; } else { // 还有更多字节要发送,保持AA=1,主机回复ACK后我们会进入0xB8状态。 // AA位在初始化时已置位,此处无需操作。 } // 3. 清除SI标志,让硬件继续发送刚装载的数据 I2CONCLR = SI_BIT; } else { // 缓冲区无数据,这是一个错误情况。可以发送一个填充字节(如0xFF)并结束。 I2DAT = 0xFF; I2CONCLR = AA_BIT; // 作为最后一个字节处理 I2CONCLR = SI_BIT; // 重置发送状态 i2c_tx_index = 0; i2c_tx_count = sizeof(i2c_tx_buffer); } } // 状态 0xB0 处理函数:仲裁丢失后作为从机被寻址 // 处理方式与0xA8完全一致 void i2c_state_B0_handler(void) { i2c_state_A8_handler(); // 直接调用相同的处理函数 } // 状态 0xB8 处理函数:发送后续数据字节 void i2c_state_B8_handler(void) { // 上一个字节发送成功,主机回复了ACK。 if (i2c_tx_count > 0) { // 1. 装载下一个数据字节 I2DAT = i2c_tx_buffer[i2c_tx_index++]; i2c_tx_count--; // 2. 判断是否为最后一个字节 if (i2c_tx_count == 0) { // 这是最后一个字节,发送完后应让主机结束传输。 I2CONCLR = AA_BIT; } // 如果还有更多字节,AA位保持为1(默认),无需操作。 // 3. 清除SI标志 I2CONCLR = SI_BIT; } else { // 理论上不应该进入这里,因为发送最后一个字节前AA位已清零,会进入0xC8。 // 但为安全起见,处理异常:发送填充字节并结束。 I2DAT = 0xFF; I2CONCLR = AA_BIT; I2CONCLR = SI_BIT; // 重置 i2c_tx_index = 0; i2c_tx_count = sizeof(i2c_tx_buffer); } } // 状态 0xC0 和 0xC8 处理函数:发送结束处理 void i2c_state_C0_C8_handler(unsigned char status) { // 状态0xC0: 主机回复NACK,要求停止。 // 状态0xC8: 最后一个字节发送完毕,主机回复ACK。 // 两者的处理通常是一样的:重置状态,准备下一次传输。 // 1. 对于0xC8状态,数据已从I2DAT发出,无需再操作I2DAT。 // 对于0xC0状态,同样无需操作I2DAT。 // 2. 重新使能AA位,以便能再次响应自身的从机地址。 // 注意:必须先SET AA位,再清除SI。顺序很重要! I2CONSET = AA_BIT; // 3. 清除SI标志 I2CONCLR = SI_BIT; // 4. 重置应用层的发送指针和计数器,为下一次传输做准备。 i2c_tx_index = 0; i2c_tx_count = sizeof(i2c_tx_buffer); // (可选) 可以在这里设置一个标志,通知主程序“一次完整的发送已完成”。 // i2c_transfer_complete = 1; } // 状态 0xA0 处理函数:收到停止或重复起始条件 void i2c_state_A0_handler(void) { // 主机主动终止了传输(发送STOP或Repeated START)。 // 我们需要重置状态,准备下一次寻址。 // 1. 无需操作I2DAT。 // 2. 确保AA位被置位,以便继续监听总线。 // 在进入0xA0状态时,AA位可能是1也可能是0,为了保险,我们显式置位。 I2CONSET = AA_BIT; // 3. 清除SI标志 I2CONCLR = SI_BIT; // 4. 重置应用层状态。注意:这可能是一次未完成的传输。 i2c_tx_index = 0; i2c_tx_count = sizeof(i2c_tx_buffer); // 可以记录一个“传输被中止”的标志。 }3.4 关键操作顺序与寄存器访问陷阱
在编写状态服务例程时,对I2CON和I2DAT寄存器的操作顺序是绝对的关键,错误的顺序会导致通信失败或总线锁死。
SI标志的清除时机:SI标志是状态机推进的“钥匙”。只有在按照当前状态的要求,完成了所有必要的设置(如写入I2DAT、设置/清除AA位)之后,才能最后清除SI标志。一旦SI被清除,硬件就会根据你刚刚配置好的I2CON和I2DAT,执行下一个动作(如发送数据、接收数据、产生ACK等)。AA位的设置与清除:- 清除
AA:必须在装载最后一个数据字节到I2DAT之后,且在清除SI标志之前进行。如果提前清除,可能导致主机在收到非最后一个字节时就收到NACK而提前终止传输。如果忘记清除,主机会一直等待更多数据,导致超时。 - 置位
AA:在传输结束状态(0xC0,0xC8,0xA0)或总线错误恢复后,必须在清除SI标志之前重新置位AA,否则从机将无法响应下一次地址呼叫。
- 清除
I2DAT的读写:只有在状态表明确要求“Load data byte”或“Read data byte”时才进行。在不需要操作数据的状态(如0xC0),去读或写I2DAT`可能会干扰硬件。
避坑指南:一个经典的顺序错误在
0xB8状态发送最后一个字节时,错误的顺序是:
I2CONCLR = AA_BIT;// 先清除AAI2DAT = last_byte;// 再装载数据I2CONCLR = SI_BIT;问题:在装载数据前清除AA,硬件可能会在装载数据的瞬间就认为应答已被否定,导致行为异常。正确顺序:I2DAT = last_byte;// 先装载数据I2CONCLR = AA_BIT;// 再清除AAI2CONCLR = SI_BIT;// 最后清除SI
4. 调试技巧、常见问题与高级话题
即使代码逻辑正确,在实际硬件调试中你依然可能会遇到各种问题。以下是一些实战中总结的经验和排查思路。
4.1 硬件连接与基础检查
在怀疑代码之前,先排除硬件问题。
- 上拉电阻:I2C总线是开漏输出,SDA和SCL线必须通过上拉电阻连接到正电源(通常3.3V或5V)。电阻值通常在4.7kΩ到10kΩ之间,具体取决于总线电容和速度。没有上拉电阻,总线永远为低。
- 电源与电平:确保主机和从机共地,并且逻辑电平兼容(例如,都是3.3V)。
- 线路连接:检查SDA和SCL线是否接反、虚焊或短路。用示波器或逻辑分析仪观察波形是最直接的方法。
4.2 使用逻辑分析仪进行状态机调试
逻辑分析仪是调试I2C的终极利器。抓取一次失败的通信波形,对照以下步骤分析:
- 看起始条件:主机是否发出了正确的起始条件(S)?SDA在SCL高电平时是否有一个明显的下降沿?
- 看地址帧:主机发送的7位地址和R/W位是否正确?你的从机地址设置(
I2ADR)是否与之匹配?从机是否回复了ACK(第9个时钟周期SDA为低)? - 看数据帧:如果地址匹配成功,进入从机发送模式。
- 在主机发送完地址(R/W=1)并收到ACK后,主机会释放SDA线,并将时钟控制权交给从机(实际上主机仍在产生SCL,但从机控制SDA)。
- 观察第一个数据字节:是否是从机发出的正确数据(
0x01)?数据位是否在SCL低电平时变化,在高电平时稳定? - 观察第一个数据字节后的ACK:这个ACK是由主机回复的。主机在第9个时钟周期是否将SDA拉低?如果主机回复了NACK(高电平),说明主机不想再要数据了,你的从机代码是否正确处理了
0xC0状态?
- 看状态码:在逻辑分析仪上标记出每个关键时间点(地址ACK后、数据ACK后等),然后在你的代码中打印或通过调试器查看进入中断时的
I2STAT值,两者是否对应?例如,在发送完第一个字节0x01后,主机回复ACK,你的中断里读到的I2STAT应该是0xB8吗?
4.3 常见问题排查表
| 现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 主机根本收不到任何数据,总线超时。 | 1. 从机未正确响应地址。 2. 从机 AA位未置位。3. 从机中断未使能或ISR未清除 SI。 | 1. 用逻辑分析仪确认主机发送的地址是否匹配I2ADR。2. 在初始化代码和传输结束后的状态中,确认 AA位被置为1。3. 检查MCU的I2C中断在VIC中是否使能,ISR是否被正确触发。在 0xA8状态处理函数入口设置断点或点灯。 |
| 主机只能收到第一个数据字节,然后总线挂起。 | 1. 从机在0xB8状态未装载新数据或未清除SI。2. 从机在发送最后一个字节后未正确处理,导致状态机停滞。 | 1. 检查0xB8状态的处理代码,确保i2c_tx_index和i2c_tx_count管理正确,并且执行了I2CONCLR = SI_BIT。2. 检查发送最后一个字节时,是否在清除 SI前清除了AA位?清除AA后应进入0xC8,检查0xC8处理函数是否清除了SI并重置了状态。 |
| 主机收到错误数据或多余数据。 | 1.I2DAT写入时机或数据错误。2. 发送缓冲区管理混乱, i2c_tx_index越界。 | 1. 确保只在0xA8和0xB8状态向I2DAT写入数据。2. 在 i2c_tx_index递增前,确保其值小于缓冲区大小。添加边界检查。 |
| 通信偶尔失败,特别是上电后第一次。 | 1. I2C模块或GPIO引脚初始化顺序问题。 2. 总线电容过大,上升沿太慢,导致时序违规。 | 1. 确保先配置好I2C引脚功能(设置为I2C模式),再使能I2C模块(I2EN)。2. 减小上拉电阻值(如从10kΩ换为4.7kΩ),或降低I2C时钟频率。检查示波器波形,看SDA/SCL的上升时间是否过长。 |
进入0x00(总线错误)状态。 | 1. 总线上出现了非法的起始或停止条件(如毛刺)。 2. 多主冲突时处理不当。 3. 代码中错误操作了 I2CON寄存器。 | 1. 检查硬件连接,排除噪声干扰。确保电源稳定。 2. 在 0x00状态处理函数中,按照手册要求执行:I2CONSET = STO_BIT;然后I2CONCLR = SI_BIT;这将释放总线并复位I2C模块到未寻址从机模式。之后需要重新置位AA和I2EN吗?手册说硬件会处理,但为了保险,可以在处理函数末尾重新初始化从机模式。 |
4.4 处理总线错误(0x00)与总线挂死
总线错误0x00和总线被意外拉低是I2C调试中的噩梦。手册第12.9.13节和12.9.12节给出了解决方案。
- 总线错误恢复(
0x00):当检测到非法起始/停止条件时,硬件自动进入此状态。你的处理代码必须:void i2c_state_00_handler(void) { // 1. 设置STO位,清除SI位。注意:这里不是I2CONSET=STO_BIT,而是操作I2CONCLR和I2CONSET的特定组合。 // 根据手册Table 190,软件响应应为:STA=0, STO=1, SI=0, AA=X。 // 在LPC的库函数或实践中,常用以下操作: I2CONSET = STO_BIT; // 设置STO位 I2CONCLR = SI_BIT; // 清除SI位。硬件会自动清除STO位。 // 2. 总线被释放,I2C模块进入未寻址从机模式。 // 3. (强烈建议) 重新使能从机监听。 I2CONCLR = AA_BIT | I2EN_BIT; // 先完全关闭 I2CONSET = AA_BIT | I2EN_BIT; // 再重新使能 // 4. 重置你的应用状态变量。 i2c_tx_index = 0; i2c_tx_count = sizeof(i2c_tx_buffer); } - SCL线被持续拉低:这是最坏的情况,通常由某个从机故障引起。LPC的I2C硬件无法解决此问题,必须由那个故障设备复位或断电。在设计中,可以考虑为每个I2C从设备增加一个独立的使能GPIO,在检测到总线锁死时,由主机循环通断从设备电源以复位它。
- SDA线被持续拉低:LPC硬件支持一种恢复机制(手册图48)。如果
STA位置位但总线因SDA为低而无法产生起始条件,硬件会自动在SCL上产生额外时钟脉冲,直到SDA被释放。这个过程无需软件干预。你可以利用这个特性实现一个“总线恢复函数”,在超时后尝试发送起始条件。
4.5 从发送模式中的“非典型”场景考量
- 主机发送重复起始条件(Repeated Start):在从机发送过程中,主机可能不发停止条件,而是发一个重复起始条件(S)来开始一次新的传输(比如切换为写模式)。这会触发从机进入
0xA0状态。你的i2c_state_A0_handler()需要妥善处理,重置发送状态,并准备好可能紧接着的从机接收模式(如果新地址的R/W位是0)。 - 动态数据准备:上面的例子使用了静态缓冲区。在实际应用中,数据可能需要实时生成。你可以在
0xA8或0xB8状态中,根据i2c_tx_index动态计算或读取数据,而不是从一个预加载的数组中获取。但要确保数据准备的操作时间足够短,不能超过SCL低电平的时间(在100kHz下约5μs),否则可能导致超时。如果计算复杂,应提前准备好数据。 - 与从机接收模式共存:一个完整的从机设备通常需要同时支持发送和接收。这意味着你的状态机需要处理
0x60(自身地址+写)、0x80(接收数据)等从机接收模式的状态。在0xA0状态,你需要判断接下来可能进入哪种模式,并做好相应的缓冲区和管理变量切换。
通过以上从理论到实践,从寄存器操作到避坑指南的详细拆解,你应该对LPC21xx/22xx的I2C从机发送模式有了透彻的理解。记住,成功的I2C从机驱动=精准的状态机理解+严谨的寄存器操作顺序+细致的硬件调试。把这套状态服务例程的框架作为你的模板,根据具体的应用需求填充数据管理和错误处理逻辑,你就能打造出稳定可靠的I2C从设备。
