第24课: I2C EEPROM

I2C协议+设备地址+START/STOP条件+EEPROM读写

🏆 读写EEPROM仿真 ✅ Verilator仿真验证通过

📖 核心概念

💡 关键思路:本课的核心是I2C协议——两线制(SCL+SDA),多设备共享总线。

💻 Verilog设计代码

设计模块源码——这是你真正要理解的硬件逻辑:

// 第24课: I2C EEPROM - 读写EEPROM仿真 module i2c_eeprom ( input wire clk, input wire rst_n, input wire cmd_valid, input wire cmd_wr, // 0=read, 1=write input wire [7:0] addr, input wire [7:0] wr_data, output reg [7:0] rd_data, output reg rd_valid, output reg busy, // I2C总线信号 output reg i2c_scl, output reg i2c_sda_out, output reg i2c_sda_oe // SDA方向: 1=驱动, 0=释放 ); // EEPROM存储: 256字节 reg [7:0] eeprom_mem [0:255]; integer i; initial begin for (i = 0; i < 256; i = i + 1) eeprom_mem[i] = 8'hFF; end // I2C时序状态机 reg [3:0] state; reg [4:0] bit_cnt; reg [7:0] addr_reg, data_reg; reg is_write; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin i2c_scl <= 1; i2c_sda_out <= 1; i2c_sda_oe <= 0; rd_data <= 0; rd_valid <= 0; busy <= 0; state <= 0; bit_cnt <= 0; addr_reg <= 0; data_reg <= 0; is_write <= 0; end else begin rd_valid <= 0; case (state) 0: begin // 空闲 i2c_scl <= 1; i2c_sda_out <= 1; i2c_sda_oe <= 0; if (cmd_valid) begin busy <= 1; addr_reg <= addr; data_reg <= wr_data; is_write <= cmd_wr; state <= 1; bit_cnt <= 0; end end 1: begin // START条件: SCL高时SDA下降 i2c_sda_out <= 0; i2c_sda_oe <= 1; state <= 2; end 2: begin // 发送设备地址: 0xA0(写) 或 0xA1(读) i2c_scl <= 0; if (bit_cnt < 8) begin begin reg [7:0] dev_addr; dev_addr = is_write ? 8'hA0 : 8'hA1; i2c_sda_out <= dev_addr[7 - bit_cnt[2:0]]; end i2c_scl <= 1; bit_cnt <= bit_cnt + 1; end else begin // ACK i2c_sda_oe <= 0; // 释放SDA i2c_scl <= 1; bit_cnt <= 0; state <= 3; end end 3: begin // 发送存储地址 i2c_scl <= 0; if (bit_cnt < 8) begin i2c_sda_out <= addr_reg[7 - bit_cnt[2:0]]; i2c_sda_oe <= 1; i2c_scl <= 1; bit_cnt <= bit_cnt + 1; end else begin i2c_sda_oe <= 0; i2c_scl <= 1; bit_cnt <= 0; state <= 4; end end 4: begin // 数据阶段 i2c_scl <= 0; if (is_write) begin // 写入数据 if (bit_cnt < 8) begin i2c_sda_out <= data_reg[7 - bit_cnt[2:0]]; i2c_sda_oe <= 1; i2c_scl <= 1; bit_cnt <= bit_cnt + 1; end else begin eeprom_mem[addr_reg] <= data_reg; rd_data <= data_reg; rd_valid <= 1; state <= 5; end end else begin // 读取数据 if (bit_cnt < 8) begin i2c_sda_oe <= 0; i2c_scl <= 1; bit_cnt <= bit_cnt + 1; end else begin rd_data <= eeprom_mem[addr_reg]; rd_valid <= 1; state <= 5; end end end 5: begin // STOP条件: SCL高时SDA上升 i2c_scl <= 0; i2c_sda_out <= 0; i2c_sda_oe <= 1; state <= 6; end 6: begin i2c_scl <= 1; i2c_sda_out <= 1; // STOP busy <= 0; state <= 0; end endcase end end endmodule

🧪 测试平台(Testbench)

testbench = 你的"手柄+屏幕",模拟输入、验证输出:

