avatar🌌
荡凌荡凌の赛博实验室

Technology change our life

醉后不知天在水,满船清梦压星河

一生一芯学习记录(F阶段)

一生一芯学习记录(F 阶段)

本文用于记录我在学习“一生一芯”项目各个阶段的具体所做内容,包含了所有的必做题和选做题,仅供参考。
正如第一节课所说:要培养自己独立解决问题的能力


F1 如何科学地提问

一、STFW、RTFM 与 RTFSC

  • STFW(Search The Friendly Web)——搜搜友好的网络
    当获得关键词后,先通过搜索引擎搜索相关信息。

  • RTFM(Read The Friendly Manual)——读读友好的手册
    如果软件或工具有官方手册,应先阅读手册;也可以去对应的 GitHub 仓库查看已有的 Issue,看看有没有类似问题。

  • RTFSC(Read The Friendly Source Code)——读读友好的源代码
    遇到问题时,尝试直接阅读源代码。

二、总结

本章介绍了“一生一芯”的基本情况,同时强调:
遇到问题时先独立思考,培养独立解决问题的能力;只有在尝试了各种方法后仍无法解决时,再去寻求帮助。
帮助别人时也要记住:授人以鱼不如授人以渔


F2 Logisim 的安装与使用

直接去 Logisim 的 GitHub 页面选择合适的版本安装即可,使用时可参考官方手册,即学习如何进行RTFM。

一、元件的功能

1. 输入元件

  • 按钮
    只有一个 1 位输出引脚。根据逻辑电平属性,按下时输出 10,弹起时输出相反的值。

  • 拨码开关
    具有多个 1 位输出引脚,数量由开关数量属性决定。
    未拨动时输出 0,拨动时输出 1

2. 输出元件

  • LED
    只有一个 1 位输入引脚。输入为 1 时显示点亮颜色,输入其他值时保持熄灭颜色。

  • 7 段数码管
    显示其 8 个 1 位输入端口的状态。根据输入值,各段分别显示为点亮颜色或浅灰色。


F3 数字逻辑基础

一、分析门电路

尝试分析以下门电路的行为和功能

门电路
ABP1P2N1N2Y
00导通导通截止截止1
01导通截止截止导通0
10截止导通导通截止0
11截止截止导通导通0

分析结论:由真值表可得Y=(A+B)',即该门电路是一个 或非门

二、或门的晶体管结构

或门的晶体管结构如下图所示

或门晶体管结构

三、两种实现所需晶体管数量对比

  • 两输入与门 + 两输入与非门
    需要 6 + 4 = 10 个晶体管

  • 三输入与非门
    仅需 6 个晶体管

四、用其他门电路搭建异或门

由异或门的逻辑式 Y=A'B+AB' 可以用与门、非门和或门设计出以下异或门。

异或门

需要 2 + 2 + 6 + 6 + 6 = 22 个晶体管

五、寻找更优的搭建方案

利用公式对异或门进行变形可以得到Y=(AB+A'B')',则异或门只需要两个或非门和一个与门,共需要 4 + 6 + 6 = 14 个晶体管,电路图如下:

更优的异或门

六、异或门的全定制电路

全定制异或门

这个全定制电路由三部分组成,最左侧是一个非门,对A取反,中间部分当A1的时候对B取反,最右侧为传输门,从而实现当输入A1时,通过中间部分实现Y=B';当输入A0时,通过右侧传输门实现Y=B的行为,即实现Y=A'B+AB'的逻辑功能。

七、设计同或门

同或门相当于对异或门取反,因此在异或门的电路基础上将最右侧的或非门改为或门就能实现同或操作。

同或门

八、同或门的全定制电路

同样在异或门的全定制电路的基础上进行修改可以得到下图所示的电路。

全定制同或门

九、八进制计数法

  • 八进制转十进制:通过每位加权进行转化,如八进制 7014 转化为十进制为 83×7+82×0+81×1+80×48^3 \times 7 + 8^2 \times 0 + 8^1 \times 1 + 8^0 \times 4
  • 十进制转八进制:类比十进制转二进制进行除八取余。
  • 八进制转二进制:每一位八进制对应三位二进制数。

十、搭建2-4译码器

根据真值表用与门和非门画出如下电路。

2-4译码器

十一、Logisim中的子电路功能

根据用户手册学习子电路的使用,简单来说就是将已经设计好的简单电路封装成模块的形式便于之后的复用以及扩展,在其他电路中可以直接调用已经封装好的模块。

十二、译码器的扩展

将2-4译码器扩展为3-8译码器需要注意到最高位输入决定输出是前四个还是后四个,因此只需要用非门和与门即可完成扩展,扩展的3-8译码器如图所示。

3-8译码器

十三、搭建七段数码管译码器

真值表如下:

ABCDabcdefg.
000011111100
100001100000
010011011010
110011110010
001001100110
101010110110
011010111110
111011100000
000111111110
100111110110
X10100000001
XX1100000001

首先利用将2-4译码器添加选通端,然后扩展为4-16译码器,之后使用或门使对应数码管点亮。

七段数码管译码器

十四、搭建七段数码管译码器(2)

若使译码器支持十六进制,只需要在之前的七段数码管译码器的真值表上进行更改,按照同样的思路搭建电路。

ABCDabcdefg.
000011111100
100001100000
010011011010
110011110010
001001100110
101010110110
011010111110
111011100000
000111111110
100111110110
010111101110
110100111110
001110011100
101101111010
011110011110
111110001110
七段数码管译码器(2)

十五、搭建编码器

搭建16-4编码器同样需要先列出真值表,之后利用或门搭建出编码器,与拨码开关和显示十六进制数字的七段数码管译码器相连,验证编码器功能。

A15A14A13A12A11A10A9A8A7A6A5A4A3A2A1A0Y3Y2Y1Y0
00000000000000010000
00000000000000100001
00000000000001000010
00000000000010000011
00000000000100000100
00000000001000000101
00000000010000000110
00000000100000000111
00000001000000001000
00000010000000001001
00000100000000001010
00001000000000001011
00010000000000001100
00100000000000001101
01000000000000001110
10000000000000001111
16-4编码器

十六、搭建4-2优先编码器

利用真值表画出卡诺图进行化简得到Y1=A3+A2Y_1 = A_3 + A_2Y0=A3+A2A1Y_0 = A_3 + A_2'A_1,之后根据逻辑式搭建电路。

A3A2A1A0Y1Y0
000100
001X01
01XX10
1XXX11
0000XX
4-2优先编码器

接下来对比4-2编码器和4-2优先编码器所需门电路个数。

  • 4-2编码器:只需要2个或门
  • 4-2优先编码器:需要1个非门、1个与门和2个或门,共4个门电路

对比结论:4-2优先编码器需要更多的门电路。

十七、优先编码器的扩展

将4-2优先编码器扩展为16-4优先编码器首先可以清楚的一点是选用4片优先4-2编码器,4片4-2优先编码器有8个输出端,而所需的输出端只有4个,因此扩展的过程主要是对输出端进行调整,调整思路则是根据不同的输入选择哪一片4-2优先编码器输出低两位以及高两位。

16-4优先编码器

十八、前导0和前导1的计数

  • 前导0:前导0的计数可以直接用优先编码器得到最高位的1在第几位,用总位数减去最高位的1所在的位数就能得到前导0的数量。
  • 前导1:前导1计数可以将原二进制数据按位取反然后计算前导0的数量,即为原二进制数前导1的数量。
  • 尾随0:将二进制数据逆向输入优先编码器,计算前导0的数量,即为原二进制数尾随0的数量。
  • 尾随1:同样采用和前导1类似的方法,将原二进制数据取反,然后按照尾随0的方法计算,即为原二进制数据尾随1的数量。

十九、搭建1位2选1选择器

当S为0时,D0可以通过与门;当S为1时,D1可以通过与门,未被选择的数据通过与门后变为0,最后通过或门将所选择的数据输出。

2选1选择器

