模块化设计:从零开始搭建你的数字世界
各位同学,今天我们来聊聊FPGA/CPLD设计中一个非常重要的概念——模块化设计。说实话,我刚开始学Verilog的时候,总喜欢把所有代码写在一个文件里,觉得这样省事。结果有一次项目做到一半,要修改一个功能,找了半天才找到对应的代码段,改完又担心影响其他地方。那叫一个痛苦啊!
后来我明白了,模块化设计就像搭积木。你把一个复杂的功能拆成几个小模块,每个模块只管自己的事。这样不仅代码清晰,调试方便,还能复用。今天我们就来系统学习一下Verilog中的模块化设计。
一、模块的基本结构:module/endmodule
在Verilog里,每个设计单元都是一个模块。模块以module开头,以endmodule结束。你可以把它想象成一个黑盒子,有输入、有输出,内部实现具体的功能。
module counter (
input wire clk,
input wire rst_n,
output reg [3:0] count
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
count <= 4'd0;
else
count <= count + 1'b1;
end
endmodule
你看,这个计数器模块,输入是时钟和复位,输出是4位的计数值。模块内部就是一个简单的计数器逻辑。我个人习惯,模块名用有意义的英文单词,比如counter、uart_tx、spi_master,这样一看就知道是干什么的。
1st_counter,结果编译报错,找了半天才发现是名字的问题。
二、端口定义:input/output/inout
端口就是模块和外界通信的通道。Verilog支持三种端口类型:
| 端口类型 | 方向 | 常用场景 |
|---|---|---|
input |
输入 | 时钟、复位、数据输入 |
output |
输出 | 数据输出、状态指示 |
inout |
双向 | I2C数据线、三态总线 |
端口定义时,还要指定数据类型。最常用的是wire和reg。简单来说:
- wire:用于组合逻辑,比如assign语句驱动的信号
- reg:用于时序逻辑,比如always块中赋值的信号
嗯,这里要注意:output reg可以直接在端口定义时声明,也可以先声明output,再在内部定义reg。我个人喜欢第一种,代码更简洁。
三、模块实例化:把积木拼起来
模块定义好了,怎么用呢?这就需要实例化。说白了,就是把你写好的模块当成一个元件,在另一个模块里调用它。
实例化有两种方式:
1. 按端口顺序连接
counter u_counter (
clk, // 连接到clk
rst_n, // 连接到rst_n
count // 连接到count
);
这种方式简单,但容易出错。我曾经在项目中用过,后来发现端口顺序搞错了,调试了两天才找到问题。从那以后,我再也不用这种方式了。
2. 按端口名称连接(推荐)
counter u_counter (
.clk (sys_clk),
.rst_n (sys_rst_n),
.count (led_count)
);
这种方式明确指定了每个端口连接什么信号,代码可读性强,也不容易出错。我强烈建议你使用这种方式。
四、模块化设计的核心逻辑
说了这么多,我们来画一张图,看看模块化设计的核心思想是什么。
你看,顶层模块调用子模块,子模块又可以调用更底层的功能模块。这样一层层搭建起来,结构清晰,每个模块只负责自己的功能。你想想看,如果所有代码都写在一个模块里,那得多乱啊!
五、实战中的模块化设计原则
说了这么多理论,我们来聊聊实际项目中怎么用。我总结了几个原则:
- 单一职责:一个模块只做一件事。比如计数器就只计数,不要在里面加串口发送的逻辑。
- 接口简洁:模块的端口不要太多。我一般控制在10个以内,太多了说明这个模块功能太复杂,需要拆分。
- 层次清晰:顶层模块只做连接,不写具体逻辑。具体功能交给子模块实现。
- 参数化设计:用parameter定义常量,方便修改。比如数据位宽、计数器最大值等。
六、一个完整的例子
最后,我们来看一个完整的例子。假设我们要设计一个LED闪烁控制器,它由一个分频模块和一个计数模块组成。
// 分频模块
module clk_div (
input wire clk,
input wire rst_n,
output reg clk_out
);
parameter DIV_CNT = 24_000_000; // 50MHz分频到约2Hz
reg [24:0] cnt;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
cnt <= 0;
clk_out <= 0;
end else if (cnt == DIV_CNT - 1) begin
cnt <= 0;
clk_out <= ~clk_out;
end else begin
cnt <= cnt + 1'b1;
end
end
endmodule
// 计数模块
module led_ctrl (
input wire clk,
input wire rst_n,
output reg [3:0] led
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
led <= 4'b0001;
else
led <= {led[2:0], led[3]}; // 循环移位
end
endmodule
// 顶层模块
module top (
input wire clk,
input wire rst_n,
output wire [3:0] led
);
wire clk_2hz;
clk_div #(
.DIV_CNT(24_000_000)
) u_clk_div (
.clk (clk),
.rst_n (rst_n),
.clk_out(clk_2hz)
);
led_ctrl u_led_ctrl (
.clk (clk_2hz),
.rst_n (rst_n),
.led (led)
);
endmodule
你看,顶层模块只负责实例化两个子模块,并把它们连接起来。分频模块和计数模块各司其职,互不干扰。这就是模块化设计的魅力。
好了,今天的内容就到这里。模块化设计是FPGA/CPLD设计的基石,一定要掌握好。下次我们聊聊组合逻辑和时序逻辑的区别,这可是个容易踩坑的地方。