1. 程式人生 > >IC設計基礎系列之CDC篇10:跨時鐘域訊號傳輸(二)——資料訊號篇

IC設計基礎系列之CDC篇10:跨時鐘域訊號傳輸(二)——資料訊號篇

因為學習了其他方面的知識,耽擱了更新。今天我們就聊聊跨時鐘域中的資料訊號傳輸的問題。主要內容預覽:

   ·使用握手訊號進行跨時鐘域的資料傳輸

  ·FIFO的介紹

  ·在進行FIFO的RTL設計前的問題

  ·FIFO的RTL設計(與模擬測試)

  ·跨時鐘域中的資料訊號傳輸總結

一、使用握手訊號進行跨時鐘域的資料傳輸

  下面敘述的意義相同:前級時鐘=傳送時鐘; 後級時鐘=取樣時鐘=接收時鐘

  使用握手訊號傳輸資料不是我們的重點,重點是FIFO的設計。在使用握手訊號進行資料傳輸之前,我們說說為什麼雙D觸發器鏈不應該用於資料的傳輸。

  一般情況下,我們要傳輸的資料都是多位的,也就是以資料匯流排

的形式傳播的。如果我們使用簡單的多組D觸發器鏈進行同步資料的話,由於每一種D觸發器鏈第一級觸發器都有可能出現亞穩態,穩定下來之後的電平可能出錯;由於有多組D觸發器鏈,就有可能傳送多個電平出錯,因而導致資料出錯,如下圖所示:

         

可以看到,原來前面的時鐘域傳送的0111資料變成1000的時候,捕獲時鐘的時鐘取樣本來要才到0111的,由於保持時間不足,導致了b[2]、b[1]出現亞穩態,而且b[2]穩定後的電平是錯誤的電平,由此就傳輸了錯誤的資料。因此直接使用觸發器鏈進行同步資料是不建議的。

  於是乎,我們就看看使用握手訊號是怎麼進行傳輸資料的。

  ·資料變化速率比取樣時鐘域低

  當資料的速率比取樣時鐘域慢時,也就是說,資料速率相對於取樣時鐘域(接收資料的時鐘域)來說是慢時鐘,可以使用控制訊號進行同步,在取樣到慢時鐘域的控制訊號後,接收取樣資料,時序圖如下所示:

                

這裡只給出了時序圖,電路可以按照時序圖進行設計。需要注意的是,這個額外的控制訊號(wr_en_s)是由前面的邏輯產生的。這與下面的電路不一樣:

在下面的這個電路中,控制訊號是由上升沿檢測電路產生的,而且是接收時鐘驅動的上升沿檢測電路(也就是說這個控制訊號是由後級的邏輯產生的),電路如下所示:

                 

下面我們來分析一下這個電路吧,時序圖如下所示:

                 

  從時序圖中可以看到,可以用上升沿檢測電路,檢測傳送時鐘的上升沿,然後這個沿相當於使能訊號。上面中,檢測到了第二個傳送時鐘的上升沿,之後就有了使能訊號,取樣的資料也是第二時鐘傳送的資料DB,因此對應起來是沒有問題。這裡由於沒有檢測到EN1第一個上升沿,所以沒有采樣到DA也是正常的,這是因為前面的波形沒有畫出的緣故。

  ·當資料的速率(或者說傳送時鐘的頻率)略高於接收時鐘端

  由於傳送時鐘比接收時鐘快,於是對於接收時鐘,傳送時鐘就相當於窄脈衝訊號,這樣我們就有思路了。我們還是上面一樣,採用上升沿檢測訊號當做使能訊號;但是問題來了,傳送時鐘是快的,可能會錯過上升沿。於是乎,我們就把窄脈衝捕獲電路和上升沿檢測電路結合起來。先是窄脈衝捕捉電路,把時鐘的沿捕捉到,然後進行邊沿檢測,檢測得到的結果作為使能訊號,電路圖如下所示:

               

具體就不分析了,需要強調的是,這個是傳送時鐘也不能太快。

  ·資料變化速率比取樣時鐘快很多

