首页 > 技术文章 > 学做8位计算机

lovexinyi 2020-08-09 15:16 原文

在B站上看到有大佬做了个8位计算机,非常感兴趣,同时想了解一下计算机底层到底是怎么运作的,就跟着做了一个。以下是笔记,写的比较细。

先show一下代码

序号 指令 说明
0 OUT 显示
1 ADD 15 加上地址15的值
2 JC 4 进位跳转到地址4
3 JMP 0 没有进位跳转到地址0
4 SUB 15 减去地址15的值
5 OUT 显示
6 JZ 0 为0跳转到地址0
7 JMP 4 不为0跳转到地址4

15地址设置成15;

代码意思是:值自增15,如果到达进位255就变成自减15,如果自荐到达0就自增。

基础知识

二极管

单项导通器件

1874年,德国科学家发现晶体的整流功能

由半导体硅材料制成,硅本身是没有电极的,在做晶体管的时候做了杂化处理,在一段加入了硼,一段加入了磷,硼这端会多出电子空穴,而磷这一端会多出自由电子,有意思的事情就发生了。

1593402180341

因为Si是4个电子,P有3个电子,N有5个电子,所以单纯的硅会形成4个共价键非常稳定。

硅:

1593405655597

磷:N

1593405759705

硼:P

1593405791651

当杂化之后,P端就会有很多电子空穴,N端会多出很多自由电子,在PN交界的地方,N端电子会自动移动到P端,形成一个耗尽区,耗尽区的电压为0.7V,所以大多5V的芯片低电压为0.2V,如果超过0.7V则视为高电压

1593402275121

1593402281081

如果加入正电压会使耗尽区扩大,造成正向偏压,如果加入反向电压,大于耗尽区0.7V电压的时候,电子从N极向P极移动没有任何障碍。

1593402339330

绘出曲线,横坐标是电源电压,纵坐标是电流,负向电压的时候几乎没有电流,负向电压特别大的时候会击穿,正向电压大于0.7V的时候会很快获得很大的电流。

1593402366314

1593402373161

二极管的这一特性可以做一个桥式整流电路

1593402782020

1593402800400

1593402818711

1593402920697

三极管

三极管就是二极管的升级,例如NPN型三极管

1593405830231

这样在NP的交界处就会形成两个耗尽区,

1593405941816

可以看成两个二极管背靠背相连,不管电源处于哪个状态总有一个二极管处于反向加压的状态,不导通。但是如果中间加一个电源(第二个电源),大量电子会从P端出来,通过电源到达N端形成通路

1593407386320

形成通路后,大量电子会到P端,形成反向偏压,

如果整体来看,P端非常的窄,并不会存储大量电子,大量电子在第一个电源的驱动下回到电源,形成电流,因为第一个电源的电压比较大,驱动力比较大,第二个电源电压比较小,驱动力比较小

1593407646617

1593406938270

这种现象简而言之就是

  1. 一个小电流被放大成一个大电流,
  2. 一个断路变成一个通路

这种晶体管叫双极结晶体管,

1593407944200

晶体管有两种工作方式:

  1. 通过电流,将一个小电流放大成大电流,

1593409536983

  1. 通过电压,只要基极和发射机有电势差,集电极和发射极就会产生大电流,这种又叫场效应管

1593409918451

1593416628720

1593408002200

双极结型晶体管做的放大电路

1593408052033

1593408059414

1593408073722

门电路

晶体管的基本原理已经知道了,门电路就是基于三极管构成相关电路

非门电路:

1593419182677

1593444105431

与门电路:

1593444284919

或门电路

1593444422758

异或门

1593444497353

1593445197081

锁存器

锁存器用来做寄存器

将或门改造一下就可以就是SR锁存器

1593483803191

SR锁存器

1593485552948

1596789044301

1596789169949

再次进阶D锁存器,D锁存器是构建寄存器的基础,本计算机种所有的寄存器都是由D锁存器构造

1596789454581

1596789543361

触发器

触发器是为了获取极短时间内的上升沿

第一种方法:

1593492156670

1593492224622

1593492248723

从0变成1的时候,非门需要几个纳秒的时候才能将状态转过来,所以在非常短的时候内会出现都为1,这个时候与门输出1,然后非门后的状态0输入,导致输出变为0,这样输出只有几个纳秒是1。

第二种方法:

通过电容来实现

1593492619163

电容和电阻,当信号来的时候电容充电,获得输出1,当几十纳秒后,电容充满电,信号就变成0了

1593492801580

计算时间

1593492862019

D触发器,就是将之前的SR锁存器的Enable改造一下

1593492909456

1593493025886

SR触发器:

1593658845351

1596790932250

1596791041295

SR触发器,在SR都为1的时候,处于一种无效的状态,没有任何输出。当SR变成0的时候,谁慢一点谁就会被触发。这是一种随机状态。

为了解决这个问题:

1593744836246

第一种情况 JK都为0,这是一种随机状态,也成为不确定状态

1596848606615

1596848685490

第二种状态K=1,J=0的时候,处于reset状态,Q=0,反Q=1

1596849714250

第三种状态K=0,J=1的时候,处于set状态Q=1,反Q=0

1596849789639

最有意思的是第四种状态K=1,J=1的时候,信号会发生一次对调

1596851913356

这样会出现问题

在这个脉冲内做了很多次转换,也就是只要两个输入都是高电平,这个转换就一直持续。

这种情况叫做抢先。

所以发现这个根本原因出现这个脉冲电路上,这个上升沿时间太多了。如果时间控制在100ns的时间内就可以只完成1次转换。

把1K电阻换成100电阻,已经控制了100ns的时间,发现还是不行

1593661113782

1593662072289

因为信号有抖动,边缘探测不锐利

用主从JK触发器来解决这个问题

1593662291409

高电压的时候使第一个锁存器工作,在低电压的时候使第二个锁存器工作。

这样就完全可以避免之前的问题

可以看到这有两个锁存器,这两个锁存器不可能同时工作,clock高电位第一个锁存器工作,clock低电位第二个锁存器工作,主从对应的RS正好相反

如果高电压,主锁存器是SET,到低电压的时候从锁存器就是reset,

如果都是1的时候,那么主锁存器执行的操作是由从锁存器的状态决定的,而从锁存器的状态正好与主锁存器状态相反

这样当一个脉冲来的时候,set和reset会执行一次交换

基本模块

计算机需要的模块:1.主脉冲,2.计数器,3.计数器寄存器,4.寄存器A,5.寄存器B,6.ROM,7.指令寄存器,9.显示模块,9.控制模块,10.标志位寄存器。

主脉冲模块

1593501552521

主脉冲

主脉冲使用555芯片

1593481876821

1593481909038

1593494846513

时许分析

开始的时候,没有上电

1593493394039

开始上电的时候

1593493859502

1593494010715

通过电容放电和充电的时间来控制方波的占空比,

外界的电容和电阻决定了方波的长度

1593494184002

通过公式来计算

总的时间是0.139S

1593494323494

在5号引脚加入一个0.01uf的电容接地,可以降噪

1593496574101

1593494488980

1593497005669

当有信号的时候,一堆晶体管需要获取更多的电量,这个时候就会从电源端拉出更多的电流,就会形成电路中非常常见的过充的现象。

电线也会产生一些阻抗,也会阻止电流的变化,所以这个电压就会跳上去,

直接的办法给电路接一个非常短的线路

1593497249577

给正极和负极加一个电容,在电路需要电流的时候给电路提供更多的电流。

1593498098174

在四号引脚接入一个5V高电平,防止Reset锁存器,这样就不存在误操作。

1593498182030

调整时钟的速度,把100K换成可变电阻

1593498641896

单步脉冲

为了更好的测试电路,需要有一个单步脉冲,类似程序的单步执行,按钮按一下给一个脉冲

