I2C (Inter-Integrated Circuit)
概述 I²C总线由飞利浦公司(现NXP)开发,是一种同步、半双工、多主多从的串行总线。仅用两根线即可连接多个设备,非常适合板内设备间的近距离、低速通信。
物理层与信号线
2根线:
`SCL` (Serial Clock): 串行时钟线。
`SDA` (Serial Data): 串行数据线。
开漏输出 (Open-Drain): I2C设备连接到总线的引脚都是开漏(或开集)结构。这意味着设备只能将线拉低,不能主动拉高。因此,SCL和SDA线路上都必须接一个上拉电阻,以在总线空闲时将其拉到高电平。这个特性是I2C实现多主仲裁和时钟拉伸的基础。
工作原理
I2C通过一套严格的地址和应答机制工作。每个从设备都有一个唯一的7位(或10位)地址。主设备发起通信时,首先会广播它想通信的从设备的地址。
通信协议/传输步骤
1. 起始条件 (START Condition): SCL保持高电平期间,SDA出现一个下降沿。这标志着通信的开始,所有从设备开始监听总线。
2. 地址帧 (Address Frame): 主设备发送7位从机地址, followed by a 读/写(R/W)位 (0: Write, 1: Read)。
3. 应答位 (Acknowledge, ACK): 在第9个时钟周期,被寻址到的从设备如果存在且正常,会将SDA线拉低作为应答(ACK)。如果主设备没有收到ACK(SDA保持高电平,即NACK),则表示通信失败。
4. 数据帧 (Data Frame): 主从双方开始传输数据,每帧8位。每传输一个字节后,接收方都必须在第9个时钟周期回复一个ACK/NACK。
5. 停止条件 (STOP Condition): SCL保持高电平期间,SDA出现一个上升沿。这标志着本次通信的结束。
关键特性:
时钟拉伸 (Clock Stretching): 如果从设备需要更多时间来准备数据,它可以将SCL线强制拉低,暂停通信,直到它准备好为止。
多主仲裁: 如果两个主设备同时试图启动通信,它们会通过SDA线的线与逻辑进行仲裁。谁先发送了一个高电平(释放总线)而检测到总线仍然是低电平(被对方拉低),谁就输掉了仲裁并退出。
第三部分:Verilog 代码实现
1. `i2c_master.v`
`timescale 1ns / 1ps
// I2C Master Controller
module i2c_master #(
parameter SYS_CLK_FREQ = 50_000_000,
parameter I2C_CLK_FREQ = 100_000 // 100kHz standard mode
)(
input wire i_clk,
input wire i_rst_n,
// User Interface
input wire i_start, // Start a transaction
input wire i_rw, // 0 for write, 1 for read
input wire [6:0] i_slave_addr, // 7-bit slave address
input wire [7:0] i_reg_addr, // Internal register address of slave
input wire [7:0] i_data_wr, // Data to write
output wire o_busy,
output wire o_ack_error,
output wire [7:0] o_data_rd,
output wire o_data_rd_dv,
// I2C Bus Interface
inout wire sda,
inout wire scl
);
// FSM States
localparam S_IDLE = 4'h0;
localparam S_START = 4'h1;
localparam S_SEND_SADDR = 4'h2;
localparam S_WAIT_ACK1 = 4'h3;
localparam S_SEND_RADDR = 4'h4;
localparam S_WAIT_ACK2 = 4'h5;
localparam S_SEND_DATA = 4'h6;
localparam S_WAIT_ACK3 = 4'h7;
localparam S_REP_START = 4'h8;
localparam S_SEND_SADDR_R= 4'h9;
localparam S_WAIT_ACK4 = 4'hA;
localparam S_READ_DATA = 4'hB;
localparam S_SEND_NACK = 4'hC;
localparam S_STOP = 4'hD;
reg [3:0] r_state = S_IDLE;
// Clock Divider for SCL
localparam DIVIDER = SYS_CLK_FREQ / (I2C_CLK_FREQ * 4); // Quarter period
reg [$clog2(DIVIDER)-1:0] r_clk_cnt = 0;
reg [1:0] r_scl_phase = 0;
reg r_scl_out = 1;
reg r_scl_en = 0;
// Data path registers
reg [7:0] r_shift_reg;
reg [2:0] r_bit_cnt;
reg r_sda_out = 1;
reg r_sda_en = 0;
// Status registers
reg r_busy = 0;
reg r_ack_error = 0;
reg [7:0] r_data_rd = 0;
reg r_data_rd_dv = 0;
// I/O assignments for open-drain simulation
assign scl = r_scl_en ? r_scl_out : 1'bz;
assign sda = r_sda_en ? r_sda_out : 1'bz;
assign o_busy = r_busy;
assign o_ack_error = r_ack_error;
assign o_data_rd = r_data_rd;
assign o_data_rd_dv = r_data_rd_dv;
// SCL Generation Logic
always @(posedge i_clk) begin
if (r_scl_en) begin
if (r_clk_cnt == DIVIDER - 1) begin
r_clk_cnt <= 0;
r_scl_phase <= r_scl_phase + 1;
if (r_scl_phase == 1) r_scl_out <= 0; // SCL low
if (r_scl_phase == 3) r_scl_out <= 1; // SCL high
end else begin
r_clk_cnt <= r_clk_cnt + 1;
end
end else begin
r_scl_out <= 1;
r_clk_cnt <= 0;
r_scl_phase <= 0;
end
end
// Main FSM
always @(posedge i_clk or negedge i_rst_n) begin
if (!i_rst_n) begin
r_state <= S_IDLE;
r_busy <= 0;
//... reset all regs
end else begin
r_data_rd_dv <= 0;
case (r_state)
S_IDLE: begin
r_busy <= 0;
r_ack_error <= 0;
r_scl_en <= 0;
r_sda_en <= 0;
if (i_start) begin
r_busy <= 1;
r_shift_reg <= {i_slave_addr, i_rw};
r_state <= S_START;
end
end
// --- Common Sequence for Write/Read ---
S_START: begin // Generate START condition
if (r_scl_out) begin
r_sda_out <= 0;
r_sda_en <= 1;
r_scl_en <= 1;
r_state <= S_SEND_SADDR;
end
end
S_SEND_SADDR: begin // Send Slave Address + R/W
if (r_scl_phase == 0) begin // SCL is high, data stable
r_bit_cnt <= 7;
r_state <= S_SEND_SADDR + 100; // Temp state
end
end
S_SEND_SADDR + 100: begin // SCL goes low
if (r_scl_phase == 2) begin
r_sda_out <= r_shift_reg[r_bit_cnt];
r_state <= S_SEND_SADDR + 200;
end
end
S_SEND_SADDR + 200: begin // SCL goes high
if (r_scl_phase == 0) begin
if (r_bit_cnt == 0) r_state <= S_WAIT_ACK1;
else begin
r_bit_cnt <= r_bit_cnt - 1;
r_state <= S_SEND_SADDR + 100;
end
end
end
S_WAIT_ACK1: begin // Release SDA and check for ACK
if (r_scl_phase == 2) begin
r_sda_en <= 0; // Master releases SDA
r_state <= S_WAIT_ACK1 + 100;
end
end
S_WAIT_ACK1 + 100: begin
if (r_scl_phase == 0) begin // Sample ACK on SCL rising
if (sda) begin
r_ack_error <= 1;
r_state <= S_STOP;
end else begin // ACK received
r_shift_reg <= i_reg_addr;
r_state <= S_SEND_RADDR;
end
end
end
S_STOP: begin
if (r_scl_out) begin
r_sda_out <= 1;
r_state <= S_IDLE;
end
end
default: r_state <= S_IDLE;
endcase
end
end
endmodule2. `i2c_slave.v`
`timescale 1ns / 1ps module i2c_slave #( parameter I2C_ADDR = 7'h2A )( inout wire sda, inout wire scl ); // FSM States localparam [2:0] S_IDLE = 3'b000; localparam [2:0] S_ADDR = 3'b001; localparam [2:0] S_ACK_ADDR = 3'b010; localparam [2:0] S_GET_RADDR = 3'b011; localparam [2:0] S_ACK_RADDR = 3'b100; localparam [2:0] S_GET_DATA = 3'b101; localparam [2:0] S_ACK_DATA = 3'b110; localparam [2:0] S_SEND_DATA = 3'b111; reg [2:0] r_state = S_IDLE; // Bus Signal Registers reg r_scl, r_sda; reg r_scl_prev, r_sda_prev; // Shift registers and counters reg [7:0] r_shift_reg; reg [2:0] r_bit_cnt; reg r_rw_bit; // Internal Memory reg [7:0] r_reg_addr; reg [7:0] r_memory [0:3]; // Open-drain control reg r_sda_en = 0; // 1 to drive SDA low assign sda = r_sda_en ? 1'b0 : 1'bz; // Detect START and STOP conditions wire w_start_cond = (r_scl == 1'b1) && (r_sda_prev == 1'b1) && (r_sda == 1'b0); wire w_stop_cond = (r_scl == 1'b1) && (r_sda_prev == 1'b0) && (r_sda == 1'b1); // Synchronize bus signals to internal clock (not shown, assuming ideal for simplicity) // In a real design, you would use a system clock to sample SCL/SDA always @(negedge scl or posedge scl) begin r_sda <= sda; r_scl <= scl; r_sda_prev <= r_sda; r_scl_prev <= r_scl; if (w_stop_cond || w_start_cond) begin r_state <= S_IDLE; end else if (r_state == S_IDLE && w_start_cond) begin r_state <= S_ADDR; r_bit_cnt <= 7; end else @(negedge scl) begin // All state transitions happen on SCL falling edge case (r_state) S_ADDR: begin r_shift_reg[r_bit_cnt] <= r_sda; if (r_bit_cnt == 0) begin r_rw_bit <= r_sda; // last bit is R/W r_state <= S_ACK_ADDR; end else begin r_bit_cnt <= r_bit_cnt - 1; end end S_ACK_ADDR: begin if (r_shift_reg[7:1] == I2C_ADDR) begin r_sda_en <= 1; // ACK if(r_rw_bit) r_state <= S_SEND_DATA; else r_state <= S_GET_RADDR; end else r_state <= S_IDLE; end S_GET_RADDR: begin r_sda_en <= 0; // Release for ACK r_shift_reg[r_bit_cnt] <= r_sda; if (r_bit_cnt == 0) r_state <= S_ACK_RADDR; else r_bit_cnt <= r_bit_cnt - 1; end S_ACK_RADDR: begin r_reg_addr <= r_shift_reg; r_sda_en <= 1; // ACK r_state <= S_GET_DATA; end S_GET_DATA: begin r_sda_en <= 0; r_shift_reg[r_bit_cnt] <= r_sda; if (r_bit_cnt == 0) r_state <= S_ACK_DATA; else r_bit_cnt <= r_bit_cnt - 1; end S_ACK_DATA: begin r_memory[r_reg_addr] <= r_shift_reg; r_sda_en <= 1; // ACK r_state <= S_IDLE; // Simplified: expect STOP end S_SEND_DATA: begin r_sda_en <= 0; // Release ACK //... logic to send data from r_memory[r_reg_addr] end default: r_state <= S_IDLE; endcase end end endmodule
3. `i2c_top.v`
`timescale 1ns / 1ps module i2c_top( input wire i_clk, input wire i_rst_n, // Testbench control signals input wire i_start, input wire i_rw, input wire [7:0] i_reg_addr, input wire [7:0] i_data_wr, output wire o_busy, output wire o_ack_error, output wire [7:0] o_data_rd, output wire o_data_rd_dv ); // I2C Bus wires wire sda; wire scl; // Simulate pull-up resistors on the bus pullup(sda); pullup(scl); // Instantiate Master i2c_master u_i2c_master ( .i_clk(i_clk), .i_rst_n(i_rst_n), .i_start(i_start), .i_rw(i_rw), .i_slave_addr(7'h2A), // Hardcode slave address for this test .i_reg_addr(i_reg_addr), .i_data_wr(i_data_wr), .o_busy(o_busy), .o_ack_error(o_ack_error), .o_data_rd(o_data_rd), .o_data_rd_dv(o_data_rd_dv), .sda(sda), .scl(scl) ); // Instantiate Slave i2c_slave #( .I2C_ADDR(7'h2A) ) u_i2c_slave ( .sda(sda), .scl(scl) ); endmodule
4. `tb_i2c_top.v`
`timescale 1ns / 1ps
module tb_i2c_top;
localparam CLK_PERIOD = 20; // 50MHz
// --- Signals ---
reg r_clk;
reg r_rst_n;
reg r_start;
reg r_rw;
reg [7:0] r_reg_addr;
reg [7:0] r_data_wr;
wire w_busy;
wire w_ack_error;
wire [7:0] w_data_rd;
wire w_data_rd_dv;
// --- DUT Instantiation ---
i2c_top u_dut (
.i_clk(r_clk),
.i_rst_n(r_rst_n),
.i_start(r_start),
.i_rw(r_rw),
.i_reg_addr(r_reg_addr),
.i_data_wr(r_data_wr),
.o_busy(w_busy),
.o_ack_error(w_ack_error),
.o_data_rd(w_data_rd),
.o_data_rd_dv(w_data_rd_dv)
);
// --- Clock and Reset ---
initial begin
r_clk = 0;
forever #(CLK_PERIOD / 2) r_clk = ~r_clk;
end
initial begin
r_rst_n = 0;
r_start = 0;
r_rw = 0;
r_reg_addr = 0;
r_data_wr = 0;
#200;
r_rst_n = 1;
end
// --- Main Test Sequence ---
initial begin
// Wait for reset
@(posedge r_rst_n);
#1000;
$display("-----------------------------------------");
$display("TB INFO: Starting I2C Communication Test...");
$display("-----------------------------------------");
// --- Test Case 1: Write 0xC3 to slave register 0x01 ---
$display("TB INFO: Test Case 1: Writing 0xC3 to slave reg 0x01...");
r_rw <= 0; // Write
r_reg_addr <= 8'h01;
r_data_wr <= 8'hC3;
r_start <= 1;
@(posedge r_clk);
r_start <= 0;
// Wait for transaction to complete
@(posedge w_busy);
@(negedge w_busy);
if (w_ack_error) begin
$display("TB FAIL: Test Case 1 Failed. ACK Error during write.");
$finish;
end else begin
$display("TB INFO: Write transaction complete without ACK errors.");
end
#2000;
// --- Test Case 2: Read back from slave register 0x01 ---
$display("-----------------------------------------");
$display("TB INFO: Test Case 2: Reading from slave reg 0x01...");
r_rw <= 1; // Read
r_reg_addr <= 8'h01; // We need to specify which reg to read from
r_start <= 1;
@(posedge r_clk);
r_start <= 0;
// Wait for read data to be valid
@(posedge w_data_rd_dv);
if (w_data_rd == 8'hC3) begin
$display("TB PASS: Test Case 2 Passed. Read back expected data: 0x%0h", w_data_rd);
end else begin
$display("TB FAIL: Test Case 2 Failed. Expected 0xC3, but read 0x%0h", w_data_rd);
end
$display("-----------------------------------------");
$display("TB INFO: All tests completed.");
$finish;
end
endmodule