第三讲:硬件描述语言(HDL)入门——Verilog与VHDL的选择、Verilog基本语法结构

各位同学,欢迎来到第三讲。

从这一讲开始,我们就要真正动手写代码了。说实话,很多初学者觉得FPGA难,其实不是难在硬件本身,而是卡在了「怎么把脑子里的电路图,翻译成电脑能懂的文本」这一步。今天我们就来解决这个问题。

3.1 Verilog还是VHDL?我的选择

这是个老生常谈的问题,也是每个新手都会纠结的问题。我当年入行时也纠结过,还特意花了两周时间把VHDL啃了一遍。结果呢?进了项目组发现大家都在用Verilog。嗯,这就是现实。

简单说下两者的区别:

对比项 Verilog VHDL
语法风格 类似C语言,简洁 类似Ada,严谨啰嗦
学习曲线 上手快,容易写 门槛高,但不易出错
行业应用 美国、中国、IC设计主流 欧洲军工、航天领域较多
数据类型 reg/wire,相对简单 signal/variable,类型严格
代码量 同样功能,代码更短 同样功能,代码更长

我个人建议:如果你是做工业控制、通信、图像处理这些主流方向,直接学Verilog。为什么?因为市面上90%的IP核、参考设计、开源项目都是Verilog写的。你想想看,拿到一个现成的模块,结果发现是VHDL写的,你还得先看懂再改,多麻烦。

当然,如果你以后打算去欧洲做军工项目,或者进某些特定的研究所,VHDL还是得补上。但作为入门,Verilog足够了。

我的小经验:我见过不少工程师,Verilog和VHDL都会,但真正写项目时只用一种。说白了,语言只是工具,电路思维才是核心。别在选语言上浪费太多时间。

3.2 Verilog的基本语法结构

好了,我们直接上手。Verilog代码的基本单元叫「模块(module)」。你可以把它想象成一个黑盒子——有输入、有输出、里面装着具体的逻辑电路。

3.2.1 模块的定义

一个最简单的模块长这样:

module and_gate (
    input  wire a,      // 输入端口a
    input  wire b,      // 输入端口b
    output wire y       // 输出端口y
);

    assign y = a & b;   // 连续赋值,实现与门

endmodule

看到了吗?结构非常清晰:

  • module 关键字后面跟模块名(and_gate)
  • 括号里是端口列表,声明了输入输出
  • endmodule 表示模块结束

我在项目中遇到过很多新手,写模块时经常忘记写端口方向。结果综合时报错,找了半天才发现是input/output写漏了。嗯,这种坑踩一次就够了。

3.2.2 端口声明

端口有三种方向:

  • input:输入端口,数据从外部流入模块内部
  • output:输出端口,数据从模块内部流出
  • inout:双向端口,既能输入也能输出(比如I2C的数据线)

端口的数据类型主要有两种:

  • wire:线网类型,用于组合逻辑赋值(assign语句)
  • reg:寄存器类型,用于时序逻辑赋值(always语句)
注意:很多初学者以为reg就一定是寄存器,其实不是。reg只是表示一个变量,在always块里被赋值。它综合出来可能是寄存器,也可能是组合逻辑。我曾经就被这个坑过——写了一个组合逻辑的always块,以为会综合成寄存器,结果综合出来一堆LUT,时序完全不对。

3.2.3 赋值语句

Verilog里有两种赋值方式,这是最核心的概念之一:

1. 连续赋值(assign)

用于组合逻辑,输入一变,输出立刻变。就像一根导线:

assign y = a & b;   // a和b变化时,y立即变化

2. 过程赋值(always块内)

用于时序逻辑或组合逻辑,需要放在always块里:

// 时序逻辑:上升沿触发
always @(posedge clk) begin
    q <= d;        // 非阻塞赋值,在时钟上升沿更新
end

// 组合逻辑:电平敏感
always @(*) begin
    y = a & b;     // 阻塞赋值,立即更新
end

这里有个关键点:阻塞赋值(=)和非阻塞赋值(<=)。我刚开始学的时候,觉得这两个差不多,结果写出来的电路仿真和实际综合完全不一样。

黄金法则
- 时序逻辑(always @(posedge clk))用 <=(非阻塞赋值)
- 组合逻辑(always @(*) 或 assign)用 =(阻塞赋值)

这个规则我建议你死记硬背,不要问为什么。等你写了几万行代码,自然就懂了。

3.3 一个完整的例子:D触发器

光说不练假把式。我们写一个真正的D触发器,带异步复位:

module d_flip_flop (
    input  wire       clk,    // 时钟
    input  wire       rst_n,  // 异步复位,低有效
    input  wire       d,      // 数据输入
    output reg        q       // 数据输出
);

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            q <= 1'b0;       // 复位时输出0
        end else begin
            q <= d;          // 时钟上升沿采样d
        end
    end

endmodule

这个例子包含了:

  • 模块定义和端口声明
  • 时序逻辑的always块写法
  • 异步复位的处理(敏感列表里同时有clk和rst_n)
  • 非阻塞赋值的使用

你想想看,这个模块综合出来是什么?就是几个查找表加一个触发器。硬件描述语言,说白了就是用文本描述电路结构。你写的每一行代码,最终都会变成实实在在的硅片上的晶体管。

避坑指南:我曾经在项目里写过一个模块,复位信号忘了加在敏感列表里。仿真时一切正常,但上板后发现复位根本不起作用。查了两天才发现是敏感列表写漏了。所以,异步复位的信号一定要写在敏感列表里,这是血的教训。

3.4 小结

今天我们讲了三个核心点:

  1. Verilog vs VHDL:入门选Verilog,主流方向够用了
  2. 模块结构:module - 端口声明 - 内部逻辑 - endmodule
  3. 赋值方式:连续赋值用assign,时序逻辑用非阻塞赋值,组合逻辑用阻塞赋值

下一讲我们会深入讲always块的更多用法,以及如何用Verilog描述更复杂的电路。记住,写Verilog不是写软件,你脑子里想的应该是「这个信号怎么走线」「这个触发器什么时候采样」。保持这个思维,你很快就能上手。

好了,今天就到这里。有问题随时交流。


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