I2C接口(2):IIC多主设备仲裁机制详解--从原理到Verilog实现
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) beginif (reset) beginactual_sda_sync <= 1'b1;actual_scl_sync <= 1'b1;sda_sample <= 2'b11;end else beginactual_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) beginif (reset) beginarbitration_lost <= 1'b0;sda_oe_out <= 1'b0;scl_oe_out <= 1'b0;end else beginif (arbitration_lost) begin// 仲裁失败后保持释放总线sda_oe_out <= 1'b0;scl_oe_out <= 1'b0;// 检测到停止条件后清除仲裁失败标志if (actual_scl_sync && sda_sample[1] && !actual_sda_sync) beginarbitration_lost <= 1'b0;endend else begin// 正常操作sda_oe_out <= my_sda_oe;scl_oe_out <= 1'b1; // 假设我们总是驱动SCL// 在SCL高电平期间检测仲裁if (actual_scl_sync && my_sda_oe) beginif (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;endendendend
endendmodule3.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) beginif (reset) beginclk_divider <= 16'd0;end else beginif (clk_divider >= I2C_DIVIDER) beginclk_divider <= 16'd0;end else beginclk_divider <= clk_divider + 1;endend
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) beginif (reset) beginstate <= 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 begincase(state)IDLE: beginsda_oe <= 1'b0;scl_oe <= 1'b0;busy <= 1'b0;done <= 1'b0;error <= 1'b0;if (start && !arbitration_lost) beginstate <= START;busy <= 1'b1;sda_oe <= 1'b1;sda_out <= 1'b1; // 准备产生START条件endendSTART: beginif (scl_edge) beginif (!scl_out) begin// SCL为低时拉低SDA产生START条件sda_out <= 1'b0;state <= ADDR;shift_reg <= {target_addr, rw_bit};bit_count <= 3'b000;endscl_out <= ~scl_out;end// 检测仲裁丢失if (arbitration_lost) beginstate <= ARB_LOST;endendADDR: beginif (scl_edge) beginif (scl_out) begin// SCL下降沿scl_out <= 1'b0;if (bit_count == 3'b111) begin// 发送完地址,准备接收ACKsda_oe <= 1'b0; // 释放SDA准备接收ACKstate <= ACK_RX;end else begin// 发送下一位sda_out <= shift_reg[7];shift_reg <= {shift_reg[6:0], 1'b0};bit_count <= bit_count + 1;endend else begin// SCL上升沿scl_out <= 1'b1;endend// 在地址传输期间检测仲裁if (arbitration_lost) beginstate <= ARB_LOST;endendACK_RX: beginif (scl_edge && !scl_out) begin// 在SCL上升沿采样ACKack_received <= ~actual_sda;scl_out <= 1'b1;end else if (scl_edge && scl_out) beginscl_out <= 1'b0;if (ack_received) beginif (rw_bit) beginstate <= DATA_RX;end else beginstate <= DATA_TX;shift_reg <= tx_data;bit_count <= 3'b000;endend else begin// 没有收到ACK,错误处理state <= STOP;error <= 1'b1;endendendDATA_TX: beginif (scl_edge) beginif (scl_out) begin// SCL下降沿scl_out <= 1'b0;if (bit_count == 3'b111) begin// 发送完数据字节,准备接收ACKsda_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;endend else begin// SCL上升沿scl_out <= 1'b1;endendif (arbitration_lost) beginstate <= ARB_LOST;endendDATA_RX: beginif (scl_edge) beginif (scl_out) begin// SCL下降沿scl_out <= 1'b0;if (bit_count == 3'b111) begin// 接收完数据字节,准备发送ACK/NACKstate <= ACK_TX;end else beginbit_count <= bit_count + 1;endend else begin// SCL上升沿采样数据位scl_out <= 1'b1;shift_reg <= {shift_reg[6:0], actual_sda};endendendACK_TX: beginif (scl_edge && !scl_out) begin// 发送ACK位sda_oe <= 1'b1;sda_out <= 1'b0; // 发送ACKscl_out <= 1'b1;end else if (scl_edge && scl_out) beginscl_out <= 1'b0;rx_data <= shift_reg;state <= STOP;endendSTOP: beginif (scl_edge) beginif (!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;endendendARB_LOST: begin// 仲裁失败处理sda_oe <= 1'b0;scl_oe <= 1'b0;error <= 1'b1;busy <= 1'b0;// 等待总线空闲后返回IDLEif (actual_scl && actual_sda) beginstate <= IDLE;endendendcaseend
endendmodule3.3.3 时钟同步实现
在多主设备环境中,时钟同步至关重要:
module i2c_clock_synchronization (input wire clk,input wire reset,input wire my_scl_out, // 本设备生成的SCLinput wire my_scl_oe, // 本设备SCL输出使能input wire actual_scl, // 总线实际SCLoutput 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) beginif (reset) beginactual_scl_sync <= 1'b1;end else beginactual_scl_sync <= actual_scl;end
end// 检测其他设备拉低SCL
always @(posedge clk or posedge reset) beginif (reset) beginscl_pulled_low <= 1'b0;wait_counter <= 16'd0;scl_out_sync <= 1'b1;scl_oe_sync <= 1'b0;end else beginif (my_scl_oe && my_scl_out && !actual_scl_sync) begin// 其他设备拉低了SCLscl_pulled_low <= 1'b1;scl_out_sync <= 1'b0;wait_counter <= 16'd0;end else if (scl_pulled_low) beginif (!actual_scl_sync) begin// 等待其他设备释放SCLwait_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 beginwait_counter <= wait_counter + 1;endend else begin// 正常时钟生成scl_out_sync <= my_scl_out;scl_oe_sync <= my_scl_oe;endend
endendmodule4 实际应用建议
4.1 地址分配策略:
// 为常用设备分配差异较大的地址
localparam MASTER1_ADDR = 7'b1010000; // 0x50
localparam MASTER2_ADDR = 7'b0001000; // 0x08
localparam MASTER3_ADDR = 7'b0111100; // 0x3C4.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) beginif (arbitration_lost) beginbackoff_counter <= FIXED_DELAY;end else if (backoff_counter > 0) beginbackoff_counter <= backoff_counter - 1;end
end缺点:如果多个设备使用相同的固定延迟,会持续冲突。
2. 随机退避(Random Backoff)
// 使用随机数生成器
reg [7:0] random_delay;always @(posedge clk) beginif (arbitration_lost) beginrandom_delay <= $random & 8'hFF; // 0-255随机延迟backoff_counter <= random_delay;end else if (backoff_counter > 0) beginbackoff_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) beginif (reset) beginbackoff_counter <= 8'd0;retry_count <= 3'd0;end else if (arbitration_lost) beginretry_count <= retry_count + 1;backoff_counter <= (8'd1 << retry_count) + $random & 8'h0F;end else if (backoff_counter > 0) beginbackoff_counter <= backoff_counter - 1;end
end5 总结
I2C的多主设备仲裁机制通过硬件的"线与"特性实现了公平、高效的冲突解决。本文通过详细的Verilog代码示例展示了:
仲裁检测:如何实时监控总线状态并检测仲裁失败
状态机设计:包含仲裁处理的完整I2C主设备状态机
时钟同步:多主环境下的时钟协调机制
错误恢复:仲裁失败后的优雅恢复策略
这种机制确保了即使在多个主设备竞争总线的情况下,I2C总线仍能保持可靠的数据传输,为复杂的嵌入式系统提供了强大的通信基础。
