1. 项目概述:从手册到实战,拆解P89LPC920的I2C接口
如果你手头正好有NXP(原Philips)的P89LPC920、921或922这几款经典的8位微控制器,并且项目里需要用到I2C总线去连接个EEPROM、传感器或者RTC时钟芯片,那你大概率会去翻看那份2003年的用户手册。手册里关于I2C接口的章节,表格密密麻麻,状态码看得人眼花缭乱,寄存器位定义也够抽象。直接照着抄,程序跑不起来是常事;完全靠自己琢磨,又容易在总线仲裁、时钟同步这些细节上栽跟头。我当年第一次用这个系列的MCU做I2C主机读取温湿度传感器,就卡在状态码0x38(仲裁丢失)上折腾了大半天。
这份手册更像是一本“字典”,它定义了所有寄存器和状态,但缺了最关键的一环:如何把这些零散的“单词”组织成一篇能流畅运行的“文章”。今天,我就结合自己踩过的坑和项目经验,带你深入P89LPC920/921/922的I2C接口内部。我们不止看手册说了什么,更要弄明白它为什么这么设计,以及在实际编程中,如何根据那些状态码(Status Code)像交警指挥交通一样,精准地控制每一字节数据的发送与接收。无论是做主设备去驱动外设,还是做从设备响应主机,你都能在这篇文章里找到清晰的路径和避坑指南。
2. I2C总线核心原理与P89LPC920的实现定位
在直接动手配置寄存器之前,我们必须先统一“语言”。I2C总线协议本身是跨平台的,但不同厂商的MCU在硬件实现上各有侧重。理解共性与特性,是写出稳定驱动的前提。
2.1 I2C协议的精髓:两线制与主从协同
I2C最迷人的地方就是它的简洁。仅凭两根线——串行数据线(SDA)和串行时钟线(SCL),就能挂上一堆设备。但这简洁背后是严格的规则。总线上的所有设备,其SDA和SCL引脚都必须是开漏输出(Open-Drain)。这意味着它们只能把线拉低(输出0),而不能主动拉高(输出1)。总线的高电平靠外部的上拉电阻维持。这种设计直接带来了两个核心机制:线与和时钟同步。
当多个主机同时尝试启动通信时,“线与”特性使得任何一个设备输出低电平,整条线就是低电平。这为多主仲裁提供了硬件基础:主机在发送地址和数据的同时,会监听SDA线上的实际电平。如果自己发送的是1(即释放总线,期望看到高电平),但检测到的是0,说明有其他设备正在发送数据,自己就仲裁失败,立刻切换为从机模式。P89LPC920的硬件完全支持这个过程,状态码0x38就是专门用来报告仲裁丢失的,软件必须妥善处理。
时钟同步则是为了解决不同设备速度差异的问题。SCL线也是开漏的。每个主机在驱动自己的低电平周期时,会开始计数自己的高电平时间。但如果另一个设备(可能是从机,也可能是另一个主机)的时钟低电平周期更长,它会一直把SCL线拉低,直到它完成操作。这样,所有设备的时钟低电平周期会取最长的那一个,实现了同步。P89LPC920在作为从机时,能自动同步外部主机的时钟;作为主机时,其内部时钟发生器也能被从机拉低SCL的行为所“拉伸”,实现等待。
2.2 P89LPC920的I2C接口特性:一个“字节型”控制器
手册里有一句关键描述:“The P89LPC920/921/922 device provides a byte-oriented I2C interface。”“字节型”这三个字是理解其编程模型的关键。这意味着它的硬件自动处理了位级别的时序、起始/停止条件生成、ACK应答位检测等底层细节,但不会自动处理整个数据帧。
具体来说,硬件帮你做了:
- 在收到或发出一个完整的8位数据字节(或地址字节)后,自动产生中断(SI位置1)。
- 根据当前操作,在状态寄存器
I2STAT中写入一个明确的状态码(如0x08表示已发出START条件)。 - 在主机模式下,自动控制SCL时钟的发出。
需要软件(也就是你)来做的是:
- 在每次中断(SI=1)发生时,读取
I2STAT。 - 根据这个状态码,查表(手册中的Table 2-5)决定下一步动作。
- 执行动作:可能是往
I2DAT写数据(发送),从I2DAT读数据(接收),或配置I2CON的STA、STO、AA位来控制总线状态。 - 最后,手动清除SI位,让硬件继续执行下一步。
这种“硬件处理字节,软件指挥流程”的模式,在早期8位MCU中非常典型。它给了开发者极大的灵活性,但也要求对状态机有清晰的理解。你可以把它想象成一个自动档汽车的变速箱(硬件),它负责精确的换挡操作,但你(软件)必须根据车速和路况(状态码),决定是踩油门(写数据)还是踩刹车(发停止条件)。
3. 核心寄存器详解与配置心法
P89LPC920的I2C功能完全由6个特殊功能寄存器(SFR)控制。吃透它们,就等于拿到了总线的指挥权。
3.1 控制核心:I2CON寄存器
I2CON(地址D8h)是整个I2C接口的大脑。它是一个可位寻址的寄存器,意味着你可以用SETB I2CON.5这样的指令单独操作某一位,非常高效。
| 位 | 符号 | 功能详解与实操要点 |
|---|---|---|
| 7 | - | 保留位。必须保持为0。 |
| 6 | I2EN | I2C功能使能。1=使能,0=关闭。注意:在初始化序列中,这通常是最后配置的位之一,确保其他寄存器(如I2ADR、I2SCLH/L)先设置好。 |
| 5 | STA | 起始条件标志。这是主机模式的“点火开关”。 设置STA=1:如果总线空闲,硬件将立即产生一个START条件;如果总线忙,硬件会等待直到检测到一个STOP条件,然后延迟半个内部时钟周期后发出START。即使当前是从机模式,也可以设置此位以尝试获取总线控制权。 关键点:STA位由软件置1,但在START条件成功发出后,硬件不会自动清除它。需要软件在适当的时候清0。 |
| 4 | STO | 停止条件标志。主机模式的“刹车”。 设置STO=1:主机将产生一个STOP条件。STOP条件成功发出后,硬件会自动清除此位。 在从机模式下的妙用:当从机检测到异常(如收到不应由自己处理的数据)时,设置STO=1可以使自身硬件状态机复位到“非寻址从机”模式,相当于一次软复位,而不会向总线发送STOP信号。 |
| 3 | SI | I2C中断标志。这是整个状态机驱动的“节拍器”。 当I2C接口进入25个有效状态中的任何一个(即发生了一个需要软件干预的事件)时,硬件将其置1。如果总中断(EA)和I2C中断(EI2C)已开启,将触发中断。 最重要规则:SI必须由软件写0来清除。清除SI是让硬件继续执行下一个操作(如发送下一字节、接收ACK等)的唯一方式。 |
| 2 | AA | 应答标志。控制下一个ACK时钟脉冲时,本机是否发出应答信号(SDA拉低)。 AA=1:表示“应答”。在以下情况有效:1) 收到自己的从机地址;2) 收到广播地址且GC位使能;3) 在主机接收或从机接收模式下收到数据字节。 AA=0:表示“非应答”。通常用于主机接收模式的最后一个字节,告知从机“不要再发数据了”。 |
| 1 | - | 保留位。必须保持为0。 |
| 0 | CRSEL | SCL时钟源选择。这是配置通信速率的关键。 CRSEL=1:SCL时钟由Timer1溢出产生。此时I2C速率 = Timer1溢出率 / 2。Timer1需工作在8位自动重载模式(模式2)。这种方式速率可调范围宽,但会占用一个定时器。 CRSEL=0(更常用):使用内部独立的SCL时钟发生器,其频率由 I2SCLH和I2SCLL寄存器决定。这是最灵活且不占用额外资源的方式。 |
实操心得:
I2CON的配置顺序有讲究。一个安全的初始化流程是:先配置好I2SCLH/L或Timer1(设定速率),再配置I2ADR(如果是从机),然后最后才将I2EN置1。避免在功能使能后总线出现不可预料的动作。
3.2 数据通道:I2DAT寄存器
I2DAT(地址DAh)是数据进出的大门。它有两个关键特性:
- 双向性:当你要发送数据时,把数据写入
I2DAT;当硬件接收完一个字节后,数据就放在I2DAT里等你读取。 - 访问时机:只能在SI=1时访问。当SI=0时,硬件可能正在移位数据,此时读写
I2DAT会导致数据错误。手册强调“Data in I2DAT remains stable as long as the SI bit is set.”,因此我们的中断服务程序(ISR)标准操作是:读状态→根据状态码决定读或写I2DAT→清SI。
3.3 状态导航仪:I2STAT寄存器
I2STAT(地址D9h)是一个只读寄存器,高5位(bit7-bit3)组成了26个可能的状态码。它是你判断“现在发生了什么”和决定“接下来该做什么”的唯一依据。状态码0xF8表示无状态信息(总线空闲),其他25个代码对应了主发送、主接收、从接收、从发送四种模式下的各个关键节点。
如何使用状态码?这就是查表法的精髓。手册中的Table 2到Table 5是必须打印出来放在手边的“驾驶指南”。例如,在主机发送模式下,你发出START条件后,进入中断发现I2STAT = 0x08。查Table 2,对应状态是“A START condition has been transmitted”。软件响应(Application software response)一栏告诉你,下一步应该“Load SLA+W”(将7位从机地址+写方向位写入I2DAT)。操作完I2DAT后,你需要设置I2CON的STA、STO、SI、AA位为表格中指定的值(通常是0,0,0,x,即清STA、STO、SI,AA任意),然后清除SI位,硬件就会自动将你刚写入I2DAT的地址发送出去。
3.4 从机身份牌:I2ADR寄存器
I2ADR(地址DBh)仅在从机模式下有意义。它的bit7-bit1存放本设备的7位I2C从机地址。bit0是GC(General Call)位,置1则使能响应广播地址0x00。在主机模式下,可以忽略此寄存器。
3.5 速率调节器:I2SCLH与I2SCLL寄存器
当CRSEL=0时,SCL时钟由这两个寄存器控制。它们分别定义了SCL高电平和低电平持续的时间,单位是PCLK(外设时钟)的周期数。
计算公式:I2C比特率 = fPCLK / [2 * (I2SCLH + I2SCLL)]
配置要点:
I2SCLH和I2SCLL的值建议都大于3,以确保稳定的时序。- 两者之和决定了频率,但它们的比值决定了占空比。标准I2C协议要求SCL高、低电平时间都需要满足最小要求,通常配置为50%占空比(即
I2SCLH = I2SCLL)最保险。 - 最终速率必须在I2C标准规定的范围内(通常模式0-100 kHz,快速模式≤400 kHz)。例如,当
fPCLK = 12 MHz,目标速率100kHz时,计算总和值:I2SCLH + I2SCLL = fPCLK / (2 * bitrate) = 12,000,000 / (2 * 100,000) = 60。若取50%占空比,则I2SCLH = I2SCLL = 30。
4. 四大操作模式状态机实战解析
理论说再多,不如一行代码。下面我们以最常见的主机发送模式为例,拆解整个状态机的跳转流程,并给出伪代码级别的思路。其他模式遵循相同的查表逻辑。
4.1 主机发送模式(Master Transmitter)流程拆解
假设我们要向一个地址为0x50的EEPROM写入一个字节数据0xAB。流程如下:
初始化:
// 假设PCLK=12MHz, 目标100kHz I2SCLH = 30; I2SCLL = 30; I2CON = 0x00; // 先关闭I2C,清空所有控制位 I2CON |= (1 << 2); // 设置AA=1 (默认应答) I2CON |= (1 << 6); // 设置I2EN=1,使能I2C模块 // 此时SI=0, STA=0, STO=0,总线空闲启动传输(发出START):
I2CON |= (1 << 5); // 设置STA=1,命令硬件发出START条件 // 硬件尝试获取总线,成功后发出START,然后置SI=1,并进入状态0x08中断服务程序(ISR)处理状态0x08:
void I2C_ISR() interrupt X { unsigned char status = I2STAT; switch(status) { case 0x08: // START已发出 I2DAT = 0xA0; // SLA+W: 0x50 << 1 | 0 = 0xA0 I2CON &= ~0x38; // 清除STA, STO, SI位 (STA=0, STO=0, SI=0) // 注意:这里是通过写I2CON来清SI,同时确保STA,STO为0 break; // ... 其他状态码 } }清SI后,硬件自动发送
I2DAT中的地址字节0xA0。处理从机应答(状态0x18或0x20): 地址发送后,从机应回复ACK。硬件接收ACK后,再次置SI=1。
- 若收到ACK,状态变为
0x18。 - 若未收到ACK(NACK),状态变为
0x20(地址错误或从机不存在)。 在ISR中处理0x18:
case 0x18: // SLA+W已发送,收到ACK I2DAT = 0xAB; // 准备要发送的数据字节 I2CON &= ~0x28; // 清除STA和SI位 (STO保持0, AA保持原样) break;清SI后,硬件发送数据字节
0xAB。- 若收到ACK,状态变为
处理数据应答(状态0x28或0x30): 数据发送后,从机会对数据回复ACK。
- 若收到ACK,状态变为
0x28。 - 若收到NACK,状态变为
0x30(可能从机无法接收更多数据)。 在ISR中处理0x28(假设只发一个字节,然后停止):
case 0x28: // 数据已发送,收到ACK I2CON |= (1 << 4); // 设置STO=1,命令硬件发出STOP条件 I2CON &= ~0x28; // 清除STA和SI位 // 硬件发出STOP后,会自动清STO位,总线释放 break;- 若收到ACK,状态变为
结束:STOP条件发出后,总线恢复空闲,状态码回到
0xF8。
4.2 主机接收、从机接收/发送模式要点
- 主机接收模式:流程与发送类似,但在发送完地址(SLA+R)后,需要将AA位清零,以便在接收最后一个字节后回复NACK。关键状态码是
0x40(地址ACK)、0x50(数据已收,ACK已发)、0x58(数据已收,NACK已发,准备停止)。 - 从机模式:核心是正确设置
I2ADR和AA位。从机的动作完全由主机发起,其状态码(如0x60、0xA8)表示自己被寻址或收到数据,软件只需根据状态码响应(读/写I2DAT,设置AA决定是否应答后续数据)。 - 重复起始条件(Repeated START):在一次通信中,不释放总线(不发STOP)直接发一个新的START。这在组合读写操作时非常有用(例如,先写EEPROM存储地址,再发起读操作)。实现方法是在某些状态(如
0x28)下,不设置STO,而是设置STA=1并清SI,硬件就会发出一个重复START(状态0x10)。
5. 实战避坑指南与高级技巧
手册不会告诉你的那些事,往往决定了项目的成败。
5.1 常见问题与排查清单
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 程序卡死,无任何反应 | 1. I2C未使能(I2EN=0)。 2. 上拉电阻未接或阻值过大(常用4.7kΩ)。 3. SI位未正确清除,导致硬件停滞。 | 1. 检查I2CON寄存器,确认I2EN=1。2. 用示波器或逻辑分析仪查看SDA/SCL是否有上拉高电平。 3.最常用:在调试时,在I2C中断入口处设置断点,看是否能进入。若不能,检查中断总开关EA和I2C中断使能位EI2C。 |
能进入中断,但状态码一直是0xF8 | 总线始终处于空闲状态,STA位可能设置后未成功产生START。 | 1. 确认总线是否被其他设备占用(用逻辑分析仪看)。 2. 检查设置STA位后,是否因为总线忙而处于等待状态?可以尝试在设置STA前,先强制发一个STOP(STO=1)清空总线状态。 |
发送地址后,总是进入状态0x20(NACK) | 1. 从机地址错误(7位 vs 8位混淆)。 2. 从机设备不存在、未上电或损坏。 3. 总线时序太快,从机跟不上。 | 1.经典错误:确保写入I2DAT的是`(slave_addr << 1) |
通信偶尔失败,出现状态0x38(仲裁丢失) | 在多主系统中,另一个主机正在使用总线。 | 1. 检查系统是否真的存在多主机。如果是单主机,可能是干扰或软件错误设置了STA。 2. 在状态 0x38的处理中,应按照手册将I2C接口复位到从机模式,或等待后重试。 |
| 从机模式无法被寻址 | 1.I2ADR寄存器设置错误。2. AA位为0,导致不响应自身地址。 3. 从机功能未正确初始化。 | 1. 确认I2ADR中写入的是7位地址,不是8位。2. 在从机初始化时,必须设置 AA=1。3. 从机模式也需要使能I2C(I2EN=1)。 |
5.2 软件框架设计建议
直接在主循环里轮询SI位和状态码是低效且容易出错的。强烈建议使用中断驱动。
状态机函数:在中断服务程序(ISR)里,不要写冗长的处理代码。而是读取
I2STAT后,调用一个对应的状态处理函数。这样主程序清晰,也便于维护。void I2C_StateHandler(unsigned char status) { switch(status) { case 0x08: Mt_Start(); break; case 0x18: Mt_SendData(); break; case 0x28: Mt_StopOrRepeatStart(); break; // ... 处理其他状态 default: // 错误处理 I2C_ErrorRecovery(); break; } }超时机制:I2C总线可能因为从机故障而被挂起(SCL被拉低)。必须在主程序中为每次I2C操作(如发送一帧数据)添加超时判断。用一个定时器或软件循环计数,如果超时仍未完成,则执行恢复操作(如发送STOP,重新初始化)。
错误恢复:设计一个
I2C_Recovery()函数。当检测到多次失败或超时时,调用它。这个函数可以:先尝试发送STOP条件(STO=1);如果无效,则暂时关闭I2C模块(I2EN=0),重新初始化GPIO口模拟几个SCL脉冲(“喂时钟”)以释放被锁死的从机,最后重新初始化I2C模块。
5.3 时钟源选择与低功耗考量
- Timer1时钟源(CRSEL=1):适用于对时钟精度要求不高,但需要极低速I2C通信(可低于标准模式)或想与其他定时任务同步的场景。注意它会占用Timer1资源。
- 内部SCL发生器(CRSEL=0):绝大多数情况下的推荐选择。独立灵活,不占用其他外设。在进入低功耗模式(如Idle)前,如果I2C作为从机需要被唤醒,则必须保持I2C模块供电和使能。此时,内部时钟发生器虽然不工作(因为PCLK可能停止),但接口的输入检测逻辑仍在运行,可以检测到START条件并产生中断唤醒MCU。
调试时,一定要用逻辑分析仪或示波器抓取SDA和SCL的波形。对照I2C协议看START、STOP、ACK、数据位的时序是否合规,这是排查硬件连接和软件时序问题最直接有效的方法。P89LPC920的I2C接口虽然需要较多的软件干预,但一旦你掌握了其状态机的工作模式,它就是一种非常可靠和高效的通信工具。这份手册提供了所有必要的零件,而你的代码,则是让这些零件协同工作的蓝图。