I2C总线状态机编程实战:从协议原理到NXP LPC驱动实现
1. I2C总线核心机制与状态机编程思想
搞嵌入式开发,I2C总线绝对是绕不开的一道坎。它用两根线(SDA数据线、SCL时钟线)就能把一堆传感器、存储器、IO扩展芯片串起来,硬件设计上确实省心。但真到了写驱动、调通信的时候,很多朋友就头疼了——时序不对、从机没响应、数据错位,各种问题层出不穷。说到底,I2C的复杂性不在于物理连接,而在于它那套基于状态机的软件交互逻辑。你得像一个交通警察,时刻盯着总线上的每一个信号变化,然后根据芯片手册里那一堆十六进制的状态码,做出正确的反应。
NXP的LPC系列微控制器(比如文档里提到的EM773)其I2C模块设计得非常经典,它把总线上发生的所有事件,都抽象成了一个个明确的状态码,保存在I2STAT寄存器里。你的中断服务程序(ISR)本质上就是一个巨大的switch-case语句,根据读到的状态码,跳转到对应的处理分支。这种编程模式,要求你对I2C协议的每一个阶段都了如指掌:START条件发出后是什么状态?地址发送成功(收到ACK)是什么状态?数据收发完成又是什么状态?更重要的是,在每一个状态下,你该往I2DAT寄存器里写什么?又该设置I2CON寄存器里的STA、STO、SI、AA这些控制位为什么值?
文档里给出的那些表格,比如Master Receiver模式下的状态表,就是你的“行动指南”。但光看表格容易懵,我们需要把它翻译成工程师能理解的逻辑。举个例子,状态0x40在Master Receiver模式下表示:“从机地址+读方向位(SLA+R)已成功发送,且收到了从机的应答(ACK)”。这时候,硬件在等待你的指令:你是想让从机继续发下一个数据(回复ACK),还是告诉它这是最后一个数据了(回复NACK)?这个决定,就通过你接下来设置AA位(在I2CON寄存器中)来传达。如果你设AA=0,下次收到数据后会回NACK;设AA=1,则回ACK。同时,你必须清除SI中断标志,总线传输才会继续。
注意:这里有个新手极易踩的坑。
I2CON寄存器通常有SET和CLR两个对应地址,用于置位和清零特定位,避免读-改-写操作。比如清除SI,应该是向I2CONCLR写入0x08(SI的位掩码),而不是直接操作I2CON。文档示例代码里用的就是I2CONSET和I2CONCLR,务必遵循。
所以,编写一个健壮的I2C驱动,核心就是构建一个精准响应这些状态的状态机。下面,我们就以Master Receiver(主设备接收)和Slave Transmitter(从设备发送)这两个互补的模式为例,拆解其中的每一个关键状态,并分享如何组织你的中断服务程序。
1.1 从硬件行为理解状态流转
在深入代码之前,我们必须建立对硬件行为的直觉。I2C模块一旦启动,就像一部自动运行的机器。你的程序(CPU)不是时时刻刻在控制总线,而是在关键节点(产生中断时)对这部机器进行“编程”或“配置”,告诉它下一步该干什么,然后放手让它自己运行,直到下一个节点到来。
关键信号与寄存器:
- SCL, SDA:物理引脚,硬件模块负责按照协议规范产生时钟和读写数据。
- I2DAT:数据寄存器。你要发送的地址或数据写到这里;你接收到的数据从这里读取。
- I2STAT:状态寄存器。只读,告诉你“刚才发生了什么”。
- I2CON:控制寄存器。通过设置其中的位,你告诉硬件“接下来要做什么”。
STA(Start Flag):置1,硬件会在总线空闲时发起一个START条件。STO(Stop Flag):置1,硬件会在当前传输完成后发起一个STOP条件。SI(Interrupt Flag):中断标志。当一个状态完成(或需要软件干预)时,硬件将其置1并产生中断。软件必须将其清零才能让硬件继续。AA(Assert Acknowledge):应答标志。置1,表示在下一次需要硬件回复ACK的场合(如收到地址或数据),硬件会自动回复ACK;置0,则回复NACK。
状态机的本质:整个传输过程被划分成许多“状态”。每个状态结束时,硬件会做三件事:1) 将当前状态码写入I2STAT;2) 将SI位置1;3) 产生中断(如果使能)。此时,硬件会暂停一切动作,等待你的ISR。你的ISR需要:1) 读取I2STAT;2) 根据状态码执行对应操作(如读/写I2DAT,设置I2CON控制位);3) 清除SI位。一旦SI被清除,硬件立即根据你刚刚设置好的I2CON等寄存器,执行下一步操作,并进入下一个状态。
2. Master Receiver模式详解与状态码实战解析
主设备接收模式,简单说就是“主设备去读取从设备的数据”。这是非常常见的操作,比如MCU从温度传感器读取测量值。我们结合文档中的图25和表130,把整个流程走一遍。
2.1 模式启动与初始状态
首先,主程序需要发起一次读取操作。通常的步骤是:
- 填充一个缓冲区(
MasterRxBuffer)的地址和准备接收数据的长度(MasterRxCount)。 - 将目标从机地址与读位(
SLA+R)组合,保存到一个变量(TargetSlaveAddr)中。 - 向
I2CONSET写入0x20,置位STA标志,发起START条件。
之后,硬件接管。当START条件成功在总线上发出后,硬件进入状态0x08,并产生中断。
状态 0x08: START条件已发送
- 硬件状态:一个START(或Repeated START)条件已成功在总线上产生。
- 软件响应:这是发送从机地址的时刻。你必须将
SLA+R(7位地址+1位读方向位‘1’)写入I2DAT寄存器。 - 控制位设置:通常,你会保持
AA=1(期待地址被应答),STA=0,STO=0,然后清除SI。 - 硬件下一步:发送
I2DAT中的地址字节,并检测SDA线上的ACK信号。
状态 0x10: Repeated START条件已发送
- 这个状态与
0x08类似,区别在于它发生在一个复合消息中(比如先写后读)。软件响应完全相同:写入SLA+R(或SLA+W以切换模式),然后清SI。
2.2 地址发送后的关键分支
地址发出后,从机可能应答(ACK)也可能不应答(NACK),这会导致进入完全不同的状态。
状态 0x40: SLA+R已发送,收到ACK
- 这是成功的路径。表示从机在线并准备好了发送数据。
- 软件响应:此时
I2DAT里是无效数据。你需要决定如何接收第一个数据字节。关键是设置AA位。- 如果你知道只接收一个字节,或者这是最后一个字节,应设
AA=0。这样,硬件在收到数据后会替我们回复NACK,告知从机停止发送。 - 如果你要接收多个字节,应设
AA=1。这样,硬件在收到数据后会回复ACK,从机会继续发送下一个字节。
- 如果你知道只接收一个字节,或者这是最后一个字节,应设
- 操作:通常无需操作
I2DAT。设置好I2CON中的AA位,然后清除SI。 - 硬件下一步:开始接收第一个数据字节,并在接收完成后根据
AA位设置回复ACK/NACK,然后进入数据接收状态。
状态 0x48: SLA+R已发送,收到NACK
- 这是失败路径。表示从机地址不对、从机忙或从机故障。
- 软件响应:你有几种选择,文档表格里列出了三种:
STA=1, STO=0:发送一个Repeated START条件,重试。STA=0, STO=1:发送一个STOP条件,结束本次传输。STA=1, STO=1:先发STOP,再发START(相当于终止当前操作并重新开始)。
- 常见选择:在大多数简单应用中,选择发送STOP条件(选项2)并向上层报告错误是最稳妥的。不断重试(选项1)可能导致总线死锁,除非你有完善的总线管理逻辑。
- 操作:根据你的策略设置
STA和STO位,然后清除SI。
2.3 数据接收阶段的状态循环
如果从机开始发送数据,我们将进入数据接收的状态循环。
状态 0x50: 数据字节已接收,ACK已回复
- 硬件状态:成功接收一个数据字节,并且之前
AA位被设置为1,所以硬件自动回复了ACK。 - 软件响应:必须立刻从
I2DAT寄存器中读取这个数据字节,保存到你的MasterRxBuffer中,并递减MasterRxCount。然后,你需要为接收下一个字节做准备。- 如果
MasterRxCount > 1(后面还有多于1个字节要收),设AA=1。 - 如果
MasterRxCount == 1(这是倒数第二个字节,下一个是最后一个),设AA=0,告诉从机下一个字节发完就别发了。
- 如果
- 操作:读
I2DAT,更新缓冲区和计数器,设置AA位,清除SI。 - 硬件下一步:继续接收下一个数据字节,并根据新的
AA设置回复ACK/NACK。
状态 0x58: 数据字节已接收,NACK已回复
- 硬件状态:成功接收一个数据字节,并且之前
AA位被设置为0,所以硬件自动回复了NACK。这通常意味着这是你计划接收的最后一个数据字节。 - 软件响应:同样,必须立刻从
I2DAT中读取这最后一个数据字节。之后,你需要决定如何结束这次传输。- 常见操作是发送STOP条件(
STO=1, STA=0),优雅地结束。 - 也可以发送Repeated START(
STA=1, STO=0)以开始下一次传输(复合消息)。
- 常见操作是发送STOP条件(
- 操作:读
I2DAT,设置STA和STO位,清除SI。 - 硬件下一步:根据设置,发送STOP或Repeated START条件。
2.4 仲裁丢失与异常处理
在多主系统中,可能存在总线仲裁。
状态 0x38: 仲裁丢失
- 硬件状态:在尝试发送地址或数据时,发现总线上有其他主设备也在通信,且本机失去了仲裁权。硬件会自动切换到从设备模式(如果
AA=1)。 - 软件响应:通常,你需要放弃本次主设备操作。可以设置
STA=1,这样当总线再次空闲时,硬件会自动尝试重新发起START条件(状态0x08),实现自动重试。 - 操作:设置
STA=1,清除SI。你的主设备发送/接收缓冲区等状态应保持不变,以便重试。
实操心得:在状态
0x50和0x58中,“读数据”和“清SI”的顺序至关重要。必须在清除SI标志之前将数据从I2DAT中读走。因为一旦SI被清除,硬件立即进入下一个动作,I2DAT寄存器可能很快被新的数据覆盖(在下一个接收状态)或被硬件内部操作改变。一个可靠的编程模式是:进入ISR,读取状态码,用switch-case分支,在分支内第一件事就是读取数据(如果需要),然后再配置控制位,最后一步才是清除SI标志并退出。
3. Slave Transmitter模式详解与状态响应策略
从设备发送模式,就是“从设备响应主设备的读请求,发送数据”。例如,一个EEPROM芯片在收到主设备的读命令后,进入此模式发送存储的数据。从设备的行为完全由主设备发起的时序所驱动,其状态机是“反应式”的。
3.1 从设备初始化与地址匹配
从设备要工作,必须先初始化,告诉硬件“我是谁”。
- 将自己的7位从机地址写入
I2ADR寄存器的高7位。最低位(GC)如果置1,则同时响应全局呼叫地址(0x00)。 - 向
I2CONSET写入0x44(二进制0100 0100)。这同时设置了I2EN=1(使能I2C模块)和AA=1(使能地址识别与应答)。此时,STA=0,STO=0,SI=0。
初始化完成后,从设备硬件就开始监听总线。当检测到START条件,并紧接着收到与自身I2ADR匹配的地址+读方向位(SLA+R)时,硬件会自动回复ACK(因为AA=1),然后进入状态0xA8,并产生中断。
状态 0xA8: 自身的SLA+R已收到,ACK已回复
- 硬件状态:主设备想读数据,并且地址匹配成功。
- 软件响应:这是你加载第一个待发送数据字节到
I2DAT的时刻。同时,你需要通过AA位告诉硬件,在发送完这个字节后,你期望主设备回复什么?AA=1:你期望主设备回复ACK(表示还要更多数据)。适用于你要发送多个字节。AA=0:你期望主设备回复NACK(表示这是最后一个数据)。适用于你只发送一个字节,或这是最后一个字节。
- 操作:将第一个数据字节写入
I2DAT,设置AA位,清除SI。 - 硬件下一步:发送
I2DAT中的数据字节,并检测主设备回复的ACK/NACK。
状态 0xB0: 仲裁丢失(作为主设备时),但自身的SLA+R已收到,ACK已回复
- 这个状态比较特殊,发生在从设备之前尝试作为主设备但仲裁丢失,随后立即被另一个主设备寻址为从设备进行读取。软件响应与状态
0xA8完全一样:加载数据到I2DAT,设置AA,清SI。
3.2 数据发送循环与结束
状态 0xB8: I2DAT中的数据字节已发送,ACK已收到
- 硬件状态:上一个字节发送成功,并且主设备回复了ACK(表示“请继续”)。
- 软件响应:主设备还想要数据。你需要加载下一个要发送的数据字节到
I2DAT。同样,通过AA位设置你对下一个字节的期望。- 如果后面还有数据要发,设
AA=1。 - 如果即将发送的是最后一个字节,设
AA=0。
- 如果后面还有数据要发,设
- 操作:写下一个数据到
I2DAT,设置AA位,清除SI。 - 硬件下一步:发送新的数据字节。
状态 0xC0: I2DAT中的数据字节已发送,NACK已收到
- 硬件状态:上一个字节发送成功,但主设备回复了NACK。这通常是主设备发出的停止读取信号。
- 软件响应:传输结束。你无需再向
I2DAT写数据。你需要决定从设备后续的状态。- 通常,保持
AA=1,以便继续响应下一次寻址。设置STA=0,STO=0,清SI即可。 - 也可以设置
STA=1,尝试在总线空闲后将自己切换回主设备模式(如果支持多主)。
- 通常,保持
- 操作:设置
I2CON控制位(通常STA=0, STO=0, AA=1),清除SI。 - 硬件下一步:进入未寻址的从设备模式,继续监听总线。
状态 0xC8: 最后一个数据字节(AA=0时)已发送,ACK已收到
- 硬件状态:你在发送上一个字节前设置了
AA=0,表明“这是最后一个”。硬件发送完该字节后,无论主设备回复ACK还是NACK(实际上主设备此时应回NACK,但ACK也可能被收到),都会进入此状态。 - 软件响应:与状态
0xC0类似,传输结束。无需操作I2DAT,只需配置I2CON并清SI。
3.3 从设备模式下的特殊控制:AA位的作用
从设备模式中,AA位是一个强大的工具。它不仅控制是否应答自身地址,还能在传输过程中动态改变从设备的行为。
- 传输中置
AA=0:如果你在状态0xA8或0xB8中将AA设为0,那么硬件在发送完当前I2DAT中的数据后,会进入状态0xC0或0xC8,然后切换到“未寻址”模式,即使主设备还在发送时钟试图读取,从设备也只会向SDA线输出高电平(1)。这相当于从设备单方面终止了传输。这在从设备需要处理耗时任务(如EEPROM内部写入)时非常有用,可以暂时“脱线”而不干扰总线。 - 重新置
AA=1:当从设备准备好再次响应时,只需在任意时刻(通常在主程序或另一个中断里)将AA位置1,它就会重新开始响应自己的地址。
注意事项:在Slave Transmitter模式下,数据必须提前准备好。在状态
0xA8和0xB8中,你必须在清除SI标志之前,将下一个要发送的数据写入I2DAT。因为SI清除后,硬件几乎会立即开始发送I2DAT寄存器中的内容。如果写入太慢,可能导致发送错误数据或时序问题。一种好的实践是在内存中维护一个发送缓冲区和指针,在ISR中快速读取指针指向的数据并写入I2DAT,然后移动指针。
4. 中断服务程序(ISR)架构与代码实现要点
理解了各个状态,接下来就是把它们组装成一个高效、可靠的中断服务程序。这份文档的精髓就在于它提供了一套基于状态码查询的ISR框架。
4.1 ISR的基本骨架
一个典型的I2C中断服务程序结构如下,它高度依赖于你使用的具体编译器和芯片,但逻辑通用:
// 假设的全局变量和缓冲区定义 volatile uint8_t I2C_Status; volatile uint8_t MasterTxBuffer[32], MasterRxBuffer[32]; volatile uint8_t MasterTxIndex, MasterTxCount, MasterRxIndex, MasterRxCount; volatile enum {IDLE, MASTER_TX, MASTER_RX, SLAVE_TX, SLAVE_RX} I2C_Mode; void I2C_IRQHandler(void) { // 1. 读取状态码 I2C_Status = I2C_GetStatus(); // 读取I2STAT寄存器 // 2. 根据状态码分支处理 switch (I2C_Status) { case 0x08: // START条件已发送 handle_status_0x08(); break; case 0x10: // Repeated START条件已发送 handle_status_0x10(); break; case 0x40: // Master Rx: SLA+R sent, ACK received handle_status_0x40(); break; case 0x48: // Master Rx: SLA+R sent, NACK received handle_status_0x48(); break; case 0x50: // Master Rx: data received, ACK returned handle_status_0x50(); break; case 0x58: // Master Rx: data received, NACK returned handle_status_0x58(); break; case 0xA8: // Slave Tx: Own SLA+R received, ACK returned handle_status_0xA8(); break; case 0xB8: // Slave Tx: data transmitted, ACK received handle_status_0xB8(); break; case 0xC0: // Slave Tx: data transmitted, NACK received handle_status_0xC0(); break; // ... 处理其他可能的状态码 case 0xF8: // 无状态信息,SI=0 // 通常直接退出,不做任何操作 I2C_ClearSI(); // 但有些实现可能需要手动清SI break; case 0x00: // 总线错误 handle_bus_error(); break; default: // 遇到未处理的状态码,进行错误恢复,如发送STOP I2C_SetSTO(); I2C_ClearSI(); I2C_Mode = IDLE; break; } }4.2 关键状态处理函数示例
我们以Master Receiver的0x50和Slave Transmitter的0xB8为例,看看处理函数内部如何实现:
// Master Receiver - 状态 0x50 处理函数 static void handle_status_0x50(void) { // 1. 读取接收到的数据 MasterRxBuffer[MasterRxIndex++] = I2C_ReadData(); // 从I2DAT读取 // 2. 更新计数器 MasterRxCount--; // 3. 决定下一个字节的应答策略 if (MasterRxCount > 1) { // 还有超过1个字节要收,回复ACK I2C_SetAA(); } else { // 这是倒数第二个字节,下一个字节收完后回复NACK I2C_ClearAA(); } // 4. 清除SI标志,让硬件继续 I2C_ClearSI(); // 5. 如果接收完成,可以在这里设置标志位通知主程序 if (MasterRxCount == 0) { // 所有数据接收完成,但最后一个字节(状态0x58)还没处理 // 通常在主程序判断完成,或状态0x58中判断 } } // Slave Transmitter - 状态 0xB8 处理函数 static void handle_status_0xB8(void) { // 1. 检查是否还有数据要发送 if (SlaveTxIndex < SlaveTxCount) { // 还有数据,加载下一个字节到I2DAT I2C_WriteData(SlaveTxBuffer[SlaveTxIndex++]); // 设置下一个字节后的应答期望 if ((SlaveTxIndex + 1) == SlaveTxCount) { // 下一个字节是最后一个 I2C_ClearAA(); // 发送完后,期望主设备回复NACK } else { I2C_SetAA(); // 期望主设备继续回复ACK } } else { // 没有更多数据了!这是一个错误状态,主设备还在要数据,但我们给不出。 // 安全做法:发送0xFF或特定错误码,并设置AA=0,强制结束。 I2C_WriteData(0xFF); I2C_ClearAA(); // 同时可以设置一个错误标志 SlaveTxError = 1; } // 2. 清除SI标志 I2C_ClearSI(); }4.3 总线错误与超时处理
状态 0x00: 总线错误
- 原因:在地址、数据或应答位传输期间,检测到非法的START或STOP条件。可能是总线干扰或设备故障。
- 标准恢复操作(见文档):
- 向
I2CONSET写入0x14(设置STO=1和AA=1)。STO=1用于恢复总线,AA=1确保从机模式被正确设置。 - 向
I2CONCLR写入0x08(清除SI)。 - 硬件会释放总线,I2C模块进入未寻址的从模式,
STO位被自动清零。
- 向
- 软件策略:在ISR中处理此状态后,一定要将你的应用程序状态(如
I2C_Mode)重置为IDLE,并设置一个错误标志,让上层任务知道本次传输失败,可能需要重试。
超时处理: 文档提到,I2C硬件本身没有超时功能。如果从设备无响应或SCL线被拉低,总线会挂起。你必须用另一个定时器实现超时机制。
- 在启动I2C传输(设置
STA)时,启动一个硬件定时器(例如,设置50ms超时)。 - 在I2C ISR中,每次正常状态处理完成时,重置(或停止)这个定时器。
- 如果定时器中断发生,说明I2C传输卡住了。在定时器中断服务程序中,你需要进行“强制访问总线”操作:
- 同时设置
STA=1和STO=1(向I2CONSET写0x30)。 - 然后清除
SI标志。 - 这个操作会强制硬件内部产生一个STOP条件的感觉,并尝试重新发送START,从而有可能恢复总线。之后,应将应用程序状态重置,并报告超时错误。
- 同时设置
5. 调试技巧与常见问题排查实录
即使完全按照手册编程,I2C调试也常让人抓狂。以下是我在实际项目中总结的一些排查经验和技巧。
5.1 问题排查速查表
| 现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 主设备发送START后无任何状态中断 | 1. I2C模块时钟未使能。 2. I2C引脚功能未正确映射(复用功能)。 3. 上拉电阻缺失或阻值过大。 4. I2EN位未置1。 | 1. 检查芯片时钟配置,确保I2C外设时钟打开。 2. 查阅数据手册,确认SDA/SCL引脚已配置为I2C功能。 3. 确保总线上有上拉电阻(通常4.7kΩ),用示波器看START条件波形。 4. 检查 I2CON寄存器,I2EN必须为1。 |
| 始终收到状态0x48(地址NACK) | 1. 从设备地址错误(7位 vs 8位混淆)。 2. 从设备电源或接地不良。 3. 从设备忙(如EEPROM在写周期)。 4. 总线电容过大,时序违规。 | 1.最常见原因:确认你写入I2DAT的是7位地址左移1位后加上R/W位。例如,地址0x68,写地址时应是`(0x68 << 1) |
| 能收到地址ACK(0x40),但收不到数据或状态不推进 | 1. 从设备输出数据太慢(时钟拉伸)。 2. 主设备在状态0x40后未正确清除 SI。3. 主设备在状态0x40后 AA位设置错误。 | 1. 确保主设备支持时钟拉伸(多数MCU的I2C硬件支持)。用逻辑分析仪看SCL线是否被从机拉低。 2.单步调试ISR,确认在状态0x40分支执行了 SI清除操作。3. 确认在状态0x40后,根据你的需求正确设置了 AA位(0或1)。 |
| Slave设备不响应自身地址 | 1. 从设备I2ADR寄存器未正确设置。2. 从设备 AA位未置1。3. 全局呼叫地址(GC)位干扰。 | 1. 确认写入I2ADR的地址是7位格式,且左对齐(通常在高7位)。2. 初始化时, I2CON中必须设置AA=1。3. 如果不使用全局呼叫,确保 I2ADR的GC位(LSB)为0。 |
| 通信随机出错,状态码混乱 | 1. 电源噪声或地线干扰。 2. 中断优先级问题,ISR被长时间阻塞。 3. 软件竞态条件,全局变量在ISR和主程序间未保护。 | 1. 加强电源滤波,缩短走线,确保共地良好。 2. 提高I2C中断优先级,确保ISR能及时响应。 3. 对 MasterRxIndex等在ISR和主程序共享的变量,使用volatile声明,或在访问临界区时禁用中断。 |
| 总线锁死,SCL线被持续拉低 | 1. 从设备故障或程序跑飞。 2. 主设备在异常状态下未正确恢复。 | 1. 尝试逐个断开从设备,定位故障源。 2. 实现总线超时与恢复机制(见4.3节)。在定时器中断中执行“强制访问”操作( STA=1, STO=1然后清SI)。 |
5.2 调试工具与实操心得
逻辑分析仪是你的最佳伙伴:投资一个哪怕是最基础的逻辑分析仪(带I2C解码功能),它能直观显示START、STOP、地址、数据、ACK/NACK的波形和时间关系。绝大部分时序和协议问题,在逻辑分析仪下一目了然。对照逻辑分析仪的波形和你的程序状态码,能快速定位问题发生在哪个环节。
充分利用芯片的调试功能:很多现代MCU的I2C模块有调试模式或可以配置为输出调试信息。如果没有,就在ISR入口处将
I2STAT值实时存入一个循环缓冲区,在主程序中打印出来,这对于追踪复杂的状态流转非常有用。从最简单的用例开始:不要一开始就实现多主、时钟拉伸、高速模式等复杂功能。先让主设备以标准模式(100kHz)读取一个已知的、简单的从设备(如EEPROM的一个固定地址),确保最基本的读写流程能走通。在此基础上,再逐步增加功能。
状态机思维要严谨:你的ISR是一个严格的状态机。确保每一个状态分支都得到处理,即使是错误状态。对于文档中“No I2DAT action”的状态,也最好有明确的空操作或日志记录。
default分支一定要有,用于捕获未知状态码,并执行安全的恢复操作(如发送STOP,重置状态机)。注意全局变量的volatile修饰:所有在ISR和主程序之间共享的缓冲区索引、计数器、状态标志,都必须用
volatile关键字声明,防止编译器进行不优化的优化。对于多字节数据的读写,考虑使用临界区保护(开关中断)。
最后,I2C状态机编程虽然繁琐,但一旦理解并实现稳定,它的可靠性是非常高的。这份NXP的文档虽然针对特定芯片,但其反映的I2C控制器设计思想和状态机处理方法,具有普遍的参考价值。当你掌握了这套方法,再去使用其他厂商的MCU(如STM32的I2C IT或DMA模式),你会发现底层逻辑是相通的,只是寄存器名称和库函数封装有所不同。
