3. 硬件描述语言基础(VHDL):实体与结构体、信号与变量、并发与顺序语句

好,咱们开始啃 VHDL 这块硬骨头。

说实话,很多做软件出身的朋友第一次看 VHDL,会觉得这玩意儿怎么这么别扭。又是实体又是结构体,信号和变量还得分清楚,写个赋值还得看是在 process 里面还是外面。嗯,我当年刚接触的时候也懵过一阵子。

但你别怕。VHDL 说白了就是描述硬件的一种语言。它不是 C 语言,不是顺序执行的。你写出来的每一行代码,最终都会变成实实在在的门电路、触发器和连线。想通了这一点,后面就好办了。

3.1 实体与结构体:芯片的“名片”和“内脏”

一个完整的 VHDL 设计,至少包含两部分:实体(Entity) 和 结构体(Architecture)。

你可以把实体想象成芯片的“名片”——它告诉外界:我这个模块有哪些输入引脚,哪些输出引脚。而结构体就是芯片的“内脏”——里面怎么连的,逻辑怎么跑的,全在这里定义。

核心理解:实体只负责接口,不负责功能。结构体只负责功能,不负责接口。两者分离,方便模块复用。

来看个最简单的例子。一个两输入与门:

-- 实体:定义接口
entity and_gate is
    port (
        a : in  std_logic;
        b : in  std_logic;
        y : out std_logic
    );
end entity and_gate;

-- 结构体:定义功能
architecture behavioral of and_gate is
begin
    y <= a and b;
end architecture behavioral;

你看,实体里用 port 声明了三个引脚:a 和 b 是输入,y 是输出。类型都是 std_logic,这是 VHDL 里最常用的标准逻辑类型,能表示 0、1、高阻、未知等状态。

结构体里就一行:y <= a and b;。这个 <= 是信号赋值符,表示把 a and b 的结果“连到” y 上。注意,这不是软件里的变量赋值,这是硬件连线!

我的习惯:写实体的时候,我会把每个端口的注释写清楚,比如“输入:时钟信号”、“输出:数据有效标志”。这样过几个月回头看,或者交给同事接手,一眼就能看懂。

3.2 信号与变量:一个天一个地

这是初学者最容易踩坑的地方。信号(Signal)和变量(Variable),名字听着差不多,但在 VHDL 里完全是两码事。

信号(Signal): 代表硬件中的一根“导线”或一个“寄存器”。它是有物理意义的。信号赋值用 <=,而且赋值不会立即生效——要等到当前进程(process)结束或者等到下一个 delta 延迟才会更新。这一点非常关键。

变量(Variable): 更像软件里的临时存储,只在 process 内部使用,没有物理对应物(或者说,综合工具会把它优化成连线或寄存器)。变量赋值用 :=,赋值立即生效。

对比项 信号(Signal) 变量(Variable)
声明位置 结构体、process 外部 process 内部
赋值符号 <= :=
生效时机 进程结束或 delta 延迟后 立即生效
物理意义 有(导线/寄存器) 无(临时存储)
跨进程通信 可以 不可以

我举个例子你就明白了。假设一个 process 里,你用信号和变量分别做累加:

-- 用信号做累加(错误示范)
signal cnt_s : unsigned(3 downto 0) := (others => '0');
begin
    process(clk)
    begin
        if rising_edge(clk) then
            cnt_s <= cnt_s + 1;  -- 这里读到的 cnt_s 是上一个时钟周期的值
            if cnt_s = "1111" then  -- 注意!这个判断用的是旧值
                -- 做点什么事
            end if;
        end if;
    end process;

-- 用变量做累加(正确示范)
begin
    process(clk)
        variable cnt_v : unsigned(3 downto 0) := (others => '0');
    begin
        if rising_edge(clk) then
            cnt_v := cnt_v + 1;  -- 立即加 1
            if cnt_v = "1111" then  -- 这里用的是新值
                -- 做点什么事
            end if;
        end if;
    end process;

我曾经踩过的坑:刚学 VHDL 时,我在一个 process 里用信号做中间计算,结果仿真死活不对。查了两天才发现,信号赋值不是立即生效的,后面的语句读到的还是旧值。从那以后,process 内部的中间计算我全用变量,只在需要跨进程传递数据时才用信号。

3.3 并发与顺序语句:硬件设计的“并行基因”

这是 VHDL 最核心的思维转变。软件是顺序执行的——上一行跑完才跑下一行。但硬件不是。硬件里所有东西都是同时工作的。

并发语句(Concurrent Statements): 写在结构体里、process 外面的语句。它们同时执行,不分先后。常见的并发语句有:

  • 简单信号赋值:y <= a and b;
  • 条件信号赋值:y <= a when sel = '1' else b;
  • 选择信号赋值:with sel select y <= a when "00", b when "01", c when others;
  • 元件例化(instantiation):把别的模块拿过来用

顺序语句(Sequential Statements): 写在 process 内部的语句。它们按顺序执行,但注意——这个“顺序”只在仿真时体现,综合出来的硬件仍然是并行的。常见的顺序语句有:

  • if-then-else
  • case
  • loop
  • 变量赋值

你想想看,为什么要有 process?因为有些逻辑用并发语句写起来太麻烦。比如一个复杂的有限状态机,用 case 语句写就清晰得多。process 就是给顺序语句一个“容器”。

来看一个带同步复位的 D 触发器:

process(clk)
begin
    if rising_edge(clk) then
        if rst = '1' then
            q <= '0';
        else
            q <= d;
        end if;
    end if;
end process;

这个 process 里,if 语句是顺序执行的。但综合出来是什么?就是一个 D 触发器,带一个同步复位。硬件上,时钟沿到来时,复位和数据的判断是同时完成的,只是我们用顺序语句把逻辑描述清楚了。

我建议:写 process 时,敏感信号列表一定要写全。如果是时钟沿触发的,就只写 clk(或者加上 rst 如果是异步复位)。如果是组合逻辑,就把所有输入信号都列上,不然仿真和综合结果可能不一致。

3.4 知识体系总览

下面这张图,把 VHDL 最核心的三个概念串起来了。你对照着看,心里就有谱了。

VHDL 基础核心结构 实体(Entity) 定义模块接口(port) 结构体(Architecture) 并发语句 • 简单信号赋值 • 条件/选择信号赋值 • 元件例化 顺序语句 • if-then-else • case • loop 信号(Signal) vs 变量(Variable) 信号:跨进程通信,物理连线 | 变量:process 内部临时存储

这张图你看懂了吗?从上到下:实体定义接口,结构体实现功能。结构体里面,要么用并发语句直接写,要么用 process 包顺序语句。而无论哪种方式,都离不开信号和变量这两个基本元素。

3.5 实战建议:从仿真开始

我个人的经验是,学 VHDL 别一上来就想着综合、烧录。先写 testbench,做仿真。仿真能看到每个信号在每个时刻的变化,能帮你理解“并发”和“顺序”到底是什么意思。

比如你写一个 process,里面用信号赋值。仿真波形一拉出来,你会发现信号的变化总是比你想的晚一个 delta 时间。这就是硬件描述语言和软件语言最本质的区别。

记住一句话:你写的每一行 VHDL,最终都会变成实实在在的硬件。想清楚你要的是什么电路,再动手写代码。不要像写软件一样,边写边想。

好了,这一章的内容就到这儿。实体和结构体是骨架,信号和变量是血肉,并发和顺序语句是灵魂。这三样东西搞明白了,VHDL 的大门就算真正推开了。