单步脉冲的意思是按1下产生1个脉冲,用555芯片来消除按钮的抖动

555芯片,消除抖动电路,可以控制灯亮的时间

1593499451377

电阻是1M,电容是2uf,0.1uf,0.1S时间间隔,这边要注意在电路不同的状态,6,7的电压应该是5V,

1593499651484

1593499743745

稳态和单稳态

1593568996652

1593569264958

切换电路

1593499976404

将两个状态的输出型号添加到一个开关中,切换开关可以切换2个状态、

但是开关会有一个新的问题,当切换的时候有一个延迟的问题,这个时候需要一个新的555芯片来解决这个问题,其实是用到555芯片内的SR锁存器

1593500313308

开关有一个特性叫做先断后连,

1593500510718

这个电路主要是解决开关弹跳的问题,

将这三个电路合并起来

1593500968557

这样就可以在自动和手动切换

1593501039719

HLT作用是关闭定时器,接入低电平,

74LS04有6个非门

这样一个电路需要用到三种芯片效率非常低,可以把电路给改一下

1593501323702

跟之前的效果一样,只用到了与非门

最终效果

1593501552521

1596857262654

1596856290057

总线

BUS的工作原理:

1593502854512

这8条线没有回路,可以跑1bit的数据这非常的灵活

Load:表示数据可以放到芯片中

Enable:表示数据从芯片放到Bus中

1593503069934

这里面边上的蓝色线就是控制线,可以看到这个控制线就是Clock,所有的部件同步Load

1593503208426

enable线来控制芯片将数据写到总线中,这需要同时只有1个芯片进行这样的操作,不然就会造成混乱

三态门

在总线中有一个非常重要的事情,就是同一时间只有一个部件向总线中输出数据,每个部件的输出端其实就是芯片内部门电路的输出端。

1593503744233

通常都会用两个这样输出,

1593504156671

三态门:,0,1,和断路三种状态

1593504288469

1593504362712

74LS245 8路三态门芯片

1593504498768

每个模块都接入一个Enable线,每个模块都接入Bus中,

同1时刻只有一个模块Enable线为true,就可以保证只有该数据写入到总线中。

当load为高电平的时候,它会在下一个时钟周期高电平到来的时候将总线中数据读取到模块中。

所有需要写入总线的模块都需要该245模块

寄存器

整个计算机需要8位寄存器A,8位寄存器B,4位计数器寄存器,8位指令寄存器

寄存器的构造是使用D锁存器,有高信号就可以保存住高信号

可以通过D触发器来构建寄存器,同时加入一个Load控制,下面这种是Load为0的情况,输出是什么输入还是什么

1593508139683

Load为1的情况,输入什么输出还是什么

1593508299194

74LS74内有2个D触发器

1593508533578

1593508684418

1593508886879

通过搭建上面的电路可以实现

1593508936549

数据不可以直接输出到总线中,需要在输出中加入74LS245 三态门

74LS173由4个D触发器,包含Load和Enable

1593587696438

1593587755157

因为需要外接小灯查看寄存器中的值,所以173芯片中的三态门一直处于打开状态,外界一个三态门来控制输出。

1593588901927

1593589161836

1596857057836

1596897104838

1596897167154

本计算机种需要用到三个相同原理的寄存器模块,寄存器A,寄存器B,指令寄存器。

指令寄存器就是与寄存器A的方向相反

1596897816078

ALU

补码

编码方式:

用最高位表示符号位,这样-5和5相加得2是不对的

1593590788676

另一种编码方式:得1补码:用反码表示负数

1593590880864

1593590946951

-5和5相加得到都是1,这就是得1补码的原因

1593591051997

比正确的结果少1;如果将结果加1就可以得到正确的结果

第三种编码方式:得2补码,反码+1表示负数

1593591153663

1593591276903

  • 每一位都有含义

    1593591514160

取反+1;

补码:取反+1表示负数,上面为解释为什么取反+1比较好。

全加器

1位加法运算,一共就8中情况,前四种不考虑前面的进位,后四种情况考虑一下之前的进位

结果有两位,第一位表示结算结果,第二位表示是否有进位

1593477274157

第一位前四种情况可以用异或门来表示

0,0 =》0

0,1=》1

1,0=》1

1,1=》0

1593477458141

第二位前四种情况可以用与门来表示

0,0=》0

0,1=》0

1,0=》0

1,1=》1

1593477943123

进位4种情况:可以发现第一位进位四种情况正好和之前的相反

那么进位的第一位变化的四种情况就可以直接在之前的结果后面加如一个异或门。异或门可以控制结果取反,

1593477962067

1593477980633

1593477989360

有进位的第二位四种情况,不仅要考虑本身有进位还要考虑第一位出现进位的情况

1593478123960

将进位情况求和

1593478310218

这个电路叫做1位全加器

1593479377583

每个全加器需要2个异或门,2个与门,1一个或门

1个异或门需要2个晶体管

1个与门需要2个晶体管

1个或门需要2个晶体管

那么可以总结出1个全加器需要10个晶体管,也就是10个三极管,也就是10个晶体管可以计算出1位计算器。

4个全加器组合成4位加法器

需要的材料和电路图

1593479516753

74LS86内有4个异或门芯片

74LS08内有4个与门芯片

74LS32内有4个或门

2个四位拨叉开关

1个面包板

4个小灯显示结果1个进位

1593481046309

ALU

Arithmetic Logic Unit:算术逻辑单元

该模块其实完全由全加器构成

用寄存器A和寄存器B,中间加入ALU逻辑电路,这样该模块就可以计算出寄存器A和寄存器B的求和或相减。

对寄存器中的数据进行操作

1593592572358

通过之前的全加器来构建逻辑单元 ,

如何做减法,

现在全加器可以实现加法,是否可以将被减数变成负数然后执行加法运算

1593592935597

通过异或门,当A为1的时候相当于取反,当A为0的时候原样输出

通过异或门获取反码

1593593651635

4位加法器有一个进位,将这个1和控制器连接起来,如果如果控制器是减法的话,那正好需要进位

这样就实现了一个数补码加1的操作。

1593593845194

1596859217576

1596859413998

1595080969662

中间的就是ALU

1593594182207

1596897448756

1593594254287

先要进行测试,测试是有必要的,

如果出现故障需要先排除故障,先从最简单的部分入手,然后慢慢缩小范围。

先设置A寄存器是0,B寄存器是0

1593595254896

然后让B存器器是0,然后让A每一位依次置1,查看是否有问题,发现问题然后跟踪这条线,

然后让A寄存器是0,然后B依次置1;

出现问题需要刨根问底将其找出来。

不要慌,从第一步开始的第一个异常,首先分析可能出现这个现象的原因,大多数情况下都想不出,

查看接线是否正常,接线正常后查看所有输出输入,特定的输入产生特定的输出,通过万用表量输入和输出电压。

1593595967786

将ALU中产生的数据直连到总线中,每当有脉冲的时候,A寄存器从总线中读取值,ALU从A中读值,从B中读值进行加操作,并将操作的结果放到总线中,1个脉冲实现加放到总线中读取总线数据的操作。

ROM

本计算机构建了16个字节的内存;

内存的构建有两种方式,

1.直接通过D锁存器构建

2.直接通过一个电容和一个晶体管构建,然后有一个电容不停刷新这个电容的数据。

1593653512871

1word的寄存器,1个字节寄存器,输入输出,写和读

16个字节

1593653574645

哪个字节的Enable开,哪个字节的数据就被读出来,

这样需要对16个字节进行编码

第一步

需要对16个字节进行编码,每个字节有8个D锁存器,也就是128个D锁存器

0-16这16个数字表示地址,也就是4个bit位,这样一个数字代表一个字节。

