第四章:硬件描述语言核心
好,咱们进入正题。这一章可以说是整个FPGA设计的基石——硬件描述语言。说白了,就是你和FPGA沟通的“普通话”。你写出来的每一行代码,最终都会变成实实在在的电路。这和写软件完全是两码事。
我个人习惯把HDL的学习分成三块:语法基础、组合逻辑与时序逻辑、状态机。咱们一个一个来聊。
4.1 Verilog/VHDL语法速览
先说说选哪个语言。我早期做项目时用过VHDL,后来全面转向了Verilog。不是说VHDL不好,而是Verilog更灵活,写起来更像C,在工业界和学术界都更流行。当然,如果你在军工或欧洲某些领域,VHDL还是主流。
这里我快速过一下最核心的语法点,你不需要死记硬背,多用几次就熟了。
Verilog 核心语法
// 模块定义
module counter #(
parameter WIDTH = 8
)(
input wire clk,
input wire rst_n,
input wire en,
output reg [WIDTH-1:0] count
);
// 时序逻辑:always @(posedge clk)
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
count <= 0;
else if (en)
count <= count + 1;
end
endmodule
看到没?always块是核心。敏感列表里写posedge clk就是时序逻辑,写*就是组合逻辑。这个区别一定要刻在脑子里。
VHDL 核心语法
-- 实体声明
entity counter is
generic (
WIDTH : integer := 8
);
port (
clk : in std_logic;
rst_n : in std_logic;
en : in std_logic;
count : out std_logic_vector(WIDTH-1 downto 0)
);
end entity;
-- 架构体
architecture rtl of counter is
signal count_reg : unsigned(WIDTH-1 downto 0);
begin
process(clk, rst_n)
begin
if rst_n = '0' then
count_reg <= (others => '0');
elsif rising_edge(clk) then
if en = '1' then
count_reg <= count_reg + 1;
end if;
end if;
end process;
count <= std_logic_vector(count_reg);
end architecture;
VHDL的语法更严谨,但写起来也啰嗦。我个人觉得,如果你刚入门,从Verilog开始会更友好。
reg类型不一定生成寄存器,wire也不一定生成连线。关键看你怎么用。在always块里赋值的变量,综合工具会自动推断成reg类型。
4.2 组合逻辑与时序逻辑
这是FPGA设计的灵魂。很多新手搞不清楚“什么时候用组合逻辑,什么时候用时序逻辑”。我举个例子你就明白了。
组合逻辑:输出只取决于当前输入。比如一个加法器,输入A和B,输出就是A+B。没有时钟,没有记忆。
时序逻辑:输出不仅取决于当前输入,还取决于之前的状态。比如计数器,它需要记住当前计数值,然后在时钟沿到来时更新。
嗯,这里要注意:组合逻辑容易产生毛刺。我在项目中遇到过,一个简单的地址译码器,因为组合逻辑路径延迟不一致,导致读出了错误的数据。后来加了一级寄存器打拍,问题就解决了。
always @(*),但漏掉了敏感列表里的某个信号,结果仿真和综合结果不一致。排查了整整两天。所以,写组合逻辑时,要么用always @(*),要么用assign语句,别手动列敏感列表。
说白了,一个简单的原则:需要记忆就用时序逻辑,不需要就用组合逻辑。但实际项目中,我建议你尽量多用时序逻辑。为什么?因为时序逻辑有寄存器隔离,能有效消除毛刺,时序也更好收敛。
4.3 状态机设计模式
状态机是FPGA设计的“万能钥匙”。无论是通信协议、数据通路控制,还是简单的按键消抖,都能用状态机来实现。
我常用的状态机写法有三种:一段式、两段式、三段式。这里我重点推荐三段式。
三段式状态机模板
// 状态编码
localparam IDLE = 2'b00;
localparam START = 2'b01;
localparam DATA = 2'b10;
localparam DONE = 2'b11;
reg [1:0] state, next_state;
// 第一段:时序逻辑,状态转移
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
state <= IDLE;
else
state <= next_state;
end
// 第二段:组合逻辑,下一状态判断
always @(*) begin
case (state)
IDLE: next_state = start_sig ? START : IDLE;
START: next_state = DATA;
DATA: next_state = data_done ? DONE : DATA;
DONE: next_state = IDLE;
default: next_state = IDLE;
endcase
end
// 第三段:时序逻辑,输出
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
data_out <= 0;
done_sig <= 0;
end else begin
case (state)
IDLE: begin
data_out <= 0;
done_sig <= 0;
end
DATA: begin
data_out <= data_in;
done_sig <= 0;
end
DONE: begin
done_sig <= 1;
end
endcase
end
end
为什么推荐三段式?因为它的结构清晰,状态转移和输出分离,代码可读性强,也方便后期维护。我在做PCIe控制器时,状态机有十几个状态,用三段式写,调试起来轻松很多。
- 第一段:时序逻辑,只做状态寄存器的更新
- 第二段:组合逻辑,根据当前状态和输入判断下一状态
- 第三段:时序逻辑,根据当前状态产生输出
你想想看,如果所有逻辑都写在一个always块里,那代码得多乱?尤其是状态多的时候,自己都看不懂。
知识体系结构图
下面这张图,是我自己总结的HDL核心知识框架。你可以把它当作学习路线图。
这张图把HDL的核心内容串起来了。从语言基础出发,分支出Verilog和VHDL两套语法体系,再延伸到组合逻辑和时序逻辑这两个设计范式,最后落到状态机这个实战技能上。你按照这个路径学,不会走偏。
好了,这一章的内容就到这里。记住,HDL不是编程语言,是硬件描述语言。你写的每一行代码,最终都会变成实实在在的门电路和触发器。带着这个意识去写代码,你的设计水平会提升得很快。
公众号:蓝海资料掘金营,微信deep3321