文章目录
- 引言
- [IIC 主机 / 控制器 (IIC Master / Controller)](#IIC 主机 / 控制器 (IIC Master / Controller))
- [IIC 从机 / 目标 (IIC Slave / Target)](#IIC 从机 / 目标 (IIC Slave / Target))
- [使用 FPGA 实现 IIC 控制器的 N 种方法](#使用 FPGA 实现 IIC 控制器的 N 种方法)
- [Verilog实现 IIC 主从机](#Verilog实现 IIC 主从机)
-
- [IIC 主机](#IIC 主机)
- [IIC 从机](#IIC 从机)
- [优点 & 缺点](#优点 & 缺点)
-
- [IIC 接口总线的优点:](#IIC 接口总线的优点:)
- [IIC 接口总线的缺点:](#IIC 接口总线的缺点:)
- 附
-
- [7-bit addressing](#7-bit addressing)
- [10-bit addressing](#10-bit addressing)
- IIC时序
引言
IIC(Inter-Integrated Circuit),也被称为 I2C 或 I²C,是由飞利浦半导体(现 NXP 半导体)于 1982 年发明的一种串行通信总线。虽然它已经诞生了 40 多年,但凭借其简单性和低成本,它依然是嵌入式系统中连接处理器与低速外设(如传感器、EEPROM、ADC/DAC、RTC 时钟等)的首选方案。
它的核心优势在于极其简洁的硬件需求:
- SDA (Serial Data Line):串行数据线,用于传输数据。
- SCL (Serial Clock Line):串行时钟线,用于同步数据传输的时序。
仅需这两根线(加上电源和地),就可以在同一块电路板上挂载多个设备,实现灵活的通信。
💡 冷知识: 尽管我们习惯称其为"主机/从机"(Master/Slave),但在 2021 年发布的 I2C 规范 Rev 7 中,为了术语的中性化,官方名称已更新为 "控制器(Controller)" 和 "目标(Target)"。不过为了方便理解,本文仍会涵盖这两个概念。

IIC 主机 / 控制器 (IIC Master / Controller)
在 IIC 总线上,主机是整个通信过程的"指挥官"。
- 核心职责:
- 生成时钟:主机负责产生 SCL 时钟信号,这是数据传输的心跳。
- 发起与结束通信:只有主机可以发送 起始信号(START) 和 停止信号(STOP)。起始信号是 SDA 在 SCL 为高电平时由高变低;停止信号则是 SDA 在 SCL 为高电平时由低变高。
- 寻址:通信开始后,主机发送 7 位(或 10 位)的目标设备地址,并指定读写方向(R/W 位)。
- 工作模式: 主机并不总是"发送者"。根据需求,它可以处于 主机发送模式 (Master Transmit) 或 主机接收模式 (Master Receive)。例如,读取传感器数据时,主机在发送完地址后,就会切换为接收模式,等待从机发回数据。
- 多主机仲裁 (Arbitration): IIC 支持多主机模式。如果两个主机同时尝试发起通信怎么办?IIC 拥有一套确定性的仲裁机制。
- 主机在发送数据的同时会监测 SDA 线上的电平。
- 因为是"线与"逻辑(Wired-AND),当一个主机发送"1"(高阻态)而另一个发送"0"(拉低)时,总线会呈现低电平。
- 发送"1"的主机检测到总线是"0",就知道自己"输了"(Lost Arbitration),它会立即停止发送并退出,而赢得仲裁的主机继续通信,数据不会损坏。
IIC 从机 / 目标 (IIC Slave / Target)
从机是总线上的"响应者",它们静默监听总线,直到被点名。
- 核心职责:
- 地址匹配:每个从机都有一个唯一的地址(通常是 7 位,也有 10 位扩展)。当主机发送的地址与自己匹配时,从机必须在第 9 个时钟周期拉低 SDA 线,发送 ACK(应答信号)。
- 数据收发:根据主机的读写指令,从机处于 从机接收 (Slave Receive) 或 从机发送 (Slave Transmit) 模式。
- 时钟拉伸 (Clock Stretching): 这是从机控制总线节奏的"杀手锏"。
- 如果从机(例如一个处理速度较慢的单片机)来不及处理接收到的字节,或者没准备好发送下一个字节,它可以将 SCL 线强制拉低。
- 由于 SCL 是开漏结构,主机发现 SCL 被拉低后,必须暂停传输,进入等待状态(Wait State),直到从机释放 SCL,使时钟线恢复高电平。
- 这是一种从机向主机发出的"流控"信号,告诉主机:"慢点,等我一下"。
使用 FPGA 实现 IIC 控制器的 N 种方法
在 Xilinx FPGA 开发中,实现 IIC 通常有几种方式:
- 使用 AXI IIC IP 核: 适合 MicroBlaze 或 Zynq PL 端扩展。
- 优点:官方 IP,驱动完善(Vitis/SDK 中直接有现成驱动)。支持多主模式(Multi-Master)和动态地址,功能最全。
- 缺点:资源消耗:会消耗几百个 LUT 和寄存器,比手写 RTL 臃肿。交互繁琐:通过 AXI4-Lite 总线配置寄存器,软件开销大,不适合极低延迟场景。
- 使用 Zynq/MPSoC PS 端的 MIO IIC:
- 优点:零逻辑资源:完全不占用 PL 端(FPGA 逻辑)资源。稳定性好:硬核电路,不受时序收敛影响。
- 缺点:引脚受限:只能映射到固定的 MIO 引脚(通常是 Bank 500/501),无法随意分配管脚。
- 使用 Zynq/MPSoC PS 端的 EMIO IIC:
- 优点:灵活性极高:解决了 MIO 引脚锁定的问题,可以映射到 FPGA 的任意管脚。继承硬核优势:软件驱动与 MIO 模式完全通用。
- 缺点:会占用少量的 PL 布线资源。
- MicroBlaze 或者 PS 端 模拟 IIC时序:
- 优点:应急之选:当硬核被占光,或者不想消耗 AXI IP 资源时使用。任意管脚:只要是个 GPIO 就能跑。
- 缺点:效率极低:极其消耗 CPU 算力,CPU 需要不断轮询延时。时序风险:由于操作系统调度或中断影响,波形容易不均匀,难以达到高速(400k 以上很难稳定)。
- 纯 RTL 手写实现(本文重点):灵活性最高。
- Slave 模式刚需:Zynq PS 端的 I2C 虽然支持 Slave 模式,但在实际工程中,FPGA 充当外部 MCU 的 I2C 从机(用于参数配置、数据交互)时,纯 RTL 状态机是响应最快、最可控的方案。
- 超低延迟:不需要 CPU 介入,直接逻辑处理,纳秒级响应。
- 特殊协议适配:有些芯片的 I2C 是"变种"(比如 16 位地址、非标时序),IP 核和硬核无法调整,只有 RTL 能随心所欲修改。
- 独立性:不依赖 ARM 或 MicroBlaze,系统挂了它还能跑,适合做看门狗或电源管理接口。
Verilog实现 IIC 主从机
IIC 主机
该iic主机模块具备以下简单功能:
- 主机模式(Master Mode): 模块作为I2C总线的主机,负责产生时钟(SCL)和启动/停止信号。
- 可配置时钟频率: 支持通过参数 sysclk_num 和 iic_clk_num 灵活配置系统时钟和IIC通信速率(默认为100MHz系统时钟,400kHz IIC速率)。
- 7位设备寻址: 支持标准的7位从机地址通信。
- 可变宽度的寄存器地址: 通过 i_addr_num 信号,支持访问 8位(1字节)或 16位(2字节)宽度的从机寄存器地址。
- 连续读写功能:
- 写操作: 支持向指定寄存器写入单个或多个字节的数据(通过 i_wrdata_num 控制)。
- 读操作: 支持从指定寄存器读取多个字节的数据(通过 i_rddata_num 控制)。
- 基本握手信号: 提供了 o_wrdata_done(写完一个字节)、o_rddata_done(读到一个数据)和 o_done(整个事务结束)标志,便于上层模块进行时序控制。
【设计文件】iic_master.v
c
module iic_master (
input i_clk // 系统时钟
,input i_rst // 系统复位 (高电平有效)
,input i_wr // 触发写操作脉冲
,input i_rd // 触发读操作脉冲
,input [5:0] i_wrdata_num // 写数据字节数 (Base 1)
,input [5:0] i_rddata_num // 读数据字节数 (Base 1)
,input [1:0] i_addr_num // 寄存器地址字节数 (1或2)
,input [6:0] i_device_addr // 器件物理地址
,input [15:0] i_reg_addr // 寄存器地址
,input [7:0] i_wrdata // 待写入的数据
,output reg o_wrdata_done // 写数据完成标志 (每写完一个字节拉高一次)
,output reg o_rddata_valid // 读数据有效标志
,output reg [7:0] o_rddata // 读到的数据
,output reg o_done // IIC 整个事务结束标志
,output reg o_ack_error // ACK错误标志
,output reg o_scl // IIC SCL 时钟
,inout o_sda // IIC SDA 数据线
);
//***************************************************************************
// Parameters
//***************************************************************************
parameter SYS_CLK_FREQ = 100_000_000;
parameter IIC_CLK_FREQ = 400_000;
localparam SCL_CNT_MAX = SYS_CLK_FREQ / IIC_CLK_FREQ;
localparam IDLE = 4'd0;
localparam START = 4'd1;
localparam SEND_DEV_ADDR = 4'd2;
localparam SEND_REG_ADDR_H = 4'd3;
localparam SEND_REG_ADDR_L = 4'd4;
localparam WRITE_DATA = 4'd5;
localparam START_READ = 4'd6;
localparam SEND_DEV_ADDR_R = 4'd7;
localparam READ_DATA = 4'd8;
localparam SEND_ACK = 4'd9;
localparam STOP = 4'd10;
//***************************************************************************
// Internal Signals
//***************************************************************************
reg [15:0] scl_cnt;
reg iic_clk_en;
reg [3:0] state;
reg [3:0] bit_cnt;
reg [5:0] byte_cnt;
reg [7:0] tx_shift_reg;
reg [7:0] rx_shift_reg;
reg sda_out_reg;
reg sda_out_en;
reg ack_bit;
// 锁存寄存器
reg r_wr_en;
reg [1:0] r_addr_num;
reg [5:0] r_wrdata_num;
reg [5:0] r_rddata_num;
reg [6:0] r_device_addr;
reg [15:0] r_reg_addr;
wire scl_low_mid = (scl_cnt == (SCL_CNT_MAX/4));
wire scl_high_mid = (scl_cnt == (SCL_CNT_MAX/4)*3);
wire scl_fall = (scl_cnt == SCL_CNT_MAX - 1);
assign o_sda = sda_out_en ? sda_out_reg : 1'bz;
wire sda_in = o_sda;
//***************************************************************************
// Code
//***************************************************************************
always @(posedge i_clk) begin
if (i_rst) begin
scl_cnt <= 16'd0;
o_scl <= 1'b1;
end else begin
if (state != IDLE && state != START) begin
if (iic_clk_en) begin
if (scl_cnt == SCL_CNT_MAX - 1) begin
o_scl <= 1'b0;
end else begin
if (scl_cnt < (SCL_CNT_MAX/2)) o_scl <= 1'b0;
else o_scl <= 1'b1;
end
end else begin
o_scl <= 1'b1;
end
end else begin
o_scl <= 1'b1;
end
if (iic_clk_en) begin
if (scl_cnt == SCL_CNT_MAX - 1) begin
scl_cnt <= 16'd0;
end else begin
scl_cnt <= scl_cnt + 1'b1;
end
end else begin
scl_cnt <= 16'd0;
end
end
end
// FSM
always @(posedge i_clk) begin
if (i_rst) begin
state <= IDLE;
iic_clk_en <= 1'b0;
bit_cnt <= 4'd0;
byte_cnt <= 6'd0;
sda_out_reg <= 1'b1;
sda_out_en <= 1'b0;
o_done <= 1'b0;
o_wrdata_done <= 1'b0;
o_rddata_valid <= 1'b0;
o_rddata <= 8'd0;
o_ack_error <= 1'b0;
tx_shift_reg <= 8'd0;
ack_bit <= 1'b1;
r_wr_en <= 1'b0;
r_addr_num <= 2'd0;
r_wrdata_num <= 6'd0;
r_rddata_num <= 6'd0;
r_device_addr <= 7'd0;
r_reg_addr <= 16'd0;
end else begin
o_wrdata_done <= 1'b0;
o_rddata_valid <= 1'b0;
o_done <= 1'b0;
case (state)
IDLE: begin
iic_clk_en <= 1'b0;
sda_out_en <= 1'b0;
sda_out_reg <= 1'b1;
bit_cnt <= 4'd0;
byte_cnt <= 6'd0;
o_ack_error <= 1'b0;
if (i_wr || i_rd) begin
state <= START;
iic_clk_en <= 1'b1;
r_wr_en <= i_wr;
r_addr_num <= i_addr_num;
r_wrdata_num <= i_wrdata_num;
r_rddata_num <= i_rddata_num;
r_device_addr <= i_device_addr;
r_reg_addr <= i_reg_addr;
end
end
START: begin
if (scl_high_mid) begin
sda_out_en <= 1'b1;
sda_out_reg <= 1'b0;
end
if (scl_fall) begin
state <= SEND_DEV_ADDR;
tx_shift_reg <= {r_device_addr, 1'b0};
bit_cnt <= 0;
end
end
SEND_DEV_ADDR: begin
if (scl_low_mid) begin
if (bit_cnt < 8) begin
sda_out_en <= 1'b1;
sda_out_reg <= tx_shift_reg[7-bit_cnt];
end else begin
sda_out_en <= 1'b0;
end
end
if (scl_high_mid && bit_cnt == 8) ack_bit <= sda_in;
if (scl_fall) begin
if (bit_cnt == 8) begin
if (ack_bit == 1'b1) o_ack_error <= 1'b1;
bit_cnt <= 0;
if (r_addr_num == 2) begin
state <= SEND_REG_ADDR_H;
tx_shift_reg <= r_reg_addr[15:8];
end else begin
state <= SEND_REG_ADDR_L;
tx_shift_reg <= r_reg_addr[7:0];
end
end else begin
bit_cnt <= bit_cnt + 1'b1;
end
end
end
SEND_REG_ADDR_H: begin
if (scl_low_mid) begin
if (bit_cnt < 8) begin
sda_out_en <= 1'b1;
sda_out_reg <= tx_shift_reg[7-bit_cnt];
end else
sda_out_en <= 1'b0;
end
if (scl_high_mid && bit_cnt == 8) ack_bit <= sda_in;
if (scl_fall) begin
if (bit_cnt == 8) begin
if (ack_bit == 1'b1) o_ack_error <= 1'b1;
bit_cnt <= 0;
state <= SEND_REG_ADDR_L;
tx_shift_reg <= r_reg_addr[7:0];
end else
bit_cnt <= bit_cnt + 1'b1;
end
end
SEND_REG_ADDR_L: begin
if (scl_low_mid) begin
if (bit_cnt < 8) begin
sda_out_en <= 1'b1;
sda_out_reg <= tx_shift_reg[7-bit_cnt];
end else
sda_out_en <= 1'b0;
end
if (scl_high_mid && bit_cnt == 8) ack_bit <= sda_in;
if (scl_fall) begin
if (bit_cnt == 8) begin
if (ack_bit == 1'b1) o_ack_error <= 1'b1;
bit_cnt <= 0;
if (r_wr_en) begin
state <= WRITE_DATA;
byte_cnt <= 0;
tx_shift_reg <= i_wrdata;
end else begin
state <= START_READ;
end
end else
bit_cnt <= bit_cnt + 1'b1;
end
end
WRITE_DATA: begin
if (scl_low_mid) begin
if (bit_cnt < 8) begin
sda_out_en <= 1'b1;
sda_out_reg <= tx_shift_reg[7-bit_cnt];
end else
sda_out_en <= 1'b0;
end
if (scl_low_mid && bit_cnt == 8) begin
o_wrdata_done <= 1'b1;
end
if (scl_high_mid && bit_cnt == 8) ack_bit <= sda_in;
if (scl_fall) begin
if (bit_cnt == 8) begin
if (ack_bit == 1'b1) o_ack_error <= 1'b1;
bit_cnt <= 0;
if (byte_cnt == r_wrdata_num - 1) begin
state <= STOP;
end else begin
byte_cnt <= byte_cnt + 1'b1;
state <= WRITE_DATA;
tx_shift_reg <= i_wrdata;
end
end else
bit_cnt <= bit_cnt + 1'b1;
end
end
START_READ: begin
if (scl_low_mid) begin
sda_out_en <= 1'b1;
sda_out_reg <= 1'b1;
end else if (scl_high_mid) begin
sda_out_reg <= 1'b0;
end
if (scl_fall) begin
state <= SEND_DEV_ADDR_R;
tx_shift_reg <= {r_device_addr, 1'b1};
bit_cnt <= 0;
end
end
SEND_DEV_ADDR_R: begin
if (scl_low_mid) begin
if (bit_cnt < 8) begin
sda_out_en <= 1'b1;
sda_out_reg <= tx_shift_reg[7-bit_cnt];
end else
sda_out_en <= 1'b0;
end
if (scl_high_mid && bit_cnt == 8) ack_bit <= sda_in;
if (scl_fall) begin
if (bit_cnt == 8) begin
if (ack_bit == 1'b1) o_ack_error <= 1'b1;
bit_cnt <= 0;
state <= READ_DATA;
end else
bit_cnt <= bit_cnt + 1'b1;
end
end
READ_DATA: begin
sda_out_en <= 1'b0;
if (scl_high_mid && bit_cnt < 8) begin
rx_shift_reg[7-bit_cnt] <= sda_in;
end
if (scl_fall) begin
if (bit_cnt == 7) begin
state <= SEND_ACK;
bit_cnt <= 0;
o_rddata <= rx_shift_reg;
o_rddata_valid <= 1'b1;
end else begin
bit_cnt <= bit_cnt + 1'b1;
end
end
end
SEND_ACK: begin
if (scl_low_mid) begin
sda_out_en <= 1'b1;
if (byte_cnt == r_rddata_num - 1)
sda_out_reg <= 1'b1;
else
sda_out_reg <= 1'b0;
end
if (scl_fall) begin
if (byte_cnt == r_rddata_num - 1) begin
state <= STOP;
end else begin
byte_cnt <= byte_cnt + 1'b1;
state <= READ_DATA;
end
end
end
STOP: begin
if (scl_low_mid) begin
sda_out_en <= 1'b1;
sda_out_reg <= 1'b0;
end else if (scl_high_mid) begin
sda_out_reg <= 1'b1;
o_done <= 1'b1;
end
if (scl_fall) begin
state <= IDLE;
iic_clk_en <= 1'b0;
end
end
default: state <= IDLE;
endcase
end
end
endmodule
【仿真文件】tb_iic_master.v
c
`timescale 1ns / 1ps
`define clk_period 10 // 100MHz = 10ns周期
module tb_iic_master();
// 1. 信号定义
reg i_clk ;
reg i_rst ;
reg i_wr ;
reg i_rd ;
reg [5:0] i_wrdata_num ;
reg [5:0] i_rddata_num ;
reg [1:0] i_addr_num ;
reg [6:0] i_device_addr ;
reg [15:0] i_reg_addr ;
reg [7:0] i_wrdata ;
wire o_wrdata_done ; // 注意:这是用来请求下一个写数据的
wire o_rddata_valid ; // 注意:这是读数据有效标志
wire [7:0] o_rddata ;
wire o_done ;
wire o_scl ;
wire o_sda ;
// 2. 例化优化后的 IIC Master
iic_master #(
.SYS_CLK_FREQ(100_000_000),
.IIC_CLK_FREQ(400_000)
) u_iic_master (
.i_clk (i_clk ),
.i_rst (i_rst ),
.i_wr (i_wr ),
.i_rd (i_rd ),
.i_wrdata_num (i_wrdata_num ),
.i_rddata_num (i_rddata_num ),
.i_addr_num (i_addr_num ),
.i_device_addr (i_device_addr ),
.i_reg_addr (i_reg_addr ),
.i_wrdata (i_wrdata ),
.o_wrdata_done (o_wrdata_done ),
.o_rddata_valid (o_rddata_valid ),
.o_rddata (o_rddata ),
.o_done (o_done ),
.o_scl (o_scl ),
.o_sda (o_sda )
);
// I2C 上拉电阻模拟
pullup(o_sda);
pullup(o_scl);
// 3. 简单的 I2C Slave 仿真模型 (替代 M24LC64)
// 这是一个简化的行为级模型,仅用于验证主机波形是否正确
// 它会自动应答 ACK,并在读操作时返回固定的计数数据
M24LC04B U_M24LC04B(
.A0 (1 ),
.A1 (0 ),
.A2 (0 ),
.WP (0 ),
.SDA (o_sda ),
.SCL (o_scl ),
.RESET (i_rst )
);
// 4. 时钟生成
initial i_clk = 0;
always #(`clk_period/2) i_clk = ~i_clk;
// 5. 自动打印读到的数据
always @(posedge i_clk) begin
if (o_rddata_valid) begin
$display("Time %t: READ DATA VALID: 8'h%h", $time, o_rddata);
end
end
// 6. 测试激励
initial begin
// 初始化信号
i_rst = 1; // 保持复位 (高电平有效)
i_wr = 0;
i_rd = 0;
i_addr_num = 0;
i_wrdata_num = 0;
i_rddata_num = 0;
// i_device_addr = 7'b1010000; // 标准EEPROM地址
i_device_addr = 7'b1010001; // 标准EEPROM地址
i_reg_addr = 0;
i_wrdata = 0;
#(`clk_period * 10);
i_rst = 0; // 释放复位
#(`clk_period * 10);
// --- Case 1: 写 1 个字节 ---
// 地址: 16'h0102, 数据: 8'hAA
$display("\n--- TEST CASE 1: WRITE 1 BYTE ---");
i_addr_num = 2; // 16位地址
i_wrdata_num = 1; // 写1个数据
// i_device_addr = 7'b1010000; // 标准EEPROM地址
i_device_addr = 7'b1010001; // 标准EEPROM地址
// 调用写任务
wr_single_byte(16'h0102, 8'hAA);
#(`clk_period * 6000); // 等待一段时间
// --- Case 2: 读 1 个字节 ---
$display("\n--- TEST CASE 2: READ 1 BYTE ---");
i_addr_num = 2;
i_rddata_num = 1;
rd_task(16'h0102);
#(`clk_period * 500);
$stop;
end
// ---------------------------------------------
// 任务定义
// ---------------------------------------------
// 单字节写任务
task wr_single_byte(
input [15:0] addr,
input [7:0] data
);
begin
i_wr = 1;
i_reg_addr = addr;
i_wrdata = data;
#(`clk_period);
i_wr = 0;
@(posedge o_done); // 等待结束
#(`clk_period);
end
endtask
// 读任务 (通用)
task rd_task(
input [15:0] addr
);
begin
i_rd = 1;
i_reg_addr = addr;
#(`clk_period);
i_rd = 0;
@(posedge o_done);
#(`clk_period);
end
endtask
endmodule
【仿真时序】

【仿真文件】M24LC04B.v
c
`timescale 1ns/10ps
module M24LC04B (A0, A1, A2, WP, SDA, SCL, RESET);
input A0; // unconnected pin
input A1; // unconnected pin
input A2; // unconnected pin
input WP; // write protect pin
inout SDA; // serial data I/O
input SCL; // serial data clock
input RESET; // system reset
// *******************************************************************************************************
// ** DECLARATIONS **
// *******************************************************************************************************
reg SDA_DO; // serial data - output
reg SDA_OE; // serial data - output enable
wire SDA_DriveEnable; // serial data output enable
reg SDA_DriveEnableDlyd; // serial data output enable - delayed
wire [02:00] ChipAddress; // hardwired chip address
reg [03:00] BitCounter; // serial bit counter
reg START_Rcvd; // START bit received flag
reg STOP_Rcvd; // STOP bit received flag
reg CTRL_Rcvd; // control byte received flag
reg ADDR_Rcvd; // byte address received flag
reg MACK_Rcvd; // master acknowledge received flag
reg WrCycle; // memory write cycle
reg RdCycle; // memory read cycle
reg [07:00] ShiftRegister; // input data shift register
reg [07:00] ControlByte; // control byte register
wire BlockSelect; // memory block select
wire RdWrBit; // read/write control bit
reg [08:00] StartAddress; // memory access starting address
reg [03:00] PageAddress; // memory page address
reg [07:00] WrDataByte [0:15]; // memory write data buffer
wire [07:00] RdDataByte; // memory read data
reg [15:00] WrCounter; // write buffer counter
reg [03:00] WrPointer; // write buffer pointer
reg [08:00] RdPointer; // read address pointer
reg WriteActive; // memory write cycle active
reg [07:00] MemoryBlock0 [0:255]; // EEPROM data memory array
reg [07:00] MemoryBlock1 [0:255]; // EEPROM data memory array
integer LoopIndex; // iterative loop index
integer tAA; // timing parameter
integer tWC; // timing parameter
// *******************************************************************************************************
// ** INITIALIZATION **
// *******************************************************************************************************
initial tAA = 900; // SCL to SDA output delay
// initial tWC = 5000000; // memory write cycle time
initial tWC = 50000; // memory write cycle time
initial begin
SDA_DO = 0;
SDA_OE = 0;
end
initial begin
START_Rcvd = 0;
STOP_Rcvd = 0;
CTRL_Rcvd = 0;
ADDR_Rcvd = 0;
MACK_Rcvd = 0;
end
initial begin
BitCounter = 0;
ControlByte = 0;
end
initial begin
WrCycle = 0;
RdCycle = 0;
WriteActive = 0;
end
assign ChipAddress = {A2,A1,A0};
// *******************************************************************************************************
// ** CORE LOGIC **
// *******************************************************************************************************
// -------------------------------------------------------------------------------------------------------
// 1.01: START Bit Detection
// -------------------------------------------------------------------------------------------------------
always @(negedge SDA) begin
if (SCL == 1) begin
START_Rcvd <= 1;
STOP_Rcvd <= 0;
CTRL_Rcvd <= 0;
ADDR_Rcvd <= 0;
MACK_Rcvd <= 0;
WrCycle <= #1 0;
RdCycle <= #1 0;
BitCounter <= 0;
end
end
// -------------------------------------------------------------------------------------------------------
// 1.02: STOP Bit Detection
// -------------------------------------------------------------------------------------------------------
always @(posedge SDA) begin
if (SCL == 1) begin
START_Rcvd <= 0;
STOP_Rcvd <= 1;
CTRL_Rcvd <= 0;
ADDR_Rcvd <= 0;
MACK_Rcvd <= 0;
WrCycle <= #1 0;
RdCycle <= #1 0;
BitCounter <= 10;
end
end
// -------------------------------------------------------------------------------------------------------
// 1.03: Input Shift Register
// -------------------------------------------------------------------------------------------------------
always @(posedge SCL) begin
ShiftRegister[00] <= SDA;
ShiftRegister[01] <= ShiftRegister[00];
ShiftRegister[02] <= ShiftRegister[01];
ShiftRegister[03] <= ShiftRegister[02];
ShiftRegister[04] <= ShiftRegister[03];
ShiftRegister[05] <= ShiftRegister[04];
ShiftRegister[06] <= ShiftRegister[05];
ShiftRegister[07] <= ShiftRegister[06];
end
// -------------------------------------------------------------------------------------------------------
// 1.04: Input Bit Counter
// -------------------------------------------------------------------------------------------------------
always @(posedge SCL) begin
if (BitCounter < 10) BitCounter <= BitCounter + 1;
end
// -------------------------------------------------------------------------------------------------------
// 1.05: Control Byte Register
// -------------------------------------------------------------------------------------------------------
always @(negedge SCL) begin
if (START_Rcvd & (BitCounter == 8)) begin
if (!WriteActive & (ShiftRegister[07:01] == {4'b1010,ChipAddress[02:00]})) begin
if (ShiftRegister[00] == 0) WrCycle <= 1;
if (ShiftRegister[00] == 1) RdCycle <= 1;
ControlByte <= ShiftRegister[07:00];
CTRL_Rcvd <= 1;
end
START_Rcvd <= 0;
end
end
assign BlockSelect = ControlByte[01];
assign RdWrBit = ControlByte[00];
// -------------------------------------------------------------------------------------------------------
// 1.06: Byte Address Register
// -------------------------------------------------------------------------------------------------------
always @(negedge SCL) begin
if (CTRL_Rcvd & (BitCounter == 8)) begin
if (RdWrBit == 0) begin
StartAddress <= {BlockSelect,ShiftRegister[07:00]};
RdPointer <= {BlockSelect,ShiftRegister[07:00]};
ADDR_Rcvd <= 1;
end
WrCounter <= 0;
WrPointer <= 0;
CTRL_Rcvd <= 0;
end
end
// -------------------------------------------------------------------------------------------------------
// 1.07: Write Data Buffer
// -------------------------------------------------------------------------------------------------------
always @(negedge SCL) begin
if (ADDR_Rcvd & (BitCounter == 8)) begin
if ((WP == 0) & (RdWrBit == 0)) begin
WrDataByte[WrPointer] <= ShiftRegister[07:00];
WrCounter <= WrCounter + 1;
WrPointer <= WrPointer + 1;
end
end
end
// -------------------------------------------------------------------------------------------------------
// 1.08: Acknowledge Generator
// -------------------------------------------------------------------------------------------------------
always @(negedge SCL) begin
if (!WriteActive) begin
if (BitCounter == 8) begin
if (WrCycle | (START_Rcvd & (ShiftRegister[07:01] == {4'b1010,ChipAddress[02:00]}))) begin
SDA_DO <= 0;
SDA_OE <= 1;
end
end
if (BitCounter == 9) begin
BitCounter <= 0;
if (!RdCycle) begin
SDA_DO <= 0;
SDA_OE <= 0;
end
end
end
end
// -------------------------------------------------------------------------------------------------------
// 1.09: Acknowledge Detect
// -------------------------------------------------------------------------------------------------------
always @(posedge SCL) begin
if (RdCycle & (BitCounter == 8)) begin
if ((SDA == 0) & (SDA_OE == 0)) MACK_Rcvd <= 1;
end
end
always @(negedge SCL) MACK_Rcvd <= 0;
// -------------------------------------------------------------------------------------------------------
// 1.10: Write Cycle Timer
// -------------------------------------------------------------------------------------------------------
always @(posedge STOP_Rcvd) begin
if (WrCycle & (WP == 0) & (WrCounter > 0)) begin
WriteActive = 1;
#(tWC);
WriteActive = 0;
end
end
always @(posedge STOP_Rcvd) begin
#(1.0);
STOP_Rcvd = 0;
end
// -------------------------------------------------------------------------------------------------------
// 1.11: Write Cycle Processor
// -------------------------------------------------------------------------------------------------------
always @(negedge WriteActive) begin
for (LoopIndex = 0; LoopIndex < WrCounter; LoopIndex = LoopIndex + 1) begin
if (StartAddress[08] == 0) begin
PageAddress = StartAddress[03:00] + LoopIndex;
MemoryBlock0[{StartAddress[07:04],PageAddress[03:00]}] = WrDataByte[LoopIndex[03:00]];
end
if (StartAddress[08] == 1) begin
PageAddress = StartAddress[03:00] + LoopIndex;
MemoryBlock1[{StartAddress[07:04],PageAddress[03:00]}] = WrDataByte[LoopIndex[03:00]];
end
end
end
// -------------------------------------------------------------------------------------------------------
// 1.12: Read Data Multiplexor
// -------------------------------------------------------------------------------------------------------
always @(negedge SCL) begin
if (BitCounter == 8) begin
if (WrCycle & ADDR_Rcvd) begin
RdPointer <= StartAddress + WrPointer + 1;
end
if (RdCycle) begin
RdPointer <= RdPointer + 1;
end
end
end
assign RdDataByte = RdPointer[08] ? MemoryBlock1[RdPointer[07:00]] : MemoryBlock0[RdPointer[07:00]];
// -------------------------------------------------------------------------------------------------------
// 1.13: Read Data Processor
// -------------------------------------------------------------------------------------------------------
always @(negedge SCL) begin
if (RdCycle) begin
if (BitCounter == 8) begin
SDA_DO <= 0;
SDA_OE <= 0;
end
else if (BitCounter == 9) begin
SDA_DO <= RdDataByte[07];
if (MACK_Rcvd) SDA_OE <= 1;
end
else begin
SDA_DO <= RdDataByte[7-BitCounter];
end
end
end
// -------------------------------------------------------------------------------------------------------
// 1.14: SDA Data I/O Buffer
// -------------------------------------------------------------------------------------------------------
bufif1 (SDA, 1'b0, SDA_DriveEnableDlyd);
assign SDA_DriveEnable = !SDA_DO & SDA_OE;
always @(SDA_DriveEnable) SDA_DriveEnableDlyd <= #(tAA) SDA_DriveEnable;
// *******************************************************************************************************
// ** DEBUG LOGIC **
// *******************************************************************************************************
// -------------------------------------------------------------------------------------------------------
// 2.01: Memory Data Bytes
// -------------------------------------------------------------------------------------------------------
wire [07:00] MemoryByte0_00 = MemoryBlock0[00];
wire [07:00] MemoryByte0_01 = MemoryBlock0[01];
wire [07:00] MemoryByte0_02 = MemoryBlock0[02];
wire [07:00] MemoryByte0_03 = MemoryBlock0[03];
wire [07:00] MemoryByte0_04 = MemoryBlock0[04];
wire [07:00] MemoryByte0_05 = MemoryBlock0[05];
wire [07:00] MemoryByte0_06 = MemoryBlock0[06];
wire [07:00] MemoryByte0_07 = MemoryBlock0[07];
wire [07:00] MemoryByte0_08 = MemoryBlock0[08];
wire [07:00] MemoryByte0_09 = MemoryBlock0[09];
wire [07:00] MemoryByte0_0A = MemoryBlock0[10];
wire [07:00] MemoryByte0_0B = MemoryBlock0[11];
wire [07:00] MemoryByte0_0C = MemoryBlock0[12];
wire [07:00] MemoryByte0_0D = MemoryBlock0[13];
wire [07:00] MemoryByte0_0E = MemoryBlock0[14];
wire [07:00] MemoryByte0_0F = MemoryBlock0[15];
wire [07:00] MemoryByte1_00 = MemoryBlock1[00];
wire [07:00] MemoryByte1_01 = MemoryBlock1[01];
wire [07:00] MemoryByte1_02 = MemoryBlock1[02];
wire [07:00] MemoryByte1_03 = MemoryBlock1[03];
wire [07:00] MemoryByte1_04 = MemoryBlock1[04];
wire [07:00] MemoryByte1_05 = MemoryBlock1[05];
wire [07:00] MemoryByte1_06 = MemoryBlock1[06];
wire [07:00] MemoryByte1_07 = MemoryBlock1[07];
wire [07:00] MemoryByte1_08 = MemoryBlock1[08];
wire [07:00] MemoryByte1_09 = MemoryBlock1[09];
wire [07:00] MemoryByte1_0A = MemoryBlock1[10];
wire [07:00] MemoryByte1_0B = MemoryBlock1[11];
wire [07:00] MemoryByte1_0C = MemoryBlock1[12];
wire [07:00] MemoryByte1_0D = MemoryBlock1[13];
wire [07:00] MemoryByte1_0E = MemoryBlock1[14];
wire [07:00] MemoryByte1_0F = MemoryBlock1[15];
// -------------------------------------------------------------------------------------------------------
// 2.02: Write Data Buffer
// -------------------------------------------------------------------------------------------------------
wire [07:00] WriteData_0 = WrDataByte[00];
wire [07:00] WriteData_1 = WrDataByte[01];
wire [07:00] WriteData_2 = WrDataByte[02];
wire [07:00] WriteData_3 = WrDataByte[03];
wire [07:00] WriteData_4 = WrDataByte[04];
wire [07:00] WriteData_5 = WrDataByte[05];
wire [07:00] WriteData_6 = WrDataByte[06];
wire [07:00] WriteData_7 = WrDataByte[07];
wire [07:00] WriteData_8 = WrDataByte[08];
wire [07:00] WriteData_9 = WrDataByte[09];
wire [07:00] WriteData_A = WrDataByte[10];
wire [07:00] WriteData_B = WrDataByte[11];
wire [07:00] WriteData_C = WrDataByte[12];
wire [07:00] WriteData_D = WrDataByte[13];
wire [07:00] WriteData_E = WrDataByte[14];
wire [07:00] WriteData_F = WrDataByte[15];
// *******************************************************************************************************
// ** TIMING CHECKS **
// *******************************************************************************************************
wire TimingCheckEnable = (RESET == 0) & (SDA_OE == 0);
specify
specparam
tHI = 600, // SCL pulse width - high
tLO = 1300, // SCL pulse width - low
tSU_STA = 600, // SCL to SDA setup time
tHD_STA = 600, // SCL to SDA hold time
tSU_DAT = 100, // SDA to SCL setup time
tSU_STO = 600, // SCL to SDA setup time
tBUF = 1300; // Bus free time
$width (posedge SCL, tHI);
$width (negedge SCL, tLO);
$width (posedge SDA &&& SCL, tBUF);
$setup (posedge SCL, negedge SDA &&& TimingCheckEnable, tSU_STA);
$setup (SDA, posedge SCL &&& TimingCheckEnable, tSU_DAT);
$setup (posedge SCL, posedge SDA &&& TimingCheckEnable, tSU_STO);
$hold (negedge SDA &&& TimingCheckEnable, negedge SCL, tHD_STA);
endspecify
endmodule
IIC 从机
该iic从机模块具备以下简单功能:
- 7-bit 从机地址寻址:支持标准的 7 位地址模式,不支持 10-bit 地址。
- 支持多种速率:由于采用高频系统时钟(如 50MHz+)采样,该模块轻松支持 Standard Mode (100 kbps) 和 Fast Mode (400 kbps)。
- Repeated Start (重复起始条件):代码中的 start_detect 逻辑具有高优先级,允许主机在不发送 STOP 的情况下直接发送新的 START。这是实现I2C 随机读(Random Read)的必要条件。
- 单字节/多字节写 (Write / Burst Write):主机发送寄存器地址 -> 写入数据 1 -> 写入数据 2 ...
- 随机读 (Random Read) ------ 最常用的读模式
- 功能:主机读取指定寄存器地址的数据。
- 流程:
- 主机写操作(发送寄存器地址)。
- 主机发送 Repeated Start。
- 主机发送读命令(设备地址 + Read位)。
- 从机返回该地址的数据。
- 特性:完美处理了写操作切换到读操作中间的状态跳转。
- 连续读 (Sequential Read / Burst Read)
- 功能:主机从当前地址开始,连续读取多个字节。
- 特性:
- 地址自增:当从机检测到主机回复 ACK(表示还要数据)时,内部地址自动加 1。
- 数据预取(Prefetch):这是代码中最复杂的优化点。在主机还在发送 ACK 的时候,模块就已经开始从 RAM 读取下一个地址的数据了,解决了 RAM 的读延迟(Latency)问题,保证连续读取时数据不发生错位。
- Xilinx BRAM 原生适配:
- 接口信号(addr, di, do, en, we)直接对应 Xilinx Block RAM IP 核的端口定义。
- 专门针对 BRAM 的 Read Latency 进行了时序补偿,确保读出的数据是正确的。
- 忙状态指示 (o_slave_busy):
- 提供给后级逻辑判断当前 I2C 总线是否正在进行数据传输。
【设计文件】iic_slave.v
c
module iic_slave #(
parameter SLAVE_ADDR_DEFAULT = 7'b1010000
,parameter DEBOUNCE_CNT = 4 // 滤波深度
)(
input i_clk // 系统时钟
,input i_rst
// I2C 物理接口
,input i_scl
,inout i_sda
,input [ 6: 0] i2c_slave_add
// 用户后端接口 (适配 Xilinx RAM)
,input [ 7: 0] i_ram_do
,output reg [ 7: 0] o_ram_di
,output reg [ 7: 0] o_ram_addr
,output reg o_ram_rw // 0: Write, 1: Read
,output reg o_ram_en
,output o_slave_busy
);
//===========================================================================
// 1. 信号同步与滤波
//===========================================================================
reg [DEBOUNCE_CNT-1:0] scl_pipe;
reg [DEBOUNCE_CNT-1:0] sda_pipe;
reg scl_stable, sda_stable;
reg scl_stable_d, sda_stable_d;
always @(posedge i_clk) begin
if (i_rst) begin
scl_pipe <= {DEBOUNCE_CNT{1'b1}};
sda_pipe <= {DEBOUNCE_CNT{1'b1}};
scl_stable <= 1'b1;
sda_stable <= 1'b1;
end else begin
scl_pipe <= {scl_pipe[DEBOUNCE_CNT-2:0], i_scl};
sda_pipe <= {sda_pipe[DEBOUNCE_CNT-2:0], i_sda};
if (&scl_pipe) scl_stable <= 1'b1;
else if (~|scl_pipe) scl_stable <= 1'b0;
if (&sda_pipe) sda_stable <= 1'b1;
else if (~|sda_pipe) sda_stable <= 1'b0;
end
end
// 边沿检测
always @(posedge i_clk) begin
scl_stable_d <= scl_stable;
sda_stable_d <= sda_stable;
end
wire scl_rise = (scl_stable && !scl_stable_d);
wire scl_fall = (!scl_stable && scl_stable_d);
// Start/Stop 检测
wire start_detect = (scl_stable && !sda_stable && sda_stable_d);
wire stop_detect = (scl_stable && sda_stable && !sda_stable_d);
//===========================================================================
// 2. 状态机定义
//===========================================================================
localparam S_IDLE = 0;
localparam S_ADDR = 1;
localparam S_ACK_ADDR = 2;
localparam S_REG_ADDR = 3;
localparam S_ACK_REG = 4;
localparam S_RX_DATA = 5;
localparam S_ACK_RX = 6;
localparam S_TX_DATA = 7;
localparam S_ACK_TX = 8;
reg [3:0] state;
reg [2:0] bit_cnt;
reg [7:0] shift_reg;
reg flag_read;
reg bit_sampled; // 采样标志位
wire addr_match = (shift_reg[7:1] == i2c_slave_add);
wire rw_bit = shift_reg[0];
reg sda_out_en;
reg sda_out_val;
assign i_sda = (sda_out_en) ? sda_out_val : 1'bz;
assign o_slave_busy = (state != S_IDLE);
//===========================================================================
// 3. 状态机逻辑 (Single Process)
//===========================================================================
// 将所有对 shift_reg 的赋值都包含在这个 always 块中,解决 multi-driven 问题
always @(posedge i_clk) begin
if (i_rst) begin
state <= S_IDLE;
bit_cnt <= 3'd7;
shift_reg <= 8'd0;
sda_out_en <= 1'b0;
sda_out_val <= 1'b0;
o_ram_addr <= 8'd0;
o_ram_di <= 8'd0;
o_ram_rw <= 1'b1;
o_ram_en <= 1'b0;
bit_sampled <= 1'b0;
end else begin
// 默认恢复读状态
o_ram_rw <= 1'b1;
if (start_detect) begin
state <= S_ADDR;
bit_cnt <= 3'd7;
sda_out_en <= 1'b0;
bit_sampled <= 1'b0;
end else if (stop_detect) begin
state <= S_IDLE;
sda_out_en <= 1'b0;
o_ram_en <= 1'b0;
end else begin
case (state)
S_IDLE: begin
sda_out_en <= 1'b0;
o_ram_en <= 1'b0;
end
//-----------------------------------------------------
// S_ADDR: 接收设备地址
//-----------------------------------------------------
S_ADDR: begin
if (scl_rise) begin
shift_reg <= {shift_reg[6:0], sda_stable};
bit_sampled <= 1'b1;
end else if (scl_fall) begin
if (bit_sampled) begin
if (bit_cnt == 0) begin
state <= S_ACK_ADDR;
bit_cnt <= 3'd7;
end else begin
bit_cnt <= bit_cnt - 1;
bit_sampled <= 1'b0;
end
end
end
end
//-----------------------------------------------------
// S_ACK_ADDR: 地址 ACK
//-----------------------------------------------------
S_ACK_ADDR: begin
if (addr_match) begin
sda_out_en <= 1'b1;
sda_out_val <= 1'b0;
flag_read <= rw_bit;
if (rw_bit) o_ram_en <= 1'b1; // 读预取
end else begin
sda_out_en <= 1'b0;
end
if (scl_fall) begin
if (addr_match) begin
if (rw_bit) begin
state <= S_TX_DATA;
// 读操作:加载第一个字节数据
shift_reg <= i_ram_do;
bit_sampled <= 1'b0;
end else begin
state <= S_REG_ADDR;
bit_sampled <= 1'b0;
end
end else begin
state <= S_IDLE;
end
sda_out_en <= 1'b0;
end
end
//-----------------------------------------------------
// S_REG_ADDR: 接收寄存器地址
//-----------------------------------------------------
S_REG_ADDR: begin
sda_out_en <= 1'b0;
if (scl_rise) begin
shift_reg <= {shift_reg[6:0], sda_stable};
bit_sampled <= 1'b1;
end else if (scl_fall) begin
if (bit_sampled) begin
if (bit_cnt == 0) begin
state <= S_ACK_REG;
bit_cnt <= 3'd7;
end else begin
bit_cnt <= bit_cnt - 1;
bit_sampled <= 1'b0;
end
end
end
end
//-----------------------------------------------------
// S_ACK_REG: 寄存器地址 ACK
//-----------------------------------------------------
S_ACK_REG: begin
sda_out_en <= 1'b1;
sda_out_val <= 1'b0;
if (scl_fall) begin
o_ram_addr <= shift_reg;
state <= S_RX_DATA;
sda_out_en <= 1'b0;
bit_sampled <= 1'b0;
end
end
//-----------------------------------------------------
// S_RX_DATA: 接收数据
//-----------------------------------------------------
S_RX_DATA: begin
sda_out_en <= 1'b0;
if (scl_rise) begin
shift_reg <= {shift_reg[6:0], sda_stable};
bit_sampled <= 1'b1;
end else if (scl_fall) begin
if (bit_sampled) begin
if (bit_cnt == 0) begin
state <= S_ACK_RX;
bit_cnt <= 3'd7;
end else begin
bit_cnt <= bit_cnt - 1;
bit_sampled <= 1'b0;
end
end
end
end
//-----------------------------------------------------
// S_ACK_RX: 写数据 ACK
//-----------------------------------------------------
S_ACK_RX: begin
sda_out_en <= 1'b1;
sda_out_val <= 1'b0;
// 写 RAM
o_ram_rw <= 1'b0;
o_ram_en <= 1'b1;
o_ram_di <= shift_reg;
if (scl_fall) begin
state <= S_RX_DATA;
sda_out_en <= 1'b0;
o_ram_addr <= o_ram_addr + 1'b1;
o_ram_rw <= 1'b1;
bit_sampled <= 1'b0;
end
end
//-----------------------------------------------------
// S_TX_DATA: 发送数据 (包含连续读逻辑)
//-----------------------------------------------------
S_TX_DATA: begin
o_ram_en <= 1'b1;
if (bit_cnt == 3'd7 && !bit_sampled) begin
shift_reg <= i_ram_do; // 持续装载新数据
sda_out_val <= i_ram_do[7]; // 直接输出RAM最高位,避免旧shift_reg数据干扰
sda_out_en <= 1'b1;
end else begin
// 正常移位输出
sda_out_val <= shift_reg[7];
sda_out_en <= 1'b1;
end
if (scl_rise) begin
bit_sampled <= 1'b1; // 标记主机已读取
end else if (scl_fall) begin
if (bit_sampled) begin
if (bit_cnt == 0) begin
state <= S_ACK_TX;
bit_cnt <= 3'd7;
sda_out_en <= 1'b0;
end else begin
shift_reg <= {shift_reg[6:0], 1'b0};
bit_cnt <= bit_cnt - 1;
bit_sampled <= 1'b0;
end
end
end
end
//-----------------------------------------------------
// S_ACK_TX: 主机 ACK 检测
//-----------------------------------------------------
S_ACK_TX: begin
sda_out_en <= 1'b0;
o_ram_en <= 1'b1;
if (scl_fall) begin
// SDA=0: ACK (继续), SDA=1: NACK (停止)
if (sda_stable == 1'b0) begin
state <= S_TX_DATA;
o_ram_addr <= o_ram_addr + 1'b1; // 地址自增
bit_sampled <= 1'b0;
// 注意:此处不给 shift_reg 赋值,而是由 S_TX_DATA
// 前半段逻辑负责装载新地址的数据
end else begin
state <= S_IDLE;
end
end
end
default: state <= S_IDLE;
endcase
end
end
end
endmodule
【仿真时序】

【仿真文件】tb_iic_slave.v
c
`timescale 1ns / 1ps
`define clk_period 10 // 100MHz = 10ns周期
module tb_iic_slave();
// 1. 信号定义
reg i_clk ;
reg i_rst ;
reg i_wr ;
reg i_rd ;
reg [5:0] i_wrdata_num ;
reg [5:0] i_rddata_num ;
reg [1:0] i_addr_num ;
reg [6:0] i_device_addr ;
reg [15:0] i_reg_addr ;
reg [7:0] i_wrdata ;
wire o_wrdata_done ; // 注意:这是用来请求下一个写数据的
wire o_rddata_valid ; // 注意:这是读数据有效标志
wire [7:0] o_rddata ;
wire o_done ;
wire o_scl ;
wire o_sda ;
// 2. 例化优化后的 IIC Master
iic_master #(
.SYS_CLK_FREQ(100_000_000),
.IIC_CLK_FREQ(400_000)
) u_iic_master (
.i_clk (i_clk ),
.i_rst (i_rst ),
.i_wr (i_wr ),
.i_rd (i_rd ),
.i_wrdata_num (i_wrdata_num ),
.i_rddata_num (i_rddata_num ),
.i_addr_num (i_addr_num ),
.i_device_addr (i_device_addr ),
.i_reg_addr (i_reg_addr ),
.i_wrdata (i_wrdata ),
.o_wrdata_done (o_wrdata_done ),
.o_rddata_valid (o_rddata_valid ),
.o_rddata (o_rddata ),
.o_done (o_done ),
.o_scl (o_scl ),
.o_sda (o_sda )
);
wire o_slave_busy ;
wire [ 7: 0] i_ram_do ;
wire [ 7: 0] o_ram_di ;
wire [ 7: 0] o_ram_addr ;
wire o_ram_rw ;
wire o_ram_en ;
iic_slave #(
.SLAVE_ADDR_DEFAULT ( 7'b1010000 )
) u_iic_slave (
.i_clk (i_clk ),
.i_rst (i_rst ),// 假设高电平复位
.i_scl (o_scl ),
.i_sda (o_sda ),
.i2c_slave_add (i_device_addr ),// 可动态配置的从机地址
.i_ram_do (i_ram_do ),// RAM读数据 (从RAM读出发送给主机)
.o_ram_di (o_ram_di ),// RAM写数据 (从主机写入RAM)
.o_ram_addr (o_ram_addr ),// RAM地址
.o_ram_rw (o_ram_rw ),// 0: Write, 1: Read
.o_ram_en (o_ram_en ),// RAM使能脉冲
.o_slave_busy (o_slave_busy ) // 忙信号
);
blk_mem_gen_0 u_blk_mem_gen_0 (
.clka (i_clk ), // input wire clka
.ena (o_ram_en ), // input wire ena
.wea (o_ram_rw ), // input wire [0 : 0] wea
.addra (o_ram_addr ), // input wire [7 : 0] addra
.dina (o_ram_di ), // input wire [7 : 0] dina
.douta (i_ram_do ) // output wire [7 : 0] douta
);
// I2C 上拉电阻模拟
pullup(o_sda);
pullup(o_scl);
// 3. 简单的 I2C Slave 仿真模型 (替代 M24LC64)
// 这是一个简化的行为级模型,仅用于验证主机波形是否正确
// 它会自动应答 ACK,并在读操作时返回固定的计数数据
// M24LC04B U_M24LC04B(
// .A0 (1 ),
// .A1 (0 ),
// .A2 (0 ),
// .WP (0 ),
// .SDA (o_sda ),
// .SCL (o_scl ),
// .RESET (i_rst )
// );
// 4. 时钟生成
initial i_clk = 0;
always #(`clk_period/2) i_clk = ~i_clk;
// 5. 自动打印读到的数据
always @(posedge i_clk) begin
if (o_rddata_valid) begin
$display("Time %t: READ DATA VALID: 8'h%h", $time, o_rddata);
end
end
// 6. 测试激励
initial begin
// 初始化信号
i_rst = 1; // 保持复位 (高电平有效)
i_wr = 0;
i_rd = 0;
i_addr_num = 0;
i_wrdata_num = 0;
i_rddata_num = 0;
// i_device_addr = 7'b1010000; // 标准EEPROM地址
i_device_addr = 7'b1010001; // 标准EEPROM地址
i_reg_addr = 0;
i_wrdata = 0;
#(`clk_period * 10);
i_rst = 0; // 释放复位
#(`clk_period * 10);
// --- Case 1: 写 1 个字节 ---
// 地址: 16'h0102, 数据: 8'hAA
$display("\n--- TEST CASE 1: WRITE 1 BYTE ---");
i_addr_num = 1; // 16位地址
i_wrdata_num = 1; // 写1个数据
// i_device_addr = 7'b1010000; // 标准EEPROM地址
i_device_addr = 7'b1010001; // 标准EEPROM地址
// 调用写任务
// wr_single_byte(16'h0102, 8'hAA);
wr_single_byte(8'h02, 8'hAA);
#(`clk_period * 3000); // 等待一段时间
// --- Case 2: 读 1 个字节 ---
$display("\n--- TEST CASE 2: READ 1 BYTE ---");
i_addr_num = 1;
i_rddata_num = 1;
rd_task(8'h02);
#(`clk_period * 500);
$stop;
end
// ---------------------------------------------
// 任务定义
// ---------------------------------------------
// 单字节写任务
task wr_single_byte(
input [15:0] addr,
input [7:0] data
);
begin
i_wr = 1;
i_reg_addr = addr;
i_wrdata = data;
#(`clk_period);
i_wr = 0;
@(posedge o_done); // 等待结束
#(`clk_period);
end
endtask
// 读任务 (通用)
task rd_task(
input [15:0] addr
);
begin
i_rd = 1;
i_reg_addr = addr;
#(`clk_period);
i_rd = 0;
@(posedge o_done);
#(`clk_period);
end
endtask
endmodule
优点 & 缺点
IIC 接口总线的优点:
- 硬件连接简单,节省资源
- 仅需两根线:IIC 只需要两根总线线路:串行数据线 (SDA) 和串行时钟线 (SCL)。
- 减少引脚和 PCB 空间:相比于像 SPI 这样需要为每个设备提供独立片选信号(Chip Select)的总线,IIC 极大地减少了微控制器 I/O 引脚的占用,同时也减少了 PCB 上的布线数量和面积,从而降低了制造成本。
- 无需额外接口逻辑:IIC 接口已集成在芯片内部,设备可以直接"挂"在总线上,无需额外的地址译码器或逻辑电路。
- 灵活性与扩展性强
- 多主机(Multi-Controller)支持:IIC 是真正的多主机总线,允许多个控制器同时存在。它拥有冲突检测和仲裁机制(Arbitration),防止多个主机同时发送数据时导致数据损坏。
- 易于添加/移除设备:可以在不影响总线上其他设备的情况下,轻松地将新设备接入或从系统中移除。这使得系统升级或修改变得非常简单。
- 软件寻址:每个设备都有唯一的地址,通信完全由软件定义。标准使用 7 位地址,也支持 10 位扩展地址,理论上可挂载大量设备。
- 完善的流控与兼容性
- 时钟拉伸(Clock Stretching):从机可以通过拉低 SCL 线来暂停通信,迫使主机进入等待状态。这允许处理速度较慢的设备与快速主机同步,防止数据丢失。
- 多种速度模式:IIC 提供多种速度等级以适应不同需求,包括标准模式(100 kbit/s)、快速模式(400 kbit/s)、快速模式+(1 Mbit/s)以及高速模式(3.4 Mbit/s),且通常向下兼容。
- 抗干扰与低功耗:IIC 兼容的 IC 通常具有高噪声免疫力、宽电压工作范围以及极低的电流消耗,非常适合电池供电系统。
IIC 接口总线的缺点:
- 传输速度与效率限制
- 速度相对较慢:相比于 SPI 等总线,IIC 的传输速率较低。虽然有高速模式,但大多数应用仍停留在 400 kbit/s 或更低。
- 数据吞吐量打折:标称的比特率(如 400 kbit/s)并不等于有效数据传输率。由于协议中包含地址帧、ACK/NACK 应答位等开销,实际用户数据的传输速率会低于标称值。
- 半双工通信:数据传输是半双工的,同一时间只能单向传输。
- 物理层与距离限制
- 传输距离短:受限于总线电容(通常限制在 400 pF),IIC 仅适用于板级或短距离通信(通常在几米以内,甚至仅限 30 厘米内的 PCB 板上)。
- 开漏输出的局限:IIC 使用开漏(Open-Drain)结构,必须接上拉电阻。这导致信号上升时间受电阻和总线电容影响,限制了速度,且在低功耗系统中,上拉电阻会消耗额外功率。
- 地址与管理复杂性
- 地址冲突:7 位地址空间有限,且许多设备的地址由厂商固定或仅有少量可配置引脚,容易导致地址冲突(Address Collisions)。
- 总线锁死风险:如果某个设备发生故障将 SDA 或 SCL 线一直拉低,会导致整个总线挂起(Hang),主机无法发送停止信号来重置总线。通常需要额外的硬件复位信号或重新上电来解决。
- 配置管理困难:IIC 缺乏自动化的设备发现机制(即插即用能力较弱),主机通常需要预先知道总线上挂载了哪些设备及其地址。
- 一致性问题
- 时钟拉伸的副作用:虽然时钟拉伸是优点,但也可能被滥用。如果某个设备过长时间拉伸时钟,会阻塞总线带宽,增加其他设备的通信延迟。
- 部分实现不完整:市场上部分标称 "Two-wire interface" 的设备可能不支持完整的 IIC 规范(如不支持时钟拉伸或仲裁),导致兼容性问题。
附
7-bit addressing

10-bit addressing

IIC时序