地址译码单元直接输出这个地址,地址译码单元怎么构造,首先需要有4个bit输入,每个输入有高低输出,然后构建一个有5个输入的与门,1位标识load,然后四位对应地址,那么就有16个5位输入与门,代表16个地址

1593652894450

这个地址电路应该在内存电路的前面,4个输入就可以让内存电路输出该地址的数据。

74LS189就是一个内存芯片,是一个64bit的存储器,有4个地址输入,16个地址位每个地址位4个输出,其使用的方式就是D寄存器的方式构建的内存

1595121792491

1593653727390

1593653794264

1596861778747

1596864742304

因为这边189的输出都是低电位有效,所以需要74LS04非门进行反转,最后接入一个245三态门输出到总线中

地址线需要处理,需求是:实现从总线中读取,或者手动设置。

通过4Bit寄存器来获得输入,地址寄存器。74LS173正好满足条件

1596865074587

地址输入

希望这个地址寄存器能切换模式手动模式和自动模式,自动模式是从总线中读取地址,手动模式用拨码开关来指定地址。

选择电路

1593655740684

74LS157可以实现二选一电路

1596865534393

1596865575731

1596865914587

对拨码开关的控制,可以获得1个明确0,1信号

1593656371930

1596897597639

值输入

希望可以手动向内存中写入值,同时也可以选择从总线中读入值。

又是一个选择电路,但是这边又8Bit输入,所以就用了2块74LS157芯片

1596866871402

1596867094024

1596897700121

到这可以控制手动输入地址和值的ROM就做好了

计数器

一个计算机仅仅只有脉冲是不可能正常运行的,必须还要有可以指示程序运行的计数器,指示程序运行到 了哪一步。

当我们从计算机中运行程序,这些程序放在内存中,它是一条条指令,为了执行这些指令需要从内存中读取它,在这个8位计算器中需要从地址0开始执行。先执行地址0的指令,然后执行地址1的指令,需要确定当前在哪个地址上执行,所以我们需要程序计数器。

1596868379690

在上面我们由JK触发器构造了一个计数器,这个程序技术器也是由4位组成

,指向下一条需要指向的指令,需要能从总线中读取数据 ,这样可以跳转到别的地址。

程序计数器的功能:

第一个CO就是程序控制器的输出,把值放到总线中

第二个J就是jump,从总线中读取数据,只获取4位数据,

第三个CE就是控制,控制计数器开始计数和停止计数。不一定每个脉冲都需要计数,当CE活动的时候,将计数器开始计数

二分电路

怎么把脉冲变成明确的计数信号呢?

这就需要之前的基础知识:主从触发器

主从触发器的特性,在一次脉冲来的时候会进行Q和反Q的切换,如果构建多个主从触发器,将第一个主从触发器的反Q接到下一个主从触发器的Q,会发生什么呢?

DM7476就是使用主从触发器来构造了JK触发器

1593665837408

可以发现这个JK触发器在下降沿的时候触发。

1596869990896

1596870028841

接了一个JK触发器可以看的更清楚一些,在每个脉冲周期,JK触发器交换了一次

1596870162055

当去掉一个显示的时候,可以发现这个Q亮到不亮再到亮用了2个脉冲周期

这个电路称为二分电路,通过JK触发器,将原来的主脉冲的周期扩大了一倍。

在原来二分电路的基础上再加一个二分JK触发器,把第一个触发器的输出接到下一个JK触发器的输入

1596870430039

第二个JK的转换速度是前一个的一半,是4倍的主脉冲周期

构建4个JK触发器,每一个都是前一个的周期的一半

1596870522435

这样我们就获得了一个2进制的计数器,可以从0计数到15,

计数器

本计算机的计数器就是使用了这一原理构建,这边我们使用74LS161作为计数器

1593668897652

其有4个输入,4个输出,是否写入控制线,CLock控制线,Enable输入输出控制线,清除控制线

这个芯片非常有用,它的Clock内部加了一个非门,这样上升沿变成下降沿,我们的JK触发器也是下降沿触发器

1593668916026

1596897924975

显示

共阴极和共阳极数码管

1593671218533

1593671296840

1593671307681

构建真值表

1593671395685

通过这个真值表可以获取a这个值什么时候亮

1593671381731

1593671495630

1593671522768

如果需要显示真正的数据,必须要建立一个真值表,将真值表转化成电路,这样的电路就是解析器,

EEPROM可以替代计算机中任何的组合逻辑。

组合逻辑:任何一个状态的输入对应一个状态的输出

时序逻辑:寄存器,锁存器,计数器,输出不进取决于当前的状态也取决于之前的状态。

有许多种ROM芯片,这个芯片是只读的,还有一种可以变成的只读芯片的就叫做PROM,提供了一个空白的芯片,只能写入一次,写入之后就不能改变了。EPROM可以重复写入,在紫外线的作用下可以擦除内部的数据

1593672678388

EEPROM是电可擦写存储器,用电就可以擦除。

AT28C16可擦写只读存储器,可以存2K个字节

1593673132260

有两种封装形式,直插和贴片,

8条IO引脚,数据引脚

11条地址引线,接地线和电源

反CE,反OE和反WE

1593673671134

需要给WE 一个100ns-1000ns的时间,

1593673640992

用一个电容和一个电阻来实现。RC震荡电路,

1nf,和680欧姆电阻。

1593673882492

1593673978192

通过EEPROM来实现真值表,左边是地址,右边的值。

Arduino写入数据

在这里插入图片描述

在这里插入图片描述

看以下Arduino Nano的引脚数根本不够,因为地址线11根,数据线8根

需要另选一个方案来向EPROM中写入数据。

通过一个引脚输出地址,8根引脚输出数据,1根引脚怎么输出数据呢

这边用到了8个D触发器,思路基本和计数器一样,只不过计数的Enable线就是脉冲线,这样脉冲来一次就+1;

这边的enable线是通过按钮输入,按下为1不按为0

这边用74LS74来构建,其有两个D触发器

1596899709888

1596899730900

用4个74芯片的D触发器输出连接到输入,构建了一个8位寄存器来获得8个连续的输入。

当脉冲来的时候按钮按下为输入1,不按为输入0

1596899208059

Arduino一根数据线输入数据问题解决就可以运用上面的思路,找到74HC595这个芯片

1596900171115

1596900195303

那么现在只需要3根线来控制数据输入,数据输入线DS,时钟线SH_CP,和控制输出线ST_CP

1596900574532

地址线有11条,所以需要2个595芯片

1596901226289

1596901745266

这样我们的Arduino写入EEPRom模块就做好了

现在来写程序吧;

//定义好各个引脚的标志
#define SHIFT_DATA 2  
#define SHIFT_CLK 3
#define SHIFT_LATCH 4
#define EEPROM_D0 5
#define EEPROM_D7 12
#define WRITE_EN 13
/*
 * 使用移位寄存器将地址数据输出
 */
void setAddress(int address, bool outputEnable) {
  shiftOut(SHIFT_DATA, SHIFT_CLK, MSBFIRST, (address >> 8) | (outputEnable ? 0x00 : 0x80));//将地址写入到595中,高8位
  shiftOut(SHIFT_DATA, SHIFT_CLK, MSBFIRST, address);//将地址写入到595中,低8位
//设置595输出地址
  digitalWrite(SHIFT_LATCH, LOW);
  digitalWrite(SHIFT_LATCH, HIGH);
  digitalWrite(SHIFT_LATCH, LOW);
}


/*
 * 从指定地址的EEPROM读取一个字节
 */
byte readEEPROM(int address) {
  for (int pin = EEPROM_D0; pin <= EEPROM_D7; pin++) {
    pinMode(pin, INPUT);
  }
  setAddress(address, /*outputEnable*/ true);

  byte data = 0;
  for (int pin = EEPROM_D7; pin >= EEPROM_D0; pin--) {
    data = (data << 1) + digitalRead(pin);
  }
  return data;
}


