学习视频:01_数字电路_从零搭建计算机引导_哔哩哔哩_bilibili
第1章数字电路基础
1.引言
数字电路是现代科技和工程领域中不可或缺的基础。从计算机系统到通信设备,从家庭电子产品到工业自动化,数字电路无处不在,影响着我们的生活和工作。本章节旨在向读者介绍数字电路的基本概念、原理和应用,为后期学习单片机开发打基础。
1.2 二进制数据表达
1.2.1 二进制简介
二进制是一种数字表示法,它使用两个不同的数字符号,0和1,来表示数值。下面是一些二进制的基本概念:
1) 位(bit)
不同于十进制中,每个位可以用0-9表示,二进制中每位都是0或1,每个二进制位我们称之为一个bit,可以表示两种状态。位一般用b代替,例如 8b代表8个bit。
2) 字节(byte)
8bit 组成一个字节,总共可以表示256种状态。字节一般用B代替,例如:8B 一般表示8个字节。
3) 其他计量单位(K,M,…)
K:表示2的十次方,例如:1KB就是1024B
M:表示2的20次方。例如:1MB就是1048576B
G:表示2的30次方。
T:表示2的 40次方。
比这些更高的计量单位,尽管有定义,但是现实中我们很少用到,这里就不再赘述。
1.2.2 二进制表达文字
1.2.3 二进制表达图片
图片数字化的过程,分为几步:
1) 图片像素化
现实中的图片是连续的,但如果想用数字表示图像,就需要将一张完整的图像,横纵向分别切成很多份,从而拆成一个一个像素点,每个像素点是一个色块,而横纵切分的份数,我们称之为分辨率。例如,我们将一张4比3比例的图像,横向拆成800份,纵向拆成600份,那么我们就将一个图像拆成了480000个像素,而这张图像的分辨率为800x600。由此可见,图像的分辨率越高,图像就越清晰。
2) 像素数字化
每个像素就是一个颜色块。具体的颜色,可以用红绿蓝三原色调配成。而每种原色,就可以用一个数字表示其深浅。例如,我们用一个字节(0-255)表示一种颜色的深浅,那么一个像素点就可以用三个字节表示。颜色的深浅是连续的,用0-255这种级别如果不够精确,我们可以选择用两个字节表示一个颜色(0-65535)。像素的深浅级别我们称为“位深”,例如 0-255为8位深,0-65535是16位深。位深越大,颜色切分越连续,越不容易出现色阶。现今网络上的图像一般都是 32 位深。
1.2.4二进制表达声音
声音的数字化和图像的过程非常类似:
1) 采样
声音来自于物体的振动,是一种连续的波形。我们如果想用数字表达声音,还是要将其离散化。首先我们将一段时间(例如1秒钟)的连续声波分成很多份,并将每一份记录一个平均振幅。拆分的份数,我们称之为采样率。拆分过程中必然会产生信息丢失,而采样率越大,信息丢失越少。目前比较常用的采样率是44100HZ或者48000HZ。
2) 量化
每个采样我们还要用一个数字表示它的高度(振幅),这又是一个连续量离散化的过程跟图像的颜色深浅类似,我们也可以用 0-255 或者 0-65535 表示振幅的高度。这个数字的取值范围叫做声音的位深。目前通用的位深一般是16bit或者 24bit。
1.2.5二进制表达视频
视频就是连续的图片和声音。当然,如果只是耿直地将图片拼接到一起,那么视频的体积会非常大。实际上视频是通过一种有损压缩将很多图片压缩在一起。目前比较流行的视频编码标准为 H.264、H.265、AV1。
1.3数字电路
将信息数字化后,我们可以方便的用二进制表示信息。而现代计算机技术的基础——数字电路,研究的就是如何用电表示二进制。其实这件事非常简单,例如下面的一个电路图:
当开关闭合,信号输出就有5v电压;当开关断开,由于下拉电阻存在,信号输出电压变为 0,我们可以将信号输出的高电平态视为1,低电平态视为0,即可用电表示二进制。所以数字电路就是对电路进行定性分析(高或者低),而不用去考虑精确电压是否会有误差。
1.3.1数字电路仿真软件--Digital
为了学习数字电路,给大家介绍一款数字电路拟真软件。
官方网站:https://github.com/hneemann/Digital
最新版下载地址:https://github.com/hneemann/Digital/releases/download/v0.30/Digital.zip
该软件运行需要Java环境,下载地址为:https://download.oracle.com/java/17/latest/jdk-17_ windows-x64_bin.msi
安装完成后打开 digital,看到如下页面即安装成功:
1.3.2基础逻辑门电路
基础逻辑电路重点掌握表述该逻辑的四种方式:逻辑表达式、真值表、逻辑符号、口诀。
1) 非门
(1) 从“组件--输入输出”中,选择一个输入和一个输出
(2) 从“组件--导线”中选择一个地、一个电源和一个下拉电阻
(3) 从“组件--开关”中选择一个继电器
2) 与门
(1) 从“组件--输入输出”中,选择两个输入和一个输出
(2) 从“组件--导线”中选择两个地、一个电源和一个下拉电阻
(3) 从“组件--开关”中选择两个继电器
给输入编号A、B,给输出编号C,按照如下图连接
3) 或门
(1) 从“组件--输入输出”中,选择两个个输入和一个输出
(2) 从“组件--导线”中选择两个地、一个电源和一个下拉电阻
(3) 从“组件--开关”中选择两个继电器
给输入编号A、B,给输出编号C,按照如下图连接:
1.3.3其他门电路
与或非三门我们一般称之为基础门电路,而其他门电路可以用这三种门电路组合得到。下面我们给出一些门电路真值表以及用与或非三门的组合实现。(这些实现并非最简实现,我们的这门数字电路基础,更多只考虑可行性,不考虑优化)
1) 异或门
真值表和电路图:
2) 与非门
真值表和电路图:
3) 或非门
真值表和电路图:
4) 异或非门
真值表和电路图:
5) 逻辑门电路总结
1.3.4运算器
1) 半加器22
现在我们来考虑如何用电路来实现1位加法。假如有两个1位二进制数A、B,它们的和为1位二进制数S,那么存在如下枚举:
(1) 如果 A=0,B=0,那么 S=0
(2) 如果 A=1,B=0,那么 S=1
(3) 如果A=0,B=1,那么 S=1
(4) 如果A=1,B=1,那么 S=0,而且产生一个进位 C=1
也就是说,对于加法运算,应该两个输入A和B,两个输出S和C,S表示和,C表示是否产生进位,也就是说真值表如下图所示:
其中 A、B与S的关系为异或,与C的关系为与,得到如下电路图:
2)4位加法器
多位加法器就是多个加法器的串联,下面我们以4位加法器为例来展示多位加法器电路:
添加输入 Cin,A,B,右键点击A和B,将位数调整为4
添加两个“组件--导线”中的分裂器,右键点击将其设置如下图所示:
将分裂器输入端与A、B相连,并添加8个“组件一一导线”里面的隧道,分别命名为A0、A1、A2、A2、B0、B1、B2、B3,并接在分裂器的输出端,如图示:
说明:图中A0管道是给标签名起名位A0来实现的。
4)减法器
1.3.5锁存器和触发器
目前我们画了很多计算电路,这些电路都有一个特点,当输入信号发生变化,输出信号会立刻变化,无法对状态进行存储。这在某些情况下会给我们的计算带来麻烦,例如我们想计算这个算式:25+37+12-9+8。在没有状态存储的情况下,我们只能用4个计算电路串联得到最终结果,这显然是不现实的。
接下来的一系列电路,我们要给大家描述的是,如何储存电信号
1.3.5.1SR 锁存器
1) SR锁存器的电路结构:
SR锁存器(set-reset-Latch)是静态存储单元中最基本、也是电路结构中最简单的一种电路。SR锁存器可以有两种构成方式。方式一,由两个或非门构成。方式二,由两个与非门构成。两种方式构成的SR锁存器功能相同。此处,我们以与非门构成的SR锁存器为例进行介绍。
2) 由两个与非门组成的SR锁存器的工作原理
或非门
1.3.5.2带 en 输入的 D 锁存器
上面的 SR锁存器尽管可以锁住输出状态,但是我们没法控制设置输出的时机。所以我们给这个电路加上一些其他组件。
1) 电路结构:锁存器的基础上,增加一个触发信号输入端。
2) 工作原理
(1) En=0时,输出保持不变
当 en=1时,与非门 G3输出就是S’,与非门G4的输出就是R’,正好是之前讲过的
SR 锁存器的输入。
3) 真值特性表
4)带 En 输入的D锁存器
当en为1时,如果我们想将O设为1,此时S应为1,R应为0;反之,如果我们想将Q设置为0,S应为 0,R应为1。由此可得到,SR恰好为反相输入时,可以顺利的设置
1.3.5.3边沿触发的 D 触发器
D锁存器尽管可以起到保存数据的作用,但是当en信号为1时,D输入和Q输出相当于是联通的,此时如果D信号有波动,Q会跟随波动。我们希望能得到更稳定的输出Q,不希望en 高电平时Q随D波动,而是希望Q只在en信号由0变成1的一瞬间随D输入变化,其他时间都保持不变。看下面的电路图
其中输入C为时钟输入,添加路径为“组件--输入输出”里面的时钟输入。关于时钟的作用,我们在之后计算机组成原理里面再为大家详细介绍,现在我们可以仅仅把时钟输入当作一个普通输入来用。
1.3.6寄存器
D触发器可以在时钟上沿存储 1bit数据,如果我们想存储多个bit的数据,就需要用多个D触发器并联实现,这种电路我们称之为寄存器。以4bit寄存器为例,看如下电路:
4bit输入D会在时钟输入上升沿存储到Q。再给这个寄存器加上en信号,最终效果如图:
今后我们只用寄存器时,不会在绘制该电路,而是直接使用“组件--存储器”中的寄存器,如图示:
带有寄存器保存结果的加法器
第2章计算机组成原理
2.1冯诺依曼模型的计算机
由科学家冯诺依曼提出的模型理论。基于通用图灵机建造的计算机都是在存储器(内存/寄存器)上存储数据。鉴于程序和数据在逻辑上是相同的,因此程序也能存储在计算机的存储器中。
1) 冯诺依曼模型的四个子系统
(1) 存储器:用来存储数据和程序的区域
(2) 算数逻辑单元(ALU):用来进行计算(算数运算、逻辑运算、位运算等)的地方。
(3) 控制单元:对存储器、算数逻辑单元、输入/输出等子系统进行控制操作。
(4) 输入/输出单元:输入子系统负责从计算机外部接收输入数据,输出子系统负责将计算机处理结果输出到计算机外部。
2) 冯诺依曼模型-存储程序概念
冯诺依曼模型要求程序也必须存储在存储器(内存)中,现代计算机的存储单元用来存储程序和数据,这意味着程序和数据应该有相同的格式,实际上他们都是以位模式(0和1)存储在内存中。
3) 冯诺依曼模型-指令的顺序执行
冯诺依曼模型中的一段程序是由一组数量有限的指令组成
控制单元从内存中提取一条指令,解释指令,接着执行指令,也就是说指令是一条接工着一条顺序执行的。
2.2计算机组成部件
(1) 计算机组成部件可以分为三大类(1)中央处理单元(CPU)
(2) 主存储器(内存)
(3) 输入/输出子系统
2.2.1中央处理单元 CPU
CPU用于数据的运算【算数逻辑单元(ALU)、控制单元、寄存器组】
1) CPU中的算数逻辑单元
算数逻辑单元:对数据进行逻辑、移位和算数运算。
(1) 算术运算:整数和浮点数的加减运算。
(2) 位移运算:逻辑移位运算和算术移位运算。
(3) 逻辑运算:非、与、或、异或,这些运算的输入数据为二进制模式,运算结果也是二进制模式。
2) CPU中的寄存器
寄存器:用来存放临时数据的高速独立存储单元。
(1) 数据存储寄存器:保存运算的中间结果
(2) 指令存储器(IR):CPU从内存中逐条取出指令,并存储在指令存储器中,解释并执行指令。
(3) 程序计数器(PC):保存当前正在执行的指令地址,当前指令执行完成后,计数器自动加1,指向下一条指令的内存地址。
3) CPU控制单元:
控制单元:控制各个子系统的操作,控制是通过从控制单元到其他子系统的信号来进行的。
2.2.2内存
内存是存储单元的集合,每个存储单元都有唯一的标识,称为地址。
数据以“字”的形式在内存中传入传出,字可以是8位、16位、32位、64位。如果字是8位,一般称为一个字节。
1) 内存类型
主要有两种类型:RAM和 ROM。
(1) 随机存取存储器(RAM)
特点:系统断电后,信息(程序或数据)丢失。
(2) 只读存储器(ROM):里面的数据由制造商写进去,用户只能读不能写
特点:系统断电数据不会丢失。常用来存储那些在开机时运行的程序。
(例如:系统开机时的引导程序)。
2) 存储器的层次
3) 高速缓冲存储器
存储数据的速度比内存快,比寄存器慢。通常容量较小,被置于CPU和主存储器(内存之间)。
4) CPU和存储器的连接
CPU 与主存储器之间通常由称为总线的三组线路进行连接。他们分别是:数据总线、地址总线、控制总线。
(1) 数据总线:由多根线组成,每根线每次传送1个位的数据。线的数量取决于计算(1)机字的大小。例如:计算机的字是32位(4个字节),那么需要32根线的数据总线,以便同一时刻同时传送 32 位的数据。
(2) 地址总线:允许访问存储器中的某个字的。地址总线的线数取决于存储空间的大小。例如:存储器的容量为2的n次方个字,那么地址总线一次需要传送n位的地址数据,因此需要n根线。
(3) 控制总线:负责传送指令的。例如:如果计算机有2的m次方条控制命令,那么控制总线就需要有 m根。
2.2.3输入/输出(I/0)系统
可以使计算机与外界进行通信,并在断电情况下存储程序和数据,分为两大类:非存储设备和存储设备。
(1) 非存储设备:键盘、鼠标、显示器、打印机等。
(2) 存储设备:也称为辅助存储设备,通常有磁介质和光介质两种。
特点:便宜,断电后数据不丢失。
1) I/0设备的连接
(1) 输入/输出设备不能直接与CPU和内存的总线相连接,因为输入/输出设备本质与CPU和内存的本质不同,输入/输出设备都是磁性或光学设备,而CPU和内存是电子设备。与CPU和内存相比,输入/输出设备的数据读取速度要慢的多,因此必须要有一个中介来处理这种差异,输入/输出控制器
(2) 输入/输出控制器:连接输入输出设备到总线上,每一个输入/输出设备都有一个特定的控制器。
(3) IO设备的连接控制器:控制器清除了输入/输出设备与CPU以及内存在本质上的障碍,控制器可以是串行或并行的设备。
串行控制器:只有一根数据线连接在设备上。
并行控制器:有多根数据线连接到设备上,一次能同时传送多个位。
(4)常用控制器:SCSI、火线、USB和HDMI。
2.2.4程序的执行
通用计算机使用程序的一系列指令来处理数据,通过执行程序,将输入数据转换为输出数据。程序和数据都放在内存中。
(1) 机器周期。
(2) 输入/输出操作。
1) 程序执行:机器周期
CPU利用重复的机器周期来执行程序中的指令,一步一条,从开始到结束一个周期包括3步:取指令→译码→执行。
2) 取指令
CPU的控制单元命令系统将下一条将要执行的指令复制到CPU的指令寄存器中,被复制的指令地址保存到程序计数器中,复制完成后,程序计数器自动加1,指向内存中的下T一条指令。
3) 译码
当指令置于指令寄存器后,该指令将由控制单元负责译码,指令译码的结果是产生一系列系统可执行的二进制代码。
4) 执行
指令译码完毕后,控制单元发送任务命令到CPU的某个部件,例如:控制单元告知系统,让它从内存中读取数据。这就是执行阶段。
2.2.5不同的体系结构
1) CISC(复杂指令集计算机)体系结构
设计策略:是使用大量的指令,包括复杂指令。
优点:程序设计更容易,因为每个简单或复杂的任务都有一条对应的指令。程序员不需要写一大堆的指令去完成复杂的任务。
缺点:指令集的复杂性使得CPU和控制单元电路非常复杂。
优化方案:程序在两个层面上运行,CPU不直接执行机器语言指令,CPU只执行被称为微操作的简单操作,复杂指令被转化为一系列简单操作后由CPU执行,使用微操作的程序设计被称为微程序设计。
应用:英特尔公司开发的奔腾系列CPU。
2) RISC(精简指令集计算机)体系结构
设计策略:是使用少量的指令完成最少的简单操作。
缺点:程序设计更难,复杂指令需要用简单指令模拟。
3)流水线
计算机对每条指令使用取指令、译码、执行三个阶段,早期计算机每条指令的这三个阶段需要串行完成,现代计算机使用流水线技术改善吞吐量(单位时间内完成的指令总数)。
如果控制单元能同时执行两个或三个阶段,那么下一条指令就可以在前一条指令完成前开始。
2.2.6简单计算机
为了解释计算机的体系结构和指令处理,引入一台简单(非真实)计算机。简单计算机有三部分组成。
(1) CPU
(2) 存储器
(3) 输入/输出(IO)系统
第 3章设计计算机硬件
3.1实现一个 ALU
ALU(算术逻辑单元)是计算机中的一种关键组件,负责执行算术运算和逻辑运算。它是中央处理器(CPU)内部的一个重要功能模块,用于处理数据和执行指令。简单的ALU,可以看作是计算电路的大杂烩,例如,接下来我们打算实现一个ALU,它能够实现A、B两个 16bit数字之间的加法、减法、按位与和按位或运算,同时有一个ZF(Zero Flag)指示 A、B 两数是否相等。
1) 选择器
在 ALU 中我们会用到复用器,两相复用器原理和封装如下图:
输出Q会跟随 sel来决定是取输入A还是输入B。而四相复用器就是两相复用器的叠加:
今后我们在使用多相复用器时,不会再绘制该电路图
2) 16 位 ALU 实现
查看如下电路:
3.2实现一个简单的计算单元
现在我们希望使用刚刚创建的 ALU 来计算一下 13+45+27+6,实现的步骤如下:(
1) A输入13,B输入45,然后可以看到S是累加后的结果。
(2) 记录S的结果,并将其输入到A,在B输入27,那么S的结果就是13+45+27,记录这个结果。
(3) 将上一步S的结果输入到A,B输入6,最终得到S结果 13+45+27+6。
上面的操作需要我们每次手动记录S运算后的结果,用这个结果作为输入进行下一步骤的计算。如果我们给 ALU添加存储功能就可以解决人工记录的问题。
接下来我们将 ALU 和寄存器连接在一起,来实现一个简单的计算单元。
添加一个寄存器组件,并设置为16位;
添加一个刚刚实现的 ALU 组件;
从“组件--输入输出”添加一个按钮组件;
从“组件--导线”添加三个常量组件;
添加一个输入和输出,并设置为16位,数据模式设置位 decimmal。
按照下图连接电路:
注意:与 sel连接的常量设置为2bit,值为0,表示我们暂时只算加法。接下来我们来尝试计算 13+45+27+6,开启仿真后,执行如下步骤:
> B输入13,拍下按钮。
> B输入45,拍下按钮。
> B输入27,拍下按钮。
> B输入6,拍下按钮。
最终可以看到寄存器输出显示91,即是此加法算式的结果。在刚刚的计算过程中,我们拍按钮的动作模拟的是计算机中的时钟信号。时钟信号就是计算机电路中的节拍器,由晶振统一产生,CPU的计算就是以时钟信号为单位进行的,时钟信号频率越高,CPU计算速度越快,但相对的单位时间内电路中的电流就越大,发热也越大,这也是现实中限制CPU 提升频率的一个重要因素。
3.3添加一块数据存储
刚刚的这个累加过程,我们完全是通过手动操作进行的。而现实情况是,我们的计算一般都是通过预设的程序自动完成。而预设的程序,需要一个外部存储。对于一般的个人计算机来说,这个存储是内存和硬盘,但是此时我们为了简化计算机的外部架构,专注于CPU内部设计,我们就统一只使用一种外部存储,一块由多个16bit寄存器组成的内存。
3.3.1用寄存器实现一块内存
目标:实现一块内存,存储空间是8*16bit。
实现思路:添加8个寄存器(16bit),并给每个寄存器编号(地址),通过地址操作对应的寄存器存取数据。8个寄存器就要有8个寄存器的地址,可以用3位数据来控制。因此,需要先创建一个3-8译码器。通过3位的数据来控制8个内存地址。
添加8个寄存器,并设置为16bit。
添加输入A(地址位),str(写允许),Din(数据),Id(输出允许),将A设置为3bit,Din 设置为16bit。
添加输出 Dout,设置为 16bit。
添加下拉电阻。
添加复用器,数据位数设置为16,选择位数设置为3.
从“组件--导线”中添加驱动器,驱动器就相当于一个继电器模块。首先将时钟输入,数据输入和数据输出按照如下方式连接:
数据输入直接并联了8个寄存器,此时我们可以通过控制en信号来决定 Din实际写入哪个寄存器;同时我们可以通过控制复用器的选择信号决定哪个寄存器的数据可以输出到Dout。例如,我们让第寄存器1号的en为1,其余的en为0,即可写入寄存器1号;我们让复用器的选择器输入0b000,即可选择从寄存器1号输出。
现在的问题是,我们可以通过复用器控制哪个寄存器输出,但是我们需要一个选择器帮我们选择输入的寄存器。这个选择器应该是3bit输入,8bit输出,封装和真值表类似下图:
这个经典电路就是3-8译码器,电路如下:
我们将封装好的3-8译码器放入电路图,效果如下:
我们就实现了一个读写可控的16x8bit内存。
现实中的内存不可能用寄存器这种高成本电路,但为了简化外部存储结构,我们并不会讲解 SRAM 和 DRAM 架构。
之后我们在使用外部存储的时候,不会再次绘制该电路。为了之后编程方便,我们选择封装好的 EEPROM,如下图:
该封装可以非常方便的预编辑其内部存储,便于我们之后的调试。
3.3.2给计算单元接入外部存储
目前我们的程序只能计算四个数的加法,那如果我们想计算13+45-27-6呢?毕竟我们的 ALU 是支持减法运算的。这时候我们就要一段输入信号去控制 ALU的 sel输入。
我们加入一块新的 EEPROM,将数据设置为16bit,地址设置为2bit,并输入数据0、0、1、1,对应 ALU 的加法、加法、减法、减法。将 EEPROM的输出连接到 ALU 的 sel,这中间注意做位数转换。
将这块 EEPROM的时钟信号和Clock相连,Din,str,ld的处理与数据 RAM 一样。地址位也连上计数器输出,最终效果如图所示:
通过分析发现,PC计数器累加后,可以从内存中读取数据到ALU进行运算,而ALU的控制引脚一直输入的是 00(加),因此,执行的结果是将数据RAM中的数据进行了累加。那如果我们改变 ALU的控制引脚,使其进行其他操作,那么,我们的程序就可以进行复杂的运算了。因此,我们考虑添加一块指令存储器,用来存储ALU的操作模式。
3.4添加一块指令存储
目前我们的程序只能计算四个数的加法,那如果我们想计算13+45-27-6呢?毕竟我们的 ALU 是支持减法运算的。这时候我们就要一段输入信号去控制 ALU 的 sel输入。
我们加入一块新的 EEPROM,将数据设置为16bit,地址设置为2bit,并输入数据0、0、1、1,对应 ALU 的加法、加法、减法、减法。将 EEPROM的输出连接到 ALU 的 sel这中间注意做位数转换。
将这块 EEPROM的时钟信号和Clock相连,Din,str,ld的处理与数据RAM 一样。地址位也连上计数器输出,最终效果如图所示:
如果我们想要实现 13+45-27-6的操作,我们只需要改变 ALU-sel引脚的控制信号即可。
因为 ALU-sel引脚的控制信号决定了 ALU 做何种运算,所以,ALU 的 sel控制信号又叫做指令。而新引入的指令寄存器中目前存储的就是 ALU的控制信号,专门用来存储指令。原来的 EEPROM存储的是数据。
3.5添加其他控制指令
3.5.1添加 hatt 信号位
上面的电路中没有程序终止的指令,我们给指令寄存器的第2位作为程序终止的halt信号位,当halt=1时,程序执行,当halt=0时,程序终止。
加入 halt 终止指令后的电路图如下:
3.5.2添加 str 存储信号位
我们希望将运算之后的结果回存到数据RAM中,那就需要对数据RAM的str引脚信号进行控制,因此指令 RAM中输出的16bit的数据中,需要有1bit对其进行控制。此处我们选择11号位。加入st存储控制后的指令信号控制位如图:
当 st=1时,将ALU运算后的结果,存储进数据RAM 中。添加str控制信号后的电路图如下:
3.5.3添加 Id数据 RAM 输入信号、selB数据选择信号
上面的电路中,当执行 str存储操作的时候,数据RAM 的输出其实并不重要,但是这个数据输出仍然会被输入给 ALU进行计算,我们不希望这样的情况出现,因此,可以通过控制数据 RAM的 ld引脚,来控制数据 RAM 的输出。当ld=1时,数据 RAM 可以输出数据到 ALU,当 1d=0时,不输出任何数据。
添加 ld指令后的信号控制如图:
增加ld信号指令后,电路图如下:
注意:此处的电路仿真时会有bug,为了再现问题,我们先使用这个电路。
指令 RAM 添加 ld信号,及 ld管道。
数据 RAM 的 ld引脚接入 !d信号管道。
仍然执行 13+45-27计算,然后将计算结果存储到3号地址,增加Id信号指令后,数据RAM 和指令RAM的数据如下:
原因在于数据RAM的Id=0时,D输出Z表示高阻态,导致ALU的B引脚输入是不确定的。这里我们需要将Id=0时的D输出设置为0,ld=1时能够正常读取数据RAM中的数据既可以解决问题。因此需要添加一个复用器,进行数据选择。并且增加一个数据选择的信号控制sel_B(2bit)。修改电路图如下:
当Id=0时,seIB=01,ALU的B输入取 0。
当 Id=1 时,seIB=00,ALU 的 B输入为数据 ROM的 D输出。
指令RAM的数据控制位如图:
开启仿真,执行后的效果如图:
3.5.4添加selA信号
我们又有了一个新的需求,希望连续计算两个表达式的值。比如:13+45-27=31的结果存放在地址3处和(15|8)&23=7的结果存放在地址7处。那么我们可以分别向数据RAM 和地址 RAM 中输入如下数据:
最终运算的结果如图,跟我们的设想不符合。
我们将电路各个时钟周期,各设备的输出情况罗列,如下图所示::
发现问题出现在第4个时钟周期,第四个时钟周期将上一次运算的结果,13+45-27=31,存储到地址3之后,在进行后面运算的时候,应该将数据RAM的值15直接存储在A寄存器累加器中,但我们这里会发现,A寄存器累加器进行完上一次运算后,并没有被清0。仍然保持了上一次运算的结果31。导致下一次运算的15与A寄存器累加器的31进行了累加得到46。导致后续的结果都错误了。
因此,我们需要增加控制位来应对,当进行多次运算时,能够直接从数据RAM中读取数据到A寄存器。如何做到呢?当要执行将数据RAM的值直接输入到A寄存器时,需要将 ALU的 A输入设置为0即可,因为ALU的B输入是从数据RAM中直接读取数据,我们选择 ALU 的运算模式为加,那么 ALU 的输出 S=A输入+B输入(数据 RAM的值),我们只需要保证此时 A输入为0,那么 ALU的输出=0+数据RAM中的值。ALU 的输出S又是A寄存器的输入。因此,数据RAM中的数据通过ALU的B输入配合加运算就会直接输入到A寄存器中。那么,通ALU的B输入一样,我们增加一个复用器,增加一个selA 的控制信号即可。
改造后的电路图如下:
当完成上一次计算进行新的计算时,我们就让selA=01,seIB=00 使得 ALU的 A输入=0,ALU的B输入是数据RAM中的数据。数据RAM中的值会直接进入A寄存器。我们将这个操作称为Id_a(load a寄存器缩写)。
添加 selA 指令后的信号控制位及指令如图:
根据最新的电路及指令完成之前的连续两次运算,13+45-27=31的结果存放在地址3处和(1518)&23=7的结果存放在地址7处。那么我们可以分别向数据RAM和指令RAM中输入如下数据。
将数据输入数据RAM和指令RAM,执行仿真查看结果,地址3的值是31,地址7的值是 7。
3.5.5添加jmp跳转信号
通过上面的学习,我们应该已经有了一种意识,我们通过控制指令RAM 中的指令的0、1状态,去控制电路中的对应开关,来实现对应的功能。我们要实现一种运算,只需要在指令 RAM 填好指令,在数据 RAM中填好要操作的数据,那么电路就会根据开关指令去操作数据得到结果。
我们现在想要实现一系列的运算 8+17+3-4+2-7+20,并将结果存放在地址7。那么,数据 RAM 和指令RAM 的数据如下图:
当执行 jmp,跳转的时候,enA=0,其他情况 enA=1。修改电路后如下图:
3.5.6添加je条件跳转信号
jmp跳转是无条件的跳转,程序流程控制也需要有条件的跳转,什么是有条件的跳转呢?比如:如果年龄=18,可以参加活动!所以,最常见的条件就是比较两个数的大小。如果相等就执行相应的操作。我们的 ALU 在设计阶段就已经实现了比较的功能。如图:
ALU 可以比较 A、B的输入,如果相等则 ZF=1,如果不相等则 ZF=0。接下来我们就可以通过 ZF 是否=1来作为跳转的依据了。我们可以引入je(jump equal 的缩写)指令。执行这条指令时,如果上一个时钟周期 ZF=1,那么跳转到指定地址继续执行。如果 ZF=0,那么不跳转。因此,我们需要一个寄存器来存储上一个时钟周期ZF比较的结果。该寄存器我们命名为比较寄存器。加入比较寄存器后的电路图如下:
ALU 增加 ZF 输出管道 cmp。
增加比较寄存器。
之前我们 jmp 指令的时候,使用了两个控制信号对跳转进行控制,Id_pc和en_pc,跳转时 ld_pc=1,en_pc=1,PC 累加时ld_pc=0,en_pc=1,可以发现控制跳转的主要信号是Id_pc,因为en_pe始终=1。我们又加入了新跳转je,所以将原来jmp,的跳转信号 ld_pe重新命名为jmp_en,je跳转信号命名为je_en。
jmp_en=1时,进行无条件的跳转。
je_en=1,并且 Qcmp=1时,进行有条件的跳转。
根据该逻辑PC计数器的电路应该修改为如图所示:
增加je_en、jmp_en后的指令控制位及最新指令如图:
修改后,首先测试一下原来的jmp无条件跳转是否仍然有效。数据RAM和指令RAM的数据不变。
3.6添加控制器
通过前面的学习,我们应该感受到了指令的本质就是控制信号的开关当前我们的控制信号如图所示:
0、1:控制 ALU 的计算选择(加、减、与、或)
2、3:ALU的A输入选择信号。
4、5:ALU的B输入选择信号。
6:寄存器A的写使能信号。
8:PC计数器的写使能信号。
9:je条件跳转控制信号。
10:jmp无条件跳转控制信号。
11:数据RAM写使能信号
12:数据RAM 读使能信号。
15:终止信号。
现在已经有11位的信号控制了,如果电路越来越复杂,控制信号会越来越多。每一个控制信号都需要一个2进制位来表示它,现代计算机的指令多达几百上千个,那我们指令占用的位就太多了,我们必须用一种方式简化指令占用的控制位。
我们知道一个二进制位表示2个数字,目前我们的系统一共有如下9条指令,那么只需要 2^4=16,也即4个二进制位就可以表示。
为了后期进一步扩展完善系统,我们选择2^5,32条指令的空间留有扩展的余地。5个二进制位在这里我们称为操作码,英文缩写opeode。
假如我们把每一条指令和一个5位opcode 按照如下方式映射起来:
也就是当 opcode=00000时,表示指令halt,当opcode=00001时,表示指令 ld_a,以此类推。这种映射有了之后,如何让我们的系统支持该映射呢?可以通过查找表来实现这种映射。
新建一个电路图,取名控制器,然后在电路图中添加查找表,如下图所示:
查找表设置输入位为5位(与opcode一致),数据位16位。
点击编辑,输入映射关系数据(查找表默认显示的16进制数据)。
完善控制器电路如图:
以上控制器电路,就实现了通过5位的opcode,映射出对应指令的控制信号。
有了控制器之后,我们的指令寄存器中存放的数据就修改为opcode即可。修改电路如下图:
指令寄存器中存放 opcode(11-15),通过 ALU 控制器中的查找表映射出控制信号。
我们来测试一下加入控制器后的电路,计算29+38-5-3,并将运算的结果存放到数据RAM的地址5的位置。
数据 RAM和指令RAM的数据如下:
仿真执行后结果如图:
3.7将两块内存合二为一
3.7.1现有系统运行方式梳理
在合并改造之前,我们先回顾一下我们现在的系统。
(1) 开始我们有一个PC程序计数器,程序计数器的输出是一个4位的地址。
(2) 该4位的地址既是我们指令RAM的地址,用来获取指令RAM中存放的opcode,也是数据 RAM的读取地址,用来读取数据RAM中存放的数据。
(3) 接下来我们的opcode通过控制器中的查找表,会映射出整个电路的信号开关控制情况,进而实现相应的功能。
(4) 不同的指令会对数据 RAM 中读取的数据进行不同的操作。
ld_a:将数据RAM中的数据加载到寄存器A中。
add:将数据RAM中的数据累加到寄存器A中。
sub:将寄存器A中的数据减掉数据RAM中的数据,并存储到寄存器A中。
str:将ALU的计算结果存储到数据RAM的指定地址。
等等。。。
(5) 数据RAM的输出有两个作用。
作为数据输出。
当执行jmp、je跳转的时候,作为要跳转的地址。
ALU 进行运算,A输入数据来源于A寄存器或0,通过selA信号控制,B输入数据来源于数据 ROM 或 0,通过 selB 控制。
(6) 比较寄存器存储A、B输入比较的结果,如果相同存储1,不同存储0,该比较结果用来做为ie跳转的依据。以上就是我们当前系统的相关功能。
3.7.2现有系统的指令
3.7.3操作数和操作码
将两个RAM合并为一个RAM之前,我们先介绍一种数据存储的方式,操作码+操作数。
现在我们的系统计算一个算式:13+45-27时,数据RAM和指令RAM中的数据存储情况如下:
各个指令执行的操作:
PC计数器为000时,指令RAM中地址为000的指令Id_a会将数据RAM中地址为000的数据13加载到寄存器A中。
PC计数器为001时,指令寄存器中地址为001的指令add会将数据RAM中地址为001的数据 45 累加到寄存器A中。
PC计数器为010时,指令寄存器中地址为010的指令sub会将数据RAM中地址为010的数据27,与寄存器A中的数据进行相减操作存入寄存器A。
PC计数器为011时,指令寄存器中地址为011的指令str,进行存储操作,会将上面计算的结果31,存入数据RAM中地址为011的位置。
从上面的例子可以看出,指令的地址是什么,那么该条指令操作(ld_a、add、sub、str.……)的数据的地址也要是什么。
这样的操作过于僵化,而且数据RAM的空间也不能得到重复的利用。我们希望指令操作的数据RAM的地址可以被手动指定。因此,引入操作数的概念,如何理解操作数呢?当前我们可以认为,操作数就是操作码要操作的数据RAM的地址。
引入操作数概念后,我们上面的计算13+45-27,在指令RAM和数据RAM中的存储如下:
ld _a 000 表示,将数据RAM 中的地址为000的数据13加载到寄存器A中。
add 001表示,将数据RAM中的地址为001的数据45累加到寄存器A中。
sub 010表示,将寄存器A中的数据减掉数据RAM中的地址为010的数据27,再回存到寄存器A中。
str 011表示,将寄存器A中的数据存储到数据RAM的011位置
会发现加入了操作数的指令跟没加操作数没有区别啊!反而更麻烦了。但是如果我们将操作数改变一下,就会发现它的优势。
ld_a 001表示,将数据RAM中地址为001的45加载到寄存器A中。
add 010表示,将数据RAM中的地址为010的数据27累加到寄存器A中。
sub 000表示,将寄存器A中的数据减掉数据RAM中的地址为000的数据13,再回存到寄存器A中。
str 000表示,将寄存器A中的数据存储到数据RAM的000位置。
我们会发现,通过操作码和操作的数的结合,使得我们的指令操作更加的灵活,并且使得数据 RAM 还可以被重复的利用。
我们合并成一个 RAM之后,RAM中存储指令的方式就是操作码+操作数的方式。这里介绍了这种方式,方便后面的理解。那么接下来就让我们来合并RAM吧。
3.7.4合并为一个 RAM
目前我们的数据存储和指令存储是分开的,这和我们使用计算机的直觉不符,毕竟我们都是用一块内存完成编程。这是因为现在主流计算机都是冯·诺依曼架构,只有一块内存,所以接下来我们就将两块内存合并成一块。
在原来的系统中,我们有两块RAM,一块数据RAM,一块指令RAM,在一个时钟周期内,我们有一根数据线从数据 RAM中读取数据,有一根指令线,从指令RAM 中读取指令。如图:
如果我们将两块 RAM合并为一块,那么数据线和指令线就变成了一条线,那么在某个时钟周期内,该怎么知道这条线中传递的是数据地址还是指令地址呢?因为在一个时钟周期内不可能既表示数据又表示指令。那该怎么办?
3.7.5取指令和执行指令
既然一个时钟周期做不到,那么我们就将程序的执行分成两个时钟周期来完成一个任务。第一个时钟周期用于读取指令。第二个时钟周期用于读取数据并执行指令。
取址阶段:在这个阶段,RAM的地址信号是指令的地址。RAM输出的是指令。
执行阶段:在这个阶段,RAM的地址信号是数据的地址。RAM输出的是数据。也是在这个阶段去执行上一个时钟周期读取到的指令。因为执行指令阶段需要获取到上一个时钟周期的指令,因此,需要有一个指令寄存器去存储上一个时钟周期获取的指令。
程序的执行过程如上图所示:
我们程序的执行状态只有两种,因此,我们可以约定 state=0时为取指阶段,state=1时I为执行指令阶段。
State 只能在 0,1之间切换。可以用D触发器来实现 state 的电路切换。如下图:
3.7.6电路实现
我们之前是一个时钟周期执行一条指令,现在是两个时钟周期执行一条指令,分为取指令(取指令就是根据PC计数器输出的地址从RAM中读取指令并存储到指令寄存器中),执行指令(读取指令寄存器中的指令,以该指令作为输入,通过控制单元产生各种控制信号,使得各个器件执行不同的功能)。
我们需要两个时钟周期执行一条指令,那指令在各个器件间是如何传输执行的呢?
3.7.6.1创建 RAM、指令寄存器
enAddr是指令寄存器的写允许控制信号。取址阶段enAdd=1,执行阶段enAdd-1,所以,state和 enAdt的映射关系也应该在控制器查找表中做映射。关于查找表的映射配置,我们在所有电路完成之后一起配置。
3.7.6.2RAM 的数据结构
指令寄存器输出的内容为 opcode和 addr,addr是11位的。addr又会作为PC计数器的输入,我们的PC计数器原来的地址位是4位的,因此要将PC计数器整体修改为11位。
同时,PC计数器输出的A地址也会变为11位,A地址又作为RAM的地址输入,因此 RAM的地址位也要改为11位。
如果改为 11位之后,前面用到PC计数器的电路都是4位的将不能使用。因此,我们需要将PC计数器做成通用组件。
(1)选择设置通用组件
打开 PC电路→「顶部菜单--编辑」→「设置当前电路」→「高级设置」→「勾选通用电路」。
(2)给器件添加通用参数
通用参数:this.Bits=args.Bits。
PC计数器需要进行如上修改的器件如下图:
3.7.6.3修改 PC计数器的地址位为 11 位
3.7.6.4修改 RAM 的地址位为11位
3.7.6.5控制器放造
2) 重新编辑查找表
(1) 查找表的映射逻辑
(2)查找表数据
3.7.6.6最终电路图
1) 控制器电路图
2) PC计数器电路图
3.7.6.7执行过程演示
现在我们要用两个时钟周期完成,第一个时钟周期获取指令,第二个时钟周期执行指工所以我们的计数器要两个周期再递增。
3.8添加立即数
现有系统的数据RAM存储方式(操作码+操作数)
现有我们的 RAM中,操作数指的是数据RAM的地址。也就是在执行指令的过程中,需要操作数指定的地址到RAM中读取数据运算所需要的数据。这个数据获取的过程是间接的。
我们希望增加一个直接获取运算的数据的方式,也就是说操作数不是数据ROM的地址,而是表示具体的要参与运算的数据。这样的操作数称为立即数。我们举例说明:比如我们要计算19+69-27。
以上就是操作码+立即数的方式。蓝色是操作码,红色是立即数。这样可以直接操作数据,但是也会有一个新的问题,就是什么时候操作码表示数据ROM的地址,什么时候操作码表示立即数呢?
比如,有一条指令Id_a7,那么7表示的是数据RAM的地址?还是立即数7呢?现有系统我们无法区分,所以,既然通过操作数无法区分,我们可以通过指令来区分,也就是设计两套指令,一套指令操作数表示数据RAM的地址,一套指令操作数表示立即数。我们现有指令就是操作数是数据RAM地址的指令,因此,我们只需要增加设计一套操作数的指令即可。
3.8.1增加立即数后的指令
3.8.2电路改造
立即数就是将操作码当做数直接进入ALU计算,我们的操作码addr 原来表示的是RAM的地址,位数是11。因此,头部补5位的0,就凑成了11位的Din,作为ALU的B信号的输入了。
将指令寄存器输出的11bit地址补全到16位接入地址总线。通过eni信号控制。整体电路如下图:
3.8.3控制器改造
1) 增加en_i控制信号
2) 修改控制器查找表
3.8.4测试立即数功能
3.9添加B寄存器
目前我们只有一个寄存器A,寄存器相当于是编程中的一个变量,也就是我们现在的系统只支持一个变量的运算。如果我们需要进行多个变量的运算,一个寄存器是不够的。因此,我们需要为系统再添加一个寄存器B。
3.9.1电路改造
1) 添加B寄存器,修改ALU 输入端信号
B寄存器写入使能端en_b,在控制器里需要增加en_b控制信号,同时需要修改控制器查找表控制en b。
2) 控制器改造
预留的6号引脚控制en b.
3) 整体电路
增加 B寄存器后,ALU的A、B输入可以有4中选择,从A、B、RAM(立即数)、0
3.9.2指令集
指令集,就是你希望CPU能够完成的操作。现在流行的指令集有x86,arm和risc-V。前两个指令集都是有知识产权的,不能随便用,而risc-V是开源指令集。指令集不光是随便设计一套指令就可的,最重要的是上下游的生态环境。比如同学们之前学过的C语言,在编译过程中,gcc工具链可以将C语言完整编译成x86指令,gcc-eabi工具链可以编译arm 指令,risc-V也有对应的工具链,但是其上下游其他配套设施无法与前两者相提并论。
无论是 x86,arm,还是risc-V指令集,对于我们设计的这个基础CPU 都过于复杂,所以我们接下来会自己设计一套指令集,相对的,我们就没有配套的编译器为我们完成编译工作,接下来的编译工作我们只能自己手动进行。我们的指令集支持无符号16位数的加、减、按位与、按位或,支持基于等值的循环和分支控制,支持立即数对寄存器的运算。
我们的指令长度为16bit,前5bit 为操作码,后11bit为地址或者立即数。操作码与指令的对照表如下:
3.9.3更新控制器查找表
3.9.4测试
x=19,y=45,计算x+y的值64存储到RAM的7号地址中。
分析:x变量看成寄存器A,y变量看成寄存器B,那么上面的功能翻译成操作数和操作码,数据如下:
第 4章设计计算机汇编语言
4.1用自己的汇编语言编写程序
上面的例子过于简单,体现不出我们系统的强大,接下来我们来实现一段程序。0到100 求和。
请看下面这段代码片段:
我们可以在 https://gcc.godbolt.org/ 网站将这段代码编译成 x86的汇编语言,结果如下:
尽管我们自己设计的汇编语言没有编译器,但是我们可以仿照上面的x86汇编将上面的代码复刻一段并在我们自己的CPU上执行。
翻译成二进制。
烧录进 eeprom,将时钟频率调整位500,开始仿真,程序结束后注意观察内存中16地址的值。
4.2测试运行
1) 替换时钟信号,并设置频率为100HZ
2) 仿真执行查看结果
4.3CPU电路
我们之前实现的电路,其实是一个完整的CPU电路,这个CPU包含完整的ALU,控制器和寄存器组,还有一个内置的RAM。那如果我们想通过CPU控制外部设备,又该如何实现呢?接下来我们以内存,硬盘和显示器为例,来讲解CPU如何通过总线控制外部设备。
4.4什么是总线
总线,在前面的课程中我们有过介绍,但是没有相应的实物,看起来比较抽象,其实总线就是CPU和外部通讯的导线,如果我们将CPU结构稍作改造,例如下图:
我们将 A,str,Id,Dout和 Din五根导线分别接上输入和输出,就可以实现 CPU 和外部数据的交换,而这五根线就是总线,其中:
A线为数据总线,用来传输CPU寻址地址
ld和 str 线为控制总线,用来传输 CPU控制信号,表明 CPU是打算对地址进行读,还是写。
Din 和 Dout为数据总线,用来传输数据。
这时就会产生一个问题:我们的Din数据源,既可以从内部RAM读取数据,也可以从外部设备读取数据,那我们以哪一个为准呢?或者说,如果存在多个外部设备,CPU如何判断自己控制的是哪一个外部设备呢?
在讲总线地址映射之前,我们会发现现在的CPU电路已经无法单独仿真了,会提示Din短路。这是为什么呢?
当 RAM的 ld=0时,内部RAM的 Din就没有输入了,当en_i=0时,立即数 Din 也没有输入源,此时,Din信号就只有外部输入的Din。所以,在实际电路应用中,我们可以通过控制信号来让电路生效。这就是我们当前CPU电路不能单独进行仿真的原因了。
4.5总线地址映射
现代计算机中,一种解决方案是,采用CPU地址映射的方式来区别外部设备。例如,我们的A信号(寻址信号)长度为11位,我们可以用其中的3位表示总线上的设备编号,后8位表示总线上的外部设备的内部地址,就可以对设备进行区分。来看如下指令:
按照我们之前学习的知识,上面这段指令的意思是从0b010 00000000读取数据,写入到0b001 00000000地址。但是如果我们按照总线地址映射的方式来看,这两句的指令的意思就是:
这样我们就可以通过简单的寻址动作,完成CPU和外部设备通信。接下来,我们来完成CPU外部地址映射的设计:
这里其实存在几个小问题,例如:
我们的寻址和标准的寻址不同,一次返回两个字节:
iRom 寻址空间一般非常小;
外部 RAM 和 ROM 都偏小;
CPU不支持中断,无法接入外部输入设备;
我们没有用完所有的寻址空间。
以上这些问题确实存在,但是我们的课程以帮助大家理解CPU内部原理为主,并没有按照现实1:1复刻。
基于以上地址映射设计,做如下电路更改:
1) CPU内部RAM电路修改
现在我们需要限制内部RAM只响应0b000 00000000~0b000 11111111的寻址,需要对其电路做一定更改,如下图:
在控制电路中,我们将高3bit组合成了en信号,从而实现只响应0b000信号;而低8bit直接接入iRom寻址地址实现寻址。改造过后,CPU电路如下图:
将CPU封装成子电路,方便我们以后调用。如下图:
2) 内存模块
内存模块 11bit地址中高3bit是0b001,低8bit是内部地址空间,那么我们就可以仿照CPU内部ROM电路,做出一点点修改即可,最终电路如下:
注意内存中的存储介质我们选用的是RAM(独立端口)模块,这个模块和EEPROM的区别在于它不能预先编程,只能在上电以后由程序进行修改。
3) 磁盘模块
磁盘模块代表的是持久化存储模块,其实和内存模块电路类似,只是高3bit相应0b010,低8bit是内部地址,并且存储介质要找一个持久化介质,这里我们还是采用了独立端口的EEPROM,电路如下:
4) 终端模块
最终我们需要在终端模块显示一串字符串。Digital里的终端如下图:
当en信号为1,经过一个时钟周期上升沿,D的数据就会显示在终端上。唯一需要注意的是D的信号为8bit。我们只需要将寻址信号组成en信号,将输出总线的低8bit接入D,即可实现终端,最终电路如下:
第5章设计引导程序
5.1概述
现代计算机的开机通常要经历一个复杂且精心设计的过程,这个过程叫做引导。引导是计算机启动和运行操作系统的初始步骤,它确保了系统能够正确地加载和执行所需的程序。引导过程随CPU架构不同而不同,我们平常接触最多的是x86架构的引导,而后面的课程我们主要涉及单片机以及ARM架构的引导。今天的课程,我们主要精力放在给我们自己设计的计算机设计一个引导过程。
5.2引号流程设计
目前我们的计算机总共有三块存储:
iRom:CPU内部很小的一块存储,只读;
内存:无法持久化保存数据,只有上电之后才能由程序修改;
硬盘:存放我们的自定义代码和数据。
CPU 在上电之后首先会去执行iRom的代码,这一段代码应该是写死在CPU内部不能修改的,所以这一段代码的主要作用应该是引导CPU去执行我们的自定义代码。而我们的自定义代码存放在硬盘上,直接执行效率很低,需要将硬盘里面的数据加载到内存执行。所以 CPU的 iRom 代码应主要功能应该是:
将硬盘中的程序拷贝到内存。
跳转到内存地址接着执行。
但这里存在一个问题:我们的程序长度是不固定的,但iRom,只能加载固定长度的数据到内存。为了容纳更长的程序,这个固定长度只能预先设计的非常大;但这样又会造成引导效率太低。所以为了解决这个问题,我们做这样一个约定:iRom只加载一段固定长度的短程序并执行,这段短程序再负责加载更长的我们的自定义程序。由于这段短程序我们可以自定义,就不存在之前的问题了。所以最终的设计如下:
iRom负责将硬盘前32个寻址长度的程序加载到内存并跳转;
硬盘前32个寻址长度负责加载自定义程序并跳转;
最终执行自定义程序。
而硬盘前 32个寻址地址存放的程序,我们就称之为引导程序,而这32个寻址地址,我们称之为引导区。
5.3程序实现
1) iRom
上面的程序可以实现将地址512-543(硬盘)的内容拷贝到地址256-287(内存),并跳转到 256(内存)继续执行,而根据主线地址映射,512-543是硬盘的头32地址,256-287是内存的头32地址,所以这一段程序可以将硬盘的程序拷贝到内存并执行。
将这段代码翻译为二进制:
保存为“CPU 内置 iROM.hex”,并加载到 CPU 内置的 iRom上。
2) 硬盘代码
硬盘 511-543 之间的代码,已经被上面的引导程序拷贝到了内存中,即便我们硬盘当前只有 17条指令,也就是只有511-528是实际有效的拷贝。但引导程序仍然会将511-543之间的程序全部拷贝过去。因为CPU中的引导程序是不可修改的。
这段代码的内容和上面的iRom内容类似,主要作用是将硬盘 544-568地址之间的程序拷贝到内存 288-312地址并执行,由于这一段代码我们可以自定义,拷贝的长度是和程序相关的。
这段代码主要就是将一个个数字存到768地址。根据总线地址映射,这个地址实际上是终端的数据输入结构,换句话说我们每次保存的数据,在下个时钟周期就会在终端显示I出来。
将这段代码翻译成二进制文件:
将上面的内容保存为“外置引导+程序.hex”,并加载到外置EEPROM里面。
3)执行
将时钟频率设置为50,开启仿真,在仿真的过程中,右键点击RAM并监视RAM内容的变化。可以看到RAM的地址内部不断有数据被拷贝进来并执行。最后会在终端上显示“Hello world!”。当然同学们也可以自己写一段程序来执行,效果是差不多的。
5.4单片机引号流程
单片机引导流程要比一般x86计算机简单。我们可以把单片机视为ROM和RAM都内置在 CPU中,只暴露一些输入输出总线接口和外部通信的设备。那么引导过程其实就相当于在 ROM上直接执行程序,只有需要用到RAM时才会向 RAM 中写入数据,并不需要向-般PC引导过程一样需要多步跳转。以我们马上就要学习的51单片机为例,我们来看一下单片机的引导程序。
来看如下代码:
这是一个简单的51单片机点灯程序。至于这个程序为什么能够点灯,我们这里不去关心,我们着重要看的是,单片机从上电到执行到我们的main函数,都经历了哪些过程。当我们开启单片机的调试程序。将程序编译,并开启调试模式,可以看到我们程序编译完成以后的汇编文件如下:
可以看到我们的 mian函数的指令地址是 0x000F,而第一条指令0x0000的作用是跳转到0x0003,从0x0003到0x000C这一段代码其实是51单片机的固定初始化流程,当这段代码执行完毕后,最后一句0x000C的代码就是跳转到0x000F,而0x000F就是我们的main函数入口了。这就是51单片机的引导过程了,可以看到由于单片机的结构简单,它的引导过程甚至没有我们设计的CPU复杂。