二十、搭建3位4选1选择器

3位4选1选择器即一共4个数据,每个数据是3位的,复用选择信号进行每位的输出。

4选1选择器

二十一、搭建可切换进位计数制的七位数码管

利用前面搭建的两种七段数码管译码器,使用与门和或门就可以完成任务。

可切换进位计数制的七位数码管

二十二、搭建比较器

单纯的比较两个数是否相等是比较容易的,只需要通过同或门按位判断就行。

比较器

二十三、搭建1位全加器

全加器有3个输入:AB和来自低位的进位Cin,2个输出:S和向高位的进位Cout,列出真值表后搭建电路。

ABCinSCout
00000
01010
10010
11001
00110
01101
10101
11111
1位全加器

二十四、搭建1位全加器(2)

用半加器实现1位全加器需要2个半加器,完成A+B+CinA+B+C_{in}的功能,2个半加器的进位是或的关系,无论哪个半加器产生进位,最后都将产生进位。

1位全加器(2)

二十五、搭建4位加法器

4位加法器就是将4个1位全加器进行级联,至于显示则直接调用之前的七位数码管显示。

4位加法器

二十六、搭建4位减法器

按照4位加法器的设计思路,首先先设计1位减法器,列出真值表后搭建出1位减法器的电路,然后进行级联。

ABCinSCout
00000
01011
10010
11000
00111
01101
10100
11111
1位减法器

有了1位减法器之后就可以进行级联实现4位减法器的功能。

4位减法器

二十七、搭建4位原码加法器

原码是一种直观的编码方式, 最高位表示符号位, 0表示正数, 1表示负数, 其余位表示对应真值的绝对值。

搭建4位原码加法器需要分类进行讨论:若2个数均为正数,则直接原码利用4位加法器相加,就能得到正确的值;若两个数均为负数,则在原码利用4位加法器相加后修改所得原码的符号位为1;若为1正1负,则需要先比较绝对值的大小,取绝对值较大的数减去绝对值较小的数为结果,符号取绝对值较大的数的符号。另外,由于存在两个数绝对值相同的情况,此时需要对此时最终结果的符号进行修正。

4位原码加法器

二十八、搭建4位反码加法器

反码是另一种编码方式,它尝试解决原码加法中涉及负数的问题。具体地,对于正数和0,其表示与原码一致;对于负数,其表示为相应相反数的原码的按位取反。

因此,在搭建4位反码加法器的时候,先将反码转换为真值等价的原码,利用原码加法器后转换回反码。如果一个反码的最高位是0,那么对应的原码就是其本身;如果一个反码的最高位是1,那么其原码就是除最高位外其余位分别取反,最高位的1保留。

4位反码加法器

二十九、搭建4位反码加法器(2)

分析3位反码的计算,可以得到反码加法按照正常的行波进位加法(RCA)后将进位加在所得值上就可以得到正确的结果,于是可以得到一种简单的4位反码加法器。

4位反码加法器(2)

三十、补码

补码是现代计算机中常用的整数编码方式, 它进一步修复了反码计算错误时结果的偏差。具体地, 对于正数和0, 其表示与原码一致;对于负数,其表示为相应相反数的原码的按位取反后加1
对于n位补码,最大的数为0b011...11,对应的真值是2n112^{n-1}-1,最小的数是0b10...00,对应的真值是2n1-2^{n-1}。在补码中,最小数不能通过对某个正数“取反加1”得到。
补码的符号位可以以2n1-2^{n-1}为权来展开,从而得到补码对应的真值。

三十一、检测补码加法是否发生溢出

溢出一共有2种情况,两个正数相加得到一个负数和两个负数相加得到一个正数。直接利用全加器的真值表,容易得到溢出的逻辑式overflow=ABCin+ABCinoverflow=A'B'C_{in}+ABC_{in}'。用在4位加法器中即根据符号位的输入判断是否溢出。

ABCinSCout
00000
00111
01011
01101
10010
10100
11000
11111
含溢出判断加法器

三十二、交叉配对反相器

交叉配对反相器的结构如下图所示,根据QQ'的值可以分析出其行为。

交叉配对反相器
Qn-1Q’n-1QnQ’n说明
0011亚稳态
0101存储0
1010存储1
1100亚稳态

三十三、搭建SR锁存器

根据SR锁存器的电路结构搭建出电路图。

SR锁存器

根据输入的不同,SR锁存器的行为可以分成4种情况讨论:

  • S=1,R=0的时候,上方或非门等价于反相器,下方或非门输出恒为0,此时Q1,将SR锁存器存储的值更新为1
  • S=0,R=1的时候,下方或非门等价于反相器,上方或非门输出恒为0,此时Q0,将SR锁存器存储的值更新为0
  • S=0,R=0的时候,两个或非门都等价于反相器,SR锁存器的行为与交叉配对反相器一致,将SR锁存器存储的值保持不变。
  • S=1,R=1的时候,两个或非门的输出都为0,无法表示有效的信息,且当S=1,R=1变为S=0,R=0时会导致SR锁存器进入亚稳态,在使用时需要避免这种情况。

三十四、用与非门搭建的SR锁存器

先搭建出S’R’锁存器的电路图,然后根据输入的不同分析S’R’锁存器的行为。

S'R'锁存器

同样分成4种情况讨论:

  • S'=0,R'=1的时候,上方与非门输出恒为1,下方与非门等价于反相器,此时Q1,将S’R’锁存器存储的值更新为1
  • S'=1,R'=0的时候,下方与非门输出恒为1,上方与非门等价于反相器,此时Q0,将S’R’锁存器存储的值更新为0
  • S'=1,R'=1的时候,两个与非门都等价于反相器,S’R’锁存器的行为与交叉配对反相器一致,将S’R’锁存器存储的值保持不变。
  • S'=0,R'=0的时候,两个与非门的输出都为1,无法表示有效的信息,且当S'=0,R'=0变为S'=1,R'=1时会导致S’R’锁存器进入亚稳态,在使用时需要避免这种情况。
S’R’Q
00禁止
011
100
11保持

三十五、分析D锁存器的行为

根据电路结构图列出真值表。

DWEQ
00保持
010
10保持
111

D锁存器的行为可以分成3种情况:

  • WE=0的时候,两个与门输出的值均为0,无论此时D0还是1,D锁存器存储的值都保持不变。
  • WE=1,D=1的时候,D锁存器的行为与S=1,R=0时的SR锁存器相同,将D锁存器存储的值更新为1
  • WE=1,D=0的时候,D锁存器的行为与S=0,R=1时的SR锁存器相同,将D锁存器存储的值更新为0

三十六、搭建D锁存器

根据电路结构图搭建出D锁存器的电路图。

D锁存器

三十七、搭建带复位功能的D锁存器

要使D锁存器含有复位功能,即当复位信号R=1时,DWE信号不会对输出产生影响,也就是S=0,R=1R=1时始终不变,在D锁存器的基础上分析门电路的行为,搭建出含有复位功能的D锁存器。

含复位功能的D锁存器

三十八、用D锁存器实现位反转功能

虽然预期看到D锁存器的输出将在01之间反复变换,但是logisim在仿真一段时间之后会放弃仿真显示电路正在振荡。具体的内容在logisim的用户指南中数值传播振荡错误中有对应的描述。如果采用单步仿真,一步一步的观察输出,就能正常观测的锁存器的输出在01之间反复变换了。

三十九、D触发器

主从式D触发器的工作过程分为如下阶段:

  • 数据准备阶段:此时时钟信号clk处于低电平,故主锁存器的写使能端有效,数据信号D可从外部进入主锁存器;但由于从锁存器的写使能端无效,故数据信号无法传播到从锁存器,因而整个D触发器的输出端Q保持不变。
  • 采样阶段:当时钟信号clk的上升沿到来时,主锁存器的写使能端无效,数据信号D无法从外部进入主锁存器,D的后续变化将无法对主锁存器造成影响,从而将时钟信号上升沿到来前的外部数据D"锁"在主锁存器中。与此同时,从锁存器的写使能端开始有效,主锁存器中"锁住"的数据将传播到从锁存器,并作为整个D触发器的输出。
  • 维持阶段:此时时钟信号clk处于高电平,故主锁存器的写使能端无效,因此不受数据信号D变化的影响;从锁存器的写使能端虽然有效,但由于主锁存器保持不变,故从锁存器也保持不变,因而整个D触发器的输出端Q保持不变。