/*
 * 将字节写入指定地址的EEPROM。
 */
void writeEEPROM(int address, byte data) {
  setAddress(address, /*outputEnable*/ false);//设置地址到595中并输出地址
  for (int pin = EEPROM_D0; pin <= EEPROM_D7; pin++) {
    pinMode(pin, OUTPUT);//设置引脚
  }

  for (int pin = EEPROM_D0; pin <= EEPROM_D7; pin++) {
    digitalWrite(pin, data & 1);//将数据写到引脚中,只取最后一位
    data = data >> 1;
  }
  digitalWrite(WRITE_EN, LOW);//写入EMROM
  delayMicroseconds(1);
  digitalWrite(WRITE_EN, HIGH);
  delay(10);
}


/*
 * 读取EEPROM的内容并将其打印到串行监视器。
 */
void printContents() {
  for (int base = 0; base <= 255; base += 16) {
    byte data[16];
    for (int offset = 0; offset <= 15; offset++) {
      data[offset] = readEEPROM(base + offset);
    }

    char buf[80];
    sprintf(buf, "%03x:  %02x %02x %02x %02x %02x %02x %02x %02x   %02x %02x %02x %02x %02x %02x %02x %02x",
            base, data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7],
            data[8], data[9], data[10], data[11], data[12], data[13], data[14], data[15]);

    Serial.println(buf);
  }
}


// 用于共阳极7段显示的4位十六进制解码器
//byte data[] = { 0x81, 0xcf, 0x92, 0x86, 0xcc, 0xa4, 0xa0, 0x8f, 0x80, 0x84, 0x88, 0xe0, 0xb1, 0xc2, 0xb0, 0xb8 };

// 用于共阴极7段显示的4位十六进制解码器
 byte data[] = { 0x7e, 0x30, 0x6d, 0x79, 0x33, 0x5b, 0x5f, 0x70, 0x7f, 0x7b, 0x77, 0x1f, 0x4e, 0x3d, 0x4f, 0x47 };


void setup() {
  // put your setup code here, to run once:
  pinMode(SHIFT_DATA, OUTPUT);
  pinMode(SHIFT_CLK, OUTPUT);
  pinMode(SHIFT_LATCH, OUTPUT);
  digitalWrite(WRITE_EN, HIGH);//写低电平有效
  pinMode(WRITE_EN, OUTPUT);
  Serial.begin(57600);

  // Erase entire EEPROM
  Serial.print("擦除 EEPROM");
  for (int address = 0; address <= 2047; address ++) {
    writeEEPROM(address, 0x55);
    if (address % 64 == 0) {
       writeEEPROM(address, 0x55);
      Serial.print(".");
    }
  }
  Serial.println(" done");


  // 写入数据
  Serial.print("编辑 EEPROM");
  
  for (int address = 0; address < sizeof(data); address ++ ) {//sizeof(data)=16
    writeEEPROM(address, data[address]);

    if (address % 64 == 0) {//数据一共64Bit,
       writeEEPROM(address, data[address]);
      Serial.print(".");
    }
  }
  Serial.println(" 完成");


  // 读EEPROM中的值
  Serial.println("读.... EEPROM");
  printContents();
}


void loop() {
  // put your main code here, to run repeatedly:

}

1596902199407

1596902279049

1596902472107

重点看一下

/*
 * 使用移位寄存器将地址数据输出
 */
void setAddress(int address, bool outputEnable) {
  shiftOut(SHIFT_DATA, SHIFT_CLK, MSBFIRST, (address >> 8) | (outputEnable ? 0x00 : 0x80));
  shiftOut(SHIFT_DATA, SHIFT_CLK, MSBFIRST, address);

  digitalWrite(SHIFT_LATCH, LOW);
  digitalWrite(SHIFT_LATCH, HIGH);
  digitalWrite(SHIFT_LATCH, LOW);
}

shiftout:一次将数据字节移出一位。从最高(即最左边)或最低(最右边)有效位开始。每个位依次写入数据引脚,然后向时钟引脚脉冲(先变高,然后变低),以指示该位可用。

MSBFIRST:最高位有效在先

至此EEPEOM的真值表写入完毕,我们只使用了16个地址的数据,真是极大的浪费呢

如何显示数据

第一种方案是用三个EEPROM来表示百,十,个三个位的数据

1596904702816

这种方案显然造成EEPROM的极大浪费

第二种方案:复杂一点点,将选择这种方案,就是顺序让每一个数码管显示,当速度非常块的时候,数码管看上去就像一直显示的一样,怎么才能让数码管顺序显示

这边我们就用到了上面计数器的原理,构建一个单独的显示脉冲,然后通过2个JK触发器就可以获得4种不同的编码状态,00,01,10,11

这边用74LS76,其正好有两个JK触发器

1596905786007

同时需要将00,01,10,11进行解码,将其变成0001,0010,0100,1000,这样将这四条线连接到4个数码管,数码管就会顺序显示,这边我们用到了74LS139

1596905987400

1596906020693

可以看到该编码器完美满足我们的需求。

1596906109499

构建公用真值表

1596904888088

就是用A10,A9,A8,来表示个位十位百位

这样真值表就比较复杂了

举个例子321这个值的真值表:

1596905121518

改进程序

#define SHIFT_DATA 2
#define SHIFT_CLK 3
#define SHIFT_LATCH 4
#define EEPROM_D0 5
#define EEPROM_D7 12
#define WRITE_EN 13

/*
   使用移位寄存器输出地址位和outputEnable信号。
*/
void setAddress(int address, bool outputEnable) {
  shiftOut(SHIFT_DATA, SHIFT_CLK, MSBFIRST, (address >> 8) | (outputEnable ? 0x00 : 0x80));
  shiftOut(SHIFT_DATA, SHIFT_CLK, MSBFIRST, address);

  digitalWrite(SHIFT_LATCH, LOW);
  digitalWrite(SHIFT_LATCH, HIGH);
  digitalWrite(SHIFT_LATCH, LOW);
}


/*
   从指定地址的EEPROM读取一个字节。
*/
byte readEEPROM(int address) {
  for (int pin = EEPROM_D0; pin <= EEPROM_D7; pin += 1) {
    pinMode(pin, INPUT);
  }
  setAddress(address, /*outputEnable*/ true);

  byte data = 0;
  for (int pin = EEPROM_D7; pin >= EEPROM_D0; pin -= 1) {
    data = (data << 1) + digitalRead(pin);
  }
  return data;
}


/*
   将字节写入指定地址的EEPROM。
*/
void writeEEPROM(int address, byte data) {
  setAddress(address, /*outputEnable*/ false);
  for (int pin = EEPROM_D0; pin <= EEPROM_D7; pin += 1) {
    pinMode(pin, OUTPUT);
  }

  for (int pin = EEPROM_D0; pin <= EEPROM_D7; pin += 1) {
    digitalWrite(pin, data & 1);
    data = data >> 1;
  }
  digitalWrite(WRITE_EN, LOW);
  delayMicroseconds(1);
  digitalWrite(WRITE_EN, HIGH);
  delay(10);
}


/*
   读取EEPROM的内容并将其打印到串行监视器。
*/
void printContents() {
  for (int base = 0; base <= 255; base += 16) {
    byte data[16];
    for (int offset = 0; offset <= 15; offset += 1) {
      data[offset] = readEEPROM(base + offset);
    }

    char buf[80];
    sprintf(buf, "%03x:  %02x %02x %02x %02x %02x %02x %02x %02x   %02x %02x %02x %02x %02x %02x %02x %02x",
            base, data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7],
            data[8], data[9], data[10], data[11], data[12], data[13], data[14], data[15]);

    Serial.println(buf);
  }
}