當資料的速率比取樣的時鐘的速率快很多時,對應到時鐘的關係就是——傳送時鐘和比接收時鐘快很多時,這個時候取樣時鐘就取樣不到資料,或者說會採漏部分資料,因此這時候就不能用握手訊號了。也許有人說我可以增加使能訊號,把資料拉長啊,等後面的取樣時鐘取樣到使能訊號、接收到資料之後,我再改變時鐘。這種方法的實質就是硬生生地把資料變化率蓋滿,也就是把傳送時鐘域的時鐘改慢,跟前面的資料變化速率比取樣時鐘域低的實質是一樣的。因此當資料變化率比取樣時鐘快很多時,就要取樣下面介紹的FIFO了。

二、FIFO的介紹

  終於寫到FIFO了,FIFO 是first in first out的縮寫,也就是“先進先出”;從字面理解,就是說,資料先進來的,就先出去。前面說了當快時鐘域傳輸資料到慢時鐘域時,就推薦用FIFO了。FIFO無論是快到慢,還是慢到快,都可以使用它進行資料的緩衝,可謂是“快慢皆宜”啊。

  FIFO的工作流程如下:

  FIFO在寫時鐘狀態訊號的控制下,根據寫使能訊號往FIFO裡面寫資料,當寫到一定程度後,FIFO存不下新資料的了(或者要以犧牲丟棄舊資料為貸款),這時候就不能往FIFO裡面寫資料了;在讀時鐘狀態訊號的控制下,根據讀使能訊號從FIFO裡面讀出資料,當讀到一定程度後,FIFO裡面沒有資料了,就不能繼續讀了,不然就會讀出錯誤的資料。根據讀寫時鐘是否一致(同步),FIFO的種類又可以分成同步FIFO和非同步FIFO。FIFO能夠讀寫資料,肯定需要資料的儲存單元,這裡儲存資料的單元往往是雙口RAM

  FIFO的寫過程:在復位的時候,FIFO(雙口RAM)裡面的資料被清零(也就是不存在資料)。復位之後,只能進行操作,因為什麼都沒有,讀資料會讀出錯誤的值。這個時候,當外部給FIFO寫使能訊號了,在時鐘的驅動下,資料就會被寫入FIFO裡面的RAM儲存單元(儲存單元的地址寫指標暫存器的內容確定,寫指標暫存器中的內容稱為寫地址,復位的時候為0),寫完資料之後(或者在允許寫資料之後),這個寫指標暫存器就會自動加一,指向下一個儲存單元。當寫到一定程度的時候(寫指標暫存器到達一定的數值),舊資料還沒有被讀出的時候,再寫入新資料就會把舊資料給覆蓋,這個時候稱為寫滿,需要產生寫滿的狀態訊號(full,簡稱滿)。在寫滿的時候,需要禁止繼續寫資料。

  FIFO的讀過程:在復位的時候,FIFO裡面沒有資料,因此這個時候是禁止讀資料的。當裡面有資料之後,外部讀訊號到來後,在時鐘訊號到來的時候,FIFO就會根據讀地址(由讀指標暫存器的內容確定,讀指標暫存器裡面的內容稱為讀地址,復位的時候為0)讀出相應的資料,讀出資料之後(或者說允許RAM讀之後),讀指標暫存器自動加一。指向下一個儲存單元。當讀到一定的程度的時候,也就是FIFO裡面沒有資料了,這個時候稱為讀空,需要產生讀空的狀態訊號(empty,簡稱)。在讀空的時候,需要禁止繼續讀資料。

  根據前面的描述,我們就可以知道,在復位的時候,FIFO空有效、滿無效,禁止讀資料,只能往裡面寫資料。當把FIFO裡面的內容都寫滿的時候,FIFO滿有效,空無效,這時候只能讀資料,而不能繼續往裡面寫資料。

