一、前置知识

RAM 的英文全称是 Random Access Memory,即随机存取存储器,可以把数据写入任一指定地址的存储单元,也可以从任一指定地址中读出数据,其读写速度是由时钟频率决定的。
RAM分为静态随机存储器SRAM和动态随机存储器DRAM。SRAM访问速度快但价格较贵;DRAM访问速度较慢但价格便宜,存储容量大。片上SRAM,板载DRAM。
利用Cyclone IV器件内部的M9K存储器模块,可以实现包括RAM、移位寄存器、ROM以及FIFO等各种存储器功能。
EP4CE10片上RAM资源容量414Kbits(Cyclone IV中文手册):
image.png
image.png
单端口RAM:只有一组端口,读写数据不能同时进行,读写公用数据通道。
伪双端口RAM:有两组端口,一组端口用来读,另一组端口负责写。
真双端口RAM:有两组端口,两组端口都可以用来读或写。
image.png

二、单端口RAM

2.1 原理分析

单端口RAM端口示例图如下:
image.png

端口 功能
data[] RAM写数据端口
address[] RAM读写地址端口,单端口RAM共用读写地址端口
wren 写使能信号,高电平有效
byteena[] 字节使能控制,仅写入指定字节,非指定的字节数据不变
addressstall 地址使能控制,高电平时有效地址时钟使能会保持之前的地址
inclocken 输入时钟使能控制,高电平有效
rden 读使能,高电平有效
aclr 异步复位,高电平有效
inclock/outclock 单口 RAM 端口支持输入与输出时钟模式和单时钟模式

2.2 创建 RAM IP 核

2.2.1 创建工程

创建名为ip_ram的工程。
image.png

2.2.2 创建 IP 核

在IP Catalog里搜索RAM: 1-PORT并创建。
image.png
添加路径为X:\FPGA\Projects\09_ip_ram\par\ipcore即创建par\ipcore文件夹。文件命名为ram_1port.v
image.png
image.png

2.2.3 配置 IP 核

  1. 配置输出位宽、存储器容量、块类型、时钟模式
  • q输出位宽设置为8位(默认)
  • 存储器容量设置为32字
  • 存储块类型设置为自动(默认)
  • 时钟模式设置为单时钟模式(默认)

image.png

  1. 配置寄存器、异步复位、读使能
  • 取消勾选'q' output port:选中会导致输出的数据延迟一个时钟周期。
  • 不选择Create an 'aclr' asynchronous for ... ports:该端口用于复位清零RAM,本次不需要。
  • 勾选Create a 'rden' read enable signal:创建读使能端口,用于控制数据输出,高电平有效。

image.png

  1. 写时读设置

Read-During-Write控制在写数据时,读出数据在输出端口应该输出什么。

  • New Data 表示在写数据的同一个上升沿,新数据可读。(选此默认项)
  • Don’t Care 表示输出不确定。

image.png

  1. 初始化RAM数据

该配置页面用来配置RAM内部的初始化数据,若不需要初始数据就选择No, leave it blank即可。
在配置ROM时,也会使用该RAM的IP核,那时就必须进行数据初始化了。
image.png

  1. EDA配置

该页面用来设置是否生成在第三方EDA工具中调试时用到的网表文件,并且提示了在仿真时需要使用到的库文件为altera_mf.v文件。
image.png

  1. 生成文件

该页面选择需要生成的与IP核相关的文件。
image.png
image.png
par/ipcore目录下生成了这四个文件:
image.png

2.2.4 IP核代码分析

以下是截取自 IP 核文件ram_1port.v的一段代码。

  1. module ram_1port (
  2. address,
  3. clock,
  4. data,
  5. rden,
  6. wren,
  7. q);
  8. input [4:0] address;
  9. input clock;
  10. input [7:0] data;
  11. input rden;
  12. input wren;
  13. output [7:0] q;
  • address:RAM读写的公共地址。
  • clock:读写驱动时钟。
  • data:写入的数据
  • rden:读数据使能,高电平有效
  • wren:写数据使能,高电平有效
  • q:读出的数据

