从零开始打造我的计算机系统【处理器设计】

从零开始打造我的计算机系统

处理器设计

某种意义上而言如今的CPU模型的设计是为了更方便的设计操作系统,比如操作系统的安全是内核空间和用户空间的概念出现,进程模型使TSS模型出现,虚拟内存使MMU出现等等,如今我们要设计一个CPU的时候,实际上是考虑两点:

一:它是否能很好的实现C语言。

二:它是否能很好的实现一个操作系统。

只要这两点满足,它已经成为一个可以用的CPU模型。

在我想从零开始设计我的计算机以来,我就设计了几种CPU模型,当然,最开始选择的是支持虚拟内存的CPU,并且支持一些系统指令,然后我发现高估了自己的编程能力,所以决定从最简单实模式CPU做起。仍然保持它的可扩充性,以便以后升级。

然而仍然保留原始的几个要求:

1.不考虑兼容性。

2.不考虑性能。

3.不考虑移植。

4.仅仅逻辑上可行。

仍按照前面的想法,我将阐释一下几个关键点:分支,调用,中断和异常。

现在我们按照一个简单的计划进行:

单核实模式CPU

单核保护模式CPU

多核保护模式CPU

2.1关键点

2.1.1 分支

分支分为条件分支和无条件分支,在条件分支中,共支持一下几种条件,或许用C语言的方式更好表达些,那就是等于跳转,不等于跳转,小于跳转,大于跳转,小于等于跳转,大于等于跳转。这些跳转模式,除了==!=之外,都支持无符号数比较和有符号数比较。(注意:不是无符号数和有符号数比较。)

条件分支都是相对PC偏移的。

无条件分支采用简单的寄存器跳转指令,抛弃了立即数跳转,只是因为我们用软件模拟,不必要考虑性能,而这样以来,编程人员的压力大大减轻。所以一个无条件分支指令看起来是这个样子。

LOAD $S0, 32位立即数。

JUMP $S0

LOAD指令看起来已经超过了4字节也就是32位大小,怎么实现它曾经在我的脑海里思索了好久,目前而言,有两种方法。

最重要的是,我们知道,当程序分支的时候,我们仅仅修改的是PC指,不进行堆栈保存。

附录:怎么设计一条加载32位立即数的指令?

最开始实现的时候,是把LOAD当作伪指令,由两条指令ORI以及LUI指令组成,ORI指令是或立即数,LUI是把一个十六位立即数加载到寄存器的高16位。

所以一条LOAD $R0,#HELLO会被汇编成以下两条指令。

ORI $R0,#HELLO_16位。

LUI $R0,#HELLO_16位。

这样看起来是解决了问题,但是怎么对这两条指令重载一直是我心中的疑问?

有两个问题:第一,LOAD指令不是仅仅能加载地址。

LOAD $R0,10000可能就是指10000而已,可以代表绝对地址,也可以不代表。

在苦苦思索好久之后,我认为汇编器是无法决定这样的一条指令是否引用了内存,除非根据上下文,但是那样又太复杂了。

于是我加上了一条汇编器规则:

规则 $0_0 引用内存地址必须使用标号,否则会产生一个运行时错误。

这样一来,LOAD $R0,#10000LOAD $R0,label是两条截然不同意义的指令,汇编器知道第一个10000不需要重载,而label可能值也是10000,但是需要重载。

第一个问题解决之后,第二个问题迎面而来,怎么对一个32位的地址进行重载而不会引起错误。在这里假设HELLO的基地址变了,怎么跟着改变呢?

ORI $R0,#HELLO_16位。

LUI $R0,#HELLO_16位。

上下文信息已经丢失了,假如ORILUI都加上重定位常量的话,就出错了。【在设计过程中我一度抛弃了LOAD指令,决定直接使用JMP LABEL指令,这样的话整个LABEL26位,又由于PC总是四字节对齐,所以其实寻址可以达到2^28次方,也就是256M的程序,我认为我永远也写不了那么大的程序,所以似乎是可以接受的,但是我后来又放弃了这个想法。】

解决方法如下,上面两条指令已经被标记为可以重载,而ORI会和重定位常量的低16位相加,而LUI会和重定位常量的高16位相加。

如上看来这是一个完美的解决方案。

还有另外一种方法,就是在整个32位的指令集里,设计一条64位的指令,无须细说你也想到了它的样子,LOAD $R0,#32位立即数里,LOAD $R0占用低内存地址的32位,而32位立即数占用高32位,看起来是这个样子:

 

目前采用第一种解决方案,也就是分散成两条指令。

2.1.2 调用

程序调用有两条指令,一条是call,一条是ret,现在我们来想一想当程序调用时保存了什么,仅仅保存返回地址到栈顶。对了,就是这样。

无论任何时候我们不考虑一个调用是否是叶调用,因为我们不考虑性能。调用call的指令形式和JUMP类似,比如call $0,我在设计的时候就决定了这是一个全局的跳转,所以我们不再关心局部跳转和远程跳转,那是Intel的事。

C语言调用时堆栈如图所示,为了更方便的实现C语言,我们添加SP寄存器和FP寄存器。

 

2.1.3 中断和异常

不管怎么说,以前我对中断和异常有误解,中断结束后,程序的流程应该回到原来的地方接着执行,然而异常却不是这样,比如说一个除以0的异常,很明显,程序不应该继续下去。但是从另外一个角度来看,缺页异常,当异常结束后,很明显应该回到原来的地方继续执行。

这么说起来,中断其实是一种异常,而异常是一个更广泛的概念。

中断向量号支持0-255,其中0-31留着系统使用,但是并不是每个都被使用了。

中断向量号

中断功能

解释

0

除以0异常

默认停止程序

1

单步调试

Flag置位

2

