基於FPGA的IIC 協議讀寫 EEPROM

更多文章內容,請關注微信公衆號“FPGA科技室
在這裏插入圖片描述


IIC 基本特性


總線信號

SDA:串行數據線

SCL:串行數據時鐘

總線空閒狀態

SDA:高電平

SCL:高電平

IIC 協議起始位

SCL 爲高電平時,SDA 出現下降沿,產生一個起始位。
在這裏插入圖片描述
IIC 協議結束位

SCL 爲高電平時,SDA 出現上升沿,產生一個結束位。在這裏插入圖片描述
IIC 讀寫單字節時序

IIC 主機對 IIC 從機寫入數據時,SDA 上的每一位數據在 SCL 的高電平期間被寫入從機中。對於主機,在 SCL 的低電平期間改變要寫入的數據。在這裏插入圖片描述
IIC 主機從 IIC 從機中讀出數據時,從機在 SCL 的低電平期間將數據輸出到 SDA 總線上,在SCL 的高電平期間保持數據穩定。對於主機,在 SCL 的高電平期間將 SDA 線上的數據讀取並存儲。

數據接收方對數據發送方的響應

每當一個字節的數據或命令傳輸完成時,都會有一位的應答位。需要應答位時,數據發出方將 SDA 總線設置爲 3 態輸入,由於 IIC 總線上都有上拉電阻,因此此時總線默認爲高電平,若數據接收方正確接收到數據,則數據接收方將 SDA 總線拉低,以示正確應答。

例如當 IIC 主機對 IIC 從機寫入數據或命令時,每個字節都需要從機產生應答信號以告訴主機數據或命令成功被寫入。所以,當 IIC 主機將 8 位的數據或命令傳出後,會將 SDA 信號設置爲輸入,等待從機應答(等待 SDA 被從機拉低爲低電平),若從機正確應答,表明當前數據或命令傳輸成功,可以結束或開始下一個命令/數據的傳輸,否則表明數據/命令寫入

失敗,主機就可以決定是否放棄寫入或者重新發起寫入。

IIC 器件地址

每個 IIC 器件都有一個器件地址,有的器件地址在出廠時地址就設置好了,用戶不可以更改(ov7670:0x42),有的確定了幾位,剩下幾位由硬件確定(比如有三位由用戶確定,就留有 3 個控制地址的引腳,最常見的爲 IIC 接口的 EEPROM 存儲器),此類較多;還有的有地址寄存器。

嚴格講,主機不是向從機發送地址,而是主機往總線上發送地址,所有的從機都能接收到主機發出的地址,然後每個從機都將主機發出的地址與自己的地址比較,如果匹配上了,這個從機就會向主機發出一個響應信號。主機收到響應信號後,開始向總線上發送數據,與這個從機的通訊就建立起來了。如果主機沒有收到響應信號,則表示尋址失敗。

通常情況下,主從器件的角色是確定的,也就是說從機一直工作在從機模式。不同的器件定義地址的方式是不同的,有的是軟件定義,有的是硬件定義。例如某些單片機的 IIC 接口作爲從機時,其器件地址是可以通過軟件修改從機地址寄存器確定的。而對於一些其他器件,如 CMOS 圖像傳感器、EEPROM 存儲器,其器件地址在出廠時就已經設定好了,具體值可以在對應的數據手冊中查到。

對於 AT24C64 這樣一顆 EEPROM 器件,其器件地址爲 1010 加 3 位的片選信號。3 位片選信號由硬件連接決定。例如 SOIC 封裝的該芯片 pin1、pin2、pin3 爲片選地址。當硬件電路上分別將這三個 pin 連接到 GND 或 VCC 時,就實現了設置不通的片選地址。IIC 協議在進行數據傳輸時,主機需要首先向總線上發出控制命令,其中,控制命令就包含了從機地址/片選信號+讀寫控制。然後等待從機響應。以下爲 IIC 控制命令傳輸的數據格式。在這裏插入圖片描述
IIC 傳輸時,按照從高到低的位序進行傳輸。控制字節的最低位爲讀寫控制位,當該位爲 0 時表示主機對從機進行寫操作,當該位爲 1 時表示主機對從機進行讀操作。例如,當需要對片選地址爲 100 的 AT24LC64 發起寫操作,則控制字節應該爲 CtrlCode = 1010_100_0。

