🏠 课程总览 > 第24课
第24课: I2C EEPROM
I2C协议+设备地址+START/STOP条件+EEPROM读写
🏆 读写EEPROM仿真
✅ Verilator仿真验证通过
📖 核心概念
- I2C协议:两线制(SCL+SDA),多设备共享总线
- START条件:SCL高时SDA下降沿
- STOP条件:SCL高时SDA上升沿
- 设备地址:0xA0(写)/0xA1(读),7位地址+R/W位
- ACK/NACK:每字节后接收方拉低SDA确认
💡 关键思路:本课的核心是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输出会拖慢仿真速度,验证通过后可以减少打印频率