从整体上看,当时钟上升沿到来时,数据被写入D触发器,并能在后续时钟周期稳定读出该数据,符合同步电路对存储元件的要求。因此,D触发器是同步电路设计中的基本存储元件。

四十、搭建D触发器

根据主从式D触发器的结构图,可以搭建出D触发器。

D触发器的结构图
D触发器

四十一、搭建带复位功能的D触发器

若要满足当复位信号有效时,触发器中存放的值变为0的要求,只需要类比带复位功能的D锁存器,更改从锁存器的逻辑功能。

含复位功能的D触发器

四十二、用D触发器实现位翻转功能

将带复位功能D触发器的输出取反后作为输入,接入时钟信号,可以观察到D触发器的输出在01之间反复变化,且不会出现电路振荡的提示,每一个上升沿D触发器的输出翻转一次。

四十三、搭建下降沿触发的D触发器

若使D触发器下降沿触发,只需要对上升沿中CLK的逻辑部分进行改变,使主锁存器的写使能端在高电平有效,从锁存器的写使能段在低电平有效。

下降沿触发的D触发器

四十四、搭建带使能端的D触发器

若使D触发器带使能端,只需要用与门使当EN=0时,DCLK无法输入进触发器即可。

含使能端的D触发器

四十五、搭建4位寄存器

4位的寄存器就是由4个D触发器共享相同的使能信号和时钟信号,实现总体存储的功能。

4位寄存器

四十六、搭建4位计数器

4位计数器只需要将寄存器的输出端接到加法器的输入端,再将加法器的输出端接到寄存器的输入端即可,由于加法器在溢出时低4位为0000,所以不需要添加额外复位,尤其注意不要将加法器的进位输出接到寄存器的输入端,由于加法器存在竞争冒险的现象,所以会出现毛刺使寄存器意外复位。

4位计数器

四十七、设计数列求和电路

设计出求1+2+...+10结果的电路首先需要8位寄存器和加法器,因此按照之前搭建4位加法器和4位寄存器的思路搭建出8位加法器和寄存器。

8位加法器
8位寄存器

实现求和的思路则是1个寄存器负责实现每次将加数+1的过程,另1个寄存器负责暂存每次相加的结果,即用上一次求和的结果加上新的加数,从而实现累加。考虑到当加数寄存器到10的下一个上升沿求和寄存器才会将10加上去,因此在设计中使得当加数到11的时候2个寄存器使能端全部拉低。此时加法器的结果即为所求和的结果,所得结果用二进制表示应该是00110111

1+2+...+10求和电路

四十八、实现电子时钟

要实现电子时钟,首先拆分电路,从而进行模块化搭建,无论是秒还是分钟,本质上都是一个计数器,每1s秒计数器+1,每60s分计数器+1,秒计数器清零,从而完成时钟的计数部分。但是为了显示方便,秒计数器和分计数器的个位和十位分别使用1个4位寄存器和1个4位加法器进行计数,即每当个位为9的时候,驱使十位寄存器在下一个上升沿存入+1后的值,同时个位寄存器存入0。仿真时调整节拍频率为2Hz,即一个时钟周期为1s。显示部分直接用前面的7段数码管译码器分别显示个位和十位就可以实现。

电子时钟

四十九、总结

F3主要是亲手搭建一些简单的组合逻辑电路和时序逻辑电路,是对数字电路相关基础知识的学习,搭建电路时尤其需要注意组合逻辑不能接入异步复位端,因为组合逻辑电路容易出现竞争冒险的现象,导致出现毛刺,从而使电路出现异常复位的现象,且时钟信号一般不接入逻辑判断。


F4 计算机系统的状态机模型

一、指令及其编码

一条指令需要给出两个方面的信息:

  • 计算机需要处理的数据有很多,因此指令中需要指定需要处理哪些数据,这称为指令的“操作数”字段
  • 计算机处理数据的方式也有很多种,因此指令中也需要指定用何种方式处理数据,这称为指令的“操作码”字段

通用寄存器(GPR):用于处理一般数据的寄存器组。

二、存储程序

程序计数器(Program Counter, PC):用于指示当前执行到哪条指令的部件。

“存储程序”的基本思想:我们只需要将一段指令序列放在存储器中,然后让PC指向第一条指令,计算机就会自动执行这一段指令序列,永不停止。

三、一个数列求和的例子

r0r1r2r3指代4个GPR,用逗号分隔指令的操作数。假设以下的指令存放在存储器中,用于计算1+2+...+10这一数列的和,其中:前的数字表示PC,#及其后的文字表示注释。

0: li r0, 10   # 这里是十进制的10
1: li r1, 0
2: li r2, 0
3: li r3, 1
4: add r1, r1, r3
5: add r2, r2, r1
6: bner0 r1, 4
7: bner0 r3, 7

我们用(PC,r0,r1,r2,r3)的格式记录寄存器的值,这一格式也反映了处理器所处的状态。我们约定在开始的时刻,处理器的状态是(0,0,0,0,0)。以下是处理器执行前若干条指令的过程。

PC r0 r1 r2 r3
(0, 0, 0, 0, 0)   # 初始状态
(1, 10, 0, 0, 0)  # 执行PC为0的指令后, r0更新为10, PC更新为下一条指令的位置
(2, 10, 0, 0, 0)  # 执行PC为1的指令后, r1更新为0, PC更新为下一条指令的位置
(3, 10, 0, 0, 0)  # 执行PC为2的指令后, r2更新为0, PC更新为下一条指令的位置
(4, 10, 0, 0, 1)  # 执行PC为3的指令后, r3更新为1, PC更新为下一条指令的位置
(5, 10, 1, 0, 1)  # 执行PC为4的指令后, r1更新为r1+r3, PC更新为下一条指令的位置
(6, 10, 1, 1, 1)  # 执行PC为5的指令后, r2更新为r2+r1, PC更新为下一条指令的位置
(4, 10, 1, 1, 1)  # 执行PC为6的指令后, 因r1不等于r0, 故PC更新为4
(5, 10, 2, 1, 1)  # 执行PC为4的指令后, r1更新为r1+r3, PC更新为下一条指令的位置
......

接下来继续执行上述指令。

