
1. 项目概述P89LPC970/971/972的通信接口核心在嵌入式系统开发中微控制器与外设或其他控制器之间的通信是构建复杂功能的基础。无论是读取传感器数据、驱动显示屏还是实现多机协同都离不开可靠、高效的通信接口。NXP的P89LPC970/971/972系列微控制器作为经典的80C51架构增强型产品其内置的增强型UART和标准I2C接口为开发者提供了两种风格迥异但同样强大的串行通信解决方案。UART以其简单、异步、点对点的特性成为调试、日志打印和与简单外设通信的首选而I2C则以其同步、多主从、总线式的架构在连接多个同类器件如EEPROM、传感器、IO扩展芯片时展现出极高的布线效率和灵活性。理解这两个接口在P89LPC97x上的具体实现、增强功能以及实际应用中的“坑”与技巧是充分发挥这颗MCU潜力的关键。本文将深入解析这两个接口的工作模式、寄存器配置、高级功能如UART的自动地址识别与双缓冲以及I2C的四种操作模式并结合实际代码示例和调试经验为你呈现一份从原理到实战的完整指南。2. UART接口深度解析与模式实战通用异步收发传输器UART是嵌入式领域最古老也最经典的通信接口之一。P89LPC97x的UART在标准80C51 UART的基础上进行了多项增强使其在稳定性、灵活性和效率上都有显著提升。2.1 UART四种工作模式详解P89LPC97x的UART支持四种工作模式由SCON寄存器中的SM0和SM1位决定。理解每种模式的差异是正确配置和应用的前提。模式0同步移位寄存器模式。这是一个较为特殊的模式并非我们通常理解的异步串行通信。在此模式下RXD引脚用于数据的输入/输出TXD引脚输出移位时钟。每次传输固定为8位数据低位LSB在先波特率固定为CPU时钟频率CCLK的1/16。这个模式通常用于扩展I/O口例如连接74HC595移位寄存器来驱动LED点阵或数码管。需要注意的是在此模式下必须禁用双缓冲功能SSTAT.7/DBMOD必须为0否则功能将不正常。模式18位UART可变波特率。这是最常用的异步通信模式。每帧数据包含1个起始位逻辑0、8个数据位LSB在先、1个停止位逻辑1。接收时停止位会存入SCON寄存器的RB8位。其波特率可变来源可以是定时器1Timer 1的溢出率也可以是独立的波特率发生器BRG。这是与PC进行串口通信、连接GPS模块、蓝牙模块的典型模式。模式29位UART固定波特率。每帧数据包含1个起始位、8个数据位、1个可编程的第9数据位、1个停止位。发送时第9数据位来自SCON寄存器的TB8位你可以自由设置其为0或1也可以将程序状态字PSW中的奇偶校验位P赋值给它用于简单的奇偶校验传输。接收时第9数据位存入RB8停止位不保存。波特率固定为CCLK的1/32或1/16具体由PCON寄存器中的SMOD1位决定SMOD10时为1/32SMOD11时为1/16。模式39位UART可变波特率。除了波特率来源与模式1相同来自定时器1或波特率发生器其余特性与模式2完全一致。模式2和模式3因其包含一个可编程的第9位常被用于实现多机通信。主机可以通过设置第9位为1来发送地址帧为0来发送数据帧从机则可以借助SM2位实现只在收到地址帧第9位为1时才产生中断从而过滤数据帧减少CPU中断负载。2.2 波特率生成定时器1与独立波特率发生器精确的波特率是串口通信稳定的基石。P89LPC97x提供了两种波特率生成方案通过BRGCON寄存器的SBRGS位进行选择。方案一使用定时器1Timer 1。这是传统80C51的方式。波特率计算公式为波特率 CCLK / [32 * 12 * (256 - TH1)]当SMOD10时 或波特率 CCLK / [16 * 12 * (256 - TH1)]当SMOD11时 其中TH1是定时器1的重载值。这种方式需要占用一个硬件定时器并且在单片机进入空闲或掉电模式时如果定时器1停止工作串口通信也会中断。方案二使用独立波特率发生器BRG。这是P89LPC97x的增强功能。波特率由BRGR1和BRGR0两个寄存器组成的16位重载值决定计算公式为波特率 CCLK / [16 * (BRG_RL 1)]其中BRG_RL是BRGR1和BRGR0组成的16位数值。独立BRG的最大优势在于其时钟源是CCLK与定时器1无关。这意味着即使定时器1用于其他用途或者单片机处于某些低功耗模式只要CCLK仍在运行UART通信依然可以继续。这为低功耗应用中的串口唤醒等功能提供了便利。关键操作更新BRGR1/BRGR0寄存器这是一个极易出错的操作点。BRGR1和BRGR0寄存器只能在波特率发生器禁用时BRGCON.0/BRGEN 0进行写入。如果在这两个寄存器被写入时BRGEN1会产生不可预知的结果通常会导致波特率严重错误通信彻底失败。安全的操作流程是先关闭BRGBRGEN0然后写入BRGR1和BRGR0最后再重新开启BRGBRGEN1。2.3 增强功能解析双缓冲、帧错误与地址识别双缓冲传输传统UART在发送数据时需要等待一个字节完全发送完毕TI标志置位才能写入下一个字节否则会覆盖尚未发送的数据。P89LPC97x的UART引入了发送双缓冲功能通过设置SSTAT.7/DBMOD1启用。启用后CPU可以在当前字节正在从移位寄存器发送出去的同时将下一个字节预先写入SBUF缓冲寄存器。这允许连续发送多个字节时字节之间仅有一个停止位间隔极大地提高了连续发送的效率减少了CPU等待时间。双缓冲仅适用于模式1、2、3在模式0下必须禁用。帧错误与间隔检测帧错误FE在接收端检测到停止位为逻辑0时置位这通常意味着线路受到干扰或波特率不匹配。间隔检测BR则在检测到接收线RXD上连续11个位时间都是逻辑0时置位。间隔信号常用于某些通信协议中表示帧开始或复位。一个重要的细节是间隔条件必然满足帧错误条件因为停止位也是0所以检测到间隔时FE标志也会同时置位。自动地址识别这是多机通信的硬件加速器。在模式2或3下通过设置SM21启用。你需要配置两个寄存器SADDR本机地址和SADEN地址掩码。SADEN用于定义SADDR中哪些位是必须匹配的掩码位为1哪些位是“不关心”的掩码位为0。当主机发送的地址字节第9位为1时从机会将接收到的地址与(SADDR SADEN)计算出的“给定地址”进行比较。只有匹配的从机才会置位RI中断标志不匹配的从机则直接忽略该帧。这避免了每个从机的CPU都需要软件判断地址大幅降低了中断负载。广播地址则是(SADDR | SADEN)所有设置了SM2的从机在收到广播地址时都会响应。3. I2C总线接口原理与四种操作模式I2CInter-Integrated Circuit总线是一种由Philips现NXP开发的双线制、同步、多主从的串行通信总线。它仅需两根线——串行数据线SDA和串行时钟线SCL就能连接多个设备极大地节省了微控制器的IO口资源和PCB布线空间。3.1 I2C总线基础协议与P89LPC97x实现I2C总线上的所有通信都遵循一套严格的时序协议由主设备发起和控制。基本信号包括起始条件S当SCL为高电平时SDA线上一个从高到低的跳变。停止条件P当SCL为高电平时SDA线上一个从低到高的跳变。数据有效性在SCL高电平期间SDA线上的数据必须保持稳定。数据只能在SCL为低电平时改变。应答ACK/NACK每个字节8位传输后接收方需要在第9个时钟脉冲期间拉低SDA线作为应答ACK。若SDA保持高电平则为非应答NACK。P89LPC97x的I2C接口是一个“字节型”接口这意味着大部分底层位时序如起始、停止、应答位的生成与检测由硬件自动处理开发者主要通过读写数据寄存器I2DAT和控制寄存器I2CON来操作极大简化了软件复杂度。接口支持高达400 kHz快速模式的通信速率。3.2 四种操作模式流程与寄存器控制P89LPC97x的I2C接口可以工作在四种模式模式间的转换完全由硬件根据总线状态和软件配置自动进行。主发送模式Master Transmitter微控制器作为主设备向从设备发送数据。软件设置I2CON寄存器发出起始条件STA1。硬件发出S信号后将SDA控制权交给软件。软件需立即向I2DAT寄存器写入目标从设备的7位地址和写方向位R/W0。地址字节发送完毕后硬件会检测从机的应答ACK。如果收到ACK状态寄存器I2STAT会进入一个特定的“主发送器已发送地址并收到ACK”的状态码例如0x18。软件查询到这个状态码后开始向I2DAT写入要发送的数据字节。每发送完一个字节硬件都会等待并报告从机的应答状态。数据发送完毕后软件设置STO1发出停止条件。主接收模式Master Receiver微控制器作为主设备从从设备读取数据。前半部分与主发送模式类似但写入I2DAT的地址字节方向位为读R/W1。收到地址ACK后状态码变为“主接收器已发送地址并收到ACK”例如0x40。软件需要先设置AA位应答使能然后发出一个“虚读”命令实际上是通过操作I2CON触发时钟硬件才会开始从SDA线上读取数据到I2DAT。读取一个字节后软件可以决定是否发送ACK。如果希望继续读取下一个字节则在读取I2DAT后保持AA1并再次触发如果读取的是最后一个字节则在读取前设置AA0这样读完最后一个字节后会回复NACK然后发出停止条件。从接收模式Slave Receiver微控制器作为从设备接收主设备发来的数据。首先软件需要将自己的7位从机地址写入I2ADR寄存器并启用自身地址识别AA1。当总线上有主机发送的地址与I2ADR匹配时硬件会自动应答并产生中断状态码指示“从接收器地址已识别并收到ACK”。进入中断服务程序软件读取状态码然后从I2DAT寄存器读取主机发来的数据字节。每读完一个字节软件需要通过设置AA位来决定是否对下一个字节进行应答。如果AA1则继续接收如果AA0则在收到下一个字节的地址匹配后从机将不应答从而释放总线。从发送模式Slave Transmitter微控制器作为从设备向主设备发送数据。同样先设置I2ADR和AA1。当主机发送的地址R/W1匹配时硬件应答并进入“从发送器地址已识别并收到ACK”状态。软件在中断中将需要发送的数据写入I2DAT寄存器。硬件会自动将数据发送出去并等待主机的应答ACK。软件需要根据主机的应答通过状态码判断来决定是继续发送下一个数据AA保持为1写入新数据还是结束发送可能在主机发送NACK或停止条件后。核心经验状态机驱动编程I2C编程的核心是状态机。硬件在完成每一个动作如发送完地址、收到一个字节、发出ACK后都会更新状态寄存器I2STAT到一个特定的代码。你的中断服务程序ISR绝不能假设流程而必须首先读取I2STAT的值然后根据这个状态码通过一个switch-case或查表结构执行对应的操作如写数据、读数据、设置AA、发出停止信号等。任何不按状态码操作的尝试都会导致总线挂死或通信错误。官方数据手册中的I2C状态流程图是必须打印出来贴在墙上的参考资料。4. UART与I2C的实战配置与代码示例理解了原理我们进入实战环节。这里以P89LPC972为例假设使用内部RC振荡器CCLK频率为7.3728MHz这是一个非常常见的频率因为它能被许多标准波特率整除。4.1 UART模式1配置115200波特率8N1我们的目标是配置UART为最常用的8位数据、无校验、1位停止位8N1波特率115200使用独立波特率发生器。步骤1计算波特率发生器重载值。根据公式波特率 CCLK / [16 * (BRG_RL 1)]推导出BRG_RL (CCLK / (16 * 波特率)) - 1代入数值BRG_RL (7372800 / (16 * 115200)) - 1 (7372800 / 1843200) - 1 4 - 1 3所以BRGR1 0x00,BRGR0 0x03。步骤2配置相关寄存器。#include REG972.H // 包含P89LPC972的SFR定义 void UART_Init(void) { // 1. 禁用波特率发生器以便安全配置BRGRx BRGCON ~0x01; // BRGEN 0 // 2. 设置波特率重载值 BRGR0 0x03; // 低字节 BRGR1 0x00; // 高字节 // 3. 选择波特率发生器作为时钟源并启用它 BRGCON 0x03; // SBRGS1, BRGEN1 (0b00000011) // 4. 配置串口模式模式18位UART可变波特率 // SM00, SM11, SM20, REN1(使能接收) SCON 0x50; // 0b01010000 // 5. 可选使能总中断和串口中断 // ES 1; // 使能串口中断 // EA 1; // 使能全局中断 }步骤3发送和接收函数查询方式。void UART_SendByte(unsigned char dat) { SBUF dat; // 将数据写入发送缓冲区启动发送 while(TI 0); // 等待发送完成中断标志 TI 0; // 软件清除发送中断标志 } unsigned char UART_ReceiveByte(void) { while(RI 0); // 等待接收完成中断标志 RI 0; // 软件清除接收中断标志 return SBUF; // 读取接收到的数据 }避坑指南TI和RI标志的清除TI和RI标志必须由软件清除。一个常见的错误是在中断服务程序中读取或发送数据后忘记清除它们导致程序卡死在等待标志的循环中或者反复进入中断。在查询方式中while(TI0);之后必须紧跟TI0;。4.2 I2C主发送模式代码框架以下是一个I2C主设备向EEPROM假设地址0xA0写入一个字节数据的简化示例采用查询方式而非中断更易于理解流程。#include REG972.H #define I2C_EEPROM_ADDR_W 0xA0 // EEPROM写地址 (7位地址左移1位末位写0) bit I2C_Start(void) { I2CONSET 0x20; // STA1, 发起起始条件 while (!(I2CONSET 0x08)); // 等待SI标志置位表示状态改变 I2CONCLR 0x28; // 清除STA和SI标志 // 读取I2STAT判断是否为0x08起始条件已发出 if (I2STAT ! 0x08) return 0; // 失败 return 1; // 成功 } bit I2C_SendAddrOrData(unsigned char dat) { I2DAT dat; // 写入地址或数据 I2CONCLR 0x28; // 清除STA, SI让硬件继续 while (!(I2CONSET 0x08)); // 等待SI置位 I2CONCLR 0x08; // 清除SI标志 // 检查状态0x18地址W已发送收到ACK或0x28数据已发送收到ACK if ((I2STAT ! 0x18) (I2STAT ! 0x28)) return 0; return 1; } void I2C_Stop(void) { I2CONSET 0x10; // STO1, 发起停止条件 I2CONCLR 0x08; // 清除SI标志 while (I2CONSET 0x10); // 等待STO标志被硬件自动清除表示停止完成 } void I2C_WriteEEPROM(unsigned char addr, unsigned char dat) { // 1. 发起起始条件 if (!I2C_Start()) goto error; // 2. 发送EEPROM器件地址写 if (!I2C_SendAddrOrData(I2C_EEPROM_ADDR_W)) goto error; // 3. 发送要写入的EEPROM内部地址 if (!I2C_SendAddrOrData(addr)) goto error; // 4. 发送要写入的数据 if (!I2C_SendAddrOrData(dat)) goto error; // 5. 发起停止条件 I2C_Stop(); return; error: // 出错处理强制产生停止条件复位总线 I2CONSET 0x10; // STO1 I2CONCLR 0x28; // 清除STA, SI // ... 其他错误处理代码 }关键细节STO标志的自动清除当软件设置STO1发起停止条件后硬件在总线上产生完停止信号后会自动将STO位清零。因此代码中通过while (I2CONSET 0x10);来等待停止操作真正完成这是一个必要的同步步骤。5. 高级应用、调试与常见问题排查掌握了基础配置后面对复杂的实际项目一些高级功能和调试技巧能让你事半功倍。5.1 UART双缓冲与自动地址识别实战双缓冲高效发送字符串void UART_SendString_DB(unsigned char *str) { SSTAT | 0x80; // 设置DBMOD1启用双缓冲 SSTAT ~0x40; // 设置INTLO0发送中断在停止位开始时产生提前通知 // 发送第一个字符会立即产生中断如果中断使能 SBUF *str; TI 0; // 清除初始中断标志如果是查询法则忽略 while (*str ! \0) { // 在第一个字符的停止位期间INTLO0中断产生此时可以安全写入下一个字符 // 查询方式等待TI置位表示双缓冲空 while(TI 0); TI 0; SBUF *str; } // 等待最后一个字符发送完成 while(TI 0); TI 0; SSTAT ~0x80; // 可选关闭双缓冲 }启用双缓冲后你可以更紧凑地安排数据发送减少字节间的空闲时间特别适合需要高速连续发送数据的场合如发送图像数据块或长数据包。自动地址识别多机通信配置假设有三个从机地址配置如下从机1地址0x02只关心低3位即地址0x02, 0x0A, 0x12...都响应掩码决定。从机2地址0x04只关心低3位。从机3地址0x06只关心低3位。 我们希望主机通过地址0x00低3位为000可以广播给所有从机。// 从机1配置 SADDR 0x02; // 地址0000 0010 SADEN 0xF9; // 掩码1111 1001 (低3位中只有bit0必须匹配为0) // 给定地址 SADDR SADEN 0000 000X // 广播地址 SADDR | SADEN 1111 1011 (0xFB) // 从机2配置 SADDR 0x04; // 0000 0100 SADEN 0xFA; // 1111 1010 (只有bit1必须匹配为0) // 给定地址 0000 0X00 // 从机3配置 SADDR 0x06; // 0000 0110 SADEN 0xFC; // 1111 1100 (只有bit2必须匹配为0) // 给定地址 0000 0XX0 // 主机广播地址0x00 (0000 0000)时 // 从机1检查: (0x00 0xF9) (0x02 0xF9)? - (0x00) (0x00) 匹配 // 从机2检查: (0x00 0xFA) (0x04 0xFA)? - (0x00) (0x00) 匹配 // 从机3检查: (0x00 0xFC) (0x06 0xFC)? - (0x00) (0x04) 不匹配 // 等等从机3为什么不匹配因为(0x06 0xFC) 0x04要求bit20但bit11。而0x00的bit10不满足。 // 这说明我们的掩码设置有问题。我们希望广播地址0x00能呼叫所有从机需要确保(SADDR SADEN)的结果中“必须匹配”的位在广播地址里都是正确的。 // 修正让所有从机的“必须匹配”位在广播地址里都为0。 // 从机3的SADEN改为0xF8 (1111 1000)则给定地址0000 0XX0广播地址0x00就能匹配了。这个例子揭示了自动地址识别配置的精髓掩码SADEN决定了地址比较的“严格程度”。你需要仔细规划地址和掩码确保每个从机有唯一的“给定地址”用于单播同时有一个共同的“广播地址”用于群发。5.2 I2C总线仲裁与时钟同步当总线上有多个主设备时I2C协议通过仲裁机制防止数据冲突。如果两个主设备同时开始发送它们会继续发送直到出现差异。当某个主设备发送高电平释放SDA而另一个发送低电平拉低SDA时发送高电平的主设备会检测到总线电平与自己输出不符立即失去仲裁关闭其输出驱动器转为从设备监听总线。获胜的主设备继续通信整个过程数据不会损坏。时钟同步则发生在多个主设备产生时钟时。SCL线是“线与”的低电平周期由时钟低电平最长的设备决定高电平周期由时钟高电平最短的设备决定。这意味着时钟速度慢的设备会拉低整体总线速度。在调试多主系统时如果发现通信速度变慢需要检查是否有时钟同步发生。5.3 通信故障排查清单与实战技巧无论是UART还是I2C通信失败时遵循系统性的排查步骤可以快速定位问题。UART通信失败排查物理层检查线是否接反TX接RXRX接TX地线是否共地波特率是否双方严格一致这是最常见的问题。电气层检查电平是否匹配P89LPC97x是TTL电平0V/3.3V或5V如果连接RS232设备需要电平转换芯片。线路过长是否有干扰可以尝试降低波特率或增加滤波电容。软件配置检查波特率计算是否正确使用示波器测量TXD引脚输出的位宽度计算实际波特率。一个115200的位宽大约是8.68us。数据格式是否匹配数据位、停止位、校验位设置是否与对方一致常用8N1。中断标志是否清除在查询法中while(TI0);后必须TI0;否则下次发送会卡死。双缓冲配置是否冲突在模式0下必须确保DBMOD0。使用逻辑分析仪或示波器这是最强大的调试工具。直接抓取TXD和RXD信号查看起始位、数据位、停止位是否完整电平是否干净波特率是否准确。I2C通信失败排查总线状态检查首先测量SCL和SDA线的静态电平。它们都应该被上拉电阻拉到高电平通常3.3V或5V。如果任何一线被持续拉低说明有设备故障或软件死锁比如在错误的时间输出了低电平。上拉电阻是否接了阻值是否合适总线电容大线长、设备多时需要减小阻值以加快上升沿但会增加功耗。通常4.7kΩ到10kΩ是常用范围。地址问题7位设备地址左移一位后最低位是R/W位。写操作时地址字节为(addr1) | 0读操作时为(addr1) | 1。这是最容易搞错的地方之一。时序问题通信速率是否超过设备极限某些EEPROM在400kHz下工作可能不稳定可以尝试降到100kHz。检查启动代码中是否对I/O口模式进行了正确配置需要开漏或准双向模式。软件状态机这是I2C编程最难的部分。务必在每次操作后检查I2STAT寄存器并根据状态码决定下一步。一个状态处理错误就可能导致总线锁死。在超时处理中一个可靠的恢复方法是连续发送几个时钟脉冲通过模拟I/O操作SCL直到SDA被释放为高然后发送一个停止条件。从设备忙像EEPROM这类设备写入周期需要几毫秒时间。在此期间它对I2C总线不响应发送NACK。主机必须通过“发送起始条件设备地址写 查询ACK”的方式进行轮询直到收到ACK才能进行下一步操作。一个实用的I2C总线恢复函数void I2C_Bus_Recovery(void) { unsigned char i; I2CONCLR 0x6C; // 清除所有控制位 (STA, STO, SI, AA) // 将SDA和SCL配置为GPIO输出模式这里需要根据你的具体端口配置 P1M1 ~0x03; // 假设SDAP1.0, SCLP1.1设置为准双向 P1M2 ~0x03; // 如果SDA被卡在低电平通过时钟脉冲尝试“解救”它 if ((P1 0x01) 0) { // 检查SDA是否为低 for (i 0; i 9; i) { // 最多尝试9个时钟脉冲 P1 ~0x02; // SCL拉低 Delay_us(5); // 短暂延时 P1 | 0x02; // SCL释放 Delay_us(5); if (P1 0x01) break; // 如果SDA变高成功 } } // 发送一个停止条件SCL高时SDA从低到高 P1 ~0x01; // SDA拉低 Delay_us(5); P1 | 0x02; // SCL拉高 Delay_us(5); P1 | 0x01; // SDA拉高 Delay_us(5); // 将端口恢复为I2C功能具体配置参考用户手册 // ... 重新初始化I2C相关寄存器 }这个函数在程序跑飞或从设备异常导致总线锁死时非常有用它通过GPIO模拟时钟信号尝试让挂在总线上的设备完成当前操作并释放SDA线最后模拟一个停止条件来复位总线状态。