4. Verilog基础(一):模块结构、数据类型、运算符、assign语句

各位同学,咱们今天开始正式进入Verilog的世界。说实话,很多初学者一上来就被各种语法吓住了,觉得这玩意儿跟C语言差不多,结果写出来的代码综合出来一堆莫名其妙的电路。我当年也踩过这个坑,所以今天咱们慢慢来,先把最核心的几个概念掰扯清楚。

4.1 模块结构——Verilog的“黑盒子”

Verilog里最基本的设计单元就是模块(module)。你可以把它想象成一个黑盒子,有输入、有输出,里面装着具体的逻辑电路。

一个完整的模块长这样:

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

    // 这里写逻辑代码
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            count <= 4'b0000;
        else
            count <= count + 1'b1;
    end

endmodule

注意看,moduleendmodule就像一对括号,把整个设计包起来。端口列表里声明了信号的方向(input/output/inout)和类型(wire/reg)。

我的习惯:端口声明时,我总喜欢把时钟和复位放在最前面,数据信号放后面。这样别人看你的代码,一眼就能找到关键信号。另外,复位信号我习惯用rst_n命名,后面的_n表示低电平有效,这是行业惯例。

4.2 数据类型——wire和reg的区别

这是新手最容易搞混的地方。我直接说结论:

  • wire:连线型,用于组合逻辑。它本身不存储值,只是把两个点连起来。
  • reg:寄存器型,用于时序逻辑。它可以在时钟沿到来时保持住值。

但注意!reg并不一定真的综合成寄存器。在always块里赋值的变量,必须声明为reg,哪怕它只是组合逻辑。这是语法规定,别问为什么,记住就行。

举个例子:

wire       a, b;      // 组合逻辑连线
wire       sum;       // 组合逻辑输出
reg        q;         // 时序逻辑寄存器

assign sum = a ^ b;   // 用assign驱动wire

always @(posedge clk) begin
    q <= sum;         // 在时钟上升沿,把sum的值锁存到q
end
我曾经踩过的坑:always块里给wire类型赋值,编译器直接报错。后来养成了习惯:只要是在alwaysinitial里赋值的变量,一律用reg;用assign驱动的,一律用wire

4.3 运算符——跟C语言很像,但有坑

Verilog的运算符大部分跟C语言一样,但有几个特别容易出问题的地方,我重点说一下。

运算符类别 符号 说明
算术运算符 + - * / % 注意:除法综合出来很费资源,尽量少用
关系运算符 > < >= <= == != 结果返回1或0
逻辑运算符 && || ! 用于条件判断,结果也是1或0
位运算符 & | ~ ^ 按位操作,注意跟逻辑运算符区分
拼接运算符 { } 把多个信号拼成一个宽信号
缩减运算符 & | ^ 对向量的所有位做与、或、异或

这里有个经典陷阱:逻辑运算符位运算符的区别。比如:

wire [3:0] a = 4'b1010;
wire [3:0] b = 4'b1100;

wire logic_result = a && b;   // 逻辑与:a非零且b非零,结果为1
wire bit_result   = a & b;     // 位与:按位操作,结果为4'b1000

你看,&&把整个向量当成一个布尔值,而&是对每一位分别操作。我见过有人把&&写成&,结果仿真波形完全不对,查了半天才发现是这里的问题。

我个人建议:写条件判断时,比如if (a && b),一定要用逻辑运算符。写数据通路时,比如assign c = a & b,用位运算符。这两个搞混了,综合出来的电路会完全不一样。

4.4 assign语句——连续赋值

assign是Verilog里最常用的语句之一,它用来描述组合逻辑。说白了,就是输入一变,输出立刻跟着变,没有延迟(当然,实际电路有门延迟,但仿真里是立即变化的)。

基本语法:

assign 信号名 = 表达式;

举个例子,实现一个半加器:

module half_adder (
    input  wire a, b,
    output wire sum, carry
);

    assign sum   = a ^ b;   // 异或,得到和
    assign carry = a & b;   // 与,得到进位

endmodule

注意,assign的左边必须是wire类型,不能是reg。而且assign语句是并行执行的,跟书写顺序无关。你想想看,硬件电路本来就是同时工作的,所以代码里写100条assign,它们也是同时赋值。

我的经验:写组合逻辑时,能用assign就用assign,少用always @(*)。因为assign更直观,综合工具优化起来也更高效。只有在需要if-elsecase这种复杂逻辑时,才考虑用always块。

4.5 知识体系总览

下面这张图,把今天讲的内容串起来了。你仔细看看,模块是外壳,里面装着数据类型、运算符和赋值语句,这三者共同构成了Verilog的基础。

Verilog基础(一)知识体系 模块结构 module / endmodule 数据类型 wire / reg 运算符 算术/逻辑/位/拼接 assign 连续赋值 组合逻辑电路设计 加法器 / 多路选择器 / 译码器 注:三个基础模块通过assign语句驱动组合逻辑

嗯,今天的内容就到这里。模块结构、数据类型、运算符、assign语句,这四个东西是Verilog的基石。你把这几个搞明白了,后面学时序逻辑、状态机什么的,就会轻松很多。

记住,写Verilog的时候,脑子里要时刻想着硬件电路,而不是软件程序。你写的每一行代码,最终都会变成实实在在的门电路。这个思维转变过来了,你就入门了。

课后小练习:用assign语句写一个4位全加器,输入两个4位数a和b,以及进位cin,输出和sum以及进位cout。试试看,写完了仿真一下,看看波形对不对。

专注资料整理