(6, 10, 2, 3, 1)    # 执行PC为5的指令后, r2更新为r2+r1, PC更新为下一条指令的位置
(4, 10, 2, 3, 1)    # 执行PC为6的指令后, 因r1不等于r0, 故PC更新为4
(5, 10, 3, 3, 1)    # 执行PC为4的指令后, r1更新为r1+r3, PC更新为下一条指令的位置
(6, 10, 3, 6, 1)    # 执行PC为5的指令后, r2更新为r2+r1, PC更新为下一条指令的位置
(4, 10, 3, 6, 1)    # 执行PC为6的指令后, 因r1不等于r0, 故PC更新为4
(5, 10, 4, 6, 1)    # 执行PC为4的指令后, r1更新为r1+r3, PC更新为下一条指令的位置
(6, 10, 4, 10, 1)   # 执行PC为5的指令后, r2更新为r2+r1, PC更新为下一条指令的位置
(4, 10, 4, 10, 1)   # 执行PC为6的指令后, 因r1不等于r0, 故PC更新为4
(5, 10, 5, 10, 1)   # 执行PC为4的指令后, r1更新为r1+r3, PC更新为下一条指令的位置
(6, 10, 5, 15, 1)   # 执行PC为5的指令后, r2更新为r2+r1, PC更新为下一条指令的位置
(4, 10, 5, 15, 1)   # 执行PC为6的指令后, 因r1不等于r0, 故PC更新为4
(5, 10, 6, 15, 1)   # 执行PC为4的指令后, r1更新为r1+r3, PC更新为下一条指令的位置
(6, 10, 6, 21, 1)   # 执行PC为5的指令后, r2更新为r2+r1, PC更新为下一条指令的位置
(4, 10, 6, 21, 1)   # 执行PC为6的指令后, 因r1不等于r0, 故PC更新为4
(5, 10, 7, 21, 1)   # 执行PC为4的指令后, r1更新为r1+r3, PC更新为下一条指令的位置
(6, 10, 7, 28, 1)   # 执行PC为5的指令后, r2更新为r2+r1, PC更新为下一条指令的位置
(4, 10, 7, 28, 1)   # 执行PC为6的指令后, 因r1不等于r0, 故PC更新为4
(5, 10, 8, 28, 1)   # 执行PC为4的指令后, r1更新为r1+r3, PC更新为下一条指令的位置
(6, 10, 8, 36, 1)   # 执行PC为5的指令后, r2更新为r2+r1, PC更新为下一条指令的位置
(4, 10, 8, 36, 1)   # 执行PC为6的指令后, 因r1不等于r0, 故PC更新为4
(5, 10, 9, 36, 1)   # 执行PC为4的指令后, r1更新为r1+r3, PC更新为下一条指令的位置
(6, 10, 9, 45, 1)   # 执行PC为5的指令后, r2更新为r2+r1, PC更新为下一条指令的位置
(4, 10, 9, 45, 1)   # 执行PC为6的指令后, 因r1不等于r0, 故PC更新为4
(5, 10, 10, 45, 1)  # 执行PC为4的指令后, r1更新为r1+r3, PC更新为下一条指令的位置
(6, 10, 10, 55, 1)  # 执行PC为5的指令后, r2更新为r2+r1, PC更新为下一条指令的位置
(7, 10, 10, 55, 1)  # 执行PC为6的指令后, 因r1等于r0, PC更新为下一条指令的位置
(7, 10, 10, 55, 1)  # 执行PC为7的指令后, 因r3不等于r0, PC更新为7
......              # 循环执行PC为7的指令

执行到最后,处理器一直循环执行PC为7的指令,4个GPR中的数据保持不变,数列求和的结果保存在r2中。

四、计算10以内的奇数之和

参考前面计算数列之和的指令,可以写出以下程序:

0: li r0, 11   
1: li r1, 1
2: li r2, 0
3: li r3, 2
4: add r2, r2, r1
5: add r1, r1, r3
6: bner0 r1, 4
7: bner0 r3, 7

则处理器状态变化如下:

PC r0 r1 r2 r3
(0, 0, 1, 0, 0)      # 初始状态
(1, 11, 1, 0, 0)     # 执行PC为0的指令后, r0更新为11, PC更新为下一条指令的位置
(2, 11, 1, 0, 0)     # 执行PC为1的指令后, r1更新为1, PC更新为下一条指令的位置
(3, 11, 1, 0, 0)     # 执行PC为2的指令后, r2更新为0, PC更新为下一条指令的位置
(4, 11, 1, 0, 2)     # 执行PC为3的指令后, r3更新为2, PC更新为下一条指令的位置
(5, 11, 1, 1, 2)     # 执行PC为4的指令后, r2更新为r2+r1, PC更新为下一条指令的位置
(6, 11, 3, 1, 2)     # 执行PC为5的指令后, r1更新为r1+r3, PC更新为下一条指令的位置
(4, 11, 3, 1, 2)     # 执行PC为6的指令后, 因r1不等于r0, 故PC更新为4
(5, 11, 3, 4, 2)     # 执行PC为4的指令后, r2更新为r2+r1, PC更新为下一条指令的位置
(6, 11, 5, 4, 2)     # 执行PC为5的指令后, r1更新为r1+r3, PC更新为下一条指令的位置
(4, 11, 5, 4, 2)     # 执行PC为6的指令后, 因r1不等于r0, 故PC更新为4
(5, 11, 5, 9, 2)     # 执行PC为4的指令后, r2更新为r2+r1, PC更新为下一条指令的位置
(6, 11, 7, 9, 2)     # 执行PC为5的指令后, r1更新为r1+r3, PC更新为下一条指令的位置
(4, 11, 7, 9, 2)     # 执行PC为6的指令后, 因r1不等于r0, 故PC更新为4
(5, 11, 7, 16, 2)    # 执行PC为4的指令后, r2更新为r2+r1, PC更新为下一条指令的位置
(6, 11, 9, 16, 2)    # 执行PC为5的指令后, r1更新为r1+r3, PC更新为下一条指令的位置
(4, 11, 9, 16, 2)    # 执行PC为6的指令后, 因r1不等于r0, 故PC更新为4
(5, 11, 9, 25, 2)    # 执行PC为4的指令后, r2更新为r2+r1, PC更新为下一条指令的位置
(6, 11, 11, 25, 2)   # 执行PC为5的指令后, r1更新为r1+r3, PC更新为下一条指令的位置
(7, 11, 11, 25, 2)   # 执行PC为6的指令后, 因r1等于r0, PC更新为下一条指令的位置
(7, 11, 11, 25, 2)   # 执行PC为7的指令后, 因r3不等于r0, PC更新为7

同样的,执行到最后,处理器一直循环执行PC为7的指令,4个GPR中的数据保持不变,数列求和的结果保存在r2中。

五、指令集架构的状态机模型

指令集架构(ISA):ISA的本质是一系列规范,这些规范通常记录在相应的手册中,它们定义了一台模型机的功能和行为。所谓的模型机就是一台只存在于思维中的机器,我们只讨论其具备的功能和行为,而不讨论其具体实现。

状态机的定义包含以下部分:

  • 状态集合:S={S1,S2,}S = \{S_1, S_2, \dots\}
  • 激励事件:EE
  • 状态转移规则:next:S×ESnext: S \times E \to S
    • 描述每个状态在不同的激励事件下的次态(next state),即二元函数next=(S,E)next = (S, E)给出了在状态SS下接收到激励事件EE后的次态
  • 初始状态:S0SS_0 \in S

六、一个简单的C语言示例

将代码输入编辑器后运行,出现结果。

#include <stdio.h>

int main(){
  int x = 1;
  int y = 2;
  int z = x + y;
  printf("z = %d\n", z);
  return 0;
}
运行结果

七、用C语言实现数列求和

用C语言计算1+2+3+...+10,考虑以下C程序:

#include <stdio.h>

/* 1 */ int main() {
/* 2 */   int sum = 0;
/* 3 */   int i = 1;
/* 4 */   do {
/* 5 */     sum = sum + i;
/* 6 */     i = i + 1;
/* 7 */   } while (i <= 10);
/* 8 */   printf("sum = %d\n", sum);
/* 9 */   return 0;
/* 10*/ }

根据上述程序的行为,前若干条语句的执行过程如下:

PC sum i
(2, ?, ?)    # 初始状态
(3, 0, ?)    # 执行PC为2的语句后, sum更新为0, PC更新为下一条语句的位置
(5, 0, 1)    # 执行PC为3的语句后, i更新为1, PC更新为下一条语句的位置(第4行无有效操作, 跳过)
(6, 1, 1)    # 执行PC为5的语句后, sum更新为sum + i, PC更新为下一条语句的位置
(7, 1, 2)    # 执行PC为6的语句后, i更新为i + 1, PC更新为下一条语句的位置
(5, 1, 2)    # 执行PC为7的语句后, 由于循环条件i <= 10成立, 因此重新进入循环体
......

继续执行上述代码:

(6, 3, 2)      # 执行PC为5的语句后, sum更新为sum + i, PC更新为下一条语句的位置
(7, 3, 3)      # 执行PC为6的语句后, i更新为i + 1, PC更新为下一条语句的位置
(5, 3, 3)      # 执行PC为7的语句后, 由于循环条件i <= 10成立, 因此重新进入循环体
(6, 6, 3)      # 执行PC为5的语句后, sum更新为sum + i, PC更新为下一条语句的位置
(7, 6, 4)      # 执行PC为6的语句后, i更新为i + 1, PC更新为下一条语句的位置
(5, 6, 4)      # 执行PC为7的语句后, 由于循环条件i <= 10成立, 因此重新进入循环体
(6, 10, 4)     # 执行PC为5的语句后, sum更新为sum + i, PC更新为下一条语句的位置
(7, 10, 5)     # 执行PC为6的语句后, i更新为i + 1, PC更新为下一条语句的位置
(5, 10, 5)     # 执行PC为7的语句后, 由于循环条件i <= 10成立, 因此重新进入循环体
(6, 15, 5)     # 执行PC为5的语句后, sum更新为sum + i, PC更新为下一条语句的位置
(7, 15, 6)     # 执行PC为6的语句后, i更新为i + 1, PC更新为下一条语句的位置
(5, 15, 6)     # 执行PC为7的语句后, 由于循环条件i <= 10成立, 因此重新进入循环体
(6, 21, 6)     # 执行PC为5的语句后, sum更新为sum + i, PC更新为下一条语句的位置
(7, 21, 7)     # 执行PC为6的语句后, i更新为i + 1, PC更新为下一条语句的位置
(5, 21, 7)     # 执行PC为7的语句后, 由于循环条件i <= 10成立, 因此重新进入循环体
(6, 28, 7)     # 执行PC为5的语句后, sum更新为sum + i, PC更新为下一条语句的位置
(7, 28, 8)     # 执行PC为6的语句后, i更新为i + 1, PC更新为下一条语句的位置
(5, 28, 8)     # 执行PC为7的语句后, 由于循环条件i <= 10成立, 因此重新进入循环体
(6, 36, 8)     # 执行PC为5的语句后, sum更新为sum + i, PC更新为下一条语句的位置
(7, 36, 9)     # 执行PC为6的语句后, i更新为i + 1, PC更新为下一条语句的位置
(5, 36, 9)     # 执行PC为7的语句后, 由于循环条件i <= 10成立, 因此重新进入循环体
(6, 45, 9)     # 执行PC为5的语句后, sum更新为sum + i, PC更新为下一条语句的位置
(7, 45, 10)    # 执行PC为6的语句后, i更新为i + 1, PC更新为下一条语句的位置
(5, 45, 10)    # 执行PC为7的语句后, 由于循环条件i <= 10成立, 因此重新进入循环体
(6, 55, 10)    # 执行PC为5的语句后, sum更新为sum + i, PC更新为下一条语句的位置
(7, 55, 11)    # 执行PC为6的语句后, i更新为i + 1, PC更新为下一条语句的位置
(8, 55, 11)    # 执行PC为7的语句后, 由于循环条件i <= 10不成立, 因此PC更新为下一条语句的位置
(9, 55, 11)    # 执行PC为8的语句后, 终端输出“sum = 55”,PC更新为下一条语句的位置
(10, 55, 11)   # 执行PC为9的语句后, PC更新为下一条语句的位置
(END, 55, 11)  # 执行PC为10的语句后,从main函数返回,程序执行结束

八、数字电路的状态机模型

根据数字电路的相关知识,我们可以归纳出数字电路的状态机模型:

  • 状态集合:在数字电路中,只有时序逻辑电路才能存储信息,因此一个状态是时序逻辑元件所存储的具体信息,二全体状态的集合SS则是时序逻辑元件所能存储信息的所有组合。
  • 激励事件集合:既然时序逻辑元件表征了数字电路的状态,而时序逻辑元件的内部状态可以通过其输入端改变,也即,让时序逻辑元件的状态发生变化的,其实是组合逻辑电路输出的信号,因此组合逻辑电路就是这个状态机的激励事件。
  • 状态转移规则:时序逻辑元件的状态具体应如何变化,是由组合逻辑电路的具体逻辑决定的。
  • 初始状态:即电路在复位时,时序逻辑元件的状态。

分析F3中所搭建的简单数列求和电路的电路状态变化过程:

r1 r2 
(0, 0)  # 初始状态
(1, 0)
(2, 1)
(3, 3)
(4, 6)
(5, 10)
(6, 15)
(7, 21)
(8, 28)
(9, 36)
(10, 45)
(11, 55) # 最终状态

九、编译 = 将C程序翻译成指令序列

编译器需要完成以下工作:

  • 将C程序的状态翻译成ISA的状态,也即
    • 将C程序的PC对应到ISA的PC
    • 将C程序的变量对应到ISA的GPR或内存
  • 将C程序的状态转移规则翻译成ISA的状态转移规则,也即,将语句翻译成指令序列

结合之前数列求和的C程序和相应的指令序列,可以发现C程序中的sumi实际上分别对应了ISA中的r2r1,C程序的循环判断条件对应了ISA中根据r1r0的关系进行PC的跳转。

编译器将每一行C程序的代码都翻译成若干行汇编代码,即指令代码,使得计算机可以理解所需要进行的工作。

十、CPU设计 = 根据ISA设计数字电路

CPU设计需要完成以下工作:

  • 用数字电路的状态实现ISA的状态,也即,用时序逻辑电路实现PC,GPR和内存
  • 用数字电路的状态转移规则实现ISA的状态转移规则,也即,用组合逻辑电路实现指令的功能

十一、程序,ISA和CPU之间的联系

程序,ISA和CPU之间的联系:
1、根据ISA手册的功能描述,画一张CPU的结构图 → 处理器微结构设计
2、根据结构图设计具体的电路 → 逻辑设计
3、开发程序 → 软件编程
4、将程序翻译成ISA手册中描述的指令序列 → 编译
5、在CPU上执行程序 = 用程序编译出的指令序列控制CPU电路进行状态转移

十二、总结

F4阶段主要学习一些关于计算机模型的知识,深入的体会指令和程序的运行过程,并且将程序,指令集和电路联系在一起,为后续设计CPU打下知识基础。


F5 支持数列求和的简单处理器

首先明确需要实现的指令集sISA的细节:

  • PC位宽为4位,初值为0
  • GPR有4个,位宽均为8位
  • 支持add,li和bner0指令

一、只有一条指令的sCPU

无论是执行什么指令,其步骤都是类似的,有一个叫“指令周期”的概念专门描述这些步骤:
1、取指(fetch):根据当前PC,在存储器中找到一条指令
2、译码(decode):看这条指令具体是什么指令,操作数是哪些
3、执行(execute):对操作数进行处理,必要时更新指定的目的寄存器
4、更新PC:让PC指向下一条指令

二、取指

存储器和寄存器都可以存储信息,但存储器还支持寻址,也即,存储器中的内容按顺序进行排布,给出一个地址,存储器可以读出该地址对应的内容。我们可以将存储器看成一个有比特构成的矩阵,矩阵的每一行称为一个存储字,地址就是行的编号,行的数量也称为存储器的深度。通常一个存储字包含多位数据,其位宽称为存储器的宽度。通常用深度×宽度深度 \times 宽度表示一个存储器的规格。

从功能上划分,存储器可以分为只读存储器(Read-Only Memory,ROM)和随机访问存储器(Random Access Memory,RAM),前者不支持写入,而后者支持。

要实现ROM并存放数列求和的指令序列,且通过PC寄存器取出指令,首先考虑ROM的规格,由于每条指令都是8位,因此ROM的位宽为8;而且一共需要8条指令,即ROM的深度为8。PC寄存器则是3位的寄存器。PC寄存器的输出作为多路选择器的选择端,多路选择器的数据端存储指令序列,根据选择端的值取出对应的指令。

实现取指功能

三、译码

译码的工作进一步分为操作码译码和操作数译码,前者是根据指令的操作码来识别指令的功能,后者是从指令的编码中识别出相依的操作数。

四、执行

