第三讲:Verilog基础与组合逻辑——模块化设计、assign语句、always块、阻塞与非阻塞赋值

各位同学,欢迎来到实战课第三讲。今天咱们聊点硬核的——Verilog里最常用的几个基础结构。说实话,很多新手觉得Verilog就是写代码,跟C语言差不多。嗯,我当年也这么想,结果第一次写出来的模块,仿真怎么跑都不对。后来才明白,Verilog不是“程序”,它是“电路描述”。你写的是硬件,不是软件。

这一讲,我会带着大家把模块化设计、assign语句、always块、阻塞与非阻塞赋值这几个核心概念彻底讲透。每个知识点我都会结合自己踩过的坑来讲,希望能帮你少走弯路。

一、模块化设计——把大电路拆成小积木

模块化设计,说白了就是“分而治之”。一个复杂的交易系统,你不可能在一个文件里写完所有逻辑。我习惯把功能拆成独立的模块,每个模块只干一件事。

举个例子,一个简单的加法器模块:

module adder (
    input  [7:0] a,
    input  [7:0] b,
    output [8:0] sum
);
    assign sum = a + b;
endmodule

你看,这个模块只有三行核心代码。它只做加法,别的什么都不管。这就是模块化的精髓——高内聚、低耦合。

我的习惯:每个模块的端口尽量少,一般不超过10个。如果端口太多,说明这个模块该拆分了。

模块化设计的好处很明显:

  • 可复用——写好的模块可以在多个项目里直接用
  • 易调试——出问题了,定位到具体模块就行
  • 好维护——改一个模块不影响其他部分

我在做量化交易系统的行情解析模块时,就把整个模块拆成了:数据接收、协议解析、校验、缓存四个子模块。每个子模块独立测试,最后再拼起来。这样出了问题,一眼就能看出是哪个环节的锅。

二、assign语句——组合逻辑的“直通车”

assign语句,是Verilog里描述组合逻辑最直接的方式。它的意思就是“持续赋值”——只要右边信号变了,左边立刻跟着变。

来看个例子:

module mux2to1 (
    input  a,
    input  b,
    input  sel,
    output y
);
    assign y = sel ? a : b;
endmodule

这个二选一多路选择器,用assign一行就搞定了。sel为1时输出a,为0时输出b。注意,这里没有时钟,没有时序,纯组合逻辑。

注意:assign语句的左边必须是wire类型,不能是reg。我刚开始学的时候,老是把左边写成reg,结果编译报错。后来养成了习惯:组合逻辑用wire,时序逻辑用reg。

assign语句可以描述各种组合逻辑:

  • 算术运算:加法、减法、乘法
  • 逻辑运算:与、或、非、异或
  • 位运算:按位与、按位或、移位
  • 条件运算:三目运算符 ? :

我个人建议,能用assign描述的逻辑,尽量用assign。它简单、直观、不容易出错。只有遇到复杂的组合逻辑,才考虑用always块。

三、always块——组合逻辑的另一种写法

always块是Verilog里最灵活的语句块。它既可以描述组合逻辑,也可以描述时序逻辑。今天我们先讲组合逻辑的用法。

组合逻辑的always块,写法是这样的:

module and_gate (
    input  a,
    input  b,
    output reg y
);
    always @(*) begin
        y = a & b;
    end
endmodule

注意几个关键点:

  • 敏感列表用 @(*),表示“所有输入信号变化都触发”
  • 输出变量要声明为 reg 类型
  • 块内使用阻塞赋值 =

你可能会问:为什么组合逻辑要用reg类型?这不是时序逻辑才用的吗?嗯,这个问题我当年也困惑过。其实,Verilog里的reg只是表示“变量”,不一定对应寄存器。在组合逻辑的always块里,reg只是用来保存中间结果的变量,综合后还是组合电路。

重要原则:组合逻辑的always块里,所有被赋值的变量,必须在每个分支里都被赋值。否则会生成锁存器(latch),这是新手最容易犯的错误。

