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基础逻辑分类,你可以把它当作本章的“地图”:
2.6 实战中的避坑指南
最后分享几个我这些年总结的实战经验:
- 敏感列表要写全:组合逻辑的
always @(*)里,如果你手动写敏感列表漏了一个信号,综合出来的电路可能跟你想象的不一样。直接用@(*)最省心。 - 别在多个always块里对同一个变量赋值:这是多驱动问题,综合会报错。一个变量只在一个
always块里赋值。 - 状态机一定要有default:否则综合工具可能会给你生成一堆不必要的Latch。我见过有人因为这个导致芯片功耗翻倍。
- 仿真和综合结果可能不一样:仿真通过不代表综合没问题。尤其是阻塞和非阻塞混用时,仿真可能“看起来对”,但综合出来的电路完全不是那么回事。
我曾经犯过的错:有一次做步进电机加速控制,状态机里漏写了 default 分支。仿真时一切正常,但上板实测电机偶尔会“抽风”一下。查了两天才发现是状态机跑飞了。从那以后,我写状态机必加 default,而且每个输出都赋默认值。
好了,这一章的内容就到这里。组合逻辑、时序逻辑、状态机、阻塞与非阻塞赋值,这四个概念你吃透了,后面写运动控制代码就会顺手很多。
公众号:蓝海资料掘金营,微信deep3321