十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Verilog三种赋值方式详解:从assign到always @(posedge clk)的硬件映射

Verilog三种赋值方式详解:从assign到always @(posedge clk)的硬件映射 刚学 Verilog 的时候我经历过一种很拧巴的状态代码能仿真通过也能下载到板子上跑但你要是问我“这句代码综合出来是什么电路”我说不出准话。最典型的困惑就是三种赋值assign led ~key;、always (*) begin led ... end、always (posedge clk) begin led ... end。它们长得像作用却完全不同。很多教程会把 assign、always (*)、always (posedge clk) 分开讲语法但很少把三种写法放到同一张地图里讲清楚它们分别对应哪一类硬件结构。我后来想明白一个更省力的判断方式——写 Verilog 赋值不是在教计算机按顺序执行而是在描述硬件连接和触发关系。assign 描述组合逻辑里的连续连线always (*) 描述用过程语句表达的组合逻辑always (posedge clk) 描述由时钟边沿驱动的寄存器逻辑。只要抓住这条主线后面写代码、看仿真、查综合告警都会顺很多。1. 先建立第一张认知地图三种赋值对应三类硬件结构1.1 连续赋值 assign只要输入变输出跟着变assign 是 Verilog 里最直白的组合逻辑描述方式。assign out a b;综合出来就是一个与门assign data en ? dout : 1bz;综合出来就是带使能的三态驱动。它的语义是“左侧输出始终由右侧表达式驱动”更准确的类比是一条“始终通电的连线”而不是某个循环里的赋值语句。这个特点带来几个硬性约束assign 左侧目标必须是 wirenet 类型不能是 reg。assign 不需要 always 块也没有敏感列表。右侧任意一个输入变化仿真中输出都会立刻重新求值。assign 适合描述简单表达式、连线拼接、总线选择这类“输入输出直接映射”的逻辑。初学者最常见的写法错误就是这样reg out; assign out a b; // 错误reg 不能被 assign 连续驱动编译阶段就会报错。反过来也一样always 块里对 wire 赋值同样不被允许。这个约束不是故意为难你而是因为两种赋值方式描述的物理结构本来就不同assign 描述的是“持续连接”而 reg 描述的是“过程赋值变量”两者不能混用。1.2 过程块 always不是“循环”是“被触发执行”always 这个英文词很容易让人误解成“反复执行”。实际上它不能独立运行必须搭配敏感列表。只有敏感列表里的事件发生了块内语句才会重新执行一遍。常见的敏感列表有三种形态always (*)块内读到的任何信号变化都会触发用来写组合逻辑。always (posedge clk)只在 clk 上升沿触发用来写时序逻辑。always (posedge clk or negedge rstn)在 clk 上升沿或 rstn 下降沿触发通常用来写带异步复位的时序逻辑。关键点在于always 本身不决定综合结果是组合逻辑还是寄存器。真正决定结果的是敏感列表是否包含时钟边沿以及块内赋值是阻塞赋值还是非阻塞赋值。也就是说always (*)和always (posedge clk)之间的区别不只是语法上的敏感列表写法而是“电平触发”和“边沿触发”两种完全不同的硬件行为。1.3 一张表建立三种写法的硬件映射写法左侧目标类型赋值方式触发条件综合结果典型场景assignwire连续驱动任一右侧输入变化组合逻辑门或连线表达式、拼接、三态控制always (*)reg阻塞赋值块内读到的任一信号变化组合逻辑if/else、case、译码、MUXalways (posedge clk)reg非阻塞赋值clk 上升沿触发器/寄存器计数器、状态机、同步逻辑注意always 块里的 reg 只是“过程赋值变量”不一定综合成寄存器。always (*)内使用阻塞赋值综合出来仍然是组合逻辑只有出现时钟边沿触发加非阻塞赋值时才会变成真实寄存器。很多人一开始看到 reg 就以为综合出来是寄存器这是把软件变量思维带进硬件描述里了。2. 组合逻辑的两条路assign 和 always (*) 是分工不是替代2.1 同一个半加器两种写法都能实现先看半加器用 assign 描述module half_adder( input wire a, input wire b, output wire sum, output wire cout ); assign sum a ^ b; assign cout a b; endmodule再用 always (*) 描述同一个电路module half_adder( input wire a, input wire b, output reg sum, output reg cout ); always (*) begin sum a ^ b; cout a b; end endmodule这里有两个细节值得停下来想想。第一输出在 always 块内赋值所以声明必须用 reg虽然它最后综合出来还是组合逻辑。第二块内用的是阻塞赋值 因为组合逻辑要求“立即反映输入变化”。从综合结果看这两个模块并没有本质区别。差别在代码组织方式assign 适合把逻辑写成一个简洁表达式always (*) 适合写需要多条语句才能表达的分支决策。2.2 always (*) 真正擅长的是分支决策用 case 写一个 4 选 1 MUXmodule mux4( input [1:0] sel, input a, b, c, d, output reg y ); always (*) begin case (sel) 2b00: y a; 2b01: y b; 2b10: y c; 2b11: y d; default: y 1b0; endcase end endmodule这种多分支选择逻辑用 assign 也能写但表达式会退化成一大段嵌套的三目运算符assign y (sel 2b00) ? a : (sel 2b01) ? b : (sel 2b10) ? c : d;小规模时还能看分支一多就失去可读性。always (*) case 更贴近人对硬件决策的理解。if/else 也一样适合描述带优先级的判断。比如“使能有效时取 a否则取 b”always (*) begin if (en) y a; else y b; end在 if/else 链里从上到下的顺序本身就是优先级顺序这种语义用赋值表达式很难直观表达。2.3 (*) 里的星号解决的是仿真和综合不一致的经典问题老式写法里经常能看到always (a or b)表示只在 a、b 变化时重新执行块内语句。问题在于如果某天你加了一个新输入 c却忘了把它加进敏感列表仿真时 c 变化不会触发块内执行但综合工具仍然会把 c 参与的逻辑综合进去。于是出现最典型的事故仿真波形完全正确下载到板子却表现不对。因为你仿真时用的模型和综合出来的电路对 c 的响应逻辑根本不一样。// 旧式写法容易漏掉新信号 always (a or b) begin y a b c; // c 没进敏感列表仿真 bug 的来源 end(*)的作用就是把“块内读到的所有信号”自动纳入敏感列表从根上消除这类人为遗漏。所以在现代 RTL 里组合逻辑块首选always (*)没必要再手工维护敏感列表。2.4 组合逻辑两个高发坑漏分支和错误赋值方式第一个坑是漏分支。在 always (*) 里如果 if 没有配 else或 case 没有配 default某个输入组合下块内 reg 没有获得新值。仿真时它会看起来“保持旧值”综合时工具会推断出一个锁存器latch。如果你不是故意设计 latch这通常就是 BUG。always (*) begin if (sel) y a; endsel 为 0 时 y 没有被赋值工具会认为这里需要保存状态于是综合出一个 latch。这个 latch 不是你想要的功能逻辑而是设计缺陷。注意组合逻辑的 always 块里如果某个输入组合没有覆盖到工具会推断出锁存器latch。这不是语法错误但往往不是你想表达的设计意图。第二个坑是在组合逻辑块里用非阻塞赋值 。非阻塞赋值的特点是“本周期右侧统一求值周期末左侧统一更新”本质是为描述寄存器并行行为而设计的。在纯组合逻辑里使用 不会直接报错但会让代码变得难读也容易掩盖信号之间的真实依赖关系。组合逻辑块用阻塞赋值 时序逻辑块用非阻塞赋值 这是默认规则。想打破规则前先确认自己真的知道为什么。3. 时序逻辑为什么挂在 posedge clk 上又为什么默认用 3.1 posedge clk 的本质把变化锁到时钟边沿时序逻辑最典型的写法是计数器always (posedge clk) begin if (!rstn) cnt 4b0; else if (en) cnt cnt 4b1; endposedge clk的意思是只有 clk 上升沿时块内语句才执行。在两次上升沿之间输入怎么变化都不影响输出。这正是寄存器的行为——在边沿采样输入在非边沿保持状态。同样一段逻辑如果只把敏感列表里的posedge clk换成(*)它就从时序逻辑变成了组合逻辑输出会变成输入的组合函数完全失去“保存状态”的能力。所以写上posedge clk不是形式而是寄存器行为的来源。3.2 异步复位和同步复位敏感列表已经告诉你答案很多教程都会出现这个写法always (posedge clk or negedge rstn) begin if (!rstn) q 1b0; else q d; endnegedge rstn表示复位信号是低有效并且是异步复位只要复位信号下降沿到来无论时钟处于什么状态输出都会立刻被复位。复位信号出现在敏感列表里是“异步”的关键。如果只想用同步复位敏感列表里写(posedge clk)就够了always (posedge clk) begin if (!rstn) q 1b0; else q d; end关于negedge rstn还有个容易绕晕的点为什么 clk 用 posedgerstn 用 negedge因为 rstn 信号约定是低电平复位。如果敏感列表里写posedge rstn真正触发复位的反而是复位信号释放的瞬间工程上很容易出问题。低有效复位和 negedge 配对高有效复位和 posedge 配对这是信号协议和仿真行为一致性的要求。实际项目里复位策略要在设计开始前定下来。是统一异步复位还是统一同步复位会影响所有模块的敏感列表写法不建议在模块内部随意混用。3.3 非阻塞赋值的意义让多个寄存器在同一拍完成更新时序硬件里上升沿到来时多个寄存器是同时采样、同时更新的。Verilog 的非阻塞赋值 恰好模拟了这种并行行为右侧表达式在本时刻统一求值左侧变量在周期末统一更新。经典例子是交换两个寄存器。用阻塞赋值写always (posedge clk) begin a b; b a; // 此时 a 已经变成旧 b所以 b 最终也变成旧 b end用非阻塞赋值写always (posedge clk) begin a b; b a; // 右侧都取旧值符合硬件并行交换 end这个例子能清楚看出为什么时序块默认用 。真实工程里的状态机、计数器、移位寄存器几乎都依赖这种“同时采样、同时更新”的语义。如果在一段逻辑里混用阻塞和非阻塞赋值仿真行为会变得非常依赖语句顺序很难预测综合结果。3.4 别把组合逻辑顺手写进时序块实际项目里很容易出现这种代码本来只想在按键按下时点亮 LED结果直接写成always (posedge clk) begin led key; end这里使用了阻塞赋值仿真时每次时钟上升沿led 都更新为 key波形看起来可能是“对”的。但综合结果会多出一个触发器输出不是实时的组合逻辑而是滞后一拍、且在边沿采样的寄存器输出。如果你要的是“key 变化led 立刻变”这个写法就错了。正确做法是拆成组合逻辑assign led key;写时序块时养成一个习惯块内只做状态寄存、计数、状态跳转不顺手塞入本可以用 assign 直接实现的组合功能。还有一个工程经验值得记住——时序块里不要做过于复杂的组合运算。大位宽乘法、超长判断链这类逻辑最好拆成多个寄存器级既方便仿真调试也有利于时序收敛。4. 把判断固化成一个流程三问、五查、一个排查链路4.1 动手前先问三问第一问我想描述的电路是组合逻辑还是时序逻辑输入变化输出希望立刻变化 → 组合逻辑 → 走 assign 或 always (*)。输出希望只在时钟边沿采样并保存 → 时序逻辑 → 走 always (posedge clk)。第二问如果是组合逻辑逻辑是“表达式”级还是“分支决策”级简单表达式、连线、拼接 → assign。if/else、case、优先级判断、多条件组合 → always (*)。第三问如果用了 always块内赋值方式对不对组合块用阻塞赋值 。时序块用非阻塞赋值 。不要在一个块里混用两种赋值风格。这三个问题把模糊的“我想实现某个功能”翻译成“我要描述什么电路”。多数初学者一上来就查语法、查端口恰恰跳过了这个最关键的定性步骤。4.2 写完代码后做五查写完代码不要急着仿真先按下面五条过一遍查目标类型assign 目标必须是 wirealways 内部赋值目标必须是 reg。查敏感列表组合逻辑用(*)时序逻辑用(posedge clk)异步复位要追加or negedge rstn。查分支完整性if 有没有 elsecase 有没有 default组合逻辑块里所有输入组合是否都有赋值路径。查赋值方式阻塞/非阻塞是否与块类型匹配。查单一驱动同一个 reg/wire 只能由一个 assign 或一个 always 块驱动。assign 和 always 同时驱动一个信号或者两个 always 驱动同一个 reg都会产生冲突或不可预期结果。在学习和早期项目里大部分低级 RTL 错误都逃不过这五查。尤其第五点综合工具通常会给 warning但含义容易被忽略。看到 multi-driver、multiple drivers 这类关键词时先检查设计意图而不是强行消除警告。4.3 仿真正确但上板不对按这个链路排查如果代码仿真正常下板后行为不对先别急着怀疑工具和板子按下面的顺序逐层排查看现象输出完全不变、输出慢一拍、出现毛刺、综合时出现 Latch 警告、时序约束不满足。看结构把所有 assign 和 always 块列成一张表逐项写出触发条件、目标类型、赋值方式。很多时候刚列完错误自己就暴露了。看综合报告搜索latch inferred、multi-driver、incomplete sensitivity这类关键告警。看 RTL 视图在 FPGA 工具的 elaborated design 或 synthesis design 里打开图形确认每个信号综合出来的是门、连线还是寄存器、锁存器。看时序约束如果问题只在高速场景出现再查时钟频率、复位释放、跨时钟域路径不要在 RTL 里盲目乱改。一条实际经验综合报告里出现 latch inferred、multi-driven、incomplete sensitivity 时先回到源码检查不要急着调约束。很多“玄学问题”其实都是仿真模型和综合电路不一致造成的。4.4 适用边界与工程化延伸这套知识适合什么场景答案是学习 Verilog、FPGA 入门、小规模 RTL 模块开发。做到“想清楚硬件再写代码”基本就度过新手阶段了。不适合的情况是项目已经进入大规模 FPGA 工程或 ASIC 流程后只靠这三种写法还不够。还要补上时钟域规划、同步/异步复位统一策略、寄存器输出打拍、面积与延迟权衡、综合约束、静态时序分析等技能。但无论后面的方法论怎么变底层映射关系不会变assign 是组合连线(*)是组合分支(posedge clk)是边沿采样。工程上还有一个习惯建议一个 always 块只负责一件清晰的事。不要把所有状态机、计数器、组合输出塞进一个超大的always (posedge clk)。拆开写每个块的可读性和可维护性都会好很多后期排查问题也更容易定位。回到开头那句话写 Verilog 不是在写程序而是在描述电路。决定用assign、(*)还是(posedge clk)不是看哪一种更短而是看你想让工具生成什么硬件。落笔之前先问自己一句“这句代码综合出来是什么”再配合三问、五查和上面的排查链路你会发现 Verilog 没有那么多“魔法”。它每一个关键字背后都对应着具体的物理结构。
返回列表