一、前置知识
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~63assign ram_wr_en = (count>=0 && count<=31) ? 1'b1 : 1'b0;assign ram_rd_en = (count>=0 && count<=31) ? 1'b0 : 1'b1;// 计数器循环计数0~63always @(posedge clk or negedge rst_n) beginif(!rst_n)count <= 6'd0;else if(count == 6'd63)count <= 6'd0;elsecount <= count + 1'b1;end// 生成写入的数据ram_wr_data:0~31always @(posedge clk or negedge rst_n) beginif(!rst_n)ram_wr_data <= 8'd0;else if(ram_wr_en)ram_wr_data <= ram_wr_data + 1'b1;elseram_wr_data <= 8'd0;end// 生成读写地址ram_address:0~31always @(posedge clk or negedge rst_n) beginif(!rst_n)ram_address <= 5'd0;else if(ram_address == 5'd31)ram_address <= 5'd0;elseram_address <= ram_address + 1'b1;endendmodule
以上代码实现的功能如下:
- 计数器循环计数: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 nsmodule ip_ram_vt();// constants// general purpose registers// reg eachvec;// test vector input registersreg clk;reg rst_n;// wiresparameter 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 beginclk = 1'b0;rst_n = 1'b0;#(5*T)rst_n = 1'b1;#(200*T) $stop;endalways begin#(T/2) clk = ~clk;endendmodule
仿真结果如下:
上述图中可以看出:
- 读写使能信号交替使能
- 在读使能时可以读到数据,在写使能时读到的数据值不变
- 在写使能时,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) beginif(rst_n)counter <= 6'd0;else if(counter == 6'd63)counter <= counter;elsecounter <= counter + 1'b1;end// 写入使能状态下生成待写入的数据always @(posedge clk or negedge rst_n) beginif(rst_n)ram_wr_data <= 8'd0;else if(ram_wr_en)ram_wr_data <= ram_wr_data + 1'b1;elseram_wr_data <= 8'd0;end// 写入使能状态下生成待写入的地址always @(posedge clk or negedge rst_n) beginif(rst_n)ram_wr_address <= 8'd0;else if(ram_wr_en)ram_wr_address <= ram_wr_address + 1'b1;elseram_wr_address <= 8'd0;endendmodule
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) beginif(rst_n)counter <= 6'd0;else if(counter == 6'd63)counter <= 6'd0;elsecounter <= counter + 1'b1;end// 读使能状态下向双端口RAM模块输出读地址always @(posedge clk or negedge rst_n) beginif(rst_n)ram_rd_address <= 8'd0;else if(ram_rd_en)ram_rd_address <= ram_rd_address + 1'b1;elseram_rd_address <= 8'd0;endendmodule
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 nsmodule ip_ram_2port_vt();// constants// general purpose registers//reg eachvec;// test vector input registersreg sys_clk;reg sys_rst_n;// wiresparameter 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));initialbegin// code that executes only once// insert code here --> beginsys_clk = 1'b0;sys_rst_n = 1'b0;#(5*T) sys_rst_n = 1'b1;#(200*T) $stop;// --> end$display("Running testbench");endalways// 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;// --> endendendmodule
创建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 ;
重新编译后再次仿真:
波形大体上看过去已经没问题了,读写使能及数据都可以正常读写。

看一下读写的地址和数据,也与预期效果一致。