不可屏蔽中断

 

3

设置断点

 

中断可以嵌套吗?

答:当一个设备发生中断的时候,设备会发起一个我发生中断了的标志,在CPU指令执行结束后,CPU检测所有发生中断的设备,检测到优先级最高的那个,假如检测得到的优先级比正在执行中断子程序的中断优先级还高,或者没有中断,则执行中断过程,即保存状态寄存器和PC地址,复写中断向量到中断寄存器,假如检测到的中断优先级没有正在执行的中断程序的优先级高,则CPU简单的抛弃这些中断请求。

当设备发现中断没有被请求的时候,它们间隔一段时间再次发送中断。

当中断完成后,CPU把中断发生标志清零,代表可以发生中断了。

无论如何,一个32位的PSW足以保存这些东西了。

软中断总是可以嵌套的,假如一个中断子程序使用了一个低优先级的软中断,我们必须完成它,所以软中断可以嵌套。

2.2 寄存器布局

我们讨论一下寄存器布局吧,首先从数量来说,我们会设置3232位的寄存器,编号从$0$31,另外4个浮点型寄存器,编号从$32$35(在我想不到更好的符号之前暂定)。

然后我们首先讨论那些重要的寄存器并进行分配编号。

重要的寄存器:PC,BP,SP,IDTR,PSW

编号

描述

0

zero寄存器,任何时候读取都是0,任何时候写它都无效,任何时候用它间接寻址产生错误。

1

中断表基址寄存器,允许你挪动表地址,不过暂时设为0

2

程序状态寄存器,目前只放置三个状态,TFtrace flag),中断许可,和中断发生,高八位存放中断发生时中断向量,再八位存放优先级数。

3

Sp栈顶寄存器

4

bp 帧寄存器

5

PC

6-31

通用寄存器(随着以后的扩展,通用寄存器会越来越少。)

 

2.3 指令集

应当无疑问,所有指令遵守opcode rs,rt,rd指令的格式,这是rs = rt (opcode) rd,我们遵守这一规定,就是目的寄存器在前。(请忽略rs,rd,rt原来的含义吧。)

一共有

l 7条整数运算指令

l 4条浮点运算指令

l 9条逻辑运算指令

l 11条分支指令

l 10条数据传输指令(只有他们和内存关联吧。)

l 7条系统指令

从我设置指令的格式来说,无论如何也不会超过64条吧?假如超过的话,又要改架构了。

opcode(6)

5

5

5

11

 

 

CPU指令集

算术运算指令

整数运算指令(7

0

rs

rt

rd

保留

add rs,rt,rd

1

rs

rt

imm

addi,rs,rt,imm

10

rs

rt

rd

保留

sub rs,rt,rd

100

rs

rt

rd

保留

mul rs,rt,rd

101

rs

rt

rd

保留

div rs,rt,rd

110

rs

rt

rd

保留

mod rs,rt,rd

浮点数运算指令(4

111

rs

rt

rd

保留

fadd rs,rt,rd

1000

rs

rt

rd

保留

fsub rs,rt,rd

1001

rs

rt

rd

保留

fmul rs,rt,rd

1010

rs

rt

rd

保留

fdiv rs,rt,rd

逻辑运算指令(9

1011

rs

rt

rd

保留

and rs,rt,rd

1100

rs

rt

rd

保留

or rs,rt,rd

1101

rs

rt

保留

保留

not rs,rt

1110

rs

rt

rd

保留

xor rs,rt,rd

1111

rs

rt

imm

andi rs,rt,imm

10000

rs

rt

imm

ori rs,rt,imm

10001

rs

rt

imm

xori rs,rt,imm

10010

rs

rt

shamt

sll rs,rd,shamt

10011

rs

rt

shamt

slr rs,rd,shamt

比较转移指令(11)

10100

rs

rt

lable

le rs,rt,lable

10101

rs

rt

lable

ga rs,rt,lable

10110

rs

rt

lable

nle rs,rt,lable

10111

rs

rt

lable

nga rs,rt,lable

11000

rs

rt

lable

leu rs,rt,lable

11001

rs

rt

lable

gau rs,rt,lable

11010

rs

rt

lable

nleu rs,rt,lable

11011

rs

rt

lable

ngau rs,rt,lable

11100

rs

rt

lable

eq rs,rt,lable

11101

rs

rt

lable

ueq rs,rt,lable

11110

rs

保留

jmp rs

 

数据传输指令(10

11111

rs

rt

 

 

mov rs,rt

100000

rs

rt

rd

保留

lword rs,rt,rd

100001

rs

rt

rd

保留

sword rs,rt,rd

100110

rs

保留

imm

lui rs,imm

100111

f1

rt

rd

保留

ldoub $f1,$r2,$r3

101000

f1

rt

rd

保留

sdoub $f1,$r2,$r3

系统指令(7)

101001

rs

保留

call rs

 

101010

保留

ret

 

101011

rs

保留

push rs

 

101100

rs

保留

pop rs

 

101101

保留

halt

 

101110

imm

int imm

 

101111

保留

IRET

 

2.4 系统架构

本系统,采用一下参数:

l 一个屏幕,用800x600的窗口,采用EGE娘的EGE库。

l 一块内存,内存暂定32MB

l 一块硬盘,用文件作为模拟,64MB

l 一个键盘

l 一个交叉编译器

l 一个程序加载器

屏幕的缓冲区映射到内存的0xb8000处(为什么?)采用80X25模型,每行80个,共25行。所以需要80字节X25=2000字节。最终为b8d70-1

键盘的缓冲区映射到内存的0xb8d70处的16字节。接外部中断16

那么,开始吧。

 

 

 

posted @ 2014-02-16 17:10  李可以  阅读(2056)  评论(0编辑  收藏  举报