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)、如何赋值(阻塞/非阻塞)、如何运算(运算符)。它们之间的关系,我画了张图:
这张图把这一章的核心逻辑串起来了。你写代码的时候,脑子里要有这张图:先想清楚我要描述什么电路(组合还是时序),再选对应的赋值方式,最后用合适的运算符把逻辑写出来。
好了,这一章就到这里。记住我今天说的这些坑,写代码的时候多留个心眼。特别是那个阻塞和非阻塞,我敢说,十个工程师里至少有五个在这上面栽过跟头。
可可精品资料库