ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STC单片机模拟IIC从机发送实战:从时序到STM32联调

STC单片机模拟IIC从机发送实战:从时序到STM32联调 简介STC模拟IIC从机发送数据源程序是一份面向单片机开发者的嵌入式通信示例适用于STC平台通过软件模拟IIC从机发送数据解决无硬件IIC外设场景下的通信需求。资源包含21个文件主要涉及Keil工程文件uvproj、uvopt、C源码、Hex固件、LST列表文件、备份文件及readme说明等压缩包约40KB适合快速查看工程结构与烧录验证便于在STM32或ARM系统中对接传感器等从机设备。目前已有1659人学习参考可结合IIC时序、地址应答、ACK检测等协议要点阅读源码。资料内附HT1621相关驱动代码与红外接收程序文档能帮助开发者掌握软件模拟IIC的时钟同步、数据传输、起始停止条件实现并学习STC单片机I/O模拟通信的排错思路具有较强的工程参考价值。1. 为什么STC要模拟IIC从机发送做项目时遇到一个挺反直觉的场景STC单片机明明资源紧张却在系统里作为IIC从机主动向主机发送数据。以前多数人用STC都是让它当主机去读传感器很少考虑让它当从机。但实际在做温湿度采集、红外遥控解码、显示驱动这类前端模块时主机往往是STM32或ARM它们有自己的硬件IIC主模式后端数据要汇总到它那里。此时让STC做IIC从机发送数据就能省掉一路UART或者SPI布线也能少两根线。难点在于STC大部分型号没有硬件IIC外设只能靠GPIO模拟而且从机发送比主机接收更被动SCL时钟完全由主机掌控从机必须实时检测起始、停止、边沿和ACK位稍有延迟就会整个总线卡死。下面先从时序和协议角度把从机发送这件事拆开再给出可直接抄的C51源程序思路最后说说对接STM32时的常见坑。2. IIC从机发送的时序基础起始、停止、地址与ACK2.1 从机发送的完整时序窗口IIC通信协议中一次读操作从机发送数据的结构是主机先发起起始条件然后发送7位从机地址加1位读标志R/W1。地址匹配后从机在第9个SCL周期内拉低SDA作为ACK应答。紧接着主机释放SDA从机开始在后续的每个SCL高电平期间保持SDA数据SCL低电平期间切换SDA上的数据位一个字节发送完成后主机在第9个时钟周期回一个ACK或NACK。如果主机回NACK表示这是最后一个字节从机应当释放SDA等待主机产生停止条件。理解这段时序的关键是从机发送数据时数据线的控制权其实在从机手里但时钟线的控制权始终在主机手里。换句话说从机只能在SCL为低电平时把SDA拉高或拉低在SCL为高电平期间必须保持SDA稳定否则主机采样到的电平就是错的。很多初写模拟IIC的人容易忽略这个切换时机结果数据位错乱。2.2 软件模拟的时钟采样策略没有硬件IIC时软件要从SCL和SDA的跳变里自己推导出状态。常见做法有两种一种是把SCL接到外部中断引脚在上升沿和下降沿分别处理另一种是在主循环里高频轮询检测边沿和电平。对于STC单片机如果主机IIC速率是100kHz一个SCL周期是10us而C51在12MHz晶振下一条简单判断指令大约1us轮询方式勉强能覆盖但最好还是用中断。因为从机在等待主机发送数据时无法预知SCL什么时候来一旦主循环被其他任务占住起始条件可能被漏掉。推荐的策略是把SCL接INT0下降沿中断SDA作为普通IO输入。下降沿到来时表示SCL变低这是数据线可以切换的时刻上升沿则通过定时查询或另一个中断处理。不过STC大部分型号只有一个外部中断所以更实用的是组合方式下降沿中断里记录状态主循环里用边沿触发辅助判断停止条件。2.3 与硬件IIC的差异STM32和ARM的硬件IIC外设会自动管理起始停止、地址匹配和ACK而STC模拟从机必须全部手动实现。最典型的差异是ACK阶段的时序硬件IIC在地址匹配后自动拉低SDA软件模拟则需要在第9个SCL低电平时把SDA配置为输出并拉低然后在高电平结束前释放改成输入模式以便读取主机发来的ACK/NACK。这个输出方向和输入方向的切换必须精确落在SCL低电平期间否则总线会冲突。另一个差异是时钟拉伸。硬件IIC支持从机拉低SCL来延长时钟但STC模拟从机通常不主动拉伸SCL因为SCL是输入引脚。如果从机没有准备好下一字节的数据只能祈祷主机足够宽容或者通过软件提前把数据准备好。这也决定了程序里最好用状态机加缓冲区而不是临时计算。2.4 关键参数上拉电阻与速率IIC总线的SDA和SCL本身是开漏结构必须外接上拉电阻。电阻大小直接影响上升沿时间和最高通信速率。对于100kHz标准模式上拉电阻选4.7kΩ比较稳妥如果主机配置为400kHz快速模式建议用到2.2kΩ否则上升沿太缓高电平采样会出错。要注意的是STC的IO口工作在准双向模式时本来就有弱上拉但驱动能力有限外接上拉电阻仍然是必需品。速率模式SCL频率上拉电阻建议总线电容允许范围标准模式100kHz4.7kΩ ~ 10kΩ 400pF快速模式400kHz2.2kΩ ~ 4.7kΩ 200pF混合速率100kHz线长20cm4.7kΩ 100pF3. STC模拟IIC从机发送源程序设计解析3.1 项目结构与相关文件这套源程序里能看到ht6121.uvproj、ht6121.hex、HT1621.c、红外接收程序.rtf和readme.txt。其中HT1621.c是LCD显示驱动红外接收程序.rtf是红外解码的说明文档ht6121.uvproj是Keil工程文件。实际生产场景里STC从机测得的数据可能来自红外接收头或者由HT1621显示的内容再通过IIC从机接口发给上位主机。readme.txt通常说明如何用STC-ISP烧录hex文件以及IO分配。从这里能看出项目正题是“模拟IIC从机发送”而不是单独的协议测试。所以核心代码大概率封装在IICSTC2函数里围绕中断、状态机、发送缓冲区展开。由于Keil C51工程里没有独立的IIC源文件可能函数都写在HT1621.c所在文件或主文件中。3.2 GPIO初始化与IO模式配置STC单片机的IO口有多种工作模式准双向、推挽、高阻输入和开漏。对于模拟IICSDA口需要在输出和输入之间切换SCL口始终作为输入。常见的初始化方式如下#include STC15F2K60S2.H sbit SCL P1^0; // 时钟线接主机SCL sbit SDA P1^1; // 数据线双向 #define SDA_IN() { P1M1 | 0x02; P1M0 0xFD; } // P1.1 高阻输入 #define SDA_OUT() { P1M1 0xFD; P1M0 | 0x02; } // P1.1 推挽输出 void IIC_GPIO_Init(void) { P1M1 ~0x03; P1M0 | 0x01; // P1.0 准双向作为SCL输入 SDA_IN(); // SDA先设置为输入 SCL 1; SDA 1; EX0 1; // 使能外部中断0 IT0 1; // 下降沿触发 EA 1; }逻辑说明SCL作为输入不需要切换方向所以用准双向模式即可准双向自带弱上拉但外部仍需加上拉电阻。SDA必须能在输入和输出之间切换这里用宏控制P1M1和P1M0寄存器。SDA_OUT后写1就是释放总线写0就是拉低总线这点和开漏模式类似但推挽输出写1时会主动输出高电平如果此时主机也在驱动SDA可能产生冲突所以每次发送完一字节后要立即切回输入模式。参数说明P1M1和P1M0是STC15系列IO模式配置寄存器对应位为01时是推挽输出为00时是准双向。如果用的是STC89C52这种老型号IO口没有模式寄存器直接用SDA1和SDA0控制方向硬件上接开漏。3.3 起始条件、停止条件与地址匹配从机程序最核心的是正确识别总线的起始和停止条件。起始条件是SCL高电平时SDA由高变低停止条件是SCL高电平时SDA由低变高。由于SCL接在外部中断0上下降沿会触发中断但起始和停止发生在SCL高电平期间所以单纯依靠中断不够必须在主循环里检测SCL为高时SDA的电平变化。unsigned char IIC_DetectStartStop(void) { if (SCL 1) { if (SDA 0 last_sda 1) { last_sda 0; return IIC_START; // 检测到起始条件 } if (SDA 1 last_sda 0) { last_sda 1; return IIC_STOP; // 检测到停止条件 } } last_sda SDA; return IIC_NONE; }逻辑说明因为SCL为高时SDA既可能发生起始/停止跳变也可能处于数据稳定状态。为了区分用last_sda记录上一次采到的SDA值。当SCL为高且SDA发生跳变时就认为是起始或停止条件。如果SDA没有跳变则只是普通的数据位采样直接更新last_sda即可。这个函数需要在主循环中频繁调用调用间隔最好小于1us否则遇到短促的停止条件可能会漏掉。3.4 从机发送一字节并读取主机ACK从机发送数据时每一位都要在SCL低电平期间设置SDA高电平期间由主机采样。每发送8位后从机释放SDA在第9个时钟高电平期间读取主机回送的ACK/NACK位。unsigned char IIC_SendByte(unsigned char dat) { unsigned char i; unsigned char ack; for (i 0; i 8; i) { while (SCL 1); // 等待SCL变低 if (dat 0x80) { SDA_OUT(); SDA 1; // 发送1 } else { SDA_OUT(); SDA 0; // 发送0 } dat 1; while (SCL 0); // 等待SCL变高 // 这里不改变SDA保证高电平期间数据稳定 } while (SCL 1); // 等待第9个时钟低电平 SDA_IN(); // 释放SDA准备接收ACK while (SCL 0); // 等待高电平 ack SDA; // 采样主机ACK/NACK while (SCL 1); // 等待总线进入下一个低电平 return ack; // 返回0表示主机应答1表示NACK }逻辑说明循环内先等SCL为低然后设置SDA。此时SDA是新数据位SCL变高后从机必须保持SDA不变所以这段代码在SCL高电平期间没有做任何写SDA的操作。8位发完后第9个时钟是ACK位这时从机必须把SDA切回输入让主机能拉低SDA表示应答。函数返回值里0代表ACK1代表NACK。主机发NACK说明它不想继续接收了从机应该结束本次传输。参数说明dat是要发送的字节高位在前。如果数据频率较高while (SCL 1)这类语句可能卡死在电平上因此实际工程中需要加超时机制比如用定时器计数超过一定时间就强制退出并复位总线状态。3.5 从机状态机设计把前面的函数组合起来就能得到一个简单的从机状态机。主循环不断检测起始和停止条件然后进入地址接收、ACK响应、数据发送、ACK检测的循环。状态定义如下typedef enum { IIC_IDLE, IIC_ADDR, IIC_SEND_DATA, IIC_WAIT_STOP } IIC_STATE; void IIC_Task(void) { unsigned char addr; switch (state) { case IIC_IDLE: if (IIC_DetectStartStop() IIC_START) { bit_count 0; byte_buf 0; state IIC_ADDR; } break; case IIC_ADDR: // 接收8位地址R/W位 if (IIC_RecvBit(addr_bit)) { byte_buf (byte_buf 1) | addr_bit; if (bit_count 8) { if ((byte_buf 1) MY_IIC_ADDR (byte_buf 0x01)) { IIC_SendAck(0); tx_index 0; state IIC_SEND_DATA; } else { IIC_SendAck(1); state IIC_IDLE; } } } break; case IIC_SEND_DATA: // 发送缓冲区数据 if (tx_index tx_len) { if (IIC_SendByte(tx_buf[tx_index]) 1) { state IIC_WAIT_STOP; // 主机回NACK } } else { state IIC_WAIT_STOP; } break; case IIC_WAIT_STOP: if (IIC_DetectStartStop() IIC_STOP) { state IIC_IDLE; } break; } }逻辑说明状态机先停在IIC_IDLE检测到起始条件后逐位接收地址字节。地址匹配且读写位为1时发送ACK并进入SEND_DATA状态。发送完一个字节后通过返回值判断主机是ACK还是NACK如果是NACK就跳转到WAIT_STOP等待停止条件释放总线。用状态机的好处是可以在等待SCL电平变化时插入其他任务比如刷新HT1621显示或采集红外信号而不是阻塞在某个函数里。4. 联调实战把STC从机接到STM32或ARM主机4.1 硬件连接与上拉电阻计算STC从机和STM32主机之间只需三根线SCL、SDA、GND。上拉电阻需要放在主机侧还是从机侧并没有严格规定常见做法是就近放在STM32的IIC引脚上因为主机侧电源电压一般比较明确。STM32的IIC引脚是开漏输出内部有弱上拉但驱动能力有限所以一定要外接。根线长度超过20cm时建议在主机和从机两端各放一个上拉电阻阻值翻倍否则反射会导致波形畸变。计算上拉电阻上限通常按Rmax trise / (0.8473 * Cbus)其中trise是上升沿时间标准模式要求不超过1000ns快速模式不超过300ns。如果总线电容有200pF那么标准模式下Rmax约5.9kΩ实际取4.7kΩ比较平衡。下限主要考虑IO灌电流能力STC和STM32的IO最大灌电流都在20mA左右总线低电平电压要低于0.4V所以Rmin (VCC-0.4)/20mA3.3V供电时约145Ω实际一般不会低于1kΩ。4.2 STM32作为主机读取STC从机使用STM32CubeMX配置IIC1为主模式速率设为100kHz开启内部上拉。HAL库读取从机数据的函数如下uint8_t stc_data[8]; HAL_StatusTypeDef ret HAL_I2C_Mem_Read(hi2c1, (STC_ADDR 1) | 1, 0, I2C_MEMADD_SIZE_8BIT, stc_data, 8, 1000); if (ret ! HAL_OK) { printf(IIC read failed: %d\r\n, ret); }逻辑说明这里用的是HAL_I2C_Mem_Read但实际上STC从机并不支持寄存器地址这个概念。调用这个函数时主机会先发送一个寄存器地址字节然后才能读数据而我们的STC从机在状态机的地址匹配后会直接进入数据发送并不会解析寄存器地址。所以更准确的做法是用HAL_I2C_Master_Receive(hi2c1, (STC_ADDR 1) | 1, stc_data, 8, 1000)直接发起读操作不从设备地址后追加额外字节。参数说明第一个参数是IIC句柄第二个参数是设备地址左移一位并置1表示读第三个是接收缓冲区第四个是长度最后一个超时时间单位毫秒。如果地址匹配失败返回HAL_ERROR这时要去查STC从机的地址宏是否和主机一致。4.3 总线卡死与ACK丢失问题排查现象原因排查方法第一个字节就无法ACK地址不匹配或SDA方向切换太慢用万用表量从机引脚电平示波器看第9个SCL上升沿前SDA是否被拉低发送1字节后总线卡死从机发送完没释放SDA主机等ACK时一直等不到检查从机是否在发送完每个字节后调用SDA_IN()数据错位读取值每次差1位软件在SCL高电平时改变了SDA检查发送循环SDA只能在SCL低电平时写NACK提前到来主机发送长度大于从机缓冲区查看STM32读取长度是否超过STC从机的tx_len最常见的问题是从机在读ACK位时错把SDA拉低当作主机应答。主机应答时从机必须让SDA处于输入状态主机才有能力拉低SDA。如果从机代码里忘了切回输入SDA一直被从机拉低主机看到的永远是ACK通信过程就失去控制。4.4 用逻辑分析仪验证IIC波形联调时不要只靠看数据对不对建议用逻辑分析仪抓一遍IIC波形。把SCL接到8通道逻辑分析仪的D0SDA接D1采样率不要低于1MHz。重点关注三个位置起始条件是否被从机识别、地址字节最后一位是否为1、第9个时钟高电平期间SDA是否被主机拉低。用逻辑分析仪自带的IIC解码器可以直接看到起始、地址、ACK、数据、NACK、停止每个阶段。如果软件模拟的从机时序太慢主机已经提前释放时钟波形图上会看到SCL高电平时间明显延长这时要把主机速率降到50kHz再试或者优化从机的电平等待逻辑减少无谓的延时。5. 进阶技巧让从机发送不丢数据——软件FIFO与多字节突发5.1 从机处理不及时的根本矛盾前面状态机里有一个隐患如果STC从机正在忙于处理HT1621显示刷新或红外解码而主机恰好在这时发起读操作从机可能来不及读取新的测量数据只能把旧数据发出去。更极端的情况是主机一次要读取10个字节但从机的数据采集任务只准备好了2个字节剩余的8个字节填充的是无效数据。解决思路是建立一个发送缓冲区把主机请求抽象成“从缓冲区取字节”而不是“现场计算字节”。5.2 环形FIFO实现一个简单的无锁环形FIFO就能满足单生产者单消费者模型。数据采集任务负责往FIFO里写入IIC从机发送任务负责从FIFO中读取。#define FIFO_SIZE 16 volatile unsigned char fifo_buf[FIFO_SIZE]; volatile unsigned char fifo_head 0; volatile unsigned char fifo_tail 0; unsigned char fifo_push(unsigned char dat) { unsigned char next (fifo_head 1) % FIFO_SIZE; if (next fifo_tail) return 0; // FIFO满 fifo_buf[fifo_head] dat; fifo_head next; return 1; } unsigned char fifo_pop(unsigned char *dat) { if (fifo_head fifo_tail) return 0; // FIFO空 *dat fifo_buf[fifo_tail]; fifo_tail (fifo_tail 1) % FIFO_SIZE; return 1; }逻辑说明head指向下一个写入位置tail指向下一个读出位置。当两者相等时队列为空当(head1)%FIFO_SIZE等于tail时队列为满。这里特意浪费一个存储单元来区分空和满避免引入额外计数变量。因为STC是单核没有多线程所以这个FIFO在中断和主循环之间共享时不需要关中断只要保证head和tail的读写是原子的。参数说明FIFO_SIZE设为16意味着最多缓存15字节。主机如果一次读超过15字节超出部分会返回空导致从机发送固定值0xFF。可以根据实际需要加大到32或64但注意FIFO占用的是片内RAMSTC15系列通常有1KB左右RAM够用。如果数据量更大可以改成读取时直接引用外部变量但那样会降低灵活性。5.3 结合HT1621与红外接收数据源把FIFO接入IIC状态机后数据来源就可以多样化了。比如红外接收头解码出一个键值就通过fifo_push把它写入缓冲区HT1621显示的实时温度也通过同样的方式写入。主机周期性读取时数据总是最新的。需要注意FIFO里可能同时存在多种类型的数据所以实际项目通常会在数据前加上类型字节比如第一个字节表示数据类型后面跟上有效数据。5.4 验证方法用上位机读取联调验证时可以在STM32主机端把读到的数据通过串口转发到上位机。上位机发送一个读指令STM32收到后发起IIC读然后把数据按十六进制显示出来。如果每次打印的帧格式固定说明FIFO和从机状态机配合正常。也可以在STC从机里临时加一个计数器每发送一次数据计数器加一用逻辑分析仪数ACK数量看是否和上位机收到的数据包数量一致。这样就完成了整条链路的验证。本文还有配套的精品资源点击获取
返回列表