单总线CPU设计:从时序控制到硬布线逻辑的实践之路
在FPGA实验平台上“造”一颗能跑通汇编程序的CPU,听起来像是计算机体系结构课程里的终极挑战。而华中科技大学在“头歌”(EduCoder)平台推出的单总线CPU实验——特别是那个要求实现 变长指令周期+三级时序控制 的设计任务——正是这样一道既考验基础又锤炼思维的经典题目。
这并不是简单的模块拼接,也不是照搬课本图示就能通关的仿真练习。它逼你深入到每一个上升沿、每一条控制线、每一次总线争用的本质层面去思考:一个指令是如何被“一步步”执行出来的?为什么T1做A操作、T2做B操作?如果某条指令比别的慢怎么办?这些问题的答案,恰恰构成了我们理解现代处理器底层机制的起点。
整个系统围绕一条共享数据总线构建。ALU、寄存器堆、PC、IR、MAR、MDR等所有核心部件都挂在这条总线上,任何时刻只能有一个部件向总线输出数据。这种设计虽然牺牲了并行性,却极大简化了控制逻辑,非常适合教学场景下的RTL建模与调试。
真正让这个实验“活起来”的,是那套显式的 三级时序控制系统(T1/T2/T3) 。你可以把它看作CPU内部的节拍器:每个机器周期被划分为三个固定阶段,每个阶段触发一组特定的微操作。比如:
- T1 :准备地址或源操作数;
- T2 :读取内存或将第二操作数送入ALU;
- T3 :启动运算或将结果写回寄存器。
这样的划分不是随意的,而是为了匹配同步电路中信号传播和锁存的时序窗口。更关键的是,这套机制允许我们通过控制器动态决定:“这条指令是否还需要下一个周期?”这就引出了 变长指令周期 的核心思想。
像
ADD R1, R2
这种纯寄存器操作,一个T1-T2-T3循环足以完成取指、译码和执行;但
LW R1, 100(R2)
就不行了——它至少需要两个完整周期:第一个周期取指并解析出基址与偏移,第二个周期计算有效地址并发起访存,甚至第三个周期才能把数据写回目标寄存器。
如果不支持变长周期,要么所有指令都按最慢的来等,浪费资源;要么就得引入复杂的流水线冲突检测机制。而在教学环境中,采用可变周期+状态保持的方式,既能体现真实CPU的行为特征,又不至于让学生陷于难以调试的状态机泥潭。
那么,是谁在指挥这一切?答案是 硬布线控制器(CU) 。它不像微程序控制器那样查表取微指令,而是直接用组合逻辑根据当前操作码(opcode)、时序节拍(T1/T2/T3)以及部分标志位生成全部控制信号。这些信号细到每一个门控开关的程度:
-
PC_out—— 是否将PC内容放到总线上? -
MAR_in—— 是否将总线上的值打入MAR? -
RAM_read/RAM_write—— 发起读/写请求? -
Rs_out—— 源寄存器输出使能? -
ALU_op—— 设置ALU执行加法还是减法? -
Reg_write—— 允许写入通用寄存器?
每一个信号的开启时机必须精确对应T1/T2/T3中的某一拍,否则就会出现“还没读出来就试图写回”或者“地址没稳定就发读命令”这类致命错误。
举个例子,对于
LW
指令的执行流程可以拆解为多周期微操作序列:
| 周期 | 节拍 | 动作 | 控制信号 |
|---|---|---|---|
| 1 | T1 | PC → MAR | PC_out=1, MAR_in=1 |
| T2 | 启动取指读操作 | RAM_read=1 | |
| T3 | MDR → IR, PC+1 | MDR_out=1, IR_in=1; PC_inc=1 | |
| 2 | T1 | Rs + Offset → ALU | Rs_out=1, Imm_ext→ALU_B, ALU_op=ADD |
| T2 | ALU结果→MAR | ALU_out=1, MAR_in=1 | |
| T3 | 再次启动RAM读 | RAM_read=1, MDR_in=1 | |
| 3 | T1 | MDR → 目标寄存器 | MDR_out=1, Reg_in=1, Reg_dst=Rd |
| T2/T3 | 空闲 | 所有信号置0 |
注意这里跨越了三个机器周期,且每一拍的动作都严格依赖前一拍的结果。控制器必须能够识别当前处于哪条指令的哪个阶段,并据此输出正确的控制字。实现上通常采用一个大的
case
语句块,以
{opcode, T1, T2, T3}
作为选择条件,逐条定义每种情况下的信号赋值。
下面是典型的三级时序发生器Verilog实现:
module timer (
input clk,
input reset,
output reg T1, T2, T3
);
reg [1:0] state;
always @(posedge clk or posedge reset) begin
if (reset) begin
state <= 2'b00;
T1 <= 0; T2 <= 0; T3 <= 0;
end else begin
case(state)
2'b00: begin
T1 <= 1; T2 <= 0; T3 <= 0;
state <= 2'b01;
end
2'b01: begin
T1 <= 0; T2 <= 1; T3 <= 0;
state <= 2'b10;
end
2'b10: begin
T1 <= 0; T2 <= 0; T3 <= 1;
state <= 2'b00;
end
default: state <= 2'b00;
endcase
end
end
endmodule
该模块使用独热编码方式输出T1/T2/T3,确保任意时刻只有一个节拍有效,避免多个控制动作同时发生导致总线冲突或寄存器误写。状态机循环运行,在无复位的情况下持续产生三拍节奏。
控制器部分则更为复杂。以下是一个简化的控制信号生成片段,展示了如何根据不同指令和节拍组合驱动硬件动作:
always @(posedge clk) begin
if (reset) begin
// 初始化所有控制信号
PC_out <= 0; MAR_in <= 0; RAM_read <= 0;
MDR_in <= 0; IR_in <= 0; PC_inc <= 0;
Rs_out <= 0; Rt_out <= 0;
ALU_op <= 0; Result_to_Reg <= 0;
Reg_write <= 0;
end else begin
case ({opcode, T1, T2, T3})
// ADD 指令:假设 opcode = 3'b000
{3'b000, 1,0,0}: begin // T1
Rs_out <= 1;
end
{3'b000, 0,1,0}: begin // T2
Rt_out <= 1;
end
{3'b000, 0,0,1}: begin // T3
ALU_op <= 2'b00; // ADD
Result_to_Reg <= 1;
Reg_write <= 1;
end
// LW 指令:opcode = 3'b001
{3'b001, 1,0,0}: begin // 第一周期 T1
PC_out <= 1; MAR_in <= 1;
end
{3'b001, 0,1,0}: begin // T2
RAM_read <= 1;
end
{3'b001, 0,0,1}: begin // T3
MDR_in <= 1; IR_in <= 1; PC_inc <= 1;
end
// 注意:后续周期需继续处理地址计算与访存
// 可结合额外状态变量判断是否仍在执行LW
...
default: begin /* idle */ end
endcase
end
end
实际项目中建议将控制逻辑拆分为多个
always
块,分别处理PC管理、存储访问、ALU调度等子系统,提升代码可读性和维护性。此外,加入调试输出(如LED显示当前T状态、opcode、MAR值)对定位问题极为重要。
回到“头歌”平台的具体任务,其实验通常分为六个递进关卡,逐步验证功能完整性:
第1关:基本取指与PC自增
重点在于建立正确的取指流程。T1将PC送出至MAR,T2触发RAM读,T3将MDR内容装入IR并完成PC+1。难点在于确保MDR锁存在RAM数据有效之后,避免因时序错配导致IR加载错误。
第2关:实现ADD类指令
需要正确解析指令字段rs、rt、rd,并在T1/T2分别将两个源寄存器内容送上总线,在T3完成ALU运算并将结果写回。此处应检查多路选择器是否正确选通寄存器输出端口。
第3关:JMP跳转指令
实现无条件跳转(PC ← immediate)。可在T1直接将立即数送入PC,无需等待后续节拍。注意跳转后应阻止原流程继续执行,可通过清空IR或设置特殊状态标记实现。
第4关:LW加载指令(跨周期)
这是首个涉及多周期执行的指令。第一周期完成取指,第二周期进行地址计算(Rs + offset),第三周期访问内存并将数据写回Rd。控制器需维持上下文状态,防止中间被打断。
第5关:SW存储指令
与LW类似,先计算地址,然后将寄存器内容写入MDR,再通过RAM_write信号写入内存。不同之处在于SW不涉及结果写回寄存器,也不需要从内存读取数据。
第6关:综合测试
运行包含多种指令的完整程序,检验整体协调性。常见失败原因包括:
- 控制信号未及时关闭,造成串扰(如ADD误触发RAM_read);
- 某些指令未正确结束周期,导致后续指令错乱;
- 复位后PC未初始化为预期起始地址。
此时强烈建议添加状态指示信号,例如用板载LED显示当前T1/T2/T3状态、当前opcode高位、是否处于访存阶段等,极大降低调试难度。
这套设计的价值远不止于“通关”。当你亲手让一条
ADD
指令在一个个节拍中逐步推进,最终看到结果正确写入寄存器时,你会突然明白:所谓“执行”,本质上就是一系列受控的时间序列事件。而现代CPU所谓的“高性能”,不过是把这些节拍组织得更紧凑、更重叠、更智能罢了。
从单总线到三级时序,再到硬连线控制,每一步都在引导学习者摆脱高级语言的直觉依赖,建立起真正的硬件时序观。这种思维方式,正是通往五级流水线、超标量架构乃至RISC-V定制核心的必经之路。
当我们在FPGA上点亮第一个自己设计的CPU运行灯时,或许才算真正理解了冯·诺依曼机的灵魂所在——不是某个模块有多先进,而是 所有部件如何在统一节拍下协同工作 。而这,也正是这门课最难也最美的地方。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
3842

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



