大俠好,歡迎來到FPGA技術(shù)江湖,江湖偌大,相見即是緣分。大俠可以關(guān)注FPGA技術(shù)江湖,在“闖蕩江湖”、"行俠仗義"欄里獲取其他感興趣的資源,或者一起煮酒言歡。
今天給大俠帶來基于FPGA的 模擬 I2C 協(xié)議設(shè)計,包括?I2C 總線解析以及模擬?I2C 接口程序的基本框架、I2C?協(xié)議的具體實現(xiàn)、程序的仿真與測試。篇幅較長,話不多說,上貨。
之前也有相關(guān)文章介紹,僅供各位大俠參考。
導讀
I2C(Inter-Integrated Circuit),其實是 I2C Bus簡稱,中文就是集成電路總線,它是一種串行通信總線,使用多主從架構(gòu),由飛利浦公司在1980年代為了讓主板、嵌入式系統(tǒng)或手機用以連接低速周邊設(shè)備而發(fā)展。I2C的正確讀法為“I平方C”("I-squared-C"),而“I二C”("I-two-C")則是另一種錯誤但被廣泛使用的讀法。自2006年10月1日起,使用 I2C 協(xié)議已經(jīng)不需要支付專利費,但制造商仍然需要付費以獲取 I2C 從屬設(shè)備地址。
I2C?簡單來說,就是一種串行通信協(xié)議,I2C的通信協(xié)議和通信接口在很多工程中有廣泛的應(yīng)用,如數(shù)據(jù)采集領(lǐng)域的串行 AD,圖像處理領(lǐng)域的攝像頭配置,工業(yè)控制領(lǐng)域的 X 射線管配置等等。除此之外,由于 I2C 協(xié)議占用的 IO 資源特別少,連接方便,所以工程中也常選用 I2C 接口做為不同芯片間的通信協(xié)議。I2C 串行總線一般有兩根信號線,一根是雙向的數(shù)據(jù)線SDA,另一根是時鐘線SCL。所有接到?I2C 總線設(shè)備上的串行數(shù)據(jù)SDA都接到總線的SDA上,各設(shè)備的時鐘線SCL接到總線的SCL上。
在現(xiàn)代電子系統(tǒng)中,有為數(shù)眾多的 IC 需要進行相互之間以及與外界的通信。為了簡化電路的設(shè)計,Philips 公司開發(fā)了一種用于內(nèi)部 IC 控制的簡單的雙向兩線串行總線 I2C(Intel-Integrated Circuit bus)。1998 年當推出?I2C?總線協(xié)議 2.0 版本時,I2C 協(xié)議實際上已經(jīng)成為一個國際標準。
在進行 FPGA 設(shè)計時,經(jīng)常需要和外圍提供?I2C?接口的芯片通信。例如低功耗的 CMOS 實時時鐘/日歷芯片 PCF8563、LCD 驅(qū)動芯片 PCF8562、并行口擴展芯片 PCF8574、鍵盤/LED 驅(qū)動器 ZLG7290 等都提供?I2C?接口。因此在 FPGA 中模擬?I2C?接口已成為 FPGA 開發(fā)必要的步驟。
本篇將詳細講解在 FPGA 芯片中使用 VHDL/Verilog HDL 模擬?I2C?協(xié)議,以及編寫 TestBench仿真和測試程序的方法。
第一篇內(nèi)容摘要:本篇會介紹?I2C 總線解析,包括?I2C 總線概述、I2C 協(xié)議的基本概念、I2C協(xié)議的時序要求,還會介紹模擬 I2C 接口程序的基本框架等相關(guān)內(nèi)容。
一、I2C 總線概述
下面先對 I2C 協(xié)議中有關(guān)數(shù)據(jù)格式和時序的內(nèi)容進行介紹,這里沒有涉及的地方請參考《THE I2C-BUS SPECIFICATION VERSION 2.1 JANUARY 2000》。
1.1?I2C?總線概述
I2C 協(xié)議作為一個串行總線標準盡管沒有并行總線的數(shù)據(jù)吞吐能力,但是它的以下特點使其有著廣泛的應(yīng)用:
? 只需要兩條總線—串行數(shù)據(jù)線 SDA 和串行時鐘線 SCL;
? 每個連接到總線的器件都可以通過惟一的地址和一直存在的簡單的主/從節(jié)點關(guān)系軟件設(shè)定地址,主節(jié)點可以發(fā)送數(shù)據(jù)或接收數(shù)據(jù);
? 是真正的多主總線,當兩個或更多主節(jié)點同時初始化數(shù)據(jù)傳輸時,可以通過沖突檢測和仲裁防止數(shù)據(jù)被破壞;
? 串行的 8 位雙向數(shù)據(jù)傳輸位速率在標準模式下可達 100kbit/s,快速模式下可達400kbit/s,高速模式下可達 3.4Mbit/s;
? 片上的濾波器可以濾去總線數(shù)據(jù)線上的毛刺波,保證數(shù)據(jù)完整;
? 連接到相同總線的 IC 數(shù)量只受到總線的最大電容(400pF)限制。
總線不僅僅是互連的線,還包含系統(tǒng)通信的所有格式和過程。I2C 總線結(jié)構(gòu)上的特點保證了其應(yīng)用時的簡潔,另外其完備的協(xié)議避免了所有混亂、數(shù)據(jù)丟失和妨礙信息的可能性。
1.2 I2C?協(xié)議的基本概念
I2C 總線支持任何 IC 生產(chǎn)過程(NMOS、CMOS 和雙極性)。串行數(shù)據(jù)線 SDA 和串行時鐘線 SCL在連接到總線的器件間傳遞信息。每個器件都有一個惟一的地址作為識別的標志(無論是微控制器、LCD 驅(qū)動器存儲器還是鍵盤接口),并且都可以發(fā)送數(shù)據(jù)和接收數(shù)據(jù)。很明顯 LCD 驅(qū)動器只需要接收數(shù)據(jù),而存儲器需要接收和發(fā)送數(shù)據(jù)。圖 1 所示的是一個高性能集成電視的例子。
圖 1 高性能集成電視
從圖 1 可以看到,應(yīng)用?I2C 總線是非常方便的。用通俗的話講 I2C 總線的硬件設(shè)計工作就是連接 SDA 和 SCL 兩條線,依靠 I2C 協(xié)議完成軟件工作。在 I2C 協(xié)議中應(yīng)理解如下的概念。
1)主/從節(jié)點
主節(jié)點負責初始化總線的數(shù)據(jù)傳輸,并產(chǎn)生允許傳輸?shù)臅r鐘信號。此時任何被尋址的器件都被認為是從節(jié)點。當有多個主節(jié)點在總線上傳輸數(shù)據(jù)時,每個主節(jié)點產(chǎn)生自己的時鐘信號。掛接到總線上的所有外圍器件、外設(shè)接口都是總線上的節(jié)點。
2)總線上節(jié)點的尋址方式
在任何時刻總線上只有一個主控器件(主節(jié)點)實現(xiàn)總線的控制操作,對總線上的其他節(jié)點尋址,可分時實現(xiàn)點-點的數(shù)據(jù)傳送。因此總線上每個節(jié)點都有一個固定的節(jié)點地址。
I2C 總線上主節(jié)點的地址由軟件給定,此地址存放在 I2C 總線的地址寄存器中。I2C 總線上所有的外圍器件都有規(guī)范的器件地址。器件地址由 7 位數(shù)字組成,它和 1 位方向位構(gòu)成了 I2C 總線器件的尋址字節(jié) SLA(Slave address)。
器件地址是 I2C 總線外圍接口器件固有的地址編碼,器件出廠時就已給定。數(shù)據(jù)方向位規(guī)定了總線上主節(jié)點對從節(jié)點的數(shù)據(jù)傳送方向。
1.3 I2C協(xié)議的時序要求
1)總線上的數(shù)據(jù)傳遞時序
I2C 總線上數(shù)據(jù)傳遞時序如圖 2 所示,具體步驟如下。
圖 2 I2C 總線的數(shù)據(jù)傳遞時序
? 首先主節(jié)點器件發(fā)送一個起始信號。
? 接下來主節(jié)點器件發(fā)送從節(jié)點地址和讀寫方式,一共 8 位。其中從節(jié)點地址 7 位,讀寫方式 1 位。
? 與傳輸?shù)刂芬恢碌膹墓?jié)點器件應(yīng)答(即 ACK)。
? 開始數(shù)據(jù)傳輸,傳輸數(shù)據(jù)數(shù)量不限。每個字節(jié)(八位)后面跟接收數(shù)據(jù)方的應(yīng)答位。例如主節(jié)點器件讀取從節(jié)點數(shù)據(jù),從節(jié)點發(fā)送數(shù)據(jù),主節(jié)點應(yīng)答;主節(jié)點器件寫數(shù)據(jù)到從節(jié)點,主節(jié)點發(fā)送數(shù)據(jù),從節(jié)點應(yīng)答。
? 數(shù)據(jù)傳輸結(jié)束,主節(jié)點器件發(fā)送一個終止信號結(jié)束整個過程。
采用 I2C 總線后對傳送的字節(jié)數(shù)沒有限制,只要求每傳送一個字節(jié)后對方回應(yīng)一個應(yīng)答位。在發(fā)送時首先發(fā)送的是數(shù)據(jù)的最高位(MSB,Most Significant Bit)。每次傳送開始有起始信號,結(jié)束時有停止信號。在總線傳送完一個字節(jié)后,可以通過對時鐘線(SCL)的控制使傳送暫停。例如當某個外圍器件接收 N 個字節(jié)數(shù)據(jù)后需要一段處理時間以便繼續(xù)接收以后的字節(jié)數(shù)據(jù),這時可在應(yīng)答信號后使 SCL 變?yōu)榈?a class="article-link" target="_blank" href="/baike/1465710.html">電平控制總線暫停。如果主節(jié)點要求總線暫停也可使時鐘線保持低電平控制總線暫停。
2)總線上的時序信號
I2C 總線為同步傳輸總線,總線信號完全與時鐘同步。I2C 總線上與數(shù)據(jù)傳送有關(guān)的信號有起始信號 S、終止信號 P、應(yīng)答信號 A 以及位傳送信號。下面將對這些信號一一介紹。
(1)起始信號
起始信號(Start Condition)如圖 3 所示。當時鐘線 SCL 為高電平時,數(shù)據(jù)線 SDA 從高電平向低電平變化將形成起始信號,啟動 I2C 總線。
(2)終止信號
終止信號(Stop Condition)如圖 3 所示。當時鐘線 SCL 為高電平時,數(shù)據(jù)線 SDA 從低電平向高電平變化將形成終止信號,停止 I2C 總線。
(3)應(yīng)答信號
如圖 3 所中 ACK 第 9 個時鐘脈沖對應(yīng)應(yīng)答位,相應(yīng)數(shù)據(jù)線上低電平時為應(yīng)答信號,高電平時為非應(yīng)答信號。
圖 3 起始信號和終止信號
(4)位傳送信號
在 I2C 總線啟動后或應(yīng)答信號后的第 1~8 個時鐘脈沖對應(yīng)于一個字節(jié)的 8 位數(shù)據(jù)傳送。脈沖高電平期間,數(shù)據(jù)串行傳送;低電平期間為數(shù)據(jù)準備,允許總線上數(shù)據(jù)電平變換。
二、模擬 I2C 接口程序的基本框架
模擬 I2C 接口程序的基本框架如圖 4 所示。
圖 4 模擬 I2C 接口程序的基本框架
1)程序接口
用于和應(yīng)用程序連接的接口,將應(yīng)用程序的數(shù)據(jù)按照 I2C 協(xié)議的方式通過 SDA 傳遞給外部器件。包括下列內(nèi)容:
? clk_I FPGA 外部時鐘信號。
? rst_I 同步重起信號。
? arst_I 異步重起信號。
? adr_I 從節(jié)點地址。
? dat_I 輸入數(shù)據(jù)。
? dat_o 輸出數(shù)據(jù)。
? we_I 寫有效信號。
? stb_I 接口有效信號。
? cyc_I 有效總線周期輸入。
? ack_o 應(yīng)答信號輸出。
? inta_o 中斷信號輸出。
2)時鐘設(shè)置寄存器
I2C 協(xié)議提供了 3 種速度模式:正常速度模式 100kbit/s、快速模式 400kbit/s、高速模式3.5Mbit/s。SCL 輸出的時鐘信號頻率和速度模式一致。程序內(nèi)部使用 5 倍 SCL 信號作為時鐘,而 FPGA 外部時鐘需要經(jīng)過分頻得到程序內(nèi)部使用的時鐘。
例如:采用正常速度 100kbit/s,F(xiàn)PGA 外部時鐘為 50MHz,則時鐘設(shè)置寄存器需要設(shè)置為(50MHz/5*100kHz – 1=99)。
3)時鐘產(chǎn)生模塊
時鐘產(chǎn)生模塊產(chǎn)生 4 倍 SCL 頻率的時鐘信號,它為位傳輸控制模塊中所有同步動作提供觸發(fā)信號。
4)命令寄存器
命令寄存器共 8 位,它決定是否在總線上產(chǎn)生各種時序信號、是否讀/寫數(shù)據(jù),各位表示的含義如表 1 所示。
表 1 命令寄存器內(nèi)容
5)狀態(tài)寄存器
狀態(tài)寄存器用來顯示當前總線的狀態(tài),例如是否接收到從節(jié)點的應(yīng)答信號、是否忙、是否在傳遞數(shù)據(jù)等,具體內(nèi)容如表 2 所示。
表 2 狀態(tài)寄存器內(nèi)容
6)數(shù)據(jù)傳輸寄存器
數(shù)據(jù)傳輸寄存器用于保存等待傳輸?shù)臄?shù)據(jù)。當傳遞從節(jié)點地址信息時,前 7 位保存從節(jié)點地址,最后一位保存讀寫命令;當傳遞普通數(shù)據(jù)時,8 位保存一個字節(jié)數(shù)據(jù)。數(shù)據(jù)傳輸寄存器具體內(nèi)容如表 3 所示。
表 3 數(shù)據(jù)傳輸寄存器內(nèi)容
7)數(shù)據(jù)接收寄存器
數(shù)據(jù)接收寄存器用于保存通過 I2C 總線接收到的最后一個字節(jié)內(nèi)容,具體內(nèi)容如表4所示。
表 4 數(shù)據(jù)接收寄存器內(nèi)容
8)字節(jié)傳輸控制模塊
字節(jié)傳輸控制模塊以字節(jié)為單位控制 I2C 總線的數(shù)據(jù)傳輸。這個模塊按照命令寄存器設(shè)置的內(nèi)容將數(shù)據(jù)傳輸寄存器內(nèi)容傳遞到 I2C 總線的接收端,或者從 I2C 總線發(fā)送端接收數(shù)據(jù)并保存到數(shù)據(jù)接收寄存器中。
9)位傳輸控制模塊
位傳輸控制模塊以位為單位進行 I2C 總線的數(shù)據(jù)傳輸和產(chǎn)生各個 I2C 協(xié)議命令(如開始、停止、重復開始等)。字節(jié)傳輸控制模塊控制位傳輸控制模塊的各種動作。例如讀取一個字節(jié)數(shù)據(jù),位傳輸控制模塊需要執(zhí)行 8 個讀的命令。
10)數(shù)據(jù)移位寄存器
數(shù)據(jù)移位寄存器保存的數(shù)據(jù)總是和當前的數(shù)據(jù)傳輸相關(guān)的。例如在進行讀操作時,主節(jié)點通過移位寄存器依次通過 SDA 獲得來自 I2C 發(fā)送端的數(shù)據(jù),完成后數(shù)據(jù)拷貝到數(shù)據(jù)接收寄存器中。在寫操作時,數(shù)據(jù)傳輸寄存器中的數(shù)據(jù)拷貝到數(shù)據(jù)移位寄存器中,然后依次通過 SDA 將數(shù)據(jù)傳輸?shù)?I2C 總線的接收端。
三、I2C?協(xié)議的具體實現(xiàn)
FPGA 設(shè)計一般按照從頂向下的模式進行:首先設(shè)計芯片功能,規(guī)劃各個模塊功能;然后按照規(guī)劃實現(xiàn)各個模塊。本篇由 3 個代碼文件組成:i2c_master_bit_ctrl.v 完成位傳輸?shù)墓δ堋2c_master_byte_ctrl.v 完成字節(jié)傳輸?shù)墓δ?、i2c_master_top.v 完成整個程序的控制功能,并提供給外部程序的接口。在 ISE 中創(chuàng)建一個項目,然后加入上面 3 個文件。下面依次介紹 3 個文件的內(nèi)容。本篇講解采用 Verilog HDL。
3.1 位傳輸?shù)膶崿F(xiàn)
i2c_master_bit_ctrl.v 完成位傳輸?shù)墓δ?。位傳輸?shù)墓δ馨〝?shù)據(jù)按位傳輸?shù)膶崿F(xiàn)和 I2C協(xié)議各個命令的實現(xiàn)兩部分。
如圖 5 所示開始和重復開始命令的產(chǎn)生包括 5 個階段:idle 和 A、B、C、D 等。停止命令包括 4 個階段:idle 和 A、B、C 等。讀、寫一個字節(jié)通過 8 次位操作完成。
圖?5 位傳輸完成數(shù)據(jù)的傳輸和各個命令的實現(xiàn)
實現(xiàn)代碼如下:
`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; // 時鐘分頻系數(shù)
input [3:0] cmd;
output cmd_ack; // 命令完成應(yīng)答
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?數(shù)據(jù)輸入
????output?sda_o;?//?I2C?數(shù)據(jù)輸出
????output?sda_oen;?//?I2C?數(shù)據(jù)輸出使能
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; // 時鐘產(chǎn)生信號
wire slave_wait;
reg [15:0] cnt; // 時鐘分頻計數(shù)器
// 模塊主體
// 當從節(jié)點沒有準備好時,下拉 SCL 來延遲周期
// 延遲 scl_oen
always @(posedge clk)
dscl_oen <= #1 scl_oen;
assign slave_wait = dscl_oen && !sSCL;
// 產(chǎn)生時鐘使能信號
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
// 產(chǎn)生總線狀態(tài)控制信號
reg dSCL, dSDA;
reg sta_condition;
reg sto_condition;
// 同步 SCL 和 SDA 輸入信號,減少不穩(wěn)定風險
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 的下降沿,即檢測開始狀態(tài)信號
// SCL 處于高時檢測到 SDA 的上升沿,即檢測停止狀態(tài)信號
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
//?產(chǎn)生?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;
// 產(chǎn)生仲裁丟失信號 generate arbitration lost signal
// 仲裁丟失發(fā)生在:
//?1)?主節(jié)點驅(qū)動?SDA?處于高,但是?I2C?總線一直處于低
// 2) 沒有請求時卻檢測到停止狀態(tài)信號
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
// 產(chǎn)生數(shù)據(jù)輸出信號,在 SCL 信號的上升沿保存 SDA
always @(posedge clk)
if(sSCL & ~dSCL)
dout <= #1 sSDA;
// 產(chǎn)生狀態(tài)機
// 狀態(tài)譯碼
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;
//狀態(tài)機
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 狀態(tài)
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 在同一狀態(tài)
sda_oen <= #1 sda_oen; // 保持 SDA 在同一狀態(tài)
sda_chk <= #1 1'b0; // 不檢查 SDA 輸出
end
// 開始狀態(tài)
start_a:
begin
c_state <= #1 start_b;
scl_oen <= #1 scl_oen; // 保持 SCL 在同一狀態(tài)
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
// 停止狀態(tài)
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
// 讀狀態(tài)
rd_a:
begin
c_state <= #1 rd_b;
scl_oen <= #1 1'b0; //保持 SCL 處于低
sda_oen <= #1 1'b1; // SDA 處于三態(tài)
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
// 寫狀態(tài)
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 字節(jié)傳輸?shù)膶崿F(xiàn)
字節(jié)傳輸?shù)木唧w實現(xiàn)流程如圖 6 所示。
圖 6 字節(jié)傳輸控制模塊流程圖
字節(jié)傳輸控制模塊控制以字節(jié)為單位的數(shù)據(jù)傳輸。它根據(jù)命令寄存器的設(shè)置將數(shù)據(jù)傳輸寄存器中的內(nèi)容傳輸?shù)酵獠抗?jié)點,將外部節(jié)點的數(shù)據(jù)接收到數(shù)據(jù)接收寄存器中。
實現(xiàn)代碼如下:
`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;
// 狀態(tài)信號輸出
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;
// 變量申明
// 狀態(tài)機
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;
// 狀態(tài)機信號
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 )
????????);
????????
// 產(chǎn)生 GO 信號,當讀/寫/停止/應(yīng)答時發(fā)生
assign go = (read | write | stop) & ~cmd_ack;
// 分配輸出到移位寄存器
assign dout = sr;
// 產(chǎn)生移位寄存器
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};
// 產(chǎn)生計數(shù)器
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);
// 狀態(tài)機
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 狀態(tài)
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 狀態(tài)
begin
c_state <= #1 ST_STOP;
core_cmd <= #1 `I2C_CMD_STOP;
// 產(chǎn)生應(yīng)答信號
cmd_ack <= #1 1'b1;
end
ld <= #1 1'b1;
end
//開始狀態(tài)
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
//寫數(shù)據(jù)狀態(tài)
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; // 保持在原來狀態(tài)
core_cmd <= #1 `I2C_CMD_WRITE; // 寫下一位數(shù)據(jù)
shift <= #1 1'b1;
end
//讀信號狀態(tài)
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; // 保留在原來狀態(tài)
core_cmd <= #1 `I2C_CMD_READ; // 讀下一位數(shù)據(jù)
end
shift <= #1 1'b1;
core_txd <= #1 ack_in;
end
//應(yīng)答數(shù)據(jù)狀態(tài)
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
// 把應(yīng)答信號輸出連接到位控制模塊
ack_out <= #1 core_rxd;
// 產(chǎn)生應(yīng)答信號
cmd_ack <= #1 1'b1;
core_txd <= #1 1'b1;
end
else
core_txd <= #1 ack_in;
//停止狀態(tài)
ST_STOP:
if (core_ack)
begin
c_state <= #1 ST_IDLE;
core_cmd <= #1 `I2C_CMD_NOP;
end
endcase
end
endmodule
3.3 程序主體的實現(xiàn)
程序主體部分完成與外部程序的接口、與總線上外部節(jié)點的連線、完成程序內(nèi)部各個寄存器的構(gòu)建、控制字節(jié)傳輸控制模塊等功能。代碼如下:
`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 );
// 參數(shù)
parameter ARST_LVL = 1'b0; // 異步 reset 信號
// 輸入、輸出信號
// 連接到外部接口的信號
input clk_i; // 主節(jié)點時鐘信號
input rst_i; // 同步 reset 信號,高有效
input arst_i; // 異步 reset 信號
input [2:0] adr_i; // 低位地址信號
input [7:0] dat_i; // 數(shù)據(jù)總線輸入
output [7:0] dat_o; // 數(shù)據(jù)總線輸出
input we_i; // 輸入使能信號
input stb_i; // 觸發(fā)信號
input cyc_i; // 總線周期輸入
output ack_o; // 應(yīng)答信號輸出
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?數(shù)據(jù)線
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; // 數(shù)據(jù)傳輸寄存器
wire [ 7:0] rxr; // 數(shù)據(jù)接收寄存器
reg [ 7:0] cr; // 命令寄存器
wire [ 7:0] sr; // 狀態(tài)寄存器
// 完成信號,命令完成后清除命令寄存器
wire done;
// 模塊使能信號
wire core_en;
wire ien;
// 狀態(tài)寄存器信號
wire irxack;
reg rxack; // 從從節(jié)點接收應(yīng)答信號
reg tip; // 傳輸進行標志
reg irq_flag; // 中斷掛起標志
wire i2c_busy; // 總線忙標志
wire i2c_al; // 總線仲裁丟失
reg al; // 狀態(tài)寄存器仲裁丟失位
// 模塊主體
// 產(chǎn)生內(nèi)部 reset
wire rst_i = arst_i ^ ARST_LVL;
wire wacc = cyc_i & stb_i & we_i;
// 產(chǎn)生應(yīng)答輸出信號
always @(posedge clk_i)
wb_ack_o <= #1 cyc_i & stb_i & ~ack_o;
// 數(shù)據(jù)輸出
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; // 寫數(shù)據(jù)傳輸寄存器
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
// 產(chǎn)生寄存器
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
// 產(chǎn)生命令寄存器
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;?//?命令完成或者仲裁丟失時清除命令寄存器內(nèi)容
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];
// 連接字節(jié)控制模塊
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 )
)
// 狀態(tài)寄存器部分和中斷請求信號
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
// 中斷請求標志
// 產(chǎn)生中斷請求信號
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 設(shè)置后產(chǎn)生中斷信號
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
四、程序的仿真與測試
I2C 協(xié)議的模擬程序完成后,還需要通過仿真程序?qū)Τ绦虻墓δ苓M行測試。對本程序的仿真包括 3 個部分:第一部分是主節(jié)點的仿真,模擬數(shù)據(jù)讀/寫;第二部分是從節(jié)點的仿真,模擬數(shù)據(jù)的接收和應(yīng)答;第三部分是仿真主程序,負責整個仿真過程的控制。
4.1 主節(jié)點的仿真
主節(jié)點仿真的內(nèi)容包括讀數(shù)據(jù)、寫數(shù)據(jù)和比較數(shù)據(jù) 3 部分,代碼如下:
`include "timescale.v"
//模塊定義
module wb_master_model(clk, rst, adr, din, dout, cyc, stb, we, sel, ack, err, rty);
//參數(shù)
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
// 寫數(shù)據(jù)周期
task wb_write;
input delay;
integer delay;
input [awidth -1:0] a;
input [dwidth -1:0] d;
begin
// 延遲
repeat(delay) @(posedge clk);
// 設(shè)置信號值
#1;
adr = a;
dout = d;
cyc = 1'b1;
stb = 1'b1;
we = 1'b1;
sel = {dwidth/8{1'b1}};
@(posedge clk);
// 等待從節(jié)點的應(yīng)答信號
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
// 讀數(shù)據(jù)周期
task wb_read;
input delay;
integer delay;
input [awidth -1:0]a;
output [dwidth -1:0] d;
begin
// 延遲
repeat(delay) @(posedge clk);
// 設(shè)置信號值
#1;
adr = a;
dout = {dwidth{1'bx}};
cyc = 1'b1;
stb = 1'b1;
we = 1'b0;
sel = {dwidth/8{1'b1}};
@(posedge clk);
// 等待從節(jié)點應(yīng)答信號
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
// 比較數(shù)據(jù)
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 從節(jié)點的仿真
從節(jié)點仿真程序需要模擬從主節(jié)點接收數(shù)據(jù),并發(fā)出應(yīng)答信號,代碼如下:
`include "timescale.v"
//模塊定義
module i2c_slave_model (scl, sda);
// 參數(shù)
// 地址
parameter I2C_ADR = 7'b001_0000;
// 輸入、輸出
input scl;
inout sda;
// 變量申明
wire debug = 1'b1;
reg [7:0] mem [3:0]; // 初始化內(nèi)存
reg [7:0] mem_adr; // 內(nèi)存地址
reg [7:0] mem_do; // 內(nèi)存數(shù)據(jù)輸出
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;
// 狀態(tài)機的狀態(tài)定義
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
// 產(chǎn)生移位寄存器
always @(posedge scl)
sr <= #1 {sr[6:0],sda};
//檢測到訪問地址與從節(jié)點一致
assign my_adr = (sr[7:1] == I2C_ADR);
//產(chǎn)生位寄存器
always @(posedge scl)
if(ld)
bit_cnt <= #1 3'b111;
else
bit_cnt <= #1 bit_cnt - 3'h1;
//產(chǎn)生訪問結(jié)束標志
assign acc_done = !(|bit_cnt);
// sda 延遲
assign #1 sda_dly = sda;
//檢測到開始狀態(tài)
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;
// 檢測到停止狀態(tài)信號
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;
//產(chǎn)生 I2C 的 RESET 信號
assign i2c_reset = sta || sto;
// 狀態(tài)機
always @(negedge scl or posedge sto)
if (sto || (sta && !d_sta) )
begin
state <= #1 idle; // reset 狀態(tài)機
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 狀態(tài)
if (acc_done && my_adr)
begin
state <= #1 slave_ack;
rw <= #1 sr[0];
sda_o <= #1 1'b0; // 產(chǎn)生應(yīng)答信號
#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: // 等待內(nèi)存地址
if(acc_done)
begin
state <= #1 gma_ack;
mem_adr <= #1 sr; // 保存內(nèi)存地址
sda_o <= #1 !(sr <= 15); // 收到合法地址信號后發(fā)出應(yīng)答信號
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: // 接收數(shù)據(jù)
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
// 從內(nèi)存讀數(shù)據(jù)
always @(posedge scl)
if(!acc_done && rw)
mem_do <= #1 {mem_do[6:0], 1'b1};
// 產(chǎn)生三態(tài)
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); // 開始狀態(tài)信號
$setup(posedge scl, posedge sda &&& scl, normal_tsu_sto); // 停止狀態(tài)信號
$setup(posedge tst_sta, posedge tst_scl, normal_sta_sto);
endspecify
endmodule
4.3 仿真主程序
仿真主程序完成主節(jié)點數(shù)據(jù)到從節(jié)點的控制,代碼如下:
`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 保存狀態(tài)寄存器內(nèi)容
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)接收到的最后一個字節(jié)的數(shù)據(jù)
parameter TXR = 3'b011; //傳輸寄存器地址,(7)傳輸?shù)刂窌r最后一位為讀寫位,1 為讀
parameter CR = 3'b100; //命令寄存器地址,
???
????//(7)開始|6?結(jié)束|5?讀|4?寫|3?應(yīng)答(作為接收方時,發(fā)送應(yīng)答信號,“0”為應(yīng)答,“1”為不應(yīng)答)|2?保留位|1?保留位|0?中斷應(yīng)答位,這八位自動清除
????parameter?SR?=?3'b100;?//狀態(tài)寄存器地址,(7)接收應(yīng)答位(“0”為接收到應(yīng)答)|6?忙位(產(chǎn)生開始信號后變?yōu)?1,結(jié)束信號后變?yōu)?0)|5?仲裁位|4-2?保留位|1?傳輸中位(1?表示正在傳輸數(shù)據(jù),0?表示傳輸結(jié)束)|中斷標志位
parameter TXR_R = 3'b101;
parameter CR_R = 3'b110;
// 產(chǎn)生時鐘信號,一個時間單位為 1ns,周期為 10ns,頻率為 100MHz。
always #5 clk = ~clk;
//連接 master 模擬模塊
wb_master_model #(8, 32) u0 (
.clk(clk), //時鐘
.rst(rstn), //重起
.adr(adr), //地址
.din(dat_i), //輸入的數(shù)據(jù)
.dout(dat_o), //輸出的數(shù)據(jù)
.cyc(cyc),
.stb(stb),
.we(we),
.sel(),
.ack(ack), //應(yīng)答
.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), //數(shù)據(jù)輸入接口
.wb_dat_o(dat_i), //數(shù)據(jù)從接口輸出
.wb_we_i(we), //寫使能信號
.wb_stb_i(stb), //片選信號,應(yīng)該一直為高
.wb_cyc_i(cyc),
.wb_ack_o(ack), //應(yīng)答信號輸出到 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 模擬模塊產(chǎn)生 scl 和 sda 的三態(tài)緩沖
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ài): %t I2C 接口測試開始!nn", $time);
// 初始值
clk = 0;
//重起系統(tǒng)
rstn = 1'b1; // negate reset
#2;
rstn = 1'b0; // assert reset
repeat(20) @(posedge clk);
rstn = 1'b1; // negate reset
$display("狀態(tài): %t 完成系統(tǒng)重起!", $time);
@(posedge clk);
// 對接口編程
// 寫內(nèi)部寄存器
// 分頻 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ài): %t 完成分頻寄存器操作!", $time);
//讀分頻寄存器內(nèi)容
u0.wb_cmp(0, PRER_LO, 8'hc8);
u0.wb_cmp(0, PRER_HI, 8'h00);
$display("狀態(tài): %t 完成分頻寄存器確認操作!", $time);
//接口使能
u0.wb_write(1, CTR, 8'h80);
$display("狀態(tài): %t 完成接口使能!", $time);
// 驅(qū)動 slave 地址
// h'a0=b'1010_0000,地址+寫狀態(tài),寫入的地址為 h'50
u0.wb_write(1, TXR, 8'ha0);
//命令內(nèi)容為 b'1001_0000,產(chǎn)生開始位,并設(shè)置寫狀態(tài)
u0.wb_write(0, CR, 8'h90);
$display("狀態(tài): %t 產(chǎn)生開始位, 然后寫命令 a0(地址+寫),命令開始!", $time);
// 檢查狀態(tài)位信息
// 檢查傳輸是否結(jié)束
u0.wb_read(1, SR, q);
while(q[1])
u0.wb_read(0, SR, q);
$display("狀態(tài): %t 地址驅(qū)動寫操作完成!", $time);
// 待寫的地址為 h'01
u0.wb_write(1, TXR, 8'h01);
// 產(chǎn)生寫命令 b'0001_0000
u0.wb_write(0, CR, 8'h10);
$display("狀態(tài): %t 待寫地址為 01,命令開始!", $time);
// 檢查狀態(tài)位
u0.wb_read(1, SR, q);
while(q[1])
u0.wb_read(0, SR, q);
$display("狀態(tài): %t 寫操作完成!", $time);
// 寫入內(nèi)容
u0.wb_write(1, TXR, 8'ha5);
u0.wb_write(0, CR, 8'h10);
$display("狀態(tài): %t 寫入內(nèi)容為 a5,開始寫入過程!", $time);
u0.wb_read(1, SR, q);
while(q[1])
u0.wb_read(1, SR, q);
$display("狀態(tài): %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ài): %t 寫 5a 到下一個地址,產(chǎn)生停止位!", $time);
u0.wb_read(1, SR, q);
while(q[1])
u0.wb_read(1, SR, q); // poll it until it is zero
$display("狀態(tài): %t 寫第二個地址結(jié)束!", $time);
// 讀
// 驅(qū)動 slave 地址
u0.wb_write(1, TXR, 8'ha0);
u0.wb_write(0, CR, 8'h90);
$display("狀態(tài): %t 產(chǎn)生開始位,寫命令 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ài): %t slave 地址驅(qū)動完成!", $time);
// 發(fā)送地址
u0.wb_write(1, TXR, 8'h01);
u0.wb_write(0, CR, 8'h10);
$display("狀態(tài): %t 發(fā)送地址 01!", $time);
u0.wb_read(1, SR, q);
while(q[1])
u0.wb_read(1, SR, q);
$display("狀態(tài): %t 地址發(fā)送完成!", $time);
// 驅(qū)動 slave 地址,1010_0001,h'50+read
u0.wb_write(1, TXR, 8'ha1);
u0.wb_write(0, CR, 8'h90);
$display("狀態(tài): %t 產(chǎn)生重復開始位, 讀地址+開始位", $time);
u0.wb_read(1, SR, q);
while(q[1])
u0.wb_read(1, SR, q);
$display("狀態(tài): %t 命令結(jié)束!", $time);
// 讀數(shù)據(jù)
u0.wb_write(1, CR, 8'h20);
$display("狀態(tài): %t 讀+應(yīng)答命令", $time);
u0.wb_read(1, SR, q);
while(q[1])
u0.wb_read(1, SR, q);
$display("狀態(tài): %t 讀結(jié)束!", $time);
// 檢查讀的內(nèi)容
u0.wb_read(1, RXR, qq);
if(qq !== 8'ha5)
$display("n 錯誤: 需要的是 a5, received %x at time %t", qq, $time);
// 讀下一個地址內(nèi)容
u0.wb_write(1, CR, 8'h20);
$display("狀態(tài): %t 讀+ 應(yīng)答", $time);
u0.wb_read(1, SR, q);
while(q[1])
u0.wb_read(1, SR, q);
$display("狀態(tài): %t 第二個地址讀結(jié)束!", $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ài): %t 讀 + 應(yīng)答", $time);
u0.wb_read(1, SR, q);
while(q[1])
u0.wb_read(1, SR, q);
$display("狀態(tài): %t 第三個地址讀完成!", $time);
u0.wb_read(1, RXR, qq);
$display("狀態(tài): %t 第三個地址內(nèi)容是 %x !", $time, qq);
// 讀
u0.wb_write(1, CR, 8'h28);
$display("狀態(tài): %t 讀 + 不應(yīng)答!", $time);
u0.wb_read(1, SR, q);
while(q[1])
u0.wb_read(1, SR, q);
$display("狀態(tài): %t 第四個地址讀完成!", $time);
u0.wb_read(1, RXR, qq);
$display("狀態(tài): %t 第四個地址內(nèi)容為 %x !", $time, qq);
// 檢查不存在的 slave 地址
// drive slave address
u0.wb_write(1, TXR, 8'ha0);
u0.wb_write(0, CR, 8'h90);
????????????$display("狀態(tài):?%t 產(chǎn)生開始位, 發(fā)送命令 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ài): %t 命令結(jié)束!", $time);
// 發(fā)送內(nèi)存地址
u0.wb_write(1, TXR, 8'h10);
u0.wb_write(0, CR, 8'h10);
$display("狀態(tài): %t 發(fā)送 slave 內(nèi)存地址 10!", $time);
u0.wb_read(1, SR, q);
while(q[1])
u0.wb_read(1, SR, q);
$display("狀態(tài): %t 地址發(fā)送完畢!", $time);
// slave 發(fā)送不應(yīng)答
$display("狀態(tài): %t 檢查不應(yīng)答位!", $time);
if(!q[7])
$display("n 錯誤: 需要 NACK, 接收到 ACKn");
// 從 slave 讀數(shù)據(jù)
u0.wb_write(1, CR, 8'h40);
$display("狀態(tài): %t 產(chǎn)生'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ài): %t 結(jié)束!", $time);
#25000; // wait 25us
$display("nn 狀態(tài): %t 測試結(jié)束!", $time);
$finish;
end
endmodule
4.4 仿真結(jié)果
在 ModelSim 中可以看到仿真的結(jié)果。如圖 7 所示是發(fā)送開始狀態(tài)并寫地址“a0”時的圖形,此時在圖上表示為 SCL 處于高時 SDA 的一個下降沿,然后是數(shù)據(jù)“1010,0000”。
圖 7 發(fā)送開始信號并寫地址 a0
如圖 8 所示為發(fā)送數(shù)據(jù)“01”和“a5”時的圖形,在圖上表示為:數(shù)據(jù)“0000,0001”和“1010,0101”。
圖 8 發(fā)送數(shù)據(jù)“01”和“a5”
如圖 9 所示的是發(fā)送停止狀態(tài)信號和數(shù)據(jù)“5a”時的圖形,在圖上表示為 SCL 處于高時SDA 的一個上升沿,然后是數(shù)據(jù)“0101,1010”。
圖 9 發(fā)送停止狀態(tài)信號和數(shù)據(jù)“5a”
仿真程序說明?I2C?程序符合?I2C?協(xié)議的時序和數(shù)據(jù)格式,可以實現(xiàn)模擬?I2C?協(xié)議的任務(wù)。
五、總結(jié)
本篇首先說明了?I2C?協(xié)議相關(guān)的內(nèi)容,介紹協(xié)議基本概念和數(shù)據(jù)傳輸各個命令的具體含義以及協(xié)議對時序的要求。接下來介紹模擬?I2C?協(xié)議程序的框架,詳細講解框架中各個模塊的功能并介紹詳細代碼。最后通過一個完成的仿真程序完成對程序的測試。I2C?在應(yīng)用中有著廣泛的用途,本篇希望通過這個例子為各位大俠提供一個可行的解決方案。