今天给大侠带来基于FPGA的 模拟 I²C 协议设计,由于篇幅较长,分三篇。今天带来第三篇,下篇,程序的仿真与测试。话不多说,上货。
之前也有相关文章介绍,这里超链接一下,仅供各位大侠参考。
源码系列:基于FPGA的 IIC 设计(附源工程)
导读
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; // 存储逻辑 //初始化 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 endtaskendmodule
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 接口测试开始!\n\n", $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, 接收到 ACK\n"); // 从 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("\n\n 状态: %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 在应用中有着广泛的用途,本篇希望通过这个例子为各位大侠提供一个可行的解决方案。
本篇到此结束,各位大侠,有缘再见!
END
后续会持续更新,带来Vivado、 ISE、Quartus II 、candence等安装相关设计教程,学习资源、项目资源、好文推荐等,希望大侠持续关注。
大侠们,江湖偌大,继续闯荡,愿一切安好,有缘再见!
精彩推荐
VHDL语法学习笔记:一文掌握VHDL语法
基于FPGA的数字视频信号处理器设计(下)
基于FPGA的CAN总线控制器的设计(下)
FPGA工程师就业班,线上直播课开启!