4. Verilog基础语法(下):always块、assign语句、阻塞与非阻塞赋值、运算符

好,咱们接着聊。上一章我们把Verilog的门面功夫——模块、端口、数据类型——都过了一遍。这一章,咱们要真正开始写“会动”的电路了。

说白了,前面学的都是搭积木的零件。而always块和assign语句,就是让这些积木活过来的“指令”。我当年刚学的时候,总觉得这俩玩意儿差不多,都是赋值嘛。结果第一次写计数器,就踩了阻塞赋值的坑,仿真波形看得我一脸懵。

嗯,咱们今天就把这些坑一个个填平。

核心要点:always块描述时序逻辑和组合逻辑,assign描述组合逻辑。阻塞赋值(=)用于组合逻辑,非阻塞赋值(<=)用于时序逻辑。这是铁律,别问为什么,先记住。

4.1 always块:电路的行为描述

always块,你可以把它理解成一个“永远在跑”的进程。只要敏感列表里的信号变了,它就执行一次。

它的基本语法长这样:

always @(敏感列表) begin
    // 你的逻辑代码
end

敏感列表有两种写法:

  • 电平敏感:always @(a or b or c) —— 用于组合逻辑
  • 边沿敏感:always @(posedge clk or negedge rst_n) —— 用于时序逻辑

我个人习惯,组合逻辑的敏感列表用 always @(*)。这个星号是通配符,表示“所有输入信号”。你想想看,这样写多省事,加个信号也不用回头改列表。

小技巧:写 always @(*) 而不是 always @(a or b or c)。前者是自动推导,后者容易漏信号。我曾经因为少写了一个信号,仿真和综合结果不一致,查了整整一个下午。

4.2 assign语句:连续赋值

assign语句,说白了就是一根“导线”。左边是wire类型,右边是表达式。只要右边变了,左边立刻更新。

assign sum = a + b;
assign data_valid = (cnt == 4'd10) ? 1'b1 : 1'b0;

assign只能用于组合逻辑,而且左边必须是wire。这一点和always块不同——always块里赋值,左边通常是reg类型。

嗯,这里要注意:reg不一定是寄存器。在always块里描述组合逻辑时,变量声明为reg,但综合出来还是组合电路。这个坑我见过不少人掉进去过。

4.3 阻塞赋值 vs 非阻塞赋值

这是Verilog里最容易让人头大的地方。我当年面试的时候,面试官就问了这个,我答得磕磕巴巴。

咱们直接看对比:

特性 阻塞赋值 (=) 非阻塞赋值 (<=)
执行顺序 顺序执行,阻塞后面的语句 并行执行,不阻塞
典型用途 组合逻辑always块 时序逻辑always块
综合结果 组合逻辑 触发器/寄存器
左边变量类型 reg reg

为什么会有这种区别?

你想想看,阻塞赋值就像流水线上的工人——第一道工序干完,第二道才能开始。而非阻塞赋值,就像所有工人同时开工,各干各的,最后统一交货。

看个例子就明白了:

// 阻塞赋值 —— 组合逻辑
always @(*) begin
    b = a;
    c = b;
end
// 结果:c == a,因为b先被赋值,然后c再取b的值
// 非阻塞赋值 —— 时序逻辑
always @(posedge clk) begin
    b <= a;
    c <= b;
end
// 结果:c == 旧b,b == a。两个赋值在时钟沿同时发生

警告:千万不要在同一个always块里混用阻塞和非阻塞赋值。我曾经在一个项目里看到有人这么写,综合出来的电路和仿真完全对不上,最后只能重写。记住:时序用<=,组合用=,泾渭分明。

4.4 运算符:从算术到移位

Verilog的运算符,大部分和C语言差不多。但有几个地方特别容易出错,我挑重点说。

4.4.1 算术运算符

+、-、*、/、%。注意,除法和取模在综合时很耗资源。能用移位解决的,就别用乘除法。

4.4.2 位运算符

&、|、^、~。这些是按位操作,和逻辑运算符&&、||、!要区分开。

举个例子:

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

assign bitwise_and = a & b;   // 4'b1000
assign logical_and = a && b;  // 1'b1(非零即为真)

嗯,这个区别我刚开始也搞混过。后来养成习惯:处理数据用位运算,判断条件用逻辑运算。

4.4.3 移位运算符

<< 左移,>> 右移。左移相当于乘2,右移相当于除2。但要注意,右移是算术右移还是逻辑右移?Verilog里,对于无符号数,右移补0;对于有符号数,右移补符号位。

reg [3:0] data = 4'b1010;
assign shifted = data >> 1;  // 4'b0101,补0

4.4.4 拼接与复制运算符

这两个在RTL设计里非常实用:

  • 拼接:{a, b},把两个信号拼在一起
  • 复制:{n{a}},把a复制n次
wire [7:0] byte_data;
wire [3:0] high = 4'ha;
wire [3:0] low  = 4'h5;

assign byte_data = {high, low};  // 8'ha5

// 复制:生成8个重复的位
wire [7:0] pattern = {8{1'b1}};  // 8'hff

实用技巧:拼接运算符在写地址生成、数据打包时特别好用。我经常用 {addr[15:8], addr[7:0]} 来交换字节序,比用移位方便多了。

4.5 知识体系总览

这一章的内容,说白了就是三个核心:如何描述电路(always/assign)、如何赋值(阻塞/非阻塞)、如何运算(运算符)。它们之间的关系,我画了张图:

Verilog基础语法(下)知识体系 电路描述方式 always块 assign语句 赋值方式 阻塞赋值(=) 非阻塞赋值(<=) 运算符 算术/位运算 移位/拼接 核心原则:时序用非阻塞,组合用阻塞;always描述行为,assign描述连线 常见错误 混用阻塞/非阻塞 敏感列表遗漏信号 reg误认为一定是寄存器

这张图把这一章的核心逻辑串起来了。你写代码的时候,脑子里要有这张图:先想清楚我要描述什么电路(组合还是时序),再选对应的赋值方式,最后用合适的运算符把逻辑写出来。

好了,这一章就到这里。记住我今天说的这些坑,写代码的时候多留个心眼。特别是那个阻塞和非阻塞,我敢说,十个工程师里至少有五个在这上面栽过跟头。

可可精品资料库