【代碼更新】IIC協議建模——讀寫EEPROM


  案例采用明德揚設計思想完成。IIC協議是非常常用的接口協議,在電子類崗位招聘要求中經常出現它的身影。關於IIC協議這里只做簡要介紹,詳細信息請自行百度或查閱相關Datasheet,網上資料非常多。該篇博文主要講如何使用verilog來描述IIC協議,以讀寫EEPROM為例帶領大家了解下明德揚四段式狀態機規范和優勢,另外還有一些自己在設計過程中總結的經驗技巧。

  IIC協議時序格式以Datasheet中時序圖的形式供大家參考。IIC協議有一條時鍾線SCL和一條雙線數據總線SDA。SDA在SCL高電平時保持穩定,否則視為開始或結束條件。

  發送端發送1byte數據后,接收端在下一個SCL高電平期間拉低總線表示應答,即接收數據成功。

  以下分別是器件地址為1字節的EEPROM的單字節寫和讀操作,需要注意的是DEVICE ADDRESS段中前四位固定是4'b1010,后三位根據EEPROM地址信號電平決定(本次實驗地址信號引腳均接地,因此后三位為000),最后一位是讀寫標志位,低電平寫。

  好了,有了以上五張時序圖我們便知道要干什么了,就是實現這些時序嘛!對於這種串行時序,時間有先后且操作差異較大的要用狀態機實現。每種類型操作定義在一個狀態中,狀態內部需要多個操作則配合計數器實現。整體設計思路如下:先構造時鍾信號SCL,這里頻率定義為200KHz,而系統時鍾有頻率為200MHz差分晶振提供,顯然需要用到分頻計數器。由於SCL高電平期間數據要保持穩定,所以我們在分頻計數器計數到1/4處拉高SCL,3/4處拉低SCL,這樣做的好處是在結束計數時正好處於SCL低電平中間點,此處作為數據變化的時刻再合適不過。

  有了時鍾信號,下一步就是通過不同的狀態實現SDA信號線滿足上述時序要求。我們先來划分狀態(實際上時序圖中都給我們標識好了),很明顯綜合考慮寫和讀兩種時序,狀態應定義為:初始狀態、開始、寫控制、響應1、寫地址、響應2、寫數據、響應3、重新開始、讀控制、響應4、讀數據、不響應、停止。這里寫控制和讀控制即是DEVICE ADDRESS階段,唯一的區別在於讀寫標志位不同。能看出以上狀態划分包括寫流程分支和讀流程分支,可以根據指令用一個標志位加以區分。

  定義狀態參數並采用獨熱碼進行編碼:

  IIC協議中每次SCL高電平期間視為一次操作,因此為了讓每個狀態都有整數個SCL周期(完整分頻計數周期),對每個狀態進行比特計數,寫控制、地址、寫數據、讀控制、讀數據階段計數周期是8,其他為1。另外為保證代碼的“健壯性”,也就是即使發送1byte數據后沒有響應也不至於掛死在等待響應階段,設定在每次等待響應階段若響應才進入下一操作,否則回到初始狀態。由此得到狀態轉移圖(只包括主要流程,轉移條件及未響應回到IDLE狀態未畫出):

  至此所有的設計工作都已經完成,接下來就是根據上述分析編寫代碼。在編寫代碼之前簡要介紹明德揚四段式狀態機的設計思想和代碼規范:四段式狀態機實質上是在三段式狀態機基礎上單獨提出狀態轉移條件定義的結構。目的是讓設計者一個時間段只專注於一件事情,也就是說當設計狀態機的時候先把狀態轉移流程確定,而條件用不同的信號名代替,等狀態轉移流程確定后再定義轉移條件。這樣做的另一個好處是作為條件的信號名可以很方便的在后續時序邏輯中使用。其中用於代替條件的信號名要遵循類似如下格式:<state_c>2<state_n>。<>處用狀態名代替。

