握手协议在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位"的三重握手机制,使其成为嵌入式系统中最持久、最广泛使用的通信协议之一。

相关推荐
仲南音17 小时前
Xilinx FPGA——时钟IP核MMCM、PLL
fpga开发
I'm a winner1 天前
基于Xilinx FPGA的LVDS高速串行通信系统(三)--数据训练及握手机制【文末源码】
fpga开发
upper20201 天前
coremark移植坑之-nostartfiles -nostdlib参数
学习·fpga开发·coremark移植·riscv设计
Eloudy2 天前
Vivado 纯命令行构建 FPGA 项目教程
fpga开发
Rambo.xia2 天前
AXI跨时钟域与数据一致性——CDC FIFO深度不够、写后读冒险、多Master仲裁、乱序返回,出了Bug根本查不出来
fpga开发·bug
upper20202 天前
FPGA部署卷积神经网络识别MINST图片
fpga开发·卷积神经网络·minst·minst识别·部署神经网络·神经网络识别图片
你是我的解忧王子2 天前
嵌入式 ADC采样 原始数据分析上层(平均值 波动率 正态分布 线性补偿 系统误差)
fpga开发·数据挖掘·数据分析
9527华安3 天前
FPGA纯verilog代码ISP图像处理培训教程,基于IMX214,提供工程源码+视频教程+FPGA开发板
图像处理·fpga开发·isp·imx214·mipi
千寻xun3 天前
二、实战篇-NVME SSD控制之ZYNQ实现(六)-读写NVME SSD硬盘数据
fpga开发·nvme·nvme ssd
千寻xun3 天前
二、实战篇-NVME SSD控制之ZYNQ实现(三)
fpga开发·nvme ssd·nvme协议