天天看點

IIC總線的原理與Verilog實作1、 軟體平台與硬體平台2、 原理介紹3、 目标任務4、 設計思路及Verilog代碼編寫5. 思考

IIC總線的原理與Verilog實作

  • 1、 軟體平台與硬體平台
  • 2、 原理介紹
    • 2.1 IIC總線的特點:
    • 2.2 IIC總線協定詳解:
      • 2.2.1 IIC主機往從機裡面寫入資料的步驟
      • 2.2.2 IIC主機從從機裡面讀出資料的步驟
      • 2.2.3 通信狀态
        • 空閑狀态
        • 起始狀态和結束狀态
        • 有效的資料位傳輸
        • 應答信号與非應答信号
      • 2.2.4 讀寫過程
        • 主機通過IIC總線往從機裡面寫資料
        • 主機通過IIC總線從從機裡面讀資料
  • 3、 目标任務
  • 4、 設計思路及Verilog代碼編寫
    • 4.1 IIC發送子產品的接口定義與整體設計
    • 4.2 IIC接收子產品的接口定義與整體設計
  • 5. 思考
    • 5.1 24LC04寫資料操作要注意的地方
    • 5.2 IIC裝置多位元組連續讀寫操作

1、 軟體平台與硬體平台

軟體平台:

  1. 作業系統:Windows10
  2. 開發套件:ISE14.7
  3. 仿真工具:ModelSim-10.4-SE 、ChipScope

硬體平台:

  1. FPGA型号:Xilinx公司的XC6SLX45-2CSG324
  2. EEPROM型号:Microchip公司的AT24LC04B

2、 原理介紹

IIC(Inter-Integrated Circuit)總線是一種由PHILIPS公司開發的兩線式串行總線,用于連接配接微控制器及其外圍裝置。I2C總線産生于在80年代,最初為音頻和視訊裝置開發,如今主要在伺服器管理中使用,其中包括單個元件狀态的通信。例如管理者可對各個元件進行查詢,以管理系統的配置或掌握元件的功能狀态,如電源和系統風扇。可随時監控記憶體、硬碟、網絡、系統溫度等多個參數,增加了系統的安全性,友善了管理。IIC資料傳輸速率有标準模式(100 kbps)、快速模式(400 kbps)和高速模式(3.4 Mbps),另外一些變種實作了低速模式(10 kbps)和快速+模式(1 Mbps)。

下圖是一個嵌入式系統中處理器僅通過2根線的IIC總線控制多個IIC外設的典型應用圖

IIC總線的原理與Verilog實作1、 軟體平台與硬體平台2、 原理介紹3、 目标任務4、 設計思路及Verilog代碼編寫5. 思考

圖中處理器是IIC主機,它僅僅通過兩根信号就可以控制IO擴充器,各種不同的傳感器,EEPROM,AD/DAs等裝置,這也是IIC總線協定相較于其他協定最有優勢的地方。

2.1 IIC總線的特點:

  1. 簡單性和有效性。由于接口直接在元件之上,是以I2C總線占用的空間非常小,減少了電路闆的空間和晶片管腳的數量,降低了互聯成本。總線的長度可高達25英尺,并且能夠以10Kbps的最大傳輸速率支援40個元件。
  2. 支援多主要(multimastering), 其中任何能夠進行發送和接收的裝置都可以成為主總線。一個主要能夠控制信号的傳輸和時鐘頻率。當然,在任何時間點上隻能有一個主要占用IIC總線。

2.2 IIC總線協定詳解:

IIC總線接口是一個标準的雙向傳輸接口,一次資料傳輸需要主機和從機按照IIC協定的标準進行。I2C總線是由資料線SDA和時鐘SCL構成的串行總線,可發送和接收資料,并且在硬體上都需要接一個上拉電阻到VCC。各種被控制電路均并聯在這條總線上,但就像電話機一樣隻有撥通各自的号碼才能工作,是以每個電路和子產品都有唯一的位址,這樣,各控制電路雖然挂在同一條總線上,卻彼此獨立,互不相關。

2.2.1 IIC主機往從機裡面寫入資料的步驟

  1. 主機發送一個起始信号和從機的裝置位址給從機
  2. 主機發送資料給從機
  3. 主機發送一個停止信号結束發送過程

2.2.2 IIC主機從從機裡面讀出資料的步驟

  1. 主機發送一個起始信号和從機的裝置位址給從機
  2. 主機發送一個要讀取的位址給從機
  3. 主機從從機接收資料
  4. 主機發送一個停止信号給從機結束整個接收過程

2.2.3 通信狀态

總的來說,IIC總線在通信的過程中一共有一下幾種狀态:

空閑狀态

IIC 總線的 SDA 和 SCL 兩條信号線同時處于高電平時,規定為總線的空閑狀态。此時各個器件的輸出級場效應管均處在截止狀态,即釋放總線,由兩條信号線各自的上拉電阻把電平拉高。

起始狀态和結束狀态