三、在進行FIFO的RTL設計前的問題

  根據FIFO的介紹內容,我們試著來推導一下FIFO大致由哪些部分構成。

  首先,FIFO需要儲存資料,因此就需要儲存器;由於需要讀,也需要寫,於是乎就需要一個DPRAM(double  port  RAM,雙埠RAM)。

  然後,RAM需要讀/寫地址,它才知道在哪裡讀/寫資料,因此需要讀/寫地址產生模組,也就是需要讀/寫地址暫存器。什麼時候進行寫,什麼時候進行讀,因此需要讀/寫控制邏輯空滿狀態的訊號產生邏輯

  最後,空滿訊號的產生需要通過對讀地址和寫地址的比較,由於讀寫地址在不同的時鐘域,因此需要同步電路進行同步。

  通過上面的簡單介紹,我們就得到了FIFO的大致框圖如下(主要是告訴大家為什麼會有這麼一個框圖):

            

現在來看看這些訊號是什麼意思吧:

w:寫時鐘域一方的訊號;r:讀時鐘域一方的訊號

wclk:寫時鐘

wrst_n:寫復位,低有效

rclk:讀時鐘

rrst_n:讀復位,低有效

winc:外部輸入的寫使能訊號

rinc:外部輸入的讀使能訊號

wdata :要寫進資料,要寫進FIFO裡面儲存的資料。

rdata:讀資料,從FIFO裡面讀取出來的資料。

wdata:要讀出的資料,要讀出FIFO裡面儲存的資料

wfull:寫滿的狀態訊號

rempty:讀空的狀態訊號

wclken:RAM的允許寫訊號,在這個訊號有效的情況下,RAM才能寫得進資料。

rclken:RAM的允許讀訊號,在這個訊號有效的情況下,RAM才能讀得出資料。

waddr:RAM的寫地址。

raddr:RAM的讀地址

wptr:要同步到寫時鐘域的讀指標(讀地址)。

rptr:要同步到讀時鐘域的寫指標(寫時鐘)

wq2_rptr:讀地址rptr同步到寫時鐘域的讀地址(格雷碼,後面會說為什麼用格雷碼)

rq2_wptr:寫地址rptr同步到讀時鐘域的讀地址(格雷碼,後面會說為什麼用格雷碼)

syn_r2w:讀同步到寫觸發器鏈中間訊號。

syn_w2r:寫同步到讀觸發器鏈中間訊號。

介紹完這些訊號之後,我開始聊聊FIFO設計前的一些問題。

  ·FIFO的空滿訊號產生

空狀態訊號:

  一開始復位的時候,空訊號是有效的,當寫了資料之後,空訊號就無效了。然後當資料被讀取完之後,空訊號就有效了。那麼什麼時候資料被讀取完了呢,也就是資料被讀取完的時候有什麼特徵呢?特徵就是讀地址和寫地址相等,如下所示:

                 

  由於讀地址要追趕寫地址,在趕上的時候,地址全等就證明了讀空了。

  也許有人會問:寫地址由於要同步到讀時鐘域去,會存在同步延時的,比如 說t=0s的時候同步過去,此時寫地址為A;在t=2s的時候A同步過來了,但是這個 時候寫地址已經變為A+2,而你同步過來的這個寫地址為A。如果在t=2s這個時候讀地址=A,即讀地址=寫地址,讀趕上了寫,按照上面的設計想法就會產生讀空訊號,但是實際上是不相等的,也就是實際上讀並沒有趕上寫,即沒有讀空的,這不就是產生錯誤的讀空訊號了嗎?

  首先,是存在這樣的情況,但是這種情況不是設計錯誤。一方面由於我們要產生讀空訊號,目的是也就是防止繼續讀從而讀出錯誤的資料;實際上沒有讀空,即使產生了讀空訊號,也是沒有影響,相當於提前判斷產生讀空訊號而已。另一方面由於是讀時鐘域取樣的讀的地址,這個讀地址是實時的;寫地址是延時的,當這個兩者相等時,我們這個實時的地址實際的寫地址的時候就產生讀空訊號,防止了讀空。因此即使產生讀空訊號,也不會因為讀空而產生錯誤的資料。因此是沒有設計錯誤的。

寫滿狀態:

  一開始復位之後,進行寫資料;由於地址(假設地址是4位,也就是深度是4位)是可以回捲的,也就是說,寫指標從3寫到15後,繼續寫又會返回到3那裡;假如復位後讀操作只讀到地址3那裡就不讀了,那麼這個時候就寫滿了。也就是說,寫滿的時候,寫地址和讀地址是相等的,如下所示:

             

