第三章 硬件描述语言基础:组合逻辑与时序逻辑

各位同学,今天我们来聊聊硬件描述语言的基础。说实话,Verilog 和 VHDL 这东西,刚接触时觉得就是写代码,但写多了你会发现——它跟软件完全是两码事。

我刚开始做通信算法加速时,就犯过一个低级错误:把组合逻辑和时序逻辑混在一起,结果仿真怎么都对,上板就崩。后来被老工程师骂了一顿,才真正理解这两者的区别。

3.1 组合逻辑 vs 时序逻辑

组合逻辑,说白了就是输入一变,输出立刻变。没有记忆,没有时钟。比如一个加法器:a + b,结果瞬间出来。

时序逻辑就不一样了。它靠时钟驱动,输出只在时钟沿变化。寄存器、计数器、状态机,都是时序逻辑。

我个人的习惯是:能用时序逻辑的地方,绝不用组合逻辑。为什么?因为组合逻辑容易产生毛刺,时序逻辑天然抗干扰。

核心区别一句话:

  • 组合逻辑:输入决定输出,无记忆
  • 时序逻辑:时钟决定输出,有记忆

来看个简单的 Verilog 例子:

// 组合逻辑:assign 语句
assign sum = a + b;

// 时序逻辑:always @(posedge clk)
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        sum_reg <= 0;
    else
        sum_reg <= a + b;
end

注意看,组合逻辑用 assign,时序逻辑用 always @(posedge clk)。这个区别,你在写代码时一定要刻在脑子里。

3.2 状态机设计

状态机是通信算法加速的核心。你想想看,一个数据包进来,要经过解析、处理、输出,每一步都是一个状态。

我常用的状态机风格是 三段式。为什么?因为好维护,好调试。

三段式状态机结构:

  1. 第一段:时序逻辑,描述状态跳转
  2. 第二段:组合逻辑,描述下一状态
  3. 第三段:时序逻辑,描述输出

举个例子,一个简单的通信协议解析状态机:

// 状态定义
localparam IDLE     = 2'b00;
localparam HEADER   = 2'b01;
localparam PAYLOAD  = 2'b10;
localparam CHECKSUM = 2'b11;

// 第一段:状态跳转
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_flag ? HEADER : IDLE;
        HEADER:  next_state = header_done ? PAYLOAD : HEADER;
        PAYLOAD: next_state = payload_done ? CHECKSUM : PAYLOAD;
        CHECKSUM:next_state = checksum_done ? IDLE : CHECKSUM;
        default: next_state = IDLE;
    endcase
end

// 第三段:输出逻辑
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        data_valid <= 1'b0;
    else if (state == PAYLOAD && payload_done)
        data_valid <= 1'b1;
    else
        data_valid <= 1'b0;
end

我的经验:三段式状态机最大的好处是——出问题了,你一眼就能看出是状态跳转错了,还是输出逻辑错了。我曾经用两段式写过,结果调试了三天,最后发现是组合逻辑的竞争问题。

3.3 流水线设计思想

流水线,说白了就是「把一个大任务拆成多个小任务,每个小任务用一级寄存器隔开」。这样做的目的是——提高时钟频率。

你想想看,一个复杂的组合逻辑,从输入到输出可能要经过 10 级门,延迟很大。如果把它拆成 5 级流水,每级只做 2 级门的工作,那时钟频率就能提高 5 倍。

在通信算法中,流水线用得特别多。比如 FIR 滤波器、FFT、信道估计,几乎都是流水线结构。

来看一个简单的 3 级流水线加法树:

// 输入:a[7:0], b[7:0], c[7:0], d[7:0]
// 输出:sum

reg [8:0] sum1, sum2;
reg [9:0] sum3;

// 第1级流水
always @(posedge clk) begin
    sum1 <= a + b;
    sum2 <= c + d;
end

// 第2级流水
always @(posedge clk) begin
    sum3 <= sum1 + sum2;
end

// 第3级流水(输出)
always @(posedge clk) begin
    sum <= sum3;
end

这个例子很简单,但体现了流水线的核心思想:每级只做一点点事,用寄存器把结果锁存起来

注意:流水线不是万能的。它有三个代价:

  • 延迟增加:N 级流水,延迟增加 N 个时钟周期
  • 面积增加:每级都要加寄存器
  • 控制复杂:流水线冲突、数据依赖都要处理

我曾经在一个项目中,为了追求高频,把流水线做到了 20 级。结果控制逻辑比计算逻辑还复杂,最后不得不回退到 8 级。

3.4 知识体系总览

下面这张图,是我自己总结的本章知识体系。你可以把它当作一个「地图」,学完本章后对照着检查自己是否掌握了每个点。

硬件描述语言基础知识体系 组合逻辑 vs 时序逻辑 • 组合:输入决定输出 • 时序:时钟决定输出 • assign vs always • 阻塞赋值 vs 非阻塞赋值 • 毛刺问题 • 时序约束基础 状态机设计 • 三段式结构 • 状态跳转逻辑 • 输出逻辑 • 状态编码方式 • 安全状态机 • 状态机调试技巧 流水线设计 • 流水线思想 • 级间寄存器 • 延迟与吞吐率 • 面积与速度权衡 • 流水线冲突 • 数据依赖处理 核心思想:用寄存器隔离逻辑,用时钟驱动数据,用状态控制流程

嗯,这张图把本章的三个核心模块串起来了。你仔细看,它们之间是有联系的:状态机里的状态跳转是时序逻辑,输出可以是组合逻辑;流水线的每一级也是时序逻辑。说白了,时序逻辑是骨架,组合逻辑是血肉,状态机是大脑,流水线是效率

本章要点回顾:

  • 组合逻辑:assign、always @(*),无记忆,易毛刺
  • 时序逻辑:always @(posedge clk),有记忆,抗干扰
  • 状态机:三段式最推荐,好调试好维护
  • 流水线:拆大任务为小任务,提高时钟频率,但增加延迟和面积

最后说一句:这些基础东西,看起来简单,但真正用好需要大量实践。我见过太多人,状态机写成一锅粥,流水线做成面条。别急,慢慢来,多写多仿真,自然就熟了。


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