2. RTL设计基础回顾:Verilog/VHDL核心语法、组合逻辑与时序逻辑、同步设计原则与亚稳态处理

各位同学,欢迎来到第二章。这一章咱们不急着上NPU的硬核架构,先把吃饭的家伙——RTL设计基础,好好捋一遍。我见过太多同学,上来就写复杂模块,结果综合出一堆latch,时序跑都跑不通。说白了,基础不牢,地动山摇。

这一章我会带着大家回顾三个核心点:Verilog/VHDL的常用语法、组合逻辑与时序逻辑的区别、以及同步设计的黄金法则。嗯,亚稳态这个坑,我也会重点讲,因为我在项目中吃过它的亏。

2.1 Verilog/VHDL核心语法:够用就行

我个人习惯用Verilog,因为它在数字IC设计里更主流。但VHDL也有它的优势,比如类型检查更严格。咱们课程里以Verilog为主,但我会在关键地方提一下VHDL的对应写法。

你不需要背下所有语法,记住常用的就够了。我列个清单,这些都是你写NPU加速器时天天要用的:

  • 模块声明:module/endmodule,端口列表要写清楚input/output/inout
  • 数据类型:wire(线网)、reg(寄存器)、integer(整数)、parameter(参数)
  • 赋值语句:assign(连续赋值)、always @(*)(组合逻辑)、always @(posedge clk)(时序逻辑)
  • 操作符:位运算(& | ^ ~)、逻辑运算(&& || !)、算术运算(+ - * /)、移位(<< >>)
  • 条件语句:if-else、case/casex/casez
  • 循环语句:for、generate(用于批量例化)

核心原则:写RTL不是写软件。你写的每一行代码,都要能映射到硬件电路上。如果脑子里画不出电路图,那代码大概率有问题。

举个例子,一个简单的D触发器,Verilog这么写:

module dff (
    input  wire       clk,
    input  wire       rst_n,
    input  wire       d,
    output reg        q
);
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            q <= 1'b0;
        else
            q <= d;
    end
endmodule

注意这里用的是非阻塞赋值 <=,不是 =。这个区别我在后面会细讲。VHDL的写法类似,但语法更啰嗦一些:

entity dff is
    port (
        clk   : in  std_logic;
        rst_n : in  std_logic;
        d     : in  std_logic;
        q     : out std_logic
    );
end entity;

architecture rtl of dff is
begin
    process(clk, rst_n)
    begin
        if rst_n = '0' then
            q <= '0';
        elsif rising_edge(clk) then
            q <= d;
        end if;
    end process;
end architecture;

我个人觉得Verilog更简洁,但VHDL的rising_edge()函数确实让代码意图更清晰。选哪个?看公司规范,或者看你心情。

2.2 组合逻辑与时序逻辑:别搞混了

这是RTL设计里最基础、也最容易犯错的地方。我每次面试新人,必问这个问题。

组合逻辑:输出只取决于当前输入,没有记忆功能。比如一个加法器、一个多路选择器。在Verilog里,用 assign 或者 always @(*) 实现。

时序逻辑:输出不仅取决于当前输入,还取决于之前的状态。说白了,它有记忆功能,靠时钟沿触发。在Verilog里,用 always @(posedge clk) 实现。

你想想看,NPU里的计算单元,大部分是组合逻辑(乘法器、加法器),但控制通路和状态机,全是时序逻辑。两者配合,才能完成复杂的计算任务。

避坑指南:我曾经在写一个累加器时,不小心把组合逻辑和时序逻辑写在了同一个always块里,结果综合出来一堆奇怪的latch。后来花了整整两天才定位到问题。记住:组合逻辑用阻塞赋值 =,时序逻辑用非阻塞赋值 <=。这个规则,打死也别违反。

来看一个典型的错误例子:

// 错误写法:组合逻辑里用了非阻塞赋值
always @(*) begin
    q <= a & b;  // 这里应该用 =
end

// 正确写法
always @(*) begin
    q = a & b;
end

为什么?因为组合逻辑要求输出立即反映输入变化,非阻塞赋值会延迟一个时间片,导致仿真行为异常。嗯,这里要注意,综合工具可能会报warning,但仿真结果已经错了。