void setup() {
  // put your setup code here, to run once:
  pinMode(SHIFT_DATA, OUTPUT);
  pinMode(SHIFT_CLK, OUTPUT);
  pinMode(SHIFT_LATCH, OUTPUT);
  digitalWrite(WRITE_EN, HIGH);
  pinMode(WRITE_EN, OUTPUT);
  Serial.begin(57600);


  // Bit patterns for the digits 0..9
  byte digits[] = { 0x7e, 0x30, 0x6d, 0x79, 0x33, 0x5b, 0x5f, 0x70, 0x7f, 0x7b };
   writeEEPROM(0,0);
  Serial.println("写入个位 ");
  
  for (int value = 0; value <= 255; value += 1) {
    writeEEPROM(value, digits[value % 10]);
  }
  Serial.println("写入十位");
  for (int value = 0; value <= 255; value += 1) {
    writeEEPROM(value + 256, digits[(value / 10) % 10]);
  }
  Serial.println("写入百位");
  for (int value = 0; value <= 255; value += 1) {
    writeEEPROM(value + 512, digits[(value / 100) % 10]);
  }
  Serial.println("写入符号位");
  for (int value = 0; value <= 255; value += 1) {
    writeEEPROM(value + 768, 0);
  }

  Serial.println("写入个位 (后半部)");
  for (int value = -128; value <= 127; value += 1) {
    writeEEPROM((byte)value + 1024, digits[abs(value) % 10]);
  }
  Serial.println("写入十位 (后半部)");
  for (int value = -128; value <= 127; value += 1) {
    writeEEPROM((byte)value + 1280, digits[abs(value / 10) % 10]);
  }
  Serial.println("写入百位 (后半部)");
  for (int value = -128; value <= 127; value += 1) {
    writeEEPROM((byte)value + 1536, digits[abs(value / 100) % 10]);
  }
  Serial.println("写入符号位 (后半部)");
  for (int value = -128; value <= 127; value += 1) {
    if (value < 0) {
      writeEEPROM((byte)value + 1792, 0x01);
    } else {
      writeEEPROM((byte)value + 1792, 0);
    }
  }

  // Read and print out the contents of the EERPROM
  Serial.println("读..... EEPROM");
  printContents();
}


void loop() {
  // put your main code here, to run repeatedly:

}

控制数据显示

现在数据显示的问题已经解决了,下面怎么控制其从Bus种读取数据显示,这边肯定不能直接显示总线的数据,因为总线的数据是不断变化的,所以需要一个8bit寄存器控制读取总线中的数据,然后控制其显示,

这边使用不同的芯片74LS273

1596937226761

1596937288930

1596937298968

这边有8个输入,8个输出,一个脉冲引脚,一个重置线

这边有一个问题,这个芯片没有IEnable线,如果主脉冲接进来,每次脉冲变化都会读取值,这个问题可以通过一个与门来解决,通过与门接入脉冲和控制线,控制线为1的时候,脉冲变化才有效

1596937862295

做个简单的总结,将已经做好的部件连接到总线

1596938308943

1596938572688

控制器

现在这个部件就缺少一个控制逻辑就可以正常工作了,来看看有多少个控制线

1596938678177

目前有14根控制线,还要做一个HTL停机线,在主脉冲中

1596939236718

如何控制

现在我们写一个程序,来手动运行这个程序

LDA 14   //将内存地址14中内容读取到A寄存器
ADD 15   //把内存地址15中内容与A寄存器中值相加放到寄存器
OUT      //把A寄存器中的内容放到输出模块

这会很奇怪,这些命令是哪里来的,在之前的计算机构造中没有构造任何与命令有关的内容,实际上这些是我们自己定义的,你可以定义任何想做的命令,这是不是非常酷。

下面我们来定义

LDA:0001

ADD:0010

OUT:1110

那么程序就被翻译成机器语言了

LADA 14   // 0001 1110
ADD  15   // 0010 1111
OUT       // 1110 xxxx

这个程序一共三行,我们在加上行号

LADA 14   // 0000 0001 1110
ADD  15   // 0001 0010 1111
OUT       // 0010 1110 xxxx

所以想要运行这个程序我们需要将值写到ROM中,进入手动模式输入ROM值

地址
0000 0001 1110
0001 0010 1111
0010 1110 0000
1110 0001 1100(28)
1111 0000 1110(14)

这个代码翻译成高级语言就是28+14=?

现在我们需要手动控制程序的运行

首先将指令从内存中读出来放到指令寄存器中,指令寄存器告诉我们数据将怎么解析。

取址周期就是将指令从内存中取出来放到指令寄存器中。

计算器中所有的组件都是由程序计数器来协调,计数器记录了当前执行到哪条指令。计数器是从0开始的。

一开始0000

  1. 首先将计数器的值放到内存地址寄存器中,

    1. 计数器输出+ CO
    2. 内存地址寄存器输入+ MI
    3. 给一个脉冲

    1596941320469

    可以看到这边计数器和内存地址寄存器都是0,

    而0地址上ROM的值就是0001 1110

  2. 将内存地址中的值放到指令寄存器中

    1. 将内存输出打开+ RO
    2. 指令寄存器输入+ II
    3. 给一个脉冲

    1596941570068

    可以看到ROM中数据给了指令寄存器

这两步操作取址的操作就完成了,要执行下一个代码,计数器加一

  1. 计数器加1 CE+

    1. 给一个脉冲,计数器加一变成0001

    1596941697583

    计数器加一

执行任何的代码都需要上面的三步,上面三步又称取址周期,其实就是将计数器对应的ROM中的值放到指令寄存器中,然后计数器加1。下面来解析命令和执行命令,这才是与命令相关的控制逻辑

LDA指令 LDA 14 ,控制器看到指令寄存器的高四位是0001,就知道这是对应LDA的操作,就会执行LDA的控制,这是由控制器完成的,我们稍后构建它,现在还是手动操作,假设自己的控制器

  1. 将指令寄存器后4BIt 输入到内存地址寄存器中 ,以获得内存地址14中的内容

    1. 指令寄存器输出 + IO
    2. 内存地址寄存器输入 + MI
    3. 给一个脉冲

    1596942075100

    因为指令寄存器只有第四位接入到总线中,所以地址寄存器获取第四位的地址数据,ROM中显示了该地址中的值,也就是0001 1100其值为28

  2. 将内存地址中的值输出到寄存器A

    1. 内存输出+ RO
    2. 寄存器A输入+ AI
    3. 给一个脉冲

    1596942293927

    可以看到内存中的值给了寄存器A,同时因为寄存器B位0,ALU就显示了A+0的值,

    至此完成了LDA的命令,将地址14中的值放到寄存器A中。下面执行第二个命令

ADD指令解析 ADD 15, 要执行到该指令现到取到该指令,跟之前的三部取址周期一样

1596942563558

指令计数器的值给地址寄存器

1596942622042

内存地址中的值给指令寄存器

1596942689773

计数器加1,这个时候控制器通过指令寄存器高四位0010分析出执行ADD控制

  1. 将指令寄存器后4bit输入到内存地址寄存器中

    1. 指令寄存器输出+ IO
    2. 内存地址寄存器输入+ MI
    3. 给一个脉冲

    1596942802043

    将指令寄存器中的低四位放到地址寄存器中,这个时候ROM显示该地址中的值 0000 1110 其值位14

  2. 将内存地址15中的值放到B寄存器中,ALU会自动计算出值

    1. 内存输出+ RO
    2. 寄存器B输入+ BI
    3. 给一个脉冲

    1596942944799

    可以看到ALU自动算出求和的值

  3. 将ALU中的值输出到寄存器A中

    1. ALU的输出 +EO
    2. 寄存器A输入+AI
    3. 给一个脉冲

    1596943047179

    这边寄存器A获得ALU的值,同时ALU更新了,这边非常酷,锁操作只发生在脉冲的上升沿,

