DDR2(3):自定义读写控制器和DDR2 IP信号说明

时间:2024-02-20 09:42:17

  官方的例程还是比较难懂,现在试着在上次的工程上进行修改,做一个简单的读写测试。

一、新建顶层工程

  建立工程 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 对应每次的写读请求。

  另一篇手册中,话变得不一样了:

  所以除了上述给的两个说明,别的组合应该也是可以的,例如特权同学给出的案例为 half rate 下 IP 核突发长度为8,而 local_size 取的是 4 。
 
  说简单一点,即 local_size 和 IP 突发长度的概念是不一样的!local_size 是 Avalon 的突发,而 IP 突发长度是固定死的 4 或 8。

 

 七、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速率)

  默认情况下,local_addr 设定的是写入的首个数据对应的地址,随后每次写入数据后地址自动递增。
4、遇到 local_ready 拉低的 burst 写操作(by特权同学,8突发,half速率)
  Local_ready 拉高时,local_write_req、local_addr 和 local_wdata 所对应的地址和数据才是有效的。
5、正常 4 个数据的 burst 读操作(by特权同学,8突发,half速率)
  默认情况下,local_addr 为读出的首个数据对应的地址,随后将读出递增地址的数据。

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