第四章:硬件描述语言核心

好,咱们进入正题。这一章可以说是整个FPGA设计的基石——硬件描述语言。说白了,就是你和FPGA沟通的“普通话”。你写出来的每一行代码,最终都会变成实实在在的电路。这和写软件完全是两码事。

我个人习惯把HDL的学习分成三块:语法基础、组合逻辑与时序逻辑、状态机。咱们一个一个来聊。

4.1 Verilog/VHDL语法速览

先说说选哪个语言。我早期做项目时用过VHDL,后来全面转向了Verilog。不是说VHDL不好,而是Verilog更灵活,写起来更像C,在工业界和学术界都更流行。当然,如果你在军工或欧洲某些领域,VHDL还是主流。

这里我快速过一下最核心的语法点,你不需要死记硬背,多用几次就熟了。

Verilog 核心语法

// 模块定义
module counter #(
    parameter WIDTH = 8
)(
    input  wire             clk,
    input  wire             rst_n,
    input  wire             en,
    output reg  [WIDTH-1:0] count
);

    // 时序逻辑:always @(posedge clk)
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            count <= 0;
        else if (en)
            count <= count + 1;
    end

endmodule

看到没?always块是核心。敏感列表里写posedge clk就是时序逻辑,写*就是组合逻辑。这个区别一定要刻在脑子里。

VHDL 核心语法

-- 实体声明
entity counter is
    generic (
        WIDTH : integer := 8
    );
    port (
        clk   : in  std_logic;
        rst_n : in  std_logic;
        en    : in  std_logic;
        count : out std_logic_vector(WIDTH-1 downto 0)
    );
end entity;

-- 架构体
architecture rtl of counter is
    signal count_reg : unsigned(WIDTH-1 downto 0);
begin
    process(clk, rst_n)
    begin
        if rst_n = '0' then
            count_reg <= (others => '0');
        elsif rising_edge(clk) then
            if en = '1' then
                count_reg <= count_reg + 1;
            end if;
        end if;
    end process;
    count <= std_logic_vector(count_reg);
end architecture;

VHDL的语法更严谨,但写起来也啰嗦。我个人觉得,如果你刚入门,从Verilog开始会更友好。

小技巧: 写代码时,reg类型不一定生成寄存器,wire也不一定生成连线。关键看你怎么用。在always块里赋值的变量,综合工具会自动推断成reg类型。

4.2 组合逻辑与时序逻辑

这是FPGA设计的灵魂。很多新手搞不清楚“什么时候用组合逻辑,什么时候用时序逻辑”。我举个例子你就明白了。

组合逻辑:输出只取决于当前输入。比如一个加法器,输入A和B,输出就是A+B。没有时钟,没有记忆。

时序逻辑:输出不仅取决于当前输入,还取决于之前的状态。比如计数器,它需要记住当前计数值,然后在时钟沿到来时更新。

嗯,这里要注意:组合逻辑容易产生毛刺。我在项目中遇到过,一个简单的地址译码器,因为组合逻辑路径延迟不一致,导致读出了错误的数据。后来加了一级寄存器打拍,问题就解决了。

避坑指南: 我曾经在组合逻辑里写了always @(*),但漏掉了敏感列表里的某个信号,结果仿真和综合结果不一致。排查了整整两天。所以,写组合逻辑时,要么用always @(*),要么用assign语句,别手动列敏感列表。

说白了,一个简单的原则:需要记忆就用时序逻辑,不需要就用组合逻辑。但实际项目中,我建议你尽量多用时序逻辑。为什么?因为时序逻辑有寄存器隔离,能有效消除毛刺,时序也更好收敛。

4.3 状态机设计模式

状态机是FPGA设计的“万能钥匙”。无论是通信协议、数据通路控制,还是简单的按键消抖,都能用状态机来实现。

我常用的状态机写法有三种:一段式、两段式、三段式。这里我重点推荐三段式。

三段式状态机模板

// 状态编码
localparam IDLE  = 2'b00;
localparam START = 2'b01;
localparam DATA  = 2'b10;
localparam DONE  = 2'b11;

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_sig ? START : IDLE;
        START:  next_state = DATA;
        DATA:   next_state = data_done ? DONE : DATA;
        DONE:   next_state = IDLE;
        default: next_state = IDLE;
    endcase
end

// 第三段:时序逻辑,输出
always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        data_out <= 0;
        done_sig <= 0;
    end else begin
        case (state)
            IDLE: begin
                data_out <= 0;
                done_sig <= 0;
            end
            DATA: begin
                data_out <= data_in;
                done_sig <= 0;
            end
            DONE: begin
                done_sig <= 1;
            end
        endcase
    end
end

为什么推荐三段式?因为它的结构清晰,状态转移和输出分离,代码可读性强,也方便后期维护。我在做PCIe控制器时,状态机有十几个状态,用三段式写,调试起来轻松很多。

核心要点:
  • 第一段:时序逻辑,只做状态寄存器的更新
  • 第二段:组合逻辑,根据当前状态和输入判断下一状态
  • 第三段:时序逻辑,根据当前状态产生输出

你想想看,如果所有逻辑都写在一个always块里,那代码得多乱?尤其是状态多的时候,自己都看不懂。

知识体系结构图

下面这张图,是我自己总结的HDL核心知识框架。你可以把它当作学习路线图。

HDL核心知识体系 语言基础 Verilog语法 VHDL语法 设计范式 模块/端口 always/assign 阻塞/非阻塞 参数化设计 entity/architecture process/signal std_logic_vector generic/port 组合逻辑 时序逻辑 状态机设计模式 一段式 两段式 三段式(推荐) Moore/Mealy

这张图把HDL的核心内容串起来了。从语言基础出发,分支出Verilog和VHDL两套语法体系,再延伸到组合逻辑和时序逻辑这两个设计范式,最后落到状态机这个实战技能上。你按照这个路径学,不会走偏。

我的建议: 刚开始学的时候,别纠结于语法细节。先写几个简单的计数器、移位寄存器,感受一下“代码变电路”的过程。等有了感觉,再回头啃语法书,效率会高很多。

好了,这一章的内容就到这里。记住,HDL不是编程语言,是硬件描述语言。你写的每一行代码,最终都会变成实实在在的门电路和触发器。带着这个意识去写代码,你的设计水平会提升得很快。


公众号:蓝海资料掘金营,微信deep3321