Verilog基础回顾:模块、端口、数据类型、赋值语句、过程块、时序逻辑与组合逻辑的基本写法

在进入FIFO与RAM控制器设计之前,我们必须先夯实Verilog语言的核心语法与建模思想。存储器控制器本质上是一个由组合逻辑(地址译码、读写控制信号生成)与时序逻辑(状态机、读写指针、计数器)交织构成的数字系统。如果对模块化设计、阻塞/非阻塞赋值、过程块执行语义理解不透彻,后续调试FIFO空满标志、处理RAM读写冲突时将会寸步难行。本章从六个核心维度展开:模块与端口、数据类型、赋值语句、过程块、组合逻辑建模、时序逻辑建模,并通过一个寄存器组读写控制的小型案例将知识点串联起来。

Verilog基础核心知识结构图 Verilog 硬件描述语言 模块与端口 module/endmodule input/output/inout 数据类型 wire / reg / integer parameter / localparam 赋值语句 阻塞赋值 = 非阻塞赋值 <= 过程块 always @(敏感列表) initial / begin...end 组合逻辑 assign 连续赋值 always @(*) 阻塞赋值 时序逻辑 always @(posedge clk) 非阻塞赋值 <= 应用目标:FIFO 与 RAM 控制器设计 读写指针 · 空满标志 · 地址译码 · 状态机

2.1 模块与端口:硬件设计的封装单元

2.1.1 核心原理

Verilog中模块(module)是硬件描述的基本封装单元,对应一个具有特定功能的硬件电路。模块通过端口(port)与外部环境通信,端口方向分为三种:input(输入)、output(输出)、inout(双向)。模块内部可以包含数据类型声明、逻辑功能描述、子模块实例化等。一个完整的模块以module关键字开始,以endmodule结束。

端口声明有两种风格:Verilog-1995风格(端口方向与类型分开声明)和Verilog-2001风格(ANSI风格,方向与类型合并声明)。现代设计推荐使用ANSI风格,代码更紧凑、可读性更强。

2.1.2 判断方法与操作流程

设计模块时,应遵循以下流程:

  1. 明确功能边界:确定模块的输入信号(数据、控制、时钟、复位)和输出信号(数据、状态、中断)。
  2. 选择端口类型:数据总线通常为input/output;三态总线(如SRAM数据线)使用inout。
  3. 定义位宽:使用[MSB:LSB]定义向量位宽,例如output [7:0] data_out。
  4. 声明端口方向:在模块头部完成所有端口声明。

2.1.3 案例拆解:RAM控制器模块端口定义

以一个简单的同步RAM控制器为例,其端口定义如下:

module ram_ctrl #(
    parameter ADDR_WIDTH = 8,
    parameter DATA_WIDTH = 16
)(
    input  wire                  clk,
    input  wire                  rst_n,
    // 写端口
    input  wire                  wr_en,
    input  wire [ADDR_WIDTH-1:0] wr_addr,
    input  wire [DATA_WIDTH-1:0] wr_data,
    // 读端口
    input  wire                  rd_en,
    input  wire [ADDR_WIDTH-1:0] rd_addr,
    output reg  [DATA_WIDTH-1:0] rd_data,
    // 状态输出
    output wire                  busy
);
    // 内部逻辑...
endmodule

该模块使用parameter定义参数化位宽,便于复用;端口方向清晰,时钟和复位作为全局控制信号单独列出。

经验提醒:端口命名建议采用前缀_功能的格式(如wr_addr、rd_data),便于在大型项目中快速识别信号方向与功能。时钟信号统一命名为clk,复位信号统一命名为rst_n(低有效)或rst(高有效),并在模块头部注释中明确有效电平。

2.1.4 常见误区与总结

  • 误区1:忘记声明端口方向,导致综合时报错或产生意外逻辑。
  • 误区2:将reg类型直接用于input端口——输入端口必须是wire类型。
  • 误区3:在模块内部重复声明端口变量,造成命名冲突。

模块化设计的核心思想是高内聚、低耦合——每个模块只负责一个明确的功能,通过端口进行标准化的信息交互。在FIFO设计中,存储阵列、读写指针、空满标志逻辑可以拆分为多个子模块,通过端口连接。

