3. Verilog基础(上):模块结构、数据类型、运算符、assign语句、always块、阻塞与非阻塞赋值

各位同学,今天我们来啃Verilog的硬骨头。说实话,很多初学者觉得Verilog就是写代码,跟C语言差不多。但我要告诉你——千万别这么想。Verilog描述的是硬件,是实实在在的电路。你写的每一行代码,最终都会变成门、触发器和连线。

我刚开始学的时候也犯过这个错。把Verilog当C写,结果综合出来的电路完全不是那么回事。嗯,今天我们就从最基础的结构讲起,把这些坑一个个填平。

核心要点:Verilog不是软件编程语言,它是硬件描述语言。你的思维要从“顺序执行”切换到“并行执行”。

3.1 模块结构——Verilog的基本单元

一个Verilog设计,说白了就是一堆模块的集合。模块就像芯片里的功能块,有输入、有输出,内部实现具体逻辑。

模块的基本骨架长这样:

module 模块名 (
    input  端口1,
    input  端口2,
    output 端口3,
    inout  端口4
);
    // 内部信号声明
    // 逻辑功能描述
endmodule

我个人习惯把端口声明和内部逻辑分开写,这样结构清晰。你看,moduleendmodule就像一对括号,把整个设计包起来。

举个例子,一个最简单的与门:

module and_gate (
    input  a,
    input  b,
    output y
);
    assign y = a & b;
endmodule

这里要注意:端口的方向必须明确。input就是输入,output就是输出,inout是双向口。我在项目中见过有人把inout当input用,结果仿真没问题,上板子就出bug——因为inout需要三态控制,不是随便拉的。

3.2 数据类型——wire、reg、integer

Verilog里最常用的数据类型就三个:wirereginteger。很多人搞不清wire和reg的区别,我当年也迷糊过。

类型 含义 赋值方式 典型用途
wire 线网型 assign连续赋值 组合逻辑连线
reg 寄存器型 always块内赋值 时序逻辑、组合逻辑
integer 整数型 过程赋值 循环计数、仿真用

wire——说白了就是一根导线。它不存储值,只是把驱动端的值传递出去。你想想看,导线本身能存东西吗?不能。所以wire必须被持续驱动。

reg——这个名字其实有误导性。很多人以为reg就是寄存器,会综合成触发器。其实不一定!reg只是表示一个变量,可以在always块里被赋值。它综合出来可能是触发器,也可能是组合逻辑,取决于你怎么写。

我曾经遇到一个同事,看到reg就以为会生成触发器,结果写了一大堆组合逻辑,综合报告里全是LUT,一个触发器都没有。嗯,这里要记住:reg不等于触发器,always @(posedge clk)才是触发器的标志

integer——这个在RTL设计里用得不多,主要是仿真和测试用。它是32位有符号整数,可以用于循环变量。但注意,不要用integer做敏感列表,否则综合工具会报错。

我的建议:RTL设计里,组合逻辑用wire+assign,时序逻辑用reg+always。这样分工明确,不容易出错。

3.3 运算符——从算术到位运算

Verilog的运算符跟C语言很像,但有些细节不一样。我列个表,大家对照着看:

类别 运算符 说明
算术 + - * / % 加减乘除取模
位运算 & | ~ ^ ^~ 与或非异或同或
逻辑 && || ! 逻辑与或非
关系 > < >= <= == != 比较运算
移位 << >> 左移右移
拼接 { } 位拼接
条件 ? : 三目运算符

这里有个容易踩的坑:位运算和逻辑运算的区别。位运算是对每一位独立操作,逻辑运算是对整个数做真假判断。比如4'b1010 & 4'b1100结果是4'b1000,而4'b1010 && 4'b1100结果是1'b1(因为两个数都不为0)。

