8位CPU硬件设计
合作方:pointer-to-bios
设计概要如下:
设计要求:
1、通用寄存器8位,ALU 8位,内部数据总线8位。
2、支持4个8位通用寄存器R0、R1、R2和R3。
3、标志寄存器(FLAGS或者PSW)应至少有CF和ZF标志位。
4、指令长度8位(指令操作码4位,指令操作数合计4位),指令寄存器IR 8位,程序(指令)计数器PC 8位。
5、至少支持下列指令:MOV、ADD、SUB、AND、OR、NOT、XOR和RTN,其中RTN指令用于将PC清零,即返回程序第一条指令重新开始执行。
6、设计SHL、SHR等移位指令。
7、设计LD、ST等内存数据读写指令。
8、可选设计JMP、JZ、JNZ等跳转指令。
9、可以具备基本的通用寄存器监控功能用于调试程序。
10、考虑与硬件外设连接的扩展设计能力。
在设计一个通用8位 CPU 之前,首先需要考虑的是如何设计一个 CPU 物理结构图,这里我们先建设一个简易的物理逻辑框架出来:

设计概述
- 地址总线
地址总线实际上是CU输出的控制信号,为了降低CU的复杂度,将部分仅有输入/输出语义控制信号的模块编址,形成3位目标/源地址总线对,大大减少CU的控制信号数量。
- 数据总线的实现
本CPU在FPGA平台 Quartus2 上实现,结构图中的所有输出到总线的信号线通过多路复用实现,否则综合器有可能将全部模块都优化掉。
- 锁相环
由于本CPU最终将运行在物理芯片中,需要使用锁相环将芯片的全局时钟频率调整到CPU时序延迟允许之内。
基于这个框架图,我们将依次设计各个部件的模块逻辑
一、PC模块设计
module PC(
input clk,
input pc_inc, // PC是否自增
input reset,
(* keep, preserve *)output reg [7:0] databus // 统一数据总线
);
// reg [7:0] cur_pc; // 当前程序计数器值
//PC自增寄存器
wire [7:0] next_pc;
wire co;
// 三态总线驱动(完全由外部控制)
Adder PCInc(
.op1(databus),
.op2(8'h01),
.suben(0),
.sum(next_pc),
.co(co)
);
// 输出到总线
// assign databus = cur_pc; // 当pc_out为1时,输出当前PC值,否则为高阻态
// PC核心逻辑
always @(posedge clk or posedge reset) begin
if (reset) // PC置零
databus <= 8'h00;
else if (pc_inc) // PC自增
databus <= next_pc;
else databus <= databus; // PC不变(无信号)
end
endmodule
PC信号指示图:
输入端有时钟信号,而通过 PC 逻辑图我们可以看出,PC 需要两个(出去输出信号),一个是控制 PC 自增的信号,一个是让 PC 计数器清零的信号,这两个信号均有CU模块控制
而在 PC 内部,PC 模块需要有一个 add 模块用于计算 PC 自增后的下一个数是多少,这个加法逻辑单元直接复用 ALU 模块中的加法器即可
PC 的信号输出由 always 模块去控制,当到达时钟上升沿或者 reset 信号上升沿(reset变为1)的时候,就会触发 PC 内部值的改变,若遇到 reset 信号,则将输出总线数据置零,若
遇到 pc_inc 信号为1,则将 PC 早已算好的 next_pc 传给 总线输出单元,PC 单元的值会再次传递给加法器 Adder 的输入,因为 Adder 为组合电路,因此,几乎在同一时刻 PC 的next_pc会
再次计算好,等待 PC 接收到下一个 pc_inc 信号
二、ALU模块设计
代码总体设计如下:
module ALU(databus,inbus,clk,alua_in,alub_in,sel,cf);
(* keep, preserve *)output reg [7:0] databus; // 统一数据总线
input [7:0] inbus;
input clk;
input alua_in;
input alub_in;
input[2:0] sel;
output cf;
wire[7:0] op1;
wire[7:0] op2;
wire [7:0] result;
OPReg A(clk,alua_in,inbus,op1);
OPReg B(clk, alub_in,inbus,op2);
ALUCore u1(op1,op2,sel,result,cf);
// OutputBufReg R(clk,result,alu_out,databus);
always @(*) begin
databus<=result;
end
endmodule
module OPReg(clk,we,din,dout);
input clk;
input we;
input[7:0] din;
output[7:0] dout;
reg[7:0] dout;
always @(posedge clk)
begin
if(we==1'b1)
begin
dout <= din;
end
end
endmodule
module ALUCore(
input [7:0] op1,
input [7:0] op2,
input [2:0] sel, // 扩展为3位操作码
output reg [7:0] result,
output reg cf
);
// 加法器接口
reg [7:0] adder_op1;
reg [7:0] adder_op2;
reg suben;
wire [7:0] adder_sum;
wire adder_cout;
Adder u1(adder_op2, adder_op1, suben, adder_sum, adder_cout);
always @(*) begin
case(sel)
// 算术运算
3'b000, 3'b001: begin // 加法/减法
// 汇编代码其实是左源右目标,所以反过来放
adder_op1 = op1;
adder_op2 = op2;
suben = sel[0];
result = adder_sum;
cf = adder_cout;
end
3'b010: begin // 左移
adder_op1 = 8'b0;
adder_op2 = 8'b0;
suben = 1'b0; // 左移不需要减法
cf = 1'b0; // 左移不产生进位
// if(adder_op1[7] | adder_op1[6] | adder_op1[5] | adder_op1[4] | adder_op1[3]) begin
// result = 8'b0; // 左移后全零
// end else begin
result = op2 << op1;
// end
end
3'b011: begin // 右移
adder_op1 = 8'b0;
adder_op2 = 8'b0;
suben = 1'b0; // 左移不需要减法
cf = 1'b0; // 左移不产生进位
// if(adder_op1[7] | adder_op1[6] | adder_op1[5] | adder_op1[4] | adder_op1[3]) begin
// result = 8'b0; // 右移后全零
// end else begin
result = op2 >> op1;
// end
end
// 逻辑运算
3'b100: begin // 与(AND)
adder_op1 = 8'b0;
adder_op2 = 8'b0;
suben = 1'b0;
result = op1 & op2;
cf = 1'b0; // 逻辑运算清零CF
end
3'b101: begin // 或(OR)
adder_op1 = 8'b0;
adder_op2 = 8'b0;
suben = 1'b0;
result = op1 | op2;
cf = 1'b0;
end
3'b110: begin // 与非(NAND)
adder_op1 = 8'b0;
adder_op2 = 8'b0;
suben = 1'b0;
result = ~(op1 & op2);
cf = 1'b0;
end
3'b111: begin // 异或(XOR)
adder_op1 = 8'b0;
adder_op2 = 8'b0;
suben = 1'b0;
result = op1 ^ op2;
cf = 1'b0;
end
endcase
end
endmodule
module Adder(
input [7:0] op1, // 操作数1
input [7:0] op2, // 操作数2
input suben, // 减法使能(1=减法)
output [7:0] sum, // 和/差结果
output co // 进位/借位
);
// 减法转换为补码加法
wire [7:0] adj_op2 = op2 ^ {8{suben}};
wire cin = suben; // 减法时cin=1(补码+1)
// 生成位(Generate)和传播位(Propagate)
wire [7:0] G = op1 & adj_op2;
wire [7:0] P = op1 ^ adj_op2;
// 超前进位计算(分组优化)
wire [7:0] C;
assign C[0] = cin;
assign C[1] = G[0] | (P[0] & C[0]);
assign C[2] = G[1] | (P[1] & G[0]) | (P[1] & P[0] & C[0]);
assign C[3] = G[2] | (P[2] & G[1]) | (P[2] & P[1] & G[0]) | (P[2] & P[1] & P[0] & C[0]);
// 4位组间进位(关键优化点)
wire C4 = G[3] | (P[3] & G[2]) | (P[3] & P[2] & G[1]) | (P[3] & P[2] & P[1] & G[0]) |
(P[3] & P[2] & P[1] & P[0] & C[0]);
assign C[4] = C4;
assign C[5] = G[4] | (P[4] & C4);
assign C[6] = G[5] | (P[5] & G[4]) | (P[5] & P[4] & C4);
assign C[7] = G[6] | (P[6] & G[5]) | (P[6] & P[5] & G[4]) | (P[6] & P[5] & P[4] & C4);
// 最终进位和结果
assign co = suben ^ (G[7] | (P[7] & G[6]) | (P[7] & P[6] & G[5]) |
(P[7] & P[6] & P[5] & G[4]) | (P[7] & P[6] & P[5] & P[4] & C4));
assign sum = P ^ C;
endmodule
ALU信号指示图:
• ALU总体顶部模块实现
module ALU(databus,inbus,clk,alua_in,alub_in,sel,cf);
(* keep, preserve *)output reg [7:0] databus; // 统一数据总线
input [7:0] inbus;
input clk;
input alua_in;
input alub_in;
input[2:0] sel;
output cf;
wire[7:0] op1;
wire[7:0] op2;
wire [7:0] result;
OPReg A(clk,alua_in,inbus,op1);
OPReg B(clk, alub_in,inbus,op2);
ALUCore u1(op1,op2,sel,result,cf);
// OutputBufReg R(clk,result,alu_out,databus);
always @(*) begin
databus<=result;
end
endmodule
输入端口包含如下:
inbus:用于接受数据总线上面的数据,与 alua_in、alub_in、sel 信号配合使用
alua_in:用于接受数据总线上面的操作数 a 的数据(四位操作数)
alub_in:用于接受数据总线上面的操作数 b 的数据(四位操作数)
sel:用于接受数据总线上面的操作符 sel 的数据(3为操作码)
cf:进位标识符,若想扩展进位信号,只需要新建一个寄存器将输入与 cf 连接即可
输出端口包含如下:
op1:输入缓冲寄存器(OPReg A)接受数据后将数据输出给计算核心(ALUCore)的输出线路
op2:输入缓冲寄存器(OPReg B)接受数据后将数据输出给计算核心(ALUCore)的输出线路
result:计算核心(ALUCore)计算结果后的输出线路
• ALU输入缓冲寄存器
module OPReg(clk,we,din,dout);
input clk;
input we;
input[7:0] din;
output[7:0] dout;
reg[7:0] dout;
always @(posedge clk)
begin
if(we==1'b1)
begin
dout <= din;
end
end
endmodule
当 we 使能信号为有效时,将总线上面的数据锁存起来,结构较为简单,不再赘述
• 加法器实现
module Adder(
input [7:0] op1, // 操作数1
input [7:0] op2, // 操作数2
input suben, // 减法使能(1=减法)
output [7:0] sum, // 和/差结果
output co // 进位/借位
);
// 减法转换为补码加法
wire [7:0] adj_op2 = op2 ^ {8{suben}};
wire cin = suben; // 减法时cin=1(补码+1)
// 生成位(Generate)和传播位(Propagate)
wire [7:0] G = op1 & adj_op2;
wire [7:0] P = op1 ^ adj_op2;
// 超前进位计算(分组优化)
wire [7:0] C;
assign C[0] = cin;
assign C[1] = G[0] | (P[0] & C[0]);
assign C[2] = G[1] | (P[1] & G[0]) | (P[1] & P[0] & C[0]);
assign C[3] = G[2] | (P[2] & G[1]) | (P[2] & P[1] & G[0]) | (P[2] & P[1] & P[0] & C[0]);
// 4位组间进位(关键优化点)
wire C4 = G[3] | (P[3] & G[2]) | (P[3] & P[2] & G[1]) | (P[3] & P[2] & P[1] & G[0]) |
(P[3] & P[2] & P[1] & P[0] & C[0]);
assign C[4] = C4;
assign C[5] = G[4] | (P[4] & C4);
assign C[6] = G[5] | (P[5] & G[4]) | (P[5] & P[4] & C4);
assign C[7] = G[6] | (P[6] & G[5]) | (P[6] & P[5] & G[4]) | (P[6] & P[5] & P[4] & C4);
// 最终进位和结果
assign co = suben ^ (G[7] | (P[7] & G[6]) | (P[7] & P[6] & G[5]) |
(P[7] & P[6] & P[5] & G[4]) | (P[7] & P[6] & P[5] & P[4] & C4));
assign sum = P ^ C;
endmodule
输入输出如注释所示,下面介绍一下该加法器的加减二路选择功能以及串并联混用逻辑:
超前进位计算技术介绍:
若我们在加法计算中每一位都通过上一位的数据以及是否需要进位来判断下一位是否需要进位,那么我们所设计的电路就是8路串行,串行效率是不如并行的,首先串行代码应当如下:
assign C[0] = cin;
assign C[1] = G[0] | (P[0] & cin); // 第0位进位
assign C[2] = G[1] | (P[1] & C[0]); // 第1位进位
assign C[3] = G[2] | (P[2] & C[1]); // 第2位进位
assign C[4] = G[3] | (P[3] & C[2]); // 第3位进位
assign C[5] = G[4] | (P[4] & C[3]); // 第4位进位
assign C[6] = G[5] | (P[5] & C[4]); // 第5位进位
assign C[7] = G[6] | (P[6] & C[5]); // 第6位进位
assign co = G[7] | (P[7] & C[6]); // 第7位进位
(注意):C[i]表示的是第i位是否需要进位
显然,我们是可以将 C[i] 的内容直接套到 C[i + 1] 里面去的,这样就不需要在知道 C[i] 后才能知道,当然,也不可能说八个电路全都用并联法去连接,咱的逻辑单元本来就不富裕,要是
到后期发现逻辑单元不够用了将其改成8路串联甚至是一种优化
因此我们将电路改为4路并联2路串联,就变成了一下这种形式:
wire [7:0] C;
assign C[0] = cin;
assign C[1] = G[0] | (P[0] & C[0]);
assign C[2] = G[1] | (P[1] & G[0]) | (P[1] & P[0] & C[0]);
assign C[3] = G[2] | (P[2] & G[1]) | (P[2] & P[1] & G[0]) | (P[2] & P[1] & P[0] & C[0]);
// 4位组间进位(关键优化点)
wire C4 = G[3] | (P[3] & G[2]) | (P[3] & P[2] & G[1]) | (P[3] & P[2] & P[1] & G[0]) |
(P[3] & P[2] & P[1] & P[0] & C[0]);
assign C[4] = C4;
assign C[5] = G[4] | (P[4] & C4);
assign C[6] = G[5] | (P[5] & G[4]) | (P[5] & P[4] & C4);
assign C[7] = G[6] | (P[6] & G[5]) | (P[6] & P[5] & G[4]) | (P[6] & P[5] & P[4] & C4);
// 最终进位和结果
assign co = suben ^ (G[7] | (P[7] & G[6]) | (P[7] & P[6] & G[5]) |
(P[7] & P[6] & P[5] & G[4]) | (P[7] & P[6] & P[5] & P[4] & C4));
至于这个加法器是如何兼顾减法操作的,若不清楚建议跳转至这篇文章,查看最下面的
部分,有详细解答:自动售货机
• 算数核心实现
module ALUCore(
input [7:0] op1,
input [7:0] op2,
input [2:0] sel, // 扩展为3位操作码
output reg [7:0] result,
output reg cf
);
// 加法器接口
reg [7:0] adder_op1;
reg [7:0] adder_op2;
reg suben;
wire [7:0] adder_sum;
wire adder_cout;
Adder u1(adder_op2, adder_op1, suben, adder_sum, adder_cout);
always @(*) begin
case(sel)
// 算术运算
3'b000, 3'b001: begin // 加法/减法
// 汇编代码其实是左源右目标,所以反过来放
adder_op1 = op1;
adder_op2 = op2;
suben = sel[0];
result = adder_sum;
cf = adder_cout;
end
3'b010: begin // 左移
adder_op1 = 8'b0;
adder_op2 = 8'b0;
suben = 1'b0; // 左移不需要减法
cf = 1'b0; // 左移不产生进位
// if(adder_op1[7] | adder_op1[6] | adder_op1[5] | adder_op1[4] | adder_op1[3]) begin
// result = 8'b0; // 左移后全零
// end else begin
result = op2 << op1;
// end
end
3'b011: begin // 右移
adder_op1 = 8'b0;
adder_op2 = 8'b0;
suben = 1'b0; // 左移不需要减法
cf = 1'b0; // 左移不产生进位
// if(adder_op1[7] | adder_op1[6] | adder_op1[5] | adder_op1[4] | adder_op1[3]) begin
// result = 8'b0; // 右移后全零
// end else begin
result = op2 >> op1;
// end
end
// 逻辑运算
3'b100: begin // 与(AND)
adder_op1 = 8'b0;
adder_op2 = 8'b0;
suben = 1'b0;
result = op1 & op2;
cf = 1'b0; // 逻辑运算清零CF
end
3'b101: begin // 或(OR)
adder_op1 = 8'b0;
adder_op2 = 8'b0;
suben = 1'b0;
result = op1 | op2;
cf = 1'b0;
end
3'b110: begin // 非(NOT)
adder_op1 = 8'b0;
adder_op2 = 8'b0;
suben = 1'b0;
result = op2 ^ 8'hFF; // 取反
cf = 1'b0;
end
3'b111: begin // 异或(XOR)
adder_op1 = 8'b0;
adder_op2 = 8'b0;
suben = 1'b0;
result = op1 ^ op2;
cf = 1'b0;
end
endcase
end
endmodule
功能表如下:
| 操作码 (sel) | 运算类型 | 功能描述 | 操作数处理 (op1, op2) |
结果 (result) |
进位标志 (cf) |
备注 |
|---|---|---|---|---|---|---|
3'b000 |
算术运算 | 加法 (op1 + op2) |
adder_op1 = op1adder_op2 = op2 |
adder_sum |
adder_cout |
直接调用加法器 |
3'b001 |
算术运算 | 减法 (op1 - op2) |
adder_op1 = op1adder_op2 = op2suben = 1 |
adder_sum |
suben ^ adder_cout |
补码减法,借位取反 |
3'b010 |
移位运算 | 左移 (op2 << op1) |
忽略加法器输入 | op2 << op1 |
1'b0 |
移位位数由 op1 指定 |
3'b011 |
移位运算 | 右移 (op2 >> op1) |
忽略加法器输入 | op2 >> op1 |
1'b0 |
逻辑右移 |
3'b100 |
逻辑运算 | 与 (op1 & op2) |
忽略加法器输入 | op1 & op2 |
1'b0 |
按位与 |
3'b101 |
逻辑运算 | 或 (op1 | op2) |
忽略加法器输入 | op1 | op2 |
1'b0 |
按位或 |
3'b110 |
逻辑运算 | 非 (~op1) |
忽略加法器输入 | ~op1 |
1'b0 |
按位与非 |
3'b111 |
逻辑运算 | 异或 (op1 ^ op2) |
忽略加法器输入 | op1 ^ op2 |
1'b0 |
按位异或 |
加法和减法运算使用的是加法器实现的,因此,也只有这两个命令是需要传递 adder_op1 和 adder_op2 以及 cf 进位标识的,其他情况直接使用 传进来的 op 计算即可,但需要
注意的是,其他逻辑运算虽然不需要操控 cf、adder_op1 以及 adder_op2 了,但仍然需要给他们赋值,不然模块会将这些无法实时确定的单元设置为寄存器单元,会导致组合电路
时序化
三、寄存器组模块设计
模块设计
module reg_file(
input clk,
// 双地址总线接口
input [2:0] t_addrbus, // 目标地址总线
(* keep, preserve *)output reg [7:0] databus0, // 双向数据总线(dMDR与寄存器组之间交互线)
(* keep, preserve *)output reg [7:0] databus1, // 双向数据总线(dMDR与寄存器组之间交互线)
(* keep, preserve *)output reg [7:0] databus2, // 双向数据总线(dMDR与寄存器组之间交互线)
(* keep, preserve *)output reg [7:0] databus3, // 双向数据总线(dMDR与寄存器组之间交互线)
input [7:0] inbus // 输入数据总线(来自ALU或其他模块)
);
//------------------------------------------
// 同步写逻辑
//------------------------------------------
always @(negedge clk) begin
// 目标寄存器写入(地址有效时)
if (t_addrbus[2] == 1'b0) begin // 最高位=0时有效
case (t_addrbus[1:0]) // 低2位选择寄存器
2'd0: databus0 <= inbus; // R0
2'd1: databus1 <= inbus; // R1
2'd2: databus2 <= inbus; // R2
2'd3: databus3 <= inbus; // R3
endcase
end
end
endmodule
工作原理
寄存器组模块包含4个8位通用寄存器(R0-R3),采用多总线架构设计:
输入接口:
inbus:8位输入数据总线,接收来自ALU或其他模块的数据t_addrbus:3位目标地址总线,用于选择要写入的寄存器
输出接口:
databus0-3:4个8位双向数据总线,分别对应R0-R3寄存器的输出
核心逻辑:
- 寄存器写入操作在时钟下降沿(
negedge clk)触发 - 地址解码规则:
t_addrbus[2]为地址有效标志(0=有效)t_addrbus[1:0]选择具体寄存器:2'b00:R02'b01:R12'b10:R22'b11:R3
- 寄存器写入操作在时钟下降沿(
数据流向:
- 写入时:
inbus数据在时钟下降沿写入选定寄存器 - 读取时:寄存器值持续输出到对应的
databus上
- 写入时:
四、指令存储器模块设计
模块设计
module inst_mem(
input clk,
input [7:0] pc_addr, // 程序计数器地址输入
output reg [7:0] databus // 数据总线输出
);
// 存储器配置 - 明确为ROM
(* preserve *)(* romstyle = "M9K" *) reg [7:0] memory [0:255];
initial begin
$readmemb("program.hex", memory); // 覆盖初始化
end
// 同步读取
always @(posedge clk) begin
databus <= memory[pc_addr]; // 输出到总线
end
endmodule
工作原理
指令存储器模块是一个256×8位的只读存储器(ROM),用于存储程序指令:
输入接口:
pc_addr:8位地址输入,来自程序计数器(PC)clk:系统时钟信号
输出接口:
databus:8位数据输出总线,输出当前地址对应的指令
核心逻辑:
- 存储器在时钟上升沿(
posedge clk)同步读取数据 - 根据
pc_addr的值从ROM中取出对应地址的指令 - 取出的指令通过
databus输出
- 存储器在时钟上升沿(
初始化方式:
- 使用
$readmemb系统任务从”program.hex”文件加载程序 - 存储器被综合为FPGA的M9K存储块
- 使用
五、主存模块
整体架构
主存系统采用分层设计,由外层包装模块(DataMemoryWrap)和内层存储核心(DataMemory)组成,实现了一个16×8位的RAM存储器系统,具有双地址总线接口和灵活的数据通路控制。
外层包装模块(DataMemoryWrap)
module DataMemoryWrap(
input clk,
// 双地址总线接口
input [2:0] s_addrbus, // 源地址总线
input [2:0] t_addrbus, // 目标地址总线
(* keep, preserve *) output reg [7:0] databus, // 双向数据总线
input [7:0] inbus, // 输入数据总线
// 控制信号
input dmdr_iin // dMDR输入使能
);
// [内部信号和DataMemory实例化]
endmodule
功能特点
双地址总线接口:
s_addrbus:源地址总线(3位)t_addrbus:目标地址总线(3位)- 地址
3'b100专用于内存访问
数据通路控制:
databus:双向数据总线(8位),作为内存数据寄存器(MDR)inbus:输入数据总线(8位),来自ALU或其他模块
控制信号:
dmdr_iin:MDR输入使能信号,控制数据加载
工作原理
内存访问识别:
wire mem_as_target = (t_addrbus == 3'b100); // 内存作为写入目标 wire mem_as_source = (s_addrbus == 3'b100); // 内存作为读取源内存接口连接:
assign mem_addr = inbus; // 地址来自输入总线 assign mem_we = mem_as_target; // 写使能信号数据流控制:
always @(negedge clk) begin if (dmdr_iin) databus <= inbus; // 从总线加载到MDR else if (mem_as_source && ~mem_we) databus <= mem_dout; // 从内存读取到MDR end
内层存储核心(DataMemory)
module DataMemory(
input clk,
input [7:0] addr,
input [7:0] din,
output [7:0] dout,
input we
);
(* ramstyle = "no_rw_check, logic" *)
(* keep, preserve *)reg [7:0] memory [0:15];
// [初始化与读写逻辑]
endmodule
存储结构
存储容量:
- 16×8位RAM存储器
- 使用FPGA逻辑单元实现(
ramstyle属性指定)
初始化方式:
initial begin $readmemb("data.hex", memory); // 从文件加载初始数据 end
读写操作
写入时序:
always @(posedge clk) begin if (we) begin memory[addr[3:0]] <= din; // 同步写入 end end读取特性:
assign dout = memory[addr[3:0]]; // 异步读取
系统协作流程
内存写入操作:
- CU设置
t_addrbus=3'b100标识内存为目标 - 地址通过
inbus送入mem_addr - 数据通过
databus送入内存 - 时钟上升沿完成写入
- CU设置
内存读取操作:
- CU设置
s_addrbus=3'b100标识内存为源 - 地址通过
inbus送入mem_addr - 数据在下一个时钟下降沿锁存到
databus
- CU设置
MDR加载操作:
- 当
dmdr_iin有效时,inbus数据在时钟下降沿加载到databus
- 当
六、指令寄存器模块
模块设计
module IR (
// 系统信号
input clk,
// 双向数据总线接口
(* keep, preserve *) output reg [7:0] databus,
input [7:0] inbus, // 输入数据总线(来自ALU或其他模块)
// 控制信号
input ir_in, // 从总线加载指令(上升沿有效)
// 译码输出
(* keep *)output [3:0] opcode, // 高4位操作码 -> CU(存在3位操作码)
(* keep *)output [3:0] operand // 低4位操作数 -> CU(存在5位操作数为立即数)
);
工作原理
指令寄存器(IR)是CPU中用于暂存当前执行指令的关键组件,具有指令锁存和译码输出功能:
输入接口:
inbus:8位输入数据总线,接收来自指令存储器的指令ir_in:控制信号,上升沿有效时加载新指令
输出接口:
databus:8位双向数据总线,可输出当前指令opcode:4位操作码输出(指令高4位)operand:4位操作数输出(指令低4位)
核心逻辑:
- 在
ir_in信号的上升沿将inbus上的指令锁存到内部寄存器 - 锁存的指令持续输出到
opcode和operand供控制单元(CU)译码 - 当前指令可通过
databus输出到数据总线
- 在
关键特性
指令锁存:严格在控制信号上升沿锁存指令,确保指令稳定性
实时译码:持续输出操作码和操作数字段,减少控制单元译码延迟
双向接口:既可从总线加载指令,也可将当前指令输出到总线
位宽优化:支持4位操作码和4位操作数输出,满足基本指令集需求
典型工作流程
取指阶段:
- 指令存储器将指令送至
inbus - CU发出
ir_in脉冲信号 - 在
ir_in上升沿,指令被锁存到IR
- 指令存储器将指令送至
译码阶段:
- 锁存的指令自动分离为
opcode和operand - 控制单元根据
opcode生成控制信号 operand直接用于执行单元或地址计算
- 锁存的指令自动分离为
指令输出:
- 当需要重新发送当前指令时,IR通过
databus输出
- 当需要重新发送当前指令时,IR通过
信号时序说明
| 信号 | 触发条件 | 功能描述 |
|---|---|---|
| ir_in | 上升沿 | 锁存新指令到IR |
| opcode | 持续输出 | 当前指令的高4位操作码 |
| operand | 持续输出 | 当前指令的低4位操作数 |
| databus | 由外部控制 | 输出当前指令内容 |
设计特点
同步加载:所有指令加载操作严格同步于控制信号上升沿
低延迟译码:操作码和操作数字段持续输出,无需额外时钟周期
灵活接口:支持指令回读功能,便于实现复杂指令流程
资源优化:仅实现必要功能,保持硬件实现简洁高效
七、CU模块
这个模块应该是整个 CPU 中最重要的模块了,需要根据指令表来译码,其中指令表如下:
| 编号 | 编码(二进制) | 汇编指令 | 操作 | 备注 |
|---|---|---|---|---|
| 0 | 0000xxyy |
mov rx, ry | ry <= rx | |
| 1 | 0001xxyy |
mov rx, (ry) | (ry) <= rx | |
| 2 | 001iiiii |
mov i | r0 <= i | 隐含目的操作数r0,i的高3位补0 |
| 3 | 010iiiii |
mov (i) | r0 <= (i) | 隐含目的操作数r0,i的高3位补0 |
| 4 | 0110xxyy |
mov (rx), ry | ry <= (rx) | |
| 5 | 01110000 |
rtn | PC <= 0 | |
| 6 | 1000xxyy |
and rx, ry | ry <= rx & ry | |
| 7 | 1001xxyy |
or rx, ry | ry <= rx | ry | |
| 8 | 101000xx |
not rx | rx <= ~rx | |
| 9 | 1011xxyy |
xor rx, ry | ry <= rx ^ ry | |
| 13 | 1100xxyy |
shl rx, ry | ry <= ry << rx | |
| 11 | 1101xxyy |
add rx, ry | ry <= ry + rx | |
| 12 | 1110xxyy |
shr rx, ry | ry <= ry >> rx | |
| 12 | 1111xxyy |
sub rx, ry | ry <= ry - rx |
模块实现如下:
module CU (
// 时钟与复位
input clk,
input rst, // 全局复位信号
// 指令接口
input [3:0] opcode,
input [3:0] operand,
// 程序地址总线控制
output reg pc_reset, // PC清零
output reg pc_out, // PC地址输出使能
output reg pc_inc, // PC自增
// 指令寄存器总线控制
output reg iMDRout, // iMDR写入总线
// 寄存器组总线控制
// ALU总线控制
output reg ALUAin, // ALU数据A输入
output reg ALUBin, // ALU数据B输入
output reg ALUout, // ALU数据输出
output reg [2:0] ALUop, // ALU操作选择
// 数据存储器总线控制
output reg dmdr_iin, // dMDR和总线输入使能
output reg dmdr_iout, // dMDR和总线输出使能(数据总线输出)
output reg ir_in, // 从总线加载指令(上升沿有效)
output reg imm_out, // 立即数输出(低五位)
output reg [2:0] s_addrbus, // 源地址选择
output reg [2:0] t_addrbus // 目标地址选择
);
reg [2:0] cur_sta;
reg fetch1_wait; // 等待信号
parameter
FETCH1 = 3'b000,
FETCH2 = 3'b001,
EXEC1 = 3'b010,
EXEC2 = 3'b011,
EXEC3 = 3'b100,
EXEC4 = 3'b101;
// reg double_clk;
// always @(posedge clk) begin
// double_clk <= ~double_clk;
// end
always @(negedge clk) begin
case (cur_sta)
// 取指阶段
FETCH1: begin
pc_out <= 1; // PC输出至数据总线
s_addrbus <= 3'b110; // 指令存储器读取指令
t_addrbus <= 3'b111; // 无目标地址
if (fetch1_wait) begin
fetch1_wait <= 1'b0; // 取指等待信号复位
cur_sta <= FETCH1; // 保持在FETCH1状态
end else begin
cur_sta <= FETCH2; // 状态转移到FETCH2
end
end
FETCH2: begin
pc_out <= 0; // PC停止总线输出
s_addrbus <= 3'b111; // 停止指令存储器读取指令
iMDRout <= 1; // 指令数据存储器输出数据至总线
ir_in <= 1; // 指令寄存器从总线中读取指令
pc_inc <= 1; // PC自增
cur_sta <= EXEC1; // 状态转移到EXEC1
end
EXEC1: begin
pc_inc <= 0; // PC停止自增
iMDRout <= 0; // 指令数据存储器停止输出数据至总线
ir_in <= 0; // 指令寄存器停止读取指令
// Start
case (opcode)
// move rx, ry step1(move)
4'b0000: begin
s_addrbus <= {1'b0, operand[3:2]}; // rx发送数据信息至数据总线
t_addrbus <= {1'b0, operand[1:0]}; // ry接受数据总线上的数据信息
end
// move rx, (ry) step1(间接寻址)
4'b0001: begin
s_addrbus <= {1'b0, operand[3:2]}; // rx发送数据信息至数据总线
dmdr_iin <= 1; // MDR接收总线数据
end
// move i step1(move)
4'b0010, 4'b0011: begin
imm_out <= 1; // IR输出立即数至数据总线
t_addrbus <= 3'b000; // R0存储立即数
end
// move (i) step1(间接寻址)
4'b0100, 4'b0101: begin
imm_out <= 1; // 立即数写入数据总线
s_addrbus <= 3'b100; // 主存根据立即数访存
end
// move (rx), ry step1(间接寻址)
4'b0110: begin
s_addrbus <= {1'b0, operand[3:2]}; // rx输出地址数据至数据总线
end
// rtn step1(rtn)
4'b0111: begin
pc_reset <= 1; // PC清零
end
// and/or/xor/add/sub/shl/shr rx ry step1(alua <= rx)
4'b1000, 4'b1001, 4'b1011, 4'b1101, 4'b1111, 4'b1100, 4'b1110: begin
s_addrbus <= {1'b0, operand[3:2]};
ALUAin <= 1;
end
default: cur_sta <= cur_sta;
endcase
cur_sta <= EXEC2; // 状态转移到EXEC2
end
EXEC2: begin
case (opcode)
// move rx, ry endstep
4'b0000: begin
s_addrbus <= 3'b111;
t_addrbus <= 3'b111;
end
// move rx, (ry) step2(move)
4'b0001: begin
dmdr_iin <= 0; // MDR停止接收总线数据
s_addrbus <= {1'b0, operand[1:0]}; // ry发送地址信息至数据总线
t_addrbus <= 3'b100; // 通过总线数据地址信息将对应内存数据存至dMDR
end
// move i endstep
4'b0010, 4'b0011: begin
imm_out <= 0; // IR停止输出立即数至数据总线
t_addrbus <= 3'b111; // R0停止存储立即数
end
// move (i) step2(move)
4'b0100, 4'b0101: begin
imm_out <= 0; // 停止立即数输出
dmdr_iout <= 1; // 主存输出数据至数据总线
t_addrbus <= 3'b000; // R0接受数据总线上的数据
s_addrbus <= 3'b100; // 停止主存地址发送数据
end
// move (rx), ry step2(move)
4'b0110: begin
dmdr_iout <= 1; // dMDR输出数据至数据总线
s_addrbus <= 3'b100; // 主存根据地址数据总线访存
t_addrbus <= {1'b0, operand[1:0]}; // ry接受数据总线上的数据
end
// rtn endstep
4'b0111: begin
pc_reset <= 0; // PC停止清零
end
// and rx ry step2(alub <= ry)
4'b1000: begin
s_addrbus <= {1'b0, operand[1:0]};
ALUAin <= 0;
ALUBin <= 1;
ALUop <= 3'b100;
end
// or rx ry step2(alub <= ry)
4'b1001: begin
s_addrbus <= {1'b0, operand[1:0]};
ALUAin <= 0;
ALUBin <= 1;
ALUop <= 3'b101;
end
// xor rx ry step2(alub <= ry)
4'b1011: begin
s_addrbus <= {1'b0, operand[1:0]};
ALUAin <= 0;
ALUBin <= 1;
ALUop <= 3'b111;
end
// not ry step2(alub <= ry)
4'b1010: begin
s_addrbus <= {1'b0, operand[1:0]};
ALUAin <= 0;
ALUBin <= 1;
ALUop <= 3'b110;
end
// shl rx, ry (alub <= ry)
4'b1100: begin
s_addrbus <= {1'b0, operand[1:0]};
ALUAin <= 0;
ALUBin <= 1;
ALUop <= 3'b010;
end
// and rx ry step2(alub <= ry)
4'b1101: begin
s_addrbus <= {1'b0, operand[1:0]};
ALUAin <= 0;
ALUBin <= 1;
ALUop <= 3'b000;
end
// shr rx ry(alub <= ry)
4'b1110: begin
s_addrbus <= {1'b0, operand[1:0]};
ALUAin <= 0;
ALUBin <= 1;
ALUop <= 3'b011;
end
// sub rx ry step2(alub <= ry)
4'b1111: begin
s_addrbus <= {1'b0, operand[1:0]};
ALUAin <= 0;
ALUBin <= 1;
ALUop <= 3'b001;
end
default: cur_sta <= cur_sta;
endcase
// 状态转移到EXEC3
cur_sta <= EXEC3;
end
EXEC3: begin
case (opcode)
// move rx, (ry) endstep
4'b0001: begin
s_addrbus <= 3'b111; // 停止源地址发送数据
t_addrbus <= 3'b111; // 停止目标地址接受数据
end
// move (i) endstep
4'b0100, 4'b0101: begin
dmdr_iout <= 0; // 主存停止输出数据至数据总线
t_addrbus <= 3'b111; // R0停止接受数据总线上的数据
s_addrbus <= 3'b111; // 停止源地址发送数据
end
// move (rx), ry endstep
4'b0110: begin
dmdr_iout <= 0; // dMDR停止输出数据至数据总线
t_addrbus <= 3'b111; // ry停止接受数据总线上的数据
s_addrbus <= 3'b111; // 停止源地址发送数据
end
// and/or/xor/add/sub/shl/shr rx ry step3 (result <= databus)
4'b1000, 4'b1001, 4'b1010, 4'b1011, 4'b1101, 4'b1111, 4'b1100, 4'b1110: begin
//clear
s_addrbus <= 3'b111;
ALUBin <= 0;
//operate
ALUout <= 1;
t_addrbus <= {1'b0, operand[1:0]}; // 计算结果写入ry
end
default: cur_sta <= cur_sta;
endcase
// 状态转移到EXEC4
cur_sta <= EXEC4;
end
EXEC4: begin
case (opcode)
// (and/or/not/xor/add/sub/shl/shr rx ry) / add/sub i endstep
4'b1000, 4'b1001, 4'b1010, 4'b1011, 4'b1100, 4'b1101, 4'b1110, 4'b1111, 4'b1100, 4'b1110: begin
ALUout <= 0;
t_addrbus <= 3'b111;
end
default: cur_sta <= cur_sta;
endcase
// 状态转移到FETCH1
cur_sta <= FETCH1;
end
endcase
if (rst) begin
cur_sta <= FETCH1; // 复位时状态机回到FETCH1
pc_reset <= 1'b0;
pc_out <= 1'b0; // PC输出停止
pc_inc <= 1'b0; // PC自增停止
iMDRout <= 1'b0; // 指令存储器输出停止
ALUAin <= 1'b0; // ALU A输入停止
ALUBin <= 1'b0; // ALU B输入停止
ALUout <= 1'b0; // ALU输出停止
ALUop <= 3'b000; // ALU操作码清零
dmdr_iin <= 1'b0; // 数据存储器输入停止
dmdr_iout <= 1'b0; // 数据存储器输出停止
ir_in <= 1'b0; // 指令寄存器输入停止
imm_out <= 1'b0; // 立即数输出停止
s_addrbus <= 3'b111; // 源地址总线置空
t_addrbus <= 3'b111; // 目标地址总线置空
fetch1_wait <= 1'b1; // 清除等待状态
end
end
endmodule
CU模块进行节拍控制,一个周期分为6个节拍,第一个节拍和第二个节拍用于取址,后面的节拍则用于指令处理
CU以输出信号为主,下面是对于每个信号的介绍,其中每一个信号在上面都介绍过,只不过在其他模块是作为输入信号来使用的,而
在CU模块中是作为输出信号来使用的,这个也是毋庸置疑的,因为正是 CU 模块来控制各个单元工作
其中 cur_sta 用于控制当前处于的节拍状态,一共有6个节拍状态,分别为 FETCH1,FETCH2,EXEC1,EXEC2,EXEC3,EXEC4
| 节拍状态 | 功能描述 |
|---|---|
| FETCH1 | 取指阶段1:PC输出指令地址到总线 |
| FETCH2 | 取指阶段2:指令存储器输出指令到总线并加载到IR |
| EXEC1 | 执行阶段1:指令译码并准备操作数 |
| EXEC2 | 执行阶段2:执行指令核心操作 |
| EXEC3 | 执行阶段3:处理指令结果 |
| EXEC4 | 执行阶段4:清理状态准备下一条指令 |
每个节拍详解如下:
| 指令类型 | FETCH1 | FETCH2 | EXEC1 | EXEC2 | EXEC3 | EXEC4 |
|---|---|---|---|---|---|---|
| move rx, ry | PC输出地址 s_addrbus=110 |
加载指令到IR PC自增 |
rx输出到总线 ry作为目标 |
无操作 | 无操作 | 无操作 |
| move rx, (ry) | PC输出地址 s_addrbus=110 |
加载指令到IR PC自增 |
rx输出到总线 MDR接收数据 |
ry输出地址 内存作为目标 |
清理总线信号 | 无操作 |
| move i (立即数) | PC输出地址 s_addrbus=110 |
加载指令到IR PC自增 |
立即数输出到总线 R0作为目标 |
无操作 | 无操作 | 无操作 |
| move (i) (内存间接) | PC输出地址 s_addrbus=110 |
加载指令到IR PC自增 |
立即数输出到总线 内存作为源 |
内存数据输出到总线 R0作为目标 |
清理总线信号 | 无操作 |
| move (rx), ry | PC输出地址 s_addrbus=110 |
加载指令到IR PC自增 |
rx输出地址到总线 | 内存数据输出到总线 ry作为目标 |
清理总线信号 | 无操作 |
| rtn | PC输出地址 s_addrbus=110 |
加载指令到IR PC自增 |
PC清零 | 无操作 | 无操作 | 无操作 |
| and/or/xor rx, ry | PC输出地址 s_addrbus=110 |
加载指令到IR PC自增 |
rx输出到ALU A输入 | ry输出到ALU B输入 设置ALU操作码 |
ALU结果输出到总线 ry作为目标 |
清理ALU输出 |
| not ry | PC输出地址 s_addrbus=110 |
加载指令到IR PC自增 |
无操作 | ry输出到ALU B输入 设置ALU操作码 |
ALU结果输出到总线 ry作为目标 |
清理ALU输出 |
| add/sub rx, ry | PC输出地址 s_addrbus=110 |
加载指令到IR PC自增 |
rx输出到ALU A输入 | ry输出到ALU B输入 设置ALU操作码 |
ALU结果输出到总线 ry作为目标 |
清理ALU输出 |
| shl/shr rx, ry | PC输出地址 s_addrbus=110 |
加载指令到IR PC自增 |
rx输出到ALU A输入 | ry输出到ALU B输入 设置ALU操作码 |
ALU结果输出到总线 ry作为目标 |
清理ALU输出 |
关键控制信号变化
取指阶段(FETCH1-FETCH2)
| 信号 | FETCH1 | FETCH2 |
|---|---|---|
| pc_out | 1 | 0 |
| s_addrbus | 110(指令存储器) | 111(无) |
| iMDRout | 0 | 1 |
| ir_in | 0 | 1 |
| pc_inc | 0 | 1 |
通用执行阶段(EXEC1-EXEC4)
| 信号 | EXEC1 | EXEC2 | EXEC3 | EXEC4 |
|---|---|---|---|---|
| ALUAin | 算术指令:1 | 0 | 0 | 0 |
| ALUBin | 0 | 算术指令:1 | 0 | 0 |
| ALUout | 0 | 0 | 算术指令:1 | 0 |
| dmdr_iin | 内存指令:1 | 0 | 0 | 0 |
| dmdr_iout | 0 | 内存指令:1 | 0 | 0 |
指令执行特点
- 统一取指周期:所有指令共享相同的FETCH1和FETCH2阶段
- 内存访问优化:内存操作需要额外节拍处理地址和数据
- 算术指令标准化:所有算术逻辑指令采用相同的三节拍执行流程
- 立即数处理:立即数移动指令可在两个节拍内完成
顶层模块
最后的任务就是将所有的模块联系起来,放在顶层模块:
代码如下:
module core8(
input clk,
input n_rst,
inout [7:0] databus, // 统一数据总线
(* keep, preserve *) // 保持信号,防止优化掉
output r00,
output r01,
output r02,
output r03,
output r04,
output r05,
output r06,
output r07,
// 串口通信接口
(* keep, preserve *)input rx, // 串口接收
(* keep, preserve *)output tx = 1 // 串口发送
);
(* keep, preserve *)wire rst = ~n_rst; // 低电平复位信号
(* keep, preserve *)wire alua_in;
(* keep, preserve *)wire alub_in;
(* keep, preserve *)wire alu_out;
(* keep, preserve *)wire[2:0] sel;
(* keep, preserve *)wire cf;
(* keep, preserve *)wire zf;
(* keep, preserve *)wire pc_inc; // 程序计数器自增
(* keep, preserve *)wire pc_jump; // 程序计数器跳转
(* keep, preserve *)wire pc_out; // 程序计数器输出到总线
(* keep, preserve *)wire pc_reset; // 程序计数器复位
(* keep, preserve *)wire[2:0] s_addrbus; // 源寄存器地址总线
(* keep, preserve *)wire[2:0] t_addrbus; // 目标寄存器地址总线
(* keep, preserve *)wire iMDRout;
(* keep, preserve *)wire dmdr_iin; // 数据存储器输入
(* keep, preserve *)wire dmdr_iout; // 数据存储器输出、
(* keep, preserve *)wire ir_in; // 指令寄存器输入
(* keep, preserve *)wire imm_out; // 立即数输出
(* keep, preserve *)wire[3:0] opcode; // 操作码
(* keep, preserve *)wire[3:0] operand; // 操作数
(* keep, preserve *)wire[7:0] cnt;
reg clk_2 = 1; // 二分频时钟输出
reg clk_4 = 1; // 四分频时钟输出
// 各部件数据总线
wire [7:0] alu_bus;
wire [7:0] pc_bus;
wire [7:0] ir_bus;
wire [7:0] reg_bus[0:3];
wire [7:0] imem_bus;
wire [7:0] dmem_bus;
(* keep, preserve *)reg [3:0] cur_send_sta = 4'b0000; // 当前发送状态寄存器
// 四分频
always @(posedge clk)
begin
clk_2 <= ~clk_2; // 反转时钟信号实现二分频
end
always @(posedge clk_2)
begin
clk_4 <= ~clk_4; // 反转时钟信号实现四分频
end
// 对databus多路复用
assign databus =
(pc_out) ? pc_bus : // 程序计数器输出
(iMDRout) ? imem_bus : // 指令存储器输出
(dmdr_iout) ? dmem_bus : // 数据存储器输出
(imm_out) ? ir_bus : // 指令寄存器输入
(alu_out) ? alu_bus : // ALU输出
s_addrbus[2] == 1'b0 ? reg_bus[{s_addrbus[1:0]}] : // 源寄存器组输出
8'bzzzz_zzzz; // 寄存器组输出
// Instantiate ALU
ALU alu_inst(
.databus(alu_bus),
.inbus(databus),
.clk(clk_4),
.alua_in(alua_in),
.alub_in(alub_in),
.sel(sel),
.cf(cf),
.zf(zf) // 零标志(ZF)
);
assign r00 = reg_bus[0][0];
assign r01 = reg_bus[0][1];
assign r02 = reg_bus[0][2];
assign r03 = reg_bus[0][3];
assign r04 = reg_bus[0][4];
assign r05 = reg_bus[0][5];
assign r06 = reg_bus[0][6];
assign r07 = reg_bus[0][7];
//----------------------------------
// 子模块实例化
//----------------------------------
// 程序计数器
PC u_PC (
.clk(clk_4),
.pc_inc(pc_inc),
.pc_jump(pc_jump),
.databus(pc_bus),
.inbus(databus), // 输入数据总线(来自ALU或其他模块)
.reset(pc_reset | rst)
);
// 通用寄存器
reg_file u_reg_file (
.clk(clk_4),
.t_addrbus(t_addrbus),
.databus0(reg_bus[0]),
.databus1(reg_bus[1]),
.databus2(reg_bus[2]),
.databus3(reg_bus[3]),
.inbus(databus)
);
// 指令存储器
inst_mem u_inst_mem (
.clk(clk_4),
.pc_addr(databus), // 从总线获取地址
.databus(imem_bus) // 输出到总线
);
// 数据存储器
DataMemoryWrap u_data_memory (
.clk(clk_4),
.s_addrbus(s_addrbus), // 源地址总线
.t_addrbus(t_addrbus), // 目标地址总线
.databus(dmem_bus), // 数据总线
.inbus(databus), // 输入数据总线(来自ALU或其他模块)
.dmdr_iin(dmdr_iin) // 输入使能
);
// 指令寄存器
IR u_IR (
.clk(clk_4),
.databus(ir_bus),
.inbus(databus), // 输入数据总线(来自ALU或其他模块)
.ir_in(ir_in),
.opcode(opcode), // 内部连接至CU
.operand(operand) // 内部连接至CU
);
// 控制单元
CU u_CU (
.clk(clk_4),
.rst(rst), // 全局复位信号
.opcode(opcode), // 直接连接IR内部信号
.operand(operand),
.pc_reset(pc_reset), // 共用复位
.pc_out(pc_out),
.pc_inc(pc_inc),
.pc_jump(pc_jump), // PC跳转
.iMDRout(iMDRout),
.ALUAin(alua_in),
.ALUBin(alub_in),
.ALUout(alu_out),
.ALUop(sel),
.cf(cf),
.zf(zf), // 零标志(ZF)
.dmdr_iin(dmdr_iin),
.dmdr_iout(dmdr_iout),
.ir_in(ir_in),
.imm_out(imm_out),
.s_addrbus(s_addrbus),
.t_addrbus(t_addrbus),
.cnt(cnt)
);
endmodule
模块架构概述
core8模块是整个8位CPU的顶层封装,负责协调所有子模块的工作,并通过统一的数据总线实现各组件间的通信。该设计采用剑桥架构,指令和数据使用不同的储存器,访存指令仅能访问数据储存器。
主要接口信号
| 信号类别 | 信号名称 | 位宽 | 方向 | 功能描述 |
|---|---|---|---|---|
| 系统信号 | clk | 1 | 输入 | 系统时钟 |
| rst | 1 | 输入 | 全局复位 | |
| 数据总线 | databus | 8 | 双向 | 统一数据总线 |
| 调试输出 | r00-r07 | 1 | 输出 | R0寄存器各位状态 |
地址总线仲裁逻辑
源地址总线(s_addrbus)控制
wire [7:0] reg_bus[0:3]; // 寄存器组输出总线
// 寄存器组输出选择
assign databus = ... s_addrbus[2] == 1'b0 ? reg_bus[{s_addrbus[1:0]}] : ...
源地址编码表:
| s_addrbus[2:0] | 选择的源设备 |
|---|---|
| 000-011 | 寄存器R0-R3 |
| 100 | 数据存储器 |
| 110 | 指令存储器 |
| 其他 | 无源设备 |
目标地址总线(t_addrbus)控制
// 在reg_file模块中实现
if (t_addrbus[2] == 1'b0) begin // 目标地址有效
case (t_addrbus[1:0])
2'd0: databus0 <= inbus; // R0
2'd1: databus1 <= inbus; // R1
2'd2: databus2 <= inbus; // R2
2'd3: databus3 <= inbus; // R3
endcase
end
目标地址编码表:
| t_addrbus[2:0] | 选择的目标设备 |
|---|---|
| 000-011 | 寄存器R0-R3 |
| 100 | 数据存储器 |
| 其他 | 无目标设备 |
数据总线仲裁逻辑
总线多路复用器设计
assign databus =
(pc_out) ? pc_bus : // 程序计数器输出
(iMDRout) ? imem_bus : // 指令存储器输出
(dmdr_iout) ? dmem_bus : // 数据存储器输出
(imm_out) ? ir_bus : // 指令寄存器立即数输出
(alu_out) ? alu_bus : // ALU输出
s_addrbus[2] == 1'b0 ? reg_bus[{s_addrbus[1:0]}] : // 寄存器组输出
8'bzzzz_zzzz; // 高阻态
总线优先级顺序
- 程序计数器输出(pc_out)
- 指令存储器输出(iMDRout)
- 数据存储器输出(dmdr_iout)
- 立即数输出(imm_out)
- ALU输出(alu_out)
- 寄存器组输出(s_addrbus选择)
- 高阻态(无设备驱动)
关键子模块交互
取指周期数据流
- CU置pc_out=1,PC地址输出到databus
- 指令存储器读取databus上的地址
- CU置iMDRout=1,指令数据输出到databus
- CU置ir_in=1,指令锁存到IR
执行周期数据流示例(MOV R0, R1)
- CU设置s_addrbus=001(选择R1)
- R1数据自动输出到databus
- CU设置t_addrbus=000(目标R0)
- 数据在时钟下降沿写入R0
设计特点
- 统一总线架构:所有数据传输通过共享的8位databus完成
- 严格时序控制:由CU精确控制各设备的输出使能
- 优先级仲裁:避免总线冲突,确保关键数据优先传输
- 同步设计:所有状态变化发生在时钟边沿
- 调试支持:提供R0寄存器各位状态输出
典型工作流程
sequenceDiagram
participant CU
participant PC
participant IMEM
participant IR
participant REG
participant ALU
participant DMEM
CU->>PC: pc_out=1
PC->>databus: 地址
CU->>IMEM: iMDRout=1
IMEM->>databus: 指令
CU->>IR: ir_in=1
IR->>CU: opcode/operand
CU->>REG: s_addrbus=src
REG->>databus: 数据
CU->>ALU: alua_in=1
ALU->>databus: 结果
CU->>REG: t_addrbus=dst
该设计通过精心设计的总线仲裁机制,在有限的硬件资源下实现了高效的指令流水线执行。
最后进行综合测试:
测试数据如下:
| 地址 | 二进制指令 | 操作码 | 操作数 | 汇编指令 | 功能描述 |
|---|---|---|---|---|---|
| 0x00 | 01001010 | 0100 | 1010 | mov (i) | R0 <= (0x0A) |
| 0x01 | 01000000 | 0100 | 0000 | mov (i) | R0 <= (0x00) |
| 0x02 | 00000001 | 0000 | 0001 | mov r0, r1 | R1 <= R0 |
| 0x03 | 00100010 | 0010 | 0010 | mov i | R0 <= 0x02 |
| 0x04 | 11110001 | 1111 | 0001 | sub r0, r1 | R1 <= R1 - R0 |
| 0x05 | 00100011 | 0010 | 0011 | mov i | R0 <= 0x03 |
| 0x06 | 11010001 | 1101 | 0001 | add r0, r1 | R1 <= R1 + R0 |
| 0x07 | 11000001 | 1100 | 0001 | shl r0, r1 | R1 <= R1 << R0 |
| 0x08 | 11100001 | 1110 | 0001 | shr r0, r1 | R1 <= R1 >> R0 |
| 0x09 | 01001010 | 0100 | 1010 | mov (i) | R0 <= (0x0A) |
测试结果如下(Quartus仿真):

MedelSim仿真:

两者仿真结果一样,注意到 Quartus 仿真时出现了bug:

展开数据可以看到结果应当为48,无需在意
附录
注意,因为第四问需要使用到移位指令以及跳转指令,CU 模块中对部分指令进行了修改,详细修改如下:
指令表:
| 编号 | 编码(二进制) | 汇编指令 | 操作 | 备注 |
|---|---|---|---|---|
| 0 | 0000xxyy |
mov rx, ry | ry <= rx | |
| 1 | 0001xxyy |
mov rx, (ry) | (ry) <= rx | |
| 2 | 001iiiii |
mov i | r0 <= i | 隐含目的操作数r0,i的高3位补0 |
| 3 | 010000yy |
jmp ry | PC <= ry | 无条件跳转 |
| 3 | 010001yy |
jc ry | PC <= ry | 进位时跳转 |
| 3 | 010010yy |
jz ry | PC <= ry | 结果为0时跳转 |
| 4 | 0110xxyy |
mov (rx), ry | ry <= (rx) | |
| 5 | 01110000 |
rtn | PC <= 0 | |
| 6 | 1000xxyy |
and rx, ry | ry <= rx & ry | |
| 7 | 1001xxyy |
or rx, ry | ry <= rx | ry | |
| 8 | 101000xx |
not rx | rx <= ~rx | |
| 9 | 1011xxyy |
xor rx, ry | ry <= rx ^ ry | |
| 13 | 1100xxyy |
shl rx, ry | ry <= ry << rx | |
| 11 | 1101xxyy |
add rx, ry | ry <= ry + rx | |
| 12 | 1110xxyy |
shr rx, ry | ry <= ry >> rx | |
| 12 | 1111xxyy |
sub rx, ry | ry <= ry - rx |
CU 部分代码修改后如下:
module CU (
// 时钟与复位
input clk,
input rst, // 全局复位信号
// 指令接口
input [3:0] opcode,
input [3:0] operand,
// 程序地址总线控制
output reg pc_reset, // PC清零
output reg pc_out, // PC地址输出使能
output reg pc_inc, // PC自增
output reg pc_jump, // PC跳转
// 指令寄存器总线控制
output reg iMDRout, // iMDR写入总线
// 寄存器组总线控制
// ALU总线控制
output reg ALUAin, // ALU数据A输入
output reg ALUBin, // ALU数据B输入
output reg ALUout, // ALU数据输出
output reg [2:0] ALUop, // ALU操作选择
input cf, // 进位标志(CF)
input zf, // 零标志(ZF)
// 数据存储器总线控制
output reg dmdr_iin, // dMDR和总线输入使能
output reg dmdr_iout, // dMDR和总线输出使能(数据总线输出)
output reg ir_in, // 从总线加载指令(上升沿有效)
output reg imm_out, // 立即数输出(低五位)
output reg [2:0] s_addrbus, // 源地址选择
output reg [2:0] t_addrbus, // 目标地址选择
output reg [7:0] cnt,
output [7:0] next_cnt
);
reg [2:0] cur_sta;
reg fetch1_wait; // 等待信号
parameter
FETCH1 = 3'b000,
FETCH2 = 3'b001,
EXEC1 = 3'b010,
EXEC2 = 3'b011,
EXEC3 = 3'b100,
EXEC4 = 3'b101;
Incrementer cnt_inc(
.op(cnt),
.next_op(next_cnt)
);
always @(negedge clk) begin
case (cur_sta)
// 取指阶段
FETCH1: begin
pc_out <= 1; // PC输出至数据总线
s_addrbus <= 3'b110; // 指令存储器读取指令
t_addrbus <= 3'b111; // 无目标地址
if (fetch1_wait) begin
fetch1_wait <= 1'b0; // 取指等待信号复位
cur_sta <= FETCH1; // 保持在FETCH1状态
end else begin
cur_sta <= FETCH2; // 状态转移到FETCH2
end
end
FETCH2: begin
pc_out <= 0; // PC停止总线输出
s_addrbus <= 3'b111; // 停止指令存储器读取指令
iMDRout <= 1; // 指令数据存储器输出数据至总线
ir_in <= 1; // 指令寄存器从总线中读取指令
pc_inc <= 1; // PC自增
cur_sta <= EXEC1; // 状态转移到EXEC1
end
EXEC1: begin
pc_inc <= 0; // PC停止自增
iMDRout <= 0; // 指令数据存储器停止输出数据至总线
ir_in <= 0; // 指令寄存器停止读取指令
// Start
case (opcode)
// move rx, ry step1(move)
4'b0000: begin
s_addrbus <= {1'b0, operand[3:2]}; // rx发送数据信息至数据总线
t_addrbus <= {1'b0, operand[1:0]}; // ry接受数据总线上的数据信息
end
// move rx, (ry) step1(间接寻址)
4'b0001: begin
s_addrbus <= {1'b0, operand[3:2]}; // rx发送数据信息至数据总线
dmdr_iin <= 1; // MDR接收总线数据
end
// move i step1(move)
4'b0010, 4'b0011: begin
imm_out <= 1; // IR输出立即数至数据总线
t_addrbus <= 3'b000; // R0存储立即数
end
// jump
4'b0100: begin
case (operand[3:2])
// ry
2'b00: begin
pc_jump <= 1; // PC跳转
s_addrbus <= {1'b0, operand[1:0]}; // 直接跳转到地址
end
// jc ry
2'b01: begin
if (cf) begin
pc_jump <= 1; // PC跳转
s_addrbus <= {1'b0, operand[1:0]}; // 直接跳转到地址
end else begin
pc_jump <= 0; // PC不跳转
end
end
// jz ry
2'b10: begin
if (zf) begin
pc_jump <= 1; // PC跳转
s_addrbus <= {1'b0, operand[1:0]}; // 直接跳转到地址
end else begin
pc_jump <= 0; // PC不跳转
end
end
endcase
end
// move (rx), ry step1(间接寻址)
4'b0110: begin
s_addrbus <= {1'b0, operand[3:2]}; // rx输出地址数据至数据总线
end
// rtn step1(rtn)
4'b0111: begin
pc_reset <= 1; // PC清零
end
// and/or/xor/add/sub/shl/shr rx ry step1(alua <= rx)
4'b1000, 4'b1001, 4'b1011, 4'b1101, 4'b1111, 4'b1100, 4'b1110: begin
s_addrbus <= {1'b0, operand[3:2]};
ALUAin <= 1;
end
default: cur_sta <= cur_sta;
endcase
cur_sta <= EXEC2; // 状态转移到EXEC2
end
EXEC2: begin
case (opcode)
// move rx, ry endstep
4'b0000: begin
s_addrbus <= 3'b111;
t_addrbus <= 3'b111;
end
// move rx, (ry) step2(move)
4'b0001: begin
dmdr_iin <= 0; // MDR停止接收总线数据
s_addrbus <= {1'b0, operand[1:0]}; // ry发送地址信息至数据总线
t_addrbus <= 3'b100; // 通过总线数据地址信息将对应内存数据存至dMDR
end
// move i endstep
4'b0010, 4'b0011: begin
imm_out <= 0; // IR停止输出立即数至数据总线
t_addrbus <= 3'b111; // R0停止存储立即数
end
// jump step2(jump)
4'b0100: begin
pc_jump <= 0; // PC停止跳转
s_addrbus <= 3'b111; // 停止源地址发送数据
t_addrbus <= 3'b111; // 停止目标地址接受数据
end
// move (rx), ry step2(move)
4'b0110: begin
dmdr_iout <= 1; // dMDR输出数据至数据总线
s_addrbus <= 3'b100; // 主存根据地址数据总线访存
end
// rtn endstep
4'b0111: begin
pc_reset <= 0; // PC停止清零
end
// and rx ry step2(alub <= ry)
4'b1000: begin
s_addrbus <= {1'b0, operand[1:0]};
ALUAin <= 0;
ALUBin <= 1;
ALUop <= 3'b100;
end
// or rx ry step2(alub <= ry)
4'b1001: begin
s_addrbus <= {1'b0, operand[1:0]};
ALUAin <= 0;
ALUBin <= 1;
ALUop <= 3'b101;
end
// xor rx ry step2(alub <= ry)
4'b1011: begin
s_addrbus <= {1'b0, operand[1:0]};
ALUAin <= 0;
ALUBin <= 1;
ALUop <= 3'b111;
end
// not ry step2(alub <= ry)
4'b1010: begin
s_addrbus <= {1'b0, operand[1:0]};
ALUAin <= 0;
ALUBin <= 1;
ALUop <= 3'b110;
end
// shl rx, ry (alub <= ry)
4'b1100: begin
s_addrbus <= {1'b0, operand[1:0]};
ALUAin <= 0;
ALUBin <= 1;
ALUop <= 3'b010;
end
// and rx ry step2(alub <= ry)
4'b1101: begin
s_addrbus <= {1'b0, operand[1:0]};
ALUAin <= 0;
ALUBin <= 1;
ALUop <= 3'b000;
end
// shr rx ry(alub <= ry)
4'b1110: begin
s_addrbus <= {1'b0, operand[1:0]};
ALUAin <= 0;
ALUBin <= 1;
ALUop <= 3'b011;
end
// sub rx ry step2(alub <= ry)
4'b1111: begin
s_addrbus <= {1'b0, operand[1:0]};
ALUAin <= 0;
ALUBin <= 1;
ALUop <= 3'b001;
end
default: cur_sta <= cur_sta;
endcase
// 状态转移到EXEC3
cur_sta <= EXEC3;
end
EXEC3: begin
case (opcode)
// move rx, (ry) endstep
4'b0001: begin
s_addrbus <= 3'b111; // 停止源地址发送数据
t_addrbus <= 3'b111; // 停止目标地址接受数据
end
// move (i) endstep
4'b0100, 4'b0101: begin
dmdr_iout <= 0; // 主存停止输出数据至数据总线
t_addrbus <= 3'b111; // R0停止接受数据总线上的数据
s_addrbus <= 3'b111; // 停止源地址发送数据
end
// move (rx), ry endstep
4'b0110: begin
dmdr_iout <= 0; // dMDR停止输出数据至数据总线
t_addrbus <= 3'b111; // ry停止接受数据总线上的数据
s_addrbus <= 3'b111; // 停止源地址发送数据
end
// and/or/xor/add/sub/shl/shr rx ry step3 (result <= databus)
4'b1000, 4'b1001, 4'b1010, 4'b1011, 4'b1101, 4'b1111, 4'b1100, 4'b1110: begin
//clear
s_addrbus <= 3'b111;
ALUBin <= 0;
//operate
ALUout <= 1;
t_addrbus <= {1'b0, operand[1:0]}; // 计算结果写入ry
end
default: cur_sta <= cur_sta;
endcase
// 状态转移到EXEC4
cur_sta <= EXEC4;
end
EXEC4: begin
case (opcode)
// (and/or/not/xor/add/sub/shl/shr rx ry) / add/sub i endstep
4'b1000, 4'b1001, 4'b1010, 4'b1011, 4'b1100, 4'b1101, 4'b1110, 4'b1111, 4'b1100, 4'b1110: begin
ALUout <= 0;
t_addrbus <= 3'b111;
end
default: cur_sta <= cur_sta;
endcase
// 状态转移到FETCH1
cur_sta <= FETCH1;
if(cnt[3] & cnt[4] & cnt[6] & cnt[7]) cnt <= 0;
else cnt <= next_cnt;
end
endcase
if (rst) begin
cur_sta <= FETCH1; // 复位时状态机回到FETCH1
pc_reset <= 1'b0;
pc_out <= 1'b0; // PC输出停止
pc_inc <= 1'b0; // PC自增停止
iMDRout <= 1'b0; // 指令存储器输出停止
ALUAin <= 1'b0; // ALU A输入停止
ALUBin <= 1'b0; // ALU B输入停止
ALUout <= 1'b0; // ALU输出停止
ALUop <= 3'b000; // ALU操作码清零
dmdr_iin <= 1'b0; // 数据存储器输入停止
dmdr_iout <= 1'b0; // 数据存储器输出停止
ir_in <= 1'b0; // 指令寄存器输入停止
imm_out <= 1'b0; // 立即数输出停止
s_addrbus <= 3'b111; // 源地址总线置空
t_addrbus <= 3'b111; // 目标地址总线置空
fetch1_wait <= 1'b1; // 清除等待状态
cnt <= 0;
end
end
endmodule
经测试,需要80ns的时钟周期才能解决延迟导致的数据异常问题,因此顶层模块添加了一个四分频(默认时钟周期为20ns)
// 四分频
always @(posedge clk)
begin
clk_2 <= ~clk_2; // 反转时钟信号实现二分频
end
always @(posedge clk_2)
begin
clk_4 <= ~clk_4; // 反转时钟信号实现四分频
end
将这部分代码加入到顶层模块,然后将各个部件的时钟信号传入换成clk_4即可