2.2 数据类型:wire与reg的深入理解

2.2.1 核心原理

Verilog中最常用的数据类型是wire和reg。

  • wire:线网类型,用于表示组合逻辑的输出或模块间的物理连接。它由连续赋值语句(assign)或模块实例化驱动,不能存储状态。
  • reg:寄存器类型,用于表示存储元件(如触发器)或过程块中使用的变量。它在always块或initial块中被赋值,可以保持上一次赋值的结果。

此外还有integer(32位有符号整数)、parameter(常量参数)、localparam(局部常量)等类型。对于存储器设计,reg类型常用于建模RAM阵列:reg [DATA_WIDTH-1:0] mem [0:DEPTH-1];。

2.2.2 判断方法:何时用wire,何时用reg?

使用场景 数据类型 说明
assign连续赋值语句的左侧 wire 组合逻辑输出,如地址译码结果
always块中赋值的变量 reg 时序逻辑或组合逻辑(在always内)
模块实例化的端口连接 wire 连接线网
存储器阵列建模 reg RAM/ROM阵列
常量定义 parameter 模块参数,可被外部覆盖

2.2.3 案例拆解:RAM阵列声明与初始化

// 声明一个深度为256、位宽为8的RAM阵列
reg [7:0] ram_mem [0:255];

// 在initial块中初始化(仅仿真用)
integer i;
initial begin
    for (i = 0; i < 256; i = i + 1) begin
        ram_mem[i] = 8'h00;
    end
end

注意:ram_mem是reg类型,但它是二维数组,访问方式为ram_mem[地址]。在FPGA中,这种声明会被综合为Block RAM或分布式RAM。

风险提示:在可综合设计中,initial块通常不可综合(仅用于仿真初始化)。FPGA的RAM初始化应使用initial配合$readmemh(仿真)或使用IP核的初始化文件(.coe/.mem)。不要依赖initial来初始化可综合逻辑。

2.2.4 常见误区与总结

  • 误区1:认为reg一定综合为寄存器——在always @(*)中赋值的reg综合为组合逻辑。
  • 误区2:在assign语句左侧使用reg类型——语法错误。
  • 误区3:忽略位宽匹配,导致数据截断或符号扩展错误。

理解wire与reg的本质区别,是避免仿真与综合不一致的关键。记住:wire是物理连线,reg是变量容器,它们的综合结果取决于驱动方式。

2.3 赋值语句:阻塞赋值与非阻塞赋值

2.3.1 核心原理

Verilog提供两种赋值方式:

  • 阻塞赋值(=):立即执行,赋值语句右侧的值立刻赋给左侧变量,后续语句基于新值继续执行。在always块中,阻塞赋值按顺序执行,类似C语言。
  • 非阻塞赋值(<=):在时间步结束时统一更新。所有非阻塞赋值语句的右侧表达式在进入always块时采样,然后在块结束时同时更新左侧变量。

这一区别是Verilog中最容易出错的地方,也是面试和实际调试中的高频考点。

2.3.2 判断方法:如何选择赋值方式?

场景 推荐赋值方式 原因
时序逻辑(always @(posedge clk)) 非阻塞赋值 <= 模拟触发器行为,避免竞争冒险
组合逻辑(always @(*)) 阻塞赋值 = 立即更新,符合组合逻辑的传播特性
连续赋值(assign) 只能使用 = assign本身就是连续驱动
测试文件(testbench)中 两者皆可 根据建模意图选择

2.3.3 案例拆解:阻塞与非阻塞的对比

以下两个always块展示了关键差异:

// 错误示例:时序逻辑中使用阻塞赋值
always @(posedge clk) begin
    a = b;
    c = a;  // c得到的是b的旧值,但a已更新
end

// 正确示例:时序逻辑中使用非阻塞赋值
always @(posedge clk) begin
    a <= b;
    c <= a;  // c得到的是a的旧值(进入块之前的值)
end

在非阻塞赋值中,c得到的是a在时钟沿之前的旧值,这符合硬件触发器的行为——所有触发器在时钟沿同时采样输入,同时更新输出。

