4. Verilog基础语法(上):模块结构、数据类型(wire/reg)、运算符、assign语句、always块

各位同学,欢迎来到第四讲。今天我们要啃的,是Verilog最核心的几块硬骨头。说实话,很多初学者一上来就被always块和reg搞晕了。别急,我当年也是这样过来的。咱们一个一个拆开讲,保证你听完就能上手写代码。

4.1 模块结构——你的第一个数字电路

Verilog里最基本的单元就是模块(module)。你可以把它想象成一个黑盒子,有输入引脚、输出引脚,里面装着你要实现的逻辑。

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

    // 内部逻辑写在这里

endmodule

看到没?模块以module开头,以endmodule结束。端口列表里声明了信号的方向(input/output)和类型(wire/reg)。

个人习惯:我写模块时,喜欢把端口声明和内部信号分开写。端口用一行一个,注释标清楚功能。这样过一个月回来再看,自己还能看懂。

4.2 数据类型——wire和reg,到底怎么选?

这是新手最容易踩坑的地方。我见过太多人把wire和reg混用,结果仿真对得上,上板就翻车。

4.2.1 wire——连线型

wire说白了就是一根导线。它只能被连续赋值(assign语句)或者模块的输入输出驱动。你不能在always块里给wire赋值。

wire [7:0] data_bus;
assign data_bus = addr & mask;  // 正确,连续赋值

4.2.2 reg——寄存器型

reg并不一定代表寄存器!它只是表示一个变量,可以在always块里被赋值。如果always块是时序逻辑(带时钟),综合出来就是寄存器;如果是组合逻辑(不带时钟),综合出来就是连线。

reg [3:0] temp;
always @(*) begin
    temp = a + b;  // 组合逻辑,综合出来是wire
end

我曾经踩过的坑:有一次写状态机,把状态变量声明成wire,然后在always块里赋值。仿真器报错我还纳闷了半天。记住:always块里赋值的变量,必须是reg类型。这是语法规定,没得商量。

特性 wire reg
默认值 高阻态z 不定态x
赋值方式 assign连续赋值 always块内赋值
综合结果 连线 寄存器或连线
端口类型 input/output/inout output(需加reg)

4.3 运算符——数字电路的数学基础

Verilog的运算符跟C语言很像,但有几个地方要特别注意。

4.3.1 算术运算符

+-*/%。注意:乘除法综合出来很耗资源。我一般只在仿真里用除法,上板的话会用移位或者查找表代替。

4.3.2 位运算符

&|^~。这些是硬件设计的核心,直接对应门电路。

4.3.3 归约运算符

这个很多人会忽略。比如&a表示把a的所有位相与,结果只有1位。我在做数据校验时经常用。

wire [7:0] data;
wire parity;
assign parity = ^data;  // 异或归约,生成奇偶校验位

4.3.4 拼接运算符

{}可以把多个信号拼在一起。这个太实用了,我几乎每个项目都会用到。

wire [3:0] high, low;
wire [7:0] combined;
assign combined = {high, low};  // 拼接成8位总线

4.4 assign语句——连续赋值的艺术

assign语句用来描述组合逻辑。它的特点是:只要右边表达式里的信号发生变化,左边就会立刻更新。

assign out = (sel) ? a : b;  // 二选一多路器

嗯,这里要注意:assign不能用在always块里,也不能对reg类型赋值。这是很多新手写代码报错的原因。

核心原则:assign描述的是硬件连线,不是软件赋值。你写assign的时候,脑子里要想的是「这根线连到了那个门的输出」,而不是「把值赋给变量」。

4.5 always块——时序逻辑的基石

always块是Verilog里最灵活、也最容易出错的构造。它有两种主要用法:

4.5.1 组合逻辑always块

always @(*) begin
    if (sel)
        out = a;
    else
        out = b;
end

敏感列表用@(*),表示所有输入信号变化都会触发。这种写法综合出来就是组合逻辑。

4.5.2 时序逻辑always块

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

敏感列表里有时钟沿和复位沿。注意这里用的是非阻塞赋值<=,这是时序逻辑的标准写法。

避坑指南:我曾经在一个项目里,把组合逻辑和时序逻辑混写在同一个always块里。结果仿真没问题,上板后时序一塌糊涂。后来我养成了习惯:组合逻辑用阻塞赋值=,时序逻辑用非阻塞赋值<=,永远不要混用

4.6 知识体系总览

下面这张图,是我根据多年经验总结的Verilog基础语法脉络。你照着这个框架去学,不会走偏。

Verilog基础语法(上) 模块结构 module/endmodule 数据类型 wire / reg 运算符 算术/位/归约/拼接 assign语句(连续赋值) 组合逻辑描述 always块(过程赋值) 组合/时序逻辑 wire类型 + assign 组合逻辑,连续驱动 不能用在always块内 reg类型 + always 组合逻辑用=,时序用<= 敏感列表决定触发方式 核心:理解硬件思维,区分组合逻辑与时序逻辑

这张图把今天讲的内容串起来了。你仔细看:模块是外壳,wire/reg是材料,assign和always是两种不同的「施工方式」。搞懂这些,你就能搭建出基本的数字电路了。

好了,今天的内容就到这里。记住我反复强调的那句话:写Verilog的时候,脑子里想的是硬件,不是软件。下一章我们会继续深入,讲阻塞赋值和非阻塞赋值的区别,那才是真正考验功底的地方。

课后练习:写一个4位加法器,用assign实现组合逻辑,再用always块实现带时钟的累加器。对比两种写法的综合结果。


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