第二讲:硬件描述语言基础——Verilog HDL基本结构、模块化设计、并行与顺序语句

各位同学,欢迎来到第二讲。今天咱们聊聊Verilog HDL的基础。说实话,很多初学者一上来就被各种语法吓住了。其实没那么复杂。你想想看,Verilog说白了就是描述硬件的一种语言。它跟C语言最大的区别是什么?并行!硬件天生就是并行的,这一点必须刻在脑子里。

2.1 Verilog HDL的基本结构

一个完整的Verilog模块长什么样?我直接给你看个例子:

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

看到了吗?module和endmodule是模块的边界。端口列表里,input、output、inout三种方向。我个人习惯把时钟和复位放在端口列表的最前面,这样一眼就能看到关键信号。

这里有个坑——wire和reg的区别。很多新手搞混。我简单说:

  • wire:连线型,用于组合逻辑。比如assign语句的输出。
  • reg:寄存器型,用于时序逻辑。比如always块里的赋值。

但注意!在always块里赋值的变量,必须声明为reg。哪怕它综合出来只是一根线。嗯,这里要记住:reg不一定是寄存器,它只是语法上的要求。

核心要点:Verilog模块 = 端口定义 + 内部信号 + 功能描述。三者缺一不可。

2.2 模块化设计——把大问题拆小

做FPGA项目,最忌讳的就是把所有逻辑写在一个模块里。我曾经接手过一个项目,一个模块写了三千行。改一个功能,牵一发动全身,调试了整整两周。从那以后,我坚持模块化设计。

模块化设计的核心思想:高内聚、低耦合。每个模块只做一件事,做好一件事。

举个例子,一个图像采集系统可以拆成这样:

// 顶层模块
module image_capture_top (
    input  wire        clk,
    input  wire        rst_n,
    // 摄像头接口
    input  wire        cam_pclk,
    input  wire        cam_vsync,
    input  wire        cam_href,
    input  wire [7:0]  cam_data,
    // 显示接口
    output wire        vga_hsync,
    output wire        vga_vsync,
    output wire [11:0] vga_rgb
);

// 实例化摄像头驱动模块
wire [7:0] pixel_data;
wire       pixel_valid;
camera_driver u_cam_drv (
    .pclk      (cam_pclk),
    .vsync     (cam_vsync),
    .href      (cam_href),
    .data_in   (cam_data),
    .pixel_out (pixel_data),
    .valid     (pixel_valid)
);

// 实例化帧缓存模块
wire [11:0] frame_data;
frame_buffer u_fbuf (
    .wr_clk   (cam_pclk),
    .wr_data  (pixel_data),
    .wr_en    (pixel_valid),
    .rd_clk   (clk),
    .rd_data  (frame_data)
);

// 实例化VGA显示模块
vga_driver u_vga (
    .clk      (clk),
    .rst_n    (rst_n),
    .frame_in (frame_data),
    .hsync    (vga_hsync),
    .vsync    (vga_vsync),
    .rgb      (vga_rgb)
);

endmodule

你看,顶层模块只做连线。每个子模块独立开发、独立测试。出了问题,定位也快。这就是模块化的好处。

我的经验:每个模块的代码量控制在200行以内。超过这个数,就该考虑拆分了。另外,模块的接口信号不要超过20个,否则可读性会急剧下降。

2.3 并行与顺序语句——硬件思维的起点

这是Verilog最核心、也最容易搞混的地方。我刚开始学的时候,也在这里栽过跟头。

2.3.1 并行语句

硬件里,所有模块、所有assign语句、所有always块,都是同时执行的。没有谁先谁后。你看下面这段:

assign a = b & c;
assign d = e | f;
assign g = a ^ d;

这三条语句是同时执行的。g的值取决于a和d的当前值。如果你用C语言的思维去理解,会觉得g用的是a和d的旧值。但在硬件里,没有「旧值」这个概念——所有赋值同时发生。

为什么会这样?因为硬件里每个逻辑门都是独立工作的。时钟沿一到,所有寄存器同时更新。这就是并行。

2.3.2 顺序语句

Verilog里也有顺序执行的地方——always块内部。但注意,只有用begin...end包裹的语句才是顺序执行的。

always @(posedge clk) begin
    temp <= a + b;
    result <= temp + c;
end

这段代码里,temp和result是同时更新的。因为用了非阻塞赋值<=。如果你用阻塞赋值=,那才是真正的顺序执行:

always @(*) begin
    temp = a + b;
    result = temp + c;
end

这里result会先计算temp,再用temp加c。顺序执行。

重要提醒:时序逻辑(带时钟的always块)一定要用非阻塞赋值<=。组合逻辑(不带时钟的always块)用阻塞赋值=。混用会导致仿真和综合结果不一致。我曾经因为这个bug,浪费了整整两天时间。

2.4 知识体系总览

下面这张图,把本章的核心知识点串起来了。你看一眼,心里就有数了:

Verilog HDL 基础知识体系 基本结构 • module/endmodule • input/output/inout • wire vs reg • 参数化设计 • 端口声明 模块化设计 • 高内聚低耦合 • 模块实例化 • 接口标准化 • 分层设计 • 复用与封装 并行与顺序 • 并行语句 • 顺序语句 • 阻塞赋值 = • 非阻塞赋值 <= • always块用法 三者关系 基本结构是骨架 → 模块化设计是方法论 → 并行与顺序是灵魂 掌握这三块,Verilog入门就完成了80%

2.5 实战中的避坑指南

最后,分享几个我踩过的坑:

  1. 端口方向搞反——input写成output,仿真能过,综合报错。检查三遍端口列表再往下写。
  2. 敏感列表不全——组合逻辑的always块,敏感列表漏了信号。仿真结果对,综合结果错。用always @(*)最保险。
  3. 多驱动问题——同一个信号在两个always块里赋值。综合会报multi-driver错误。一个信号只能在一个always块里赋值。
  4. 锁存器意外生成——组合逻辑里,if没有else,case没有default。综合会生成锁存器。我刚开始做项目时,因为这个原因导致芯片面积大了30%。

我的习惯:每写完一个模块,先做语法检查,再做功能仿真。仿真通过后,再看综合报告。三步走,能避免90%的低级错误。

好了,这一讲的内容就到这里。记住:Verilog不是编程语言,是硬件描述语言。用硬件思维去理解它,你会发现它其实很简单。

可可精品资料库