OUT命令 OUT,前3步是一样的

1596943163560

1596943188827

1596943211095

  1. 将A寄存器中的值显示出来

    1. 将A寄存器输出+ AO
    2. output寄存器输入 OI
    3. 给一个脉冲

    1596943388471

到这程序执行完了

总结一下

1596943438035

这些小的指令称为微指令,这些微指令的前三步都是相同的,之后的操作是不同的,

所以需要控制位对每个指令构造控制逻辑

反正我控制位按照一定的顺序排序

每一种微指令对应一种控制序列。

真正的微指令会占用余下的时间片,实际上我们需要一个独立的计数器,所以需要一个独立的计数器

上面通过手动的方式设置控制位,然后手动发送一次主脉冲,在两个主脉冲之间改变它的控制位,,所以我们实际上还需要另一个脉冲来控制 ,这边可以用主脉冲的倒转,通过非门开获得另一个脉冲

这边还要将各个指令分步,才能够让控制器知道执行到了哪一步,可以看到每个指令最多5步,有些步数可以合并就合并了。从T0-T4,而有些指令用不到4步,那么多余的步数计算机什么也不做就浪费了。这是无法避免的

1596943665929

现在脉冲有了,步数分解有了,需要将脉冲变成步数,这和程序计数器是一样的,使用74LS161,这是一个四位的计数器,

1596944835708

1596944992084

1596945315281

计数器有了,现在要将计数器解码,这边用到了74LS138芯片,

1596946104190

1596946117754

可以看到其转换成明确信号,这边和显示部分用到的139解码是一样的逻辑

1596946666073

1596946961129

这边我们可以可以清晰的看到程序走到了哪个时间片,哪一步

下面我们构建非常酷的事情,也就是控制器的真值表

1596947212731

第一个取址,可以看到前两步,

第二个LDA用了剩余的三步,最后一步什么也没做。

第三个ADD也是三部

1596947378724

用两个28C16就可以完成其组合逻辑,其有11条地址线,8个输出线。

1596947463538

将真值表输入到28C16中就可以完成控制

Reset

1596949153296

这边如果程序执行完成,需要将所有的寄存器清空,这边我们构建这样一个reset电路用来一个74LS00来构建

1596949310054

将reset和~reset接到所有的寄存器

1596949536837

到目前为止,计算机的主体部分就做好了

Arduino写入指令

Arduino的接线方式和之前的显示解码器的方式相同,这边就不过多说了。

直接上程序

#define SHIFT_DATA 2
#define SHIFT_CLK 3
#define SHIFT_LATCH 4
#define EEPROM_D0 5         
#define EEPROM_D7 12
#define WRITE_EN 13

#define HLT 0b1000000000000000  // Halt clock                   HLT信号
#define MI  0b0100000000000000  // Memory address register in   内存地址输入
#define RI  0b0010000000000000  // RAM data in                  内存数据输入
#define RO  0b0001000000000000  // RAM data out                 内存数据输出
#define IO  0b0000100000000000  // Instruction register out     指令寄存器输出
#define II  0b0000010000000000  // Instruction register in      指令寄存器输入
#define AI  0b0000001000000000  // A register in                A寄存器输入
#define AO  0b0000000100000000  // A register out               A寄存器输出
#define EO  0b0000000010000000  // ALU out                      ALU输出
#define SU  0b0000000001000000  // ALU subtract                 减法
#define BI  0b0000000000100000  // B register in                B寄存器输入
#define OI  0b0000000000010000  // Output register in           输出寄存器输入
#define CE  0b0000000000001000  // Program counter enable       程序计数允许
#define CO  0b0000000000000100  // Program counter out          程序计数器输出
#define J   0b0000000000000010  // Jump (program counter in)    程序计数器输入(JUMP)

uint16_t data[] = {             // 列是步数,行是不同的指令
  MI|CO,  RO|II|CE,  0,      0,      0,         0, 0, 0,   // 0000 - NOP
  MI|CO,  RO|II|CE,  IO|MI,  RO|AI,  0,         0, 0, 0,   // 0001 - LDA 加载
  MI|CO,  RO|II|CE,  IO|MI,  RO|BI,  EO|AI,     0, 0, 0,   // 0010 - ADD 加法
  MI|CO,  RO|II|CE,  IO|MI,  RO|BI,  EO|AI|SU,  0, 0, 0,   // 0011 - SUB 减法
  MI|CO,  RO|II|CE,  IO|MI,  AO|RI,  0,         0, 0, 0,   // 0100 - STA 将寄存器A中值写入ROM中
  MI|CO,  RO|II|CE,  IO|AI,  0,      0,         0, 0, 0,   // 0101 - LDI 将指令寄存器中值写入寄存器A
  MI|CO,  RO|II|CE,  IO|J,   0,      0,         0, 0, 0,   // 0110 - JMP 跳转到指令寄存器第四位的计数
  MI|CO,  RO|II|CE,  0,      0,      0,         0, 0, 0,   // 0111
  MI|CO,  RO|II|CE,  0,      0,      0,         0, 0, 0,   // 1000
  MI|CO,  RO|II|CE,  0,      0,      0,         0, 0, 0,   // 1001
  MI|CO,  RO|II|CE,  0,      0,      0,         0, 0, 0,   // 1010
  MI|CO,  RO|II|CE,  0,      0,      0,         0, 0, 0,   // 1011
  MI|CO,  RO|II|CE,  0,      0,      0,         0, 0, 0,   // 1100
  MI|CO,  RO|II|CE,  0,      0,      0,         0, 0, 0,   // 1101
  MI|CO,  RO|II|CE,  AO|OI,  0,      0,         0, 0, 0,   // 1110 - OUT 输出
  MI|CO,  RO|II|CE,  HLT,    0,      0,         0, 0, 0,   // 1111 - HLT 停机
};


/*
 *使用移位寄存器输出地址位和outputEnable信号。
 */
void setAddress(int address, bool outputEnable) {
  shiftOut(SHIFT_DATA, SHIFT_CLK, MSBFIRST, (address >> 8) | (outputEnable ? 0x00 : 0x80));
  shiftOut(SHIFT_DATA, SHIFT_CLK, MSBFIRST, address);

  digitalWrite(SHIFT_LATCH, LOW);
  digitalWrite(SHIFT_LATCH, HIGH);
  digitalWrite(SHIFT_LATCH, LOW);
  
}


/*
 * 从指定地址的EEPROM读取一个字节。
 */
byte readEEPROM(int address) {
  for (int pin = EEPROM_D0; pin <= EEPROM_D7; pin += 1) {
    pinMode(pin, INPUT);
  }
  setAddress(address, /*outputEnable*/ true);

  byte data = 0;
  for (int pin = EEPROM_D7; pin >= EEPROM_D0; pin -= 1) {
    data = (data << 1) + digitalRead(pin);
  }
  return data;
}


/*
 * 将字节写入指定地址的EEPROM。
 */
void writeEEPROM(int address, byte data) {
  setAddress(address, /*outputEnable*/ false);//设置地址
  for (int pin = EEPROM_D0; pin <= EEPROM_D7; pin += 1) {
    pinMode(pin, OUTPUT);//设置数据输出引脚
  }

  for (int pin = EEPROM_D0; pin <= EEPROM_D7; pin += 1) {
    digitalWrite(pin, data & 1);//每个数据引脚赋值
    data = data >> 1;
  }
  digitalWrite(WRITE_EN, LOW);//设置脉冲
  delayMicroseconds(1);
  digitalWrite(WRITE_EN, HIGH);
  delay(10);
}


/*
 * 读取EEPROM的内容并将其打印到串行监视器。
 */
