握手协议在I2C中的应用

握手协议在I2C中的应用

I2C协议是嵌入式领域最经典、最直观的硬件握手协议实例。 它完美展示了如何在两根线上实现完整的双向通信握手。


1. I2C握手的本质:时钟线仲裁

I2C的核心握手机制不是通过显式的"REQ/ACK"信号,而是通过时钟线(SCL)和数据线(SDA)的线与逻辑实现的。

信号线 角色 握手功能
SCL (Serial Clock) 时钟同步 提供通信节拍,每个时钟脉冲都是一次握手
SDA (Serial Data) 数据传输 承载数据,并在特定时刻(第9个时钟)用于应答握手

关键特性

  • 所有设备共享SCL和SDA(真正的总线)
  • 线与(Wire-AND)逻辑 :任何设备都可以拉低线路
  • 时钟拉伸(Clock Stretching) :从机可以控制SCL实现硬件级等待

2. I2C的三层握手结构

层1:字节级握手(ACK/NACK)

这是最显式的握手,每个字节传输后都有一个应答位。

ACK规则

  • ACK (低电平) :从机正确接收,继续传输
  • NACK (高电平) :三种情况:
  1. 从机未响应(地址不匹配)
  2. 从机无法接收更多数据(缓冲区满)
  3. 主机作为接收方时,发送NACK表示读取结束

层2:事务级握手(START/STOP条件)

定义传输的开始和结束。

复制代码
开始条件(S):SCL高时,SDA从高→低
结束条件(P):SCL高时,SDA从低→高

SDA: ______/           \__________
SCL: ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
       ↓START           STOP↓

层3:时钟级握手(时钟拉伸)

这是最底层的硬件握手,允许从机控制通信节奏。

复制代码
主机驱动SCL低 → 从机需要更多时间 → 从机保持SCL低 → 主机等待SCL变高 → 继续

SCL(主机驱动):___/‾‾\___________/‾‾\___
SCL(从机干预):___/‾‾\_____/     \_____   ← 从机拉低SCL
实际SCL总线:    ___/‾‾\_____/‾‾\________   ← "线与"结果
                主机  从机  从机  主机
                输出  保持  释放  检测

3. I2C完整传输的握手流程

让我们看一个主机写数据到从机的完整过程:

c

复制代码
// 时序分解:
// S: START条件
// Sr: REPEATED START条件
// P: STOP条件
// A: ACK (低电平)
// N: NACK (高电平)

// 主机写入2个字节到从机地址0x50
S | 0xA0 | A | 0x00 | A | 0x12 | A | P
   ↑      ↑  ↑      ↑  ↑      ↑  ↑  ↑
   地址   应答 数据1 应答 数据2 应答 停止
   写  

握手状态机实现(从机侧)

verilog

