单总线CPU设计与硬布线控制

AI助手已提取文章相关产品:

单总线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),仅供参考

您可能感兴趣的与本文相关内容

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值