第三讲:硬件描述语言基础(Verilog/VHDL)

各位同学好,今天我们聊聊硬件描述语言的基础。说实话,很多新手一上来就被Verilog和VHDL的选择问题给卡住了。我个人觉得,语言只是工具,核心是你能不能把硬件逻辑想清楚。不过既然要做工业级设计,这两者的差异还是得搞明白。

3.1 Verilog与VHDL语法对比

先说说我的个人经历。我最早用的是VHDL,后来转到Verilog。为什么?因为工业界,尤其是ASIC和高端FPGA领域,Verilog的生态更成熟。但VHDL在军工、航天领域依然很强势。你想想看,如果你去面试一家做卫星通信的公司,他们可能更看重VHDL。

下面这张表是我自己总结的,比较实用:

对比项 Verilog VHDL
语法风格 类C语言,简洁 类Ada语言,严谨
大小写敏感
数据类型 wire/reg,相对简单 std_logic/std_logic_vector,类型严格
设计抽象 偏向行为级描述 偏向结构级描述
库管理 无显式库声明 需要use ieee.std_logic_1164.all
并发语句 always块 process块

这里有个坑,我提醒一下。Verilog的reg类型,名字叫"寄存器",但它不一定综合成寄存器。比如在组合逻辑的always块里,reg只是表示一个变量,综合出来是连线。我曾经见过一个同事,看到reg就以为一定是触发器,结果综合出来的电路跟他想的不一样,排查了半天。

3.2 模块化设计思想

模块化设计,说白了就是"分而治之"。一个复杂系统,你不可能在一个文件里写完所有逻辑。我习惯把设计分成三层:

  • 顶层模块:负责例化子模块,连接信号,不写具体逻辑
  • 功能模块:实现具体功能,比如计数器、状态机、FIFO
  • 基础单元:比如D触发器、多路选择器,通常由工具自动推断

举个例子,一个简单的SPI控制器,我会这样划分:

// 顶层模块
module spi_top (
  input  clk, rst_n,
  input  spi_cs, spi_sck, spi_mosi,
  output spi_miso
);
  wire [7:0] tx_data, rx_data;
  wire       tx_valid, rx_valid;
  
  spi_ctrl u_ctrl (
    .clk(clk), .rst_n(rst_n),
    .spi_cs(spi_cs), .spi_sck(spi_sck),
    .spi_mosi(spi_mosi), .spi_miso(spi_miso),
    .tx_data(tx_data), .tx_valid(tx_valid),
    .rx_data(rx_data), .rx_valid(rx_valid)
  );
  
  spi_fifo u_fifo (
    .clk(clk), .rst_n(rst_n),
    .tx_data(tx_data), .tx_valid(tx_valid),
    .rx_data(rx_data), .rx_valid(rx_valid)
  );
endmodule

你看,顶层模块只做连线,不写逻辑。这样调试的时候,我可以单独仿真每个子模块,定位问题快很多。

3.3 常用数据类型

Verilog里最常用的就是wirereg。SystemVerilog引入了logic,算是把两者统一了。我个人的建议是:

  • wire:用于连线,比如模块之间的信号连接
  • reg:用于always块内的变量赋值
  • logic:SystemVerilog推荐,可以替代wire和reg

这里有个容易混淆的地方。我记得有一次,一个新手问我:"为什么我用了logic,综合出来还是组合逻辑?" 嗯,logic只是数据类型,它不决定综合结果。决定综合结果的是你怎么写always块。比如:

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

// 时序逻辑
always @(posedge clk) begin
  if (rst_n)
    q <= 0;
  else
    q <= d;
end

看到了吗?敏感列表不同,赋值方式不同(阻塞赋值 vs 非阻塞赋值),综合出来的电路就完全不同。

3.4 运算符与表达式

运算符这块,Verilog和C语言很像,但有几个地方要特别注意:

运算符类别 符号 说明
算术 + - * / % 乘除法综合出来资源很大,慎用
逻辑 && || ! 返回1位布尔值
按位 & | ~ ^ 按位操作,返回多位
归约 & | ^ 单目运算符,将多位归约为1位
移位 << >> 逻辑移位,空位补0
拼接 {a, b} 将多个信号拼接成更宽的向量

我特别想说说归约运算符。比如&data,它会把data的所有位做与操作,返回1位结果。这个在检测全1或者全0的时候非常有用。我曾经用|data来判断一个总线是否空闲,比写循环快多了。

3.5 代码风格规范

代码风格,说白了就是"让别人能看懂你的代码"。我见过太多"天书"代码了,变量名用a、b、c,没有注释,缩进乱七八糟。这种代码,别说别人看不懂,过两个月你自己都看不懂。

我总结了几条硬性规范:

  • 命名规范:信号名用下划线分隔,比如tx_data_valid;参数用大写,比如DATA_WIDTH
  • 缩进:统一用2个空格或4个空格,不要混用Tab和空格
  • 注释:每个模块头部写清楚功能、输入输出、修改历史
  • 代码块:begin/end对齐,always块内每个if-else对齐
  • 避免魔法数字:用parameter定义常量,比如parameter CLK_DIV = 100_000

这里我画了一张图,把本章的知识体系串起来:

硬件描述语言基础 Verilog vs VHDL 模块化设计 数据类型 运算符 语法对比表 顶层/功能/基础 wire/reg/logic 算术/逻辑/归约 VHDL更严谨 SPI控制器示例 组合vs时序 归约运算符 代码风格规范:命名、缩进、注释、参数化

核心要点:语言只是工具,模块化设计才是灵魂。代码风格决定了你的设计能不能被团队理解和维护。

个人小技巧:我习惯在代码开头写一个// Revision History的注释块,记录每次修改的内容和日期。这样出了问题,可以快速回溯。

注意:不要滥用always @(posedge clk)。每个always块只描述一个功能,比如一个计数器、一个状态机。把多个功能塞进一个always块,代码会变得难以阅读和调试。

好了,这一讲的内容就到这里。记住,写代码不是写作文,是画电路。每一行代码,你都要能想象出它综合出来是什么样子。下次我们聊聊组合逻辑和时序逻辑的深入对比。


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