
🔥小龙报:个人主页
🎬作者简介:C++研发,嵌入式,机器人方向学习者
❄️个人专栏:《工科必装软件安装教程》《嵌入式的开端 ---- 51单片机》
✨ 永远相信美好的事情即将发生

文章目录
- 前言
- 一、如何现IIC的各类信号
-
- [1.1 起始信号](#1.1 起始信号)
- [1.2 终止信号](#1.2 终止信号)
- [1.3 应答信号&非应答信号](#1.3 应答信号&非应答信号)
-
- [1.3.1 应答信号](#1.3.1 应答信号)
- [1.3.2 非应答信号](#1.3.2 非应答信号)
- [1.3.3 等待应答/非应答信号](#1.3.3 等待应答/非应答信号)
- [1.4 数据信号](#1.4 数据信号)
-
- [1.4.1 发送数据](#1.4.1 发送数据)
- [1.4.2 读取数据](#1.4.2 读取数据)
- [1.4.3 效果演示](#1.4.3 效果演示)
- 二、温湿度传感器:CJ-GXHT3L
-
- [2.1 单次转换模式](#2.1 单次转换模式)
- [2.2 连续转换模式](#2.2 连续转换模式)
- [2.3 连续转换模式具体实现](#2.3 连续转换模式具体实现)
-
- [2.3.1 查看从机地址](#2.3.1 查看从机地址)
- [2.3.2 设置转换模式](#2.3.2 设置转换模式)
- [2.3.3 进行数据读取](#2.3.3 进行数据读取)
- [2.3.4 温湿度转换](#2.3.4 温湿度转换)
- [2.3.5 串口打印温湿度](#2.3.5 串口打印温湿度)
- [2.3.6 main函数实现](#2.3.6 main函数实现)
- [2.3.7 最终工程展现](#2.3.7 最终工程展现)
- 三、拓展
-
- [3.1 类型A:无地址映射的从机(直接读取)](#3.1 类型A:无地址映射的从机(直接读取))
- [3.2 类型B:有地址映射的从机(需要寄存器地址)](#3.2 类型B:有地址映射的从机(需要寄存器地址))
- 总结与每日励志
前言
IIC 是嵌入式开发中连接传感器、存储芯片等外设的核心串行协议,本文从 IIC 的起始 / 终止、应答 / 非应答、数据传输等基础信号入手,结合 51 单片机代码,手把手实现 IIC 的各类时序。再以温湿度传感器 GXHT3L 为例,从从机地址、连续转换模式、数据读取到串口打印,完整呈现 IIC 驱动开发的全流程,帮助初学者从时序理解到实战落地,掌握嵌入式外设驱动的核心方法。
一、如何现IIC的各类信号

空闲状态 :SCL线与SDA线均处于高电平状态,等待起始信号
起始信号 :SCL线为高电平期间,SDA线由高电平向低电平的变化表示起始信号
终止信号 :SCL线为高电平期间,SDA线由低电平向高电平的变化表示终止信号

ACK应答信号 :SCL从低到高再到低时,SDA都为低,表示接收方成功接收字节,要求继续传输。
NACK非应答信号 :SCL从低到高再到低时,SDA都为高,表示接收方未成功接收或要求停止传输。

数据传输:IIC总线进行数据传送时,时钟信号为高电平期间,数据线上的数据必须保持稳定,只有在时钟线上的信号为低电平期间,数据线上的高电平或低电平状态才允许变化。

数据传输过程中,先发送高位(MSB),再发送低位(LSB)。
例如:IIC发送0xF5(0b11110101)
1.1 起始信号
发送数据前要先发送起始信号,告知总线和从机开始通讯
起始信号 :SCL线为高电平期间,SDA线由高电平向低电平的变化表示起始信号

csharp
#include <reg52.h>
sbit sda = P0^1;
sbit scl = P0^2;
void i2c_start()
{
scl = 1;
sda = 1;
delay10ms(); //起始信号建立时间,高电平时间持续大于4.7us
sda = 0; //SDA拉低,下降沿
delay10ms(); //起始信号保持时间
}
延时函数:
csharp
void delay5ms(void) {
unsigned char i, j;
i = 9;
j = 244;
do
{
while (--j);
} while (--i);
}
void delay10ms(void) {
unsigned char i, j;
i = 18;
j = 235;
do
{
while (--j);
} while (--i);
}
1.2 终止信号
发送完成需要发结束信号,告知总线和从机通讯完成
终止信号: SCL线为高电平期间,SDA线由低电平向高电平的变化表示终止信号

csharp
void i2c_stop()
{
sda = 0;
scl = 1;
delay10ms(); //停止信号建立时间,SDA高电平时间持续大于4.7us
sda = 1; //SDA拉高,上升沿
delay10ms(); //总线空闲时间保持
}
1.3 应答信号&非应答信号
1.3.1 应答信号
当接收方成功接收数据,回复应答,要求发送方继续传输。
信号说明:SCL电平从低到高再到低时,SDA都为低。

csharp
//发送应答信号
void i2c_ack()
{
scl = 0;
sda = 0; //SDA拉低,发出应答信号
delay10ms();
scl = 1;
delay10ms();
scl = 0;
delay10ms();
}
1.3.2 非应答信号
当接收方未成功接收或要求停止传输时,发送非应答信号。
信号说明:SCL从低到高再到低时,SDA都为高。

csharp
void i2c_nack()
{
scl = 0;
sda = 1; //SDA拉高,发出非应答信号
delay10ms();
scl = 1;
delay10ms();
scl = 0;
delay10ms();
}
1.3.3 等待应答/非应答信号
用于IIC通信时,判断是否接收到应答信号。
SDA拉高(使得总线处于空闲状态),SCL从低到高时,检查SDA是否被拉低,如果被拉低,则获取到应答信号,如果还为高,则是获取到非应答信号
csharp
unsigned char i2c_wait_ack()
{
unsigned char ack_level = 0;
sda = 1; //sda为高电平释放总线
scl = 0;
delay10ms();
scl = 1;
delay10ms(); //延时使得数据稳定
ack_level = sda; // 读取SDA电平,0为ACK,1为NACK
scl = 0;
delay10ms();
return ack_level;
}
1.4 数据信号
1.4.1 发送数据
逐位发送数据或读取数据

发送1字节(8bit)数据的流程:
- SCL拉低,延时
- 获取需要发送数据的最高位数值,设置SDA电平,
- SCL拉高,延时
- 数据左移一位,重复1-3步8次,发送1字节(8bit)数据
注: 延时多长?这个延时时长会影响I2C的通信速率,例如拉低延时10ms,拉高延时10ms,那一个周期就是20ms,速率 = 1 / 20ms =50 Hz
csharp
void i2c_write_byte(unsigned char wdata)
{
unsigned char i;
for(i = 0; i < 8; i++)
{
scl = 0; //开始发送前,设置scl为0
delay10ms();
//判断最高位是1/0,是1设置sda输出高电平,是0设置sda输出低电平。
if(wdata& 0x80)
sda=1;
else
sda=0;
scl=1;
delay10ms();
wdata=wdata<<1;//左移一位,for循环8次,直到数据发送完
}
}
1.4.2 读取数据
读取1字节(8bit)数据的流程:
- SDA拉高,释放总线
- SCL拉低,延时
- SCL拉高,延时等待数据稳定
- 读取SDA电平
- SCL拉低,延时
- 重复2-5步8次获取1字节(8bit)数据
csharp
unsigned char i2c_read_byte()
{
int i,value;
sda=1; //释放总线
for(i=0;i<8;i++)
{
scl=0;
delay10ms();
scl=1;
delay10ms(); //等待数据稳定再读
value=value<<1; //移出位置空位准备接收
if(sda == 1){
//读取数据放到最低位
//sda=1的时候就把最后一位置1,
//sda=0的时候则不用置,因为默认为0
value = value|0x01;
}
}
return value;
}
1.4.3 效果演示
使用代码发送下方时序数据的实现:


二、温湿度传感器:CJ-GXHT3L
GXHT3L-DIS 是中科银河芯开发的新一代单芯片集成温湿度一体传感器。
★ I2C 接口,通信速度高达 1MHz
★ 两个用户可选择的地址
★ 精度为±4%RH 和±0.5°C
★ 单芯片集成温湿传感器
★ 高可靠性和长期稳定性
★ 测量 0-100%范围相对湿度
★ 测量-45-130℃范围内温度
此传感器支持单次转换模式和连续转换模式。
单次转换模式一般在低功耗设备上面使用,转换一次后进入低功耗模式,连续转换模式一般用于持续测量。
2.1 单次转换模式
芯片进入单次转换模式时,完成一次完整的温湿度转换后,将温湿度数据存放在接口寄存器,等待上位机读取测量数据。单次转换模式的时序,我们可以参考下方图:

其中MSB和LSB解释如下
例如:设置高重复率,开启长转换持续时间,读取测量得到的温湿度数据
例: 0x2C06分别对应:0x2C:clock stretching (时钟拉伸)开启,0x06:高重复率
2.2 连续转换模式
在收到周期转换温湿度命令后,芯片会周期性转换温湿度。可以选择不同的周期转换模式,如表所示。这些命令的主要 差别在于重复率(高、中、低)和周期转换频率(如每秒 0.5 次,1 次,2 次,4 次和 10 次), 周期转换频率和重复率的不同会影响测量的时间和功耗。

例如:0x2130中,21代表每秒转换一次,30代表高重复率。
设置完参数后,可以通过0xE000设置传感器,进入连续转换模式,时序如下:
2.3 连续转换模式具体实现
2.3.1 查看从机地址

引脚&外围说明:

关于ADDR管脚说明:

在这里,我们可以知道从机地址是0x44(0b0100 0100)
0b100 01000 -> 0b1000 1000 -> 0x88
0b100 01001 -> 0b1000 1001 -> 0x89
csharp
//注意:addr=0x44时,实际发送到总线的数据应该是0x44+读写标志位,0 为写,1 为读
#define GXHT3L_ADDR_WRITE (0x44<<1)+0 //10001000
#define GXHT3L_ADDR_READ (0x44<<1)+1 //10001001
2.3.2 设置转换模式
高重复率和周期转换频率,例如0x2130中,21代表每秒转换一次,30代表高重复率。

csharp
//MSB和LSB设置为0x2130 初始化进入连续转换模式,高重复率,每秒测量一次
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();
}
2.3.3 进行数据读取
开始连续转换读取的命令0xE000

(1) 发送写标志+设备地址,发送开始连续指令(0xE000)
(2) 发送读标志+设备地址,等待SCL拉低,读取温湿度数据。
csharp
//周期测量模式读取数据命令
void gxht30_set_read_mode() {
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();
}
void gxht30_read(){
int index = 0;
unsigned char buffer[6];
i2c_start();
i2c_write_byte(GXHT3L_ADDR_READ);
i2c_wait_ack();
/*
buffer[0]=i2c_read_byte(1);//温度高8位
i2c_ack();
buffer[1]=i2c_read_byte(1);//温度低8位
i2c_ack();
buffer[2]=i2c_read_byte(1);//CRC
i2c_ack();
buffer[3]=i2c_read_byte(1);//湿度高8位
i2c_ack();
buffer[4]=i2c_read_byte(1);//湿度低9位
i2c_ack();
buffer[5]=i2c_read_byte(0);//CRC
i2c_nack();
i2c_stop();
*/
for(index = 0;index < 6;index ++){
buffer[index]=i2c_read_byte();
if(index == 5)
i2c_nack();
else
i2c_ack();
}
i2c_stop();
}
2.3.4 温湿度转换

csharp
void gxht30_read_data() {
unsigned short tem, hum;
int index = 0;
float temperature, humidity;
unsigned char buffer[6];
i2c_start();
i2c_write_byte(GXHT3L_ADDR_READ);
i2c_wait_ack();
for (index = 0; index < 6; index++) {
buffer[index] = i2c_read_byte();
if (index == 5)
i2c_nack();
else
i2c_ack();
}
i2c_stop();
//合并两个8bit的数据为一个16bit的数据
tem = (buffer[0] << 8) | buffer[1];
hum = (buffer[3] << 8) | buffer[4];
//进行温湿度转换
temperature = (175.0 * (float) tem / 65535.0 - 45.0); // T = -45 + 175 * tem / (2^16-1)
humidity = (100.0 * (float) hum / 65535.0); // RH = hum*100 / (2^16-1)
}
2.3.5 串口打印温湿度
csharp
#include <stdio.h>
void uart_init(void) //9600bps@11.0592MHz
{
PCON &= 0x7F; //波特率不倍速
SCON = 0x50; //8位数据,可变波特率
TMOD &= 0x0F; //清除定时器1模式位
TMOD |= 0x20; //设定定时器1为8位自动重装方式
TL1 = 0xFD; //设定定时初值
TH1 = 0xFD; //设定定时器重装值
ET1 = 0; //禁止定时器1中断
TR1 = 1; //启动定时器1
}
/*
**重写printf调用的putchar函数,重定向到串口输出
**需要引入头文件<stdio.h>
*****/
char putchar(char dat) {
//输出重定向到串口
SBUF = dat; //写入发送缓冲寄存器
while (!TI); //等待发送完成,TI发送溢出标志位 置1
TI = 0; //对溢出标志位清零
return dat; //返回给函数的调用者printf
}
2.3.6 main函数实现
csharp
//带参延时函数
void delay_ms(unsigned int xms) //@12MHz
{
unsigned int i, j;
for (i = xms; i > 0; i--) {
for (j = 124; j > 0; j--) {}
}
}
void main() {
uart_init();
gxht30_init();
while (1) {
delay_ms(1000);
gxht30_read_mode();
gxht30_read_data();
}
}
2.3.7 最终工程展现
csharp
#include <reg52.h>
#include <stdio.h>
sbit sda = P0 ^ 1;
sbit scl = P0 ^ 2;
#define GXHT3L_ADDR_WRITE 0x44<<1 //0b0100 0100 -> 0b1000 1000
#define GXHT3L_ADDR_READ (0x44<<1)+1//0b0100 0100 -> 0b1000 1001
void delay5ms(void) {
unsigned char i, j;
i = 9;
j = 244;
do
{
while (--j);
} while (--i);
}
void delay10ms(void) {
unsigned char i, j;
i = 18;
j = 235;
do
{
while (--j);
} while (--i);
}
void i2c_start()
{
scl = 1;
sda = 1;
delay10ms(); //起始信号建立时间,高电平时间持续大于4.7us
sda = 0; //SDA拉低,下降沿
delay10ms(); //起始信号保持时间
}
void i2c_stop()
{
sda = 0;
scl = 1;
delay10ms(); //停止信号建立时间,SDA高电平时间持续大于4.7us
sda = 1; //SDA拉高,上升沿
delay10ms(); //总线空闲时间保持
}
void i2c_write_byte(unsigned char wdata)
{
unsigned char i;
for(i = 0; i < 8; i++)
{
scl = 0; //开始发送前,设置scl为0
delay10ms();
//判断最高位是1/0,是1设置sda输出高电平,是0设置sda输出低电平。
if(wdata& 0x80)
sda=1;
else
sda=0;
scl=1;
delay10ms();
wdata=wdata<<1;//左移一位,for循环8次,直到数据发送完
}
}
unsigned char i2c_read_byte()
{
int i,value;
sda=1; //释放总线
for(i=0;i<8;i++)
{
scl=0;
delay10ms();
scl=1;
delay10ms(); //等待数据稳定再读
value=value<<1; //移出位置空位准备接收
if(sda == 1){
//读取数据放到最低位
//sda=1的时候就把最后一位置1,
//sda=0的时候则不用置,因为默认为0
value = value|0x01;
}
}
return value;
}
//发送应答信号
void i2c_ack()
{
scl = 0;
sda = 0; //SDA拉低,发出应答信号
delay10ms();
scl = 1;
delay10ms();
scl = 0;
delay10ms();
}
void i2c_nack()
{
scl = 0;
sda = 1; //SDA拉高,发出非应答信号
delay10ms();
scl = 1;
delay10ms();
scl = 0;
delay10ms();
}
unsigned char i2c_wait_ack()
{
unsigned char ack_level = 0;
sda = 1; //sda为高电平释放总线
scl = 0;
delay10ms();
scl = 1;
delay10ms(); //延时使得数据稳定
ack_level = sda; // 读取SDA电平,0为ACK,1为NACK
scl = 0;
delay10ms();
return ack_level;
}
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();
}
void gxht30_set_read_mode() {
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();
}
void gxht30_read_data() {
unsigned short tem, hum;
int index = 0;
float temperature, humidity;
unsigned char buffer[6];
i2c_start();
i2c_write_byte(GXHT3L_ADDR_READ);
i2c_wait_ack();
for (index = 0; index < 6; index++) {
buffer[index] = i2c_read_byte();
if (index == 5)
i2c_nack();
else
i2c_ack();
}
i2c_stop();
//合并两个8bit的数据为一个16bit的数据
tem = (buffer[0] << 8) | buffer[1];
hum = (buffer[3] << 8) | buffer[4];
//进行温湿度转换
temperature = (175.0 * (float) tem / 65535.0 - 45.0); // T = -45 + 175 * tem / (2^16-1)
humidity = (100.0 * (float) hum / 65535.0); // RH = hum*100 / (2^16-1)
printf("temperature=%f humidity=%f\n", temperature, humidity);
}
void uart_init(void) //9600bps@11.0592MHz
{
PCON &= 0x7F; //波特率不倍速
SCON = 0x50; //8位数据,可变波特率
TMOD &= 0x0F; //清除定时器1模式位
TMOD |= 0x20; //设定定时器1为8位自动重装方式
TL1 = 0xFD; //设定定时初值
TH1 = 0xFD; //设定定时器重装值
ET1 = 0; //禁止定时器1中断
TR1 = 1; //启动定时器1
}
/*
**重写printf调用的putchar函数,重定向到串口输出
**需要引入头文件<stdio.h>
*****/
char putchar(char dat) {
//输出重定向到串口
SBUF = dat; //写入发送缓冲寄存器
while (!TI); //等待发送完成,TI发送溢出标志位 置1
TI = 0; //对溢出标志位清零
return dat; //返回给函数的调用者printf
}
//带参延时函数
void delay_ms(unsigned int xms) //@12MHz
{
unsigned int i, j;
for (i = xms; i > 0; i--) {
for (j = 124; j > 0; j--) {}
}
}
void main() {
uart_init();
gxht30_init();
while (1) {
delay_ms(1000);
gxht30_set_read_mode();
gxht30_read_data();
}
}
三、拓展
实际应用时,IIC设备非常多。
有简单的传感器、ADC芯片设备,这类设备可以使用前面讲到的方法使用IIC直接进行读写操作,例如下方类型A。
也有复杂的,例如存储类芯片,复杂传感器等等,这类设备一般有内部地址(可以从芯片或传感器的数据手册了解),这类设备除了需要指定设备地址,还需要指定操作的内部地址才能正常读写数据,例如下方类型B。
3.1 类型A:无地址映射的从机(直接读取)
-
特点: 每次读取都返回当前状态或数据,无需指定内部地址
-
示例: 某些ADC芯片、简单的状态读取设备
通信流程:
START\] → \[器件地址+写\] → \[等待ACK\] → \[写数据\] → \[等待ACK\] → \[STOP
读:
START\] → \[器件地址+读\] → \[等待ACK\] → \[读数据\] → \[发送NACK\] → \[STOP

3.2 类型B:有地址映射的从机(需要寄存器地址)
- 特点:内部有多个寄存器或存储单元
- 示例 :EEPROM、传感器芯片、RTC芯片等
例如:存储芯片内部的存储地址和存储的数据

通信流程:
START\] → \[器件地址+写\] → \[等待ACK\] → \[寄存器地址\] → \[等待ACK\] → \[写数据\] → \[等待ACK\] → \[STOP
读:
START\] → \[器件地址+写\] → \[等待ACK\] → \[寄存器地址\] → \[等待ACK\] → \[START\] → \[器件地址+读\] → \[等待ACK\] → \[读数据\] → \[NACK\] → \[STOP

总结与每日励志
✨本文从 IIC 基础时序拆解到 GXHT3L 温湿度传感器实战,清晰呈现了 "时序驱动 + 协议交互 + 数据解析" 的嵌入式开发逻辑。IIC 驱动开发的核心是对时序的精准把控,每一次 SCL/SDA 的电平翻转、每一次应答信号的判断,都是对细节的打磨。✨ 嵌入式学习没有捷径,沉下心吃透每一个协议细节,你的代码终将精准驾驭各类外设,在技术之路上稳步前行,不负每一份坚持与热爱!