整體代碼如下:

  1 `timescale 1ns / 1ps
  2 
  3 module i2c_interface#(parameter SCL_CYC = 1000)//200KHz
  4 (
  5     input clk,
  6     input rst_n,
  7     
  8     //用戶側接口
  9     input write_en,//寫指令
 10     input read_en, //讀指令
 11     input [7:0]share_addr,    //讀寫復用地址
 12     input [7:0] wri_data,//代寫入數據
 13     input wri_data_vld,
 14     
 15     output reg busy,//總線忙信號
 16     output reg [7:0] rd_data,//讀回數據
 17     output reg rd_data_vld,
 18     
 19     //仿真用接口
 20     output reg [13:0] state_c,
 21     
 22     //eeprom側接口
 23     output reg scl, //時鍾
 24     input sda_in,
 25     output reg sda_en,
 26     output reg sda_reg
 27     
 28     );
 29     
 30     reg [11:0] div_cnt;
 31     reg high_middle,low_middle;
 32     reg [3:0] bit_cnt;
 33     reg [3:0] N;
 34     //(*keep = "true"*)reg [13:0] state_c;
 35     reg [13:0] state_n;
 36     reg [7:0] wri_byte;
 37     reg rd_flag;
 38     reg [7:0] rd_buf;
 39     reg [13:0] state_c_tmp;
 40     reg [7:0] device_addr_wr_shift;
 41     
 42     wire add_bit_cnt,end_bit_cnt;
 43     wire add_div_cnt,end_div_cnt;
 44     wire idle2start,start2wri_ctrl,wri_ctrl2ack1,ack12addr,addr2ack2,ack22wri_data;
 45     wire wri_data2ack3,ack32stop,ack22re_start,re_start2rd_ctrl,rd_ctrl2ack4;
 46     wire ack42rd_data,rd_data2nack,nack2stop,stop2idle,ack2idle;
 47     reg ack_valid,ack_invalid;
 48     wire [2:0] cs;
 49     wire wri_vld;
 50     wire [7:0] device_addr_rd,device_addr_wr;
 51     wire [7:0] word_addr;
 52     wire ack_state;
 53     
 54     //狀態編碼
 55     localparam IDLE     = 14'b00_0000_0000_0001,//1
 56                START    = 14'b00_0000_0000_0010,//2
 57                WRI_CTRL = 14'b00_0000_0000_0100,//4
 58                ACK1     = 14'b00_0000_0000_1000,//8
 59                ADDR     = 14'b00_0000_0001_0000,//10
 60                ACK2     = 14'b00_0000_0010_0000,//20
 61                WRI_DATA = 14'b00_0000_0100_0000,//40
 62                ACK3     = 14'b00_0000_1000_0000,//80
 63                RE_START = 14'b00_0001_0000_0000,//100
 64                RD_CTRL  = 14'b00_0010_0000_0000,//200
 65                ACK4     = 14'b00_0100_0000_0000,//400
 66                RD_DATA  = 14'b00_1000_0000_0000,//800
 67                NACK     = 14'b01_0000_0000_0000,//1000
 68                STOP     = 14'b10_0000_0000_0000;//2000
 69     
 70     //分頻計數器 在響應操作直到完成或退出到IDLE中間都計數
 71     always@(posedge clk or negedge rst_n)begin
 72         if(!rst_n)
 73             div_cnt <= 0;
 74         else if(add_div_cnt)begin
 75             if(end_div_cnt)
 76                 div_cnt <= 0;
 77             else 
 78                 div_cnt <= div_cnt + 1'b1;
 79         end
 80         else 
 81             div_cnt <= 0;
 82     end
 83     
 84     assign add_div_cnt = busy == 1;
 85     assign end_div_cnt = add_div_cnt && div_cnt == SCL_CYC - 1;
 86     
 87     //比特計數器
 88     always@(posedge clk or negedge rst_n)begin
 89         if(!rst_n)
 90             bit_cnt <= 0;
 91         else if(add_bit_cnt)begin
 92             if(end_bit_cnt)
 93                 bit_cnt <= 0;
 94             else 
 95                 bit_cnt <= bit_cnt + 1'b1;
 96         end
 97     end
 98     
 99     assign add_bit_cnt = end_div_cnt;
100     assign end_bit_cnt = add_bit_cnt && bit_cnt == N - 1;
101     
102     always@(*)begin
103         case(state_c)
104             WRI_CTRL:N = 8;
105             ADDR:N = 8;
106             WRI_DATA:N = 8;
107             RD_CTRL:N = 8;
108             RD_DATA:N = 8;
109             default:N = 1;
110         endcase
111     end
112     
113     //---------------------iic時序四段式狀態機部分-------------------------
114     
115     //時序邏輯描述狀態轉移
116     always@(posedge clk or negedge rst_n)begin
117         if(!rst_n)
118             state_c <= IDLE;
119         else 
120             state_c <= state_n;
121     end
122     
123     //組合邏輯描述狀態轉移條件
124     always@(*)begin
125         case(state_c)
126             IDLE:begin       //空閑狀態
127                 if(idle2start)
128                     state_n = START;
129                 else 
130                     state_n = state_c;
131             end
132             
133             START:begin    //產生開始條件 即SCL高電平期間SDA拉低
134                 if(start2wri_ctrl)
135                     state_n = WRI_CTRL;
136                 else 
137                     state_n = state_c;
138             end
139             
140             WRI_CTRL:begin  //寫器件地址和寫標志位
141                 if(wri_ctrl2ack1)
142                     state_n = ACK1;
143                 else 
144                     state_n = state_c;
145             end
146             
147             ACK1:begin   //等待響應
148                 if(ack12addr)
149                     state_n = ADDR;
150                 else if(ack2idle)
151                     state_n = IDLE;
152                 else 
153                     state_n = state_c;
154             end
155             
156             ADDR:begin  //寫存儲單元地址
157                 if(addr2ack2)
158                     state_n = ACK2;
159                 else 
160                     state_n = state_c;
161             end
162             
163             ACK2:begin   //等待響應2
164                 if(ack22wri_data)   //寫操作
165                     state_n = WRI_DATA;
166                 else if(ack22re_start)//讀操作
167                     state_n = RE_START;
168                 else if(ack2idle)
169                     state_n = IDLE;
170                 else 
171                     state_n = state_c;
172             end
173             
174             WRI_DATA:begin   //寫數據 8bit
175                 if(wri_data2ack3)
176                     state_n = ACK3;
177                 else 
178                     state_n = state_c;
179             end
180             
181             ACK3:begin   //等待響應3
182                 if(ack32stop)
183                     state_n = STOP;
184                 else if(ack2idle)
185                     state_n = IDLE;
186                 else 
187                     state_n = state_c;
188             end
189             
190             RE_START:begin  //若為讀操作在響應2后再次構造開始條件
191                 if(re_start2rd_ctrl)
192                     state_n = RD_CTRL;
193                 else 
194                     state_n = state_c;
195             end
196             
197             RD_CTRL:begin   //寫入存儲單元地址和讀標志位
198                 if(rd_ctrl2ack4)
199                     state_n = ACK4;
200                 else 
201                     state_n = state_c;
202             end
203             
204             ACK4:begin  //等待響應4
205                 if(ack42rd_data)
206                     state_n = RD_DATA;
207                 else if(ack2idle)
208                     state_n = IDLE;
209                 else 
210                     state_n = state_c;
211             end
212             
213             RD_DATA:begin  //讀數據 8bit
214                 if(rd_data2nack)
215                     state_n = NACK;
216                 else 
217                     state_n = state_c;
218             end
219             
220             NACK:begin  //不響應 無操作即可
221                 if(nack2stop)
222                     state_n = STOP;
223                 else 
224                     state_n = state_c;
225             end
226             
227             STOP:begin  //構造停止條件
228                 if(stop2idle)
229                     state_n = IDLE;
230                 else 
231                     state_n = state_c;
232             end
233             
234             default:
235                 state_n = IDLE;
236         endcase
237     end
238     
239     //連續賦值語句定義狀態轉移條件
240     assign idle2start       = state_c  == IDLE     && (write_en || read_en);
241     assign start2wri_ctrl   = state_c  == START    && end_bit_cnt;  
242     assign wri_ctrl2ack1    = state_c  == WRI_CTRL && end_bit_cnt;
243     assign ack12addr        = state_c  == ACK1     && ack_valid && end_bit_cnt;
244     assign addr2ack2        = state_c  == ADDR     && end_bit_cnt;
245     assign ack22wri_data    = state_c  == ACK2     && ack_valid && !rd_flag && end_bit_cnt;
246     assign wri_data2ack3    = state_c  == WRI_DATA && end_bit_cnt;
247     assign ack32stop        = state_c  == ACK3     && ack_valid && end_bit_cnt;
248     assign ack22re_start    = state_c  == ACK2     && ack_valid && rd_flag && end_bit_cnt;
249     assign re_start2rd_ctrl = state_c  == RE_START && end_bit_cnt;
250     assign rd_ctrl2ack4     = state_c  == RD_CTRL  && end_bit_cnt;
251     assign ack42rd_data     = state_c  == ACK4     && ack_valid && end_bit_cnt;
252     assign rd_data2nack     = state_c  == RD_DATA  && end_bit_cnt;
253     assign nack2stop        = state_c  == NACK     && end_bit_cnt;
254     assign stop2idle        = state_c  == STOP     && end_bit_cnt;
255     assign ack2idle         = ack_state && ack_invalid;
256     
257 
258     
259     always@(posedge clk or negedge rst_n)begin
260         if(!rst_n)
261             ack_valid <= 0;
262         else if(ack12addr || ack22wri_data || ack32stop || ack22re_start || ack42rd_data || ack2idle)
263             ack_valid <= 0;
264         else if(ack_state && high_middle && !sda_en && !sda_in)
265             ack_valid <= 1;
266     end
267     
268     assign ack_state = state_c == ACK1 || state_c == ACK2 || state_c == ACK3 || state_c == ACK4;
269     
270     always@(posedge clk or negedge rst_n)begin
271         if(!rst_n)
272             ack_invalid <= 0;
273         else if(state_c == NACK && high_middle && !sda_en && sda_in)
274             ack_invalid <= 1;
275         else if(end_bit_cnt)
276             ack_invalid <= 0;
277     end
278     
279     //時序邏輯描述狀態輸出
280     
281     //scl時鍾信號
282     always@(posedge clk or negedge rst_n)begin
283         if(!rst_n)
284             scl <= 0;
285         else if(add_div_cnt && div_cnt == SCL_CYC/4 - 1)
286             scl <= 1;
287         else if(add_div_cnt && div_cnt == SCL_CYC/4 + SCL_CYC/2 - 1)
288             scl <= 0;
289     end
290     
291     //找到scl高低電平中間點
292     always@(posedge clk or negedge rst_n)begin
293         if(!rst_n)
294             high_middle <= 0;
295         else if(add_div_cnt && div_cnt == SCL_CYC/2 - 1)
296             high_middle <= 1;
297         else 
298             high_middle <= 0;
299     end
300     
301     //三態門輸出使能
302     always@(posedge clk or negedge rst_n)begin
303         if(!rst_n)
304             sda_en <= 1;
305         else if(idle2start || ack12addr || ack22wri_data || ack32stop || ack22re_start || nack2stop|| rd_data2nack)
306             sda_en <= 1;
307         else if(wri_ctrl2ack1 || addr2ack2 || wri_data2ack3 || rd_ctrl2ack4 || ack2idle || stop2idle)
308             sda_en <= 0;
309     end
310     
311     //數據總線輸出寄存器
312     always@(posedge clk or negedge rst_n)begin
313         if(!rst_n)
314             sda_reg <= 1;
315         else if(idle2start)
316             sda_reg <= 1;
317         else if((state_c == START || state_c == RE_START) && high_middle)
318             sda_reg <= 0;
319         else if(state_c == WRI_CTRL)
320             sda_reg <= device_addr_wr[7-bit_cnt];
321         else if(state_c == ADDR)
322             sda_reg <= word_addr[7 - bit_cnt];
323         else if(state_c == WRI_DATA)
324             sda_reg <= wri_data[7 - bit_cnt];
325         else if(state_c == STOP && high_middle)
326             sda_reg <= 1;
327         else if(ack22re_start)
328             sda_reg <= 1;
329         else if(state_c == RE_START && high_middle)
330             sda_reg <= 0;
331         else if(state_c == RD_CTRL)
332             sda_reg <= device_addr_rd[7- bit_cnt];
333         else if(ack_state)
334             sda_reg <= 0;
335         else if(rd_data2nack)
336             sda_reg <= 1;
337         else if(nack2stop)
338             sda_reg <= 0;
339     end
340     
341     assign device_addr_wr = {4'b1010,cs,1'b0};
342     assign cs             = 3'b000;
343     assign word_addr      = share_addr;
344     assign device_addr_rd = {4'b1010,cs,1'b1};
345     
346     //讀取數據緩存
347     always@(posedge clk or negedge rst_n)begin
348         if(!rst_n)
349             rd_buf <= 0;
350         else if(state_c == RD_DATA && high_middle)
351             rd_buf <= {rd_buf[6:0],sda_in};
352     end
353     
354     //讀數據有效指示
355     always@(posedge clk or negedge rst_n)begin
356         if(!rst_n)
357             rd_data_vld <= 0;
358         else if(rd_data2nack)
359             rd_data_vld <= 1;
360         else 
361             rd_data_vld <= 0;
362     end
363     
364     //讀數據輸出
365     always@(posedge clk or negedge rst_n)begin
366         if(!rst_n)
367             rd_data <= 0;
368         else 
369             rd_data <= rd_buf;
370     end
371     
372     //讀標志位
373     always@(posedge clk or negedge rst_n)begin
374         if(!rst_n)
375             rd_flag <= 0;
376         else if(read_en)
377             rd_flag <= 1;
378         else if(rd_flag && (stop2idle || state_c == IDLE))
379             rd_flag <= 0;
380     end
381     
382     //總線忙信號
383     always@(posedge clk or negedge rst_n)begin
384         if(!rst_n)
385             busy <= 0;
386         else if(write_en || read_en)
387             busy <= 1;
388         else if(busy == 1 &&(stop2idle || state_c == IDLE))
389             busy <= 0;
390     end
391     
392 endmodule
i2c_interface.v

   可以看出狀態機部分依次分為:時序邏輯描述狀態轉移,組合邏輯描述狀態轉移條件,連續賦值定義狀態轉移條件以及時序邏輯描述狀態相關輸出。並且至始至終使用state_c和state_n兩個信號表示現態和次態,使邏輯更加清晰。接口部分為了方便仿真和調試,加入狀態信號state_c。這里涉及到一個雙向端口sda,用三個信號:輸出使能sda_en,輸出寄存器sda_reg和輸入緩存sda_in表示。在頂層模塊中使用這三個信號通過三態門的形式給出,關於三態門的使用細節和仿真方式稍后講述。

  先設計其他模塊和頂層模塊,之后對頂層模塊進行仿真測試,這時觀察各個模塊中信號數值分析排查問題。有了時序接口模塊,在正確無誤情況下,已經可以實現對EEPROM的讀寫操作。現在明確設計目的,我們要實現EEPROM的一字節數據讀寫,因此可以通過按鍵發送指令向EEPROM中某地址中寫入任意一個數據,之后用另一個按鍵發送讀指令將剛寫入地址中數據讀出的方式驗證讀寫操作是否正常工作。編寫控制模塊(控制模塊僅實現IIC總線空閑時才響應操作,實際上用按鍵方式猶豫時間間隔較長,不會出現多個指令搶占總線的情況,這里設計控制模塊是為了適應其他場合或功能擴展用途)

 1 `timescale 1ns / 1ps
 2 
 3 module iic_ctrl(
 4     input clk,
 5     input rst_n,
 6     input local_rd,
 7     input local_wr,
 8     
 9     input iic_busy,
10     output reg com_rd,
11     output reg com_wr
12     );
13     
14     wire ready;
15     
16     assign ready = !iic_busy;
17     
18     //寫命令
19     always@(posedge clk or negedge rst_n)begin
20         if(!rst_n)
21             com_wr <= 0;
22         else if(local_wr && ready)//iic總線空閑時才響應操作
23             com_wr <= 1;
24         else 
25             com_wr <= 0;
26     end
27     
28     //讀命令
29     always@(posedge clk or negedge rst_n)begin
30         if(!rst_n)
31             com_rd <= 0;
32         else if(local_rd && ready)
33             com_rd <= 1;
34         else 
35             com_rd <= 0;
36     end
37     
38     
39 endmodule

   剩下只需加入按鍵消抖模塊,並把按鍵消抖模塊,控制模塊還有時序接口模塊都例化在頂層文件中即可。按鍵消抖模塊在之前的博文中有講述,這里使用計數器配合狀態標志位的方式實現。需要說明的是多個按鍵使用一個按鍵消抖模塊的設計方式:只需將信號位寬定義為可變參數。

 1 `timescale 1ns / 1ps
 2 
 3 module key_filter
 4 #(parameter DATA_W    = 24,
 5             KEY_W     = 2,
 6             TIME_20MS = 4_000_000)
 7 (
 8    input clk    ,
 9    input rst_n  ,
10    input [KEY_W-1 :0] key_in ,    //按鍵 按下為低電平
11    output reg [KEY_W-1 :0] key_vld 
12 );
13 
14     reg [DATA_W-1:0] cnt;
15     reg flag;
16     reg [KEY_W-1 :0] key_in_ff1;
17     reg [KEY_W-1 :0] key_in_ff0;
18 
19     wire add_cnt,end_cnt;
20     
21     //延時計數器
22     always  @(posedge clk or negedge rst_n)begin
23         if(rst_n==1'b0)
24             cnt <= 0;
25         else if(add_cnt)begin
26             if(end_cnt)
27                 cnt <= 0;
28             else
29                 cnt <= cnt + 1'b1;
30         end
31         else
32             cnt <= 0;
33     end
34     //按下狀態才計數,松手清零
35     assign add_cnt = flag == 1'b0 && (key_in_ff1 != 2'b11); 
36     assign end_cnt = add_cnt && cnt == TIME_20MS - 1;
37     
38     //計數標志位,0有效 為了只計數一個周期
39     always  @(posedge clk or negedge rst_n)begin 
40         if(rst_n==1'b0)begin
41             flag <= 1'b0;
42         end
43         else if(end_cnt)begin
44             flag <= 1'b1;
45         end
46         else if(key_in_ff1 == 2'b11)begin//松手重新清零
47             flag <= 1'b0;
48         end
49     end
50     
51     //同步處理
52     always  @(posedge clk or negedge rst_n)begin 
53         if(rst_n==1'b0)begin
54             key_in_ff0 <= 0;
55             key_in_ff1 <= 0;
56         end
57         else begin
58             key_in_ff0 <= key_in    ;
59             key_in_ff1 <= key_in_ff0;
60         end
61     end
62 
63     //輸出有效
64     always  @(posedge clk or negedge rst_n)begin 
65         if(rst_n==1'b0)begin
66             key_vld <= 0;
67         end
68         else if(end_cnt)begin
69             key_vld <= ~key_in_ff1;
70         end
71         else begin
72             key_vld <= 0;
73         end
74     end
75     
76 endmodule

 頂層模塊例化子模塊:

  1 `timescale 1ns / 1ps
  2 
  3 module eeprom_top(
  4     
  5     input sys_clk_p,
  6     input sys_clk_n,
  7     input rst_n,
  8     input [1:0] key,
  9     //仿真接口
 10     output sda_en,
 11     output [13:0] state_c,
 12     
 13     //EEPROM接口
 14     output scl,
 15     inout sda
 16     );
 17     
 18     wire sys_clk_ibufg;
 19     (*keep = "true"*)wire busy;
 20     (*keep = "true"*)wire read,write;
 21     wire [7:0] rd_data;
 22     wire rd_data_vld;
 23     (*keep = "true"*)wire sda_reg,sda_in;
 24     (*keep = "true"*)wire [1:0] key_vld;
 25     //(*keep = "true"*)wire sda_en;
 26     //(*keep = "true"*)wire [13:0] state_c;
 27     wire [39:0] probe0;
 28     
 29     IBUFGDS #
 30     (
 31     .DIFF_TERM ("FALSE"),
 32     .IBUF_LOW_PWR ("FALSE")
 33     )
 34     u_ibufg_sys_clk
 35     (
 36     .I (sys_clk_p),     //差分時鍾的正端輸入,需要和頂層模塊的端口直接連接
 37     .IB (sys_clk_n),    // 差分時鍾的負端輸入,需要和頂層模塊的端口直接連接
 38     .O (sys_clk_ibufg)  //時鍾緩沖輸出
 39     );
 40     
 41     
 42     key_filter
 43     #(.DATA_W(24),
 44       .KEY_W(2),
 45       .TIME_20MS(4_000_000))
 46     key_filter
 47     (
 48        .clk (sys_clk_ibufg)   ,
 49        .rst_n(rst_n)  ,
 50        .key_in (key),    //按鍵 按下為低電平
 51        .key_vld(key_vld) 
 52     );
 53     
 54     iic_ctrl iic_ctrl(
 55     .clk(sys_clk_ibufg),
 56     .rst_n(rst_n),
 57     .local_wr(key_vld[1]),
 58     .local_rd(key_vld[0]),
 59     
 60     .iic_busy(busy),
 61     .com_rd(read),
 62     .com_wr(write)
 63     );
 64     
 65     iic_interface
 66     #(.SCL_CYC(1000))
 67     iic_interface(
 68     .clk(sys_clk_ibufg),
 69     .rst_n(rst_n),
 70     
 71     //用戶側接口
 72     .write_en(write),  //寫指令
 73     .read_en(read),    //讀指令
 74     .share_addr(8'h15),//讀寫復用地址
 75     .wri_data(8'h32),  //待寫入數據
 76     .wri_data_vld(1'b1),
 77     .busy(busy),       //總線忙信號
 78     .rd_data(rd_data), //讀回數據
 79     .rd_data_vld(rd_data_vld),
 80     //仿真接口
 81     .state_c(state_c),
 82     //eeprom側接口
 83     .scl(scl), //時鍾
 84     .sda_in(sda_in),
 85     .sda_en(sda_en),
 86     .sda_reg(sda_reg)
 87     );
 88     
 89     //三態門
 90     assign sda    = sda_en ? sda_reg : 1'bz;
 91     assign sda_in = sda;
 92     
 93     ila_0 ila_0 (
 94     .clk(sys_clk_ibufg), // input wire clk
 95     .probe0(probe0) // input wire [39:0] probe0
 96 );
 97 
 98     assign probe0[13:0] = state_c; //14bit
 99     assign probe0[14] = busy;
100     assign probe0[15] = scl;
101     assign probe0[16] = sda_en;
102     assign probe0[17] = sda_reg;
103     assign probe0[18] = sda_in;
104     assign probe0[19] = write;
105     assign probe0[20] = read;
106     assign probe0[39:21] = 0;
107     
108 endmodule

  看一下軟件分析出的原理圖結構(ILA IP核是之后添加的):

  此處詳細說明下雙向端口使用:頂層模塊中建立三態門結構,在輸出使能有效時作為輸出端口,無效是呈現高阻態,此時作為輸入端口,由sda_in信號讀取數值。那雙向端口如何仿真呢?很簡單,在測試文件中也構造一個三態門結構,而輸出使能信號為設計中輸出使能信號的相反值,這樣在設計中該端口呈現高阻態時,正好在測試文件中相應端口作為輸出的階段。可以注意到我在頂層模塊中加入了兩個仿真接口:state_c和sda_en,方便在測試文件中找到給出響應的位置。測試文件如下:

  1 `timescale 1ns / 1ps
  2 
  3 module eeprom_top_tb;
  4     
  5     reg sys_clk_p,sys_clk_n;
  6     reg rst_n;
  7     reg [1:0] key;
  8     
  9     wire scl;
 10     wire sda;
 11     wire sda_en;//高電平時待測試文件為輸出
 12     
 13     reg [15:0] myrand;
 14     reg sda_tb_out;
 15     wire [13:0] state_c;
 16     
 17     eeprom_top eeprom_top(
 18     .sys_clk_p(sys_clk_p),
 19     .sys_clk_n(sys_clk_n),
 20     .rst_n(rst_n),
 21     .key(key),
 22     .sda_en(sda_en),
 23     .state_c(state_c),
 24     .scl(scl),
 25     .sda(sda)
 26     );
 27     
 28     assign sda = (!sda_en) ? sda_tb_out : 1'bz;
 29     
 30     parameter CYC = 5,
 31               RST_TIME = 2;
 32     
 33     defparam eeprom_top.key_filter.TIME_20MS = 200;
 34     
 35     initial begin
 36         sys_clk_p = 0;
 37         forever #(CYC/2) sys_clk_p = ~sys_clk_p;
 38     end
 39     
 40     initial begin
 41         sys_clk_n = 1;
 42         forever #(CYC/2) sys_clk_n = ~sys_clk_n;
 43     end
 44     
 45     localparam IDLE     = 14'b00_0000_0000_0001,
 46                START    = 14'b00_0000_0000_0010,
 47                WRI_CTRL = 14'b00_0000_0000_0100,
 48                ACK1     = 14'b00_0000_0000_1000,
 49                ADDR     = 14'b00_0000_0001_0000,
 50                ACK2     = 14'b00_0000_0010_0000,
 51                WRI_DATA = 14'b00_0000_0100_0000,
 52                ACK3     = 14'b00_0000_1000_0000,
 53                RE_START = 14'b00_0001_0000_0000,
 54                RD_CTRL  = 14'b00_0010_0000_0000,
 55                ACK4     = 14'b00_0100_0000_0000,
 56                RD_DATA  = 14'b00_1000_0000_0000,
 57                NACK     = 14'b01_0000_0000_0000,
 58                STOP     = 14'b10_0000_0000_0000;
 59     
 60     initial begin
 61         rst_n = 1;
 62         #1;
 63         rst_n = 0;
 64         #(CYC*RST_TIME);
 65         rst_n = 1;
 66     end
 67     
 68     initial begin
 69         #1;
 70         key = 2'b11;
 71         #(CYC*RST_TIME);
 72         #(CYC*10);
 73         
 74         press_key_wr;
 75         #120_000;
 76         press_key_rd;
 77         #80_000;
 78         $stop;
 79     end
 80     
 81     //構造響應條件
 82     always@(*)begin
 83         if(state_c == ACK1 || state_c == ACK2 || state_c == ACK3 || state_c == ACK4)
 84             sda_tb_out <= 0;
 85         else 
 86             sda_tb_out <= 1;
 87     end
 88     
 89     task press_key_wr;
 90     begin
 91         repeat(20)begin//模擬抖動過程
 92             myrand = {$random}%400;
 93             #myrand key[1] = ~key[1];
 94         end
 95         key[1] = 0;
 96         #3000;
 97         repeat(20)begin
 98             myrand = {$random}%400;
 99             #myrand key[1] = ~key[1];
100         end
101         key[1] = 1;
102         #3000;
103     end
104     endtask
105     
106     task press_key_rd;
107     begin
108         repeat(20)begin//模擬抖動過程
109             myrand = {$random}%400;
110             #myrand key[0] = ~key[0];
111         end
112         key[0] = 0;
113         #3000;
114         repeat(20)begin
115             myrand = {$random}%400;
116             #myrand key[0] = ~key[0];
117         end
118         key[0] = 1;
119         #3000;
120     end
121     endtask
122     
123 endmodule

  我的開發板使用差分晶振作為系統時鍾,在測試文件中也要以差分信號的形式給出時鍾。與單端時鍾唯一的區別在於給出兩個初始值不同周期相同的時鍾信號。其中為了找到響應位置,引入狀態編碼,並在需要給出響應的時刻拉低總線。運行行為仿真:

整體結構:

寫操作:

讀操作:

  讀寫操作過程中狀態轉移、比特計數器、sda 、scl這些核心信號數據正常,仿真通過。實際上這是設計過程中遇到些小問題,修改代碼后的結果。下一步要在線調試了,這里是本篇博文最后一個重點要說明的內容。以往我會使用添加屬性的方式(*mark_debug = "true"*)標志要觀察的信號,再在綜合后使用debug設置向導引入調試IP核。經過實驗發現調試核的引入是通過添加約束的方式實現的,而且當要觀察別的信號時該約束部分必須改動否則報錯,所以這里使用IP核例化調試探測流程,直接在IP catalog中生成ILA IP核。這里有一個小技巧:生成IP核是只使用一個探針信號,並把位寬設置的較大,且使用OOC方式。在例化IP核后使用這個信號的不同位寬部分連接需要在線觀察的信號。這樣可以避免在反復綜合、布局布線的過程中重新編譯ILA IP核部分,節約時間。

  打開硬件管理器,下載bit流后自動打開調試界面。設置觸發條件觀察波形,這里可以很方便的利用狀態信號的不同狀態設置觸發條件。

寫操作:

 讀操作:

  寫入數據定義為8'h32,讀取bit依次是0011_0010,即為32,說明正確將寫入數據讀出。大家可以在本次實驗基礎上擴展,比如實現頁寫模式,或是使用串口來發送讀寫指令並讀回數據等。經過本次博文,掌握了IIC協議的四段式狀態機實現方式,雙向端口的三態門結構及仿真方法,並能夠靈活運用ILA IP核進行在線調試。希望大家和我一樣收獲很多。歡迎交流~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM