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 自动读写控制器的整理就到这。终于搞完这个项目了,接下来得复习前面的项目和基础知识,差不多准备秋招了。

 

posted @ 2020-06-20 22:14  咸鱼IC  阅读(3272)  评论(2编辑  收藏  举报