2. 硬件描述语言基础:Verilog/VHDL核心语法回顾、可综合与不可综合的代码风格、RTL建模原则
说实话,很多刚入行的朋友觉得硬件描述语言就是写代码。其实不然。你写的每一行代码,最终都要变成实实在在的电路。我见过太多人把Verilog当C语言写,结果综合出来的电路乱七八糟。
这一章,咱们就聊聊硬件描述语言的那些事儿。我会结合自己踩过的坑,帮你理清哪些是真正有用的。
2.1 Verilog核心语法回顾
Verilog是我最常用的语言。它的语法其实不复杂,但有几个关键点必须吃透。
2.1.1 模块与端口
模块是Verilog的基本单元。每个模块就像一个黑盒子,有输入、输出和内部逻辑。
module counter (
input wire clk,
input wire rst_n,
input wire en,
output reg [7:0] count
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
count <= 8'd0;
else if (en)
count <= count + 1'b1;
end
endmodule
我个人习惯把端口声明和内部信号分开写。这样代码结构清晰,后期维护也方便。记得有一次接手别人的代码,所有信号混在一起,我看了三天才理清楚。
2.1.2 阻塞赋值与非阻塞赋值
这是新手最容易搞混的地方。我建议你记住一句话:组合逻辑用阻塞赋值,时序逻辑用非阻塞赋值。
核心区别:
- 阻塞赋值(=):立即执行,后面的语句会等待它完成
- 非阻塞赋值(<=):并行执行,所有赋值在always块结束时同时更新
为什么会这样?因为综合工具会把非阻塞赋值映射成触发器,而阻塞赋值映射成组合逻辑。你想想看,如果把两者混用,综合出来的电路可能和你预期完全不同。
我曾经踩过的坑:在同一个always块里同时用了阻塞和非阻塞赋值,结果仿真通过了,但综合后的电路功能完全不对。查了两天才发现是赋值方式的问题。
2.1.3 参数化设计
用`parameter`定义常量,可以让你的代码更灵活。我习惯把模块的参数都放在顶部,一目了然。
module fifo #(
parameter DATA_WIDTH = 8,
parameter DEPTH = 16
) (
input wire clk,
input wire rst_n,
input wire wr_en,
input wire [DATA_WIDTH-1:0] wr_data,
output reg [DATA_WIDTH-1:0] rd_data
);
// 内部实现
endmodule
2.2 VHDL核心语法回顾
虽然我现在主要用Verilog,但VHDL也有它的优势。它的类型系统更严格,适合大型项目。
2.2.1 实体与结构体
VHDL把接口和实现分开了。实体(entity)定义端口,结构体(architecture)定义功能。
entity counter is
port (
clk : in std_logic;
rst_n : in std_logic;
en : in std_logic;
count : out std_logic_vector(7 downto 0)
);
end entity;
architecture rtl of counter is
signal count_reg : unsigned(7 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啰嗦。但它的好处是,你很难写出模棱两可的代码。每个信号的类型都清清楚楚。
2.2.2 常用数据类型
| 类型 | 说明 | 示例 |
|---|---|---|
| std_logic | 单比特信号 | '0', '1', 'Z', 'X' |
| std_logic_vector | 多比特总线 | "1010", x"FF" |
| unsigned | 无符号数 | "1010" = 10 |
| signed | 有符号数 | "1110" = -2 |
2.3 可综合与不可综合的代码风格
这是本章的重点。你写的代码,综合工具能不能把它变成电路?能,就是可综合;不能,就是不可综合。
2.3.1 可综合的代码风格
说白了,可综合的代码就是那些能映射到标准单元库的代码。我总结了几条原则:
- 使用明确的敏感列表:always @(posedge clk) 或 always @(*)
- 避免延迟语句:不要用 #10 这种延迟
- 避免循环中的变量:for循环的循环次数必须是常数
- 使用标准的结构:if-else、case、三目运算符
// 可综合的代码
always @(posedge clk) begin
if (rst_n)
q <= 1'b0;
else
q <= d;
end
// 不可综合的代码
always @(posedge clk) begin
#10 q = d; // 延迟不可综合
end
2.3.2 不可综合的代码风格
不可综合的代码通常用于仿真和测试。比如:
- initial块:只在仿真中执行一次
- 延迟控制:#10, @(posedge clk)
- 文件操作:$readmemh, $fopen
- 系统函数:$display, $monitor
小技巧:写testbench时,我习惯把可综合和不可综合的代码分开放在不同的文件中。这样综合时直接排除testbench文件,省心省力。
2.4 RTL建模原则
RTL(寄存器传输级)建模是数字设计的核心。说白了,就是描述数据如何在寄存器之间流动和处理。
2.4.1 时序逻辑与组合逻辑分离
我建议你把时序逻辑和组合逻辑写在不同的always块里。这样代码清晰,综合工具也容易优化。
// 时序逻辑
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) ? READ : IDLE;
READ: next_state = (done) ? IDLE : READ;
default: next_state = IDLE;
endcase
end
2.4.2 避免锁存器
锁存器(latch)是RTL设计的大敌。它会让时序分析变得复杂,而且容易出问题。
为什么会生成锁存器?说白了,就是条件分支没有覆盖所有情况。比如:
// 会生成锁存器
always @(*) begin
if (sel)
q = a;
// 缺少else分支
end
// 不会生成锁存器
always @(*) begin
if (sel)
q = a;
else
q = b;
end
我曾经踩过的坑:有一次写状态机,case语句的default分支忘了写,结果综合出来一堆锁存器。芯片面积大了30%,功耗也上去了。从那以后,我写case必带default。
2.4.3 模块化设计
把复杂功能拆分成多个小模块,每个模块只做一件事。这是RTL建模的基本原则。
- 单一职责:每个模块只完成一个功能
- 接口清晰:模块间的信号尽量少,且定义明确
- 层次化:顶层模块只做连接,不处理具体逻辑
2.5 知识体系总览
下面这张图,是我整理的本章知识体系。你可以把它当作一个快速参考。
嗯,这张图把本章的核心内容都串起来了。你可以看到,Verilog和VHDL是基础,可综合与不可综合是分水岭,RTL建模原则是实战指南。
我的建议:刚开始学的时候,先掌握Verilog。等用熟了,再学VHDL。两个都会当然更好,但别贪多嚼不烂。
好了,这一章的内容就到这里。记住,写硬件描述语言,心里要时刻想着电路。每一行代码,都要问自己:这个综合出来是什么样子?
公众号:蓝海资料掘金营,微信deep3321