一、硬件描述语言基础(Verilog):从模块到赋值

各位同学,欢迎来到Verilog的世界。说实话,很多初学者一上来就被各种语法吓住了。其实没那么复杂。你想想看,Verilog说白了就是描述硬件的一种语言。我们不是在写软件,而是在画电路。

今天这一章,我带你搞定四个核心概念:模块结构、端口定义、常用数据类型、赋值语句。掌握了这些,你就拿到了FPGA设计的入场券。

1.1 模块(module)结构——Verilog的基本单元

在Verilog里,模块就是最小的设计单元。每个模块代表一个硬件电路块。我习惯把模块想象成一个黑盒子:有输入、有输出,里面是具体的逻辑实现。

一个完整的模块长这样:

module 模块名 (
    端口列表
);
    // 端口声明
    // 内部信号声明
    // 逻辑功能描述
endmodule

嗯,这里要注意:moduleendmodule必须成对出现。少了任何一个,编译器都会报错。我曾经有个学生,写了半天代码,结果编译一直报错,最后发现是endmodule拼成了endmodulee——多了一个e。这种低级错误,排查起来真的很浪费时间。

核心要点:每个模块必须有一个唯一的模块名,且文件名最好和模块名保持一致。这是行业惯例,也是好习惯。

1.2 端口定义——input、output、inout

端口就是模块和外界通信的通道。有三种类型:

端口类型 方向 说明
input 输入 数据从外部流入模块内部
output 输出 数据从模块内部流出到外部
inout 双向 既可以输入也可以输出(常用于总线)

举个例子:

module counter (
    input        clk,      // 时钟信号
    input        rst_n,    // 复位信号(低有效)
    output reg   [3:0] cnt // 4位计数器输出
);
    // 内部逻辑
endmodule

我个人习惯把时钟和复位放在端口列表的最前面。这样一眼就能看到关键信号。另外,inout类型我建议新手慎用。为什么?因为双向端口涉及到三态门控制,处理不好容易出问题。我在项目中就遇到过因为inout端口没处理好,导致总线冲突,整个板子直接冒烟——嗯,夸张了点,但确实烧了一个IO口。

避坑指南:inout端口必须配合三态控制信号使用。如果不需要双向功能,尽量用input或output代替。

1.3 常用数据类型——wire、reg、parameter

Verilog里数据类型很多,但最常用的就三个:wireregparameter

wire(线网类型)

wire相当于物理连线。它本身不存储值,只是传递信号。在assign语句中,赋值号左边必须是wire类型。

wire a, b, c;
assign c = a & b;  // c是wire类型,由a和b的与运算驱动

reg(寄存器类型)

reg代表寄存器,可以存储值。注意:reg不一定综合成寄存器,它只是行为描述中的变量。在always块中,被赋值的变量必须是reg类型。

reg [7:0] data_reg;  // 8位寄存器
always @(posedge clk) begin
    data_reg <= data_in;
end

这里有个坑:很多新手以为reg就一定是寄存器。其实不然。如果reg在组合逻辑中使用(比如always@(*)),它综合出来只是连线。我曾经在面试时问过这个问题,十个有八个答错。

parameter(参数)

parameter用来定义常量。它的好处是让代码更灵活、可复用。

parameter WIDTH = 8;  // 定义数据宽度为8
reg [WIDTH-1:0] data; // 使用参数定义位宽

个人经验:我建议把所有可能变化的常量都用parameter定义。比如数据位宽、计数器最大值、状态编码等。这样后期修改只需要改一个地方,不用满篇找数字。

1.4 赋值语句——assign和always

赋值语句是Verilog的灵魂。主要有两种:连续赋值(assign)过程赋值(always)

assign(连续赋值)

assign描述组合逻辑。它右边的表达式一旦变化,左边立即更新。说白了就是:输入变了,输出马上跟着变。

assign sum = a + b;       // 加法器
assign flag = (cnt == 0); // 比较器

assign语句的左边必须是wire类型。这个规则很严格,别搞混了。

always(过程赋值)

always块可以描述组合逻辑,也可以描述时序逻辑。区别在于敏感列表:

  • 组合逻辑:always @(*) 或 always @(a, b, c)
  • 时序逻辑:always @(posedge clk) 或 always @(negedge rst_n)

看个例子:

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

// 组合逻辑:多路选择器
always @(*) begin
    case(sel)
        2'b00: out = a;
        2'b01: out = b;
        2'b10: out = c;
        default: out = d;
    endcase
end

注意看:时序逻辑我用的是<=(非阻塞赋值),组合逻辑我用的是=(阻塞赋值)。这个区别很重要。非阻塞赋值在always块结束时才更新,而阻塞赋值是立即更新。用错了,仿真结果会完全不一样。

黄金法则:时序逻辑用非阻塞赋值(<=),组合逻辑用阻塞赋值(=)。这是FPGA设计的铁律,违反它,你的仿真和综合结果可能天差地别。

知识体系总览

下面这张图,是我为你整理的本章知识结构。建议你保存下来,学完本章后对照着复习:

Verilog基础:四大核心 模块结构 module...endmodule 端口定义 input/output/inout 数据类型 wire/reg/parameter 赋值语句 assign/always 端口列表声明 内部信号声明 逻辑功能描述 input:输入 output:输出 inout:双向 wire:连线 reg:寄存器 parameter:参数 assign:连续赋值 always:过程赋值 阻塞/非阻塞赋值 💡 记住:模块是骨架,端口是接口,数据类型是材料,赋值语句是工具 四者结合,就能搭建出任意数字电路

好了,这一章的内容就到这里。你可能会觉得信息量有点大,但没关系。Verilog的学习就像搭积木,先把这几块基础砖头拿稳了,后面盖高楼就顺了。下一章我们会深入讨论组合逻辑和时序逻辑的具体写法,到时候你会发现自己已经能看懂大部分代码了。


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