新手自学嵌入式 | 学习日记:I2C总线协议、时序分析、读写流程与代码实现

今天的学习聚焦于嵌入式领域最常用的两线制总线------I2C。从波形图入手,逐步拆解了起始信号、数据传输、应答机制和结束信号,并重点梳理了I2C的读写时序与代码实现框架。


一、 什么是I2C?

I2C (Inter-Integrated Circuit) 是一种同步、半双工、多主多从的串行通信总线。

  • 两根线 :SCL (Serial Clock,时钟线) 和 SDA (Serial Data,数据线)。

  • 数据传输 :采用高位先行 (MSB First)的原则。例如 0xAC 的二进制是 1010 1100,传输时先发 1,最后发 0。

  • 通信速率 :标准模式为 100KHz,快速模式为 400KHz,高速模式下有 1.x MHz 甚至 3.x MHz。


二、 I2C核心时序剖析

结合波形图,I2C的时序由以下几个核心部分组成:

1. 起始信号 (Start)

  • 规则 :当 SCL 时钟线为高电平 时,SDA 数据线由高变低出现下降沿。

  • 作用:标志着一次通信的开始。

2. 数据传输

  • SCL高电平期间 :SDA 数据线上的电平必须保持稳定,此时接收方进行数据采样。

  • SCL低电平期间 :SDA 数据线上的电平可以改变,此时发送方发送数据。

3. 应答信号 (ACK / NACK)

  • 规则 :在每次传输完8个位(1个字节)后的第9个时钟周期,接收方需要向发送方回复应答信号。

  • ACK (低电平):表示接收方成功接收到数据。

  • NACK (高电平):表示接收方未接收/拒绝接收。

4. 结束信号 (Stop)

  • 规则 :当 SCL 时钟线为高电平 时,SDA 数据线由低变高出现上升沿。

  • 作用:标志着一次通信的结束。


三、 设备地址与数据流向

起始信号后的第一个字节非常关键,它包含了设备地址和读写方向:

  • 高7位 :从机设备地址。例如图中 0x10,其二进制为 0001 000,这就意味着总线上最多可以有 2^7 = 127 个从机设备。

  • 低1位 (LSB):数据流向位。

    • 0:表示主机发送数据给从机(写)。

    • 1:表示从机发送数据给主机(读)。


四、 I2C 读写操作流程

结合图片中的流程图,I2C的读写时序如下:

1. I2C 写时序(主机向从机指定寄存器写数据)
  1. 主机发送 起始信号。

  2. 主机发送 设备地址 + 写位(0) ,等待从机回复 ACK。

  3. 主机发送 寄存器地址 ,等待从机回复 ACK。

  4. 主机向该寄存器发送数据 ,等待从机回复 ACK。

  5. 主机发送 停止信号。

2. I2C 读时序(主机从从机指定寄存器读数据)
  1. 主机发送 起始信号。

  2. 主机发送 设备地址 + 写位(0) ,等待从机回复 ACK。(注:必须先写,告诉从机我们要操作哪个寄存器)

  3. 主机发送 寄存器地址 ,等待从机回复 ACK。

  4. 主机重启起始信号 (Restart)。

  5. 主机发送 设备地址 + 读位(1) ,等待从机回复 ACK。

  6. 主机接收从机发来的数据(如果是读取多个字节,除了最后一个字节回 NACK 外,其余都回 ACK)。

  7. 主机发送 停止信号。


五、 代码实战:实现 i2c_read 函数

结合上述读时序,这里基于裸机硬件操作(或软件模拟I2C)写出 i2c_read 的核心逻辑框架。通常我们需要封装底层的起始、停止、发送字节、接收字节函数。

c

复制代码
/* 
 * 假设已有底层基础函数:
 * I2C_Start(); I2C_Stop(); I2C_SendByte(); I2C_WaitAck(); I2C_SendAck(); I2C_SendNack(); I2C_ReadByte();
 */

unsigned char i2c_read(unsigned char dev_addr, unsigned char reg_addr)
{
    unsigned char data = 0;

    // 1. 发送起始信号
    I2C_Start();

    // 2. 发送设备地址 + 写位 (0)
    I2C_SendByte((dev_addr << 1) | 0x00); 
    I2C_WaitAck(); // 等待从机ACK应答

    // 3. 发送寄存器地址
    I2C_SendByte(reg_addr);
    I2C_WaitAck(); // 等待从机ACK应答

    // 4. 重新发送起始信号 (Restart)
    I2C_Start();

    // 5. 发送设备地址 + 读位 (1)
    I2C_SendByte((dev_addr << 1) | 0x01);
    I2C_WaitAck(); // 等待从机ACK应答

    // 6. 接收数据
    data = I2C_ReadByte();
    
    // 只读一个字节,主机需要发送 NACK (非应答) 告诉从机不用再发数据了
    I2C_SendNack(); 

    // 7. 发送停止信号
    I2C_Stop();

    return data;
}

(注:如果是多字节连续读,需要在循环里 I2C_SendAck() 读取前面的数据,最后一次再 I2C_SendNack()。)


六、 硬件连接与寄存器寻址

从图示底部的内存映射/寄存器图可以看出,I2C从机设备(如EEPROM、传感器等)内部通常有 0x00 到 0xFF 的地址空间(即256个8位寄存器)。我们通过I2C总线发送的"寄存器地址",实际上就是去定位这些内部的格子,进而读写其中的数据。


今日总结与反思

  1. 时序是总线协议的灵魂:I2C的起始/停止信号非常巧妙,利用了电平的高低状态与跳变沿。必须在SCL高电平期间保持SDA稳定,这是最核心的采样规则。

  2. "读"的本质是"先写后读":I2C读数据并不是直接读的,必须先发送写指令锁定寄存器地址,再重启发读指令,这个步骤在写代码时极其容易漏掉重启信号(Restart)。

  3. ACK与NACK:主机在读完最后一个字节时,必须发送NACK(高电平)释放SDA线,这样主机才能成功发出停止信号。

相关推荐
叼烟扛炮2 小时前
[LangChain] 学习笔记(06):突破大模型限制——结构化输出(Structured Output)
windows·笔记·学习·langchain
且从容.3 小时前
VUE3学习笔记(根据黑马教程总结)
笔记·学习
蒸蒸yyyyzwd5 小时前
关于 AI 开发相关的一些面经。
笔记·学习
坤坤子吖11 小时前
Linux网络编程:TCP服务器的多进程与多线程实现
linux·服务器·开发语言·网络·c++·笔记·学习
蒸蒸yyyyzwd12 小时前
后端相关学习笔记。
笔记·学习
旖旎夜光13 小时前
【C++ 基础入门】C++ 多态(上):虚函数、重写、override/final 与抽象类
开发语言·c++·笔记·学习
橙橙笔记15 小时前
QT的学习第四部分
linux·qt·学习·音视频
叼烟扛炮16 小时前
Linux 学习笔记(五十四):内网穿透实战——用 frp 让外网访问你的私人电脑
linux·服务器·网络·笔记·学习
在猴站学算法16 小时前
(《统计学习方法》(第2版) 李航)第2章 感知机算法
学习·算法·机器学习·统计学习
旖旎夜光16 小时前
【C++ 基础入门】C++ 继承(上):概念、切片、隐藏、默认成员函数与友元
c++·笔记·学习