FPGA對EEPROM驅動控制(I2C協議)

Handat發表於2024-06-24

本文摘要:本文首先對I2C協議的通訊模式和AT24C16-EEPROM晶片時序控制進行分析和理解,設計了一個i2c通訊方案。人為按下寫操作按鍵後,FPGA(Altera EP4CE10)對EEPROM指定地址寫入位元組資料,並接後按下讀操作按鍵,讀取該地址上的一個位元組資料在數碼管低兩位顯示出來。其中包括了對此方案的Modelsim模擬測試,並且接續完成板級驗證。(過程筆記)

關鍵詞:EEPROM、I2C協議、Verilog HDL、FPGA

框圖設計:

輸入埠包括系統時鐘、復位訊號、寫操作和讀操作按鍵,輸出埠包括IIC通訊介面、數碼管段選和位選訊號。

image

共計用到了四種子模組,分別是按鍵消抖、i2c通訊資料處理、i2c通訊控制、2khz時鐘分配。按邏輯連結成體,完成本文設計的測試方案。在除錯過程中,為方便觀察操作反應,另外加入了兩個LED燈介面,具體框圖如下。


【I2C協議通訊模式】

I2C協議(通常稱為IIC協議)是一種序列、同步、半雙工通訊協議。I2C協議支援多主機功能,允許多個具備主控能力的裝置在同一匯流排上競爭控制權,並透過硬體仲裁機制避免衝突。其使用序列資料線(SDA)和序列時鐘線(SCL)進行通訊,標準模式的傳輸速率為100 kbit/s(Standard-mode, Sm),快速模式下的I2C傳輸速率提高至400 kbit/s(Fast-mode,Fm),在高速模式下可達3.4Mbit/s

傳送到SDA線上的每個位元組必須為8位,且每次傳輸可以傳送的位元組數量不受限制,每個位元組後必須跟一個ACK響應,位首先傳輸的是資料的最高位MSB,SDA線上的資料必須在SCL時鐘的高電平週期保持穩定,資料線的高或低電平狀態只有在SCL時鐘是低電平時才能改變。

image

傳輸訊號型別:(見上圖)

  • 啟動訊號(START)(S條件):在SCL線處於高電平,SDA上的資料由高向低轉換,則表示啟動IIC匯流排;
  • 應答訊號(ACK):在接收到了8bit的資訊後, 接收資料一方需要向傳送資訊的另一方傳遞預設的低電平脈衝作為訊號,表明已經獲取了資料;
  • 結束訊號(STOP)(P條件):在SCL線處於高電平,SDA上的資料由低向高轉換,則表示停止IIC匯流排。

起始(S)和停止(P)條件一般由主機產生,匯流排在起始條件後被認為處於忙的狀態,在停止條件的某段時間後匯流排被認為再次處於空閒狀態 。

由於I2C 匯流排沒有中央控制,其控制只能由地址或主機碼以及競爭主機傳送的資料決定,匯流排也沒有任何定製的優先權。所以,當發生多主機通訊時,需要一個控制仲裁機制來決定通訊優先。

仲裁過程中的時鐘同步:

在I2C通訊中,所有主機在SCL線上生成自己的時鐘訊號以傳輸資料。資料僅在時鐘高電平時有效,因此需要同步時鐘以實現逐位仲裁。時鐘同步透過線與連線實現,即SCL線狀態由所有裝置共同決定。

image

①見上圖中,當SCL線從高變低時,所有裝置開始計數其低電平週期。若某裝置時鐘先變低,則它會保持SCL線在低電平直到其時鐘再次變高。若此時有其他裝置仍處於低電平週期,它們的時鐘變化不會改變SCL線狀態,直至最長低電平週期的裝置完成計數。

②一旦所有裝置完成低電平週期,SCL線釋放並變為高電平,隨後所有裝置同步開始計數高電平週期。