复制代码
module i2c_slave #(
    parameter I2C_ADDR = 7'h50
)(
    inout wire SDA,
    inout wire SCL,
    // 用户接口
    output reg [7:0] rx_data,
    output reg rx_valid,
    input wire [7:0] tx_data,
    input wire tx_ready
);

    // I2C从机状态机
    typedef enum logic [3:0] {
        S_IDLE,           // 等待START条件
        S_ADDR,           // 接收地址字节
        S_ADDR_ACK,       // 发送地址ACK
        S_RX_DATA,        // 接收数据字节
        S_RX_ACK,         // 发送数据ACK
        S_TX_DATA,        // 发送数据字节
        S_TX_ACK,         // 等待主机ACK
        S_WAIT_STOP       // 等待STOP条件
    } state_t;
  
    state_t state, next_state;
    reg [2:0] bit_counter;    // 位计数器 (0-7)
    reg [7:0] shift_reg;      // 移位寄存器
    reg own_sda;              // SDA输出值
    reg sda_oe;               // SDA输出使能
  
    // 线与逻辑实现
    assign SDA = sda_oe ? own_sda : 1'bz;
    wire sda_in = SDA;  // 输入采样
  
    // 边沿检测
    reg scl_prev, sda_prev;
    wire scl_rising = (SCL && !scl_prev);
    wire scl_falling = (!SCL && scl_prev);
    wire start_cond = (scl_prev && sda_prev && !SDA);
    wire stop_cond = (scl_prev && !sda_prev && SDA);
  
    always @(posedge SCL or negedge SCL) begin
        scl_prev <= SCL;
        sda_prev <= SDA;
    end
  
    // 主状态机
    always @(posedge SCL, posedge start_cond, posedge stop_cond) begin
        if (start_cond) begin
            state <= S_ADDR;
            bit_counter <= 3'd0;
            sda_oe <= 1'b0;  // 释放SDA
        end
        else if (stop_cond) begin
            state <= S_IDLE;
            sda_oe <= 1'b0;
        end
        else if (scl_rising) begin
            state <= next_state;
        
            case (state)
                S_IDLE: begin
                    // 等待START
                end
            
                S_ADDR: begin
                    // 采样地址位
                    shift_reg <= {shift_reg[6:0], sda_in};
                    bit_counter <= bit_counter + 1;
                    if (bit_counter == 7) begin
                        // 检查地址匹配
                        if (shift_reg[7:1] == I2C_ADDR) begin
                            next_state <= S_ADDR_ACK;
                        end else begin
                            next_state <= S_IDLE;  // 地址不匹配
                        end
                    end
                end
            
                S_ADDR_ACK: begin
                    // 发送ACK (拉低SDA)
                    own_sda <= 1'b0;
                    sda_oe <= 1'b1;
                    if (shift_reg[0] == 1'b0) begin  // 写操作
                        next_state <= S_RX_DATA;
                    end else begin                   // 读操作
                        next_state <= S_TX_DATA;
                    end
                    bit_counter <= 3'd0;
                end
            
                S_RX_DATA: begin
                    // 接收数据位
                    shift_reg <= {shift_reg[6:0], sda_in};
                    bit_counter <= bit_counter + 1;
                    if (bit_counter == 7) begin
                        rx_data <= {shift_reg[6:0], sda_in};
                        rx_valid <= 1'b1;
                        next_state <= S_RX_ACK;
                    end
                end
            
                S_RX_ACK: begin
                    // 发送数据ACK
                    own_sda <= 1'b0;
                    sda_oe <= 1'b1;
                    next_state <= S_RX_DATA;  // 继续接收下一个字节
                end
            
                S_TX_DATA: begin
                    // 发送数据位
                    if (bit_counter == 0)
                        shift_reg <= tx_data;  // 加载发送数据
                
                    own_sda <= shift_reg[7];
                    sda_oe <= 1'b1;
                    shift_reg <= {shift_reg[6:0], 1'b1};
                    bit_counter <= bit_counter + 1;
                
                    if (bit_counter == 7)
                        next_state <= S_TX_ACK;
                end
            
                S_TX_ACK: begin
                    // 释放SDA,等待主机ACK
                    sda_oe <= 1'b0;
                    if (sda_in == 1'b0) begin  // 收到ACK
                        next_state <= S_TX_DATA;  // 继续发送
                    end else begin                // 收到NACK
                        next_state <= S_WAIT_STOP;
                    end
                end
            
                S_WAIT_STOP: begin
                    // 等待STOP条件
                    sda_oe <= 1'b0;
                end
            endcase
        end
    end

endmodule

4. I2C握手协议的精妙之处

4.1 线与逻辑实现的隐式仲裁

verilog

复制代码
// 所有设备都这样连接:
assign SCL = (scl_out1 & scl_out2 & ... & scl_outN);
assign SDA = (sda_out1 & sda_out2 & ... & sda_outN);

规则 :只有所有设备都输出1,线路才为1;任一设备输出0,线路即为0。

4.2 时钟拉伸:硬件级流控制

当时钟由主机驱动时:

  1. 主机拉低SCL开始一个时钟周期
  2. 从机如果需要更多时间处理,保持SCL为低
  3. 主机检测到SCL仍为低,进入等待
  4. 从机完成处理后释放SCL
  5. 主机检测到SCL变高,继续传输

这相当于从机说:"等一下,我还没准备好"

4.3 多主机仲裁

当两个主机同时启动传输时:

  1. 每个主机监控SDA,同时输出自己的数据
  2. 如果某个主机输出1但检测到SDA=0,它知道自己失去了仲裁
  3. 失去仲裁的主机立即转为从机模式

仲裁发生在每一位传输期间

text

复制代码
主机A发送: 1 0 1 0 1 0 0 1 ...
主机B发送: 1 0 1 0 1 0 1 0 ...
SDA实际:  1 0 1 0 1 0 0 ← 在第7位,B输出1但检测到0,B退出

5. I2C vs 其他握手协议对比

特性 I2C SPI UART AHB/AXI
握手机制 SCL时钟+ACK位 硬件片选(CS) 无硬件握手 Ready/Valid信号
线数 2 (SCL+SDA) 3-4 (CS+CLK+MISO+MOSI) 2 (TX+RX) 数十到数百
流控制 时钟拉伸+ACK 无(全双工) 软件(XON/XOFF)或硬件(CTS/RTS) 硬件Ready信号
多主机 支持(仲裁) 不支持 不支持 支持(仲裁器)
吞吐量 低-中 (100k-3.4Mbps) 高 (可达100Mbps+) 低-中 (通常<3Mbps) 极高 (GBps级)

6. I2C握手的实际应用模式

模式1:寄存器配置(最常用)

text

复制代码
// 写入配置寄存器
START → 设备地址(W) → ACK → 寄存器地址 → ACK → 数据 → ACK → STOP

// 读取状态寄存器  
START → 设备地址(W) → ACK → 寄存器地址 → ACK →
REPEATED START → 设备地址(R) → ACK → 读取数据 → NACK → STOP

这里使用了两次握手 :第一次设置地址,第二次读取数据。

模式2:传感器数据读取

c

复制代码
// 从温度传感器读取
S | 0x90 | A | 0x00 | A | Sr | 0x91 | A | D1 | A | D2 | N | P
   ↑      ↑  ↑      ↑  ↑    ↑      ↑  ↑     ↑  ↑     ↑  ↑  ↑
   地址写 应答 寄存器 应答 重开始 地址读 应答 数据1 应答 数据2 不应答 停止

模式3:多字节传输的流控制

text

复制代码
主机: 发送字节1 → 等待ACK → 发送字节2 → 等待ACK → ...
从机: 接收字节 → 处理 → 发送ACK → 接收下一字节
      如果缓冲区满: 接收字节 → 发送NACK → 主机停止

7. I2C握手协议的局限性

7.1 吞吐量瓶颈

  • 每个字节都需要ACK :8位数据+1位ACK = 11.1%开销
  • 时钟拉伸不可预测 :从机可以无限期拉伸时钟
  • 总线电容限制 :长总线降低速度

7.2 可靠性问题

  • 无重传机制 :一旦传输开始,无法重传错误数据
  • 从机挂死风险 :从机故障可能拉死SCL/SDA
  • 竞争条件 :多主机时可能产生不可预测的行为

总结:I2C作为握手协议的哲学

I2C的握手体现了一种 优雅的简约

  1. 两根线解决所有问题 :时钟同步+数据+应答
  2. 硬件握手自动完成 :无需软件干预基础通信
  3. 自我仲裁的多主架构 :冲突自动解决
  4. 速率自适应 :从机通过时钟拉伸控制节奏

I2C握手的核心智慧 :在硬件层面建立一套 自我协调的通信礼仪 ,让不同速度、不同功能的设备可以在同一总线上和谐共存。这种"线与逻辑+时钟拉伸+ACK位"的三重握手机制,使其成为嵌入式系统中最持久、最广泛使用的通信协议之一。

相关推荐
FPGA小c鸡3 小时前
FPGA通信基带算法完全指南:从理论到实战的DSP加速方案
算法·fpga开发
博览鸿蒙6 小时前
2026 年 FPGA 行业现状:回归工程价值,进入稳定增长阶段
fpga开发
XINVRY-FPGA8 小时前
XCZU47DR-2FFVE1156I XilinxFPGA Zynq UltraScale+ RFSoC
嵌入式硬件·fpga开发·云计算·硬件工程·射频工程·fpga
hfut02889 小时前
systemverilog interface总结
fpga开发
tiantianuser9 小时前
RDMA设计33:RoCE v2 接收模块
fpga开发·rdma·高速传输·cmac·roce v2
博览鸿蒙1 天前
FPGA 开发软件学习笔记分享(内含安装与环境配置)
笔记·学习·fpga开发
心勤则明1 天前
基于 Debezium Server 与 Redis 的高可用 CDC 架构实践
debezium·cdc·ha
希言自然也1 天前
赛灵思KU系列FPGA的ICAPE3原语和MultiBoot功能
fpga开发
Flamingˢ1 天前
FPGA实战:基于Verilog的数码管动态扫描驱动设计与仿真验证
fpga开发