第二讲: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
注意几个关键点:
- module 和 endmodule 是成对出现的,就像括号一样,少一个都不行。
- 端口列表写在括号里,每个端口要声明方向:
input、output或者inout(双向口,用得少)。 - 分号
;是语句结束符,千万别漏。我刚开始学的时候,经常漏分号,编译报错找半天。
input wire clk。这样代码更紧凑,看着也清楚。
2.2 数据类型——wire 和 reg 是主角
Verilog里数据类型很多,但实战中你主要跟两个打交道:wire 和 reg。
| 类型 | 含义 | 典型用法 |
|---|---|---|
| wire | 线网型,相当于物理连线 | assign 语句赋值、模块端口连接 |
| reg | 寄存器型,可以存储值 | always 块中赋值、时序逻辑 |
这里有个常见的坑:reg 不一定是寄存器。它只是表示一个变量,可以在 always 块里被赋值。如果 always 块是组合逻辑(后面会讲),那 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 同时生效。
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检测。到时候这些语法都会用上。
嗯,今天就到这里。回去把代码敲一遍,仿真跑一跑。光看是学不会的,动手才是硬道理。