第4章:Verilog基础与Lattice风格

说实话,很多初学者一上来就啃Verilog语法书,结果越看越晕。我当年也是这样,看了半个月语法,写出来的代码还是被老工程师批得一无是处。后来才明白——Verilog不是C语言,它是用来描述硬件的。你写的是电路,不是程序。

这一章,咱们不讲那些花里胡哨的语法细节。我只挑光纤通信接口开发中最常用的部分,结合Lattice FPGA的特点,带你快速上手。嗯,咱们直接开干。

4.1 模块与端口:你的第一个硬件单元

Verilog里最基本的单元就是module。你可以把它想象成一个黑盒子,有输入、有输出,里面装着逻辑电路。我在项目中习惯把每个功能块都封装成一个模块,比如串行器、解串器、时钟管理单元,各管各的,调试起来特别方便。

看一个最简单的例子——一个D触发器,这在光纤通信的时钟域同步中经常用到:

module d_flip_flop (
    input  wire clk,    // 时钟
    input  wire rst_n,  // 复位,低有效
    input  wire d,      // 数据输入
    output reg q        // 数据输出
);

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            q <= 1'b0;
        else
            q <= d;
    end

endmodule

注意几个要点:

  • 端口方向inputoutputinout。光纤接口里常用inout做SDA、SCL这类双向信号,但新手慎用,容易出问题。
  • 数据类型wirereg。简单记——assign左边用wirealways里赋值用reg。我见过有人把wirereg混用,仿真能过,综合就报错,折腾半天。
  • 位宽声明:比如[7:0] data表示8位数据总线。在光纤通信中,数据总线宽度通常和SerDes的位宽对齐,比如10位、20位、40位。
我的小习惯:端口声明时,我总把时钟和复位放在最前面,然后是数据输入,最后是数据输出。这样别人看你的模块接口,一眼就能找到关键信号。

4.2 assign与always:两种描述方式

Verilog里描述逻辑有两种主要方式:assignalways。说白了,assign用来描述组合逻辑,always既可以描述组合逻辑,也可以描述时序逻辑。

4.2.1 assign——组合逻辑的简洁表达

assign就像一根导线,输入变了,输出立刻跟着变。在光纤通信中,我常用它来做数据位宽转换、信号取反、或者简单的多路选择。

// 数据位宽转换:8位转16位
assign data_out_16bit = {data_in_8bit, data_in_8bit};

// 信号取反
assign tx_enable_n = ~tx_enable;

// 多路选择
assign data_out = (sel) ? data_a : data_b;

这里有个坑——assign不能给reg类型赋值。如果你看到编译报错说"左侧不是网络类型",八成是这个问题。

4.2.2 always——时序逻辑的核心

always块是FPGA设计的灵魂。它有两种常用写法:

时序逻辑(带时钟)

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

组合逻辑(不带时钟)

always @(*) begin
    case (state)
        2'b00: next_state = 2'b01;
        2'b01: next_state = 2'b10;
        2'b10: next_state = 2'b00;
        default: next_state = 2'b00;
    endcase
end
注意:组合逻辑的always块里,必须把所有输入信号都写在敏感列表里,或者直接用@(*)。我曾经漏写了一个信号,仿真结果对,上板子就乱跳,查了两天才发现是敏感列表不全。

4.3 Lattice推荐编码风格

Lattice FPGA和Xilinx、Altera有些不同。它的LUT结构、布线资源都有自己的特点。我在做Lattice项目时,总结了几条实用的编码风格:

4.3.1 使用同步复位

Lattice官方文档明确推荐使用同步复位。为什么?因为Lattice的寄存器本身没有专用的异步复位引脚,用异步复位反而会消耗额外的逻辑资源。

// 推荐:同步复位
always @(posedge clk) begin
    if (!rst_n)
        data_out <= 8'd0;
    else
        data_out <= data_in;
end

