第四章 Verilog/VHDL基础与实战:核心语法、组合逻辑、时序逻辑、状态机
各位同学,欢迎来到第四章。这一章可以说是整个FPGA设计的“内功心法”。
我见过太多人,上来就写代码,结果综合出来一堆莫名其妙的latch,或者时序跑不过。说白了,就是基础没打牢。今天咱们就把Verilog/VHDL的核心语法、组合逻辑、时序逻辑、状态机这四块硬骨头啃下来。
本章核心脉络:从语法规则出发,理解硬件描述语言和软件语言的根本区别,然后掌握两种基本逻辑单元,最后用状态机把复杂控制逻辑串起来。
4.1 核心语法:别把Verilog当C语言写
很多初学者最大的问题,就是拿Verilog当C语言写。我刚开始带项目时,有个同事用for循环写了个累加器,综合出来一堆莫名其妙的硬件。你想想看,C语言是顺序执行的,Verilog是并行执行的——这是根本区别。
模块与端口:每个设计都是一个模块,就像一块芯片。
// Verilog 示例
module counter (
input wire clk, // 时钟
input wire rst_n, // 复位,低有效
input wire en, // 使能
output reg [7:0] count // 8位计数器
);
// 功能代码
endmodule
个人习惯:我写端口时,input/output后面一定跟wire或reg。虽然有些编译器不强制,但这样写,代码的“数据流向”一目了然。VHDL里则是用in/out,配合signal定义。
数据类型:Verilog里最常用的就是wire和reg。wire是线网,reg是寄存器——但注意,reg不一定综合成寄存器!在组合逻辑的always块里,reg只是变量。
| 类型 | Verilog | VHDL | 说明 |
|---|---|---|---|
| 线网 | wire | signal | 组合逻辑连线 |
| 变量 | reg | variable | 过程赋值用 |
| 整数 | integer | integer | 仿真用,综合慎用 |
我曾经踩过的坑:用integer做循环变量,结果综合出来一堆LUT,面积爆炸。后来改用genvar + generate,才搞定。所以,能不用integer就别用,除非你很清楚自己在干什么。
4.2 组合逻辑:always @(*) 和 assign
组合逻辑,说白了就是输入一变,输出立刻变,没有时钟参与。实现方式有两种:assign连续赋值,或者always @(*)过程赋值。
assign 用法:适合简单的逻辑。
assign sum = a + b;
assign sel = (addr > 8'hFF) ? 1'b1 : 1'b0;
always @(*) 用法:适合复杂的逻辑。
always @(*) begin
if (sel)
out = in_a;
else
out = in_b;
end
关键点:组合逻辑的always块里,必须用阻塞赋值“=”。而且敏感列表里要写全所有输入信号,或者直接用@(*)。我见过有人漏写敏感信号,仿真和综合结果不一致,查了一整天——嗯,这种教训一次就够了。
4.3 时序逻辑:always @(posedge clk) 的规矩
时序逻辑是FPGA的“心脏”。所有时序逻辑都依赖时钟沿来更新数据。说白了,就是D触发器。
基本模板:
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
q <= 1'b0;
else if (en)
q <= d;
end
我建议:所有时序逻辑都用非阻塞赋值“<=”。为什么?因为非阻塞赋值能模拟硬件中寄存器的并行更新行为。如果你在时序逻辑里用了阻塞赋值,综合结果可能和你想象的不一样——我当年就因为这个,把一个流水线设计搞成了串行,性能直接腰斩。
同步复位 vs 异步复位:
- 异步复位:复位信号不依赖时钟,立即生效。适合上电初始化。
- 同步复位:复位信号只在时钟沿采样。抗干扰能力强。
我个人习惯:用异步复位、同步释放。既保证复位及时,又避免亚稳态。代码大概长这样:
always @(posedge clk) begin
rst_sync <= {rst_sync[0], rst_n}; // 两级同步
end
always @(posedge clk or negedge rst_sync[1]) begin
if (!rst_sync[1])
q <= 1'b0;
else
q <= d;
end
4.4 状态机:把控制逻辑画成图
状态机是FPGA控制逻辑的“骨架”。我做过一个通信协议解析模块,里面就一个状态机,跑了十几个状态。你想想看,如果没有状态机,用一堆if-else嵌套,代码能写到崩溃。
两种经典模型:
| 类型 | 输出 | 特点 |
|---|---|---|
| Moore型 | 只与当前状态有关 | 稳定,无毛刺 |
| Mealy型 | 与当前状态和输入都有关 | 响应快,可能有毛刺 |
三段式状态机(我强烈推荐):
// 第一段:状态转移
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
state <= IDLE;
else
state <= next_state;
end
// 第二段:次态逻辑(组合逻辑)
always @(*) begin
case (state)
IDLE: if (start) next_state = RUN;
else next_state = IDLE;
RUN: if (done) next_state = DONE;
else next_state = RUN;
DONE: next_state = IDLE;
default: next_state = IDLE;
endcase
end
// 第三段:输出逻辑(组合逻辑或时序逻辑)
always @(*) begin
case (state)
IDLE: out = 2'b00;
RUN: out = 2'b01;
DONE: out = 2'b10;
default: out = 2'b00;
endcase
end
避坑指南:我曾经写过一个状态机,漏了default分支。结果综合出来一堆latch,功能完全不对。从那以后,我每个case都写default,哪怕只是回到IDLE。另外,状态编码用独热码(one-hot)还是格雷码?我的经验是:状态少于8个用独热码,面积小;多于8个用格雷码,减少毛刺。
4.5 实战要点:从代码到硬件的思维转变
学完这些,你可能会问:我写出来的代码,到底会综合成什么?
我的建议是:写代码前,先在脑子里画出硬件结构图。比如写一个计数器,你脑子里应该是一个D触发器加一个加法器。写一个状态机,脑子里应该是一堆触发器和组合逻辑。
如果你脑子里没有硬件图,那代码大概率会出问题。说白了,FPGA设计就是“画电路图”的另一种方式。
一个小技巧:写完代码后,打开综合工具的RTL视图看一眼。如果综合出来的电路和你想象的不一样,那就要反思了。我每次带新人,都让他们先看RTL视图,再改代码——效果比单纯讲语法好得多。
好了,这一章的内容就到这里。核心语法、组合逻辑、时序逻辑、状态机,这四个东西是FPGA设计的“四梁八柱”。你把这四个搞透了,后面学什么接口协议、高速设计,都会轻松很多。
公众号:蓝海资料掘金营,微信deep3321