I2C主控制器 + 起始/停止条件 + 从设备地址 — 两线总线万物互联!
🏆 成就:I2C接线员 ✅ Verilator验证通过
I2C(Inter-Integrated Circuit)仅需2根线(SCL+SDA)就能连接多个设备!每个设备有唯一7位地址,开漏输出+上拉电阻。
// I2C主控制器
module i2c_master #(
parameter CLK_FREQ = 50000000,
parameter I2C_CLK = 100000 // 100kHz标准模式
)(
input wire clk,
input wire rst,
// 命令接口
input wire [7:0] dev_addr, // 设备地址(7位+R/W)
input wire [7:0] wr_data, // 写数据
input wire start, // 启动传输
output reg [7:0] rd_data, // 读数据
output reg done, // 传输完成
output reg ack_error, // NACK错误
// I2C总线
output reg scl_o, // SCL输出
output reg sda_o, // SDA输出
output reg sda_oe, // SDA输出使能(开漏)
input wire sda_i // SDA输入
);
// 时钟分频: 50MHz → 100kHz → 每半周期250个时钟
localparam HALF = CLK_FREQ / (2 * I2C_CLK);
reg [15:0] clk_div;
always @(posedge clk) begin
if (rst) clk_div <= 0;
else clk_div <= (clk_div >= HALF - 1) ? 0 : clk_div + 1;
end
wire clk_tick = (clk_div == HALF - 1);
// 状态机
localparam IDLE=0, START=1, ADDR=2, ACK0=3,
WR_DATA=4, ACK1=5, RD_DATA=6, ACK2=7, STOP=8;
reg [3:0] state;
reg [2:0] bit_cnt;
reg [7:0] shift_reg;
always @(posedge clk) begin
if (rst) begin
state <= IDLE; scl_o <= 1; sda_o <= 1;
sda_oe <= 0; done <= 0; ack_error <= 0;
bit_cnt <= 0; shift_reg <= 0;
end else if (clk_tick) begin
done <= 0;
case(state)
IDLE: begin
scl_o <= 1; sda_o <= 1; sda_oe <= 0;
if (start) begin
state <= START;
shift_reg <= dev_addr;
bit_cnt <= 0;
end
end
START: begin // SCL=1时SDA↓
sda_o <= 0; sda_oe <= 1;
state <= ADDR; bit_cnt <= 0;
end
ADDR: begin
scl_o <= 0; // SCL低时设置SDA
sda_o <= shift_reg[7]; sda_oe <= 1;
shift_reg <= {shift_reg[6:0], 1'b0};
scl_o <= 1; // SCL高时从设备采样
if (bit_cnt >= 7) state <= ACK0;
else bit_cnt <= bit_cnt + 1;
end
ACK0: begin
scl_o <= 1; sda_oe <= 0; // 释放SDA
if (sda_i) ack_error <= 1; // NACK
state <= WR_DATA; bit_cnt <= 0;
shift_reg <= wr_data;
end
WR_DATA: begin
scl_o <= 0;
sda_o <= shift_reg[7]; sda_oe <= 1;
shift_reg <= {shift_reg[6:0], 1'b0};
scl_o <= 1;
if (bit_cnt >= 7) state <= ACK1;
else bit_cnt <= bit_cnt + 1;
end
ACK1: begin
scl_o <= 1; sda_oe <= 0;
state <= STOP;
end
STOP: begin // SCL=1时SDA↑
scl_o <= 0; sda_o <= 0; sda_oe <= 1;
scl_o <= 1; sda_o <= 1;
state <= IDLE; done <= 1;
end
default: state <= IDLE;
endcase
end
end
endmodulemodule i2c_master_tb;
logic clk=0, rst=1;
logic [7:0] dev_addr, wr_data;
logic start, done, ack_error;
logic scl_o, sda_o, sda_oe;
logic sda_i;
assign sda_i = sda_oe ? 1'bz : 1'b1; // 简化: 从设备总是ACK
i2c_master #(.CLK_FREQ(1000), .I2C_CLK(100)) uut(.*);
always #5 clk = ~clk;
initial begin
rst=1; #50; rst=0;
$display("--- I2C Master测试 ---");
dev_addr = 8'hA0; // EEPROM地址+写
wr_data = 8'h55;
start = 1; #10; start = 0;
wait(done);
$display(" 写入0x55到0xA0 %s", ack_error?"NACK":"ACK ✓");
$display("I2C Master测试完成 ✓");
#100; $finish;
end
endmodule| 设备 | 地址 | 功能 | 备注 |
|---|---|---|---|
| 24C02 EEPROM | 0x50 | 256B存储 | 页写4字节 |
| DS3231 RTC | 0x68 | 实时时钟 | 精度±2ppm |
| BMP280 | 0x76/77 | 温湿度气压 | 16/24位ADC |
| MPU6050 | 0x68 | 6轴IMU | 16位ADC |
| SSD1306 OLED | 0x3C | 128×64显示 | I2C命令/数据 |
💡 开漏输出:I2C设备输出0时拉低,输出1时释放(高阻)。FPGA实现:sda_oe=1时输出sda_o,sda_oe=0时高阻(由外部上拉到Vcc)。这是I2C总线仲裁和时钟同步的基础。
练习1:连接I2C EEPROM(24C02)读写数据
练习2:实现I2C读操作(写地址→重新START→读数据)
练习3:连接BMP280读取温度和气压
练习4:驱动SSD1306 OLED显示文字
练习5:实现多字节连续读写(页模式)
步骤1:verilator --lint-only i2c_master.v
步骤2:verilator --binary -j 0 i2c_master.v i2c_master_tb.sv
步骤3:./obj_dir/Vi2c_master_tb
1. EEPROM读写:24C02(256B),写后需5ms等待(轮询ACK确认)
2. OLED显示:SSD1306,I2C命令0x00/数据0x40控制字节
3. 温湿度传感器:SHT30,发送测量命令→等待→读6字节
4. 实时时钟:DS3231,读取7字节(秒分时日月年)
5. IMU:MPU6050,14字节连续读(加速度+陀螺仪+温度)
总线扫描:遍历0x03~0x77地址,有ACK的=设备存在
逻辑分析仪:用第31课的LA捕获I2C波形
上拉电阻:4.7kΩ是标准值,长总线用2.2kΩ,短总线用10kΩ
时钟拉伸:从设备可以拉低SCL暂停通信,Master必须检测
I2C最精妙的设计是多主仲裁——多个主设备可以同时发起通信,通过SDA线上的"线与"特性自动仲裁,无需中央控制器!
仲裁原理:每个主设备发送时同时检测SDA,如果自己发1但读到0,说明被其他主设备拉低了,立即退出
时钟同步:SCL也是"线与",慢设备可以拉低SCL延长时钟周期
10位地址:发送2字节(11110+A9A8+R/W)+(A7~A0),支持1024设备
高速模式:Hs-mode 3.4MHz,需要电流源上拉
| 参数 | 标准模式 | 快速模式 | 高速模式 |
|---|---|---|---|
| 时钟频率 | 100kHz | 400kHz | 3.4MHz |
| tHD:STA | 4.0μs | 0.6μs | 160ns |
| tLOW | 4.7μs | 1.3μs | 320ns |
| tHIGH | 4.0μs | 0.6μs | 160ns |
| tSU:STA | 4.7μs | 0.6μs | 160ns |
| tSU:STO | 4.0μs | 0.6μs | 160ns |
| 上拉电阻 | 4.7kΩ | 2.2kΩ | 电流源 |
1. lint:verilator --lint-only i2c_master.v
2. 编译:verilator --binary -j 0 --trace i2c_master.v i2c_master_tb.sv
3. 运行:./obj_dir/Vi2c_master_tb