DDR2(5):DDR2自动读写控制器
本讲整理一下,如何利用上一讲的 DDR2_burst 打造一个可以自动读写的 DDR2 控制器,让其能够方便的使用于我们的工程中。以摄像头ov7725 采集 640x480 分辨率的显示为例,整理这次的设计过程。
1、模块例化
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。
2、端口和信号
`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 ; //读完成状态
3、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数据源同步信号 );
4、简单信号
//========================================================================== //== 简单信号 //========================================================================== //读写突发长度,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 位,这就是上一讲遗留的问题了。
5、场同步信号处理
从上面的模块例化可以看到,我将摄像头的场同步信号和 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
6、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,其他信号都好理解了,联系上下代码后就知道了。
7、状态机
//========================================================================== //== 状态机 //========================================================================== 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 里数据的个数来确定是进行写还是读,同时写和读又是写的优先级更高。
8、读写请求
//========================================================================== //== 读写请求 //========================================================================== 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。
9、写和读的列计数
计算一下写和读的列计数,计满一帧就清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
10、读写地址
//========================================================================== //== 读写地址设计 //========================================================================== 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,是用于乒乓操作的。
11、乒乓操作
在之前的博客中,介绍了乒乓操作的原理,当时没有贴乒乓操作的代码,因为当时写的原理直接变成代码虽然可以用,但总的来说比较复杂。这次的乒乓操作代码就简单多了。
//========================================================================== //== 乒乓操作,写完一帧图像乒乓操作切换分区 //========================================================================== 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 个站台,而读和写相当于两辆车,永远不会撞车,并且每次的读和写都是完整的一帧,达到了乒乓操作的目的。
12、基于 DDR2 的摄像头采集项目
仿真测试的过程就不贴了,来看看效果吧。
(1)单目摄像头
上面的代码没有什么遗漏,可以直接复制组合成 DDR2_driver.v 代码,并且组装成自己的摄像头采集项目,摄像头的使用前面写过了就不再赘述,来看看效果。
唉,摄像头好像有问题,出现了一些波纹一样的东西,但是 DDR2 控制器没问题,图像正确的显示了,而且没有出现图像撕裂的情况,乒乓操作还是不错的。
(2)8 目摄像头
这是我毕业设计的一部分,就是将上面的代码改成独立的 8 通道,最后的图像再通过 VGA 进行拼接。这需要一些技巧,由于是毕业项目的一部分,这里就不介绍细节了。
OK,DDR2 自动读写控制器的整理就到这。终于搞完这个项目了,接下来得复习前面的项目和基础知识,差不多准备秋招了。