第二讲:组合逻辑与数据通路——从assign到MUX的实战之路

各位同学,欢迎来到《Verilog量化交易核心代码实战》的第二讲。今天我们要聊的是组合逻辑与数据通路。说白了,就是那些不依赖时钟、输入一变输出立马跟着变的电路。在量化交易系统里,这类电路负责处理最基础的数据运算——比如计算两个价格之间的差值、判断某个阈值是否被突破。

我个人习惯把组合逻辑比作「流水线上的传送带」。你放一个零件上去,它立刻就被加工成下一个形态。没有等待,没有缓存。嗯,这就是组合逻辑的精髓。

2.1 assign语句——最直接的连线方式

先看最简单的。在Verilog里,assign语句就是用来描述组合逻辑的。它的语法很直白:

assign 输出信号 = 表达式;

举个例子,假设我们要计算两个8位价格数据的和:

module price_adder (
    input  [7:0] price_a,
    input  [7:0] price_b,
    output [7:0] sum
);
    assign sum = price_a + price_b;
endmodule

你看,就是这么简单。只要price_aprice_b变了,sum立刻跟着变。我在项目中遇到过一个问题:有人用assign给一个reg类型赋值,结果编译报错。记住,assign的左边必须是wire类型,不能是reg

⚠️ 避坑指南
我曾经在写一个高频交易系统的价格计算模块时,把assign写成了always块,结果综合出来的面积大了三倍。后来才意识到,能用assign解决的问题,别用always。

2.2 always@(*)——更灵活的组合逻辑描述

有时候,组合逻辑比较复杂,用一条assign搞不定。这时候就要请出always@(*)了。这个星号表示「所有输入信号」,只要任何一个输入变了,块内的代码就会重新执行。

module mux_example (
    input  [7:0] data0,
    input  [7:0] data1,
    input        sel,
    output reg [7:0] out
);
    always @(*) begin
        if (sel)
            out = data1;
        else
            out = data0;
    end
endmodule

注意看,这里out声明成了reg类型。很多人刚学的时候会困惑:组合逻辑里怎么用reg?其实在Verilog里,reg只是表示一个变量,不一定对应寄存器。在always@(*)块里,它就是个组合逻辑的输出。

💡 小技巧
我建议初学者养成一个习惯:只要用always块描述组合逻辑,就在块内给所有分支都赋值。否则会生成锁存器(latch),那玩意儿在量化交易系统里是灾难——它会记住不该记住的值。

2.3 加法器与乘法器——量化交易的核心运算

量化交易里,加法器和乘法器无处不在。计算移动平均线需要加法,计算价格比率需要乘法。我们来看看怎么在FPGA里高效实现它们。

2.3.1 加法器实现

最简单的加法器就是直接用+运算符。但综合工具会帮你生成什么?它可能生成一个行波进位加法器,也可能生成一个超前进位加法器。这取决于你的时序要求。

module adder_8bit (
    input  [7:0] a,
    input  [7:0] b,
    input        cin,
    output [7:0] sum,
    output       cout
);
    assign {cout, sum} = a + b + cin;
endmodule

这里用了一个拼接操作{cout, sum},把进位和结果一起输出。我在做期权定价模型时,经常需要做大量加法。那时候我发现,用流水线结构把加法拆成多级,可以大幅提升时钟频率。

2.3.2 乘法器实现

乘法比加法复杂得多。直接写assign result = a * b当然可以,但综合出来的面积可能让你哭。

module multiplier_4bit (
    input  [3:0] a,
    input  [3:0] b,
    output [7:0] product
);
    assign product = a * b;
endmodule

你想想看,一个4位乘法器,综合出来大概需要几十个LUT。如果是16位乘法,面积会爆炸式增长。在量化交易系统里,我一般会用DSP48硬核来做乘法,又快又省资源。

🔑 关键点
在FPGA里,乘法器有两种选择:
1. 用LUT拼出来的乘法器——灵活但慢
2. 用DSP硬核——快但数量有限
我建议:如果乘法数量不多,直接用DSP;如果数量多,考虑用查找表或者CORDIC算法。

2.4 数据选择器MUX设计

MUX,说白了就是多路开关。在量化交易系统里,MUX用来选择不同的数据源。比如,根据市场状态选择不同的交易策略。

2.4.1 基础MUX实现

module mux_4to1 (
    input  [7:0] in0, in1, in2, in3,
    input  [1:0] sel,
    output reg [7:0] out
);
    always @(*) begin
        case (sel)
            2'b00: out = in0;
            2'b01: out = in1;
            2'b10: out = in2;
            2'b11: out = in3;
            default: out = 8'b0;
        endcase
    end
endmodule

这里用了case语句。注意那个default分支,我建议永远加上。为什么?因为如果sel出现未知状态(比如上电瞬间),没有default就会生成锁存器。

2.4.2 用三元运算符实现MUX

有时候,简单的2选1MUX用三元运算符更简洁:

assign out = sel ? data1 : data0;

这一行代码等价于前面那个always@(*)块。我个人习惯在简单场景用三元运算符,复杂场景用case。代码可读性很重要,毕竟你写的代码可能要被团队其他成员维护。

知识体系总览

下面这张图展示了本章的知识结构,我把它画成了SVG,方便你理解各个知识点之间的关系:

组合逻辑与数据通路知识体系 assign语句 always@(*) 运算单元 连续赋值 · wire类型 表达式直接映射到硬件 敏感列表 · reg类型 if/case · 避免latch 加法器 · 乘法器 DSP硬核 · 流水线 数据选择器 MUX case语句实现 三元运算符 避免锁存器

实战要点总结

好了,这一讲的内容就到这里。我帮你梳理一下核心要点:

  • assign 适合简单组合逻辑,左边必须是wire
  • always@(*) 适合复杂组合逻辑,注意避免生成锁存器
  • 加法器 直接用+运算符,注意进位处理
  • 乘法器 优先考虑DSP硬核,面积和速度要权衡
  • MUX 用case或三元运算符,永远加default分支

在量化交易系统里,这些基础模块就像乐高积木。你搭好了加法器、乘法器、MUX,就能拼出价格计算、策略选择、风险控制等复杂功能。下一讲我们会把这些模块组合起来,构建真正的数据通路。

📌 课后练习
写一个8位数据选择器,支持4路输入。要求:
1. 用always@(*)实现
2. 用assign+三元运算符实现
3. 比较两种写法的综合结果(面积和延迟)
我在项目中遇到过,同样的功能,不同写法综合出来的面积能差30%。试试看你的结果如何?

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