本节摘要:实体声明电路的对外接口,架构描述电路的内部实现,二者分离是 VHDL 模块化的根基。本节覆盖端口四种方向的读写权限、泛型参数化、一个实体配多个架构的工程用法,并给出可直接编译的完整示例。
上一章你已经写过 in 和 out,但实体声明的完整能力不止于此。VHDL 给端口四种方向,每种方向都是一份编译器强制执行的合同:
| 方向 | 能否读 | 能否赋值 | 典型用途 |
|---|---|---|---|
| in | 可以 | 不可以 | 时钟、复位、数据输入 |
| out | 不可以 | 可以 | 结果输出 |
| inout | 可以 | 可以 | 双向总线(配合高阻态) |
| buffer | 可以(读自己) | 可以 | 需要回读的输出,如计数器当前值 |
合同是认真的。给 in 端口赋值,编译器立即报错;在架构里读 out 端口同样报错。buffer 方向看似方便,但它会传染——上层把 buffer 端口接到普通信号时可能要求上层也用 buffer,所以我更倾向另一种写法:内部信号算好再赋给 out 端口,端口保持朴素的 out。看一个计满回零的计数器骨架,体会这种写法:
library ieee; use ieee.std_logic_1164.all; use ieee.numeric_std.all; entity count10 is port ( clk : in std_logic; rst : in std_logic; -- 同步复位,高有效 tick : in std_logic; -- 计数允许 count : out std_logic_vector(3 downto 0); -- 输出用向量类型 carry : out std_logic -- 计满指示 ); end entity count10; architecture rtl of count10 is signal cnt : unsigned(3 downto 0) := (others => '0'); -- 内部计数器 begin process (clk) begin if rising_edge(clk) then if rst = '1' then cnt <= (others => '0'); -- 复位优先 elsif tick = '1' then if cnt = 9 then cnt <= (others => '0'); -- 模十:计到 9 归零 else cnt <= cnt + 1; -- unsigned 可以直接加 end if; end if; end if; end process; count <= std_logic_vector(cnt); -- 内部算好,出口只做类型适配 carry <= '1' when cnt = 9 and tick = '1' else '0'; end architecture rtl;
注意出口那两行:内部用 unsigned 运算,出口处一次性转成 std_logic_vector 输出。内部类型服务于运算,端口类型服务于互联,各司其职。
实体还能声明泛型(generic)——编译期常量,实例化时才给定具体值。用泛型把上面的模十计数器改造成任意模值计数器:
entity counter_n is generic ( WIDTH : positive := 8; -- 位宽,实例化时可改 MAX : positive := 255 -- 计数上限 ); port ( clk : in std_logic; rst : in std_logic; count : out std_logic_vector(WIDTH-1 downto 0) ); end entity counter_n;
实例化时传参:u1 : entity work.counter_n generic map (WIDTH => 4, MAX => 9) port map (...);。同一个模块源码,实例化出四位模十计数器、八位模二百五十五计数器,代码一行不改。泛型是复用的第一道门,后面第 4 章的 ALU、第 5 章的移位寄存器都会用它。

实体与架构是"一对多"关系。给 counter_n 再写一个纯行为级架构,用整型变量直接数:
architecture behavioral of counter_n is signal cnt : integer range 0 to MAX := 0; begin process (clk) begin if rising_edge(clk) then if rst = '1' then cnt <= 0; elsif cnt = MAX then cnt <= 0; else cnt <= cnt + 1; end if; end if; end process; count <= std_logic_vector(to_unsigned(cnt, WIDTH)); end architecture behavioral;
同一实体名下有两个架构,仿真时用配置或直接实例化指定:u1 : entity work.counter_n(behavioral) port map (...);。括号里就是架构名。工程习惯:行为级架构用于快速验证算法思路,rtl 架构用于真正实现;两者端口完全一致,切换零成本。
⚠️ 常见坑:忘了写
end entity或架构名不配对,报错行号常指向文件末尾,与真实出错位置差很远。实体名、架构名、结尾的 end 三处对齐检查,是排查"莫名列号报错"的第一动作。
💡 关键直觉:把实体当芯片数据手册的"引脚与参数页",把架构当"内部原理图"。写代码前先写实体,等于先画框图再填电路——顺序反过来,十有八九要返工。
实体接口命名上,推荐"动词短语 + 方向"风格,如 read_data_i(输入)、write_enable_o(输出)、clock_buffer_io(双向)。避免单字母或缩写(d、we、clk_buf)——这种命名在大型工程里会迅速变成维护噩梦。
后缀 _i / _o / _io 标注方向,让阅读者一眼看清信号走向。Vivado 和 Quartus 的 lint 工具也支持这种命名风格,能自动验证端口方向一致性。
把内部信号也带 _i / _o 后缀。_i / _o 只应出现在实体端口。内部信号应该用描述性命名(state_counter、fifo_wr_ptr),不带方向后缀——因为内部信号方向在结构里是明确的。