2025.9.15知识笔记

Sep 15 2025 每日笔记

今天的主线任务就是了解一下ROM和RAM相关IP核的使用

1、创建工程

在创建工程的时候,要选取下面这个信息
Series (系列)​​ 芯片的产品系列,决定架构和性能等级。 GW5A(高云5A系列)
Device (器件)​​ 系列下的具体型号,决定逻辑资源规模。 GW5A-25
​Speed (速度等级)​​ 芯片的性能等级,等级越高,时序性能越好。 ES(工程样片)

2、地址宽度和数据宽度区别

数据宽度 (Data Width)​​ ​每个存储单元存放的数据的位数​​(即ROM每个地址输出的数据位宽) 决定了你​​一次能读出多少位数据​​

​​地址宽度 (Address Depth)​​ ​存储单元的数量​​(寻址范围) 决定了你的ROM​​能存多少个数据​

举个例子:
假设你要用ROM存储一个简单的4色调色板,颜色值为:3’b000(黑), 3’b001(红), 3’b010(绿), 3’b011(蓝)。

分析:每个颜色值有 ​​3位​​。共有 ​​4个​​ 颜色值。

​​选择参数​​:
​数据宽度 (Data Width)​​:每个颜色值是3位,所以设为 ​​3​​。
​​地址深度 (Address Depth)​​:有4个颜色值,所以至少需要4个地址。2^2=4,所以地址宽度设为 ​​2​​ 位刚好满足。如果你想预留空间,可以设更大(如3位,地址深度8)。

图片中显示的ROM16符号确实只有 ​​addr[0..0]​​(1位地址线),但这只是因为您当前配置的 ​​地址深度(Address Depth)只有2​​(只需要1位地址就能索引2个位置:0和1)。
​核心原理:地址宽度是动态的​​
ROM16模块的​​地址端口宽度不是固定的​​,它会根据您在配置界面中设置的 ​​“Address Depth”​​ 的值​​自动调整​​!

存储器总容量 (bits) = 数据宽度 (bits) × 地址深度 (寻址单元数)​​
​地址深度 = 2 ^ (地址宽度)​

sine为正弦波的意思

3、PROM的输入输出端口

PROM端口作用:ad输入地址,clk同步时钟,oce控制输出更新,ce使能芯片,reset复位,dout输出数据。

首先来看一下oce端口的作用:只有oce使能了,才能更新得输出数据。只有 oce信号使能(为高电平 1)时,ROM 的输出数据 dout才会在时钟上升沿被更新。​
①当oce为高电平(1)时,允许ROM在时钟沿更新输出数据,新数据覆盖原值,dout随地址变化。
②当oce为低电平(0)时,ROM的输出数据被冻结并保持最后一个值,内部逻辑继续正常工作。

PROM(ROM)的 ​​bypass模式​​ 和 ​​pipeline模式:
bypass模式​​ 和 ​​pipeline模式​​ 的核心区别在于​​数据输出与时钟的时序关系​​。
​Bypass模式(旁路模式)​​:数据输出​​无时钟延迟​​,地址变化后数据立即更新(异步读取)。
​Pipeline模式(流水线/寄存器模式)​​:数据输出​​有1个时钟周期的延迟​​,在时钟上升沿锁存输出(同步读取)。

​ROM时序波形图(Bypass模式):
Bypass模式:clk上升沿读取地址,下降沿读出数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
+-----------------------------------------------------------------------+
| |
| CLK: __|‾|__|‾|__|‾|__|‾|__|‾|__|‾|__|‾|__|‾|__|‾|__|‾|__|‾|__|‾|__ |
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
| |
| CE: ________|‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ |
| |
| AD: [ ad_0 ] [ ad_1 ] [ ad_2 ] [ ad_3 ] |
| |
| DO: [MEM(ad_0)] [MEM(ad_1)] [MEM(ad_2)] [MEM(ad_3)] |
| |
| |
| 特征:DO 随 AD 变化立即更新,无时钟延迟 |
+-----------------------------------------------------------------------+

