P89LPC930/931单片机I2C接口实战:寄存器配置、状态机驱动与避坑指南
1. 项目概述与I2C总线核心原理
搞嵌入式开发这么多年,I2C总线绝对是我打交道最多的串行通信协议之一。它只用两根线——串行时钟线SCL和串行数据线SDA,就能把一堆传感器、EEPROM、实时时钟这些外设挂到单片机上,硬件布线简单,软件协议也相对清晰。今天咱们不聊那些泛泛的概念,直接切入实战,以Philips(现在归NXP了)的P89LPC930/931这颗经典的8位单片机为例,把它的I2C接口从硬件原理到寄存器配置,再到实际编程中的坑和技巧,一次性讲透。如果你正在用这款老将,或者想深入理解I2C在硬件层面的运作机制,这篇文章就是为你准备的。
I2C总线的精髓在于它的“线与”逻辑和主从架构。总线上所有设备的SDA和SCL引脚都是开漏或集电极开路输出,必须外接上拉电阻。这种设计天然支持“多主”和“仲裁”。想象一下,几个设备都想发言,谁先把数据线拉低谁就赢,输了的自动退避,数据一点都不会乱。时钟同步机制也让不同速度的设备能和谐共处,快的主设备等一等慢的从设备,大家步调一致。在P89LPC930/931上,I2C接口被做成了一个相当标准的字节操作型模块,通过六个特殊功能寄存器(SFR)来操控,支持主发送、主接收、从发送、从接收这四种模式。别看它是个老器件,但把它的I2C玩明白了,你对I2C协议的理解能上一个台阶,再玩其他更复杂的MCU上的I2C也会轻松很多。
2. P89LPC930/931 I2C接口硬件与寄存器深度解析
P89LPC930/931的I2C模块是其外设中的一大亮点,设计得相当规整。它占用P1.2和P1.3两个引脚,分别复用为SCL和SDA。在硬件上,内部包含了移位寄存器、地址比较器、位计数器、仲裁与同步逻辑、时钟发生器以及核心的控制逻辑。对我们程序员来说,所有这些硬件功能,都抽象成了六个可以直接读写的寄存器。理解每个寄存器的每一位是干什么的,是写出稳定可靠I2C驱动代码的前提。
2.1 I2C控制寄存器(I2CON - 0xD8)
这是整个I2C模块的“大脑”,是一个可位寻址的寄存器,意味着你可以用SETB I2CON.6这样的指令单独操作某一位,非常方便。
- I2EN (I2CON.6): I2C功能使能位。这是总开关,必须置1,I2C模块才开始工作。在初始化任何其他参数前,先把它关了(清0),等所有配置(比如从机地址、时钟速率)都设好了,再打开它,这是一个好习惯,能避免总线出现不可预料的毛刺。
- STA (I2CON.5): 起始条件标志位。这是你作为主设备发起通信的“发令枪”。软件置1后,硬件会检测总线是否空闲(SDA和SCL都为高)。如果空闲,它就发出一个START信号(SCL高时,SDA一个下降沿);如果总线忙,它会一直等到检测到一个STOP信号,再等待半个内部时钟周期后发出START。即使在从机模式下,你也可以设置STA,这意味着本机可以随时尝试去夺取总线控制权,切换为主机。
- STO (I2CON.4): 停止条件标志位。在主模式下,置1会令硬件产生一个STOP信号(SCL高时,SDA一个上升沿),发送完成后硬件会自动将其清0。在从模式下,置1不会向总线发STOP,而是用于从错误状态中恢复,硬件会模拟收到了一个STOP,使自身进入“非寻址”的从机接收模式,然后自动清0STO位。
- SI (I2CON.3): I2C中断标志位。这是状态机的“心跳”。每当I2C模块进入25个有效状态(状态码非0xF8)中的任何一个时,硬件都会将此位置1。如果总中断(EA)和I2C中断(IEN1.0)都打开了,就会触发中断。最关键的一点:这个标志必须由软件清0!你需要在中断服务程序里读取I2STAT判断状态后,手动写0清除它,总线传输才会继续。
- AA (I2CON.2): 应答标志位。这个位决定了在接下来的应答时钟脉冲里,本机是拉低SDA(应答ACK)还是释放SDA(非应答NACK)。它影响四种情况:1) 收到自己的从机地址时;2) 收到广播地址(0x00)且I2ADR的GC位为1时;3) 本机处于主接收模式时;4) 本机处于被寻址的从接收模式时。简单说,AA=1就应答,AA=0就不应答。在主接收模式下,收到最后一个字节前,AA要置1,收到最后一个字节后,AA要清0,通知从机停止发送。
- CRSEL (I2CON.0): SCL时钟源选择位。这是配置通信速率的关键。CRSEL=1时,SCL由Timer1的溢出率分频得到。此时Timer1必须工作在8位自动重载模式(模式2)。I2C速率 = Timer1溢出率 / 2 = PCLK / (2 * (256 - TH1))。假设主频
fosc=12MHz,TH1重载值范围0-255,那么I2C速率范围约为11.72 Kbps 到 3000 Kbps。CRSEL=0时,则使用内部的SCL发生器,其频率由I2SCLH和I2SCLL两个寄存器决定,这是更常用的方式,因为更灵活独立。
实操心得:调试I2C,第一步先看SI和AA。SI不置1,说明状态机没动,检查初始化、使能位和硬件连接。AA设错了,会导致应答异常,通信立马中断。CRSEL选择上,除非系统里Timer1有富余且你对时序要求不高,否则强烈建议用内部发生器(CRSEL=0),独立可控,不干扰其他定时功能。
2.2 I2C数据寄存器(I2DAT - 0xDA)
这是一个8位的数据缓冲器。要发送的数据写到这里,接收到的数据从这里读取。有一个至关重要的限制:只有在SI标志位为1时(即一次字节传输完成,等待软件处理时),才能安全地读写I2DAT。在数据移位过程中访问它,会得到不确定的结果。数据总是高位(MSB)先发送或接收。也就是说,你写入I2DAT的字节,bit7会最先出现在SDA线上。
2.3 I2C从机地址寄存器(I2ADR - 0xDB)
这个寄存器仅在从机模式下有意义。高7位(I2ADR.7-I2ADR.1)存放本机的7位从机地址。最低位(I2ADR.0)是GC(General Call)位,如果置1,则器件会响应广播地址0x00。在主模式下,这个寄存器被忽略。在初始化时,即使你计划只做主设备,也最好给它赋一个值,因为一旦总线仲裁丢失,MCU会瞬间切换到从模式,如果没设置地址,可能会意外响应不该响应的数据。
2.4 I2C状态寄存器(I2STAT - 0xD9)
这是一个只读寄存器,高5位(bit7-bit3)组成了26个可能的状态码之一。低3位恒为0。状态码0xF8表示总线空闲,无状态信息,SI也为0。其他25个状态码(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)都对应一个明确的I2C总线状态,并且会伴随SI置1。驱动程序的本质,就是一个根据I2STAT状态码进行跳转的巨大状态机。后面的表格就是你的“行动指南”。
2.5 SCL占空比寄存器(I2SCLH & I2SCLL)
当CRSEL=0选择内部时钟发生器时,这两个寄存器决定了SCL的频率和占空比。I2SCLH定义SCL高电平持续的PCLK周期数,I2SCLL定义SCL低电平持续的PCLK周期数。因此,SCL的频率计算公式为:fSCL = fPCLK / (2 * (I2SCLH + I2SCLL))。占空比 =I2SCLH / (I2SCLH + I2SCLL)。标准I2C速率在100kHz(标准模式)和400kHz(快速模式),P89LPC930/931最高支持到400kHz。为了保证信号质量,官方建议两个寄存器的值都至少大于3。例如,当fPCLK = 12MHz,想要100kHz的速率,可以设I2SCLH = I2SCLL = 30(因为12M / (2*(30+30)) = 100k)。想要40%占空比的400kHz,可以设I2SCLH=10,I2SCLL=20(12M/(2*(10+20))=200k,注意这里计算是200k,需调整,I2SCLH=8, I2SCLL=7可得约12M/(2*15)=400k,占空比约53%)。
3. I2C四种操作模式的状态机与软件实现
理解了寄存器,我们来看核心的四种操作模式。P89LPC930/931的I2C驱动完全是状态机驱动的。你的中断服务程序(ISR)就是根据当前的I2STAT状态码,执行对应操作(读/写I2DAT,设置I2CON),然后清除SI位,总线动作就会自动继续。
3.1 主发送器模式(Master Transmitter Mode)
在这种模式下,MCU作为主设备,向从设备写数据。流程如下:
- 初始化:配置I2SCLH/L设置速率,设置自身从地址(I2ADR,可选但建议),配置I2CON(I2EN=1, AA=0/1, CRSEL=0, STA=0, STO=0, SI=0)。
- 发起起始条件:软件置位STA。硬件在总线空闲后发出START,状态码变为0x08,SI置1。
- 发送从机地址+写位:在状态0x08的中断里,向I2DAT写入
(SLA << 1) | 0(写方向)。然后清除SI。 - 等待地址应答:从机应答后,进入状态0x18(ACK收到)或0x20(NACK收到)。0x18表示从机就绪。
- 发送数据字节:在状态0x18的中断里,向I2DAT写入第一个数据字节,清除SI。
- 等待数据应答:从机应答后,进入状态0x28(ACK收到)。在此状态,你可以选择:继续发送下一个数据(写I2DAT,清SI),或者发出重复起始条件(置位STA,清SI),或者发出停止条件(置位STO,清SI)。
- 结束传输:发送停止条件(STO=1, SI=0)或重复起始条件(STA=1, SI=0)。
3.2 主接收器模式(Master Receiver Mode)
在这种模式下,MCU作为主设备,从从设备读数据。
- 初始化与发起起始:同主发送模式。
- 发送从机地址+读位:在状态0x08的中断里,向I2DAT写入
(SLA << 1) | 1(读方向)。清SI。 - 等待地址应答:进入状态0x40(ACK收到)或0x48(NACK收到)。0x40表示从机同意发送。
- 准备接收数据:在状态0x40,你需要设置AA位来决定如何应答第一个数据字节。如果准备接收多个字节,在收倒数第二个字节之前,AA都应置1(应答),告诉从机继续发。清SI。
- 接收数据字节:从机发送数据后,进入状态0x50(已接收字节并已发送ACK)或0x58(已接收字节并已发送NACK)。在状态0x50/0x58的中断里,必须从I2DAT读取收到的数据。
- 控制应答与结束:在读取数据后,根据是否还要接收下一个字节来设置AA位,然后清SI。当收到最后一个字节时,应在状态0x50(倒数第二个字节)之后将AA清0,这样在接收最后一个字节后,硬件会自动回复NACK,状态变为0x58。在0x58状态,你可以发出停止条件(STO=1)或重复起始条件(STA=1)。
3.3 从接收器模式(Slave Receiver Mode)
MCU作为从设备,接收主设备发来的数据。
- 初始化:向I2ADR写入自己的7位从机地址(左移一位,空出最低位)。配置I2CON:I2EN=1,AA=1(必须置1,否则不应答自身地址),STA=0, STO=0, SI=0。
- 等待寻址:完成后,I2C硬件开始监听总线。当检测到自己的地址(或广播地址且GC=1)且方向位为写(0)时,硬件会回复ACK,并进入状态0x60(自身地址)或0x70(广播地址),SI置1。
- 准备接收数据:在状态0x60/0x70,你可以通过设置AA位来决定是否应答后续的数据字节。然后清SI。
- 接收数据:每收到一个数据字节,状态变为0x80(ACK已发)或0x88(NACK已发)。在中断里读取I2DAT,并根据是否需要继续接收来设置AA,清SI。
- 传输结束:当主设备发送STOP或重复START时,状态变为0xA0。在此状态,你可以重新配置AA和STA,为下一次通信做准备。
3.4 从发送器模式(Slave Transmitter Mode)
MCU作为从设备,向主设备发送数据。
- 初始化:同从接收模式,设置自身地址和I2CON(AA=1)。
- 等待寻址:当检测到自身地址且方向位为读(1)时,硬件回复ACK,进入状态0xA8,SI置1。
- 加载发送数据:在状态0xA8,你需要将第一个要发送的数据字节写入I2DAT。通过设置AA位,可以控制发送完这个字节后是否期望主机的ACK(AA=1期望ACK继续发,AA=0表示这是最后一个字节)。清SI。
- 数据发送与应答:数据发出后,根据主机的应答,进入状态0xB8(ACK收到)或0xC0(NACK收到)。在0xB8状态,你可以继续加载下一个数据(写I2DAT)并设置AA。在0xC0或0xC8(最后一个字节发完且收到ACK)状态,表示主机不再要数据或传输结束,你可以进行后续处理。
4. 关键配置、调试与避坑指南
手册里的状态表(Table 2-5)是你的圣经,但直接看容易懵。我把它翻译成更易懂的实战逻辑。
4.1 速率计算与配置实战
假设你的系统时钟fosc = 12.000MHz,且不分频(fPCLK = fosc)。目标是配置标准100kHz的I2C时钟。
- 选择内部时钟发生器:
CRSEL = 0。 - 计算寄存器值:
fSCL = fPCLK / (2 * (I2SCLH + I2SCLL))。所以I2SCLH + I2SCLL = fPCLK / (2 * fSCL) = 12M / (2 * 100k) = 60。 - 设定占空比:标准I2C要求SCL高电平和低电平时间都至少4.7us(对于100kHz)。我们按50%占空比设置,即
I2SCLH = I2SCLL = 30。 - 验证:
30 + 30 = 60,符合。每个电平持续时间为30 / 12MHz = 2.5us,满足大于4.7us吗?注意:这里计算有误!30个PCLK周期的时间是30 / 12MHz = 2.5us,而100kHz的周期是10us,半周期是5us。2.5us < 4.7us,不满足最低要求。因此,对于12MHz的PCLK,100kHz的I2C需要I2SCLH + I2SCLL = 60,但为了满足高低电平最小宽度,需要调整占空比或降低速率。实际上,12MHz主频下,要满足100kHz和电平宽度,计算更复杂,可能需要降低目标速率到约90kHz,或检查芯片手册是否有特殊说明。一个更稳妥的经验值是:对于12MHz,设I2SCLH = I2SCLL = 50,得到fSCL = 12M / (2*100) = 60kHz,这个速率兼容性最好。
避坑指南:速率配置是第一个大坑。务必用示波器测量实际的SCL波形!计算值只是理论,PCB布线、上拉电阻强度、负载电容都会影响实际波形。如果波形上升沿太缓,会导致数据采样错误。上拉电阻通常选4.7kΩ,如果总线电容大(线长、设备多),要减小到2.2kΩ甚至1kΩ,以加快上升沿。
4.2 中断服务程序(ISR)编写框架
一个健壮的I2C ISR骨架如下(以C语言为例,假设已定义好SFR):
void I2C_ISR(void) interrupt 8 { // 假设I2C中断号是8 unsigned char status = I2STAT; // 首先读取状态寄存器 switch(status) { case 0x08: // START已发送 I2DAT = (slave_addr << 1) | 0; // 发送地址+写 I2CON &= ~0x08; // 清除SI位 break; case 0x18: // SLA+W已发送,收到ACK I2DAT = tx_buffer[tx_index++]; // 发送数据 I2CON &= ~0x08; break; case 0x28: // 数据已发送,收到ACK if(tx_index < tx_length) { I2DAT = tx_buffer[tx_index++]; } else { I2CON |= 0x10; // 设置STO,产生停止条件 } I2CON &= ~0x08; break; case 0x40: // SLA+R已发送,收到ACK,进入主接收 if(rx_length > 1) { I2CON |= 0x04; // 设置AA=1,准备接收多个字节并应答 } else { I2CON &= ~0x04; // AA=0,只接收一个字节,之后发NACK } I2CON &= ~0x08; break; case 0x50: // 数据已接收,且已发送ACK rx_buffer[rx_index++] = I2DAT; if(rx_index == (rx_length - 1)) { // 下一个是最后一个字节,准备发NACK I2CON &= ~0x04; // AA=0 } I2CON &= ~0x08; break; case 0x58: // 数据已接收,且已发送NACK(最后一个字节) rx_buffer[rx_index++] = I2DAT; I2CON |= 0x10; // 发送STOP I2CON &= ~0x08; communication_done = 1; // 设置完成标志 break; // ... 处理其他必要状态,如0x20(NACK),0x38(仲裁丢失)等 case 0x38: // 仲裁丢失 // 通常进行错误处理,可能重试 I2CON &= ~0x08; // 清SI,总线释放,本机变为从机 break; default: // 未预期的状态,进行错误恢复 I2CON |= 0x10; // 尝试发送STOP(主模式)或恢复(从模式) I2CON &= ~0x08; error_flag = 1; break; } }4.3 常见问题排查实录
通信完全没反应,SI标志从不置1:
- 检查硬件:首先用万用表量SCL和SDA电压,空闲时应为高电平(接近VCC)。如果为低,检查是否有设备死锁拉低总线,或者上拉电阻未接/损坏。
- 检查初始化:确认I2EN位已置1。确认P1.2和P1.3的引脚功能已设置为I2C(某些MCU需要配置引脚复用)。
- 检查中断:确认EA(总中断)和I2C专用中断使能位(如IEN1.0)已打开。
能发起START,但地址发送后收到NACK(状态0x20):
- 从机地址错误:确认7位从机地址是否正确,是否左移了一位。用逻辑分析仪抓取波形,看发出的地址是否与从设备手册一致。
- 从机设备问题:从机是否上电?是否处于复位或休眠状态?从机的I2C引脚是否连接正确?
- 总线冲突:是否有其他主设备在通信?仲裁丢失会进入状态0x38。
通信时好时坏,数据错误:
- 时序问题:最常见原因。用示波器测量SCL和SDA波形,看上升/下降时间是否过长(应陡峭),高低电平是否平整。调整上拉电阻阻值(减小可加快上升沿)。
- 电源噪声:在VCC和GND之间靠近芯片处加一个0.1uF的退耦电容。
- 软件时序:在状态处理中,清除SI后,硬件会立即进行下一步操作。确保你的状态处理代码足够快,不会错过下一个字节的传输。避免在ISR中进行复杂运算或长时间操作。
从机模式无法被寻址:
- AA位未置1:在从机初始化时,必须设置
AA=1,否则芯片不会应答自己的地址。 - I2ADR寄存器未设置:确认写入了正确的7位自身地址。
- GC位影响:如果从机需要响应广播地址,需将I2ADR的GC位置1。
- AA位未置1:在从机初始化时,必须设置
仲裁丢失(状态0x38)频繁发生:
- 这发生在多主系统中。检查你的主设备在发起传输前,是否通过检测总线空闲(SCL和SDA都为高)来避免冲突。P89LPC930/931的硬件在设置STA时会自动检测,但如果软件控制不当,仍可能冲突。确保在完成一次完整传输(发出STOP)后再尝试下一次传输。
5. 进阶应用与性能优化思考
掌握了基本操作后,可以思考一些进阶用法。比如利用重复起始条件(Repeated START)。它不像STOP那样释放总线,而是在不释放总线的情况下,改变数据传输方向(例如,先写一个存储器的寄存器地址,然后立刻发起读操作)。这在访问EEPROM等设备时非常高效。在代码中,就是在状态0x28或0x58等位置,不设置STO,而是设置STA,并发送新的地址+方向位。
另一个重点是低功耗设计。P89LPC930/931本身是低功耗单片机。在从机模式下,当未被寻址时,I2C模块几乎不耗电。你可以利用这一点,让MCU进入空闲或掉电模式,当主设备发起呼叫其地址时,I2C地址匹配硬件可以产生中断唤醒MCU,实现极低功耗的待机通信。
最后,关于代码的健壮性。工业环境复杂,一定要加入超时机制。例如,在发送START后等待SI置1,如果超过一定时间(如10ms)仍未发生,则应重置I2C模块(先关I2EN,再重新初始化),并置位错误标志。对于关键数据通信,建议加入CRC校验或简单的和校验,在应用层确保数据完整性。
折腾P89LPC930/931的I2C,就像和一位老派但严谨的工程师打交道,它不花哨,但每一步都必须遵循明确的规则。把状态表吃透,把波形看懂,把常见坑位标记好,你就能让这个老旧的接口在新的项目里稳定可靠地跑起来。这份细致,对于理解任何嵌入式通信协议,都是通用的财富。
