简介这份资源面向使用STC8G系列单片机的嵌入式开发者与学习者提供I2C从模式的中断编程实现代码帮助解决从机角色下如何响应主机请求、收发数据的问题。压缩包内共1个文件为一份C语言源码整体约1KB可直接用于工程参考或移植。已有2667人学习下载说明其在STC单片机I2C从机开发中具有一定参考价值。代码围绕I2C控制、状态与数据寄存器的配置展开涵盖从模式地址设置、中断使能、中断服务程序中事件判断与数据读写以及总线冲突、数据溢出等错误处理思路读者可据此理解从机在接收起始条件、数据收发等场景下的完整流程。对于需要快速搭建STC8G从机通信框架、对照寄存器操作与中断逻辑的开发者这份代码可作为实践起点便于进一步调试与扩展。1. 从一次“主从对调”的调试说起STC8G 的 I2C 从模式到底能干什么很多人第一次用 STC8G 的 I2C都是拿它当主机去读传感器发个起始位、写个地址、收几个字节逻辑分析仪一挂就通了。可一旦项目里角色反过来——让 STC8G 当从机等着另一颗主控来读它、写它事情立刻变得玄学起来主机那边死活收不到 ACK或者数据错位、丢包甚至从机把总线拉死。我见过太多人卡在这一步最后干脆换芯片了事。这份代码要解决的就是这个场景把 STC8G 配置成 I2C 从设备让它像一颗普通 EEPROM 或传感器那样被动响应外部主机的读写请求。它适合两类人——一类是手里有块 STC8G 开发板、想搞明白从模式寄存器怎么配的初学者另一类是做多主控系统、需要让 STC8G 作为协处理器被主控轮询的工程开发者。下面我按“先跑通、再拆解、最后能改”的顺序把这份从模式代码从头到尾讲一遍。2. 环境搭建与最小可跑工程从寄存器配置到第一个 ACK2.1 硬件连接与上拉电阻的坑STC8G 的 I2C 从模式用的是硬件 I2C 模块引脚通常固定在 P1.4SDA和 P1.5SCL具体以你手上型号的数据手册为准。接线本身不复杂SDA、SCL 各接一根线到主机GND 共地。但这里有个血泪经验——上拉电阻不能省。I2C 是开漏输出没有上拉总线永远拉不到高电平主机发完起始位就卡死。常见做法是 SDA 和 SCL 各接一个 4.7kΩ 上拉到 3.3V 或 5V跟主机器件电平一致。如果主机那边已经有上拉从机这边可以不重复加但总线电容大的时候阻值要适当减小比如 2.2kΩ。我一般会在板子上预留两个上拉焊盘调试时用示波器看上升沿太缓就换小阻值。// STC8G I2C 从模式引脚配置以 P1.4/P1.5 为例 // 注意必须设置为开漏模式否则总线电平会冲突 P1M1 | 0x30; // P1.4, P1.5 设为开漏 P1M0 | 0x30; P1PU | 0x30; // 使能内部上拉弱上拉外部仍需强上拉这段代码把 P1.4 和 P1.5 配成开漏输出。STC8G 的端口模式寄存器 P1M1/P1M0 组合决定模式M11、M01 是开漏。P1PU 是内部上拉使能但内部上拉阻值很大几十 kΩ只能辅助不能替代外部 4.7kΩ。参数上如果你用的是 STC8G1K08 这类小封装引脚可能不同改对应的端口寄存器即可。2.2 从模式寄存器初始化I2CCFG、I2CSLCR、I2CSLSTSTC8G 的 I2C 从模式涉及几个关键寄存器I2CCFG 配使能和时钟I2CSLCR 配从机控制I2CSLST 是状态寄存器。初始化顺序不能乱先使能模块再配从机地址最后开中断。// I2C 从模式初始化 I2CCFG 0x80; // 使能 I2C 模块主机模式位清零 I2CSLCR 0x00; // 先清零从机控制寄存器 I2CSLCR | 0x80; // 使能从机模式 I2CSLCR | 0x40; // 使能从机接收中断可选 I2CSLADR 0x50; // 从机地址7 位地址左移一位这里 0x50 表示地址 0x28 I2CSLST 0x00; // 清状态I2CCFG 的 bit7 是 I2C 使能位bit6 是主机模式选择从模式必须清零。I2CSLCR 的 bit7 是从机模式使能bit6 是从机接收中断使能。I2CSLADR 存的是从机地址注意 STC8G 的地址寄存器是 7 位地址放在高 7 位最低位是读写位所以实际地址 0x28 要写成 0x50。这个移位很容易翻车我第一次配的时候就是忘了左移主机怎么都寻址不到。2.3 中断服务函数从机事件的处理骨架从模式的核心在中断里。STC8G 的 I2C 从机中断会给出各种状态收到起始位、收到地址匹配、收到数据、需要发数据、收到停止位等。代码里用一个 switch 根据 I2CSLST 的高 3 位判断状态。void I2C_ISR() interrupt 24 using 1 { unsigned char st I2CSLST 0xE0; // 取状态位 switch(st) { case 0x00: // 收到起始位 break; case 0x20: // 收到地址需要判断读写 if(I2CSLST 0x10) { // 读请求 // 主机要读数据准备发送第一个字节 I2CTXD send_buf[0]; } else { // 写请求 // 主机要写数据准备接收 } break; case 0x40: // 收到一个数据字节 recv_buf[recv_idx] I2CRXD; break; case 0x60: // 需要发送数据 I2CTXD send_buf[send_idx]; break; case 0x80: // 收到停止位 recv_idx 0; send_idx 0; break; default: break; } I2CSLST ~0xE0; // 清状态位必须做 }这段中断服务函数是整个从模式的骨架。状态位在 I2CSLST 的高 3 位0x20 表示地址匹配此时要读 I2CSLST 的 bit4 判断是读还是写。0x40 是收到数据直接读 I2CRXD。0x60 是主机要读数据把要发的字节写入 I2CTXD。0x80 是停止位用来复位缓冲区索引。最后必须清状态位否则中断会反复触发。参数上send_buf 和 recv_buf 的大小根据你的协议定一般 16 或 32 字节够用。3. 核心模块拆解地址匹配、数据收发与状态机3.1 地址匹配逻辑为什么你的从机总是不响应地址匹配是从模式的第一道关。主机发完起始位后会发一个字节高 7 位是从机地址最低位是读写位。STC8G 硬件会自动比较 I2CSLADR 里的地址匹配成功才会置位状态。但这里有几个细节容易忽略。第一I2CSLADR 的格式。前面说过7 位地址要左移一位。比如你的从机地址是 0x28二进制 0101000左移后是 0x50。如果你直接写 0x28硬件比较的是 0x28 的高 7 位也就是 0x14永远匹配不上。第二地址匹配后硬件不会自动区分读还是写需要你在中断里读 I2CSLST 的 bit4。bit4 为 1 表示主机要读从机发为 0 表示主机要写从机收。这个判断错了后面数据方向就全反了。第三如果总线上有多个从机地址不能冲突。我一般会在代码里留一个宏定义从机地址方便改。#define SLAVE_ADDR 0x28 I2CSLADR (SLAVE_ADDR 1); // 左移一位写入3.2 数据接收与发送缓冲区管理和索引复位数据收发看着简单但缓冲区管理不当会丢包。主机写数据时每收到一个字节中断状态 0x40你把 I2CRXD 存进 recv_buf索引加一。主机读数据时状态 0x60你把 send_buf 里的字节写入 I2CTXD索引加一。关键点是索引复位。主机发停止位时状态 0x80此时要把 recv_idx 和 send_idx 都清零否则下一次传输会从上次的位置继续数据错位。我见过有人只在收到起始位时复位结果主机连续两次读操作第二次读到的还是上次的残留数据。另外缓冲区大小要留余量。如果主机一次写超过缓冲区大小索引会溢出。常见做法是在索引达到最大值时回绕或者直接丢弃多余数据并置一个溢出标志。#define BUF_SIZE 32 unsigned char recv_buf[BUF_SIZE]; unsigned char send_buf[BUF_SIZE]; unsigned char recv_idx 0; unsigned char send_idx 0; // 在状态 0x40 里 if(recv_idx BUF_SIZE) { recv_buf[recv_idx] I2CRXD; } else { // 溢出处理可以置标志或丢弃 }3.3 状态机梳理从起始位到停止位的完整流转把中断里的状态串起来就是一个完整的状态机。主机发起始位从机进 0x00主机发地址匹配后进 0x20根据读写位决定方向如果是写后续每个字节进 0x40如果是读后续每个字节进 0x60主机发停止位进 0x80复位。这个状态机里有个容易翻车的地方重复起始位。有些主机会在传输中间发重复起始位而不是停止位再起始。STC8G 的从机在收到重复起始位时状态可能还是 0x00 或 0x20你的代码要能处理这种情况不能假设一定是停止位才复位。稳妥的做法是在收到起始位时也复位索引。case 0x00: // 收到起始位包括重复起始 recv_idx 0; send_idx 0; break;这样无论主机怎么发每次起始都从头开始避免残留数据干扰。4. 避坑与常见问题排查从机不响应、数据错位、总线拉死4.1 主机收不到 ACK从机毫无反应现象主机发完地址后逻辑分析仪上看不到 ACK 位从机中断也没进。原因最常见的是地址没左移或者 I2CCFG 的主机模式位没清零。还有一种可能是引脚模式配错了SDA/SCL 不是开漏导致总线电平被拉死。解决先确认 I2CSLADR 的值是地址左移一位再检查 I2CCFG 的 bit6 是否为 0最后用万用表量 SDA/SCL 对地电压正常空闲时应该是高电平如果一直是低查引脚模式和上拉。4.2 数据错位读到的字节对不上现象主机读回来的数据跟从机 send_buf 里的对不上或者第一次对、第二次错。原因索引没复位或者发送缓冲区在中断外被修改了。中断里读写 send_buf 时如果主循环也在改会竞争。解决在停止位和起始位都复位索引send_buf 的更新放在主循环里但要用一个标志位跟中断同步或者干脆在中断里只读不写主循环更新前先关中断。4.3 总线被拉死主机再也发不出起始位现象调试过程中突然总线不动了SDA 或 SCL 一直是低电平。原因从机在某个状态卡住没有释放总线。比如中断里清状态位之前就返回了或者缓冲区溢出导致死循环。解决在中断里确保每个分支都清状态位加一个超时机制如果从机长时间没收到停止位强制复位 I2C 模块。我一般会在主循环里加一个计数器超过一定时间没进中断就重新初始化 I2C。4.4 中断进不去或者进了就死循环现象从机中断完全不触发或者触发后程序卡死。原因中断向量号写错或者中断优先级配置不对。STC8G 的 I2C 中断向量号是 24不是 24 的话进错中断。另外中断里用了浮点运算或者调用了不可重入函数也会卡死。解决确认中断函数写的是interrupt 24中断里只做简单赋值和状态判断不要调 printf 之类的函数如果用了 RTOS注意中断优先级和临界区。4.5 多主机场景下的仲裁丢失现象总线上有两个主机同时发起传输从机收到乱码。原因STC8G 作为从机不参与仲裁但多主机时总线电平会冲突从机可能收到错误数据。解决这种场景下从机只能被动接收建议在协议层加校验比如 CRC 或者和校验发现错误就丢弃。硬件上没法完全避免只能靠软件容错。5. 二次开发改造把从模式代码改成你自己的协议5.1 协议层扩展从裸数据到带寄存器地址的读写原始代码只是简单的收发缓冲区实际项目里通常需要像 EEPROM 那样先写寄存器地址再读写数据。改造思路是在 recv_buf 里把第一个字节当作寄存器地址后续字节才是数据。// 在状态 0x40 里 if(recv_idx 0) { reg_addr I2CRXD; // 第一个字节是寄存器地址 recv_idx; } else { reg_data[reg_addr] I2CRXD; // 后续是数据 recv_idx; }这样主机可以先写地址再写数据读的时候也是先写地址再读。注意读操作时主机发完地址后会发重复起始位然后读数据你的代码要在重复起始位时保留 reg_addr不能复位。5.2 中断与主循环的分工怎么避免数据竞争从模式的中断触发很频繁如果所有处理都放中断里主循环没机会跑。我一般这样分工中断里只做最紧急的事——收数据存缓冲区、发数据从缓冲区取、复位索引。协议解析、寄存器更新、校验计算都放主循环用一个标志位通知。volatile unsigned char i2c_event 0; // 中断里 i2c_event 1; // 主循环里 if(i2c_event) { i2c_event 0; // 处理协议 }注意 i2c_event 要加 volatile否则编译器优化后主循环可能看不到变化。5.3 验证方法用逻辑分析仪和主机模拟器对测改完代码怎么验证最直接的是用逻辑分析仪抓波形看起始位、地址、ACK、数据、停止位是否完整。如果没有逻辑分析仪可以用另一块单片机做主机发固定的读写序列对比从机返回的数据。我一般会写一个简单的测试主机代码循环读从机的几个寄存器看返回值是否跟预期一致。测试用例要覆盖单字节读、多字节读、单字节写、多字节写、地址不匹配、重复起始位。这几个场景过了基本就稳了。5.4 一个具体技巧用状态变量替代 switch 里的硬编码原始代码用 switch 判断状态可读性一般。改造时可以定义一个状态变量把状态流转画清楚这样加新状态时不容易漏。typedef enum { I2C_STATE_IDLE, I2C_STATE_ADDR, I2C_STATE_RECV, I2C_STATE_SEND, I2C_STATE_STOP } i2c_state_t; i2c_state_t state I2C_STATE_IDLE;然后在中断里根据状态和事件转移。这样代码更像一个正经的状态机维护起来舒服很多。从那以后我每次配 I2C 从模式都强制先量上拉电阻、再确认地址左移、最后抓一遍波形这三步走完再调代码省了太多来回。希望帮到你。本文还有配套的精品资源点击获取