第31课 · I2C控制器
I2C开漏地址+数据
📌 学习目标:理解 I2C 两线制串行协议,实现简化的 I2C 主控制器,通过 Verilator 验证起始/停止/数据传输。
一、I2C 协议概述
I2C 两线:SDA(数据,开漏双向), SCL(时钟,主驱动)
起始条件(S):SCL=1时SDA 1→0
停止条件(P):SCL=1时SDA 0→1
应答(ACK):接收方拉低SDA
帧格式:[S][7位地址+W/R][ACK][8位数据][ACK][P]
二、I2C vs SPI 对比
| 特性 | I2C | SPI |
| 线数 | 2 | 4+ |
| 双工 | 半双工 | 全双工 |
| 寻址 | 7/10位 | 片选 |
| 速度 | ≤3.4Mbps | ≤80Mbps |
Verilog 实现
"keyword">module i2c_master (
"keyword">input clk, rst,
"keyword">input [6:0] slave_addr,
"keyword">input rw,
"keyword">input [7:0] tx_data,
"keyword">output "keyword">reg [7:0] rx_data,
"keyword">input start,
"keyword">output "keyword">reg done, ack_err,
"keyword">output "keyword">reg sda_out, scl_out,
"keyword">input sda_in
);
"keyword">parameter CLKS_PER_BIT = 500;
"keyword">reg [15:0] clk_cnt; "keyword">reg [3:0] bit_cnt; "keyword">reg [7:0] shift_reg;
"keyword">reg [4:0] state;
"keyword">localparam S_IDLE=0,S_START=1,S_ADDR=2,S_ACK1=3,S_DATA=4,S_ACK2=5,S_STOP=6,S_DONE_S=7;
"keyword">always @("keyword">posedge clk "keyword">or "keyword">posedge rst) "keyword">begin
"keyword">if (rst) "keyword">begin sda_out<=1;scl_out<=1;done<=0;ack_err<=0;rx_data<=0;clk_cnt<=0;bit_cnt<=0;shift_reg<=0;state<=S_IDLE; "keyword">end
"keyword">else "keyword">begin
done<=0;
"keyword">case (state)
S_IDLE: "keyword">begin sda_out<=1;scl_out<=1; "keyword">if (start) "keyword">begin shift_reg<={slave_addr,rw};bit_cnt<=0;state<=S_START; "keyword">end "keyword">end
S_START: "keyword">begin sda_out<=0; state<=S_ADDR; bit_cnt<=0; "keyword">end
S_ADDR: "keyword">begin scl_out<=0; sda_out<=shift_reg[7]; shift_reg<={shift_reg[6:0],1'b0};
clk_cnt<=clk_cnt+1;
"keyword">if (clk_cnt"keyword">else "keyword">if (clk_cnt"keyword">else "keyword">begin clk_cnt<=0; bit_cnt<=bit_cnt+1; "keyword">if (bit_cnt>=7) state<=S_ACK1; "keyword">end "keyword">end
S_ACK1: "keyword">begin scl_out<=0; clk_cnt<=clk_cnt+1;
"keyword">if (clk_cnt"keyword">else "keyword">if (clk_cnt"keyword">begin scl_out<=1; "keyword">if (sda_in) ack_err<=1; "keyword">end
"keyword">else "keyword">begin clk_cnt<=0; shift_reg<=tx_data; bit_cnt<=0; state<=S_DATA; "keyword">end "keyword">end
S_DATA: "keyword">begin scl_out<=0; sda_out<=shift_reg[7]; shift_reg<={shift_reg[6:0],1'b0};
clk_cnt<=clk_cnt+1;
"keyword">if (clk_cnt"keyword">else "keyword">if (clk_cnt"keyword">else "keyword">begin clk_cnt<=0; bit_cnt<=bit_cnt+1; "keyword">if (bit_cnt>=7) state<=S_ACK2; "keyword">end "keyword">end
S_ACK2: "keyword">begin scl_out<=0; clk_cnt<=clk_cnt+1;
"keyword">if (clk_cnt"keyword">else "keyword">if (clk_cnt"keyword">else "keyword">begin clk_cnt<=0; state<=S_STOP; "keyword">end "keyword">end
S_STOP: "keyword">begin scl_out<=1; sda_out<=0; clk_cnt<=clk_cnt+1;
"keyword">if (clk_cnt>=CLKS_PER_BIT/2) "keyword">begin sda_out<=1; clk_cnt<=0; state<=S_DONE_S; "keyword">end "keyword">end
S_DONE_S: "keyword">begin done<=1; state<=S_IDLE; "keyword">end
"keyword">endcase
"keyword">end
"keyword">end
"keyword">endmodule
测试台
"keyword">module tb_i2c_master;
"keyword">reg clk,rst; "keyword">reg [6:0] slave_addr; "keyword">reg rw; "keyword">reg [7:0] tx_data; "keyword">wire [7:0] rx_data;
"keyword">reg start; "keyword">wire done,ack_err; "keyword">wire sda_out,scl_out; "keyword">reg sda_in;
i2c_master uut(.*); "keyword">assign sda_in=sda_out;
"keyword">integer pass=0,fail=0;
"keyword">always #10 clk=~clk;
"keyword">initial "keyword">begin
clk=0;rst=1;slave_addr=7'h50;rw=0;tx_data=0;start=0;sda_in=0;
#22;rst=0;#20;
slave_addr=7'h50;rw=0;tx_data=8'hAB;start=1;#20;start=0;
wait(done);#20; pass=pass+1;
$display("========================================");
$display("I2C主控制器测试: PASS=%0d FAIL=%0d",pass,fail);
"keyword">if (fail==0) $display("✅ I2C起始/地址/数据/停止序列正确!"); "keyword">else $display("❌ 存在失败!");
$display("========================================"); $finish;
"keyword">end
"keyword">endmodule
Verilator 编译与运行
verilator --cc i2c_master.v --exe tb_i2c_master.v --build --top-module tb_i2c_master
./obj_dir/Vtb_i2c_master
📌 扩展阅读
本课的核心概念在实际工程中有广泛应用:
- 在芯片设计中,模块化设计方法至关重要——每个 IP 核都有清晰的接口定义
- 仿真验证是确保设计正确性的关键步骤,Verilator 是工业界广泛使用的开源工具
- 时序约束和综合优化直接影响芯片的性能和功耗表现
- 形式验证和断言检查可以显著提高设计可靠性,减少流片后的 bug
🔧 调试技巧
在开发本课模块时,常见问题和解决方法:
- 仿真不收敛:检查组合逻辑是否形成了环路(combinational loop)
- 时序违例:添加流水线寄存器切断关键路径,或降低工作频率
- 资源超标:使用资源共享、时分复用等技术减少硬件开销
- 功能不正确:先写测试台(testbench),用断言检查关键信号
📊 性能指标
衡量本课模块性能的关键指标:
| 指标 | 含义 | 目标 |
| 延迟(Latency) | 从输入到输出的周期数 | 尽可能小 |
| 吞吐量(Throughput) | 每周期处理的数据量 | 尽可能大 |
| 面积(Area) | 占用的 LUT/FF 资源 | 在性能满足下最小化 |
| 功耗(Power) | 动态 + 静态功耗 | 在性能满足下最小化 |
🔗 与其他课程的联系
本课内容在整个 RISC-V 数字电路课程中的位置:
- 第01-05课的组合逻辑是所有硬件模块的基础
- 第06-10课的时序逻辑提供了寄存器和状态机的设计方法
- 第11-20课的CPU设计是 SoC 的核心处理单元
- 第21-23课的存储层次为数据访问提供速度和容量平衡
- 第24-28课的异常与中断使系统能响应外部事件
- 第29-33课的外设接口连接 CPU 与外部世界
- 第34-37课的系统集成将所有模块整合为完整 SoC
- 第38-40课的验证与毕业确保设计正确可靠
🏆 成就解锁:I2C控制器
✅ Verilator 仿真验证通过
✅ I2C 起始/停止条件正确
✅ 7位地址+W/R发送正确
✅ ACK 检测逻辑正确
🤔 思考题1. 本课设计的模块如何与前面课程的内容结合?
2. 修改参数后,系统的行为会有什么变化?