本作業的任務:
在 Logisim 上打造一台能執行 Hack 的電腦!
剩餘時間拿去多練組語——課程接下來的一切都建立在 組語上,而本週作業主要只與本週教材相關 (開始前至少應完成第五週作業第 5 節)。
素材盤點:skeleton 檔已含第二週的 ALU 與第四週的 程式計數器;RAM、ROM、暫存器用 Logisim 預建版。 影片 2 的架構輪廓不是完整解答—— 填補空隙正是本作業的核心工作,也是本詳解逐步示範的內容。
把「Hack 電腦」的規格整理成可以動工的藍圖: 需要哪些元件?指令如何編碼?記憶體為什麼用 64KB/15 位元位址而不是「真正的」32KB/14 位元?
整機藍圖(Harvard 架構): 指令與資料分開存放——這是 Hack 能用 單一時脈週期執行每條指令的關鍵。
| 元件 | 來源 | 規格 |
|---|---|---|
| ALU | 第二週作業(skeleton 內附) | 16 位元、 \(zx,nx,zy,ny,f,no\) 控制、\(zr,ng\) 旗標 |
| PC | 第四週作業(skeleton 內附) | 16 位元、reset \(>\) load \(>\) inc 優先序 |
| A、D 暫存器 | Logisim memory 函式庫 Register | 16 位元、時脈上升緣載入 |
| ROM | Logisim memory 函式庫 | 位址 15 位元、資料 16 位元、 存程式 |
| RAM | Logisim memory 函式庫 | 位址 15 位元 (64KB)、資料 16 位元 |
為什麼 RAM 用 15 位元位址(64KB)? 真正的 Hack 只有 32KB RAM(14 位元位址, 0x0000–0x3FFF),螢幕與鍵盤是 獨立的記憶體映射裝置(0x4000–0x6000)。 本課程是架構課不是硬體設計課,不直接模擬螢幕鍵盤—— 把 RAM 直接加大到 15 位元位址空間, 讓 0x4000–0x6000 這段位址落在 普通 RAM 裡:程式照常讀寫「螢幕」與「鍵盤」, 只是沒有真的畫面;想模擬按鍵就暫停模擬、 手動改 RAM[0x6000] 再繼續。
指令編碼(本週影片的核心,動工前先背熟):
| 型態 | 位元 15 | 格式 |
|---|---|---|
| A-指令 | 0 |
0vvvvvvvvvvvvvvv (\(v\) = 15 位元常數) |
| C-指令 | 1 |
111 \(a\,c_1c_2c_3c_4c_5c_6\,
d_1d_2d_3\,j_1j_2j_3\) |
C-指令的欄位對應(位元編號是除錯時的救命索):
| 欄位 | 指令位元 | 意義 |
|---|---|---|
| 型態 | \(i_{15}\) | 0 = A-指令、1 = C-指令 |
| \(a\) | \(i_{12}\) | ALU 的 \(y\) 輸入:0 = A、1 = M |
| \(c_1..c_6\) | \(i_{11}..i_{6}\) | 直通 ALU 的 \(zx,nx,zy,ny,f,no\) |
| \(d_1\,d_2\,d_3\) | \(i_5\,i_4\,i_3\) | 寫入 A/D/M |
| \(j_1\,j_2\,j_3\) | \(i_2\,i_1\,i_0\) | 結果 \(<0\)/\(=0\)/\(>0\) 時跳躍 |
未定義行為的經驗法則(題目原話): 規格沒說的情況,你的 CPU 做什麼都可以。 例如 A \(=\) 0xFFFF 時執行 D=M 的結果 無所謂——M 只在 A 是合法位址時有定義。 工程意義:不要為不存在的情況多蓋硬體 (RAM 位址直接取 A 的低 15 位、忽略最高位即可)。
資料通路設計(填補影片 2 的空隙)
畫出完整的資料通路,並為每個元件推導控制訊號: A 暫存器什麼時候載入、載入什麼?D 呢?RAM 的寫入使能? ALU 的輸入從哪來?PC 什麼時候跳躍?
整機資料通路:
(粗線 = 16 位元匯流排;細線 = 1 位元控制訊號。)
控制訊號逐一推導(這就是「影片 2 沒給的空隙」):
Mx\(_A\)(A 的來源):A-指令要把 指令本身裝進 A(最高位是 0, 整條 16 位元直接接進去就等於零延伸的 15 位元常數); C-指令若寫 A(
A=...)裝的是 ALU 輸出。 \[\text{sel} = i_{15}, \qquad \text{in}_0 = \text{instruction}, \quad \text{in}_1 = \text{ALUout}\]A 的載入:A-指令必定載入; C-指令只在 \(d_1\)(\(i_5\))時載入: \[\boxed{A.\mathrm{load} = \lnot i_{15} \lor (i_{15} \land i_5)}\]
D 的載入:只有 C-指令的 \(d_2\)(\(i_4\)): \[\boxed{D.\mathrm{load} = i_{15} \land i_4}\] 必須用 \(i_{15}\) 把關——不然 A-指令
@21(位元圖案裡恰好 \(i_4=1\) 的常數, 例如@16)會誤寫 D!這是本作業第一大坑。RAM:位址永遠是 A 的低 15 位; 寫入資料永遠是 ALUout;寫入使能只有 C-指令的 \(d_3\)(\(i_3\)): \[\boxed{\mathrm{RAM.str} = i_{15} \land i_3}, \qquad \mathrm{RAM.addr} = A[14..0]\]
ALU:\(x = D\) 固定;\(y\) 由 \(a\) 位(\(i_{12}\)) 在 A 與 M(RAM 輸出)之間選; 六條控制線按順序直通: \[(zx,nx,zy,ny,f,no) = (i_{11},i_{10},i_9,i_8,i_7,i_6)\] (順序接反是本作業第二大坑——\(c_1\) 是 高位 \(i_{11}\)。)
跳躍邏輯:ALU 附贈兩面旗子 \(zr\)(結果 \(=0\))與 \(ng\)(結果 \(<0\)), 「結果 \(>0\)」\(= \lnot zr \land \lnot ng\): \[jump = (j_1 \land ng) \lor (j_2 \land zr) \lor \bigl(j_3 \land \lnot zr \land \lnot ng\bigr)\] \[\boxed{PC.\mathrm{load} = i_{15} \land jump}, \qquad PC.\mathrm{in} = A\] 再度用 \(i_{15}\) 把關:A-指令絕不跳躍 (不然
@7這種常數會被當成JMP—— 第三大坑)。PC 的 inc:第四週的 PC 優先序是 reset \(>\) load \(>\) inc,所以 inc 可以恆為 1 ——不跳躍、不重設時自然 \(+1\); reset 接一顆按鈕(開機/重跑)。
一個時脈週期的生命: 上升緣 \(\to\) PC 輸出位址 \(\to\) ROM 吐出指令 \(\to\) 解碼(splitter 拆位元)\(\to\) ALU 組合運算 (讀 A、D、M 都是舊值)\(\to\) 下一個上升緣把結果同時寫進 A/D/M/PC。 取指與執行在同一個週期完成—— Harvard 架構讓指令記憶體與資料記憶體互不打架, 這就是 Hack 能單週期的原因。
Logisim 實作細節
把第 2 節的藍圖搬進 Logisim:元件屬性怎麼設? 指令位元怎麼拆?時脈怎麼接?
(1) 指令解碼:一顆 splitter 搞定。 ROM 輸出接 16 路 splitter(Fan Out \(=16\), 或按欄位分組:15、12、11-6、 5-3、2-0)。建議在每條拆出的線上 貼 Logisim 的 Text 標籤(\(a\)、\(c_1\)、…)—— 兩週後回來除錯時你會感謝自己。 控制邏輯只需要幾顆小閘:
1 顆 NOT(\(\lnot i_{15}\))、1 顆 OR、3 顆 AND 做 A/D/RAM 的載入訊號;
跳躍邏輯:2 顆 NOT、4 顆 AND、1 顆三輸入 OR (或兩顆二輸入)。
(2) RAM 屬性(wiring 好之前先設好):
Address Bit Width \(= 15\)、Data Bit Width \(= 16\);
Data Interface \(=\) “Separate load and store ports”——讀寫分開、不用三態緩衝器, 東側輸出(load 腳恆 1)隨時反映
RAM[addr], 恰好是 Hack 要的「M 隨時可讀」; 西側輸入接 ALUout,str接 \(i_{15} \land d_3\)、時脈接全域時脈;ROM 同理:Address 15、Data 16,位址接 PC 低 15 位。
(3) 時脈與重設: A、D、PC、RAM 全部吃同一個時脈。 Logisim 小技巧(題目原話):電路裡可以放多個 時脈輸入,“Simulate \(\to\) Tick once”(Ctrl+T) 會讓它們一起走一步——比拉一條蜘蛛網般的 時脈線到每個角落乾淨得多。 reset 按鈕接 PC 的 reset;想重跑程式: 按住 reset、tick 一次、放開。
(4) 兩個容易忽略的接線細節:
A-指令進 Mx\(_A\) 時整條 16 位元直接接—— 不需要特別做「零延伸」,因為 A-指令的 \(i_{15}\) 本來就是 0;
RAM 位址接 A\([14..0]\)(splitter 丟掉最高位) ——A \(=\)
0xFFFF時會讀寫RAM[0x7FFF], 沒關係:ISA 沒定義這個情況,經驗法則說隨便它。
測試你的 CPU
先測幾條單獨的指令,再載入夠複雜的程式做壓力測試 (skeleton 的 ROM 已預載上週 Collatz 題的解答), 必要時與 CPU 模擬器並排對照。
階段一:單指令煙霧測試。 手工組譯幾條指令、直接填進 ROM(右鍵 \(\to\) Edit contents), 每 tick 一次核對暫存器。建議的最小測試集 (十六進位已算好,直接貼):
| 指令 | 機器碼(hex) | 預期效果(tick 後) |
|---|---|---|
@21 |
0015 |
A \(=21\)(測 A-指令載入) |
D=A |
EC10 |
D \(=21\)(測 ALU 傳 A、寫 D) |
@16 |
0010 |
A \(=16\) (D 必須不變——驗證 \(i_{15}\) 把關!) |
M=D |
E308 |
RAM[16] \(=21\)(測寫 M) |
D=D+M |
F090 |
D \(=42\)(測 \(a\) 位選 M) |
M=M+1 |
FDC8 |
RAM[16] \(=22\)(測讀改寫 M) |
D;JGT |
E301 |
PC 跳到 A \(=16\) (D\(>0\);測跳躍) |
0;JMP |
EA87 |
PC \(=\) A(測無條件跳) |
檢查點(每條指令後看三個地方): PC 是否 \(+1\)(或跳對地方)、目標暫存器是否更新、 其他暫存器是否沒有被誤寫。 第三點最容易抓到控制訊號忘了用 \(i_{15}\) 把關的 bug。
階段二:Collatz 壓力測試。 skeleton 的 ROM 已預載上週 Collatz 解答:
在 RAM 編輯器把
RAM[0]設成0x0005,開自動 tick(Simulate \(\to\) Ticks Enabled,頻率調最高);預期 3–4 秒跑完;
RAM[0x20]起應出現軌跡 \(5, 16, 8, 4, 2, 1\)(hex:5, 10, 8, 4, 2, 1);再試
RAM[0]\(=\)0x002C(44), 預期約 10 秒,軌跡 \(44,22,11,34,17,52,26,13,40,20,10,5,16,8,4,2,1\);跑太久 = 出事了(題目給的基準)。 最常見原因:跳躍邏輯錯(無窮迴圈跳不出去) 或 PC 的 load/inc 優先序接反。
並排除錯法:同一支程式開在 nand2tetris CPU 模擬器裡,兩邊同步單步—— 第一個分歧的週期就是 bug 所在; 分歧時看四個值:PC、A、D、剛寫入的 RAM 格。
階段三:載入自己的程式(bin2hex 流程)。
用 nand2tetris 組譯器把
.asm編成.hack(0/1 文字檔);命令列跑
bin2hex 輸入.hack 輸出.txt——把每個二進位字轉成以空白分隔的十六進位字 (非 Windows 請自行編譯它的 C 原始碼:cc bin2hex.c -o bin2hex);文字編輯器開輸出檔、全選複製;
Logisim 中右鍵 ROM \(\to\) “Edit contents...”;
游標點到位址 0、Ctrl+V(Mac:Cmd+V)貼上;
關閉視窗,程式已進 ROM。
模擬鍵盤輸入(題目提供的方法): 暫停模擬 \(\to\) 直接編輯 RAM[0x6000] 為想要的 掃描碼(c 鍵 \(= 99 = \texttt{0x63}\))\(\to\) 繼續模擬。 放開按鍵就改回 0。螢幕同理:程式寫進 0x4000–0x5FFF 的值可以在 RAM 編輯器裡直接觀察(沒有畫面,但位元圖案就在那裡)。
常見錯誤型錄(除錯速查)
按「症狀 \(\to\) 病因」整理本作業最常見的八種 bug:
| 症狀 | 病因 |
|---|---|
@16 之類的 A-指令把 D 或 RAM 改掉 |
D/RAM 的載入訊號忘了 \(\land\, i_{15}\) (常數的 \(i_4\)/\(i_3\) 位恰好是 1) |
| A-指令後 PC 亂跳 | 跳躍訊號忘了 \(\land\, i_{15}\) (常數的低 3 位恰好非零,被當 jump 欄位) |
D=A 對、D=M 錯(或相反) |
Mx\(_Y\) 的 select 接反(\(a=1\) 應選 M) |
D+1、D-A 等個別 comp 錯 |
\(c_1..c_6\) 與 \(zx..no\) 順序接反 (\(c_1\) 是高位 \(i_{11}\)!) |
JGT 在結果為負時也跳 |
「\(>0\)」少算 \(\lnot zr \land \lnot ng\) (只接了 \(\lnot ng\),0 也會跳) |
| 每條指令 PC 都不動(或跳躍後又 \(+1\)) | PC 的 load/inc 優先序錯、或 inc 沒接 1 |
M=M+1 少加或跳拍 |
RAM 讀取不是組合式(Data Interface 沒選 separate ports / 非同步讀),M 舊值晚一拍 |
| Collatz 跑超過 30 秒 | 以上任一;並排單步找第一個分歧週期 |
你剛蓋好的是「單週期微架構」。 每條指令恰好一個時脈週期,週期長度必須容納最長 的組合路徑:ROM 取指 \(\to\) 解碼 \(\to\)(可能經 RAM 讀 M) \(\to\) ALU \(\to\) 跳躍邏輯 \(\to\) PC——這條 關鍵路徑決定時脈上限。 真實 CPU 的做法是管線化(第七週講義後半): 把這條路切成取指/解碼/執行/訪存/寫回五段, 五條指令同時在不同段流水——吞吐量升五倍, 但也帶來危障(hazard)要處理。 Hack 能單週期的兩大功臣: (1)Harvard 架構——取指與資料訪問用不同記憶體, 不搶埠;(2)指令定長且解碼極簡—— 欄位直通 ALU,不需要微碼。 對照 x86:變長指令、上千條微操作, 解碼器本身就是一個小 CPU。 這就是 RISC 哲學的完整體現:讓硬體簡單,把複雜留給編譯器。
附錄:速查表
控制訊號總表
| 訊號 | 布林式 |
|---|---|
| Mx\(_A\).sel | \(i_{15}\)(0 = 指令、1 = ALUout) |
| A.load | \(\lnot i_{15} \lor (i_{15} \land i_5)\) |
| D.load | \(i_{15} \land i_4\) |
| RAM.str | \(i_{15} \land i_3\) |
| RAM.addr | A\([14..0]\) |
| Mx\(_Y\).sel | \(i_{12}\)(0 = A、1 = M) |
| ALU 控制 | \((zx..no) = (i_{11}..i_6)\) |
| \(jump\) | \((i_2 \land ng) \lor (i_1 \land zr) \lor (i_0 \land \lnot zr \land \lnot ng)\) |
| PC.load | \(i_{15} \land jump\);PC.in \(=\) A |
| PC.inc | 恆 1(優先序 reset \(>\) load \(>\) inc) |
C-指令編碼總表
格式:111 \(a\,c_1c_2c_3c_4c_5c_6\,
d_1d_2d_3\,j_1j_2j_3\)
| \(a{=}0\) | \(a{=}1\) | \(c_1\) | \(c_2\) | \(c_3\) | \(c_4\) | \(c_5\) | \(c_6\) |
|---|---|---|---|---|---|---|---|
0 |
1 | 0 | 1 | 0 | 1 | 0 | |
1 |
1 | 1 | 1 | 1 | 1 | 1 | |
-1 |
1 | 1 | 1 | 0 | 1 | 0 | |
D |
0 | 0 | 1 | 1 | 0 | 0 | |
A |
M |
1 | 1 | 0 | 0 | 0 | 0 |
!D |
0 | 0 | 1 | 1 | 0 | 1 | |
!A |
!M |
1 | 1 | 0 | 0 | 0 | 1 |
-D |
0 | 0 | 1 | 1 | 1 | 1 | |
-A |
-M |
1 | 1 | 0 | 0 | 1 | 1 |
D+1 |
0 | 1 | 1 | 1 | 1 | 1 | |
A+1 |
M+1 |
1 | 1 | 0 | 1 | 1 | 1 |
D-1 |
0 | 0 | 1 | 1 | 1 | 0 | |
A-1 |
M-1 |
1 | 1 | 0 | 0 | 1 | 0 |
D+A |
D+M |
0 | 0 | 0 | 0 | 1 | 0 |
D-A |
D-M |
0 | 1 | 0 | 0 | 1 | 1 |
A-D |
M-D |
0 | 0 | 0 | 1 | 1 | 1 |
D&A |
D&M |
0 | 0 | 0 | 0 | 0 | 0 |
D|A |
D|M |
0 | 1 | 0 | 1 | 0 | 1 |
| dest | \(d_1\) | \(d_2\) | \(d_3\) |
|---|---|---|---|
| — | 0 | 0 | 0 |
M= |
0 | 0 | 1 |
D= |
0 | 1 | 0 |
DM= |
0 | 1 | 1 |
A= |
1 | 0 | 0 |
AM= |
1 | 0 | 1 |
AD= |
1 | 1 | 0 |
ADM= |
1 | 1 | 1 |
| jump | \(j_1\) | \(j_2\) | \(j_3\) |
|---|---|---|---|
| — | 0 | 0 | 0 |
JGT |
0 | 0 | 1 |
JEQ |
0 | 1 | 0 |
JGE |
0 | 1 | 1 |
JLT |
1 | 0 | 0 |
JNE |
1 | 0 | 1 |
JLE |
1 | 1 | 0 |
JMP |
1 | 1 | 1 |
煙霧測試指令的手工組譯
以 D=D+M 為例示範一次完整推導: C-指令 \(\to\) 111;用到 M \(\to a=1\); comp D+M \(\to c=000010\);dest D \(\to
d=010\);無跳躍 \(\to j=000\)。 串起來:1111 0000 1001 0000 \(=\) 0xF090。
| 指令 | 二進位 | hex |
|---|---|---|
@21 |
0000000000010101 |
0015 |
D=A |
1110110000010000 |
EC10 |
M=D |
1110001100001000 |
E308 |
D=D+M |
1111000010010000 |
F090 |
M=M+1 |
1111110111001000 |
FDC8 |
D;JGT |
1110001100000001 |
E301 |
0;JMP |
1110101010000111 |
EA87 |
記憶體配置(本作業的 64KB 版本)
| 位址 | 用途 |
|---|---|
0x0000–0x3FFF |
一般 RAM (真 Hack 的全部 RAM) |
0x4000–0x5FFF |
螢幕映射區 (本作業:普通 RAM,可觀察位元圖案) |
0x6000 |
鍵盤映射(手動編輯模擬按鍵) |
0x6001–0x7FFF |
真 Hack 不存在; 行為未定義,隨便它 |
參考資料
COMSM1302 第七週講義與影片(Hack ISA、機器碼編碼、 CPU 架構輪廓、管線化),University of Bristol.
Nisan & Schocken, The Elements of Computing Systems(nand2tetris),Ch. 5(Computer Architecture) ——CPU/Memory/Computer 晶片的官方規格.
Logisim Library Reference——RAM 的 Data Interface 屬性(“Separate load and store ports”).
第二週作業(ALU)、第四週作業(PC)、 第五週作業(Collatz 程式)——本作業的三塊積木.