第三章 硬件描述语言核心:Verilog/VHDL语法要点、组合逻辑与时序逻辑设计、状态机设计方法、可综合代码风格

FPGA 开发的本质是使用硬件描述语言(HDL)对数字电路进行建模与实现。Verilog 与 VHDL 作为两大主流 HDL,其语法核心并非软件编程语言的翻版,而是对并行硬件行为的抽象描述。本章将聚焦于可综合的语法子集、组合逻辑与时序逻辑的建模差异、状态机的工程化设计方法,以及影响最终电路性能与稳定性的代码风格。理解这些核心要点,是跨越“会写仿真代码”到“能设计可交付硬件”这一关键分水岭的基础。

HDL 核心知识体系与设计映射关系 HDL 设计源文件 语法要点与可综合子集 组合/时序逻辑建模 状态机设计方法 数据类型/运算符 always/process 块 阻塞/非阻塞赋值 参数化与 generate 组合逻辑:assign/always@* 时序逻辑:always@(posedge clk) 同步复位 vs 异步复位 锁存器规避策略 Moore / Mealy 选择 三段式编码规范 状态编码(二进制/格雷/独热) 安全状态与异常恢复 可综合的网表与物理实现 注:所有路径均需通过综合、布局布线验证

上图展示了本章知识体系的整体映射关系。语法要点是描述手段,逻辑建模是思维框架,状态机是复杂控制的核心载体,而最终目标始终是生成可综合、可时序收敛的硬件网表。下面逐一深入展开。

3.1 可综合语法要点:从软件思维到硬件描述

核心原理:可综合子集与仿真原语的区别

