3. Verilog基础回顾:模块化设计、组合逻辑与时序逻辑、状态机编写技巧、Testbench编写
各位同学,欢迎来到第三章。说实话,每次开课讲到Verilog基础,总有人觉得“这我早会了”。但我在伺服控制项目里摸爬滚打这么多年,发现很多bug的根源,恰恰是基础不牢。今天咱们不搞枯燥的语法罗列,我挑几个在FPGA伺服控制中真正高频、真正容易踩坑的点,好好捋一遍。
3.1 模块化设计:别把鸡蛋放在一个篮子里
模块化设计,说白了就是“分而治之”。一个复杂的伺服控制算法,比如电流环的PID计算,你要是全写在一个always块里,那调试起来简直要命。我个人习惯,把功能拆成独立的module,每个module只干一件事。
为什么必须模块化?
- 可读性:别人看你的代码,一眼就知道哪个模块是干嘛的。
- 可复用性:写好的PID模块,下次换个项目直接拿来用。
- 可调试性:出问题了,定位到具体模块,不用从头翻到尾。
来看一个简单的模块化示例。假设我们要做一个加法器模块:
// 加法器模块
module adder #(
parameter WIDTH = 16
)(
input wire [WIDTH-1:0] a,
input wire [WIDTH-1:0] b,
output reg [WIDTH-1:0] sum
);
always @(*) begin
sum = a + b;
end
endmodule
然后在顶层例化它:
module top (
input wire [15:0] data_in1,
input wire [15:0] data_in2,
output wire [15:0] result
);
adder #(.WIDTH(16)) u_adder (
.a(data_in1),
.b(data_in2),
.sum(result)
);
endmodule
你看,这样多清爽。每个模块的接口清晰,内部实现细节被隐藏了。这就是模块化的精髓。
3.2 组合逻辑与时序逻辑:分清“现在”和“未来”
这是新手最容易混淆的地方。组合逻辑的输出只取决于当前输入,而时序逻辑的输出还取决于之前的状态。在伺服控制中,组合逻辑常用于计算误差、生成使能信号;时序逻辑则用于状态寄存、计数器累加。
组合逻辑:用 assign 或 always @(*)
// 组合逻辑:计算误差
assign error = target - current;
时序逻辑:用 always @(posedge clk)
// 时序逻辑:累加误差
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
error_sum <= 0;
else
error_sum <= error_sum + error;
end
为什么会这样?因为非阻塞赋值是“并行”的,它会在所有赋值语句计算完成后才更新寄存器。而阻塞赋值是“顺序”的,会立即更新。在时序逻辑里用阻塞赋值,很容易产生意想不到的竞争冒险。
3.3 状态机编写技巧:让控制逻辑井井有条
状态机是伺服控制的核心。比如电机启动、停止、急停、故障处理,这些都需要状态机来管理。我个人偏爱三段式状态机,因为它结构清晰,且组合逻辑和时序逻辑分离,不容易出错。
三段式状态机模板:
// 第一段:时序逻辑,状态跳转
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
current_state <= IDLE;
else
current_state <= next_state;
end
// 第二段:组合逻辑,下一状态判断
always @(*) begin
case (current_state)
IDLE: begin
if (start_signal)
next_state = RUN;
else
next_state = IDLE;
end
RUN: begin
if (stop_signal)
next_state = STOP;
else
next_state = RUN;
end
STOP: begin
next_state = IDLE;
end
default: next_state = IDLE;
endcase
end
// 第三段:时序逻辑,输出控制
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
motor_enable <= 0;
fault_clear <= 0;
end else begin
case (current_state)
IDLE: begin
motor_enable <= 0;
fault_clear <= 0;
end
RUN: begin
motor_enable <= 1;
fault_clear <= 0;
end
STOP: begin
motor_enable <= 0;
fault_clear <= 1;
end
endcase
end
end
你想想看,这样写的好处是什么?状态跳转逻辑和输出逻辑完全分开。改输出时不会影响状态跳转,改状态跳转时也不会误触输出。我在做多轴同步控制时,就是靠这种结构,轻松管理了六个电机的协同状态。
3.4 Testbench编写:别让仿真成为摆设
很多同学写完RTL代码就直接上板,结果一跑就炸。其实,花半小时写个Testbench,能省下半天调试时间。Testbench说白了就是给设计“喂”激励,然后看它输出对不对。
一个基本的Testbench结构:
`timescale 1ns / 1ps
module tb_adder;
// 信号定义
reg [15:0] a, b;
wire [15:0] sum;
// 例化待测模块
adder #(.WIDTH(16)) uut (
.a(a),
.b(b),
.sum(sum)
);
// 生成时钟(如果需要)
reg clk;
always #5 clk = ~clk; // 10ns周期
// 测试激励
initial begin
// 初始化
a = 0; b = 0;
#10;
// 测试用例1
a = 16'd10; b = 16'd20;
#10;
if (sum !== 16'd30) $display("Error: 10+20 != 30");
// 测试用例2
a = 16'd100; b = 16'd200;
#10;
if (sum !== 16'd300) $display("Error: 100+200 != 300");
// 测试完成
#10;
$finish;
end
// 波形输出
initial begin
$dumpfile("wave.vcd");
$dumpvars(0, tb_adder);
end
endmodule
另外,别忘了用`timescale定义时间单位。我见过有人忘了写,结果仿真时间全乱套了。嗯,这里要注意,时间精度要足够小,比如1ps,否则高精度仿真会出问题。
知识体系总览
为了让你更直观地理解本章的知识结构,我画了一张图:
这张图把本章的四个核心知识点串起来了。模块化是骨架,组合与时序逻辑是血肉,状态机是大脑,Testbench是体检仪。缺了哪个,你的设计都跑不稳。
好了,这一章的内容就到这里。记住,Verilog基础不是背语法,而是理解硬件思维。多写、多仿、多踩坑,自然就熟了。