计算机组成原理的数据通路尤其是单周期CPU那种一张大图全是线和MUX的画法几乎是劝退重灾区。我第一次把教材里那张单周期CPU数据通路图完整看下来脑子里的反应是这到底是怎么设计出来的为什么这里要多一根线、那里要多一个MUX后来把指令一条一条拆开才意识到它其实就是一个特别朴素的循环——取指、读寄存器、运算、访存、写回。这次笔记就集中写一件事基于7条MIPS指令的单周期CPU数据通路怎么从零搭起来。这7条指令分别是add、sub、and、or、slt、lw、sw覆盖了R型算术逻辑指令和访存指令后面我会额外讲beq和j的扩展接法。适合正在学计组、被数据通路折磨的本科生也适合复习备考准备机试的同学——把这张图吃透单周期CPU的设计题基本就稳了。1. 为什么是这7条指令先把指令格式和硬件需求对上号1.1 32位定长指令让数据通路省心MIPS最大的特点是指令定长32位处理器取指时不需要根据指令类型去猜这条指令有多长。这个特性直接影响数据通路的取指设计PC每次加4从指令存储器取出一条完整的32位指令然后送进译码逻辑。指令格式主要分两种R型和I型。R型指令的字段是6位op 5位rs 5位rt 5位rd 5位shamt 6位funct总共32位。I型指令是6位op 5位rs 5位rt 16位立即数。7条指令里add、sub、and、or、slt都是R型lw和sw都是I型。选这7条指令有个好处它们的op和funct很有代表性。R型指令全是op0真正区分操作的是funct字段lw的op0x23sw的op0x2b区分它们靠op。控制单元在译码时先看op判断是哪类指令再看funct决定ALU做什么。这个先分类、再细看的思想后面设计控制信号真值表时是核心。1.2 指令类型决定你需要哪些硬件模块指令要做什么直接决定了数据通路里要有什么部件。R型算术逻辑指令需要寄存器堆提供两个读口读出rs和rt需要ALU做运算再把结果写回rd。lw需要寄存器堆读base地址寄存器、符号扩展立即数作为偏移、ALU算地址、数据存储器读数据最后写回rt。sw则相反寄存器堆要提供base地址和写入数据ALU算地址数据存储器写数据。所以这张数据通路上必有的模块是PC、指令存储器、寄存器堆、符号扩展器、ALU、数据存储器、控制单元再加上若干MUX。7条指令虽然不多但已经把组合逻辑、读写时序、控制信号这几大核心问题都覆盖到了。真正把通路的各个模块连接起来以后再去看教材上的大图每根线都有出处了。2. 取指部分PC和指令存储器的“加4循环”2.1 PC为什么加4而不是加1取指是每条指令执行的第一步。PC存的是当前指令的地址取完这条指令后PC要更新到下一条指令的地址。MIPS按字节编址每条指令恰好4字节所以PC要加4而不是加1。这里有个很容易糊涂的点为什么不能用PC加1因为PC里存的是字节地址相邻两个地址之间差1就是相邻两个字节但一条指令占了4个字节。你想要的不是下一个字节而是下一条指令。如果你天真地把PC加1取出来的指令会从指令的正中间开始读整个译码全乱。所以PC更新就是PC 4硬件上用一个专门的加法器输入是当前的PC输出直接送回到PC寄存器的输入端口。2.2 指令存储器是只读的取指不需要额外的读使能指令存储器在单周期CPU里只做一件事给定一个地址输出该地址对应的32位指令。它是纯组合逻辑的读——每个周期一到这里就自动出结果不需要专门的读使能信号也不需要时钟来控制。你给它PC的值它就吐指令天经地义。也正因为指令存储器只读设计里不需要写端口整体电路就是一个巨大的ROM。写程序进去是仿真的初始化步骤不属于运行时的数据通路考虑。PC本身在单周期CPU里也是每个周期都更新因此不需要额外的写使能。只要时钟上升沿一到PC就把前面加法器算出来的新地址锁存进去。至于要不要暂停PC这种需求是后面做流水线、多周期CPU才考虑的事单周期阶段不用操心。3. 寄存器堆与ALU运算核心的搭法3.1 两个读口一个写口寄存器堆是数据通路的通用存储点MIPS有32个32位寄存器结构上要有两个读口和一个写口。两个读口对应R型指令的rs和rtlw/sw的base和写入数据。以add $rd, $rs, $rt为例rs字段送读口1rt字段送读口2ALUSrc0时选择rt的数据作为ALU第二输入运算结果写回rd。寄存器堆的组合逻辑读是纯接线的读口地址进来数据就出来不需要等时钟。写口反而是同步的RegWrite1且时钟沿到来时才真正把数据写进寄存器。两个读口一个写口的代价是寄存器堆内部需要更多端口但这换来了单周期内完成读两个寄存器 运算 写一个寄存器的可能。如果只有一个读口R型指令读rs和rt就得分开两个周期那就不是单周期CPU了。3.2 符号扩展lw/sw的立即数是偏移量I型指令的16位立即数在参与地址计算前要扩展成32位关键是符号扩展而不是零扩展。原因在于lw/sw的16位立即数是一个有符号偏移量可能是负数比如lw $t0, -8($s1)。如果做零扩展-8这个用补码表示的立即数就会变成一个很大的正数计算出来的地址完全错掉。符号扩展的硬件实现其实简单得离谱把第15位符号位复制16份拼在高16位就行。零扩展则是高16位填0。这两种扩展不是同一种电路设计时千万别混。我做仿真时有一次lw指令算出来的地址总是不对查了半天发现扩展器写成了零扩展——一个负偏移直接算成了三十二位正数地址飞到宇宙边缘去了。3.3 ALU控制从funct到真实运算ALU需要完成add、sub、and、or、slt这5种操作它的控制信号不是直接来自funct而是来自ALU控制单元输入是funct和2位ALUOp。7条指令的funct分别如下add是0x20sub是0x22and是0x24or是0x25slt是0x2a。ALU控制单元根据funct输出4位控制信号常见编码是需要的操作ALU控制信号and0000or0001add0010sub0110slt0111结合ALUOp的规则是ALUOp00时lw/sw一律用addALUOp10时由funct细粒度决定。这样设计的意义在于控制单元主控不需要认识每一条R型指令的funct它只需要给ALUOp10让ALU控制单元去解析。功能分离各管一段逻辑清楚很多。4. 数据存储器接入lw和sw的读写通路4.1 lw的完整路径从内存取数据到写回寄存器lw $rt, offset($rs)这条指令本质是用rs的值加立即数偏移算出地址从数据存储器读出32位数据写回rt。它经过的路径依次是PC指向的指令存储器读出这条lw指令控制单元译码得到op0x23。寄存器堆读口1按rs地址读出base值读口2按rt读出当前值对lw来说这个值没用但寄存器堆的读口是固定的硬件无法跳过。立即数符号扩展成32位。ALUSrc1ALU第二输入选择扩展后的立即数ALU做加法算出访存地址。MemRead1数据存储器按地址输出数据。MemtoReg1写回数据选通来自数据存储器的数据RegDst0选择rt字段作为写寄存器地址。时钟沿到来RegWrite1数据写进rt。这条路径几乎把所有关键部件都走了一遍所以单周期CPU的时钟周期长度通常是拿lw这条最慢的指令来决定的——它要经过寄存器堆读口、ALU、数据存储器、MUX、写口链条最长。4.2 sw的数据通路地址和数据的走向sw $rt, offset($rs)和lw方向相反把rt的值写进内存。它的数据路径不需要经过数据存储器再回寄存器而是寄存器堆读口1按rs读base地址读口2按rt读出待写数据。ALUSrc1ALU算地址。MemWrite1数据存储器在时钟沿把WrData来自rt的数据写入地址。sw的RegWrite0寄存器堆不写入MemRead0因为不需要读数据。MemtoReg在sw里是dont care因为写回根本不发生。这里有一个常见的粗心坑sw的数据存储器写入数据WrData必须接rt读出的数据而不是ALU的结果。很多人画数据通路时顺手把WrData接到了ALU出口结果仿真时sw写进去的全是地址值那调试起来相当酸爽。4.3 数据存储器的读写时序组合读、同步写数据存储器的读是组合逻辑写是时序逻辑。MemRead1时地址信号到达后数据会立即出现在ReadData输出上但MemWrite1不会立即改内存必须等时钟上升沿才真正写入。这个设计恰好对应单周期CPU的整体思路一个周期内前面所有组合逻辑部件算好结果最后在时钟沿那一刻统一把状态PC、寄存器、数据存储器更新。所有写使能信号都在同一边沿生效避免出现同一周期内又读又写的竞争问题。实际工程里寄存器堆和存储器也会处理同一周期内读旧值还是新值的问题单周期设计一般约定写时钟沿生效、读使用旧值教材的常见模型也是这个约定。5. 控制信号真值表整张数据通路的“总指挥”5.1 逐个信号拆解别怕那张表控制单元的输入是指令的op字段输出是各个控制信号。7条指令的控制信号真值表如下分支和跳转我暂时不放进去后面单独扩展指令RegDstALUSrcMemtoRegRegWriteMemReadMemWriteALUOpadd10010010sub10010010and10010010or10010010slt10010010lw01111000swx1x00100逐个看每个信号的含义RegDst决定写回寄存器堆的地址选rd还是rt。R型选rd1lw选rt0。ALUSrc决定ALU第二输入。R型选寄存器rt0lw/sw选符号扩展立即数1。MemtoReg决定写回寄存器的数据来自ALU结果还是内存读出的数据。lw选内存1R型选ALU0。RegWrite寄存器堆写使能。R型和lw都是1sw是0。MemRead / MemWrite数据存储器读、写使能。lw读sw写。ALUOp两位操作码00表示add10表示R型。ALU控制单元再根据funct细化为真正的ALU操作。5.2 反推真值表的方法刚开始学的时候这张表是靠这条指令每一步要做什么反推出来的。比如slt它和add的控制信号为什么完全相同因为slt的硬件路径和add一模一样只是ALU内部算的东西不同而ALU到底算什么是ALU控制单元根据funct决定的主控制单元根本不关心。所以slt的RegDst、ALUSrc、RegWrite和add全部相同ALUOp也都是10。再比如sw为啥ALUSrc1而RegWrite0因为它要ALU算地址所以要立即数扩展因为它不写寄存器堆所以RegWrite0RegDst和MemtoReg自然无所谓。逐信号过完一遍这张表就不再是死记硬背了。5.3 控制单元为什么是纯组合逻辑控制单元的输出全部由当前指令的op决定不依赖时钟也不依赖之前的任何状态所以是纯组合逻辑。在一个时钟周期里op从指令存储器出来经过控制单元各信号在组合逻辑门间传播并在时钟沿之前稳定下来最后写动作统一发生在时钟沿。这也是单周期CPU周期长度要吃满最慢路径的原因最坏情况的组合逻辑延迟决定了时钟周期必须拉多长。控制单元的传播延迟虽然不大但也在这条关键路径上。理解这一点后面学流水线时会特别有体会——流水线通过拆短组合逻辑链来压缩时钟周期本质目标就和这相关。6. 进阶扩展beq和j指令往哪里接6.1 beq加一个加法器和PC选通MUXbeq $rs, $rt, offset的执行逻辑是如果rs的值等于rt的值PC就跳转到目标地址否则顺序执行。这一步要在数据通路上加的东西有三件第一一个用来比较两个寄存器值是否相等的部件实际就是让ALU做减法并输出零标志Zero。Zero1表示两数相等。第二一个专门计算分支目标地址的加法器PC4当前下一条指令地址加上符号扩展并左移2位后的offset。第三一个控制PC输入的MUX选顺序地址还是分支目标地址。控制单元增加Branch信号。beq时Branch1当且仅当Branch1且Zero1时PC选择分支目标地址。硬件上用两个信号一起选通MUX等价于一个与门。ALUOp此时要为01让ALU做减法。6.2 j指令直接构造跳转地址j指令更省事它不比较任何东西跳转目标地址由26位立即数左移2位后拼上PC4的高4位组成。数据通路上只需要把PC的输入MUX再增加一个入口选择这个拼接出来的32位地址并增加Jump信号。这里有个容易忽略的硬件细节26位左移2位后是28位而PC是32位高4位必须从PC4的高4位取。因为MIPS地址空间是256MB对齐的跳转指令跳不出自己所在256MB区域。控制器增加Jump信号为1时直接强制PC选择跳转地址不需要任何条件。加上beq和j以后控制信号表多了Branch和Jump两列ALUSrc、MemtoReg这些同款信号的逻辑不变只是多个真值行。整张数据通路的MUX也多了两级PC选择Jump优先于BranchBranch优先于加4优先级由MUX的选通顺序天然决定。7. 仿真自测与排错经验7.1 写一个最小汇编测试程序我仿真自己写的单周期CPU时习惯先写一段能覆盖所有指令边界的汇编程序。思路很简单先用add/sub/and/or/slt把R型全部跑一遍再用sw把结果存进内存最后用lw读回来跟预期值比对。段内不要出现分支跳转免得操作码复杂以后出问题不知道怪谁。比如这么一小段逻辑addi $t0, $zero, 0 # 可以用add $t0, $zero, $zero 代替 add $t1, $t0, $zero # R型通路 sub $t2, $t1, $t0 # 运算 sw $t2, 0($t0) # 写入内存 lw $t3, 0($t0) # 读回 slt $t4, $zero, $t3 # 比较单周期CPU每条指令一个周期仿真波形上观察PC按4递增每个PC对应一条指令寄存器堆在每条指令结束后的时钟沿更新。这样出问题时振一下波形基本能定位是取指、运算、访存还是写回出问题。7.2 常见bug实录我自己调试过程中遇到的几个典型问题列出来可以当检查清单。第一个是R型指令跑了但寄存器没更新。出现这种情况九成是RegWrite没拉高或者RegDst信号接反导致写寄存器地址不是rd。检查波形时优先看RegWrite在时钟沿之前是否稳定为1。第二个是lw算出来的地址不对。原因多半是立即数扩展写成了零扩展、符号扩展端口接错或者ALUSrc多接了恒定1导致R型指令的ALU第二输入变成了立即数。这个坑排查起来比较费时间建议把每条指令对应的ALU两个输入用波形拉出来逐条核对是否符合R型用rt数据、lw/sw用扩展立即数。第三个是sw写入的数据不对。检查数据存储器WrData的输入是否确实来自寄存器堆rt的读出口。数据存储器内部如果读端口和写端口复用地址线还要检查控制信号有没有让读和写在同一个周期同时触发这在单周期里一般通过MemRead和MemWrite互斥来避免。第四个是PC不按4递增。单周期CPU里PC每个周期都加4如果你看到PC跳变或停滞先查PC寄存器的写使能是不是被反复拉低、PC加法器输出是否接到了正确端口。PC这条环是整个CPU的中枢它对了其它部分才谈得上调。7.3 自测清单照着过一遍就稳了我在验证CPU的时候会按这张清单逐条打勾[ ] 取指PC每个周期加4指令存储器输出的32位指令与汇编对应。[ ] R型rs、rt读出正确ALU输出正确rd在时钟沿被写入。[ ] lw地址base符号扩展偏移内存读出的数据写回rt。[ ] sw地址同上rt数据写入内存对应地址。[ ] 控制信号每类指令的RegDst、ALUSrc、MemtoReg、RegWrite、MemRead、MemWrite符合目标机器码的真值表。[ ] 边界值扩展后负数偏移、全0寄存器、寄存器0写入不被误触发都要测一遍。当初我把这套自测流程走完数据通路这张图才真正从教材印刷品变成了自己能从头推一遍的逻辑.现在每次画新的CPU数据通路也仍然沿用这个思路先确认指令需求再理通路最后用真值表把所有控制信号钉死。