第1章:Verilog/VHDL基础语法——模块结构、数据类型与组合/时序逻辑

各位同学,欢迎来到《基于FPGA的电子对抗信号处理实战》的第一课。

说实话,很多初学者一上来就盯着复杂的算法、高速的接口,结果连最基本的模块结构都写错了。我见过太多人,代码能跑通,但综合出来一堆警告,最后定位问题花了两三天。嗯,咱们今天就把地基打牢。

1.1 模块结构——FPGA设计的“骨架”

一个FPGA设计,说白了就是一堆模块拼在一起。每个模块就像一块乐高积木,有自己的接口和内部逻辑。

在Verilog里,模块用 moduleendmodule 包起来。VHDL则是 entityarchitecture。我个人习惯用Verilog,因为写起来更简洁,尤其在电子对抗这种需要快速迭代的场景下。

来看一个最简单的模块例子:

// Verilog 模块示例
module counter (
    input  wire       clk,      // 时钟
    input  wire       rst_n,    // 复位,低有效
    output reg  [7:0] count     // 8位计数器输出
);

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            count <= 8'd0;
        else
            count <= count + 1'b1;
    end

endmodule

注意看,模块的端口声明里,我用了 input wireoutput reg。这里有个坑——端口类型和内部信号类型是两回事。我曾经在项目里看到有人把输出端口写成 output wire,然后在内部又用 assign 赋值,结果综合出来一堆锁存器。嗯,这个后面会细说。

我的习惯: 顶层模块的端口一律用 wire,内部模块的端口根据实际需要选择。这样顶层连线清晰,内部逻辑灵活。

1.2 数据类型:wire 与 reg——别再傻傻分不清

这是新手最容易搞混的地方。我当年刚学的时候,也纠结过“什么时候用wire,什么时候用reg”。

其实很简单:

  • wire:相当于一根导线。它只能被 assign 连续赋值,或者被模块实例化的端口连接。说白了,它就是个“被动”的信号。
  • reg:相当于一个寄存器。它只能在 always 块里被赋值。注意,它不一定会被综合成寄存器——如果你在组合逻辑的 always 块里用 reg,它可能只是根线。

来看个对比:

// wire 用法:连续赋值
wire [3:0] sum;
assign sum = a + b;

// reg 用法:时序逻辑赋值
reg [3:0] sum_reg;
always @(posedge clk) begin
    sum_reg <= a + b;
end

你想想看,如果我把 sum 声明成 reg,然后用 assign 去赋值,编译器会报错。反过来,把 sum_reg 声明成 wire,然后在 always 块里赋值,同样报错。这就是语法规则,没得商量。

避坑指南: 我曾经在调试一个高速ADC接口时,把数据总线声明成了 reg,但实际只用了组合逻辑赋值。结果综合工具把它当成锁存器处理,导致时序收敛失败。后来花了半天才找到原因——组合逻辑用 wire,时序逻辑用 reg,这是铁律。

1.3 组合逻辑:assign 与 always 的“组合”用法

组合逻辑,就是输出只取决于当前输入,跟时钟没关系。在电子对抗中,很多信号预处理(比如求绝对值、比较阈值)都用组合逻辑,因为延迟小。

Verilog里实现组合逻辑有两种方式:

  • assign:连续赋值,简洁明了。适合简单的逻辑运算。
  • always @(*):敏感列表用 * 表示所有输入信号。适合复杂的组合逻辑,比如多路选择、状态译码。

举个例子:

// 用 assign 实现
wire [7:0] abs_val;
assign abs_val = (a > b) ? (a - b) : (b - a);

// 用 always 实现(注意:这里 reg 类型,但综合出来是组合逻辑)
reg [7:0] abs_val_reg;
always @(*) begin
    if (a > b)
        abs_val_reg = a - b;
    else
        abs_val_reg = b - a;
end

注意看,always @(*) 里用的是阻塞赋值 =,而不是非阻塞赋值 <=。这是组合逻辑和时序逻辑的关键区别之一。我见过有人把 <= 用在组合逻辑里,结果仿真波形完全不对——因为非阻塞赋值是“先计算,后更新”,在组合逻辑里会引入一个时钟周期的延迟。

核心要点:
  • 组合逻辑用 assignalways @(*) + 阻塞赋值 =
  • 时序逻辑用 always @(posedge clk) + 非阻塞赋值 <=
  • 混用会导致仿真和综合结果不一致,切记!

1.4 时序逻辑:posedge 与 negedge——时钟的“边沿”艺术

时序逻辑是FPGA的灵魂。没有时钟边沿,就没有寄存器,也就没有状态机、计数器、FIFO这些东西。

在Verilog里,我们用 posedge 表示上升沿,negedge 表示下降沿。最常见的写法是:

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

这里 posedge clk 是时钟上升沿触发,negedge rst_n 是异步复位(低有效)。注意,敏感列表里可以同时有时钟和复位,但不能有两个时钟边沿——除非你是在做跨时钟域处理,但那又是另一门课了。

为什么用 negedge rst_n 而不是 posedge rst?嗯,这其实是历史遗留问题。早期FPGA的复位信号大多是低有效,而且下降沿复位可以避免复位信号在上升沿附近抖动。我个人习惯统一用低有效复位,这样代码风格一致,不容易出错。

我的经验: 在电子对抗信号处理中,很多高速接口(比如JESD204B)要求用 posedge 采样数据。如果你不小心用了 negedge,数据会错位半个周期。我曾经在调试一个1.6Gbps的LVDS接口时,就因为这个问题浪费了两天。所以,一定要仔细看芯片手册的时序图

1.5 实战小练习:一个简单的边沿检测器

学完基础语法,咱们来个小练习。边沿检测在电子对抗中很常见——比如检测脉冲信号的上升沿,用来触发采样或计数。

module edge_detector (
    input  wire       clk,
    input  wire       rst_n,
    input  wire       sig_in,      // 输入信号
    output reg        rising_edge, // 上升沿标志
    output reg        falling_edge // 下降沿标志
);

    reg sig_dly;  // 延迟一拍

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            sig_dly <= 1'b0;
            rising_edge  <= 1'b0;
            falling_edge <= 1'b0;
        end else begin
            sig_dly <= sig_in;  // 延迟一拍
            // 上升沿:当前为1,前一拍为0
            rising_edge  <= sig_in & ~sig_dly;
            // 下降沿:当前为0,前一拍为1
            falling_edge <= ~sig_in & sig_dly;
        end
    end

endmodule

这个模块很简单,但很实用。你想想看,如果输入信号有毛刺,这个检测器会误触发。怎么办?可以在前面加一个同步器,或者用三拍采样法。这些咱们后面章节会讲到。

1.6 本章小结

今天咱们聊了模块结构、wire和reg的区别、组合逻辑和时序逻辑的写法。这些都是FPGA设计的“基本功”,就像练武要先扎马步一样。

最后送大家一句话:写代码之前,先想清楚它是组合逻辑还是时序逻辑。想清楚了,数据类型和赋值方式自然就对了。

下一章,咱们会深入讲“阻塞赋值与非阻塞赋值”的底层原理,以及它们在实际项目中的坑。到时候我会分享一个我当年在雷达信号处理项目中踩过的雷——嗯,那可真是一段难忘的经历。

课后思考:
  • 如果我把 always @(*) 里的阻塞赋值 = 改成非阻塞赋值 <=,会怎样?
  • 为什么复位信号通常用 negedge 而不是 posedge
  • 边沿检测器如果输入信号频率很高,会有什么问题?

好,今天就到这里。咱们下节课见。