學習筆記-Verilog實現IIC匯流排協議

混子研究生在讀發表於2020-12-26

學習筆記-Verilog實現IIC匯流排協議

1 IIC匯流排協議原理

1.1開始和結束

IIC匯流排只有兩根線,適合低速傳輸。可工作在100kb/s,400kb/s,1Mb/s和3.4Mb/s。序列匯流排中SPI和MIPI會有更高的傳輸速率。
START和STOP
IIC匯流排中SCL為時鐘訊號SDA為資料訊號。IIC匯流排的地址分為7位和10位兩種,本文用的是7位地址。由於自帶始終,工作頻率可以不準確。
IIC匯流排的SCL和SDA必須上拉

1.2資料傳輸

所有資料都是在時鐘SCL位低電平時變化,高電平時取樣資料。
每次傳輸9位資料,其中前8位(1個位元組)位主從機傳送的資料,第9位為接收端的反饋訊號,為低電平是稱為(ACK),高電平稱為(NACK)。ACK表示繼續傳輸資料。
產生一個佔空比和相位不同的時鐘訊號
資料傳輸時的波形
上圖的scl_o和sda_o產生一個佔空比和相位不同的同頻時鐘訊號,scl和sda為真實的資料傳輸波形。
在這裡插入圖片描述
上圖中表示了一個訊號的傳輸過程。第八位為讀寫訊號,高電平讀,低電平寫。ACK訊號和NACK訊號是由讀訊號的裝置發出的。接受到NACK訊號是stop或者restart。

1.3IIC匯流排仲裁

IIC匯流排可以掛很多主機和從機,一般最多不能超過400pF的負載電容。如裝置太多IIC多路選擇器擴充套件。
掛那麼多主機和從機,怎麼進行有效的通訊呢,這就需要匯流排仲裁,由於IIC匯流排都是上拉,誰先輸出0,誰贏得仲裁,輸出資料,地址對上了才能讀資料

2 Verilog程式碼

//注意:這裡的SCL和SDA都是上拉的,如果匯流排上有多個輸出,有0輸入匯流排上邏輯為0
//由於匯流排上拉,匯流排上很多訊號都是低電平有效。但是輸出0功耗高。
//完整的IP還需要加入tx_fifo,rx_fifo,時鐘相位佔空比模組和頂層控制模組
module iic_core 
  (
   input 	    CLK_SDA, CLK_SCL,    //時鐘訊號見上圖中的scl_o和sda_o訊號
   input 	    reset_n,

   input [7:0] 	    iic_addr,                //地址輸入
   input [7:0] 	    iic_datain,             
   output reg [7:0] iic_dataout,

   input 	    iic_enable,
   output reg 	    iic_busy,  
   output reg 	    iic_ackerror, 
   output reg 	    iic_arbitlost,
   
   input 	    scl_i, sda_i,
   output           scl_o, sda_o,
   output reg 	    scl_t, sda_t       //高電平輸出有效,低電平輸出無效
   );

   parameter [2:0] IIC_IDLE=3'h1, IIC_START=3'h2, IIC_ADDR=3'h3, 
     IIC_ACKA=3'h4, IIC_DATA=3'h5, IIC_ACKD=3'h6, IIC_STOP=3'h7;
   
   reg [3:0] 	    iic_core_state, iic_core_nextstate; 
   reg [7:0] 	    addr_i, datain_i;   
   reg [7:0] 	    sdain;
   reg [3:0] 	    bit_count;  
   reg              sdaout;
   
   always @(posedge CLK_SDA or negedge reset_n)
     if (~reset_n) iic_core_state <= IIC_IDLE;
     else iic_core_state <= iic_core_nextstate;
   
   always @(*) begin            
      iic_core_nextstate <= IIC_IDLE;           
      case (iic_core_state)
        IIC_IDLE  :  iic_core_nextstate <= iic_enable ? 
                                           IIC_START : IIC_IDLE;
        
        IIC_START :  iic_core_nextstate <= IIC_ADDR;
        
        IIC_ADDR  :  iic_core_nextstate <= iic_arbitlost ? 
                                           IIC_IDLE : bit_count ? IIC_ADDR : IIC_ACKA;
        
        IIC_ACKA  :  iic_core_nextstate <= iic_ackerror ? 
                                           IIC_STOP : IIC_DATA;
        
        IIC_DATA  :  iic_core_nextstate <= iic_arbitlost ? 
                                           IIC_IDLE : bit_count ? IIC_DATA : IIC_ACKD;
        
        IIC_ACKD  :  iic_core_nextstate <= (iic_enable & (addr_i == iic_addr)) ? 
                                           IIC_DATA : iic_enable ? IIC_IDLE : IIC_STOP;
        
        IIC_STOP  :  iic_core_nextstate <= IIC_IDLE;
      endcase 
   end 
   
   always @(posedge CLK_SDA or negedge reset_n)
     if (~reset_n) begin
        sda_t <= 0; sdaout <= 1;
        bit_count <= 0; 
        iic_busy <= 0;
     end else begin
        sda_t <= 1; sdaout <= 1;
        iic_busy <= 1;
        case (iic_core_nextstate)     //用iic_core_nextstate是為了使每個狀態執行的操作在同一週期上一致
          IIC_IDLE  : begin 
             sda_t <= 0;
             iic_busy <= 0;
          end
          IIC_START : begin 
             sdaout <= 0;
             bit_count <= 8;
          end
          IIC_ADDR  : begin
             sdaout <= iic_addr[bit_count - 1];
             bit_count <= bit_count - 1;             
          end
          IIC_ACKA  : begin
             sda_t <= 0;
             bit_count <= 8;
          end
          IIC_DATA  : begin
             sda_t <= ~addr_i[0];
             sdaout <= addr_i[0] ? 1'b1 : datain_i[bit_count - 1]; 
             bit_count <= bit_count - 1; 
          end
          IIC_ACKD  : begin
             sda_t <= (iic_enable & addr_i[0]) & (addr_i == iic_addr);
             sdaout <= ~((iic_enable & addr_i[0]) & (addr_i == iic_addr));	     
             bit_count <= 8; iic_busy <= 0;
          end
          IIC_STOP  : begin 
             sdaout <= 0;
             iic_busy <= 0;
          end
        endcase 
     end
   
