官方的例程还是比较难懂,现在试着在上次的工程上进行修改,做一个简单的读写测试。
一、新建顶层工程
建立工程 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