2. FPGA基础与Verilog语法速通:组合逻辑、时序逻辑、状态机、阻塞与非阻塞赋值

好,咱们正式开始啃FPGA运动控制的第一块硬骨头。

很多初学者一上来就盯着Verilog语法背,结果写出来的代码综合出来一堆莫名其妙的Latch。我当年也踩过这个坑。其实你只要搞明白三件事——组合逻辑、时序逻辑、状态机,再配上阻塞与非阻塞赋值的正确用法,FPGA的大门就算敲开了。

2.1 组合逻辑:纯硬件的“即时反应”

组合逻辑,说白了就是输入一变,输出立马跟着变。没有时钟,没有记忆,纯粹是门电路搭出来的函数。

举个例子,一个简单的与门:

assign y = a & b;

只要a或b变了,y在纳秒级就跟着变。这就是组合逻辑。

核心特征:

  • 输出只取决于当前输入
  • 没有存储单元(没有D触发器)
  • 没有时钟沿触发

在运动控制里,组合逻辑常用于译码器、多路选择器、比较器。比如编码器信号的边沿检测,就需要组合逻辑先把脉冲信号整形成干净的跳变沿。

我的习惯:写组合逻辑时,我尽量用 assign 或者 always @(*)。千万别在组合逻辑里用 always @(posedge clk),那是时序逻辑的活儿。

2.2 时序逻辑:让电路“记住”过去

时序逻辑就不一样了。它依赖时钟沿,能记住之前的状态。FPGA里最常用的时序单元就是D触发器。

你想想看,运动控制里的位置累加器、速度积分器、PWM计数器,哪个不需要记忆?没有时序逻辑,这些全玩不转。

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

这段代码里,cnt 只在时钟上升沿更新。复位时清零,使能时加1。这就是最典型的时序逻辑。

我曾经踩过的坑:刚学FPGA时,我在时序逻辑里用了阻塞赋值 =,结果仿真波形全乱套了。后来才明白,时序逻辑必须用 <=(非阻塞赋值),这是铁律。

2.3 阻塞赋值 vs 非阻塞赋值:一字之差,天壤之别

这块是新手最容易翻车的地方。我直接给你结论:

赋值方式 符号 适用场景 执行特点
阻塞赋值 = 组合逻辑(always @(*) 顺序执行,立即更新
非阻塞赋值 <= 时序逻辑(always @(posedge clk) 并行执行,块结束时统一更新

为什么会这样?因为硬件是并行的。你写 a <= b; c <= a; 在时序逻辑里,c 拿到的是 a 的旧值,不是 b 的新值。这符合D触发器的物理行为——每个时钟沿只采样一次输入。

记住这个口诀:

  • 写组合逻辑,用 =
  • 写时序逻辑,用 <=
  • 一个 always 块里,别混用

2.4 状态机:运动控制的“大脑”

状态机是FPGA里最强大的设计范式之一。运动控制里的加减速过程、换向逻辑、故障处理,全得靠状态机来调度。

我常用的三段式状态机结构:

// 第一段:状态转移(时序逻辑)
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 = STOP;
                else       next_state = RUN;
        STOP:   next_state = IDLE;
        default: next_state = IDLE;
    endcase
end

// 第三段:输出逻辑(组合逻辑或时序逻辑)
always @(*) begin
    case (state)
        IDLE:   motor_en = 1'b0;
        RUN:    motor_en = 1'b1;
        STOP:   motor_en = 1'b0;
    endcase
end

嗯,这里要注意:第二段用阻塞赋值,因为它是组合逻辑;第一段用非阻塞赋值,因为它是时序逻辑。这个习惯我从入行第一天就养成了,从来没出过问题。

我的建议:初学者先别碰一段式状态机。虽然代码短,但可读性差,调试起来想哭。老老实实写三段式,后期维护你会感谢我的。

2.5 知识体系总览

下面这张图是我自己整理的FPGA基础逻辑分类,你可以把它当作本章的“地图”:

FPGA基础逻辑知识体系 组合逻辑 时序逻辑 状态机 assign / always @(*) 阻塞赋值 = 译码器 / 多路选择器 always @(posedge clk) 非阻塞赋值 <= 计数器 / 移位寄存器 三段式结构 状态转移 + 次态 + 输出 运动控制调度核心 核心原则 组合逻辑用阻塞赋值,时序逻辑用非阻塞赋值 状态机用三段式,可读性强、综合稳定

2.6 实战中的避坑指南

最后分享几个我这些年总结的实战经验:

  • 敏感列表要写全:组合逻辑的 always @(*) 里,如果你手动写敏感列表漏了一个信号,综合出来的电路可能跟你想象的不一样。直接用 @(*) 最省心。
  • 别在多个always块里对同一个变量赋值:这是多驱动问题,综合会报错。一个变量只在一个 always 块里赋值。
  • 状态机一定要有default:否则综合工具可能会给你生成一堆不必要的Latch。我见过有人因为这个导致芯片功耗翻倍。
  • 仿真和综合结果可能不一样:仿真通过不代表综合没问题。尤其是阻塞和非阻塞混用时,仿真可能“看起来对”,但综合出来的电路完全不是那么回事。

我曾经犯过的错:有一次做步进电机加速控制,状态机里漏写了 default 分支。仿真时一切正常,但上板实测电机偶尔会“抽风”一下。查了两天才发现是状态机跑飞了。从那以后,我写状态机必加 default,而且每个输出都赋默认值。

好了,这一章的内容就到这里。组合逻辑、时序逻辑、状态机、阻塞与非阻塞赋值,这四个概念你吃透了,后面写运动控制代码就会顺手很多。


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