第四章 Verilog/VHDL基础与实战:核心语法、组合逻辑、时序逻辑、状态机

各位同学,欢迎来到第四章。这一章可以说是整个FPGA设计的“内功心法”。

我见过太多人,上来就写代码,结果综合出来一堆莫名其妙的latch,或者时序跑不过。说白了,就是基础没打牢。今天咱们就把Verilog/VHDL的核心语法、组合逻辑、时序逻辑、状态机这四块硬骨头啃下来。

本章核心脉络:从语法规则出发,理解硬件描述语言和软件语言的根本区别,然后掌握两种基本逻辑单元,最后用状态机把复杂控制逻辑串起来。

FPGA设计基础 核心语法 组合逻辑 时序逻辑 状态机 wire/reg · always/process assign · 阻塞/非阻塞 D触发器 · 同步/异步复位 Moore/Mealy · 三段式

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只是变量。

类型VerilogVHDL说明
线网wiresignal组合逻辑连线
变量regvariable过程赋值用
整数integerinteger仿真用,综合慎用

我曾经踩过的坑:用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