在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年,德国科学家发现晶体的整流功能
由半导体硅材料制成,硅本身是没有电极的,在做晶体管的时候做了杂化处理,在一段加入了硼,一段加入了磷,硼这端会多出电子空穴,而磷这一端会多出自由电子,有意思的事情就发生了。
因为Si是4个电子,P有3个电子,N有5个电子,所以单纯的硅会形成4个共价键非常稳定。
硅:
磷:N
硼:P
当杂化之后,P端就会有很多电子空穴,N端会多出很多自由电子,在PN交界的地方,N端电子会自动移动到P端,形成一个耗尽区,耗尽区的电压为0.7V,所以大多5V的芯片低电压为0.2V,如果超过0.7V则视为高电压
如果加入正电压会使耗尽区扩大,造成正向偏压,如果加入反向电压,大于耗尽区0.7V电压的时候,电子从N极向P极移动没有任何障碍。
绘出曲线,横坐标是电源电压,纵坐标是电流,负向电压的时候几乎没有电流,负向电压特别大的时候会击穿,正向电压大于0.7V的时候会很快获得很大的电流。
二极管的这一特性可以做一个桥式整流电路
三极管
三极管就是二极管的升级,例如NPN型三极管
这样在NP的交界处就会形成两个耗尽区,
可以看成两个二极管背靠背相连,不管电源处于哪个状态总有一个二极管处于反向加压的状态,不导通。但是如果中间加一个电源(第二个电源),大量电子会从P端出来,通过电源到达N端形成通路
形成通路后,大量电子会到P端,形成反向偏压,
如果整体来看,P端非常的窄,并不会存储大量电子,大量电子在第一个电源的驱动下回到电源,形成电流,因为第一个电源的电压比较大,驱动力比较大,第二个电源电压比较小,驱动力比较小
这种现象简而言之就是
- 一个小电流被放大成一个大电流,
- 一个断路变成一个通路
这种晶体管叫双极结晶体管,
晶体管有两种工作方式:
- 通过电流,将一个小电流放大成大电流,
- 通过电压,只要基极和发射机有电势差,集电极和发射极就会产生大电流,这种又叫场效应管
双极结型晶体管做的放大电路
门电路
晶体管的基本原理已经知道了,门电路就是基于三极管构成相关电路
非门电路:
与门电路:
或门电路
异或门
锁存器
锁存器用来做寄存器
将或门改造一下就可以就是SR锁存器
SR锁存器
再次进阶D锁存器,D锁存器是构建寄存器的基础,本计算机种所有的寄存器都是由D锁存器构造
触发器
触发器是为了获取极短时间内的上升沿
第一种方法:
从0变成1的时候,非门需要几个纳秒的时候才能将状态转过来,所以在非常短的时候内会出现都为1,这个时候与门输出1,然后非门后的状态0输入,导致输出变为0,这样输出只有几个纳秒是1。
第二种方法:
通过电容来实现
电容和电阻,当信号来的时候电容充电,获得输出1,当几十纳秒后,电容充满电,信号就变成0了
计算时间
D触发器,就是将之前的SR锁存器的Enable改造一下
SR触发器:
SR触发器,在SR都为1的时候,处于一种无效的状态,没有任何输出。当SR变成0的时候,谁慢一点谁就会被触发。这是一种随机状态。
为了解决这个问题:
第一种情况 JK都为0,这是一种随机状态,也成为不确定状态
第二种状态K=1,J=0的时候,处于reset状态,Q=0,反Q=1
第三种状态K=0,J=1的时候,处于set状态Q=1,反Q=0
最有意思的是第四种状态K=1,J=1的时候,信号会发生一次对调
这样会出现问题
在这个脉冲内做了很多次转换,也就是只要两个输入都是高电平,这个转换就一直持续。
这种情况叫做抢先。
所以发现这个根本原因出现这个脉冲电路上,这个上升沿时间太多了。如果时间控制在100ns的时间内就可以只完成1次转换。
把1K电阻换成100电阻,已经控制了100ns的时间,发现还是不行
因为信号有抖动,边缘探测不锐利
用主从JK触发器来解决这个问题
高电压的时候使第一个锁存器工作,在低电压的时候使第二个锁存器工作。
这样就完全可以避免之前的问题
可以看到这有两个锁存器,这两个锁存器不可能同时工作,clock高电位第一个锁存器工作,clock低电位第二个锁存器工作,主从对应的RS正好相反
如果高电压,主锁存器是SET,到低电压的时候从锁存器就是reset,
如果都是1的时候,那么主锁存器执行的操作是由从锁存器的状态决定的,而从锁存器的状态正好与主锁存器状态相反
这样当一个脉冲来的时候,set和reset会执行一次交换。
基本模块
计算机需要的模块:1.主脉冲,2.计数器,3.计数器寄存器,4.寄存器A,5.寄存器B,6.ROM,7.指令寄存器,9.显示模块,9.控制模块,10.标志位寄存器。
主脉冲模块
主脉冲
主脉冲使用555芯片
时许分析
开始的时候,没有上电
开始上电的时候
通过电容放电和充电的时间来控制方波的占空比,
外界的电容和电阻决定了方波的长度
通过公式来计算
总的时间是0.139S
在5号引脚加入一个0.01uf的电容接地,可以降噪
当有信号的时候,一堆晶体管需要获取更多的电量,这个时候就会从电源端拉出更多的电流,就会形成电路中非常常见的过充的现象。
电线也会产生一些阻抗,也会阻止电流的变化,所以这个电压就会跳上去,
直接的办法给电路接一个非常短的线路
给正极和负极加一个电容,在电路需要电流的时候给电路提供更多的电流。
在四号引脚接入一个5V高电平,防止Reset锁存器,这样就不存在误操作。
调整时钟的速度,把100K换成可变电阻
单步脉冲
为了更好的测试电路,需要有一个单步脉冲,类似程序的单步执行,按钮按一下给一个脉冲
单步脉冲的意思是按1下产生1个脉冲,用555芯片来消除按钮的抖动
555芯片,消除抖动电路,可以控制灯亮的时间
电阻是1M,电容是2uf,0.1uf,0.1S时间间隔,这边要注意在电路不同的状态,6,7的电压应该是5V,
稳态和单稳态
切换电路
将两个状态的输出型号添加到一个开关中,切换开关可以切换2个状态、
但是开关会有一个新的问题,当切换的时候有一个延迟的问题,这个时候需要一个新的555芯片来解决这个问题,其实是用到555芯片内的SR锁存器
开关有一个特性叫做先断后连,
这个电路主要是解决开关弹跳的问题,
将这三个电路合并起来
这样就可以在自动和手动切换
HLT作用是关闭定时器,接入低电平,
74LS04有6个非门
这样一个电路需要用到三种芯片效率非常低,可以把电路给改一下
跟之前的效果一样,只用到了与非门
最终效果
总线
BUS的工作原理:
这8条线没有回路,可以跑1bit的数据这非常的灵活
Load:表示数据可以放到芯片中
Enable:表示数据从芯片放到Bus中
这里面边上的蓝色线就是控制线,可以看到这个控制线就是Clock,所有的部件同步Load
enable线来控制芯片将数据写到总线中,这需要同时只有1个芯片进行这样的操作,不然就会造成混乱
三态门
在总线中有一个非常重要的事情,就是同一时间只有一个部件向总线中输出数据,每个部件的输出端其实就是芯片内部门电路的输出端。
通常都会用两个这样输出,
三态门:,0,1,和断路三种状态
74LS245 8路三态门芯片
每个模块都接入一个Enable线,每个模块都接入Bus中,
同1时刻只有一个模块Enable线为true,就可以保证只有该数据写入到总线中。
当load为高电平的时候,它会在下一个时钟周期高电平到来的时候将总线中数据读取到模块中。
所有需要写入总线的模块都需要该245模块
寄存器
整个计算机需要8位寄存器A,8位寄存器B,4位计数器寄存器,8位指令寄存器
寄存器的构造是使用D锁存器,有高信号就可以保存住高信号
可以通过D触发器来构建寄存器,同时加入一个Load控制,下面这种是Load为0的情况,输出是什么输入还是什么
Load为1的情况,输入什么输出还是什么
74LS74内有2个D触发器
通过搭建上面的电路可以实现
数据不可以直接输出到总线中,需要在输出中加入74LS245 三态门
74LS173由4个D触发器,包含Load和Enable
因为需要外接小灯查看寄存器中的值,所以173芯片中的三态门一直处于打开状态,外界一个三态门来控制输出。
本计算机种需要用到三个相同原理的寄存器模块,寄存器A,寄存器B,指令寄存器。
指令寄存器就是与寄存器A的方向相反
ALU
补码
编码方式:
用最高位表示符号位,这样-5和5相加得2是不对的
另一种编码方式:得1补码:用反码表示负数
-5和5相加得到都是1,这就是得1补码的原因
比正确的结果少1;如果将结果加1就可以得到正确的结果
第三种编码方式:得2补码,反码+1表示负数
取反+1;
补码:取反+1表示负数,上面为解释为什么取反+1比较好。
全加器
1位加法运算,一共就8中情况,前四种不考虑前面的进位,后四种情况考虑一下之前的进位
结果有两位,第一位表示结算结果,第二位表示是否有进位
第一位前四种情况可以用异或门来表示
0,0 =》0
0,1=》1
1,0=》1
1,1=》0
第二位前四种情况可以用与门来表示
0,0=》0
0,1=》0
1,0=》0
1,1=》1
进位4种情况:可以发现第一位进位四种情况正好和之前的相反
那么进位的第一位变化的四种情况就可以直接在之前的结果后面加如一个异或门。异或门可以控制结果取反,
有进位的第二位四种情况,不仅要考虑本身有进位还要考虑第一位出现进位的情况
将进位情况求和
这个电路叫做1位全加器
每个全加器需要2个异或门,2个与门,1一个或门
1个异或门需要2个晶体管
1个与门需要2个晶体管
1个或门需要2个晶体管
那么可以总结出1个全加器需要10个晶体管,也就是10个三极管,也就是10个晶体管可以计算出1位计算器。
4个全加器组合成4位加法器
需要的材料和电路图
74LS86内有4个异或门芯片
74LS08内有4个与门芯片
74LS32内有4个或门
2个四位拨叉开关
1个面包板
4个小灯显示结果1个进位
ALU
Arithmetic Logic Unit:算术逻辑单元
该模块其实完全由全加器构成
用寄存器A和寄存器B,中间加入ALU逻辑电路,这样该模块就可以计算出寄存器A和寄存器B的求和或相减。
对寄存器中的数据进行操作
通过之前的全加器来构建逻辑单元 ,
如何做减法,
现在全加器可以实现加法,是否可以将被减数变成负数然后执行加法运算
通过异或门,当A为1的时候相当于取反,当A为0的时候原样输出
通过异或门获取反码
4位加法器有一个进位,将这个1和控制器连接起来,如果如果控制器是减法的话,那正好需要进位
这样就实现了一个数补码加1的操作。
中间的就是ALU
先要进行测试,测试是有必要的,
如果出现故障需要先排除故障,先从最简单的部分入手,然后慢慢缩小范围。
先设置A寄存器是0,B寄存器是0
然后让B存器器是0,然后让A每一位依次置1,查看是否有问题,发现问题然后跟踪这条线,
然后让A寄存器是0,然后B依次置1;
出现问题需要刨根问底将其找出来。
不要慌,从第一步开始的第一个异常,首先分析可能出现这个现象的原因,大多数情况下都想不出,
查看接线是否正常,接线正常后查看所有输出输入,特定的输入产生特定的输出,通过万用表量输入和输出电压。
将ALU中产生的数据直连到总线中,每当有脉冲的时候,A寄存器从总线中读取值,ALU从A中读值,从B中读值进行加操作,并将操作的结果放到总线中,1个脉冲实现加放到总线中读取总线数据的操作。
ROM
本计算机构建了16个字节的内存;
内存的构建有两种方式,
1.直接通过D锁存器构建
2.直接通过一个电容和一个晶体管构建,然后有一个电容不停刷新这个电容的数据。
1word的寄存器,1个字节寄存器,输入输出,写和读
16个字节
哪个字节的Enable开,哪个字节的数据就被读出来,
这样需要对16个字节进行编码
第一步
需要对16个字节进行编码,每个字节有8个D锁存器,也就是128个D锁存器
0-16这16个数字表示地址,也就是4个bit位,这样一个数字代表一个字节。
地址译码单元直接输出这个地址,地址译码单元怎么构造,首先需要有4个bit输入,每个输入有高低输出,然后构建一个有5个输入的与门,1位标识load,然后四位对应地址,那么就有16个5位输入与门,代表16个地址
这个地址电路应该在内存电路的前面,4个输入就可以让内存电路输出该地址的数据。
74LS189就是一个内存芯片,是一个64bit的存储器,有4个地址输入,16个地址位每个地址位4个输出,其使用的方式就是D寄存器的方式构建的内存
因为这边189的输出都是低电位有效,所以需要74LS04非门进行反转,最后接入一个245三态门输出到总线中
地址线需要处理,需求是:实现从总线中读取,或者手动设置。
通过4Bit寄存器来获得输入,地址寄存器。74LS173正好满足条件
地址输入
希望这个地址寄存器能切换模式手动模式和自动模式,自动模式是从总线中读取地址,手动模式用拨码开关来指定地址。
选择电路
74LS157可以实现二选一电路
对拨码开关的控制,可以获得1个明确0,1信号
值输入
希望可以手动向内存中写入值,同时也可以选择从总线中读入值。
又是一个选择电路,但是这边又8Bit输入,所以就用了2块74LS157芯片
到这可以控制手动输入地址和值的ROM就做好了
计数器
一个计算机仅仅只有脉冲是不可能正常运行的,必须还要有可以指示程序运行的计数器,指示程序运行到 了哪一步。
当我们从计算机中运行程序,这些程序放在内存中,它是一条条指令,为了执行这些指令需要从内存中读取它,在这个8位计算器中需要从地址0开始执行。先执行地址0的指令,然后执行地址1的指令,需要确定当前在哪个地址上执行,所以我们需要程序计数器。
在上面我们由JK触发器构造了一个计数器,这个程序技术器也是由4位组成
,指向下一条需要指向的指令,需要能从总线中读取数据 ,这样可以跳转到别的地址。
程序计数器的功能:
第一个CO就是程序控制器的输出,把值放到总线中
第二个J就是jump,从总线中读取数据,只获取4位数据,
第三个CE就是控制,控制计数器开始计数和停止计数。不一定每个脉冲都需要计数,当CE活动的时候,将计数器开始计数
二分电路
怎么把脉冲变成明确的计数信号呢?
这就需要之前的基础知识:主从触发器
主从触发器的特性,在一次脉冲来的时候会进行Q和反Q的切换,如果构建多个主从触发器,将第一个主从触发器的反Q接到下一个主从触发器的Q,会发生什么呢?
DM7476就是使用主从触发器来构造了JK触发器
可以发现这个JK触发器在下降沿的时候触发。
接了一个JK触发器可以看的更清楚一些,在每个脉冲周期,JK触发器交换了一次
当去掉一个显示的时候,可以发现这个Q亮到不亮再到亮用了2个脉冲周期
这个电路称为二分电路,通过JK触发器,将原来的主脉冲的周期扩大了一倍。
在原来二分电路的基础上再加一个二分JK触发器,把第一个触发器的输出接到下一个JK触发器的输入
第二个JK的转换速度是前一个的一半,是4倍的主脉冲周期
构建4个JK触发器,每一个都是前一个的周期的一半
这样我们就获得了一个2进制的计数器,可以从0计数到15,
计数器
本计算机的计数器就是使用了这一原理构建,这边我们使用74LS161作为计数器
其有4个输入,4个输出,是否写入控制线,CLock控制线,Enable输入输出控制线,清除控制线
这个芯片非常有用,它的Clock内部加了一个非门,这样上升沿变成下降沿,我们的JK触发器也是下降沿触发器
显示
共阴极和共阳极数码管
构建真值表
通过这个真值表可以获取a这个值什么时候亮
如果需要显示真正的数据,必须要建立一个真值表,将真值表转化成电路,这样的电路就是解析器,
EEPROM可以替代计算机中任何的组合逻辑。
组合逻辑:任何一个状态的输入对应一个状态的输出
时序逻辑:寄存器,锁存器,计数器,输出不进取决于当前的状态也取决于之前的状态。
有许多种ROM芯片,这个芯片是只读的,还有一种可以变成的只读芯片的就叫做PROM,提供了一个空白的芯片,只能写入一次,写入之后就不能改变了。EPROM可以重复写入,在紫外线的作用下可以擦除内部的数据
EEPROM是电可擦写存储器,用电就可以擦除。
AT28C16可擦写只读存储器,可以存2K个字节
有两种封装形式,直插和贴片,
8条IO引脚,数据引脚
11条地址引线,接地线和电源
反CE,反OE和反WE
需要给WE 一个100ns-1000ns的时间,
用一个电容和一个电阻来实现。RC震荡电路,
1nf,和680欧姆电阻。
通过EEPROM来实现真值表,左边是地址,右边的值。
Arduino写入数据
看以下Arduino Nano的引脚数根本不够,因为地址线11根,数据线8根
需要另选一个方案来向EPROM中写入数据。
通过一个引脚输出地址,8根引脚输出数据,1根引脚怎么输出数据呢
这边用到了8个D触发器,思路基本和计数器一样,只不过计数的Enable线就是脉冲线,这样脉冲来一次就+1;
这边的enable线是通过按钮输入,按下为1不按为0
这边用74LS74来构建,其有两个D触发器
用4个74芯片的D触发器输出连接到输入,构建了一个8位寄存器来获得8个连续的输入。
当脉冲来的时候按钮按下为输入1,不按为输入0
Arduino一根数据线输入数据问题解决就可以运用上面的思路,找到74HC595这个芯片
那么现在只需要3根线来控制数据输入,数据输入线DS,时钟线SH_CP,和控制输出线ST_CP
地址线有11条,所以需要2个595芯片
这样我们的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:
}
重点看一下
/*
* 使用移位寄存器将地址数据输出
*/
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来表示百,十,个三个位的数据
这种方案显然造成EEPROM的极大浪费
第二种方案:复杂一点点,将选择这种方案,就是顺序让每一个数码管显示,当速度非常块的时候,数码管看上去就像一直显示的一样,怎么才能让数码管顺序显示
这边我们就用到了上面计数器的原理,构建一个单独的显示脉冲,然后通过2个JK触发器就可以获得4种不同的编码状态,00,01,10,11
这边用74LS76,其正好有两个JK触发器
同时需要将00,01,10,11进行解码,将其变成0001,0010,0100,1000,这样将这四条线连接到4个数码管,数码管就会顺序显示,这边我们用到了74LS139
可以看到该编码器完美满足我们的需求。
构建公用真值表
就是用A10,A9,A8,来表示个位十位百位
这样真值表就比较复杂了
举个例子321这个值的真值表:
改进程序
#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
这边有8个输入,8个输出,一个脉冲引脚,一个重置线
这边有一个问题,这个芯片没有IEnable线,如果主脉冲接进来,每次脉冲变化都会读取值,这个问题可以通过一个与门来解决,通过与门接入脉冲和控制线,控制线为1的时候,脉冲变化才有效
做个简单的总结,将已经做好的部件连接到总线
控制器
现在这个部件就缺少一个控制逻辑就可以正常工作了,来看看有多少个控制线
目前有14根控制线,还要做一个HTL停机线,在主脉冲中
如何控制
现在我们写一个程序,来手动运行这个程序
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
-
首先将计数器的值放到内存地址寄存器中,
- 计数器输出+ CO
- 内存地址寄存器输入+ MI
- 给一个脉冲
可以看到这边计数器和内存地址寄存器都是0,
而0地址上ROM的值就是0001 1110
-
将内存地址中的值放到指令寄存器中
- 将内存输出打开+ RO
- 指令寄存器输入+ II
- 给一个脉冲
可以看到ROM中数据给了指令寄存器
这两步操作取址的操作就完成了,要执行下一个代码,计数器加一
-
计数器加1 CE+
- 给一个脉冲,计数器加一变成0001
计数器加一
执行任何的代码都需要上面的三步,上面三步又称取址周期,其实就是将计数器对应的ROM中的值放到指令寄存器中,然后计数器加1。下面来解析命令和执行命令,这才是与命令相关的控制逻辑
LDA指令 LDA 14 ,控制器看到指令寄存器的高四位是0001,就知道这是对应LDA的操作,就会执行LDA的控制,这是由控制器完成的,我们稍后构建它,现在还是手动操作,假设自己的控制器
-
将指令寄存器后4BIt 输入到内存地址寄存器中 ,以获得内存地址14中的内容
- 指令寄存器输出 + IO
- 内存地址寄存器输入 + MI
- 给一个脉冲
因为指令寄存器只有第四位接入到总线中,所以地址寄存器获取第四位的地址数据,ROM中显示了该地址中的值,也就是0001 1100其值为28
-
将内存地址中的值输出到寄存器A
- 内存输出+ RO
- 寄存器A输入+ AI
- 给一个脉冲
可以看到内存中的值给了寄存器A,同时因为寄存器B位0,ALU就显示了A+0的值,
至此完成了LDA的命令,将地址14中的值放到寄存器A中。下面执行第二个命令
ADD指令解析 ADD 15, 要执行到该指令现到取到该指令,跟之前的三部取址周期一样
指令计数器的值给地址寄存器
内存地址中的值给指令寄存器
计数器加1,这个时候控制器通过指令寄存器高四位0010分析出执行ADD控制
-
将指令寄存器后4bit输入到内存地址寄存器中
- 指令寄存器输出+ IO
- 内存地址寄存器输入+ MI
- 给一个脉冲
将指令寄存器中的低四位放到地址寄存器中,这个时候ROM显示该地址中的值 0000 1110 其值位14
-
将内存地址15中的值放到B寄存器中,ALU会自动计算出值
- 内存输出+ RO
- 寄存器B输入+ BI
- 给一个脉冲
可以看到ALU自动算出求和的值
-
将ALU中的值输出到寄存器A中
- ALU的输出 +EO
- 寄存器A输入+AI
- 给一个脉冲
这边寄存器A获得ALU的值,同时ALU更新了,这边非常酷,锁操作只发生在脉冲的上升沿,
OUT命令 OUT,前3步是一样的
-
将A寄存器中的值显示出来
- 将A寄存器输出+ AO
- output寄存器输入 OI
- 给一个脉冲
到这程序执行完了
总结一下
这些小的指令称为微指令,这些微指令的前三步都是相同的,之后的操作是不同的,
所以需要控制位对每个指令构造控制逻辑
反正我控制位按照一定的顺序排序
每一种微指令对应一种控制序列。
真正的微指令会占用余下的时间片,实际上我们需要一个独立的计数器,所以需要一个独立的计数器
上面通过手动的方式设置控制位,然后手动发送一次主脉冲,在两个主脉冲之间改变它的控制位,,所以我们实际上还需要另一个脉冲来控制 ,这边可以用主脉冲的倒转,通过非门开获得另一个脉冲
这边还要将各个指令分步,才能够让控制器知道执行到了哪一步,可以看到每个指令最多5步,有些步数可以合并就合并了。从T0-T4,而有些指令用不到4步,那么多余的步数计算机什么也不做就浪费了。这是无法避免的
现在脉冲有了,步数分解有了,需要将脉冲变成步数,这和程序计数器是一样的,使用74LS161,这是一个四位的计数器,
计数器有了,现在要将计数器解码,这边用到了74LS138芯片,
可以看到其转换成明确信号,这边和显示部分用到的139解码是一样的逻辑
这边我们可以可以清晰的看到程序走到了哪个时间片,哪一步
下面我们构建非常酷的事情,也就是控制器的真值表
第一个取址,可以看到前两步,
第二个LDA用了剩余的三步,最后一步什么也没做。
第三个ADD也是三部
用两个28C16就可以完成其组合逻辑,其有11条地址线,8个输出线。
将真值表输入到28C16中就可以完成控制
Reset
这边如果程序执行完成,需要将所有的寄存器清空,这边我们构建这样一个reset电路用来一个74LS00来构建
将reset和~reset接到所有的寄存器
到目前为止,计算机的主体部分就做好了
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左右
是否需要乘法,指数,对数,三角函数等指令,这些指令肯定是做不出来的,那么问题就回来了我们真正需要什么样的指令,什么样的指令才能称为计算机,计算机是什么?
计算机:
可以完成任何的指令
可以完成任何的可计算的问题
什么是可计算的什么是不可计算的
这不是计算机性能的问题,是通过算法能完成的问题
那么问题就变成了我们需要完成什么样的算法。
这个问题在计算机早期图灵就进行研究过
1936年 他写了关于这个问题的一篇论文。这篇论文得出的结论是,他可以发明一种机器,可以完成任何计算序列
他是这样描述的:
有一个无限长的纸带,上面有方格,有1和0两种状态,有一个小旗子可以指向这些方格,小旗子有一个状态A,一次只能移动一个。
有一个小旗子和其状态的真值表
现在这个状态,A ,浏览状态是1,就将1写到袋子上,然后向左移动一格,自身的状态变成C,就变成了下面的状态
根据这个真值表进行一直不停的循环做,一旦停止到Halt,纸带上就是结果,
这个机器就能完成任何的数学算法。只需要设置好这个指令表就好了
实际上图灵还提高一个更好的计算机,称为通用计算机,这个机器上有一个指令表,是一个最基本的状态,其他计算机可以通过编码的方法将算法映射到这个指令表上
到这边就知道了任何可计算的问题都可以变成一个可计算的序列
在同一个时期邱奇也思考了相同的问题
他写了一篇论文关于什么是计算能力的定义,从完全不同的角度切入这个问题,他提出新的数学系统称为论的演算。
这便有一些变量,有一些函数,还有一些函数的结果
在论文的后面,他定义了一些函数,他用这个方法表达计算机,有点像现在的Lambda表达式
这篇论文的结论是:不是所有的问题都可以通过计算解决,有些可以,有些不可以,
在1936年两个人从两种不同的角度思考了这个问题
当图灵在8月份读到邱奇的论文,将邱奇的论文放到了附录中,任何问题可以转换成论的计算的问题都可以转化成一个可计算的问题
我们计算机和图灵机比较还缺少什么呢,图灵机有一个操作我们做不到,同一个指令可以有不同的操作
如果纸带是空格向右移动如果纸带为1向左移动,
有一种指令叫做有条件跳转指令可以做到这一点,它和我们的跳转指令有一点像,现在的跳转指令只能跳转到固定的地址
左右等价
根据不同的值来进行不同的行为
所以我们可以说如果实现条件跳转指令我们就可以模拟任何图灵机
条件跳转
准备实现两个条件跳转指令,为0跳转和进位跳转0
为0跳转,这个跳转需要计算ALU中所有的值是否为0 ,
使用这个电路我们就可以判断是否为0
74LS08有4个与门和74LS02有4个Nor门
进位跳转
ALU中高4位芯片有一个进位引脚,我们很容易就可以判断出是否进位了。
这边就搭建好了2个标识,但是有一个问题,
在获得这个标识后,加命令还有一步就是将ALU中的值放到寄存器A中,这样在进行跳转指令的时候标识就没有了,
所以这边需要将进位标识存起来,这边我们需要一个173芯片
其实Internal x86也有进位标识计数器
一共32位
这样就多了一个控制线,FI:标识Flag的输入,
这是新的真值表,用了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和控制线,输出正常,
经验
- 每个模块先用跳线接一下再进行测试,如果发现测试没有问题再用标准接线将其接通,
- 正常调试需要一步步执行,当出现异常了先解决出现的第一个异常,然后再解决剩余的异常,遇到异常不要慌,一步步解决,不要跳过问题进行下一个问题。
引用
大佬的视频教程,截图基本都源自于该大佬,并稍加改动
https://space.bilibili.com/413461202/