3. Verilog基础回顾(上):模块结构、数据类型、运算符、assign语句
各位同学,欢迎来到第三讲。说实话,每次带新人做FPGA项目,我都要先花半小时跟他们捋一遍Verilog基础。不是他们不会写,而是很多细节没吃透,写出来的代码要么综合不过,要么跑起来时序一塌糊涂。今天这一讲,咱们就把地基打牢。
我会从四个核心维度展开:模块结构、数据类型、运算符、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,还有integer、real这些类型。但做FPGA开发,integer用得比较多,real基本不用——综合工具不支持浮点数。
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。
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语句不能用于时序逻辑。如果你想描述一个触发器,必须用always @(posedge clk)。这个界限要分清楚,否则综合工具会报错。
好了,这一讲的内容就到这里。模块结构、数据类型、运算符、assign语句,这四个基础点你掌握了吗?下一讲我们会继续深入,聊聊always块和阻塞赋值与非阻塞赋值的区别——那可是FPGA设计的"分水岭"。
公众号:蓝海资料掘金营,微信deep3321