2.3 同步设计原则:让时钟说话

同步设计,说白了就是所有时序逻辑都受同一个时钟(或同源时钟)控制。这是现代数字IC设计的基石。为什么?因为同步电路的行为是可预测的,时序分析工具也能轻松搞定。

同步设计的几个黄金法则:

  1. 单时钟域原则:尽量使用同一个时钟。如果必须跨时钟域,一定要用同步器或FIFO。
  2. 避免异步复位:尽量使用同步复位,或者至少做异步复位同步释放。
  3. 避免组合逻辑反馈:组合逻辑的输出不要直接反馈到自己的输入,否则会形成latch或振荡器。
  4. 寄存器输出原则:模块的输出尽量用寄存器打一拍,避免组合逻辑直接驱动外部。

我的经验:在NPU设计中,数据通路往往很长,比如一个卷积计算链。我习惯在每个计算阶段末尾加一级流水线寄存器。这样虽然多了一个时钟周期延迟,但时序收敛会容易很多。你想想看,是牺牲一个周期划算,还是流片回来跑不到目标频率划算?

来看一个同步设计的典型结构:

module sync_design (
    input  wire       clk,
    input  wire       rst_n,
    input  wire [7:0] data_in,
    output reg  [7:0] data_out
);
    reg [7:0] data_reg;

    // 第一级寄存器
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            data_reg <= 8'd0;
        else
            data_reg <= data_in;
    end

    // 第二级寄存器(输出)
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            data_out <= 8'd0;
        else
            data_out <= data_reg;
    end
endmodule

这个例子很简单,但体现了同步设计的精髓:所有寄存器都在同一个时钟沿更新,数据流是可控的。

2.4 亚稳态处理:别让芯片"抽风"

亚稳态,是每个数字芯片工程师的噩梦。什么是亚稳态?简单说,就是当触发器的建立时间或保持时间不满足时,输出会进入一个不确定的状态——既不是0,也不是1,而是在两者之间振荡。这个状态可能会持续一段时间,然后随机稳定到0或1。

为什么会这样?因为触发器内部是一个正反馈环路,需要一定的时间来锁存数据。如果数据在时钟沿附近变化,环路来不及稳定,就进入了亚稳态。

亚稳态的后果是什么?轻则数据错误,重则整个系统崩溃。我在项目中遇到过,一个跨时钟域的信号没有做同步处理,结果导致状态机跳到了非法状态,整个NPU死机。那次debug花了我整整一周。

核心对策:对于跨时钟域的单比特信号,用两级同步器(两个触发器串联)。对于多比特信号或数据总线,用异步FIFO或握手协议。

两级同步器的Verilog实现:

module sync_2stage (
    input  wire       clk,
    input  wire       rst_n,
    input  wire       async_in,
    output wire       sync_out
);
    reg sync_reg1, sync_reg2;

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            sync_reg1 <= 1'b0;
            sync_reg2 <= 1'b0;
        end
        else begin
            sync_reg1 <= async_in;
            sync_reg2 <= sync_reg1;
        end
    end

    assign sync_out = sync_reg2;
endmodule

注意,两级同步器并不能完全消除亚稳态,但可以把亚稳态发生的概率降低到可以忽略的程度。对于绝大多数应用,这已经足够了。如果你需要更高的可靠性,可以用三级同步器。

避坑指南:我曾经看到有人把同步器的输出直接接到组合逻辑上,这是不对的。同步器的输出仍然可能有短暂的亚稳态,必须再接一级寄存器才能使用。另外,同步器只能用于单比特信号,多比特信号必须用FIFO或格雷码处理。

最后,总结一下本章的核心要点:

知识点 核心要点 常见错误
Verilog语法 模块、赋值、条件、循环 阻塞/非阻塞赋值混用
组合逻辑 输出只取决于当前输入 产生latch
时序逻辑 输出取决于历史状态 异步复位不处理
同步设计 单时钟域、寄存器输出 组合逻辑反馈
亚稳态处理 两级同步器、异步FIFO 跨时钟域不处理

好了,这一章就到这里。下一章我们会开始搭建NPU的顶层架构,到时候这些基础知识都会用上。记住,写RTL就像盖房子,地基打不牢,后面全是白费功夫。