1、DDR 基础知识
1.1、DDR3 全称与特点
DDR3全称为Double Data Rate Type 3 Synchronous Dynamic Random-Access Memory(双倍数据率三代同步动态随机存储器)。
特点:
- 掉电无法保存数据,需要周期的刷新;
- 双沿传输:时钟上升沿和下降沿都会传输数据;
- 突发传输,突发长度 Brust Length 一般为8
1.2、DDR3 的存储
DDR3的存储流程是先指定Bank地址,再指定行地址,最后指定列地址。可以将其理解为一本有很多页的书,首先确定要读取或写入的是哪一页(Bank),然后确定该页的哪一行(Row),最后选择该行的哪一列(Column)。
1.3、DDR3 容量计算
容量计算:Bank数量 x 行(Row)数量 x 列(Column)数量 x 存储单元的容量。
以逻辑派板子DDR3为例:2^3 x 2^14 x 2^10 x 16bit = 2 Gbit
1.4、DDR3 命名规则
还是以逻辑派板子DDR3为例:MT41J128M16JT-125:K
- MT41J :为镁光芯片的代号。
- Configuration :是寻址,具体怎么看呢! 往下看:
Bank addressing :Bank数
Row addressing : 行数
Column addressing :列数
注:以行数列的 16K (A[13:0]) 他的16K是怎么来的:2^14/1024 = 16K 其余的依次类推。
X16 :代表是DDR3的数据位宽。
- Package :是封装编号
- -125 :是速度等级。
2、DDR3 接口描述
| 接口 | 类型 | 功能描述 |
|---|---|---|
| ODT | 输入 | 使能DDR3内部的终结电阻,为高时有效,优化DQ/DM/DQS的信号质量 |
| RST | 输入 | 复位,低有效 |
| CKE | 输入 | 时钟使能输入:CKE为高时,使能时钟正常工作;CKE为低时,使能Power-down模式或自刷新功能 |
| CK、CK# | 输入 | 差分时钟输入:所有地址和命令输入信号,在CK的上升沿和CK#的下降沿交叉点锁存;输出数据DQ、DQS、DQS#参考CK和CK#的交叉点 |
| CS# | 输入 | 芯片选择使能:CS#为高时,芯片不使能,屏蔽外部所有命令;CS#为低时,芯片使能,响应外部命令 |
| RAS#,CAS#,WE# | 输入 | 命令输入:三个信号,连同CS#,定义一个命令 |
| A[13:0] | 输入 | 地址输入:为ACTIVATE命令提供行地址和列地址和自动预充位(A10)的读/写命令,选择一个在各自存储库的内存数组外的位置。 |
| BA[2:0] | 输入 | BANK地址输入:在激活、读取、写入、预充电命令操作时,提供BANK地址;在加载模式寄存器命令时,决定设置哪种模式寄存器 |
| DQ[15:0] | 输入/输出 | 数据接口 |
| LDQS、LDQS#、 UDQS、UDQS# | 输入/输出 | 低/高字节选择信号:在读取时作为输出信号,数据采用边沿对齐;在写入时作为输入信号,数据采用中心对齐。 |
| LDM、UDM | 输入/输出 | 低/高字节选择信号:在读取时作为输出信号,数据采用边沿对齐;在写入时作为输入信号,数据采用中心对齐。 |
引脚接口说明:
3、DDR3 讲解
3.1、接口
接下来我们介绍 Gowin DDR3 Memory Interface IP 核用户接口的信号及其说明。详细的查看官网的UG281手册
3.2、初始化接口
init_calib_complete:DDR3 SDRAM 必须经过校准操作才能进行正常的写、读操作。因此上电后 PHY 会对 DDR3 SDRAM 进行初始化校准操作,初始化完成后 init_calib_complete拉高,
3.3、命令和地址接口
命令:用户可通过 cmd 及 cmd_en 向 IP 写入命令,控制器根据写入命令的先后顺序,向DDR3 颗粒发起读写事务。
- cmd 为命令端口;
- cmd_en 为命令使能信号,高电平时 cmd 有效
地址:addr 是用户侧地址总线,与 cmd 一同写入控制器,当 cmd_en 有效时,addr 有效。我们需要在应用中,注意提供的地址顺序。
addr 为 DDR 地址,即 addr 直接反映 DDR 内存地址。当 DDR3 burst_mode 配置 BC4时,一次写/读需向 DDR 内写入/读取 4 个 dq 数据,因此一次 DDR 写/读占用 4 个地址;当DDR3 burst_mode 配置 BL8 时,一次写/读需向 DDR 内写入/读取 8 个 dq 数据,因此一次DDR 写/读占用 8 个地址。
3.4、地址与命令时序
cmd_ready 为高电平,表示 DDR 控制器可以接收用户命令。cmd_en 为 1 且 cmd_ready 为 1 时将 cmd 及 addr 写入 IP,addr1 和 addr2 无任何关系,不必是相邻地址。如下图所示:
3.5、写数据接口
- wr_data:数据总线接口,用户可通过此接口写入需要存储 DDR 内的数据。
- wr_data_en:数据写入使能接口,高电平时 wr_data 有效。
- wr_data_end:表明当前周期 wr_data 总线上的数据是当前写入的最后一个数据。
- wr_data_rdy:当 wr_data_rdy 为高电平时,表示控制器可以接收 user 数据,用户可通过接口 wr_data、wr_data_en 与 wr_data_end 将数据写入控制器。
3.6、读数据接口
用户可通过用户接口 rd_data、rd_data_valid 与 rd_data_end 读取 DDR3 SDRAM 返回的数据。
- 端口 rd_data 为返回的读数据端口;
- 端口信号 rd_data_valid 为读数据有效端口,当其为高电平时,指示此时返回的 rd_data 有效;
- 端口 rd_data_end 指示在当前 burst_mode 下所返回的最后一组数据,高电平时有效。
3.7、IP 核讲解
3.7.1 type
Select Memory :
- Memory Type : 存储其类型
- Memory Clock: DDR接口时钟
- CLK Ratio : 用户接口时钟频率与DDR接口时钟频率比值
- DIMM Type : DDR的DIMM类型
Data Bus :
- Dq Width : Dq数据位宽
- Dram Width : 单个DDR的数据位宽
Memory Address :
- Rank Address:Rank地址
- Bank Address:DDR的内内存BANK地址
- Row Address:DDR行地址宽度
- Column Address:DDR列地址宽度
Addr Width: 地址宽度 Data Width: 数据宽度
3.7.2 option
- Burst Mode: 4 (突发模式: 4) 一次读/写操作连续传 4 个数据 (需要与代码中的配置一致)
- CAS Latency: 5 (CAS潜伏期: 5) 从FPGA发出“读”命令,到第一个数据出现在数据线上,要等 5 个时钟周期
- CW Latency: 5 (CAS写入潜伏期: 5) 从发出“写”命令,到第一个数据被写入,要等 5 个时钟周期
- Burst Type: SEQ (突发类型: 顺序) 连续传输的数据,按地址顺序依次访问
- Rtt Nom: 40 (标称终端电阻: 40欧姆)
- Addr Cmd Mode: 1T (地址命令模式: 1T)
- User Refresh (手动刷新)
- Write Recovery: 6 (写恢复时间: 6)
- SLOT_1_CONFIG: OFF (插槽1配置: 关闭)
- OUTPUT DRV: LOW (输出驱动强度: 低)
3.7.3 timimg
这个在选 DDR3 芯片型号的时候自动会填好 这里不用管
锁相环ip核配置
这里我们要给 DDR3 一个 200 MHz 的时钟作为他的 memory clk ,rPPL 的具体操作我们不再赘述,可以到 IP 核 PPL 那一章去看
4、程序编写
注:部分注释有AI生成。
`timescale 1ps /1ps
module ddr3_syn_top
(
input clk, // ← 板子上50MHz晶振输入
input rst_nn, // ← 按键复位,低有效
// ↓ 下面这些全部连到 DDR3 芯片的物理引脚
output [13:0] ddr_addr, // 14根地址线
output [2:0] ddr_bank, // 3根Bank选择线
output ddr_cs, // 片选
output ddr_ras, // 行选通
output ddr_cas, // 列选通
output ddr_we, // 写使能
output ddr_ck, // 差分时钟正
output ddr_ck_n, // 差分时钟负
output ddr_cke, // 时钟使能
output ddr_odt, // 片上终端
output ddr_reset_n, // DDR3芯片复位
output [1:0] ddr_dm, // 2根数据掩码
inout [15:0] ddr_dq, // 16根数据线(双向!)
inout [1:0] ddr_dqs, // 2对差分数据时钟(双向!)
inout [1:0] ddr_dqs_n, //
output [3:0] led // 4个LED指示灯
);
//第 25-47 行:内部连线(wire)声明
wire app_wdf_wren; // 写使能:"这64位数据有效"
wire [7:0] app_wdf_mask; // 写掩码:8bit对应8字节,bit=1则那字节不写
wire app_wdf_end; // 写结束:"这是最后一个数据"
wire [63:0] app_wdf_data; // 写数据本体
wire app_wdf_rdy; // ←IP核给的:"写FIFO有空位"
// 写数据通道,你把数据递给IP核的接口。
wire app_en; // 命令使能:"请执行这条命令"
wire [2:0] app_cmd; // 命令码:000=写 001=读
wire [27:0] app_addr; // 地址(不是DDR3物理行/列地址!是用户空间地址)
wire app_burst; // 0=BL4 1=BL8
wire app_rdy; // ←IP核给的:"可以接命令了"
//命令通道,告诉IP核"做什么操作、在哪个地址"。
wire app_rd_data_valid; // ←IP核给的:"读数据有效,拿走!"
wire app_rd_data_end; // ←IP核给的:"这是最后一个读数据"
wire [63:0] app_rd_data; // 读回来的数据
// 读数据通道,IP核把读出来的数据还给你。
wire memory_clk; // PLL输出的200MHz,给IP核物理层
wire pll_lock; // PLL稳定标志
reg [31:0] run_cnt; // 心跳计数器
wire init_calib_complete; // ☆DDR3初始化完成标志
wire error; // 数据错误
wire error_flag; // 错误脉冲
wire running; // 1Hz心跳
// `define DEBUG_PORT_ENABLE
//条件编译
`ifdef DEBUG_PORT_ENABLE
wire [16-1:0] dbg_vector4_out; //[8*DQS_WIDTH-1:0]
wire [6-1:0] dbg_vector3_out; //[3*DQS_WIDTH-1:0]
wire [4-1:0] dbg_vector2_out; //[4*DQS_WIDTH-1:0]
wire [7:0] dbg_vector1_out;
`endif
assign rst_n = rst_nn; //把外部按键的复位信号直接传给内部。rst_nn 是 FPGA 引脚上进来的,rst_n 是内部用的。
assign led[0] = ~init_calib_complete; // 常亮=DDR3初始化成功
assign led[1] = running; // 1Hz闪烁=系统正常
//assign led[0] = error; //无错亮
//assign led[1] = ~error_flag; //F14 注释掉:出错脉冲闪
wire clk_x1;
//LED test 心跳计数器函数。计数一秒
always @(posedge clk_x1 or negedge rst_n)
if(!rst_n)
run_cnt <= 32'd0;
else if(run_cnt >= 32'd50_000_000)
run_cnt <= 32'd0;
else
run_cnt <= run_cnt + 1'b1;
assign running = (run_cnt < 32'd25_000_000) ? 1'b1 : 1'b0; // 前0.5秒=1(亮),后0.5秒=0(灭)→ 1Hz闪烁
DDR3_test #
(
.ADDR_WIDTH (28) , // 参数:地址28位
.APP_DATA_WIDTH(64) , // 参数:数据64位
.APP_MASK_WIDTH(8), // 参数:掩码8位
.BURST_MODE ("4")//4,8
)u_rd( // 起个名叫u_rd
.clk (clk_x1), // 用IP核输出的用户钟
.rst (~rst_n), // 取反!外部低复位→内部高复位
.app_rdy (app_rdy), // IP→用户:可接命令
.app_en (app_en), // 用户→IP:命令有效
.app_cmd (app_cmd), // 用户→IP:命令有效
.app_addr (app_addr), // 用户→IP:000写/001读
.app_wdf_data (app_wdf_data), // 用户→IP:地址
// ... 把所有用户接口信号接上 ...
.app_wdf_wren (app_wdf_wren),
.app_wdf_end (app_wdf_end),
.app_wdf_mask (app_wdf_mask),
.app_wdf_rdy (app_wdf_rdy),
.app_burst (app_burst),
.app_rd_data_valid (app_rd_data_valid),
.app_rd_data_end (app_rd_data_end),
.app_rd_data (app_rd_data),
.init_calib_complete(init_calib_complete),
.error (error), // 比较结果输出回顶层
.error_flag (error_flag)
);
Gowin_rPLL pll(
.clkout(memory_clk), //output clkout // 输出:200MHz
.lock(pll_lock), //output lock // 输出:锁定了=1
.reset(~rst_n), //input reset // 输入:复位(高有效,取反)
.clkin(clk) //input clkin // 输入:外部50MHz
);
// PLL = 锁相环。作用:50MHz × 4 = 200MHz。lock 不稳定时 IP 核不启动
//ddr的IP核实例化模块
DDR3_Memory_Interface_Top u_ddr3 (
.clk (clk), // 50MHz参考
.memory_clk (memory_clk), // 200MHz高速
.pll_lock (pll_lock), // PLL锁了才工作
.rst_n (rst_n), // 复位
// 两个时钟:clk是低速参考钟(初始化用),memory_clk是高速运行钟(物理层用)。
.cmd_ready (app_rdy), // ←IP:"可接命令"
.cmd (app_cmd), // →IP:000写/001读
.cmd_en (app_en), // →IP:执行!
.addr (app_addr), // →IP:地址
// 命令通道:你先看 cmd_ready 是不是1,是的话就把 cmd + addr + cmd_en 一起发出去。
.wr_data_rdy (app_wdf_rdy), // ←IP:"写FIFO有空"
.wr_data (app_wdf_data), // →IP:64位数据
.wr_data_en (app_wdf_wren), // →IP:数据有效
.wr_data_end (app_wdf_end), // →IP:最后一个
.wr_data_mask (app_wdf_mask), // →IP:掩码
// 写数据通道:命令和数据可以不同拍发。
.rd_data (app_rd_data), // ←IP:读到的数据
.rd_data_valid (app_rd_data_valid), // ←IP:数据有效!
.rd_data_end (app_rd_data_end), // ←IP:最后一个
//读数据通道:rd_data_valid 拉高时,rd_data 才是有效的。
.sr_req (1'b0), // 不手动自刷新
.ref_req (1'b0), // 不手动刷新
.sr_ack (), // 悬空
.ref_ack (), // 悬空
// IP核自动管理刷新,你不用管
.init_calib_complete(init_calib_complete), //☆等这个变1了才能读写DDR3!
.clk_out (clk_x1), //IP核给用户的时钟
//条件编译
`ifdef ECC
.ecc_err (ecc_err),
`endif
.burst (app_burst),
.ddr_rst (),
`ifdef DEBUG_PORT_ENABLE
.dbg_vector4_out (dbg_vector4_out),
.dbg_vector3_out (dbg_vector3_out),
.dbg_vector2_out (dbg_vector2_out),
.dbg_vector1_out (dbg_vector1_out),
`endif
// mem interface --- DDR3物理层,直接连芯片引脚 ---
.O_ddr_addr (ddr_addr), // O_ = Output(FPGA→DDR3)
.O_ddr_ba (ddr_bank),
.O_ddr_cs_n (ddr_cs),
.O_ddr_ras_n (ddr_ras),
.O_ddr_cas_n (ddr_cas),
.O_ddr_we_n (ddr_we),
.O_ddr_clk (ddr_ck),
.O_ddr_clk_n (ddr_ck_n),
.O_ddr_cke (ddr_cke),
.O_ddr_odt (ddr_odt),
.O_ddr_reset_n (ddr_reset_n),
.O_ddr_dqm (ddr_dm),
.IO_ddr_dq (ddr_dq), // IO表示是双向
.IO_ddr_dqs (ddr_dqs),
.IO_ddr_dqs_n (ddr_dqs_n)
);
/*→IP 的意思是:这个信号是你发给 IP 核的,方向是从你(DDR3_test)到 IP 核。
←IP 的意思是:这个信号是 IP 核发给你的,方向是从 IP 核到你(DDR3_test)。
*/
endmodule2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
// ==================== 仿真参数 ====================
// `timescale 告诉仿真器时间单位和精度
// 1ps/1ps 意思是:时间单位=1皮秒,仿真精度=1皮秒
// ⚠️ 这行只在仿真软件(如 Modelsim)里有效,真正烧到 FPGA 里不起作用
`timescale 1ps/1ps
module DDR3_test #(
parameter ADDR_WIDTH = 28, // 地址位宽,默认 28 位
parameter APP_DATA_WIDTH = 64, // 数据位宽,默认 64 位
parameter APP_MASK_WIDTH = 8, // 掩码位宽,默认 8 位(64位÷8字节=8根) 每根控制一个字节
parameter BURST_MODE = "4" //4,8 // 突发模式,默认 "4"(BL4)
)
(
// ====== 输入 ======
clk, // 用户时钟,来自 IP 核的 clk_out
rst, // 复位,高有效(注意和顶层不一样!)
app_rdy, // IP核→我:"可以接命令了"
app_wdf_rdy, // IP核→我:"写FIFO有空位"
app_rd_data_valid, // IP核→我:"读数据有效"
app_rd_data_end, // IP核→我:"最后一个读数据"
app_rd_data, // IP核→我:读回来的数据
init_calib_complete, // IP核→我:"DDR3初始化完毕,你可以操作了"
// ====== 输出 ======
app_en, // 我→IP核:"执行命令!"
app_cmd, // 我→IP核:000=写 001=读
app_addr, // 我→IP核:操作地址
app_wdf_data, // 我→IP核:要写的数据
app_wdf_wren, // 我→IP核:写数据有效
app_wdf_end, // 我→IP核:最后一个写数据
app_wdf_mask, // 我→IP核:掩码
app_burst, // 我→IP核:BL4还是BL8
error, // 输出给顶层:数据比较出错
error_flag // 输出给顶层:错误脉冲
);
//输入端口的位宽声明
// input 关键字声明:这些信号是从外面进来的
input clk; // 1 位时钟
input rst; // 1 位复位(高有效!)
input app_rdy; // 1 位:命令就绪
input app_wdf_rdy; // 1 位:写 FIFO 就绪
input app_rd_data_valid; // 1 位:读数据有效
input app_rd_data_end; // 1 位:读数据结束
input [APP_DATA_WIDTH-1:0] app_rd_data; // 64 位:[63:0]
input init_calib_complete; // 1 位:初始化完成
//输出端口的位宽声明
output reg app_en; // reg类型,在always里赋值
output reg [2:0] app_cmd; // 3位,000=写 001=读
output reg [ADDR_WIDTH-1:0] app_addr; // [27:0] 地址 28 根地址线
output reg [APP_DATA_WIDTH-1:0] app_wdf_data/* synthesis syn_keep=1 */; // [63:0] 写数据 syn_keep=1:告诉综合工具"保留这个信号名,不要优化掉" 方便在 FPGA 调试工具里抓波形看
output reg app_wdf_wren; // 1 位:写数据有效
output reg app_wdf_end; // 1 位:写数据结束
output reg [APP_MASK_WIDTH-1:0] app_wdf_mask; // 8 位:[7:0] 掩码
output reg app_burst; // 1 位:突发模式选择
output error; // 注意:这两个是 wire,不是 reg
output error_flag;
//内部变量声明
reg [7:0] cnt; // 8位计数器,从0计到255,自动溢出归零,控制读写时序节奏
reg [27:0] reg_addr; // 28位地址寄存器,存放"当前要对 DDR3 的哪个地址操作", 每轮读写结束(cnt=255)后 +addr_step
wire[4:0] mem_index; // 5位索引,用来从预存的32个数据里选一个, 从 reg_addr 中间抽 5 位出来, 用来从 32 组预存数据里选一组
wire[3:0] addr_step; // 4位步长,每次地址加多少(BL4=4,BL8=8)
reg [1:0] error_d; // 2位错误锁存(打两拍,消除亚稳态,同步用)
reg [1:0] error_flag_d; // 2位错误脉冲锁存
//cnt计数器
always@(posedge clk or posedge rst)
begin
if (rst)
cnt <= 8'd0;
else if(app_rdy & app_wdf_rdy & init_calib_complete)
cnt <= cnt + 1'b1;
else
cnt <= cnt;
end
/* 这里 cnt 加 1 要同时满足三个条件:
1. init_calib_complete = 1:DDR3 初始化完毕,可以用了
2. app_rdy = 1:IP 核可以接命令
3. app_wdf_rdy = 1:IP 核写 FIFO 有空位
*/
//reg_addr 地址寄存器
always@(posedge clk or posedge rst)
begin
if (rst)
reg_addr <= 28'd0;
else if(cnt==8'd255 && app_rdy && app_wdf_rdy && init_calib_complete) // cnt计满且系统可用
reg_addr <= reg_addr + addr_step; // 地址+步长
else
reg_addr <= reg_addr;
end
// ==================== mem_index ====================
// 从 28 位地址中抽取 5 位,作为预存数据的索引
// 为什么是 [6:2]?因为 addr_step(步长) 最小是 4(二进制 100)
// 低 2 位 [1:0] 永远不变(00),抽出来没意义
// [6:2] 一共 5 位,可以表示 0~31,正好对应 32 组预存数据
assign mem_index = reg_addr[6:2];
// ==================== 预存测试数据(32组) ====================
// mem_data 是一个 32 元素的数组,每个元素 64 位
// 相当于 C 语言里的:uint64_t mem_data[32];
//
// 这些数据都是"魔数"——没什么特殊含义,就是用来测试的
// 写的时候把这些数据写入 DDR3,读的时候读回来比较
// 如果读回来的数据和这些不一样 → 说明 DDR3 读写出了问题
wire [APP_DATA_WIDTH-1:0] mem_data [0:31]; // 64位 × 32个 = 一维数组
// ----- 第一组(索引 0~7):看起来像随机数 -----
assign mem_data[0] = 64'h8808_7728_e878_f685; // 64'h... = 64位宽的十六进制数
assign mem_data[1] = 64'h1505_5a21_25b5_fa1a; // 每个都是随意填的,没有实际意义
assign mem_data[2] = 64'h2404_0bf2_d464_ab25;
assign mem_data[3] = 64'h3303_45e3_f173_213a;
assign mem_data[4] = 64'h42e2_6694_42b2_f245;
assign mem_data[5] = 64'h5101_6735_d351_435a;
assign mem_data[6] = 64'h6000_0826_d440_f465;
assign mem_data[7] = 64'h7606_8947_86b6_357a;
// ----- 第二组(索引 8~15):有规律的递增 -----
assign mem_data[8] = 64'h1000_0100_2180_f290; // 注意看:高16位 1000, 3000, 5000...
assign mem_data[9] = 64'h3000_0300_429e_d4a1; // 每个数据的高位都有递增规律
assign mem_data[10] = 64'h5000_0500_63ac_b6b2;
assign mem_data[11] = 64'h7000_0700_84ba_98c3;
assign mem_data[12] = 64'h9000_0900_a5c8_7ad4;
assign mem_data[13] = 64'hb000_0b00_c6d6_5ce5;
assign mem_data[14] = 64'hd000_0d00_e7e4_3ef6;
assign mem_data[15] = 64'hf000_0f00_08f2_1f07;
// ----- 第三组(索引 16~23):和第一组很像但尾数不同 -----
assign mem_data[16] = 64'h8808_7728_b870_f688; // 和 mem_data[0] 比:e878→b870, f685→f688
assign mem_data[17] = 64'h1505_5a21_25b1_fa19;
assign mem_data[18] = 64'h2404_0bf2_a462_ab2a;
assign mem_data[19] = 64'h3303_45e3_d173_213b;
assign mem_data[20] = 64'h42e2_6694_42b4_f24c;
assign mem_data[21] = 64'h5101_6735_d355_435d;
assign mem_data[22] = 64'h6000_0826_f446_f46e;
assign mem_data[23] = 64'h7606_8947_86b7_357f;
// ----- 第四组(索引 24~31):和第二组类似但不同 -----
assign mem_data[24] = 64'h1000_0100_1c42_12ce;
assign mem_data[25] = 64'h3000_0300_3a54_34df;
assign mem_data[26] = 64'h5000_0500_5a66_56ea;
assign mem_data[27] = 64'h7000_0700_7a78_78f3;
assign mem_data[28] = 64'h9000_0900_9b8a_9a02;
assign mem_data[29] = 64'hb000_0b00_bb9c_bc11;
assign mem_data[30] = 64'hd000_0d00_dbae_de20;
assign mem_data[31] = 64'hf000_0f00_fcb0_f03d;
// ==================== generate:条件编译 ====================
generate
if(BURST_MODE == "4") begin: app_burst_4 //BL4模式
assign addr_step = 4'd4;
always@(posedge clk or posedge rst)
begin
if(rst)
//复位,全部输出归零
begin
app_en <= 1'b0;
app_cmd <= 3'b000;
app_wdf_wren <= 1'b0;
app_wdf_end <= 1'b0;
app_wdf_mask <= {APP_MASK_WIDTH{1'b0}};
app_burst <= 1'b0;
app_addr <= {ADDR_WIDTH{1'b0}}; //{}是拼接符,这里表示ADDR_WIDTH宽带的位数全为零
app_wdf_data <= {APP_DATA_WIDTH{1'b0}};
end
// ----- 写命令:cnt=10 时触发 -----
else if(app_rdy && app_wdf_rdy && init_calib_complete && cnt==8'd10)
begin
app_en <= 1'b1; // ☆ 告诉IP核:"执行命令!"
app_cmd <= 3'b000; // ☆ 000 = 写操作
app_wdf_wren <= 1'b1; // ☆ "写数据有效"
app_wdf_end <= 1'b1; // ☆ "这是最后一个写数据"
// BL4 只发一次 64 位数据,所以这既是第一个也是最后一个
app_wdf_mask <= {APP_MASK_WIDTH{1'b0}}; // 8个字节全写,不屏蔽
app_burst <= 1'b0; // 0 = BL4
app_addr <= reg_addr; // 当前地址
app_wdf_data <= mem_data[mem_index]; // 从预存数据里选一组发出去
end
// ----- 读命令:cnt=70 时触发 -----
else if(app_rdy && init_calib_complete && cnt==8'd70)
begin
app_en <= 1'b1; // ☆ "执行命令!"
app_cmd <= 3'b001; // ☆ 001 = 读操作
app_wdf_wren <= 1'b0; // 读的时候不写数据
app_wdf_end <= 1'b0;
app_wdf_mask <= {APP_MASK_WIDTH{1'b0}};
app_burst <= 1'b0; // BL4
app_addr <= reg_addr; // 同一个地址(先写后读同一个地址)
app_wdf_data <= {APP_DATA_WIDTH{1'b0}}; // 读的时候写数据通道不用,全0
end
// ---------- 其他时刻:什么都不做 ----------
else
begin、
app_en <= 1'b0;
app_cmd <= 3'b000;
app_wdf_wren <= 1'b0;
app_wdf_end <= 1'b0;
app_wdf_mask <= {APP_MASK_WIDTH{1'b0}};
app_burst <= 1'b0;
app_addr <= {ADDR_WIDTH{1'b0}};
app_wdf_data <= {APP_DATA_WIDTH{1'b0}};
end
end
// ====== 错误检测 always 块 ======
// 第一级:比较读回来的数据和预存数据
always@(posedge clk or posedge rst)
begin
if(rst)
begin
error_d[0] <= 1'b0; // 复位时清0:没有错误
error_flag_d[0] <= 1'b1; //没有错误为1 做对照
end
else if(app_rd_data_valid & (app_rd_data !== mem_data[mem_index])) //读数据有效且,读回来的 ≠ 写进去的
begin
error_d[0] <= 1'b1;
error_flag_d[0] <= 1'b1;
end
else
begin
error_d[0] <= error_d[0];
error_flag_d[0] <= 1'b0;
end
end
//二级寄存器等一拍消除毛刺
always@(posedge clk or posedge rst)
begin
if(rst)
begin
error_d[1] <= 1'b0;
error_flag_d[1] <= 1'b0;
end
else
begin
error_d[1] <= error_d[0];
error_flag_d[1] <= error_flag_d[0];
end
end
end //app_burst_4
//BL8模式
else if(BURST_MODE == "8") begin: app_burst_8
assign addr_step = 4'd8; // BL8 模式下,每次操作传输 8 拍数据
always@(posedge clk or posedge rst)
begin
if(rst)
begin
// ---------- 复位 ----------
app_en <= 1'b0;
app_cmd <= 3'b000;
app_wdf_wren <= 1'b0;
app_wdf_end <= 1'b0;
app_wdf_mask <= {APP_MASK_WIDTH{1'b0}};
app_burst <= 1'b0;
app_addr <= {ADDR_WIDTH{1'b0}};
app_wdf_data <= {APP_DATA_WIDTH{1'b0}};
end
// ----- cnt=10:第一次写数据 -----
else if(app_rdy && app_wdf_rdy && init_calib_complete && cnt==8'd10)
begin
app_en <= 1'b1;
app_cmd <= 3'b000; // 写
app_wdf_wren <= 1'b1;
app_wdf_end <= 1'b0; // ⚠️ 不是最后一个!还有下一拍
app_wdf_mask <= {APP_MASK_WIDTH{1'b0}};
app_burst <= 1'b0;
app_addr <= reg_addr;
app_wdf_data <= mem_data[mem_index]; // 第一半:用当前索引的数据
end
// ----- cnt=11:第二次写数据(紧接上一拍)-----
else if(app_rdy && app_wdf_rdy && init_calib_complete && cnt==8'd11)
begin
app_en <= 1'b1;
app_cmd <= 3'b000; // 写
app_wdf_wren <= 1'b1;
app_wdf_end <= 1'b1; // ☆ 这拍是最后一个!
app_wdf_mask <= {APP_MASK_WIDTH{1'b0}};
app_burst <= 1'b0;
app_addr <= reg_addr; // 同一个地址
app_wdf_data <= mem_data[mem_index+1'b1]; // 第二半:用索引+1的数据
end
// ----- cnt=70:读命令(和 BL4 一样)-----
else if(app_rdy && init_calib_complete && cnt==8'd70)
begin
app_en <= 1'b1;
app_cmd <= 3'b001; // 读
app_wdf_wren <= 1'b0;
app_wdf_end <= 1'b0;
app_wdf_mask <= {APP_MASK_WIDTH{1'b0}};
app_burst <= 1'b0;
app_addr <= reg_addr;
app_wdf_data <= {APP_DATA_WIDTH{1'b0}};
end
else
begin
// ---------- 其他时刻 ----------
app_en <= 1'b0;
app_cmd <= 3'b000;
app_wdf_wren <= 1'b0;
app_wdf_end <= 1'b0;
app_wdf_mask <= {APP_MASK_WIDTH{1'b0}};
app_burst <= 1'b0;
app_addr <= {ADDR_WIDTH{1'b0}};
app_wdf_data <= {APP_DATA_WIDTH{1'b0}};
end
end
//-------
always@(posedge clk or posedge rst)
begin
if(rst)
begin
error_d[0] <= 1'b0;
error_flag_d[0] <= 1'b0;
end
else if((app_rd_data_valid & !app_rd_data_end & (app_rd_data !== mem_data[mem_index])) ||
(app_rd_data_valid & app_rd_data_end & (app_rd_data !== mem_data[mem_index+1'b1])))
/*----- 两种情况,任意一种出错都算 -----
情况1:第一拍数据(app_rd_data_end=0)
app_rd_data_valid=1 且 不是最后一拍 且 数据不匹配
情况2:第二拍数据(app_rd_data_end=1)
app_rd_data_valid=1 且 是最后一拍 且 数据不匹配*/
begin
error_d[0] <= 1'b1;
error_flag_d[0] <= 1'b1;
end
else
begin
error_d[0] <= error_d[0];
error_flag_d[0] <= 1'b0;
end
end
always@(posedge clk or posedge rst)
begin
if(rst)
begin
error_d[1] <= 1'b0;
error_flag_d[1] <= 1'b0;
end
else
begin
error_d[1] <= error_d[0];
error_flag_d[1] <= error_flag_d[0];
end
end
end//app_burst_8
endgenerate
////////////////////////////////////////////////////////////
assign error = error_d[1];
assign error_flag = error_flag_d[1];
endmodule2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
`timescale 1ps /1ps
module tb();
reg clk,rst_n; //输入
wire ddr_cs; //输出
wire ddr_ras;
wire ddr_cas;
wire ddr_we;
wire ddr_ck;
wire ddr_ck_n;
wire ddr_cke;
wire ddr_odt;
wire ddr_reset_n;
wire [2-1:0] ddr_dm; //DM_WIDTH=2
wire [16-1:0] ddr_dq; //DQ_WIDTH=16
wire [2-1:0] ddr_dqs; //DQS_WIDTH=2
wire [2-1:0] ddr_dqs_n;
wire [14-1:0] ddr_addr; //ROW_WIDTH=14
wire [3-1:0] ddr_bank; //BANK_WIDTH=3
wire [3:0] led;
wire init_calib_complete;
wire error;
wire error_flag;
wire running;
GSR GSR(.GSRI(1'b1));
//顶层实例化
ddr3_syn_top u_top(
.clk(clk),
.rst_nn(rst_n),
.ddr_addr(ddr_addr),
.ddr_bank(ddr_bank),
.ddr_cs(ddr_cs),
.ddr_ras(ddr_ras),
.ddr_cas(ddr_cas),
.ddr_we(ddr_we),
.ddr_ck(ddr_ck),
.ddr_ck_n(ddr_ck_n),
.ddr_cke(ddr_cke),
.ddr_odt(ddr_odt),
.ddr_reset_n(ddr_reset_n),
.ddr_dm(ddr_dm),
.ddr_dq(ddr_dq),
.ddr_dqs(ddr_dqs),
.ddr_dqs_n(ddr_dqs_n),
.led(led)
);
assign init_calib_complete = ~led[0];
assign running = led[1];
assign error = ~led[2];
assign error_flag = ~led[3];
ddr3_model ddr3_model(
.rst_n (ddr_reset_n),
.ck (ddr_ck),
.ck_n (ddr_ck_n),
.cke (ddr_cke),
.cs_n (ddr_cs),
.ras_n (ddr_ras),
.cas_n (ddr_cas),
.we_n (ddr_we),
.dm_tdqs (ddr_dm),
.ba (ddr_bank),
.addr (ddr_addr),
.dq (ddr_dq),
.dqs (ddr_dqs),
.dqs_n (ddr_dqs_n),
.tdqs_n (),
.odt (ddr_odt)
);
always #10000 clk = ~clk; //每10000ps翻转一次 周期为50Mhz
initial begin
$fsdbDumpfile("tb.fsdb");
$fsdbDumpvars;
end
initial
begin
clk = 0;
rst_n = 1; //初始拉高复位 复位低电平有效
#2400000 //unit ps
rst_n = 0;
#480000 //unit ps
rst_n = 1;
#2000000000;//unit ps, 2ms
$finish;
end
endmodule2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
程序说明:
- ddr3_syn_top.v:rPPL 输出的频率(CLK OUT)的是 200 Mhz,这是给 DDR 芯片的记忆时钟(memory clk)不用管,而 clk_x1 是给我们的用户时钟,频率是 100 MHz, DDR3_test 模块中的所有时钟用的都是 clk_x1
- DDR3_test.v:generate 是条件编译,意思是会根据条件选择性编译符合条件的内容,不管是BL4还是BL8都有‘打两拍的操作,作用主要是为了稳定和消除亚稳态。而我们这里用的是 BL4 模式,BL8 会被条件编译给忽略,所以不用管。
5、仿真
可以从图中看到 init_calib_complete 信号 ,app_en 信号和 app_wdf_en 信号均为1,说明初始化没问题
从图中可以看到 app_wdf_wren 拉高时 app_wdf_data 不为 0 ,其他时候为 0 ,且 error 信号一直为 0 说明读数据没问题
需要注意的是只有当 app_wdf_wren / app_rd_data_valid 信号拉高时才会进行 读/写 操作 其余时间都为0
用大白话总结一下:
6、 I/O 引脚绑定
在仿真验证后,接下来使用gowin软件对时钟约束和引脚进行分配并上板验证
引脚分配如图:
由于 DDR3 芯片所设计的引脚绑定太多,为了节省大家的时间,可以在我们的资料包里面找到现成的分好类的 .cst 引脚约束文件
具体位置在我们给的百度网盘的 04 配套程序 -- 立创逻辑派FPGA_G1_IO.cst 中,大家可以直接选中 DDR3 中的所有内容复制粘贴到自己的 .cst 文件当中 粘贴后保存再重新布局布线即可。
7、 程序下载
连接开发板的下载器,下载码流文件到开发板里面
下载完成之后,如果程序初始话正常且系统正常的话,LED0 会常亮,LED1 1Hz闪烁=系统正常
assign led[0] = ~init_calib_complete; // 常亮=DDR3初始化成功
assign led[1] = running; // 1Hz闪烁=系统正常
//assign led[0] = error; //无错亮
//assign led[1] = error_flag; //F14 注释掉:出错脉冲闪2
3
4
根据我们的代码,验证完系统正常后如果想验证数据读写是否有误,在顶层模块中把前两行注释掉,然后取消后两行的注释看相应的 led 灯是否亮即可
如注释所示,如果读写正确,则 led 0 会亮
如果出错,则 led 0 会灭 led 1 会有脉冲闪
注意:逻辑派FPGA-G1的LED是低电平亮