// 不推荐:异步复位
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        data_out <= 8'd0;
    else
        data_out <= data_in;
end

4.3.2 避免使用三态总线

Lattice内部逻辑不建议用三态总线。如果你需要多路驱动,用多路选择器代替。我在一个SFP管理接口项目中,一开始用了三态总线,结果时序收敛不了,改成MUX后问题迎刃而解。

// 不推荐:三态总线
assign data_bus = (enable) ? data_out : 8'bz;

// 推荐:多路选择器
always @(*) begin
    case (sel)
        2'b00: data_bus = data_0;
        2'b01: data_bus = data_1;
        2'b10: data_bus = data_2;
        2'b11: data_bus = data_3;
    endcase
end

4.3.3 寄存器输出,减少组合逻辑级联

Lattice的LUT4结构决定了它适合做浅层逻辑。如果组合逻辑级数太多,建议在中间插入寄存器。说白了,就是「打一拍」。这在光纤通信的高速接口中尤其重要,因为时序裕量本来就紧张。

// 组合逻辑级联太长
assign result = (a & b) | (c & d) | (e & f) | (g & h);

// 改进:插入寄存器
reg temp1, temp2;
always @(posedge clk) begin
    temp1 <= (a & b) | (c & d);
    temp2 <= (e & f) | (g & h);
end
assign result = temp1 | temp2;

4.4 避免Latch——新手最容易踩的坑

Latch是电平敏感的存储单元,在FPGA里通常是不想要的。为什么?因为Latch会导致时序分析困难,而且综合工具很难优化。我见过有人因为Latch太多,导致芯片工作不稳定,最后不得不重新改版。

Latch是怎么产生的?说白了,就是组合逻辑的条件分支没有覆盖所有情况。看几个典型例子:

4.4.1 if语句缺少else

// 会产生Latch
always @(*) begin
    if (enable)
        data_out = data_in;
    // 缺少else分支
end

// 正确写法
always @(*) begin
    if (enable)
        data_out = data_in;
    else
        data_out = 8'd0;  // 或者 data_out = data_out;
end

4.4.2 case语句缺少default

// 会产生Latch
always @(*) begin
    case (sel)
        2'b00: data_out = data_a;
        2'b01: data_out = data_b;
        // 缺少default分支
    endcase
end

// 正确写法
always @(*) begin
    case (sel)
        2'b00: data_out = data_a;
        2'b01: data_out = data_b;
        default: data_out = 8'd0;
    endcase
end

4.4.3 组合逻辑中对自己赋值

// 会产生Latch
always @(*) begin
    if (hold)
        data_out = data_out;  // 保持原值
    else
        data_out = data_in;
end

// 正确写法:用寄存器实现保持功能
always @(posedge clk) begin
    if (hold)
        data_out <= data_out;
    else
        data_out <= data_in;
end
避坑指南:我曾经在一个SFP控制模块里,因为case语句漏了default分支,综合后多出十几个Latch。仿真时功能完全正常,但上板后控制逻辑时不时出错。后来用Lattice的Reveal逻辑分析仪抓波形,才发现是Latch导致的毛刺。从那以后,我写组合逻辑always块,第一件事就是检查有没有漏掉else或default。

4.5 本章知识体系

下面这张图总结了本章的核心内容,你可以把它当作一个快速参考:

第4章:Verilog基础与Lattice风格 Verilog基础与Lattice风格 模块与端口 module/endmodule input/output/inout assign与always assign(组合逻辑) always(时序/组合) Lattice编码风格 同步复位 避免三态总线 避免Latch if缺少else case缺少default 组合逻辑自赋值 核心:写的是电路,不是程序

好了,这一章的内容就这些。记住三个核心点:模块端口要规范、组合逻辑要完整、Latch一定要避免。你写代码的时候,多想想「这个综合出来是什么电路」,而不是「这个语法对不对」。养成这个习惯,你的Verilog水平会进步很快。


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