③首先完成高電平週期的裝置將再次拉低SCL線。因此,SCL時鐘的低電平週期由最長低電平週期的裝置決定,而高電平週期由最短高電平週期的裝置決定。(巧記:保證最長低電平週期)

仲裁判定優先:

主機只能在匯流排空閒時啟動傳輸,兩個或多個主機可能在起始條件的最小持續時間內產生一個起始條件,結果在匯流排上產生一個規範的起始條件(如下框S條件內)。當SCL線是高電平時,仲裁在SDA線發生,在其他主機保持低電平狀態時,首先拉高電平的主機將斷開資料輸出級,如下圖DATA1失去了通訊的優先權。

image

仲裁可以持續多位,第一個階段是比較地址位。 如果各主機都嘗試定址同一器件,仲裁機制會持續到資料位。在仲裁過程中不會丟失資訊(仲裁區間有效資訊一致),丟失仲裁的主機可以產生時鐘脈衝直到丟失仲裁的該位元組末尾 。

補充:如果主機也結合了從機功能,並且在定址階段丟失仲裁,由於它存在是贏得仲裁的主機所定址的器件。因此,丟失仲裁的主機必須立即切換到從機模式。

【AT24C16時序分析】

AT24C16 EEPROM儲存晶片的相關資訊:儲存容量為16Kbit,即2048位元組。晶片內部分成128頁,每頁16位元組,讀寫操作都是以位元組為基本單位。AT24C16具有低電壓工作、高速序列通訊和硬體防寫等特點。(下圖為晶片引腳說明)

image

  • 地址組成:AT24C16的器件地址包括高4位固定地址(1010)和使用者需設定的低3位地址(A0、A1、A2);
  • 地址設定:透過連線晶片的A0、A1、A2這3個引腳到VCC或GND來實現地址的低3位設定。例如這3個引腳均連線到VCC,則器件地址為1010_111。由於該 3 位只能組合出 8 種情況,因此,一個主機最多可以連線8個AT24C16儲存晶片。

1、EEPROM驅動寫時序進行分析

image

位元組寫入時序 (Byte Write):通訊開始,由主機傳送一個起始條件。緊接著,主機傳送EEPROM的裝置地址,選擇目標EEPROM晶片。如果EEPROM支援字地址定址,主機將接續傳送一個或多個位元組的字地址來指定要寫入資料的記憶體位置。然後,主機寫入資料位元組,最高有效位(MSB)首先傳送。 EEPROM在接收到每個位元組後,返回一個應答訊號(ACK),應答訊號是低電平脈衝,表示已成功接收位元組並準備接收下一個位元組或停止訊號。待所有資料位元組都傳送完畢後,主機傳送停止訊號(STOP)來結束目前的通訊。

頁寫入時序 (Page Write):與位元組寫入時序相比,頁寫入時序類似,但資料被分組為多個位元組,作為一個連續的資料塊傳送。 傳送起始裝置地址後,接續傳送一系列的資料位元組(DATA (n), DATA (n + 1),......, DATA (n + x))單個位元組傳送完成,EEPROM都會返回一個應答訊號(ACK)。

補充:所有I2C裝置均支援單位元組資料寫入操作,但只有部分I2C裝置支援頁寫操作;且支援頁寫操作的裝置,一次頁寫操作寫入的位元組數不能超過裝置單頁包含的儲存單元數。

2、讀時序進行分析

IIC協議支援三種EEPROM讀時序。首先是指定地址單位元組讀取方式:操作時序和寫時序類似,不同的是,在寫入目標地址後,主機的操作方式換為讀取。

image

順序讀取時序:主機傳送一個起始地址後,EEPROM開始連續傳送資料(DATA n, DATA n+1, DATA n+2, ... DATA n+X)。在每個資料位元組的末尾,EEPROM同樣等待主機的應答訊號(ACK)。主機在接收到每個資料位元組後傳送ACK訊號,直到所有資料都被接收。當所有資料傳送完畢或主機決定停止時,它會傳送一個停止訊號(STOP),結束順序讀取操作。