需要写入数据时,将wren写使能信号拉高,再给出写入的数据data和写入RAM的地址address即可。
需要读出数据时,将rden读使能信号拉高,再给出读取RAM的地址address,数据会被读到q端口。

2.3 读写测试模块

image.png
编写ram_rw模块测试RAM的读写操作。

  1. module ram_rw(
  2. input clk,
  3. input rst_n,
  4. input [7:0] ram_rd_data,
  5. output reg [4:0] ram_address,
  6. output reg [7:0] ram_wr_data,
  7. output reg ram_wr_en,
  8. output reg ram_rd_en
  9. );
  10. reg [5:0] count;
  11. // 写时序0~31,读时序32~63
  12. assign ram_wr_en = (count>=0 && count<=31) ? 1'b1 : 1'b0;
  13. assign ram_rd_en = (count>=0 && count<=31) ? 1'b0 : 1'b1;
  14. // 计数器循环计数0~63
  15. always @(posedge clk or negedge rst_n) begin
  16. if(!rst_n)
  17. count <= 6'd0;
  18. else if(count == 6'd63)
  19. count <= 6'd0;
  20. else
  21. count <= count + 1'b1;
  22. end
  23. // 生成写入的数据ram_wr_data:0~31
  24. always @(posedge clk or negedge rst_n) begin
  25. if(!rst_n)
  26. ram_wr_data <= 8'd0;
  27. else if(ram_wr_en)
  28. ram_wr_data <= ram_wr_data + 1'b1;
  29. else
  30. ram_wr_data <= 8'd0;
  31. end
  32. // 生成读写地址ram_address:0~31
  33. always @(posedge clk or negedge rst_n) begin
  34. if(!rst_n)
  35. ram_address <= 5'd0;
  36. else if(ram_address == 5'd31)
  37. ram_address <= 5'd0;
  38. else
  39. ram_address <= ram_address + 1'b1;
  40. end
  41. endmodule

以上代码实现的功能如下:

  1. 计数器循环计数:0~63周期
  2. 控制读写使能:0~31个周期内写使能;32~63个周期内读使能
  3. 写使能时(0~31),生成待写入的数据ram_wr_data,范围0~31
  4. 读使能时(32~63),读地址为0~31,读出数据的过程需要IP核处理
  5. 读写地址ram_address在0~31之间循环

编写顶层模块进行IP核与读写模块的测试:

  1. module ip_ram(
  2. input clk,
  3. input rst_n
  4. );
  5. wire [7:0] ram_rd_data;
  6. wire [4:0] ram_address;
  7. wire [7:0] ram_wr_data;
  8. wire ram_wr_en;
  9. wire ram_rd_en;
  10. ram_1port u_ram_1port(
  11. .address(ram_address),
  12. .clock(clk),
  13. .data(ram_wr_data),
  14. .rden(ram_rd_en),
  15. .wren(ram_wr_en),
  16. .q(ram_rd_data)
  17. );
  18. ram_rw u_ram_rw(
  19. .clk(clk),
  20. .rst_n(rst_n),
  21. .ram_rd_data(ram_rd_data),
  22. .ram_address(ram_address),
  23. .ram_wr_data(ram_wr_data),
  24. .ram_wr_en(ram_wr_en),
  25. .ram_rd_en(ram_rd_en)
  26. );
  27. endmodule

image.png

2.4 仿真

先编写仿真激励文件,我这里直接用生成的ip_ram.vt模板修改。

  1. // Generated on "04/26/2022 14:34:15"
  2. // Verilog Test Bench template for design : ip_ram
  3. //
  4. // Simulation tool : ModelSim-Altera (Verilog)
  5. //
  6. `timescale 1 ns/ 1 ns
  7. module ip_ram_vt();
  8. // constants
  9. // general purpose registers
  10. // reg eachvec;
  11. // test vector input registers
  12. reg clk;
  13. reg rst_n;
  14. // wires
  15. parameter T = 20;
  16. // assign statements (if any)
  17. ip_ram i1 (
  18. // port map - connection between master ports and signals/registers
  19. .clk(clk),
  20. .rst_n(rst_n)
  21. );
  22. initial begin
  23. clk = 1'b0;
  24. rst_n = 1'b0;
  25. #(5*T)
  26. rst_n = 1'b1;
  27. #(200*T) $stop;
  28. end
  29. always begin
  30. #(T/2) clk = ~clk;
  31. end
  32. endmodule

仿真结果如下:
image.png
上述图中可以看出:

  1. 读写使能信号交替使能
  2. 在读使能时可以读到数据,在写使能时读到的数据值不变
  3. 在写使能时,ram_wr_data写数据端口生成了数据;在读使能时,ram_wr_data端口数据清零
  4. 读写地址里的数据一直在改变

再看一下时序图的细节(从第0ns开始):
image.png
可以看到,ram_wr_data写数据端口的数据是与预期效果一样的从0~31,但是ram_address在增加到15之后下一个数据出现了负数,是因为在选择显示内容时数制选成了有符号的十进制,最高位被当成了符号位。改成无符号数制就好了,如下。
image.png
image.png
上图可以看到,ram_rd_data读出来的数据有一位延迟,即在第N位地址读到的数据是N-1

三、双端口RAM

3.1 原理分析

image.png
image.png

  1. 创建双端口RAM模块(IP核)和PLL锁存器模块(IP核)
  2. 分别创建读写RAM模块
  3. PLL模块产生两个时钟源(写时钟和读时钟)
  4. 写RAM模块用来产生数据,输入到双端口RAM IP模块;读RAM模块用来读取双端口RAM IP模块的数据
  5. 写一次数据,然后循环读取数据,验证数据正确性

最终程序编写完成后生成的RTL视图:
09_ip_ram_2port.pdf

3.2 创建 RAM IP 核

3.2.1 创建工程

image.png

3.2.2 创建 IP 核

image.png
保存路径:X:/FPGA/Projects/09_ip_ram_2port/par/ipcore/ram_2port/ram_2port.v
image.png

3.2.3 配置 IP 核

  1. 通用配置
  • 选择简单双端口(伪双端口)类型
  • 存储单位选择默认的字存储

image.png

  1. 位宽设置
  • 设置存储深度为32字
  • 配置输入输出位宽均为8位

image.png

  1. 时钟和使能信号
  • 创建读写双时钟信号
  • 创建读使能位

image.png

  1. 寄存器和异步清零
  • 不创建q输出寄存器
  • 不创建读写时钟的使能信号
  • 不创建异步复位信号aclr

image.png

  1. 初始化RAM

不进行RAM初始化
image.png

  1. 仿真依赖文件

image.png

  1. 生成的文件

image.png
image.png

3.3 创建 PLL IP 核

3.3.1 创建 IP 核

image.png
保存路径:X:/FPGA/Projects/09_ip_ram_2port/par/ipcore/pll_clk/pll_clk.v
image.png

3.3.2 配置 IP 核

  1. 通用设置
  • 输入时钟50MHz
  • 常规模式

image.png

  1. 输入和锁信号
  • 创建异步复位信号
  • 创建locked保持稳定信号

image.png

  1. 使能时钟 | 时钟名 | 频率 | 角度 | 占空比 | | —- | —- | —- | —- | | c0 | 50MHz | 0deg | 50% | | c1 | 25MHz | 0deg | 50% |

image.png
image.png

  1. 仿真所需文件

image.png

  1. 生成的文件

image.png
image.png

3.4 模块代码编写

3.4.1 写模块代码

  1. module ram_wr(
  2. input clk,
  3. input rst_n,
  4. output ram_wr_en,
  5. output reg [7:0] ram_wr_data,
  6. output reg [4:0] ram_wr_address
  7. );
  8. reg [5:0] counter;
  9. assign ram_wr_en = (counter>=6'd0 && counter<=6'd31) ? 1'b1 : 1'b0;
  10. // 计时器模块:0~63自增,到63后维持状态
  11. always @(posedge clk or negedge rst_n) begin
  12. if(rst_n)
  13. counter <= 6'd0;
  14. else if(counter == 6'd63)
  15. counter <= counter;
  16. else
  17. counter <= counter + 1'b1;
  18. end
  19. // 写入使能状态下生成待写入的数据
  20. always @(posedge clk or negedge rst_n) begin
  21. if(rst_n)
  22. ram_wr_data <= 8'd0;
  23. else if(ram_wr_en)
  24. ram_wr_data <= ram_wr_data + 1'b1;
  25. else
  26. ram_wr_data <= 8'd0;
  27. end
  28. // 写入使能状态下生成待写入的地址
  29. always @(posedge clk or negedge rst_n) begin
  30. if(rst_n)
  31. ram_wr_address <= 8'd0;
  32. else if(ram_wr_en)
  33. ram_wr_address <= ram_wr_address + 1'b1;
  34. else
  35. ram_wr_address <= 8'd0;
  36. end
  37. endmodule

3.4.2 读模块代码

  1. module ram_rd(
  2. input clk,
  3. input rst_n,
  4. input [7:0] ram_rd_data,
  5. output ram_rd_en,
  6. output reg [4:0] ram_rd_address
  7. );
  8. reg [5:0] counter;
  9. assign ram_rd_en = (counter>=6'd0 && counter<=6'd31) ? 1'b1 : 1'b0;
  10. // 计时器模块:0~63循环
  11. always @(posedge clk or negedge rst_n) begin
  12. if(rst_n)
  13. counter <= 6'd0;
  14. else if(counter == 6'd63)
  15. counter <= 6'd0;
  16. else
  17. counter <= counter + 1'b1;
  18. end
  19. // 读使能状态下向双端口RAM模块输出读地址
  20. always @(posedge clk or negedge rst_n) begin
  21. if(rst_n)
  22. ram_rd_address <= 8'd0;
  23. else if(ram_rd_en)
  24. ram_rd_address <= ram_rd_address + 1'b1;
  25. else
  26. ram_rd_address <= 8'd0;
  27. end
  28. endmodule

3.4.3 顶层模块代码

  1. module ip_ram_2port(
  2. input sys_clk,
  3. input sys_rst_n
  4. );
  5. wire rst_n ;
  6. wire wr_clk_50m ;
  7. wire rd_clk_25m ;
  8. wire locked ;
  9. wire ram_wr_en ;
  10. wire ram_wr_data ;
  11. wire ram_wr_address ;
  12. wire ram_rd_data ;
  13. wire ram_rd_en ;
  14. wire ram_rd_address ;
  15. assign rst_n = sys_rst_n & locked;
  16. pll_clk u_pll_clk(
  17. .areset (~sys_rst_n),
  18. .inclk0 (sys_clk ),
  19. .c0 (wr_clk_50m),
  20. .c1 (rd_clk_25m),
  21. .locked (locked )
  22. );
  23. ram_wr u_ram_wr(
  24. .clk (wr_clk_50m ),
  25. .rst_n (rst_n ),
  26. .ram_wr_en (ram_wr_en ),
  27. .ram_wr_data (ram_wr_data ),
  28. .ram_wr_address (ram_wr_address)
  29. );
  30. ram_2port u_ram_2port (
  31. .data (ram_wr_data ),
  32. .rdaddress (ram_rd_address),
  33. .rdclock (rd_clk_25m ),
  34. .rden (ram_rd_en ),
  35. .wraddress (ram_wr_address),
  36. .wrclock (wr_clk_50m ),
  37. .wren (ram_wr_en ),
  38. .q (ram_rd_data )
  39. );
  40. ram_rd u_ram_rd(
  41. .clk (rd_clk_25m ),
  42. .rst_n (rst_n ),
  43. .ram_rd_data (ram_rd_data ),
  44. .ram_rd_en (ram_rd_en ),
  45. .ram_rd_address (ram_rd_address)
  46. );
  47. endmodule

3.5 程序仿真

编写仿真激励文件,首先将ip_ram_2port设置成顶层模块,然后直接使用模板修改即可。

  1. // Copyright (C) 2019 Intel Corporation. All rights reserved.
  2. // Your use of Intel Corporation's design tools, logic functions
  3. // and other software and tools, and any partner logic
  4. // functions, and any output files from any of the foregoing
  5. // (including device programming or simulation files), and any
  6. // associated documentation or information are expressly subject
  7. // to the terms and conditions of the Intel Program License
  8. // Subscription Agreement, the Intel Quartus Prime License Agreement,
  9. // the Intel FPGA IP License Agreement, or other applicable license
  10. // agreement, including, without limitation, that your use is for
  11. // the sole purpose of programming logic devices manufactured by
  12. // Intel and sold by Intel or its authorized distributors. Please
  13. // refer to the applicable agreement for further details, at
  14. // https://fpgasoftware.intel.com/eula.
  15. // *****************************************************************************
  16. // This file contains a Verilog test bench template that is freely editable to
  17. // suit user's needs .Comments are provided in each section to help the user
  18. // fill out necessary details.
  19. // *****************************************************************************
  20. // Generated on "04/28/2022 13:39:08"
  21. // Verilog Test Bench template for design : ip_ram_2port
  22. //
  23. // Simulation tool : ModelSim-Altera (Verilog)
  24. //
  25. `timescale 1 ns/ 1 ns
  26. module ip_ram_2port_vt();
  27. // constants
  28. // general purpose registers
  29. //reg eachvec;
  30. // test vector input registers
  31. reg sys_clk;
  32. reg sys_rst_n;
  33. // wires
  34. parameter T = 20;
  35. // assign statements (if any)
  36. ip_ram_2port i1 (
  37. // port map - connection between master ports and signals/registers
  38. .sys_clk(sys_clk),
  39. .sys_rst_n(sys_rst_n)
  40. );
  41. initial
  42. begin
  43. // code that executes only once
  44. // insert code here --> begin
  45. sys_clk = 1'b0;
  46. sys_rst_n = 1'b0;
  47. #(5*T) sys_rst_n = 1'b1;
  48. #(200*T) $stop;
  49. // --> end
  50. $display("Running testbench");
  51. end
  52. always
  53. // optional sensitivity list
  54. // @(event1 or event2 or .... eventn)
  55. begin
  56. // code executes for every event on sensitivity list
  57. // insert code here --> begin
  58. #(T/2) sys_clk = ~sys_clk;
  59. @eachvec;
  60. // --> end
  61. end
  62. endmodule

创建ModelSim工程,工程名为ip_ram_2port,工程路径D:/Projects/FPGA/Projects/09_ip_ram_2port/sim/tb
image.png
添加文件:

  • 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

仿真之后的波形如下:
image.png
可以看到数据和地址都只是方波01的形式,与预期的0~31效果不一致。
原因是在顶层模块ip_ram_2port.v文件中,定义的线束类型忘记加位宽了。。
image.png
应修改成如下部分:

  1. wire rst_n ;
  2. wire wr_clk_50m ;
  3. wire rd_clk_25m ;
  4. wire locked ;
  5. wire ram_wr_en ;
  6. wire [7:0] ram_wr_data ;
  7. wire [4:0] ram_wr_address ;
  8. wire ram_rd_en ;
  9. wire [7:0] ram_rd_data ;
  10. wire [4:0] ram_rd_address ;

重新编译后再次仿真:
image.png
波形大体上看过去已经没问题了,读写使能及数据都可以正常读写。
image.png
image.png
看一下读写的地址和数据,也与预期效果一致。
unknown.bmp