在時鐘線 SCL 保持高電平期間,資料線 SDA 上的電平被拉低(即負跳變),定義為 I2C 總線總線的起始信号,它标志着一次資料傳輸的開始。起始信号是由主要器主動建立的,在建立該信号之前 I2C 總線必須處于空閑狀态。

在時鐘線 SCL 保持高電平期間,資料線 SDA 被釋放,使得 SDA 傳回高電平(即正跳變),稱為 I2C 總線的停止信号,它标志着一次資料傳輸的終止。停止信号也是由主要器主動建立的,建立該信号之後,I2C 總線将傳回空閑狀态。

起始信号和結束信号如下圖所示:

IIC總線的原理與Verilog實作1、 軟體平台與硬體平台2、 原理介紹3、 目标任務4、 設計思路及Verilog代碼編寫5. 思考

有效的資料位傳輸

在 IIC 總線上傳送的每一位資料都有一個時鐘脈沖相對應(或同步控制),即在 SCL 串行時鐘的配合下,資料在 SDA 上從高位向低位依次串行傳送每一位的資料。進行資料傳送時,在 SCL 呈現高電平期間,SDA 上的電平必須保持穩定,低電平為資料 0,高電平為資料 1。隻有在 SCL 為低電平期間,才允許 SDA 上的電平改變狀态。下圖是0xaa在IIC總線上有效傳輸(有效傳輸是指第9個時鐘的高電平期間,從機給主機回報了一個有效的應答位的圖示:

IIC總線的原理與Verilog實作1、 軟體平台與硬體平台2、 原理介紹3、 目标任務4、 設計思路及Verilog代碼編寫5. 思考

應答信号與非應答信号

I2C 總線上的所有資料都是以 8 位位元組傳送的,發送器(主機)每發送一個位元組,就在第9個時鐘脈沖期間釋放資料線,由接收器(從機)回報一個應答信号。應答信号為低電平時,規定為有效應答位(ACK簡稱應答位),表示接收器已經成功地接收了該位元組;應答信号為高電平時,規定為非應答位(NACK),一般表示接收器接收該位元組沒有成功。對于回報有效應答位 ACK 的要求是,接收器在第 9 個時鐘脈沖之前的低電平期間将 SDA 線拉低,并且確定在該時鐘的高電平期間為穩定的低電平。

對非應答位(NACK)還要特别說明的是,還有以下四種情況IIC通信過程中會産生非應答位:

  1. 接收器(從機)正在處理某些實時的操作無法與主機實作IIC通信的時候,接收器(從機)會給主機回報一個非應答位(NACK)
  2. 主機發送資料的過程中,從機無法解析發送的資料,接收器(從機)也會給主機回報一個非應答位(NACK)
  3. 主機發送資料的過程中,從機無法再繼續接收資料,接收器(從機)也會給主機回報一個非應答位(NACK)
  4. 主機從從機中讀取資料的過程中,主機不想再接收資料,主機會給從機回報一個非應答位(NACK),注意,這種情況是主機給從機回報一個非應答位(NACK)

關于有效應答位的圖示在上一傳輸0xaa的圖中可以清楚的看到,關于非應答位的圖示見下圖:

IIC總線的原理與Verilog實作1、 軟體平台與硬體平台2、 原理介紹3、 目标任務4、 設計思路及Verilog代碼編寫5. 思考

2.2.4 讀寫過程

了解清楚IIC總線在通信的過程中的幾種狀态以後接下來就具體看看IIC總線的讀寫過程吧。

主機通過IIC總線往從機裡面寫資料

主機通過IIC總線往從機中寫資料的時候,主機首先會發送一個起始信号,接着把IIC從機的7位裝置位址後面添一個0(裝置位址後面的0表示主機向從機寫資料,1表示主機從從機中讀資料)組成一個8位的資料,把這個8位的資料發給從機,發完這8位的資料以後主機馬上釋放SDA信号線等待從機的應答,如果從機正确收到這個資料,從機就會發送一個有效應答位0給主機告訴主機自己已經收到了資料,主機收到從機的有效應答位以後 ,接下來主機會發送想要寫入的寄存器位址,寄存器發送完畢以後主機同樣會釋放SDA信号線等待從機的應答,從機如果正确收到了主機發過來的寄存器位址,從機會再次發送一個有效應答位給主機,主機收到從機的有效應答位0以後,接下來主機就會給從機發送想要寫入從機的資料,從機正确收到這個資料以後仍然像之前兩次一樣會給主機發送一個有效應答位,主機收到這個有效應答位以後給從機發送一個停止信号,整個傳輸過程就結束了。下圖是整個傳輸過程的示意圖:

IIC總線的原理與Verilog實作1、 軟體平台與硬體平台2、 原理介紹3、 目标任務4、 設計思路及Verilog代碼編寫5. 思考

特别注意:上圖中灰色的地方表示主機正在控制SDA信号線,白色的地方表示從機正在控制SDA信号線。

主機通過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給從機,主機發送一個停止信号給從機結束整個讀資料的過程。下圖是整個讀資料過程的示意圖:

IIC總線的原理與Verilog實作1、 軟體平台與硬體平台2、 原理介紹3、 目标任務4、 設計思路及Verilog代碼編寫5. 思考

特别注意:上圖中灰色的地方表示主機正在控制SDA信号線,白色的地方表示從機正在控制SDA信号線。

3、 目标任務

  1. 編寫IIC總線主機給從機發送資料的代碼,實作FPGA(主機)往EEPROM(從機)的0x23這個位址寫入0x45這個資料
  2. 編寫IIC總線主機從從機接收資料的代碼,實作FPGA(主機)從EEPROM(從機)的0x23這個位址讀出0x45這個資料,并用0x45這個資料的低四位驅動4個LED

4、 設計思路及Verilog代碼編寫

4.1 IIC發送子產品的接口定義與整體設計

Verilog編寫的IIC發送子產品除了進行IIC通信的兩根信号線(SCL和SDA)以外還要包括一些時鐘、複位、使能、并行的輸入輸出以及完成标志位。其框圖如下所示:

IIC總線的原理與Verilog實作1、 軟體平台與硬體平台2、 原理介紹3、 目标任務4、 設計思路及Verilog代碼編寫5. 思考

其中:

  • 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發送過程的時序貼一遍

IIC總線的原理與Verilog實作1、 軟體平台與硬體平台2、 原理介紹3、 目标任務4、 設計思路及Verilog代碼編寫5. 思考

注意,上圖中的控制位元組(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高電平正中間标志位
           
  1. 有了SCL信号低電平正中間标志位和高電平正中間标志位以後最好還産生一個下降沿的标志位。原因是在發送第一個8-bit資料以後,處理這個8-bit資料應答位的位置在SCL信号高電平的正中間,由于要複用發送8-bit資料的那個狀态,是以必須在第二次進入發送8-bit資料的狀态時必須提前把資料再次加載好,是以可以在這個下降沿的标志來加載第二次要發送的資料,然後在SCL下降沿的正中間把8-bit資料發出去。這裡必須結合代碼來了解,這裡可以暫時有個印象。
  2. 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
           
  1. 發送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抓時序了,下面是我抓到的時序圖:

IIC總線的原理與Verilog實作1、 軟體平台與硬體平台2、 原理介紹3、 目标任務4、 設計思路及Verilog代碼編寫5. 思考

為了更清晰的說明上面的時序,我把起始信号,停止信号,每個比特以及應答位全部框出來進一步解釋如下:

IIC總線的原理與Verilog實作1、 軟體平台與硬體平台2、 原理介紹3、 目标任務4、 設計思路及Verilog代碼編寫5. 思考

通過上面的時序圖可以清楚的看到:

  • 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)以外還要包括一些時鐘、複位、使能、并行的輸入輸出以及完成标志位。其框圖如下所示:

IIC總線的原理與Verilog實作1、 軟體平台與硬體平台2、 原理介紹3、 目标任務4、 設計思路及Verilog代碼編寫5. 思考

其中:

  • 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接收過程的時序貼一遍

IIC總線的原理與Verilog實作1、 軟體平台與硬體平台2、 原理介紹3、 目标任務4、 設計思路及Verilog代碼編寫5. 思考

注意,上圖中的控制位元組(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燈會間隔亮起來。下面是我抓到的接收資料時序圖:

IIC總線的原理與Verilog實作1、 軟體平台與硬體平台2、 原理介紹3、 目标任務4、 設計思路及Verilog代碼編寫5. 思考

通過上面的時序圖可以清楚的看到成功讀出了EEPROM中的0x45這個資料,并且我闆子上的四個LED燈也間隔亮了起來。

為了更清晰的說明上面的時序,我把起始信号,停止信号,每個比特,應答位和非應答位全部框出來進一步解釋如下:

IIC總線的原理與Verilog實作1、 軟體平台與硬體平台2、 原理介紹3、 目标任務4、 設計思路及Verilog代碼編寫5. 思考
  • 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. 思考

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将不發送有效應答信号,是以當發送完停止信号又立馬給一個起始信号重新發送時會出現下面的時序。

IIC總線的原理與Verilog實作1、 軟體平台與硬體平台2、 原理介紹3、 目标任務4、 設計思路及Verilog代碼編寫5. 思考

這種情況由于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.

時序圖如下所示:

IIC總線的原理與Verilog實作1、 軟體平台與硬體平台2、 原理介紹3、 目标任務4、 設計思路及Verilog代碼編寫5. 思考

其實要實作這個時序并不是難事,隻要多增加幾個加載資料的狀态就可以了,大家可以直接在上面發送資料子產品的基礎上改。

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.

時序圖如下所示:

IIC總線的原理與Verilog實作1、 軟體平台與硬體平台2、 原理介紹3、 目标任務4、 設計思路及Verilog代碼編寫5. 思考

有了上面接收子產品的基礎,實作這段時序應該也不算困難。以後有空再實作。