双线协议 + 起始/停止条件 + ACK
🏆 成就:I2C使者 ✅ Verilator验证
SCL(时钟)+SDA(数据)。开漏输出+上拉电阻=线与逻辑。7位地址寻址,每字节后ACK握手。
localparam I2C_DIV=250; // 50MHz→100kHz
always @(posedge clk) case(state)
0: if(i2c_start) begin // START: SCL=1,SDA 1→0
sda_out<=0; sda_oe<=1;
shift_reg<={addr,1'b0}; // 地址+写位
state<=1; end
1: begin // 发送8位地址
if(clk_div==I2C_DIV-1) begin
sclk<=~sclk_phase; sclk_phase<=~sclk_phase;
if(!sclk_phase) begin sda_out<=shift_reg[7]; sda_oe<=1; end
else begin shift_reg<=shift_reg<<1;
if(bit_cnt==7) state<=2; end
end else clk_div<=clk_div+1; end
2: begin // ACK时隙
sda_oe<=0; // 释放SDA让从机驱动
if(sda_in==0) ack_ok<=1; // ACK!
state<=3; end // → 发数据
// ... 数据发送 + 最后STOP
endcaseSTART:SCL=1时SDA从1→0(唯一的SCL=1时SDA下降情况)
STOP:SCL=1时SDA从0→1(唯一的SCL=1时SDA上升情况)
数据有效性:SCL=1时SDA必须稳定,数据在SCL上升沿被采样
ACK/NACK:每8位后第9个时钟由接收方拉低SDA表示ACK
开漏输出:设备只能拉低,上拉电阻负责拉高→"线与"逻辑
module i2c_master_tb;
logic clk,rst_n; logic [6:0] addr;
logic [7:0] wr_data; logic i2c_start,i2c_busy,i2c_done,ack_ok;
logic scl_out,sda_out,sda_oe,sda_in;
i2c_master uut(.*);
initial clk=0; always #10 clk=~clk;
// 从机模拟: 地址0x50, 收到后ACK
reg [7:0] slave_reg; reg [3:0] s_bit_cnt;
always @(posedge clk) begin
if(scl_out && !$past(scl_out) && !sda_oe) begin
slave_reg <= {slave_reg[6:0],sda_out};
s_bit_cnt <= s_bit_cnt + 1;
end
end
assign sda_in = (s_bit_cnt==8||s_bit_cnt==17) ? 1'b0 : 1'b1;
task i2c_write(input [6:0] a, input [7:0] d, input string desc);
@(posedge clk); addr=a; wr_data=d; i2c_start=1;
@(posedge clk); i2c_start=0;
wait(i2c_done);
$display("✓ %s: addr=0x%02H, data=0x%02H, ACK=%s",
desc, a, d, ack_ok?"OK":"FAIL");
#5000;
endtask
initial begin
rst_n=0;#500;rst_n=1; s_bit_cnt=0;
$display("========== I2C验证 ==========");
i2c_write(7'h50, 8'hAB, "EEPROM写");
i2c_write(7'h50, 8'hCD, "EEPROM写2");
$display("==========================================");
$finish;
end
initial #20_000_000 $finish;
endmodule1. EEPROM — AT24C02/AT24C64,非易失存储
2. 温度传感器 — TMP102, LM75
3. 加速度计 — MPU6050(6轴IMU)
4. RTC时钟 — DS3231(高精度)
5. OLED显示 — SSD1306 I2C模式
verilator --binary -j 0 --trace i2c_master.v i2c_master_tb.sv ./obj_dir/Vi2c_master_tb # 预期输出: # ========== I2C验证 ========== # ✓ EEPROM写: addr=0x50, data=0xAB, ACK=OK # ✓ EEPROM写2: addr=0x50, data=0xCD, ACK=OK # ========================================== # 提示:在GTKWave中查看SCL和SDA信号 # 可以看到START条件(SCL=1时SDA下降)和STOP条件(SCL=1时SDA上升)
✅ I2C只需要哪2根线?
✅ START和STOP条件分别是什么?
✅ 7位地址+读写位如何组成第一个字节?
✅ ACK/NACK握手如何工作?
✅ 为什么I2C使用开漏输出+上拉电阻?
✅ I2C和SPI最关键的区别?
✅ 多设备如何共用总线?
最后一课学习七段数码管——多路复用扫描、七段编码、字符映射。在数码管上显示"HELLO"!🎉
Q: 从机不ACK? 检查地址是否正确(7位地址不含R/W位),上拉电阻是否连接。
Q: SDA无法拉高? 开漏输出+FPGA只能拉低!释放SDA:sda_oe=0让上拉电阻拉高。
Q: 总线卡死? 从机可能卡在发送状态。发送9个额外SCL脉冲+STOP条件恢复。
| 参数 | 值 | 说明 |
|---|---|---|
| 信号线 | 2根 | SCL+SDA |
| 标准速率 | 100kHz | Standard Mode |
| 快速速率 | 400kHz | Fast Mode |
| 高速速率 | 3.4MHz | High Speed |
| 地址宽度 | 7位或10位 | 7位=128设备 |
| 输出方式 | 开漏 | 只能拉低 |
| 上拉电阻 | 4.7kΩ(标准)/1kΩ(快速) | VCC到SDA/SCL |
| 应答方式 | ACK/NACK | 接收方拉低SDA=ACK |
💡 I2C为什么最省线?:只需要2根线就能连接128个设备!秘诀是开漏输出+地址寻址。每个设备有唯一地址,主机广播地址后只有匹配的设备回应。代价是速度慢——每个字节需要9个时钟(8数据+1ACK)且半双工。
START:SCL=1时SDA从1→0
STOP:SCL=1时SDA从0→1
数据有效性:SCL=1时SDA必须稳定
ACK:每8位后第9个时钟由接收方拉低SDA
// 简单I2C从机模拟:地址0x50
reg [7:0] slave_reg; reg [3:0] s_bit_cnt;
always @(posedge sclk_out) if(!sda_oe) begin
slave_reg<={slave_reg[6:0],sda_out};
s_bit_cnt<=s_bit_cnt+1;
end
// 从机ACK时隙拉低SDA
assign sda_in=(s_bit_cnt==8||s_bit_cnt==17)?1'b0:1'b1;💡 开漏输出:I2C设备只能拉低SDA/SCL,上拉电阻负责拉高。多设备同时驱动不怕短路——"线与"逻辑!