3.3 并发语句与信号变量之辨


3.3 并发语句与信号变量之辨

本节摘要:架构语句区的并发语句本质都是进程的语法糖,信号赋值与变量赋值相差一个 delta 周期的生效时机。本节用"交换两个数"的对照实验把这一拍之差钉死,并整理条件赋值、选择赋值与进程的互改写关系。

先看一段会骗人的代码

下面这段代码想实现"同步交换两个寄存器的值",用信号写:

architecture wrong of swap is signal x, y : unsigned(7 downto 0) := (others => '0'); begin process (clk) begin if rising_edge(clk) then x <= y; -- 请求:下个 delta 把 y 的旧值给 x y <= x; -- 请求:下个 delta 把 x 的旧值给 y end if; end process; end architecture wrong;

它不但没错,而且是完全正确的交换电路!两个信号赋值都读的是"本周期开始时的旧值",delta 结算时同时换值——这正是硬件里两根线交叉对接的行为。但把其中一处换成 C 语言思维的"先存临时变量再交换",或者把 x 换成变量,行为立刻不同。理解差异之前,先把并发语句的家底盘一遍。

并发语句是进程的语法糖

架构语句区能写的并发语句,每一种都能改写成进程。对照表如下:

并发写法 等价进程写法 用途倾向
y <= a and b; process(a,b) begin y <= a and b; end process 一句话逻辑
y <= a when s='1' else b; process(a,b,s) begin if s='1' then y<=a; else y<=b; end if; end process 二选一带默认
with s select y <= a when "00", b when others; process(s,a,b) begin case s is ... end case; end process 多分支并列
实例化语句 不可改写 模块互联

改写关系说明:when-else 是 if 的糖,with-select 是 case 的糖。写一行的逻辑用糖,逻辑复杂到需要中间变量时退回进程——判断标准不是行数,是"是否需要中间状态"。另外注意并发赋值的多驱动问题:同一目标信号在架构里被两条并发赋值各写一次,编译能过,仿真出 'X'——第 1 章见过,这里是机制层面的完整解释:两份驱动器打架。

信号与变量:一拍之差定乾坤

两者的差异浓缩成一张表:

对比项 信号 signal 变量 variable
声明位置 架构声明区或进程外 进程声明区
赋值符号 <= :=
生效时机 delta 结算后(下一瞬间) 立即
作用范围 全架构可见 仅本进程
物理对应 连线或寄存器 无直接对应,综合后融入逻辑

"立即生效"与"下一瞬间生效"的差异,用计数实验最能看清。需求:时钟沿上对计数器先加二,再判断是否超过上限,超过就归零。用信号写:

process (clk) begin if rising_edge(clk) then cnt <= cnt + 2; -- 请求加二 if cnt > 250 then cnt <= (others=>'0'); -- 判断的是旧值! end if; end if; end process;

判断用的 cnt 是加二之前的旧值,边界检查慢一拍。用变量改写:

process (clk) variable v : unsigned(7 downto 0); begin if rising_edge(clk) then v := cnt + 2; -- 立即生效 if v > 252 then v := (others=>'0'); -- 判断的是新值 end if; cnt <= v; -- 结算回信号 end if; end process;

变量版本在进程内实时反映运算结果,边界判断精确。这就是变量的正确用法:进程内的便签纸,算完把最终结果抄给信号。工程纪律也由此而来:变量永远在进程末尾把值交给某个信号,别让重要状态只活在变量里——否则别的进程看不到它。

图:信号与变量的生效时机对照

图:信号与变量的生效时机对照

回到开头:亲手验证交换电路

把 wrong 架构配上测试台跑仿真:初始化 x 为三、y 为七,给一个时钟沿,波形里 x 变七、y 变三——同步交换成功。再把其中一个信号改成变量试试,行为立刻坏掉:变量读不到"对面"的值,因为变量只在进程内存在,另一根线的旧值得靠信号传递。这个实验做完,"信号是进程间通信的唯一通道、变量是进程内便签"这两句话就长在直觉里了。

⚠️ 常见坑:变量在进程里"记住"了上次的值,于是有人用变量存状态想做寄存器——可以,但只有"进程末尾被读且跨周期存活"的变量才会综合成寄存器,规则绕脑。清晰起见:状态一律用信号存,变量只做进程内中间计算

💡 关键直觉:并发语句是"看电路"的视角,进程是"看行为"的视角,两者互相可译。而信号与变量的选择,本质是在选择"要不要让下一行代码立刻看到结果"。

本节要点回顾

  • 并发皆糖:when-else 译作 if,with-select 译作 case,实例化不可译;
  • 信号跨 delta 生效:进程内读到的是旧值,交叉赋值恰是硬件交换的正确写法;
  • 变量当场生效:进程内便签,累积运算与边界判断的利器;
  • 状态用信号存:跨进程可见性与综合映射都更可控;
  • 多驱动即冲突:同一信号两条并发赋值,仿真出 X,别指望综合器纠错。

实战延伸:信号赋值的"延迟"特性

VHDL 信号赋值不是立即生效的,而是到 wait / 进程挂起时才更新。这是 VHDL 最反直觉的地方:

process(clock) begin if rising_edge(clock) then a <= b; -- a 立即"应该"等于 b c <= a; -- 但 c 拿到的是 a 的旧值,不是 a <= b 之后的值 end if; end process;

正确写法(用变量作中间缓冲):

process(clock) variable tmp : std_logic; begin if rising_edge(clock) then tmp := b; -- 变量立即生效 a <= tmp; -- 信号在进程末更新 c <= tmp; -- a 和 c 都拿到新值 end if; end process;

工程建议

在时钟进程里需要"先读再写"的链式赋值时,用变量作缓冲。在组合进程里避免链式信号赋值(容易产生 delta cycle 警告,且综合结果不可预期)。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U