若要讀,則控制字節應該爲 CtrlCode = 1010_100_1。

IIC 存儲器地址

我們要對一個器件中的存儲單元(寄存器和存儲器以下簡稱存儲單元)進行讀寫,就必須要能夠指定存儲單元的地址。IIC 協議設計了有從機存儲單元尋址地址段,該地址段爲一個字節或兩個字節長度,在主機確認收到從機返回的控制字節響應後,由主機發出。地址段長度視不同的器件類型,長度不同在這裏插入圖片描述
IIC 讀寫時序
IIC 單字節寫時序

1 字節地址段器件單字節寫時序在這裏插入圖片描述
2 字節地址段器件單字節寫時序在這裏插入圖片描述
從主機角度看一次寫入過程

a. 主機設置 SDA 爲輸出

b. 主機發起起始信號

c. 主機傳輸器件地址字節,其中最低爲 0,表明爲寫操作。

d. 主機設置 SDA 爲輸入三態,讀取從機應答信號。

e. 讀取應答信號成功,傳輸 1 字節地址數據

f. 主機設置 SDA 爲輸入三態,讀取從機應答信號。

g. 對於兩字節地址段器件,傳輸地址數據低字節,對於 1 字節地址段器件,傳輸待寫入的數據

h. 設置 SDA 爲輸入三態,讀取從機應答信號。

i. 對於兩字節地址段器件,傳輸待寫入的數據(2 字節地址段器件可選)

j. 設置 SDA 爲輸入三態,讀取從機應答信號(2 字節地址段器件可選)。

k. 主機產生 STOP 位,終止傳輸

IIC 連續寫時序(頁寫時序)

1 字節地址段器件多字節寫時序在這裏插入圖片描述
2 字節地址段器件多字節寫時序在這裏插入圖片描述
從主機角度看一次寫入過程

a. 主機設置 SDA 爲輸出

b. 主機發起起始信號

c. 主機傳輸器件地址字節,其中最低爲 0,表明爲寫操作。

d. 主機設置 SDA 爲輸入三態,讀取從機應答信號。

e. 讀取應答信號成功,傳輸 1 字節地址數據

f. 主機設置 SDA 爲輸入三態,讀取從機應答信號。

g. 對於兩字節地址段器件,傳輸低字節地址數據,對於 1 字節地址段器件,傳輸待寫

入的第一個數據

h. 設置 SDA 爲輸入三態,讀取從機應答信號。

i. 寫入待寫入的第 2 至第 n 個數據並讀取應答信號。對於 AT24Cxx,一次可寫入的最

大長度爲 32 字節。

j. 主機產生 STOP 位,終止傳輸。

IIC 單字節讀時序

1 字節地址段器件單節讀時序在這裏插入圖片描述
2 字節地址段器件單節讀時序在這裏插入圖片描述
從主機角度看一次讀取過程

a. 主機設置 SDA 爲輸出

b. 主機發起起始信號

c. 主機傳輸器件地址字節,其中最低爲 0,表明爲寫操作。

d. 主機設置 SDA 爲輸入三態,讀取從機應答信號。

e. 讀取應答信號成功,傳輸 1 字節地址數據

f. 主機設置 SDA 爲輸入三態,讀取從機應答信號。

g. 對於兩字節地址段器件,傳輸低字節地址數據,對於 1 字節地址段器件,無此段數

據傳輸。

h. 主機發起起始信號

i. 主機傳輸器件地址字節,其中最低爲 1,表明爲寫操作。