/* verilator lint_off WIDTHEXPAND */ /* verilator lint_off WIDTHTRUNC */ /* verilator lint_off UNOPTFLAT */ module tb; reg clk, rst_n; reg cmd_valid, cmd_wr; reg [7:0] addr, wr_data; wire [7:0] rd_data; wire rd_valid, busy; wire i2c_scl, i2c_sda_out, i2c_sda_oe; i2c_eeprom uut ( .clk(clk), .rst_n(rst_n), .cmd_valid(cmd_valid), .cmd_wr(cmd_wr), .addr(addr), .wr_data(wr_data), .rd_data(rd_data), .rd_valid(rd_valid), .busy(busy), .i2c_scl(i2c_scl), .i2c_sda_out(i2c_sda_out), .i2c_sda_oe(i2c_sda_oe) ); always clk = #10 ~clk; task send_cmd; input wr; input [7:0] a; input [7:0] d; begin while (busy) @(posedge clk); cmd_wr = wr; addr = a; wr_data = d; cmd_valid = 1; @(posedge clk); cmd_valid = 0; end endtask initial begin $dumpfile("i2c.vcd"); $dumpvars(0, tb); clk = 0; rst_n = 0; cmd_valid = 0; cmd_wr = 0; addr = 0; wr_data = 0; repeat(5) @(posedge clk); rst_n = 1; $display("=== I2C EEPROM仿真 ==="); $display("读写EEPROM仿真"); $display(""); $display("--- I2C EEPROM参数 ---"); $display(" 设备地址: 0xA0(写) / 0xA1(读)"); $display(" 容量: 256字节"); $display(" 协议: I2C, 标准模式(100kHz)"); $display(" 时序: START + 设备地址 + 存储地址 + 数据 + STOP"); $display(""); // 测试1: 写0xBE到地址0 $display("--- 测试1: 写0xBE到地址0 ---"); send_cmd(1, 8'h00, 8'hBE); while (busy) @(posedge clk); repeat(10) @(posedge clk); $display(" EEPROM[0]=0x%02h (期望0xBE)", uut.eeprom_mem[0]); if (uut.eeprom_mem[0] == 8'hBE) $display(" ✅ 写入正确"); // 测试2: 读地址0 $display(""); $display("--- 测试2: 读地址0 ---"); send_cmd(0, 8'h00, 8'h00); while (busy) @(posedge clk); repeat(10) @(posedge clk); $display(" 读回: eeprom[0]=0x%02h", uut.eeprom_mem[0]); if (uut.eeprom_mem[0] == 8'hBE) $display(" ✅ 读回正确"); // 测试3: 多字节写入 $display(""); $display("--- 测试3: 多字节写入 ---"); send_cmd(1, 8'h10, 8'hCA); while (busy) @(posedge clk); send_cmd(1, 8'h11, 8'hFE); while (busy) @(posedge clk); repeat(10) @(posedge clk); $display(" EEPROM[0x10]=0x%02h, [0x11]=0x%02h", uut.eeprom_mem[16], uut.eeprom_mem[17]); if (uut.eeprom_mem[16] == 8'hCA && uut.eeprom_mem[17] == 8'hFE) $display(" ✅ 多字节写入正确 (0xCAFE!)"); // 测试4: 验证未写入地址仍为0xFF $display(""); $display("--- 测试4: 未写入区域 ---"); $display(" EEPROM[0x80]=0x%02h (期望0xFF)", uut.eeprom_mem[128]); if (uut.eeprom_mem[128] == 8'hFF) $display(" ✅ 未写入区域保持0xFF"); $display(""); $display("✅ 读写EEPROM仿真验证通过!"); $display("🏆 成就解锁: 读写EEPROM仿真!"); $finish; end endmodule

✅ 仿真输出

运行 verilator --cc *.sv --exe sim_main.cpp --top-module tb --timing --trace --build -j 4 -o sim 后的输出:

=== I2C EEPROM仿真 === 读写EEPROM仿真 --- I2C EEPROM参数 --- 设备地址: 0xA0(写) / 0xA1(读) 容量: 256字节 协议: I2C, 标准模式(100kHz) 时序: START + 设备地址 + 存储地址 + 数据 + STOP --- 测试1: 写0xBE到地址0 --- EEPROM[0]=0xbe (期望0xBE) ✅ 写入正确 --- 测试2: 读地址0 --- 读回: eeprom[0]=0xbe ✅ 读回正确 --- 测试3: 多字节写入 --- EEPROM[0x10]=0xca, [0x11]=0xfe ✅ 多字节写入正确 (0xCAFE!) --- 测试4: 未写入区域 --- EEPROM[0x80]=0xff (期望0xFF) ✅ 未写入区域保持0xFF ✅ 读写EEPROM仿真验证通过! 🏆 成就解锁: 读写EEPROM仿真! - tb.sv:92: Verilog $finish

🔧 编译和运行

# 编译 verilator --cc *.sv --exe sim_main.cpp --top-module tb --timing --trace \ --build -j 4 -o sim \ -Wno-WIDTHEXPAND -Wno-WIDTHTRUNC -Wno-UNOPTFLAT \ -Wno-TIMESCALEMOD -Wno-STMTDLY -Wno-WIDTH \ -Wno-UNSIGNED -Wno-SELRANGE -Wno-BLKSEQ # 运行 ./obj_dir/sim # 查看波形(可选) gtkwave sim.vcd
🏆
读写EEPROM仿真
✅ Verilator仿真验证通过

🧠 知识扩展

I2C协议:I2C(内部集成电路)由Philips发明,两线制(SCL+SDA),支持多主多从。标准模式100kHz,快速模式400kHz。7位地址空间支持128个设备。

EEPROM特性:EEPROM按页写入(通常8-128字节),写入后需要twr时间(5-10ms)完成。读取无限制。AT24C02=2Kbit=256字节,是最常见的I2C EEPROM。

⚡ 性能提示

• 使用--trace选项生成VCD波形文件,用GTKWave查看

• 使用-j 4选项并行编译,加快构建速度

• 使用--build选项让Verilator自动调用make

• 大量$display输出会拖慢仿真速度,验证通过后可以减少打印频率