news 2026/5/20 20:19:41

用Verilog手搓一个单周期CPU:从指令集到数据通路的保姆级实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用Verilog手搓一个单周期CPU:从指令集到数据通路的保姆级实现

用Verilog手搓一个单周期CPU:从指令集到数据通路的保姆级实现

在数字电路和计算机体系结构的学习中,没有什么比亲手实现一个CPU更能深入理解计算机的工作原理了。本文将带你从零开始,用Verilog HDL实现一个完整的单周期MIPS CPU。不同于教科书上的理论讲解,我们将聚焦于实际编码中的各种细节和陷阱,让你不仅能理解原理,更能亲手实现一个真正可运行的CPU模型。

1. 单周期CPU基础与设计准备

单周期CPU是指每条指令的执行都在一个时钟周期内完成。这种设计虽然简单直观,但包含了CPU的所有核心组件和工作原理,是学习计算机体系结构的绝佳起点。

1.1 MIPS指令集架构

我们的CPU将实现MIPS-32指令集的一个子集,包括以下基本指令类型:

  • R型指令:寄存器-寄存器操作,如add、sub、and、or等
  • I型指令:立即数操作,如addi、ori、lw、sw等
  • J型指令:跳转指令,如j、jal等

MIPS指令的三种基本格式如下:

类型31-26位25-21位20-16位15-11位10-6位5-0位
R型opcodersrtrdshamtfunct
I型opcodersrt立即数(16位)
J型opcode跳转地址(26位)

1.2 开发环境准备

在开始编码前,我们需要准备好开发环境:

# 推荐工具链 iverilog -o simv cpu_tb.v cpu.v # 编译仿真 vvp simv # 运行仿真 gtkwave dump.vcd # 查看波形

或者使用商业工具如:

  • Xilinx Vivado
  • Intel Quartus Prime
  • ModelSim

2. 核心模块设计与实现

2.1 程序计数器(PC)模块

PC模块负责保存当前指令地址,并在每个时钟周期更新。关键设计点包括:

  • 复位时PC初始化为0
  • 正常运行时PC更新为下一条指令地址
  • 支持跳转指令的地址更新
module PC( input clk, input reset, input [31:0] next_addr, output reg [31:0] current_addr ); always @(posedge clk or posedge reset) begin if (reset) current_addr <= 32'b0; else current_addr <= next_addr; end endmodule

注意:在FPGA实现中,建议对PC模块添加异步复位信号,确保系统可预测地启动。

2.2 指令存储器(IMEM)设计

指令存储器存储CPU要执行的程序代码。在仿真环境中,我们可以用Verilog数组实现:

module IMEM( input [31:0] addr, output [31:0] instr ); reg [31:0] mem [0:255]; // 256x32位存储器 // 初始化指令存储器 initial begin mem[0] = 32'h20010008; // addi $1, $0, 8 mem[1] = 32'h3402000C; // ori $2, $0, 12 mem[2] = 32'h00221820; // add $3, $1, $2 // ... 更多指令 end assign instr = mem[addr[9:2]]; // 按字寻址 endmodule

2.3 算术逻辑单元(ALU)实现

ALU是CPU的执行核心,需要支持多种运算操作:

