第1章:Verilog/VHDL基础语法——模块结构、数据类型与组合/时序逻辑
各位同学,欢迎来到《基于FPGA的电子对抗信号处理实战》的第一课。
说实话,很多初学者一上来就盯着复杂的算法、高速的接口,结果连最基本的模块结构都写错了。我见过太多人,代码能跑通,但综合出来一堆警告,最后定位问题花了两三天。嗯,咱们今天就把地基打牢。
1.1 模块结构——FPGA设计的“骨架”
一个FPGA设计,说白了就是一堆模块拼在一起。每个模块就像一块乐高积木,有自己的接口和内部逻辑。
在Verilog里,模块用 module 和 endmodule 包起来。VHDL则是 entity 加 architecture。我个人习惯用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 wire 和 output 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 块里赋值,同样报错。这就是语法规则,没得商量。
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 @(*) 里用的是阻塞赋值 =,而不是非阻塞赋值 <=。这是组合逻辑和时序逻辑的关键区别之一。我见过有人把 <= 用在组合逻辑里,结果仿真波形完全不对——因为非阻塞赋值是“先计算,后更新”,在组合逻辑里会引入一个时钟周期的延迟。
- 组合逻辑用
assign或always @(*)+ 阻塞赋值= - 时序逻辑用
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的复位信号大多是低有效,而且下降沿复位可以避免复位信号在上升沿附近抖动。我个人习惯统一用低有效复位,这样代码风格一致,不容易出错。
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? - 边沿检测器如果输入信号频率很高,会有什么问题?
好,今天就到这里。咱们下节课见。