4. Verilog基础回顾:模块化设计、组合逻辑与时序逻辑、状态机编写规范
各位同学,咱们今天聊点实在的。做数控系统加减速控制,Verilog是绕不开的工具。但说实话,很多新手一上来就想着写多复杂的代码,结果综合出来一堆问题。我个人的经验是——先把基础打牢,后面才能飞得起来。
这一节,咱们就回顾三个核心点:模块化设计、组合逻辑与时序逻辑、状态机编写规范。这些都是我这些年做项目反复踩坑又爬出来的经验,希望能帮你少走弯路。
核心观点:好的Verilog设计,不是代码写得有多花哨,而是结构清晰、逻辑严谨、可读性强。数控系统对时序要求极高,任何一点马虎,都可能让电机抖得像筛子。
4.1 模块化设计——别把所有代码塞进一个文件
我记得刚入行那会儿,特别喜欢把整个设计写在一个模块里。结果呢?代码上千行,自己都看晕了。后来带我的老工程师说了一句话,我记到现在:「一个模块只做一件事,做好它。」
模块化设计说白了,就是把大系统拆成小功能块。每个模块有明确的输入输出,内部实现对外部透明。这样做的好处太多了:
- 可读性强——别人看你的代码,一眼就知道每个模块是干嘛的
- 可复用——加减速模块写好了,换个项目直接拿来用
- 调试方便——出问题了,定位到具体模块,不用大海捞针
举个简单的例子,一个加减速控制器,我会拆成这样:
// 顶层模块:加减速控制器
module accel_decel_controller (
input wire clk,
input wire rst_n,
input wire start,
input wire [15:0] target_speed,
output reg [15:0] current_speed,
output reg done
);
// 子模块实例化
wire [15:0] accel_rate;
wire [15:0] decel_rate;
// 速度计算模块
speed_calculator u_speed_calc (
.clk(clk),
.rst_n(rst_n),
.start(start),
.target_speed(target_speed),
.accel_rate(accel_rate),
.decel_rate(decel_rate),
.current_speed(current_speed),
.done(done)
);
// 加减速曲线生成模块
curve_generator u_curve_gen (
.clk(clk),
.rst_n(rst_n),
.target_speed(target_speed),
.current_speed(current_speed),
.accel_rate(accel_rate),
.decel_rate(decel_rate)
);
endmodule
我的小技巧:每个模块的端口尽量控制在10个以内。超过这个数,说明这个模块可能太复杂了,该拆了。我在做五轴联动数控系统时,加减速模块拆成了7个小模块,每个模块的代码不超过100行,调试起来特别爽。
4.2 组合逻辑与时序逻辑——搞清楚谁是谁
这个问题,我见过太多人搞混了。你想想看,组合逻辑就是「输入一变,输出马上变」,没有时钟参与。时序逻辑则是「等到时钟沿来了,才更新输出」。
为什么会搞混?因为Verilog里都用always块,但敏感列表不一样。
组合逻辑:always@(*)
组合逻辑用always@(*),或者用assign语句。这里要注意,组合逻辑里不能有寄存器赋值,否则综合出来就是锁存器。
// 组合逻辑示例:加减速状态判断
always @(*) begin
if (current_speed < target_speed)
state_next = ACCEL;
else if (current_speed > target_speed)
state_next = DECEL;
else
state_next = IDLE;
end
避坑指南:我曾经在一个项目里,组合逻辑的always块漏写了敏感列表里的一个信号,结果仿真和综合结果完全不一样。查了两天才发现,气得我差点砸电脑。所以,组合逻辑一律用always@(*),别自己手写敏感列表!
时序逻辑:always@(posedge clk)
时序逻辑的核心是寄存器。数控系统里,速度值、位置值、状态值,这些都需要寄存器来保持。
// 时序逻辑示例:速度寄存器更新
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
current_speed <= 16'd0;
else if (load_enable)
current_speed <= speed_next;
else
current_speed <= current_speed; // 保持
end
这里有个关键点:时序逻辑用非阻塞赋值 <=,组合逻辑用阻塞赋值 =。这个规矩别打破,否则仿真结果会把你搞疯。
| 类型 | 敏感列表 | 赋值方式 | 典型应用 |
|---|---|---|---|
| 组合逻辑 | always@(*) | = (阻塞赋值) | 状态判断、数据选择、译码 |
| 时序逻辑 | always@(posedge clk) | <= (非阻塞赋值) | 寄存器、计数器、状态机状态 |
4.3 状态机编写规范——三段式是王道
状态机在数控系统里太常见了。加减速过程本身就是个状态机:空闲→加速→匀速→减速→停止。我见过有人用一段式、两段式,但我个人强烈推荐三段式状态机。
三段式就是:
- 第一段:时序逻辑,描述状态跳转
- 第二段:组合逻辑,描述下一状态
- 第三段:组合逻辑或时序逻辑,描述输出
这样做的好处是——结构清晰,可读性强,而且不容易产生竞争冒险。
// 三段式状态机示例:加减速控制
// 第一段:状态寄存器
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
state <= IDLE;
else
state <= state_next;
end
// 第二段:下一状态组合逻辑
always @(*) begin
case (state)
IDLE: if (start) state_next = ACCEL;
else state_next = IDLE;
ACCEL: if (speed_ok) state_next = RUN;
else state_next = ACCEL;
RUN: if (stop) state_next = DECEL;
else state_next = RUN;
DECEL: if (speed_zero) state_next = IDLE;
else state_next = DECEL;
default: state_next = IDLE;
endcase
end
// 第三段:输出逻辑
always @(*) begin
case (state)
IDLE: begin
accel_en = 1'b0;
decel_en = 1'b0;
done = 1'b0;
end
ACCEL: begin
accel_en = 1'b1;
decel_en = 1'b0;
done = 1'b0;
end
RUN: begin
accel_en = 1'b0;
decel_en = 1'b0;
done = 1'b0;
end
DECEL: begin
accel_en = 1'b0;
decel_en = 1'b1;
done = 1'b0;
end
default: begin
accel_en = 1'b0;
decel_en = 1'b0;
done = 1'b0;
end
endcase
end
重点提醒:状态编码尽量用独热码(One-Hot)或者格雷码(Gray Code)。独热码适合状态数少于8个的情况,速度快;格雷码适合状态数多的情况,功耗低。我在数控系统里一般用独热码,因为加减速状态通常就4-5个,独热码的译码逻辑最简单。
我的经验:写状态机时,一定要把default写全。别觉得所有情况都覆盖了就不写default。我曾经在一个项目里漏了default,结果综合出来一个奇怪的锁存器,仿真怎么都跑不对。从那以后,我每个case都老老实实写default,哪怕只是回到IDLE状态。
嗯,这一节的内容就到这里。模块化设计让你代码更清爽,组合逻辑和时序逻辑让你搞清楚信号什么时候变,三段式状态机让你写出来的代码别人看得懂、自己也放心。这些都是基本功,但基本功越扎实,后面做加减速控制的时候就越顺手。