第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
注意几个要点:
- 端口方向:
input、output、inout。光纤接口里常用inout做SDA、SCL这类双向信号,但新手慎用,容易出问题。 - 数据类型:
wire和reg。简单记——assign左边用wire,always里赋值用reg。我见过有人把wire和reg混用,仿真能过,综合就报错,折腾半天。 - 位宽声明:比如
[7:0] data表示8位数据总线。在光纤通信中,数据总线宽度通常和SerDes的位宽对齐,比如10位、20位、40位。
4.2 assign与always:两种描述方式
Verilog里描述逻辑有两种主要方式:assign和always。说白了,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
4.5 本章知识体系
下面这张图总结了本章的核心内容,你可以把它当作一个快速参考:
好了,这一章的内容就这些。记住三个核心点:模块端口要规范、组合逻辑要完整、Latch一定要避免。你写代码的时候,多想想「这个综合出来是什么电路」,而不是「这个语法对不对」。养成这个习惯,你的Verilog水平会进步很快。
公众号:蓝海资料掘金营,微信deep3321