🎓 第五课:模块化设计与层次结构
上节回顾:我们学会了用状态机控制复杂时序。这节课学习模块化设计——像搭乐高积木一样组装电路!
🧱 5.1 为什么要模块化?
🎯 生活比喻:组装电脑 vs 焊接电路板
不模块化的设计(全焊死在一块板上):
❌ CPU坏了 → 整块板报废
❌ 想升级内存 → 无法实现
❌ 电路复杂 → 找不到故障点
模块化设计(用标准接口连接):
✅ 坏了哪个换哪个
✅ 可以随时升级
✅ 每个模块独立测试
📊 硬件模块化的优势
| 优势 | 说明 | 类比 |
|---|---|---|
| 可复用 | 一次设计,多处使用 | 像函数库 |
| 易调试 | 单独测试每个模块 | 像单元测试 |
| 协同开发 | 多人并行工作 | 像Git分支 |
| 层次清晰 | 顶层只看接口 | 像API文档 |
🔌 5.2 模块实例化基础
📚 基本语法结构
模块名 实例名(
.端口名(连接的信号),
.端口名(连接的信号)
);
比喻:像"插插座"
- 模块 = 电器(有固定插头规格)
- 实例化 = 把电器插到插座上
- 端口连接 = 电线连接
🎯 例题9:用半加器组装全加器
📐 设计思路
全加器功能:A + B + Cin = {Cout, Sum}
半加器1: A + B = {C1, S1}
半加器2: S1 + Cin = {C2, Sum}
或门: Cout = C1 | C2
电路示意图:
A ──┐
├→[半加器1]──S1──┐
B ──┘ ↓ ├→[半加器2]─→ Sum
C1 │ ↓
│ Cin─┘ C2
└────┬──────────┘
↓
[或门]─→ Cout
📝 完整代码实现
底层模块:半加器
module half_adder(
input wire a,
input wire b,
output wire sum,
output wire cout
);
assign sum = a ^ b; // 异或
assign cout = a & b; // 与
endmodule
顶层模块:全加器
module full_adder(
input wire a, // 🔢 被加数
input wire b, // 🔢 加数
input wire cin, // ⬆️ 低位进位输入
output wire sum, // 📝 和
output wire cout // ⬆️ 进位输出
);
// 🔌 内部连线(像电路板上的走线)
wire s1; // 半加器1的和
wire c1; // 半加器1的进位
wire c2; // 半加器2的进位
// 🧩 实例化第1个半加器
half_adder ha1(
.a(a), // 端口名 .a 连接到信号 a
.b(b), // 端口名 .b 连接到信号 b
.sum(s1), // 输出连到内部线 s1
.cout(c1) // 输出连到内部线 c1
);
// 🧩 实例化第2个半加器
half_adder ha2(
.a(s1), // 用第1个的输出作输入
.b(cin), // 加上低位进位
.sum(sum), // 最终的和
.cout(c2) // 第2个进位
);
// 🔌 或门:合并两个进位
assign cout = c1 | c2;
endmodule
🔍 关键语法解析
1️⃣ 端口连接的两种方式
按位置连接(不推荐):
half_adder ha1(a, b, s1, c1); // ❌ 容易出错
// ↑ ↑ ↑ ↑
// 必须严格对应声明顺序
按名称连接(推荐):
half_adder ha1(
.a(a), // ✅ 一目了然
.b(b), // 顺序可以打乱
.sum(s1),
.cout(c1)
);
2️⃣ 内部连线的作用
wire s1; // 声明内部信号
// ↑
// 像电路板上的铜箔走线
硬件对应:
模块A的输出引脚 ──[s1这根线]── 模块B的输入引脚
3️⃣ 实例命名规范
half_adder ha1(...);
// ↑
// 实例名(必须唯一)
命名建议:
- 用
u_前缀:u_adder,u_counter - 用功能缩写:
ha1,ha2(半加器) - 用层级标识:
u_top_ctrl
🧪 测试全加器
module tb_full_adder;
reg a, b, cin;
wire sum, cout;
full_adder uut(
.a(a),
.b(b),
.cin(cin),
.sum(sum),
.cout(cout)
);
integer i;
initial begin
$display("A B Cin | Sum Cout | 验算(十进制)");
$display("-----------------------------------");
// 🔄 遍历所有8种输入组合(2³=8)
for (i = 0; i < 8; i = i + 1) begin
{a, b, cin} = i; // 🎨 并行赋值技巧
#10;
$display("%b %b %b | %b %b | %d+%d+%d=%d%b",
a, b, cin, sum, cout,
a, b, cin, cout, sum);
end
$finish;
end
endmodule
📊 预期输出
A B Cin | Sum Cout | 验算(十进制)
-----------------------------------
0 0 0 | 0 0 | 0+0+0=00 ✓
0 0 1 | 1 0 | 0+0+1=01 ✓
0 1 0 | 1 0 | 0+1+0=01 ✓
0 1 1 | 0 1 | 0+1+1=10 ✓ (进位!)
1 0 0 | 1 0 | 1+0+0=01 ✓
1 0 1 | 0 1 | 1+0+1=10 ✓
1 1 0 | 0 1 | 1+1+0=10 ✓
1 1 1 | 1 1 | 1+1+1=11 ✓ (3的二进制)
🎯 例题10:4位行波进位加法器
📚 设计思路
目标:实现 A[3:0] + B[3:0] = Sum[3:0], Cout
方案:用4个全加器级联
A[0]+B[0] → FA0 → Sum[0]
↓进位
A[1]+B[1] → FA1 → Sum[1]
↓进位
A[2]+B[2] → FA2 → Sum[2]
↓进位
A[3]+B[3] → FA3 → Sum[3], Cout
📝 代码实现
module adder_4bit(
input wire [3:0] a, // 4位被加数
input wire [3:0] b, // 4位加数
input wire cin, // 最低位进位输入
output wire [3:0] sum, // 4位和
output wire cout // 最高位进位输出
);
// 🔌 内部进位线(连接相邻全加器)
wire c1, c2, c3;
// 🧩 实例化4个全加器
full_adder fa0(
.a(a[0]),
.b(b[0]),
.cin(cin), // 外部进位输入
.sum(sum[0]),
.cout(c1) // 传递给下一级
);
full_adder fa1(
.a(a[1]),
.b(b[1]),
.cin(c1), // 接收上一级进位
.sum(sum[1]),
.cout(c2)
);
full_adder fa2(
.a(a[2]),
.b(b[2]),
.cin(c2),
.sum(sum[2]),
.cout(c3)
);
full_adder fa3(
.a(a[3]),
.b(b[3]),
.cin(c3),
.sum(sum[3]),
.cout(cout) // 最终进位输出
);
endmodule
🔍 向量切片技巧
wire [3:0] a; // 声明4位向量
↑ ↑
最高 最低
a[0] // 访问第0位(最低位)
a[3] // 访问第3位(最高位)
a[2:1] // 访问第2,1位(切片)
硬件对应:
a[3] a[2] a[1] a[0]
↓ ↓ ↓ ↓
[像4根独立的电线捆在一起]
🧪 测试代码
module tb_adder_4bit;
reg [3:0] a, b;
reg cin;
wire [3:0] sum;
wire cout;
adder_4bit uut(
.a(a), .b(b), .cin(cin),
.sum(sum), .cout(cout)
);
initial begin
$display(" A + B + Cin = Cout Sum | 十进制验算");
$display("------------------------------------------");
// 🎲 测试案例
a=4'd5; b=4'd3; cin=0; #10;
$display("%d + %d + %d = %d %d | %d (✓)",
a, b, cin, cout, sum, a+b+cin);
a=4'd15; b=4'd1; cin=0; #10;
$display("%d + %d + %d = %d %d | %d (溢出!)",
a, b, cin, cout, sum, a+b+cin);
a=4'd7; b=4'd8; cin=1; #10;
$display("%d + %d + %d = %d %d | %d (✓)",
a, b, cin, cout, sum, a+b+cin);
$finish;
end
endmodule
📐 5.3 参数化设计(Parameter)
🎯 问题场景
如果要设计8位、16位、32位加法器,难道要写3遍代码?
答案:用parameter参数化!
📝 参数化模块示例
module adder_nbit #(
parameter WIDTH = 4 // 📊 可配置参数(默认4位)
)(
input wire [WIDTH-1:0] a, // 位宽自适应
input wire [WIDTH-1:0] b,
input wire cin,
output wire [WIDTH-1:0] sum,
output wire cout
);
// 🧮 直接用算术运算(综合器会优化)
assign {cout, sum} = a + b + cin;
// ↑拼接运算符,将进位和结果合并
endmodule
🔧 实例化时覆盖参数
// 🔹 方法1:按位置传参
adder_nbit #(8) u_adder8(...); // 8位加法器
// 🔹 方法2:按名称传参(推荐)
adder_nbit #(
.WIDTH(16)
) u_adder16(...); // 16位加法器
🧪 测试不同位宽
module tb_adder_nbit;
// 🔹 测试8位加法器
reg [7:0] a8, b8;
wire [7:0] sum8;
wire cout8;
adder_nbit #(.WIDTH(8)) u8(
.a(a8), .b(b8), .cin(1'b0),
.sum(sum8), .cout(cout8)
);
// 🔹 测试16位加法器
reg [15:0] a16, b16;
wire [15:0] sum16;
wire cout16;
adder_nbit #(.WIDTH(16)) u16(
.a(a16), .b(b16), .cin(1'b0),
.sum(sum16), .cout(cout16)
);
initial begin
// 测试8位
a8 = 8'd200; b8 = 8'd100; #10;
$display("8位: %d + %d = %d", a8, b8, sum8);
// 测试16位
a16 = 16'd50000; b16 = 16'd20000; #10;
$display("16位: %d + %d = %d", a16, b16, sum16);
$finish;
end
endmodule
🏗️ 5.4 层次化设计实战:数字时钟
📐 系统架构
顶层模块(top)
├── 分频器(divider) → 1Hz时钟
├── 秒计数器(counter_sec) → 0~59
├── 分计数器(counter_min) → 0~59
├── 时计数器(counter_hour) → 0~23
└── 数码管驱动(seg7_driver)
📝 底层模块1:计数器
module counter #(
parameter MAX_VAL = 59 // 最大计数值
)(
input wire clk,
input wire rst_n,
input wire en, // 使能信号
output reg [$clog2(MAX_VAL):0] count, // 自动计算位宽
output wire carry // 进位输出
);
assign carry = (count == MAX_VAL) && en; // 计满产生进位
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
count <= 0;
end
else if (en) begin
if (count >= MAX_VAL) begin
count <= 0;
end
else begin
count <= count + 1;
end
end
end
endmodule
📝 底层模块2:分频器
module clock_divider #(
parameter DIV_FACTOR = 50_000_000 // 分频系数
)(
input wire clk_in, // 输入时钟(如50MHz)
input wire rst_n,
output reg clk_out // 输出时钟(如1Hz)
);
reg [$clog2(DIV_FACTOR)-1:0] cnt;
always @(posedge clk_in or negedge rst_n) begin
if (!rst_n) begin
cnt <= 0;
clk_out <= 0;
end
else begin
if (cnt >= DIV_FACTOR/2 - 1) begin
cnt <= 0;
clk_out <= ~clk_out; // 翻转输出
end
else begin
cnt <= cnt + 1;
end
end
end
endmodule
📝 顶层模块组装
module digital_clock(
input wire clk_50m, // 板载50MHz时钟
input wire rst_n,
output wire [5:0] sec, // 秒输出
output wire [5:0] min, // 分输出
output wire [4:0] hour // 时输出
);
// 🔌 内部信号
wire clk_1hz; // 1Hz时钟
wire carry_sec; // 秒进位
wire carry_min; // 分进位
// 🧩 分频器:50MHz → 1Hz
clock_divider #(
.DIV_FACTOR(50_000_000)
) u_div (
.clk_in(clk_50m),
.rst_n(rst_n),
.clk_out(clk_1hz)
);
// 🧩 秒计数器(0~59)
counter #(
.MAX_VAL(59)
) u_sec (
.clk(clk_1hz),
.rst_n(rst_n),
.en(1'b1), // 始终使能
.count(sec),
.carry(carry_sec)
);
// 🧩 分计数器(0~59)
counter #(
.MAX_VAL(59)
) u_min (
.clk(clk_1hz),
.rst_n(rst_n),
.en(carry_sec), // 秒进位时才计数
.count(min),
.carry(carry_min)
);
// 🧩 时计数器(0~23)
counter #(
.MAX_VAL(23)
) u_hour (
.clk(clk_1hz),
.rst_n(rst_n),
.en(carry_min), // 分进位时才计数
.count(hour),
.carry() // 悬空(不需要)
);
endmodule
🔍 关键设计要点
1️⃣ $clog2函数
$clog2(59) // 计算log₂(59)并向上取整 = 6
// ↑
// 自动计算需要的位宽
用途:避免手动计算位宽出错
2️⃣ 进位信号级联
秒满60 → carry_sec=1 → 触发分计数器
分满60 → carry_min=1 → 触发时计数器
硬件意义:像机械齿轮的咬合传动
3️⃣ 端口悬空
.carry() // 不连接任何信号(合法)
场景:当某个输出不需要时
⚠️ 模块化设计常见错误
❌ 错误1:忘记声明内部连线
module top;
half_adder ha1(.sum(s1), ...); // ❌ s1未声明
endmodule
正确做法:
wire s1; // ✅ 先声明
half_adder ha1(.sum(s1), ...);
❌ 错误2:端口位宽不匹配
wire [3:0] data;
module_8bit u(.in(data)); // ❌ 4位连到8位
后果:综合警告,高位补0或截断
❌ 错误3:循环实例化
module A;
B u_b(...); // A调用B
endmodule
module B;
A u_a(...); // ❌ B又调用A,形成死循环
endmodule
🎓 本课核心总结
📋 模块化设计原则
| 原则 | 说明 | 类比 |
|---|---|---|
| 单一职责 | 每个模块只做一件事 | Unix哲学 |
| 接口清晰 | 端口命名明确 | API设计 |
| 参数化 | 用parameter增强复用 | 泛型编程 |
| 层次分明 | 顶层只连接,底层做计算 | MVC架构 |
✅ 设计检查清单
□ 模块功能是否单一?
□ 端口命名是否清晰?
□ 是否使用参数化?
□ 内部信号是否都声明?
□ 是否有未连接的端口?
□ 层次是否合理(不超过5层)?
🚀 综合大作业:简易CPU
💡 项目目标
设计一个4位微处理器,包含:
- ALU模块:加减与或运算
- 寄存器堆:4个通用寄存器
- 指令译码器:解析4位指令
- 控制器:状态机控制执行流程
📐 模块划分建议
cpu_top
├── u_alu (算术逻辑单元)
├── u_regfile (寄存器堆)
├── u_decoder (指令译码)
└── u_controller (主控制器)
📌 下节预告
第六课:仿真进阶与调试技巧
学习内容:
- 高级testbench编写
$monitor、$strobe等系统任务- 波形调试技巧
- 覆盖率分析
- 实战:UART协议仿真验证
💬 本课学习检查:
✅ 理解模块实例化的语法
✅ 会用内部连线连接模块
✅ 掌握按名称端口连接
✅ 能用parameter参数化设计
✅ 理解层次化设计思想
✅ 会组装多模块系统
✅ 知道如何分解复杂功能

被折叠的 条评论
为什么被折叠?



