案例采用明德揚設計思想完成。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
可以看出狀態機部分依次分為:時序邏輯描述狀態轉移,組合邏輯描述狀態轉移條件,連續賦值定義狀態轉移條件以及時序邏輯描述狀態相關輸出。並且至始至終使用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核進行在線調試。希望大家和我一樣收獲很多。歡迎交流~