//SCL用下降沿是為了在SDA穩定後在進行操作
   always @(negedge CLK_SCL or negedge reset_n)
     if (~reset_n) begin 
        sdain <= 0; 
        scl_t <= 0;
        iic_ackerror <= 0;
        iic_arbitlost <= 0;
        addr_i <= 0; datain_i <= 0; 
     end else begin
        sdain <= {sdain[6:0],sda_i};
        scl_t <= 1;
        case (iic_core_state)
          IIC_IDLE  : scl_t <= 0;
          IIC_START : begin
             addr_i <= iic_addr; datain_i <= iic_datain;
          end
          IIC_ADDR  : iic_arbitlost <= (sdaout != sda_i);    //匯流排仲裁訊號
          IIC_ACKA  : iic_ackerror  <= sda_i;      //是否收到ACK訊號,是否有從機的地址匹配成功(低電平表示匹配成功)
          IIC_DATA  : iic_arbitlost <= sda_t ? (sdaout != sda_i) : 0;    //匯流排仲裁訊號
          IIC_ACKD  : begin 
             iic_ackerror  <= sda_t ? sda_i : 0;  
             datain_i <= iic_datain;
             if (addr_i[0]) iic_dataout <= sdain;  //讀操作時,輸出ACK或NACK訊號
          end
          IIC_STOP  : scl_t <= 0;
        endcase 
     end 
  
   assign scl_o = CLK_SCL;
   assign sda_o = sdaout & CLK_SDA;
//IIC匯流排輸出在頂層實現
// assign scl = scl_t ? scl_o : 1'bz;
// assign sda = sda_t ? sda_o : 1'bz;

寫操作
IDLE-START-ADDR-ACKA成功後,addr_i(0)最後最低位為0,所以進行寫操作,在DATA狀態時輸出一個位元組資料後進入ACKD狀態,判斷是否繼續寫或者換地址寫,重新寫,本程式碼寫時最簡單的IIC從機程式碼,所以很多情況沒有考慮到。

讀操作
IDLE-START-ADDR-ACKA成功後,addr_i(0)最後最低位為1,所以進行讀操作,DATA狀態時sda_t為0,sda時不輸出的,ACKD狀態輸出ACK訊號。

相關文章