IIC 的總線協議和時序
IIC 標准速率為 100kbit/s,快速模式 400kbit/s,支持多機通訊, 支持多主控模塊,但同一時刻只允許有一個主控。 由數據線 SDA 和時鍾 SCL 構成串行總線;每個電路和模塊都有唯一的地址。
在這里以 AT24C04 為例說明 I2C 讀寫的基本操作和時序,I2C 設備的操作可分為寫單個存儲字節,寫多個存儲字節,讀單個存儲字節和讀多個存儲字節。 各個操作如下圖所示。
下面對 I2C 總線通信過程中出現的幾種信號狀態和時序進行分析。
①總線空閑狀態
I2C 總線總線的 SDA 和 SCL 兩條信號線同時處於高電平時,規定為總線的空閑狀態。此時各個器件的輸出級場效應管均處在截止狀態,即釋放總線,由兩條信號線各自的上拉電阻把電平拉高。
②啟動信號(Start)
在時鍾線 SCL 保持高電平期間,數據線 SDA 上的電平被拉低(即負跳變),定義為 I2C 總線 總線的啟動信號,它標志着一次數據傳輸的開始。啟動信號是由主控器主動建立的,在建立該信號之前 I2C 總線必須處於空閑狀態,如下圖所示。
③停止信號(Stop)
在時鍾線 SCL 保持高電平期間,數據線 SDA 被釋放,使得 SDA 返回高電平(即正跳變),稱為 I2C 總線的停止信號,它標志着一次數據傳輸的終止。停止信號也是由主控器主動建立的,建立該信號之后,I2C 總線將返回空閑狀態。
④數據位傳送
在 I2C 總線上傳送的每一位數據都有一個時鍾脈沖相對應(或同步控制),即在 SCL 串行時鍾的配合下,在 SDA 上逐位地串行傳送每一位數據。進行數據傳送時,在 SCL 呈現高電平期間,SDA 上的電平必須保持穩定,低電平為數據 0,高電平為數據 1。只有在 SCL 為低電平期間,才允許 SDA 上的電平改變狀態。
⑤應答信號(ACK 和 NACK)
I2C 總線上的所有數據都是以 8 位字節傳送的,發送器每發送一個字節,就在時鍾脈沖 9 期間釋放數據線,由接收器反饋一個應答信號。應答信號為低電平時,規定為有效應答位(ACK簡稱應答位),表示接收器已經成功地接收了該字節;應答信號為高電平時,規定為非應答位(NACK),一般表示接收器接收該字節沒有成功。對 於反饋有效應答位 ACK 的要求是,接收器在第 9 個時鍾脈沖之前的低電平期間將 SDA 線拉低,並且確保在該時鍾的高電平期間為穩定的低電平。
如果接收器是主控器,則在它收到最后一個字節后,發送一個 NACK 信號,以通知被控發送器結束數據發送,並釋放 SDA 線,以便主控接收器發送一個停止信號。
1. 編寫 IIC 的通信程序 iic_com.v
module iic_com ( input CLK, input RSTn, input [1:0] Start_Sig, //read or write command input [7:0] Addr_Sig, //eeprom words address input [7:0] WrData, //eeprom write data output [7:0] RdData, //eeprom read data output Done_Sig, //eeprom read/write finish output SCL, inout SDA ); parameter F100K = 9'd500; //100Khz的時鍾分頻系數 reg [4:0]i; reg [4:0]Go; reg [9:0]C1; reg [7:0]rData; reg rSCL; reg rSDA; reg isAck; reg isDone; reg isOut; assign Done_Sig = isDone; assign RdData = rData; assign SCL = rSCL; assign SDA = isOut ? rSDA : 1'bz; //SDA數據輸出選擇 //****************************************// //* I2C讀寫處理程序 *// //****************************************// always @ ( posedge CLK or negedge RSTn ) if( !RSTn ) begin i <= 5'd0; Go <= 5'd0; C1 <= 9'd0; rData <= 8'd0; rSCL <= 1'b1; rSDA <= 1'b1; isAck <= 1'b1; isDone <= 1'b0; isOut <= 1'b1; end else if( Start_Sig[0] ) //I2C 數據寫 case( i ) 0: // iic Start begin isOut <= 1; //SDA端口輸出 if( C1 == 0 ) rSCL <= 1'b1; else if( C1 == 400 ) rSCL <= 1'b0; //SCL由高變低 if( C1 == 0 ) rSDA <= 1'b1; else if( C1 == 200 ) rSDA <= 1'b0; //SDA先由高變低 if( C1 == F100K -1) begin C1 <= 9'd0; i <= i + 1'b1; end else C1 <= C1 + 1'b1; end 1: // Write Device Addr begin rData <= {4'b1010, 3'b000, 1'b0}; i <= 5'd7; Go <= i + 1'b1; end 2: // Wirte Word Addr begin rData <= Addr_Sig; i <= 5'd7; Go <= i + 1'b1; end 3: // Write Data begin rData <= WrData; i <= 5'd7; Go <= i + 1'b1; end 4: //iic Stop begin isOut <= 1'b1; if( C1 == 0 ) rSCL <= 1'b0; else if( C1 == 100 ) rSCL <= 1'b1; //SCL先由低變高 if( C1 == 0 ) rSDA <= 1'b0; else if( C1 == 300 ) rSDA <= 1'b1; //SDA由低變高 if( C1 == F100K -1 ) begin C1 <= 9'd0; i <= i + 1'b1; end else C1 <= C1 + 1'b1; end 5: begin isDone <= 1'b1; i <= i + 1'b1; end //寫I2C 結束 6: begin isDone <= 1'b0; i <= 5'd0; end 7,8,9,10,11,12,13,14: //發送Device Addr/Word Addr/Write Data begin isOut <= 1'b1; rSDA <= rData[14-i]; //高位先發送 if( C1 == 0 ) rSCL <= 1'b0; else if( C1 == 100 ) rSCL <= 1'b1; else if( C1 == 300 ) rSCL <= 1'b0; if( C1 == F100K -1 ) begin C1 <= 9'd0; i <= i + 1'b1; end else C1 <= C1 + 1'b1; end 15: // waiting for acknowledge begin isOut <= 1'b0; //SDA端口改為輸入 if( C1 == 100 ) isAck <= SDA; if( C1 == 0 ) rSCL <= 1'b0; else if( C1 == 100 ) rSCL <= 1'b1; else if( C1 == 300 ) rSCL <= 1'b0; if( C1 == F100K -1 ) begin C1 <= 9'd0; i <= i + 1'b1; end else C1 <= C1 + 1'b1; end 16: if( isAck != 0 ) i <= 5'd0; else i <= Go; endcase else if( Start_Sig[1] ) //I2C 數據讀 case( i ) 0: //iic Start begin isOut <= 1; //SDA端口輸出 if( C1 == 0 ) rSCL <= 1'b1; else if( C1 == 400 ) rSCL <= 1'b0; //SCL由高變低 if( C1 == 0 ) rSDA <= 1'b1; else if( C1 == 200 ) rSDA <= 1'b0; //SDA先由高變低 if( C1 == F100K -1 ) begin C1 <= 9'd0; i <= i + 1'b1; end else C1 <= C1 + 1'b1; end 1: // Write Device Addr begin rData <= {4'b1010, 3'b000, 1'b0}; i <= 5'd9; Go <= i + 1'b1; end 2: // Wirte Word Addr begin rData <= Addr_Sig; i <= 5'd9; Go <= i + 1'b1; end 3: //iic Start again begin isOut <= 1'b1; if( C1 == 0 ) rSCL <= 1'b0; else if( C1 == 100 ) rSCL <= 1'b1; else if( C1 == 500 ) rSCL <= 1'b0; //SCL后變低 if( C1 == 0 ) rSDA <= 1'b0; else if( C1 == 100 ) rSDA <= 1'b1; else if( C1 == 300 ) rSDA <= 1'b0; //SDA先變低 if( C1 == 600 -1 ) begin C1 <= 9'd0; i <= i + 1'b1; end else C1 <= C1 + 1'b1; end 4: // Write Device Addr ( Read ) begin rData <= {4'b1010, 3'b000, 1'b1}; i <= 5'd9; Go <= i + 1'b1; end 5: // Read Data begin rData <= 8'd0; i <= 5'd19; Go <= i + 1'b1; end 6: //iic Stop begin isOut <= 1'b1; if( C1 == 0 ) rSCL <= 1'b0; else if( C1 == 100 ) rSCL <= 1'b1; //SCL先變高 if( C1 == 0 ) rSDA <= 1'b0; else if( C1 == 300 ) rSDA <= 1'b1; //SDA后變高 if( C1 == F100K -1 ) begin C1 <= 9'd0; i <= i + 1'b1; end else C1 <= C1 + 1'b1; end 7: //寫I2C 結束 begin isDone <= 1'b1; i <= i + 1'b1; end 8: begin isDone <= 1'b0; i <= 5'd0; end 9,10,11,12,13,14,15,16: //發送Device Addr(write)/Word Addr/Device Addr(read) begin isOut <= 1'b1; rSDA <= rData[16-i]; if( C1 == 0 ) rSCL <= 1'b0; else if( C1 == 100 ) rSCL <= 1'b1; else if( C1 == 300 ) rSCL <= 1'b0; if( C1 == F100K -1 ) begin C1 <= 9'd0; i <= i + 1'b1; end else C1 <= C1 + 1'b1; end 17: // waiting for acknowledge begin isOut <= 1'b0; //SDA端口改為輸入 if( C1 == 200 ) isAck <= SDA; if( C1 == 0 ) rSCL <= 1'b0; else if( C1 == 100 ) rSCL <= 1'b1; else if( C1 == 300 ) rSCL <= 1'b0; if( C1 == F100K -1 ) begin C1 <= 9'd0; i <= i + 1'b1; end else C1 <= C1 + 1'b1; end 18: if( isAck != 0 ) i <= 5'd0; else i <= Go; 19,20,21,22,23,24,25,26: // Read data begin isOut <= 1'b0; if( C1 == 200 ) rData[26-i] <= SDA; if( C1 == 0 ) rSCL <= 1'b0; else if( C1 == 100 ) rSCL <= 1'b1; else if( C1 == 300 ) rSCL <= 1'b0; if( C1 == F100K -1 ) begin C1 <= 9'd0; i <= i + 1'b1; end else C1 <= C1 + 1'b1; end 27: // no acknowledge begin isOut <= 1'b1; if( C1 == 0 ) rSCL <= 1'b0; else if( C1 == 100 ) rSCL <= 1'b1; else if( C1 == 300 ) rSCL <= 1'b0; if( C1 == F100K -1 ) begin C1 <= 9'd0; i <= Go; end else C1 <= C1 + 1'b1; end endcase endmodule
Iic_com 讀寫程序分為 IIC 數據讀和 IIC 數據寫,通過狀態機 i 來切換 IIC 的不同狀態,譬如接收到寫命令,狀態機i=0 轉入 Start 狀態,SDA 先變低,再 SCL 變低;狀態機i=1 開 始轉入寫 IIC 從設備地址 0x80; 之后狀態機轉到7開始發送8位的數據,其中狀態機i=7,8,9,10,11,12,13,14 是 IIC 發送8位的數據,然后狀態機進入 i=15 等待 IIC 從設備的應答信號。狀態機 i=16 為判斷是否有應答,如果有的話狀態機轉到 i=2 寫 IIC 的地址,然后狀態機又是重復i=7,8,9,10,11,12,13,14 發送8位的地址和 i=15 等待應答,i=16 判斷應答。最后狀態機 i=3 開始發送 IIC 寫數據。發送完數據 i=4 發送 Stop 信號。IIC 讀的流程跟寫差不多,我就不再重復了,大家具體看程序和程序的說明吧!
2. 編寫頂層文件 eeprom_test.v
`timescale 1ns / 1ps ////////////////////////////////////////////////////////////////////////////////// // Module Name: eeprom_test // Function: write and read eeprom using I2C bus ////////////////////////////////////////////////////////////////////////////////// module eeprom_test ( input CLK_50M, input RSTn, output [3:0]LED, output SCL, inout SDA ); wire [7:0] RdData; wire Done_Sig; reg [3:0] i; reg [3:0] rLED; reg [7:0] rAddr; reg [7:0] rData; reg [1:0] isStart; assign LED = rLED; /***************************/ /* EEPROM write and read */ /***************************/ always @ ( posedge CLK_50M or negedge RSTn ) if( !RSTn ) begin i <= 4'd0; rAddr <= 8'd0; rData <= 8'd0; isStart <= 2'b00; rLED <= 4'b0000; end else case( i ) 0: if( Done_Sig ) begin isStart <= 2'b00; i <= i + 1'b1; end else begin isStart <= 2'b01; rData <= 8'h12; rAddr <= 8'd0; end //eeprom write 0x12 to EEPROM addr 0 1: if( Done_Sig ) begin isStart <= 2'b00; i <= i + 1'b1; end else begin isStart <= 2'b10; rAddr <= 8'd0; end //eeprom read data from EEPROM addr 0 2: begin rLED <= RdData[3:0]; end endcase /***************************/ //I2C通信程序// /***************************/ iic_com U1 ( .CLK ( CLK_50M ), .RSTn ( RSTn ), .Start_Sig ( isStart ), .Addr_Sig ( rAddr ), .WrData ( rData ), .RdData ( RdData ), .Done_Sig ( Done_Sig ), .SCL ( SCL ), .SDA ( SDA ) ); endmodule
頂層文件實現以下兩部分功能:
1. 上電后寫一個數據到 EEPROM 的地址 0, 再讀出地址 0 的內容。這里我們寫的數據是 0x12,用戶可以自行修改。
2. 例化 iic_com 模塊,verilog 通過模塊調用或稱為模塊實例化的方式來實現 iic_com 子模塊與頂層模塊 eeprom_test 的連接, 模塊例化有利於簡化每一個模塊的代碼,易於維護和修改。