【Verilog/FPGA】存储器(数组)

这是 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

常见坑:

  1. 位宽和元素数别写反 :reg [7:0] mem [2:0] 是 3 个 8 位元素,和你的 8 个 3 位完全是两回事。
  2. 下标越界(超出 7~0)仿真中会读/写出 x。
  3. 综合后通常是 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

版权声明:本文为原创技术分享,转载请注明出处。如有疑问欢迎评论区交流!

相关推荐
szxinmai主板定制专家1 小时前
RK3588+FPGA异构架构|高速数据采集+边缘AI落地应用全解析
人工智能·嵌入式硬件·fpga开发·架构·zynq
M78佐菲1 小时前
ARM学习笔记(11)
linux·arm开发·笔记·嵌入式硬件·学习
M78佐菲1 小时前
ARM学习笔记(10)
linux·arm开发·笔记·嵌入式硬件·学习
小程序设计1 小时前
基于STM32的智慧厨房多参数安全监测与预警系统设计
stm32·嵌入式硬件·安全
小诗懂技术2 小时前
【裸机lcd】framebuffer的配置与小型图像库的移植
arm开发·单片机·嵌入式硬件·裸机
comyccccc4 小时前
3.3V稳压电源设计与PCB实现学习笔记
单片机·嵌入式硬件
小程序设计4 小时前
基于STM32森林火灾远程监测模块设计
stm32·单片机·嵌入式硬件
魔猴疯猿4 小时前
香橙派(Orange Pi)入门使用教程:从开箱到跑通第一个项目
单片机·嵌入式硬件·物联网
嵌入式-老费6 小时前
esp32开发与应用(用Python+PySide6解决上位机开发)
linux·单片机·嵌入式硬件