新手自学嵌入式 | 学习日记: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线,这样主机才能成功发出停止信号。

相关推荐
鬼手点金3 小时前
构建有效 Agent 的核心原则
java·人工智能·git·学习·自动化
姜九笙5613 小时前
python学习---归来仍是新人
java·python·学习
海绵宝宝转agent4 小时前
2026-10-19学习笔记
笔记·学习
云水初4 小时前
【agent篇】RAG Agent避坑指南(附高级RAG Agent汇总)
开发语言·笔记·python·学习·langchain·agent·rag
外收内放4 小时前
数据分析(图表信息详解)
python·学习·数据分析·matplotlib
气质、小青年!4 小时前
进程1学习
linux·学习
坤坤子吖5 小时前
Linux 线程同步与互斥——互斥锁
linux·服务器·c++·笔记·学习
撩得Android一次心动5 小时前
Jetpack Compose 知识点整理3【个人用】
android·笔记·学习·kotlin·android compose
阿梦Anmory5 小时前
规划领域定义语言(PDDL)学习笔记
笔记·学习