在实际硬件开发、嵌入式系统设计或计算机体系结构学习过程中,很多人会遇到一个核心矛盾:理论教材讲得头头是道,但面对一个真实的数字电路或CPU设计项目时,却不知道如何从零开始,将晶体管、逻辑门、Verilog代码、仿真、综合直到最终在FPGA上运行这一整套链路打通。苏黎世联邦理工学院(ETH Zurich)的“数字设计与计算机体系结构”课程,以其深度和系统性闻名,被许多从业者称为“神课”。其2026年的重制版本,更是聚焦于从底层到高层的全链路实践。
本文旨在为硬件描述语言初学者、计算机体系结构爱好者、以及希望深入理解CPU/GPU内部工作机制的工程师,提供一个基于该课程核心思想的实践指南。我们将不局限于观看视频,而是通过一个具体的、可操作的路径:从理解数字逻辑基础开始,使用Verilog编写关键模块,进行功能仿真,最终在FPGA开发板上实现一个简化的CPU核心。这个过程将串联起关键词中的Digital Design、Verilog、CPU、缓存等核心概念,并解释GPU计算、缓存原理等相关扩展知识。你将能获得一套可复现的方法论,用于构建自己的数字系统原型。
1. 理解从晶体管到CPU的设计层次与工具链
在动手写第一行Verilog代码之前,必须建立清晰的层次化设计观念。现代计算机系统是一个复杂的层次结构,学习时需要自底向上,但设计时往往自顶向下。
1.1 设计抽象层次
数字系统的设计通常分为多个层次,每一层都对下一层进行抽象:
- 系统级:描述整个计算机系统的功能,如CPU、内存、外设如何交互。
- 算法/架构级:用高级语言(如C)描述组件的行为。对于CPU设计,这就是指令集架构(ISA)的定义。
- 寄存器传输级(RTL):这是用硬件描述语言(如Verilog、VHDL)工作的核心层级。它描述数据如何在寄存器之间流动,以及每个时钟周期发生的逻辑操作。你的Verilog代码主要描述这一层。
- 逻辑门级:由基本逻辑门(与、或、非等)和触发器构成的电路。综合工具会将RTL描述转换到这一层。
- 晶体管级:用MOSFET等晶体管实现逻辑门。这是物理实现的起点。
- 物理级:涉及芯片的布局、布线和制造。
我们的实践将聚焦在RTL级,用Verilog描述一个CPU的寄存器传输行为,然后依赖工具链(综合器)将其转换为更低层的网表。
1.2 核心工具链介绍
一个完整的数字设计流程需要一系列工具协同工作:
- 文本编辑器/IDE:如VS Code,用于编写Verilog代码。需要安装相关插件(如Verilog-HDL/SystemVerilog)实现语法高亮和代码跳转。
- 仿真器:如ModelSim、Icarus Verilog(iverilog)配合GTKWave。用于验证RTL代码的逻辑功能是否正确,无需实际硬件。这是排查逻辑错误的主要阶段。
- 综合工具:如Xilinx Vivado(用于Xilinx FPGA)、Intel Quartus(用于Intel FPGA)中的综合引擎,或开源工具Yosys。它将RTL代码转换为目标工艺(FPGA或ASIC)的基本逻辑单元网表。
- 实现工具:通常集成在FPGA厂商的IDE中(如Vivado、Quartus)。负责将综合后的网表进行布局布线,映射到具体的FPGA芯片资源上,并生成最终的比特流文件。
- 编程/调试工具:将比特流文件下载到FPGA开发板,并通过ILA(集成逻辑分析仪)等工具进行片上调试。
对于初学者,可以先用Icarus Verilog + GTKWave进行仿真验证,再使用Vivado或Quartus进行FPGA综合与实现。
2. 环境准备与第一个Verilog模块
我们选择Verilog作为实践语言,因为它广泛用于工业界和学术界。以下环境配置以Windows/Linux/macOS通用性较高的方式为例。
2.1 安装仿真工具链(Icarus Verilog + GTKWave)
- Windows:访问 Icarus Verilog官方发布页 下载安装包。GTKWave可从其 官网 下载。安装后确保将可执行文件路径(如
C:\iverilog\bin和C:\gtkwave\bin)添加到系统环境变量PATH中。 - Linux (Ubuntu/Debian):使用包管理器安装。
sudo apt-get update sudo apt-get install iverilog gtkwave - macOS:使用Homebrew安装。
brew install icarus-verilog gtkwave
安装完成后,在终端中运行iverilog -v和gtkwave --version验证安装。
2.2 编写与仿真一个简单的组合逻辑模块
我们从最基础的与门开始,建立“编写-编译-仿真-查看波形”的完整流程。
项目结构:
first_circuit/ ├── and_gate.v // Verilog源文件 ├── testbench.v // 测试平台文件 └── run_sim.sh // 仿真脚本(可选)1. 设计文件and_gate.v:
// 模块定义:模块名、端口列表 module and_gate ( input wire a, // 输入端口a input wire b, // 输入端口b output wire y // 输出端口y ); // 连续赋值语句,描述逻辑功能:y = a & b assign y = a & b; endmodule2. 测试平台文件testbench.v:测试平台(Testbench)是一个特殊的Verilog模块,用于实例化待测设计,并施加测试激励。
`timescale 1ns / 1ps // 定义时间单位/精度 module tb_and_gate; // 声明连接到待测模块的变量 reg a, b; wire y; // 实例化待测模块(Unit Under Test, UUT) and_gate uut ( .a(a), .b(b), .y(y) ); // 初始化过程块,生成测试激励 initial begin // 生成波形文件供GTKWave查看 $dumpfile("tb_and_gate.vcd"); $dumpvars(0, tb_and_gate); // 0表示转储所有层次的信号 // 测试用例 a = 0; b = 0; #10; // 等待10个时间单位 a = 0; b = 1; #10; a = 1; b = 0; #10; a = 1; b = 1; #10; // 结束仿真 $finish; end endmodule3. 运行仿真:在项目目录下打开终端,执行以下命令:
# 1. 编译设计文件和测试平台 iverilog -o sim_output and_gate.v testbench.v # 2. 运行仿真,这会生成波形文件 tb_and_gate.vcd vvp sim_output # 3. 使用GTKWave打开波形文件查看结果 gtkwave tb_and_gate.vcd在GTKWave中,将左侧的信号(a, b, y)拖到波形视图区,即可看到模拟的时序波形。你可以验证在a和b的不同输入组合下,输出y是否符合与门的真值表。
注意:仿真(Simulation)和综合(Synthesis)是两回事。仿真用于验证逻辑正确性,可以包含不可综合的语句(如
$dumpfile)。综合是将代码转换为实际电路,必须使用可综合的Verilog子集。
3. 构建一个简化的单周期CPU核心
现在,我们利用RTL设计方法,构建一个极度简化的CPU,它能够执行几条基本的指令。这将串联起指令集、控制器、数据通路等核心概念。
3.1 定义指令集架构(ISA)
我们设计一个名为“SimpleCPU”的简化RISC架构:
- 字长:32位。
- 寄存器文件:32个32位通用寄存器(x0-x31),其中x0恒为0。
- 指令格式:采用类似RISC-V的固定32位长度。我们只实现三种类型:
- R-type:寄存器-寄存器操作,如ADD(加法)。
- I-type:立即数操作,如ADDI(加立即数)、LW(加载字)。
- B-type:条件分支,如BEQ(相等时分支)。
- 内存:统一的指令/数据内存(哈佛结构更优,但为简化先使用统一内存)。
我们实现以下5条指令:
ADD rd, rs1, rs2:rd = rs1 + rs2ADDI rd, rs1, imm:rd = rs1 + imm(imm为12位有符号立即数)LW rd, offset(rs1):rd = Memory[rs1 + offset]SW rs2, offset(rs1):Memory[rs1 + offset] = rs2BEQ rs1, rs2, offset: 若rs1 == rs2,则PC = PC + offset
3.2 顶层模块与数据通路设计
CPU的核心是数据通路(Datapath)和控制单元(Control Unit)。数据通路是执行指令的硬件路径,控制单元根据指令产生控制信号,指挥数据通路上的多路选择器、寄存器写入等操作。
项目结构:
simple_cpu/ ├── rtl/ │ ├── simple_cpu_top.v │ ├── pc_reg.v │ ├── instruction_memory.v │ ├── register_file.v │ ├── alu.v │ ├── control_unit.v │ └── data_memory.v ├── sim/ │ └── tb_simple_cpu.v └── scripts/ └── run_simulation.tcl1. 程序计数器pc_reg.v:
module pc_reg ( input wire clk, input wire rst_n, input wire [31:0] next_pc, output reg [31:0] pc ); always @(posedge clk or negedge rst_n) begin if (!rst_n) begin pc <= 32'h0000_0000; // 复位时PC指向0地址 end else begin pc <= next_pc; // 每个时钟上升沿更新PC end end endmodule2. 算术逻辑单元alu.v(部分):
module alu ( input wire [31:0] operand_a, input wire [31:0] operand_b, input wire [ 2:0] alu_op, // 操作码,如000=ADD, 001=SUB output reg [31:0] alu_result, output wire zero // 结果是否为0,用于BEQ判断 ); assign zero = (alu_result == 32'b0); always @(*) begin case (alu_op) 3'b000: alu_result = operand_a + operand_b; // ADD 3'b001: alu_result = operand_a - operand_b; // SUB 3'b010: alu_result = operand_a & operand_b; // AND 3'b011: alu_result = operand_a | operand_b; // OR 3'b100: alu_result = (operand_a < operand_b) ? 32'b1 : 32'b0; // SLT default: alu_result = 32'b0; endcase end endmodule3. 控制单元control_unit.v(部分):控制单元是指令的“解码器”,根据指令的操作码(opcode)和功能码(funct3/funct7)产生所有控制信号。
module control_unit ( input wire [6:0] opcode, // 指令[6:0] output reg reg_write, // 是否写寄存器 output reg mem_to_reg, // 数据来源:内存还是ALU结果 output reg mem_write, // 是否写内存 output reg alu_src, // ALU操作数B来源:寄存器还是立即数 output reg [2:0] alu_op, // ALU操作类型 output reg branch // 是否为分支指令 ); always @(*) begin // 默认值 {reg_write, mem_to_reg, mem_write, alu_src, alu_op, branch} = 8'b0; case (opcode) 7'b0110011: begin // R-type (ADD, SUB...) reg_write = 1'b1; alu_src = 1'b0; // 操作数来自寄存器 alu_op = 3'b000; // 具体操作由funct3进一步决定,此处简化 end 7'b0010011: begin // I-type (ADDI) reg_write = 1'b1; alu_src = 1'b1; // 操作数B来自立即数 alu_op = 3'b000; end 7'b0000011: begin // I-type (LW) reg_write = 1'b1; mem_to_reg = 1'b1; // 结果来自内存 alu_src = 1'b1; alu_op = 3'b000; // 计算地址 end 7'b0100011: begin // S-type (SW) mem_write = 1'b1; alu_src = 1'b1; alu_op = 3'b000; // 计算地址 end 7'b1100011: begin // B-type (BEQ) branch = 1'b1; alu_op = 3'b001; // 使用SUB操作进行比较 end default: begin // 处理非法指令或NOP end endcase end endmodule4. 顶层模块simple_cpu_top.v:顶层模块将各个子模块像搭积木一样连接起来,形成完整的数据通路。这包括PC寄存器、指令内存、寄存器文件、立即数生成器、ALU、控制单元、数据内存以及多个多路选择器。
module simple_cpu_top ( input wire clk, input wire rst_n ); // 内部信号声明(省略部分) wire [31:0] pc, next_pc, instruction; wire [31:0] read_data1, read_data2, write_data; wire [31:0] imm_ext, alu_result, mem_read_data; wire pc_src, reg_write, mem_to_reg, mem_write, alu_src, branch, zero; wire [2:0] alu_ctrl; wire [31:0] alu_operand_b; wire [31:0] branch_target; // 模块实例化 pc_reg u_pc_reg (.clk(clk), .rst_n(rst_n), .next_pc(next_pc), .pc(pc)); instruction_memory u_imem (.addr(pc), .instruction(instruction)); register_file u_reg_file (.clk(clk), .we(reg_write), ...); control_unit u_ctrl (.opcode(instruction[6:0]), ...); alu u_alu (.operand_a(read_data1), .operand_b(alu_operand_b), .alu_op(alu_ctrl), .alu_result(alu_result), .zero(zero)); data_memory u_dmem (.clk(clk), .we(mem_write), .addr(alu_result), .write_data(read_data2), .read_data(mem_read_data)); // 多路选择器(用条件运算符实现) assign alu_operand_b = alu_src ? imm_ext : read_data2; assign write_data = mem_to_reg ? mem_read_data : alu_result; assign branch_target = pc + (imm_ext << 1); // 分支偏移计算(简化) assign pc_src = branch & zero; assign next_pc = pc_src ? branch_target : (pc + 4); // 下一条PC endmodule3.3 编写测试程序与仿真
我们需要编写一个测试平台,并将一段简单的机器码程序加载到指令内存中。这段程序可以是一段计算斐波那契数列或数组求和的代码。
测试平台tb_simple_cpu.v:
module tb_simple_cpu; reg clk; reg rst_n; simple_cpu_top uut (.clk(clk), .rst_n(rst_n)); // 生成时钟信号,周期10个时间单位 always #5 clk = ~clk; initial begin $dumpfile("simple_cpu.vcd"); $dumpvars(0, tb_simple_cpu); // 初始化 clk = 0; rst_n = 0; #20; // 保持复位一段时间 rst_n = 1; // 释放复位 // 运行足够多的时钟周期 #500; // 可以在这里添加断言,检查最终寄存器x3的值是否为预期结果 // if (uut.u_reg_file.regs[3] != 32'd预期值) $error("Test failed!"); $finish; end // 预加载指令内存(需要在instruction_memory模块中实现初始化) // 通常通过$readmemh系统任务从文件加载 initial begin // 假设有一个汇编程序编译成的机器码文件 `program.mem` // $readmemh("program.mem", uut.u_imem.mem); end endmodule汇编程序示例(伪代码,需手动或通过工具编译为机器码):
ADDI x1, x0, 5 # x1 = 5 ADDI x2, x0, 1 # x2 = 1 LOOP: ADD x3, x1, x2 # x3 = x1 + x2 ADDI x1, x1, -1 # x1 = x1 - 1 BNE x1, x0, LOOP # if x1 != 0, jump to LOOP你需要一个汇编器(或手动查表)将上述汇编转换为对应的32位二进制机器码,并保存到program.mem文件中(每行一个32位十六进制数)。然后在测试平台的initial块中使用$readmemh加载。
运行仿真,观察波形中PC的变化、指令的执行、寄存器的写入以及内存的读写,验证CPU是否按预期工作。
4. 引入缓存(Cache)概念与简单模型
现代CPU性能的关键之一在于缓存。我们可以为上面的SimpleCPU添加一个极其简化的缓存模型来理解其工作原理。
4.1 缓存基本原理
缓存是位于CPU核心和主存之间的小容量高速存储器,用于存放最近可能被访问的数据副本。其核心思想是时间局部性和空间局部性。
- 时间局部性:刚被访问的数据很可能再次被访问。
- 空间局部性:访问某个地址后,其邻近地址很可能被访问。
缓存的基本单位是缓存行,一次从主存加载一个缓存行(如64字节)的数据。我们实现一个简化的直接映射缓存。
4.2 实现一个简化的直接映射缓存模块
module simple_dcache #( parameter DATA_WIDTH = 32, parameter CACHE_SIZE = 256, // 缓存大小,单位字节 parameter LINE_SIZE = 16 // 行大小,单位字节 )( input wire clk, input wire rst_n, // CPU侧接口 input wire cpu_req, // 请求有效 input wire cpu_wr, // 1=写,0=读 input wire [31:0] cpu_addr, input wire [DATA_WIDTH-1:0] cpu_wdata, output reg [DATA_WIDTH-1:0] cpu_rdata, output reg cpu_ready, // 请求完成 // 内存侧接口(模拟慢速内存) output reg mem_req, output reg mem_wr, output reg [31:0] mem_addr, output reg [DATA_WIDTH-1:0] mem_wdata, input wire [DATA_WIDTH-1:0] mem_rdata, input wire mem_ready ); // 缓存参数计算 localparam LINE_WORDS = LINE_SIZE / (DATA_WIDTH/8); // 每行有多少个字 localparam NUM_LINES = CACHE_SIZE / LINE_SIZE; localparam INDEX_WIDTH = $clog2(NUM_LINES); localparam OFFSET_WIDTH = $clog2(LINE_SIZE); // 缓存存储体:标签(Tag)、数据(Data)、有效位(Valid) reg [31-INDEX_WIDTH-OFFSET_WIDTH:0] tag_array [0:NUM_LINES-1]; reg [DATA_WIDTH-1:0] data_array [0:NUM_LINES-1][0:LINE_WORDS-1]; reg valid_array [0:NUM_LINES-1]; // 地址分解 wire [INDEX_WIDTH-1:0] index; wire [OFFSET_WIDTH-1:0] offset; wire [31-INDEX_WIDTH-OFFSET_WIDTH:0] tag; assign {tag, index, offset} = cpu_addr; // 状态机 typedef enum logic [1:0] { IDLE, COMPARE, MEM_READ, MEM_WRITEBACK } state_t; state_t current_state, next_state; // 状态寄存器 always @(posedge clk or negedge rst_n) begin if (!rst_n) current_state <= IDLE; else current_state <= next_state; end // 下一状态逻辑和输出逻辑(简化版,仅示意读命中流程) always @(*) begin next_state = current_state; cpu_ready = 1'b0; mem_req = 1'b0; // ... 其他默认值 case (current_state) IDLE: begin if (cpu_req) begin if (valid_array[index] && tag_array[index] == tag) begin // 缓存命中 cpu_rdata = data_array[index][offset]; cpu_ready = 1'b1; next_state = IDLE; end else begin // 缓存未命中,进入内存访问状态 next_state = MEM_READ; end end end MEM_READ: begin mem_req = 1'b1; mem_wr = 1'b0; mem_addr = {cpu_addr[31:OFFSET_WIDTH], {OFFSET_WIDTH{1'b0}}}; // 对齐到行首 if (mem_ready) begin // 从内存读取一整行数据到缓存(此处简化,只读一个字) data_array[index][offset] = mem_rdata; tag_array[index] = tag; valid_array[index] = 1'b1; cpu_rdata = mem_rdata; cpu_ready = 1'b1; next_state = IDLE; end end // ... 其他状态(如写分配、写回等) endcase end endmodule这个模块可以集成到simple_cpu_top中,位于CPU核心和数据内存之间。当CPU发起访存请求时,先查询缓存。命中则快速返回数据;未命中则访问主存,并将数据载入缓存。
通过仿真,你可以对比添加缓存前后,执行一段循环访问数组的程序所需的时钟周期数,直观感受缓存对性能的影响。
5. 综合到FPGA与常见问题排查
将RTL代码转换为能在FPGA上运行的比特流,是学习的最后一步,也会遇到最多实际问题。
5.1 使用Vivado进行综合与实现
- 创建项目:打开Vivado,创建新项目,选择目标FPGA型号(例如Basys 3开发板对应的是
xc7a35tcpg236-1)。 - 添加源文件:将
rtl/目录下的所有.v文件添加到项目中。 - 添加约束文件:创建XDC约束文件,定义时钟引脚、复位引脚、可能的调试端口(如LED用于显示状态)等。
# 示例:Basys 3 100MHz时钟和复位按钮 set_property PACKAGE_PIN W5 [get_ports clk] set_property IOSTANDARD LVCMOS33 [get_ports clk] create_clock -add -name sys_clk_pin -period 10.00 -waveform {0 5} [get_ports clk] set_property PACKAGE_PIN U18 [get_ports rst_n] set_property IOSTANDARD LVCMOS33 [get_ports rst_n] - 综合:运行综合(Synthesis)。此步骤将RTL转换为逻辑网表。
- 实现:运行实现(Implementation),包括布局布线(Place & Route)。
- 生成比特流:生成
.bit文件。 - 下载:连接开发板,下载比特流。
5.2 常见问题与排查路径
在从仿真到上板的整个流程中,你会遇到各种问题。下表列出了常见问题及其排查思路:
| 问题阶段 | 现象/错误信息 | 可能原因 | 检查与解决思路 |
|---|---|---|---|
| 仿真 | 波形全为X(不定态) | 1. 寄存器未初始化。 2. 组合逻辑环路。 3. 多驱动源。 | 1. 检查所有reg型变量在复位时是否有确定值。2. 检查 always @(*)块中是否对同一变量既读又写(非阻塞赋值可能形成环路)。3. 检查是否有两个 assign或always块驱动同一根线网。 |
| 仿真 | 行为与预期不符 | 1. 控制信号生成错误。 2. 立即数符号扩展错误。 3. 内存地址对齐问题。 | 1. 仔细对照指令格式,检查控制单元解码逻辑。 2. 确认 I-type和B-type指令的立即数符号扩展是否正确。3. 确保 LW/SW地址是字对齐的(低2位为0)。 |
| 综合 | 综合警告:[Synth 8-*] | 1. 未连接的端口。 2. 锁存器推断。 3. 多时钟驱动。 | 1. 检查模块实例化时是否所有端口都已连接。 2. 在 always块中,如果条件分支不全,会综合出锁存器。确保所有条件下输出都有赋值,或赋默认值。3. 检查是否在同一模块中混用了不同时钟域的信号。 |
| 综合 | 时序违例 | 关键路径延迟过长。 | 1. 查看时序报告,找到关键路径。 2. 考虑插入流水线寄存器,打破长组合逻辑链。 3. 优化代码结构,减少路径上的逻辑级数。 |
| 实现 | 布局布线失败 | 1. 资源不足。 2. 约束过紧。 3. 设计中有不可布线的结构。 | 1. 查看资源利用率报告(LUT、FF、BRAM等)。简化设计或更换更大容量的FPGA。 2. 放松时钟约束或I/O延迟约束。 3. 检查是否使用了器件不支持的原语或特性。 |
| 上板 | 程序无反应/LED不亮 | 1. 时钟未连接或频率错误。 2. 复位信号极性错误或毛刺。 3. I/O约束错误。 | 1. 用示波器或ILA测量时钟引脚是否有信号。 2. 确认复位按钮是低有效还是高有效,在代码中是否匹配。可添加复位去抖电路。 3. 核对约束文件中引脚编号和电平标准是否与开发板原理图一致。 |
| 上板 | 行为随机/不稳定 | 1. 跨时钟域问题。 2. 亚稳态。 3. 电源噪声。 | 1. 对跨时钟域信号使用同步器(两级触发器)。 2. 检查是否在异步复位释放时违反了恢复/移除时间。 3. 确保电源稳定,必要时在代码中增加上电初始化延时。 |
5.3 使用ILA进行片上调试
当代码在FPGA上行为异常时,仿真正确的代码也可能出问题。Vivado的集成逻辑分析仪(ILA)是强大的调试工具。
- 标记调试网络:在RTL代码中,对需要观察的信号添加
(* mark_debug = “true” *)属性。(* mark_debug = "true" *) wire [31:0] debug_pc; assign debug_pc = u_pc_reg.pc; - 在Vivado中设置ILA IP:综合后,在“Netlist”窗口中可以看到标记的信号。将其拖入ILA IP核的调试端口。
- 重新综合与实现:生成新的比特流并下载。
- 触发与捕获:在Hardware Manager中设置触发条件(如当
debug_pc == 32’h0000_0010),然后运行捕获。你可以像看仿真波形一样,查看FPGA运行时的真实信号。
6. 扩展方向与最佳实践
完成基础CPU实现后,你可以沿着多个方向深化理解和实践:
6.1 架构扩展
- 流水线:将单周期CPU改为5级流水线(取指、译码、执行、访存、写回),这是提升CPU频率的关键。需要处理数据冒险(通过前推或停顿)和控制冒险(通过分支预测或延迟槽)。
- 缓存优化:实现组相联或全相联缓存,研究替换算法(LRU、随机)。添加写缓冲和写回策略。
- 总线与外设:通过AXI或Wishbone总线连接UART、GPIO、定时器等外设,实现一个简单的SoC。
- 协处理器:添加一个简单的硬件乘法/除法单元,或者实现一个浮点运算单元。
6.2 工具与流程优化
- 自动化脚本:使用Tcl或Python脚本自动化整个仿真、综合、实现流程。
- 形式验证:学习使用形式化工具(如JasperGold)验证某些关键属性,作为仿真的补充。
- 高级综合:尝试使用高层次综合(HLS)工具,如Xilinx Vitis HLS,用C/C++描述算法并生成RTL,与手写RTL进行对比。
6.3 代码与设计最佳实践
- 可综合性:始终区分可综合代码和仿真代码。
initial、$display、#delay等不可综合,仅用于测试。 - 同步设计:对几乎所有时序逻辑使用单一的全局时钟和同步复位。避免使用门控时钟和异步电路,除非有特殊需求且完全理解其风险。
- 清晰命名:使用有意义的信号和模块名。对于低有效信号,使用
_n后缀(如rst_n)。常量使用PARAMETER或localparam定义。 - 模块化与层次化:将功能独立的单元封装成模块,并通过清晰的接口连接。顶层模块只做例化,不包含复杂逻辑。
- 参数化设计:使用
parameter使模块可配置,如数据位宽、缓存大小、地址宽度等,提高代码复用性。 - 注释与文档:为每个模块编写头注释,说明功能、端口、参数。为复杂的状态机或算法绘制流程图或波形图。
- 版本控制:使用Git管理代码,特别是当项目变得复杂时。
从晶体管物理原理到现代CPU的复杂架构,这条学习路径漫长但回报丰厚。通过这个从Verilog模块、CPU核心、缓存模型到FPGA实现的完整实践,你获得的不再是孤立的知识点,而是一套理解、设计和调试数字系统的工程化思维框架。下一步,可以尝试将你的SimpleCPU移植到其他FPGA平台,或者参考RISC-V官方规范实现一个更完整的RV32IMC核心,那将是通向专业芯片设计领域的坚实一步。