於是乎,我們該怎麼區分在讀地址寫地址相同的時候是讀空還是寫滿呢?下面來介紹一種常用的方法:

  將地址深度拓寬1位當做標誌位,回捲一次標誌位取反。比如上面的例子中,4bit地址拓寬為5bit,那麼讀地址就是3(由於讀地址沒有回捲,所以是(0)0011)那裡,當寫地址回捲之後與讀地址相同(由於寫地址回捲了,最高位取反,所以是(1)0011),因此這就是寫滿了。當讀地址回捲之後,變成10011,這個時候,就讀空了。也就說,雖然DPRAM的深度還是4bit,但是我們在進行設計地址暫存器的時候,增多一位當做狀態。然後讀寫地址全相等的時候,表示是讀空;除了標誌位外,剩餘的地址為全部相等,那麼就表示是寫滿

  這裡還是會產生與前面的空訊號一樣的問題,也就是同步過來的讀訊號是延時的值,與前面一樣,是不會影響寫滿訊號的,不屬於設計錯誤。

  除了上面這種方法之外,在同步FIFO中,還可以使用計數器的方法。設定一個狀態計數器,復位的時候為0。的時候,計數器加1的時候,計數器減1。那麼很容易得出,計數器為0的時候,就是讀空就有效了;當計數器等於FIFO的深度(2^n  -  1)時,就說明寫滿了。這種方法如果FIFO深度很大的話,就需要很大的計數器了,所以有侷限性。

  從上面的分析中,由此也可以知道,空訊號的產生需要把寫地址同步到讀時鐘域,然後進行比較(比較之後產生);滿的訊號需要把讀地址同步到寫時鐘域,然後進行比較(比較之後產生)。

  ·為什麼要選擇格雷碼作為同步地址的編碼

  首先,我們知道,讀地址需要跟寫地址比較來產生空和滿訊號,然後對於非同步FIFO,讀寫為不同時鐘,如果直接取樣,就會有:類似前面資料產生多位亞穩態的問題,(時序圖就不畫了)比如寫地址從00111改變從01000的時候,讀時鐘恰好取樣,那麼除了最高位外,其它的4位都有可能產生亞穩態,有可能同步得錯誤的地址。這是引入格雷碼的一個原因。另外一個原因就是:無論是讀地址還是寫地址,在(允許)進行讀和寫之後,地址都是加1,而不是加2或者加3等其他的值。為什麼會這樣呢?我們來看看格雷碼的編碼:

               

  從上圖中我們可以知道,從地址0變成地址1,格雷碼和二進位制碼都是0000變成0001;地址從1變成地址2,格雷碼是0001變成0011,而二進位制是0010......我們很容易得到,在相鄰地址變化中,格雷碼只有一位發生變化,如地址從7變為8時,格雷碼是0100變成1100,也就是隻有最高位傳送變化;我們再來看看二進位制編碼,二進位制編碼則有可能全部都改變,地址從7變為8時,二進位制碼是0111變成1000,4位都發生了變化。假如取樣的時候地址恰好從7變為8時,那麼二進位制編碼就有多位發生亞穩態,穩定後的值什麼都有可能;而格雷碼由於只有最高位跳變,第三位由於沒有跳變,不會產生亞穩態可以穩定正確取樣,穩定後的值只有0100和1100,地址只差數值1,是不會影響判斷的結果的(因為是同步過來的,是個延時的值,不打緊)。

  知道了格雷碼的優點之後,我們就要使用各格雷碼了。由於RAM的讀寫地址都是(傳統)二進位制編碼,這裡使用格雷碼有兩種使用方法,第一種使用方式是,將二進位制編碼轉換成格雷碼,然後把格雷碼同步過去,再把同步過來的格雷碼反轉換成二進位制碼,進行二進位制地址和二進位制地址的比較;另外一種使用方式是,將二進位制編碼轉換成格雷碼,然後把格雷碼同步過去,然後使用格雷碼進行比較。這裡使用第一種方式,雖然這種方式比較需要多兩塊格雷碼轉二進位制的電路,但是我們可以實時比較,能將定址的二進位制馬上與同步過來的“延時”二進位制進行比較;使用格雷碼比較的話,實際值會慢一拍(因為實時方的格雷碼需要寄存輸出,會慢一拍,如果不寄存輸出,就有可能產生毛刺)。

