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 endmodule
2. `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