void printContents() {
  for (int base = 0; base <= 255; base += 16) {
    byte data[16];
    for (int offset = 0; offset <= 15; offset += 1) {
      data[offset] = readEEPROM(base + offset);
    }

    char buf[80];
    sprintf(buf, "%03x:  %02x %02x %02x %02x %02x %02x %02x %02x   %02x %02x %02x %02x %02x %02x %02x %02x",
            base, data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7],
            data[8], data[9], data[10], data[11], data[12], data[13], data[14], data[15]);

    Serial.println(buf);
  }
}


void setup() {
  // put your setup code here, to run once:
  pinMode(SHIFT_DATA, OUTPUT);
  pinMode(SHIFT_CLK, OUTPUT);
  pinMode(SHIFT_LATCH, OUTPUT);
  digitalWrite(WRITE_EN, HIGH);
  pinMode(WRITE_EN, OUTPUT);
  Serial.begin(57600);

  // 写数据
  Serial.print("写 EEPROM");
  writeEEPROM(0, 0);        
  // 将微码的8个高位写到EEPROM的前128个字节中
  for (int address = 0; address < sizeof(data)/sizeof(data[0]); address += 1) {
    writeEEPROM(address, data[address] >> 8);

    if (address % 64 == 0) {
       writeEEPROM(address, data[address] >> 8);
      Serial.print(".");
    }
  }

  // 将微码的8个低位写到EEPROM的前128个字节中
  for (int address = 0; address < sizeof(data)/sizeof(data[0]); address += 1) {
    writeEEPROM(address + 128, data[address]);

    if (address % 64 == 0) {
      writeEEPROM(address + 128, data[address]);
      Serial.print(".");
    }
  }

  Serial.println(" done");


  // 读并打印出EERPROM的内容
  Serial.println("读 EEPROM");
  printContents();
}


void loop() {
  // put your main code here, to run repeatedly:

}

添加了更多的指令 ,SUB,STA,LDI,JMP

这时候计算机可以做更多的功能了。

标志跳转

现在讨论一个问题:

这是不是计算机

这是不是计算机,还只是一个计算器

这个计算机的频率只有300HZ左右

是否需要乘法,指数,对数,三角函数等指令,这些指令肯定是做不出来的,那么问题就回来了我们真正需要什么样的指令,什么样的指令才能称为计算机,计算机是什么?

计算机:

可以完成任何的指令

可以完成任何的可计算的问题

什么是可计算的什么是不可计算的

这不是计算机性能的问题,是通过算法能完成的问题

那么问题就变成了我们需要完成什么样的算法。

1596950967140

这个问题在计算机早期图灵就进行研究过

1936年 他写了关于这个问题的一篇论文。这篇论文得出的结论是,他可以发明一种机器,可以完成任何计算序列

他是这样描述的:

1596951099319

有一个无限长的纸带,上面有方格,有1和0两种状态,有一个小旗子可以指向这些方格,小旗子有一个状态A,一次只能移动一个。

有一个小旗子和其状态的真值表

现在这个状态,A ,浏览状态是1,就将1写到袋子上,然后向左移动一格,自身的状态变成C,就变成了下面的状态

1596951323841

根据这个真值表进行一直不停的循环做,一旦停止到Halt,纸带上就是结果,

这个机器就能完成任何的数学算法。只需要设置好这个指令表就好了

实际上图灵还提高一个更好的计算机,称为通用计算机,这个机器上有一个指令表,是一个最基本的状态,其他计算机可以通过编码的方法将算法映射到这个指令表上

1596951496917

到这边就知道了任何可计算的问题都可以变成一个可计算的序列

在同一个时期邱奇也思考了相同的问题

1596951629710

他写了一篇论文关于什么是计算能力的定义,从完全不同的角度切入这个问题,他提出新的数学系统称为论的演算。

1596951770800

这便有一些变量,有一些函数,还有一些函数的结果

1596951837610

在论文的后面,他定义了一些函数,他用这个方法表达计算机,有点像现在的Lambda表达式

这篇论文的结论是:不是所有的问题都可以通过计算解决,有些可以,有些不可以,

在1936年两个人从两种不同的角度思考了这个问题

当图灵在8月份读到邱奇的论文,将邱奇的论文放到了附录中,任何问题可以转换成论的计算的问题都可以转化成一个可计算的问题

1596952089932

我们计算机和图灵机比较还缺少什么呢,图灵机有一个操作我们做不到,同一个指令可以有不同的操作

1596952196605

如果纸带是空格向右移动如果纸带为1向左移动,

有一种指令叫做有条件跳转指令可以做到这一点,它和我们的跳转指令有一点像,现在的跳转指令只能跳转到固定的地址

1596952407072

1596952609827

左右等价

根据不同的值来进行不同的行为

所以我们可以说如果实现条件跳转指令我们就可以模拟任何图灵机

1596952687837

条件跳转

准备实现两个条件跳转指令,为0跳转和进位跳转0

为0跳转,这个跳转需要计算ALU中所有的值是否为0 ,

1596952884977

1596953419345

使用这个电路我们就可以判断是否为0

74LS08有4个与门和74LS02有4个Nor门

1596953519894

1596953553123

进位跳转

ALU中高4位芯片有一个进位引脚,我们很容易就可以判断出是否进位了。

1596953063355

1596953205213

这边就搭建好了2个标识,但是有一个问题,

1596953868102

在获得这个标识后,加命令还有一步就是将ALU中的值放到寄存器A中,这样在进行跳转指令的时候标识就没有了,

所以这边需要将进位标识存起来,这边我们需要一个173芯片

其实Internal x86也有进位标识计数器

1596953993712

一共32位

1596954106259

这样就多了一个控制线,FI:标识Flag的输入,

1596954530834

这是新的真值表,用了10个地址位,非常棒

直接用Arduino写入真值表

#define SHIFT_DATA 2
#define SHIFT_CLK 3
#define SHIFT_LATCH 4
#define EEPROM_D0 5
#define EEPROM_D7 12
#define WRITE_EN 13

#define HLT 0b1000000000000000  // Halt clock                   HLT信号
#define MI  0b0100000000000000  // Memory address register in   内存地址输入
#define RI  0b0010000000000000  // RAM data in                  内存数据输入
#define RO  0b0001000000000000  // RAM data out                 内存数据输出
#define IO  0b0000100000000000  // Instruction register out     指令寄存器输出
#define II  0b0000010000000000  // Instruction register in      指令寄存器输入
#define AI  0b0000001000000000  // A register in                A寄存器输入
#define AO  0b0000000100000000  // A register out               A寄存器输出
#define EO  0b0000000010000000  // ALU out                      ALU输出
#define SU  0b0000000001000000  // ALU subtract                 减法
#define BI  0b0000000000100000  // B register in                B寄存器输入
#define OI  0b0000000000010000  // Output register in           输出寄存器输入
#define CE  0b0000000000001000  // Program counter enable       程序计数允许
#define CO  0b0000000000000100  // Program counter out          程序计数器输出
#define J   0b0000000000000010  // Jump (program counter in)    程序计数器输入(JUMP)
#define FI  0b0000000000000001  // Flags in                     Flags 标志位输入    

#define FLAGS_Z0C0 0
#define FLAGS_Z0C1 1
#define FLAGS_Z1C0 2
#define FLAGS_Z1C1 3

#define JC  0b0111
#define JZ  0b1000