j. 設置 SDA 爲輸入三態,讀取從機應答信號。

k. 讀取 SDA 總線上的一個字節的數據

l. 產生無應答信號(高電平)(無需設置爲輸出高點片,因爲總線會被自動拉高)

m. 主機產生 STOP 位,終止傳輸。

IIC 多字節連續讀時序(頁讀取)

1 字節地址段器件多字節讀時序在這裏插入圖片描述
2 字節地址段器件多字節讀時序

在這裏插入圖片描述
從主機角度看一次讀取過程

a. 主機設置 SDA 爲輸出

b. 主機發起起始信號

c. 主機傳輸器件地址字節,其中最低爲 0,表明爲寫操作。

d. 主機設置 SDA 爲輸入三態,讀取從機應答信號。

e. 讀取應答信號成功,傳輸 1 字節地址數據

f. 主機設置 SDA 爲輸入三態,讀取從機應答信號。

g. 對於兩字節地址段器件,傳輸低字節地址數據,對於 1 字節地址段器件,無此段數據傳輸。

h. 主機發起起始信號

i. 主機傳輸器件地址字節,其中最低爲 1,表明爲寫操作。

j. 設置 SDA 爲輸入三態,讀取從機應答信號。

k. 讀取 SDA 總線上的 n 個字節的數據(對於 AT24Cxx,一次讀取長度最大爲 32 字節)

l. 產生無應答信號(高電平)(無需設置爲輸出高點片,因爲總線會被自動拉高)主機產生 STOP 位,終止傳輸

EEPROM 讀寫控制程序設計

EEPROM 存儲器芯片的型號爲 AT24C64,其存儲器容量爲 64kbit,器件

片選地址有 3 位,A2、A1、A0。數據存儲地址是 13 位,屬於 2 字節地址段器件。

根據上面 IIC 的基本概念中有關讀寫時 SDA 與 SCL 時序,不管對於從機還是主機 SDA上的每一位數據在 SCL 的高電平期間爲有效數據,在 SCL 的低電平期間是要改變的數據。

根據這個用 2 個標誌位對時鐘 SCL 的高電平和低電平進行標記,如下圖所示:scl_high 對SCL 高電平中間進行標誌,scl_low 對 SCL 低電平中間進行標誌。這個在具體的實現中也不難實現。在這裏插入圖片描述
IIC 讀寫狀態機設計

SCL 時鐘總線以及其高低電平中間標誌位產生完成後其後就是 SDA 數據線的產生,這個需要根據具體的讀寫操作完成。這裏主要採用狀態機實現在這裏插入圖片描述

module IIC_24LC64(

     clk50M,

     reset,

     iic_en,

     cs_bit,

     address,

     write,

     write_data,

     read,

     read_data,

     scl,

     sda,

     done

);

 input clk50M; //系統時鐘 50MHz

 input reset; //異步復位信號

 input iic_en; //使能信號

 input [2:0]cs_bit; //器件選擇地址

input [12:0]address; //13 位數據讀寫地址,24LC64 有 13 位數據存儲