隨機讀取時序:起始條件後,傳送裝置地址和讀寫位(R/W=1),接著傳送隨機地址。EEPROM傳送資料,主機接收後傳送ACK。所有資料傳送完畢後,傳送停止訊號。EEPROM在接收到有效地址後,透過SDA線連續傳送資料(DATA n, DATA n+1, ...)。在每個資料位元組的末尾,EEPROM等待主機的應答訊號(ACK)。主機待接收到資料位元組後,透過傳送ACK訊號來確認接收,並告知EEPROM可以繼續傳送下一個資料位元組。

【時序邏輯設計方案】

1、時鐘處理與i2c通訊啟動

系統時脈頻率為50Mhz,頻率很高,這裡首先需要從系統時鐘分頻提供一個1Mhz的i2c_clk時鐘用於i2c通訊處理。下圖中cnt_clk為一個時鐘分頻計數器。

image

以寫操作為例,待寫觸發訊號write發生,拉高寫有效訊號write_valid,並且為使i2c_clk時鐘訊號要在上升沿檢測到其高電平,write觸發要保持≥2個時鐘週期,對應100個系統時鐘週期。這裡設定有效計數器cnt_wr,計數150個系統週期,即300ns,滿足要求。計數完成,拉低寫有效訊號write_valid完成觸發操作,具體時序見下圖,當然,寫有效時序也是如此。

image

寫有效訊號write_valid觸發,在其拉高的下一個i2c_clk時鐘上升沿,觸發寫使能wr_en訊號,並且,啟動cnt_start計數器,計數值為5000(1Mhz ->1us)≈ 5ms。這是因為AT64C16單次操作間隔週期需要保持5ms空閒狀態。完成計數後,啟動i2c_start起始訊號觸發,後面就是i2c通訊的具體流程。當完成操作後,i2c_end結束訊號標誌通訊完成,從而拉低寫使能wr_en訊號。過程中,設定了寫操作的目標地址為16'H00_4D,寫入資料為8'H8A。

image

注意,大部分訊號時鐘觸發源都是i2c_clk,而不是系統時鐘。當i2c_start起始訊號觸發,i2c_clk啟動,作為i2c_scl通訊時鐘的觸發源。同時,根據下圖邏輯,狀態機狀態由IDLE轉到START1。起始訊號僅佔一個i2c_clk週期。cnt_i2c_clki2c_scl通訊時鐘線的計數器,計數值範圍0-3,使得i2c_scl週期為250khz。

image

完成後,進入SEND_ADDR狀態,向IIC匯流排發生器件地址1010011+控制位0,表示寫入。位元位計數器cnt_bit用於位元位0-7的計數。在ACK1狀態,等待匯流排回應。ACK是從機,即EEPROM,傳回來的低電平訊號。對於主機來說SDA線此時是高阻態,匯流排的上拉電阻將電位鉗位在高電位(sda為inout型別)。所以在後面,可以看到這裡是處於高阻態的。通訊後面的流程根據狀態機的跳轉,在時序圖表現得明顯。在STOP停止狀態,FPGA向EEPROM傳送停止訊號,一次單位元組資料寫操作完成,拉低使能訊號i2c_clk_enwe_en,並且及時觸發i2c_end,隨後狀態機跳回IDLE初始狀態。

image

讀控制時序處理與寫時序處理流程類似,具體見上圖,不同在ACK3狀態的跳轉,接收一個週期回應訊號後,進入的是起始狀態START2,從而觸發第二次起始訊號,再次寫入器件地址,讀取一位元組資料。rd_data_reg作為一個暫存器,儲存讀取到的位元組資料,待完成後,轉錄到rd_data。在N_ACK等待回應一個高電平回應訊號,將i2c_scl由地拉高產生一個停止訊號,同時觸發i2c_end。兩個操作過程具體狀態判斷條件,需具體分析,但都類似。