ALU控制码运算类型
0000加法
0001减法
0010按位与
0011按位或
0100按位异或
0101左移
0110右移
module ALU( input [31:0] a, b, input [3:0] alu_ctrl, output reg [31:0] result, output zero ); always @(*) begin case(alu_ctrl) 4'b0000: result = a + b; 4'b0001: result = a - b; 4'b0010: result = a & b; 4'b0011: result = a | b; 4'b0100: result = a ^ b; 4'b0101: result = b << a[4:0]; 4'b0110: result = b >> a[4:0]; default: result = 32'b0; endcase end assign zero = (result == 32'b0); endmodule

3. 数据通路与控制信号

3.1 寄存器文件设计

寄存器文件包含32个32位通用寄存器,支持同时读写:

module RegFile( input clk, input [4:0] read_reg1, input [4:0] read_reg2, input [4:0] write_reg, input [31:0] write_data, input reg_write, output [31:0] read_data1, output [31:0] read_data2 ); reg [31:0] regs [0:31]; // 初始化寄存器0为0,其他随机 integer i; initial begin regs[0] = 32'b0; for(i=1; i<32; i=i+1) regs[i] = $random; end assign read_data1 = regs[read_reg1]; assign read_data2 = regs[read_reg2]; always @(posedge clk) begin if(reg_write && write_reg != 0) // $0始终为0 regs[write_reg] <= write_data; end endmodule

3.2 数据存储器(DMEM)实现

数据存储器与指令存储器分离,支持加载(load)和存储(store)操作:

module DMEM( input clk, input mem_write, input [31:0] addr, input [31:0] write_data, output [31:0] read_data ); reg [31:0] mem [0:255]; // 初始化数据存储器 initial begin for(integer i=0; i<256; i=i+1) mem[i] = i; end assign read_data = mem[addr[9:2]]; // 按字寻址 always @(posedge clk) begin if(mem_write) mem[addr[9:2]] <= write_data; end endmodule

3.3 控制单元设计

控制单元根据指令操作码生成各种控制信号:

module ControlUnit( input [5:0] opcode, output reg reg_dst, output reg branch, output reg mem_read, output reg mem_to_reg, output reg [3:0] alu_op, output reg mem_write, output reg alu_src, output reg reg_write, output reg jump ); always @(*) begin case(opcode) 6'b000000: begin // R-type reg_dst = 1; alu_src = 0; mem_to_reg = 0; reg_write = 1; mem_read = 0; mem_write = 0; branch = 0; alu_op = 4'b0000; jump = 0; end 6'b100011: begin // lw reg_dst = 0; alu_src = 1; mem_to_reg = 1; reg_write = 1; mem_read = 1; mem_write = 0; branch = 0; alu_op = 4'b0000; jump = 0; end // ... 其他指令控制信号 endcase end endmodule

4. 完整CPU集成与测试

4.1 顶层模块连接

将所有模块连接起来形成完整的CPU:

module SingleCycleCPU( input clk, input reset ); // 内部信号声明 wire [31:0] pc_next, pc_current, instr; wire [31:0] read_data1, read_data2, alu_result; wire [31:0] mem_read_data, write_back_data; wire [3:0] alu_ctrl; wire zero, reg_dst, branch, mem_read, mem_to_reg; wire mem_write, alu_src, reg_write, jump; // 模块实例化 PC pc(.clk(clk), .reset(reset), .next_addr(pc_next), .current_addr(pc_current)); IMEM imem(.addr(pc_current), .instr(instr)); RegFile regfile(.clk(clk), .read_reg1(instr[25:21]), .read_reg2(instr[20:16]), .write_reg(write_reg), .write_data(write_back_data), .reg_write(reg_write), .read_data1(read_data1), .read_data2(read_data2)); ALU alu(.a(alu_in1), .b(alu_in2), .alu_ctrl(alu_ctrl), .result(alu_result), .zero(zero)); DMEM dmem(.clk(clk), .mem_write(mem_write), .addr(alu_result), .write_data(read_data2), .read_data(mem_read_data)); ControlUnit ctrl(.opcode(instr[31:26]), .reg_dst(reg_dst), .branch(branch), .mem_read(mem_read), .mem_to_reg(mem_to_reg), .alu_op(alu_ctrl), .mem_write(mem_write), .alu_src(alu_src), .reg_write(reg_write), .jump(jump)); // 多路选择器和其它组合逻辑 // ... endmodule

4.2 测试程序编写

编写测试程序验证CPU功能:

module CPU_Testbench; reg clk = 0; reg reset = 1; SingleCycleCPU cpu(.clk(clk), .reset(reset)); // 时钟生成 always #5 clk = ~clk; initial begin #10 reset = 0; // 释放复位 #200 $finish; // 仿真结束 end initial begin $dumpfile("dump.vcd"); $dumpvars(0, CPU_Testbench); end endmodule

4.3 常见问题与调试技巧

在实现单周期CPU时,经常会遇到以下问题:

  1. 时序问题:确保所有组合逻辑在一个时钟周期内完成
  2. 阻塞与非阻塞赋值
    • 时序逻辑使用<=(非阻塞)
    • 组合逻辑使用=(阻塞)
  3. 信号未初始化:所有寄存器变量都应该有明确的复位状态
  4. 指令解码错误:仔细检查控制单元的真值表

调试建议:

  • 使用波形查看器逐步跟踪信号变化
  • 先验证单个指令,再测试指令序列
  • 编写自动化测试脚本验证关键功能
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/20 20:15:43

J-LINK V8 固件修复与升级实战指南

1. J-LINK V8固件故障的常见表现 J-LINK V8作为嵌入式开发中常用的调试工具&#xff0c;固件损坏的情况并不少见。很多开发者都遇到过这样的场景&#xff1a;昨天还能正常使用的调试器&#xff0c;今天突然就无法识别了。设备管理器里可能显示为"未知USB设备"&#x…

作者头像 李华
网站建设 2026/5/20 20:11:11

Molflow | 实战指南:从模型导入到结果可视化的真空仿真全流程

1. Molflow真空仿真入门指南 第一次接触Molflow时&#xff0c;我被它强大的蒙特卡洛模拟能力所震撼。作为欧洲核子研究中心开发的专用工具&#xff0c;它能精准模拟任意形状腔体在超高真空环境下的气体分子行为。不同于常见的CFD软件&#xff0c;Molflow特别适合处理分子流态下…

作者头像 李华
网站建设 2026/5/20 20:10:44

别再死记硬背了!用bgp.tools和Wireshark抓包,5分钟搞懂BGP报文和状态机

用实战拆解BGP协议&#xff1a;从抓包分析到状态机可视化 BGP协议常被称作互联网的"外交官"&#xff0c;但它的复杂性也让不少网络工程师望而生畏。传统学习方式往往陷入死记硬背状态机和报文类型的泥潭&#xff0c;而今天我们要用工程师的母语——真实数据包来说话…

作者头像 李华
网站建设 2026/5/20 20:09:23

Python爬虫实战(七):Selenium自动化采集苏宁易购商品数据

一、前言 在前六篇实战中&#xff0c;我们分别掌握了API接口型爬虫&#xff08;图书网站&#xff09;、静态网页解析型爬虫&#xff08;百度热搜&#xff09;、大规模分页爬取&#xff08;水果行情&#xff09;、高对抗性网站爬取&#xff08;豆瓣评论&#xff09;、二进制文件…

作者头像 李华
网站建设 2026/5/20 20:08:21

STM32标准库与HAL库深度对比:从原理到实战选型指南

1. 项目概述&#xff1a;从“库”的选择开始你的STM32之旅当你拿到一块STM32开发板&#xff0c;准备点亮第一个LED&#xff0c;或者驱动一个传感器时&#xff0c;第一个绕不开的问题就是&#xff1a;我该用哪种库来写代码&#xff1f;是传说中的“经典”标准库&#xff0c;还是…

作者头像 李华