这是 Verilog 里的存储器(数组)声明,先读一下可视化技能,然后我给你画个内存布局图讲清楚。
reg [2:0] data_byte_pre [7:0]; 声明的是一个存储器(数组),不是一个大位宽信号:
reg [2:0]→ 每个元素是 3 bit 的寄存器(bit2~bit0)data_byte_pre→ 名字[7:0]→ 一共 8 个元素(下标 7~0)
也就是:8 个 3 位寄存器,总共 24 bit 。内存布局如下(示意):

访问方式:
-
data_byte_pre[i]→ 第 i 个元素(整个 3 bit) -
data_byte_pre[i][b]→ 第 i 个元素的第 b 位 -
数组不支持整体赋值 (
data_byte_pre = 24'hxxx;编译报错),必须逐元素操作,初始化用 for 循环:integer i;
initial begin
for (i = 0; i < 8; i = i + 1)
data_byte_pre[i] = 3'b000; // 逐个元素初始化
end
常见坑:
- 位宽和元素数别写反 :
reg [7:0] mem [2:0]是 3 个 8 位元素,和你的 8 个 3 位完全是两回事。 - 下标越界(超出 7~0)仿真中会读/写出
x。 - 综合后通常是 24 个触发器(寄存器组);如果在 always 块里用变量下标
data_byte_pre[addr]读写,会综合成小 RAM/寄存器堆。
结合你的 UART 工程:如果你是想把 8 个字节 (8 bit)先缓存再逐个发送,那每个元素应该声明成 8 位,即 reg [7:0] data_byte_pre [7:0];------你现在的写法每个元素只有 3 bit,存不下完整字节,确认一下是不是笔误。
根据您提供的 Verilog 存储器数组声明笔记内容,我来为您撰写这篇 CSDN 技术博客文章。
【Verilog/FPGA】存储器(数组)声明完全指南:从语法到综合,避坑必读
前言
在 Verilog 硬件描述语言中,存储器(Memory)是一种非常重要的数据结构,常用于数据缓存、查找表、FIFO 实现等场景。然而,Verilog 的存储器声明语法与 C 语言等软件语言差异较大,很多初学者容易把"位宽"和"元素个数"写反,导致综合结果与预期完全不同。
本文以 reg [2:0] data_byte_pre [7:0]; 这一典型声明为例,系统讲解 Verilog 存储器的声明语法、内存布局、访问方式、初始化方法以及常见陷阱,并结合 UART 发送工程给出实际使用建议。
一、存储器声明语法解析
1.1 声明结构
verilog
reg [2:0] data_byte_pre [7:0];
这条语句声明的是一个存储器(数组),而不是一个大位宽信号。它的结构可以拆解为两部分:
| 语法部分 | 含义 |
|---|---|
reg [2:0] |
每个元素是 3 bit 的寄存器(bit2 ~ bit0) |
data_byte_pre |
存储器名称 |
[7:0] |
一共 8 个元素(下标 7 ~ 0) |
核心记忆口诀 :
reg [位宽-1:0] 名字 [元素数-1:0];先写每个元素几位,再写一共多少个元素。
1.2 与 C 语言的对比
在 C 语言中,声明一个数组是这样的:
c
int data[8]; // 8 个 int 元素
而在 Verilog 中,位宽和元素数的声明是分开的,且顺序相反:
verilog
// C 语言:int data[8] → 8 个 32 位元素
// Verilog 等效写法:
reg [31:0] data [7:0]; // 8 个 32 位元素
// C 语言:char data[3] → 3 个 8 位元素
// Verilog 等效写法:
reg [7:0] data [2:0]; // 3 个 8 位元素
最常见的坑 :
reg [7:0] mem [2:0]是 3 个 8 位元素 ,而reg [2:0] mem [7:0]是 8 个 3 位元素,两者完全不同!位宽和元素数千万别写反。
二、内存布局
以 reg [2:0] data_byte_pre [7:0]; 为例,它的内存布局如下:

图1:reg [2:0] data_byte_pre [7:0] 的内存布局------8 个元素,每个元素 3 bit,共 24 bit
具体展开:
| 下标 | 位宽 | 位编号 | 初始值 |
|---|---|---|---|
| data_byte_pre7 | 3 bit | 2:0 | 3'b000 |
| data_byte_pre6 | 3 bit | 2:0 | 3'b000 |
| data_byte_pre5 | 3 bit | 2:0 | 3'b000 |
| data_byte_pre4 | 3 bit | 2:0 | 3'b000 |
| data_byte_pre3 | 3 bit | 2:0 | 3'b000 |
| data_byte_pre2 | 3 bit | 2:0 | 3'b000 |
| data_byte_pre1 | 3 bit | 2:0 | 3'b000 |
| data_byte_pre0 | 3 bit | 2:0 | 3'b000 |
总计:8 个元素 × 3 bit = 24 bit
三、访问方式
Verilog 存储器支持两种访问粒度:
3.1 访问整个元素
verilog
data_byte_pre[i] // 第 i 个元素,整个 3 bit
例如:
verilog
data_byte_pre[0] = 3'b101; // 给第 0 个元素赋值为 101
assign some_signal = data_byte_pre[3]; // 读取第 3 个元素的值
3.2 访问单个位
verilog
data_byte_pre[i][b] // 第 i 个元素的第 b 位
例如:
verilog
data_byte_pre[2][1] = 1'b1; // 第 2 个元素的第 1 位设为 1
assign bit_out = data_byte_pre[5][0]; // 读取第 5 个元素的第 0 位
3.3 拼接与拆分
verilog
// 将 8 个 3bit 元素拼接成 24bit 宽信号
assign [23:0] flat_data = {data_byte_pre[7], data_byte_pre[6], data_byte_pre[5],
data_byte_pre[4], data_byte_pre[3], data_byte_pre[2],
data_byte_pre[1], data_byte_pre[0]};
// 将 24bit 宽信号拆分到 8 个 3bit 元素
always @(*) begin
data_byte_pre[7] = flat_data[23:21];
data_byte_pre[6] = flat_data[20:18];
data_byte_pre[5] = flat_data[17:15];
data_byte_pre[4] = flat_data[14:12];
data_byte_pre[3] = flat_data[11:9];
data_byte_pre[2] = flat_data[8:6];
data_byte_pre[1] = flat_data[5:3];
data_byte_pre[0] = flat_data[2:0];
end
四、初始化方法
4.1 重要限制:不支持整体赋值
存储器不支持整体赋值操作。以下写法会编译报错:
verilog
// ❌ 编译报错!不支持整体赋值
data_byte_pre = 24'h000000;
4.2 正确做法:使用 for 循环初始化
在 initial 块中使用 for 循环逐个元素初始化:
verilog
integer i;
initial begin
for (i = 0; i < 8; i = i + 1)
data_byte_pre[i] = 3'b000; // 逐个元素初始化为 0
end
注意 :for 循环初始化只能在
initial或always块中使用,不能在模块顶层直接写。
4.3 其他初始化方式
方式一:用 case 语句逐个赋值(适合元素较少的情况)
verilog
initial begin
data_byte_pre[0] = 3'b001;
data_byte_pre[1] = 3'b010;
data_byte_pre[2] = 3'b011;
// ... 依此类推
end
方式二:用 $readmemh 从文件加载(适合大数据量)
verilog
integer f;
initial begin
f = $fopen("init_data.txt");
if (f) begin
for (integer i = 0; i < 8; i = i + 1)
$fscanf(f, "%h", data_byte_pre[i]);
$fclose(f);
end
end
五、综合结果
综合后,存储器通常会被映射为以下硬件结构:
| 综合场景 | 综合结果 |
|---|---|
| 静态索引(下标为常量) | 综合为 24 个触发器(寄存器组) |
动态索引 (下标为变量,如 data_byte_pre[addr]) |
综合成小 RAM / 寄存器堆(Register File) |
| 只读存储器(initial 赋值 + 只读访问) | 可能综合为 ROM 或 LUT |
工程提示 :如果在
always块里用变量下标data_byte_pre[addr]进行读写,综合工具通常会将其实现为真正的 RAM 或寄存器堆(Register File),而不是 24 个独立的触发器。这意味着它的资源占用和时序行为会与普通寄存器不同,需要特别注意。
六、常见陷阱
陷阱 1:位宽和元素数写反
verilog
// 意图:8 个 3 位元素
reg [2:0] data_byte_pre [7:0]; // ✅ 正确
// 误写:3 个 8 位元素(完全不是想要的效果!)
reg [7:0] data_byte_pre [2:0]; // ❌ 错误!
陷阱 2:下标越界
verilog
// data_byte_pre 的下标范围是 [7:0]
data_byte_pre[8] = 3'b000; // ❌ 越界!仿真中会读/写出 x(不确定值)
陷阱 3:整体赋值报错
verilog
data_byte_pre = 24'h000; // ❌ 编译错误!必须逐元素操作
陷阱 4:在组合逻辑中未给所有分支赋值
verilog
// ❌ 可能产生锁存器(Latch)
always @(*) begin
if (en)
data_byte_pre[idx] = data_in;
// 当 en=0 时,data_byte_pre[idx] 没有被赋值,综合出锁存器
end
// ✅ 正确写法:给所有分支都赋值
always @(*) begin
for (integer i = 0; i < 8; i = i + 1)
data_byte_pre[i] = (i == idx && en) ? data_in : data_byte_pre[i];
end
陷阱 5:在时序逻辑中使用非阻塞赋值的注意事项
verilog
// ✅ 时序逻辑中用非阻塞赋值
always @(posedge clk) begin
for (integer i = 0; i < 8; i = i + 1)
data_byte_pre[i] <= new_data[i];
end
七、与 UART 发送工程的结合
在实际的 UART 发送工程中,存储器常用于缓存待发送的数据。这里有一个非常关键的工程问题:
7.1 位宽匹配问题
如果你是想把 8 个字节(8 bit)先缓存再逐个发送,那每个元素应该声明成 8 位:
verilog
// 缓存 8 个字节,每个字节 8 bit
reg [7:0] data_byte_pre [7:0]; // 8 个 8 位元素,共 64 bit
而你当前的写法:
verilog
reg [2:0] data_byte_pre [7:0]; // 8 个 3 位元素,共 24 bit
每个元素只有 3 bit,存不下完整的一个字节(8 bit)。如果你的 UART 发送逻辑是按字节发送的,那这里就是一个 bug!
7.2 UART 发送缓存典型实现
verilog
// UART 发送数据缓存:8 个字节
reg [7:0] tx_buffer [7:0];
reg [3:0] tx_idx;
reg tx_busy;
// 数据加载(并行写入)
always @(posedge clk) begin
if (load_req && !tx_busy) begin
for (integer i = 0; i < 8; i = i + 1)
tx_buffer[i] <= tx_data[i]; // 并行加载 8 个字节
tx_idx <= 4'd0;
tx_busy <= 1'b1;
end
end
// UART 逐个字节发送
always @(posedge clk) begin
if (tx_busy && tx_ready) begin
tx_dout <= tx_buffer[tx_idx]; // 输出当前字节
tx_idx <= tx_idx + 1;
if (tx_idx == 4'd7)
tx_busy <= 1'b0; // 发送完成
end
end
八、存储器 vs 二维数组 vs 一维数组
很多开发者会混淆 Verilog 中的几种"类数组"写法:
| 写法 | 含义 | 总位宽 |
|---|---|---|
reg [2:0] mem [7:0]; |
8 个 3 位元素(存储器) | 24 bit |
reg [7:0] mem [2:0]; |
3 个 8 位元素(存储器) | 24 bit |
reg [23:0] mem; |
1 个 24 位信号(不是存储器) | 24 bit |
reg [2:0] mem [7:0]; 按位访问 mem[i][b] |
等价于二维数组 mem[8][3] |
24 bit |
关键区别 :
reg [23:0] mem;是一个 24 位宽的普通寄存器,支持整体赋值mem = 24'hxxxx;;而reg [2:0] mem [7:0];是存储器,不支持整体赋值,但支持按元素和按位访问。
九、操作速查表
| 操作 | 语法示例 | 说明 |
|---|---|---|
| 声明存储器 | reg [2:0] mem [7:0]; |
8 个 3 位元素 |
| 访问整个元素 | mem[i] |
第 i 个元素(3 bit) |
| 访问单个位 | mem[i][b] |
第 i 个元素的第 b 位 |
| 整体赋值 | 不支持! | 会编译报错 |
| 初始化 | for 循环 + initial | 逐个元素赋值 |
| 从文件加载 | $readmemh() | 适合仿真初始化 |
| 拼接 | {mem[7], mem[6], ..., mem[0]} |
展开为宽信号 |
| 拆分 | mem[i] = wide_sig[offset+:3] |
用位选择切片 |
十、常见问题
Q1:存储器可以综合成真正的 RAM 吗?
可以。当使用变量下标进行读写时(如 mem[addr]),综合工具会将其实现为 RAM 或寄存器堆(Register File)。如果需要特定的 RAM 模板,可以使用综合工具的 (* ram_style = "block" *) 或 (* ram_style = "distributed" *) 属性来指导综合。
Q2:存储器可以综合成 ROM 吗?
可以。如果在 initial 块中初始化且只在组合逻辑中读取(不写),综合工具通常会将其实现为 ROM 或 LUT。
Q3:for 循环初始化在综合时会占用资源吗?
不会。initial 块中的 for 循环仅用于仿真初始化,综合时会被优化掉,不会生成额外的硬件资源。
Q4:可以用 $readmemh 初始化存储器吗?
可以,这是大数据量初始化的推荐做法。文件内容格式为十六进制,每行一个值:
// init_data.txt
01
02
03
04
05
06
07
08
Q5:Verilog-2001 和 Verilog-1995 在存储器声明上有区别吗?
Verilog-1995 不支持存储器(数组)声明,Verilog-2001 才引入了 reg [width-1:0] name [size-1:0] 语法。现代综合工具默认都支持 Verilog-2001。
Q6:存储器可以跨模块传递吗?
不能。存储器是模块内部的局部数据结构,不能直接作为模块端口传递。如果需要跨模块访问,可以将其拆分为:用 reg [width-1:0] 信号 + 地址总线 + 读写控制信号的方式实现接口化的 RAM。
十一、总结
Verilog 存储器(数组)声明是 FPGA/ASIC 设计中非常实用的数据结构,核心要点归纳如下:
- 声明语法 :
reg [位宽-1:0] 名字 [元素数-1:0];,先写位宽再写元素数,千万别写反 - 内存布局 :8 个 3 位元素 = 24 bit,支持按元素
mem[i]和按位mem[i][b]两种访问粒度 - 初始化:不支持整体赋值,必须用 for 循环逐个元素初始化
- 综合结果:静态索引 → 触发器组;动态索引 → 寄存器堆/RAM
- 常见陷阱:位宽和元素数写反、下标越界、整体赋值报错、组合逻辑未全赋值产生锁存器
- 工程匹配 :UART 发送缓存需要 8 bit 元素时,应声明
reg [7:0] mem [7:0],而非reg [2:0] mem [7:0]
掌握存储器的正确用法,能够让你的 Verilog 代码更加清晰、高效,避免综合结果与预期不符的调试噩梦。
标签:Verilog FPGA 存储器 数组 寄存器 RAM 初始化 位宽 UART
版权声明:本文为原创技术分享,转载请注明出处。如有疑问欢迎评论区交流!