官方的例程還是比較難懂,現在試着在上次的工程上進行修改,做一個簡單的讀寫測試。
一、新建頂層工程
建立工程 top.v,其效果即原先的 DDR2_example_top.v,記得右鍵設置為頂層模塊,主要修改了以下幾點:
(1)端口信號名字;
(2)增加 PLL 生成 100Mhz 時鍾供給 DDR2 IP 用;
(3)增加自己寫的 DDR2_ctrl.v 代替之前的 DDR2_example_driver.v;
代碼如下所示:
1 //************************************************************************** 2 // *** 名稱 : top.v 3 // *** 作者 : xianyu_FPGA 4 // *** 博客 : https://www.cnblogs.com/xianyufpga/ 5 // *** 日期 : 2020-6-10 6 // *** 工具 : Quartus 13.0 7 // *** 芯片 : Cyclone IV E 8 // *** 型號 : EP4CE10F30C6 9 // *** 描述 : DDR2 控制器的頂層文件 10 //************************************************************************** 11 12 module top 13 //========================< 端口 >========================================== 14 ( 15 //system -------------------------------------------- 16 input wire CLK_50M , //時鍾 17 input wire RST_N , //復位 18 //DDR2 ---------------------------------------------- 19 output wire DDR2_ODT , //DDR2片內終止控制 20 output wire DDR2_CS_N , //DDR2片選 21 output wire DDR2_CKE , //DDR2時鍾使能 22 output wire [15:0] DDR2_ADDR , //DDR2地址總線 23 output wire [ 2:0] DDR2_BA , //DDR2組地址總線 24 output wire DDR2_RAS_N , //DDR2行地址選擇 25 output wire DDR2_CAS_N , //DDR2列地址選擇 26 output wire DDR2_WE_N , //DDR2寫使能 27 output wire [ 1:0] DDR2_DM , //DDR2數據屏蔽 28 inout wire DDR2_CLK , //DDR2時鍾 29 inout wire DDR2_CLK_N , //DDR2時鍾反相 30 inout wire [15:0] DDR2_DQ , //DDR2數據總線 31 inout wire [ 1:0] DDR2_DQS //DDR2數據源同步 32 ); 33 //========================< 信號 >========================================== 34 wire CLK_100M ; //PLL分出100M時鍾 35 wire phy_clk ; //讀寫DDR2的工作時鍾 36 wire local_init_done ; //初始化完成 37 wire [27:0] local_address ; //地址總線 38 wire local_write_req ; //數據寫入請求 39 wire local_read_req ; //數據讀出請求 40 wire local_burstbegin ; //突發起始 41 wire [31:0] local_wdata ; //寫數據總線 42 wire [ 3:0] local_be ; //字節使能標志 43 wire [ 2:0] local_size ; //突發大小 44 wire local_ready ; //讀寫請求被接收指示 45 wire [31:0] local_rdata ; //讀數據總線 46 //========================================================================== 47 //== PLL 48 //========================================================================== 49 pll u_pll 50 ( 51 .inclk0 (CLK_50M ), //時鍾輸入端口 52 .c0 (CLK_100M ) //100M時鍾輸出 53 ); 54 //========================================================================== 55 //== DDR2 IP 56 //========================================================================== 57 DDR2 u_DDR2 58 ( 59 .pll_ref_clk (CLK_100M ), //IP核中的PLL輸入參考時鍾 60 .global_reset_n (RST_N ), //全局異步復位 61 .soft_reset_n (RST_N ), //全局異步復位(不復位PLL) 62 .phy_clk (phy_clk ), //讀寫DDR2的工作時鍾 63 .reset_phy_clk_n ( ), //IP核提供的復位 64 .reset_request_n ( ), //IP核中的PLL鎖定 65 .aux_full_rate_clk ( ), //全速率時鍾 66 .aux_half_rate_clk ( ), //半速率時鍾 67 //用戶控制 -------------------------------------- 68 .local_address (local_address ), //地址總線 69 .local_write_req (local_write_req ), //數據寫入請求 70 .local_read_req (local_read_req ), //數據讀出請求 71 .local_burstbegin (local_burstbegin ), //突發起始 72 .local_wdata (local_wdata ), //寫數據總線 73 .local_be (local_be ), //字節使能標志 74 .local_size (local_size ), //突發大小 75 .local_ready (local_ready ), //讀寫請求被接收指示 76 .local_rdata (local_rdata ), //讀數據總線 77 .local_rdata_valid (local_rdata_valid ), //讀數據有效 78 .local_refresh_ack ( ), //刷新請求 79 .local_init_done (local_init_done ), //初始化完成 80 //外部引腳 -------------------------------------- 81 .mem_odt (DDR2_ODT ), //DDR2片內終止控制 82 .mem_cs_n (DDR2_CS_N ), //DDR2片選 83 .mem_cke (DDR2_CKE ), //DDR2時鍾使能 84 .mem_addr (DDR2_ADDR ), //DDR2地址總線 85 .mem_ba (DDR2_BA ), //DDR2組地址總線 86 .mem_ras_n (DDR2_RAS_N ), //DDR2行地址選擇 87 .mem_cas_n (DDR2_CAS_N ), //DDR2列地址選擇 88 .mem_we_n (DDR2_WE_N ), //DDR2寫使能 89 .mem_dm (DDR2_DM ), //DDR2數據屏蔽 90 .mem_clk (DDR2_CLK ), //DDR2時鍾 91 .mem_clk_n (DDR2_CLK_N ), //DDR2時鍾反相 92 .mem_dq (DDR2_DQ ), //DDR2數據總線 93 .mem_dqs (DDR2_DQS ) //DDR2數據源同步 94 ); 95 //========================================================================== 96 //== DDR2 IP核控制模塊 97 //========================================================================== 98 DDR2_ctrl u_DDR2_ctrl 99 ( 100 .phy_clk (phy_clk ), //讀寫DDR2的工作時鍾 101 .rst_n (local_init_done ), //初始化完成 102 .local_address (local_address ), //地址總線 103 .local_write_req (local_write_req ), //數據寫入請求 104 .local_read_req (local_read_req ), //數據讀出請求 105 .local_burstbegin (local_burstbegin ), //突發起始 106 .local_wdata (local_wdata ), //寫數據總線 107 .local_be (local_be ), //字節使能標志 108 .local_size (local_size ), //突發大小 109 .local_ready (local_ready ), //讀寫請求被接收指示 110 .local_rdata (local_rdata ) //讀數據總線 111 ); 112 113 endmodule
二、DDR2_ctrl 讀寫測試
DDR2_ctrl.v 文件用於 DDR2 IP 的讀寫測試,替換之前的 DDR2_example_driver.v。實現的功能將 16 個偶數依次寫入地址 0-15,然后再讀出,循環反復。
代碼如下所示:
1 //************************************************************************** 2 // *** 名稱 : DDR2_ctrl.v 3 // *** 作者 : xianyu_FPGA 4 // *** 博客 : https://www.cnblogs.com/xianyufpga/ 5 // *** 日期 : 2020年6月 6 // *** 描述 : DDR2控制模塊,寫入16個偶數,再讀出來 7 //************************************************************************** 8 9 module DDR2_ctrl 10 //========================< 端口 >========================================== 11 ( 12 input wire phy_clk , //時鍾 13 input wire rst_n , //復位 14 //--------------------------------------------------- 15 input wire local_ready , //讀寫請求被接收指示 16 input wire [31:0] local_rdata , //讀數據總線 17 output wire local_write_req , //數據寫入請求 18 output wire local_read_req , //數據讀出請求 19 output wire local_burstbegin , //突發起始 20 output reg [27:0] local_address , //地址總線 21 output reg [31:0] local_wdata , //寫數據總線 22 output wire [ 3:0] local_be , //字節使能標志 23 output wire [ 2:0] local_size //突發大小 24 ); 25 //========================< 信號 >========================================== 26 reg [ 7:0] time_cnt ; //計數器 27 //========================================================================== 28 //== 用戶信號 29 //========================================================================== 30 assign local_be = 4'hF; //始終使能全部數據 31 assign local_size = 3'h1; //突發長度始終為1 32 //========================================================================== 33 //== 計數器,local_ready為高時加1,計100下 34 //========================================================================== 35 always @ (posedge phy_clk or negedge rst_n) begin 36 if(!rst_n) begin 37 time_cnt <= 8'h0; 38 end 39 else if(time_cnt == 8'd99 && local_ready) begin 40 time_cnt <= 8'h0; 41 end 42 else if(local_ready) begin 43 time_cnt <= time_cnt + 8'h1; 44 end 45 end 46 //========================================================================== 47 //== 設計讀寫請求 48 //========================================================================== 49 assign local_write_req = (time_cnt >= 8'h0 && time_cnt <= 8'd15); //寫請求 50 assign local_read_req = (time_cnt >= 8'd30 && time_cnt <= 8'd45); //讀請求 51 assign local_burstbegin = local_write_req || local_read_req; //突發開始 52 //========================================================================== 53 //== 設計地址 54 //========================================================================== 55 always @ (posedge phy_clk or negedge rst_n) begin 56 if(!rst_n) begin 57 local_address <= 28'h0; 58 end 59 else if(local_ready) begin 60 if(time_cnt == 8'd15 || time_cnt == 8'd45) //清0,注意時序對齊 61 local_address <= 28'h0; 62 else if(local_write_req || local_read_req) 63 local_address <= local_address + 28'h1; 64 end 65 end 66 //========================================================================== 67 //== 設計寫數據,遞增2,剛好對齊地址的0-15 68 //========================================================================== 69 always @ (posedge phy_clk or negedge rst_n) begin 70 if(!rst_n) begin 71 local_wdata <= 32'h0; 72 end 73 else if(local_ready) begin 74 if(time_cnt < 8'd15) 75 local_wdata <= local_wdata + 32'h2; 76 else 77 local_wdata <= 32'h0; 78 end 79 end 80 81 82 endmodule
三、testbench
1、將之前工程的 DDR2_example_top_tb.v 文件復制一份命名為 top_tb.v,打開將里面的模塊名和例化名改成和 top.v 一致即可。
2、修改 QuartusII 的 testbench 文件,將原先的刪除,引入這次的 top_tb.v 和仿真模型 DDR2_mem_model.v。
四、打開仿真查看波形
1、點擊仿真打開仿真軟件,出波形界面后點 stop,將所有信號刪除,轉而將 top 模塊和 DDR2_ctrl 模塊的信號加到波形那,然后仿真時間改為 302us,run。
2、總體波形如下所示:
3、寫過程細節
4、讀過程細節
讀過程需要注意一點,local_read_req拉高時,local_rdata 數據不是立刻出來的,而是延時一段時間后和 local_rdata_valid 信號一起出來。從波形中可以看出,本次 DDR2 IP 讀寫測試成功。
經過自己寫一遍讀寫控制,雖然簡單,但對理解信號還是有很大幫助的。上板就不做了,編譯太慢了。
ps:自己開發 DDR2 時需要仿真,不會像上面那樣寫設計文件測試,而是自己寫仿真文件testbench,即自己建立一個 top_tb,將仿真模型引入即可,詳細的可以看 DDR3 部分的開發過程。
五、DDR2 IP 核信號說明
特權同學寫了這樣一段解釋:
用戶邏輯和 DDR2 IP 核之間的接口並不是什么新發明的特殊接口,不過是 Avalon-MM 總線而已。有人說這個美眉(Memory-Map) 會不會太慢了,關鍵時刻耽誤事?非也,MM 總線的 burst 模式也可以流水線式連續傳輸數據,絲毫不遜色於ST(stream)傳輸方式。
這里我們可以簡單了解一下帶【local_*】的 Avalon-MM 總線 burst 模式傳輸協議的使用方法。可以比較簡單山寨的理解前面已經給出的帶【local_*】的 Avalon-MM 信號接口:
- local_size:burst 讀寫的最大數據數量。通常 IP 核內部有 FIFO 用於支持這樣的連續數據讀寫,在Megafunction中設定好的最大數據數量是 Avl_size 的上限值。
- local_be:byte enable 信號,用於使能或說是屏蔽讀寫數據的各個高低字節。
- local_ready:總線當前狀態指示。這里高電平表示 ready,此時的 local_read_req 和 local_write_req 能夠被鎖存。
- local_burstbegin:突發傳輸起始標志位。它不受 local_ready 的影響,在發起一次讀或寫操作的第一個時鍾周期,只需保持一個時鍾周期的 local_burstbegin 為高電平狀態,並且不用管此時的local_ ready狀態如何。
- local_addr:讀寫共用的總線地址,位寬由 DDR2 的存儲總量和總線上讀寫數據的位寬來決定。如1Gbit的DDR2,外部芯片的數據位寬為 16bit,Avalon-MM 讀寫的數據位寬 64bit,那么它的地址不是以 16bit 位寬來計算的,而是以 64bit 位寬來計算的,即16M(24位)。
- local_read_req:讀請求, 配合地址local_addr 和突發傳輸起始標志位 local_burstbegin 發起一次 burst 讀操作。在 local_burstbegin 拉高后,只需要確保在同一個時鍾周期或其后第一次 local_ready 有效的時鍾周期拉高一次 local_read_req 信號即可。
- local_rdata_valid:讀出數據的有效標志位。IP 核在收到 burst 讀請求(local_read_req) 后的若千個時鍾周期開始連續送出數據(數據可能分多次連續送出),該信號和讀出數據配合,高電平表示當前讀出數據有效。
- local_rdata:讀出數據。和local_rdata_valid 配合送給用戶邏輯。
- local_write_req:寫請求信號。若發起一次 n 個數據寫入的 burst 傳輸,第一個傳輸時鍾周期首先拉高 local_burstbegin 以及 local_write_req,且local_write_req 必須保持到 n 個數據寫入完成。只有在 local_ready 有效時,當前的 local_write_req、local_addr 和 local_wdata 才是有效的。
- local_wdata:寫入數據。
其實我覺得吧,還是看官方英文原版手冊更好一點,《emi_ddr_ug》對這些說得很清楚。
六、local_size再說明
這里還要重點說一下【local_size】,該數據的最大突發長度在 DDR2 IP 里的設置如下所示:
- Local Maximum Burst Count:指定突發數來配置控制器從端口能接收的最大 Avalon 突發數,選擇 4(100),則外部 local_size 位寬為3。
上篇博客說過,full rate 下,IP 核突發長度只能是4,half rate下,IP核突發長度只能是8:
而 local_size 一般翻譯為本地突發長度,即連續讀或寫到 IP 里的字的個數。手冊中有如下一段話:
- 如果選擇memory burst length=4,half rate,local burst lenth(我需要的突發長度) 是1,所以 local_zise 總是1。
- 如果選擇memory burst length=4,full rate,local burst lenth(我需要的突發長度)是2,所以 local_size 應該設置為 1 或 2 對應每次的寫讀請求。
另一篇手冊中,話變得不一樣了:
七、DDR2 讀寫時序
DDR2 IP 核的出現讓我們不需要再像 SDRAM 一樣關注它的初始化時序、刷新時序、各種命令組成等,只需要關心讀寫時序即可。
1、寫時序(by鋯石科技,4突發,full速率)
從上圖可以看出,除了 phy_clk 時鍾信號外,寫時序涉及到了 7 個信號,分別是 local_ready、local_burstbegin、local_write_req、local_size、local_address、local_be 和 local_wdata。在這 7 個信號中只有 local_ready 是輸出信號,這意味着我們需要通過 local_ready 來得知控制器是否准備好了接收我們的指令。如果 local_ready 為高,則拉高 local_burstbegin 和 local_write_req 可以向控制器發出一次突發寫指令,由於一次突發指令可能不止傳輸一個數據,因此 local_burstbegin 只需在突發開始時拉高一個時鍾周期,而 local_write_req 在整個寫數據期間都需拉高。在一次突發開始時需要指定突發的起始地址 local_address、突發大小 local_size,而在整個突發寫期間,將每個數據以及它對應的字節使能信號順序放在local_wdata 和 local_be 總線上。如果 local_ready 為低,則表示控制器不能接收指令,在此期間除了 local_burstbegin 其他的信號都必須保持原來的狀態,直至 local_ready 為高。
2、讀時序(by鋯石科技,4突發,full速率)
從上圖可以看出,讀時序和寫時序類似,也有 8 個信號,除了 phy_clk 時鍾信號,其他的信號有 local_ready、local_burstbegin、local_read_req 、local_size 、local_address、local_rdata_valid 和 local_rdata。如果 local_ready 為高,則拉高local_burstbegin 和 local_read_req 一個時鍾周期可以向控制器發出一次讀突發指令,與此同時,必須給定突發起始地址 local_address 和突發大小 local_size。由於不需要像寫時序那樣逐個把數據放在 local_wdata 上,因此通常我們會連續發出讀指令,當然前提是 local_ready 一直為高。如果 local_ready 為低,則除了 local_ burstbegin 其他的信號都必須保持原狀態,直到 local_ready 變為高。發出讀指令一段時間后,local_rdata_valid 才會被拉高,表示 local_rdata 總線上的數據被讀出。最后需要說明的是,實際測試發現,local_ burstbegin 信號和控制器內部邏輯沒有任何關聯,也就是說不管 local_burstbegin 信號是什么狀態都不影響讀寫數據過程。為此可以這么理解,local_ burstbegin信號的存在僅僅是為了兼容Avalon-MM總線,可實際上它卻沒有起到作用。
3、正常 4 個數據的 burst 寫操作(by特權同學,8突發,half速率)



6、遇到 local_ready 拉低的 burst 讀操作(by特權同學,8突發,half速率)
必須保持 local_read_req、local_size 和 local_addr 到 local_ready 拉高為止。
貼了兩家的時序解釋,結合上面的實驗,對 DDR2 IP 的讀寫時序應該就比較清楚了。
參考資料:
1、鋯石科技FPGA教程
2、特權同學《vip_ex2 DDR2控制器讀寫測試》
3、emi_ddr_ug