资讯详情

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

📅 2026/10/24 1:04:46 | 华诺云谱 👁 阅读
VHDL并行语句全解析:从信号赋值到process、generate
第一次在VHDL代码里看到三条并行语句同时起作用的时候我整个人是懵的。写软件养成的从上到下的顺序思维在这里完全不适用——硬件描述语言VHDL的并行语句每一行就像接好的一根导线输入一变所有相关语句在同一时刻响应没有先后、没有主次。这篇内容就给还没跨过这个坎的朋友把VHDL并行语句的基本使用讲透它们有哪些、怎么用、为什么“同时执行”以及我实际踩过的坑。如果你是刚学FPGA/CPLD的入门者或者写了一段代码但仿真总不符合预期这篇文章应该能帮到你。1. 并行语句的本质先忘掉“从上到下”的阅读习惯1.1 软件顺序思维和硬件并行思维的分水岭写过C语言的人都知道代码是从上往下一条一条执行的上一行的运行结果直接影响下一行。到了VHDL这里这个规则突然不成立了。你在结构体里写两行并行赋值语句它们之间的关系不是“先执行A再执行B”而是“A和B同时被硬件实现了”。我见过太多初学者在这里栽跟头。举个例子你就明白了-- VHDL并行赋值两条语句同时生效 a b; c a;如果这是在C语言里执行完a b;之后a已经变成了新值接着执行c a;时c得到的是b的新值。但在VHDL里这两条并行赋值语句是同时执行的c得到的是a在执行这一轮之前的旧值不是b传过来的新值。刚接触硬件描述语言的人第一次仿真这种代码十有八九会怀疑自己写错了。为什么会有这种差异因为硬件世界里电路是真实存在的。你写a b;相当于把a这个信号用一条导线接到了b上写c a;又相当于把c用导线接到了a上。在一个时钟边沿或者其他事件触发的瞬间所有导线上的信号是同时更新的不可能存在“先执行第一句、再执行第二句”这种时序。并行语句的本质就是描述这种同时发生的硬件行为。想通这一点再看任何VHDL代码都会顺眼很多结构体architecture里所有并行语句的地位是平等的没有谁先谁后。1.2 并行语句大家族先知道有哪些成员VHDL标准里的并行语句其实有好几类我先把最常用的列出来心里有个框架后再逐个展开。并行语句类型常用关键字典型用途信号赋值语句、when-else、with-select描述组合逻辑、数据通路进程语句process内部写顺序逻辑外部看作并行模块生成语句generate、for-generate、if-generate批量生成重复结构参数化设计块语句block把一组并行语句打包形成局部作用域并行断言assert仿真时检查信号约束辅助调试并行过程调用procedure复用一段组合逻辑多个并行位置同时调用实际工程里最常用的是前四类其中信号赋值和进程几乎占了90%的代码量。这篇内容我会重点拆解它们generate和block也会讲清楚并行断言和过程调用简单提一下即可。先记住一个核心观念所有出现在结构体中的这些语句无论它们在源文件里写的顺序如何综合工具都会把它们当成同时工作的硬件模块来看待。1.3 并行语句的“同时执行”是怎么实现的有朋友问过我既然是同时执行那仿真器到底怎么模拟这种“同时”这就要说到事件驱动和模拟时间的概念。VHDL仿真器维护着一个时间队列。某个信号的值一变所有“监听”这个信号的并行语句都会被触发进入等待执行的队列。仿真器把这些事件放到同一个模拟时间点处理处理完之后如果又有信号的值发生了新变化就继续下一轮。这个过程看起来就像所有电路在同一时刻响了实际上背后是严格按照事件顺序一轮一轮推进的。这也解释了为什么我在1.1里给的那个例子c拿到的是a的旧值两条赋值语句在同一轮事件里被处理c读的a还是进入这一轮之前的值。硬件上这等效于真实的传播延迟只不过VHDL用离散时间步近似了它。理解这一层后面看仿真波形时就不会疑惑为什么信号更新的时刻总是比敏感信号变化晚一点点。2. 三种信号赋值语句从简单到复杂逐个过2.1 简单信号赋值最基础的并发赋值最简单的并行语句就是一条带的赋值signal a, b, c : std_logic; begin c a and b; end;这条语句的意思是结构体一执行c就持续被a and b的结果驱动。a或b任何时候变化c都会自动跟着更新。综合成电路就是一个与门没有任何顺序可言。这种简单赋值可以在结构体里写很多条每条各管一个信号q0 d0 when enable 1 else 0; q1 d1 and enable; q2 not d1;它们互不影响、同时工作。写这种代码的时候我不建议在脑子里把它们想成“程序行”而是把它们想成“一排并排摆放的门电路”。2.2 条件信号赋值when-else优先级逻辑怎么表达条件赋值在工程里非常常见写法是这样的z a when en 1 else b when sel 1 else c;这个语法的语义很直白先判断第一个条件en 1成立就赋a不成立再接着判断下一个条件sel 1成立赋b都不成立就赋c。注意一个关键点when-else是有优先级顺序的写在最前面的条件优先级最高。这一点和C语言的if-else if-else是一致的。很多初学者会搞反以为最后写的那条优先级最高其实不是。工具从第一个when开始依次判断先满足的先用。这个特性非常适合描述优先级电路。比如按键扫描里的优先级判断、中断请求的优先级仲裁、带复位优先级的输出int_out int_high when priority_level 00 else int_mid when priority_level 01 else int_low;如果两个条件同时为真priority_level 00会赢。这个行为在综合后就是一个带优先级的组合逻辑网络不是平级的多路选择器。使用when-else还有一个容易忽略的点最后必须有个兜底赋值。哪怕所有条件都分析了我也建议写一条独立的else让工具知道所有情况都有出路不然综合器可能推断出你意想不到的锁存器。2.3 选择信号赋值with-select真正的多路并行选择和when-else形成对比的是选择信号赋值with sel select mux_out data0 when 00, data1 when 01, data2 when 10, data3 when others;它的语义和C语言的switch-case更像根据sel的值直接命中某一个分支去赋值。所有分支是平级的没有优先级概念。这里有个重要语法选择值必须覆盖完整。sel是两位std_logic_vector的话有“00、01、10、11”四种取值上面的代码用when others兜住了最后一个分支。如果不写when others某些取值没有对应分支仿真时可能没反应综合时也可能产生锁存器。with-select很适合描述多路数据选择器、译码器、查表逻辑。我写过最典型的应用是七段数码管的字形译码表with hex_val select seg_out 1000000 when 0000, 1111001 when 0001, -- 中间省略 0000110 when others;这种平级多分支的逻辑用with-select写出来可读性比一长串if-else强得多。2.4 三种赋值怎么选一张对比表看完赋值形式优先级典型场景综合结果简单赋值无优先级组合逻辑、连续驱动门电路/导线连接条件赋值when-else从前往后递减优先级编码、带复位逻辑优先逻辑网络选择赋值with-select各分支平等多路选择器、译码器、查表多选一MUX/译码网络我的实际选择经验是这样的需要“如果满足某个条件就怎样否则怎样”这种带优先级的逻辑用when-else需要根据某个选择信号去向多个分支之一并且分支之间没有优先级用with-select至于最单纯的组合逻辑比如c a and b这样的直接简单赋值不要绕弯子。这三种语句都是并行语句写在结构体里彼此伴随信号变化同时更新。理解它们之间的语义差异是写出清晰VHDL代码的第一步。3. process进程并行语句里的“顺序岛”3.1 进程是并行语句内部却是顺序语句结构体里的process本身是一个并行语句。你在结构体里写了三个process这三个process之间是并行关系同时独立运行。但进入任何一个process内部语句就变成了顺序执行从上到下一条条跑上一句的结果会影响下一句。这就相当于一片“并行海洋里的顺序小岛”。很多初学者看到process的大括号就以为是函数其实不是。一个process更像一个独立的硬件模块内部可以有自己的一套顺序逻辑。process(a, b) begin if sel 0 then y a; else y b; end if; end process;上面的process是一个组合逻辑模块敏感列表里有a、b两个输入以及sel。如果我把敏感列表写全了那么这个process和下面这条并行条件赋值语句是等价的y a when sel 0 else b;所以你看process和直接写并行赋值只是表述方式的差别描述出来的硬件可以是同一个东西。3.2 敏感列表进程的“触发开关”process怎么知道什么时候该执行靠敏感列表。process(a, b)的意思是a或b的值一旦变化这个进程就会被唤醒从头到尾执行一遍内部语句。敏感列表必须写全。组合逻辑进程里所有出现在if条件中、赋值号右边的输入信号都应该放进敏感列表。如果遗漏了一个仿真时这个信号变化不会触发进程逻辑就像“卡住”了一样但综合工具是按完整逻辑推断硬件的于是就会出现仿真正常、上板行为和仿真不一致的经典事故。时序逻辑进程则通常只在敏感列表里放时钟和复位信号process(clk, rst_n) begin if rst_n 0 then q 0; elsif rising_edge(clk) then q d; end if; end process;这个process只在复位拉低或时钟上升沿到来的时刻被唤醒内部语句在时钟边沿那一刻执行一次实现了D触发器的行为。如果没有时钟边沿事件q保持原值不变。这里还要提一个有历史包袱的规则传统VHDL标准要求敏感列表和wait语句不能同时出现在一个进程中。我在老项目里见过用process加wait on clk的写法后来新代码全部改用敏感列表避免工具兼容性问题仿真和综合行为也更直观。3.3 信号与变量进程里最容易混淆的一对在process内部你能声明两类对象信号signal和变量variable。它们的差别非常关键写错会让仿真结果完全不是你想的那样。信号赋值用变量赋值用:。信号赋值有延迟效应process内部对信号赋值不会立刻把这个值更新到信号上而是等到process执行完、挂起之后才真正生效。变量赋值则立即生效下一句马上能读到新值。看这个例子process(clk) variable tmp : std_logic; begin if rising_edge(clk) then tmp : d; q1 tmp; q2 tmp; end if; end process;因为tmp是变量:立刻生效所以q1和q2最终都会拿到同一下降沿之前d的值。如果我把tmp改成一个信号情况就变了process(clk) begin if rising_edge(clk) then tmp_sig d; q1 tmp_sig; q2 tmp_sig; end if; end process;tmp_sig的信号赋值要到进程结束时才更新所以在执行q1 tmp_sig;时读到的还是tmp_sig的旧值。最终q1拿到的不是d而是上一个状态留下的旧值。同样一段逻辑信号和变量的差异直接改变了电路行为。实际项目里我的习惯是时序逻辑里要做中间暂存如果那个值需要在多个语句之间共享并且希望它在下一个时钟沿“成为一个寄存器”就用信号如果只是组合逻辑里算到一半的临时结果不想引入额外的存储行为就用变量。最终综合结果通常符合这个预期。3.4 组合逻辑与时序逻辑的process标准写法组合逻辑process有个原则所有输入进敏感列表所有分支写全避免锁存器推断。典型写法process(a, b, sel) begin if sel 1 then y a; else y b; end if; end process;时序逻辑process则记住三要素异步复位优先判断、时钟边沿触发、内部用信号状态赋给输出寄存器process(clk, rst_n) begin if rst_n 0 then count (others 0); elsif rising_edge(clk) then if en 1 then count count 1; end if; end if; end process;复位写成异步复位也就是rst_n在敏感列表里且rst_n 0的判断写在最前面如果要同步复位就把复位条件放在rising_edge(clk)后面。异步复位和同步复位的选择要看项目需求如果你做的是跨时钟域模块我的经验是复位释放同步化一定要做否则很容易出现亚稳态问题。4. generate循环与block块结构级并行语句4.1 for-generate一条语句生成一片电路写FPGA时经常遇到完全重复的电路结构比如八路D触发器、三十二位寄存器阵列、同类型的滤波运算单元。这种场景最适合用for-generategen_regs : for i in 0 to 7 generate q(i) d(i) when en 1 else q(i); end generate;上面的代码生成了八条并行的赋值语句等价于手工写八行但代码更简洁、参数可调。如果换成例化模块批量连接端口的优势更明显gen_unit : for i in 0 to 7 generate u_reg : entity work.reg_dff port map ( clk clk, rst_n rst_n, d din(i), q dout(i) ); end generate;for-generate里有一个循环变量i它只在生成期存在用来生成不同索引的硬件。注意循环范围可以写成0 to 7或7 downto 0生成的方向不同索引对应关系也不同。项目里我喜欢用downto和位宽定义保持一致减少后期对位号的困惑。用for-generate时有个小细节generate必须有标签就像上面写的gen_regs和gen_unit否则综合器会报语法错误。这个标签同时也是综合后硬件层次结构里的一个节点名字调试时可以从RTL视图里顺着标签找到生成的每个子模块。4.2 if-generate按条件生成不同结构参数化设计时如果想让同一份代码根据位宽参数生成不同的电路就用if-generateif GEN_WIDTH 8 generate data_out data_in(7 downto 0); else generate data_out data_in(15 downto 8); end generate;这里的if后面不写then这是generate结构VHDL-2008之前特有的语法和进程里的if完全两码事。它的作用是在编译阶段就确定保留哪个分支、抛弃哪个分支。综合工具看到的是定稿后的电路而不是同时生成两个分支再选一个。我用if-generate最典型的场景是做一个同时支持多种位宽FIFO的顶层根据generic传入的宽度决定实例化哪个实现版本。这能让模块复用性大幅提高只需要改顶层的一个generic参数就能在8位和16位之间切换整个数据通路。需要提醒的是if-generate不能单独出现在结构体顶层它必须被包在一个generate语句块里。也就是说前面的gen_regs : for ... generate这种带标签的generate块里内部可以放if-generate和for-generate组合。单独裸写一个if ... generate在某些工具里能过但为了兼容性我建议始终给外层加标签。4.3 block语句给并行语句“画边界”block语句可以把一组并行语句包成一个相对独立的区域并声明局部信号。用法类似名字空间blk_comb : block signal internal_sig : std_logic; begin internal_sig a and b; c internal_sig; end block;internal_sig只在blk_comb内可见不会跑到结构体的其他位置去。这在大型代码里可以起到隔离局部连线、避免信号名冲突的作用。不过说实话现代工程里我很少用block。一是因为代码分层通常靠多个architecture和entity实现block的封装能力有点鸡肋二是因为有些老工具对block的支持会引入奇奇怪怪的仿真层次问题。知道这个语法、能读得懂老代码就够了新设计我更推荐用子模块例化和generate来组织结构。5. 常见错误与排查经验我踩过并且你也会踩的坑5.1 敏感列表不完整仿真正确、上板失灵这个坑我印象最深。当年写一个组合逻辑模块进程里读到了a和b两个输入但敏感列表只写了a。仿真的时候恰好每次都先动a所以结果看起来一直是对的。后来某个场景只动b仿真立刻不对了。更恐怖的是综合工具生成的实际电路是同时受a和b控制的上板后硬件行为跟“完整敏感列表”一样于是出现仿真和实物不一致。排查方法很简单组合逻辑进程里把所有条件表达式和右侧表达式用到的输入信号全部列进敏感列表。如果觉得麻烦可以直接用并发赋值语句替代组合逻辑process让工具的依赖分析帮你搞定。新手阶段不要在这上面节省时间。5.2 多个驱动源一个信号被两条并行语句赋值结构体里两条并行语句同时给同一个信号赋值是很容易踩的标准错误y a; y b;这在VHDL里是多个驱动源。仿真器可能按某种顺序解析但结果不可预测综合工具更会直接报错。硬件上一个信号只能有一个驱动源就像一根导线只能接一个输出端口一样。如果确实需要二选一就把它们合并成一条语句或一个processprocess(a, b, sel) begin if sel 1 then y a; else y b; end if; end process;还有一种多驱动是跨模块造成的这个模块给某个信号赋值了顶层又有一个并行语句给同一个信号赋值。比如某个输出端口在entity里定义成了out然后在两个不同的实例化模块里都试图驱动它。排查时可以从综合错误信息里找“multiple drivers”这个关键词顺着路径找到两个来源再统一到单一驱动源。5.3 组合逻辑忘写else综合出意外的锁存器写进程时如果组合逻辑只给了部分条件路径综合器会推断出锁存器。典型情况process(a, b) begin if a 1 then y b; end if; end process;如果a 0这个进程没有告诉综合器y应该等于什么。综合器为了“保持住”上一次的值只能生成锁存器。这在组合逻辑模块里往往不是你想要的东西会带来时序分析异常和毛刺问题。我的习惯是组合逻辑的if一定配齐elsecase一定配齐others或者开头的默认赋值写掉。比如上面的进程改成process(a, b) begin y 0; if a 1 then y b; end if; end process;先给默认值再覆盖这样所有分支都有明确赋值锁存器不会出现。检查综合报告里的latch警告或者在RTL视图里看到非预期的锁存图标是两种常用的确认手段。5.4 when-else写“优先级”最容易搞反when-else的优先级顺序我之前已经讲了再强调一次排在上面的条件先判断优先级最高。我见过不止一个同事在评审代码时指着最下面那个else说“这个是最高优先级”实际上恰好相反。判断优先级时可以从硬件角度想第一个条件判断如果成立后面所有逻辑直接失能这就形成了一个串联的优先网络越靠前的分支权重越高。如果你需要实现“复位信号优先于使能信号”就把复位判断放在第一行使能判断放在第二行最后再写兜底值。5.5 从波形看并行调试并行语句的实操方法最后分享一个经验值极高的调试方法看仿真波形时把关键信号全部添加到波形窗口然后观察更新时刻。你会发现并行语句触发的信号更新往往和源信号的跳变沿差了一个仿真时间步这是delta cycle在起作用。出现“我以为同时更新结果差了一个节拍”的情况时先想信号赋值和变量赋值是不是混用了再想是不是敏感列表漏了信号最后想是不是两个并行语句顺序上产生了依赖。另外在仿真里加assert语句也很实用assert not (en 1 and rst_n 0) report enable and reset should not be active at the same time severity warning;这是一条并行断言语句它会持续监控条件条件成立时在仿真控制台输出提示。做约束检查比肉眼盯波形可靠得多遇到复杂状态机时能节省大量排查时间。写到这里并行语句的基本面貌就完整了。最后再说点我自己的体会每写完一段并行语句先不要急着仿真试着把它用门电路草图画一遍。画得出来代码大概率没问题画不出来多半思维还停在软件顺序执行的惯性里。VHDL并行语句没什么高深技巧重点就是建立“信号变化驱动电路更新”的直觉。多写几个例子、多看几次仿真波形这个坎很快就能过去。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