Verilog 与 VHDL 均包含丰富的语法结构,但仅有部分子集能被综合工具映射为实际逻辑门电路。可综合语法描述的是硬件结构,而仿真语法(如 initial、#延时、fork/join)仅用于测试平台。综合工具将 always/process 块、assign/信号赋值语句、实例化等转化为寄存器传输级(RTL)网表。理解这一区别是避免“仿真通过、上板失败”的关键。

语法类别Verilog 示例VHDL 示例可综合性
信号赋值assign a = b & c;a <= b and c;是(组合逻辑)
时序逻辑always @(posedge clk)process(clk)是(寄存器)
延时控制#10 a = b;after 10 ns;否(仅仿真)
循环语句for (i=0;i<8;i=i+1)for i in 0 to 7 loop部分可综合(需静态展开)
文件操作$readmemhtextio否(仅仿真)

判断方法:识别可综合代码的边界

判断一段代码是否可综合,可遵循以下操作流程:

  1. 检查敏感列表:时序逻辑必须包含时钟沿(posedge/negedge),组合逻辑使用 always @* 或 process(all)。
  2. 检查赋值方式:同一变量不能在多个 always 块中赋值;阻塞赋值(=)用于组合逻辑,非阻塞赋值(<=)用于时序逻辑。
  3. 检查循环边界:循环变量必须是常量边界,综合工具会展开为固定硬件。
  4. 检查数据类型:避免使用 real、time 等不可综合类型;VHDL 中避免使用未约束的整数范围。

实战场景:计数器模块的两种写法对比

设计一个 8 位同步计数器,分别用 Verilog 和 VHDL 实现。Verilog 版本:

module counter8(
    input wire clk,
    input wire rst_n,
    output reg [7:0] count
);
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        count <= 8'd0;
    else
        count <= count + 1'b1;
end
endmodule

VHDL 版本:

entity counter8 is
    Port ( clk : in STD_LOGIC;
           rst_n : in STD_LOGIC;
           count : out STD_LOGIC_VECTOR(7 downto 0));
end counter8;
architecture Behavioral of counter8 is
    signal cnt_reg : unsigned(7 downto 0) := (others => '0');
begin
    process(clk, rst_n)
    begin
        if rst_n = '0' then
            cnt_reg <= (others => '0');
        elsif rising_edge(clk) then
            cnt_reg <= cnt_reg + 1;
        end if;
    end process;
    count <= std_logic_vector(cnt_reg);
end Behavioral;

两个版本均采用异步复位、同步使能的经典结构,综合后资源消耗一致。关键点在于复位信号 rst_n 必须出现在敏感列表中,否则综合器会推断为同步复位,导致行为差异。

常见误区:初学者常将 C 语言的“变量赋值”思维带入 HDL,在多个 always 块中对同一变量赋值,或使用阻塞赋值描述时序逻辑。这会导致仿真时出现竞争冒险,综合后产生意想不到的锁存器或时序违规。务必牢记:一个变量只能在一个 always/process 块中赋值,时序逻辑使用非阻塞赋值,组合逻辑使用阻塞赋值或连续赋值。

3.2 组合逻辑与时序逻辑设计:建模范式的分野

核心原理:两种逻辑的本质差异

组合逻辑的输出仅取决于当前输入,无记忆功能,对应与门、或门、多路选择器等。时序逻辑的输出不仅取决于当前输入,还取决于历史状态,由触发器构成,受时钟沿控制。在 HDL 中,组合逻辑通过 assign 语句或 always @* 块描述,时序逻辑通过 always @(posedge clk) 描述。理解二者的边界是设计稳定系统的基础。

判断方法:如何避免意外生成锁存器

锁存器(Latch)是组合逻辑中因分支条件不完整而意外生成的存储单元,会破坏时序分析。规避方法如下:

  • 确保所有分支都有赋值:在 if-else 或 case 语句中,每个分支都必须对所有输出变量赋值。
  • 使用默认赋值:在 always 块开头先给所有输出赋默认值,再写具体分支。
  • 组合逻辑中避免反馈:输出不能反馈到自身输入。
设计类型敏感列表赋值方式典型应用
组合逻辑always @* 或 assign阻塞赋值 =地址译码、数据选择、算术运算
时序逻辑always @(posedge clk)非阻塞赋值 <=计数器、移位寄存器、状态寄存器
混合逻辑时钟沿 + 电平敏感非阻塞赋值 + 阻塞赋值带组合输出的状态机

案例拆解:同步 FIFO 读指针的时序建模

以同步 FIFO 的读指针为例,展示时序逻辑的完整设计闭环。背景:某网络接口模块需要 16 级深度的同步 FIFO 缓存数据包,读指针需在时钟上升沿更新,并产生空标志。初始现象:直接使用组合逻辑计算读指针导致空标志抖动。分析:读指针更新必须与时钟同步,空标志由读指针与写指针比较产生,属于组合逻辑,但指针本身必须寄存。操作步骤:

  1. 定义读指针寄存器 rd_ptr(4 位宽,支持 16 深度)。
  2. 在时钟上升沿,当读使能有效且 FIFO 非空时,rd_ptr <= rd_ptr + 1。
  3. 空标志 empty = (rd_ptr == wr_ptr) 且未写入新数据,用组合逻辑 assign 实现。
  4. 仿真验证:读写同时发生时,空标志在时钟沿后一个周期内稳定。

结果:空标志不再抖动,FIFO 吞吐率提升至每时钟周期 1 个数据。风险:若读指针使用阻塞赋值,仿真时可能出现读指针提前更新导致空标志误判。复盘:时序逻辑必须严格遵循“时钟沿触发 + 非阻塞赋值”范式,组合逻辑与时序逻辑分离建模。

经验提醒:在复杂设计中,建议将组合逻辑与时序逻辑分开编写为不同的 always 块。例如,状态机的下一状态计算用组合逻辑,状态寄存器更新用时序逻辑。这种“两段式”或“三段式”写法不仅可读性强,且综合工具更容易优化时序路径。

3.3 状态机设计方法:从状态图到健壮实现

核心原理:Moore 与 Mealy 状态机

状态机是数字系统中的“大脑”,用于控制数据流与操作序列。Moore 状态机的输出仅取决于当前状态,稳定但延迟一个时钟周期;Mealy 状态机的输出取决于当前状态和输入,响应快但易产生毛刺。在 FPGA 网络通信中,链路训练、MAC 控制、协议解析等场景广泛使用状态机。选择依据:若对输出时序要求严格且希望避免组合毛刺,选 Moore;若需要快速响应输入变化,选 Mealy。

操作流程:三段式状态机编码规范

推荐使用三段式写法,将状态转移、状态寄存器和输出逻辑分离:

  1. 第一段(时序逻辑):状态寄存器更新,always @(posedge clk or negedge rst_n),使用非阻塞赋值。
  2. 第二段(组合逻辑):下一状态计算,always @*,根据当前状态和输入决定 next_state。
  3. 第三段(输出逻辑):可以是组合逻辑或时序逻辑,根据状态(Moore)或状态+输入(Mealy)产生输出。
// 三段式状态机示例(Verilog)
localparam IDLE = 2'b00, SEND = 2'b01, WAIT = 2'b10;
reg [1:0] state, next_state;
// 第一段:状态寄存器
always @(posedge clk or negedge rst_n) begin
    if (!rst_n) state <= IDLE;
    else state <= next_state;
end
// 第二段:下一状态组合逻辑
always @(*) begin
    case (state)
        IDLE: next_state = start ? SEND : IDLE;
        SEND: next_state = done ? WAIT : SEND;
        WAIT: next_state = ready ? IDLE : WAIT;
        default: next_state = IDLE;
    endcase
end
// 第三段:输出逻辑(Moore 型)
always @(posedge clk or negedge rst_n) begin
    if (!rst_n) tx_en <= 1'b0;
    else if (state == SEND) tx_en <= 1'b1;
    else tx_en <= 1'b0;
end

案例拆解:以太网帧接收状态机

背景:设计一个千兆以太网 MAC 接收控制器,需解析前导码、帧起始定界符(SFD)、目的地址、长度/类型字段。约束:时钟 125MHz,数据位宽 8bit,需在 20ns 内完成状态判断。初始现象:使用两段式状态机,输出信号出现毛刺导致 CRC 校验失败。分析:两段式中输出逻辑直接由组合逻辑产生,数据变化时输出不稳定。操作步骤:

  1. 改用三段式,将 rx_valid 等关键输出寄存器化。
  2. 状态定义:IDLE → PREAMBLE → SFD → HEADER → PAYLOAD → FCS → IDLE。
  3. 在 HEADER 状态解析 MAC 地址,使用计数器 cnt 统计字节数。
  4. 增加异常状态:当检测到错误字节时跳转至 DISCARD 状态,丢弃当前帧。

结果:输出毛刺消除,CRC 校验通过率从 92% 提升至 99.99%。风险:状态机增加 DISCARD 状态后,恢复时间变长,需在时序约束中设置多周期路径。复盘:状态机设计必须考虑异常恢复路径,不能仅关注正常流程。

常见误区:状态编码选择不当。二进制编码节省触发器但组合逻辑复杂,独热编码(One-Hot)增加触发器但简化译码逻辑,适合 FPGA 中触发器资源丰富的场景。对于超过 8 个状态的状态机,建议使用独热编码;对于状态较少且时序紧张的设计,二进制编码更优。此外,必须包含 default 分支或 when others 语句,防止综合出锁存器。

3.4 可综合代码风格:从“能跑”到“可靠”

核心原理:代码风格影响时序与资源

可综合代码风格不仅关乎可读性,更直接影响综合后的电路性能。良好的风格包括:层次化设计、参数化模块、避免 latch、合理使用流水线、控制扇出。在 FPGA 网络通信系统中,时序收敛是硬指标,代码风格必须为时序服务。

操作流程:代码风格检查清单

检查项推荐做法不推荐做法
时钟使用单一主时钟,避免门控时钟内部生成时钟或使用数据信号作为时钟
复位策略异步复位、同步释放仅异步复位或仅同步复位
组合逻辑使用 assign 或 always @*在时序块中混合阻塞赋值
参数化使用 parameter/localparam 定义常量硬编码数字
模块划分按功能划分,接口清晰单模块过大,耦合度高

实战场景:参数化 CRC 校验模块

设计一个可配置的 CRC32 校验器,用于以太网帧校验。使用参数化设计,支持不同多项式。关键代码:

module crc32 #(
    parameter WIDTH = 8,
    parameter POLY = 32'h04C11DB7
)(
    input wire clk,
    input wire rst_n,
    input wire [WIDTH-1:0] data_in,
    input wire data_valid,
    output reg [31:0] crc_out
);
reg [31:0] crc_reg;
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        crc_reg <= 32'hFFFFFFFF;
    else if (data_valid)
        crc_reg <= crc_next(crc_reg, data_in);
end
// 函数实现 CRC 计算
function [31:0] crc_next;
    input [31:0] crc;
    input [WIDTH-1:0] data;
    integer i;
    begin
        crc_next = crc;
        for (i = 0; i < WIDTH; i = i + 1) begin
            if ((crc_next[31] ^ data[i]) == 1'b1)
                crc_next = {crc_next[30:0], 1'b0} ^ POLY;
            else
                crc_next = {crc_next[30:0], 1'b0};
        end
    end
endfunction
endmodule

该模块通过参数 WIDTH 支持 8/16/32 位数据输入,POLY 参数可配置不同 CRC 标准。综合后资源占用仅取决于 WIDTH,便于复用。

总结复盘:可综合代码风格的核心原则可归纳为“三个分离”:组合逻辑与时序逻辑分离、控制逻辑与数据通路分离、参数定义与功能实现分离。遵循这些原则,代码不仅易于维护,还能在综合工具中获得更好的时序优化空间。在 FPGA 网络通信系统中,任何微小的代码风格问题都可能在高数据速率下放大为时序违规或功能错误,因此从编码初期就应建立硬件思维。