大侠好,欢迎来到FPGA技术江湖,江湖偌大,相见即是缘分。大侠可以关注FPGA技术江湖,在“闯荡江湖”、”行侠仗义”栏里获取其他感兴趣的资源,或者一起煮酒言欢。
今天给大侠带来基于FPGA的 模拟 I²C 协议设计,由于篇幅较长,分三篇。今天带来第三篇,下篇,程序的仿真与测试。话不多说,上货。
之前也有相关文章介绍,这里超链接一下,仅供各位大侠参考。
源码系列:基于FPGA的 IIC 设计(附源工程)
这里也给出前两篇的超链接:
基于 FPGA 的模拟 I²C协议设计(上)
基于 FPGA 的模拟 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 协议的模拟程序完成后,还需要通过仿真程序对程序的功能进行测试。对本程序的仿真包括 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;// 存储逻辑//初始化initialbeginadr = {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}};endendtask// 读数据周期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;endendtask// 比较数据task wb_cmp;input delay;integer delay;input [awidth -1:0] a;input [dwidth -1:0] d_exp;beginwb_read (delay, a, q);if (d_exp !== q)$display("Data compare error. Received %h, expected %h at time %t", q, d_exp,$time);endendtaskendmodule
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;// 模块主体//初始化initialbeginsda_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;elsebit_cnt <= #1 bit_cnt - 3'h1;//产生访问结束标志assign acc_done = !(|bit_cnt);// sda 延迟assign #1 sda_dly = sda;//检测到开始状态always @(negedge sda)if(scl)beginsta <= #1 1'b1;if(debug)$display("DEBUG i2c_slave; start condition detected at %t", $time);endelsesta <= #1 1'b0;always @(posedge scl)d_sta <= #1 sta;// 检测到停止状态信号always @(posedge sda)if(scl)beginsto <= #1 1'b1;if(debug)$display("DEBUG i2c_slave; stop condition detected at %t", $time);endelsesto <= #1 1'b0;//产生 I2C 的 RESET 信号assign i2c_reset = sta || sto;// 状态机always @(negedge scl or posedge sto)if (sto || (sta && !d_sta) )beginstate <= #1 idle; // reset 状态机sda_o <= #1 1'b1;ld <= #1 1'b1;endelsebegin// 初始化sda_o <= #1 1'b1;ld <= #1 1'b0;case(state)idle: // idle 状态if (acc_done && my_adr)beginstate <= #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)beginmem_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]);endendendslave_ack:beginif(rw)beginstate <= #1 data;sda_o <= #1 mem_do[7];endelsestate <= #1 get_mem_adr;ld <= #1 1'b1;endget_mem_adr: // 等待内存地址if(acc_done)beginstate <= #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);endgma_ack:beginstate <= #1 data;ld <= #1 1'b1;enddata: // 接收数据beginif(rw)sda_o <= #1 mem_do[7];if(acc_done)beginstate <= #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);endif(!rw)beginmem[ mem_adr[3:0] ] <= #1 sr; // store data in memoryif(debug)#2 $display("DEBUG i2c_slave; data block write %x to address %x", sr, mem_adr);endendenddata_ack:beginld <= #1 1'b1;if(rw)if(sda) //beginstate <= #1 idle;sda_o <= #1 1'b1;endelsebeginstate <= #1 data;sda_o <= #1 mem_do[7];endelsebeginstate <= #1 data;sda_o <= #1 1'b1;endendendcaseend// 从内存读数据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;//指定各个信号的上升沿和下降沿specifyspecparam 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);endspecifyendmodule
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 lineassign sda = sda_oen ? 1'bz : sda_o; // create tri-state buffer for i2c_master sda line//上拉pullup p1(scl); // pullup scl linepullup p2(sda); // pullup sda line//初始化initialbegin$display("n 状态: %t I2C 接口测试开始!nn", $time);// 初始值clk = 0;//重起系统rstn = 1'b1; // negate reset#2;rstn = 1'b0; // assert resetrepeat(20) @(posedge clk);rstn = 1'b1; // negate reset$display("状态: %t 完成系统重起!", $time);@(posedge clk);// 对接口编程// 写内部寄存器// 分频 100M/100K*5=O'200=h'C8u0.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'50u0.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'01u0.wb_write(1, TXR, 8'h01);// 产生写命令 b'0001_0000u0.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);// 写入下一个地址 5au0.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+readu0.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 addressu0.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;endendmodule
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 在应用中有着广泛的用途,本篇希望通过这个例子为各位大侠提供一个可行的解决方案。
本篇到此结束,各位大侠,有缘再见!