Pipeline模式:上升沿读取地址,下个下降沿读取数据,慢一拍
ROM时序波形图(Pipeline模式)​:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
+-----------------------------------------------------------------------+
| |
| CLK: __|‾|__|‾|__|‾|__|‾|__|‾|__|‾|__|‾|__|‾|__|‾|__|‾|__|‾|__|‾|__ |
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
| |
| CE: ________|‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ |
| |
| AD: [ ad_0 ] [ ad_1 ] [ ad_2 ] [ ad_3 ] |
| |
| DO: ________ [MEM(ad_0)] [MEM(ad_1)] [MEM(ad_2)] |
| |
| |
| 特征:DO 较 AD 延迟1个时钟周期更新 |
+-----------------------------------------------------------------------+

​模式选择建议​​:
​1、​追求性能和数据稳定性​​:选择 ​​Pipeline模式​​。这是最常用的模式,能保证数据在稳定时被捕获,避免系统因异步读取而产生冒险。

​2、​需要立即获取数据​​:仅在极少数对延迟极其敏感且能处理潜在毛刺的场景下,考虑 ​​Bypass模式​​。

复 位 模 式 配 置 , 支 持 同 步 复 位 (Synchronous) 和 异 步 复 位(Asynchronous),reset 信号复位锁存器和输出寄存器,因此当设置reset 信号有效时,不管用户使用的是寄存器输出模式还是旁路输出模式,端口都输出 0。同步复位有效时,DO 在 CLK 上升沿复位为 0,异步复位有效时,DO 随之复位为 0,不需要等到 CLK 上升沿。

配置初始值,初始值以二进制、十六进制或带地址十六进制的格式写在初始化文件中。“Memory Initialization File”选取的初始化文件可通过手动或者在IDE菜单栏中依次点击“File->New->Memory Initialization File”获取。

4、PROM使用代码

直接使用Gowin_pROM进行实例化即可
IP核代码

1
2
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
module Gowin_pROM (dout, clk, oce, ce, reset, ad);

output [7:0] dout;
input clk;
input oce;
input ce;
input reset;
input [7:0] ad;

wire [23:0] prom_inst_0_dout_w;
wire gw_gnd;

assign gw_gnd = 1'b0;

pROM prom_inst_0 (
.DO({prom_inst_0_dout_w[23:0],dout[7:0]}),
.CLK(clk),
.OCE(oce),
.CE(ce),
.RESET(reset),
.AD({gw_gnd,gw_gnd,gw_gnd,ad[7:0],gw_gnd,gw_gnd,gw_gnd})
);

defparam prom_inst_0.READ_MODE = 1'b0;
defparam prom_inst_0.BIT_WIDTH = 8;
defparam prom_inst_0.RESET_MODE = "SYNC";
defparam prom_inst_0.INIT_RAM_00 = 256'hD7D5D3D0CECBC9C6C4C1BEBCB9B6B3B0ADAAA7A5A29E9B9895928F8C89868380;
defparam prom_inst_0.INIT_RAM_01 = 256'hFFFFFFFEFEFEFDFDFCFBFAFAF9F8F6F5F4F3F1F0EEEDEBEAE8E6E4E2E0DEDCDA;
defparam prom_inst_0.INIT_RAM_02 = 256'hDCDEE0E2E4E6E8EAEBEDEEF0F1F3F4F5F6F8F9FAFAFBFCFDFDFEFEFEFFFFFFFF;
defparam prom_inst_0.INIT_RAM_03 = 256'h8386898C8F9295989B9EA2A5A7AAADB0B3B6B9BCBEC1C4C6C9CBCED0D3D5D7DA;
defparam prom_inst_0.INIT_RAM_04 = 256'h282A2C2F313436393B3E414346494C4F5255585A5D6164676A6D707376797C80;
defparam prom_inst_0.INIT_RAM_05 = 256'h0000000101010202030405050607090A0B0C0E0F1112141517191B1D1F212325;
defparam prom_inst_0.INIT_RAM_06 = 256'h23211F1D1B1917151412110F0E0C0B0A09070605050403020201010100000000;
defparam prom_inst_0.INIT_RAM_07 = 256'h7C797673706D6A6764615D5A5855524F4C494643413E3B393634312F2C2A2825;

endmodule //Gowin_pROM

实例化代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
module rom_top( 
clk,
reset_n,
dout
);
input clk;
input reset_n;
output wire [7:0]dout;

reg [9:0]addr;
always@(posedge clk or negedge reset_n)
if(!reset_n)
addr<=0;
else
addr<=addr+1'b1;

