ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

STC模拟IIC从机发送源程序:双沿检测状态机实现

2026/9/16 21:21:28 拓冰建站 浏览量
STC模拟IIC从机发送源程序:双沿检测状态机实现 简介面向STC单片机的IIC从机发送源程序包适合嵌入式开发者在缺少硬件IIC接口时参考解决STC单片机与STM32/ARM等主机之间的低速串行通信问题。程序围绕数据线SDA与时钟线SCL的时序模拟、起始停止条件判断、七位从机地址寻址以及应答位生成与检测等关键环节展开并附有HT1621显示驱动相关代码和红外接收程序说明文档可辅助理解外设驱动与IIC协议的配合方式。压缩包共21个文件约40KB以C语言源码和Keil工程文件为主体另有hex烧录文件、编译生成的列表文件与内存映射文件以及若干自动备份文件其中的readme说明文档给出编译、烧录与运行指引整体目录结构清晰。已有1659人学习下载适合正在调试IIC从机时序、需要参考软件模拟实现与排错思路的单片机开发者。1. STC模拟IIC从机发送的适用场景单片机应用里经常出现这种需求STC单片机通过IIC总线把采集数据传给外置主板数据量不大但主机端协议固定必须是主机发起读请求STC作为从机应答。大多数程序员写“模拟IIC”时下意识只写了主机波形先摆好SDA再翻转SCL因为时钟完全由自己掌握。一旦角色反转成从机时钟得由对方提供STC不能控制SCL只能顺着边沿把数据放到SDA上这就是标题里说的“模拟IIC从机发送数据源程序”。文章面向STC89C52、STC15系列这类没有硬件IIC或不想占用硬件外设的场景从时序约束、状态机代码到排错顺序整理一份改改地址和缓冲区就能用的源程序思路也说明什么时候模拟方案够用、什么时候该换硬件IIC。2. 从机发送必须守住的时序点与引脚选型2.1 从机发送器与主机的时序责任分配IIC协议里每一位都依赖SCL高电平期间的SDA稳定性。从机发送器要扮演的角色是主机提供SCL时钟从机在SCL低电平期间把SDA放到目标电平SCL高电平期间保持住不动主机在高电平后期采样SDA这样才形成有效的“0”或“1”。一次读事务从主机发出起始条件开始第一个字节是7位从机地址加R/W位R/W为1表示读。STC从机在地址匹配后于第9个时钟把SDA拉低作为应答之后在紧接着的8个时钟里逐位移出数据第9个时钟检测主机是继续读还是发NACK最后等主机发出停止条件。从机不能主动叫停主机也不能在空闲时主动把SCL拉高SCL引脚从头到尾都必须当作输入看。这个分工一旦清楚后续调试就不容易把“主机时钟”和“数据位”两件事搅在一起。2.2 硬件 IIC、模拟轮询与模拟中断三种方案对照STC89C52没有硬件IICSTC15W408AS等部分型号也没有独立I2C模块常见的做法就是把SDA、SCL分别选两个GPIO口配合外部主控或USB转IIC工具使用。动手前先决定用轮询还是中断实现方式需要的硬件资源可靠速率主要限制硬件 IICI2C外设引脚复用400kHz 常见STC89C52、部分STC15没有外设模拟轮询任意两个GPIO100kHz 以下主循环里有长任务会丢位模拟中断SC连接到外部中断脚100~400kHz中断里动作多响应延迟会漂标题里的“源程序”一般就是指轮询版状态机它不依赖特定中断号换一片STC也几乎不用改。轮询方案最大的优势在于发送动作集中在主循环一个函数里调试时可以直接在分支里打断点或翻转测试引脚很容易对照逻辑分析仪波形定位是哪一位出了问题。2.3 STC 引脚模式对上拉电阻和 ACK 电平的影响STC89C52的P1口是准双向口输出低电平能力强输出高电平时只能靠内部弱上拉驱动。IIC总线空闲时SDA、SCL都是高电平所以SDA和SCL都需要外接4.7kΩ左右的上拉电阻或者把引脚配成开漏后依赖外部上拉。STC15、STC8系列多了端口模式寄存器以STC8的P1.0SDA、P1.1SCL为例/* 把P1.0和P1.1配置为开漏模式 */ P1M1 | 0x03; /* P1M1对应位置1 */ P1M0 ~0x03; /* P1M0对应位清0 */这段代码把两个引脚设成开漏。开漏模式下SDA低电平由内部下拉管驱动高电平完全靠外部上拉电阻电平标准与IIC规范一致。若用准双向口输出高电平时内部弱上拉会和外部上拉叠加虽然不影响通信但在低功耗场景会多耗电。影响最大的是上拉电阻取值。ACK位拉低SDA时主机正握着SCL从机是靠开漏管把SDA拉低的外部上拉太大时SDA恢复到高电平变慢100kHz下一般选4.7kΩ换成400kHz就建议用2.2kΩ。软件上同样要留意从机发送模式下SDA的电平翻转都发生在SCL下降沿之后、上升沿之前这个窗口只有半个SCL周期上拉过慢会把翻转沿拖到下半个周期主机就采到错误电平。3. 双沿检测状态机模拟 IIC 从机发送源程序的核心结构3.1 轮询周期与边沿侦测变量的选取假使主机提供的是100kHz时钟SCL半周期5μs从机程序必须在5μs内至少完成一轮状态检查。STC89C52在12MHz晶振下一条带判断的语句大约1到2μs主循环里若只有这个状态机5μs读取一轮没有问题主机跑到400kHz时半周期只有1.25μs轮询方案基本跟不住所以要限制模拟方案适合的主机速率。代码里用两个静态变量记录上一轮电平static bit last_scl 1; static bit last_sda 1; bit scl_up, scl_dn; scl_up (last_scl 0) (IIC_SCL 1); /* SCL上升沿 */ scl_dn (last_scl 1) (IIC_SCL 0); /* SCL下降沿 */ last_scl IIC_SCL;上升沿之后的SCL高电平期是数据采样窗口下降沿之后的SCL低电平期是SDA更新窗口。这个“双沿”概念是整套状态机的基础下降沿更新发送位上升沿采样接收位。只要主循环每次调用都先算出这两个边沿标志后续所有分支都不需要再重复判断电平变化。3.2 从机发送的主循环源程序下面是最小可用的轮询版状态机只处理纯读事务地址和缓冲区分开定义方便直接套用#define IIC_ADDR 0x50 /* 7位从机地址 */ static unsigned char state 0; /* 0空闲 1收地址 2应答 */ /* 3发数据 4收NACK 5等停止 */ static unsigned char bitcnt 0; static unsigned char byte 0; static unsigned char tx_index 0; unsigned char tx_buf[8] {0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08}; unsigned char tx_len 8; void iic_slave_poll(void) { bit sda_now IIC_SDA; bit scl_now IIC_SCL; bit scl_up (last_scl 0) scl_now; bit scl_dn (last_scl 1) (!scl_now); if (state 0) { /* 空闲态检测起始条件 */ if (scl_now last_sda (sda_now 0)) { state 1; bitcnt 0; byte 0; } } else if (state 1) { /* 逐位收集地址和R/W位 */ if (scl_up) { byte (byte 1) | sda_now; bitcnt; if (bitcnt 8) { state 2; } } } else if (state 2) { /* 第9个时钟的应答位 */ if (scl_dn) { if (((byte 1) IIC_ADDR) (byte 0x01)) { IIC_SDA 0; /* 拉低SDA应答 */ tx_index 0; bitcnt 0; state 3; } else { IIC_SDA 1; /* 地址不匹配释放总线 */ state 0; } } } else if (state 3) { /* 发送数据字节 */ if (scl_dn) { if (bitcnt 8) { IIC_SDA (tx_buf[tx_index] (7 - bitcnt)) 1; bitcnt; } else { IIC_SDA 1; /* 第9个时钟释放SDA */ state 4; } } } else if (state 4) { /* 等待主机NACK或继续读 */ if (scl_up) { if (sda_now 1) { state 5; /* 主机发出NACK */ } else { tx_index; if (tx_index tx_len) tx_index 0; bitcnt 0; state 3; } } } else if (state 5) { /* 等待停止条件 */ if (scl_now (last_sda 0) (sda_now 1)) { IIC_SDA 1; state 0; } } last_scl scl_now; last_sda sda_now; }这段代码有三个关键参数。第一个是IIC_ADDR它对应主机发送地址的高7位主机用0xA1读时byte收到二进制1010 0001右移一位得到0x50也就是代码里的IIC_ADDR。第二个是tx_buf和tx_len属于发送缓冲区发送时从tx_buf[0]开始发完tx_len字节后自动回到头部避免越界。第三个是状态4的循环逻辑主机在第9个时钟保持SDA低表示还要继续读于是tx_index并重新进入发数据状态主机释放SDA为高表示不想再读进入等待停止状态。细节上数据字节是MSB先出所以用7 - bitcnt做移位地址字节也是MSB先收判断匹配时要整体右移一位把R/W位去掉。起始条件必须同时满足“SCL为高”和“SDA出现下降跳变”只判断SDA低电平不判断上一轮状态会在总线空闲时误触发。3.3 状态跳转与位计数对照表为了方便对照逻辑分析仪波形把状态跳转整理成表当前状态触发条件动作去向空闲SCL高且SDA下降沿清零位计数和地址寄存器收地址收地址SCL上升沿移位采样SDA计数到8应答应答SCL下降沿地址匹配且读请求拉低SDA发数据发数据前8个SCL下降沿按位输出tx_buf[tx_index]发数据或收NACK收NACKSCL上升沿SDA1停止SDA0继续等停止或发下一字节这张表对应的是单次读事务。主机发出停止条件后状态机回到空闲等待下一次起始条件。实际抓波形时发现位计数对不上多半是起始条件误判或SCL边沿漏检优先检查last_scl和last_sda的更新位置。3.4 把发送动作放进中断函数的前提如果主循环里有LCD刷新、按键扫描这类长耗时任务轮询状态机很容易漏边沿。常见做法是把SCL接到外部中断引脚在下降沿中断里更新SDA/* SCL接INT0下降沿触发 */ void ext0_isr(void) interrupt 0 { if (state 3 bitcnt 8) { IIC_SDA (tx_buf[tx_index] (7 - bitcnt)) 1; bitcnt; } }这个写法只适合数据位输出不适合应答位和NACK检测。原因是应答位和停止条件都发生在SCL高电平期间下降沿中断看不到那部分时序。所以一般建议中断版只处理“发数据”分支地址接收和停止检测仍留在主循环里轮询。若中断服务里还有其它耗时操作要保证响应时刻距离SCL下降沿不超过1μs否则高速主机已经进入高电平采样区间SDA还没来得及稳定。4. 从机发送不上数据的排查顺序与验证方法4.1 先测真实时钟速率再看状态变量从机连不上数据时最容易犯的错是改代码前不看主机实际的SCL频率。主机端如果也是模拟IIC它的边沿可能没有规范占空比名义100kHz实际可能只有60kHz或跑到200kHz。先用一段辅助代码把SCL频率量化/* P3.2在每次SCL下降沿翻转用示波器测P3.2频率再乘2 */ void scl_probe(void) { if (last_scl (!IIC_SCL)) { /* SCL下降沿 */ P3_2 !P3_2; } last_scl IIC_SCL; }把这段scs_probe()放到主循环最前面用示波器看P3.2翻转频率。P3.2翻转一次对应两个SCL周期所以示波器读数×2就是主机SCL频率。如果读数超过200kHz轮询方案基本没戏要么降主机速率要么换中断版。这步做完再检查状态变量因为频率不对会同时造成起始条件漏检和位计数错乱先花一分钟排除物理层问题比在状态机里干找效率高。4.2 逻辑分析仪与主机配置的对应关系逻辑分析仪解IIC时通常要填7位从机地址。代码里IIC_ADDR是0x50主机发的读地址是0xA1逻辑分析仪上应该把地址设成0x507位模式或0x50左移一位后的0xA0加读位。很多排查卡在“地址匹配”这一关原因是主机工具里填写的是8位地址0xA1而逻辑分析仪设置里用的是7位两边差一位从机侧却始终没有收到匹配的地址字节。看波形时重点抓两处第一个字节的第8位是否为1第9个时钟的ACK位SDA是否被拉低。第8位为0时主机发的是写请求不会进入发送数据状态。常见做法是把主机配置里的读取长度设为8字节与tx_len保持一致避免主机在数据中间额外插入停止条件。4.3 常见异常对照表现象可能原因优先检查项主机读到的全是0xFF从机没应答或SDA驱动弱ACK位波形、引脚是否开漏、上拉阻值第一个字节对后续乱tx_index越界后回绕异常状态4里tx_index递增时机地址匹配但进不了发送R/W位被当成地址的一部分byte 0x01是否判断在读请求每次读返回旧数据缓冲区在主循环里被改写发送期间锁存缓冲区内容SDA被长时间拉低应答后从机没释放总线查看状态2的IIC_SDA 1分支结合状态机语句看第一行问题的根因通常在开漏配置或上拉电阻引脚设成准双向时内部弱上拉可能让SDA恢复速度不够主机仍能读到正常数据引脚误配成推挽输出时从机低电平期间推挽管和主机上拉电阻会对抗严重时烧坏引脚这是模拟IIC最容易踩的硬件坑。4.4 总线释放操作与上位机读超时设置主机端读超时设置一般给到1秒。调试阶段如果发现SDA一直被STC拉低那就是从机在ACK位之后没有正确进入释放分支多见于状态2中“地址不匹配”分支被跳过。此时主机侧连续发9个SCL时钟再拉高SDA一般能把总线解锁代码里最直接的做法是在空闲状态里强制把IIC_SDA置1并把状态机复位到0。配合上位机调试时先让上位机用单次读命令读固定长度8字节读成功后再打开连续读。顺序反过来的话连续读模式下主机可能在从机还没准备好时就开始下一笔事务造成偶发NACK误判成从机代码问题。5. 从单字节到整页搬运模拟 IIC 从机发送的进阶处理5.1 缓冲区滚动与跨页地址复位基础代码发送完tx_len字节后从头部重新开始适合状态数据。若要把缓冲区当成环形队列让主机连续读最新数据可以在状态4每次继续读时递增tx_indextx_index; if (tx_index tx_len) tx_index 0;这套写法在主循环读取外部数据更新缓冲区时需要注意指针和内容的一致性。避免在发送过程中改写当前字节可以在进入状态3前把发送内容复制到临时数组或引入双缓冲标志主循环写新数据时置“更新中”标记状态机在应答位时只读取标记位读到更新完成后再切换发送源。5.2 读请求之外的写请求怎么共存很多主从协议是先写寄存器地址再重复起始读模拟从机需要同时支持写请求。只需在状态2里扩展判断分支地址匹配且R/W为0时进入收数据状态把后续字节放入接收缓冲区地址匹配且R/W为1时沿用原有发送流程。支持的寄存器和数据长度可以写在同一组缓冲区里读操作返回该寄存器当前值写操作更新该寄存器值。这样就用同一对GPIO完成半双工通信不增加任何硬件。5.3 单板回环验证从机数据一致性的做法条件允许时用两块STC一块配成模拟主机、一块配成模拟从机SDA和SCL分别互联共地两块板都跑同一套代码的收发函数。验证从机发送一致性时让主机连续读100次每次8字节从机侧在每次状态4返回时递增一个序号字节主机回传序号出现跳号就说明丢字节。这个回环测试比单纯看波形更靠近真实应用能直接暴露出位计数在长时间运行后的漂移问题。若只有一块板子就拿STC8的两组引脚互连跑同样的测试前提是两组引脚的模式和上拉配置完全一致。本文还有配套的精品资源点击获取