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 知识体系总览

下面这张图,是我整理的本章知识体系。你可以把它当作一个快速参考。

硬件描述语言基础 - 知识体系 HDL基础 Verilog核心语法 模块与端口 阻塞/非阻塞赋值 参数化设计 VHDL核心语法 实体与结构体 数据类型 进程与信号 可综合代码风格 明确敏感列表 避免延迟语句 标准结构 不可综合代码风格 initial块 延迟控制 文件操作 RTL建模原则 时序/组合分离 · 避免锁存器 · 模块化

嗯,这张图把本章的核心内容都串起来了。你可以看到,Verilog和VHDL是基础,可综合与不可综合是分水岭,RTL建模原则是实战指南。

我的建议:刚开始学的时候,先掌握Verilog。等用熟了,再学VHDL。两个都会当然更好,但别贪多嚼不烂。

好了,这一章的内容就到这里。记住,写硬件描述语言,心里要时刻想着电路。每一行代码,都要问自己:这个综合出来是什么样子?


公众号:蓝海资料掘金营,微信deep3321