大侠好,欢迎来到FPGA技术江湖,江湖偌大,相见即是缘分。大侠可以关注FPGA技术江湖,在“闯荡江湖”、"行侠仗义"栏里获取其他感兴趣的资源,或者一起煮酒言欢。
今天给大侠带来基于FPGA的 模拟 I²C 协议设计,包括 I²C 总线解析以及模拟 I²C 接口程序的基本框架、I²C 协议的具体实现、程序的仿真与测试。篇幅较长,话不多说,上货。
之前也有相关文章介绍,仅供各位大侠参考。
导读
I²C(Inter-Integrated Circuit),其实是 I²C Bus简称,中文就是集成电路总线,它是一种串行通信总线,使用多主从架构,由飞利浦公司在1980年代为了让主板、嵌入式系统或手机用以连接低速周边设备而发展。I²C的正确读法为“I平方C”("I-squared-C"),而“I二C”("I-two-C")则是另一种错误但被广泛使用的读法。自2006年10月1日起,使用 I²C 协议已经不需要支付专利费,但制造商仍然需要付费以获取 I²C 从属设备地址。
I²C 简单来说,就是一种串行通信协议,I²C的通信协议和通信接口在很多工程中有广泛的应用,如数据采集领域的串行 AD,图像处理领域的摄像头配置,工业控制领域的 X 射线管配置等等。除此之外,由于 I²C 协议占用的 IO 资源特别少,连接方便,所以工程中也常选用 I²C 接口做为不同芯片间的通信协议。I²C 串行总线一般有两根信号线,一根是双向的数据线SDA,另一根是时钟线SCL。所有接到 I²C 总线设备上的串行数据SDA都接到总线的SDA上,各设备的时钟线SCL接到总线的SCL上。
在现代电子系统中,有为数众多的 IC 需要进行相互之间以及与外界的通信。为了简化电路的设计,Philips 公司开发了一种用于内部 IC 控制的简单的双向两线串行总线 I²C(Intel-Integrated Circuit bus)。1998 年当推出 I²C 总线协议 2.0 版本时,I²C 协议实际上已经成为一个国际标准。
在进行 FPGA 设计时,经常需要和外围提供 I²C 接口的芯片通信。例如低功耗的 CMOS 实时时钟/日历芯片 PCF8563、LCD 驱动芯片 PCF8562、并行口扩展芯片 PCF8574、键盘/LED 驱动器 ZLG7290 等都提供 I²C 接口。因此在 FPGA 中模拟 I²C 接口已成为 FPGA 开发必要的步骤。
本篇将详细讲解在 FPGA 芯片中使用 VHDL/Verilog HDL 模拟 I²C 协议,以及编写 TestBench仿真和测试程序的方法。
第一篇内容摘要:本篇会介绍 I²C 总线解析,包括 I²C 总线概述、I²C 协议的基本概念、I²C协议的时序要求,还会介绍模拟 I²C 接口程序的基本框架等相关内容。
一、I2C 总线概述
下面先对 I²C 协议中有关数据格式和时序的内容进行介绍,这里没有涉及的地方请参考《THE I²C-BUS SPECIFICATION VERSION 2.1 JANUARY 2000》。
1.1 I²C 总线概述
I²C 协议作为一个串行总线标准尽管没有并行总线的数据吞吐能力,但是它的以下特点使其有着广泛的应用:
• 只需要两条总线—串行数据线 SDA 和串行时钟线 SCL;
• 每个连接到总线的器件都可以通过惟一的地址和一直存在的简单的主/从节点关系软件设定地址,主节点可以发送数据或接收数据;
• 是真正的多主总线,当两个或更多主节点同时初始化数据传输时,可以通过冲突检测和仲裁防止数据被破坏;
• 串行的 8 位双向数据传输位速率在标准模式下可达 100kbit/s,快速模式下可达400kbit/s,高速模式下可达 3.4Mbit/s;
• 片上的滤波器可以滤去总线数据线上的毛刺波,保证数据完整;
• 连接到相同总线的 IC 数量只受到总线的最大电容(400pF)限制。
总线不仅仅是互连的线,还包含系统通信的所有格式和过程。I²C 总线结构上的特点保证了其应用时的简洁,另外其完备的协议避免了所有混乱、数据丢失和妨碍信息的可能性。
1.2 I²C 协议的基本概念
I²C 总线支持任何 IC 生产过程(NMOS、CMOS 和双极性)。串行数据线 SDA 和串行时钟线 SCL在连接到总线的器件间传递信息。每个器件都有一个惟一的地址作为识别的标志(无论是微控制器、LCD 驱动器存储器还是键盘接口),并且都可以发送数据和接收数据。很明显 LCD 驱动器只需要接收数据,而存储器需要接收和发送数据。图 1 所示的是一个高性能集成电视的例子。
图 1 高性能集成电视
从图 1 可以看到,应用 I²C 总线是非常方便的。用通俗的话讲 I²C 总线的硬件设计工作就是连接 SDA 和 SCL 两条线,依靠 I²C 协议完成软件工作。在 I²C 协议中应理解如下的概念。
1)主/从节点
主节点负责初始化总线的数据传输,并产生允许传输的时钟信号。此时任何被寻址的器件都被认为是从节点。当有多个主节点在总线上传输数据时,每个主节点产生自己的时钟信号。挂接到总线上的所有外围器件、外设接口都是总线上的节点。
2)总线上节点的寻址方式
在任何时刻总线上只有一个主控器件(主节点)实现总线的控制操作,对总线上的其他节点寻址,可分时实现点-点的数据传送。因此总线上每个节点都有一个固定的节点地址。
I²C 总线上主节点的地址由软件给定,此地址存放在 I²C 总线的地址寄存器中。I²C 总线上所有的外围器件都有规范的器件地址。器件地址由 7 位数字组成,它和 1 位方向位构成了 I²C 总线器件的寻址字节 SLA(Slave address)。
器件地址是 I²C 总线外围接口器件固有的地址编码,器件出厂时就已给定。数据方向位规定了总线上主节点对从节点的数据传送方向。
1.3 I²C协议的时序要求
1)总线上的数据传递时序
I²C 总线上数据传递时序如图 2 所示,具体步骤如下。
图 2 I²C 总线的数据传递时序
• 首先主节点器件发送一个起始信号。
• 接下来主节点器件发送从节点地址和读写方式,一共 8 位。其中从节点地址 7 位,读写方式 1 位。
• 与传输地址一致的从节点器件应答(即 ACK)。
• 开始数据传输,传输数据数量不限。每个字节(八位)后面跟接收数据方的应答位。例如主节点器件读取从节点数据,从节点发送数据,主节点应答;主节点器件写数据到从节点,主节点发送数据,从节点应答。
• 数据传输结束,主节点器件发送一个终止信号结束整个过程。
采用 I²C 总线后对传送的字节数没有限制,只要求每传送一个字节后对方回应一个应答位。在发送时首先发送的是数据的最高位(MSB,Most Significant Bit)。每次传送开始有起始信号,结束时有停止信号。在总线传送完一个字节后,可以通过对时钟线(SCL)的控制使传送暂停。例如当某个外围器件接收 N 个字节数据后需要一段处理时间以便继续接收以后的字节数据,这时可在应答信号后使 SCL 变为低电平控制总线暂停。如果主节点要求总线暂停也可使时钟线保持低电平控制总线暂停。
2)总线上的时序信号
I²C 总线为同步传输总线,总线信号完全与时钟同步。I²C 总线上与数据传送有关的信号有起始信号 S、终止信号 P、应答信号 A 以及位传送信号。下面将对这些信号一一介绍。
(1)起始信号
起始信号(Start Condition)如图 3 所示。当时钟线 SCL 为高电平时,数据线 SDA 从高电平向低电平变化将形成起始信号,启动 I²C 总线。
(2)终止信号
终止信号(Stop Condition)如图 3 所示。当时钟线 SCL 为高电平时,数据线 SDA 从低电平向高电平变化将形成终止信号,停止 I²C 总线。
(3)应答信号
如图 3 所中 ACK 第 9 个时钟脉冲对应应答位,相应数据线上低电平时为应答信号,高电平时为非应答信号。
图 3 起始信号和终止信号
(4)位传送信号
在 I²C 总线启动后或应答信号后的第 1~8 个时钟脉冲对应于一个字节的 8 位数据传送。脉冲高电平期间,数据串行传送;低电平期间为数据准备,允许总线上数据电平变换。
二、模拟 I2C 接口程序的基本框架
模拟 I²C 接口程序的基本框架如图 4 所示。
图 4 模拟 I²C 接口程序的基本框架
1)程序接口
用于和应用程序连接的接口,将应用程序的数据按照 I²C 协议的方式通过 SDA 传递给外部器件。包括下列内容:
• clk_I FPGA 外部时钟信号。
• rst_I 同步重起信号。
• arst_I 异步重起信号。
• adr_I 从节点地址。
• dat_I 输入数据。
• dat_o 输出数据。
• we_I 写有效信号。
• stb_I 接口有效信号。
• cyc_I 有效总线周期输入。
• ack_o 应答信号输出。
• inta_o 中断信号输出。
2)时钟设置寄存器
I²C 协议提供了 3 种速度模式:正常速度模式 100kbit/s、快速模式 400kbit/s、高速模式3.5Mbit/s。SCL 输出的时钟信号频率和速度模式一致。程序内部使用 5 倍 SCL 信号作为时钟,而 FPGA 外部时钟需要经过分频得到程序内部使用的时钟。
例如:采用正常速度 100kbit/s,FPGA 外部时钟为 50MHz,则时钟设置寄存器需要设置为(50MHz/5*100kHz – 1=99)。
3)时钟产生模块
时钟产生模块产生 4 倍 SCL 频率的时钟信号,它为位传输控制模块中所有同步动作提供触发信号。
4)命令寄存器
命令寄存器共 8 位,它决定是否在总线上产生各种时序信号、是否读/写数据,各位表示的含义如表 1 所示。
表 1 命令寄存器内容
5)状态寄存器
状态寄存器用来显示当前总线的状态,例如是否接收到从节点的应答信号、是否忙、是否在传递数据等,具体内容如表 2 所示。
表 2 状态寄存器内容
6)数据传输寄存器
数据传输寄存器用于保存等待传输的数据。当传递从节点地址信息时,前 7 位保存从节点地址,最后一位保存读写命令;当传递普通数据时,8 位保存一个字节数据。数据传输寄存器具体内容如表 3 所示。
表 3 数据传输寄存器内容
7)数据接收寄存器
数据接收寄存器用于保存通过 I²C 总线接收到的最后一个字节内容,具体内容如表4所示。
表 4 数据接收寄存器内容
8)字节传输控制模块
字节传输控制模块以字节为单位控制 I²C 总线的数据传输。这个模块按照命令寄存器设置的内容将数据传输寄存器内容传递到 I²C 总线的接收端,或者从 I²C 总线发送端接收数据并保存到数据接收寄存器中。
9)位传输控制模块
位传输控制模块以位为单位进行 I²C 总线的数据传输和产生各个 I²C 协议命令(如开始、停止、重复开始等)。字节传输控制模块控制位传输控制模块的各种动作。例如读取一个字节数据,位传输控制模块需要执行 8 个读的命令。
10)数据移位寄存器
数据移位寄存器保存的数据总是和当前的数据传输相关的。例如在进行读操作时,主节点通过移位寄存器依次通过 SDA 获得来自 I²C 发送端的数据,完成后数据拷贝到数据接收寄存器中。在写操作时,数据传输寄存器中的数据拷贝到数据移位寄存器中,然后依次通过 SDA 将数据传输到 I²C 总线的接收端。
三、I²C 协议的具体实现
FPGA 设计一般按照从顶向下的模式进行:首先设计芯片功能,规划各个模块功能;然后按照规划实现各个模块。本篇由 3 个代码文件组成:i2c_master_bit_ctrl.v 完成位传输的功能、i2c_master_byte_ctrl.v 完成字节传输的功能、i2c_master_top.v 完成整个程序的控制功能,并提供给外部程序的接口。在 ISE 中创建一个项目,然后加入上面 3 个文件。下面依次介绍 3 个文件的内容。本篇讲解采用 Verilog HDL。
3.1 位传输的实现
i2c_master_bit_ctrl.v 完成位传输的功能。位传输的功能包括数据按位传输的实现和 I²C协议各个命令的实现两部分。
如图 5 所示开始和重复开始命令的产生包括 5 个阶段:idle 和 A、B、C、D 等。停止命令包括 4 个阶段:idle 和 A、B、C 等。读、写一个字节通过 8 次位操作完成。
图 5 位传输完成数据的传输和各个命令的实现
实现代码如下:
`include "timescale.v"
`include "i2c_master_defines.v"
//模块名称及 IO
module i2c_master_bit_ctrl(
clk, rst, nReset,
clk_cnt, ena, cmd, cmd_ack, busy, al, din, dout,
scl_i, scl_o, scl_oen, sda_i, sda_o, sda_oen
);
// 输入、输出
input clk;
input rst;
input nReset;
input ena; // 模块使能信号
input [15:0] clk_cnt; // 时钟分频系数
input [3:0] cmd;
output cmd_ack; // 命令完成应答
reg cmd_ack;
output busy; // 总线忙
reg busy;
output al; // 总线仲裁丢失
reg al;
input din;
output dout;
reg dout;
// I2C 连线
input scl_i; // I2C 时钟输入
output scl_o; // I2C 时钟输出
output scl_oen; // I2C 时钟输出使能
reg scl_oen;
input sda_i; //I2C 数据输入
output sda_o; // I2C 数据输出
output sda_oen; // I2C 数据输出使能
reg sda_oen;
// variable declarations
reg sSCL, sSDA; // 同步后的 SCL 和 SDA 输入
reg dscl_oen; // 延迟后的 scl_oen
reg sda_chk; // 检 查 后 的 SDA output (Multi-master
arbitration)
reg clk_en; // 时钟产生信号
wire slave_wait;
reg [15:0] cnt; // 时钟分频计数器
// 模块主体
// 当从节点没有准备好时,下拉 SCL 来延迟周期
// 延迟 scl_oen
always @(posedge clk)
dscl_oen <= #1 scl_oen;
assign slave_wait = dscl_oen && !sSCL;
// 产生时钟使能信号
always @(posedge clk or negedge nReset)
if(~nReset)
begin
cnt <= #1 16'h0;
clk_en <= #1 1'b1;
end
else if (rst)
begin
cnt <= #1 16'h0;
clk_en <= #1 1'b1;
end
else if ( ~|cnt || ~ena)
if (~slave_wait)
begin
cnt <= #1 clk_cnt;
clk_en <= #1 1'b1;
end
else
begin
cnt <= #1 cnt;
clk_en <= #1 1'b0;
end
else
begin
cnt <= #1 cnt - 16'h1;
clk_en <= #1 1'b0;
end
// 产生总线状态控制信号
reg dSCL, dSDA;
reg sta_condition;
reg sto_condition;
// 同步 SCL 和 SDA 输入信号,减少不稳定风险
always @(posedge clk or negedge nReset)
if (~nReset)
begin
sSCL <= #1 1'b1;
sSDA <= #1 1'b1;
dSCL <= #1 1'b1;
dSDA <= #1 1'b1;
end
else if (rst)
begin
sSCL <= #1 1'b1;
sSDA <= #1 1'b1;
dSCL <= #1 1'b1;
dSDA <= #1 1'b1;
end
else
begin
sSCL <= #1 scl_i;
sSDA <= #1 sda_i;
dSCL <= #1 sSCL;
dSDA <= #1 sSDA;
end
// SCL 处于高时检测到 SDA 的下降沿,即检测开始状态信号
// SCL 处于高时检测到 SDA 的上升沿,即检测停止状态信号
always @(posedge clk or negedge nReset)
if (~nReset)
begin
sta_condition <= #1 1'b0;
sto_condition <= #1 1'b0;
end
else if (rst)
begin
sta_condition <= #1 1'b0;
sto_condition <= #1 1'b0;
end
else
begin
sta_condition <= #1 ~sSDA & dSDA & sSCL;
sto_condition <= #1 sSDA & ~dSDA & sSCL;
end
// 产生 I2C 总线忙信号
always @(posedge clk or negedge nReset)
if(!nReset)
busy <= #1 1'b0;
else if (rst)
busy <= #1 1'b0;
else
busy <= #1 (sta_condition | busy) & ~sto_condition;
// 产生仲裁丢失信号 generate arbitration lost signal
// 仲裁丢失发生在:
// 1) 主节点驱动 SDA 处于高,但是 I2C 总线一直处于低
// 2) 没有请求时却检测到停止状态信号
reg cmd_stop, dcmd_stop;
always @(posedge clk or negedge nReset)
if (~nReset)
begin
cmd_stop <= #1 1'b0;
dcmd_stop <= #1 1'b0;
al <= #1 1'b0;
end
else if (rst)
begin
cmd_stop <= #1 1'b0;
dcmd_stop <= #1 1'b0;
al <= #1 1'b0;
end
else
begin
cmd_stop <= #1 cmd == `I2C_CMD_STOP;
dcmd_stop <= #1 cmd_stop;
al <= #1 (sda_chk & ~sSDA & sda_oen) | (sto_condition & ~dcmd_stop);
end
// 产生数据输出信号,在 SCL 信号的上升沿保存 SDA
always @(posedge clk)
if(sSCL & ~dSCL)
dout <= #1 sSDA;
// 产生状态机
// 状态译码
parameter [16:0] idle = 17'b0_0000_0000_0000_0000;
parameter [16:0] start_a = 17'b0_0000_0000_0000_0001;
parameter [16:0] start_b = 17'b0_0000_0000_0000_0010;
parameter [16:0] start_c = 17'b0_0000_0000_0000_0100;
parameter [16:0] start_d = 17'b0_0000_0000_0000_1000;
parameter [16:0] start_e = 17'b0_0000_0000_0001_0000;
parameter [16:0] stop_a = 17'b0_0000_0000_0010_0000;
parameter [16:0] stop_b = 17'b0_0000_0000_0100_0000;
parameter [16:0] stop_c = 17'b0_0000_0000_1000_0000;
parameter [16:0] stop_d = 17'b0_0000_0001_0000_0000;
parameter [16:0] rd_a = 17'b0_0000_0010_0000_0000;
parameter [16:0] rd_b = 17'b0_0000_0100_0000_0000;
parameter [16:0] rd_c = 17'b0_0000_1000_0000_0000;
parameter [16:0] rd_d = 17'b0_0001_0000_0000_0000;
parameter [16:0] wr_a = 17'b0_0010_0000_0000_0000;
parameter [16:0] wr_b = 17'b0_0100_0000_0000_0000;
parameter [16:0] wr_c = 17'b0_1000_0000_0000_0000;
parameter [16:0] wr_d = 17'b1_0000_0000_0000_0000;
reg [16:0] c_state;
//状态机
always @(posedge clk or negedge nReset)
if (!nReset)
begin
c_state <= #1 idle;
cmd_ack <= #1 1'b0;
scl_oen <= #1 1'b1;
sda_oen <= #1 1'b1;
sda_chk <= #1 1'b0;
end
else if (rst | al)
begin
c_state <= #1 idle;
cmd_ack <= #1 1'b0;
scl_oen <= #1 1'b1;
sda_oen <= #1 1'b1;
sda_chk <= #1 1'b0;
end
else
begin
cmd_ack <= #1 1'b0;
if (clk_en)
case (c_state)
// idle 状态
idle:
begin
case (cmd)
`I2C_CMD_START:
c_state <= #1 start_a;
`I2C_CMD_STOP:
c_state <= #1 stop_a;
`I2C_CMD_WRITE:
c_state <= #1 wr_a;
`I2C_CMD_READ:
c_state <= #1 rd_a;
default:
c_state <= #1 idle;
endcase
scl_oen <= #1 scl_oen; // 保持 SCL 在同一状态
sda_oen <= #1 sda_oen; // 保持 SDA 在同一状态
sda_chk <= #1 1'b0; // 不检查 SDA 输出
end
// 开始状态
start_a:
begin
c_state <= #1 start_b;
scl_oen <= #1 scl_oen; // 保持 SCL 在同一状态
sda_oen <= #1 1'b1; // 保持 SDA 处于高
sda_chk <= #1 1'b0; // 不检查 SDA 的输出
end
start_b:
begin
c_state <= #1 start_c;
scl_oen <= #1 1'b1;
sda_oen <= #1 1'b1;
sda_chk <= #1 1'b0;
end
start_c:
begin
c_state <= #1 start_d;
scl_oen <= #1 1'b1;
sda_oen <= #1 1'b0;
sda_chk <= #1 1'b0;
end
start_d:
begin
c_state <= #1 start_e;
scl_oen <= #1 1'b1;
sda_oen <= #1 1'b0;
sda_chk <= #1 1'b0;
end
start_e:
begin
c_state <= #1 idle;
cmd_ack <= #1 1'b1;
scl_oen <= #1 1'b0;
sda_oen <= #1 1'b0;
sda_chk <= #1 1'b0;
end
// 停止状态
stop_a:
begin
c_state <= #1 stop_b;
scl_oen <= #1 1'b0;
sda_oen <= #1 1'b0;
sda_chk <= #1 1'b0;
end
stop_b:
begin
c_state <= #1 stop_c;
scl_oen <= #1 1'b1;
sda_oen <= #1 1'b0;
sda_chk <= #1 1'b0;
end
stop_c:
begin
c_state <= #1 stop_d;
scl_oen <= #1 1'b1;
sda_oen <= #1 1'b0;
sda_chk <= #1 1'b0;
end
stop_d:
begin
c_state <= #1 idle;
cmd_ack <= #1 1'b1;
scl_oen <= #1 1'b1;
sda_oen <= #1 1'b1;
sda_chk <= #1 1'b0;
end
// 读状态
rd_a:
begin
c_state <= #1 rd_b;
scl_oen <= #1 1'b0; //保持 SCL 处于低
sda_oen <= #1 1'b1; // SDA 处于三态
sda_chk <= #1 1'b0; // 不检查 SDA 输出
end
rd_b:
begin
c_state <= #1 rd_c;
scl_oen <= #1 1'b1;
sda_oen <= #1 1'b1;
sda_chk <= #1 1'b0;
end
rd_c:
begin
c_state <= #1 rd_d;
scl_oen <= #1 1'b1;
sda_oen <= #1 1'b1;
sda_chk <= #1 1'b0;
end
rd_d:
begin
c_state <= #1 idle;
cmd_ack <= #1 1'b1;
scl_oen <= #1 1'b0;
sda_oen <= #1 1'b1;
sda_chk <= #1 1'b0;
end
// 写状态
wr_a:
begin
c_state <= #1 wr_b;
scl_oen <= #1 1'b0;
sda_oen <= #1 din;
sda_chk <= #1 1'b0;
end
wr_b:
begin
c_state <= #1 wr_c;
scl_oen <= #1 1'b1;
sda_oen <= #1 din;
sda_chk <= #1 1'b1;
end
wr_c:
begin
c_state <= #1 wr_d;
scl_oen <= #1 1'b1;
sda_oen <= #1 din;
sda_chk <= #1 1'b1;
end
wr_d:
begin
c_state <= #1 idle;
cmd_ack <= #1 1'b1;
scl_oen <= #1 1'b0;
sda_oen <= #1 din;
sda_chk <= #1 1'b0;
end
endcase
end
// 分配 SCL 和 SDA 输出一直处于低
assign scl_o = 1'b0;
assign sda_o = 1'
endmodule
3.2 字节传输的实现
字节传输的具体实现流程如图 6 所示。
图 6 字节传输控制模块流程图
字节传输控制模块控制以字节为单位的数据传输。它根据命令寄存器的设置将数据传输寄存器中的内容传输到外部节点,将外部节点的数据接收到数据接收寄存器中。
实现代码如下:
`include "timescale.v"
`include "i2c_master_defines.v"
//模块
module i2c_master_byte_ctrl (
clk, rst, nReset, ena, clk_cnt, start, stop, read, write, ack_in, din,
cmd_ack, ack_out, dout, i2c_busy, i2c_al, scl_i, scl_o, scl_oen, sda_i, sda_o, sda_oen );
// 输入、输出
input clk; // 主时钟
input rst; // 同步 RESET,高有效
input nReset; // 异步 RESET,低有效
input ena; // 模块使能信号
input [15:0] clk_cnt; // 4 倍 SCL 信号
// 控制信号输入
input start;
input stop;
input read;
input write;
input ack_in;
input [7:0] din;
// 状态信号输出
output cmd_ack;
reg cmd_ack;
output ack_out;
reg ack_out;
output i2c_busy;
output i2c_al;
output [7:0] dout;
// I2C 信号
input scl_i;
output scl_o;
output scl_oen;
input sda_i;
output sda_o;
output sda_oen;
// 变量申明
// 状态机
parameter [4:0] ST_IDLE = 5'b0_0000;
parameter [4:0] ST_START = 5'b0_0001;
parameter [4:0] ST_READ = 5'b0_0010;
parameter [4:0] ST_WRITE = 5'b0_0100;
parameter [4:0] ST_ACK = 5'b0_1000;
parameter [4:0] ST_STOP = 5'b1_0000;
// 位控制模块的信号
reg [3:0] core_cmd;
reg core_txd;
wire core_ack, core_rxd;
// 移位寄存器信号
reg [7:0] sr; //8 位移位寄存器
reg shift, ld;
// 状态机信号
wire go;
reg [2:0] dcnt;
wire cnt_done;
// 模块主体
// 连接位控制模块
i2c_master_bit_ctrl bit_controller (
.clk ( clk ),
.rst ( rst ),
.nReset ( nReset ),
.ena ( ena ),
.clk_cnt ( clk_cnt ),
.cmd ( core_cmd ),
.cmd_ack ( core_ack ),
.busy ( i2c_busy ),
.al ( i2c_al ),
.din ( core_txd ),
.dout ( core_rxd ),
.scl_i ( scl_i ),
.scl_o ( scl_o ),
.scl_oen ( scl_oen ),
.sda_i ( sda_i ),
.sda_o ( sda_o ),
.sda_oen ( sda_oen )
);
// 产生 GO 信号,当读/写/停止/应答时发生
assign go = (read | write | stop) & ~cmd_ack;
// 分配输出到移位寄存器
assign dout = sr;
// 产生移位寄存器
always @(posedge clk or negedge nReset)
if (!nReset)
sr <= #1 8'h0;
else if (rst)
sr <= #1 8'h0;
else if (ld)
sr <= #1 din;
else if (shift)
sr <= #1 {sr[6:0], core_rxd};
// 产生计数器
always @(posedge clk or negedge nReset)
if (!nReset)
dcnt <= #1 3'h0;
else if (rst)
dcnt <= #1 3'h0;
else if (ld)
dcnt <= #1 3'h7;
else if (shift)
dcnt <= #1 dcnt - 3'h1;
assign cnt_done = ~(|dcnt);
// 状态机
reg [4:0] c_state;
always @(posedge clk or negedge nReset)
if (!nReset)
begin
core_cmd <= #1 `I2C_CMD_NOP;
core_txd <= #1 1'b0;
shift <= #1 1'b0;
ld <= #1 1'b0;
cmd_ack <= #1 1'b0;
c_state <= #1 ST_IDLE;
ack_out <= #1 1'b0;
end
else if (rst | i2c_al)
begin
core_cmd <= #1 `I2C_CMD_NOP;
core_txd <= #1 1'b0;
shift <= #1 1'b0;
ld <= #1 1'b0;
cmd_ack <= #1 1'b0;
c_state <= #1 ST_IDLE;
ack_out <= #1 1'b0;
end
else
begin
// 初始化所有信号
core_txd <= #1 sr[7];
shift <= #1 1'b0;
ld <= #1 1'b0;
cmd_ack <= #1 1'b0;
case (c_state)
//IDLE 状态
ST_IDLE:
if (go)
begin
if (start)
begin
c_state <= #1 ST_START;
core_cmd <= #1 `I2C_CMD_START;
end
else if (read)
begin
c_state <= #1 ST_READ;
core_cmd <= #1 `I2C_CMD_READ;
end
else if (write)
begin
c_state <= #1 ST_WRITE;
core_cmd <= #1 `I2C_CMD_WRITE;
end
else // 缺省的是 stop 状态
begin
c_state <= #1 ST_STOP;
core_cmd <= #1 `I2C_CMD_STOP;
// 产生应答信号
cmd_ack <= #1 1'b1;
end
ld <= #1 1'b1;
end
//开始状态
ST_START:
if (core_ack)
begin
if (read)
begin
c_state <= #1 ST_READ;
core_cmd <= #1 `I2C_CMD_READ;
end
else
begin
c_state <= #1 ST_WRITE;
core_cmd <= #1 `I2C_CMD_WRITE;
end
ld <= #1 1'b1;
end
//写数据状态
ST_WRITE:
if (core_ack)
if (cnt_done)
begin
c_state <= #1 ST_ACK;
core_cmd <= #1 `I2C_CMD_READ;
end
else
begin
c_state <= #1 ST_WRITE; // 保持在原来状态
core_cmd <= #1 `I2C_CMD_WRITE; // 写下一位数据
shift <= #1 1'b1;
end
//读信号状态
ST_READ:
if (core_ack)
begin
if (cnt_done)
begin
c_state <= #1 ST_ACK;
core_cmd <= #1 `I2C_CMD_WRITE;
end
else
begin
c_state <= #1 ST_READ; // 保留在原来状态
core_cmd <= #1 `I2C_CMD_READ; // 读下一位数据
end
shift <= #1 1'b1;
core_txd <= #1 ack_in;
end
//应答数据状态
ST_ACK:
if (core_ack)
begin
if (stop)
begin
c_state <= #1 ST_STOP;
core_cmd <= #1 `I2C_CMD_STOP;
end
else
begin
c_state <= #1 ST_IDLE;
core_cmd <= #1 `I2C_CMD_NOP;
end
// 把应答信号输出连接到位控制模块
ack_out <= #1 core_rxd;
// 产生应答信号
cmd_ack <= #1 1'b1;
core_txd <= #1 1'b1;
end
else
core_txd <= #1 ack_in;
//停止状态
ST_STOP:
if (core_ack)
begin
c_state <= #1 ST_IDLE;
core_cmd <= #1 `I2C_CMD_NOP;
end
endcase
end
endmodule
3.3 程序主体的实现
程序主体部分完成与外部程序的接口、与总线上外部节点的连线、完成程序内部各个寄存器的构建、控制字节传输控制模块等功能。代码如下:
`include "timescale.v"
`include "i2c_master_defines.v"
//模块定义
module i2c_master_top(
wb_clk_i, wb_rst_i, arst_i, wb_adr_i, wb_dat_i, wb_dat_o,
wb_we_i, wb_stb_i, wb_cyc_i, wb_ack_o, wb_inta_o,
scl_pad_i, scl_pad_o, scl_padoen_o, sda_pad_i, sda_pad_o, sda_padoen_o );
// 参数
parameter ARST_LVL = 1'b0; // 异步 reset 信号
// 输入、输出信号
// 连接到外部接口的信号
input clk_i; // 主节点时钟信号
input rst_i; // 同步 reset 信号,高有效
input arst_i; // 异步 reset 信号
input [2:0] adr_i; // 低位地址信号
input [7:0] dat_i; // 数据总线输入
output [7:0] dat_o; // 数据总线输出
input we_i; // 输入使能信号
input stb_i; // 触发信号
input cyc_i; // 总线周期输入
output ack_o; // 应答信号输出
output inta_o; // 中断请求信号输出
reg [7:0] wb_dat_o;
reg wb_ack_o;
reg wb_inta_o;
// I2C 信号
// I2C 时钟信号线
input scl_pad_i; // SCL 输入
output scl_pad_o; // SCL 输出
output scl_padoen_o; // SCL 输出使能
// I2C 数据线
input sda_pad_i; // SDA 输入
output sda_pad_o; // SDA 输出
output sda_padoen_o; // SDA 输出使能
// 变量申明
// 寄存器
reg [15:0] prer; // 时钟分频寄存器
reg [ 7:0] ctr; // 控制寄存器
reg [ 7:0] txr; // 数据传输寄存器
wire [ 7:0] rxr; // 数据接收寄存器
reg [ 7:0] cr; // 命令寄存器
wire [ 7:0] sr; // 状态寄存器
// 完成信号,命令完成后清除命令寄存器
wire done;
// 模块使能信号
wire core_en;
wire ien;
// 状态寄存器信号
wire irxack;
reg rxack; // 从从节点接收应答信号
reg tip; // 传输进行标志
reg irq_flag; // 中断挂起标志
wire i2c_busy; // 总线忙标志
wire i2c_al; // 总线仲裁丢失
reg al; // 状态寄存器仲裁丢失位
// 模块主体
// 产生内部 reset
wire rst_i = arst_i ^ ARST_LVL;
wire wacc = cyc_i & stb_i & we_i;
// 产生应答输出信号
always @(posedge clk_i)
wb_ack_o <= #1 cyc_i & stb_i & ~ack_o;
// 数据输出
always @(posedge clk_i)
begin
case (adr_i)
3'b000: wb_dat_o = prer[ 7:0];
3'b001: wb_dat_o = prer[15:8];
3'b010: wb_dat_o = ctr;
3'b011: wb_dat_o = rxr; // 写数据传输寄存器
3'b100: wb_dat_o = sr; // 写命令寄存器
3'b101: wb_dat_o = txr;
3'b110: wb_dat_o = cr;
3'b111: wb_dat_o = 0; // 保留位
endcase
end
// 产生寄存器
always @(posedge wb_clk_i or negedge rst_i)
if (!rst_i)
begin
prer <= #1 16'hffff;
ctr <= #1 8'h0;
txr <= #1 8'h0;
end
else if (wb_rst_i)
begin
prer <= #1 16'hffff;
ctr <= #1 8'h0;
txr <= #1 8'h0;
end
else
if (wb_wacc)
case (wb_adr_i) // synopsis full_case parallel_case
3'b000 : prer [ 7:0] <= #1 wb_dat_i;
3'b001 : prer [15:8] <= #1 wb_dat_i;
3'b010 : ctr <= #1 wb_dat_i;
3'b011 : txr <= #1 wb_dat_i;
endcase
// 产生命令寄存器
always @(posedge wb_clk_i or negedge rst_i)
if (~rst_i)
cr <= #1 8'h0;
else if (wb_rst_i)
cr <= #1 8'h0;
else if (wb_wacc)
begin
if (core_en & (wb_adr_i == 3'b100) )
cr <= #1 wb_dat_i;
end
else
begin
if (done | i2c_al)
cr[7:4] <= #1 4'h0; // 命令完成或者仲裁丢失时清除命令寄存器内容
cr[2:1] <= #1 2'b0; // 保留位
cr[0] <= #1 2'b0; // 清除 IRQ_ACK 位
end
// 译码命令寄存器
wire sta = cr[7];
wire sto = cr[6];
wire rd = cr[5];
wire wr = cr[4];
wire ack = cr[3];
wire iack = cr[0];
// 译码控制寄存器
assign core_en = ctr[7];
assign ien = ctr[6];
// 连接字节控制模块
i2c_master_byte_ctrl byte_controller (
.clk ( wb_clk_i ),
.rst ( wb_rst_i ),
.nReset ( rst_i ),
.ena ( core_en ),
.clk_cnt ( prer ),
.start ( sta ),
.stop ( sto ),
.read ( rd ),
.write ( wr ),
.ack_in ( ack ),
.din ( txr ),
.cmd_ack ( done ),
.ack_out ( irxack ),
.dout ( rxr ),
.i2c_busy ( i2c_busy ),
.i2c_al ( i2c_al ),
.scl_i ( scl_pad_i ),
.scl_o ( scl_pad_o ),
.scl_oen ( scl_padoen_o ),
.sda_i ( sda_pad_i ),
.sda_o ( sda_pad_o ),
.sda_oen ( sda_padoen_o )
)
// 状态寄存器部分和中断请求信号
always @(posedge wb_clk_i or negedge rst_i)
if (!rst_i)
begin
al <= #1 1'b0;
rxack <= #1 1'b0;
tip <= #1 1'b0;
irq_flag <= #1 1'b0;
end
else if (wb_rst_i)
begin
al <= #1 1'b0;
rxack <= #1 1'b0;
tip <= #1 1'b0;
irq_flag <= #1 1'b0;
end
else
begin
al <= #1 i2c_al | (al & ~sta);
rxack <= #1 irxack;
tip <= #1 (rd | wr);
irq_flag <= #1 (done | i2c_al | irq_flag) & ~iack;
end
// 中断请求标志
// 产生中断请求信号
always @(posedge wb_clk_i or negedge rst_i)
if (!rst_i)
wb_inta_o <= #1 1'b0;
else if (wb_rst_i)
wb_inta_o <= #1 1'b0;
else
wb_inta_o <= #1 irq_flag && ien; //中断使能位 IEN 设置后产生中断信号
assign sr[7] = rxack;
assign sr[6] = i2c_busy;
assign sr[5] = al;
assign sr[4:2] = 3'h0; // reserved
assign sr[1] = tip;
assign sr[0] = irq_flag;
endmodule
四、程序的仿真与测试
I²C 协议的模拟程序完成后,还需要通过仿真程序对程序的功能进行测试。对本程序的仿真包括 3 个部分:第一部分是主节点的仿真,模拟数据读/写;第二部分是从节点的仿真,模拟数据的接收和应答;第三部分是仿真主程序,负责整个仿真过程的控制。
4.1 主节点的仿真
主节点仿真的内容包括读数据、写数据和比较数据 3 部分,代码如下:
`include "timescale.v"
//模块定义
module wb_master_model(clk, rst, adr, din, dout, cyc, stb, we, sel, ack, err, rty);
//参数
parameter dwidth = 32;
parameter awidth = 32;
//输入、输出
input clk, rst;
output [awidth -1:0] adr;
input [dwidth -1:0] din;
output [dwidth -1:0] dout;
output cyc, stb;
output we;
output [dwidth/8 -1:0] sel;
input ack, err, rty;
//WIRE 定义
reg [awidth -1:0] adr;
reg [dwidth -1:0] dout;
reg cyc, stb;
reg we;
reg [dwidth/8 -1:0] sel;
reg [dwidth -1:0] q;
// 存储逻辑
//初始化
initial
begin
adr = {awidth{1'bx}};
dout = {dwidth{1'bx}};
cyc = 1'b0;
stb = 1'bx;
we = 1'hx;
sel = {dwidth/8{1'bx}};
#1;
end
// 写数据周期
task wb_write;
input delay;
integer delay;
input [awidth -1:0] a;
input [dwidth -1:0] d;
begin
// 延迟
repeat(delay) @(posedge clk);
// 设置信号值
#1;
adr = a;
dout = d;
cyc = 1'b1;
stb = 1'b1;
we = 1'b1;
sel = {dwidth/8{1'b1}};
@(posedge clk);
// 等待从节点的应答信号
while(~ack) @(posedge clk);
#1;
cyc = 1'b0;
stb = 1'bx;
adr = {awidth{1'bx}};
dout = {dwidth{1'bx}};
we = 1'hx;
sel = {dwidth/8{1'bx}};
end
endtask
// 读数据周期
task wb_read;
input delay;
integer delay;
input [awidth -1:0]a;
output [dwidth -1:0] d;
begin
// 延迟
repeat(delay) @(posedge clk);
// 设置信号值
#1;
adr = a;
dout = {dwidth{1'bx}};
cyc = 1'b1;
stb = 1'b1;
we = 1'b0;
sel = {dwidth/8{1'b1}};
@(posedge clk);
// 等待从节点应答信号
while(~ack) @(posedge clk);
#1;
cyc = 1'b0;
stb = 1'bx;
adr = {awidth{1'bx}};
dout = {dwidth{1'bx}};
we = 1'hx;
sel = {dwidth/8{1'bx}};
d = din;
end
endtask
// 比较数据
task wb_cmp;
input delay;
integer delay;
input [awidth -1:0] a;
input [dwidth -1:0] d_exp;
begin
wb_read (delay, a, q);
if (d_exp !== q)
$display("Data compare error. Received %h, expected %h at time %t", q, d_exp,$time);
end
endtask
endmodule
4.2 从节点的仿真
从节点仿真程序需要模拟从主节点接收数据,并发出应答信号,代码如下:
`include "timescale.v"
//模块定义
module i2c_slave_model (scl, sda);
// 参数
// 地址
parameter I2C_ADR = 7'b001_0000;
// 输入、输出
input scl;
inout sda;
// 变量申明
wire debug = 1'b1;
reg [7:0] mem [3:0]; // 初始化内存
reg [7:0] mem_adr; // 内存地址
reg [7:0] mem_do; // 内存数据输出
reg sta, d_sta;
reg sto, d_sto;
reg [7:0] sr; // 8 位移位寄存器
reg rw; // 读写方向
wire my_adr; // 地址
wire i2c_reset; // RESET 信号
reg [2:0] bit_cnt;
wire acc_done; // 传输完成
reg ld;
reg sda_o;
wire sda_dly;
// 状态机的状态定义
parameter idle = 3'b000;
parameter slave_ack = 3'b001;
parameter get_mem_adr = 3'b010;
parameter gma_ack = 3'b011;
parameter data = 3'b100;
parameter data_ack = 3'b101;
reg [2:0] state;
// 模块主体
//初始化
initial
begin
sda_o = 1'b1;
state = idle;
end
// 产生移位寄存器
always @(posedge scl)
sr <= #1 {sr[6:0],sda};
//检测到访问地址与从节点一致
assign my_adr = (sr[7:1] == I2C_ADR);
//产生位寄存器
always @(posedge scl)
if(ld)
bit_cnt <= #1 3'b111;
else
bit_cnt <= #1 bit_cnt - 3'h1;
//产生访问结束标志
assign acc_done = !(|bit_cnt);
// sda 延迟
assign #1 sda_dly = sda;
//检测到开始状态
always @(negedge sda)
if(scl)
begin
sta <= #1 1'b1;
if(debug)
$display("DEBUG i2c_slave; start condition detected at %t", $time);
end
else
sta <= #1 1'b0;
always @(posedge scl)
d_sta <= #1 sta;
// 检测到停止状态信号
always @(posedge sda)
if(scl)
begin
sto <= #1 1'b1;
if(debug)
$display("DEBUG i2c_slave; stop condition detected at %t", $time);
end
else
sto <= #1 1'b0;
//产生 I2C 的 RESET 信号
assign i2c_reset = sta || sto;
// 状态机
always @(negedge scl or posedge sto)
if (sto || (sta && !d_sta) )
begin
state <= #1 idle; // reset 状态机
sda_o <= #1 1'b1;
ld <= #1 1'b1;
end
else
begin
// 初始化
sda_o <= #1 1'b1;
ld <= #1 1'b0;
case(state)
idle: // idle 状态
if (acc_done && my_adr)
begin
state <= #1 slave_ack;
rw <= #1 sr[0];
sda_o <= #1 1'b0; // 产生应答信号
#2;
if(debug && rw)
$display("DEBUG i2c_slave; command byte received (read) at %t",$time);
if(debug && !rw)
$display("DEBUG i2c_slave; command byte received (write) at %t",$time);
if(rw)
begin
mem_do <= #1 mem[mem_adr];
if(debug)
begin
#2 $display("DEBUG i2c_slave; data block read %x from address %x (1)", mem_do, mem_adr);
#2 $display("DEBUG i2c_slave; memcheck [0]=%x, [1]=%x, [2]=%x", mem[4'h0], mem[4'h1], mem[4'h2]);
end
end
end
slave_ack:
begin
if(rw)
begin
state <= #1 data;
sda_o <= #1 mem_do[7];
end
else
state <= #1 get_mem_adr;
ld <= #1 1'b1;
end
get_mem_adr: // 等待内存地址
if(acc_done)
begin
state <= #1 gma_ack;
mem_adr <= #1 sr; // 保存内存地址
sda_o <= #1 !(sr <= 15); // 收到合法地址信号后发出应答信号
if(debug)
#1 $display("DEBUG i2c_slave; address received. adr=%x, ack=%b",sr, sda_o);
end
gma_ack:
begin
state <= #1 data;
ld <= #1 1'b1;
end
data: // 接收数据
begin
if(rw)
sda_o <= #1 mem_do[7];
if(acc_done)
begin
state <= #1 data_ack;
mem_adr <= #2 mem_adr + 8'h1;
sda_o <= #1 (rw && (mem_adr <= 15) );
if(rw)
begin
#3 mem_do <= mem[mem_adr];
if(debug)
#5 $display("DEBUG i2c_slave; data block read %x from address %x (2)", mem_do, mem_adr);
end
if(!rw)
begin
mem[ mem_adr[3:0] ] <= #1 sr; // store data in memory
if(debug)
#2 $display("DEBUG i2c_slave; data block write %x to address %x", sr, mem_adr);
end
end
end
data_ack:
begin
ld <= #1 1'b1;
if(rw)
if(sda) //
begin
state <= #1 idle;
sda_o <= #1 1'b1;
end
else
begin
state <= #1 data;
sda_o <= #1 mem_do[7];
end
else
begin
state <= #1 data;
sda_o <= #1 1'b1;
end
end
endcase
end
// 从内存读数据
always @(posedge scl)
if(!acc_done && rw)
mem_do <= #1 {mem_do[6:0], 1'b1};
// 产生三态
assign sda = sda_o ? 1'bz : 1'b0;
// 检查时序
wire tst_sto = sto;
wire tst_sta = sta;
wire tst_scl = scl;
//指定各个信号的上升沿和下降沿
specify
specparam normal_scl_low = 4700,
normal_scl_high = 4000,
normal_tsu_sta = 4700,
normal_tsu_sto = 4000,
normal_sta_sto = 4700,
fast_scl_low = 1300,
fast_scl_high = 600,
fast_tsu_sta = 1300,
fast_tsu_sto = 600,
fast_sta_sto = 1300;
$width(negedge scl, normal_scl_low);
$width(posedge scl, normal_scl_high);
$setup(negedge sda &&& scl, negedge scl, normal_tsu_sta); // 开始状态信号
$setup(posedge scl, posedge sda &&& scl, normal_tsu_sto); // 停止状态信号
$setup(posedge tst_sta, posedge tst_scl, normal_sta_sto);
endspecify
endmodule
4.3 仿真主程序
仿真主程序完成主节点数据到从节点的控制,代码如下:
`include "timescale.v"
//模块定义
module tst_bench_top();
//连线和寄存器
reg clk;
reg rstn;
wire [31:0] adr;
wire [ 7:0] dat_i, dat_o;
wire we;
wire stb;
wire cyc;
wire ack;
wire inta;
//q 保存状态寄存器内容
reg [7:0] q, qq;
wire scl, scl_o, scl_oen;
wire sda, sda_o, sda_oen;
//寄存器地址
parameter PRER_LO = 3'b000; //分频寄存器低位地址
parameter PRER_HI = 3'b001; //高位地址
parameter CTR = 3'b010; //控制寄存器地址,(7)使能位|6 中断使能位|5-0其余保留位
parameter RXR = 3'b011; //接收寄存器地址,(7)接收到的最后一个字节的数据
parameter TXR = 3'b011; //传输寄存器地址,(7)传输地址时最后一位为读写位,1 为读
parameter CR = 3'b100; //命令寄存器地址,
//(7)开始|6 结束|5 读|4 写|3 应答(作为接收方时,发送应答信号,“0”为应答,“1”为不应答)|2 保留位|1 保留位|0 中断应答位,这八位自动清除
parameter SR = 3'b100; //状态寄存器地址,(7)接收应答位(“0”为接收到应答)|6 忙位(产生开始信号后变为 1,结束信号后变为 0)|5 仲裁位|4-2 保留位|1 传输中位(1 表示正在传输数据,0 表示传输结束)|中断标志位
parameter TXR_R = 3'b101;
parameter CR_R = 3'b110;
// 产生时钟信号,一个时间单位为 1ns,周期为 10ns,频率为 100MHz。
always #5 clk = ~clk;
//连接 master 模拟模块
wb_master_model #(8, 32) u0 (
.clk(clk), //时钟
.rst(rstn), //重起
.adr(adr), //地址
.din(dat_i), //输入的数据
.dout(dat_o), //输出的数据
.cyc(cyc),
.stb(stb),
.we(we),
.sel(),
.ack(ack), //应答
.err(1'b0),
.rty(1'b0)
);
//连接 i2c 接口
i2c_master_top i2c_top (
//连接到 master 模拟模块部分
.wb_clk_i(clk), //时钟
.wb_rst_i(1'b0), //同步重起位
.arst_i(rstn), //异步重起
.wb_adr_i(adr[2:0]), //地址输入
.wb_dat_i(dat_o), //数据输入接口
.wb_dat_o(dat_i), //数据从接口输出
.wb_we_i(we), //写使能信号
.wb_stb_i(stb), //片选信号,应该一直为高
.wb_cyc_i(cyc),
.wb_ack_o(ack), //应答信号输出到 master 模拟模块
.wb_inta_o(inta), //中断信号输出到 master 模拟模块
//输出的 i2c 信号,连接到 slave 模拟模块
.scl_pad_i(scl),
.scl_pad_o(scl_o),
.scl_padoen_o(scl_oen),
.sda_pad_i(sda),
.sda_pad_o(sda_o),
.sda_padoen_o(sda_oen)
);
//连接到 slave 模拟模块
i2c_slave_model #(7'b1010_000) i2c_slave (
.scl(scl),
.sda(sda)
);
//为 master 模拟模块产生 scl 和 sda 的三态缓冲
assign scl = scl_oen ? 1'bz : scl_o; // create tri-state buffer for i2c_master scl line
assign sda = sda_oen ? 1'bz : sda_o; // create tri-state buffer for i2c_master sda line
//上拉
pullup p1(scl); // pullup scl line
pullup p2(sda); // pullup sda line
//初始化
initial
begin
$display("n 状态: %t I2C 接口测试开始!nn", $time);
// 初始值
clk = 0;
//重起系统
rstn = 1'b1; // negate reset
#2;
rstn = 1'b0; // assert reset
repeat(20) @(posedge clk);
rstn = 1'b1; // negate reset
$display("状态: %t 完成系统重起!", $time);
@(posedge clk);
// 对接口编程
// 写内部寄存器
// 分频 100M/100K*5=O'200=h'C8
u0.wb_write(1, PRER_LO, 8'hc7);
u0.wb_write(1, PRER_HI, 8'h00);
$display("状态: %t 完成分频寄存器操作!", $time);
//读分频寄存器内容
u0.wb_cmp(0, PRER_LO, 8'hc8);
u0.wb_cmp(0, PRER_HI, 8'h00);
$display("状态: %t 完成分频寄存器确认操作!", $time);
//接口使能
u0.wb_write(1, CTR, 8'h80);
$display("状态: %t 完成接口使能!", $time);
// 驱动 slave 地址
// h'a0=b'1010_0000,地址+写状态,写入的地址为 h'50
u0.wb_write(1, TXR, 8'ha0);
//命令内容为 b'1001_0000,产生开始位,并设置写状态
u0.wb_write(0, CR, 8'h90);
$display("状态: %t 产生开始位, 然后写命令 a0(地址+写),命令开始!", $time);
// 检查状态位信息
// 检查传输是否结束
u0.wb_read(1, SR, q);
while(q[1])
u0.wb_read(0, SR, q);
$display("状态: %t 地址驱动写操作完成!", $time);
// 待写的地址为 h'01
u0.wb_write(1, TXR, 8'h01);
// 产生写命令 b'0001_0000
u0.wb_write(0, CR, 8'h10);
$display("状态: %t 待写地址为 01,命令开始!", $time);
// 检查状态位
u0.wb_read(1, SR, q);
while(q[1])
u0.wb_read(0, SR, q);
$display("状态: %t 写操作完成!", $time);
// 写入内容
u0.wb_write(1, TXR, 8'ha5);
u0.wb_write(0, CR, 8'h10);
$display("状态: %t 写入内容为 a5,开始写入过程!", $time);
u0.wb_read(1, SR, q);
while(q[1])
u0.wb_read(1, SR, q);
$display("状态: %t 写 a5 到地址 h'01 中完成!", $time);
// 写入下一个地址 5a
u0.wb_write(1, TXR, 8'h5a); // present data
// 写入并停止
u0.wb_write(0, CR, 8'h50); // set command (stop, write)
$display("状态: %t 写 5a 到下一个地址,产生停止位!", $time);
u0.wb_read(1, SR, q);
while(q[1])
u0.wb_read(1, SR, q); // poll it until it is zero
$display("状态: %t 写第二个地址结束!", $time);
// 读
// 驱动 slave 地址
u0.wb_write(1, TXR, 8'ha0);
u0.wb_write(0, CR, 8'h90);
$display("状态: %t 产生开始位,写命令 a0 (slave 地址+write)", $time);
u0.wb_read(1, SR, q);
while(q[1])
u0.wb_read(1, SR, q); // poll it until it is zero
$display("状态: %t slave 地址驱动完成!", $time);
// 发送地址
u0.wb_write(1, TXR, 8'h01);
u0.wb_write(0, CR, 8'h10);
$display("状态: %t 发送地址 01!", $time);
u0.wb_read(1, SR, q);
while(q[1])
u0.wb_read(1, SR, q);
$display("状态: %t 地址发送完成!", $time);
// 驱动 slave 地址,1010_0001,h'50+read
u0.wb_write(1, TXR, 8'ha1);
u0.wb_write(0, CR, 8'h90);
$display("状态: %t 产生重复开始位, 读地址+开始位", $time);
u0.wb_read(1, SR, q);
while(q[1])
u0.wb_read(1, SR, q);
$display("状态: %t 命令结束!", $time);
// 读数据
u0.wb_write(1, CR, 8'h20);
$display("状态: %t 读+应答命令", $time);
u0.wb_read(1, SR, q);
while(q[1])
u0.wb_read(1, SR, q);
$display("状态: %t 读结束!", $time);
// 检查读的内容
u0.wb_read(1, RXR, qq);
if(qq !== 8'ha5)
$display("n 错误: 需要的是 a5, received %x at time %t", qq, $time);
// 读下一个地址内容
u0.wb_write(1, CR, 8'h20);
$display("状态: %t 读+ 应答", $time);
u0.wb_read(1, SR, q);
while(q[1])
u0.wb_read(1, SR, q);
$display("状态: %t 第二个地址读结束!", $time);
u0.wb_read(1, RXR, qq);
if(qq !== 8'h5a)
$display("n 错误: 需要的是 5a, received %x at time %t", qq, $time);
// 读
u0.wb_write(1, CR, 8'h20);
$display("状态: %t 读 + 应答", $time);
u0.wb_read(1, SR, q);
while(q[1])
u0.wb_read(1, SR, q);
$display("状态: %t 第三个地址读完成!", $time);
u0.wb_read(1, RXR, qq);
$display("状态: %t 第三个地址内容是 %x !", $time, qq);
// 读
u0.wb_write(1, CR, 8'h28);
$display("状态: %t 读 + 不应答!", $time);
u0.wb_read(1, SR, q);
while(q[1])
u0.wb_read(1, SR, q);
$display("状态: %t 第四个地址读完成!", $time);
u0.wb_read(1, RXR, qq);
$display("状态: %t 第四个地址内容为 %x !", $time, qq);
// 检查不存在的 slave 地址
// drive slave address
u0.wb_write(1, TXR, 8'ha0);
u0.wb_write(0, CR, 8'h90);
$display("状态: %t 产生开始位, 发送命令 a0 (slave 地址+写). 检查非法地址!",$time);
u0.wb_read(1, SR, q);
while(q[1])
u0.wb_read(1, SR, q); // poll it until it is zero
$display("状态: %t 命令结束!", $time);
// 发送内存地址
u0.wb_write(1, TXR, 8'h10);
u0.wb_write(0, CR, 8'h10);
$display("状态: %t 发送 slave 内存地址 10!", $time);
u0.wb_read(1, SR, q);
while(q[1])
u0.wb_read(1, SR, q);
$display("状态: %t 地址发送完毕!", $time);
// slave 发送不应答
$display("状态: %t 检查不应答位!", $time);
if(!q[7])
$display("n 错误: 需要 NACK, 接收到 ACKn");
// 从 slave 读数据
u0.wb_write(1, CR, 8'h40);
$display("状态: %t 产生'stop'位", $time);
u0.wb_read(1, SR, q);
while(q[1])
u0.wb_read(1, SR, q); // poll it until it is zero
$display("状态: %t 结束!", $time);
#25000; // wait 25us
$display("nn 状态: %t 测试结束!", $time);
$finish;
end
endmodule
4.4 仿真结果
在 ModelSim 中可以看到仿真的结果。如图 7 所示是发送开始状态并写地址“a0”时的图形,此时在图上表示为 SCL 处于高时 SDA 的一个下降沿,然后是数据“1010,0000”。
图 7 发送开始信号并写地址 a0
如图 8 所示为发送数据“01”和“a5”时的图形,在图上表示为:数据“0000,0001”和“1010,0101”。
图 8 发送数据“01”和“a5”
如图 9 所示的是发送停止状态信号和数据“5a”时的图形,在图上表示为 SCL 处于高时SDA 的一个上升沿,然后是数据“0101,1010”。
图 9 发送停止状态信号和数据“5a”
仿真程序说明 I²C 程序符合 I²C 协议的时序和数据格式,可以实现模拟 I²C 协议的任务。
五、总结
本篇首先说明了 I²C 协议相关的内容,介绍协议基本概念和数据传输各个命令的具体含义以及协议对时序的要求。接下来介绍模拟 I²C 协议程序的框架,详细讲解框架中各个模块的功能并介绍详细代码。最后通过一个完成的仿真程序完成对程序的测试。I²C 在应用中有着广泛的用途,本篇希望通过这个例子为各位大侠提供一个可行的解决方案。