然後格雷碼的與二進位制的互相轉換如上圖,下面是轉換講解(左邊為格雷轉二進位制,右邊為二進位制轉格雷):

                  

在布林代數裡面有A^B=C →A=B^C

·FIFO的深度選擇

  首先,FIFO是有寬度深度的。FIFO的寬度就是RAM的位寬,也是要存入/取出資料的位寬;然後深度就RAM的地址深度,也就是最多可以存多少個數據。例如FIFO的寬度是8bit,那麼FIFO每個時鐘存入的資料的寬度也是8bit;FIFO的深度是10bit,那麼FIFO就最多可以存2^10=1024個8bit的資料。

  我們要儲存資料,FIFO的深度選小了,在寫的時候就很有可能寫溢位;深度選大了,就會浪費儲存面積。選擇一個合適的深度,最主要的就是防止寫溢位;由於FIFO要讀也要寫,那麼FIFO的(地址)深度該選多少合適呢?

  這就和你的讀寫速度有關了,根據讀寫速度來選擇FIFO深度,此外需要注意的是,在使用FIFO的時候,寫的平均吞吐量要和讀的平均吞吐量相等。

現在舉例來說明:設你的寫時鐘頻率為100M,讀時鐘為200M;寫速度為:100個時鐘寫如60個數據,讀速度為:100個時鐘讀出30個數據 。

①首先驗證你的資料吞吐量是否相等:

    寫的平均吞吐量=100M*60/100=60M個數據/S

    讀的平均吞吐量=200M*30/100=60M個數據/S

因此這兩個是相等的,不會發生寫溢位;如果寫大於讀,那麼FIFO早晚會很快寫滿溢位;如果讀大於寫,那麼FIFO遲早會讀空。因此需要讀寫吞吐量相同。

②求最低深度

  我們知道讀寫速度之後,就可以判斷FIFO要多少深度才合適了,由於FIFO的深度考慮是出於我們要防止寫溢位(寫滿),因此我們考慮寫的情況:

寫的時候是100個時鐘寫60個數據,我們不知道它是怎麼樣子寫的,我們從悲觀的角度出發,也就是從寫得最密集的角度出發:前100個寫時鐘的最後60個時鐘寫60個數據,然後後100個寫時鐘的最前60個時鐘寫入60個數據,也就是在120個寫時鐘內寫入了120資料。

這120個寫時鐘的時間是:

                      

在這段時間內,根據讀寫的速率要求,肯定是要讀資料的,讀出的資料為:

                      

因此我們FIFO需要的深讀就等於沒有讀出的資料的個數就是:

                  120-72 =48

然而由於上面讀的方式是一個平均的方式,此外FIFO的深度一般是2的整數次冪,要符合格雷碼的編碼轉換規則,因此我們深度一般不選擇48,而是選擇比它大的2的整數次冪的數,比如64或者128。FIFO深度的選擇過程就如上面所述,(這裡參考《FPGA深度解析》)。

四、FIFO的設計(與模擬測試)

接下來我們就要設計一個非同步FIFO了,這裡我們設計的FIFO跟上面的有點不同,整體結構如下所示:

                 

這裡主要是多出了兩個狀態訊號:

  wfull_almost:將滿訊號。為了預防萬一,FIFO要滿的時候,使這個訊號有效,當面的模組時鐘(前級電路)檢測到這個訊號有效後,就把winc變為無效,用來提供給前面電路的指示訊號。這個訊號要比full訊號提前,因為考慮到在判斷出滿之後,還需要一些動作(延時),才能不寫;於是乎我們就用將滿訊號來補充這些延時,而不是等到滿訊號才做出反應。

  rempty_almost:將空訊號。這個也是為了考慮在空訊號判斷出來之後,到禁止繼續讀可能有延時,從而設立這個標識,在將空的時候就禁止繼續讀資料。

