ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Verilog并行与串行语句:从仿真到综合的硬件思维

Verilog并行与串行语句:从仿真到综合的硬件思维 1. 从一个反直觉的仿真结果说起如果你写过一段时间的Verilog大概率遇到过这样一种情况明明代码里写的是一段顺序执行的always块综合出来的电路却能在同一个时钟沿同时更新十几个寄存器反过来你写了几行看起来并行的连续赋值语句仿真波形却呈现出明显的先后依赖关系。这种写的和想的不是一回事的割裂感几乎每个刚接触硬件描述语言的人都会经历一遍。这个项目的标题是Verilog中的并行与串行语句它要解决的核心问题其实就一句话Verilog代码的书写顺序和它最终映射到硬件上的执行顺序是两套完全不同的逻辑。搞清楚这两套逻辑之间的映射关系是从能写Verilog到能写好Verilog的分水岭。它适合所有正在学习数字电路设计、FPGA开发、或者从软件编程转向硬件描述语言的工程师尤其适合那些被仿真结果和预期不一致折磨过的朋友。我见过太多初学者把Verilog当成C语言来写在always块里用循环变量做累加然后困惑为什么综合工具报出一堆警告或者为什么上板之后时序完全不对。问题的根源不在于语法不熟而在于没有建立起并行和串行这两个概念在硬件层面的正确认知。这篇内容会从设计思路、核心机制、实操验证、常见坑四个维度把这件事彻底讲透。2. 整体设计思路为什么Verilog要区分并行与串行2.1 硬件本质决定了描述方式要理解Verilog里并行和串行的区分得先回到一个最根本的事实数字电路天生是并行的。一块FPGA芯片上几十万个查找表和触发器在同一时刻各干各的活它们之间通过布线资源连接信号沿着路径传播没有谁在等谁。时钟沿到来时所有满足条件的触发器同时翻转这就是硬件的工作方式。Verilog作为硬件描述语言它的首要任务是描述这种并行结构而不是指挥一个处理器去一步步执行。所以语言在设计之初就必须提供两种表达能力一种用来描述这些电路同时存在、同时工作另一种用来描述在某个触发条件下这些操作按逻辑顺序发生。前者对应并行语句后者对应串行语句。这个设计选择背后有一个很实际的考量如果Verilog只有并行语句那描述复杂的状态机和控制逻辑会非常痛苦因为你没法表达先判断条件A再根据A的结果决定是否执行B这种逻辑。如果只有串行语句那描述数据通路的并行运算又会变得极其低效。所以两种语句并存是语言设计上的必然。2.2 并行语句与串行语句的边界在Verilog里并行语句和串行语句的划分并不是按关键字来的而是按执行语义来的。我习惯用这样一个标准来判断如果一段代码描述的是结构那它就是并行的如果描述的是过程那它就是串行的。具体来说以下几类属于并行语句的范畴连续赋值语句assign它描述的是一条物理连线右侧表达式的值一旦变化左侧信号立刻跟着变没有先后之分。实例化语句模块实例化、门级原语实例化它们描述的是电路中一个个独立的硬件单元天然并行。always块和initial块本身多个always块之间是并行的它们在仿真开始时同时启动在硬件上对应不同的电路模块。generate生成的语句展开后是并行的硬件结构。而串行语句主要出现在always块和initial块内部阻塞赋值按书写顺序依次执行前一条执行完才执行下一条。非阻塞赋值在同一个时间步内右侧表达式同时求值左侧在时间步结束时同时更新。顺序块begin...end块内语句按顺序执行。条件语句和循环语句if-else、case、for、while等在过程块内按逻辑顺序执行。这里有一个容易混淆的点always块本身是并行语句但块内部的语句是串行的。这个外并行、内串行的结构是Verilog最核心的设计模式也是理解后续所有内容的基础。2.3 为什么这个区分如此重要我刚开始学Verilog的时候最大的困惑就是既然硬件是并行的为什么还要在always块里写串行语句直接用assign描述所有逻辑不就行了后来做多了项目才明白串行语句的存在是为了描述时序逻辑和控制流。举个例子你要实现一个状态机状态从IDLE跳到WORK再跳到DONE这个跳的过程在硬件上确实是同时发生的所有触发器同时更新但你在描述它的时候需要表达当前状态是IDLE时下一个状态是WORK这种条件依赖关系。这种依赖关系用并行语句很难优雅地表达而用case语句在always块里写就非常自然。另一个重要原因是仿真语义。仿真器需要一个明确的执行顺序来模拟电路行为如果所有语句都并行执行仿真器就没法处理先算A再算B这种逻辑。所以串行语句在仿真层面提供了确定的执行顺序而综合工具会把这个顺序翻译成并行的硬件结构。理解这个仿真串行、硬件并行的映射关系是避免仿真与综合不一致的关键。3. 核心细节解析并行与串行的底层机制3.1 连续赋值语句的并行本质assign语句是Verilog里最纯粹的并行语句。它的语义是只要右侧表达式的任何一个操作数发生变化左侧信号就在零延迟后更新。注意这里的零延迟是理想情况实际仿真中会有delta延迟但概念上可以理解为瞬时。assign y a b; assign z c | d;这两条语句之间没有任何顺序关系。a或b变化时y更新c或d变化时z更新两者互不影响。如果你把这两条语句的顺序调换仿真结果完全一样综合出来的电路也完全一样。这里有一个实操中容易踩的坑连续赋值语句的右侧不能是寄存器类型左侧必须是线网类型。我见过有同学写assign reg_a b c;综合工具直接报错。原因很简单assign描述的是连线连线的驱动端必须是持续驱动的信号而寄存器只在时钟沿更新两者语义冲突。另一个细节是多驱动问题。如果你对同一个线网写了多条assign语句仿真时会出现x值综合时会报多驱动错误。这从侧面印证了assign的并行本质多条语句同时驱动同一个节点就像两个电源直接并联硬件上是不允许的。3.2 always块内部的串行执行always块内部的语句是串行执行的但这个串行有特定的含义。在仿真器中always块被触发后块内语句按书写顺序依次执行前一条语句的结果会影响后一条语句的输入。这就是为什么阻塞赋值会阻塞后续语句的执行。always (posedge clk) begin a b c; d a e; end在这个例子里d用的是a更新后的值因为a b c先执行a立刻得到新值然后d a e再执行。如果你把这两条语句的顺序调换d就会用a的旧值结果完全不同。但这里有一个关键认知综合工具会把这段代码翻译成并行的硬件。综合后的电路里a和d都是触发器它们在同一个时钟沿同时更新。d的输入是a的当前值也就是上一个时钟周期的值加上e而不是a更新后的值。这就是为什么在时序逻辑里我们通常推荐使用非阻塞赋值因为它的语义更接近硬件的真实行为。3.3 阻塞赋值与非阻塞赋值的本质区别阻塞赋值和非阻塞赋值的区别是Verilog学习中最容易混淆、也最重要的知识点。我用一个生活化的类比来解释想象一个会议室里有三个人A、B、C他们要依次发言。阻塞赋值就像A说完B立刻接着说B的发言内容可以引用A刚说的话。非阻塞赋值就像三个人同时拿到发言稿各自准备然后在同一时刻一起说出来B的发言内容引用的是A上一轮的发言而不是这一轮刚说的。在仿真层面这个区别体现在更新时机上特性阻塞赋值非阻塞赋值执行方式立即执行立即更新右侧求值后时间步结束时统一更新顺序依赖后续语句能看到新值后续语句看到的是旧值适用场景组合逻辑建模时序逻辑建模综合结果可能产生意外锁存器映射为触发器仿真与综合一致性组合逻辑下一致时序逻辑下一致我个人的经验是在always (posedge clk)里一律用非阻塞赋值在always (*)里一律用阻塞赋值。这条规则不是死板的教条而是无数人踩坑总结出来的最佳实践。违反这条规则轻则仿真结果诡异重则综合出完全错误的电路。3.4 多个always块之间的并行关系一个模块里可以写多个always块它们之间是并行的。仿真开始时所有always块同时启动各自等待自己的敏感信号。硬件上每个always块对应一块独立的电路它们通过信号线连接同时工作。always (posedge clk) begin counter counter 1; end always (posedge clk) begin if (counter 8hFF) flag 1b1; else flag 1b0; end这两个always块同时工作counter和flag在同一个时钟沿更新。flag判断的是counter的当前值更新前的值而不是加1后的值。如果你希望flag在counter达到255时置位这段代码是正确的如果你希望flag在counter变成255的那一刻置位就需要调整逻辑。这里有一个实操心得多个always块之间不要有隐含的顺序依赖。虽然仿真器会按某种顺序调度它们但这个顺序是不确定的不同仿真器可能不一样。如果你的设计依赖这个顺序那说明设计本身有问题应该把相关逻辑合并到一个always块里。4. 实操过程用仿真验证并行与串行的行为差异4.1 搭建验证环境光看理论容易似懂非懂我建议你跟着下面的步骤实际跑一遍仿真亲眼看到波形差异理解会深刻得多。我用的是一个最简化的验证平台不需要任何外部IP纯Verilog就能跑。先写一个测试模块包含阻塞赋值和非阻塞赋值两种写法module blocking_vs_nonblocking ( input wire clk, input wire rst_n, input wire [7:0] in_a, input wire [7:0] in_b, output reg [7:0] out_blocking, output reg [7:0] out_nonblocking ); // 阻塞赋值版本 always (posedge clk or negedge rst_n) begin if (!rst_n) begin out_blocking 8d0; end else begin out_blocking in_a; out_blocking out_blocking in_b; end end // 非阻塞赋值版本 always (posedge clk or negedge rst_n) begin if (!rst_n) begin out_nonblocking 8d0; end else begin out_nonblocking in_a; out_nonblocking out_nonblocking in_b; end end endmodule这段代码故意写得很别扭目的是让两种赋值的差异暴露出来。在阻塞赋值版本里out_blocking先被赋值为in_a然后立刻加上in_b最终结果是in_a in_b。在非阻塞赋值版本里两条语句的右侧同时求值第一条把out_nonblocking更新为in_a第二条把out_nonblocking更新为旧值加in_b但因为是同一时间步最终生效的是最后一条语句的结果也就是旧值加in_b。4.2 编写测试激励接下来写一个简单的testbench给不同的输入值观察输出timescale 1ns/1ps module tb_blocking_vs_nonblocking; reg clk; reg rst_n; reg [7:0] in_a; reg [7:0] in_b; wire [7:0] out_blocking; wire [7:0] out_nonblocking; blocking_vs_nonblocking uut ( .clk(clk), .rst_n(rst_n), .in_a(in_a), .in_b(in_b), .out_blocking(out_blocking), .out_nonblocking(out_nonblocking) ); // 时钟生成 initial begin clk 0; forever #5 clk ~clk; end // 激励 initial begin rst_n 0; in_a 8d10; in_b 8d20; #20; rst_n 1; #10; in_a 8d30; in_b 8d40; #20; in_a 8d50; in_b 8d60; #20; $stop; end endmodule跑完仿真后你会看到这样的波形out_blocking在每个时钟沿变成in_a in_b的当前值而out_nonblocking变成的是上一个周期的out_nonblocking加上in_b。这个差异在波形上非常直观比看一百遍文字描述都管用。4.3 综合结果对比仿真验证完之后我建议你再跑一次综合看看两种写法映射出来的电路有什么不同。用常见的综合工具把上面两个always块分别综合然后看RTL视图。阻塞赋值版本综合出来的电路out_blocking的输入是一个加法器加法器的两个输入分别是in_a和in_b中间没有额外的触发器。这是因为阻塞赋值的先赋值再运算在综合时被优化掉了综合工具识别出out_blocking in_a; out_blocking out_blocking in_b;等价于out_blocking in_a in_b;。非阻塞赋值版本综合出来的电路out_nonblocking的输入是一个加法器加法器的一个输入是out_nonblocking自己的输出经过反馈另一个输入是in_b。这就是一个典型的累加器结构每个时钟周期把in_b累加到当前值上。这个对比说明了一个重要事实综合工具会按照硬件语义来理解你的代码而不是按照仿真语义。阻塞赋值在时序逻辑里产生的顺序执行效果综合工具会尝试优化掉而非阻塞赋值的同时更新效果正好对应触发器的行为综合工具会如实映射。4.4 参数计算与选择过程在实际项目中选择阻塞还是非阻塞除了看逻辑类型还要考虑时序收敛和资源消耗。我拿一个实际的例子来说明。假设你要实现一个8位计数器每个时钟周期加1同时输出一个进位标志。有两种写法写法一用非阻塞赋值always (posedge clk) begin if (cnt 8hFF) begin cnt 8h00; carry 1b1; end else begin cnt cnt 1b1; carry 1b0; end end写法二用阻塞赋值always (posedge clk) begin cnt cnt 1b1; if (cnt 8h00) carry 1b1; else carry 1b0; end写法一综合出来的电路cnt和carry是两个独立的触发器它们的输入逻辑分别计算。写法二综合出来的电路carry的组合逻辑依赖于cnt的当前值但cnt是触发器输出所以实际上carry也是触发器只是它的输入逻辑用了cnt的当前值而不是加1后的值。从资源消耗上看两种写法差不多都是一个8位触发器加一个1位触发器。但从时序上看写法一的carry逻辑路径更短因为它的判断条件是cnt 8hFF而写法二的判断条件是cnt 8h00加1后的值需要先算加法再比较路径更长。在高速设计里这个差异可能决定能不能跑到目标频率。我的经验是时序逻辑一律用非阻塞赋值组合逻辑一律用阻塞赋值不要混用。这条规则能帮你避开90%以上的坑。剩下的10%等你对硬件语义理解足够深了自然知道什么时候可以灵活处理。5. 常见问题与排查技巧实录5.1 仿真与综合结果不一致这是最让人头疼的问题没有之一。仿真波形完全正确上板之后行为诡异或者综合报告里一堆警告。我整理了一个排查清单按优先级排序问题现象可能原因排查方法仿真对综合后逻辑错误时序逻辑用了阻塞赋值检查所有posedge clk块把改成仿真出现x值多驱动或未初始化检查是否有多个assign驱动同一线网综合出锁存器组合逻辑条件不完整检查if和case是否覆盖所有分支仿真结果随机多个always块竞争合并相关逻辑到一个always块上板后偶尔出错跨时钟域未同步检查是否有信号跨时钟域直接使用我重点说一下综合出锁存器这个问题。在组合逻辑的always块里如果if语句没有else分支或者case语句没有default分支综合工具会认为条件不满足时保持原值从而推断出一个锁存器。锁存器在FPGA里通常是不推荐的因为它对毛刺敏感时序分析也麻烦。// 会综合出锁存器 always (*) begin if (sel) y a; end // 不会综合出锁存器 always (*) begin if (sel) y a; else y b; end5.2 阻塞赋值在时序逻辑中的陷阱我见过一个非常典型的案例某同学写了一个状态机在always (posedge clk)里用阻塞赋值更新状态结果状态跳转完全乱了。代码大概长这样always (posedge clk) begin state next_state; case (state) IDLE: if (start) next_state WORK; WORK: if (done) next_state DONE; DONE: next_state IDLE; endcase end这段代码的问题在于state next_state先执行state立刻变成新值然后case (state)判断的是新值而不是当前状态。这导致状态机在同一个时钟沿内穿越了多个状态行为完全错误。正确的写法应该是用非阻塞赋值always (posedge clk) begin state next_state; end always (*) begin case (state) IDLE: if (start) next_state WORK; WORK: if (done) next_state DONE; DONE: next_state IDLE; default: next_state IDLE; endcase end把状态更新和状态判断分开到两个always块里一个用时序逻辑一个用组合逻辑这是状态机的标准写法。5.3 非阻塞赋值的同时更新陷阱非阻塞赋值也不是万能的。它的同时更新特性在某些场景下会导致意想不到的结果。比如你想实现一个移位寄存器always (posedge clk) begin shift_reg[0] data_in; shift_reg[1] shift_reg[0]; shift_reg[2] shift_reg[1]; shift_reg[3] shift_reg[2]; end这段代码是正确的每个时钟沿shift_reg[0]拿到data_inshift_reg[1]拿到旧的shift_reg[0]以此类推数据依次右移。但如果你用阻塞赋值写always (posedge clk) begin shift_reg[0] data_in; shift_reg[1] shift_reg[0]; shift_reg[2] shift_reg[1]; shift_reg[3] shift_reg[2]; end数据会在同一个时钟沿穿透整个移位寄存器shift_reg[3]直接拿到data_in移位功能完全失效。这就是阻塞赋值在时序逻辑里的典型陷阱。5.4 独家避坑技巧分享几个我从实际项目中总结出来的技巧常规文档里不会写技巧一用$display打印赋值前后的值。当你怀疑赋值顺序有问题时在always块里加$display打印关键信号在每条语句执行前后的值。仿真日志会清楚地告诉你哪条语句先执行、哪条后执行。技巧二把always块按功能拆分。一个always块只做一件事比如只更新状态、只计算输出、只处理计数器。这样每个块的逻辑简单赋值方式的选择也明确不容易出错。技巧三用lint工具做静态检查。很多综合工具自带lint功能能检测出阻塞/非阻塞混用、锁存器推断、多驱动等问题。在综合之前先跑一遍lint能省下大量调试时间。技巧四仿真时打开delta延迟观察。有些仿真器支持显示delta延迟能看到同一时间步内信号的多次更新。这对理解非阻塞赋值的同时更新机制非常有帮助。技巧五写代码时先画电路图。在写Verilog之前先在纸上画出你想要的电路结构标出哪些是触发器、哪些是组合逻辑、信号怎么连。然后照着电路图写代码赋值方式的选择就变得很自然触发器用非阻塞组合逻辑用阻塞。6. 从语句到电路建立硬件思维6.1 仿真语义与综合语义的映射Verilog最反直觉的地方在于同一段代码在仿真器和综合工具眼里含义是不一样的。仿真器按过程式语义执行综合工具按硬件语义映射。理解这个映射关系是写出可靠Verilog的关键。我习惯用一个翻译表来理解这个过程Verilog语句仿真语义综合语义assign y a b;a或b变化时y更新一个与门always (posedge clk) q d;时钟沿到来时q更新为d一个D触发器always (*) y a b;a或b变化时y更新一个与门if (sel) y a; else y b;条件判断选择执行一个多路选择器for (i0; i8; ii1) ...循环8次8份并行硬件展开后这张表的核心信息是综合工具会把过程式的描述展开成并行的硬件结构。for循环在仿真里是串行执行8次在硬件上是8份电路同时工作。if-else在仿真里是选择执行在硬件上是一个多路选择器。理解这个展开过程你就能预判综合结果。6.2 什么时候用并行什么时候用串行经过前面的分析选择并行还是串行其实有一个清晰的判断标准用并行语句的场景描述纯组合逻辑且逻辑简单用assign一行能写完实例化子模块或门级原语描述多个独立的硬件单元用串行语句的场景描述时序逻辑需要触发器描述复杂的状态机描述需要条件判断和循环的逻辑描述需要先算A再算B的组合逻辑我个人的习惯是能用assign搞定的组合逻辑就用assign搞不定的用always (*)加阻塞赋值时序逻辑一律用always (posedge clk)加非阻塞赋值。这个习惯让我很少在赋值方式上纠结代码也容易维护。6.3 一个完整的例子UART发送模块为了把前面的知识点串起来我拿一个UART发送模块的核心逻辑做例子。这个模块的功能是收到发送使能后把8位数据按波特率逐位发送出去。module uart_tx ( input wire clk, input wire rst_n, input wire tx_start, input wire [7:0] tx_data, output reg tx, output reg tx_busy ); reg [3:0] bit_cnt; reg [7:0] shift_reg; reg [15:0] baud_cnt; reg [1:0] state; localparam IDLE 2d0; localparam START 2d1; localparam DATA 2d2; localparam STOP 2d3; // 状态机时序逻辑用非阻塞赋值 always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; bit_cnt 4d0; shift_reg 8d0; baud_cnt 16d0; tx 1b1; tx_busy 1b0; end else begin case (state) IDLE: begin tx 1b1; tx_busy 1b0; if (tx_start) begin state START; shift_reg tx_data; tx_busy 1b1; baud_cnt 16d0; end end START: begin tx 1b0; if (baud_cnt 16d5207) begin baud_cnt 16d0; state DATA; bit_cnt 4d0; end else begin baud_cnt baud_cnt 1b1; end end DATA: begin tx shift_reg[0]; if (baud_cnt 16d5207) begin baud_cnt 16d0; shift_reg {1b0, shift_reg[7:1]}; if (bit_cnt 4d7) begin state STOP; end else begin bit_cnt bit_cnt 1b1; end end else begin baud_cnt baud_cnt 1b1; end end STOP: begin tx 1b1; if (baud_cnt 16d5207) begin baud_cnt 16d0; state IDLE; tx_busy 1b0; end else begin baud_cnt baud_cnt 1b1; end end default: state IDLE; endcase end end endmodule这段代码里always块内部全部用非阻塞赋值因为它是时序逻辑。状态机用case语句描述每个状态的跳转条件清晰。波特率计数器baud_cnt的阈值5207是根据时钟频率和波特率算出来的假设时钟50MHz波特率9600那么每个比特需要50_000_000 / 9600 ≈ 5208个时钟周期所以计数器从0数到5207。这个例子展示了并行与串行语句的典型配合方式模块内部只有一个always块块内是串行的状态机逻辑但综合出来的是一块并行的时序电路。tx、tx_busy、state、bit_cnt、shift_reg、baud_cnt这些寄存器在同一个时钟沿同时更新它们之间的逻辑关系由case语句描述。6.4 代码风格建议最后分享几条我在实际项目中坚持的代码风格它们帮我避免了很多并行/串行相关的坑第一条一个模块一个时钟域。如果一个模块里有两个时钟域把它们拆成两个模块中间用同步器连接。这样每个模块内部的时序逻辑简单不会出现跨时钟域的赋值混乱。第二条时序逻辑和组合逻辑分开写。状态机的次态逻辑用always (*)加阻塞赋值状态更新用always (posedge clk)加非阻塞赋值。输出逻辑单独用一个always (*)块。这样每个块的职责单一赋值方式明确。第三条不要在多个always块里写同一个信号。这是多驱动问题的根源综合工具会报错仿真器会出x值。如果一个信号需要在多个地方更新把它们合并到一个always块里。第四条组合逻辑的always块用(*)而不是手动列敏感信号。手动列敏感信号容易漏漏了会导致仿真和综合不一致。(*)让工具自动推断省心又可靠。第五条给所有if加else给所有case加default。这是避免锁存器的基本操作也是良好代码风格的体现。即使else分支什么都不做也写一个y y;或者赋默认值让综合工具知道你的意图。这些规则看起来简单但坚持下来不容易。我见过太多项目因为赋值方式混用、敏感信号漏列、锁存器推断这些问题在后期调试上浪费了大量时间。与其事后排查不如写代码的时候就按规矩来。7. 进阶话题并行语句的展开与优化7.1 generate语句的并行展开generate语句是Verilog里描述并行结构的高级工具。它可以在编译时展开成多份硬件每份硬件独立工作。比如你要实例化8个相同的模块genvar i; generate for (i 0; i 8; i i 1) begin : gen_inst sub_module u_sub ( .clk(clk), .rst_n(rst_n), .data_in(data_in[i]), .data_out(data_out[i]) ); end endgenerate这段代码在综合时会展开成8个sub_module实例它们完全并行。genvar变量在编译时求值不占用硬件资源。generate块里的for循环和always块里的for循环有本质区别前者是编译时展开后者是仿真时循环、综合时展开。7.2 并行语句的资源共享与优化并行语句虽然描述的是并行硬件但综合工具会做一些优化把可以共享的资源合并。比如assign y1 a b; assign y2 a b;综合工具可能会用一个加法器输出同时给y1和y2而不是两个加法器。这种优化叫资源共享在资源紧张的设计里很有用。但资源共享也有代价它可能增加组合逻辑的路径延迟影响时序收敛。所以综合工具通常会在面积优先和速度优先之间做权衡。你可以通过综合约束来指导工具的选择。7.3 流水线设计中的并行与串行流水线是FPGA设计里常用的提速手段它的本质是把一个长的组合逻辑路径拆成多级每级之间插入触发器。在Verilog里流水线用多个always (posedge clk)块描述每个块对应一级。// 第一级乘法 always (posedge clk) begin mult_result a * b; end // 第二级加法 always (posedge clk) begin add_result mult_result c; end // 第三级输出 always (posedge clk) begin y add_result; end这三个always块是并行的它们在同一个时钟沿同时更新。但数据在它们之间是串行流动的a * b的结果在下一个时钟沿进入第二级再过一拍进入第三级。这种并行结构、串行数据流的模式是流水线设计的核心。理解这个模式你就能明白为什么Verilog要同时提供并行和串行两种语句并行语句描述硬件的空间结构串行语句描述数据的时间流动。两者结合才能完整地描述一个数字系统。8. 我个人的一些经验体会写了这么多年Verilog我对并行与串行的理解也经历了一个从困惑到清晰的过程。最开始我总想把Verilog当成C语言来写用循环和条件判断描述算法结果综合出来的电路又大又慢。后来慢慢意识到Verilog不是用来编程的而是用来画电路的。你写的每一行代码都应该能在脑子里映射成具体的硬件结构。这个认知转变之后很多问题就迎刃而解了。看到assign我知道那是一条线看到always (posedge clk)我知道那是一组触发器看到if-else我知道那是一个多路选择器看到for循环我知道那是多份并行硬件。代码和电路之间的映射关系清晰了赋值方式的选择、敏感信号的列写、锁存器的避免都变成了自然而然的事情。如果你现在还在被并行和串行搞得头晕我的建议是找一块开发板写几个最简单的例子跑仿真、看波形、综合、上板把每个例子的电路结构画出来。画上十几个例子你就会有感觉了。纸上得来终觉浅硬件设计这门手艺最终还是要靠动手才能掌握。最后再分享一个小技巧当你拿不准一段代码综合出来是什么样的时候先跑综合看RTL视图。综合工具不会骗你它画出来的电路就是你的代码的真实含义。对着RTL视图反推代码是提升硬件思维最快的方法之一。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进