3. Verilog基础回顾(上):模块结构、数据类型、运算符、assign语句

各位同学,欢迎来到第三讲。说实话,每次带新人做FPGA项目,我都要先花半小时跟他们捋一遍Verilog基础。不是他们不会写,而是很多细节没吃透,写出来的代码要么综合不过,要么跑起来时序一塌糊涂。今天这一讲,咱们就把地基打牢。

我会从四个核心维度展开:模块结构、数据类型、运算符、assign语句。这四样东西,你玩明白了,后面写套利信号检测的逻辑才能行云流水。

Verilog基础回顾(上) 模块结构 module / endmodule 数据类型 wire / reg / integer 运算符 算术 / 逻辑 / 位 / 归约 assign语句 连续赋值 / 组合逻辑

3.1 模块结构——FPGA设计的"细胞"

Verilog里最基本的单元就是module。你可以把它想象成一个黑盒子:有输入引脚、输出引脚,内部封装了逻辑功能。我习惯把模块比作C语言里的函数,只不过它描述的是硬件电路。

一个标准的模块长这样:

module adder (
    input  wire [3:0] a,      // 4位输入
    input  wire [3:0] b,      // 4位输入
    output wire [4:0] sum     // 5位输出(考虑进位)
);
    assign sum = a + b;
endmodule

这里有几个关键点,我当年刚学的时候踩过坑:

  • 端口方向:input、output、inout。inout我建议新手慎用,双向总线处理不好容易出毛刺。
  • 端口类型:wire和reg。模块端口默认是wire类型,只有reg类型才能在always块里被赋值。
  • 位宽声明:[3:0]表示4位,高位在左,低位在右。这个顺序别搞反了,我曾经见过有人写成[0:3],综合出来位序全乱套。
我的习惯:写模块时,我会把输入和输出分开声明,中间空一行。这样代码可读性高,别人接手你的代码时一眼就能看清接口。

3.2 数据类型——wire和reg的"爱恨情仇"

数据类型这块,很多初学者容易混淆。说白了,就记住两句话:

  • wire:导线,只能被assign驱动。它不存储值,只是连接。
  • reg:寄存器,可以在always块里被赋值。它存储值,但综合出来不一定是寄存器——这很重要!

嗯,这里要特别强调一下:reg不一定会综合成触发器。如果你在always块里写组合逻辑(比如用always @(*)),reg综合出来就是组合电路。我刚开始做项目时,以为reg就是寄存器,结果综合报告里一堆LUT,触发器一个没用上。

看个例子:

reg [7:0] counter;      // 这个reg在always @(posedge clk)里,综合成触发器
reg [7:0] result;       // 这个reg在always @(*)里,综合成组合逻辑

// 时序逻辑
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        counter <= 8'd0;
    else
        counter <= counter + 1'b1;
end

// 组合逻辑
always @(*) begin
    result = counter * 2;
end

除了wire和reg,还有integerreal这些类型。但做FPGA开发,integer用得比较多,real基本不用——综合工具不支持浮点数。

避坑指南:我曾经在仿真里用integer做循环变量,结果综合时报错。后来才明白,integer是32位有符号数,综合工具对它的支持有限。建议用reg定义位宽明确的变量。

3.3 运算符——从算术到归约

Verilog的运算符种类挺多,但常用的就那么几类。我按使用频率给你排个序:

3.3.1 算术运算符

+、-、*、/、%。加减乘除都支持,但除法和取模要小心——综合工具对除法的支持很差,尤其是变量做除数。我一般只在仿真里用除法,综合时用移位或查找表替代。

3.3.2 逻辑运算符

&&、||、!。这些返回的结果是1位布尔值(0或1)。注意和位运算符区分开。

3.3.3 位运算符

&、|、^、~。按位操作,返回结果位宽和操作数相同。这是FPGA里最常用的运算符,套利信号检测里大量用到。

3.3.4 归约运算符

&、|、^、~&、~|、~^。这些是单目运算符,对一个向量的所有位进行操作,返回1位结果。比如:

wire [3:0] data = 4'b1010;
wire all_ones = &data;   // 结果为0,因为不是所有位都是1
wire any_one  = |data;   // 结果为1,因为至少有一位是1
wire parity   = ^data;   // 结果为0,1的个数为偶数

归约运算符在检测信号变化时特别好用。比如你想知道一个多bit信号是否有任何位发生变化,用^就可以快速算出奇偶校验。

3.3.5 关系运算符和移位运算符

>、<、>=、<=、==、!=。这些返回1位布尔值。移位运算符<<、>>在乘法除法里经常用来替代乘2或除2。

实战经验:在套利信号检测中,我经常用位运算符和归约运算符来快速比较两个价格序列的差异。比如用XOR检测变化,用归约AND判断是否全部满足条件。这些操作在一个时钟周期内就能完成,比CPU快几个数量级。

3.4 assign语句——连续赋值的"灵魂"

assign语句是Verilog里描述组合逻辑最直接的方式。它的核心思想是:只要右边表达式发生变化,左边立即更新。这跟硬件里的导线行为完全一致。

基本语法:

assign 左边信号 = 右边表达式;

几个要点:

  • 左边必须是wire类型(或者wire的拼接)
  • 右边可以是任何表达式,包括运算符、函数调用、条件表达式
  • 多个assign语句是并行执行的,跟书写顺序无关

看个实际例子,这是我在套利信号检测里用过的:

// 检测两个价格是否在套利窗口内
wire [31:0] price_a, price_b;
wire [31:0] spread;
wire        arbitrage_signal;

assign spread = (price_a > price_b) ? (price_a - price_b) : (price_b - price_a);
assign arbitrage_signal = (spread > THRESHOLD) && (spread < MAX_SPREAD);

这里用了条件运算符(?:)来计算价差绝对值,然后用逻辑与判断是否在套利窗口内。整个逻辑用两个assign就搞定了,清晰又高效。

我的建议:能用assign描述的组合逻辑,尽量用assign。它比always @(*)更直观,综合出来的电路也更可控。只有在需要中间变量或复杂条件分支时,我才用always块。

最后提醒一点:assign语句不能用于时序逻辑。如果你想描述一个触发器,必须用always @(posedge clk)。这个界限要分清楚,否则综合工具会报错。

好了,这一讲的内容就到这里。模块结构、数据类型、运算符、assign语句,这四个基础点你掌握了吗?下一讲我们会继续深入,聊聊always块和阻塞赋值与非阻塞赋值的区别——那可是FPGA设计的"分水岭"。


公众号:蓝海资料掘金营,微信deep3321