一、 軟件平臺與硬件平臺
軟件平臺:
1、操作系統:Windows-8.1
2、開發套件:ISE14.7
3、仿真工具:ModelSim-10.4-SE 、ChipScope
硬件平臺:
1、 FPGA型號:Xilinx公司的XC6SLX45-2CSG324
2、 EEPROM型號:Microchip公司的AT24LC04B
二、 原理介紹
IIC(Inter-Integrated Circuit)總線是一種由PHILIPS公司開發的兩線式串行總線,用于連接微控制器及其外圍設備。I2C總線產生于在80年代,最初為音頻和視頻設備開發,如今主要在服務器管理中使用,其中包括單個組件狀態的通信。例如管理員可對各個組件進行查詢,以管理系統的配置或掌握組件的功能狀態,如電源和系統風扇。可隨時監控內存、硬盤、網絡、系統溫度等多個參數,增加了系統的安全性,方便了管理。IIC數據傳輸速率有標準模式(100 kbps)、快速模式(400 kbps)和高速模式(3.4 Mbps),另外一些變種實現了低速模式(10 kbps)和快速+模式(1 Mbps)。
下圖是一個嵌入式系統中處理器僅通過2根線的IIC總線控制多個IIC外設的典型應用圖
圖中處理器是IIC主機,它僅僅通過兩根信號就可以控制IO擴展器,各種不同的傳感器,EEPROM,AD/DAs等設備,這也是IIC總線協議相較于其他協議最有優勢的地方。
IIC總線的特點:
1、 簡單性和有效性。由于接口直接在組件之上,因此I2C總線占用的空間非常小,減少了電路板的空間和芯片管腳的數量,降低了互聯成本。總線的長度可高達25英尺,并且能夠以10Kbps的最大傳輸速率支持40個組件。
2、 支持多主控(multimastering), 其中任何能夠進行發送和接收的設備都可以成為主總線。一個主控能夠控制信號的傳輸和時鐘頻率。當然,在任何時間點上只能有一個主控占用IIC總線。
IIC總線協議詳解:
IIC總線接口是一個標準的雙向傳輸接口,一次數據傳輸需要主機和從機按照IIC協議的標準進行。I2C總線是由數據線SDA和時鐘SCL構成的串行總線,可發送和接收數據,并且在硬件上都需要接一個上拉電阻到VCC。各種被控制電路均并聯在這條總線上,但就像電話機一樣只有撥通各自的號碼才能工作,所以每個電路和模塊都有唯一的地址,這樣,各控制電路雖然掛在同一條總線上,卻彼此獨立,互不相關。
IIC主機往從機里面寫入數據的步驟如下:
1、 主機發送一個起始信號和從機的設備地址給從機
2、 主機發送數據給從機
3、 主機發送一個停止信號結束發送過程
IIC主機從從機里面讀出數據的步驟如下:
1、 主機發送一個起始信號和從機的設備地址給從機
2、 主機發送一個要讀取的地址給從機
3、 主機從從機接收數據
4、 主機發送一個停止信號給從機結束整個接收過程
總的來說,IIC總線在通信的過程中一共有一下幾種狀態:
1、空閑狀態
IIC 總線的 SDA 和 SCL 兩條信號線同時處于高電平時,規定為總線的空閑狀態。此時各個器件的輸出級場效應管均處在截止狀態,即釋放總線,由兩條信號線各自的上拉電阻把電平拉高。
2、起始狀態和結束狀態
在時鐘線 SCL 保持高電平期間,數據線 SDA 上的電平被拉低(即負跳變),定義為 I2C 總線總線的起始信號,它標志著一次數據傳輸的開始。起始信號是由主控器主動建立的,在建立該信號之前 I2C 總線必須處于空閑狀態。
在時鐘線 SCL 保持高電平期間,數據線 SDA 被釋放,使得 SDA 返回高電平(即正跳變),稱為 I2C 總線的停止信號,它標志著一次數據傳輸的終止。停止信號也是由主控器主動建立的,建立該信號之后,I2C 總線將返回空閑狀態。
起始信號和結束信號如下圖所示
3、有效的數據位傳輸
在 IIC 總線上傳送的每一位數據都有一個時鐘脈沖相對應(或同步控制),即在 SCL 串行時鐘的配合下,數據在 SDA 上從高位向低位依次串行傳送每一位的數據。進行數據傳送時,在 SCL 呈現高電平期間,SDA 上的電平必須保持穩定,低電平為數據 0,高電平為數據 1。只有在 SCL 為低電平期間,才允許 SDA 上的電平改變狀態。下圖是0xaa在IIC總線上有效傳輸(有效傳輸是指第9個時鐘的高電平期間,從機給主機反饋了一個有效的應答位0)的圖示
4、應答信號與非應答信號
I2C 總線上的所有數據都是以 8 位字節傳送的,發送器(主機)每發送一個字節,就在第9個時鐘脈沖期間釋放數據線,由接收器(從機)反饋一個應答信號。應答信號為低電平時,規定為有效應答位(ACK簡稱應答位),表示接收器已經成功地接收了該字節;應答信號為高電平時,規定為非應答位(NACK),一般表示接收器接收該字節沒有成功。對于反饋有效應答位 ACK 的要求是,接收器在第 9 個時鐘脈沖之前的低電平期間將 SDA 線拉低,并且確保在該時鐘的高電平期間為穩定的低電平。
對非應答位(NACK)還要特別說明的是,還有以下四種情況IIC通信過程中會產生非應答位:
1、接收器(從機)正在處理某些實時的操作無法與主機實現IIC通信的時候,接收器(從機)會給主機反饋一個非應答位(NACK)
2、主機發送數據的過程中,從機無法解析發送的數據,接收器(從機)也會給主機反饋一個非應答位(NACK)
3、主機發送數據的過程中,從機無法再繼續接收數據,接收器(從機)也會給主機反饋一個非應答位(NACK)
4、主機從從機中讀取數據的過程中,主機不想再接收數據,主機會給從機反饋一個非應答位(NACK),注意,這種情況是主機給從機反饋一個非應答位(NACK)
關于有效應答位的圖示在上一傳輸0xaa的圖中可以清楚的看到,關于非應答位的圖示見下圖
了解清楚IIC總線在通信的過程中的幾種狀態以后接下來就具體看看IIC總線的讀寫過程吧。
1、主機通過IIC總線往從機里面寫數據
主機通過IIC總線往從機中寫數據的時候,主機首先會發送一個起始信號,接著把IIC從機的7位設備地址后面添一個0(設備地址后面的0表示主機向從機寫數據,1表示主機從從機中讀數據)組成一個8位的數據,把這個8位的數據發給從機,發完這8位的數據以后主機馬上釋放SDA信號線等待從機的應答,如果從機正確收到這個數據,從機就會發送一個有效應答位0給主機告訴主機自己已經收到了數據,主機收到從機的有效應答位以后 ,接下來主機會發送想要寫入的寄存器地址,寄存器發送完畢以后主機同樣會釋放SDA信號線等待從機的應答,從機如果正確收到了主機發過來的寄存器地址,從機會再次發送一個有效應答位給主機,主機收到從機的有效應答位0以后,接下來主機就會給從機發送想要寫入從機的數據,從機正確收到這個數據以后仍然像之前兩次一樣會給主機發送一個有效應答位,主機收到這個有效應答位以后給從機發送一個停止信號,整個傳輸過程就結束了。下圖是整個傳輸過程的示意圖:
特別注意:上圖中灰色的地方表示主機正在控制SDA信號線,白色的地方表示從機正在控制SDA信號線。
2、主機通過IIC總線從從機里面讀數據
主機通過IIC總線從從機中讀數據的過程與寫數據的過程有相似之處,但是讀數據的過程還多了一些額外的步驟。主機從從機讀數據時主機首先會發送一個起始信號,接著把IIC從機的7位設備地址后面添一個0(設備地址后面的0表示主機向從機寫數據,1表示主機從從機中讀數據),把這個8位的數據發給從機,發完這8位的數據以后主機馬上釋放SDA信號線等待從機的應答,如果從機正確收到這個數據,從機就會發送一個有效應答位0給主機告訴主機自己已經收到了數據,主機收到從機的有效應答位以后 ,接下來主機會發送想要讀的寄存器地址,寄存器發送完畢以后主機同樣會釋放SDA信號線等待從機的應答,從機如果正確收到了主機發過來的寄存器地址,從機會再次發送一個有效應答位給主機,主機收到從機的有效應答位0以后,主機會給從機再次發送一次起始信號,接著把IIC從機的7位設備地址后面添一個1(設備地址后面的0表示主機向從機寫數據,1表示主機從從機中讀數據),注意,第一次是在設備地址后面添0,這一次是在設備地址后面添1,把這個8位的數據發給從機,發完這8位的數據以后主機馬上釋放SDA信號線等待從機的應答,如果從機正確收到這個數據,從機就會發送一個有效應答位0給主機告訴主機自己已經收到了數據,接著從機繼續占用SDA信號線給主機發送寄存器中的數據,發送完畢以后,主機再次占用SDA信號線發送一個非應答信號1給從機,主機發送一個停止信號給從機結束整個讀數據的過程。下圖是整個讀數據過程的示意圖
特別注意:上圖中灰色的地方表示主機正在控制SDA信號線,白色的地方表示從機正在控制SDA信號線。
三、 目標任務
1、編寫IIC總線主機給從機發送數據的代碼,實現FPGA(主機)往EEPROM(從機)的0x23這個地址寫入0x45這個數據
2、編寫IIC總線主機從從機接收數據的代碼,實現FPGA(主機)從EEPROM(從機)的0x23這個地址讀出0x45這個數據,并用0x45這個數據的低四位驅動4個LED
四、 設計思路與Verilog代碼編寫
4.1、 IIC發送模塊的接口定義與整體設計
Verilog編寫的IIC發送模塊除了進行IIC通信的兩根信號線(SCL和SDA)以外還要包括一些時鐘、復位、使能、并行的輸入輸出以及完成標志位。其框圖如下所示
其中:
I_clk是系統時鐘;
I_rst_n是系統復位;
I_iic_send_en發送使能信號,當I_iic_send_en為1時IIC主機(FPGA)才能給IIC從機發送數據;
I_dev_addr[6:0]是IIC從機的設備地址;
I_word_addr[7:0]是字地址,也就是我們想要操作的IIC設備的內部存儲地址;
I_write_data[7:0]是主機(FPGA)要往IIC字地址中寫入的數據;
O_done_flag是主機(FPGA)發送一個字節完成標志位,發送完成后會產生一個高脈沖;
O_scl是IIC總線的串行時鐘線;
IO_sda是IIC總線的串行數據線;
要想實現iic_send模塊的功能,還是先得抽象出發送一個字節數據時序的狀態機,這里把24LC04B發送過程的時序貼一遍
注意,上圖中的控制字節(CONTROL BYTE)實際上就是代碼里面定義的7-bit設備物理地址與最后1-bit的讀寫控制位拼接組成的。
通過觀察上面的時序圖可以看出,發送一個字節的數據之前必須要先發送起始位,然后發送控制字節,接著等待應答,然后在發送字地址,接著在等待應答。數據發送完畢以后,在等待最后一個應答,應答成功后發送停止信號結束整個過程。所以,根據這個流程,可以歸納出如下幾個狀態:
狀態0:空閑狀態,用來初始化各個寄存器的值
狀態1:加載IIC設備的物理地址
狀態2:加載IIC設備的字地址
狀態3:加載要發送的數據
狀態4:發送起始信號
狀態5:發送一個字節,從高位開始發送
狀態6:接收應答狀態的應答位
狀態7:校驗應答位
狀態8:發送停止信號
狀態9:IIC寫操作結束
需要注意的是上面的各個狀態并不是按照順序執行的,有些狀態要復用多次,比如狀態5發送字節的狀態就需要復用三次用來發送三個8-bit的數據;同樣,狀態6和狀態7也要復用多次。
抽象出狀態機以后,寫代碼之前先分析一下代碼中要注意的一些關鍵點:
1、由于IIC時序要求數據線SDA在串行時鐘線的高電平保持不變,在串行時鐘線的低電平才能變化,所以代碼里面必須在串行時鐘線低電平的正中間產生一個標志位,寫代碼的時候在這個標志位處改變SDA的值,這樣就可以保證SDA在SCL的高電平期間保持穩定了。同理,由于IIC從機(24LC04)在接收到主機(FPGA)發送的有效數據以后會在SCL高電平期間產生一個有效應答信號0,所以為了保證采到的應答信號準確,必須在SCL高電平期間的正中間判斷應答信號是否滿足條件(0為有效應答,1為無效應答),因此代碼里面還必須在串行時鐘線高電平的正中間產生一個標志位,在這個標志下接收應答位并進行校驗。
這部分的代碼通過一個計數器就很容易實現,代碼如下:
?
?
parameter C_DIV_SELECT = 10'd500 ; // 分頻系數選擇 parameter C_DIV_SELECT0 = (C_DIV_SELECT >> 2) - 1 , // 用來產生IIC總線SCL低電平最中間的標志位 C_DIV_SELECT1 = (C_DIV_SELECT >> 1) - 1 , C_DIV_SELECT2 = (C_DIV_SELECT0 + C_DIV_SELECT1) + 1 , // 用來產生IIC總線SCL高電平最中間的標志位 C_DIV_SELECT3 = (C_DIV_SELECT >> 1) + 1 ; // 用來產生IIC總線SCL下降沿標志位 always @(posedge I_clk or negedge I_rst_n) begin if(!I_rst_n) R_scl_cnt <= 10'd0 ; else if(R_scl_en) begin if(R_scl_cnt == C_DIV_SELECT - 1'b1) R_scl_cnt <= 10'd0 ; else R_scl_cnt <= R_scl_cnt + 1'b1 ; end else R_scl_cnt <= 10'd0 ; end assign O_scl = (R_scl_cnt <= C_DIV_SELECT1) ? 1'b1 : 1'b0 ; // 產生串行時鐘信號O_scl assign W_scl_low_mid = (R_scl_cnt == C_DIV_SELECT2) ? 1'b1 : 1'b0 ; // 產生scl低電平正中間標志位 assign W_scl_high_mid = (R_scl_cnt == C_DIV_SELECT0) ? 1'b1 : 1'b0 ; // 產生scl高電平正中間標志位
?
2、有了SCL信號低電平正中間標志位和高電平正中間標志位以后最好還產生一個下降沿的標志位。原因是在發送第一個8-bit數據以后,處理這個8-bit數據應答位的位置在SCL信號高電平的正中間,由于要復用發送8-bit數據的那個狀態,所以必須在第二次進入發送8-bit數據的狀態時必須提前把數據再次加載好,因此可以在這個下降沿的標志來加載第二次要發送的數據,然后在SCL下降沿的正中間把8-bit數據發出去。這里必須結合代碼來理解,這里可以暫時有個印象。
3、IIC總線的SDA數據線是一個雙向IO口,關于雙向IO在Verilog代碼中如何進行處理,我在《QSPI Flash的原理與QSPI時序的實現》這篇博客已經做了說明,這里不再贅述,直接給出代碼如下:
?
module Test_inout ( input I_clk, input I_rst_n, . . . inout IO_data, . . . ) reg R_data_out ; wire I_data_in ; assign IO_data = Control ? R_data_out : 1'bz ; assign I_data_in = IO_data ; always @(posedge I_clk or negedge I_rst_n) begin . . . ; end endmodule
?
4、發送8-bit數據的整個過程如下:加載8-bit數據->發送8-bit數據->接收應答位->校驗應答位->加載第二個8-bit數據……....。所以為了復用中間標紅的這幾個狀態,必須在加載8-bit數據這個狀態提前設置好校驗應答位狀態執行完畢以后的后一個狀態的位置,這在代碼里面通過R_jump_state這個變量來完成。這一點也必須對照著代碼來進行理解。
思路理清楚以后就可以直接編寫Verilog代碼了,iic_send模塊的代碼如下:
?
module iic_send ( input I_clk , // 系統50MHz時鐘 input I_rst_n , // 系統全局復位 input I_iic_send_en , // IIC發送使能位 input [6:0] I_dev_addr , // IIC設備的物理地址 input [7:0] I_word_addr , // IIC設備的字地址,即我們想操作的IIC的內部地址 input [7:0] I_write_data , // 往IIC設備的字地址寫入的數據 output reg O_done_flag , // 讀或寫IIC設備結束標志位 // 標準的IIC設備總線 output O_scl , // IIC總線的串行時鐘線 inout IO_sda // IIC總線的雙向數據線 ); parameter C_DIV_SELECT = 10'd500 ; // 分頻系數選擇 parameter C_DIV_SELECT0 = (C_DIV_SELECT >> 2) - 1 , // 用來產生IIC總線SCL低電平最中間的標志位 C_DIV_SELECT1 = (C_DIV_SELECT >> 1) - 1 , C_DIV_SELECT2 = (C_DIV_SELECT0 + C_DIV_SELECT1) + 1 , // 用來產生IIC總線SCL高電平最中間的標志位 C_DIV_SELECT3 = (C_DIV_SELECT >> 1) + 1 ; // 用來產生IIC總線SCL下降沿標志位 reg [9:0] R_scl_cnt ; // 用來產生IIC總線SCL時鐘線的計數器 reg R_scl_en ; // IIC總線SCL時鐘線使能信號 reg [3:0] R_state ; reg R_sda_mode ; // 設置SDA模式,1位輸出,0為輸入 reg R_sda_reg ; // SDA寄存器 reg [7:0] R_load_data ; // 發送/接收過程中加載的數據,比如設備物理地址,字地址和數據等 reg [3:0] R_bit_cnt ; // 發送字節狀態中bit個數計數 reg R_ack_flag ; // 應答標志 reg [3:0] R_jump_state ; // 跳轉狀態,傳輸一個字節成功并應答以后通過這個變量跳轉到導入下一個數據的狀態 wire W_scl_low_mid ; // SCL的低電平中間標志位 wire W_scl_high_mid ; // SCL的高電平中間標志位 wire W_scl_neg ; // SCL的下降沿標志位 assign IO_sda = (R_sda_mode == 1'b1) ? R_sda_reg : 1'bz ; always @(posedge I_clk or negedge I_rst_n) begin if(!I_rst_n) R_scl_cnt <= 10'd0 ; else if(R_scl_en) begin if(R_scl_cnt == C_DIV_SELECT - 1'b1) R_scl_cnt <= 10'd0 ; else R_scl_cnt <= R_scl_cnt + 1'b1 ; end else R_scl_cnt <= 10'd0 ; end assign O_scl = (R_scl_cnt <= C_DIV_SELECT1) ? 1'b1 : 1'b0 ; // 產生串行時鐘信號O_scl assign W_scl_low_mid = (R_scl_cnt == C_DIV_SELECT2) ? 1'b1 : 1'b0 ; // 產生scl低電平正中間標志位 assign W_scl_high_mid = (R_scl_cnt == C_DIV_SELECT0) ? 1'b1 : 1'b0 ; // 產生scl高電平正中間標志位 assign W_scl_neg = (R_scl_cnt == C_DIV_SELECT3) ? 1'b1 : 1'b0 ; // 產生scl下降沿標志位 always @(posedge I_clk or negedge I_rst_n) begin if(!I_rst_n) begin R_state <= 4'd0 ; R_sda_mode <= 1'b1 ; R_sda_reg <= 1'b1 ; R_bit_cnt <= 4'd0 ; O_done_flag <= 1'b0 ; R_jump_state <= 4'd0 ; R_ack_flag <= 1'b0 ; end else if(I_iic_send_en) // 往IIC設備發送數據 begin case(R_state) 4'd0 : // 空閑狀態設置SCL與SDA均為高 begin R_sda_mode <= 1'b1 ; // 設置SDA為輸出 R_sda_reg <= 1'b1 ; // 設置SDA為高電平 R_scl_en <= 1'b0 ; // 關閉SCL時鐘線 R_state <= 4'd1 ; // 下一個狀態是加載設備物理地址狀態 R_bit_cnt <= 4'd0 ; // 發送字節狀態中bit個數計數清零 O_done_flag <= 1'b0 ; R_jump_state <= 4'd0 ; end 4'd1 : // 加載IIC設備物理地址 begin R_load_data <= {I_dev_addr, 1'b0} ; R_state <= 4'd4 ; R_jump_state <= 4'd2 ; end 4'd2 : // 加載IIC設備字地址 begin R_load_data <= I_word_addr ; R_state <= 4'd5 ; R_jump_state <= 4'd3 ; end 4'd3 : // 加載要發送的數據 begin R_load_data <= I_write_data ; R_state <= 4'd5 ; R_jump_state <= 4'd8 ; end 4'd4 : // 發送起始信號 begin R_scl_en <= 1'b1 ; // 打開SCL時鐘線 R_sda_mode <= 1'b1 ; // 設置SDA為輸出 if(W_scl_high_mid) begin R_sda_reg <= 1'b0 ; // 在SCL高電平中間把SDA信號拉低,產生起始信號 R_state <= 4'd5 ; end else R_state <= 4'd4 ; // 如果SCL高電平中間標志沒出現就一直在這個狀態等著 end 4'd5 : // 發送1個字節,從高位開始發 begin R_scl_en <= 1'b1 ; // 打開SCL時鐘線 R_sda_mode <= 1'b1 ; // 設置SDA為輸出 if(W_scl_low_mid) begin if(R_bit_cnt == 4'd8) begin R_bit_cnt <= 4'd0 ; R_state <= 4'd6 ; // 字節發完以后進入應答狀態 end else begin R_sda_reg <= R_load_data[7-R_bit_cnt] ; // 先發送高位 R_bit_cnt <= R_bit_cnt + 1'b1 ; end end else R_state <= 4'd5 ; // 字節沒發完時在這個狀態一直等待 end 4'd6 : // 接收應答狀態的應答位 begin R_scl_en <= 1'b1 ; // 打開SCL時鐘線 R_sda_mode <= 1'b0 ; // 設置SDA為輸入 if(W_scl_high_mid) begin R_ack_flag <= IO_sda ; R_state <= 4'd7 ; end else R_state <= 4'd6 ; end 4'd7 : // 校驗應答位 begin R_scl_en <= 1'b1 ; // 打開SCL時鐘線 if(R_ack_flag == 1'b0) // 校驗通過 begin if(W_scl_neg == 1'b1) begin R_state <= R_jump_state ; R_sda_mode <= 1'b1 ; // 設置SDA的模式為輸出 R_sda_reg <= 1'b0 ; // 讀取完應答信號以后要把SDA信號設置成輸出并拉低,因為如果這個狀 // 態后面是停止狀態的話,需要SDA信號的上升沿,所以這里提前拉低它 end else R_state <= 4'd7 ; end else R_state <= 4'd0 ; end 4'd8 : // 發送停止信號 begin R_scl_en <= 1'b1 ; // 打開SCL時鐘線 R_sda_mode <= 1'b1 ; // 設置SDA為輸出 if(W_scl_high_mid) begin R_sda_reg <= 1'b1 ; R_state <= 4'd9 ; end end 4'd9 : // IIC寫操作結束 begin R_scl_en <= 1'b0 ; // 關閉SCL時鐘線 R_sda_mode <= 1'b1 ; // 設置SDA為輸出 R_sda_reg <= 1'b1 ; // 拉高SDA保持空閑狀態情況 O_done_flag <= 1'b1 ; R_state <= 4'd0 ; R_ack_flag <= 1'b0 ; end default : R_state <= 4'd0 ; endcase end else begin R_state <= 4'd0 ; R_sda_mode <= 1'b1 ; R_sda_reg <= 1'b1 ; R_bit_cnt <= 4'd0 ; O_done_flag <= 1'b0 ; R_jump_state <= 4'd0 ; R_ack_flag <= 1'b0 ; end end wire [35:0] CONTROL0 ; wire [54:0] TRIG0 ; icon icon_inst ( .CONTROL0(CONTROL0) // INOUT BUS [35:0] ); ila ila_inst ( .CONTROL(CONTROL0), // INOUT BUS [35:0] .CLK(I_clk), // IN .TRIG0(TRIG0) // IN BUS [49:0] ); assign TRIG0[0] = O_scl ; assign TRIG0[1] = IO_sda ; assign TRIG0[11:2] = R_scl_cnt ; assign TRIG0[12] = R_scl_en ; assign TRIG0[16:13] = R_state ; assign TRIG0[17] = R_sda_mode ; assign TRIG0[18] = R_sda_reg ; assign TRIG0[26:19] = R_load_data ; assign TRIG0[30:27] = R_bit_cnt ; assign TRIG0[31] = R_ack_flag ; assign TRIG0[36:32] = R_jump_state ; assign TRIG0[37] = W_scl_low_mid ; assign TRIG0[38] = W_scl_high_mid ; assign TRIG0[39] = O_done_flag ; assign TRIG0[40] = I_rst_n ; endmodule
?
整個代碼的流程與之前分析的流程完全一致。本來想寫一個測試文件用ModelSim進行基本的仿真,但是由于應答信號是取決于IIC從設備的,所以還是決定用ChipScope直接抓。在用ChipScope抓之前先寫一個頂層文件把上面的代碼例化進去,頂層代碼如下:
?
module iic_send_top ( input I_clk , // 系統50MHz時鐘 input I_rst_n , // 系統全局復位 // 標準的IIC設備總線 output O_scl , // IIC總線的串行時鐘線 inout IO_sda // IIC總線的雙向數據線 ); wire W_done_flag ; iic_send U_iic_send ( .I_clk (I_clk ), // 系統50MHz時鐘 .I_rst_n (I_rst_n ), // 系統全局復位 .I_iic_send_en (1'b1 ), // 發送使能位,高電平有效 .I_dev_addr (7'b1010_000 ), // IIC設備的物理地址 .I_word_addr (8'h23 ), // IIC設備的字地址,即我們想操作的IIC的內部地址 .I_write_data (8'h45 ), // 往IIC設備的字地址寫入的數據 .O_done_flag (W_done_flag ), // 讀或寫IIC設備結束標志位 // 標準的IIC設備總線 .O_scl (O_scl ), // IIC總線的串行時鐘線 .IO_sda (IO_sda ) // IIC總線的雙向數據線 ); endmodule
?
綁定好管腳以后就可以生成bit文件下載到FPGA里面用ChipScope抓時序了,下面是我抓到的時序圖:
為了更清晰的說明上面的時序,我把起始信號,停止信號,每個比特以及應答位全部框出來進一步解釋如下:
通過上面的時序圖可以清楚的看到:
1號紅框是起始信號,在SCL高電平期間SDA有一個下降沿
2~9號紅框是發送設備物理地址8’ha0(8’b1010_0000)
10號紅框是應答位,在這個期間R_sda_mode保持低電平,SDA為輸入
11~18號紅框是發送字地址8’h23(8’b0010_0011)
19號紅框是應答位,在這個期間R_sda_mode保持低電平,SDA為輸入
20~27號紅框是發送數據8’h45(8’b0100_0101)
28號紅框是應答位,在這個期間R_sda_mode保持低電平,SDA為輸入
29號紅框是停止信號,在SCL高電平期間SDA有一個上升沿
其他變量的時序細節這里不再展開,大家可以自己抓出來。至此,IIC發送模塊全部設計完畢。
4.2、 IIC接收模塊的接口定義與整體設計
Verilog編寫的IIC接收模塊除了進行IIC通信的兩根信號線(SCL和SDA)以外還要包括一些時鐘、復位、使能、并行的輸入輸出以及完成標志位。其框圖如下所示
其中:
I_clk是系統時鐘;
I_rst_n是系統復位;
I_iic_recv_en接收使能信號,當I_iic_recv_en為1時IIC主機(FPGA)才能從IIC從機接收數據;
I_dev_addr[6:0]是IIC從機的設備地址;
I_word_addr[7:0]是字地址,也就是我們想要讀取的IIC設備的內部存儲地址;
O_read_data[7:0]是主機(FPGA)從IIC設備字地址中讀取的數據;
O_done_flag是主機(FPGA)接收一個字節完成標志位,接收完成后會產生一個高脈沖;
O_scl是IIC總線的串行時鐘線;
IO_sda是IIC總線的串行數據線;
要想實現iic_send模塊的功能,還是先得抽象出發送一個字節數據時序的狀態機,這里把24LC04B接收過程的時序貼一遍
注意,上圖中的控制字節(CONTROL BYTE)實際上就是代碼里面定義的7-bit設備物理地址與最后1-bit讀寫控制位組成的。
通過觀察上面的時序圖可以看出,接收一個字節的數據的過程與發送一個字節數據相比多了一個第二次的起始信號與控制字節(CONTROL BYTE),而且第二個控制字節(CONTROL BYTE)的最低位應該為1,表示IIC主機(FPGA)從IIC從機(24LC04)中讀數據,當主機(FPGA)想結束讀數據的過程時,它會給IIC設備發送一個非應答位1,最后在發送停止信號結束整個讀數據的過程。所以,根據這個流程,可以歸納出如下幾個狀態:
狀態0:空閑狀態,用來初始化各個寄存器的值
狀態1:加載IIC設備的物理地址
狀態2:加載IIC設備的字地址
狀態3:發送第一個起始信號(讀過程要求發送兩次起始信號)
狀態4:發送一個字節數據,從高位開始發送
狀態5:接收應答狀態的應答位
狀態6:校驗應答位
狀態7:發送第二個起始信號(讀過程要求發送兩次起始信號
狀態8:再次加載IIC設備的物理地址,但這次物理地址最后一位應該為1,表示讀操作
狀態9:接收一個字節數據,從高位開始接收
狀態10:主機發送一個非應答信號1給從機
狀態11:等確定從機收到這個非應答信號1以后,初始化SDA的值為0,準備產生停止信號
狀態12:發送停止信號
狀態13:讀操作結束
需要注意的是上面的各個狀態和發送模塊一樣,并不是按照順序執行的,有些狀態也要復用多次。
接收模塊有以下幾個關鍵點要注意:
1、和發送模塊一樣,需要產生SCL信號高電平中間標志位,低電平中間標志位以及下降沿標志位
2、由于讀數據的過程需要發送第二次起始位,而起始位的條件是在SCL高電平期間SDA有一個下降沿,所以一定要在處理完寫設備地址與寫字地址的應答位之后,在SCL的下降沿標志處把SDA信號設置成輸出并拉高方便產生第二次起始信號。具體細節對照著代碼理解。
3、第一次發送的設備物理地址的最低位是0,表示寫數據;第二次發送的設備物理地址的最低位是1,表示讀數據
4、讀完一個字節數據以后,一定要記住是主機(FPGA)給從機(24LC04)發送一個非應答信號1
有了上面這些儲備以后就可以編寫接收模塊的代碼了,接收模塊的代碼如下:
?
module iic_recv ( input I_clk , // 系統50MHz時鐘 input I_rst_n , // 系統全局復位 input I_iic_recv_en , // IIC發送使能位 input [6:0] I_dev_addr , // IIC設備的物理地址 input [7:0] I_word_addr , // IIC設備的字地址,即我們想操作的IIC的內部地址 output reg [7:0] O_read_data , // 從IIC設備的字地址讀出來的數據 output reg O_done_flag , // 讀或寫IIC設備結束標志位 // 標準的IIC設備總線 output O_scl , // IIC總線的串行時鐘線 inout IO_sda // IIC總線的雙向數據線 ); parameter C_DIV_SELECT = 10'd500 ; // 分頻系數選擇 parameter C_DIV_SELECT0 = (C_DIV_SELECT >> 2) - 1 , // 用來產生IIC總線SCL低電平最中間的標志位 C_DIV_SELECT1 = (C_DIV_SELECT >> 1) - 1 , // 用來產生IIC串行時鐘線 C_DIV_SELECT2 = (C_DIV_SELECT0 + C_DIV_SELECT1) + 1 , // 用來產生IIC總線SCL高電平最中間的標志位 C_DIV_SELECT3 = (C_DIV_SELECT >> 1) + 1 ; // 用來產生IIC總線SCL下降沿標志位 reg [9:0] R_scl_cnt ; // 用來產生IIC總線SCL時鐘線的計數器 reg R_scl_en ; // IIC總線SCL時鐘線使能信號 reg [3:0] R_state ; reg R_sda_mode ; // 設置SDA模式,1位輸出,0為輸入 reg R_sda_reg ; // SDA寄存器 reg [7:0] R_load_data ; // 發送/接收過程中加載的數據,比如設備物理地址,字地址和數據等 reg [3:0] R_bit_cnt ; // 發送字節狀態中bit個數計數 reg R_ack_flag ; // 應答標志 reg [3:0] R_jump_state ; // 跳轉狀態,傳輸一個字節成功并應答以后通過這個變量跳轉到導入下一個數據的狀態 reg [7:0] R_read_data_reg ; wire W_scl_low_mid ; // SCL的低電平中間標志位 wire W_scl_high_mid ; // SCL的高電平中間標志位 assign IO_sda = (R_sda_mode == 1'b1) ? R_sda_reg : 1'bz ; always @(posedge I_clk or negedge I_rst_n) begin if(!I_rst_n) R_scl_cnt <= 10'd0 ; else if(R_scl_en) begin if(R_scl_cnt == C_DIV_SELECT - 1'b1) R_scl_cnt <= 10'd0 ; else R_scl_cnt <= R_scl_cnt + 1'b1 ; end else R_scl_cnt <= 10'd0 ; end assign O_scl = (R_scl_cnt <= C_DIV_SELECT1) ? 1'b1 : 1'b0 ; // 產生串行時鐘信號O_scl assign W_scl_low_mid = (R_scl_cnt == C_DIV_SELECT2) ? 1'b1 : 1'b0 ; // 產生scl低電平正中間標志位 assign W_scl_high_mid = (R_scl_cnt == C_DIV_SELECT0) ? 1'b1 : 1'b0 ; // 產生scl高電平正中間標志位 assign W_scl_neg = (R_scl_cnt == C_DIV_SELECT3) ? 1'b1 : 1'b0 ; // 產生scl下降沿標志位 always @(posedge I_clk or negedge I_rst_n) begin if(!I_rst_n) begin R_state <= 4'd0 ; R_sda_mode <= 1'b1 ; R_sda_reg <= 1'b1 ; R_bit_cnt <= 4'd0 ; O_done_flag <= 1'b0 ; R_jump_state <= 4'd0 ; R_read_data_reg <= 8'd0 ; R_ack_flag <= 1'b0 ; O_read_data <= 8'd0 ; end else if(I_iic_recv_en) // 往IIC設備發送數據 begin case(R_state) 4'd0 : // 空閑狀態,用來初始化相關所有信號 begin R_sda_mode <= 1'b1 ; // 設置SDA為輸出 R_sda_reg <= 1'b1 ; // 設置SDA為高電平 R_scl_en <= 1'b0 ; // 關閉SCL時鐘線 R_state <= 4'd1 ; // 下一個狀態是加載設備物理地址狀態 R_bit_cnt <= 4'd0 ; O_done_flag <= 1'b0 ; R_jump_state <= 5'd0 ; R_read_data_reg <= 8'd0 ; end 4'd1 : // 加載IIC設備物理地址 begin R_load_data <= {I_dev_addr, 1'b0} ; R_state <= 4'd3 ; // 加載完設備物理地址以后進入起始狀態 R_jump_state <= R_state + 1'b1 ; end 4'd2 : // 加載IIC設備字地址 begin R_load_data <= I_word_addr ; R_state <= 4'd4 ; R_jump_state <= R_state + 5'd5 ; // 設置這里是為了這一輪發送并應答后跳到第二次啟始位 end 4'd3 : // 發送第一個起始信號 begin R_scl_en <= 1'b1 ; // 打開時鐘 R_sda_mode <= 1'b1 ; // 設置SDA的模式為輸出 if(W_scl_high_mid) begin R_sda_reg <= 1'b0 ; // 在SCL高電平的正中間把SDA引腳拉低產生一個下降沿 R_state <= 4'd4 ; // 下一個狀態是發送一個字節數據(IIC設備的物理地址) end else R_state <= 4'd3 ; end 4'd4 : // 發送一個字節 begin R_scl_en <= 1'b1 ; // 打開時鐘 R_sda_mode <= 1'b1 ; // 設置SDA的模式為輸出 if(W_scl_low_mid) // 在SCL低電平的最中間改變數據 begin if(R_bit_cnt == 4'd8) begin R_bit_cnt <= 4'd0 ; R_state <= 4'd5 ; end else begin R_sda_reg <= R_load_data[7-R_bit_cnt] ; R_bit_cnt <= R_bit_cnt + 1'b1 ; end end else R_state <= 4'd4 ; end 4'd5 : // 接收應答狀態應答位 begin R_scl_en <= 1'b1 ; // 打開時鐘 R_sda_reg <= 1'b0 ; R_sda_mode <= 1'b0 ; // 設置SDA的模式為輸入 if(W_scl_high_mid) begin R_ack_flag <= IO_sda ; R_state <= 4'd6 ; end else R_state <= 4'd5 ; end 4'd6 : // 校驗應答位 begin R_scl_en <= 1'b1 ; // 打開時鐘 if(R_ack_flag == 1'b0) // 校驗通過 begin if(W_scl_neg == 1'b1) begin R_state <= R_jump_state ; R_sda_mode <= 1'b1 ; // 設置SDA的模式為輸出 R_sda_reg <= 1'b1 ; // 設置SDA的引腳電平拉高,方便后面產生第二次起始位 end else R_state <= 4'd6 ; end else R_state <= 4'd0 ; end 4'd7 : // 第二次起始位(IIC讀操作要求有2次起始位) begin R_scl_en <= 1'b1 ; // 打開時鐘 R_sda_mode <= 1'b1 ; // 設置SDA的模式為輸出 if(W_scl_high_mid) begin R_sda_reg <= 1'b0 ; R_state <= 4'd8 ; end else R_state <= 4'd7 ; end 4'd8 : // 再次加載IIC設備物理地址 ,但這次地址最后一位應該為1,表示讀操作 begin R_load_data <= {I_dev_addr, 1'b1} ; // 前7bit是設備物理地址,最后一位1表示讀操作 R_state <= 4'd4 ; R_jump_state <= 4'd9 ; // 設置這里是為了這一輪發送并應答后跳到第二次啟始位 end 4'd9 : // 讀一個字節數據 begin R_scl_en <= 1'b1 ; // 打開時鐘 R_sda_mode <= 1'b0 ; // 設置SDA的模式為輸入 if(W_scl_high_mid) begin if(R_bit_cnt == 4'd7) begin R_bit_cnt <= 4'd0 ; R_state <= 4'd10 ; O_read_data <= {R_read_data_reg[6:0],IO_sda} ; end else begin R_read_data_reg <= {R_read_data_reg[6:0],IO_sda} ; R_bit_cnt <= R_bit_cnt + 1'b1 ; end end else R_state <= 4'd9 ; end 4'd10 : // 讀完一個字節數據以后進入10,主機發送一個非應答信號1 begin R_scl_en <= 1'b1 ; // 打開時鐘 R_sda_mode <= 1'b1 ; // 設置SDA的模式為輸入 if(W_scl_low_mid) begin R_state <= 4'd11 ; R_sda_reg <= 1'b1 ; end else R_state <= 4'd10 ; end 4'd11 : begin R_scl_en <= 1'b1 ; // 打開時鐘 R_sda_mode <= 1'b1 ; // 設置SDA的模式為輸入 if(W_scl_low_mid) begin R_state <= 4'd12 ; R_sda_reg <= 1'b0 ; end else R_state <= 4'd11 ; end 4'd12 : //停止位Stop begin R_scl_en <= 1'b1 ; // 打開時鐘 R_sda_mode <= 1'b1 ; // 設置SDA的模式為輸出 if(W_scl_high_mid) begin R_sda_reg <= 1'b1 ; R_state <= 4'd13 ; end else R_state <= 4'd12 ; end 4'd13 : begin R_scl_en <= 1'b0 ; // 關閉SCL時鐘線 R_sda_mode <= 1'b1 ; // 設置SDA為輸出 R_sda_reg <= 1'b1 ; // 拉高SDA保持空閑狀態情況 O_done_flag <= 1'b1 ; R_state <= 4'd0 ; R_read_data_reg <= 8'd0 ; end default: R_state <= 4'd0 ; endcase end else begin R_state <= 4'd0 ; R_sda_mode <= 1'b1 ; R_sda_reg <= 1'b1 ; R_bit_cnt <= 4'd0 ; O_done_flag <= 1'b0 ; R_jump_state <= 4'd0 ; R_read_data_reg <= 8'd0 ; R_ack_flag <= 1'b0 ; end end wire [35:0] CONTROL0 ; wire [54:0] TRIG0 ; icon icon_inst ( .CONTROL0(CONTROL0) // INOUT BUS [35:0] ); ila ila_inst ( .CONTROL(CONTROL0), // INOUT BUS [35:0] .CLK(I_clk), // IN .TRIG0(TRIG0) // IN BUS [49:0] ); assign TRIG0[0] = O_scl ; assign TRIG0[1] = IO_sda ; assign TRIG0[11:2] = R_scl_cnt ; assign TRIG0[12] = R_scl_en ; assign TRIG0[16:13] = R_state ; assign TRIG0[17] = R_sda_mode ; assign TRIG0[18] = R_sda_reg ; assign TRIG0[26:19] = R_load_data ; assign TRIG0[30:27] = R_bit_cnt ; assign TRIG0[31] = R_ack_flag ; assign TRIG0[36:32] = R_jump_state ; assign TRIG0[37] = W_scl_low_mid ; assign TRIG0[38] = W_scl_high_mid ; assign TRIG0[39] = O_done_flag ; assign TRIG0[40] = I_rst_n ; assign TRIG0[48:41] = O_read_data ; assign TRIG0[49] = W_scl_neg ; endmodule
?
整個代碼的流程與之前分析的流程完全一致。在用ChipScope抓之前先寫一個頂層文件把上面的代碼例化進去,頂層代碼如下:
?
module iic_recv_top ( input I_clk , // 系統50MHz時鐘 input I_rst_n , // 系統全局復位 output [3:0] O_led_out , // 從IIC設備的字地址讀出來的數據 // 標準的IIC設備總線 output O_scl , // IIC總線的串行時鐘線 inout IO_sda // IIC總線的雙向數據線 ); wire W_done_flag ; wire [7:0] W_read_data ; // 從IIC設備的字地址讀出來的數據 assign O_led_out = W_read_data[3:0] ; iic_recv U_iic_recv ( .I_clk (I_clk ), // 系統50MHz時鐘 .I_rst_n (I_rst_n ), // 系統全局復位 .I_iic_recv_en (1'b1 ), // 接收使能位,高電平有效 .I_dev_addr (7'b1010_000 ), // IIC設備的物理地址 .I_word_addr (8'h23 ), // IIC設備的字地址,即我們想操作的IIC的內部地址 .O_read_data (W_read_data ), // 從IIC設備的字地址讀出來的數據 .O_done_flag (W_done_flag ), // 讀或寫IIC設備結束標志位 // 標準的IIC設備總線 .O_scl (O_scl ), // IIC總線的串行時鐘線 .IO_sda (IO_sda ) // IIC總線的雙向數據線 ); endmodule
?
綁定好管腳以后就可以生成bit文件下載到FPGA里面用ChipScope抓時序了,由于EEPROM是一種非易失性存儲器,所以做在IIC發送數據的實驗中往24LC04的0x23地址中的0x45這個數據在掉電以后并不會丟失。剛好可以通過這個接收模塊給讀出來,并用讀出數據的最低位驅動四個LED燈,如果時序正確的話,四個LED燈會間隔亮起來。下面是我抓到的接收數據時序圖:
通過上面的時序圖可以清楚的看到成功讀出了EEPROM中的0x45這個數據,并且我板子上的四個LED燈也間隔亮了起來。
為了更清晰的說明上面的時序,我把起始信號,停止信號,每個比特,應答位和非應答位全部框出來進一步解釋如下:
1號紅框是起始信號,在SCL高電平期間SDA有一個下降沿
2~9號紅框是發送設備物理地址8’ha0(8’b1010_0000)
10號紅框是應答位,在這個期間R_sda_mode保持低電平,SDA為輸入
11~18號紅框是發送字地址8’h23(8’b0010_0011)
19號紅框是應答位,在這個期間R_sda_mode保持低電平,SDA為輸入
20號紅框是第二次起始位,在SCL高電平期間SDA有一個下降沿
21~28號紅框是發送數據8’ha1(8’b1010_0001)
29號紅框是應答位,在這個期間R_sda_mode保持低電平,SDA為輸入
30~37號紅框是讀出的8-bit數據8’h45(8’b0100_0101),在這個期間R_sda_mode保持低電平,SDA為輸入
38號紅框是非應答位,在這個期間R_sda_mode保持高電平,主機(FPGA)通過SDA輸出一個非應答位1
39號紅框是停止信號,在SCL高電平期間SDA有一個上升沿
其他變量的時序細節這里不再展開,大家可以自己抓出來。至此,IIC接收模塊全部設計完畢。
五、 進一步思考
5.1、 24LC04寫數據操作要注意的地方
Following the start condition from the master, the device code (4 bits), the block address (3 bits), and the R/W bit which is a logic low is placed onto the bus by the master transmitter. This indicates to the addressed slave receiver that a byte with a word address will follow after it has generated an acknowledge bit during the ninth clock cycle. Therefore the next byte transmitted by the master is the word address and will be written into the address pointer of the 24LC04B/08B. After receiving another acknowledge signal from the 24LC04B/08B the master device will transmit the data word to be written into the addressed memory location.The 24LC04B/08B acknowledges again and the master generates a stop condition. This initiates the internal write cycle, and during this time the 24LC04B/08B will not generate acknowledge signals。
這是24LC04芯片手冊對它的寫操作的描述, 所以我們寫進去的數據其實是放在24LC04的一個緩沖區中,等主機(FPGA)發送停止信號以后24LC04內部才開始工作把緩沖區中的數據寫入它內部的ROM中,在這個過程中24LC04將不發送有效應答信號,所以當發送完停止信號又立馬給一個起始信號重新發送時會出現下面的時序
這種情況由于24LC04內部還在處理緩沖區中的數據,所以即使主機(FPGA)發送了正確的時序,從機(24LC04)也不會有效應答。
5.2、 IIC設備多字節連續讀寫操作
24LC04支持16-Bytes的連續寫操作,當超過16-Bytes是后面寫入的數據會覆蓋先前寫入的數據,下面是關于這一段的描述:
The write control byte, word address and the first data byte are transmitted to the 24LC04B/08B in the same way as in a byte write. But instead of generating a stop condition the master transmits up to 16 data bytes to the 24LC04B/08B which are temporarily stored in the on-chip page buffer and will be written into the memory after the master has transmitted a stop condition. After the receipt of each word, the four lower order address pointer bits are internally incremented by one. The higher order seven bits of the word address remains constant. If the master should transmit more than 16 words prior to generating the stop condition, the address counter will roll over and the previously received data will be overwritten. As with the byte write operation, once the stop condition is received an internal write cycle will begin.
時序圖如下所示:
其實要實現這個時序并不是難事,只要多增加幾個加載數據的狀態就可以了,大家可以直接在上面發送數據模塊的基礎上改。
24LC04支持整塊存儲器的連續讀操作,下面是關于這一段的描述:
Sequential reads are initiated in the same way as a random read except that after the 24LC04B/08B transmits the first data byte, the master issues an acknowledge as opposed to a stop condition in a random read. This directs the 24LC04B/08B to transmit the next sequentially addressed 8-bit word (Figure 7-3).To provide sequential reads the 24LC04B/08B contains an internal address pointer which is incremented by one at the completion of each operation. This address pointer allows the entire memory contents to be serially read during one operation.
時序圖如下所示:
有了上面接收模塊的基礎,實現這段時序應該也不算困難。以后有空再實現。
審核編輯:劉清
評論
查看更多