將滿訊號和將空訊號的關鍵因素就是讀寫地址之間的舉例(間隔),那我們來看看寫和讀的間隔怎麼產生:

對於寫時鐘域,我們是要產生幾乎滿訊號,這對應的間隔就是看看寫地址還有多少就趕上了讀地址,求出這兩個地址之間的間隔,然後再與預設的間距比較,如果這個間隔小於預設的間距,那麼就產生幾乎滿的訊號。那麼我們這個間隔怎麼求:

  ·當讀寫狀態位相同的時候,如下圖所示:

                    

  由於最高位相同,所以寫需要回卷才能最上讀,那麼間隔也就是A+B;假設FIFO的深度是D,寫地址為waddr,讀地址為raddr,那麼間隔就是D-C=D-(waddr-raddr)=D+raddr-waddr.(注意,這裡的讀/寫地址不包括狀態位)

  ·當讀寫狀態不同位時,如下圖所示:

                   

這時候waddr再有C就追上raddr了,因此間隔就是raddr-waddr。

上面是對於寫區域間隔的生成,下面就來說說讀區域的間隔怎麼產生吧:

  ·狀態位一樣的時候,也就是沒有回捲的時候,如下圖所示:

                     

很顯然,無論是加不加狀態位,都是間隔都是waddr-raddr,也就是說,還有waddr-raddr的舉例,raddr就追上了waddr。

  ·當狀態位不一樣時:

                   

  需要回捲才能追上寫,因此間隔就是:

  FIFO的深度-(raddr-waddr)=FIFO+waddr-raddr(這裡的讀寫地址不包括狀態位)。

  當加上狀態位之後,我們發現,間隔是可以用raddr-waddr來表示的,比如raddr=1011,waddr是0011,間隔是8;加上狀態位後,Raddr=01011,Waddr=10011,間隔也可以表示為01011-10011=01000=8(借位是會被省略掉的),因此用加上狀態位後,間隔可以表示為waddr-raddr。

因此在讀時鐘域,間隔的表示就是帶狀態位的waddr-raddr。

  說完了幾乎滿和幾乎空訊號,我們再聊聊上面的框圖,整個FIFO可以分成寫邏輯模組、讀邏輯模組、寫/讀同步讀/寫模組。其中

·寫邏輯的模組的功能是:根據狀態訊號和外部的寫訊號產生對RAM的寫控制訊號、產生RAM的地址訊號、產生空和將空的狀態的狀態訊號。因此寫邏輯模組可以分成3個部分:①(RAM)寫控制邏輯部分;②RAM寫地址產生部分;③狀態產生部分。

  ①RAM寫控制邏輯部分的功能就是,產生RAM的寫使能訊號:wclken有效的條件是:外部訊號寫使能訊號winc來了,而且此時滿訊號沒有效,這個時候就允許往RAM裡面寫資料了。

  ②RAM寫地址產生部分的功能就是產生RAM的地址和產生格雷碼地址(產生的格雷碼地址傳輸給同步模組):復位的時候,RAM的地址waddr為0;此後,在寫時鐘上升沿檢測到RAM的寫使能wclken有效之後,waddr自動加一,指向下一個單元,wclken無效waddr則不變。我們使用比RAM地址寬1位的地址暫存器進行遞增,地址暫存器的最高位充當空滿訊號時候的狀態比較位。

  ③狀態產生部分的功能就是:將同步模組過來的格雷碼轉換成二進位制,然後跟RAM寫地址產生部分傳來的地址進行比較,產生將滿訊號和滿訊號。(將滿訊號的產生就是兩個地址小於某個間隔時有效,滿訊號產生則是間隔等於0或者:間隔只有一個地址只差,但是這個時候RAM的寫訊號還有效)。

·讀邏輯也是一樣,這裡不再詳述,具體細節我們在程式碼後面進行討論。

·寫/讀同步讀/寫模組其實就是雙D觸發器(鏈)

程式碼如下所示:

 頂層模組