GPR通常包含多个寄存器,一次访问通常只访问其中的几个寄存器,因此GPR也应该支持寻址。GPR电路的本质也是一个存储器,不过GPR需要作为目的寄存器被指令写入,因此GPR是一个支持写入的存储器,即RAM。

由于GPR位宽为8位,一共4个GPR,因此需要一个8位的4选1多路选择器,对GPR写入则当选中对应的寄存器和写使能同时有效时才写入,因此需要对地址进行译码。

实现GPR功能

五、实现仅支持li指令的sCPU

更新PC的操作十分简单,对PC寄存器加1即可,可以通过数字电路中的计数器来实现。

将上述所有实现li的指令周期涉及的各个部件连接起来,让sCPU执行前几条li指令,最终应该使得GPR中r0=10r1=0r2=0r3=1

仅支持li指令的sCPU

六、添加add指令

对于所有指令来说,取指和更新PC这两个步骤的行为都一样,因此可以直接复用之前实现的取指逻辑。

对于译码,由于add指令的行为和li指令不同,因此有必要识别当前取出的是什么指令。为了实现这一点,需要检查指令的opcode字段,如果是00,则是add指令,如果是10,则是li指令。要识别输入的取指,最适合的电路就是译码器。由于opcode只有2位,我们可以使用一个2-4译码器,它输出的独热码可以指示当前指令属于何种指令,这样的译码器称为指令译码器。这组独热码通常作为控制信号,用于控制部分电路如何工作,从而支持不同指令的功能。

考虑完操作码后,还需要考虑操作数。和li指令相比,add指令除了需要写入rd,还需要读出rs1rs2作为源操作数,需要同时通过两个读端口和一个写端口访问GPR。

读出源操作数后,add指令的行为是将两数相加,然后将加法结果写入rd寄存器。前者可以通过加法器实现,但后者写入GPR时,GPR的wdata端口已经被li指令的结果占用。考虑到指令opcode编码的唯一性,一条指令不可能既是li指令又是add指令,因此可以根据指令的类别对写入GPR的数据进行选择,如果当前指令是加法指令,就选择加法器的结果,否则选择li指令的立即数。可以通过多路选择器实现这样的选择功能,将写入GPR的两个数据来源作为多路选择器的数据端,然后让译码时独热码当中合适的信号连接到多路选择器的选择端,来控制多路选择器按照指令类型选出正确的数据,即可实现上述功能。

根据上文可以为sCPU添加add指令,除了根据上文内容外,GPR的读取端口的多路选择器需要添加使能端,使得仅在add指令下读取GPR。实现后继续执行求和数列程序的几条add指令,最终GPR的状态应该是r0=10r1=1r2=1r3=1

添加add指令

七、添加bner0指令

最后是bner0指令。为了识别bner0指令,我们可以复用指令译码器的功能。至于操作数,除了指令中的rs2addr,还有一个隐含的R[0]。由于bner0指令中rs2字段的位置和add指令中rs2字段的位置一样,因此可以复用add指令中读出rs2寄存器的逻辑。但bner0还需要读出R[0],因此可以把0作为GPR的raddr1端口的输入。不过这个端口已经被add指令的rs1占用,但也同样可以通过多路选择器解决问题。

读出源操作数后,bner0指令需要比较两数是否相等,这可以通过比较器来实现。若比较结果不相等,需要将PC更新为addr字段。换句话说,只有当前指令为bner0指令,且比较结果不相等,才将PC更新为addr字段,其余情况应将PC更新为PC加1。同样可以借助多路选择器对PC寄存器的输入端进行选择。

最后,bner0指令不会写入GPR,因此需要将GPR的wen置为无效。

根据上文,继续为sCPU添加bner0指令,注意读出端的多路选择器,如果需要使能端,则需要使多路选择器在禁用时得到一个确定的值,否则比较器将会产生不定态,导致PC无法正常工作。即使不需要使能端,也需要使得在初始状态时使多路选择器输出一个确定的值。最终实现的结果是PC始终为7,GPR的r2存放求和结果55

添加bner0指令

八、和数列求和电路进行对比

同样是实现数列的求和,通过加法器和寄存器实现需要更少的部件,容易搭建,但是它的可复用性比较差,当需要计算其他内容时需要对整个电路进行修改。而通过sCPU进行计算,虽然电路搭建比较复杂,但是需要计算其他内容时修改电路比较方便,只需要对指令部分进行修改,便于复用。

九、重新审视CPU

上文讨论如何添加指令时,都是先分析指令的预期行为,然后根据指令的行为在数据流动的方向上依次添加所需的部件。数据在CPU中流动的路径和路径上的相关部件,称为CPU的数据通路,在上文的讨论中,属于数据通路的部件有GPR,加法器,存储器,比较器等。

当多条指令的数据通路出现冲突时,通常需要引入一些额外的电路,来控制数据如何流动,使得每条指令都能完成符合ISA规范的操作。这些额外的电路属于CPU的控制逻辑,其中决定控制逻辑行为的信号称为控制信号。事实上,设计数据通路和控制信号,正是CPU设计中最关键的两个步骤。

十、计算10以内的奇数之和

在前面F4中我们已经编写出了一段程序用于计算10以内的奇数之和。直接引用过来转换成指令序列,由于指令与我们所设计的sCPU支持的指令是相同的,所以只需要更改sCPU存储的指令部分即可。

0: li r0, 11   
1: li r1, 1
2: li r2, 0
3: li r3, 2
4: add r2, r2, r1
5: add r1, r1, r3
6: bner0 r1, 4
7: bner0 r3, 7

转换后的指令序列:

0: 10001011   
1: 10010001
2: 10100000
3: 10110010
4: 00101001
5: 00010111
6: 11010001
7: 11011111

修改电路后运行仿真,结果最终实现的结果应该是是PC始终为7,GPR的r2存放求和结果25

计算10以内的奇数之和

十一、添加新指令

首先先定义out rs指令。

| 01 | 0000 | rs | out rs指令,输出R[rs]

之后为sCPU添加out指令,由于out指令只涉及读取部分,为了防止其他读取部分会影响显示,额外添加一个读取端口,由out指令控制使能,由于读取指令结束后会发生指令跳转,因此利用一个高电平触发的寄存器存储读取的结果进行显示,这样就不会导致数码管显示的计算结果发生闪烁。最后修改数列求和程序后进行仿真,观察是否正常显示求和结果。

0: li r0, 10   
1: li r1, 0
2: li r2, 0
3: li r3, 1
4: add r1, r1, r3
5: add r2, r2, r1
6: bner0 r1, 4
7: out r2
8: bner0 r3, 8
添加out指令

十二、能设计一条让10个数相加的指令吗?

虽然设计一条让10个数相加的指令可以简化直接计算,但是实际上并没有必要,由于计算机并不只是简单计算固定数量的数,不方便将指令进行复用。如果像这样为了提升效率而创造指令集,指令集中指令的数目将会非常多,显得十分冗余,由于需要计算10个数会导致操作数的长度增加,从而使整个指令的长度增加。且一次性计算10个数相加的指令在电路设计上难以设计,需要同时读取10个寄存器的内容。

十三、总结

在F5阶段,通过搭建简单的sCPU去实现数列的求和,通过与前面数字电路直接搭建数列求和进行对比,更加深入的了解指令的作用,并且对F4阶段所学习的计算机的相关内容有了更深一步的理解。


F6 功能完备的迷你RISC-V处理器

一、通过RTFM初步了解RISC-V指令集

通过查阅RISC-V手册的目录,在手册的第二章介绍了RV32I指令集的相关内容。阅读后提取出以下关键内容:

  • 1、PC寄存器为32位
  • 2、GPR共有32个,从x0-x31,每个GPR均为32位,其中寄存器x0的所有位始终为0
  • 3、RV32I中的R[0]通过硬件连接使得每位均为0,而sISA的R[0]是GPR
  • 4、指令编码为32位,指令有4种基本格式(R/I/S/U)
  • 5、在指令的基本格式中用5位来表示1个GPR,5位二进制数可以产生32个地址,可以完整覆盖全部的GPR
  • 6、add指令采用R格式,具体格式如下:
     31     25 24 20 19 15 14 12 11 7 6      0
    | 0000000 | rs2 | rs1 | add | rd | opcode |
  • 7、还有一种指令集称为RV32E,它是RV32I的精简版本,将整数寄存器的数量减少到16个

