自己编写一个超简单的CPU
来源:电子工程世界发布时间:2023-07-141838浏览
询问 AI考虑下最简单的CPU是什么样子呢?最少,需要有读取程序(指令),并执行指令的过程。指令存放在一块内存当中,CPU每步取一条指令来执行,根据读出的指令内容,内部的状态发生转变——比如寄存器按指令要求进行运算,比如访问外部的端口(或总线)。指令是一个编码,描述这一步需要做的事情;执行指令的过程就是状态转移的过程。我实验的这个超简单CPU是这样:

上图中,PC是Program Counter,就是程序计数器,选择ROM中程序执行的地址。opr用来存放当前的指令,它的内容从ROM中读到。寄存器还有A寄存器和R0~R7寄存器,用来计算和存放结果,另外还有一个1-bit的“零"标志位zflag,是给条件转移指令用的。
当然,若只是里面的寄存器变来变去,这个CPU就没有实用价值了,所以还有一个输入端口,以及一个输出端口,用来和寄存器A交换数据。设计指令字长为8-bit,寄存器宽度也为8-bit。每条指令都是从ROM中读8-bit,可以最多有256种不同的指令,当然指令中能编码立即数,所以指令不会有那么多种。我给这个CPU设计了14条指令:

跳转指令有2条,无条件转移和Z条件转移,转移范围为5-bit相对地址,即-16~+15。带立即数指令有4条,因为指令才8-bit,立即数只好分配4-bit了。装入A寄存器的高4位或低4位,以及与A做加减法。R0~R7寄存器只能与A寄存器进行copy和比较操作。影响zflag标志的指令有位测试指令TESTB, 比较指令COMP和加减法指令。指令空间并没有用完,可以根据需要再补充指令。用Verilog语言来写这个CPU的状态转移部分:
module cpu0(clk, Iaddr, Ibus, PortI, PortO);
input clk;
output [9:0] Iaddr;
input [7:0] Ibus;
input [7:0] PortI;
output reg [7:0] PortO;
reg [9:0] pc;
reg [7:0] RA;
reg [7:0] Rn[0:7];
reg zflag;
assign Iaddr=pc;
reg [7:0] opr;
always @(posedge clk)
opr <= Ibus;
wire [1:0] opc1=opr[7:6];
wire [5:0] opx=opr[5:0];
wire [1:0] opc2=opr[5:4];
wire [3:0] imm4=opr[3:0];
wire [2:0] sel=opr[2:0];
reg branch;
always @(posedge clk) begin
pc <= pc + 1'b1; // default increment
branch <= 1'b0;
if(~branch) begin
if(opc1==2'd3)
if(opr[5] | zflag) begin
pc <= pc + {{5{opr[4]}},opr[4:0]};// jump instruction
branch <= 1'b1;
end
end
end
always @(posedge clk) begin
if(~branch) begin
if(opc1==2'd1 opc2==2'd0)
Rn[sel] <= RA;
end
end
always @(posedge clk) begin
if(~branch) begin
case(opc1)
2'd0: begin
if(opx==6'd0)
RA <= PortI;
end
2'd1: begin
if(opc2==2'd1)
RA <= Rn[sel];
end
2'd2: begin
case(opc2)
2'd0: RA[7:4] <= imm4;
2'd1: RA[3:0] <= imm4;
2'd2: RA <= RA + imm4;
2'd3: RA <= RA - imm4;
endcase
end
endcase
end
end
always @(posedge clk) begin
if(~branch) begin
if(opc1==2'd0 opx==6'd1)
PortO <= RA;
end
end
always @(posedge clk) begin
if(~branch) begin
if(opc1==2'd1) begin
case(opc2)
2'd3: zflag <= ~RA[sel];
2'd2: zflag <= (RA==Rn[sel]);
endcase
end
if(opc1==2'd2) begin
if(opc2[1])
zflag <= (RA==8'd0);
end
end
end
endmodule
除了指令所描述的寄存器的操作外,还多了一个branch寄存器和条件判断,这是做什么呢?
请注意,PC寄存器所指的是下一条要执行的指令地址(默认总是 pc <= pc + 1),但是如果遇到跳转指令,下一条指令是紧接着跳转指令的,将在下一个时钟沿上被读入opr,但是这条指令不该被执行,所以需要条件判断一下。而要跳转的位置的指令需要在PC更新之后的下一拍才能够被读入opr,这就是转移指令比普通指令要多花一个时钟周期的原因(这个CPU是两级流水线)。写测试程序了,没有编译器,汇编程序都得自己写呢。先就手写机器码吧!
module coderom(addr, data);
input [9:0] addr;
output reg [7:0] data;
always @(addr) begin
case(addr)
0 : data = 8'h80; // LOADAL 0
1 : data = 8'h90; // LOADAH 0
2 : data = 8'h01; // OUT A
3 : data = 8'hA1; // ADDA #1
4 : data = 8'h40; // MOV R0, A
5 : data = 8'h00; // IN A
6 : data = 8'h77; // TESTB A,7
7 : data = 8'h50; // MOV A, R0
8 : data = 8'hDB; // JUMPZ 4
9 : data = 8'hF8; // JUMP 2
default: data=8'h00;
endcase
end
endmodule
这个程序不干啥有价值的,就是检测到输入端口第7位为高时,循环加一计数,输出到端口点LED.顶层模块,将ROM和CPU连起来:
module cpu_top(clk, PortI, PortO);
input clk;
input [7:0] PortI;
output [7:0] PortO;
wire [7:0] rom_q;
wire [9:0] rom_addr;
cpu0 minicpu(.clk(clk),
.PortI(PortI),
.PortO(PortO),
.Ibus(rom_q),
.Iaddr(rom_addr));
coderom rom(
.addr(rom_addr),
.data(rom_q));
endmodule
推荐阅读
淘汰迷你电脑,英特尔宣布放弃 NUC 业务放弃千亿投资,富士康为何退出印度?丰田:充电10分钟能跑1200km!中科大:开发新型固态电解质,降本96%台积电回应镓、锗出口管制:不会对生产造成直接影响

如果您想经常看到我们的文章,可以进入我们的主页,点击屏幕右上角“三个小点”,点击“设为星标”。

欢迎扫码关注



新闻来源:电子工程世界,文中所述为作者独立观点,不代表icspec立场。更多精彩资讯请下载icspec App。如对本稿件有异议,请联系微信客服specltkj。

