RSIC CPU的初级解密一(指令篇)
0赞CPU是一个神秘而强大的东西,在我们的生活中,几乎每个人都离不开CPU了,电脑里有CPU,手机里有CPU,尤其对于我们从事电子设计行业的人来说,CPU更是与我们密不可分,我们最开始接触的51是一种CPU,stm32是一种CPU,ARM也是CPU,原来FPGA里面集成软核CPU,现在新款的FPGA都开始走SOC路线,集成硬核CPU了。也就是说CPU几乎无处不在,与我们密不可分,可是CPU究竟是个什么东西,我估计大多数初学者估计还是云里雾里。这周在老师的指导下,我设计了一款简单的8位CPU,虽然只支持9条指令。虽然CPU比较简单,没有什么商用价值,但是已足够让我们把什么是CPU,CPU究竟是如何工作的,这几个问题弄明白。
在开始设计CPU的一两天,老师不断强调,要想设计CPU,必须先弄明白什么是CPU,他的架构是什么样的。就像前面写通讯协议,我们必须的先弄明白协议的时序,才有可能写得出正确的协议吧。所以我在写这篇博客总结这周的学习心得的时候,也打算用这样一种思路,先把CPU的架构,以及工作原理弄清楚,再考虑代码的设计。
我们知道CPU的作用就是用来执行指令的,那么在说CPU之前,我们先来细说一下指令的问题。
1、高级语言、汇编语言和指令的关系
学过编程语言的都知道,我们写的代码都需要经过编译和链接,才能够被CPU所执行。这里编译和链接的目的简单说来就是将代码翻译成CPU可执行的二进制机器码,然后将机器码放到存储器对应的位置。这样CPU通过给存储器地址来获得相应指令,程序就能跑起来了。由于我们只是想设计一个CPU来学习,并不想投入商用,所以没有设计代码编译器,所以我们就用机器码的方式直接给CPU编程。
机器码大家应该都有所耳闻了。我们在学一些高级语言的时候应该都会听老师说,你们现在多么幸福啊,写代码都用高级语言了,以前的程序猿苦逼啊,都是直接对存储器敲机器码的,那写出来就是一堆0101的二进制数啊,除了他们自己和电脑认识,谁都不认识。而且老师一般还会说,C语言可移植性好,这个CPU上写的代码,在那个CPU上也能用。那么高级语言为什么移植性好,汇编语言为什么移植性差,他们和机器码,和指令之间有什么关系。
C语言之所以可移植性好,是因为他有一套通用的标准。就像类似于以前的手机数据线一样,摩托罗拉的手机线就没法往诺基亚的手机上插,现在好了,大家都协商一致,我们都用USB。C语言也是一样,各芯片厂商要想自己的器件支持C语言开发,就必须的为用户提供一套C语言的编译软件,就像现在假设在开一个什么国际会议,习大大在上面发言,底下各国领导人要想听懂,就需要将中文翻译成自己国家的语言。所以现在假设我在keil上写一段51的C语言代码,keil能将它翻译成51对应的汇编指令,那么我把这段代码复制到IAR上,IAR也能把它翻译成MSP430对应的汇编指令。但是由于各个厂商的CPU架构和指令集不一样,实现同样功能的汇编指令肯定是不一样的。
讲到这里,我们应该已经非常清楚高级语言和汇编语言之间的区别了。那么汇编语言和机器码,也就是指令之间是一种什么关系呢?下面我把这个简易CPU的测试代码贴上来,答案就一目了然了。
指令集:
4’h0 HLT 停机
4’h1 SKZ 零转移
4’h2 ADD 加法
4’h3 AND 与运算
4’h4 XOR 运算或
4’h5 LDA 读数据
4’h6 STO 写数据
4’h7 JMP 跳转
4’h8 RTI 中断返回
上图就是CPU的一个测试代码, 我们暂且不管这段代码是什么意思,实现了什么功能。我们只要知道最右边就是所谓的机器码,中间部分就是我们的汇编语言。我们可以发现汇编语言就是一种指令的助记符,就好像7个葫芦娃,长得几乎一个样,为了让小孩子容易区分,作者就把他们弄成不同的颜色。它并没有一个统一的标准,我们只是给机器码穿上了一层衣服,他就变成汇编语言了。所以,在这个CPU中,我们是这样定义汇编助记符,但是也许到了另外一款CPU,我们就不这样定义汇编助记符了,甚至有可能,我们压根连这条指令都不支持了。这就是为什么汇编语言的可移植性很差的原因。
下面我们再来看一下保存程序指令的mif文件,毕竟我们程序是要存入ROM中才能正确运行的,所以我们就用mif文件来对ROM进行初始化。
由于没有编译器,所以就只能手敲指令了,这是第一次用机器码写代码,一想到以前的前辈们都是这样敲代码的,我就………………….
现在我们应该明白机器码,汇编语言,和高级语言中的差别了吧。下面我们再来看一下CPU的架构,看看CPU是如何如何执行这些指令的。
2、指令设计
我们在这个CPU中共设计了9条指令,当然根据需要,我们还可以增加新的指令,指令越多,CPU的功能越强大,CPU的结构也会更复杂,消耗的资源也会更多,这也是为什么个人电脑使用复杂指令集CSIC,而嵌入式设备使用的是精简指令集RSIC的一个重要原因。下面我们看一下具体的指令设计,了解了该CPU的9条指令之后,可以对照着前面的汇编程序,应该就能明白这个程序所描述的是什么意思了。
2.1 指令结构
指令长度:16比特
指令结构:4比特操作码+12比特操作数(opcode + operand)
2.2 指令编码表
Instruction 指令 |
Opcode 操作码 |
Operand 操作数 |
说明 |
|
助记符 |
2进制 |
16进制 |
||
HLT |
4’b0000 |
‘h0 |
12’b0000_0000_0000 |
停机时,CPU驱动HLT端口高电平 |
SKZ |
4’b0001 |
‘h1 |
12’b0000_0000_0000 |
为零转移 |
ADD |
4’b0010 |
‘h2 |
12’b0xxx_xxxx_xxxx |
直接寻址操作数与累加器相加 |
AND |
4’b0011 |
‘h3 |
12’b0xxx_xxxx_xxxx |
直接寻址操作数与累加器相与 |
XOR |
4’b0100 |
‘h4 |
12’b0xxx_xxxx_xxxx |
直接寻址操作数与累加器相异或 |
LDA |
4’b0101 |
‘h5 |
12’b11xx_xxxx_xxxx |
将直接寻址操作数装入累加器 |
STO |
4’b0110 |
‘h6 |
12’b11xx_xxxx_xxxx |
将累加器写入直接寻址操作数 |
JMP |
4’b0111 |
‘h7 |
12’b0xxx_xxxx_xxxx |
无条件跳转 |
RTI |
4’b1000 |
‘h8 |
12’b0000_0000_0000 |
中断返回 |
2.3 地址分配表
寻址空间名称 |
分配地址 |
起始地址 |
终止地址 |
指令存储区 |
12’h000~ 12’hBFF |
12’b0000_0000_0000 |
12’b1011_1111_1111 |
数据存储区 |
12’hC00 ~ 12’hDFF |
12’b1100_0000_0000 |
12’b1101_1111_1111 |
端口A |
12’hE01 |
12’b1110_0000_0001 |
|
保留 |
其它地址 |
注:中断向量为:12’h030;复位向量为:12’h000
2.4 指令详细设计
2.4.1 算数运算指令
指令名称:ADD
运算:直接寻址操作数与累加器相加
汇编语言格式:ADD <operand>(例如:ADD FN1)
执行描述:(<operand>)+ Acc => Acc
程序指针:PC + 2 => PC
标志位:Z
寻址方式:直接寻址
说明:加法运算指令中的12位操作数为目的操作数的12位地址,CPU将目的操作数取出,然后与累加器 相加,最后将计算结果存入累加器。出于简化目的,CPU仅设计一个标志位,既零标志位。故该指令忽略溢 出以及进位等标志。减法运算可以使用补码形式进行,同样出于简化目的,该CPU没有设置补码指令。
2.4.2 逻辑运算指令
指令名称:AND
运算:直接寻址操作数与累加器相与
汇编语言格式:AND <operand>(例如:AND FN1)
执行描述:(<operand>)And Acc => Acc
程序指针:PC + 2 => PC
标志位:Z
寻址:直接寻址
说明:指令中的12位操作数为目的操作数的12位地址,CPU将目的操作数取出,然后与累加器相加,最后 将计算结果存入累加器。
指令名称:XOR
运算:直接寻址操作数与累加器相异或
汇编语言格式:XOR <operand>(例如:XOR TEMP)
执行描述:(<operand>)Xor Acc => Acc
程序指针:PC + 2 => PC
标志位:Z
寻址:直接寻址
说明:指令中的12位操作数为目的操作数的12位地址,CPU将目的操作数取出,然后与累加器相异或,最 后将计算结果存入累加器。
2.4.3 数据访问指令
指令名称:LDA
运算:直接寻址操作数装入累加器
汇编语言格式:LDA <operand>(例如:LDA DATA1)
执行描述:(<operand>)=> Acc
程序指针:PC + 2 => PC
标志位:Z
寻址:直接寻址
说明:指令中的12位操作数为目的操作数的12位地址,CPU将目的操作数取出并存入累加器。该指令用于读 数据,故操作地址应该指向数据存储器区,既12’b1100_0000_0000至12’b1101_1111_1111范围。
指令名称:STO
运算:累加器装入直接寻址操作数
汇编语言格式:STO <operand>(例如:STO TEMP)
执行描述:Acc =>(<operand>)
程序指针:PC + 2 => PC
寻址:直接寻址
说明:指令中的12位操作数为目的操作数的12位地址,CPU将累加器存入目的操作数。该指令用于写数据, 故操作地址应该指向数据存储器区,既12’b1100_0000_0000至12’b1101_1111_1111范围。
2.4.4 转移指令
指令名称:HLT
运算:停机
汇编语言格式:HLT <operand>(例如:HLT)
执行描述:nop
程序指针:PC + 2 => PC
寻址:操作数忽略
说明:停机指令被执行时,CPU仅执行一条空指令,但其端口信号HLT将在该指令的执行期间输出高电平。 该指令用于调试程序时用硬件方式通知外围设备(如LED灯)。
指令名称:SKZ
运算:零标志转移
汇编语言格式:SKZ <operand>
例如:SKZ
JMP L1 // Z不等于1
ADD TEMP // Z等于1
执行描述:
程序指针:if (Z)
PC + 4 => PC
else
PC + 2 => PC
寻址:操作数忽略
说明:如果标志位Z=1,最近的CPU运算有零结果产生,则SKZ将使得当前PC指针加4,指向后续第2条指令 (下下条指令),否则正常执行PC指针加2,指向下一条指令。
指令名称:JMP
运算:无条件跳转
汇编语言格式:JMP <operand>(例如:JMP L1)
执行描述:
程序指针:<operand> => PC
寻址:操作数忽略
说明:CPU执行该指令后,将操作数装入PC,然后从新的PC开始执行。
2.4.5 中断返回指令
指令名称:RTI
运算:中断返回
汇编语言格式:RTI <operand>(例如:RTI)
执行描述:
程序指针:int_ret_addr=> PC
寻址:操作数忽略
说明:CPU在进入中断时(由外部中断信号线interrup_n触发),将自动将当前PC保存 到int_ret_addr,然后将PC指向中断向量。中断向量为中断服务程序的入口,因此之后CPU将开始执 行中断服务程序。中断服务程序的最后一条语句应该是中断返回RTI,CPU将保存的中断前的PC指针恢 复。ZX007的中断向量为interrupt_vector=12’h030。
3. 汇编程序解析
这个汇编程序就是为了实现一个斐波那契数列,什么是斐波那契数列?
斐波纳契(Fibonacci), 13世纪意大利数学家,其著名的兔子谜题是:有一对兔子,如果每个月生一对小兔子,而刚生下来的兔子两个月后同样每个月生一对小兔子,那么,一对兔子一年内总共能生下几对兔子?现在许多高中生都知道,这就是著名的斐波纳契序列:1,2, 3,5,8,13,21,34,…...。斐波纳契序列是最早知道的无穷数序列,但其隐含着非常深刻的自然之数的奥秘(植物叶片,花朵,天体等等)。
总结成一个公式就是:f(n) = f(n-1) + f(n-2)。我们把上面那段汇编程序的数据流向用图的形式来描述就会清楚很多。
是中断服务程序的代码,进入中断之后,
1:将累加器的值保存到STA
2:将FN2的值读到累加器
3:累加器的值保存到TEMP(FN2的值)
4:FN1的值与累加器(也就是之前FN2的值)相加
5:相加的结果保存到FN2
6:并把结果输出
7:把TEMP的值(之前FN2的值)读到累加器
8:把累加器的值保存到FN1,也就是将FN1的值更新为之前FN2的值。
9:将累加器的值与最大数LIMIT(LIMIT是斐波那契数列中的一个数)比较,看是否达到最大
10:异或的结果如果为0,那么说明没有达到最大值,经过SKZ之后执行JMP LT,跳出中断;
异或结果如果为1,说明达到了最大值,把DATA_1,DATA_2中的值赋给FN1,FN2,开始新一轮的斐波那 契数列。
以上是关于CPU指令的全部内容,在下一篇博客我再来介绍CPU的结构,以及CPU是如何取指令和执行指令的。