【51单片机】51 单片机 IIC 协议深度解析:时序实现 + GXHT3L 连续转换模式 + 数据解析

🔥小龙报:个人主页

🎬作者简介: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)数据的流程:

  1. SCL拉低,延时
  2. 获取需要发送数据的最高位数值,设置SDA电平,
  3. SCL拉高,延时
  4. 数据左移一位,重复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)数据的流程:

  1. SDA拉高,释放总线
  2. SCL拉低,延时
  3. SCL拉高,延时等待数据稳定
  4. 读取SDA电平
  5. SCL拉低,延时
  6. 重复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 的电平翻转、每一次应答信号的判断,都是对细节的打磨。✨ 嵌入式学习没有捷径,沉下心吃透每一个协议细节,你的代码终将精准驾驭各类外设,在技术之路上稳步前行,不负每一份坚持与热爱!

相关推荐
REDcker1 小时前
Paho MQTT C 开发者快速入门
c语言·开发语言·mqtt
Eloudy2 小时前
动态库中不透明数据结构的设计要点总结
数据结构·设计模式
良木生香2 小时前
【C++初阶】C++入门相关知识(1):C++历史 & 第一个C++程序 & 命名空间
c语言·开发语言·c++
重生之后端学习2 小时前
108. 将有序数组转换为二叉搜索树
数据结构·算法·深度优先
YJlio10 小时前
1.7 通过 Sysinternals Live 在线运行工具:不下载也能用的“云端工具箱”
c语言·网络·python·数码相机·ios·django·iphone
2013编程爱好者11 小时前
【C++】树的基础
数据结构·二叉树··二叉树的遍历
NEXT0611 小时前
二叉搜索树(BST)
前端·数据结构·面试
化学在逃硬闯CS11 小时前
Leetcode1382. 将二叉搜索树变平衡
数据结构·算法
不做无法实现的梦~12 小时前
ros2实现路径规划---nav2部分
linux·stm32·嵌入式硬件·机器人·自动驾驶