地址

 input write; //寫數據信號

 input [7:0]write_data; //寫數據

 input read; //讀數據信號

 output reg[7:0]read_data; //讀數據

 

 output reg scl; //IIC 時鐘信號

 inout sda; //IIC 數據總線

 

 output reg done; //一次 IIC 讀寫完成

 

 parameter SYS_CLOCK = 50_000_000; //系統時鐘採用 50MHz

 parameter SCL_CLOCK = 200_000; //scl 總線時鐘採用 200kHz

 

 //狀態

 parameter

      Idle = 16'b0000_0000_0000_0001,

      Wr_start = 16'b0000_0000_0000_0010,

      Wr_ctrl = 16'b0000_0000_0000_0100,

      Ack1 = 16'b0000_0000_0000_1000,

      Wr_addr1 = 16'b0000_0000_0001_0000,

      Ack2 = 16'b0000_0000_0010_0000,

      Wr_addr2 = 16'b0000_0000_0100_0000,

      Ack3 = 16'b0000_0000_1000_0000,

      Wr_data = 16'b0000_0001_0000_0000,

      Ack4 = 16'b0000_0010_0000_0000,

      Rd_start = 16'b0000_0100_0000_0000,

      Rd_ctrl = 16'b0000_1000_0000_0000,

      Ack5 = 16'b0001_0000_0000_0000,

      Rd_data = 16'b0010_0000_0000_0000,

      Nack = 16'b0100_0000_0000_0000,

      Stop = 16'b1000_0000_0000_0000;

 

 //sda 數據總線控制位

 reg sda_en;

 

 //sda 數據輸出寄存器

 reg sda_reg;

 

 assign sda = sda_en ? sda_reg : 1'bz;

 

 //狀態寄存器

 reg [15:0]state;

//讀寫數據標誌位

 reg W_flag;

 reg R_flag;

 

 //寫數據到 sda 總線緩存器

 reg [7:0]sda_data_out;

 reg [7:0]sda_data_in;

 reg [3:0]bit_cnt;

 

 

 reg [7:0]scl_cnt;

 parameter SCL_CNT_M = SYS_CLOCK/SCL_CLOCK; //計數最大值

 reg scl_cnt_state;

 

 //產生 SCL 時鐘狀態標誌 scl_cnt_state,爲 1 表示 IIC 總線忙,爲 0 表示總線閒

 always@(posedge clk50M or negedge reset)

     begin

     if(!reset)

         scl_cnt_state <= 1'b0;

     else if(iic_en)

         scl_cnt_state <= 1'b1;

     else if(done)

         scl_cnt_state <= 1'b0;

     else

         scl_cnt_state <= scl_cnt_state;

     end

 

 //scl 時鐘總線產生計數器

 always@(posedge clk50M or negedge reset)

       begin

          if(!reset)

              scl_cnt <= 8'b0;

          else if(scl_cnt_state)

             begin

                 if(scl_cnt == SCL_CNT_M - 1)

                     scl_cnt <= 8'b0;

                 else

                     scl_cnt <= scl_cnt + 8'b1;

             end

          else

                 scl_cnt <= 8'b0;

          end

//scl 時鐘總線產生

 always@(posedge clk50M or negedge reset)

         begin

             if(!reset)

                 scl <= 1'b1;

             else if(scl_cnt == (SCL_CNT_M>>1)-1)

                 scl <= 1'b0;

             else if(scl_cnt == SCL_CNT_M - 1)

                 scl <= 1'b1;

             else

                 scl <= scl;

          end

 

 //scl 時鐘電平中部標誌位

 reg scl_high;

 reg scl_low;

 

 always@(posedge clk50M or negedge reset)

        begin

             if(!reset)

             begin

                 scl_high <= 1'b0;

                 scl_low <= 1'b0;

             end 

             else if(scl_cnt == (SCL_CNT_M>>2))

                 scl_high <= 1'b1;

             else if(scl_cnt == (SCL_CNT_M>>1)+(SCL_CNT_M>>2))

                 scl_low <= 1'b1;

             else

             begin

                 scl_high <= 1'b0;

                 scl_low <= 1'b0; 

 end

 end

 

 //狀態機

 always@(posedge clk50M or negedge reset)

 begin

      if(!reset)

      begin

   		state <= Idle;

		 sda_en <= 1'b0;

		 sda_reg <= 1'b1;

		 W_flag <= 1'b0;

		R_flag <= 1'b0; 

		 done <= 1'b0;

	 end

 else 

	 case(state)

	 Idle:

 	begin 

		 done <= 1'b0;

		 W_flag <= 1'b0; 

		 R_flag <= 1'b0;

		 sda_en <= 1'b0; 

 		sda_reg <= 1'b1;

 if(iic_en && write) //使能 IIC 並且爲寫操作
	
	 begin

		 W_flag <= 1'b1; //寫標誌位置 1 

		 sda_en <= 1'b1; //設置 SDA 爲輸出模式

		 sda_reg <= 1'b1; //SDA 輸出高電平

		 state <= Wr_start; //跳轉到起始狀態 

	 end

 else if(iic_en && read) //使能 IIC 並且爲讀操作

 	begin

 		R_flag <= 1'b1; //讀標誌位置 1 

		 sda_en <= 1'b1; //設置 SDA 爲輸出模式

 		sda_reg <= 1'b1; //SDA 輸出高電平

		 state <= Wr_start; //跳轉到起始狀態

	 end

 else

 	state <= Idle; 

 end 

 

 Wr_start:

		 begin

		 if(scl_high)

		 begin

 			sda_reg <= 1'b0;

			 state <= Wr_ctrl;

 			sda_data_out <= {4'b1010, cs_bit,1'b0}; 

			 bit_cnt <= 4'd8;

		 end

	 else

		 begin

		 sda_reg <= 1'b1;

		 state <= Wr_start;

 end

