第二讲:Verilog基础语法——模块结构、数据类型、运算符、assign语句、always块

各位同学好,欢迎来到《弹载FPGA数字信号处理实战》的第二讲。

上一讲我们聊了开发环境搭建和FPGA的基本概念。今天要讲的东西,是Verilog的“地基”——语法基础。说白了,就是你要开始写代码了,得先知道代码长什么样,有哪些零件,怎么拼起来。

我个人习惯,讲语法不喜欢死记硬背。咱们边写边理解,你自然就记住了。

2.1 模块结构——Verilog的“黑盒子”

Verilog里最基本的单元叫模块(module)。你可以把它想象成一个黑盒子:有输入口、有输出口,里面装着逻辑电路。

一个模块长这样:

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

    // 内部逻辑写在这里

endmodule

注意几个关键点:

  • moduleendmodule 是成对出现的,就像括号一样,少一个都不行。
  • 端口列表写在括号里,每个端口要声明方向:inputoutput 或者 inout(双向口,用得少)。
  • 分号 ; 是语句结束符,千万别漏。我刚开始学的时候,经常漏分号,编译报错找半天。
小技巧: 我习惯把端口声明和数据类型写在一行,比如 input wire clk。这样代码更紧凑,看着也清楚。

2.2 数据类型——wire 和 reg 是主角

Verilog里数据类型很多,但实战中你主要跟两个打交道:wirereg

类型 含义 典型用法
wire 线网型,相当于物理连线 assign 语句赋值、模块端口连接
reg 寄存器型,可以存储值 always 块中赋值、时序逻辑

这里有个常见的坑:reg 不一定是寄存器。它只是表示一个变量,可以在 always 块里被赋值。如果 always 块是组合逻辑(后面会讲),那 reg 综合出来就是连线,不是寄存器。

避坑指南: 我曾经在项目里看到有人把 reg 和“寄存器”划等号,结果写组合逻辑时用了 reg,综合出来一堆没必要的锁存器。记住:reg 只是语法上的变量类型,不是硬件类型。

除了 wire 和 reg,还有 integer(整数)、parameter(参数)等。parameter 用得很多,比如定义位宽:

parameter DATA_WIDTH = 8;
reg [DATA_WIDTH-1:0] data_reg;

这样改位宽时只改一个地方,方便维护。

2.3 运算符——加减乘除,还有位操作

Verilog的运算符跟C语言很像。我列几个常用的:

  • 算术运算符: + - * /。注意乘除法综合出来资源很大,弹载FPGA资源有限,尽量用移位代替乘除。
  • 位运算符: & | ^ ~。按位与、或、异或、取反。
  • 逻辑运算符: && || !。结果是布尔值(0或1)。
  • 关系运算符: > < >= <= == !=
  • 拼接运算符: {a, b}。把多个信号拼在一起,非常实用。

举个例子:

wire [3:0] a = 4'b1010;
wire [3:0] b = 4'b1100;
wire [3:0] c;

assign c = a & b;  // 结果是 4'b1000
assign c = a ^ b;  // 结果是 4'b0110

拼接运算符我经常用在数据打包上:

wire [7:0] high_byte, low_byte;
wire [15:0] word = {high_byte, low_byte};
重点: 弹载雷达信号处理中,^(异或)和 &(与)用得特别多。比如校验和计算、位掩码操作。你想想看,雷达回波数据经常要做门限检测,一个比较器加一个与门就搞定了。

2.4 assign语句——连续赋值,组合逻辑的“连线”

assign 语句用来描述组合逻辑。它的意思是:等号右边的值一旦变化,左边立刻更新。就像一根导线,输入变了,输出马上跟着变。

基本语法:

assign  wire_name = expression;

注意:assign 左边必须是 wire 类型,不能是 reg。

实战例子:

wire [7:0] sum;
assign sum = a + b;  // 加法器,组合逻辑

多个 assign 语句是并行执行的,跟书写顺序无关。这一点跟软件完全不同。你写软件时,代码从上往下执行;但硬件里,所有 assign 同时生效。

我的经验: 写 assign 时,尽量让每个语句只做一件事。比如一个 assign 做加法,另一个做比较。这样代码可读性好,也方便调试。我曾经接手过一个项目,一个 assign 写了半屏幕,改起来想哭。

2.5 always块——时序逻辑的“心脏”

always 块是Verilog里最强大的结构。它描述的是:当某些条件满足时,反复执行块内的语句。

基本格式:

always @(敏感列表) begin
    // 语句
end

敏感列表决定了 always 块什么时候执行。最常见的有两种:

  • 组合逻辑: always @(*) —— 所有输入信号变化时执行。
  • 时序逻辑: always @(posedge clk or negedge rst_n) —— 时钟上升沿或复位下降沿时执行。

举个例子,一个带复位的计数器:

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

注意这里用的是 非阻塞赋值 <=。时序逻辑里一定要用 <=,组合逻辑里用 =。这个区别很重要,搞错了仿真结果会不对。

避坑指南: 我曾经在时序逻辑里用了阻塞赋值 =,结果仿真波形看起来没问题,但上板实测就是不对。后来查了半天,发现是赋值方式搞错了。记住:时序逻辑用 <=,组合逻辑用 =,这是铁律。

always 块里可以写 if-else、case 等语句。比如:

always @(*) begin
    case (sel)
        2'b00: out = a;
        2'b01: out = b;
        2'b10: out = c;
        default: out = 8'd0;
    endcase
end

这是多路选择器,组合逻辑。注意 case 要写全,或者加 default,否则会综合出锁存器。

2.6 模块实例化——把黑盒子连起来

写一个模块只是开始。真正的工程里,你要把多个模块连起来。这就叫实例化

比如我写了一个计数器模块 counter,现在要在顶层用:

counter u_counter (
    .clk    (sys_clk),
    .rst_n  (sys_rst_n),
    .cnt    (counter_out)
);

这里 u_counter 是实例名,括号里是端口连接。点号左边是模块的端口名,右边是顶层信号。

实例化时,端口可以按名字连接(推荐),也可以按位置连接(容易出错,不推荐)。

我的习惯: 实例化时,每个端口单独一行,对齐写好。这样代码清晰,也方便后期修改。你想想看,如果所有端口挤在一行,改一个信号名时多麻烦。

2.7 总结与实战建议

好了,这一讲的内容就这些。我们来捋一捋:

  • 模块结构: module/endmodule 是框架,端口声明是接口。
  • 数据类型: wire 是连线,reg 是变量,别搞混。
  • 运算符: 位运算和拼接用得最多,乘除要谨慎。
  • assign: 组合逻辑,连续赋值,并行执行。
  • always: 时序逻辑的核心,注意敏感列表和赋值方式。

下一讲我们会开始写真正的雷达信号处理模块——比如脉冲积累、CFAR检测。到时候这些语法都会用上。

嗯,今天就到这里。回去把代码敲一遍,仿真跑一跑。光看是学不会的,动手才是硬道理。