想象你经营一家中餐厅。清晨开业前,你要做一套一次性准备:清洗食材、调配酱汁、把炉灶预热到合适温度——这些动作只做一遍,做完就正式开始营业。而营业中,每来一位客人,后厨大厨就要根据订单忙碌起来;客人走了一波又来一波,同样的炒菜步骤被一遍遍重复。 在Verilog的世界里,这两种节奏恰好对应两类“过程块”。Verilog是硬件描述语言,它不描述一段顺序执行的软件,而是描述一个持续运转的硬件电路。硬件不像软件那样“从main函数开始,一路执行到结束”,而是像餐厅一样:有些事只初始化一次(initial块),有些事则持续响应外部事件(always块)。 本文将以这个餐厅比喻为主线,带你理解过程块、敏感列表、组合与时序逻辑、赋值语句选择以及测试激励的写法。读完你会明白,所谓“写电路”,不过是把厨师的节奏翻译成代码。
对AI从业者而言,掌握过程块是读懂AI芯片(NPU/GPU)RTL代码的钥匙,也是参与智能硬件协同设计的基础能力。
可视化演示
展示always时序逻辑时钟驱动计数器、always组合逻辑多路选择器及initial测试激励波形,交互切换理解过程块核心机制。
本节配有交互动效(进入视口后自动加载,约 3KB)。
时序逻辑always块 posedge clk触发非阻塞赋值 计数器每个时钟沿递增 always @(posedge clk) 时序逻辑使用非阻塞赋值<=
过程块初识
开餐厅前,有两本手册要认真读:一本写“开业准备清单”,另一本写“日常经营SOP”。前者从开业那一刻起,从上到下执行一遍就归档;后者则成为餐厅每一天的运营框架,只要在营业时间,每次触发都要执行一遍。
Verilog中的initial块和always块正是这样两个截然不同的过程块。它们都写在module(模块)内部,是行为描述的基本单位。initial块从仿真0时刻开始执行一次,适合初始化变量、生成激励信号;always块则伴随敏感列表中的事件反复执行,用来建模实际硬件的持续行为。
过程块与assign连续赋值语句有本质区别:assign描述的是模块端口之间的实时连接,而过程块内部可以使用if-else、case、for等高级语句,描述更复杂的逻辑。一个module内可以同时存在多个initial块和always块,它们相互独立、并发执行——就像厨房可以同时开多个灶台,各自忙碌互不阻塞。
记住:initial是开店前的一把火,always是营业中不断响起的订单铃。两种过程块各司其职,构成了Verilog行为建模的双引擎。
- initial块语义仿真启动时自动执行一次,执行到最后一条语句后结束,如同开业前的备料清单只做一遍
- always块语义每个敏感事件发生时执行一次,仿真期间永不停止,如同营业中每来一单就进入操作流程
- 并发执行多个过程块在仿真中并行推进,相互独立,不依赖书写顺序
敏感列表设计
餐厅里的大厨并不是时时刻刻都在炒菜,而是听到“订单提醒铃”或看到传菜口出现新单才动手。也就是说,厨师的工作必须被某个“信号”唤醒。在Verilog中,唤醒always块的机制叫做敏感列表。
always块后面必须跟着一个括号,列出触发它执行的事件。例如always@(a or b)意味着只要a或b发生变化,块就会被重新执行一次。这个括号里的内容,就是敏感列表。
敏感列表分为两大类——电平敏感和边沿敏感。电平敏感列表(如always@(*)或always@(a,b,c))适合组合逻辑,它把所有被读取的输入信号都列为触发源;边沿敏感列表(如always@(posedge clk))适合时序逻辑,它只在时钟信号的上升沿瞬间触发一次。敏感列表的完备性至关重要:如果组合逻辑遗漏某个输入信号,仿真时该信号变化不会触发块重新执行,而综合生成的硬件却对所有输入敏感,最终产生“仿真与综合不一致”的陷阱。
给大厨安装好“通知系统”,他才不会漏单;给always块配好敏感列表,电路才不会漏响应。敏感列表设计是过程块编程的第一道门槛。
- 自动推导
@(*)让综合工具自动列出块内所有读取的信号,常用于组合逻辑 - 电平触发任意一个信号电平翻转都会唤醒块,执行与具体时延无关
- 边沿触发只在信号跳变的瞬间触发一次,上升沿posedge或下降沿negedge
组合逻辑建模
餐厅里的冷菜窗口讲究“即点即出”:客人说拍黄瓜,厨师手起刀落,黄瓜立刻出现在盘里。输出完全由当前订单决定,不问前世、不记旧账——这就是组合逻辑的哲学。
组合逻辑是指输出仅由当前输入决定的电路,没有记忆功能。最简单的描述方式是连续赋值assign,但复杂的组合逻辑需要if-else或case分支结构,这时就要借助always块了。
用always块描述组合逻辑的标准写法,是配合电平敏感的always@(*)。在块内部,用if-else或case对输入信号进行判断,以阻塞赋值“=“给输出赋新值。这里有一个常见陷阱:如果某个分支没有被覆盖,综合器会推断出一个锁存器(latch)——一个脆弱的存储单元——让组合逻辑偷偷”记忆”旧值,导致电路行为与设计意图不符。
即点即出的冷菜窗口,必须把每道菜谱都写在墙上;组合逻辑也要求把每种输入组合都写在分支里。漏写一道菜,顾客的订单就会被“锁”在窗口里不新鲜了。
- 功能定义输出Y是输入A、B和sel的纯函数,无状态记忆
- 分支覆盖if-else必须覆盖所有可能输入组合,否则会出现未定义的输出
- 阻塞赋值组合逻辑中使用
=立即更新,保证后续语句能看到新值
时序逻辑建模
厨房里有一位点心师傅,只在每个整点(时钟响起)时查看蒸笼并补充新品;没到点绝不掀盖,以免蒸汽散失。这种“按节拍行动”的节奏,正是时序逻辑的精髓。
时序逻辑是带记忆的电路,寄存器的输出不仅取决于当前输入,还取决于过去的历史状态。在Verilog中,我们通常用always@(posedge clk)描述时序逻辑,把时钟上升沿当作节拍器。
在时钟触发的always块内部,我们描述的是一个“状态更新”过程:每个时钟边沿到来时,寄存器把某个新值捕获并保存,直到下一个边沿。复位(reset)是时序逻辑中最常见的输入,它让电路回到确定的初始状态。复位有两种写法:同步复位仅在时钟沿查看复位信号;异步复位的敏感列表中同时包含时钟沿与复位沿,复位一旦有效立即生效,不受时钟控制。计数器是最典型的时序电路:每个时钟沿计数加一,或复位清零。
点心师傅守着的蒸笼,就是一个个寄存器;整点的钟声,就是时钟信号。复位如同“厨房紧急清空”按钮——按下即清(异步),或等到下一个整点再清理(同步)。掌握节拍,才驾驭得了大规模硬件。
- 状态更新下一个状态是当前状态和输入的函数,只在时钟边沿提交
- 同步复位仅当时钟沿到达且复位信号有效时清零
- 异步复位复位信号单独发生有效沿时立即清零,不依赖时钟
- 计数器每个时钟边沿q加一,是时序逻辑的基础应用
赋值语句选择
中餐讲究火候和顺序:有的菜必须一道一道完成,炒完糖色再放肉,这是“前一步做完才做后一步”;有的菜品则要先全部备菜,最后统一入锅,每道菜在锅里同时发生反应。Verilog中的阻塞赋值与非阻塞赋值,正对应这两种哲学。
阻塞赋值(=)按书写顺序立即更新变量,后一句能看见前一句的结果;非阻塞赋值(<=)则并行求值:所有右侧表达式在进入块时立即计算,但左侧变量的更新被推迟到整个块结束的时刻。这个看似微小的差异,在时序逻辑中会导致天壤之别的仿真结果。
业界有一条铁律:“组合逻辑用阻塞赋值,时序逻辑用非阻塞赋值。”为什么时序逻辑中不能混用阻塞赋值?考虑always@(posedge clk)中两个寄存器a和b互相关联:若用阻塞赋值,先写a=b再写b=a+1,执行顺序会让二者在同一时钟沿“串行传递”,产生仿真与硬件行为的不一致;若用非阻塞赋值,两条赋值并行求值、块末统一提交,仿真结果才与真实寄存器行为一致。
一道菜做完再做下一道,还是所有菜同时备料一起下锅?在时序厨房里,选错顺序哲学,菜就会串味、电路就会乱套。记住金科玉律:时序用<=,组合用=。
- 阻塞赋值=右侧值立即写入左侧变量,后面的语句能看到新值
- 非阻塞赋值<=右侧在赋值发生时求值,但写回推迟到块结束
- 适用场景组合逻辑用
=,时序逻辑用<=,两者分开走
initial块应用
每天餐厅开门前,都有固定的开机动作:点亮灯箱、拉出菜牌、摆好预订桌牌。这些动作只发生一次,却是整天运营的基石。initial块,就是Verilog仿真世界中的“开工仪式”。
initial块从仿真时间0开始执行,运行到块的最后一条语句后自动关闭。它无法被综合成实际硬件(除少数FPGA寄存器初始化场景外),因此在RTL设计中几乎不出现,但在测试平台(testbench)中它是绝对的主角。
testbench是验证设计(DUT)正确性的仿真环境。我们用initial块做三件事:第一,初始化信号变量,比如把clk设为0、把reset设为1;第二,生成激励波形,比如在特定时刻拉高valid信号;第三,驱动时钟源,通过initial配合forever循环每隔半周期翻转clk,形成周期震荡时钟。此外,#延时语句是initial块中的核心工具,它控制仿真时间的推进,让激励按设计时序注入。
有了开门前的精心布置,白天的营业才能忙而不乱;有了initial块的初始化与激励,testbench才能把电路驱动到正确的轨道上。它虽不能成为硬件,却是验证电路真实性的第一步。
- 初始化在0时刻给所有信号赋予一个稳定初值
- 延时推进
#后面的数值表示仿真时间推进量 - forever循环配合延时在initial块内产生周期信号
AI 应用场景
AI加速器RTL设计
阶段:AI芯片设计的RTL编码阶段
解决的问题:用always块实现CNN加速器中的乘累加单元与流水线寄存器,将并行数据流按时钟节拍推进;用always@(*)组合逻辑实现数据选择MUX,保证每个时钟周期完成一层卷积运算。
具体案例:某FPGA AI加速器项目中,设计者用always@(posedge clk)实例化输入寄存器、流水线寄存器与输出寄存器,用组合always块计算数据有效信号,在100MHz时钟下完成8路并行卷积数据流调度。
AI硬件仿真验证
阶段:验证仿真的testbench搭建阶段
解决的问题:用initial块生成AI加速器DUT的复位序列、输入图像像素激励与时钟信号,验证RTL在不同并行配置下的吞吐是否符合软硬件接口规范。
具体案例:验证团队使用initial块先拉高rst持续20个时钟周期,再释放复位,随后喂入一组随机化像素流,每个时钟沿比较DUT输出与软件参考模型的差值,快速定位流水线冲突。
AI芯片SoC互连
阶段:AI硬件集成阶段的握手协议实现
解决的问题:在AXI或自定义握手协议中,用always@(posedge clk)登记valid/ready状态,用组合always@(*)产生仲裁条件与FIFO满空标志,实现多AI核间的数据同步。
具体案例:在NPU多核互联设计中,设计者用always块实现同步FIFO的读写指针,使用组合逻辑产生almost-full/almost-empty信号,避免数据包拥塞导致推理延迟抖动。
Python 演示
#!/usr/bin/env python3"""模拟Verilog过程块中阻塞与非阻塞赋值的时序差异""" def simulate(nonblocking: bool, cycles: int): """模拟一个时钟驱动的always块在两种赋值方式下的行为""" a, b = 0, 1 # 寄存器初始值 results = [] for _ in range(cycles): if nonblocking: # 非阻塞赋值: 右侧并行求值, 块末统一提交 a_new, b_new = b, a + 1 a, b = a_new, b_new else: # 阻塞赋值: 按书写顺序立即生效 a = b b = a + 1 results.append((a, b)) return results def main(): print("Cycle | Non-blocking (a, b) | Blocking (a, b)") print("------ | ------------------- | ----------------") nb_list = simulate(True, 6) bl_list = simulate(False, 6) for i, (nb, bl) in enumerate(zip(nb_list, bl_list)): print(f" {i} | {nb[0]}, {nb[1]} | {bl[0]}, {bl[1]}") print() print("Non-blocking: a and b swap in parallel at each clock edge") print("Blocking: a updates first, then b sees new a (serial)") if __name__ == "__main__": main()
a_new, b_new = b, a + 1模拟非阻塞赋值的“右侧先并行求值”a, b = a_new, b_new模拟非阻塞赋值的“块末统一提交”a = b阻塞赋值立即生效,后面的语句能看见新值b = a + 1阻塞赋值中这里读到的a已经是更新后的值simulate(True, 6)` 与 `simulate(False, 6)对比两种赋值语义在6个时钟周期的结果if nonblocking:用分支切换两种赋值策略,对应硬件设计中的编码风格选择
常见误区
误区
正解
误区
正解
误区
正解
误区
正解
面试题
Q1. 下列哪个事件会触发always块执行?
- A.模块实例化时自动执行
- B.敏感列表事件发生时执行← 正确答案
- C.仿真开始时执行一次程序
- D.编译过程中自动执行一遍
答案:B。always块由敏感列表中的事件驱动。选项A是模块实例化的作用,选项C更接近initial块行为,选项D是编译器的动作,都与always触发机制无关。敏感列表好比订单铃,只有铃响(事件发生)厨师才会动手。
Q2. initial块在仿真中的执行特征是?
- A.无限循环执行,永不结束
- B.仿真0时刻执行一次即结束← 正确答案
- C.受时钟上升沿反复触发
- D.只能写一个语句且必须完整
答案:B。initial块在仿真0时刻启动,执行完最后一条语句便结束,如同开店前的一次性备料。选项A是无休止运行,选项C是always块的边沿触发特征,选项D过于绝对,initial块可以包含多条语句。
Q3. 组合逻辑建模应使用哪种敏感列表?
- A.时钟上升沿触发事件
- B.时钟下降沿触发事件
- C.所有输入电平变化事件← 正确答案
- D.复位信号上升沿事件
答案:C。组合逻辑要求任何输入变化都立即影响输出,因此使用always@(*)电平敏感列表。时钟边沿触发的选项A、B用于时序逻辑,选项D仅对复位信号敏感,无法覆盖其他输入变化。
Q4. 关于非阻塞赋值,下列说法正确的是?
- A.在赋值语句处立即更新
- B.在时间步末统一更新← 正确答案
- C.只在initial块内可用
- D.只适合组合逻辑电路
答案:B。非阻塞赋值<=右侧先求值,左侧在时间步末统一提交,这种并行性正好符合寄存器在时钟边沿同时更新的硬件行为。选项A是阻塞赋值的特征,选项C、D的说法均以偏概全。
Q5. Verilog中异步复位的正确写法是?
- A.敏感列表含时钟与复位沿← 正确答案
- B.敏感列表只用时钟信号
- C.在块外单独检测复位信号
- D.用initial块完成复位动作
答案:A。异步复位要求复位信号一旦有效就立即复位,因此敏感列表必须同时包含时钟沿和复位沿,形如always@(posedge clk or negedge rst)。若只用时钟信号(选项B)则是同步复位;选项C、D都不是寄存器建模的复位方式。
Q6. 下列哪些是Verilog编码的最佳实践?
- A.组合逻辑使用阻塞赋值← 正确答案
- B.时序逻辑使用非阻塞赋值← 正确答案
- C.组合逻辑使用always@(*)← 正确答案
- D.时序逻辑使用always@(*)
答案:ABC。A、B分别对应组合和时序的赋值规范,是业界标准;C用always@(*)描述组合逻辑也正确。D错在时序逻辑必须使用边沿触发(如posedge clk),单纯电平敏感无法提供寄存器所需的时钟边沿采样。
Q7. 在时序逻辑中混用阻塞赋值的主要风险是?
- A.仿真与综合行为不一致← 正确答案
- B.电路功耗显著增大
- C.综合工具自动纠正
- D.输入信号被自动屏蔽
答案:A。阻塞赋值会强制寄存器同沿串行更新,例如a=b; b=a+1会让b使用a的新值,而真实硬件中两个寄存器在同一时钟沿并行采样。这种“仿真对、电路错”的偏差,是混用阻塞赋值最大的风险。
Q8. 在always@(*)块中遗漏一个输入信号,会导致什么问题?
- A.仿真行为与硬件不一致← 正确答案
- B.综合工具必定报错
- C.时钟频率相较预期降低
- D.端口方向无法被确定
答案:A。仿真时该信号变化不会触发块重新执行,输出保持旧值;而综合生成的硬件天然对所有输入敏感。两者行为出现偏差,表现为仿真与综合不一致,这正是敏感列表完备性要求的原因。
Q9. 下列关于锁存器(latch)的说法正确的有?
- A.if分支不全会生成锁存器← 正确答案
- B.case缺少default生成锁存器← 正确答案
- C.时序逻辑中也一定会生成锁存器
- D.组合逻辑应保证分支完整← 正确答案
答案:ABD。组合逻辑中分支不完整,综合器会推断锁存器保留旧值,A、B正确,D是避免锁存器的整改手段。C错误:时序逻辑中分支不完整时保留旧值属于寄存器行为,不会额外生成锁存器。
Q10. initial块的典型应用场景包括?
- A.在仿真中生成周期性时钟← 正确答案
- B.在仿真前初始化信号值← 正确答案
- C.在RTL中实现复杂状态机
- D.在测试台中对DUT进行复位← 正确答案
答案:ABD。initial块常用于测试平台:配合forever生成时钟(A)、对信号赋初值(B)、在仿真开始阶段拉高或拉低复位信号(D)。C错误——RTL中的状态机必须用可综合的always块实现,initial块无法被综合成硬件逻辑。