end

 

 Wr_ctrl: //寫控制字節 4'b1010+3 位片選地址+1 位寫控制

 begin

 if(scl_low)

 begin

 bit_cnt <= bit_cnt -4'b1;

 sda_reg <= sda_data_out[7];

 sda_data_out <= {sda_data_out[6:0],1'b0};

 if(bit_cnt == 0)

 begin

 state <= Ack1;

 sda_en <= 1'b0;

 end

 else

 state <= Wr_ctrl; 

 end

 else

 state <= Wr_ctrl; 

 end

 

 Ack1: //通過判斷 SDA 是否拉低來判斷是否有從機響應

 begin 

 if(scl_high)

 if(sda == 1'b0)

 begin

 state <= Wr_addr1; 

 sda_data_out <= {3'bxxx,address[12:8]};

 bit_cnt <= 4'd8;

 end

 else

 state <= Idle;

 else

 state <= Ack1; 

 end

 

 Wr_addr1: //寫 2 字節地址中的高地址字節中的低五位

 begin

 if(scl_low)

 begin

 sda_en <= 1'b1;

 bit_cnt <= bit_cnt -4'b1;

 sda_reg <= sda_data_out[7];

 sda_data_out <= {sda_data_out[6:0],1'b0};

if(bit_cnt == 0)

 begin

 state <= Ack2; 

 sda_en <= 1'b0; 

 end

 else

 state <= Wr_addr1; 

 end

 else

 state <= Wr_addr1;

 end

 

 Ack2: //通過判斷 SDA 是否拉低來判斷是否有從機響應

 begin 

 if(scl_high)

 if(sda == 1'b0)

 begin

 state <= Wr_addr2; 

 sda_data_out <= address[7:0];

 bit_cnt <= 4'd8;

 end

 else

 state <= Idle;

 else

 state <= Ack2; 

 end

 

 Wr_addr2: //寫 2 字節地址中的低地址字節

 begin

 if(scl_low)

 begin

 sda_en <= 1'b1;

 bit_cnt <= bit_cnt -4'b1;

 sda_reg <= sda_data_out[7];

 sda_data_out <= {sda_data_out[6:0],1'b0};

 if(bit_cnt == 0)

 begin

 state <= Ack3; 

 sda_en <= 1'b0; 

 end

 else

 state <= Wr_addr2; 

 end

 else

state <= Wr_addr2;

 end

 

 Ack3: //通過判斷 SDA 是否拉低來判斷是否有從機響應

 begin 

 if(scl_high)

 if(sda == 1'b0) //有響應就判斷是讀還是寫操作

 begin 

 if(W_flag) //如果是寫數據操作,進入寫數據狀態

 begin 

 sda_data_out <= write_data;

 bit_cnt <= 4'd8;

 state <= Wr_data;

 end

 else if(R_flag) //如果是讀數據操作,進入讀數據開始狀

態

 begin

 state <= Rd_start;

 sda_reg <= 1'b1;

 end

 end

 else

 state <= Idle;

 else

 state <= Ack3; 

 end

 

 Wr_data: //寫數據狀態,向 EEPROM 寫入數據

 begin 

 if(scl_low)

 begin

 sda_en <= 1'b1;

 bit_cnt <= bit_cnt -4'b1;

 sda_reg <= sda_data_out[7];

 sda_data_out <= {sda_data_out[6:0],1'b0};

 if(bit_cnt == 0)

 begin

 state <= Ack4;

 sda_en <= 1'b0;

 end

 else

 state <= Wr_data; 

 end

 else

state <= Wr_data;

 end 

 

 Ack4: //通過判斷 SDA 是否拉低來判斷是否有從機響應

 begin

 if(scl_high)

 if(sda == 1'b0) //有響應就進入停止狀態

 begin

 sda_reg <= 1'b0;

 state <= Stop; 

 end

 else

 state <= Idle;

 else

 state <= Ack4;

 end

 

 Rd_start: //讀數據的開始操作 

 begin

 if(scl_low)

 begin

 sda_en <= 1'b1;

 end

 else if(scl_high)

 begin

 sda_reg <= 1'b0;

 state <= Rd_ctrl;

 sda_data_out <= {4'b1010, cs_bit,1'b1};

 bit_cnt <= 4'd8;

 end

 else

 begin

 sda_reg <= 1'b1;

 state <= Rd_start;

 end

 end

 

 

 Rd_ctrl: //寫控制字節 4'b1010+3 位片選地址+1 位讀控制 

 begin

 if(scl_low)

 begin

 bit_cnt <= bit_cnt -4'b1;

 sda_reg <= sda_data_out[7];

sda_data_out <= {sda_data_out[6:0],1'b0};

 if(bit_cnt == 0)

 begin

 state <= Ack5;

 sda_en <= 1'b0;

 end

 else

 state <= Rd_ctrl; 

 end

 else

 state <= Rd_ctrl; 

 end 

 

 Ack5: //通過判斷 SDA 是否拉低來判斷是否有從機響應 

 begin 

 if(scl_high)

 if(sda == 1'b0) //有響應就進入讀數據狀態

 begin

 state <= Rd_data;

 sda_en <= 1'b0; //SDA 總線設置爲 3 態輸入

 bit_cnt <= 4'd8;

 end

 else

 state <= Idle;

 else

 state <= Ack5; 

 end 

 

 Rd_data: //讀數據狀態

 begin

 if(scl_high) //在時鐘高電平讀取數據

 begin

 sda_data_in <= {sda_data_in[6:0],sda};

 bit_cnt <= bit_cnt - 4'd1;

 state <= Rd_data;

 end

 else if(scl_low && bit_cnt == 0) //數據接收完成進入無應答

響應狀態

 begin

 state <= Nack; 

 end

 else

 state <= Rd_data; 

 end

Nack: //不做應答響應

 begin

 read_data <= sda_data_in;

 if(scl_high)

 begin

 state <= Stop; 

 sda_reg <= 1'b0;

 end

 else

 state <= Nack; 

 end

 

 Stop: //停止操作,在時鐘高電平,SDA 上升沿

 begin

 if(scl_low)

 begin

 sda_en <= 1'b1; 

 end 

 else if(scl_high)

 begin

 sda_en <= 1'b1;

 sda_reg <= 1'b1; 

 state <= Idle;

 done <= 1'b1;

 end 

 else

 state <= Stop;

 end

 

 default:

 begin

 state <= Idle;

 sda_en <= 1'b0;

 sda_reg <= 1'b1;

 W_flag <= 1'b0;

 R_flag <= 1'b0;

 done <= 1'b0;

 end 

 endcase 

 end

endmodule




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章