多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

从lw/sw指令入手,掌握单周期MIPS CPU数据通路与控制单元设计

从lw/sw指令入手,掌握单周期MIPS CPU数据通路与控制单元设计 1. 从两条指令切入理解CPU设计的核心脉络如果你刚开始接触计算机体系结构或者正在做CPU设计的课程实验那么“lw”和“sw”这两条指令绝对是你绕不开的起点。它们看起来简单——一个从内存读数据到寄存器一个把寄存器的数据写回内存——但恰恰是这种简单构成了我们理解CPU如何工作的最佳切片。很多同学在搭建第一个单周期CPU时会觉得控制信号纷繁复杂数据通路像一团乱麻。其实抓住lwLoad Word和swStore Word这两条最基础的内存访问指令把它们的完整执行路径彻底搞懂整个CPU的骨架就清晰了。为什么是这两条因为在MIPS指令集里lw和sw完整地展现了CPU与内存交互的典型流程涉及了指令读取、译码、地址计算、内存访问、数据回写对lw而言等几乎全部关键阶段。理解了它们像add、sub这些纯寄存器运算的指令无非就是去掉内存访问环节而beq、j这些控制指令则是改变了数据流的方向。所以这次我们不谈空洞的理论直接动手从零开始一步步推导出支持lw和sw指令的单周期MIPS CPU数据通路与控制信号设计。我会结合自己当年做课程设计和后来教学中的经验把那些容易卡壳的细节和“为什么非得这么设计”的道理讲清楚。2. 指令解剖lw与sw的格式与语义解读在动手画图之前我们必须像医生解剖一样把lw和sw这两条指令的格式和它要求CPU完成的任务彻底弄清楚。这是所有设计的出发点一步错后面全乱。MIPS指令是32位定长的主要分为三种格式R型寄存器-寄存器操作、I型立即数操作、访存、分支和J型跳转。lw和sw都属于I型指令。它们的二进制编码格式是完全统一的opcode(6位) | rs(5位) | rt(5位) | immediate(16位)opcode (操作码) 这是指令的“身份证”告诉CPU这是什么指令。对于lw其操作码是100011二进制对应十六进制0x23对于sw操作码是101011二进制对应十六进制0x2B。控制单元主要就是靠识别这两位来生成后续的所有控制信号。rs (源寄存器) 这5位指定了一个通用寄存器的编号这个寄存器里存放的是一个基地址base address。在lw和sw中rs的作用是一样的提供一个内存访问的起始点。rt (目标寄存器) 这5位在lw和sw中的作用有微妙且重要的区别这是第一个容易混淆的点。对于lw (Load Word) rt是目标寄存器destination register。这条指令的意思是从内存某个地址读出一个字32位然后放到编号为rt的寄存器里。所以rt在lw里是“写入”的目标。对于sw (Store Word) rt是源寄存器source register。这条指令的意思是把编号为rt的寄存器里的一个字32位写入到内存的某个地址。所以rt在sw里是“读出”数据的源头。immediate (立即数) 这是一个16位的有符号整数采用二进制补码表示。它的作用是作为偏移量offset。最终要访问的内存地址是通过将寄存器rs的值与这个16位偏移量进行符号扩展至32位后再相加得到的。即内存地址 [rs] SignExtend(immediate)。这个“基址加偏移”的寻址模式非常灵活是访问数组、结构体成员和栈变量的基础。用更直白的语言描述一下这两条指令要CPU干什么lw $rt, offset($rs) “CPU老兄请你先去寄存器堆里把编号为rs的那个寄存器里的值拿出来当作基地址。然后把这个16位的偏移量符号扩展成32位加到基地址上算出最终的内存地址。接着你去内存Memory的这个地址把里面的32位数据给我读出来。最后把这个读出来的数据存回到寄存器堆里编号为rt的那个寄存器中去。”sw $rt, offset($rs) “CPU老兄请你先去寄存器堆里把编号为rs的那个寄存器里的值拿出来当作基地址。同时把编号为rt的那个寄存器里的值也拿出来这是要存的数据。然后把偏移量符号扩展后加到基地址上算出最终的内存地址。最后请你把rt寄存器里拿出来的那个数据写入到内存的这个地址里去。”理解了这个“任务清单”我们才能有的放矢地去设计数据通路看看需要哪些部件数据怎么流动每个部件需要什么控制信号来配合完成这个清单。3. 数据通路搭建为lw/sw铺就执行轨道数据通路是CPU的“高速公路网”指令执行过程中数据就在这些“路”上流动。我们的目标是设计一条能同时满足lw和sw以及其他指令需求的通路。我们从指令进入CPU开始一步步推导。3.1 公共前端取指与译码所有指令的执行都始于从内存中取出指令本身。我们需要一个部件来保存当前要执行指令的地址这就是程序计数器PC。在单周期CPU中每个时钟周期执行一条指令下一条指令的地址通常是当前PC4因为每条指令占4个字节。所以我们需要一个加法器专门用于PC PC 4。注意这个“PC4”的加法器与后续计算内存地址的加法器是两个不同的部件。一个专门用于指令地址递增另一个通常称为ALU用于计算数据地址。别混为一谈。取出的32位指令被送到两个地方控制单元和寄存器堆。控制单元主要看指令的[31:26]位也就是opcode来判定这是lw、sw还是其他指令并据此生成全局的控制信号如RegWrite, MemRead等。寄存器堆需要根据指令中的rs和rt字段去读取对应寄存器的值。这里就引出了设计上的一个关键点读寄存器堆的操作是“无条件”的。只要指令里有rs和rt字段像lw/sw都有寄存器堆就会尝试去读这两个编号对应的寄存器值。即使对于某些指令比如sw指令中的rt值最终是写入内存的源数据但寄存器堆依然要先把它读出来这个“读”操作也照常进行。控制信号控制的是写RegWrite而不是读。同时指令中的16位立即数字段需要被符号扩展成32位以便与32位的寄存器值进行运算。符号扩展单元很简单就是把最高位第15位复制16遍填充到高16位。至此我们有了指令地址PC、下条指令地址PC4、从寄存器堆读出的两个数据Read data1, Read data2、以及符号扩展后的立即数。lw和sw的“公共准备阶段”就完成了。3.2 分水岭地址计算与ALU的角色接下来lw和sw都需要计算内存地址地址 [rs] SignExtend(immediate)。这个加法由**算术逻辑单元ALU**来完成。我们需要把寄存器rs的值Read data1和符号扩展后的立即数送到ALU的两个输入端并设置ALU的控制信号为“加法”ALUOp00, ALUSrc1这里先留个悬念。这里就遇到了数据通路设计中的第一个多路选择器Mux需求ALU的第二个操作数来源可以是寄存器rt的值Read data2也可以是立即数。对于lw和sw的地址计算显然第二个操作数应该是立即数。因此我们需要一个Mux在ALU的B输入端前进行选择其选择信号我们通常称为ALUSrc。当ALUSrc0时选择寄存器数据Read data2当ALUSrc1时选择立即数。那么对于lw和swALUSrc应该设为1以选择立即数进行地址计算。ALU执行加法后输出的32位结果就是有效内存地址。这个地址将被送到**数据存储器Data Memory**的地址输入端。3.3 核心操作内存访问与数据流向这是lw和sw差异最大的地方也是控制信号发挥作用的核心舞台。对于lw指令计算出的内存地址已经就绪。我们需要从数据存储器的这个地址读取一个32位的字。因此必须激活控制信号MemRead设为1。从内存读出的数据需要写回到寄存器堆中的rt寄存器。因此必须激活控制信号RegWrite设为1并且要正确地将数据写入rt指向的位置。这里引出了两个关键问题写回数据从哪里来对于lw写回数据来自内存MemtoReg。对于add指令写回数据来自ALU。所以在写回寄存器堆的数据输入端前我们需要第二个多路选择器其选择信号称为MemtoReg。当MemtoReg0时选择ALU的结果当MemtoReg1时选择从内存读出的数据。对于lwMemtoReg1。写入寄存器堆的哪个位置指令格式中lw的“目标寄存器”字段是rt位[20:16]而R型指令如add的目标寄存器是rd位[15:11]。因此在决定写入哪个寄存器时我们需要第三个多路选择器其选择信号称为RegDst。当RegDst0时选择rt字段用于lw等I型指令当RegDst1时选择rd字段用于R型指令。对于lwRegDst0。对于sw指令计算出的内存地址同样就绪。我们要把寄存器rt里的数据写入到这个内存地址。因此需要激活控制信号MemWrite设为1并且必须关闭MemRead设为0。同时绝对不能激活RegWrite设为0因为sw不写寄存器。要写入内存的数据从哪里来来自寄存器堆读出的第二个数据Read data2也就是rt寄存器的值。这个数据需要连接到数据存储器的“写数据”输入端。这里有一个非常重要的实操心得在单周期设计中MemRead和MemWrite信号绝对不能同时为1。这不仅逻辑错误不能同时对同一地址又读又写在实际电路中也可能导致总线冲突或存储单元状态不稳定。控制单元必须保证对于任何一条指令这两个信号是互斥的。lw时MemRead1, MemWrite0sw时MemRead0, MemWrite1对于不访问内存的指令如add两者都为0。3.4 整合与通路可视化把上面的所有部件和选择器连接起来就形成了支持lw和sw的单周期CPU核心数据通路。我们可以梳理一下关键数据流lw指令数据流 PC - 指令存储器 - 控制单元 寄存器堆 寄存器堆[rs] - ALU输入端A 符号扩展(立即数) - Mux(ALUSrc1) - ALU输入端B ALU(加法) - 数据存储器地址端 控制信号(MemRead1) - 从数据存储器读出数据 读出数据 - Mux(MemtoReg1) - 寄存器堆写数据端 控制信号(RegWrite1, RegDst0) - 将数据写入寄存器堆的rt位置sw指令数据流 PC - 指令存储器 - 控制单元 寄存器堆 寄存器堆[rs] - ALU输入端A 符号扩展(立即数) - Mux(ALUSrc1) - ALU输入端B ALU(加法) - 数据存储器地址端 寄存器堆[rt] - 数据存储器写数据端 控制信号(MemWrite1) - 将数据写入数据存储器的对应地址4. 控制单元设计生成正确的信号组合数据通路是硬件控制信号就是指挥硬件的“软件”逻辑。控制单元是一个组合逻辑电路其输入是指令的opcode字段6位输出就是一系列控制数据通路上各个多路选择器和功能部件使能端的信号。根据前面对lw和sw执行过程的分析我们可以列出它们所需的控制信号值控制信号lw (opcode100011)sw (opcode101011)信号含义RegDst0X (无关)写寄存器选择0rt, 1rdALUSrc11ALU的B操作数选择0Read data2, 1立即数MemtoReg1X (无关)写回寄存器数据选择0ALU结果, 1内存数据RegWrite10寄存器堆写使能1允许写入MemRead10数据存储器读使能MemWrite01数据存储器写使能Branch00分支指令使能本例中未涉及ALUOp00 (待细化)00 (待细化)指示ALU执行的操作类型上表中X表示“无关项”Don‘t Care。对于sw因为它不写寄存器所以RegDst和MemtoReg信号是什么值都无所谓后续电路不会使用到这些路径。但为了逻辑清晰和后续扩展通常也会给它们赋予一个确定值比如0。ALUOp信号需要特别说明。在简单的控制设计中ALUOp可以只是2位信号直接指示ALU做哪种运算比如00代表加法用于地址计算01代表减法用于比较10代表由funct字段决定用于R型指令。对于lw和sw它们都需要ALU做加法来计算地址所以ALUOp都应该设为00或对应的“加法”编码。更复杂的控制设计可能会将ALUOp作为更高层的编码再由一个独立的ALU控制单元根据ALUOp和指令的funct字段生成具体的ALU操作信号如AND, OR, ADD, SUB等。但在仅支持lw/sw的初期我们可以简化直接让控制单元产生最终的ALU操作信号假设为ALU operation对于lw/sw这个信号就是“加法”。控制单元的实现本质上就是一个真值表或者用硬件描述语言如Verilog写的一个case语句module control_unit ( input [5:0] opcode, output reg reg_dst, alu_src, mem_to_reg, reg_write, output reg mem_read, mem_write, branch, output reg [1:0] alu_op ); always (*) begin // 默认值通常设为全0或安全值 {reg_dst, alu_src, mem_to_reg, reg_write, mem_read, mem_write, branch, alu_op} 0; case (opcode) 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 2‘b00; // 表示加法 end 6’b101011: begin // sw reg_dst 0; // 无关可设为0 alu_src 1; mem_to_reg 0; // 无关可设为0 reg_write 0; mem_read 0; mem_write 1; branch 0; alu_op 2‘b00; // 表示加法 end // 其他指令的case分支... default: begin // 处理非法指令或未实现指令可将所有控制信号置为0 end endcase end endmodule5. 关键细节、常见陷阱与调试心得设计图看起来清晰但真正用硬件描述语言实现或者用数字电路仿真时坑就来了。下面分享几个我踩过或者学生常踩的坑。5.1 信号时序与数据竞争在单周期CPU中所有操作都在一个时钟周期内完成。这意味着从PC读出指令到寄存器堆读出数据到ALU计算到内存访问再到写回寄存器这一长串组合逻辑的延迟必须小于时钟周期。但这里有一个隐藏的陷阱写回寄存器堆的时刻。对于lw指令在时钟周期的末尾当MemtoReg多路选择器输出稳定的内存数据时RegWrite信号必须也是有效的才能将数据写入rt寄存器。如果RegWrite信号因为组合逻辑路径较长比数据晚一点点到达寄存器堆就可能造成写入失败或写入错误数据。在仿真中这表现为lw指令执行后目标寄存器的值没有更新。解决办法确保控制单元生成RegWrite信号的逻辑路径尽可能短或者检查寄存器堆的写使能端是否对时钟边沿敏感如果是同步写则需要确保在时钟有效沿到来前数据和写使能信号都已稳定。5.2 内存接口的字节寻址与对齐MIPS架构是字节寻址的但lw和sw指令访问的是字Word32位。这意味着我们提供给数据存储器的地址其最低两位bit[1:0]应该是00因为一个字占4个字节。在我们设计的数据通路中ALU计算出的地址是字节地址。一个常见的错误是直接将这个地址连到存储器的地址线而存储器模块可能期望的是字地址即地址右移2位。必须明确你使用的存储器IP核或模型是字节寻址还是字寻址。如果存储器模型是字节寻址那么地址线输入addr数据线是32位通常意味着从addr开始的连续4个字节addr,addr1,addr2,addr3构成了一个可一次读写的字。此时ALU计算的地址可以直接接入。但要注意如果地址不是4的倍数即addr[1:0] ! 2‘b00在真实的MIPS CPU中会触发地址错误异常Address Error Exception。在我们的简化设计中可以暂时不做这个检查但心里要知道这是个不规范的访问。如果存储器模型是字寻址那么地址线输入addr一次访问的就是第addr个字即字节地址为addr*4的区域。此时必须将ALU计算出的地址右移2位即addr[31:2]再送给存储器。这是非常容易遗漏的一点在仿真中表现为你计算出的地址是0x1000但实际访问的内存位置却是0x400数据完全对不上。5.3 立即数符号扩展的“坑”符号扩展单元看似简单但必须严格按补码规则进行将16位立即数的最高位第15位复制16份填充到高16位。在Verilog中正确的做法是使用{{16{immediate[15]}}, immediate}。如果错误地使用了零扩展如{16‘h0, immediate}那么当立即数是负数最高位为1时扩展后的32位数就变成了一个很大的正数导致计算出的内存地址完全错误。在调试时如果发现lw/sw访问的地址莫名其妙非常大首先就应该检查符号扩展是否正确。5.4 仿真与调试技巧分模块仿真不要一开始就把所有模块连起来。先单独仿真寄存器堆、ALU、存储器、控制单元确保每个模块功能正确。给关键信号命名在顶层连接时网线名尽量有意义如pc_value,instr,read_data1,alu_result,mem_read_data等这样在波形图里一目了然。构造有意义的测试程序不要只用一两条指令测试。写一个小程序段比如lw $t0, 4($zero) // 从内存地址4加载数据到$t0 addi $t1, $t0, 1 // $t1 $t0 1 sw $t1, 8($zero) // 将$t1的值存到内存地址8在仿真前先在数据存储器中地址4的位置手动存入一个已知值比如32‘hA5A5A5A5。然后单步执行观察执行lw后$t0是否变成了A5A5A5A5执行addi后$t1是否变成了A5A5A5A6执行sw后内存地址8的内容是否变成了A5A5A5A6善用波形图沿着数据通路从PC开始一步步追踪信号的改变。重点看指令码是否正确取出控制信号RegWrite, MemRead等在对应指令周期是否为预期值ALU的输入操作数是否正确输出结果内存地址是否正确内存的读/写使能是否在正确的时间激活读出的数据或写入的数据是否正确写回寄存器的数据选择MemtoReg和地址选择RegDst是否正确6. 从lw/sw扩展到完整指令集当你成功实现了lw和sw并且通过了仿真测试恭喜你你已经掌握了单周期CPU最核心的访存机制。接下来扩展支持其他指令就变成了“填空”游戏。R型指令如add, sub, and, or它们不需要访问内存所以MemRead和MemWrite都为0。它们的目标寄存器是rd字段所以RegDst1。它们写回的数据来自ALU结果所以MemtoReg0。需要写寄存器所以RegWrite1。ALU的第二个操作数来自寄存器Read data2所以ALUSrc0。ALU的具体操作add/sub/and/or由指令的funct字段位[5:0]决定控制单元需要生成更复杂的ALUOp信号或者结合一个ALU控制模块来解析funct。I型算术指令如addi, andi与R型类似但使用立即数作为第二个操作数所以ALUSrc1。目标寄存器是rt字段所以RegDst0。同样MemRead0,MemWrite0,MemtoReg0,RegWrite1。分支指令如beq不写内存也不写寄存器所以MemRead0,MemWrite0,RegWrite0。需要ALU做减法[rs] - [rt]比较结果是否为零所以ALUSrc0两个操作数都来自寄存器ALU操作为减法。需要额外的电路计算分支目标地址PC 4 (sign-extended immediate 2)并有一个多路选择器根据ALU的零结果Zero flag选择下一条指令是PC4还是分支目标地址。这需要引入Branch控制信号。通过这样分析每类指令的需求补充控制信号的真值表并在数据通路上增加必要的部件如分支地址计算器、跳转用的多路选择器等你就能一步步搭建起一个支持完整基础指令集的单周期MIPS CPU。这个过程其实就是计算机组成原理教科书上那个经典数据通路图的由来。而这一切的起点就是深刻理解了lw和sw这一对“读写兄弟”在CPU中是如何奔跑起来的。
返回列表