握手协议在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 (高电平) :三种情况:
- 从机未响应(地址不匹配)
- 从机无法接收更多数据(缓冲区满)
- 主机作为接收方时,发送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 时钟拉伸:硬件级流控制
当时钟由主机驱动时:
- 主机拉低SCL开始一个时钟周期
- 从机如果需要更多时间处理,保持SCL为低
- 主机检测到SCL仍为低,进入等待
- 从机完成处理后释放SCL
- 主机检测到SCL变高,继续传输
这相当于从机说:"等一下,我还没准备好"
4.3 多主机仲裁
当两个主机同时启动传输时:
- 每个主机监控SDA,同时输出自己的数据
- 如果某个主机输出1但检测到SDA=0,它知道自己失去了仲裁
- 失去仲裁的主机立即转为从机模式
仲裁发生在每一位传输期间 :
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的握手体现了一种 优雅的简约 :
- 两根线解决所有问题 :时钟同步+数据+应答
- 硬件握手自动完成 :无需软件干预基础通信
- 自我仲裁的多主架构 :冲突自动解决
- 速率自适应 :从机通过时钟拉伸控制节奏
I2C握手的核心智慧 :在硬件层面建立一套 自我协调的通信礼仪 ,让不同速度、不同功能的设备可以在同一总线上和谐共存。这种"线与逻辑+时钟拉伸+ACK位"的三重握手机制,使其成为嵌入式系统中最持久、最广泛使用的通信协议之一。