了解RISC-V指令集的一些细节之后,给出minirv这一ISA的规范,具体如下:

  • PC初值为0
  • GPR数量与RV32E中定义的GPR数量一致
  • 支持如下8条指令:addaddiluilwlbuswsbjalr
  • 其他的ISA细节与RV32I相同

二、RTFM(2)

通过查阅RISC-V手册,找到addi指令的编码和相应的功能描述。

addi指令将符号扩展的12位立即数与寄存器rs1相加,忽略算数溢出,保留结果的低XLEN位。MV rd,rs1这条汇编伪指令将被编译成addi rd,rs1,0实现。

addi指令的编码格式如下:

 31       20 19 15 14 12 11 7 6       0
| imm[11:0] | rs1 | 000 | rd | 0010011 |

三、RTFM(3)

一个RISC-V硬件线程拥有一个统一的,按字节寻址的地址空间,大小为2XLEN2^{XLEN}字节,用于所有内存访问。内存中的一个字定义为32位(4字节)。相应的,半字为16位(2字节),双字为64位(8字节),四字为128位(16字节)。该内存地址空间是环形的,因此地址2XLEN12^{XLEN}-1处的字节与地址0处的字节相邻。因此,硬件进行的内存地址计算会忽略溢出,而是按照模2XLEN2^{XLEN}进行回绕。

简单来说就是在ISA层面上,PC寄存器是按照字节寻址的,每个PC对应一个字节,但是实际电路中指令的存储不一定是按照一个字节存储的,比如ROM的宽度是32位,那么如果顺序读指令,每一条指令后就需要PC+4

四、RTFM(4)

通过查阅RISC-V手册,找到jalr指令的编码和相应的功能描述。

jalr指令使用I型编码。通过将经符号扩展的12位I型立即数与寄存器rs1的数据相加,然后将最低有效位清零得到目标地址。将下一条指令的地址(PC+4)存入寄存器rd。如果不需要保存返回地址的话,可以选择寄存器x0作为目标寄存器。

jalr指令的编码格式如下:

 31       20 19 15 14 12 11 7 6       0
| imm[11:0] | rs1 | 000 | rd | 1100111 |

五、实现两条指令的minirv处理器

首先实现PC寄存器的功能,由于所设计存放指令的ROM为32位,而PC寄存器按字节寻址,因此顺序执行后PC+4,即PC寄存器的低两位始终为0,由PC寄存器的剩余位数作为地址,根据后续的程序大小选择合适的地址宽度即可。且因为包含jalr指令,PC寄存器有额外的输入,采用多路选择器进行实现。

接着实现译码功能,直接采用比较器判断是否为addi指令或jalr指令。

之后实现addijalr指令,都需要读取rs1寄存器的值,使用加法器和位扩展器将扩展后的立即数与rs1的值相加,但是addi指令需要将结果写入rd寄存器,而jalr指令将结果的最低位清零得到PC寄存器的跳转地址。

GPR则需要两个给定的GPR子模块,根据指令中寄存器的最高位判断需要哪一个子模块,低4位决定选择具体哪一个寄存器。

基本搭建完成后将程序存入ROM,先通过指令集的状态机理解程序的功能,之后检查处理器运行结果是否符合预期。

00000000 <_start>:
   0:	01400513          	addi	a0,zero,20
   4:	010000e7          	jalr	ra,16(zero) # 10 <fun>
   8:	00c000e7          	jalr	ra,12(zero) # c <halt>

0000000c <halt>:
   c:	00c00067          	jalr	zero,12(zero) # c <halt>

00000010 <fun>:
  10:	00a50513          	addi	a0,a0,10
  14:	00008067          	jalr	zero,0(ra)

解析指令可得a0是寄存器rs10ra是寄存器rs1。则根据程序执行结果如下:

PC  r0  r1  r10
(0   0   0   0)      # 初始状态
(4   0   0   20)
(16  0   8   20)
(20  0   8   30)
(8   0   8   30)
(12  0   12  30)
(12  0   12  30)
(12  0   12  30)
...
两条指令的minirv处理器

六、测试addi指令

编写一些立即数为负数的addi指令,放在ROM中执行,测试符号扩展功能是否正常实现。

ff800093            addi  r1,zero,-8
00a08113            addi  r2,r1,10
ff608193            addi  r3,r1,-10
00608213            addi  r4,r1,6
01000067            jalr  zero,16(zero)

运行结果应该是r1=-8r2=2r3=-18r4=-2

测试addi指令

七、实现完整的minirv处理器

add指令读取rs1寄存器和rs2寄存器的值相加后存入rd寄存器。addi指令的编码格式如下:

 31     25 24 20 19 15 14 12 11 7 6       0
| 0000000 | rs2 | rs1 | 000 | rd | 0110011 |

lui指令将指令中的立即数左移12位,在低12位补0形成32位立即数,然后存入rd寄存器。lui指令的编码格式如下:

 31        12 11 7 6       0
| imm[31:12] | rd | 0110111 |

仿照之前设计sISA中的add指令和li指令实现minirv中的add指令和lui指令。add指令需要两个GPR的读端口。对于GPR的数据输入端,可以用编码器对四个指令进行编码后作为多路选择器的输入端,由于Logisim中没有现成的编码器,但是在PC某一值时,有且仅有一条指令执行,因此可以用优先编码器替代。电路搭建完成后编写如下指令测试:

000010b7            lui   4096,r1
00002137            lui   8192,r2
002081b3            add   r3,r1,r2
00c00067            jalr  zero,12(zero)

测试结果应该是r1=4096r2=8192r3=12288

完整的minirv处理器

剩余的4条指令都是访存指令,都需要访问存储器。访存操作分为读内存和写内存两种,由于写内存指令需要写入内存,因此需要采用RAM。

在Logisim中实例化RAM后,按照以下配置修改其中的一些关键参数:

  • Address Bit Width(地址位宽):根据后续的程序的大小和你的理解进行配置
  • Data Bit Width(数据位宽):32
  • Enables(启动方式):Use byte enables(使用字节启用)
  • Ram type(RAM型):non volatile(非易失型)
  • Use clear pin(使用清除销):No(否)
  • Trigger(触发器):Rising Edge(上升沿)
  • Asynchronous read(异步读取):Yes(是)
  • Read write control(读写控制):Use byte enables(使用字节启用)
  • Data bus implementation(数据总线实现):Separate data bus for read and write(用于读写的独立数据总线)

八、RTFM(5)

加载和存储指令用于寄存器和内存之间传输数据。加载指令采用 I 型编码格式,存储指令采用 S 型编码格式。有效地址通过将寄存器rs1与经符号扩展的12位偏移量相加得到。加载指令将内存中的值复制到寄存器rd中;存储指令将寄存器rs2的值复制到内存中。

lw指令从内存中加载一个32位值存入寄存器rdlw指令的编码格式如下:

 31       20 19 15 14 12 11 7 6       0
| imm[11:0] | rs1 | 010 | rd | 0000011 |

lbu指令从内存中加载一个8位值,将其零扩展为32位后存入rdlbu指令的编码格式如下:

 31       20 19 15 14 12 11 7 6       0
| imm[11:0] | rs1 | 100 | rd | 0000011 |

sw指令将寄存器rs2的值存储到内存中。sw指令的编码格式如下:

 31       25 24 20 19 15 14 12 11       7 6       0
| imm[11:5] | rs2 | rs1 | 010 | imm[4:0] | 0100011 |

sb指令将寄存器rs2的低8位存入内存。sb指令的编码格式如下:

 31       25 24 20 19 15 14 12 11       7 6       0