Gowin_pROM Gowin_pROM(
.dout(dout), //output [7:0] dout
.clk(clk), //input clk
.oce(1'b1), //input oce
.ce(1'b1), //input ce
.reset(~reset_n), //input reset
.ad(addr) //input [7:0] ad
);

endmodule

5、RAM代码

具有两个地址,写地址WAD 和读地址 RAD,这两个地址端口是异步的。WRE 为高电平时进行写操作,此时会在 CLK 的上升沿将数据加载到存储器对应写地址。读操作则由读地址确
定输出 RAM 对应位置的数据,其时序波形图如下所示。:

上面这个图片,时钟沿一来,同时写入地址和读数据

1
2
3
4
5
6
7
8
9
10
// 实例化Gowin FPGA的16位深度单端口RAM模块 (RAM16SDP)
// 该模块提供同步写、异步读功能
Gowin_RAM16SDP your_instance_name(
.dout(dout_o), // 输出:8位数据输出线,连接到dout_o信号
.wre(wre_i), // 输入:写使能信号,高电平有效时允许写入操作
.wad(wad_i), // 输入:8位写地址总线,指定数据写入位置(0-255)
.di(di_i), // 输入:8位数据输入总线,提供要写入存储器的数据
.rad(rad_i), // 输入:8位读地址总线,指定数据读取位置(0-255)
.clk(clk_i) // 输入:系统时钟,同步写入操作(上升沿触发)
);

测试文件:

具体 tb代码实现的是在地址从 016上写入数据为从 255 减至 240。延时一段时间后读地址为 016 上的数据。

1
2
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
`timescale 1ns/1ns  // 仿真时间单位1ns/精度1ns
`define CLK_PERIOD 20 // 定义时钟周期为20ns(对应50MHz)

module ram_tb();
//==== 信号声明 ====//
reg clk; // 主时钟信号(用于同步RAM操作)
reg wea; // 写使能信号(高电平有效)
reg [7:0] addra; // 写地址总线(实际只用低4位,寻址范围0-15)
reg [7:0] dina; // 写入数据总线(8位宽)
reg [7:0] addrb; // 读地址总线(实际只用低4位)
wire [7:0] doutb;// 读取数据输出
integer i; // 循环控制变量

//==== 硬件原语实例化 ====//
GSR GSR(.GSRI(1'b1)); // 全局复位控制(此处保持无效状态)

//==== 待测RAM模块实例化 ====//
// 信号连接关系:
// - 写操作:wea + addra + dina -> 在clk上升沿写入
// - 读操作:addrb -> 异步输出到doutb
Gowin_RAM16SDP your_instance_name(
.dout(doutb), // 输出:读取数据(连接到doutb)
.wre(wea), // 输入:写使能(连接到wea)
.wad(addra), // 输入:写地址(连接到addra)
.di(dina), // 输入:写入数据(连接到dina)
.rad(addrb), // 输入:读地址(连接到addrb)
.clk(clk) // 输入:工作时钟(连接到clk)
);

//==== 时钟生成 ====//
initial clk = 1'b1; // 时钟初始化为高电平
always #(`CLK_PERIOD/2) clk = ~clk; // 每10ns翻转一次(生成50MHz时钟)

//==== 测试主程序 ====//
initial begin
// 初始化阶段
wea = 0; // 写使能关闭
addra = 0; // 写地址清零
dina = 0; // 写入数据清零
addrb = 255; // 读地址初始化为255(测试高位地址是否被忽略)

// 等待10.5个时钟周期(210ns)让系统稳定
#(`CLK_PERIOD*10 + 1);

//=== 测试阶段1:写入16个数据 ===//
wea = 1; // 使能写入
for (i = 0; i <= 15; i = i + 1) begin
dina = 255 - i; // 写入数据从255递减到240
addra = i; // 地址从0递增到15
#`CLK_PERIOD; // 每个数据保持20ns(1个时钟周期)
end
wea = 0; // 关闭写入使能
#1; // 额外等待1ns(避免竞争冒险)

//=== 测试阶段2:读取16个地址 ===//
for (i = 0; i <= 15; i = i + 1) begin
addrb = i; // 地址从0递增到15
#`CLK_PERIOD; // 每个地址保持20ns
// 注意:此处应添加读取验证逻辑(当前缺失)
// 例如:if (doutb !== 255-i) $error("读取错误");
end

// 结束仿真
#200; // 额外延迟200ns(观察最终状态)
$stop; // 终止仿真
end
endmodule