//Async_FIFO ,4bit字寬,4bit深度
module Async_FIFO #(
                    parameter DATA_WIDTH = 4    ,
                    parameter DEEP_WIDTH = 4
                    )(
    //寫時鐘域訊號
    output                        wfull            ,
    output                        wfull_almost    ,
    input    [DATA_WIDTH-1:0]    wdata            ,
    input                        winc            ,
    input                        wclk             ,
    input                        wrst_n            ,
    //讀時鐘域訊號
    output                        rempty            ,
    output                        rempty_almost    ,
    output    [DATA_WIDTH-1:0]    rdata            ,
    input                        rinc            ,
    input                        rclk            ,
    input                        rrst_n
    );
//中間的連線訊號
wire [DEEP_WIDTH-1:0] raddr    ;
wire [DEEP_WIDTH:0]      rptr    ;
wire rclken                 ;
wire [DEEP_WIDTH:0] rq2_wptr;    
    
wire [DEEP_WIDTH-1:0] waddr    ;
wire [DEEP_WIDTH:0]      wptr    ;
wire wclken                 ;
wire [DEEP_WIDTH:0] wq2_rptr;        
    
Read_Data    inst_Read_Data(
    .rempty            ( rempty ),
    .rempty_almost    ( rempty_almost ),
    .raddr            ( raddr ),   
    .rptr            ( rptr ),    
    .rclken            ( rclken ),    
    .rinc            ( rinc ),    
    .rq2_wptr        ( rq2_wptr ), //input,同步過來寫格雷碼指標
    .rclk            ( rclk ),    
    .rrst_n         ( rrst_n )
);


DFF_Sync    inst_r2w(
    .dff_out        ( wq2_rptr ),
    .dff_in         ( rptr ),
    .dff_clk        ( wclk ),
    .dff_rst_n        ( wrst_n )
);




Write_Data    inst_Write_Data(
    .wfull            ( wfull ), //幾乎滿訊號
    .wfull_almost    ( wfull_almost ), //幾乎空訊號
    .waddr            ( waddr ), //輸出給RAM的地址
    .wptr            ( wptr ), //格雷碼地址指標
    .wclken            ( wclken ), //寫RAM訊號
    .wq2_rptr        ( wq2_rptr ), //同步過來的讀格雷碼指標
    .winc            ( winc ), //外部輸入的使能訊號
    .wclk             ( wclk ), //寫時鐘
    .wrst_n            ( wrst_n )  //寫復位
    
);    


DFF_Sync    inst_w2r(
    .dff_out        ( rq2_wptr ),
    .dff_in         ( wptr ),
    .dff_clk        ( rclk ),
    .dff_rst_n        ( rrst_n )
);


ram_16x16    ram_16x16_inst (
    .data ( wdata ),
    .rdaddress ( raddr ),
    .rdclock ( rclk ),
    .rden ( rclken ),
    .wraddress ( waddr ),
    .wrclock ( wclk ),
    .wren ( wclken ),
    .q ( rdata )
    );




endmodule 



頂層模組
 寫時鐘域
module Write_Data #(
                    parameter DEEP_WIDTH = 4    ,
                    parameter FIFO_DEEP  = 5'd16     ,
                    parameter GAP_WIDTH  = 3
                    )(
    output                            wfull            , //幾乎滿訊號
    output    reg                        wfull_almost    , //幾乎空訊號
    output        [ DEEP_WIDTH-1:0]    waddr            , //輸出給RAM的地址
    output    reg    [ DEEP_WIDTH:0]        wptr            , //格雷碼地址指標
    output                            wclken            , //寫RAM訊號
    input        [ DEEP_WIDTH:0]        wq2_rptr        , //同步過來的讀格雷碼指標
    input                            winc            , //外部輸入的使能訊號
    input                            wclk             , //寫時鐘
    input                            wrst_n              //寫復位
    
);
reg [ DEEP_WIDTH:0] waddr_reg ;//地址暫存器,5位
reg    [ DEEP_WIDTH:0] wq2_rptr_bin ;//讀指標同步到寫時鐘域後,從格雷碼轉換成二進位制
reg [ DEEP_WIDTH:0] wgap_reg ;//寄存間隔的距離


//第一部分,寫RAM使能訊號的生成
assign wclken = winc &&(~ wfull );


//--------------------------------------//


//第二部分,產生RAM的地址