4、硬件描述语言基础:Verilog/VHDL基础语法、模块化设计思想、组合逻辑与时序逻辑

各位同学,咱们今天聊聊硬件描述语言。说白了,这就是硬件工程师的“编程语言”。你写的是代码,但最终跑在芯片上的是电路。这个思维转换,是入门的第一道坎。

我个人习惯用Verilog,因为它语法更灵活,写起来像C语言。VHDL则更严谨,像Ada。但不管用哪个,核心思想是一样的:描述硬件,而不是描述算法

4.1 Verilog基础语法:从“线”到“寄存器”

先看一个最简单的例子。我想实现一个与门:

module and_gate (
    input  wire a,      // 输入信号,用wire类型
    input  wire b,
    output wire y       // 输出信号
);

assign y = a & b;      // 连续赋值,组合逻辑

endmodule

这里有个关键点:assign 语句是持续赋值的。只要a或b变了,y立刻跟着变。这跟软件里的“赋值”完全不同——软件是顺序执行,硬件是并行执行。

我在项目中遇到过新手把 assign 当成软件里的“=”来用,结果仿真死活不对。嗯,这里要注意:Verilog里的“=”是阻塞赋值,用于组合逻辑;而“<=”是非阻塞赋值,用于时序逻辑。这个区别,后面会细讲。

4.2 VHDL基础语法:更“啰嗦”但更安全

同样的与门,用VHDL写是这样的:

library IEEE;
use IEEE.STD_LOGIC_1164.ALL;

entity and_gate is
    Port ( a : in  STD_LOGIC;
           b : in  STD_LOGIC;
           y : out STD_LOGIC);
end and_gate;

architecture Behavioral of and_gate is
begin
    y <= a and b;   -- 并发信号赋值
end Behavioral;

你看,VHDL把“实体”(entity)和“架构”(architecture)分开了。实体定义接口,架构定义功能。这种分离设计,在大型项目中特别有用——接口稳定了,内部随便改。

我个人觉得VHDL的语法虽然啰嗦,但可读性更好。尤其是 STD_LOGIC 类型,它不止有'0'和'1',还有'Z'(高阻)、'X'(未知)等状态。这在仿真时能帮你发现很多潜在问题。

4.3 模块化设计思想:搭积木的艺术

你想想看,一个复杂的芯片可能有上亿个晶体管。如果全写在一个文件里,那调试起来简直是噩梦。所以,模块化是硬件设计的基石

模块化设计有几个原则:

  • 高内聚、低耦合:每个模块只做一件事,接口尽量简单
  • 层次化:顶层模块只负责连接子模块,不实现具体逻辑
  • 可复用:写好的模块,下次换个项目还能用

举个例子,我要做一个计数器模块:

module counter (
    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

这个模块只做一件事:在时钟上升沿计数。复位信号是低电平有效(rst_n 的 _n 后缀是我个人的命名习惯,一看就知道是低有效)。

然后顶层模块可以这样调用它:

module top (
    input  wire       clk,
    input  wire       rst_n,
    output wire [7:0] led
);

counter u_counter (
    .clk   (clk),
    .rst_n (rst_n),
    .count (led)
);

endmodule

你看,顶层模块就像搭积木一样,把计数器模块“实例化”进来。这种设计方式,让代码结构清晰,也方便团队协作。我曾经在一个项目中,三个人同时开发不同的模块,最后在顶层一拼接,一次仿真通过——那种感觉,真的很爽。

4.4 组合逻辑与时序逻辑:硬件的“阴阳”两面

这是硬件设计最核心的概念。我刚开始学的时候也迷糊过,后来想明白了一个道理:

  • 组合逻辑:输出只取决于当前输入,没有记忆功能。比如与门、或门、加法器。
  • 时序逻辑:输出不仅取决于当前输入,还取决于之前的状态。比如触发器、计数器、状态机。

用Verilog实现组合逻辑,通常用 assignalways @(*)

// 组合逻辑:用 assign
assign sum = a + b;

// 组合逻辑:用 always @(*)
always @(*) begin
    if (sel)
        out = a;
    else
        out = b;
end

时序逻辑则必须用 always @(posedge clk)

// 时序逻辑:D触发器
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        q <= 1'b0;
    else
        q <= d;
end

这里有个避坑指南:我曾经犯过一个错误,在时序逻辑的 always 块里用了阻塞赋值(=)。结果仿真看起来没问题,但综合出来的电路多了一级不必要的锁存器。后来我养成了习惯:时序逻辑用非阻塞赋值(<=),组合逻辑用阻塞赋值(=)。这个规则,请你一定记住。

4.5 知识体系总览

下面这张图,是我自己总结的硬件描述语言学习路径。你可以把它当成一张地图,随时回来看看自己走到哪了。

硬件描述语言知识体系 基础语法 Verilog: module/endmodule, assign, always VHDL: entity/architecture, signal, process 模块化设计思想 高内聚低耦合 | 层次化设计 | 可复用 实例化、端口映射、参数化 组合逻辑 vs 时序逻辑 组合: assign, always @(*), 阻塞赋值(=) 时序: always @(posedge clk), 非阻塞赋值(<=) 实践应用:仿真验证 → 综合 → 布局布线 → 流片

核心要点总结

  • Verilog和VHDL都是硬件描述语言,不是编程语言——你写的是电路
  • 模块化设计是工程化的基础,别把所有代码塞进一个文件
  • 组合逻辑没有记忆,时序逻辑有记忆——这是硬件设计的“阴阳”两面
  • 阻塞赋值(=)用于组合逻辑,非阻塞赋值(<=)用于时序逻辑——这个规则别搞反

我的小技巧:刚开始学的时候,可以先用Verilog写一些简单的组合逻辑(比如加法器、多路选择器),然后再加时钟变成时序逻辑(比如计数器、移位寄存器)。这样一步步来,思维转换会顺畅很多。

注意:千万不要在 always @(posedge clk) 块里用阻塞赋值!我曾经因为这个bug,花了两天时间才找到问题——仿真波形看起来完全正确,但综合出来的电路多了一个锁存器。后来我养成了习惯:写代码前先想清楚,这是组合逻辑还是时序逻辑,然后再动笔。

好了,这一章的内容就到这里。记住:硬件描述语言的核心不是语法,而是电路思维。你写的每一行代码,最终都会变成芯片上的晶体管。带着这个意识去学习,你会发现很多问题都迎刃而解。

专注资料整理