上一篇教程介绍了NEXYS4 开发板中SPI flash的使用方式,这一篇介绍另一种板载存储DDR2
FPGA基础入门【9】开发板外部存储器DDR2访问
板载DDR2
NEXYS 4开发板上包含有一块DDR2 SDRAM,编号为MT47H64M16HR-25:H,MT47H是产品号,64M16是配置,HR是包装,-25是速度等级,:H是版本号,文档链接:DDR2 datasheet
虽然文档里说是MT47H的DDR2芯片,但在相应位置看到的芯片却是Mira P2R1GE4KGF - G8E,看网上说的,这两种芯片似乎没啥区别,可以用同种方式使用。
参数表如下:
DDR2细节
在文档中的DDR2状态图如下
DDR2内部结构如下
引脚功能如下(有些引脚没有在架构中显示)
- A[12:0],输入地址,在ACTIVATE指令中作为行地址,在READ/WRITE指令中[9:0]作为列地址,并且[10]作为预充电指示位
- BA[2:0],bank地址,我们用的DDR2中有16个bank,需要有4位,最后一位来自控制寄存器
- CK, CK#,一对差分时钟信号
- CKE,时钟使能
- CS#,片选
- LDM, UDM, DM,输入数据mask
- ODT,终端匹配
- RAS#, CAS#, WE#,指令输入
- DQ[15:0],数据输入输出
- DQS, DQS#,差分数据同步信号
- LDQS, LDQS#,差分低位数据同步信号
- UDQS, UDQS#,差分高位数据同步信号
- V_DD, V_DDQ, V_DDL, V_REF, V_SS, V_SSDL, V_SSQ是各种电源或者接地
- NC, NF, NU, RFU都是无用引脚
输入命令的形式如下表,H是高电平,L是低电平,X不关心
从这些信息中可以得到大致的DDR2读写流程:
- 充电完成,准备状态
- 激活ACTIVATE
- 激活bank
- 读写操作,可重复burst读写
- 预充电,回到准备状态
调用方式
NEXYS 4的文档给了我们两种途径来实现DDR2的调用,一种相对容易的方式是使用Digilent提供的DDR-to-SRAM适配模块
,另一种更基础的方式是使用Xilinx提供的Memory Interface Generator (MIG) Wizard,其实Digilent提供的适配模块也是以MIG为核心,既然这个教程是为了了解DDR,这篇就直接使用MIG了。
在这里我们需要第一次调用Xilinx的IP,所谓IP是自动生成打包好的模块,Xilinx把一些常用的功能做成了可以通过GUI配置的选项,这样可以省去大量的设计时间,使用者不需要知道所有细节知识,比如此处对DDR2的配置细节。
使用的IP名为MIG,具体可以参考此文档:UG586
MIG IP生成和调用
根据UG586的介绍开始MIG的配置(由于版本原因,与文档稍有不同),大致参照Digilent提供的DDR-to-SRAM适配模块
中的parameter setting表格。
先新建一个名为ddr2的新工程文件。第一步到左侧的Flow Navigator中找到IP Catalog
搜索MIG找到它,双击开始生成配置
系统自动读取工程配置
这里的控制接口我们使用初始的,不使用AXI4
引脚兼容就不需要了,只要我们现在用的芯片就够了
这里自然是选择DDR2 SDRAM
来到这一步,先把时钟周期从默认的3000ps改成3077ps,下面的文字建议我们使用默认配置,但我们不接受它的建议,我们接受NEXYS 4文档中的建议,让默认系统时钟100MHz能够方便的生成DDR控制器所需的时钟。
存储器系列自然是MT47H64M16HR-25E,其他就不用管了
到这一步选择输入时钟周期为10000ps也就是100MHz。前面之所以用3077ps而不用3000ps就是因为,3000ps无法在这个列表中找到100MHz
时钟配置时系统时钟system clock和参考时钟reference clock都选择No Buffer,因为这么选择可以自己调配时钟,不然MIG IP的配置会要求你给它调配真实引脚。系统时钟要100MHz,参考时钟要200MHz,之后我们再调用一个时钟生成器IP给这两个接口调配时钟。
我们不在IP内部调配ChipScope,不需要选择debug signal。
ODT是50欧姆
引脚配置我们使用Digilent提供的DDR-to-SRAM适配模块
中提供的UCF文件,避免手打的失误。需要选择第二个选项
进入该界面选择Read XDC/UCF
跳转到下载好的ucf文件路径
点击校验Validate,确定没问题后进入下一步。
这里要配置系统复位信号sys_rst,初始校准完成信号init_calib_complete和误码信号tg_compare_error。这个误码信号实际上在生成后找不到,traffic generator是一个用来生成读写数据的测试模块,在MIG的示例工程中使用,但我们并没有使用到。
这里把复位信号连到第一个拨动开关J15,初始校准完成信号连到第一个LED H17,误码信号连到第二个LED K15。
后面几步都同意,之后就点击Generate开始编译生成
时钟生成器IP配置
在IP Catalog中搜索Clocking Wizard找到时钟生成器IP,按照下面的配置后同样点击Generate生成
逻辑设计
数据接口
在配置MIG的时候我们使用的是初始数据接口,而不是AXI4。从MIG的文档中找到相应的接口介绍如下
- app_addr[26:0],数据地址,从高位到低位分别是bank 4位,行地址13位和列地址10位
- app_cmd[2:0],用户指令,3’b001为读,3’b000为写
- app_en,指令使能,当app_addr和app_cmd都准备好后,将其拉高来送出指令
- app_rdy,指令接收信号,此信号升高表示送入的指令已经被接受。如果迟迟不升高,有可能DDR在初始化,或者FIFO已满无法在读写,或者正在处理其他的读操作
- app_wdf_data[127:0],写数据,它会先经过app_wdf_mask,将指定部分覆盖为全1后送出
- app_wdf_mask[15:0],写数据mask,16位的mask,每一位对应数据中的8位,当mask的[0]为高时,app_wdf_data[7:0]送入DDR时会变成全1;当mask的[15]为高时,app_wdf_data[127:120]送入DDR时会变成全1
- app_wdf_end,写数据末端信号,当输入的数据是最后一个时,将此信号拉高,表示数据已送完
- app_wdf_wren,写使能,当数据准备好时,将此信号拉高
- app_wdf_rdy,写数据准备,此信号升高表示FIFO已经准备好接收新数据
- app_rd_data[127:0],读数据
- app_rd_data_end,读数据末端信号,最后一个读出的信号
- app_rd_data_valid,读数据准备,此信号升高表示读数据准备,可以接受数据
- app_sr_req,保留引脚,应该拉低
- app_ref_req,拉高该信号表示送一个refresh刷新指令给DRAM,必须是一个单时钟脉冲
- app_zq_req,拉高该信号表示送一个ZQ校准指令(用来校准ODT电阻值)给DRAM,必须是一个单时钟脉冲
- app_sr_active,保留输出引脚,无视
- app_ref_ack,该信号升高表示refresh刷新指令被确认
- app_zq_ack,该信号升高表示ZQ校准指令被确认
- ui_clk,IP输出给用户使用的时钟,一般是送给SDRAM的二分之一或者四分之一,根据IP的配置决定
- ui_clk_sync_rst,IP输出给用户使用的复位信号,和上面的时钟同步
综合上面的介绍,官方给的读写示范是这样的:
状态机设计
计划写8个数再读8个数,循环往复,因此根据上面的接口定义设计状态机如下
顶层代码
顶层代码ddr2.v拆解如下
端口定义和资源定义,和之前一样,定义dont_touch让ChipScope可以方便找到这些信号,但对系统复杂度有影响,真实工程中,最后应该把这些去掉
module ddr2(
// Reference Clock Ports
input clk,
input rst,
// Memory interface ports
output [12:0] ddr2_addr,
output [2:0] ddr2_ba,
output ddr2_cas_n,
output [0:0] ddr2_ck_n,
output [0:0] ddr2_ck_p,
output [0:0] ddr2_cke,
output ddr2_ras_n,
output ddr2_we_n,
inout [15:0] ddr2_dq,
inout [1:0] ddr2_dqs_n,
inout [1:0] ddr2_dqs_p,
output init_calib_complete,
output [0:0] ddr2_cs_n,
output [1:0] ddr2_dm,
output [0:0] ddr2_odt
);
// App commands
parameter WRITE = 3'b000;
parameter READ = 3'b001;
// State machine
parameter IDLE = 4'd1;
parameter CMD = 4'd2;
parameter WR_DATA = 4'd3;
parameter WR_END = 4'd4;
parameter RD_WAIT = 4'd5;
parameter RD_DATA = 4'd6;
parameter ENDING = 4'd7;
(* dont_touch = "true" *)reg [3:0] state;
(* dont_touch = "true" *)reg [3:0] next_state;
// Definition for APP interface
wire clk_ref_i;
(* dont_touch = "true" *)reg [26:0] app_addr;
(* dont_touch = "true" *)reg [2:0] app_cmd;
(* dont_touch = "true" *)reg app_en;
(* dont_touch = "true" *)reg [127:0] app_wdf_data;
reg [15:0] app_wdf_mask;
(* dont_touch = "true" *)reg app_wdf_end;
(* dont_touch = "true" *)reg app_wdf_wren;
wire [127:0] app_rd_data;
wire app_rd_data_end;
wire app_rd_data_valid;
wire app_rdy;
wire app_wdf_rdy;
reg app_sr_req;
reg app_ref_req;
reg app_zq_req;
wire app_sr_active;
wire app_ref_ack;
wire app_zq_ack;
wire ui_clk;
wire ui_clk_sync_rst;
// Control of the data
(* dont_touch = "true" *)reg [26:0] addr_start;
(* dont_touch = "true" *)reg wrh_rdl;
(* dont_touch = "true" *)reg [2:0] command;
reg [2:0] command_d;
(* dont_touch = "true" *)reg [15:0] data_start;
(* dont_touch = "true" *)reg [7:0] write_length;
(* dont_touch = "true" *)reg [7:0] write_count;
(* dont_touch = "true" *)reg [7:0] read_length;
(* dont_touch = "true" *)reg [7:0] read_count;
reg [127:0] data_from_RAM;
状态机部分,首先把次要信号拉低,再用状态机控制其他读写信号。具体可以参考上面设计的状态机
// State machine
always @ (posedge ui_clk or posedge ui_clk_sync_rst) begin
if(ui_clk_sync_rst | (~init_calib_complete)) begin
state <= IDLE;
end
else begin
state <= next_state;
end
end
always @(posedge ui_clk) begin
app_wdf_mask <= 16'h0000;
app_sr_req <= 1'b0;
app_ref_req <= 1'b0;
app_zq_req <= 1'b0;
end
always @(posedge ui_clk) begin
case(state)
IDLE: begin
app_addr <= 27'h0;
app_cmd <= READ;
app_en <= 1'b0;
app_wdf_data <= {8{16'h0}};
app_wdf_end <= 1'b0;
app_wdf_wren <= 1'b0;
next_state <= CMD;
data_from_RAM <= 128'd0;
write_count <= 8'd0;
read_count <= 8'd0;
end
CMD: begin
if(app_rdy) begin
next_state <= (wrh_rdl) ? WR_DATA : RD_DATA;
app_addr <= addr_start;
app_wdf_data <= {8{data_start}};
app_cmd <= command;
app_en <= 1'b1;
end
else begin
next_state <= CMD; // Keep here until it's ready
end
end
WR_DATA: begin
app_wdf_wren <= 1'b1;
if(app_wdf_rdy) begin
app_wdf_data <= {8{app_wdf_data[15:0] + 16'h0001}}; // change the data only when the FIFO is available
end
if(write_count < write_length) begin
write_count <= write_count + 8'd1;
next_state <= WR_DATA;
end
else begin
next_state <= WR_END;
end
end
WR_END: begin
app_en <= 1'b0;
if(app_wdf_rdy) begin
app_wdf_data <= {8{app_wdf_data[15:0] + 16'h0001}}; // write the last data
end
app_wdf_end <= 1'b1; // assert write end
app_wdf_wren <= 1'b0;
next_state <= ENDING;
end
RD_DATA: begin
if(app_rd_data_valid && (read_count < read_length)) begin
next_state <= RD_DATA;
data_from_RAM <= app_rd_data;
read_count <= read_count + 8'd1;
end
else if(read_count == read_length) begin
next_state <= ENDING; // wait until read data valid
end
end
ENDING: begin
app_en <= 1'b0;
read_count <= 8'd0;
write_count <= 8'd0;
app_wdf_end <= 1'b0;
next_state <= IDLE;
end
endcase
end
写个简单的控制逻辑
// address and data setting for test
always @(posedge ui_clk) begin
addr_start <= 27'h0000d00;
data_start <= 16'h0005;
write_length <= 8'd8;
read_length <= 8'd8;
end
always @(posedge ui_clk or posedge ui_clk_sync_rst) begin
if(ui_clk_sync_rst) begin
command <= WRITE;
command_d <= WRITE;
wrh_rdl <= 1'b1;
end
else if(state == ENDING)begin
command_d <= (command[0]) ? WRITE : READ; // change every time reach ending
wrh_rdl <= (command_d[0]) ? 1'b0 : 1'b1;
command <= command_d;
end
end
调用时钟生成器和MIG IP
wire locked;
clk_wiz_0 clk_wiz(
// Clock out ports
.clk_out1(sys_clk_i), // output clk_out1, 100MHz
.clk_out2(clk_ref_i), // output clk_out2, 200MHz
// Status and control signals
.reset(rst), // input reset
.locked(locked), // output locked
// Clock in ports
.clk_in1(clk) // input clk_in1, 100MHz
);
mig_7series_0 MIG (
// Memory interface ports
.ddr2_addr (ddr2_addr), // output [12:0] ddr2_addr
.ddr2_ba (ddr2_ba), // output [2:0] ddr2_ba
.ddr2_cas_n (ddr2_cas_n), // output ddr2_cas_n
.ddr2_ck_n (ddr2_ck_n), // output [0:0] ddr2_ck_n
.ddr2_ck_p (ddr2_ck_p), // output [0:0] ddr2_ck_p
.ddr2_cke (ddr2_cke), // output [0:0] ddr2_cke
.ddr2_ras_n (ddr2_ras_n), // output ddr2_ras_n
.ddr2_we_n (ddr2_we_n), // output ddr2_we_n
.ddr2_dq (ddr2_dq), // inout [15:0] ddr2_dq
.ddr2_dqs_n (ddr2_dqs_n), // inout [1:0] ddr2_dqs_n
.ddr2_dqs_p (ddr2_dqs_p), // inout [1:0] ddr2_dqs_p
.init_calib_complete (init_calib_complete), // output init_calib_complete
.ddr2_cs_n (ddr2_cs_n), // output [0:0] ddr2_cs_n
.ddr2_dm (ddr2_dm), // output [1:0] ddr2_dm
.ddr2_odt (ddr2_odt), // output [0:0] ddr2_odt
// Application interface ports
.app_addr (app_addr), // input [26:0] app_addr
.app_cmd (app_cmd), // input [2:0] app_cmd
.app_en (app_en), // input app_en
.app_wdf_data (app_wdf_data), // input [127:0] app_wdf_data
.app_wdf_end (app_wdf_end), // input app_wdf_end
.app_wdf_wren (app_wdf_wren), // input app_wdf_wren
.app_rd_data (app_rd_data), // output [127:0] app_rd_data
.app_rd_data_end (app_rd_data_end), // output app_rd_data_end
.app_rd_data_valid (app_rd_data_valid), // output app_rd_data_valid
.app_rdy (app_rdy), // output app_rdy
.app_wdf_rdy (app_wdf_rdy), // output app_wdf_rdy
.app_sr_req (app_sr_req), // input app_sr_req
.app_ref_req (app_ref_req), // input app_ref_req
.app_zq_req (app_zq_req), // input app_zq_req
.app_sr_active (app_sr_active), // output app_sr_active
.app_ref_ack (app_ref_ack), // output app_ref_ack
.app_zq_ack (app_zq_ack), // output app_zq_ack
.ui_clk (ui_clk), // output ui_clk
.ui_clk_sync_rst (ui_clk_sync_rst), // output ui_clk_sync_rst
.app_wdf_mask (app_wdf_mask), // input [15:0] app_wdf_mask
// System Clock Ports
.sys_clk_i (sys_clk_i),
// Reference Clock Ports
.clk_ref_i (clk_ref_i), // input clk_ref_i
.sys_rst (rst) // input sys_rst
);
endmodule
模拟仿真
Xilinx的IP在仿真时需要将IP编译后生成的netlist文件拷贝到仿真根目录,或者在编译时直接引用到netlist所在路径,还是比较方便的。
这里墙裂吐槽批评一下Intel Altera!不把IP整合到一个文件里,还在后面加上一个哈希值,每次稍微改点东西都要生成一个新的哈希值,生成一个新文件,对使用git之类的version control时候很不友好。
除了MIG和时钟生成器两个IP的netlist之外,还需要一个DDR2的仿真模型,这个模型可以从网上下载,但更方便的办法是从IP生成的文件中找。Xilinx为了仿真方便在生成的文件中自带了一个DDR2的模型,如果IP生成的路径是默认的,那么你可以在这个路径中找到ddr2_model.v和ddr2_model_parameters.vh两个文件:
[your_project].srcs\sources_1\ip\mig_7series_0\mig_7series_0\example_design\sim
可以把这俩拷贝到仿真根目录后直接调用使用,很方便
Testbench代码tb_ddr2.v如下:
`timescale 1ns/1ns
module tb_ddr2;
reg clock;
reg reset;
wire led;
wire [12:0] ddr2_addr;
wire [2:0] ddr2_ba;
wire ddr2_cas_n;
wire [0:0] ddr2_ck_n;
wire [0:0] ddr2_ck_p;
wire [0:0] ddr2_cke;
wire ddr2_ras_n;
wire ddr2_we_n;
wire [15:0] ddr2_dq;
wire [1:0] ddr2_dqs_n;
wire [1:0] ddr2_dqs_p;
wire [0:0] ddr2_cs_n;
wire [1:0] ddr2_dm;
wire [0:0] ddr2_odt;
initial begin
clock = 1'b0;
reset = 1'b0;
// Reset for 1us
#100
reset = 1'b1;
#1000
reset = 1'b0;
end
// Generate 100MHz clock signal
always #5 clock <= ~clock;
定义信号以及生成必要的100MHz时钟以及复位
ddr2 ddr2_top(
// Reference Clock Ports
.clk (clock),
.rst (reset),
.init_calib_complete (led),
// Memory interface ports
.ddr2_addr (ddr2_addr ),
.ddr2_ba (ddr2_ba ),
.ddr2_cas_n (ddr2_cas_n),
.ddr2_ck_n (ddr2_ck_n ),
.ddr2_ck_p (ddr2_ck_p ),
.ddr2_cke (ddr2_cke ),
.ddr2_ras_n (ddr2_ras_n),
.ddr2_we_n (ddr2_we_n ),
.ddr2_dq (ddr2_dq ),
.ddr2_dqs_n (ddr2_dqs_n),
.ddr2_dqs_p (ddr2_dqs_p),
.ddr2_cs_n (ddr2_cs_n ),
.ddr2_dm (ddr2_dm ),
.ddr2_odt (ddr2_odt )
);
调用工程的顶层代码
ddr2_model ddr2_model(
.ck (ddr2_ck_p),
.ck_n (ddr2_ck_n),
.cke (ddr2_cke),
.cs_n (ddr2_cs_n),
.ras_n (ddr2_ras_n),
.cas_n (ddr2_cas_n),
.we_n (ddr2_we_n),
.dm_rdqs (ddr2_dm),
.ba (ddr2_ba),
.addr (ddr2_addr),
.dq (ddr2_dq),
.dqs (ddr2_dqs_p),
.dqs_n (ddr2_dqs_n),
.rdqs_n (),
.odt (ddr2_odt)
);
endmodule
调用刚刚从IP生成文件中找到的DDR2模型
vlib work
vlog ../src/ddr2.v ./tb_ddr2.v ./ddr2_model.v ./clk_wiz_0_sim_netlist.v ./mig_7series_0_sim_netlist.v ./glbl.v
vsim -L xpm -L secureip -L unisims_ver -L unimacro_ver -L unifast_ver -L simprims_ver work.tb_ddr2 work.glbl -voptargs=+acc +notimingchecks
log -depth 7 /tb_ddr2/*
do wave.do
run 2ms
和之前一样,使用Xilinx的IP需要调用初始化文件glbl.v,仿真命令也需要把glbl.v一起仿真进去,具体位置可以参考上一篇教程,也可以直接下载附带工程文件。
ModelSim中把路径改为仿真根目录后执行do sim.do即可开始
仿真中DDR的校准需要很长时间,大概1.3ms左右init_calib_complete信号才会拉高,要耐心等待,这里我们跑2ms,可以看到如下波形
编译烧写
在前面配置IP时我们已经新建了一个工程文件ddr2,现在可以add source加入上面的顶层代码ddr2.v
引脚定义
下一步加入约束constraint文件ddr2.xdc,同样这是用标准模板取自己需要部分修改出来的(NEXYS 4 DDR Master XDC)
## This file is a general .xdc for the Nexys4 DDR Rev. C
## To use it in a project:
## - uncomment the lines corresponding to used pins
## - rename the used ports (in each line, after get_ports) according to the top level signal names in the project
## Clock signal
set_property -dict { PACKAGE_PIN E3 IOSTANDARD LVCMOS33 } [get_ports { clk }]; #IO_L12P_T1_MRCC_35 Sch=clk100mhz
create_clock -add -name sys_clk_pin -period 10.00 -waveform {0 5} [get_ports {clk}];
##Switches
set_property -dict { PACKAGE_PIN J15 IOSTANDARD LVCMOS33 } [get_ports { rst }]; #IO_L24N_T3_RS0_15 Sch=sw[0]
## LEDs
set_property -dict {PACKAGE_PIN H17 IOSTANDARD LVCMOS33} [get_ports init_calib_complete]
这个文件只定义了clk, rst和init_calib_complete三个信号,其他和DDR2相连的引脚都在MIG配置时生成好了。在IP生成目录中,你可以找到这个文件
[your_project].srcs\sources_1\ip\mig_7series_0\mig_7series_0\user_design\constraints\mig_7series_0.xdc
在编译时这个也作为约束文件被编译进去了,摘取其中的一部分看看:
...
# PadFunction: IO_L23P_T3_34
set_property SLEW FAST [get_ports {ddr2_dq[0]}]
set_property IN_TERM UNTUNED_SPLIT_50 [get_ports {ddr2_dq[0]}]
set_property IOSTANDARD SSTL18_II [get_ports {ddr2_dq[0]}]
set_property PACKAGE_PIN R7 [get_ports {ddr2_dq[0]}]
# PadFunction: IO_L20N_T3_34
set_property SLEW FAST [get_ports {ddr2_dq[1]}]
set_property IN_TERM UNTUNED_SPLIT_50 [get_ports {ddr2_dq[1]}]
set_property IOSTANDARD SSTL18_II [get_ports {ddr2_dq[1]}]
set_property PACKAGE_PIN V6 [get_ports {ddr2_dq[1]}]
# PadFunction: IO_L24P_T3_34
set_property SLEW FAST [get_ports {ddr2_dq[2]}]
set_property IN_TERM UNTUNED_SPLIT_50 [get_ports {ddr2_dq[2]}]
set_property IOSTANDARD SSTL18_II [get_ports {ddr2_dq[2]}]
set_property PACKAGE_PIN R8 [get_ports {ddr2_dq[2]}]
...
ChipScope配置
具体配置方法可以参照之前几个教程,这里我们再synthesis完成后Set Up Debug,并将之前定义了(* dont_touch = “true” *)的信号全部加入进来了
成果
编译综合后生成bitstream,USB线连接开发板,进入hardware manager烧写进芯片后,把连接到reset的开关拨到0,观察到LED0亮起了绿灯,这代表着DDR和我们的MIG校准连接已经完成
进入ChipScope,不需要设置trigger,点击立即抓取(上面一排小按钮中双右箭头那个),抓取结果如下
总结
这篇教程从DDR的芯片介绍开始一步步到最终实现DDR读写,这离最终应用还有一些控制逻辑方面的距离,但至少可以帮助我们理解这个接口的原理。之后具体将所有接口综合应用起来的教程中我们会用更简化的方式来实现这些接口
下一篇是局域网接口RJ45的使用