本原創文章由深圳市小眼睛科技有限公司創作,版權歸本公司所有,如需轉載,需授權并注明出處(www.meyesemi.com)
1.實驗簡介
? 實驗目的:
? ?掌握紫光平臺的 RAM、ROM、FIFO IP 的使用
? 實驗環境: Window11 PDS2022.2-SP6.4
? ?芯片型號: PG2L50H-484
2.實驗原理
? ?不管是 Logos 系列或者是 Logos2 系列,其 IP 配置以及模式和功能均一致,不會像 PLL 那樣有動態配置以及內部反饋選項的選擇等之間的差異,所以是 RAM、ROM、FIFO 是通用的。
2.1. RAM 介紹
? ? ? RAM 即隨機存取存儲器。它可以在運行過程中把數據寫進任意地址,也可以把數據從任意地址中讀出。其作用可以拿來做數據緩存,也可以跨時鐘,也可以存放算法中間的運算結果等。
? ? ? 注意,PDS的IP配置工具中提供兩種不同的RAM,一種是Distributed RAM(分布式RAM) 另一種是 DRM Based RAM,分布式 RAM 用的是 LUT(查找表)資源去構成的 RAM,這種 RAM 會消耗大量 LUT 資源,因此通常在一些比較小的存儲才會用到這種 RAM,以節省 DRM 資源。而 DRM Based RAM 是利用片內的 DRM 資源去構成的 RAM,不占用邏輯資源,而且速度快, 通常設計中均使用 DRM Based RAM。
RAM 分為三種,如下表所示:
RAM類型 | 特點 |
單端口 RAM | 只有一個端口可以讀寫,只有一個讀寫口和地址口 |
偽雙端口 RAM | 有 wr 和 rd 兩個端口,顧名思義,wr 只能寫,rd 只能讀 |
真雙端口 RAM | 提供 A 和 B 兩個端口,兩個端口均可以獨立進行讀寫 |
注意,當使用真雙端口時,要避免出現同時讀寫同個地址,這會造成寫入失敗,在邏輯設計上需要避開這個情況。
以下給出比較常用的 RAM 的配置作為介紹,通常我們比較常用偽雙端口 RAM 來設計,
如圖所示:
下圖為 IP 配置:
? ? ? 注意,如果勾選 Enable Output Register(輸出寄存),輸出數據會延遲一個時鐘周期。具體每個端口的含義這里參考官方手冊,大家也可以自行查看 IP 手冊,如下圖所示:
? ? ? DRM Resource Type:用于配置所建 RAM IP 核用的是哪種資源,不同芯片型號可選資源是不一樣的,有的是 9K,有的是 18K,有的是 36K,如果沒有特殊情況,直接 AUTO 即可。
2.1.1. RAM 的讀寫時序
? ? ?配置成不同模式的時候,RAM 的讀寫時序是不一樣的,真雙端口和單端口的 RAM 配置均有三種模式,而偽雙端口只有一種。由于真雙端口和單端口的配置是一樣的,這里以真雙端口為例子。
? ? 分為 NORMAL_WRITE( 正 常 模 式 ) 、 TRANSPARENT_WRITE( 直 寫 ) 、 READ_BEFORE_WRITE(讀優先模式)三種模式。
? ? 而偽雙端口不屬于上面三種模式,有它獨特的模式。這幾種模式的差異就在于讀寫時序的不同,接下來,我們來分析讀寫時序。
? ? 以下時序圖均來自官方 IP 手冊,并且均未使能輸出寄存。注意 wr_en 為 1 時表示寫數據, 為 0 表示讀數據。
2.1.1.1.NORMAL_WRITE
? ??在 NORMAL_WRITE 這種模式下,可以看到,當時鐘的上升沿到來,且 clk_en 和 wr_en均為高電平時,就會把數據寫到對應的地址里面,如圖中的 1 時刻。然后看讀數據端口,當wr_en 不為 0 的時候,a_rd_data 一直為 Don’t Care 狀態,而當時鐘上升沿到來,且 clk_en為高電平,wr_en 為低電平時,a_rd_data 輸出當前 a_addr 里的數據,即 Mem(ADDR1)和 ADDR0 里的 D0。
2.1.1.2.READ_BEFORE_WRITE
? ? ?在 READ_BEFORE_WRITE 這種模式下,可以看到在 1 的時刻,時鐘上升沿到來,且clk_en 和 wr_en 均為高電平,D0 寫進了 ADDR0 里面,但是注意看此時的 a_rd_data 和a_addr,可以發現,此時 a_wr_en 并不為 0,可 a_rd_data 還是輸出了上一刻 ADDR0 的數據(因為不是輸出 D0)。之后,a_wr_en 拉低,此時才是讀數據,在 3 時刻,把 ADDR0 的數據讀出來,a_rd_data 才輸出了 D0。
? ? ?所以總結一下,這個模式其實就是進行寫操作時,讀端口會把當前寫的地址的原始數據輸出,因此叫讀優先模式很合情合理對吧,顧名思義,就是我優先把原來的數據讀出來。
2.1.1.3.Transparent_Write
? ??在 Transparent_Write 這種模式下,可以看到在 1 的時刻,時鐘上升沿到來, 且clk_en 和 wr_en 均為高電平,D0 寫進了 ADDR0 里面,但是注意看此時的 a_rd_data 和a_addr,可以發現,此時 a_wr_en 并不為 0,可 a_rd_data 居然直接輸出了 D0,之后 a_wr_en拉低,進入讀狀態,在 2 時刻,再一次把 ADDR0 的數據讀出來,輸出了 D0。
? ??分析總結一下,根據 1 時刻的情況,我們可以得出結論,在這種模式下,當我們進行寫操作時,讀端口會馬上輸出我們寫入的數據。所以叫直寫模式。
2.1.1.4. 偽雙端口的讀寫時序
? ??注意:wr_en 為 1 時是寫操作,為 0 是讀操作。
? ??偽雙端口的讀寫時序與上面三種都不同,我們看圖 8 的時序來分析:
? ??注意看 1 時刻,此時 wr_en 和 wr_clk_en 均為高電平,所以是寫操作,所以 1 時刻就是往地址 ADDR0 里寫入 D0,注意此時的 rd_addr 和 rd_data,可以看到這一時刻 rd_addr 是 ADDR2,然后進行寫操作時,rd_data 同樣輸出了 ADDR2 里的數據,而此時 wr_en 還是高電平。接下來看 2 和 3 時刻,此時 wr_en 為 0,rd_clk_en 是高電平,所以是讀操作,此時分別讀出 ADDR1 和 ADDR0 里的數據,之后 rd_clk_en 變成低電平,讀時鐘無效,可以看到rd_data 保持 D0 輸出。
? ??分析總結一下,主要是 1 時刻,大家可以看到 1 時刻往 ADDR0 寫入了 D0,讀端口卻輸出了 ADDR2 中的數據。仔細觀察可以得出結論:偽雙端口 RAM 在進行寫操作的時候,會把當前讀端口指向的地址的數據輸出。是不是有點像直寫?只不過直寫是輸出寫入的數據,而偽雙端口是輸出讀端口指向的地址的數據。
? ??具體大家可以結合視頻講解。
2.2 ROM 介紹
? ? ?ROM 即只讀存儲器,在程序的運行過程中他只能被讀取,無法被寫入,因此我們應該在初始化的時候就給他配置初值,一般是在生成 IP 的時候通過導入.dat 文件對其進行初值配置。
? ? ?注意,PDS的 IP配置工具中提供兩種不同的 ROM,一種是 Distributed ROM(分布式 ROM)另一種是 DRM Based ROM,分布式 ROM 用的是 LUT(查找表)資源去構成的 ROM,這種 ROM會消耗大量 LUT 資源,因此通常在一些比較小的存儲才會用到這種 RAM,以節省 DRM 資源。而 DRM Based ROM 是利用片內的 DRM 資源去構成的 ROM,不占用邏輯資源,而且速度快,通常設計中均使用 DRM Based ROM。
? ? ?以下給出比較常用的 ROM 的配置作為介紹,由于只能讀,因此其均為單端口 ROM 如圖所示:
? ? 下圖為 IP 配置:
? ??注意,如果勾選 Enable Output Register(輸出寄存),輸出數據會延遲一個時鐘周期。
? ??同時,可以看到 Enable Init 選項是默認勾選的,并且不可取消。
? ??導入的數據的格式只能為二進制或者是十六進制,demo 選擇十六進制。
? ??具體每個端口的含義這里參考官方手冊,大家也可以自行查看 IP 手冊,如圖所示:
? ? ? 可以看到圖中給出的是完整的接口列表,一般我們只需要 addr、rd_data、clk、rst這四個信號即可。
? ? ?以下時序圖均來自官方 IP 手冊,并且均未使能輸出寄存。
2.2.1. ROM 的讀時序
? ? ? 可以看到該時序是非常簡單的,比如在 TI 時刻,當 clk 上升沿到來時,且 clk_en 為高電平時,給出要讀出的地址,rd_data 就會輸出數據,在不勾選輸出使能寄存的情況下,rd_data的輸出會有延遲,具體時間可以從仿真里看到,所以我們在下個時鐘周期的上升沿即 T2 時刻的上升沿才能獲取到 ROM 讀出的值。
? ? ??所以整體時序非常簡單,如果勾選了 clk_en 信號,就要給 clke_en 高電平才能讀數據,如果不勾選 clk_en 信號,就一直根據地址讀取 ROM 數據。
2.3. FIFO 介紹
? ? ?FIFO 即先入先出,在 FPGA 中,FIFO 的作用就是對存儲進來的數據具有一個先入先出特性的一個緩存器,經常用作數據緩存或者進行數據跨時鐘域傳輸。FIFO 和 RAM 最大的區別就是 FIFO 不需要地址,采用的是順序寫入,順序讀出。
? ? ?在紫光的 IP 工具中又分為 Distribute FIFO 和 DRM FIFO,其實就是用不同的資源去構成,前者 Distribute FIFO 也就是分布式 FIFO,使用的是片上的 LUT 資源去構成,而 DRM FIFO 使用的是片上的 DRM 資源去構成,DRM 構成的 FIFO 其性能大于 LUT 資源構成的,不僅容量更大,且可配置更多功能。
? ? ?本章著重介紹 DRM Based FIFO。
? ? ?注意:FIFO 寫滿后禁止繼續寫入數據,否則將會寫溢出。
? ? ?注意:FIFO 讀空后禁止繼續讀數據,否則將會讀溢出。
? ? ?以下給出常用的 FIFO 的配置作為介紹。
? ? ?注意,如果勾選 Enable Output Register(輸出寄存),輸出數據會延遲一個時鐘周期。
? ? ?FIFO Type 有 SYNC 和 ASYNC 兩種,第一種是同步 FIFO,讀寫端口共用一個時鐘和復位,另一種是異步 FIFO,讀寫時鐘和復位均獨立。在平常設計中,比較常用的是異步 FIFO,因為同步 FIFO 和異步 FIFO 的讀寫時序一模一樣,只有讀寫端口的時鐘復位有差異,當異步 FIFO 的讀寫端口使用相同的時鐘和復位,此時異步 FIFO 和同步 FIFO 基本是一致的。
? ? ?Reset Type 也可以選擇 SYNC 和 ASYNC 兩種,SYNC 模式下需要時鐘的上升沿采樣到復位有效才會復位,而在 ASYNC 模式下,復位一旦有,FIFO 立即復位。
? ? ?其余端口說明引用官方 IP 手冊,如圖所示:
? ? ?其中 rd_water_level 和 wr_water_level 分別代表”可讀的數據量”和”已寫入的數據量”,其含義與 Xilinx 的 FIFO 的 wr_data_count 和 rd_data_count 是一致的。
? ? ?當我們將 Enable Almost Full Water Level 和 Enable Almost Empty Water Level 勾選上,才能看到 rd_water_level 和 wr_water_level,而下面的 Almost Full Numbers 的設置是表示當寫入 1020 個數據時,Almost Full 信號就會拉高,Almost Empty Numbers 的設置表示當可讀數據剩下 4 個時 Almost Empty 信號就會拉高。
? ? ?同時 FIFO 支持混合位寬,例如寫端口 16bit,讀端口 8bit。如果寫入 16’h0102,那么讀出來會是 8’h02,8’h01,會先讀出低位。
? ? ?如果寫端口 8bit,讀端口 16bit。當寫入 8’h01,8’h02 時,讀出來是 16’h0201,先寫入的數據存放在低位。
2.3.1.FIFO的讀寫時序
? ? ?因為同步 FIFO 和異步 FIFO 的讀寫時序一致,這里用異步 FIFO 的讀寫時序圖來做介紹。
? ? ?注意:復位時高電平有效。讀出數據均未勾選 Enable Output Register(輸出寄存)。
2.3.1.1.FIFO 未滿時的寫時序
? ? ?可以看到在 1 時刻,復位信號時低電平,處于工作狀態,此時在 wr_clk 的上升沿且 wr_en 為高電平時將數據 D0 寫入 FIFO,wr_water_level 也從 0 變 1,表示已經寫入了一個數據,此時注意看讀端口的 empty 信號,在 3 時刻 empty 信號從高變低,意味著讀端口已經有數據可以讀了,FIFO不再為空,而注意看,rd_clk和 wr_clk是不一樣的,從 1寫入到 3時刻 empty 拉低時,經過了 3 個 rd_clk。
? ? ?所以這里我們可以得出結論:rd_water_level 要滯后 wr_water_level 三個 rd_clk。
2.3.1.2.FIFO 將滿時的寫時序
? ? ?將滿時主要分析 full 和 almost_full 信號。假設 Almost Full Numbers 設置為 N-2,在 1時刻,此時已經寫入了 N-6 個數據,意味著再寫 6 個數據 FIFO 就滿了,從 1 時刻到 2 時刻一共寫入了 4 個數據,因此當 wr_water_level 變成 N-2 時,滿足條件,可以看到 Almost Full信號拉高,再寫兩個數據 FIFO 就滿了,所以再經過兩個時鐘周期后,Full 信號拉高。
2.3.1.3.FIFO 在滿狀態下的讀時序
? ? ?在滿狀態下,FIFO 已經有 N 個數據了,此時在 1 狀態下,rd_clk 的上升沿,且 rd_en 為 高 電 平 時 ,此 時 從 FIFO 里 讀 出 數 據 (數 據 的 輸 出 有 延 時 ,仿 真 中 延 時 0.2ns) 。此時 rd_water_level 變成 N-1,rd_data 輸出 D0。然后看 2 時刻,full 信號拉低,此時可以看以下,在 1 時刻到 2 時刻期間一共經過了 3 個 wr_clk 寫端口才能判斷到此時數據量已經不為滿。
? ? ?所以我們可以得出結論,wr_water_level 要滯后 rd_water_level 三個 wr_clk。
2.3.1.4.FIFO 將空時的讀時序
? ? ?在 1 時刻,可讀的數據量剩下 4,假設 Almost Empty Number 設為 2,在 1 時刻和 2 時刻分別讀出了兩個數據,所以在 2 時刻下,可讀數據量剩下兩個,達到 Almost Empty Number 觸發條件,因此 almost_empty 信號拉高,再過兩個時鐘周期,即再讀兩個數據,FIFO 將變成空狀態,也就是狀態 3,此時 empty 信號拉高。
2.4. 接口列表
? ?該部分介紹每個頂層模塊的接口。
ram_test_top.v
rom_test_top.v
fifo_test_top.v
3.代碼仿真說明
? ? ?本次的頂層模塊實際就是例化 IP,然后把端口引出而已,主要代碼都在 testbench 里面,所以我們直接介紹仿真代碼。
3.1.?RAM 仿真測試
`timescale 1ns/1nsmodule ram_test_tb();reg sys_clk;reg rd_clk;reg rst_n;reg rw_en; //讀寫使能信號reg [7:0] wr_data;reg [4:0] wr_addr;reg [4:0] rd_addr;wire [7:0] rd_data;reg [1:0] state;initial beginrst_n <= 1'd0;sys_clk <= 1'd0;rd_clk <= 1'd0;#20rst_n <= 1'd1;end//讀寫控制always@(posedge sys_clk or negedge rst_n) beginif(!rst_n) beginstate <= 2'd0;wr_data <= 8'd0;rw_en <= 1'd0;wr_addr <= 8'd0;rd_addr <= 8'd0;endelse begincase(state)2'd0: beginrw_en <= 1'd1;state <= 2'd1;end2'd1: beginif(wr_addr == 5'd31) beginrw_en <= 1'd0;state <= 2'd2;wr_data <= 8'd0;wr_addr <= 5'd0;rd_addr <= 5'd0;endelse beginstate <= 2'd1;wr_data <= wr_data+1'b1;rd_addr <= rd_addr+1'b1;wr_addr <= wr_addr+1'b1;endend2'd2: beginif(rd_addr == 5'd31) beginstate <= 2'd3;rd_addr <= 5'd0;endelse beginstate <= 2'd2;rd_addr <= rd_addr+1'b1;endend2'd3: beginstate <= 2'd0;enddefault: state <= 2'd0;endcaseendend//50MHZalways #10 sys_clk = ~sys_clk;GTP_GRS GRS_INST(.GRS_N(1'b1));ram_test_top u_ram_test_top(.wr_clk (sys_clk),.rd_clk (sys_clk),.rst_n (rst_n),.rw_en (rw_en),.wr_addr (wr_addr),.rd_addr (rd_addr),.wr_data (wr_data),.rd_data (rd_data));
endmodule
? ? ?涉及到 tb 的一些基礎操作這里就不再詳細講解,只關注重點邏輯部分。從代碼的 27 行到 80 行是 ram 的讀寫控制狀態機。主要用來控制讀寫地址的生成和使能以及寫入的數據。這里只講解主要實現的功能,首先代碼的 38-42 行,也就是 state=0 的時候,拉高 rw_en,并跳轉到狀態 1,此時進入寫操作(沒有使能 clk_en,可以不管),下個時鐘周期開始寫入數據(注意是時序邏輯,邊沿采樣,所以是下個時鐘周期才開始寫數據),即 state=1 的時候是一直在往 ram 里面寫數據,在代碼的 44 到 60 行就是寫操作了,可以看到,當 wr_addr 不等于 31 的時候,wr_data 和 wr_addr 不斷加 1(rd_addr 這里+1,可以看視頻講解,主要為了驗證偽雙端口的時序),當 wr_addr 等于 31 的時候,在下個時鐘周期把數據清 0,狀態跳轉,在當前時鐘周期下還會再往地址 31 里面寫入數據,所以在該時鐘周期,一共寫入了 32 個數據(從地址 0 寫到地址 31)。即狀態 1 完成寫入 32 個數據后跳轉到 state=2 的邏輯。代碼的 61-72 行,也就是 state=2 的時候,在每個周期的上升沿讓 rd_addr 不斷累加,直到 rd_addr=31 的時候,在下個時鐘周期清空地址并讓狀態跳轉的操作,而在當前時鐘周期會繼續把地址 31 的數據讀出來,完成讀取地址 0-31 的數據,一共 32 個數據,所以該狀態主要完成讀取 32 個數據,然后在下個時鐘周期就跳轉到 state=3。state=3 可以看到其主要作用就是等待一個時鐘周期,然后跳轉回去 state=0 下,起到一個延時作用。
?上圖為寫數據的波形,數據從 0 開始遞增到 31,地址也是從 0 到 31。
上圖為讀數據波形,從地址 0-31 讀出了 0-31 個數據。
? ? ? 具體波形大家可以看視頻仿真,或者自己嘗試仿真,根據波形來看代碼。因為這里是時序邏輯,所以如果是初學者,純看文字可能會對 rd_addr=31 這一時刻還會再讀一個數據感到疑惑,建議直接仿真,或者觀看視頻講解的仿真部分,可以幫助快速理解。
? ? ? 可以總結出一句話就是時序邏輯的賦值總在下一個時鐘周期才生效。所以在rd_addr=31 時執行的操作要在下一個時鐘周期才會被采樣生效。所以當前時鐘還是會再從RAM 讀出一個數據。
? ? ??仿真代碼的講解到此結束,大家要注意時序邏輯的特點,具體的內容請看視頻講解。
3.2.?ROM 仿真測試
`timescale 1ns/1nsmodule rom_test_tb();reg sys_clk;reg rst_n;reg [9:0] rd_addr;wire [63:0] rd_data;initial beginrst_n <= 1'd0;sys_clk <= 1'd0;#20rst_n <= 1'd1;end//50MHZalways #10 sys_clk = ~sys_clk;GTP_GRS GRS_INST(.GRS_N(1'b1));always@(posedge sys_clk or negedge rst_n) beginif(!rst_n)rd_addr <= 10'd0;elserd_addr <= #2 rd_addr + 1'b1;endrom_test_top u_rom_test_top(.rd_clk (sys_clk),.rst_n (rst_n),.rd_addr (rd_addr),.rd_data (rd_data));endmodule
? ? ? 代碼 31-36 行例化了 ROM 的頂層模塊,該模塊里面其實就是調用了 ROM IP,然后把信號引出端口,沒有任何邏輯操作。
? ? ??代碼 24-29 行通過一個 always 塊不斷生成地址,任何給到 ROM IP,將數據讀出,由于沒勾選 clk_en 信號,所以數據在 ROM 復位完成后就會不斷讀出。所以并沒有復雜的邏輯,就是讓地址從 0 不斷累加,把數據讀出。
? ? ??上圖為讀出數據的波形圖,可以看到讀出的數據和 dat 文件里的數據一致。
? ?仿真代碼的講解到此結束,大家要注意時序邏輯的特點,具體的內容請看視頻講解。
3.3.?FIFO 仿真測試
`timescale 1ns/1nsmodule fifo_test_tb();reg sys_clk;reg rst_n;reg [7:0] wr_data;reg wr_en;reg rd_en;reg rd_state; //讀狀態reg wr_state;wire [7:0] rd_data;reg [7:0] rd_cnt;wire [7:0] rd_water_level;wire [7:0] wr_water_level;initial beginrst_n <= 1'd0;sys_clk <= 1'd0;#20rst_n <= 1'd1;endalways #10 sys_clk = ~sys_clk; //50MHZalways@(posedge sys_clk or negedge rst_n) beginif(!rst_n) beginwr_state <= 1'd0;wr_en <= 1'd0;wr_data <= 8'd0;endelse begincase(wr_state)1'd0: if(wr_water_level == 127) //128 個數據beginwr_en <= #2 1'd0;wr_data <= #2 8'd0;wr_state <= #2 1'd1;endelsebeginwr_en <= #2 1'd1;wr_data <= #2 wr_data+1'b1;wr_state <= #2 1'd0;end1'd1: if(rd_cnt == 127)wr_state <= #2 1'd0;default: wr_state <= 1'd0;endcaseendendalways@(posedge sys_clk or negedge rst_n) beginif(!rst_n) beginrd_state <= 1'd0;rd_en <= 1'd0;rd_cnt <= 8'd0;endelse begincase(rd_state)1'd0: if(rd_water_level >= 8'd128) //等待 128 個數據beginrd_state <= #2 1'd1;rd_en <= #2 1'd1;endelsebeginrd_cnt <= #2 8'd0;rd_state <= #2 1'd0;end1'd1: beginrd_cnt <= #2 rd_cnt + 1'b1;if(rd_cnt == 127)beginrd_en <= #2 1'd0;rd_state <= #2 1'd0;endenddefault: rd_state <= 1'd0;endcaseendendGTP_GRS GRS_INST(.GRS_N(1'b1));fifo_test_top u_fifo_test_top(.sys_clk (sys_clk),.rst_n (rst_n),.wr_data (wr_data),.wr_en (wr_en),.rd_en (rd_en),.wr_water_level (wr_water_level),.rd_water_level (rd_water_level),.rd_data (rd_data));
endmodule
? ? ? 涉及到 tb 的一些基礎操作這里就不再詳細講解,只關注重點邏輯部分。整個設計分為讀寫兩個狀態的控制。分別完成了寫入 128 個數據,和讀出 128 個數據,由于 FIFO 不需要地址,所以只需要產生使能信號即可。
? ? ??首先看寫狀態,在 wr_state=0 時,拉高寫使能,并讓 wr_data 不斷累加,往 FIFO 里面寫數據,當 wr_water_level=127 的時候,拉低寫使能,寫數據置 0,寫狀態跳轉到 1,注意此時還會再寫入一個數據,所以到此一個寫入了 128 個數據。至于拉低寫使能,寫數據置 0,寫狀態跳轉到 1 這些操作將在下一個時鐘周期才會被采樣生效。之后,在 wr_state=1 時,不斷等待 rd_cnt,該條件就是判斷當讀出 128 個數據的時候,wr_state 跳轉到 0 狀態。
? ? ??接下來看讀狀態,在 rd_state=0 的時候,一旦可讀的數據量超過 128 個(包括 128),狀態跳轉到 rd_state=1 下,然后開始讀出數據,同時在 rd_state=1 下用變量 rd_cnt 對我們的讀出數據也進行計數,rd_cnt 從 0 開始計數,當 rd_cnt=127 的時候會再往 FIFO 讀出一個數據,所以此時就一共讀出了 128 個數據,下一個時鐘周期 rd_en 和 rd_state 都將置 0。
寫數據的波形如上所示,一共寫入 128 個數據,從 1 寫到 128。
讀數據的波形如上所示,一共讀出 128 個數據,從 1 讀到 128。
? ? ? 具體波形大家可以看視頻仿真,或者自己嘗試仿真,根據波形來看代碼。因為這里是時序邏輯,所以如果是初學者,純看文字可能會對 rd_cnt=127 這一時刻還會再讀一個數據感到疑惑,建議直接仿真,或者觀看視頻講解的仿真部分,可以幫助快速理解。
? ? ??可以總結出一句話就是時序邏輯的賦值總在下一個時鐘周期才生效,所以在 rd_cnt=127 時執行的操作要在下一個時鐘周期才會被采樣生效。所以當前時鐘 rd_en 還是 為 1,會再從 FIFO 讀出一個數據。
? ? ??仿真代碼的講解到此結束,具體的內容請看視頻講解。