第二讲:Verilog语言入门——模块结构、端口定义、数据类型与基本语句

各位同学,欢迎来到第二讲。今天我们要聊的是Verilog语言最基础的部分。说实话,很多初学者一上来就被各种语法细节吓住了,其实大可不必。你想想看,Verilog说白了就是描述硬件的一种语言,它的核心思想很简单:告诉工具,你的电路长什么样。

我个人习惯把这一讲称为「搭积木入门」。因为模块就像积木块,端口就是积木的接口,而wire和reg就是积木块之间的连线。好,我们开始。

一、模块结构——你的第一个积木块

一个Verilog模块,从结构上看,就像是一个黑盒子。它有名字,有接口,有内部实现。我刚开始学的时候,老师告诉我一句话,我一直记到现在:「模块就是硬件电路的封装」。

来看一个最简单的模块例子:

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

    // 内部逻辑
    assign c = a & b;

endmodule

这个模块做了什么?它把两个输入信号a和b做了与运算,结果送到输出c。就这么简单。

模块的基本结构包括:

  • 模块名:就是module后面的名字,比如my_first_module
  • 端口列表:括号里定义的所有输入输出
  • 内部逻辑:module和endmodule之间的所有代码
小提示:模块名最好用有意义的英文单词组合,别用a、b、c这种。我在项目中见过一个模块叫module1,三个月后连写它的人自己都忘了是干嘛的。

二、端口定义——给积木块装上接口

端口就是模块与外界通信的通道。Verilog里端口有三种方向:

方向 关键字 说明
输入 input 信号从外部流入模块内部
输出 output 信号从模块内部流出到外部
双向 inout 信号可以双向流动(初学者先了解即可)

端口定义有两种写法。我个人习惯用ANSI风格,就是上面那种把端口类型和数据类型写在一起的:

module counter (
    input  wire       clk,
    input  wire       rst_n,
    output reg  [7:0] count
);

还有一种老式的写法,把端口列表和类型分开写。嗯,这里要注意,现在新工具都支持ANSI风格,咱们就用这种,清晰明了。

三、数据类型——wire和reg

这是初学者最容易搞混的地方。我当年也踩过这个坑,所以今天咱们把它彻底说清楚。

wire(线网类型):

  • 说白了就是一根导线
  • 只能被assign语句驱动
  • 不能存储值,值由驱动源决定
  • 默认值是z(高阻态)

reg(寄存器类型):

  • 可以理解为一个存储单元
  • 只能在always块中被赋值
  • 可以保持上一次的值
  • 默认值是x(未知态)
核心记忆法:wire是「连线」,reg是「寄存器」。但注意!reg不一定会综合成寄存器,它也可能综合成组合逻辑。这个我后面会详细讲。

我曾经犯过一个错误:在always块里给wire赋值,结果编译报错。后来才明白,wire和reg的使用场景是严格区分的。

四、assign语句——连续赋值

assign语句,说白了就是「一直连着」。只要右边表达式的值变了,左边wire的值立刻跟着变。

assign c = a & b;
assign sum = a + b;
assign data_out = (sel) ? data_a : data_b;

这里有个关键点:assign的左边必须是wire类型。右边可以是wire、reg或者常量。

我建议初学者把assign想象成「硬件连线」。你写assign c = a & b,就相当于在电路里把a和b接到一个与门上,输出接到c上。就这么直观。

五、always块——过程赋值

always块是Verilog里最灵活、也最容易出错的语句。它的基本格式是:

always @(敏感列表) begin
    // 过程赋值语句
end

敏感列表决定了always块什么时候执行。常见的写法有:

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

来看一个典型的时序逻辑例子:

always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        count <= 8'd0;
    end else begin
        count <= count + 1'b1;
    end
end

这个代码描述了一个带异步复位的8位计数器。注意我用了<=(非阻塞赋值),这是时序逻辑的标准写法。

避坑指南:我曾经在同一个always块里混用阻塞赋值和非阻塞赋值,结果仿真结果和综合结果完全不一样。记住:时序逻辑用<=,组合逻辑用=,别混着用。

六、知识体系总览

为了让大家对今天的内容有个整体认识,我画了一张图:

Verilog模块结构 端口定义 input / output / inout 数据类型 wire / reg 内部逻辑 assign / always 模块 = 端口定义 + 数据类型声明 + 内部逻辑 wire用assign驱动,reg在always中赋值 module endmodule

这张图把今天的内容串起来了。你看,一个Verilog模块,就是由端口定义、数据类型声明和内部逻辑三部分组成的。端口决定了模块怎么跟外界通信,数据类型决定了信号怎么存储和传递,而assign和always则描述了具体的逻辑行为。

七、实战小贴士

最后,分享几个我这些年积累的小经验:

  1. 写代码前先画框图。我习惯在纸上先画出模块的输入输出,再动手写代码。这样不容易漏掉端口。
  2. wire和reg的选择:如果你不确定用哪个,先问自己「这个信号是在always块里赋值吗?」如果是,用reg;如果不是,用wire。
  3. 敏感列表要完整:写组合逻辑时,用always @(*),别手动列敏感信号。我见过有人漏掉一个信号,仿真结果怎么都不对。
  4. 代码风格要统一:比如端口定义用ANSI风格,赋值操作符统一用<=或=,别混着来。
我的习惯:每个模块开头都写一段注释,说明这个模块是干嘛的、输入输出是什么。三个月后你回头看自己的代码,会感谢现在的自己。

好了,这一讲的内容就到这里。记住,Verilog入门不难,关键是理解「硬件思维」——你写的每一行代码,最终都会变成实实在在的电路。下一讲我们会深入讨论组合逻辑和时序逻辑的区别,到时候见。

可可精品资料库