简介:这份文档面向计算机组成原理课程的学习者与硬件实验备考者,聚焦华中科技大学(HUST)教学场景下的单总线CPU设计,帮助读者理清CPU内部各部件通过单一总线互联时的数据通路与时序控制逻辑。资源共1个doc文件,压缩包约3.34MB,文档中对总线结构、运算器、控制器、寄存器、总线仲裁机制以及时钟周期与信号同步等关键环节做了系统梳理,并结合Logisim数字电路仿真工具,说明如何用逻辑门与触发器搭建并验证可运行的单总线CPU电路。借助其中的设计思路与模块划分,读者可以对照指令执行流程理解取指、译码、送操作数与写回等阶段的信号配合关系,掌握在总线竞争与带宽受限条件下安排时序、规避数据冲突的常用方法。目前已有1617人学习下载,适合正在完成组成原理课程设计或希望在仿真环境中动手实现简易CPU的读者参考。
1. 单总线 CPU 与现代时序,先把这个组合的边界划清楚
单总线 CPU 设计(现代时序)这套课设里,卡人的从来不是触发器怎么接,而是两条容易被忽略的硬边界:单总线决定了数据通路在任何时刻只允许一个部件驱动总线,现代时序决定了控制器每个时钟周期只推进一步微操作。前者约束连线,后者约束微指令的编排节奏。把这两条先立住,后面无论是 ADD、LOAD 还是条件跳转,都只是往字段里填不同的位。这篇按数据通路的搭建、微指令格式与条件判别测试逻辑、取指到执行的完整序列、最后到波形排错的顺序展开,写给正在 Logisim 或头歌上做单总线 CPU、需要把连线图和微程序都调到能跑的人。适合已经学过寄存器传输级描述、但还没把"控制器怎么指挥数据通路"这件事串起来的读者。
2. 单总线数据通路搭建:寄存器、ALU 与三态门的分时复用
2.1 单总线为什么必须靠三态门分时复用
内部总线是一组被所有部件共享的导线。如果 PC 和 R0 同时把自己的值驱到同一根线上,一个驱高一个驱低,轻则读到不定态,重则逻辑上形成短路。所以每个输出源后面都要挂一个三态门,由控制器给出使能信号,同一时刻只允许一个使能有效。这解释了为什么微指令里"输出"和"输入"要成对出现:PCout 有效时把 PC 送上总线,MARin 有效时同一周期从总线锁存进 MAR。一个时钟周期内完成"源输出 + 目标输入"这一个微操作,正是现代时序的基本颗粒度。
提示:Logisim 里的隧道(Tunnel)只是连线别名,不具备三态特性。真正实现"多源共线"要用分线器配合使能端,或直接用一个多路选择器做总线汇聚,功能等价。
2.2 最小部件清单与连接关系
把数据通路拆到能跑通取指和几条基本指令,部件清单大致如下,每个部件都只通过一个三态门接到总线上。
| 部件 | 作用 | 典型控制信号 |
|---|---|---|
| PC | 程序计数器,指向下一条指令 | PCout / PCin / PCinc |
| MAR | 访存地址寄存器 | MARout / MARin |
| MDR | 访存数据寄存器 | MDRout / MDRin |
| IR | 指令寄存器,存放当前指令 | IRout / IRin |
| R0–R3 | 通用寄存器组 | Rnout / Rnin |
| ALU | 算术逻辑单元 | ALUout / ALUop |
| PSW | 状态标志(ZF、CF) | Zin / Cin |
| 主存 | 指令与数据 | Read / Write |
关键连接原则只有三条:所有部件的输入都来自总线,所以每个 in 信号都是从总线锁存;所有部件的输出都经过三态门到总线;ALU 的两个操作数至少有一个来自总线,另一个可以是寄存器或暂存器。写回结果时,ALU 输出再经三态门送回总线,由目标寄存器的 in 信号锁存。
2.3 用 Verilog 描述单总线数据通路
下面这段把三态总线和时钟锁存的核心写出来,综合到 FPGA 时多驱动会被映射成多路选择器,逻辑行为一致。
module single_bus_datapath(
input clk, rst
);
tri [7:0] bus; // 8 位共享内部总线
reg [7:0] PC, MAR, MDR, IR, R0;
reg PCout, MARin, MDRin, MDRout, IRin, IRout, R0out, R0in;
// 每个输出源一个三态门,同一时刻只允许一个 out 有效
assign bus = PCout ? PC : 8'bz;
assign bus = MDRout ? MDR : 8'bz;
assign bus = IRout ? IR : 8'bz;
assign bus = R0out ? R0 : 8'bz;
// 时钟上升沿锁存:谁被选中谁更新
always @(posedge clk or posedge rst) begin
if (rst) begin
PC <= 8'b0; MAR <= 8'b0; MDR <= 8'b0; IR <= 8'b0; R0 <= 8'b0;
end else begin
if (MARin) MAR <= bus;
if (MDRin) MDR <= bus;
if (IRin) IR <= bus;
if (R0in) R0 <= bus;
if (PCout) PC <= PC + 8'b1; // 简化:输出周期自增
end
end
endmodule
逻辑上, tri 类型允许多个 assign 同时驱动同一根线,但任一时刻只应有一个驱动非高阻。参数上, bus 宽度按指令字长取 8 位或 16 位,寄存器宽度必须与总线一致。注意 PC <= PC + 1 放在输出周期只是简化演示,真实设计里自增通常由独立的 PCinc 信号控制,避免把"输出"和"自增"绑成同一个微操作。
注意:一段代码里若出现两个 out 信号同时有效,仿真会给出 X 态。这是单总线冲突最典型的信号,排错时优先查这里。
3. 现代时序控制:微指令格式与条件判别测试逻辑
3.1 现代时序与传统三级时序的差别
传统时序把机器周期切成若干节拍电位,每个节拍电位里再切节拍脉冲,时序信号由节拍发生器集中产生,控制信号分散在各个逻辑门里,调试时要在多个时间尺度上对齐。现代时序则把时钟周期当作唯一节拍单位,每个上升沿推进一步微操作,微操作的先后由微程序的下地址决定,不再区分节拍电位和节拍脉冲。
这个转变带来的直接影响是:设计者不再画时序图去对齐多个脉冲,而是把精力放在微指令序列上。每条微指令描述"本周期谁输出、谁输入、下一条去哪",控制器的复杂度从时序电路转移到了微地址逻辑。对单总线 CPU 来说,这恰好匹配它的限制——反正一个周期只能有一个输出源,微指令天然就是"一周期一微操作"。
3.2 微指令字段怎么划分
微指令通常分三段:控制字段给出本周期所有 out/in 使能;测试字段指明本条微指令要不要做条件判别、判别哪个标志;下地址字段给出后继微地址。控制字段的每一位对应一个控制信号,位数等于信号总数。
| 字段 | 位数示例 | 内容 |
|---|---|---|
| 控制字段 | 16 位 | PCout、MARin、MDRin、MDRout、IRin、R0in、R0out、ALUout、ALUop、Zin… |
| 测试字段 | 3 位 | 0 无条件,1 判 ZF,2 判 CF,3 判 IR 操作码… |
| 条件选通 | 1 位 | 是否启用条件分支 |
| 下地址字段 | 8 位 | 后继微地址,或分支候选地址 |
控制字段的编码方式有两种常见做法:水平型每一位独立,微指令长但译码简单;垂直型用编码字段压缩操作,微指令短但要多一级译码。课设里数据通路信号不多,我一般用水平型,直接展开好调试。
3.3 条件判别测试逻辑怎么实现
条件分支的核心是把测试结果拼进微地址。常见做法是让下地址字段给出两路候选的基址,最低位由测试结果填充,测试成立走一条、否则走另一条。下面这段演示微地址生成。
module microctrl(
input clk, rst,
input ZF, CF,
input [7:0] IR_op
);
reg [7:0] uaddr;
reg [15:0] uIR; // 微指令寄存器
reg [15:0] urom [0:255]; // 微指令 ROM
reg [2:0] test;
reg cond_en;
// 测试字段译码:选择当前要判定的条件
wire test_bit = (test == 3'd1) ? ZF :
(test == 3'd2) ? CF :
(test == 3'd3) ? IR_op[0] : 1'b0;
always @(posedge clk or posedge rst) begin
if (rst) begin
uaddr <= 8'b0;
end else begin
uIR <= urom[uaddr]; // 取微指令
test <= uIR[13:11]; // 解析测试字段
cond_en <= uIR[10]; // 是否启用条件分支
// 条件成立时下地址最低位置 1,否则清零
uaddr <= {uIR[7:1], (cond_en & test_bit)};
end
end
endmodule
逻辑说明:每周期先用当前 uaddr 从 ROM 取微指令,锁进 uIR ;下一周期根据 uIR 里的测试字段选出 test_bit ,若 cond_en 有效就把它拼到下地址最低位。参数上,下地址字段宽度决定微程序容量,8 位对应 256 条微指令,通常够一个教学 CPU 用。把 cond_en & test_bit 换成多路选择可以让测试结果占两位,从而支持四路分支,代价是下地址字段要留出对应的低位。
提示:测试字段和条件选通分开设,是为了让"无条件微指令"也能复用下地址字段做顺序跳转,避免所有微指令都背上分支开销。
4. 从取指到执行的微指令序列:能跑通的一版编排
4.1 取指周期的三步微指令
取指是每条指令都要走的公共路径,用现代时序一般拆成三步:地址送 MAR、访存取回送 MDR、MDR 送 IR 并计算下一条 PC。三步写成微指令如下。
| 微地址 | 控制字段(out/in) | 说明 |
|---|---|---|
| 0x00 | PCout, MARin | PC 送 MAR |
| 0x01 | MDRin, MEMread, PCinc | 启动读主存,PC 自增 |
| 0x02 | MDRout, IRin | MDR 送 IR,取指结束 |
| 0x03 | 依据 IR 操作码分支 | 进入执行周期 |
第一步把 PC 值送上总线锁进 MAR;第二步发出读信号,MDR 接收主存返回的数据,同时 PC 自增指向下一条;第三步把 MDR 里的指令送进 IR,控制权交给译码。这三步的地址是固定的,所以微地址可以直接顺序 0x00→0x01→0x02→0x03。
4.2 数据传送、运算与跳转的微操作拆解
取指结束后,微地址跳到执行入口,不同指令走不同分支。把三条典型指令拆成微操作,可以看清时序是怎么"一周期一步"推进的。
| 指令 | 微操作序列 |
|---|---|
| MOV R0, R1 | R1out, R0in |
| ADD R0, R1 | R1out, ALUin, ALUop=ADD;ALUout, R0in, Zin |
| JZ addr | 判 ZF:成立则 addr 送 PC;否则继续顺序取下条 |
ADD 被拆成两拍是单总线的必然结果:第一拍把 R1 送 ALU 的一个入口,第二拍 ALU 结果经三态门回总线锁进 R0。如果数据通路里只给 ALU 一个输入口,ADD 还需要一拍把另一个操作数先暂存进一个 A 寄存器。JZ 的关键在测试字段——它把 ZF 接进条件判别逻辑,微地址最低位随判定结果翻转,指向"改 PC"或"取下一指令"两条路径。
4.3 把微指令写进 ROM
Logisim 里可以用一个 ROM 组件存微指令,地址端接微地址寄存器输出;头歌平台则通常提供微程序存储单元的编辑入口,把每条微指令按字段填进去。填的时候按控制字段、测试字段、下地址字段从左到右对齐,别把条件选通位错位。写完后用一个简单的计数器节拍单步运行,观察微地址是否按预期跳转。
注意:执行周期的微地址入口由取指第三步的分支给出。若把译码放在微程序外部,记得取指结束后给微地址一个固定偏移,否则会跳回取指序列形成死循环。
5. 验证与排错:单总线冲突、时序毛刺与微地址跳错怎么定位
单总线 CPU 调不出来,现象大多集中在三类:总线出现 X 态、微地址乱跳、结果差一拍。前两类在仿真里就能锁定,第三类往往要对着节拍逐周期看。
先看 X 态。总线读到不定值,九成是两个 out 同时有效。做法是把所有 out 使能接到一个显示探针,单步运行时盯着看哪个周期亮了两个灯。常见诱因是控制字段编码时把两个信号编到了同一位,或微指令 ROM 里某条记录填错位——比如把测试字段的值填进了控制字段。
| 现象 | 高概率原因 | 定位手段 |
|---|---|---|
| 总线 X | 两个 out 同时有效 | 探针查 out 使能,核对微指令编码 |
| 微地址乱跳 | 条件选通位错位 | 打印每拍 uIR 与 uaddr |
| 结果差一拍 | 锁存沿与输出沿同周期 | 把写回挪到下一拍 |
| PC 不自增 | PCinc 未编入取指第二步 | 检查 0x01 控制字段 |
再看微地址跳错。把每拍的 uIR 和 uaddr 打到日志里,逐条比对预期序列。如果某条条件微指令无论标志如何都走同一分支,检查测试字段译码时 test_bit 是否被常量覆盖——比如 cond_en 一直是 0,条件分支就永远走低位清零那条路。
最后是时序毛刺。现代时序用一个时钟沿推进一步,如果某个寄存器在输出周期同时被写回,仿真里会看到它本周期就被更新,下一拍再读就对不上了。稳妥做法是让"输出"和"写回"落在相邻两个周期,写回始终晚一拍。调试时把时钟频率降到肉眼能跟的程度,用单步按钮一拍一拍走,比一次跑完再猜哪里错效率高得多。
301



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