黄金法则:在always @(posedge clk)块中,一律使用非阻塞赋值(<=);在always @(*)组合逻辑块中,一律使用阻塞赋值(=)。混合使用会导致仿真与综合结果不一致,甚至产生意想不到的锁存器。

2.3.4 常见误区与总结

  • 误区1:在时序逻辑中使用阻塞赋值,导致仿真时出现竞争,综合后电路行为与仿真不符。
  • 误区2:在同一个always块中混合使用阻塞和非阻塞赋值——强烈不推荐,容易引发不可预测行为。
  • 误区3:在组合逻辑中忘记为所有分支赋值,导致综合出锁存器(latch)。

赋值语句的选择直接决定了电路的综合质量。对于FIFO控制器中的读写指针更新、空满标志翻转,必须使用非阻塞赋值来保证时序正确性。

2.4 过程块:always与initial的深入理解

2.4.1 核心原理

过程块是Verilog中描述硬件行为的主要载体,包括:

  • always块:可综合,用于描述时序逻辑或组合逻辑。敏感列表(sensitivity list)指定触发条件,如@(posedge clk)、@(*)(自动包含所有输入信号)。
  • initial块:仅仿真使用,在仿真开始时执行一次,常用于初始化信号、生成激励。

一个模块可以包含多个always块,它们并行执行,彼此独立。每个always块内部语句按顺序执行。

2.4.2 操作流程:编写规范的always块

  1. 确定触发条件:时序逻辑使用@(posedge clk)或@(negedge clk);组合逻辑使用@(*)。
  2. 定义变量类型:块内赋值的变量必须声明为reg。
  3. 编写逻辑体:使用if/else、case等语句描述行为。
  4. 检查完整性:组合逻辑中所有分支必须赋值,避免锁存器。

2.4.3 案例拆解:同步FIFO的读写指针更新

// 写指针更新逻辑
always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        wr_ptr <= 0;
    end else if (wr_en && !full) begin
        wr_ptr <= wr_ptr + 1'b1;
    end
end

// 读指针更新逻辑
always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        rd_ptr <= 0;
    end else if (rd_en && !empty) begin
        rd_ptr <= rd_ptr + 1'b1;
    end
end

两个always块并行执行,分别维护写指针和读指针。注意异步复位(negedge rst_n)的使用,这是FPGA设计的标准做法。

经验提醒:敏感列表必须包含所有被读取的信号。对于组合逻辑,使用@(*)可以自动包含所有输入,避免遗漏。对于时序逻辑,敏感列表通常只包含时钟和复位,不要将数据信号加入敏感列表,否则会导致仿真行为异常。

2.4.4 常见误区与总结

  • 误区1:在always块中对同一个变量在多个块中赋值——导致多驱动冲突,综合报错。
  • 误区2:时序逻辑的敏感列表遗漏复位信号,导致仿真时复位无效。
  • 误区3:在组合逻辑的always块中使用非阻塞赋值,导致仿真延迟一个时间步。

过程块是Verilog建模的核心,掌握always的并行语义和敏感列表规则,是编写高质量RTL的基础。

2.5 组合逻辑与时序逻辑的基本写法

2.5.1 核心原理

组合逻辑的输出仅取决于当前输入,无记忆功能。常见实现方式:assign连续赋值、always @(*)过程赋值。时序逻辑的输出不仅取决于当前输入,还取决于历史状态(由时钟驱动)。常见实现方式:always @(posedge clk)。

2.5.2 判断方法:组合还是时序?

特征 组合逻辑 时序逻辑
输出与输入关系 仅取决于当前输入 取决于当前输入和过去状态
时钟依赖 无 有(时钟沿触发)
存储能力 无 有(触发器/寄存器)
典型写法 assign / always @(*) always @(posedge clk)
赋值方式 阻塞赋值 = 非阻塞赋值 <=

2.5.3 案例拆解:RAM地址译码与读写控制

以下代码展示了组合逻辑(地址译码)与时序逻辑(读写控制)的配合:

