2. 硬件描述语言基础:Verilog语法速览、组合逻辑与时序逻辑、仿真与调试
好,咱们进入第二讲。说实话,很多初学者觉得Verilog就是另一门编程语言,跟C差不多。嗯,这个想法其实挺危险的。我见过太多人把Verilog写成C,结果综合出来的电路完全不是那么回事。
硬件描述语言,说白了就是「画电路」的另一种方式。你写的每一行代码,最终都会变成实实在在的门电路、触发器和连线。所以,脑子里要时刻有电路图。
核心思维转变:写Verilog不是在「编程」,而是在「描述硬件结构」。你的代码是给综合工具看的,不是给CPU跑的。
2.1 Verilog语法速览——够用就行
我不会给你列一本语法手册。咱们只挑FPGA行情解析中最常用的语法讲。其他的,用到再查。
2.1.1 模块结构
一个Verilog模块,就是一个硬件单元。它有输入、输出,还有内部逻辑。
module parser_core (
input wire clk, // 时钟
input wire rst_n, // 复位,低有效
input wire [7:0] data_in, // 8位数据输入
output reg [31:0] price_out // 32位价格输出
);
// 内部逻辑写在这里
endmodule
我个人习惯把端口声明和内部信号分开写。这样模块的接口一目了然,别人看你的代码也舒服。
2.1.2 常用数据类型
| 类型 | 说明 | 我的用法 |
|---|---|---|
wire |
线网类型,用于组合逻辑连线 | assign语句的输出、模块间的连接 |
reg |
寄存器类型,用于存储状态 | always块中的赋值对象 |
integer |
32位有符号整数 | 仿真中的循环变量,不可综合 |
小技巧:很多新手搞不清wire和reg。你记住一条:assign左边用wire,always块里赋值用reg。综合工具会自动处理,但写清楚是职业素养。
2.1.3 运算符速览
Verilog的运算符跟C很像,但有几个坑要注意。
- 位宽匹配:赋值时左右两边位宽必须一致。我曾经因为一个16位赋值给8位变量,查了整整一下午的波形。
- 拼接运算符
{}:行情解析中经常用到。比如把两个8位数据拼成16位:{high_byte, low_byte} - 三目运算符
?::综合出来是一个多路选择器。比if-else更直观。
// 实际项目中的例子:解析行情包中的价格字段
wire [15:0] raw_price;
wire [31:0] scaled_price;
assign scaled_price = {16'h0000, raw_price}; // 高位补0
2.2 组合逻辑与时序逻辑——这是分水岭
搞不清这两个,你的FPGA设计就是一团浆糊。我面试新人时,第一个问题就是:「组合逻辑和时序逻辑的区别是什么?」
2.2.1 组合逻辑
组合逻辑的输出只取决于当前输入。没有记忆功能,没有时钟边沿。
写法有两种:
- assign语句:适合简单的逻辑
- always @(*):适合复杂的组合逻辑
// 方式一:assign
assign is_buy_order = (order_type == 2'b01) ? 1'b1 : 1'b0;
// 方式二:always @(*)
always @(*) begin
if (price > 1000)
high_price_flag = 1'b1;
else
high_price_flag = 1'b0;
end
注意:组合逻辑的always块里,敏感列表必须用@(*)。如果你手动写敏感列表漏了信号,仿真和综合结果会不一致。我吃过这个亏,真的会谢。
2.2.2 时序逻辑
时序逻辑的输出不仅取决于当前输入,还取决于之前的状态。说白了,它有「记忆」。
核心写法:always @(posedge clk or negedge rst_n)
// 一个简单的计数器,用于行情包序号检测
reg [7:0] pkt_cnt;
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
pkt_cnt <= 8'd0;
else if (pkt_valid)
pkt_cnt <= pkt_cnt + 1'b1;
end
注意这里用的是 <=(非阻塞赋值),不是 =。为什么?
非阻塞赋值在always块结束时才统一更新。这模拟了触发器的行为——所有触发器在时钟沿同时采样、同时更新。如果你用阻塞赋值,综合出来的电路可能多出一级不必要的延迟。
2.2.3 组合 vs 时序:一张图说清楚
2.3 仿真与调试——别等上板才后悔
我见过太多人,代码写完直接烧到板子上,然后对着示波器一脸懵逼。仿真,是FPGA开发中最值得花时间的环节。
2.3.1 写一个简单的testbench
Testbench就是给你的设计喂数据、看结果。它本身不需要综合,所以可以写得随意一些。
`timescale 1ns / 1ps
module tb_parser_core;
reg clk;
reg rst_n;
reg [7:0] data_in;
wire [31:0] price_out;
// 实例化待测模块
parser_core uut (
.clk (clk),
.rst_n (rst_n),
.data_in (data_in),
.price_out(price_out)
);
// 生成时钟
always #5 clk = ~clk; // 100MHz时钟
// 测试激励
initial begin
clk = 0;
rst_n = 0;
data_in = 8'd0;
#20 rst_n = 1; // 释放复位
// 模拟一个行情包到达
#10 data_in = 8'hA5;
#10 data_in = 8'h3C;
#10 data_in = 8'hF1;
#100 $finish;
end
// 波形导出
initial begin
$dumpfile("wave.vcd");
$dumpvars(0, tb_parser_core);
end
endmodule
我的习惯:testbench里一定要加$dumpfile和$dumpvars。这样仿真完直接打开波形看,比看打印日志高效十倍。
2.3.2 调试三板斧
遇到bug别慌。按这个顺序来:
- 看波形:打开VCD文件,看信号是不是按预期变化。重点关注时钟沿附近的数据采样点。
- 加断言:在testbench里写
assert,让仿真自动检查关键条件。比如「价格字段不能为0」。 - 分段隔离:把大模块拆成小模块,逐个仿真。我一般先测数据接收部分,再测解析部分,最后测输出。
2.3.3 一个常见的坑:仿真通过,上板失败
为什么会这样?
仿真时,所有信号都是理想状态。但实际FPGA里有建立时间、保持时间、时钟抖动。最常见的问题是:
- 异步信号没同步:外部输入直接接到触发器,没有打两拍。仿真时没问题,上板后偶尔出错。
- 组合逻辑环路:仿真器能跑,但综合工具会报错或产生奇怪的振荡。
避坑指南:我曾经做一个高频行情解析项目,仿真完美,上板后每10万个包就丢一个。查了三天,发现是复位信号没做异步复位同步释放。从那以后,我的所有模块都加上了标准的复位同步器。
2.4 本章小结
Verilog语法不难,难的是硬件思维。组合逻辑是「线」,时序逻辑是「沿」。仿真不是走过场,是你和硬件对话的方式。
下一讲,我们会把这些基础用到真正的行情解析模块中。到时候你会发现,今天学的每一条规则,都是为性能服务的。
公众号:蓝海资料掘金营,微信deep3321