我曾经在一个项目里,写了一个组合逻辑的always块,忘了给某个分支赋值。结果综合出来一堆锁存器,功耗和面积都超标。后来花了整整两天才排查出来。从那以后,我每次写完always块,都会检查一遍:每个分支是不是都赋值了?

四、阻塞赋值与非阻塞赋值——一字之差,天壤之别

这是Verilog里最坑的地方,没有之一。阻塞赋值用 =,非阻塞赋值用 <=。看起来就差一个符号,但行为完全不同。

特性 阻塞赋值 (=) 非阻塞赋值 (<=)
执行顺序 立即执行,阻塞后续语句 并行执行,不阻塞后续语句
使用场景 组合逻辑 时序逻辑
综合结果 组合电路 寄存器/触发器
典型错误 在时序逻辑中用阻塞赋值 在组合逻辑中用非阻塞赋值

来看个对比的例子:

// 错误写法:时序逻辑里用阻塞赋值
always @(posedge clk) begin
    a = b;
    c = a;  // 这里c拿到的是a的新值,不是旧值
end

// 正确写法:时序逻辑里用非阻塞赋值
always @(posedge clk) begin
    a <= b;
    c <= a;  // 这里c拿到的是a的旧值,符合寄存器行为
end

为什么会有这种区别?因为硬件里,寄存器是边沿触发的。在同一个时钟沿,所有寄存器同时采样输入,同时更新输出。非阻塞赋值正好模拟了这个行为——先计算所有右边的值,再统一赋值给左边。

铁律:时序逻辑用非阻塞赋值,组合逻辑用阻塞赋值。这条规则我建议你刻在脑子里。我见过太多人因为搞混了这两个,仿真和实际行为不一致,排查到崩溃。

我记得有一次,帮同事调试一个交易系统的订单处理模块。他写了一个状态机,时序逻辑里用了阻塞赋值。仿真跑得好好的,但上板子就不对。我一看代码,立马发现问题。改成非阻塞赋值后,一切正常。他当时感叹:原来坑在这里。

五、知识体系总览

下面这张图,是我自己总结的本章知识体系。你可以把它当作一个思维导图来看:

Verilog组合逻辑基础 模块化设计 高内聚、低耦合 端口精简、功能单一 可复用、易调试 assign语句 持续赋值、组合逻辑 左边必须是wire 适合简单逻辑 always块 敏感列表 @(*) 输出声明为reg 避免生成锁存器 阻塞赋值 vs 非阻塞赋值 阻塞赋值 = 立即执行,顺序执行 用于组合逻辑 非阻塞赋值 <= 并行执行,不阻塞 用于时序逻辑 核心原则:组合逻辑用阻塞,时序逻辑用非阻塞

这张图把今天讲的内容串起来了。模块化设计是架构层面的思想,assign和always是实现手段,阻塞与非阻塞赋值是具体细节。三者层层递进,缺一不可。

六、避坑指南

最后,我把自己这些年踩过的坑总结一下,希望能帮你避开:

  1. 组合逻辑里用非阻塞赋值——仿真可能对,但综合出来的电路行为不对。我曾经因为这个,浪费了整整一周。
  2. always块里漏掉分支——会生成锁存器。我的习惯是,写完后用lint工具检查一遍。
  3. 模块端口太多——超过10个端口,就该考虑拆分了。我见过一个模块有30多个端口,那代码简直没法看。
  4. assign和always混用——同一个信号,不要在assign里赋值,又在always里赋值。这样会报多驱动错误。

好了,这一讲的内容就到这里。记住我今天讲的这些原则,写代码的时候多想想:这是组合逻辑还是时序逻辑?该用阻塞还是非阻塞?模块是不是该拆分了?养成好习惯,后面会少很多麻烦。

一句话总结:组合逻辑用assign或always+阻塞赋值,时序逻辑用always+非阻塞赋值。模块化设计,功能单一,端口精简。

专注资料整理