// 组合逻辑:地址译码(产生片选信号)
wire cs_ram = (addr[7:6] == 2'b00) ? 1'b1 : 1'b0;

// 组合逻辑:读写信号生成(always @(*))
reg wr_pulse;
always @(*) begin
    if (wr_en && cs_ram)
        wr_pulse = 1'b1;
    else
        wr_pulse = 1'b0;
end

// 时序逻辑:数据写入
always @(posedge clk) begin
    if (wr_pulse)
        ram_mem[addr[5:0]] <= wr_data;
end

地址译码使用assign连续赋值,读写脉冲生成使用always @(*)组合逻辑,数据写入使用always @(posedge clk)时序逻辑。三者协同完成RAM的读写操作。

风险提示:在组合逻辑中,如果if语句缺少else分支,综合器会推断出锁存器(latch)。锁存器在FPGA中占用额外资源且时序难以分析,应尽量避免。务必为所有条件分支提供明确的赋值。

2.5.4 常见误区与总结

  • 误区1:将组合逻辑的always块敏感列表写成@(posedge clk),导致综合出时序逻辑。
  • 误区2:在时序逻辑中直接使用assign语句——语法错误,时序逻辑必须通过always块实现。
  • 误区3:忽略复位信号,导致上电后寄存器状态不确定。

组合逻辑与时序逻辑是数字电路的两大基石。在FIFO设计中,空满标志的生成通常使用组合逻辑(基于读写指针比较),而指针更新使用时序逻辑。理解两者的边界和配合方式,是设计可靠存储器的前提。

2.6 综合案例:寄存器组读写控制器

2.6.1 案例背景与目标

设计一个8路寄存器组(每个寄存器8位),支持单周期读写操作。要求:写操作在时钟上升沿完成,读操作输出组合逻辑直接给出。该案例综合运用了本章所有知识点。

2.6.2 设计实现

module reg_file #(
    parameter DEPTH = 8,
    parameter WIDTH = 8
)(
    input  wire                  clk,
    input  wire                  rst_n,
    input  wire                  wr_en,
    input  wire [2:0]            wr_addr,
    input  wire [WIDTH-1:0]      wr_data,
    input  wire [2:0]            rd_addr,
    output wire [WIDTH-1:0]      rd_data
);
    // 寄存器阵列
    reg [WIDTH-1:0] reg_array [0:DEPTH-1];
    
    // 写操作(时序逻辑)
    integer i;
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            for (i = 0; i < DEPTH; i = i + 1)
                reg_array[i] <= {WIDTH{1'b0}};
        end else if (wr_en) begin
            reg_array[wr_addr] <= wr_data;
        end
    end
    
    // 读操作(组合逻辑)
    assign rd_data = reg_array[rd_addr];
    
endmodule

2.6.3 分析过程与验证

该设计的关键点:

  • 写操作使用always @(posedge clk)时序逻辑,非阻塞赋值保证所有寄存器在时钟沿同步更新。
  • 读操作使用assign连续赋值,输出为组合逻辑,无额外延迟。
  • 异步复位将所有寄存器清零,保证上电后状态确定。

仿真验证时,需要检查:复位后所有寄存器为0;写入地址5数据0xAB后,读地址5输出0xAB;写入地址5数据0xCD后,读地址5输出0xCD;未写入的寄存器保持原值。

复盘总结:该案例展示了模块化设计、参数化定义、wire/reg选择、阻塞/非阻塞赋值、组合/时序逻辑划分的完整流程。在FIFO设计中,寄存器组可以扩展为RAM阵列,读写指针替代地址信号,空满标志替代简单的读写使能。掌握这些基础,后续章节将水到渠成。

2.6.4 风险与代价

该设计存在一个潜在风险:读操作是组合逻辑,当rd_addr变化时,rd_data会立即变化,可能产生毛刺。在高速系统中,建议在输出端增加一级寄存器(流水线化),以牺牲一个时钟周期延迟换取时序稳定性。这是存储器设计中常见的权衡。

通过本章的学习,读者应能够熟练编写规范的Verilog模块,正确选择数据类型和赋值方式,清晰划分组合逻辑与时序逻辑。这些基础能力将直接应用于后续FIFO与RAM控制器的设计中。