资讯动态

VHDL并行语句全解析:从信号赋值到process与generate

发布时间:2026/10/24 6:08:10 来源:尧图企业网站定制
搞了这么多年FPGA我遇到过不少刚开始学VHDL的同学一上来就看代码觉得无非就是按顺序写逻辑结果一到仿真就傻眼——输出和预期完全对不上。问题大多出在一个地方没有真正理解并行语句Concurrent Statements是VHDL的骨架而process、赋值这些只是它的外壳。这篇文章我就拿实际项目里最常用的几种并行语句出来拆开讲从分类到用法再到踩过的坑一次性说清楚。1. 为什么说并行语句是VHDL的“第一道坎”很多从C语言或者Python转过来的人写VHDL习惯性地从上往下读代码认为每条语句是按先后顺序执行的。在软件语言里这个理解没错但在硬件描述语言里这是最大的误区。VHDL描述的是一个实际的电路电路里面所有门、触发器、连线是同时工作着的没有“先执行A再执行B”的概念。并行语句就是为了描述这种同时性而存在的。1.1 硬件并行和软件顺序的本质区别可以这么想软件程序的每条语句占据CPU的一个时间片CPU一次只能干一件事而硬件电路里每一个组合逻辑门、每一个寄存器都同时在工作。比如我一个模块里写了两条信号赋值语句a b AND c; d e OR f;综合之后就是两个独立的逻辑门b和c的变化会影响ae和f的变化会影响d这两条链路互不干扰、同时行为。这就是所谓“并行”的第一层含义。1.2 并行语句在整个设计里的角色VHDL的设计通常由一个或多个entity实体构成entity里architectural body结构体的核心内容全部是并行语句。拿一个加法器模块来说entity定义输入输出端口结构体里可能是三五个并行语句组合在一起完成功能。换句话说并行语句就是你描述电路行为的主要手段。谁把并行语句搞懂了VHDL就算入门了一半。我个人的经验是不要试图一开始就掌握所有语法先把下面这些并行语句的类别、适用场景和使用套路摸清楚后面做复杂设计就顺了。2. VHDL并行语句的完整分类和选型思路VHDL标准里并行语句包括信号赋值语句含条件信号赋值、选择信号赋值、process语句、block语句、generate语句、并行断言语句、并行过程调用以及元件例化。业界日常开发中使用频率从高到低大概是process 信号赋值 generate 元件例化 block 断言。这里我给新手一个粗暴但管用的选型思路你要实现的内容用什么并行语句简单的组合逻辑一两句话的事条件信号赋值when-else或选择信号赋值with-select时序逻辑时钟驱动、计数器、状态机process需要局部变量、循环控制组合逻辑算法process重复结构的批量例化多路数据通道generate大模块内部做功能分区代码组织block模块之间互相连接元件例化component instantiation这个表基本覆盖了日常95%以上的场景。下面每个大类我都展开讲讲尤其是大家容易迷糊的细节。3. 不带process的并行信号赋值最灵活的“接线工”3.1 简单信号赋值语句这个最简单就是a b AND c;这种写法适合直接把输入信号经过逻辑后送给输出。有些初学者会有疑问赋值语句到底是“连线”还是“逻辑门”严格说综合工具会根据右边的表达式推断出逻辑门但从行为上讲它等于时时刻刻在“接线”。这种赋值在结构体里是并行执行的顺序无关。这里有个易错点同一个信号不能在多个语句里赋值。比如a b AND c; a e OR f;这样写是错的因为两个驱动源同时驱动一个信号综合工具会报错。正确的做法是把逻辑合并a (b AND c) OR (e OR f);除非你用解析函数resolution function明确处理多驱动的情况否则一律避免对同一信号多处赋值。3.2 条件信号赋值——表达能力最接近日常逻辑条件信号赋值就是when-else写法y input_a WHEN sel 00 ELSE input_b WHEN sel 01 ELSE input_c;它的语义和if-else很像判断从上到下依次进行第一个命中的分支生效所以优先级是隐含的。这一点对做优先级编码器特别方便。比如按键扫描里多个按键同时按下时我们要读取最左边那个直接把按键枚举列上去前面的优先。不过要注意when-else每次判断的对象可以是信号表达式比较灵活但如果分支太多代码读起来冗长。我见过有人写出十几个分支的when-else其实大部分可以改写成with-select或者查表后面讲。3.3 选择信号赋值——做译码器和数据选择器非常顺手选择信号赋值是with-select写法WITH sel SELECT y input_a WHEN 00, input_b WHEN 01, input_c WHEN 10, input_d WHEN OTHERS;和when-else最大的区别是with-select是根据一个选择表达式的不同取值来分支分支之间是平级的没有优先级。这种写法天然适合mux、译码器、状态输出映射这些场合。比如把4位状态编码转换成段码WITH state_code SELECT display 1000000 WHEN 0000, 1111001 WHEN 0001, ... 0000000 WHEN OTHERS;这里必须要提OTHERS的用法。在实际工程项目里凡是信号类型是std_logic_vector之类的非全枚举类型最后一个分支必须写OTHERS否则综合工具会认为你遗漏了某些输入组合悄悄给你插锁存器latch到时候仿真正确但板子上电就出问题。这个坑我栽过一次找了两天才明白是综合工具插了不期望的锁存器。提示写完when-else或with-select之后建议先做一步“分支完整性检查”看看所有可能的输入组合是否都有对应分支。缺分支在仿真阶段不一定报错但在综合阶段极大概率产生latch这是并行信号赋值里最隐蔽的坑。4. process语句并行语句里的“小宇宙”4.1 process是被敏感列表激活的并行代码块process是VHDL里最有分量的并行语句。它本质上是一个“放在并行区域里的顺序代码块”块内部按照顺序执行但多个process之间是并行的。这样设计的目的很简单有些逻辑用几句话写不清楚比如带复位和使能的多周期操作你需要顺序描述的能力但最终还是要嵌入到整个并行框架中。一个最典型的process长这样process(clk, rst_n) begin if rst_n 0 then counter (others 0); elsif rising_edge(clk) then if en 1 then counter counter 1; end if; end if; end process;process括号里的是敏感列表sensitivity list意思是这些信号发生变化时process里的代码被“唤醒”重新执行一次。这里我再强调一个很多新手搞不清的点敏感列表列哪些信号取决于你process内部读哪些信号。组合逻辑的process要把所有输入都放进敏感列表时序逻辑通常只要时钟和异步复位。4.2 信号赋值和变量赋值在process里的行为差异process内部既能用信号赋值也能用变量赋值:。两者最大的行为差异是信号赋值在process结束时才会真正更新而变量赋值是立即生效的。举例说明process(clk) variable temp : integer : 0; begin if rising_edge(clk) then temp : temp 1; -- 立即生效 counter temp; -- 等process结束才更新 end if; end process;如果这里把temp也换成信号写成temp_sig temp_sig 1; counter temp_sig;那么counter拿到的是更新前的temp_sig仿真结果就完全不一样了。调试时序逻辑时第一反应先检查是不是信号和变量的使用混淆了这个错误排查起来非常让人头大。4.3 敏感列表缺失导致的经典仿真错误我见过太多人写组合逻辑的processprocess(a, b) begin y a AND b; end process;这没问题。可一旦内部逻辑还读了c却忘记把c写进敏感列表process(a, b) -- 忘了c begin y (a AND b) OR c; end process;仿真时c变化根本不会触发process重新执行输出就一直停留在旧值。硬件实测反而可能是对的综合工具往往无视敏感列表完整性问题直接给你生成正确的组合逻辑这就是常见的“仿真和综合不一致”。解决的办法也很简单组合逻辑process敏感列表要么手工列全所有输入信号要么直接写process(all)。现在主流工具都支持VHDL-2008里的process(all)写法省心很多。4.4 分支完整性复习时序逻辑里同样要留意讨论process时分支完整性同样重要。比如process(clk) begin if rising_edge(clk) then if en 1 then q d; end if; end if; end process;这个写法没有写en0时的行为意味着en0时q保持旧值。这是个典型的时序保持逻辑不是latch。区别在于时序逻辑里else分支缺失通常表示保持组合逻辑里else分支缺失才会推断latch。初学者把这俩搞混是个高频事故所以给自己留一条检视规则组合逻辑的process确保每个分支都有赋值时序逻辑的process允许部分分支保持但要弄清是不是你想要的。4.5 状态机和计数器是process的主场状态机的三段式写法里状态转移逻辑、次态组合逻辑、输出逻辑通常各用一个process也可能是两个process加一个组合赋值。硬件上这些寄存器、组合逻辑各自独立工作并行语句正好对应这种结构。计数器方面包括时钟分频、定时、PWM发生基本都是一个process搞定。关键是记住时钟边沿触发的条件必须放在if rising_edge(clk)里面这几乎是一个教条级规则违反它的后果很严重。经验宁可把process写得长一点也千万别写半个进程。也就是说一个process要么负责一组相关的寄存器更新要么负责一块完整的组合逻辑不要一会儿管时钟逻辑一会儿又管组合逻辑尤其是不要在一块process里既定义时序寄存器又到处给输出赋初值维护起来非常痛苦。5. block语句被很多人忽略的组织利器block语句在VHDL里有点像给代码块加一个“局部作用域”。它的基本形式block_name : BLOCK signal local_sig : std_logic; BEGIN -- 并行语句 END BLOCK block_name;block内部可以声明局部信号、常量甚至子程序。局部信号只在block内可见不影响外部连接。这对于大模块的层次化组织很有用。比如底层是一个16位的算术单元内部有好几条中间数据总线不想让这些中间信号出现在结构体顶层信号列表里就可以用block包起来。外部端口通过port map映射进去对外只暴露输入输出这样entity的端口列表保持干净顶层看起来一目了然。block还有一个带guard的特性加guard条件后可以控制内部信号的赋值使能。比如guarded_block : BLOCK (clk 1 AND clkEVENT) BEGIN q GUARDED d; END BLOCK;不过在目前的主流设计风格中guard机制用得相对少更常见的是把使能逻辑放进process里。block现在更多是作为结构组织工具帮你把层级理清楚。我的建议是工程规模小的时候不用刻意使用block一旦模块内部功能分区明显比如控制、数据通路、输出驱动block确实能让代码可读性上一个台阶。6. generate语句批量复制结构是它的看家本领6.1 for-generate实现重复结构的模板化在FPGA里经常要处理位宽很大、结构重复的数据通路比如32路并行数据取反、奇偶校验阵列、寄存器堆。手写32遍赋值语句不现实这时用for-generate最合适。GEN_BITS : FOR i IN 0 TO 31 GENERATE y(i) NOT x(i); END GENERATE;这里的核心意义不只是少写代码而是让综合工具明确看到这是一个规则的复制结构在布线时能做优化。for-generate里的索引可以用于信号下标也可以用来搭建更复杂的内部连接。6.2 if-generate实现条件生成有些场景下需要根据参数决定生成哪一块电路。比如同一个RTL想兼容两种接口位宽用generic参数传递再在generate里判断GEN_MODE : IF data_width 8 GENERATE -- 8bit逻辑 ELSIF data_width 16 GENERATE -- 16bit逻辑 END GENERATE;if-generate在参数化设计里几乎是标配。需要留意的是if-generate里的表达式只能是常量或generic参数不能是动态信号。综合工具在编译期就决定了哪一块电路被保留所以它本质是一个“编译期的条件编译”。想清楚这一点就不会拿它去判断数据值了。6.3 generate使用中最容易被咬的坑generate和process是VHDL最常见的组合在generate内部例化process时process的标签是允许重名的。只要generate的标号不同综合工具就能区分。另一个更常见的坑是for-generate里如果内部有信号需要逐位处理信号的索引写法容易出错。比如GEN : FOR i IN 0 TO 7 GENERATE data_tmp(i) data_in((i1)*8-1 DOWNTO i*8); END GENERATE;这句话在语法上没问题但使用的时候要非常小心generate里索引类型必须是整数而向量的高下标和低下标写法必须和声明保持一致。而且如果你定义的向量是downto方向那data_tmp(i)拿到的就是第i位顺序别搞反。提示在for-generate里例化子模块时注意位宽参数和数组下标的匹配。我见过有同学写接口位宽按8行8列复制结果错把generate的循环变量当成了数据位宽的一部分仿真过了但综合后资源占用比预计翻倍排查起来特别费劲。7. 并行语句使用中绕不开的常见错误清单7.1 同一信号的多驱动问题这是并行语句世界里最容易犯也最容易让人抓狂的问题。一个信号只能有一个驱动源除非你用了解析函数。在超大设计里有时两个process各自都对同一个信号赋值平时通过某些控制信号能保证不同时生效但综合工具和仿真器都会提出异议。正确姿势是把多驱动合并成单驱动或者通过内部信号中转再输出。7.2 组合逻辑环路combinational loop并行语句描述组合逻辑时如果输出反馈回输入且中间没有寄存器就会形成组合环路。仿真时经常会看到信号不断抖动或者卡在中间态综合时会报警告。判断方法很简单追踪每个信号的驱动链如果绕了一圈回到自己就是环路了。解决方式是加一级寄存器来打破循环。7.3 仿真和综合行为不一致这类问题往往和敏感列表不完整、分支不完整、信号初值依赖有关。仿真工具按行为级语义跑综合工具按实际电路推断两者在边界语法处理上可能不一致。所以写完并行语句我习惯用三步自查每个process的敏感列表是否完整最好直接用process(all)每个when-else/with-select/case是否覆盖了所有分支每个信号是不是只有一个赋值来源。这三条能做到大约九成的“仿真对、上板错”问题都能提前拦住。8. 并行语句在分层设计中的组织经验并行语句不只是语法层面的东西它还影响模块的组织方式。我参与过的项目中常见做法是顶层结构体用几个大block或直接若干process把功能分开每个block内部通过端口映射port map或信号连外。这里的组织准则有三个接口清晰每个并行语句块process/block/generate的输入输出尽量限定在指定端口或局部信号上避免一个process直接读几十个全局信号。单一职责一个process只做一件事。比如时钟分频的process不去同时产生其他控制脉冲状态机输出逻辑单独写不让状态寄存器process顺便把输出也算了。命名规范process标签、generate标号、block标签都要有实际含义。比如P_MAIN_FSM、G_DATA_PATH、B_ALU_CORE。命名清晰不仅在维护时有帮助综合工具的报错定位也要靠它。此外很多团队会要求并行语句和顺序语句在文件中的排布有统一约定比如先信号声明再并行语句块从上到下按“复位/时钟生成 → 数据通路 → 控制逻辑 → 输出驱动”排列。这样做的最大好处是代码review的时候可以直接按块审查不需要每次从头看到尾。9. 实际调试过程中总结的几条心得最后说说我自己实战里的体会。其一当你怀疑并行语句有问题时先看敏感性列表其次看分支完整性。这两点能覆盖大量仿真异常。我在一个串口接收模块的调试里数据采样一直慢半拍最后发现是一个process里把波特率分频计数器的中间信号漏写了敏感列表导致计数器更新后接收判断进程没被唤醒加上一个信号就解决了。其二多驱动问题不要靠掩盖。有些人为了消除警告在内部加一个三态门风险很大。正确的做法是梳理清楚驱动的来源必要时拆信号。信号驱动不过来的地方用辅助寄存器做同步才是最稳的。其三分支完整性检查不只是“有else就行”。最重要的是看else分支里到底赋了什么值。如果else分支把输出置为高阻或零但实际电路运行时不会走到else那一开始就不要写这个分支否则掩盖了设计意图。如果必须写注释里说明清楚为什么这个分支是安全兜底。其四多多利用generate做参数化设计。前期把模块的位宽和结构参数化好后面做不同规格的产品时只改generic不改代码。这也是并行语句能在复用层面发挥价值的场景。比如我做过一个可配置CRC计算模块位宽由generic决定内部用for-generate展开从8位改到64位只花了十分钟省下的事后调试时间非常可观。再分享一个小技巧如果你经常在结构体顶层看到一长串信号赋值可以尝试把它们全部挪进带标号的block或process里并使用局部信号。这样不但作用域清爽综合后的网表命名也更清晰跟波形图的时候一眼能找到对应的内部节点。VHDL的并行语句不是死记语法就能掌握的关键在于把代码映射到硬件图上想清楚信号从哪来、往哪去、哪个块驱动、哪个块读取。把这一层理顺了看别人的VHDL代码会轻松很多自己写起来也不容易翻车。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