1. 51单片机IIC协议深度解析:从时序到实战
在嵌入式开发中,IIC(Inter-Integrated Circuit)总线是最常用的串行通信协议之一。它只需要两根线(SCL时钟线和SDA数据线)就能实现多个设备之间的通信,非常适合连接各类传感器、存储芯片等外设。对于51单片机开发者来说,掌握IIC协议的底层实现是必备技能。
1.1 IIC协议基础特性
IIC总线有以下几个关键特性:
- 两线制:SCL(Serial Clock)和SDA(Serial Data)
- 多主多从架构:支持多个主设备和从设备
- 7位或10位地址:可寻址多达112个设备(7位地址时)
- 半双工通信:同一时间只能发送或接收数据
- 标准模式(100kbps)、快速模式(400kbps)和高速模式(3.4Mbps)
在实际应用中,大多数51单片机项目使用标准模式或快速模式。由于51单片机通常没有硬件IIC控制器,我们需要用GPIO模拟IIC时序,这也是理解IIC协议底层原理的好机会。
1.2 IIC总线状态解析
IIC总线在任何时刻都处于以下四种状态之一:
- 空闲状态:SCL和SDA都为高电平
- 起始状态:SCL为高时,SDA从高变低
- 数据传输状态:SCL为低时改变SDA,SCL为高时读取SDA
- 停止状态:SCL为高时,SDA从低变高
理解这些基本状态是正确实现IIC协议的基础。下面我们将深入分析每个信号的实现细节。
2. IIC基础信号实现
2.1 起始信号实现
起始信号(START Condition)是IIC通信的开始标志。它的时序要求是:
- SCL和SDA初始都为高电平(空闲状态)
- 保持SCL为高电平
- SDA从高电平变为低电平
- 保持一段时间后,SCL变为低电平
c复制void i2c_start() {
scl = 1;
sda = 1;
delay10ms(); // 起始信号建立时间,需大于4.7μs
sda = 0; // SDA拉低产生下降沿
delay10ms(); // 起始信号保持时间
scl = 0; // 准备数据传输
}
注意事项:
- 起始信号建立时间(tSU;STA)至少4.7μs
- 起始信号保持时间(tHD;STA)至少4.0μs
- 在实际应用中,适当增加延时可以提高稳定性
2.2 终止信号实现
终止信号(STOP Condition)标志IIC通信结束。它的时序要求是:
- SCL初始为高电平,SDA为低电平
- SDA从低电平变为高电平
- 保持一段时间后,总线进入空闲状态
c复制void i2c_stop() {
sda = 0;
scl = 1;
delay10ms(); // 停止信号建立时间,需大于4.7μs
sda = 1; // SDA拉高产生上升沿
delay10ms(); // 总线空闲时间保持
}
注意事项:
- 停止信号建立时间(tSU;STO)至少4.7μs
- 总线空闲时间(tBUF)至少4.7μs后才能发起新的起始信号
- 在连续多次通信时,必须确保足够的停止时间
2.3 应答信号机制
IIC协议使用应答(ACK)和非应答(NACK)机制来确认数据传输是否成功。
2.3.1 应答信号(ACK)
接收方在成功接收一个字节后,会在第9个时钟周期将SDA拉低,表示应答。
c复制void i2c_ack() {
scl = 0;
sda = 0; // SDA拉低表示应答
delay10ms();
scl = 1;
delay10ms();
scl = 0;
delay10ms();
}
2.3.2 非应答信号(NACK)
接收方如果未能成功接收数据或希望结束传输,会在第9个时钟周期保持SDA为高电平。
c复制void i2c_nack() {
scl = 0;
sda = 1; // SDA保持高电平表示非应答
delay10ms();
scl = 1;
delay10ms();
scl = 0;
delay10ms();
}
2.3.3 等待应答信号
主设备发送完数据后,需要检测从设备是否返回应答信号。
c复制unsigned char i2c_wait_ack() {
unsigned char ack_level = 0;
sda = 1; // 释放SDA线
scl = 0;
delay10ms();
scl = 1;
delay10ms(); // 等待数据稳定
ack_level = sda; // 读取SDA电平
scl = 0;
delay10ms();
return ack_level; // 0表示ACK,1表示NACK
}
经验分享:
- 在实际应用中,建议增加超时检测机制,避免因从设备故障导致程序死等
- 某些设备在特定条件下会故意返回NACK,如EEPROM在内部写周期时
- 读取多个字节时,最后一个字节通常用NACK结束读取
3. IIC数据传输实现
3.1 数据发送实现
IIC协议规定数据在SCL为高电平时必须保持稳定,在SCL为低电平时可以变化。数据发送总是从最高位(MSB)开始。
c复制void i2c_write_byte(unsigned char wdata) {
unsigned char i;
for(i = 0; i < 8; i++) {
scl = 0;
delay10ms();
if(wdata & 0x80) // 检测最高位
sda = 1;
else
sda = 0;
scl = 1;
delay10ms();
wdata = wdata << 1; // 左移一位
}
}
3.2 数据接收实现
数据接收同样遵循MSB优先的原则。读取数据时需要先将SDA设置为输入模式(对于51单片机,就是先释放SDA线)。
c复制unsigned char i2c_read_byte() {
unsigned char i, value = 0;
sda = 1; // 释放总线,准备读取
for(i = 0; i < 8; i++) {
scl = 0;
delay10ms();
scl = 1;
delay10ms(); // 等待数据稳定
value = value << 1; // 左移一位
if(sda == 1) {
value |= 0x01; // 设置最低位
}
scl = 0;
delay10ms();
}
return value;
}
注意事项:
- 读取数据前必须确保SDA处于高阻态(释放总线)
- 每个时钟周期后需要将SCL拉低,保持总线控制
- 读取多个字节时,最后一个字节后应发送NACK
4. GXHT3L温湿度传感器驱动开发
4.1 GXHT3L传感器概述
GXHT3L是中科银河芯推出的高精度数字温湿度传感器,主要特性包括:
- IIC接口,最高1MHz通信速率
- 温度测量范围:-45℃~130℃,精度±0.5℃
- 湿度测量范围:0~100%RH,精度±4%RH
- 支持单次转换和连续转换模式
- 两种可选的IIC地址(0x44和0x45)
4.2 传感器地址配置
GXHT3L的地址由ADDR引脚决定:
- ADDR接GND:器件地址0x44(写地址0x88,读地址0x89)
- ADDR接VDD:器件地址0x45(写地址0x8A,读地址0x8B)
c复制#define GXHT3L_ADDR_WRITE (0x44<<1) // 0x88
#define GXHT3L_ADDR_READ ((0x44<<1)|1) // 0x89
4.3 连续转换模式配置
连续转换模式适合需要持续监测的应用场景。配置步骤如下:
- 发送起始信号
- 发送器件写地址(0x88)
- 发送配置命令(0x2130表示每秒测量一次,高重复率)
- 发送停止信号
c复制void gxht30_init() {
i2c_start();
i2c_write_byte(GXHT3L_ADDR_WRITE);
i2c_wait_ack();
i2c_write_byte(0x21); // 配置命令高位
i2c_wait_ack();
i2c_write_byte(0x30); // 配置命令低位
i2c_wait_ack();
i2c_stop();
}
4.4 数据读取实现
在连续转换模式下,读取数据的流程如下:
- 发送起始信号
- 发送器件写地址(0x88)
- 发送读取命令(0xE000)
- 发送停止信号
- 发送起始信号
- 发送器件读地址(0x89)
- 读取6字节数据(温度高/低字节、CRC、湿度高/低字节、CRC)
- 发送停止信号
c复制void gxht30_read_data() {
unsigned char buffer[6];
unsigned short temp_raw, humi_raw;
float temperature, humidity;
// 发送读取命令
i2c_start();
i2c_write_byte(GXHT3L_ADDR_WRITE);
i2c_wait_ack();
i2c_write_byte(0xE0);
i2c_wait_ack();
i2c_write_byte(0x00);
i2c_wait_ack();
i2c_stop();
// 读取数据
i2c_start();
i2c_write_byte(GXHT3L_ADDR_READ);
i2c_wait_ack();
for(int i = 0; i < 6; i++) {
buffer[i] = i2c_read_byte();
if(i == 5)
i2c_nack(); // 最后一个字节发送NACK
else
i2c_ack(); // 其他字节发送ACK
}
i2c_stop();
// 数据处理
temp_raw = (buffer[0] << 8) | buffer[1];
humi_raw = (buffer[3] << 8) | buffer[4];
temperature = -45 + 175.0 * temp_raw / 65535.0;
humidity = 100.0 * humi_raw / 65535.0;
printf("Temperature: %.2f C, Humidity: %.2f %%RH\n", temperature, humidity);
}
注意事项:
- 温湿度数据是16位无符号整数,需要按照公式转换为实际值
- 温度转换公式:T = -45 + 175 × (temp_raw / 65535)
- 湿度转换公式:RH = 100 × (humi_raw / 65535)
- 建议增加CRC校验以提高数据可靠性
5. 完整工程实现
5.1 硬件连接
GXHT3L与51单片机的典型连接方式:
- VDD:接3.3V或5V电源
- GND:接地
- SCL:接P0.2(或其他GPIO)
- SDA:接P0.1(或其他GPIO)
- ADDR:接地(地址0x44)或接VDD(地址0x45)
5.2 软件实现
完整工程包含以下功能:
- IIC基础信号实现
- GXHT3L初始化与配置
- 温湿度数据读取与转换
- 串口打印输出
c复制#include <reg52.h>
#include <stdio.h>
sbit sda = P0^1;
sbit scl = P0^2;
#define GXHT3L_ADDR_WRITE (0x44<<1)
#define GXHT3L_ADDR_READ ((0x44<<1)|1)
// 延时函数
void delay10ms() {
unsigned char i, j;
i = 18;
j = 235;
do {
while (--j);
} while (--i);
}
// IIC基础信号实现
void i2c_start() { /* 同上 */ }
void i2c_stop() { /* 同上 */ }
void i2c_ack() { /* 同上 */ }
void i2c_nack() { /* 同上 */ }
unsigned char i2c_wait_ack() { /* 同上 */ }
void i2c_write_byte(unsigned char wdata) { /* 同上 */ }
unsigned char i2c_read_byte() { /* 同上 */ }
// GXHT3L驱动
void gxht30_init() { /* 同上 */ }
void gxht30_read_data() { /* 同上 */ }
// 串口初始化
void uart_init() {
PCON &= 0x7F;
SCON = 0x50;
TMOD &= 0x0F;
TMOD |= 0x20;
TL1 = 0xFD;
TH1 = 0xFD;
ET1 = 0;
TR1 = 1;
}
// 重定向putchar
char putchar(char c) {
SBUF = c;
while(!TI);
TI = 0;
return c;
}
// 主函数
void main() {
uart_init();
gxht30_init();
while(1) {
gxht30_read_data();
delay_ms(1000); // 1秒间隔
}
}
5.3 常见问题排查
-
无应答信号:
- 检查器件地址是否正确
- 确认硬件连接正常(上拉电阻、电源等)
- 检查时序是否符合规范
-
数据异常:
- 检查电源是否稳定
- 增加适当的延时
- 验证CRC校验(如果支持)
-
通信不稳定:
- 缩短通信距离
- 增加上拉电阻(通常4.7kΩ)
- 降低通信速率
6. IIC设备类型扩展
6.1 简单设备(无寄存器地址)
这类设备通常功能简单,每次读取都返回当前状态值。通信流程为:
- 发送起始信号
- 发送器件地址+写
- 发送命令/数据
- 发送停止信号
- (如需读取)发送起始信号
- 发送器件地址+读
- 读取数据
- 发送停止信号
6.2 复杂设备(有寄存器地址)
这类设备内部有多个寄存器,读写时需要指定寄存器地址。典型流程为:
写操作:
- 发送起始信号
- 发送器件地址+写
- 发送寄存器地址
- 发送数据
- 发送停止信号
读操作:
- 发送起始信号
- 发送器件地址+写
- 发送寄存器地址
- 发送起始信号(重复起始)
- 发送器件地址+读
- 读取数据
- 发送停止信号
在实际开发中,务必参考具体器件的数据手册,因为不同厂商的IIC设备可能有特殊的协议要求。