本講整理一下,如何利用上一講的 DDR2_burst 打造一個可以自動讀寫的 DDR2 控制器,讓其能夠方便的使用於我們的工程中。以攝像頭ov7725 采集 640x480 分辨率的顯示為例,整理這次的設計過程。
一、模塊例化
DDR2_driver u_DDR2_driver ( //時鍾和復位 ------------------------------------ .pll_ref_clk (clk_100m ), //DDR2輸入時鍾 .global_reset_n (sys_rst_n ), //全局復位信號 //DDR2端口 -------------------------------------- .mem_odt (mem_odt ), //DDR2片上終結信號 .mem_cs_n (mem_cs_n ), //DDR2片選信號 .mem_cke (mem_cke ), //DDR2時鍾使能信號 .mem_addr (mem_addr ), //DDR2地址總線 .mem_ba (mem_ba ), //DDR2BANK信號 .mem_ras_n (mem_ras_n ), //DDR2行地址選擇信號 .mem_cas_n (mem_cas_n ), //DDR2列地址選擇信號 .mem_we_n (mem_we_n ), //DDR2寫使能信號 .mem_dm (mem_dm ), //DDR2數據掩膜信號 .mem_clk (mem_clk ), //DDR2時鍾信號 .mem_clk_n (mem_clk_n ), //DDR2時鍾反相信號 .mem_dq (mem_dq ), //DDR2數據總線 .mem_dqs (mem_dqs ), //DDR2數據源同步信號 //DDR2控制 -------------------------------------- .DDR2_init_done ( ), //DDR2 IP核初始化信號 .DDR2_phy_clk ( ), //DDR2 IP核輸出時鍾 .DDR2_phy_rst_n ( ), //DDR2 IP核輸出的同步復位信號 //讀寫 ------------------------------------------ .width (WIDTH ), //寬度 .height (HEIGHT ), //高度 .wr_clk (wr_clk ), //寫時鍾 .wr_data (wr_data ), //輸入數據 .wr_vld (wr_vld ), //輸入數據有效信號 .wr_vsync (wr_vsync ), //場信號(寫) .wr_en (1'b1 ), //寫使能 .rd_clk (clk_VGA ), //讀時鍾 .rd_data (VGA_din ), //輸出數據 .rd_req (VGA_req ), //輸出請求 .rd_vsync (VGA_vsync ), //場信號(讀) .rd_en (1'b1 ) //讀使能 );
從例化可以看出,本次 DDR2 設計外部給如的只有100Mhz時鍾、復位、讀列表、寫列表,參數列表,非常簡潔。其中參數的寬度和高度即這次圖像采集的 640 和 480。
二、端口和信號
`include "DDR2_param.v" //************************************************************************** // *** 名稱 : DDR2_driver.v // *** 作者 : xianyu_FPGA // *** 博客 : https://www.cnblogs.com/xianyufpga/ // *** 日期 : 2020年6月 // *** 描述 : 視頻讀寫突發的控制模塊,wrFIFO深度1024,rdFIFO深度256 //************************************************************************** module DDR2_driver //============================< 端口 >====================================== ( //時鍾和復位 ---------------------------------------- input pll_ref_clk , //DDR2輸入時鍾 input global_reset_n , //全局復位信號 //DDR2端口 ------------------------------------------ output mem_odt , //DDR2片上終結信號 output mem_cs_n , //DDR2片選信號 output mem_cke , //DDR2時鍾使能信號 output [`MEM_ADDR_W -1:0] mem_addr , //DDR2地址總線 output [`MEM_BANK_W -1:0] mem_ba , //DDR2BANK信號 output mem_ras_n , //DDR2行地址選擇信號 output mem_cas_n , //DDR2列地址選擇信號 output mem_we_n , //DDR2寫使能信號 output [`MEM_DM_W -1:0] mem_dm , //DDR2數據掩膜信號 inout mem_clk , //DDR2時鍾信號 inout mem_clk_n , //DDR2時鍾反相信號 inout [`MEM_DQ_W -1:0] mem_dq , //DDR2數據總線 inout [`MEM_DQS_W -1:0] mem_dqs , //DDR2數據源同步信號 //DDR2控制 ------------------------------------------ output DDR2_init_done , //DDR2 IP核初始化信號 output DDR2_phy_clk , //DDR2 IP核輸出時鍾 output DDR2_phy_rst_n , //DDR2 IP核輸出的同步復位信號 //讀寫 ---------------------------------------------- input [15:0] width , //寬度 input [15:0] height , //高度 input wr_clk , //寫時鍾 input [15:0] wr_data , //輸入數據 input wr_vld , //輸入數據有效信號 input wr_vsync , //場信號(寫) input wr_en , //寫使能 input rd_clk , //讀時鍾 output [15:0] rd_data , //輸出數據 input rd_req , //輸出請求 input rd_vsync , //場信號(讀) input rd_en //讀使能 ); //============================< 信號 >====================================== wire [13:0] burst_len ; //讀寫突發長度 wire [`LOCAL_DATA_W -1:0] burst_rddata ; //讀突發數據 wire [`LOCAL_DATA_W -1:0] burst_wrdata ; //寫突發數據 wire burst_rdack ; //讀突發應答信號 wire burst_wrack ; //寫突發應答信號 wire burst_rddone ; //突發讀完成信號 wire burst_wrdone ; //突發寫完成信號 //--------------------------------------------------- wire phy_clk ; //DDR2 IP核工作時鍾 wire reset_phy_clk_n ; //DDR2 IP核同步后的復位信號 wire local_init_done ; //DDR2 IP核初始化完成信號 wire rst_n ; //本模塊使用的復位信號 //--------------------------------------------------- reg wr_vsync_r ; //場信號打一拍 reg wr_rst ; //場復位信號 reg rd_vsync_r ; //場信號打一拍 reg rd_rst ; //場復位信號 wire [ 8:0] wrFIFO_rdusedw ; //寫FIFO剩余數據個數 wire [ 8:0] rdFIFO_wrusedw ; //讀FIFO剩余數據個數 reg [ 3:0] fsm_cs ; //狀態機的當前狀態 reg [ 3:0] fsm_ns ; //狀態機的下一個狀態 reg [15:0] wr_vcnt ; //寫行計數 reg [15:0] rd_vcnt ; //讀行計數 reg [`LOCAL_ADDR_W -1:0] burst_wraddr ; //寫突發地址 reg [`LOCAL_ADDR_W -1:0] burst_rdaddr ; //讀突發地址 reg burst_wrreq ; //突發寫請求 reg burst_rdreq ; //突發讀請求 reg [ 1:0] wraddr_msb ; //乒乓操作寫分區 reg [ 1:0] rdaddr_msb ; //乒乓操作讀分區 //============================< 參數 >====================================== parameter FSM_IDLE = 4'h0 ; //空閑狀態 parameter FSM_ARBIT = 4'h1 ; //仲裁狀態 parameter FSM_WRITE = 4'h2 ; //寫狀態 parameter FSM_WREND = 4'h3 ; //寫完成狀態 parameter FSM_READ = 4'h4 ; //讀狀態 parameter FSM_RDEND = 4'h5 ; //讀完成狀態
三、DDR2_burst 例化
上一講整理了 DDR2_burst.v,因此這一講將它例化進來。
//========================================================================== //== DDR2突發讀寫模塊,實現一段長度的突發讀寫 //========================================================================== DDR2_burst u_DDR2_burst ( //IP核引出接口 ---------------------------------- .pll_ref_clk (pll_ref_clk ), //DDR2 參考時鍾 .global_reset_n (global_reset_n ), //全局復位信號,連接外部復位 .phy_clk (phy_clk ), //DDR2 IP核工作時鍾 .reset_phy_clk_n (reset_phy_clk_n ), //DDR2 IP核同步后的復位信號 .local_init_done (local_init_done ), //DDR2 IP核初始化完成信號 //突發讀寫接口 ---------------------------------- .burst_rdreq (burst_rdreq ), //突發讀請求 .burst_wrreq (burst_wrreq ), //突發寫請求 .burst_rdlen (burst_len ), //突發讀長度 .burst_wrlen (burst_len ), //突發寫長度 .burst_rdaddr (burst_rdaddr ), //突發讀地址 .burst_wraddr (burst_wraddr ), //突發寫地址 .burst_rddata (burst_rddata ), //突發讀數據 .burst_wrdata (burst_wrdata ), //突發寫數據 .burst_rdack (burst_rdack ), //突發讀應答,連接FIFO .burst_wrack (burst_wrack ), //突發寫應答,連接FIFO .burst_rddone (burst_rddone ), //突發讀完成信號 .burst_wrdone (burst_wrdone ), //突發寫完成信號 //芯片接口 -------------------------------------- .mem_odt (mem_odt ), //DDR2片上終結信號 .mem_cs_n (mem_cs_n ), //DDR2片選信號 .mem_cke (mem_cke ), //DDR2時鍾使能信號 .mem_addr (mem_addr ), //DDR2地址總線 .mem_ba (mem_ba ), //DDR2bank信號 .mem_ras_n (mem_ras_n ), //DDR2行地址選擇信號 .mem_cas_n (mem_cas_n ), //DDR2列地址選擇信號 .mem_we_n (mem_we_n ), //DDR2寫使能信號 .mem_dm (mem_dm ), //DDR2數據掩膜信號 .mem_clk (mem_clk ), //DDR2時鍾信號 .mem_clk_n (mem_clk_n ), //DDR2時鍾反相信號 .mem_dq (mem_dq ), //DDR2數據總線 .mem_dqs (mem_dqs ) //DDR2數據源同步信號 );
三、簡單信號
//========================================================================== //== 簡單信號 //========================================================================== //讀寫突發長度,16和64互轉,長度/4 assign burst_len = width[15:2]; //DDR2初始化完成信號 assign DDR2_init_done = local_init_done; //DDR2輸出時鍾信號 assign DDR2_phy_clk = phy_clk; //DDR2復位信號 assign DDR2_phy_rst_n = reset_phy_clk_n; //本模塊復合復位信號 assign rst_n = reset_phy_clk_n && local_init_done;
將圖像的寬度舍去低 2 位,其實就是寬度除以4,這個結果作為讀寫突發長度。其原因是因為寫入和讀出 DDR2 的數據是 16 位的,而 DDR2 內部數據位寬是 64 位的,下面的 FIFO 會起到 16 轉 64、64 轉 16的作用,這是一個 4 倍的關系,因此這里除以4,而一次突發長度其實剛好就是圖像的一行寬度:寬度x16 = (寬度/4)x64 。因為舍去了低 2 位,因此 burst_len 的位寬為 14 位,這就是上一講遺留的問題了。
四、場同步信號處理
從上面的模塊例化可以看到,我將攝像頭的場同步信號和 VGA 的場同步信號引入了進來,其目的是用於寫讀 FIFO 的異步清 0,避免圖像移位或錯誤。
//========================================================================== //== 利用寫側場同步信號設計寫FIFO的異步復位 //========================================================================== //打拍 always @ (posedge phy_clk or negedge rst_n) begin if(!rst_n) begin wr_vsync_r <= 1'h0; end else begin wr_vsync_r <= wr_vsync; end end //場起始信號當作場復位信號,高有效 always @ (posedge phy_clk or negedge rst_n) begin if(!rst_n) begin wr_rst <= 1'h0; end else if(!wr_vsync_r && wr_vsync) begin wr_rst <= 1'b1; end else begin wr_rst <= 1'b0; end end //========================================================================== //== 利用讀側場同步信號設計讀FIFO的異步復位 //========================================================================== //打拍 always @ (posedge phy_clk or negedge rst_n) begin if(!rst_n) begin rd_vsync_r <= 1'h0; end else begin rd_vsync_r <= rd_vsync; end end //場起始信號當作場復位信號,高有效 always @ (posedge phy_clk or negedge rst_n) begin if(!rst_n) begin rd_rst <= 1'h0; end else if(!rd_vsync_r && rd_vsync) begin rd_rst <= 1'b1; end else begin rd_rst <= 1'b0; end end
五、FIFO例化
玩過攝像頭的都知道 FIFO 的作用,這里不再贅述。寫 FIFO 的寫位寬為16,讀位寬為64,因為攝像頭數據是16位,而 DDR2 的數據位寬是 64 位,所以這是一個 16 轉 64 的過程,寫側深度設置為 1024,那最多可以支持一行的分辨率為 1024個像素點。讀 FIFO 的寫位寬為 64,寫 FIFO 的讀位寬為 16,寫側深度設置為 256,剛好和前面反過來,因為 VGA 的輸入是 16 位,所以這是一個 64 轉 16 的過程。
//========================================================================== //== FIFO //========================================================================== //寫FIFO //--------------------------------------------------- wrFIFO_wr16_rd64_1024 wrFIFO ( .aclr (!rst_n || wr_rst ), .data (wr_data ), .rdclk (phy_clk ), .rdreq (burst_wrack ), .wrclk (wr_clk ), .wrreq (wr_vld ), .q (burst_wrdata ), .rdempty ( ), .rdusedw (wrFIFO_rdusedw ), .wrfull ( ), .wrusedw ( ) ); //讀FIFO //--------------------------------------------------- rdFIFO_wr64_rd16_256 rdFIFO ( .aclr (!rst_n || rd_rst ), .data (burst_rddata ), .rdclk (rd_clk ), .rdreq (rd_req ), .wrclk (phy_clk ), .wrreq (burst_rdack ), .q (rd_data ), //輸出到端口 .rdempty ( ), .rdusedw ( ), .wrfull ( ), .wrusedw (rdFIFO_wrusedw ) );
利用上面場同步產生的場同步起始信號進行 FIFO 的異步清0,其他信號都好理解了,聯系上下代碼后就知道了。
六、狀態機
//========================================================================== //== 狀態機 //========================================================================== always @ (posedge phy_clk or negedge rst_n) begin if(!rst_n) fsm_cs <= FSM_IDLE; else fsm_cs <= fsm_ns; end always @ (*) begin case(fsm_cs) //--------------------------------------------------- 空閑 FSM_IDLE: fsm_ns = FSM_ARBIT; //--------------------------------------------------- 仲裁 FSM_ARBIT: if(wr_en && wrFIFO_rdusedw >= burst_len) fsm_ns = FSM_WRITE; else if(rd_en && 9'd256 - rdFIFO_wrusedw >= burst_len) fsm_ns = FSM_READ; else if(!wr_en && !rd_en) fsm_ns = FSM_IDLE; else fsm_ns = fsm_cs; //--------------------------------------------------- 寫 FSM_WRITE: if(burst_wrdone) fsm_ns = FSM_WREND; else fsm_ns = fsm_cs; //--------------------------------------------------- 寫完成 FSM_WREND: fsm_ns = FSM_IDLE; //--------------------------------------------------- 讀 FSM_READ: if(burst_rddone) fsm_ns = FSM_RDEND; else fsm_ns = fsm_cs; //--------------------------------------------------- 讀完成 FSM_RDEND: fsm_ns = FSM_IDLE; default: fsm_ns = FSM_IDLE; endcase end
注意一下讀寫 FIFO 的 usedw,要及時判斷 FIFO 里數據的個數來確定是進行寫還是讀,同時寫和讀又是寫的優先級更高。
七、讀寫請求
//========================================================================== //== 讀寫請求 //========================================================================== always @ (posedge phy_clk or negedge rst_n) begin if(!rst_n) begin burst_wrreq <= 1'h0; end else if(burst_wrreq == 1'h0 && fsm_cs == FSM_WRITE) begin burst_wrreq <= 1'b1; end else if(burst_wrreq == 1'h1 && fsm_cs == FSM_WRITE && burst_wrdone) begin burst_wrreq <= 1'b0; end end always @ (posedge phy_clk or negedge rst_n) begin if(!rst_n) begin burst_rdreq <= 1'h0; end else if(burst_rdreq == 1'h0 && fsm_cs == FSM_READ) begin burst_rdreq <= 1'b1; end else if(burst_rdreq == 1'h1 && fsm_cs == FSM_READ && burst_rddone) begin burst_rdreq <= 1'b0; end end
跟着前面狀態機來的,到了誰的狀態就給誰請求,完了歸 0。
八、寫和讀的列計數
計算一下寫和讀的列計數,計滿一幀就清0,這兩個信號的目的是為了后面的信號使用的。
//========================================================================== //== 完成一次行突發后,寫列的計數遞增 //========================================================================== always @ (posedge phy_clk or negedge rst_n) begin if(!rst_n) begin wr_vcnt <= 16'h0; end else if((fsm_cs == FSM_WRITE && burst_wrdone && wr_vcnt == height - 16'h1) || wr_rst) begin wr_vcnt <= 16'h0; end else if(fsm_cs == FSM_WRITE && burst_wrdone) begin wr_vcnt <= wr_vcnt + 16'h1; end end //========================================================================== //== 完成一次行突發后,讀列的計數遞增 //========================================================================== always @ (posedge phy_clk or negedge rst_n) begin if(!rst_n) begin rd_vcnt <= 16'h0; end else if((fsm_cs == FSM_READ && burst_rddone && rd_vcnt == height - 16'h1) || rd_rst) begin rd_vcnt <= 16'h0; end else if(fsm_cs == FSM_READ && burst_rddone) begin rd_vcnt <= rd_vcnt + 16'h1; end end
八、讀寫地址
//========================================================================== //== 讀寫地址設計 //========================================================================== always @ (posedge phy_clk or negedge rst_n) begin if(!rst_n) begin burst_wraddr <= `LOCAL_ADDR_W'h0; burst_rdaddr <= `LOCAL_ADDR_W'h0; end else begin burst_wraddr <= {wraddr_msb,23'h0} + {wr_vcnt[11:0],11'h0}; burst_rdaddr <= {rdaddr_msb,23'h0} + {rd_vcnt[11:0],11'h0}; end end
上面的列計數在這里起到了作用,這樣每一行都寫在了 DDR2 不同的地址區域,同時留有 11 位寬作為行數據本身,非常清晰。高 2 位為 msb,是用於乒乓操作的。
九、乒乓操作
在之前的博客中,介紹了乒乓操作的原理,當時沒有貼乒乓操作的代碼,因為當時寫的原理直接變成代碼雖然可以用,但總的來說比較復雜。這次的乒乓操作代碼就簡單多了。
//========================================================================== //== 乒乓操作,寫完一幀圖像乒乓操作切換分區 //========================================================================== always @ (posedge phy_clk or negedge rst_n) begin if(!rst_n) begin wraddr_msb <= 2'h1; end else if(burst_wrdone && wr_vcnt == height - 16'h1 && rdaddr_msb != wraddr_msb + 2'h1) begin wraddr_msb <= wraddr_msb + 2'h1; end end always @ (posedge phy_clk or negedge rst_n) begin if(!rst_n) begin rdaddr_msb <= 2'h0; end else if(burst_rddone && rd_vcnt == height - 16'h1 && wraddr_msb != rdaddr_msb + 2'h1) begin rdaddr_msb <= rdaddr_msb + 2'h1; end end
乒乓操作的原理不再贅述,可以查看博客《OV7670/OV7725/OV5640開發記錄(4):SDRAM和乒乓操作》,這次的設計也符合當時的思想,但代碼簡單了。其原理就是划分出 4 片區域來做乒乓(00,01,10,11)。以這個工程為例,讀端是 VGA 需要 60 幀每秒,寫端攝像頭只有 30 幀每秒。
一開始讀在 0 區,寫在 1 區。讀在 0 區,讀完一幀后如果判定寫端在 1 區,則下一次的讀還是保持在自己的 0 區,直到寫端不在 1 區了,讀才能跨到 1 區去。寫在 1 區,寫完一幀后如果判定讀不在 2 區,則下一次寫就跳轉到 2 區。這樣循環反復,讀不斷的追寫,但永不碰面。msb 構成 4 個站台,而讀和寫相當於兩輛車,永遠不會撞車,並且每次的讀和寫都是完整的一幀,達到了乒乓操作的目的。
十、基於 DDR2 的攝像頭采集項目
仿真測試的過程就不貼了,來看看效果吧。
1、單目攝像頭
上面的代碼沒有什么遺漏,可以直接復制組合成 DDR2_driver.v 代碼,並且組裝成自己的攝像頭采集項目,攝像頭的使用前面寫過了就不再贅述,來看看效果。
唉,攝像頭好像有問題,出現了一些波紋一樣的東西,但是 DDR2 控制器沒問題,圖像正確的顯示了,而且沒有出現圖像撕裂的情況,乒乓操作還是不錯的。
2、8 目攝像頭
這是我畢業設計的一部分,就是將上面的代碼改成獨立的 8 通道,最后的圖像再通過 VGA 進行拼接。這需要一些技巧,由於是畢業項目的一部分,這里就不介紹細節了。
OK,DDR2 自動讀寫控制器的整理就到這。終於搞完這個項目了,接下來得復習前面的項目和基礎知識,差不多准備秋招了。