FPGA-DDR-SDRAM/README.md

455 lines
37 KiB
Markdown
Raw Normal View History

2021-01-27 18:06:35 +08:00
![test](https://img.shields.io/badge/test-passing-green.svg)
![docs](https://img.shields.io/badge/docs-passing-green.svg)
![platform](https://img.shields.io/badge/platform-Quartus|Vivado-blue.svg)
FPGA DDR-SDRAM
===========================
在低端FPGA设计中用 DDR-SDRAMDDR1替换 SDR-SDRAM。
# 简介
|-------------------------| |--------------|
| | | |
----->| 驱动时钟和复位 | | |
| | | |
------------| | DDR-SDRAM接口 |--------->| |
| AXI4 | | | |
AXI4 master |----------->| AXI4 slave | | |
| | | | |
------------| |-------------------------| |--------------|
用户逻辑 DDR1 控制器 DDR1 芯片
很多低端 FPGA 开发板(例如 [DE0-Nano](https://www.terasic.com.tw/cgi-bin/page/archive.pl?Language=English&No=593))使用 SDR-SDRAM 作为片外存储,而 DDR-SDRAM (DDR1) 比 SDR-SDRAM 容量更大价格更低。且与SDR-SDRAM一样DDR1也能使用低端FPGA的普通的IO管脚直接驱动。我编写了一个软核的 AXI4 接口的 DDR1 控制器(见 [RTL/ddr_sdram_ctrl.sv](https://github.com/WangXuan95/FPGA-DDR-SDRAM/blob/main/RTL/ddr_sdram_ctrl.sv) )。该控制器的特点有:
* **平台无关** :纯 RTL 编写,可以在 Altera 和 Xilinx 等各种 FPGA 上运行。
* **兼容性强** :支持各种位宽和容量的 DDR1 已在MICRON所有位宽和容量的DDR1型号上仿真通过
为了展示该控制器的使用方法,我提供了两个示例程序:
* **自测程序**通过DDR1控制器将数据写入 DDR1 ,然后读出,比较读出的数据是否与写入的吻合。
* **UART读写程序**:将 UART 命令转化成 AXI4 总线动作通过DDR1控制器读写DDR1。
另外,由于各代 DDR-SDRAM例如DDR3、DDR2、DDR1的接口时序大同小异本库也可以方便那些熟悉 Verilog 的人来学习 DDR-SDRAM 接口。
# 目录
* [简介](#简介)
* [硬件设计指南](#硬件设计指南)
* [硬件设计示例](#硬件设计示例)
* [DDR1 控制器](#DDR1 控制器)
* [模块参数](#模块参数)
* [模块接口 - 驱动时钟和复位](#模块接口 - 驱动时钟和复位)
* [时钟频率的选取](#时钟频率的选取)
* [模块接口 - DDR1 接口](#模块接口 - DDR1 接口)
* [模块接口 - AXI4 从接口](#模块接口 - AXI4 从接口)
* [位宽参数的确定](#位宽参数的确定)
* [时序参数的确定](#时序参数的确定)
* [示例程序](#示例程序)
* [示例程序 - 自测](#示例程序 - 自测)
* [示例程序 - UART读写](#示例程序 - UART读写)
* [Verilog 仿真](#Verilog 仿真)
* [建立仿真工程](#建立仿真工程)
* [运行仿真](#运行仿真)
* [修改仿真参数](#修改仿真参数)
# 硬件设计指南
对于 FPGA 的选型,只需要有足够数量的普通 IO 的 FPGA 就可以驱动 DDR1。DDR1 的 IO 电平标准往往是 SSTL-2 ,与 2.5V LVTTL 或 2.5V LVCMOS 兼容,因此相应的 FPGA 的 IO bank 的电源应该是 2.5V,且应在开发软件配置为 2.5V LVTTL 或 2.5V LVCMOS 。
下表展示了 DDR1 颗粒 的引脚,以及其与 FPGA 连接时应该注意的点。
| 引脚名称 | 方向 | 位宽 | 说明 | 连接方法 | 注意事项 |
| :---: | :---: | :---: | :--- | :--- | :--- |
| ddr_ck_p | FPGA output | 1 | 差分时钟正≥75MHz | 应与 FPGA 的 2.5V LVTTL管脚连接 | 布线尽量短,与 ddr_ck_n 差分布线 |
| ddr_ck_n | FPGA output | 1 | 差分时钟负≥75MHz | 应与 FPGA 的 2.5V LVTTL管脚连接 | 布线尽量短,与 ddr_ck_p 差分布线 |
| ddr_cke、ddr_cs_n | FPGA output | 1 | 低速 | 应与 FPGA 的 2.5V LVTTL管脚连接 | |
| ddr_ras_n、ddr_cas_n、ddr_we_n | FPGA output | 1 | 与 ddr_ck_p 下降沿同步 | 应与 FPGA 的 2.5V LVTTL管脚连接 | 布线尽量短,与 ddr_ck_p 大致等长(长度别太离谱即可) |
| ddr_ba | FPGA output | 2 | 与 ddr_ck_p 下降沿同步 | 应与 FPGA 的 2.5V LVTTL管脚连接 | 布线尽量短,与 ddr_ck_p 大致等长(别太离谱即可) |
| ddr_a | FPGA output | 取决于芯片型号 | 与 ddr_ck_p 下降沿同步 | 应与 FPGA 的 2.5V LVTTL管脚连接 | 布线尽量短,与 ddr_ck_p 大致等长(别太离谱即可) |
| ddr_dqs | inout | 取决于芯片型号 | | 应与 FPGA 的 2.5V LVTTL管脚连接 | 布线尽量短 |
| ddr_dm、ddr_dq | inout | 取决于芯片型号 | 与 ddr_dqs 双沿同步 | 应与 FPGA 的 2.5V LVTTL管脚连接 | 布线尽量短,与 ddr_dqs 大致等长(别太离谱即可) |
# 硬件设计示例
我用 Altera Cyclone IV 的一个非常低端和廉价的 FPGA 型号EP4CE6E22C8N) 和 MICRON 的 64MB DDR1 (型号 MT46V64M8TG-6T画了一个小板子本库的所有例子可以直接在该板子上直接运行。
| ![board-image](https://github.com/WangXuan95/FPGA-DDR-SDRAM/blob/main/images/board.jpg) |
| :---: |
| 图FPGA + DDR1 测试板 |
原理图见 [hardware/sch.pdf](https://github.com/WangXuan95/FPGA-DDR-SDRAM/blob/main/hardware/sch.pdf)PCB制造文件见 [hardware/gerber.zip](https://github.com/WangXuan95/FPGA-DDR-SDRAM/blob/main/hardware/gerber.zip) ,在布局布线时,使用双层板即可,不需像 DDR2 以上的设计那样刻意注意等长和阻抗匹配,因为该电路工作频率为双边沿 75MHz不是特别高只需注意让FPGA与DDR距离尽量近布线尽量短即可。比如我在布局时把DDR1芯片放在了FPGA芯片正对的背面从而保证布线都较短。
该板子的设计在立创EDA中开放见[这里](https://oshwhub.com/wangxuan/fpga-ddr-ce-shi-ban)。
# DDR1 控制器
DDR1 控制器代码见 [RTL/ddr_sdram_ctrl.sv](https://github.com/WangXuan95/FPGA-DDR-SDRAM/blob/main/RTL/ddr_sdram_ctrl.sv) ,是一个用 SystemVerilog 编写的模块,它能自动对 DDR1 进行初始化并定时进行刷新Refresh。该模块有一个简化但完备的 AXI4 从接口,通过它可以完成对 DDR1 的读写。本节详细解释该模块的使用方法。
## 模块参数
模块参数和默认值在 Verilog 中定义如下:
```Verilog
module ddr_sdram_ctrl #(
parameter READ_BUFFER = 1,
parameter BA_BITS = 2,
parameter ROW_BITS = 13,
parameter COL_BITS = 11,
parameter DQ_LEVEL = 1,
parameter [9:0] tREFC = 10'd256,
parameter [7:0] tW2I = 8'd7,
parameter [7:0] tR2I = 8'd7
)
```
对这些参数说明如下:
| 参数名 | 类型 | 取值范围 | 默认值 | 说明 |
| :--: | :--: | :--: | :--: | :-- |
| READ_BUFFER | 配置参数 | 0 或 1 | 1 | 若设为0DDR1控制器内将没有读数据缓冲读出的数据将不会等待AXI4主机是否能接受即rvalid信号不会等待rready信号直接以最高速率灌出去此时AXI4是不完备的但可以降低读出的时延。若设为1则DDR1控制器内会有一个足够大的读数据缓冲rvalid信号会与rready信号握手确认AXI4主机准备好后再读出数据。 |
| BA_BITS | 位宽参数 | 1~4 | 2 | 规定了 DDR BANK ADDRESS (ddr_ba) 的宽度,常规的 DDR1 的 BANK ADDRESS 都是 2bit因此该参数通常固定为默认值不用改 |
| ROW_BITS | 位宽参数 | 1~15 | 13 | 规定了 DDR ROW ADDRESS 的宽度,同时也决定了 DDR1 芯片的地址线引脚(ddr_a)的宽度。该参数取决于DDR1芯片的选型。例如 MT46V64M8 每个 BANK 有 8192 个 COL考虑到2^11=8192该参数应为13。同理对于 MT46V128M8该参数应为14 |
| COL_BITS | 位宽参数 | 1~14 | 11 | 规定了 DDR COL ADDRESS 的宽度。该参数取决于DDR1芯片的选型。例如 MT46V64M8 每个 ROW 有 2048 个 COL考虑到2^11=2048因此该参数应该为11。同理对于MT46V32M16该参数应为10。 |
| DQ_LEVEL | 位宽参数 | 0~7 | 1 | 规定了 DDR1 芯片的数据位宽对于位宽x4的芯片例如MT46V64M4该参数应取0对于位宽x8的芯片例如MT46V64M8该参数应取1对于位宽x16的芯片例如MT46V64M16该参数应取2对于位宽x32的情况例如两片MT46V64M16扩展位宽该参数应取3以此类推。
| tREFC | 时序参数 | 10'd1 ~ 10'd1023 | 10'd256 | 该控制器会周期性地刷新 DDR1该参数规定了刷新的间隔。详见下文[时序参数的确定](#时序参数的确定) |
| tW2I | 时序参数 | 8'd1~8'd255 | 8'd7 | 该参数规定了一个写动作的最后一个写命令到下一个动作的激活命令(ACT)的间隔。详见下文[时序参数的确定](#时序参数的确定) |
| tR2I | 时序参数 | 8'd1~8'd255 | 8'd7 | 该参数规定了一个读动作的最后一个读命令到下一个动作的激活命令(ACT)的间隔。详见下文[时序参数的确定](#时序参数的确定) |
## 模块接口 - 驱动时钟和复位
该模块需要一个时钟和一个复位进行驱动,如下:
```Verilog
input wire rstn,
input wire clk,
```
rstn 是低电平复位信号正常工作时应该置高。clk 是驱动时钟,频率是用户时钟的 4 倍。
模块开始工作前rstn信号应该置低让模块复位当 clk 的频率稳定后,可以把 rstn 置高,让模块处于工作状态。
如果 clk 一开始就是稳定的(例如直接来自晶振输入),那么 rstn 可以直接置为 1'b1 。
如果 clk 经过一段时间后才能稳定(例如来自 FPGA 的 PLL 或 MMCM那么 rstn 应该接 PLL/MMCM 的 locked 信号,保证 PLL 锁相成功(稳定)后,模块再开始工作。
## 时钟频率的选取
模块内使用寄存器分频 4 倍产生 DDR1 驱动时钟ddr_ck_p/ddr_ck_n和 AXI4 总线用户时钟aclk。本节讲述如何决定驱动时钟 clk 的频率。
首先,时钟频率受限于 DDR1 芯片。考虑到所有的 DDR1 的接口频率至少为 75MHz ,则 clk 的下限是 75\*4=300MHz。而 clk 的上限就取决于 DDR1 的芯片型号,例如对于 MT46V64M8P-5B ,查芯片手册可知,-5B 后缀的 DDR1 在 CL=1 时最高时钟频率是 133MHz则 clk 的上限是 133\*4=532MHz 。
另外,时钟频率的上限还受限于 FPGA 的速度,太高的时钟频率容易导致时序不收敛。该控制器充分考虑时序安全设计,大多数寄存器工作在频率较低用户时钟域;个别寄存器工作在用户时钟的 2 倍频率的时钟下,且输入端口的组合逻辑非常短;还有一个寄存器工作在高频的 clk 下,但输入端口直接来自于上一级的寄存器输出(没有组合逻辑)。因此,即使在速度级别很低的 EP4CE6E22C8N 上,在 300MHz 的驱动时钟下也能保证模块正确运行。在速度等级更高的 FPGA (例如 EP4CE22F17C6N驱动时钟的频率可以更高例如400MHz
> 注意:本控制器固定 CAS Latency (CL) = 2。
## 模块接口 - DDR1 接口
以下是该模块的 DDR1 接口。这些接口应该直接从 FPGA 引出,连接到 DDR1 芯片上。
```Verilog
output wire ddr_ck_p, ddr_ck_n,
output wire ddr_cke,
output reg ddr_cs_n,
output reg ddr_ras_n,
output reg ddr_cas_n,
output reg ddr_we_n,
output reg [ BA_BITS-1:0] ddr_ba,
output reg [ ROW_BITS-1:0] ddr_a,
output wire [((1<<DQ_LEVEL)+1)/2-1:0] ddr_dm,
inout [((1<<DQ_LEVEL)+1)/2-1:0] ddr_dqs,
inout [ (4<<DQ_LEVEL)-1:0] ddr_dq
```
可以看出 DDR1 接口的一些信号的位宽是和参数有关的,用户需要根据 DDR1 的芯片选型来确定模块参数。详见 [位宽参数的确定](#位宽参数的确定)。
想了解 DDR1 接口在初始化、读写、刷新时的波形,请进行 [Verilog 仿真](#Verilog 仿真)。
## 模块接口 - AXI4 从接口
DDR1 控制器对外提供 AXI4 从接口AXI4 slave的时钟和复位信号如下。AXI4 主机应该用它们作为自己的时钟和复位。
```Verilog
output reg aresetn,
output reg aclk,
```
以下是该模块的 AXI4 从接口,它们都与 aclk 时钟的上升沿同步,应该连接到 FPGA 内的的 AXI4 主机。
```Verilog
input wire awvalid,
output wire awready,
input wire [BA_BITS+ROW_BITS+COL_BITS+DQ_LEVEL-2:0] awaddr,
input wire [ 7:0] awlen,
input wire wvalid,
output wire wready,
input wire wlast,
input wire [(8<<DQ_LEVEL)-1:0] wdata,
output wire bvalid,
input wire bready,
input wire arvalid,
output wire arready,
input wire [BA_BITS+ROW_BITS+COL_BITS+DQ_LEVEL-2:0] araddr,
input wire [ 7:0] arlen,
output wire rvalid,
input wire rready,
output wire rlast,
output wire [(8<<DQ_LEVEL)-1:0] rdata,
```
AXI4 总线的数据wdata和rdata的位宽是 DDR1 数据位宽的 2 倍。例如,对于位宽为 8bit 的 MT46V64M8wdata 和 rdata 的位宽是 16bit 。
AXI4 总线的地址awaddr和araddr统一是字节地址模块会根据用户指定的参数来计算 awaddr 和 araddr 的位宽 = BA_BITS+ROW_BITS+COL_BITS+DQ_LEVEL-1 。例如对于 64MB 的 MT46V64M8 awaddr/araddr 位宽为 26而 2^26 等于 64MB。
每次读写访问时地址值awaddr和araddr都要和整个字对齐例如若 wdata 和 rdata 的位宽是 16bit2字节那么 wdata 和 rdata 要按 2 字节对齐,即能整除 2。
该模块的 AXI4 接口不支持字节选通写入,没有 wstrb 信号,每次至少写入一整个数据位宽,例如若 wdata 的位宽是 16那么一次至少写入2字节。
2021-01-27 18:13:39 +08:00
该模块的 AXI4 接口支持突发读写,突发长度可取 1\~256 之间的任意值,即 awlen 和 arlen 取值范围为 0\~255 。例如在一次写入动作中awlen=12则突发长度为 13若 wdata 的位宽是 16 ,那么该次突发写一共写了 13\*2=26B。
2021-01-27 18:06:35 +08:00
另外,每次突发读写不能超越 DDR1 内的一个行的边界。例如对于 MT46V64M8每个行有 8\*2^11/8 = 2048B那么每次突发读写就不能超越 2048B 的边界。
### AXI4 写操作时序
__ __ __ __ __ __ __ __ __ __ __ __ _
aclk __/ \__/ \__/ \__/ \__/ \__/ \__/ \__/ \__/ \__/ \__/ \__/ \__/
___________
awvalid ___/ \____________________________________________________________
_____
awready _________/ \____________________________________________________________
___________
awaddr XXXX____ADDR___XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
___________
awlen XXXX____8'd3___XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
___________ ___________ _____
wvalid _______________/ \_____/ \_____/ \__________________
___________________________________
wready _____________________/ \__________________
_____
wlast ___________________________________________________/ \__________________
___________ _____ _____ _____
wdata XXXXXXXXXXXXXXXX_____D0____XXXXXXX__D1_X__D2_XXXXXXX__D3_XXXXXXXXXXXXXXXXXXX
___________
bvalid _________________________________________________________/ \______
_____
bready _______________________________________________________________/ \______
一个典型的,突发长度为 4 awlen=3 的 AXI4 写操作如上图。分为 3 步:
* **地址通道握手**AXI4 主机把 awvalid 信号拉高指示想要发起写动作图中经过一周期后DDR1 控制器才把 awready 信号拉高说明地址通道握手成功在此之前DDR1 控制器可能在处理上一次读写动作,或者在进行刷新,因此暂时没把 awready 拉高。握手成功的同时DDR1 控制器收到待写地址awaddr和突发长度awlenawlen=8'd3 说明突发长度为 4。
* **数据传送**AXI4 总线上的 wvalid 和 wready 进行 4 次握手,把突发传输的 4 个数据写入 DDR1。在握手时若 AXI4 主机还没准备好数据,可以把 wvalid 拉低,若 DDR1 控制器还没准备好接受数据,会把 wready 拉低。注意,根据 AXI4 总线的规定,在突发传输的最后一个数据传输的同时需要把 wlast 信号置高实际上即使不置高DDR1 控制器也能根据突发长度自动结束数据传送,进入写响应状态)。
* **写响应**数据传送结束后DDR1 控制器还需要和 AXI4 主机进行一次握手才能彻底结束本次写动作。DDR1 控制器在数据传送结束后立即把 wvalid 信号拉高,并等待 wready 为高。完成该握手后DDR1才能响应下一次读写操作。
### AXI4 读操作时序
__ __ __ __ __ __ __ __ __ __ __ __ _
aclk __/ \__/ \__/ \__/ \__/ \__/ \__/ \__/ \__/ \__/ \__/ \__/ \__/
___________
arvalid ___/ \____________________________________________________________
_____
arready _________/ \____________________________________________________________
___________
araddr XXXX____ADDR___XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
___________
arlen XXXX____8'd4___XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
___________________________________
rvalid _________________________________/ \______
___________________________________________________ __________________
rready \_____/
_____
rlast _______________________________________________________________/ \______
_____ _____ _____ ___________ _____
rdata XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX__D0_X__D1_X__D2_X_____D3____X__D4_XXXXXXX
一个典型的,突发长度为 5 arlen=4 的 AXI4 读操作如上图。分为 2 步:
* **地址通道握手**AXI4 主机把 arvalid 信号拉高指示想要发起写动作图中经过一周期后DDR1 控制器才把 arready 信号拉高说明地址通道握手成功在此之前DDR1 控制器可能在处理上一次读写动作,或者在进行刷新,因此暂时没把 arready 拉高。握手成功的同时DDR1 控制器收到待读地址araddr和突发长度arlenarlen=8'd4 说明突发长度为 5。
* **数据传送**AXI4 总线上的 rvalid 和 rready 进行 5 次握手,把突发传输的 5 个读出。在握手时,若 AXI4 主机还没准备好接受数据,可以把 rready 拉低,则 DDR1 控制器会保持当前数据,直到 AXI4主机能接受此数据即把rready拉高为止。在传送最后一个数据时DDR1 控制器会把 rlast 拉高。
> 注:当模块参数 READ_BUFFER = 0 时,模块将节省一个 BRAM 资源,同时还能降低地址通道握手和数据传送之间的延迟。但 DDR1 控制器会忽略 rready=0 的情况,不等待 AXI4 主机是否准备好接受数据。这样将会破坏 AXI4 协议的完备性,但在一些简单的场合或许有用。
## 位宽参数的确定
本节讲述如何确定 **BA_BITS**、**ROW_BITS**、**COL_BITS** 和 **DQ_LEVEL** 这 4 个参数。
从[模块接口 - DDR1 接口](#模块接口 - DDR1 接口)一节可以看出 DDR1 接口的一些信号的位宽是和参数有关的,用户需要根据 DDR1 的芯片选型来确定模块参数。
以 [MICRON 公司的 DDR-SDRAM](https://www.micron.com/products/dram/ddr-sdram) 系列芯片为例,不同芯片有不同的 ROW ADDRESS BITS (行地址宽度、COL ADDRESS BITS列地址宽度、DATA BITS数据宽度决定了他们的位宽参数也不同如下表这些参数都能从芯片datasheet中查到
| 芯片名称 | ddr_dq 位宽 | ddr_dm 和 ddr_dqs 位宽 | DQ_LEVEL 取值 | BA_BITS | ROW_BITS | COL_BITS | 每行字节数 | 容量 | awaddr/araddr 位宽 |
| :--: | :--: | :--: | :--: | :--: | :--: | :--: | :--: | :-- | :--: |
| MT46V64M4 | 4 | 1 | 0 | 2 | 13 | 11 | 1024 | 4\*2^(2+13+11)=256Mb=32MB | 25 |
| MT46V128M4 | 4 | 1 | 0 | 2 | 13 | 12 | 2048 | 4\*2^(2+13+12)=512Mb=64MB | 26 |
| MT46V256M4 | 4 | 1 | 0 | 2 | 14 | 12 | 4096 | 4\*2^(2+14+12)=1Gb=128MB | 27 |
| MT46V32M8 | 8 | 1 | 1 | 2 | 13 | 10 | 1024 | 8\*2^(2+13+10)=256Mb=32MB | 25 |
| MT46V64M8 | 8 | 1 | 1 | 2 | 13 | 11 | 2048 | 8\*2^(2+13+11)=512Mb=64MB | 26 |
| MT46V128M8 | 8 | 1 | 1 | 2 | 14 | 11 | 4096 | 8\*2^(2+14+11)=1Gb=128MB | 27 |
| MT46V16M16 | 16 | 2 | 2 | 2 | 13 | 9 | 1024 | 16\*2^(2+13+9)=256Mb=32MB | 25 |
| MT46V32M16 | 16 | 2 | 2 | 2 | 13 | 10 | 2048 | 16\*2^(2+13+10)=512Mb=64MB | 26 |
| MT46V64M16 | 16 | 2 | 2 | 2 | 14 | 10 | 4096 | 16\*2^(2+14+10)=1Gb=128MB | 27 |
## 时序参数的确定
本节讲述如何确定 **tREFC**、**tW2I** 和 **tR2I** 这 3 个时序参数。
我们知道DDR1 需要周期性的刷新动作,**tREFC** 就规定了刷新的时钟周期间隔(以 aclk为准。例如若用户时钟为 75MHz根据 MT46V64M8 的芯片手册,需要至多 7.8125us 刷新一次,考虑到 75MHz * 7.8125us = 585.9,则该参数可以设置为一个小于 585 的值,例如 10'd512。
**tW2I** 规定了一个写操作的最后一个写命令到下一个操作的激活命令(ACT)的时钟周期数(以 aclk 为准)的最小值。下图展示了一个 DDR1 接口上的写操作ddr_cas_n 的第一个上升沿代表了一个写操作的最后一个写命令结束ddr_ras_n 的第二个下降沿代表了下一个操作(可能是读、写、刷新)的开始,它们之间有 5 个时钟周期,**tW2I** 就是用来规定该周期数的下限的。**tW2I** 的默认值 8'd7 是一个兼容绝大多数 DDR1 的保守值,对于不同的 DDR1 芯片,有不同的缩小的余地(详见 DDR1 芯片 datasheet
__ __ __ __ __ __ __ __ __ __ __ __
ddr_ck_p __/ \__/ \__/ \__/ \__/ \__/ \__/ \__/ \__/ \__/ \__/ \__/ \__
_____ _______________________________________________ ________
ddr_ras_n \_____/ \_____/
_________________ ____________________________________________
ddr_cas_n \___________/
_________________ ____________________________________________
ddr_we_n \___________/
_____
ddr_a[10] XXXXXXXXXXXXXXXXXX_____/ XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
_______________________
ddr_ba XXXXXX__________BA___________XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
_______________________
ddr_a XXXXXX__RA_XXXXXXX_CA0_X_CA1_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
**tR2I** 规定了一个读操作的最后一个读命令到下一个操作的激活命令(ACT)的时钟周期数(以 aclk 为准)的最小值。下图展示了一个 DDR1 接口上的读操作ddr_cas_n 的第一个上升沿代表了一个读操作的最后一个读命令结束ddr_ras_n 的第二个下降沿代表了下一个操作(可能是读、写、刷新)的开始,它们之间有 5 个时钟周期,**tR2I** 就是用来规定该周期数的下限的。**tR2I** 的默认值 8'd7 是一个兼容绝大多数 DDR1 的保守值,对于不同的 DDR1 芯片,有不同的缩小的余地(详见 DDR1 芯片 datasheet
__ __ __ __ __ __ __ __ __ __ __ __
ddr_ck_p __/ \__/ \__/ \__/ \__/ \__/ \__/ \__/ \__/ \__/ \__/ \__/ \__
_____ _______________________________________________ ________
ddr_ras_n \_____/ \_____/
_________________ ____________________________________________
ddr_cas_n \___________/
__________________________________________________________________________
ddr_we_n
_____
ddr_a[10] XXXXXXXXXXXXXXXXXX_____/ XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
_______________________
ddr_ba XXXXXX__________BA___________XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
_______________________
ddr_a XXXXXX__RA_XXXXXXX_CA0_X_CA1_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
# 示例程序
## 示例程序 - 自测
我基于我画的 [FPGA+DDR1测试板](#硬件设计示例) 做了一个 DDR1 读写自测程序,工程目录是 [example-selftest](https://github.com/WangXuan95/FPGA-DDR-SDRAM/blob/main/example-selftest),请用 Quartus II 13.1 打开它。
该工程包含以下文件:
| 文件名称 | 用途 |
| :---- | :--- |
| [top.sv](https://github.com/WangXuan95/FPGA-DDR-SDRAM/blob/main/example-selftest/RTL/top.sv) | 顶层 |
| [axi_self_test_master.sv](https://github.com/WangXuan95/FPGA-DDR-SDRAM/blob/main/example-selftest/RTL/axi_self_test_master.sv) | 是 AXI4 主机,通过 AXI4 先把有规律的数据写入 DDR1然后读回比较读回的数据是否符合规律并对不匹配的情况进行计数。 |
| [ddr_sdram_ctrl.sv](https://github.com/WangXuan95/FPGA-DDR-SDRAM/blob/main/RTL/ddr_sdram_ctrl.sv) | DDR1 控制器 |
| [pll.v](https://github.com/WangXuan95/FPGA-DDR-SDRAM/blob/main/example-selftest/IP/pll.v) | 使用板上 50MHz 晶振产生 300MHz 时钟,输出给 DDR1 控制器 |
**写入**:该工程开始运行后,会先通过 AXI4 把整个 DDR1 都写一遍,直接把地址字写入相应的地址。例如,如果 AXI4 的数据宽度是 16bit那么地址 0x000002 处写 0x0001。地址 0x123456 处写 0x3456。
**读取&错误校验**:写完整个 DDR1 后,该工程会一轮一轮地反复读取整个 DDR1若读出的数据不符合上述规律就认为出现错误在错误信号error上产生一个高电平脉冲并把错误计数信号error_cnt+1。如果 DDR1 配置正确,是不该出现 error 信号的。你可以测量 error_cnt 对应的引脚若为0全低电平说明不存在错误。
2021-01-27 18:13:39 +08:00
**SignalTap抓波形**:该工程包含一个 SignalTap 文件([stp1.stp](https://github.com/WangXuan95/FPGA-DDR-SDRAM/blob/main/example-selftest/SignalTap)),在程序运行时,可以用它查看 DDR1 接口上的波形。它以 error=1 为触发信号,因此如果读写自测没有出错,它就不会被触发。因为该工程随时都在读取 DDR1要想看 DDR1 接口上的波形,直接按“停止”按钮即可。
2021-01-27 18:06:35 +08:00
**修改 AXI4 突发长度**:在 [top.sv](https://github.com/WangXuan95/FPGA-DDR-SDRAM/blob/main/example-selftest/RTL/top.sv) 的第 8283 行可以修改 WBURST_LEN 和 RBURST_LEN从而修改自测时的写/读突发长度,该自测程序只支持 2^n-1 这种突发长度,即 WBURST_LEN 和 RBURST_LEN 必须取形如 0,1,3,7,15,31,…… 的值注意这只是我编写的自测程序的限制DDR1 控制器是支持 0~255 之间的任意突发长度的。
> WBURST_LEN 和 RBURST_LEN 可以设置的不一样。
## 示例程序 - UART读写
我基于我画的 [FPGA+DDR1测试板](#硬件设计示例) 做了一个 UART 读写程序,使用该程序,你可以通过 UART 命令,以不同的突发长度来读写 DDR1。工程目录是 [example-uart-read-write](https://github.com/WangXuan95/FPGA-DDR-SDRAM/blob/main/example-uart-read-write),请用 Quartus II 13.1 打开它。
该工程包含以下文件:
| 文件名称 | 用途 |
| :---- | :--- |
| [top.sv](https://github.com/WangXuan95/FPGA-DDR-SDRAM/blob/main/example-uart-read-write/RTL/top.sv) | 顶层 |
| [uart2axi4.sv](https://github.com/WangXuan95/FPGA-DDR-SDRAM/blob/main/example-uart-read-write/RTL/uart2axi4.sv) | 是 AXI4 主机,能把 UART RX 收到的命令转换成 AXI4 读写操作,并把读操作读出的数据通过 UART TX 发送出去 |
| [uart_rx.sv](https://github.com/WangXuan95/FPGA-DDR-SDRAM/blob/main/example-uart-read-write/RTL/uart_rx.sv) | UART RX 接收器,被 uart2axi4.sv 调用 |
| [axis2uarttx.sv](https://github.com/WangXuan95/FPGA-DDR-SDRAM/blob/main/example-uart-read-write/RTL/axis2uarttx.sv) | UART TX 发送器,被 uart2axi4.sv 调用 |
| [ddr_sdram_ctrl.sv](https://github.com/WangXuan95/FPGA-DDR-SDRAM/blob/main/RTL/ddr_sdram_ctrl.sv) | DDR1 控制器 |
| [pll.v](https://github.com/WangXuan95/FPGA-DDR-SDRAM/blob/main/example-uart-read-write/IP/pll.v) | 使用板上 50MHz 晶振产生 300MHz 时钟,输出给 DDR1 控制器 |
[FPGA+DDR1测试板](#硬件设计示例)上有一个 CH340E 芯片USB 转 UART因此插上 USB 线后就可以在电脑上看见 UART 对应的 COM 口(需要先在[这里](http://www.wch.cn/product/CH340.html)下载安装 CH341 的驱动)。
工程上传 FPGA 后,双击打开我编写的一个串口小工具 [UartSession.exe](https://github.com/WangXuan95/FPGA-DDR-SDRAM/blob/main/UartSession.exe) ,根据提示打开板子对应的 COM 口,然后打如下的命令+回车,可以把 0x0123 0x4567 0x89ab 0xcdef 这 4 个数据写入起始地址0x12345。AXI4总线上会产生一个突发长度为 4 的写操作)。
w12345 0123 4567 89ab cdef
然后用以下命令+回车,可以以 0x12345 为起始地址,以 7 为突发长度,读取 8 个数据。
r12345 7
效果如下图前4个数据 (0123 4567 89ab cdef) 就是我们已经写入 DDR1 的后4个数据我们没写过是 DDR1 初始化后自带的随机数据。
| ![UART读写](https://github.com/WangXuan95/FPGA-DDR-SDRAM/blob/main/images/UartSession.png) |
| :--: |
| 图:使用 UartSession.exe 测试 DDR1 读写 |
写命令里有多少数据,写突发长度就是多少,例如以下写命令的突发长度是 9将 10 个数据写入起始地址 0x00000
w0 1234 2345 3456 4567 5678 6789 789a 89ab 9abc abcd
读命令则直接指定突发长度,例如以下命令的突发长度为 30 0x1e从起始地址 0x00000 将 31 个数据读出
r0 1e
# Verilog 仿真
## 建立仿真工程
仿真所需要的文件在目录 [sim-selftest](https://github.com/WangXuan95/FPGA-DDR-SDRAM/blob/main/sim-selftest) 里,你可以用以下 Verilog 文件建立仿真工程:
| 文件路径 | 用途 |
| :---- | :--- |
| [sim-selftest/SRC/tb_ddr_ctrl.sv](https://github.com/WangXuan95/FPGA-DDR-SDRAM/blob/main/sim-selftest/SRC/tb_ddr_ctrl.sv) | 仿真顶层 |
| [sim-selftest/SRC/axi_self_test_master.sv](https://github.com/WangXuan95/FPGA-DDR-SDRAM/blob/main/sim-selftest/SRC/axi_self_test_master.sv) | 是 AXI4 主机,通过 AXI4 先把有规律的数据写入 DDR1然后读回比较读回的数据是否符合规律并对不匹配的情况进行计数。 |
| [RTL/ddr_sdram_ctrl.sv](https://github.com/WangXuan95/FPGA-DDR-SDRAM/blob/main/RTL/ddr_sdram_ctrl.sv) | DDR1 控制器 |
| [sim-selftest/DDR_MODEL/ddr.v](https://github.com/WangXuan95/FPGA-DDR-SDRAM/blob/main/sim-selftest/DDR_MODEL/ddr.v) | [MICRON 公司提供的 DDR1 仿真模型](https://www.micron.com/products/dram/ddr-sdram/part-catalog/mt46v64m8p-5b) |
| [sim-selftest/DDR_MODEL/ddr_parameters.vh](https://github.com/WangXuan95/FPGA-DDR-SDRAM/blob/main/sim-selftest/DDR_MODEL/ddr_parameters.vh) | DDR1 仿真模型的参数配置文件 |
该仿真工程的行为类似[自测程序](#示例程序 - 自测)[axi_self_test_master.sv](https://github.com/WangXuan95/FPGA-DDR-SDRAM/blob/main/sim-selftest/SRC/axi_self_test_master.sv) 作为 AXI4 主机,将有规律的数据写入 DDR1 中,只不过不是全部写入,而是只写入 DDR1 的前 16KB (因为仿真模型的存储空间有限),然后一轮一轮地反复读出数据,比较是否有不匹配的数据,若有,则在 error 信号上产生一个时钟周期的高电平。
## 运行仿真
建立工程后,直接运行仿真,得到如下波形。前 294usAXI4 主机在进行前 16KB 的写入294us 之后AXI4 主机在不断的读出数据。error 信号一直为低电平说明读出的数据无误。
| ![仿真波形](https://github.com/WangXuan95/FPGA-DDR-SDRAM/blob/main/images/sim.png) |
| :--: |
| 图:仿真波形 |
你可以展开查看 AXI4 总线和 DDR1 接口的波形细节。这里不做赘述。
## 修改仿真参数
以上仿真默认配置的参数是使用 MT46V64M8 ,即 ROW_BITS=13COL_BITS=11DQ_BITS=8。如果想对其它型号的 DDR1 芯片进行仿真,需要修改的参数如下:
* 仿真顶层中的 **BA_BITS**:见 [sim-selftest/SRC/tb_ddr_ctrl.sv](https://github.com/WangXuan95/FPGA-DDR-SDRAM/blob/main/sim-selftest/SRC/tb_ddr_ctrl.sv) 第 8 行。
* DDR1 模型中的 **BA_BITS**:见 [sim-selftest/DDR_MODEL/ddr_parameters.vh](https://github.com/WangXuan95/FPGA-DDR-SDRAM/blob/main/sim-selftest/DDR_MODEL/ddr_parameters.vh) 第 35 行。
* 仿真顶层中的 **ROW_BITS**:见 [sim-selftest/SRC/tb_ddr_ctrl.sv](https://github.com/WangXuan95/FPGA-DDR-SDRAM/blob/main/sim-selftest/SRC/tb_ddr_ctrl.sv) 第 9 行。
* DDR1 模型中的 **ROW_BITS**:见 [sim-selftest/DDR_MODEL/ddr_parameters.vh](https://github.com/WangXuan95/FPGA-DDR-SDRAM/blob/main/sim-selftest/DDR_MODEL/ddr_parameters.vh) 第 36 行。
* 仿真顶层中的 **COL_BITS**:见 [sim-selftest/SRC/tb_ddr_ctrl.sv](https://github.com/WangXuan95/FPGA-DDR-SDRAM/blob/main/sim-selftest/SRC/tb_ddr_ctrl.sv) 第 10 行。
* DDR1 模型中的 **COL_BITS**:见 [sim-selftest/DDR_MODEL/ddr_parameters.vh](https://github.com/WangXuan95/FPGA-DDR-SDRAM/blob/main/sim-selftest/DDR_MODEL/ddr_parameters.vh) 第 37 行。
* 仿真顶层中的 **DQ_LEVEL**:见 [sim-selftest/SRC/tb_ddr_ctrl.sv](https://github.com/WangXuan95/FPGA-DDR-SDRAM/blob/main/sim-selftest/SRC/tb_ddr_ctrl.sv) 第 11 行。
* DDR1 模型中的 **DQ_BITS**:见 [sim-selftest/DDR_MODEL/ddr_parameters.vh](https://github.com/WangXuan95/FPGA-DDR-SDRAM/blob/main/sim-selftest/DDR_MODEL/ddr_parameters.vh) 第 38 行。
对于 MICRON 公司的 DDR1 系列,这些参数应该这样修改:
| 芯片名称 | BA_BITS | ROW_BITS | COL_BITS | DQ_LEVEL | DQ_BITS
| :--: | :--: | :--: | :--: | :--: | :--: | :--: | :--: | :--: | :--: |
| MT46V64M4 | 2 | 13 | 11 | 0 | 4 |
| MT46V128M4 | 2 | 13 | 12 | 0 | 4 |
| MT46V256M4 | 2 | 14 | 12 | 0 | 4 |
| MT46V32M8 | 2 | 13 | 10 | 1 | 8 |
| MT46V64M8 | 2 | 13 | 11 | 1 | 8 |
| MT46V128M8 | 2 | 14 | 11 | 1 | 8 |
| MT46V16M16 | 2 | 13 | 9 | 2 | 16 |
| MT46V32M16 | 2 | 13 | 10 | 2 | 16 |
| MT46V64M16 | 2 | 14 | 10 | 2 | 16 |
另外,你可以修改 [tb_ddr_ctrl.sv](https://github.com/WangXuan95/FPGA-DDR-SDRAM/blob/main/sim-selftest/SRC/tb_ddr_ctrl.sv) 的第 18 和 19 行来修改仿真时的突发读写的长度。
# 参考资料
* MICRON 公司提供的 DDR1 仿真模型: https://www.micron.com/products/dram/ddr-sdram/part-catalog/mt46v64m8p-5b
* MT46V64M8 芯片手册: https://media-www.micron.com/-/media/client/global/documents/products/data-sheet/dram/ddr1/512mb_ddr.pdf?rev=4e1e995d6d2240e293286770f193d57d