51单片机模拟IIC从机实战:手把手教你用两块STC89C52实现双向通信(附完整代码)
51单片机模拟IIC从机实战:双机通信系统设计与实现
在嵌入式开发领域,IIC总线因其简洁的两线制结构和多主机支持特性,成为设备间通信的常见选择。当手头只有基础51单片机(如STC89C52)却需要验证IIC通信协议时,完全可以通过软件模拟实现双机交互。本文将详细演示如何用两块STC89C52搭建IIC通信系统,其中一块作为主机,另一块模拟从机功能,实现双向数据交换。
1. 硬件准备与电路连接
1.1 所需材料清单
- STC89C52单片机 x2
- 杜邦线若干
- USB转TTL下载器 x2
- 5V电源适配器或USB供电线
- 面包板(可选,用于固定连接)
关键连接要点:
- 主从机的P3.4(SCL)相互连接
- 主从机的P3.5(SDA)相互连接
- 两片单片机共地(GND相连)
注意:实际连接时建议加入4.7KΩ上拉电阻到VCC,确保信号稳定性。若使用开发板,通常已内置上拉电阻。
1.2 引脚定义与寄存器配置
对于STC89C52,我们使用P3.4和P3.5作为IIC引脚:
sbit SCL = P3^4; // 时钟线 sbit SDA = P3^5; // 数据线2. IIC协议核心原理剖析
2.1 基础通信时序
IIC通信包含几个关键时序节点:
| 信号类型 | 时序特征 | 持续时间(典型值) |
|---|---|---|
| 起始信号 | SCL高电平时SDA下降沿 | >4.7μs |
| 停止信号 | SCL高电平时SDA上升沿 | >4.0μs |
| 数据有效 | SCL高电平期间保持SDA稳定 | >4.7μs |
| 数据变化 | SCL低电平期间允许SDA变化 | >4.7μs |
2.2 从机地址分配
在模拟从机时,需要定义设备地址。标准IIC地址为7位,我们采用0xA0作为从机写地址,0xA1作为读地址:
#define SLAVE_ADDR_W 0xA0 #define SLAVE_ADDR_R 0xA13. 从机端软件实现
3.1 起始/停止信号检测
从机需要持续监测总线状态:
bit I2C_CheckStart(void) { if(SCL && !SDA) { delay_us(5); if(SCL && !SDA) return 1; } return 0; } bit I2C_CheckStop(void) { if(SCL && SDA) { delay_us(5); if(SCL && SDA) return 1; } return 0; }3.2 数据接收与发送
从机响应主机的关键函数:
unsigned char I2C_SlaveReceive() { unsigned char i, dat = 0; SDA = 1; // 释放总线 for(i=0; i<8; i++) { while(!SCL); // 等待时钟高电平 dat <<= 1; if(SDA) dat |= 0x01; while(SCL); // 等待时钟低电平 } return dat; } void I2C_SlaveSend(unsigned char dat) { unsigned char i; for(i=0; i<8; i++) { SDA = (dat & 0x80) ? 1 : 0; dat <<= 1; while(!SCL); // 等待时钟高电平 while(SCL); // 等待时钟低电平 } SDA = 1; // 释放总线 }4. 主机端控制逻辑
4.1 基本信号生成
主机需要主动产生IIC时序信号:
void I2C_Start() { SDA = 1; SCL = 1; delay_us(5); SDA = 0; delay_us(5); SCL = 0; } void I2C_Stop() { SDA = 0; SCL = 1; delay_us(5); SDA = 1; delay_us(5); }4.2 完整读写流程
主机向从机写入数据的典型流程:
- 发送起始信号
- 发送从机地址+写标志
- 等待从机应答
- 发送寄存器地址
- 发送数据字节
- 发送停止信号
bit I2C_WriteByte(unsigned char addr, unsigned char dat) { I2C_Start(); I2C_SendByte(SLAVE_ADDR_W); if(!I2C_CheckAck()) { I2C_Stop(); return 0; } I2C_SendByte(addr); if(!I2C_CheckAck()) { I2C_Stop(); return 0; } I2C_SendByte(dat); if(!I2C_CheckAck()) { I2C_Stop(); return 0; } I2C_Stop(); return 1; }5. 系统调试与优化
5.1 常见问题排查
- 无应答信号:检查地址匹配、上拉电阻、电源电压
- 数据错误:调整延时参数,确保时序满足规范
- 总线冲突:确认从机在非通信时段释放SDA线
5.2 性能优化建议
- 使用定时器产生精确延时替代delay_us()
- 添加CRC校验提高通信可靠性
- 实现超时机制避免死锁
bit I2C_CheckAck() { unsigned int timeout = 1000; SDA = 1; // 释放总线 SCL = 1; while(SDA && timeout--) delay_us(1); SCL = 0; return (timeout > 0); }6. 完整应用示例
实现一个简单的温度监控系统,主机定期读取从机采集的数据:
从机程序片段:
void main() { unsigned char temp_data = 0; while(1) { if(I2C_CheckStart()) { if(I2C_SlaveReceive() == SLAVE_ADDR_W) { I2C_SendAck(); // 处理写操作... } else if(I2C_SlaveReceive() == SLAVE_ADDR_R) { I2C_SendAck(); I2C_SlaveSend(temp_data++); // 模拟温度变化 } while(!I2C_CheckStop()); // 等待停止信号 } // 其他任务... } }主机程序片段:
void main() { unsigned char recv_data; while(1) { if(I2C_ReadByte(0x00, &recv_data)) { printf("Current Temp: %d\n", recv_data); } delay_ms(1000); } }