uint16_t UCODE_TEMPLATE[16][8] = {
  { MI|CO,  RO|II|CE,  0,      0,      0,           0, 0, 0 },   // 0000 - NOP
  { MI|CO,  RO|II|CE,  IO|MI,  RO|AI,  0,           0, 0, 0 },   // 0001 - LDA
  { MI|CO,  RO|II|CE,  IO|MI,  RO|BI,  EO|AI|FI,    0, 0, 0 },   // 0010 - ADD
  { MI|CO,  RO|II|CE,  IO|MI,  RO|BI,  EO|AI|SU|FI, 0, 0, 0 },   // 0011 - SUB
  { MI|CO,  RO|II|CE,  IO|MI,  AO|RI,  0,           0, 0, 0 },   // 0100 - STA
  { MI|CO,  RO|II|CE,  IO|AI,  0,      0,           0, 0, 0 },   // 0101 - LDI
  { MI|CO,  RO|II|CE,  IO|J,   0,      0,           0, 0, 0 },   // 0110 - JMP
  { MI|CO,  RO|II|CE,  0,      0,      0,           0, 0, 0 },   // 0111 - JC
  { MI|CO,  RO|II|CE,  0,      0,      0,           0, 0, 0 },   // 1000 - JZ
  { MI|CO,  RO|II|CE,  0,      0,      0,           0, 0, 0 },   // 1001
  { MI|CO,  RO|II|CE,  0,      0,      0,           0, 0, 0 },   // 1010
  { MI|CO,  RO|II|CE,  0,      0,      0,           0, 0, 0 },   // 1011
  { MI|CO,  RO|II|CE,  0,      0,      0,           0, 0, 0 },   // 1100
  { MI|CO,  RO|II|CE,  0,      0,      0,           0, 0, 0 },   // 1101
  { MI|CO,  RO|II|CE,  AO|OI,  0,      0,           0, 0, 0 },   // 1110 - OUT
  { MI|CO,  RO|II|CE,  HLT,    0,      0,           0, 0, 0 },   // 1111 - HLT
};

uint16_t ucode[4][16][8];//主要把指令根据进位划分一下

void initUCode() {
  // ZF = 0, CF = 0
  memcpy(ucode[FLAGS_Z0C0], UCODE_TEMPLATE, sizeof(UCODE_TEMPLATE));

  // ZF = 0, CF = 1
  memcpy(ucode[FLAGS_Z0C1], UCODE_TEMPLATE, sizeof(UCODE_TEMPLATE));
  ucode[FLAGS_Z0C1][JC][2] = IO|J;

  // ZF = 1, CF = 0
  memcpy(ucode[FLAGS_Z1C0], UCODE_TEMPLATE, sizeof(UCODE_TEMPLATE));
  ucode[FLAGS_Z1C0][JZ][2] = IO|J;

  // ZF = 1, CF = 1
  memcpy(ucode[FLAGS_Z1C1], UCODE_TEMPLATE, sizeof(UCODE_TEMPLATE));
  ucode[FLAGS_Z1C1][JC][2] = IO|J;
  ucode[FLAGS_Z1C1][JZ][2] = IO|J;
}

/*
 * 使用移位寄存器输出地址位和outputEnable信号。
 */
void setAddress(int address, bool outputEnable) {
  shiftOut(SHIFT_DATA, SHIFT_CLK, MSBFIRST, (address >> 8) | (outputEnable ? 0x00 : 0x80));
  shiftOut(SHIFT_DATA, SHIFT_CLK, MSBFIRST, address);

  digitalWrite(SHIFT_LATCH, LOW);
  digitalWrite(SHIFT_LATCH, HIGH);
  digitalWrite(SHIFT_LATCH, LOW);
}


/*
 * 从指定地址的EEPROM读取一个字节。
 */
byte readEEPROM(int address) {
  for (int pin = EEPROM_D0; pin <= EEPROM_D7; pin += 1) {
    pinMode(pin, INPUT);
  }
  setAddress(address, /*outputEnable*/ true);

  byte data = 0;
  for (int pin = EEPROM_D7; pin >= EEPROM_D0; pin -= 1) {
    data = (data << 1) + digitalRead(pin);
  }
  return data;
}


/*
 * 将字节写入指定地址的EEPROM。
 */
void writeEEPROM(int address, byte data) {
  setAddress(address, /*outputEnable*/ false);
  for (int pin = EEPROM_D0; pin <= EEPROM_D7; pin += 1) {
    pinMode(pin, OUTPUT);
  }

  for (int pin = EEPROM_D0; pin <= EEPROM_D7; pin += 1) {
    digitalWrite(pin, data & 1);
    data = data >> 1;
  }
  digitalWrite(WRITE_EN, LOW);
  delayMicroseconds(1);
  digitalWrite(WRITE_EN, HIGH);
  delay(10);
}


/*
 *读取EEPROM的内容并将其打印到串行监视器。
 */
void printContents(int start, int length) {
  for (int base = start; base < length; base += 16) {
    byte data[16];
    for (int offset = 0; offset <= 15; offset += 1) {
      data[offset] = readEEPROM(base + offset);
    }

    char buf[80];
    sprintf(buf, "%03x:  %02x %02x %02x %02x %02x %02x %02x %02x   %02x %02x %02x %02x %02x %02x %02x %02x",
            base, data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7],
            data[8], data[9], data[10], data[11], data[12], data[13], data[14], data[15]);

    Serial.println(buf);
  }
}


void setup() {
  // put your setup code here, to run once:
  initUCode();

  pinMode(SHIFT_DATA, OUTPUT);
  pinMode(SHIFT_CLK, OUTPUT);
  pinMode(SHIFT_LATCH, OUTPUT);
  digitalWrite(WRITE_EN, HIGH);
  pinMode(WRITE_EN, OUTPUT);
  Serial.begin(57600);

  // Program data bytes
  Serial.print("写 EEPROM");

 // 将微码的8个高位写到EEPROM的前128个字节中
  writeEEPROM(0,0);
  for (int address = 0; address < 1024; address += 1) {
    int flags       = (address & 0b1100000000) >> 8;//flag标识
    int byte_sel    = (address & 0b0010000000) >> 7;//高低位标识
    int instruction = (address & 0b0001111000) >> 3;//指令
    int step        = (address & 0b0000000111);//步数

    if (byte_sel) {//高低位
      writeEEPROM(address, ucode[flags][instruction][step]);
    } else {
      writeEEPROM(address, ucode[flags][instruction][step] >> 8);
    }

    if (address % 64 == 0) {
       if (byte_sel) {
          writeEEPROM(address, ucode[flags][instruction][step]);
       } else {
         writeEEPROM(address, ucode[flags][instruction][step] >> 8);
       }
      Serial.print(".");
    }
  }

  Serial.println(" done");


  // Read and print out the contents of the EERPROM
  Serial.println("读 EEPROM");
  printContents(0, 1024);
}


void loop() {
  // put your main code here, to run repeatedly:

}

到这就做好了。

总结

我收获了什么:

计算机底层是怎么运行,控制器是怎么控制

调试的时候也遇到一些坑

寄存器没有正常工作

指令计数器工作正常,寄存器A和寄存器B工作不正常,这三个模块是同一个脉冲线接过来的,先接入指令计数器,再接入寄存器A和寄存器B,

一开始并没有怀疑脉冲线的问题,因为指令计数器正常工作,寄存器没有正常工作,检查了寄存器的接线发现没有问题,量了电压发现脉冲电压非常小0.02V波动,这也太不正常了,量了下指令计数器的电压是正常的,这就很奇怪了,后来发现最后寄存器脉冲线短路接地了,导致一直没有脉冲,

控制器没有正常工作

发现控制器是输出不正常,做了个简单的测试电路,手动检查控制器的eprom内存的值,发现确实没有输出正确的值,检查Arduino nano的写入接线和视频中接线不同,导致写入数据地址也不相同,调整Arduino nano和控制线,输出正常,

经验

  1. 每个模块先用跳线接一下再进行测试,如果发现测试没有问题再用标准接线将其接通,
  2. 正常调试需要一步步执行,当出现异常了先解决出现的第一个异常,然后再解决剩余的异常,遇到异常不要慌,一步步解决,不要跳过问题进行下一个问题。

引用

大佬的视频教程,截图基本都源自于该大佬,并稍加改动

https://space.bilibili.com/413461202/

推荐阅读