文章目录
一、sdram
同步动态随机存取内存(synchronous dynamic random-access memory,简称SDRAM)是有一个同步接口的动态随机存取内存(DRAM)。
二、看sdram手册找关键
1.描述
有4个bank,1个bank是8192行512列,1列是1个存储单元,1个存储单元是16bit,也就是1个sdram的数据总量是4 * 8192 * 512 * 16bit
需要在64ms内刷新8192行,这里我们设置接口的时候就行刷新,在7.8us内刷新1行
突发长度:
1,2,4,8或者全页突发(512)个数据
2.接口信号
这是sdram需要的接口(就是我们sdram接口给sdram要输出的)
信号 | 类型 | 位宽 | 说明 |
---|---|---|---|
clk | input | 100MHZ时钟 | |
cke | input | 时钟使能 | |
cs_n | input | 片选信号 | |
bank | input | 2 | bank地址 |
address | input | 13 | 行地址 |
ras_n | input | ras | |
cas_n | input | cas | |
we_n | input | we | |
dqm | input | 2 | 数据掩码 |
dq | inout | 16 | 数据 |
3.关键的时间
这里有几个参数需要注意一下
名称 | 时间 | 说明 |
---|---|---|
tRRC | >60ns(6个周期) 一般取7个周期 | 自动刷新需要等待的时间 |
tRP | >18ns(2个周期) 一般取3个周期 | 预充电需要等待的时间 |
tRCD | >18ns(2个周期) 一般取3个周期 | 行激活需要等待的时间 |
TMRD | >2个周期 一般取3个周期 | 模式寄存器设置需要等待的时间 |
tDPL | >2个周期 就取2个周期 | 数据写入到预充电等待延时的时间 |
这里的时钟是100MHZ,所以我们对参数的设置要根据100MHZ的
4.模式寄存器设置
在模式寄存器设置状态的时候 是2个bank地址+13个行地址
突发长度BURST LENGTH看行地址A3A2A1A0
A2A1A0 | A3=0 | A3=1 | |
---|---|---|---|
3’b000 | 1 | 1 | 突发长度为1 |
3’b001 | 2 | 2 | 突发长度为2 |
3’b010 | 4 | 4 | 突发长度为4 |
3’b011 | 8 | 8 | 突发长度为8 |
3’b100 | X | X | |
3’b101 | X | X | |
3’b110 | X | X | |
3’b111 | Full Page | X | 突发长度为512(当A3=0) |
写延时CAS LATENCY
A6A5A4 | CAS LATENCY | |
---|---|---|
3’b010 | 2 | 延时2个周期 |
3’b011 | 3 | 延时3个周期 |
OP CODE
就常规的突发写突发读
A9 | |
---|---|
0 | 突发读突发写 |
1 | 突发读单字节写 |
5.各个状态不同的数据
各个指令 | CS,RAS,CAS,WE | ADDR[12:0] + A10 +BA[1:0] |
---|---|---|
Mode Register Set | 4’b0000 | BA(2’b00) + 3’b000,OP Code,2’b00,CAS Latency,BT,Burst Length |
Bank Active | 4’b0011 | BA(2’b00) + Row Address(addr[21:9]) |
Read | 4’b0101 | BA(2’b00) + A10(1’b0) + Column Address(addr[8:0])(其余补齐0) |
Write | 4’b0100 | BA(2’b00) + A10(1’b0) + Column Address(addr[8:0])(其余补齐0) |
Precharge All Banks | 4’b0010 | A10(11’b100_0000_0000) |
Auto Refresh | 4’b0001 | |
No Operation | 4’b0111 |
6.手册里的状态机
这是手册里的状态图,我们只需要红色的状态,然后写自己的状态图
- WAIT
- PRECH
- MOREST
- IDLE
- AUREF
- ROWACT
- READ
- WRITE
7.写时序
8.写到预充电时序
写到预充电有一个tDPL的时间
9.读时序
这张图告诉我们在read的时候要延长2-3个周期才能读到数据,所以对于输出有效要延长2-3个周期
10.电源启动
上电之后要等待200us
11.初始化的刷新
第一次初始化的时候刷新要刷新8次刷新等待时间 8*tREF
三、状态机设计
1.sdram接口状态机
这个状态机看上去很麻烦,确实,因为要初始化要刷新
-
未初始化:PRECHARGE -->MODE REGISTER SET -->IDLE
-
初始化完成刷新时间到或者刷新标志挂载 :IDLE–> AUTO REFRESH AUTO REFRESH --> IDLE
-
初始化完成刷新时间到或者刷新标志挂载 : PRECHARGE --> AUTO REFRESH --> IDLE
-
初始化完成未到刷新时间:PRECHARGE --> IDLE
四、代码实现sdram接口
1.sdram_interface.v
module sdram_interface(
input clk,
input sclk,
input rst_n,
//sdram接口和control
input [15:0] din,// 从wrfifo来的数据
output [15:0] dout,// 给rdfifo的数据
output dout_vld,
input [23:0] brc_address,// bank row col 地址 2+13+9
input rd_req,// 读请求
input wr_req,// 写请求
output busy,// 接口busy
output ack,// 接口回应
//sdram接口和sdram
output sdram_clk,
output sdram_cke,
output sdram_cs_n,
output [1:0] sdram_bank,
output [12:0] sdram_address,
output sdram_ras_n,
output sdram_cas_n,
output sdram_we_n,
output [1:0] sdram_dqm,
//单总线dq三态门
input [15:0] sdram_dq_in,
output [15:0] sdram_dq_out,
output sdram_dq_out_en
);
parameter MODE_BURST = 3'b011,// A2A1A0
BURST_TYPE = 1'b0,// A3
CAS_LATENCY = 3'b010,// 模式寄存器延时
OP_CODE = 1'b0;// 突发读和突发写
parameter BURST_LENGTH = 8;// 突发长度
// cs ras cas we
parameter MRS_CMD = 4'b0000,// 模式寄存器选择命令
BA_CMD = 4'b0011,// bank激活命令
READ_CMD = 4'b0101,// 读命令
WRITE_CMD = 4'b0100,// 写命令
PAB_CMD = 4'b0010,// 预充电命令
AF_CMD = 4'b0001,// 自刷新命令
NOOP_CMD = 4'b0111;// 没有操作的命令
// 时间都是按照周期来算的
// sdram是100MHZ 1个周期是10ns
parameter POWAIT_TIME = 20000 ,// 开启电源等待时间200us=200000ns
TRRC = 7 ,// 刷新等待时间>60ns 70ns
TRP = 3 ,// 预充电等待时间>18ns 30ns
TRCD = 3 ,// 行激活等待时间>18ns 30ns
TMRD = 3 ,// 寄存器模式设置等待时间>2clk 3clk 30ns
TDPL = 2 ,// 数据写入到预充电等待延时的时间>2clk 2clk
AUFRESH_TIME = 780 ,// 每7.8us自动刷新
CAS_TIME = 2 ;// 读延时时间2clk
// 状态机
localparam POWAIT = 8'b0000_0001,// 上电等待状态
PRECH = 8'b0000_0010,// 预充电状态
MDREST = 8'b0000_0100,// 模式寄存器设置状态
IDLE = 8'b0000_1000,// 初始状态
AUREFR = 8'b0001_0000,// 自刷新状态
ROWACT = 8'b0010_0000,// 行激活状态
READ = 8'b0100_0000,// 读状态
WRITE = 8'b1000_0000;// 写状态
// 状态机
reg [7:0] state_c;
reg [7:0] state_n;
wire powait2prech ;
wire prech2idle ;
wire prech2aurefr ;
wire mdrest2idle ;
wire idle2aurefr ;
wire idle2rowact ;
wire aurefr2idle ;
wire aurefr2mdrest;
wire rowact2write ;
wire rowact2read ;
wire read2prech ;
wire write2prech ;
// 不同状态不同的计数器
reg [15:0] cnt;
wire add_cnt;
wire end_cnt;
reg [15:0] X;
// 自动刷新计数器
reg [9:0] cnt_ref;
wire add_cnt_ref;
wire end_cnt_ref;
// 初始化标志 0代表初始化未完成 1代表初始化完成
reg init_flag;
// 自动刷新标志 0代表刷新时间未到 1代表要刷新
reg refresh_flag;
// 读写标志
reg rd_flag;
reg wr_flag;
// command = cs+ras+cas+we
reg [3:0] command;
// address = bank + 13位a
reg [14:0] address;
// dq_mask数据掩码
reg [1:0] dq_mask;
// 三态门
reg [15:0] dq_out;
reg dq_out_en;
reg [15:0] dq_in;
// 打两拍的dout_vld
reg dout_vld_r0;
reg dout_vld_r1;
reg dout_vld_r2;
// 接口响应
reg interface_ack;
// 状态机
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
state_c <= POWAIT;
end
else begin
state_c <= state_n;
end
end
always @(*)begin
case (state_c)
POWAIT :begin
if(powait2prech)begin
state_n = PRECH;
end
else begin
state_n = state_c;
end
end
PRECH :begin
if(prech2idle)begin
state_n = IDLE;
end
else if(prech2aurefr)begin
state_n = AUREFR;
end
else begin
state_n = state_c;
end
end
MDREST :begin
if(mdrest2idle)begin
state_n = IDLE;
end
else begin
state_n = state_c;
end
end
IDLE :begin
if(idle2rowact)begin
state_n = ROWACT;
end
else if(idle2aurefr)begin
state_n = AUREFR;
end
else begin
state_n = state_c;
end
end
AUREFR :begin
if(aurefr2idle)begin
state_n = IDLE;
end
else if(aurefr2mdrest)begin
state_n = MDREST;
end
else begin
state_n = state_c;
end
end
ROWACT :begin
if(rowact2write)begin
state_n = WRITE;
end
else if(rowact2read)begin
state_n = READ;
end
else begin
state_n = state_c;
end
end
READ :begin
if(read2prech)begin
state_n = PRECH;
end
else begin
state_n = state_c;
end
end
WRITE :begin
if(write2prech)begin
state_n = PRECH;
end
else begin
state_n = state_c;
end
end
default: state_n = IDLE;
endcase
end
assign powait2prech = state_c == POWAIT && (end_cnt);// 上电等待200us结束
assign prech2idle = state_c == PRECH && (init_flag) && (end_cnt);// 初始化完成并且预充电等待30ns结束
assign prech2aurefr = state_c == PRECH && (~init_flag) && (end_cnt);// 初始化未完成并且预充电等待30ns结束
assign mdrest2idle = state_c == MDREST && (end_cnt);// 模式寄存器设置等待30ns结束
assign idle2aurefr = state_c == IDLE && ((refresh_flag) || (end_cnt_ref));// 刷新标志挂起或者刷新时间到
assign idle2rowact = state_c == IDLE && (rd_flag || wr_flag);// 读写标志
assign aurefr2idle = state_c == AUREFR && (init_flag) && (end_cnt);// 初始化完成并且自动刷新等待70ns
assign aurefr2mdrest = state_c == AUREFR && (~init_flag) && (end_cnt);// 初始化未完成并且自动刷新等待70ns
assign rowact2write = state_c == ROWACT && (end_cnt) && (wr_flag);// 写标志并且行激活等待30ns
assign rowact2read = state_c == ROWACT && (end_cnt) && (rd_flag);// 读标志并且行激活等待30ns
assign read2prech = state_c == READ && (end_cnt);// 读突发长度 + 读延迟结束
assign write2prech = state_c == WRITE && (end_cnt);// 写突发长度 + 写延迟结束
// 不同状态不同的计数器
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
cnt <= 0;
end
else if(add_cnt)begin
if(end_cnt)begin
cnt <= 0;
end
else begin
cnt <= cnt + 1;
end
end
else begin
cnt <= cnt;
end
end
assign add_cnt = (state_c != IDLE);
assign end_cnt = add_cnt && cnt == X - 1;
always @(*)begin
if(state_c == POWAIT)begin
X = POWAIT_TIME;
end
else if(state_c == PRECH)begin
X = TRP;
end
else if(state_c == MDREST)begin
X = TMRD;
end
else if((state_c == AUREFR) && (~init_flag))begin
X = 8*TRRC;
end
else if((state_c == AUREFR) && (init_flag))begin
X = TRRC;
end
else if(state_c == ROWACT)begin
X = TRCD;
end
else if(state_c == WRITE)begin
X = BURST_LENGTH + TDPL; // 突发长度 + 写到预充电延迟
end
else if(state_c == READ)begin
X = BURST_LENGTH + CAS_TIME;// 突发长度 + 读延迟
end
else begin
X = 0;
end
end
// 自动刷新计数器
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
cnt_ref <= 0;
end
else if(add_cnt_ref)begin
if(end_cnt_ref)begin
cnt_ref <= 0;
end
else begin
cnt_ref <= cnt_ref + 1;
end
end
else begin
cnt_ref <= cnt_ref;
end
end
assign add_cnt_ref = init_flag;
assign end_cnt_ref = add_cnt_ref && cnt_ref == AUFRESH_TIME - 1;
// 初始化标志init_flag
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
init_flag <= 0;
end
else if(mdrest2idle)begin
init_flag <= 1'b1;
end
end
// 自动刷新标志refresh_flag
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
refresh_flag <= 0;
end
else if((~refresh_flag) && (end_cnt_ref))begin // 计时结束自动刷新标志拉高
refresh_flag <= 1'b1;
end
else if((refresh_flag) && (idle2aurefr))begin
refresh_flag <= 1'b0;
end
end
// 读写标志
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
wr_flag <= 0;
end
else if(wr_req)begin
wr_flag <= 1'b1;
end
else if(write2prech)begin
wr_flag <= 1'b0;
end
end
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
rd_flag <= 0;
end
else if(rd_req)begin
rd_flag <= 1'b1;
end
else if(read2prech)begin
rd_flag <= 1'b0;
end
end
// command = cs+ras+cas+we
// 各个状态的cs ras cas we
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
command <= 0;
end
else if(aurefr2mdrest)begin
command <= MRS_CMD;
end
else if(idle2rowact)begin
command <= BA_CMD;
end
else if(rowact2read)begin
command <= READ_CMD;
end
else if(rowact2write)begin
command <= WRITE_CMD;
end
else if(powait2prech || read2prech || write2prech)begin
command <= PAB_CMD;
end
else if(prech2aurefr || idle2aurefr)begin
command <= AF_CMD;
end
else begin
command <= NOOP_CMD;
end
end
// address
// MSB高字节
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
address <= 0;
end
else if(aurefr2mdrest)begin
address <= {2'b00,3'b000,OP_CODE,2'b00,CAS_LATENCY,BURST_TYPE,MODE_BURST};
end
else if(idle2rowact)begin
address <= {brc_address[23:22],brc_address[21:9]};
end
else if(rowact2read)begin
address <= {brc_address[23:22],4'b0000,brc_address[8:0]};
end
else if(rowact2write)begin
address <= {brc_address[23:22],4'b0000,brc_address[8:0]};
end
else if(powait2prech || read2prech || write2prech)begin
address <= 1'b1 << 10;
end
end
// 数据掩码dqm
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
dq_mask <= 2'b00;
end
else if((state_c == WRITE) && (cnt == BURST_LENGTH - 1))begin// dq_mask在写数据的时候最后需要延长2个周期,所以最后两个周期不写数据
dq_mask <= 2'b11;
end
else if(write2prech)begin
dq_mask <= 2'b00;
end
end
// 三态门
// dq_out_en
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
dq_out_en <= 0;
end
else if(rowact2write)begin// 开始写就输出使能拉高
dq_out_en <= 1'b1;
end
else if((state_c == WRITE) && (cnt >= BURST_LENGTH - 1))begin// 写完数据后输出使能拉低
dq_out_en <= 1'b0;
end
end
// dq_in
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
dq_in <= 0;
end
else begin
dq_in <= sdram_dq_in;
end
end
// dout_vld输出数据有效
// 因为read延长2个周期 打两拍
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
dout_vld_r0 <= 0;
dout_vld_r1 <= 0;
dout_vld_r2 <= 0;
end
else begin
dout_vld_r0 <= (state_c == READ);
dout_vld_r1 <= dout_vld_r0;
dout_vld_r2 <= dout_vld_r1;
end
end
//interface_ack
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
interface_ack <= 0;
end
else if(rowact2read | rowact2write)begin
interface_ack <= 1'b1;
end
else if((state_c == WRITE && cnt == BURST_LENGTH-1) || read2prech)begin
interface_ack <= 1'b0;
end
end
// sdram串行时钟
assign sdram_clk = sclk;
// 时钟使能一直拉高
assign sdram_cke = 1'b1;
// cs ras cas we = command
assign {sdram_cs_n,sdram_ras_n,sdram_cas_n,sdram_we_n} = command;
// assign sdram_cs_n = command[3];
// assign sdram_ras_n = command[2];
// assign sdram_cas_n = command[1];
// assign sdram_we_n = command[0];
// bank地址
assign sdram_bank = address[14:13];
// 给sdram的地址数据
assign sdram_address = address[12:0];
// 给sdram的数据掩码
assign sdram_dqm = dq_mask;
// 三态门
assign sdram_dq_out = din;
assign sdram_dq_out_en = dq_out_en;
// 从sdram读到的数据
assign dout = dq_in;
assign dout_vld = dout_vld_r2;
// sdram接口busy
assign busy = (state_c != IDLE) ? 1:0;
// sdram接口相应ack
assign ack = interface_ack;
endmodule
五、仿真验证
这里使用了一个sdram的仿真模型
- 初始化
- 数据输入输出
六、quartus调用ip核实现sdram接口
打开platform designer
搜索sdram
根据上面的手册的参数去配置一下
创建完毕后连线,命名
user_port 用户接口
mem_port sdram接口
最后将生成的.qip文件导入到工程中
看看ip组件有无sdram_interface,有说明导入成功了
最后ip生成的sdram接口
使用的时候例化即可
七、总结
以上两种sdram接口的实现方式
- 自己手写一个sdram的接口,也就500行左右,虽然麻烦了点,但是自己懂得sdram的原理,自己日后使用接口的时候懂得如何去修改
- quartus调用一个sdram接口的ip核,直接一步到位,省去了很多的麻烦,但是日后调用的使用,例如以后要做的项目便是ov5640摄像头的时候就会出现很多问题,因为里面的原码也看不懂,所以难以理解里面的原理
我的sdram接口连续写了好几天,我真的很菜,只能跟着老师的代码修修改改,最后实现了,大致的原理也明白了。
下一步是sdram的读写控制,把sdram项目写完