天天看點

基于FPGA的IIC 協定讀寫 EEPROM

更多文章内容,請關注微信公衆号“FPGA科技室”

基于FPGA的IIC 協定讀寫 EEPROM

IIC 基本特性

總線信号

SDA:串行資料線

SCL:串行資料時鐘

總線空閑狀态

SDA:高電平

SCL:高電平

IIC 協定起始位

SCL 為高電平時,SDA 出現下降沿,産生一個起始位。

基于FPGA的IIC 協定讀寫 EEPROM

IIC 協定結束位

SCL 為高電平時,SDA 出現上升沿,産生一個結束位。

基于FPGA的IIC 協定讀寫 EEPROM

IIC 讀寫單位元組時序

IIC 主機對 IIC 從機寫入資料時,SDA 上的每一位資料在 SCL 的高電平期間被寫入從機中。對于主機,在 SCL 的低電平期間改變要寫入的資料。

基于FPGA的IIC 協定讀寫 EEPROM

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 控制指令傳輸的資料格式。

基于FPGA的IIC 協定讀寫 EEPROM

IIC 傳輸時,按照從高到低的位序進行傳輸。控制位元組的最低位為讀寫控制位,當該位為 0 時表示主機對從機進行寫操作,當該位為 1 時表示主機對從機進行讀操作。例如,當需要對片選位址為 100 的 AT24LC64 發起寫操作,則控制位元組應該為 CtrlCode = 1010_100_0。

若要讀,則控制位元組應該為 CtrlCode = 1010_100_1。

IIC 存儲器位址

我們要對一個器件中的存儲單元(寄存器和存儲器以下簡稱存儲單元)進行讀寫,就必須要能夠指定存儲單元的位址。IIC 協定設計了有從機存儲單元尋址位址段,該位址段為一個位元組或兩個位元組長度,在主機确認收到從機傳回的控制位元組響應後,由主機發出。位址段長度視不同的器件類型,長度不同

基于FPGA的IIC 協定讀寫 EEPROM

IIC 讀寫時序

IIC 單位元組寫時序

1 位元組位址段器件單位元組寫時序

基于FPGA的IIC 協定讀寫 EEPROM

2 位元組位址段器件單位元組寫時序

基于FPGA的IIC 協定讀寫 EEPROM

從主機角度看一次寫入過程

a. 主機設定 SDA 為輸出

b. 主機發起起始信号

c. 主機傳輸器件位址位元組,其中最低為 0,表明為寫操作。

d. 主機設定 SDA 為輸入三态,讀取從機應答信号。

e. 讀取應答信号成功,傳輸 1 位元組位址資料

f. 主機設定 SDA 為輸入三态,讀取從機應答信号。

g. 對于兩位元組位址段器件,傳輸位址資料低位元組,對于 1 位元組位址段器件,傳輸待寫入的資料

h. 設定 SDA 為輸入三态,讀取從機應答信号。

i. 對于兩位元組位址段器件,傳輸待寫入的資料(2 位元組位址段器件可選)

j. 設定 SDA 為輸入三态,讀取從機應答信号(2 位元組位址段器件可選)。

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

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

1 位元組位址段器件多位元組寫時序

基于FPGA的IIC 協定讀寫 EEPROM

2 位元組位址段器件多位元組寫時序

基于FPGA的IIC 協定讀寫 EEPROM

從主機角度看一次寫入過程

a. 主機設定 SDA 為輸出

b. 主機發起起始信号

c. 主機傳輸器件位址位元組,其中最低為 0,表明為寫操作。

d. 主機設定 SDA 為輸入三态,讀取從機應答信号。

e. 讀取應答信号成功,傳輸 1 位元組位址資料

f. 主機設定 SDA 為輸入三态,讀取從機應答信号。

g. 對于兩位元組位址段器件,傳輸低位元組位址資料,對于 1 位元組位址段器件,傳輸待寫

入的第一個資料

h. 設定 SDA 為輸入三态,讀取從機應答信号。

i. 寫入待寫入的第 2 至第 n 個資料并讀取應答信号。對于 AT24Cxx,一次可寫入的最

大長度為 32 位元組。

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

IIC 單位元組讀時序

1 位元組位址段器件單節讀時序

基于FPGA的IIC 協定讀寫 EEPROM

2 位元組位址段器件單節讀時序

基于FPGA的IIC 協定讀寫 EEPROM

從主機角度看一次讀取過程

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 位元組位址段器件多位元組讀時序

基于FPGA的IIC 協定讀寫 EEPROM

2 位元組位址段器件多位元組讀時序

基于FPGA的IIC 協定讀寫 EEPROM

從主機角度看一次讀取過程

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 低電平中間進行标志。這個在具體的實作中也不難實作。

基于FPGA的IIC 協定讀寫 EEPROM

IIC 讀寫狀态機設計

SCL 時鐘總線以及其高低電平中間标志位産生完成後其後就是 SDA 資料線的産生,這個需要根據具體的讀寫操作完成。這裡主要采用狀态機實作

基于FPGA的IIC 協定讀寫 EEPROM
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