1 引言
在复杂的嵌入式系统中,经常需要多个主设备共享同一I2C总线。I2C协议的多主设备仲裁机制正是为此而生,它确保了在总线冲突时能够公平、可靠地解决竞争问题。本文将深入探讨I2C仲裁的工作原理,并通过实际的Verilog代码示例展示实现方法。
2 仲裁基础原理
I2C总线的仲裁能力源于其开漏输出的物理特性。所有设备都只能将总线拉低,而不能主动拉高,这自然形成了"线与"(wired-AND)逻辑:
// I2C总线物理模型
wire sda_actual;
wire scl_actual;
// 多个设备的输出连接在一起
assign sda_actual = (device1_sda_oe ? 1'b0 : 1'bz) &
(device2_sda_oe ? 1'b0 : 1'bz) &
(device3_sda_oe ? 1'b0 : 1'bz);
assign scl_actual = (device1_scl_oe ? 1'b0 : 1'bz) &
(device2_scl_oe ? 1'b0 : 1'bz) &
(device3_scl_oe ? 1'b0 : 1'bz);
关键特性:
-
任何设备拉低总线都会使总线变为低电平
-
只有所有设备都释放总线时,总线才被上拉电阻拉高
-
这种特性使得总线状态可以反映所有设备的输出情况
3 仲裁过程详解
3.1 仲裁的基本规则
仲裁过程遵循一个简单而强大的规则:
发送高电平但检测到低电平的设备立即退出仲裁
这意味着设备只有在它发送的电平与总线实际电平一致时才能继续传输。
3.2 仲裁时序示例

