導讀
I2C(Inter-Integrated Circuit),其實是I2CBus 簡稱,中文就是集成電路總線,它是一種串行通信總線,使用多主從架構,由飛利浦公司在1980年代為了讓主板、嵌入式系統或手機用以連接低速周邊設備而發展。I2C的正確讀法為“I平方C”("I-squared-C"),而“I二C”("I-two-C")則是另一種錯誤但被廣泛使用的讀法。自2006年10月1日起,使用 I2C 協議已經不需要支付專利費,但制造商仍然需要付費以獲取 I2C 從屬設備地址。
I2C簡單來說,就是一種串行通信協議,I2C的通信協議和通信接口在很多工程中有廣泛的應用,如數據采集領域的串行 AD,圖像處理領域的攝像頭配置,工業控制領域的 X 射線管配置等等。除此之外,由于I2C協議占用的 IO 資源特別少,連接方便,所以工程中也常選用I2C接口做為不同芯片間的通信協議。I2C串行總線一般有兩根信號線,一根是雙向的數據線SDA,另一根是時鐘線SCL。所有接到I2C總線設備上的串行數據SDA都接到總線的SDA上,各設備的時鐘線SCL接到總線的SCL上。
在現代電子系統中,有為數眾多的 IC 需要進行相互之間以及與外界的通信。為了簡化電路的設計,Philips 公司開發了一種用于內部 IC 控制的簡單的雙向兩線串行總線I2C(Intel-Integrated Circuit bus)。1998 年當推出I2C總線協議 2.0 版本時,I2C協議實際上已經成為一個國際標準。
在進行 FPGA 設計時,經常需要和外圍提供I2C接口的芯片通信。例如低功耗的 CMOS 實時時鐘/日歷芯片 PCF8563、LCD 驅動芯片 PCF8562、并行口擴展芯片 PCF8574、鍵盤/LED 驅動器 ZLG7290 等都提供I2C接口。因此在 FPGA 中模擬I2C接口已成為 FPGA 開發必要的步驟。
本篇將詳細講解在 FPGA 芯片中使用 VHDL/Verilog HDL 模擬I2C協議,以及編寫 TestBench仿真和測試程序的方法。
第三篇內容摘要:本篇會介紹程序的仿真與測試,包括主節點的仿真、從節點的仿真、仿真主程序、仿真結果以及總結等相關內容。
四、程序的仿真與測試
I2C 協議的模擬程序完成后,還需要通過仿真程序對程序的功能進行測試。對本程序的仿真包括 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("Datacompareerror.Received%h,expected%hattime%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"
//模塊定義
moduletst_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; //高位地址
parameterCTR=3'b010;//控制寄存器地址,(7)使能位|6中斷使能位|5-0其余保留位
parameterRXR=3'b011;//接收寄存器地址,(7)接收到的最后一個字節的數據
parameter TXR = 3'b011; //傳輸寄存器地址,(7)傳輸地址時最后一位為讀寫位,1 為讀
parameter CR = 3'b100; //命令寄存器地址,
//(7)開始|6結束|5讀|4寫|3應答(作為接收方時,發送應答信號,“0”為應答,“1”為不應答)|2保留位|1保留位|0中斷應答位,這八位自動清除
parameterSR=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("
狀態: %t I2C 接口測試開始!
", $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("
錯誤: 需要的是 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("
錯誤: 需要的是 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("
錯誤: 需要 NACK, 接收到 ACK
");
// 從 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("
狀態: %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” 仿真程序說明I2C程序符合I2C協議的時序和數據格式,可以實現模擬I2C協議的任務。
五、總結
本篇首先說明了I2C協議相關的內容,介紹協議基本概念和數據傳輸各個命令的具體含義以及協議對時序的要求。接下來介紹模擬I2C協議程序的框架,詳細講解框架中各個模塊的功能并介紹詳細代碼。最后通過一個完成的仿真程序完成對程序的測試。I2C在應用中有著廣泛的用途,本篇希望通過這個例子為各位大俠提供一個可行的解決方案。
審核編輯:劉清
-
FPGA
+關注
關注
1660文章
22411瀏覽量
636281 -
集成電路
+關注
關注
5452文章
12572瀏覽量
374531 -
SDA
+關注
關注
0文章
125瀏覽量
29609 -
SCL
+關注
關注
1文章
244瀏覽量
18016 -
LCD驅動芯片
+關注
關注
0文章
21瀏覽量
8256
原文標題:往期精選:基于FPGA的模擬 I2C協議系統設計(附代碼)
文章出處:【微信號:HXSLH1010101010,微信公眾號:FPGA技術江湖】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
I2C協議基本概念和數據傳輸
【FPGA學習】模擬 I2C 接口程序的基本框架
基于EasyFPGA030的I2C總線接口模塊
I2C總線協議及其應用
關于stm32通信協議:軟件模擬SPI、軟件模擬I2C的總結(fishing_8)
硬件I2C與模擬I2C
基于FPGA的模擬I2C協議系統設計
評論