简介:这份资源面向使用STC8G系列单片机的嵌入式开发者,提供I2C从模式的中断编程实现代码,帮助解决从机响应主机请求、数据收发与错误处理等实际问题。压缩包内共1个文件,为C语言源码,整体约1KB,可直接加入工程参考或移植。已有2667人学习下载,说明其在STC单片机I2C从机开发中具有较高的参考价值。代码围绕I2C控制、状态与数据寄存器的配置展开,涵盖从模式地址设置、中断使能、中断服务程序中事件判断与数据读写,以及总线冲突、数据溢出等异常的处理思路,读者可据此理解从机在接收起始条件、数据收发等环节的完整流程。对于需要快速搭建STC8G从机通信框架、减少底层调试时间的开发者而言,这份代码是一个可直接复用的起点,也便于进一步对照手册验证寄存器操作与中断逻辑。
1. 从机不是“接上就能跑”:STC8G I2C从模式到底在解决什么
很多人第一次用 STC8G 的 I2C 从模式,都是被项目逼出来的。主控那边跑着 Linux 或者另一颗 MCU,I2C 主机时序已经写死了,你手里这块 STC8G 只能当从机,负责响应地址、接收命令、回传数据。听起来简单,真上手才发现:主机读的时候从机要能及时把数据塞进寄存器,主机写的时候从机要能正确区分“地址帧”和“数据帧”,稍慢一步就是 NACK 或者数据错位。STC8G 的 I2C 从模式硬件外设就是干这个的——它把起始位检测、地址匹配、ACK 生成这些脏活揽过去,你只需要在中断里处理状态码。这份代码包适合两类人:一是被主机 I2C 时序卡住、需要一块稳定从机来对接的嵌入式工程师;二是想搞明白 STC8G 从模式中断状态机到底怎么流转的开发者。下面我按实际调试顺序,把寄存器配置、中断处理、时序边界和踩坑点拆开讲。
2. STC8G I2C从模式寄存器配置:从机地址、时钟与中断使能
2.1 从模式相关寄存器一览与选型理由
STC8G 的 I2C 外设挂在 P1.4(SDA)和 P1.5(SCL)上,做从机时不需要你手动翻转 GPIO,硬件会接管时序。核心寄存器就几个:I2CCFG 管使能和时钟分频,I2CSLCR 管从机控制,I2CSLST 是状态寄存器,I2CSLADR 存从机地址,I2CDAT 是数据收发口。为什么强调“从模式”要单独配?因为 STC8G 的 I2C 主机和从机共用一套寄存器,但使能位和中断源不同。主机模式你关心的是发送起始位、等待 ACK;从机模式你关心的是地址匹配中断和接收/发送中断。我一般先把 I2CCFG 的 ENI2C 置 1,再把 I2CSLCR 的 SLEN 置 1,这样从机功能才真正打开。时钟分频在从模式下其实不决定通信速率——速率由主机 SCL 决定——但分频值会影响数字滤波采样,设得太高会滤掉快速边沿,设得太低又容易受毛刺干扰。常见做法是保持默认分频或设成 0x04 左右,具体看主机 SCL 频率。
2.2 从机地址设置与中断入口配置
从机地址写在 I2CSLADR 里,注意 STC8G 的地址是 7 位,左对齐放在高 7 位,最低位是读写位,硬件在地址匹配时会自动比对。比如你想用 0x3A 作为从机地址,就写I2CSLADR = 0x3A << 1;。中断使能要开两个:EA 总中断和 I2C 从机中断。STC8G 的从机中断向量号查手册确认,中断服务函数里第一件事是读 I2CSLST 判断状态,而不是急着清标志。下面这段初始化代码是我在多个项目里反复用的模板,参数按实际主机速率和地址改。
#include "stc8g.h" #define SLAVE_ADDR 0x3A // 7位从机地址,按项目改 void I2C_Slave_Init(void) { P1M1 &= ~0x30; // P1.4 P1.5 设为准双向 P1M0 &= ~0x30; P14 = 1; // 释放 SDA P15 = 1; // 释放 SCL I2CCFG = 0x80; // 使能 I2C,主机模式位先不设 I2CSLCR = 0x80; // 使能从机模式 SLEN=1 I2CSLADR = (SLAVE_ADDR << 1); // 地址左对齐,最低位读写位由硬件处理 I2CSLST = 0x00; // 清状态 // 中断使能:从机中断 + 总中断 IE2 |= 0x01; // 使能 I2C 从机中断(具体位查手册确认) EA = 1; }逻辑说明:先配 GPIO 为准双向,避免推挽输出和 I2C 总线冲突。I2CCFG 的 ENI2C 置 1 是外设总开关,I2CSLCR 的 SLEN 置 1 才切到从机逻辑。地址左移一位是因为硬件把 bit0 当读写位,你写地址时不能占掉它。中断使能位各型号略有差异,STC8G 系列一般在 IE2 里,具体位号以手头手册为准,别照抄别的系列。参数上唯一需要你改的是 SLAVE_ADDR,其余保持默认就能跑通大部分主机。
2.3 中断状态机:从地址匹配到数据收发的完整流转
从机中断里最关键的是读 I2CSLST 的状态码,然后决定下一步动作。STC8G 从机状态大致分几类:地址匹配且主机要写、地址匹配且主机要读、收到一个数据字节、发送完一个字节等待 ACK、收到 STOP 或重复起始位。我习惯用 switch 把状态码分桶,每个桶里做一件事:如果是地址匹配写,就准备接收缓冲区指针;如果是地址匹配读,就把要发的第一个字节塞进 I2CDAT;如果是收到数据,就存进缓冲区并递增指针;如果是发送完等 ACK,就判断主机是 ACK 还是 NACK,NACK 表示主机读完了。下面是一个精简但能跑的状态机骨架。
volatile unsigned char rx_buf[16]; volatile unsigned char rx_idx = 0; volatile unsigned char tx_buf[16]; volatile unsigned char tx_len = 0; volatile unsigned char tx_idx = 0; void I2C_Slave_ISR(void) interrupt 24 // 中断号以手册为准 { unsigned char st = I2CSLST; I2CSLST = 0x00; // 清状态,具体清法看手册 switch (st & 0xF8) { case 0x60: // 收到地址+写 rx_idx = 0; break; case 0x80: // 收到一个数据字节 if (rx_idx < sizeof(rx_buf)) { rx_buf[rx_idx++] = I2CDAT; } break; case 0xA8: // 收到地址+读 tx_idx = 0; I2CDAT = tx_buf[tx_idx++]; // 先塞第一个字节 break; case 0xB8: // 发送完一个字节,收到 ACK if (tx_idx < tx_len) { I2CDAT = tx_buf[tx_idx++]; } break; case 0xC0: // 发送完一个字节,收到 NACK // 主机不想再读了,准备 STOP break; case 0xA0: // 收到 STOP // 一帧结束,可以置标志给主循环处理 break; default: break; } }逻辑说明:状态码的高 5 位是有效判断位,低 3 位可能含其他标志,所以用& 0xF8屏蔽。0x60 和 0xA8 是地址匹配的两个分支,分别对应主机写和主机读。0x80 是收到数据,直接读 I2CDAT 就能拿到。0xB8 是发送后收到 ACK,继续塞下一个字节;0xC0 是 NACK,说明主机读够了,别再塞了。参数上 rx_buf 和 tx_buf 大小按你的协议定,但别超过 16 字节太多,STC8G 的 RAM 有限。中断号 24 是示例,实际以你用的具体型号手册为准,写错中断号程序会跑飞。
3. 主机读写时序对接:从机响应速度与缓冲区设计
3.1 主机写从机:地址帧、数据帧与 STOP 的边界
主机写从机时,时序是:START → 地址+W → ACK → 数据1 → ACK → 数据2 → ACK → … → STOP。从机这边看到的就是 0x60 状态先来,然后每收一个字节进一次 0x80 状态,最后 STOP 进 0xA0。这里最容易翻车的地方是:主机连续写多个字节时,从机中断处理必须足够快,否则下一个字节来了你还没读走 I2CDAT,硬件就会覆盖或者拉低 SCL 做时钟延展。STC8G 支持时钟延展,但延展能力有限,如果中断里做了耗时操作(比如浮点运算、长循环),主机就会超时。我一般把中断里的事压到最少:只存缓冲区和改指针,协议解析放到主循环。缓冲区大小要能装下主机一次连写的最长帧,常见做法是 16 或 32 字节,看协议。另外注意:主机写完后发 STOP,从机收到 0xA0 后要把 rx_idx 置个标志,主循环看到标志再处理,别在中断里直接解析。
3.2 主机读从机:预装载与 NACK 处理
主机读从机时序是:START → 地址+R → ACK → 数据1 → ACK → 数据2 → ACK → … → NACK → STOP。从机在收到地址+R 时(0xA8 状态)就要把第一个要发的字节写进 I2CDAT,这个叫预装载。如果忘了预装载,主机读到的就是上次残留的数据,这种玄学问题我遇到过不止一次。之后每发完一个字节进 0xB8,判断 tx_idx 是否小于 tx_len,是就继续塞,不是就等主机 NACK。主机 NACK 后进 0xC0,此时从机不要再写 I2CDAT,否则会干扰总线。最后 STOP 进 0xA0,一帧结束。这里有个细节:tx_len 要在地址匹配读的时候根据协议确定,比如主机读的是某个寄存器,你就把对应数据准备好。常见做法是维护一个寄存器映射表,地址匹配读时根据之前写命令解析出要返回哪个寄存器的值。
3.3 时钟延展与中断响应时间的实测边界
STC8G 从机支持时钟延展,意思是当从机还没准备好数据时,可以把 SCL 拉低,让主机等着。但延展不是无限的,主机那边通常有超时机制。我实测过,在 24MHz 主频下,中断响应加状态判断大概几微秒,主机 SCL 100kHz 时一个位 10 微秒,完全来得及。但如果主频降到 11.0592MHz 或者中断里干了太多事,就可能触发主机超时。所以我的习惯是:从机中断优先级设最高,中断里只做搬运,不做计算。如果你发现主机读到的数据偶尔错位,先查中断响应时间,再看是不是时钟延展被主机忽略了。参数上,I2CCFG 的分频值会影响数字滤波,设成 0x04 左右对 100kHz 和 400kHz 都兼容,再高可能滤掉 400kHz 的边沿。
4. 避坑与排查:STC8G I2C从模式最常见的五个翻车点
4.1 地址匹配成功但数据错位
现象:主机写从机,第一个字节对,后面全错。原因:中断里读 I2CDAT 太慢,硬件已经收到下一个字节并覆盖了数据寄存器。解决:把中断优先级提到最高,中断里只做rx_buf[rx_idx++] = I2CDAT;,其他事一律放主循环。如果还错,检查主频是否太低,适当提高主频或降低主机 SCL 速率。
4.2 主机读从机时第一个字节是 0xFF 或随机值
现象:主机读回来的第一个字节不对,后面正常。原因:地址匹配读的时候没有预装载第一个字节,I2CDAT 里是上次残留。解决:在 0xA8 状态里立刻I2CDAT = tx_buf[0]; tx_idx = 1;,确保第一个字节在主机开始读之前就准备好。
4.3 从机不响应地址,主机一直 NACK
现象:主机发地址后收不到 ACK,示波器看 SDA 一直高。原因:I2CSLCR 的 SLEN 没置 1,或者 I2CCFG 的 ENI2C 没开,或者 GPIO 模式配成了推挽导致总线冲突。解决:检查初始化顺序,先开 ENI2C 再开 SLEN,GPIO 设为准双向,P14 和 P15 写 1 释放总线。另外确认从机地址左移了一位,没占读写位。
4.4 中断进不去或进去就跑飞
现象:程序卡死或复位。原因:中断号写错,或者中断服务函数里没清状态标志导致反复进中断。解决:查手头型号手册确认 I2C 从机中断向量号,中断里第一件事读 I2CSLST 并清标志,清标志的方法各型号略有不同,有的写 0 有的写 1,以手册为准。
4.5 长帧传输时主机超时
现象:主机写超过 8 个字节后超时,从机没反应。原因:从机中断里做了耗时操作,或者缓冲区溢出导致指针越界。解决:中断里只搬运,协议解析放主循环;缓冲区开够大,加边界判断if (rx_idx < sizeof(rx_buf))。如果主机速率高,考虑用 DMA 或者降低 SCL。
5. 进阶技巧:用状态机+寄存器映射把从机做成可复用模块
5.1 寄存器映射表的设计与代码实现
如果你做的项目里主机要读写多个寄存器,别在中断里写一堆 if-else。我一般用一个结构体数组做寄存器映射,每个条目包含地址、读回调、写回调。中断里只负责把收到的数据存进一个命令缓冲区,主循环解析命令后查表执行。这样从机代码可以跨项目复用,换协议只改表。下面是一个简化实现。
typedef struct { unsigned char reg_addr; unsigned char (*read)(void); void (*write)(unsigned char val); } reg_map_t; unsigned char reg0_read(void) { return 0x12; } void reg0_write(unsigned char val) { /* 存起来 */ } reg_map_t reg_map[] = { {0x00, reg0_read, reg0_write}, // 更多寄存器... }; void process_command(void) { unsigned char cmd = rx_buf[0]; unsigned char val = rx_buf[1]; for (int i = 0; i < sizeof(reg_map)/sizeof(reg_map[0]); i++) { if (reg_map[i].reg_addr == cmd) { if (reg_map[i].write) reg_map[i].write(val); break; } } }逻辑说明:中断只负责收帧,主循环调 process_command 解析。reg_map 把寄存器地址和操作函数绑在一起,增删寄存器只改数组。参数上 rx_buf[0] 是命令,rx_buf[1] 是数据,具体协议你可以扩展。这种写法比在中断里 switch 一堆 case 清爽得多,也更容易测。
5.2 用逻辑分析仪抓 I2C 时序验证从机行为
调 I2C 从机,光看代码不够,得抓波形。我习惯用逻辑分析仪接 SDA 和 SCL,抓一帧完整的主机读写。重点看几个点:地址帧后从机有没有拉低 SDA 做 ACK;数据帧后 ACK 是否正常;STOP 条件是否干净。如果从机 ACK 没出来,先查 SLEN 和地址;如果数据错位,看从机是不是在 SCL 高电平期间改了 SDA。逻辑分析仪上还能看到时钟延展,如果 SCL 被从机拉低了一段时间,说明中断响应慢,得优化。这个习惯帮我省了很多猜的时间。
5.3 从机低功耗唤醒与多地址响应的取舍
STC8G 从机可以在掉电模式下等 I2C 地址匹配唤醒,适合电池供电的从机节点。但低功耗模式下主频低,中断响应慢,主机 SCL 不能太快,一般降到 10kHz 以下。另外 STC8G 支持多地址响应,可以配两个从机地址,但地址匹配中断里要判断是哪个地址,逻辑会复杂一点。我的建议是:如果项目对功耗不敏感,别开低功耗,老老实实全速跑,稳定性高得多。如果非要低功耗,先把主机速率降下来,再测唤醒时间。从那以后我每次做从机项目,都强制先用逻辑分析仪抓一帧完整时序再写代码,这个习惯让我少熬了很多夜。希望帮到你。
本文还有配套的精品资源,点击获取