拼接运算符{ }是我个人非常喜欢用的。比如把两个4位信号拼成8位:{a, b}。还可以重复拼接:{4{1'b0}}生成4个0。这个在数据打包时特别方便。

3.4 assign语句——连续赋值

assign是Verilog里最直接的赋值方式。它描述的是组合逻辑,只要右边信号变化,左边立即更新。

assign y = a & b;  // y随a、b变化

注意:assign的左边必须是wire类型,不能是reg。为什么?因为wire是线,可以随时被驱动;reg是变量,需要过程赋值。

多个assign语句是并行执行的,跟书写顺序无关。这一点跟软件编程完全不同。你想想看,硬件里所有连线都是同时工作的,哪有先后顺序?

警告:不要在一个assign里给同一个信号多次赋值,综合工具会报多驱动错误。我曾经在代码里不小心写了两个assign驱动同一个wire,仿真没问题,综合报了一堆error。

3.5 always块——过程赋值

always块是Verilog里最灵活、也最容易出错的构造。它既可以描述组合逻辑,也可以描述时序逻辑,关键看敏感列表怎么写。

基本语法:

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

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

  • 组合逻辑:always @(*)always @(a or b or c)——所有输入信号变化时触发
  • 时序逻辑:always @(posedge clk)——时钟上升沿触发
  • 异步复位:always @(posedge clk or negedge rst_n)——时钟或复位变化

我个人强烈建议:组合逻辑用always @(*),时序逻辑用always @(posedge clk)。不要自己手写敏感列表,容易漏信号。我见过有人写always @(a or b),结果后来加了c忘了更新列表,仿真结果完全不对。

3.6 阻塞与非阻塞赋值——FPGA设计的灵魂

这是Verilog里最重要的概念,没有之一。很多bug都出在这里。我们先看两种赋值方式:

赋值方式 符号 特点 典型用途
阻塞赋值 = 顺序执行,立即更新 组合逻辑
非阻塞赋值 <= 并行执行,延迟更新 时序逻辑

阻塞赋值(=):就像软件编程一样,先执行完当前语句,再执行下一条。如果a=b,然后c=a,那么c得到的是b的新值。

非阻塞赋值(<=):所有赋值同时计算,但更新是在块结束时统一进行。如果a<=b,c<=a,那么c得到的是a的旧值,不是b的新值。

为什么会这样?因为硬件里触发器是边沿触发的,所有触发器在同一时钟沿同时采样、同时更新。非阻塞赋值正好模拟了这个行为。

我曾经在项目里犯过一个经典错误:在always @(posedge clk)里用了阻塞赋值写移位寄存器,结果仿真波形完全不对,数据跳来跳去。后来改成非阻塞赋值,一切正常。

黄金法则:

  • 组合逻辑用阻塞赋值(=)
  • 时序逻辑用非阻塞赋值(<=)
  • 同一个always块里不要混用两种赋值

记住这个法则,能避免90%的时序问题。剩下的10%?嗯,等你遇到再说吧。

3.7 知识体系总览

下面这张图总结了本章的核心内容,我把它画成了结构图,方便你理解各个知识点之间的关系:

Verilog基础(上)知识体系 模块结构 module/endmodule 端口声明(input/output/inout) 数据类型 wire(线网) reg(寄存器型) integer(整数型) 运算符 算术/位/逻辑/关系 移位/拼接/条件 assign语句 连续赋值 组合逻辑描述 always块(过程赋值) 阻塞赋值(=) 顺序执行,立即更新 用于组合逻辑 非阻塞赋值(<=) 并行执行,延迟更新 用于时序逻辑 黄金法则:组合逻辑用阻塞赋值,时序逻辑用非阻塞赋值 同一个always块内不要混用两种赋值方式

这张图把本章的内容串起来了。从模块结构出发,到数据类型和运算符,再到assign和always两种描述方式,最后落脚到阻塞与非阻塞赋值这个核心概念。你学完这一章,应该能看懂简单的Verilog代码,也能自己写一些基础模块了。

记住,Verilog不是写代码,是画电路。你写的每一行,都要在脑子里想象出对应的硬件结构。wire就是一根线,reg就是一个存储单元,always块就是一堆逻辑门和触发器。这样想,很多问题就迎刃而解了。

课后练习建议:写一个4位加法器,用assign实现组合逻辑;再写一个8位移位寄存器,用always @(posedge clk)实现时序逻辑。仿真看看波形,体会阻塞和非阻塞赋值的区别。


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