深入解析P89V660单片机ISP/IAP编程与双I2C总线驱动开发
1. 项目概述与核心价值在嵌入式开发的日常工作中我们经常会遇到一个经典难题产品已经焊在板子上甚至已经部署到现场却发现程序有bug需要修复或者需要增加新功能。这时候如果每次都要把芯片吹下来用编程器烧录不仅效率低下成本高昂在工业现场几乎是不可能完成的任务。这正是ISP在系统编程和IAP在应用编程技术大显身手的地方。它们就像给单片机装上了“空中升级”的能力允许我们通过芯片上已有的通信接口比如最常用的串口直接对内部的Flash存储器进行读写擦除实现固件的远程更新。今天要深入聊的是NXP恩智浦经典的P89V660/662/664系列增强型80C51单片机。我手头有不少项目用过这个系列特别是那些需要稳定可靠、又要兼顾成本控制的工业传感和控制器。这个系列芯片最吸引我的两点一是其成熟且功能丰富的ISP/IAP机制让后期维护变得异常轻松二是它居然集成了两个独立的I2C总线接口这在同级别的51核单片机里并不多见对于需要连接多个I2C从设备比如温湿度传感器、EEPROM、RTC时钟等的场景来说简直是“神器”可以避免单条总线上的地址冲突和调度麻烦。简单来说P89V660/662/664就像一位“内外兼修”的选手对外通过双I2C接口轻松组建传感器网络对内通过成熟的Bootloader和IAP函数实现对自身“大脑”Flash程序存储器的在线管理和更新。无论是正在学习51单片机高级应用的工程师学生还是正在寻找稳定、可远程升级方案的嵌入式开发者理解这套机制都至关重要。接下来我就结合数据手册和实际调试经验把ISP/IAP编程和I2C总线接口这两块“硬骨头”拆开、揉碎讲清楚它们的工作原理、怎么用以及我踩过哪些坑。2. 深入拆解P89V660的Flash存储器架构要玩转ISP和IAP首先得摸清P89V660系列Flash存储器的“家底”。这就像你要给房子重新布线总得先知道承重墙在哪哪些是活动隔断。2.1 Flash组织与三种编程模式根据数据手册P89V660/662/664分别对应16KB、32KB、64KB的用户代码空间。这块Flash不是铁板一块而是被划分成128字节为一页Page的结构。“页擦除”是这个系列的一个关键特性意味着你可以单独擦除某一页128字节而不必动整个芯片这对于存储一些需要频繁修改的校准参数或日志数据非常有用。当然它也支持全片擦除Chip Erase会连安全位一起抹掉。芯片提供了三种“刷新”大脑的方法这也是ISP和IAP的核心区别所在IAP在应用编程这是最灵活的方式。你的用户应用程序跑在0000H地址开始的代码可以主动调用存放在Boot Block引导块里的底层擦写例程。简单说就是你的程序在运行过程中可以自己修改自己的某一部分代码或者数据。这常用于实现设备功能的动态加载、现场参数标定或者实现一个简单的文件系统。ISP在系统编程这种方式更侧重于“外部干预”。芯片出厂时在Flash的高地址区FC00H-FFFFH固化了一个Bootloader程序。通过特定的硬件时序如复位时拉高某个引脚或软件方式可以让芯片一上电就运行这个Bootloader而不是你的主程序。然后你就可以通过串口TXD/RXD使用特定的协议后面会详细讲Intel Hex格式给芯片发送新程序了。这常用于产线烧录、设备返厂升级或通过预留的调试接口进行更新。并行编程最传统的方式需要专用的并行编程器把芯片拆下来放到烧录座上操作。在批量生产初期或开发深度变砖的设备时可能会用到但显然不具备前两者的便利性。2.2 Boot Block、Boot Vector与Status Bit的玄机这是理解P89V660启动和编程模式切换的关键也是我早期调试时最容易糊涂的地方。Boot Block引导块这是一段物理上独立、逻辑上映射在FC00H-FFFFH地址范围的只读存储器。它里面存放了所有Flash操作读、写、擦除、安全位设置的底层驱动代码。无论是ISP模式下的Bootloader还是IAP模式下你的应用程序最终都是通过一个统一的入口点PGM_MTP地址通常在FFF0H来调用这些底层函数。你可以把Boot Block想象成芯片内部的一个“硬件驱动库”。Boot Vector引导向量和Status Bit状态位这两个是Flash中的特殊配置位共同决定了芯片上电后第一条指令从哪里执行。Status Bit这是关键开关。如果Status Bit为0单片机正常从0000H地址开始执行你的用户应用程序。如果Status Bit非0芯片就会进入“寻址”模式。Boot Vector当Status Bit非0时Boot Vector的值就作为程序计数器的高字节低字节固定为00H。例如出厂默认的Boot Vector是FCH那么上电地址就是(Boot Vector 8) | 0x00 0xFC00这正是固化ISP Bootloader的起始地址。硬件激活Bootloader即使Status Bit是0你仍然可以通过硬件方式强制进入ISP模式。方法是在芯片上电复位Power-on Reset过程中的特定时刻将RST引脚保持为高电平并配合串口信号。具体时序需要查数据手册但常见的做法是先确保串口工具就绪然后给目标板断电再上电在上电瞬间由串口工具如FlashMagic自动控制DTR/RTS信号来模拟这个时序。这样即使产品里烧录的是正常应用程序也可以通过这个“后门”进入Bootloader来升级。一个重要的实践细节当你通过ISP模式完成程序烧录后务必记得将Status Bit编程为0否则下次上电芯片又会傻傻地跑回Bootloader而不是你的新程序。很多新手烧录完程序发现芯片“没反应”问题往往就出在这里。2.3 IAP实战在用户程序中操作FlashIAP功能让你的应用程序具备了“自我进化”的能力。所有IAP操作都通过同一个软中断入口PGM_MTP地址FFF0H来调用。你需要像调用函数一样设置好参数寄存器然后跳转到这个地址。IAP调用通用流程准备参数根据你想执行的操作读ID、擦除块、编程字节、读字节等按照手册的表12设置寄存器R1功能代码、DPTR地址指针和ACC数据。喂狗考虑注意看每个功能代码都有两个值如00H和80H。带80H的选项表示在执行IAP操作前先“喂”一下看门狗定时器WDT防止操作时间过长导致看门狗复位。如果你的系统开启了看门狗务必使用带WDT喂狗的功能码否则可能在擦写过程中意外复位。发起调用使用汇编指令LCALL PGM_MTP或ACALL PGM_MTP取决于你的代码地址跳转到FFF0H。检查结果操作完成后结果会返回在ACC寄存器中。ACC 0x00表示成功非零则表示失败。示例用IAP擦除一个8KB的块假设我们要擦除第1个8KB块地址 0x0000 - 0x1FFF。查表可知擦除8KB块的功能码是0x01或0x81喂狗块选择码DPL 0x00。MOV R1, #81H ; 功能码擦除块且喂狗 MOV DPL, #00H ; 选择块0 (0k-8k) MOV DPH, #00H ; 高字节任意对于擦除块操作DPH无用 LCALL 0FFF0H ; 调用IAP入口 ; 调用后检查ACC是否为0 CJNE A, #00H, ERROR_HANDLER示例用IAP编程一个字节想要在地址0x2000处写入数据0x5A。MOV R1, #82H ; 功能码编程用户代码且喂狗 MOV DPTR, #2000H ; 目标地址 MOV A, #5AH ; 要写入的数据 LCALL 0FFF0H ; 调用IAP入口 CJNE A, #00H, ERROR_HANDLER重要提示IAP操作期间CPU会暂停执行你的程序代码直到Flash操作完成。这意味着中断必须关闭在调用IAP前务必用CLR EA关闭总中断操作完成后再SETB EA打开。否则中断发生时PC指针乱飞大概率导致程序跑飞或硬件错误。代码不能自修改你不能擦除或编程当前正在执行的那一页Flash代码通常的做法是将执行IAP操作的代码段以及相关的中断服务程序完全复制到RAM中运行或者确保它们位于不会被擦除的Flash区域比如Boot Block但用户一般写不进去。更常见的策略是IAP代码只负责更新应用程序区A区而IAP代码本身作为一个独立的、固定的引导程序存放在B区通过跳转切换。3. ISP协议深度解析与上位机通信实战如果说IAP是“自助餐”那么ISP就是“外卖服务”。你通过外部串口工具按照一套固定的“暗号”协议给芯片的Bootloader发送指令让它来帮你更新程序。3.1 ISP通信的建立从“U”开始P89V660的ISP Bootloader设计得很巧妙它支持自适应波特率。这省去了你非要匹配一个特定波特率的麻烦。建立连接的过程如下硬件连接确保目标板的VDD、VSS、RST、TXD、RXD这五个引脚连接到你的USB转串口工具。注意交叉连接目标的TXD接工具的RXD目标的RXD接工具的TXD。RST引脚通常需要通过一个电容接到VDD同时留出一个测试点或连接器便于上位机控制复位。发送同步字符上位机如FlashMagic、自己写的Python脚本首先发送一个大写字母UASCII码 0x55二进制 01010101。这个01交替的波形让Bootloader能够测量其位时间从而计算出当前系统的时钟频率并自适应地设置通信波特率。发送U后Bootloader会回显这个字符如果上位机收到回显的U说明物理链路和波特率自适应成功。切换至Hex记录模式连接建立后后续所有的通信都必须使用Intel Hex记录格式。Bootloader不再理会普通字符只解析Hex记录。3.2 Intel Hex记录格式详解这是ISP通信的“语言”。每一条记录都是一行ASCII字符串格式严格固定: NNAAAARR DD DD DD ... DD CCCRLF:起始符每条记录开头。NN一个字节用两个十六进制ASCII字符表示本记录中数据字节的数量。最大值是320x20。注意地址、类型、校验和不算在NN里。AAAA两个字节用四个十六进制ASCII字符表示数据区的起始地址。RR一个字节用两个十六进制ASCII字符表示记录类型。ISP主要用到00数据记录。用于发送要编程的数据。01文件结束记录。表示Hex文件发送完毕。03/04/05/06/07/08特殊命令记录。用于擦除、读ID、设置安全位等。DD数据区。每个数据字节用两个十六进制ASCII字符表示共有NN个。CC一个字节用两个十六进制ASCII字符表示校验和。计算方法是从NN开始到最后一个DD将所有字节的二进制值相加然后取和的低8位再计算其二进制补码即0x100 - sum 0xFF。CC加上前面所有字节包括CC自己的和低8位应该等于0。举例解析一条数据记录:100000000102030405060708090A0B0C0D0E0F70:起始符。10表示有16个数据字节。0000表示数据起始地址是0x0000。00表示这是数据记录。010203...0F这是16个数据字节0x01, 0x02, ... 0x0F。70校验和。计算0x10 0x00 0x00 0x00 0x01 ... 0x0F 0x190。取低8位0x90其补码为0x100 - 0x90 0x70。校验正确。3.3 核心ISP命令手册与使用示例Bootloader支持一系列通过03、04、05、06类型记录发送的命令。理解这些命令是编写或调试自定义ISP工具的基础。1. 擦除操作记录类型 03子功能码擦除块子功能 01:0300000301SS00CCSS是块代码00H (0-8K), 20H (8-16K), 40H (16-32K), 80H (32-48K), C0H (48-64K)。例擦除0-8K块:03000003010000FD擦除页子功能 08:0300000308AABBBBCCAA是页地址高字节BB是低字节。地址必须是128字节对齐的。例擦除地址0xE000所在的页:0300000308E000F2全片擦除子功能 07:03000003070000FA。这会擦除所有用户代码和安全位并将Boot Vector和Status Bit恢复为默认值。2. 编程与配置记录类型 03子功能码编程安全位子功能 05:0300000305SS00CCSS选择00(位1), 01(位2), 02(位3)。一旦编程相应的安全功能如读保护即被激活且通常不可逆除了全片擦除。编程状态位/引导向量子功能 06:0400000306SSDD00CCSS选择00(编程Status Bit), 01(编程Boot Vector), 02(编程6x/12x时钟模式位)。DD是当SS01时要编程的Boot Vector值。关键操作编程完成后将Status Bit设为0才能从用户程序启动。命令示例:0400000306000000FB设置Status Bit为0。3. 读取与校验记录类型 04, 05显示内存数据类型04子功能00:0500000400001FFF00D9请求显示地址0x0000到0x1FFF的内容。Bootloader会将数据以Hex记录格式回传。空白检查类型04子功能01:0500000400001FFF01D8检查指定地址范围是否全为0xFF。读器件ID/安全位类型05:020000050000F9读取制造商ID。其他子功能码可读设备ID、安全位状态等。4. 通信控制记录类型 06直接加载波特率类型06:02000006HHLLCCHHLL是定时器T2的重载值用于直接设定波特率跳过自动检测。一般用不到自适应波特率已经足够好用。上位机交互流程上位机发送一条Hex记录后会等待Bootloader的回应。成功通常是回一个点号.0x2E失败则回X0x58。上位机必须严格遵循这个“一问一答”的同步流程发送下一条记录前必须收到上一条的应答。4. 双I2C总线接口原理与驱动编写P89V660系列的一大亮点是集成了两个独立的I2C总线控制器。I2C是一种简单、高效的两线式串行总线在嵌入式领域连接各种低速外设几乎是无处不在。4.1 I2C总线基础与P89V660实现特点I2C总线只靠两根线SCLSerial Clock串行时钟线由主设备产生。SDASerial Data串行数据线双向。P89V660的I2C控制器是“字节型”的这意味着它每传输完一个字节8位数据1位应答就会产生一个中断你需要在中服务程序里根据当前状态码Status Code来决定下一步做什么。这种方式比“位操作”软件模拟I2C要高效、可靠得多尤其是作为主设备时。需要特别注意的一点是数据手册提到第二个I2C接口Secondary I2C使用的是准双向I/O口而不是开漏输出。这意味着在硬件上你可能不需要为第二个I2C总线的SDA和SCL引脚外接上拉电阻因为准双向口在输出高电平时有内部上拉。但为了保险和兼容性尤其是总线较长或负载较多时我强烈建议依然加上外部上拉电阻通常4.7kΩ到10kΩ。两个I2C总线在电气特性上略有差异在驱动代码初始化时它们的特殊功能寄存器SFR地址也不同别搞混了。4.2 特殊功能寄存器SFR精讲驱动I2C就是跟这四个SFR打交道以主I2C为例地址在括号内S1CON (0xD8) - 控制寄存器这是大脑配置工作模式。ENS1 (位6)总开关置1使能I2C功能。STA (位5)起始位。置1硬件会在总线空闲时发送START信号在主机模式下已发送数据后置1会发送Repeated START。STO (位4)停止位。置1硬件会发送STOP信号。发送成功后硬件自动清零。SI (位3)中断标志位。这是核心当I2C硬件完成一个操作如发送完START、地址、数据或接收到数据后SI会被硬件置1。如果总中断和I2C中断使能就会进入中断服务程序。你必须在中断服务程序里手动写0清除SI硬件才能继续下一步。AA (位2)应答标志。置1表示在下一个应答时钟周期本设备将拉低SDA发送ACK清0则释放SDA为高发送NACK。这在主机接收最后一个字节或从机不想再接收数据时非常有用。CR2, CR1, CR0 (位7,1,0)时钟速率选择位。用于设置当I2C作为主机时的SCL频率。具体分频值需查表数据手册表17根据你的系统时钟6分频或12分频模式选择。S1DAT (0xDA) - 数据寄存器要发送或刚接收到的数据就放在这里。重要原则只有在SI1即一次传输完成等待你处理时才能安全地读写这个寄存器。S1STA (0xD9) - 状态寄存器只读。它的高5位位7-3是状态码这是你编写中断服务程序的“路线图”。状态码共有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每个都对应一个精确的总线状态。0xF8表示无状态信息SI也为0。S1ADR (0xDB) - 从机地址寄存器当P89V660作为从机时这个寄存器存放它自己的7位从机地址。最低位S1GC是通用呼叫地址识别使能位。4.3 编写主机模式驱动程序以读取EEPROM为例理论讲再多不如看代码。假设我们要作为主机从一个I2C EEPROM地址0xA0的某个位置读取数据。以下是基于状态机的中断服务程序思路和关键代码片段。第一步初始化void I2C_Master_Init(void) { S1CON 0x40; // ENS11, 使能I2C其他位先清零。CR2:0根据所需波特率设置例如设为001。 // 假设系统时钟12MHz12分频模式CR2:0001对应约107kHz查表17 // S1CON 0x45; // 即 0100 0101, ENS11, CR[2:0]101 IEN1 | 0x01; // 使能I2C中断 (EI2C) EA 1; // 开启总中断 }第二步启动读序列我们用一个全局变量i2c_state来跟踪状态用i2c_buffer和i2c_index来处理数据。// 全局状态变量 unsigned char i2c_state; unsigned char i2c_buffer[32]; unsigned char i2c_index; unsigned char eeprom_addr_high, eeprom_addr_low; void Read_EEPROM(unsigned char addr_h, unsigned char addr_l, unsigned char len) { eeprom_addr_high addr_h; eeprom_addr_low addr_l; i2c_index 0; // 状态1发送START i2c_state I2C_STATE_START; S1CON | 0x20; // 设置STA1硬件将产生START信号 }第三步中断服务程序状态机核心这是最关键的部分你需要根据S1STA的状态码来驱动整个流程。void I2C_ISR(void) interrupt 8 { // I2C中断向量号需查数据手册 unsigned char status S1STA 0xF8; // 取高5位状态码 switch(status) { case 0x08: // 0x08: START条件已发送 if (i2c_state I2C_STATE_START) { // 发送EEPROM设备地址 写(0) S1DAT 0xA0; // 7位地址0x50左移1位最后一位0表示写 i2c_state I2C_STATE_SEND_ADDR_W; } S1CON ~0x08; // 清除SI位让硬件继续 break; case 0x18: // 0x18: SLAW已发送收到ACK if (i2c_state I2C_STATE_SEND_ADDR_W) { // 发送要读取的EEPROM内部地址高字节 S1DAT eeprom_addr_high; i2c_state I2C_STATE_SEND_ADDR_H; } else if (i2c_state I2C_STATE_SEND_ADDR_H) { // 发送要读取的EEPROM内部地址低字节 S1DAT eeprom_addr_low; i2c_state I2C_STATE_SEND_ADDR_L; } S1CON ~0x08; break; case 0x28: // 0x28: 数据字节已发送收到ACK if (i2c_state I2C_STATE_SEND_ADDR_L) { // 地址发送完毕现在发送一个Repeated START开始读操作 S1CON | 0x20; // 再次设置STA1发送Repeated START i2c_state I2C_STATE_RESTART; } S1CON ~0x08; break; case 0x10: // 0x10: Repeated START已发送 if (i2c_state I2C_STATE_RESTART) { // 发送EEPROM设备地址 读(1) S1DAT 0xA1; // 0x50左移1位最后一位1表示读 i2c_state I2C_STATE_SEND_ADDR_R; } S1CON ~0x08; break; case 0x40: // 0x40: SLAR已发送收到ACK // 准备接收数据。设置AA位表示接收前N-1个字节时发ACK S1CON | 0x04; // AA1 i2c_state I2C_STATE_RECEIVE_DATA; S1CON ~0x08; break; case 0x50: // 0x50: 数据字节已接收ACK已返回 // 读取数据 i2c_buffer[i2c_index] S1DAT; if (i2c_index BUFFER_SIZE) { // 如果已经收到足够数据在接收下一个字节前发NACK S1CON ~0x04; // AA0下次收到数据将回NACK } S1CON ~0x08; break; case 0x58: // 0x58: 数据字节已接收NACK已返回这是最后一个字节 // 读取最后一个字节 i2c_buffer[i2c_index] S1DAT; // 发送STOP条件结束传输 S1CON | 0x10; // STO1 S1CON ~0x08; // 清除SI i2c_state I2C_STATE_IDLE; // 回到空闲状态 // 在这里可以设置一个标志通知主循环数据已就绪 data_ready_flag 1; break; // ... 还需要处理其他状态如0x20收到NACK0x38仲裁丢失等错误情况 default: // 错误处理发送STOP复位状态 S1CON | 0x10; // STO1 S1CON ~0x08; i2c_state I2C_STATE_IDLE; error_flag 1; break; } }这个状态机清晰地描绘了I2C主机读取一个字节序列的完整过程START - 发送设备地址(写) - 发送内存地址 - Repeated START - 发送设备地址(读) - 接收数据(ACK) - 接收最后一个数据(NACK) - STOP。5. 常见问题、调试技巧与实战心得搞定了原理和代码在实际项目中依然会遇到各种“坑”。下面分享一些我积累的经验和排查方法。5.1 ISP编程失败排查指南问题现象可能原因排查步骤与解决方案上位机连接超时收不到任何回显1. 物理连接错误TX/RX接反、地线未共2. 目标板未供电或电压不足3. Bootloader未成功激活Status Bit非0或硬件时序不对4. 晶振未起振或频率偏差太大1. 用万用表或示波器检查VDD、GND、TXD、RXD电平。2. 确认芯片供电电压在允许范围内如3.0V-3.6V。3.重点用示波器抓RST引脚和串口信号。确保上电复位过程中RST引脚有完整的高低电平变化且上位机在正确时刻控制了RST如拉高。可以尝试在代码里将Status Bit清0后再用硬件方式强制进入ISP看能否连上。4. 检查晶振电路负载电容是否匹配。ISP波特率自适应有一定范围晶振偏差太大会导致通信失败。能收到回显U但发送Hex记录后无响应或返回X1. 波特率自适应后实际通信波特率不匹配2. Hex记录格式错误校验和、记录长度3. 目标地址非法或Flash被写保护安全位4. 电源噪声大导致数据传输错误1. 虽然自适应但后续通信可能因时钟偏差累积出错。尝试降低波特率在ISP工具中可选。2. 用十六进制查看工具检查你发送的记录手动计算校验和。确保每行以\r\n结尾。3. 确认你编程的地址在芯片Flash范围内。如果安全位已编程可能需要先执行全片擦除命令03000003070000FA来清除保护。4. 在目标板VDD靠近芯片处加一个10uF电解电容并联一个0.1uF瓷片电容并检查布线。编程成功但程序不运行1. Status Bit未正确设置为02. 中断向量表未正确处理程序从0000H开始但你的代码入口不是0000H3. 看门狗未禁用或未及时喂狗导致不断复位1.最常见原因编程最后一定要发送:0400000306000000FB将Status Bit写为0。2. 确保你的代码链接脚本正确启动代码包含中断向量跳转表位于0000H开始处。3. 在程序开头初始化看门狗或将其禁用。5.2 I2C通信故障与调试技巧问题现象可能原因排查步骤与解决方案总线死锁SCL或SDA被拉低1. 主设备在传输中异常复位未发送STOP2. 从设备故障持续占用总线3. 电气冲突多个设备同时驱动总线1. 这是I2C的经典问题。解决方案是实现一个总线恢复程序。可以尝试连续发送9个或更多个SCL时钟脉冲控制GPIO模拟同时释放SDA直到SDA被拉高然后发送一个STOP条件。P89V660的I2C模块本身在超时或错误时也可能自动恢复但软件恢复更可靠。2. 暂时断开可疑从设备看总线是否恢复。3. 检查所有设备是否都是开漏/集电极输出并确认上拉电阻已正确连接。能发送地址但收不到ACKNACK1. 从设备地址错误2. 从设备未上电或损坏3. 总线电平不匹配如3.3V主机与5V从机未电平转换4. 从设备忙如EEPROM正在写周期1. 用逻辑分析仪抓取波形看发送的7位地址读写位是否正确。注意地址通常是7位左移1位后最低位是R/W。2. 检查从设备电源和复位电路。3. 使用电平转换芯片如TXS0102或电阻分压网络。4. 对于有写周期的设备发送地址后收到NACK需等待一段时间查从设备手册的tWR再重试。通信随机出错数据不对1. 电源噪声或地线干扰2. 总线电容过大导致上升沿太慢3. 中断服务程序处理太慢错过状态4. 主频太高I2C时钟设置过快1. 加强电源滤波确保数字地和模拟地单点连接总线走线远离噪声源。2. 总线上的负载电容导线电容器件引脚电容总和过大。减小上拉电阻值如从10kΩ降到4.7kΩ可以加快上升时间但会增加功耗。更根本的是减少挂载设备或缩短走线。3.确保I2C中断是最高优先级之一并且中断服务程序尽可能短只做状态判断和寄存器操作将数据处理放到主循环。4. 降低I2C时钟频率调整S1CON的CR2:0位尤其是在长线或高容性负载时。5.3 关于双I2C总线使用的特别提醒引脚复用P89V660的I2C引脚是与GPIO复用的。在使用I2C功能前除了配置S1CON等寄存器还必须将对应的端口引脚配置为开漏模式对于主I2C或准双向模式对于从I2C特别是第二个接口。具体配置方法需参考数据手册的端口配置章节通常是通过设置某个特殊功能寄存器如PxM1, PxM2来实现。忘记这一步是导致I2C无法工作的常见原因。中断冲突两个I2C接口有各自独立的中断向量。在你的启动代码和中断服务程序中要正确区分INTERRUPT_I2C_PRIMARY和INTERRUPT_I2C_SECONDARY。如果只用一个最好在初始化时禁用另一个的中断。地址规划拥有两条独立的I2C总线意味着你可以将地址冲突的设备分开连接。例如总线上有三个相同的温度传感器地址固定不可改你可以将其中两个挂在I2C0另一个挂在I2C1完美解决地址冲突问题。最后无论是ISP/IAP还是I2C示波器或逻辑分析仪都是你最好的朋友。不要只依赖打印调试。抓取RST、TXD、RXD的波形可以直观看到ISP握手过程抓取SCL和SDA的波形可以清晰分析I2C的每一位数据、地址和ACK/NACK绝大部分通信问题在此面前都无所遁形。花时间学习使用这些工具对嵌入式开发者来说是绝对值得的投资。