在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和控制線,輸出正常,
經驗
- 每個模組先用跳線接一下再進行測試,如果發現測試沒有問題再用標準接線將其接通,
- 正常除錯需要一步步執行,當出現異常了先解決出現的第一個異常,然後再解決剩餘的異常,遇到異常不要慌,一步步解決,不要跳過問題進行下一個問題。
引用
大佬的視訊教程,截圖基本都源自於該大佬,並稍加改動