3.3 Verilog实现示例
3.3.1 基础仲裁检测模块
module i2c_arbitration_detector (
input wire clk, // 系统时钟
input wire reset, // 系统复位
input wire my_sda_out, // 本设备要发送的SDA值
input wire my_sda_oe, // 本设备SDA输出使能
input wire actual_sda, // 总线实际SDA状态
input wire actual_scl, // 总线实际SCL状态
output reg arbitration_lost, // 仲裁丢失标志
output reg sda_oe_out, // 调整后的SDA输出使能
output reg scl_oe_out // 调整后的SCL输出使能
);
// 内部信号
reg actual_sda_sync;
reg actual_scl_sync;
reg [1:0] sda_sample;
// 同步输入信号,避免亚稳态
always @(posedge clk or posedge reset) begin
if (reset) begin
actual_sda_sync <= 1'b1;
actual_scl_sync <= 1'b1;
sda_sample <= 2'b11;
end else begin
actual_sda_sync <= actual_sda;
actual_scl_sync <= actual_scl;
sda_sample <= {sda_sample[0], actual_sda_sync};
end
end
// 仲裁检测逻辑
always @(posedge clk or posedge reset) begin
if (reset) begin
arbitration_lost <= 1'b0;
sda_oe_out <= 1'b0;
scl_oe_out <= 1'b0;
end else begin
if (arbitration_lost) begin
// 仲裁失败后保持释放总线
sda_oe_out <= 1'b0;
scl_oe_out <= 1'b0;
// 检测到停止条件后清除仲裁失败标志
if (actual_scl_sync && sda_sample[1] && !actual_sda_sync) begin
arbitration_lost <= 1'b0;
end
end else begin
// 正常操作
sda_oe_out <= my_sda_oe;
scl_oe_out <= 1'b1; // 假设我们总是驱动SCL
// 在SCL高电平期间检测仲裁
if (actual_scl_sync && my_sda_oe) begin
if (my_sda_out == 1'b1 && actual_sda_sync == 1'b0) begin
// 发送高电平但总线为低电平 → 仲裁失败
arbitration_lost <= 1'b1;
sda_oe_out <= 1'b0;
scl_oe_out <= 1'b0;
end
end
end
end
end
endmodule
3.3.2 完整的I2C主设备控制器(含仲裁)
module i2c_master_with_arbitration (
input wire clk,
input wire reset,
input wire start,
input wire [6:0] target_addr,
input wire rw_bit,
input wire [7:0] tx_data,
output reg [7:0] rx_data,
output reg busy,
output reg done,
output reg error,
inout wire sda,
inout wire scl
);
// 状态定义
localparam [3:0]
IDLE = 4'b0000,
START = 4'b0001,
ADDR = 4'b0010,
DATA_TX = 4'b0011,
DATA_RX = 4'b0100,
ACK_TX = 4'b0101,
ACK_RX = 4'b0110,
STOP = 4'b0111,
ARB_LOST = 4'b1000;
// 内部信号
reg [3:0] state;
reg [7:0] shift_reg;
reg [2:0] bit_count;
reg sda_out;
reg sda_oe;
reg scl_out;
reg scl_oe;
reg arbitration_lost;
reg ack_received;
// 三态缓冲
assign sda = sda_oe ? sda_out : 1'bz;
assign scl = scl_oe ? scl_out : 1'bz;
// 时钟分频
reg [15:0] clk_divider;
wire scl_edge = (clk_divider == 16'd0);
always @(posedge clk or posedge reset) begin
if (reset) begin
clk_divider <= 16'd0;
end else begin
if (clk_divider >= I2C_DIVIDER) begin
clk_divider <= 16'd0;
end else begin
clk_divider <= clk_divider + 1;
end
end
end
// 仲裁检测实例
wire actual_sda, actual_scl;
assign actual_sda = sda;
assign actual_scl = scl;
i2c_arbitration_detector arb_detector (
.clk(clk),
.reset(reset),
.my_sda_out(sda_out),
.my_sda_oe(sda_oe),
.actual_sda(actual_sda),
.actual_scl(actual_scl),
.arbitration_lost(arbitration_lost),
.sda_oe_out(sda_oe_arb),
.scl_oe_out(scl_oe_arb)
);
// 主状态机
always @(posedge clk or posedge reset) begin
if (reset) begin
state <= IDLE;
sda_out <= 1'b1;
sda_oe <= 1'b0;
scl_out <= 1'b1;
scl_oe <= 1'b0;
shift_reg <= 8'h00;
bit_count <= 3'b000;
busy <= 1'b0;
done <= 1'b0;
error <= 1'b0;
ack_received <= 1'b0;
end else begin
case(state)
IDLE: begin
sda_oe <= 1'b0;
scl_oe <= 1'b0;
busy <= 1'b0;
done <= 1'b0;
error <= 1'b0;
if (start && !arbitration_lost) begin
state <= START;
busy <= 1'b1;
sda_oe <= 1'b1;
sda_out <= 1'b1; // 准备产生START条件
end
end
START: begin
if (scl_edge) begin
if (!scl_out) begin
// SCL为低时拉低SDA产生START条件
sda_out <= 1'b0;
state <= ADDR;
shift_reg <= {target_addr, rw_bit};
bit_count <= 3'b000;
end
scl_out <= ~scl_out;
end
// 检测仲裁丢失
if (arbitration_lost) begin
state <= ARB_LOST;
end
end
ADDR: begin
if (scl_edge) begin
if (scl_out) begin
// SCL下降沿
scl_out <= 1'b0;
if (bit_count == 3'b111) begin
// 发送完地址,准备接收ACK
sda_oe <= 1'b0; // 释放SDA准备接收ACK
state <= ACK_RX;
end else begin
// 发送下一位
sda_out <= shift_reg[7];
shift_reg <= {shift_reg[6:0], 1'b0};
bit_count <= bit_count + 1;
end
end else begin
// SCL上升沿
scl_out <= 1'b1;
end
end
// 在地址传输期间检测仲裁
if (arbitration_lost) begin
state <= ARB_LOST;
end
end
ACK_RX: begin
if (scl_edge && !scl_out) begin
// 在SCL上升沿采样ACK
ack_received <= ~actual_sda;
scl_out <= 1'b1;
end else if (scl_edge && scl_out) begin
scl_out <= 1'b0;
if (ack_received) begin
if (rw_bit) begin
state <= DATA_RX;
end else begin
state <= DATA_TX;
shift_reg <= tx_data;
bit_count <= 3'b000;
end
end else begin
// 没有收到ACK,错误处理
state <= STOP;
error <= 1'b1;
end
end
end
DATA_TX: begin
if (scl_edge) begin
if (scl_out) begin
// SCL下降沿
scl_out <= 1'b0;
if (bit_count == 3'b111) begin
// 发送完数据字节,准备接收ACK
sda_oe <= 1'b0;
state <= ACK_RX;
end else begin
// 发送下一位
sda_oe <= 1'b1;
sda_out <= shift_reg[7];
shift_reg <= {shift_reg[6:0], 1'b0};
bit_count <= bit_count + 1;
end
end else begin
// SCL上升沿
scl_out <= 1'b1;
end
end
if (arbitration_lost) begin
state <= ARB_LOST;
end
end
DATA_RX: begin
if (scl_edge) begin
if (scl_out) begin
// SCL下降沿
scl_out <= 1'b0;
if (bit_count == 3'b111) begin
// 接收完数据字节,准备发送ACK/NACK
state <= ACK_TX;
end else begin
bit_count <= bit_count + 1;
end
end else begin
// SCL上升沿采样数据位
scl_out <= 1'b1;
shift_reg <= {shift_reg[6:0], actual_sda};
end
end
end
ACK_TX: begin
if (scl_edge && !scl_out) begin
// 发送ACK位
sda_oe <= 1'b1;
sda_out <= 1'b0; // 发送ACK
scl_out <= 1'b1;
end else if (scl_edge && scl_out) begin
scl_out <= 1'b0;
rx_data <= shift_reg;
state <= STOP;
end
end
STOP: begin
if (scl_edge) begin
if (!scl_out) begin
// 准备产生STOP条件
sda_oe <= 1'b1;
sda_out <= 1'b0;
scl_out <= 1'b1;
end else begin
// SCL为高时释放SDA产生STOP条件
sda_out <= 1'b1;
state <= IDLE;
done <= 1'b1;
end
end
end
ARB_LOST: begin
// 仲裁失败处理
sda_oe <= 1'b0;
scl_oe <= 1'b0;
error <= 1'b1;
busy <= 1'b0;
// 等待总线空闲后返回IDLE
if (actual_scl && actual_sda) begin
state <= IDLE;
end
end
endcase
end
end
endmodule
3.3.3 时钟同步实现
在多主设备环境中,时钟同步至关重要:
module i2c_clock_synchronization (
input wire clk,
input wire reset,
input wire my_scl_out, // 本设备生成的SCL
input wire my_scl_oe, // 本设备SCL输出使能
input wire actual_scl, // 总线实际SCL
output reg scl_out_sync, // 同步后的SCL输出
output reg scl_oe_sync // 同步后的SCL输出使能
);
reg actual_scl_sync;
reg scl_pulled_low;
reg [15:0] wait_counter;
// 同步输入
always @(posedge clk or posedge reset) begin
if (reset) begin
actual_scl_sync <= 1'b1;
end else begin
actual_scl_sync <= actual_scl;
end
end
// 检测其他设备拉低SCL
always @(posedge clk or posedge reset) begin
if (reset) begin
scl_pulled_low <= 1'b0;
wait_counter <= 16'd0;
scl_out_sync <= 1'b1;
scl_oe_sync <= 1'b0;
end else begin
if (my_scl_oe && my_scl_out && !actual_scl_sync) begin
// 其他设备拉低了SCL
scl_pulled_low <= 1'b1;
scl_out_sync <= 1'b0;
wait_counter <= 16'd0;
end else if (scl_pulled_low) begin
if (!actual_scl_sync) begin
// 等待其他设备释放SCL
wait_counter <= 16'd0;
scl_out_sync <= 1'b0;
end else if (wait_counter >= CLK_HOLD_TIME) begin
// 其他设备已释放SCL,我们可以继续
scl_pulled_low <= 1'b0;
scl_out_sync <= 1'b1;
end else begin
wait_counter <= wait_counter + 1;
end
end else begin
// 正常时钟生成
scl_out_sync <= my_scl_out;
scl_oe_sync <= my_scl_oe;
end
end
end
endmodule
4 实际应用建议
4.1 地址分配策略:
// 为常用设备分配差异较大的地址
localparam MASTER1_ADDR = 7'b1010000; // 0x50
localparam MASTER2_ADDR = 7'b0001000; // 0x08
localparam MASTER3_ADDR = 7'b0111100; // 0x3C
4.2 退避算法:
在I2C多主设备环境中,当多个设备同时竞争总线时,仲裁失败后的重试策略至关重要。退避算法(Backoff Algorithm)就是用来确定设备在仲裁失败后应该等待多长时间再重新尝试的机制。
4.2.1 为什么需要退避算法?
问题场景
时间线: t0: 设备A、B、C同时检测到总线空闲 t1: 同时发起传输 → 仲裁冲突 t2: 设备B、C仲裁失败 t3: 设备B、C立即重试 → 再次冲突 t4: 设备B、C再次立即重试 → 持续冲突
结果:总线被"饿死",所有设备都无法完成传输。
4.2.2 常见退避算法类型
1. 固定退避(Fixed Backoff)
// 最简单的退避策略
localparam FIXED_DELAY = 100; // 固定等待100个时钟周期
always @(posedge clk) begin
if (arbitration_lost) begin
backoff_counter <= FIXED_DELAY;
end else if (backoff_counter > 0) begin
backoff_counter <= backoff_counter - 1;
end
end
缺点:如果多个设备使用相同的固定延迟,会持续冲突。
2. 随机退避(Random Backoff)
// 使用随机数生成器
reg [7:0] random_delay;
always @(posedge clk) begin
if (arbitration_lost) begin
random_delay <= $random & 8'hFF; // 0-255随机延迟
backoff_counter <= random_delay;
end else if (backoff_counter > 0) begin
backoff_counter <= backoff_counter - 1;
end
end
优点:减少重复冲突的概率。
3. 指数退避(Binary Exponential Backoff)
// 指数退避算法
reg [7:0] backoff_counter;
reg [2:0] retry_count;
always @(posedge clk or posedge reset) begin
if (reset) begin
backoff_counter <= 8'd0;
retry_count <= 3'd0;
end else if (arbitration_lost) begin
retry_count <= retry_count + 1;
backoff_counter <= (8'd1 << retry_count) + $random & 8'h0F;
end else if (backoff_counter > 0) begin
backoff_counter <= backoff_counter - 1;
end
end
5 总结
I2C的多主设备仲裁机制通过硬件的"线与"特性实现了公平、高效的冲突解决。本文通过详细的Verilog代码示例展示了:
-
仲裁检测:如何实时监控总线状态并检测仲裁失败
-
状态机设计:包含仲裁处理的完整I2C主设备状态机
-
时钟同步:多主环境下的时钟协调机制
-
错误恢复:仲裁失败后的优雅恢复策略
这种机制确保了即使在多个主设备竞争总线的情况下,I2C总线仍能保持可靠的数据传输,为复杂的嵌入式系统提供了强大的通信基础。
4万+

被折叠的 条评论
为什么被折叠?



