一、前置知识
RAM 的英文全称是 Random Access Memory,即随机存取存储器,可以把数据写入任一指定地址的存储单元,也可以从任一指定地址中读出数据,其读写速度是由时钟频率决定的。
RAM分为静态随机存储器SRAM和动态随机存储器DRAM。SRAM访问速度快但价格较贵;DRAM访问速度较慢但价格便宜,存储容量大。片上SRAM,板载DRAM。
利用Cyclone IV器件内部的M9K存储器模块,可以实现包括RAM、移位寄存器、ROM以及FIFO等各种存储器功能。
EP4CE10片上RAM资源容量414Kbits(Cyclone IV中文手册):
单端口RAM:只有一组端口,读写数据不能同时进行,读写公用数据通道。
伪双端口RAM:有两组端口,一组端口用来读,另一组端口负责写。
真双端口RAM:有两组端口,两组端口都可以用来读或写。
二、单端口RAM
2.1 原理分析
单端口RAM端口示例图如下:
端口 | 功能 |
---|---|
data[] | RAM写数据端口 |
address[] | RAM读写地址端口,单端口RAM共用读写地址端口 |
wren | 写使能信号,高电平有效 |
byteena[] | 字节使能控制,仅写入指定字节,非指定的字节数据不变 |
addressstall | 地址使能控制,高电平时有效地址时钟使能会保持之前的地址 |
inclocken | 输入时钟使能控制,高电平有效 |
rden | 读使能,高电平有效 |
aclr | 异步复位,高电平有效 |
inclock/outclock | 单口 RAM 端口支持输入与输出时钟模式和单时钟模式 |
2.2 创建 RAM IP 核
2.2.1 创建工程
2.2.2 创建 IP 核
在IP Catalog里搜索RAM: 1-PORT
并创建。
添加路径为X:\FPGA\Projects\09_ip_ram\par\ipcore
即创建par\ipcore
文件夹。文件命名为ram_1port.v
。
2.2.3 配置 IP 核
- 配置输出位宽、存储器容量、块类型、时钟模式
- q输出位宽设置为8位(默认)
- 存储器容量设置为32字
- 存储块类型设置为自动(默认)
- 时钟模式设置为单时钟模式(默认)
- 配置寄存器、异步复位、读使能
- 取消勾选
'q' output port
:选中会导致输出的数据延迟一个时钟周期。 - 不选择
Create an 'aclr' asynchronous for ... ports
:该端口用于复位清零RAM,本次不需要。 - 勾选
Create a 'rden' read enable signal
:创建读使能端口,用于控制数据输出,高电平有效。
- 写时读设置
Read-During-Write控制在写数据时,读出数据在输出端口应该输出什么。
- New Data 表示在写数据的同一个上升沿,新数据可读。(选此默认项)
- Don’t Care 表示输出不确定。
- 初始化RAM数据
该配置页面用来配置RAM内部的初始化数据,若不需要初始数据就选择No, leave it blank
即可。
在配置ROM时,也会使用该RAM的IP核,那时就必须进行数据初始化了。
- EDA配置
该页面用来设置是否生成在第三方EDA工具中调试时用到的网表文件,并且提示了在仿真时需要使用到的库文件为altera_mf.v
文件。
- 生成文件
该页面选择需要生成的与IP核相关的文件。
在par/ipcore
目录下生成了这四个文件:
2.2.4 IP核代码分析
以下是截取自 IP 核文件ram_1port.v
的一段代码。
module ram_1port (
address,
clock,
data,
rden,
wren,
q);
input [4:0] address;
input clock;
input [7:0] data;
input rden;
input wren;
output [7:0] q;
- address:RAM读写的公共地址。
- clock:读写驱动时钟。
- data:写入的数据
- rden:读数据使能,高电平有效
- wren:写数据使能,高电平有效
- q:读出的数据
需要写入数据时,将wren写使能信号拉高,再给出写入的数据data和写入RAM的地址address即可。
需要读出数据时,将rden读使能信号拉高,再给出读取RAM的地址address,数据会被读到q端口。
2.3 读写测试模块
编写ram_rw
模块测试RAM的读写操作。
module ram_rw(
input clk,
input rst_n,
input [7:0] ram_rd_data,
output reg [4:0] ram_address,
output reg [7:0] ram_wr_data,
output reg ram_wr_en,
output reg ram_rd_en
);
reg [5:0] count;
// 写时序0~31,读时序32~63
assign ram_wr_en = (count>=0 && count<=31) ? 1'b1 : 1'b0;
assign ram_rd_en = (count>=0 && count<=31) ? 1'b0 : 1'b1;
// 计数器循环计数0~63
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
count <= 6'd0;
else if(count == 6'd63)
count <= 6'd0;
else
count <= count + 1'b1;
end
// 生成写入的数据ram_wr_data:0~31
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
ram_wr_data <= 8'd0;
else if(ram_wr_en)
ram_wr_data <= ram_wr_data + 1'b1;
else
ram_wr_data <= 8'd0;
end
// 生成读写地址ram_address:0~31
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
ram_address <= 5'd0;
else if(ram_address == 5'd31)
ram_address <= 5'd0;
else
ram_address <= ram_address + 1'b1;
end
endmodule
以上代码实现的功能如下:
- 计数器循环计数:0~63周期
- 控制读写使能:0~31个周期内写使能;32~63个周期内读使能
- 写使能时(0~31),生成待写入的数据ram_wr_data,范围0~31
- 读使能时(32~63),读地址为0~31,读出数据的过程需要IP核处理
- 读写地址ram_address在0~31之间循环
编写顶层模块进行IP核与读写模块的测试:
module ip_ram(
input clk,
input rst_n
);
wire [7:0] ram_rd_data;
wire [4:0] ram_address;
wire [7:0] ram_wr_data;
wire ram_wr_en;
wire ram_rd_en;
ram_1port u_ram_1port(
.address(ram_address),
.clock(clk),
.data(ram_wr_data),
.rden(ram_rd_en),
.wren(ram_wr_en),
.q(ram_rd_data)
);
ram_rw u_ram_rw(
.clk(clk),
.rst_n(rst_n),
.ram_rd_data(ram_rd_data),
.ram_address(ram_address),
.ram_wr_data(ram_wr_data),
.ram_wr_en(ram_wr_en),
.ram_rd_en(ram_rd_en)
);
endmodule
2.4 仿真
先编写仿真激励文件,我这里直接用生成的ip_ram.vt
模板修改。
// Generated on "04/26/2022 14:34:15"
// Verilog Test Bench template for design : ip_ram
//
// Simulation tool : ModelSim-Altera (Verilog)
//
`timescale 1 ns/ 1 ns
module ip_ram_vt();
// constants
// general purpose registers
// reg eachvec;
// test vector input registers
reg clk;
reg rst_n;
// wires
parameter T = 20;
// assign statements (if any)
ip_ram i1 (
// port map - connection between master ports and signals/registers
.clk(clk),
.rst_n(rst_n)
);
initial begin
clk = 1'b0;
rst_n = 1'b0;
#(5*T)
rst_n = 1'b1;
#(200*T) $stop;
end
always begin
#(T/2) clk = ~clk;
end
endmodule
仿真结果如下:
上述图中可以看出:
- 读写使能信号交替使能
- 在读使能时可以读到数据,在写使能时读到的数据值不变
- 在写使能时,ram_wr_data写数据端口生成了数据;在读使能时,ram_wr_data端口数据清零
- 读写地址里的数据一直在改变
再看一下时序图的细节(从第0ns开始):
可以看到,ram_wr_data写数据端口的数据是与预期效果一样的从0~31,但是ram_address在增加到15之后下一个数据出现了负数,是因为在选择显示内容时数制选成了有符号的十进制,最高位被当成了符号位。改成无符号数制就好了,如下。
上图可以看到,ram_rd_data读出来的数据有一位延迟,即在第N位地址读到的数据是N-1
三、双端口RAM
3.1 原理分析
- 创建双端口RAM模块(IP核)和PLL锁存器模块(IP核)
- 分别创建读写RAM模块
- PLL模块产生两个时钟源(写时钟和读时钟)
- 写RAM模块用来产生数据,输入到双端口RAM IP模块;读RAM模块用来读取双端口RAM IP模块的数据
- 写一次数据,然后循环读取数据,验证数据正确性
最终程序编写完成后生成的RTL视图:
09_ip_ram_2port.pdf
3.2 创建 RAM IP 核
3.2.1 创建工程
3.2.2 创建 IP 核
保存路径:X:/FPGA/Projects/09_ip_ram_2port/par/ipcore/ram_2port/ram_2port.v
3.2.3 配置 IP 核
- 通用配置
- 选择简单双端口(伪双端口)类型
- 存储单位选择默认的字存储
- 位宽设置
- 设置存储深度为32字
- 配置输入输出位宽均为8位
- 时钟和使能信号
- 创建读写双时钟信号
- 创建读使能位
- 寄存器和异步清零
- 不创建q输出寄存器
- 不创建读写时钟的使能信号
- 不创建异步复位信号aclr
- 初始化RAM
不进行RAM初始化
- 仿真依赖文件
- 生成的文件
3.3 创建 PLL IP 核
3.3.1 创建 IP 核
保存路径:X:/FPGA/Projects/09_ip_ram_2port/par/ipcore/pll_clk/pll_clk.v
3.3.2 配置 IP 核
- 通用设置
- 输入时钟50MHz
- 常规模式
- 输入和锁信号
- 创建异步复位信号
- 创建locked保持稳定信号
- 使能时钟 | 时钟名 | 频率 | 角度 | 占空比 | | —- | —- | —- | —- | | c0 | 50MHz | 0deg | 50% | | c1 | 25MHz | 0deg | 50% |
- 仿真所需文件
- 生成的文件
3.4 模块代码编写
3.4.1 写模块代码
module ram_wr(
input clk,
input rst_n,
output ram_wr_en,
output reg [7:0] ram_wr_data,
output reg [4:0] ram_wr_address
);
reg [5:0] counter;
assign ram_wr_en = (counter>=6'd0 && counter<=6'd31) ? 1'b1 : 1'b0;
// 计时器模块:0~63自增,到63后维持状态
always @(posedge clk or negedge rst_n) begin
if(rst_n)
counter <= 6'd0;
else if(counter == 6'd63)
counter <= counter;
else
counter <= counter + 1'b1;
end
// 写入使能状态下生成待写入的数据
always @(posedge clk or negedge rst_n) begin
if(rst_n)
ram_wr_data <= 8'd0;
else if(ram_wr_en)
ram_wr_data <= ram_wr_data + 1'b1;
else
ram_wr_data <= 8'd0;
end
// 写入使能状态下生成待写入的地址
always @(posedge clk or negedge rst_n) begin
if(rst_n)
ram_wr_address <= 8'd0;
else if(ram_wr_en)
ram_wr_address <= ram_wr_address + 1'b1;
else
ram_wr_address <= 8'd0;
end
endmodule
3.4.2 读模块代码
module ram_rd(
input clk,
input rst_n,
input [7:0] ram_rd_data,
output ram_rd_en,
output reg [4:0] ram_rd_address
);
reg [5:0] counter;
assign ram_rd_en = (counter>=6'd0 && counter<=6'd31) ? 1'b1 : 1'b0;
// 计时器模块:0~63循环
always @(posedge clk or negedge rst_n) begin
if(rst_n)
counter <= 6'd0;
else if(counter == 6'd63)
counter <= 6'd0;
else
counter <= counter + 1'b1;
end
// 读使能状态下向双端口RAM模块输出读地址
always @(posedge clk or negedge rst_n) begin
if(rst_n)
ram_rd_address <= 8'd0;
else if(ram_rd_en)
ram_rd_address <= ram_rd_address + 1'b1;
else
ram_rd_address <= 8'd0;
end
endmodule
3.4.3 顶层模块代码
module ip_ram_2port(
input sys_clk,
input sys_rst_n
);
wire rst_n ;
wire wr_clk_50m ;
wire rd_clk_25m ;
wire locked ;
wire ram_wr_en ;
wire ram_wr_data ;
wire ram_wr_address ;
wire ram_rd_data ;
wire ram_rd_en ;
wire ram_rd_address ;
assign rst_n = sys_rst_n & locked;
pll_clk u_pll_clk(
.areset (~sys_rst_n),
.inclk0 (sys_clk ),
.c0 (wr_clk_50m),
.c1 (rd_clk_25m),
.locked (locked )
);
ram_wr u_ram_wr(
.clk (wr_clk_50m ),
.rst_n (rst_n ),
.ram_wr_en (ram_wr_en ),
.ram_wr_data (ram_wr_data ),
.ram_wr_address (ram_wr_address)
);
ram_2port u_ram_2port (
.data (ram_wr_data ),
.rdaddress (ram_rd_address),
.rdclock (rd_clk_25m ),
.rden (ram_rd_en ),
.wraddress (ram_wr_address),
.wrclock (wr_clk_50m ),
.wren (ram_wr_en ),
.q (ram_rd_data )
);
ram_rd u_ram_rd(
.clk (rd_clk_25m ),
.rst_n (rst_n ),
.ram_rd_data (ram_rd_data ),
.ram_rd_en (ram_rd_en ),
.ram_rd_address (ram_rd_address)
);
endmodule
3.5 程序仿真
编写仿真激励文件,首先将ip_ram_2port
设置成顶层模块,然后直接使用模板修改即可。
// Copyright (C) 2019 Intel Corporation. All rights reserved.
// Your use of Intel Corporation's design tools, logic functions
// and other software and tools, and any partner logic
// functions, and any output files from any of the foregoing
// (including device programming or simulation files), and any
// associated documentation or information are expressly subject
// to the terms and conditions of the Intel Program License
// Subscription Agreement, the Intel Quartus Prime License Agreement,
// the Intel FPGA IP License Agreement, or other applicable license
// agreement, including, without limitation, that your use is for
// the sole purpose of programming logic devices manufactured by
// Intel and sold by Intel or its authorized distributors. Please
// refer to the applicable agreement for further details, at
// https://fpgasoftware.intel.com/eula.
// *****************************************************************************
// This file contains a Verilog test bench template that is freely editable to
// suit user's needs .Comments are provided in each section to help the user
// fill out necessary details.
// *****************************************************************************
// Generated on "04/28/2022 13:39:08"
// Verilog Test Bench template for design : ip_ram_2port
//
// Simulation tool : ModelSim-Altera (Verilog)
//
`timescale 1 ns/ 1 ns
module ip_ram_2port_vt();
// constants
// general purpose registers
//reg eachvec;
// test vector input registers
reg sys_clk;
reg sys_rst_n;
// wires
parameter T = 20;
// assign statements (if any)
ip_ram_2port i1 (
// port map - connection between master ports and signals/registers
.sys_clk(sys_clk),
.sys_rst_n(sys_rst_n)
);
initial
begin
// code that executes only once
// insert code here --> begin
sys_clk = 1'b0;
sys_rst_n = 1'b0;
#(5*T) sys_rst_n = 1'b1;
#(200*T) $stop;
// --> end
$display("Running testbench");
end
always
// optional sensitivity list
// @(event1 or event2 or .... eventn)
begin
// code executes for every event on sensitivity list
// insert code here --> begin
#(T/2) sys_clk = ~sys_clk;
@eachvec;
// --> end
end
endmodule
创建ModelSim工程,工程名为ip_ram_2port
,工程路径D:/Projects/FPGA/Projects/09_ip_ram_2port/sim/tb
。
添加文件:
- ram_2port.v:X:/FPGA/Projects/09_ip_ram_2port/par/ipcore/ram_2port/ram_2port.v
- pll_clk.v:X:/FPGA/Projects/09_ip_ram_2port/par/ipcore/pll_clk/pll_clk.v
- ip_ram_2port.v:X:/FPGA/Projects/09_ip_ram_2port/rtl/ip_ram_2port.v
- ram_rd.v:X:/FPGA/Projects/09_ip_ram_2port/rtl/ram_rd.v
- ram_wr.v:X:/FPGA/Projects/09_ip_ram_2port/rtl/ram_wr.v
- ip_ram_2port.vt:X:/FPGA/Projects/09_ip_ram_2port/par/simulation/modelsim/ip_ram_2port.vt
- altera_mf.v:D:/Quartus/install/quartus/eda/sim_lib/altera_mf.v
仿真之后的波形如下:
可以看到数据和地址都只是方波01的形式,与预期的0~31效果不一致。
原因是在顶层模块ip_ram_2port.v
文件中,定义的线束类型忘记加位宽了。。
应修改成如下部分:
wire rst_n ;
wire wr_clk_50m ;
wire rd_clk_25m ;
wire locked ;
wire ram_wr_en ;
wire [7:0] ram_wr_data ;
wire [4:0] ram_wr_address ;
wire ram_rd_en ;
wire [7:0] ram_rd_data ;
wire [4:0] ram_rd_address ;
重新编译后再次仿真:
波形大体上看过去已经没问题了,读写使能及数据都可以正常读写。
看一下读写的地址和数据,也与预期效果一致。