| imm[11:5] | rs2 | rs1 | 000 | imm[4:0] | 0100011 |

九、实现完整的minirv处理器(2)

首先实现lw指令和sw指令,指令的译码按照之前译码的方式采用比较器和与门实现。由于指令中rs1rs2的位置和之前相同,因此之前的读取线可以直接复用。lw指令写入寄存器的部分可以继续扩展之前的优先编码器和多路选择器。按照加载指令格式解析出读取RAM的地址即可。sw指令同样按照存储指令的格式解析出存入RAM的地址,在字节写使能部分由于是全字节写入,因此每个字节写使能端均应该为1,当然,提前用或门预留其余端口用于选择正确的字节写使能。RV32I指令集规定按字节寻址,因此需要将解析出的RAM地址右移两位得到实际写入RAM的地址。

电路搭建完成后,提前在RAM中存放一些数据,之后编写简单的指令序列测试电路功能是否正常。

00402083      lw    r1,4(zero)
00102423      sw    r1,8(zero)
00800113      addi  8,zero,r2
00012183      lw    r3,0(r2)
00112223      sw    r1,4(r2)
01400067      jalr  zero,20(zero)

预先将RAM中地址为1的数据写入4,测试结果应该是r1=4r2=8r3=4,RAN中地址为2的数据是4,地址为3的数据是4

完整的minirv处理器(2)

十、实现完整的minirv处理器(3)

接着实现lbu指令,由于lbu指令只读出一个字节,而RAM的一个地址对应4个字节,因此需要考虑读地址的转换,并且根据指令中的地址判断读出哪个字节,之后写回目的寄存器。

lbu指令中的地址为a,则对应的RAM地址是将a地址的低2位清零所得到的地址,和处理lw指令中的地址步骤一样。若a的低2位是00,则读的是第0字节;若为01,读的是第1字节;同理,若为10,读的是第2字节;若为11,读的是第3字节。利用Logisim中的位选择器可以轻松的完成这一步。

搭建完成后同样测试功能是否正常,测试过程为:先在RAM为1的地址存放一个4字节的数据0x12345678,并通过lw指令读出,确认读出结果为0x12345678。然后通过若干条lbu指令分别从内存地址4567中读出数据,我们预期这些数据分别读出0x78(对应地址4),0x560x340x12(对应地址7)。测试所用的指令序列如下:

00402083      lw    r1,4(zero)
00404103      lbu   r2,4(zero)
00504183      lbu   r3,5(zero)
00604203      lbu   r4,6(zero)
00704283      lbu   r5,7(zero)
01400067      jalr  zero,20(zero)
完整的minirv处理器(3)

十一、实现完整的minirv处理器(4)

最后实现sb指令,读取rs2部分和sw指令相同,只是在写入内存部分需要根据指令地址选择正确的字节写使能这个功能直接用比较器判断指令地址的低2位即可,并且对写入数据进行调整。写入数据的修改是将从寄存器读出的数据的低8位复制成32位数据,利用多路选择器和优先编码器区分sb指令和sw指令写入内存的数据。

测试过程如下:在RAM为1的地址放置一个4字节的数据0x12345678,并通过lw指令读出它,确认读出结果为0x12345678,然后通过四条sb指令分别往内存地址4567中写入0x90(对应地址7),0xab0xcd0xef(对应地址4);最后再次通过lw指令读出新数据,预期读出结果为0x90abcdef。 测试所用指令序列如下:

00402083      lw    r1,4(zero)
09000113      addi  r2,zero,0x90
0ab00193      addi  r3,zero,0xab
0cd00213      addi  r4,zero,0xcd
0ef00293      addi  r5,zero,0xef
002003a3      sb    r2,7(zero)
00300323      sb    r3,6(zero)
004002a3      sb    r4,5(zero)
00500223      sb    r5,4(zero)
00402083      lw    r1,4(zero)
02800067      jalr  zero,40(zero)
完整的minirv处理器(4)

十二、在minirv处理器上执行C程序

将电路中的ROM替换为Logisim中的ROM组件,然后调整ROM和RAM的地址位宽为20位,并且利用寄存器和加法器额外实现一个每个周期加1的64位计数器,用于观察程序运行了多久,当计数器的值超过给出的周期数,即可认为执行结束。

分别加载并运行mem.hexsum.hex。运行指定时间后,检查处理器的状态,若PC位于halt函数附近,且a0寄存器为0,则说明程序运行正确。两个程序的预期运行时间均为6000周期。即运行mem.hex时预期PC寄存器的值在1218121c1220附近,运行sum.hex时预期PC寄存器的值在224228附近,rs100

运行mem.hex的结果
运行sum.hex的结果

十三、为minirv处理器添加图形显示功能

首先实例化一个屏幕组件,实例化后可以看到,RGB Video包含以下端口:时钟,复位,写使能,X坐标,Y坐标,以及待写入像素数据。不难得知,其功能是:在写使能有效时,将像素数据更新到组件的X-Y坐标处。接下来我们需要考虑的是,处理器如何通过指令来向RGB Video组件写入像素。

对处理器来说,类似RGB Video这样的部件称为外部设备,简称“外设”。事实上,如何访问外设属于ISA规范的其中一部分。具体到RISC-V中,访问外设是通过“内存映射I/O”方式来进行的。这种方式的本质是,根据访存地址的范围来决定处理器的访问对象是内存还是外设。

具体到RGB Video中,根据上述配置,一个像素数据占3字节。但为了方便处理,我们可以将其视为4字节。这样,整个屏幕所存储的像素数据大小为256×256×4B=256KB。我们约定屏幕上每个像素数据对应的地址是连续的,因此我们需要为整个屏幕的像素数据划分出一段连续的地址区域,例如[0x20000000,0x20040000)。当访存指令的目标地址落在这个范围之内,相应指令将访问RGB Video,而不是访问RAM。

为了实现“根据访存地址范围就决定访问对象”的功能,你需要在处理器的数据通路上添加一个地址译码器模块,它输入访存地址,输出两个控制信号isVGAisMem。其中当访存地址落在上述区间时,isVGA有效,否则isMem有效。接下来就可以通过这两个控制信号来控制相应组件的访问行为了。

RGB Video来说,其写入操作需要收到isVGA信号的控制,也即,只有当前指令为store指令,且地址落在上述区间时,才能写入RGB Video。为了简化处理,我们约定程序只能通过sw指令来将像素写入屏幕,因此sw的待写入数据可以直接连接到RGB Video。最后还要考虑X坐标和Y坐标的连接。事实上,因为像素数据对应的地址是连续的,给出一个落在RGB Video范围内的地址,我们很容易得到这个像素的X坐标和Y坐标,例如地址0x20000000对应第0行第0列的像素,而地址0x20000408对应第1行第2列的像素。

对RAM来说,其写操作也需要受到isMem信号的控制,从而避免在访问RGB Video时错误地往RAM中写入。

具体来说,添加屏幕模块后,利用比较器,与门和译码器根据地址范围输出isMemisVGA信号,X坐标和Y坐标则直接将地址右移两位后取低8位为Y坐标,15-8位为X坐标。之后加载vga.hex程序,等待程序运行结束后,RGB Video组件显示“一生一芯”logo。当然,可以调整仿真时的时钟频率,加快程序的运行。

在测试过程中,发现所生成的图像很接近logo但又不是,那么这种情况是解析坐标时出现的问题,实际上,如地址0x20000408对应第1行第2列的像素,它的X坐标是2,Y坐标是1。将X坐标和Y坐标的连线互换,就能解决这个问题。最终成功显示“一生一芯”的logo。

添加图形显示功能

十四、小结

本章通过学习如何查阅手册,获取想要的内容,对RISC-V指令集有了简单的了解,之后通过亲手搭建一个minirv的处理器,最终能够实现显示“一生一芯”的logo。

一生一芯学习记录(E阶段)
Valaxy v0.28.4 驱动|主题-Yunv0.28.4