2、STATE狀態機轉換邏輯

i2c整個通訊的過程可以分為很多階段,將它們細分開:空閒、起始、器件地址寫入、字/位元組地址寫入、各級響應、位元組資料寫入、讀取位元組資料、停止。主機視角對從裝置操作過程,具體可見如下。

image

裝置啟動,主機處於空閒狀態IDLE,待檢測到寫/讀操作訊號後,程式轉入到第一個起始狀態START_1,表現為SDA線產生一個由高到低的電平轉換訊號。接續進入器件地址寫入狀態SEND_ADDR,根據硬體圖,這裡預先設定器件是1010_011。器件確認後,向主機發生一個低電平回應ACK_1。暫存器儲存地址型別分為字地址和位元組地址,分別對應不同的狀態切換。完成後ACK_3響應後,寫操作,進入位元組資料寫入狀態WR_DATA,寫入一個預先資料,並回應ACK_4進入結束停止狀態STOP。而ACK_3響應後,進入讀操作流程,需再次發生起始START_2,接後寫入讀取目標地址,等待從機完成資料傳送後,進入結束停止狀態STOP。最終保持一定週期,再次轉換至空閒狀態IDLE

根據時序圖邏輯,確定狀態機狀態轉換條件:

當前狀態 目標跳轉狀態 跳轉條件 操作型別
IDLE START1 i2c_start拉高 讀/寫
START1 SEND_ADDR cnt_i2c_clk == 2'd3(一個i2c_clk週期 ) 讀/寫
SEND_ADDR ACK1 (cnt_i2c_clk == 2'd3)&&(!ack) 讀/寫
ACK1 SEND_BH (cnt_bit == 3'd7)&&(cnt_i2c_clk == 2'd3) 讀/寫
SEND_BH ACK2 (cnt_bit == 3'd7)&&(cnt_i2c_clk == 2'd3) 讀/寫
ACK2 SEND_BL (cnt_i2c_clk == 2'd3)&&(!ack) 讀/寫
SEND_BL ACK3 (cnt_bit == 3'd7)&&(cnt_i2c_clk == 2'd3) 讀/寫
ACK3 WR_DATA (cnt_i2c_clk == 2'd3)&&(!ack) 讀/寫
WR_DATA ACK4 (cnt_bit == 3'd7)&&(cnt_i2c_clk == 2'd3)
ACK4 STOP (cnt_i2c_clk == 2'd3)&&(!ack)
ACK3 START2 rd_en拉高
START2 SEND_RA cnt_i2c_clk == 2'd3
SEND_RA ACK5 (cnt_bit == 3'd7)&&(cnt_i2c_clk == 2'd3)
ACK5 RD_DATA (cnt_i2c_clk == 2'd3)&&(!ack)
RD_DATA N_ACK (cnt_bit == 3'd7)&&(cnt_i2c_clk == 2'd3)
N_ACK STOP cnt_i2c_clk == 2'd3
STOP IDLE (cnt_bit == 3'd3)&&(cnt_i2c_clk == 2'd3) 讀/寫

對上面的表格可以總結,ACK1 - ACK5條件都是相同的,只是跳轉目標不同,單位元組讀/寫結束後狀態跳轉判斷條件相同,兩個起始狀態跳轉判斷條件也是相同的 ,N_ACKSTOP的跳轉註意區別。跳轉機Verilog HDL具體程式如下:

//dispose state condition
always @(posedge i2c_clk or negedge sys_rst)begin
    if(!sys_rst) state <= IDLE;
    else case(state)
        IDLE:   if(i2c_start) state <= START1;else state<= state;
        START1:  if(cnt_i2c_clk == 2'd3) state <= SEND_ADDR;
                else state <= state;
        SEND_ADDR:  
                if((cnt_bit == 3'd7)&&(cnt_i2c_clk == 2'd3))
                        state <= ACK1;
                else state <= state;
        ACK1:   if((cnt_i2c_clk == 2'd3)&&(!ack))begin
                    if(addr_num)state <= SEND_BH;
                    else state<= SEND_BL;
                end else state <= state;
        SEND_BH:if((cnt_bit == 3'd7)&&(cnt_i2c_clk == 2'd3))
                    state <= ACK2;
                else state<= state;
        ACK2:   if((cnt_i2c_clk == 2'd3)&&(!ack))
                    state <= SEND_BL;
                else state <= state;
        SEND_BL:if((cnt_bit == 3'd7)&&(cnt_i2c_clk == 2'd3))
                    state <= ACK3;
                else state <= state;
        ACK3:   if((cnt_i2c_clk == 2'd3)&&(!ack))begin
                    if(wr_en) state <= WR_DATA;
                    else if(rd_en) state <= START2;
                end
                else state <= state;
        WR_DATA:if((cnt_bit == 3'd7)&&(cnt_i2c_clk == 2'd3))
                    state <= ACK4;
                else state <= state;
        ACK4:   if((cnt_i2c_clk == 2'd3)&&(!ack))
                    state <= STOP;
                else state <= state;
        START2: if(cnt_i2c_clk == 2'd3) state <= SEND_RA;
                else state <= state;
        SEND_RA:if((cnt_bit == 3'd7)&&(cnt_i2c_clk == 2'd3))
                    state <= ACK5;
                else state <= state;
        ACK5:   if((cnt_i2c_clk == 2'd3)&&(!ack))
                    state <= RD_DATA;
                else state <= state;
        RD_DATA:if((cnt_bit == 3'd7)&&(cnt_i2c_clk == 2'd3))
                    state <= N_ACK;
                else state <= state;
        N_ACK:  if(cnt_i2c_clk == 2'd3)
                    state <= STOP;
                else state <= state;
        STOP:   if((cnt_bit == 3'd3)&&(cnt_i2c_clk == 2'd3))
                    state <= IDLE;
                else state <= state;
    endcase
end

STATE狀態機在整個程式過程中十分重要,i2c_scl時鐘電平和i2c_sda輸出電平情況,也要根據其狀態的不同做出具體分析。

//dispose i2c_scl sequence
always@(*)begin
    case(state)
        IDLE: i2c_scl <= 1'b1;
        START1:
            if(cnt_i2c_clk == 2'd3) i2c_scl <= 1'b0;
            else i2c_scl <= 1'b1;
        SEND_ADDR,ACK1,SEND_BH,ACK2,SEND_BL,
        ACK3,WR_DATA,ACK4,START2,SEND_RA,ACK5,RD_DATA,N_ACK:
            if((cnt_i2c_clk == 2'd1) || (cnt_i2c_clk == 2'd2)) i2c_scl <= 1'b1;
            else i2c_scl <= 1'b0;
        STOP:
            if((cnt_bit == 3'd0) &&(cnt_i2c_clk == 2'd0)) i2c_scl <=  1'b0;
            else i2c_scl <=  1'b1;
        default:    i2c_scl <=  1'b1;
    endcase
end

//dispose i2c_sda_reg & rd_data_reg sequence
always @(*)begin
    case(state)
        IDLE:       begin
                        i2c_sda_reg <= 1'b1;
                        rd_data_reg  <= 8'd0;
                    end
        START1:     if(cnt_i2c_clk == 2'd0) i2c_sda_reg <= 1'b1;
                    else i2c_sda_reg <= 1'b0;
        SEND_ADDR:  if(cnt_bit <= 3'd6) i2c_sda_reg<= DEVICE_ADDR[6-cnt_bit];
                    else i2c_sda_reg<= 1'b0;
        ACK1:       i2c_sda_reg<= 1'b1;
        SEND_BH:    i2c_sda_reg<= byte_addr[15-cnt_bit];
        ACK2:       i2c_sda_reg<= 1'b1;
        SEND_BL:    i2c_sda_reg<= byte_addr[7-cnt_bit];
        ACK3:       i2c_sda_reg<= 1'b1;
        WR_DATA:    i2c_sda_reg<= wr_data[7-cnt_bit];
        ACK4:       i2c_sda_reg<= 1'b1;
        START2:     if(cnt_i2c_clk <= 2'd1)i2c_sda_reg <= 1'b1;
                    else i2c_sda_reg <= 1'b0;
        SEND_RA:    if(cnt_bit <= 3'd6)i2c_sda_reg<= DEVICE_ADDR[6-cnt_bit];
                    else i2c_sda_reg<= 1'b1;
        ACK5:       i2c_sda_reg<= 1'b1;
        RD_DATA:    if(cnt_i2c_clk  == 2'd2) rd_data_reg[7-cnt_bit] <= sda_in;
                    else rd_data_reg <= rd_data_reg;
        N_ACK:      i2c_sda_reg<= 1'b1;
        STOP:       if((cnt_bit==3'd0)&&(cnt_i2c_clk<2'd3))i2c_sda_reg <= 1'b0;
                    else i2c_sda_reg<= 1'b1;
        default:    begin
                        i2c_sda_reg<= 1'b1;
                        rd_data_reg <=  rd_data_reg;
                    end
    endcase
end

其他訊號在此不再列舉,它們的判斷條件相對狀態機來說處理起來比較簡單,邏輯也很清晰。

【階段模擬驗證】

下面看下程式的模擬結果,檢視四個:讀/寫操作啟動時刻、寫操作整體時序、讀操作整體時序、讀/寫操作結束時序。

操作啟動:write訊號發生,write_valid拉高,並在下一i2c_clk上升沿拉高wr_en使能訊號,等待cnt_wr完成計數150後,放低write_validcnt_startwr_en拉高後,開始計數。

image

寫操作(這裡器件地址設定1010_011):cnt_start計數到4900處,發起i2c_start觸發,state值變為4'h01,表示IDLE進入START1狀態,依次完成後續的狀態切換。STOP狀態下,待cnt_bit保持到3‘h3,結束停止狀態返回IDLEwr_en拉低,i2c_end拉高一個週期後放下。從下面的模擬圖可以看到,整個寫操作的時序表現正常。

image

讀操作及讀操作結束模擬圖如下:

image

image

【最終上機驗證】

image

Quartus II生成框圖如下,兩個按鍵做為輸入觸發,輸出包括IIC通訊匯流排SDA和SCL,數碼管段選SEG_SEL和位選SEG_LED,以及外部掛載的兩個LED。程式燒錄後,裝置低二位數碼管顯示00,LED均處於熄滅狀態,按下key_wr後,led_wr點亮,表示寫操作啟動;接續,按下key_rd後,led_rd點亮,表示讀操作啟動,並且數碼管顯示讀取資料8A。最終得到的現象與預期一致。

image

文獻參考:

[1] I2C匯流排規範 (https://sumcu.suda.edu.cn/_upload/article/files/74/e5/d4eb93de45808d71ad8aad542ede/a3cb5873-aaf4-4af0-9e5f-521793fbba46.pdf);

[2] 基於I2C協議的EEPROM驅動控制(https://doc.embedfire.com/fpga/altera/ep4ce10_mini/zh/latest/fpga/I2C.html);

[3] 王榮華. 可配置的IIC協議控制器IP核的設計[D]. 黑龍江:哈爾濱理工大學,2011.( DOI:10.7666/d.y2012472);


本篇文章中使用的Verilog程式模組,若有需見網頁左欄Gitee倉庫連結:https://gitee.com/silly-big-head/little-mouse-funnyhouse/tree/FPGA-Verilog/

相關文章