本作業的任務:

  1. 寫幾個基本的 Hack 組合語言程式;

  2. 學會用 Hack 組譯器(Assembler)與 CPU 模擬器 (CPU Emulator)測試與除錯;

  3. 學習位元遮罩(bit masking);

  4. 學會在 Hack 組語中使用螢幕與鍵盤。

為什麼分兩部分?第一部分(本卷)涵蓋考試所需的內容, 應在第 8 週開始前完成——之後課程的一切都建立在 你對組語的理解上。第二部分是較難的練習題,做不完沒關係。 所需軟體:nand2tetris 套件的 Assembler 與 CPU Emulator(需要 Java 執行環境;實驗室機器 用 module load nand2tetris 載入)。

(1)載入課堂的 add.asm,用組譯器轉成 add.hack;在 CPU 模擬器載入,設 RAM[0]\(=3\)RAM[1]\(=4\),執行後 RAM[2] 應為 24。 (2)調整動畫速度、認識 “No animation” 模式的注意事項。 (3)試著把 add.asm 直接載入模擬器。 (4)自己寫 sub.asm:把 RAM[0] 減去 RAM[1],結果存入 RAM[2](假設無溢位)。

(1) 工作流程。 組譯器把 .asm(人類可讀的組語)翻譯成 .hack(0/1 組成的機器碼文字檔)。 在 Assembler 中開啟 add.asm \(\to\) 按 “Fast translation” \(\to\) 存出 add.hack \(\to\) 在 CPU Emulator 以 ROM 載入 \(\to\) 在 RAM 顯示區直接點格子輸入 RAM[0]\(=3\)RAM[1]\(=4\) \(\to\) 按雙箭頭(run)。

為什麼是 \(24\) 而不是 \(7\)?因為課堂的 add.asm 算的是 \(2 \times \texttt{RAM[0]} + 6 \times \texttt{RAM[1]} = 6 + 18 = 24\)……開個玩笑——真正的原因依課堂版本而定, 重點是:先讀程式再預測結果,執行只是驗證你的預測。 若載入的是 nand2tetris 官方的 Add.asm (計算 \(2+3\) 存入 R0)行為又不同—— 務必用課程 unit page 提供的版本。

(2) 模擬器的三個實用旋鈕(考試必備):

  • 動畫速度滑桿(上方工具列):預設很慢, 調到最快省下大量等待;

  • “Animate: No animation”:完全不畫動畫, 大程式必用。兩個陷阱:此模式下 (a) 不能在執行中編輯 RAM 的值; (b) 執行中 RAM 顯示不會更新—— 程式明明已經算完,畫面上的 RAM 看起來卻沒變, 要按停止(或程式跑到無窮迴圈後按暫停)才會刷新。 考試時別被這點騙到;

  • 清除記憶體按鈕(RAM 顯示區上方): 第二輪測試前把 RAM 歸零。

(3) 直接載入 .asm CPU 模擬器可以直接開 .asm(它會即時翻譯), 考試時快得多。但你要從體感上理解: 組譯器能把 .asm 翻成可直接進 ROM 的機器碼—— 第 7 週談硬體時,CPU 讀的是機器碼不是組語; 第 8 週我們會拆開組譯器本身的原理。 組譯器另一個用途是除錯(見第 2 節的 fast translation)。

(4) sub.asm 思路:D 暫存器當運輸工具——先載 RAM[0], 減掉 RAM[1],存到 RAM[2]

// sub.asm -- RAM[2] = RAM[0] - RAM[1]
    @R0
    D=M        // D = RAM[0]
    @R1
    D=D-M      // D = RAM[0] - RAM[1]
    @R2
    M=D        // RAM[2] = D
(END)
    @END
    0;JMP      // 慣例:無窮迴圈結束程式

三個值得養成的習慣: (a)用 @R0 而不是 @0——雖然等價, 但明確表示「這是暫存器不是常數」; (b)每行註解——第 2 節除錯時你會感謝自己; (c)結尾放 (END) 無窮迴圈—— Hack 沒有「halt」指令,不加的話 PC 會繼續往下執行 ROM 裡的垃圾。 測試:模擬器中設 RAM[0]\(=9\)RAM[1]\(=5\), 跑完 RAM[2]\(=4\) ✓; 再到 codestuff.online 貼上跑多組測資。

乘法與除錯(Multiplication and Debugging)

mult.asm:先寫 C、再翻譯

mult.asm:把 RAM[0] 乘以 RAM[1],結果存入 RAM[2]。 假設無溢位、兩數皆非負。 開始前先想:不用 * 運算子的 C 要怎麼寫? 再把 C 翻譯成組語。

第一步:C 版本(不用 *)。 乘法 = 重複加法:\(a \times b\) 就是把 \(b\)\(a\) 次。

// C 偽代碼
result = 0;
n = RAM[0];            // 還要再加幾次
while (n > 0) {
    result = result + RAM[1];
    n = n - 1;
}
RAM[2] = result;

第二步:逐句翻譯成 Hack。 C 的每一行變成一小段組語,C 註解保留下來當地圖:

// mult.asm -- RAM[2] = RAM[0] * RAM[1]
// (RAM[0], RAM[1] >= 0,無溢位)

// result = 0  (直接用 RAM[2] 當 result)
    @R2
    M=0
// n = RAM[0]
    @R0
    D=M
    @n
    M=D
(LOOP)
// while (n > 0) ... 條件不成立就離開
    @n
    D=M
    @END
    D;JLE      // n <= 0 → 結束
// result = result + RAM[1]
    @R1
    D=M
    @R2
    M=D+M
// n = n - 1
    @n
    M=M-1
    @LOOP
    0;JMP
(END)
    @END
    0;JMP

翻譯模式(第 5-3 講的套路): while \(\to\) 「標籤+條件跳離+回跳」; 變數 \(\to\) @名字 的自動配置(從 RAM[16] 起)。

正確性檢查(邊界情況先想好再跑): \(\texttt{RAM[0]}=0 \to\) 迴圈零次、RAM[2]\(=0\) ✓; \(3 \times 4\):加 3 次 4 \(\to 12\) ✓。 效率:迴圈次數 \(=\) RAM[0], 所以把較小的數當計數器會快很多 (\(2 \times 10000\) 跑 2 圈 vs 10000 圈)—— 進階版可先比較兩數再決定誰當 n

按題目要求:先不要急著除錯, 往下一節學工具,最後再回來修它。

odd.asm:三件除錯武器

下載 odd.asm(內含刻意種下的 bug)。 它的規格:RAM[0] 是正數; 依奇偶性把 RAM[0] 或 42 存入 RAM[1] (兩個測試案例:RAM[0]\(=501\) 應存 42、 RAM[0]\(=500\) 應存 500)。 依序用三種工具修好它: (1)組譯器的 fast translation 找語法錯誤; (2)Asm/Sym 模式對照找 501 案例的 bug; (3)中斷點(breakpoint)找 500 案例的 bug。

規格小注:題目正文寫「奇數存 RAM[0]、 否則存 42」,但兩個測試案例(\(501 \to 42\)\(500 \to 500\)) 一致指向相反方向:偶數存 RAM[0]、 奇數存 42。以測試案例為準——這本身就是除錯的第一課: 規格與測試不一致時,先弄清楚哪個才是真相

由於每年下載的 odd.asm 內容可能不同, 以下把「怎麼用工具」與「這類 bug 長什麼樣」講透—— 方法對了,什麼變種都修得掉。

武器一:組譯器的 Fast Translation(語法錯誤)。

CPU 模擬器拒絕載入 .asm 時只給一句沒用的錯誤訊息, 但拒載必定表示語法錯誤。 開 Assembler \(\to\)Fast translation (工具列右數第二顆)\(\to\) 它會停在出錯的那一行 並反白。常見語法錯誤型態:

  • 打錯指令拼字:M=D+(少運算元)、 D=M+2(Hack 沒有 +2, 合法常數只有 01-1);

  • 目的地順序錯:DM=... 在舊版組譯器要寫 MD=...

  • 括號/分號用錯:(LOOP 少右括號、 D;JGT; 多分號;

  • @ 後面接了非法內容:@32768 (超過 15 位元)、@2x(變數不能以數字開頭)。

修一個、重跑 fast translation,直到成功產出 .hack

武器二:Asm/Sym 模式對照(501 案例)。

ROM 顯示區上方的下拉選單:

  • Sym(symbolic)模式:顯示你寫的原始碼 ——變數名、標籤名、行尾註解都在;

  • Asm 模式:顯示 CPU 實際執行的樣子 ——變數與標籤全部換成數字位址、註解消失。

對照兩個模式就能看穿「符號層」的謊言。 這類「有點壞心但極常見」的 bug 的代表是: 打錯標籤或變數名。Hack 的規則: @名字 若不是已宣告的標籤, 就默默變成一個新變數(配到 RAM[16] 之後)—— 不會有任何錯誤訊息!症狀與診斷:

  • 症狀:程式跳到莫名其妙的地方 (@LOOP 打成 @LOOp, 跳躍目標從 ROM 位址變成「值為某小數字的新變數」, 0;JMP 就跳去 ROM 開頭附近鬼打牆); 或某變數永遠是 0(讀到的是打錯字的新變數);

  • 診斷:在 Asm 模式下看那條 @ 指令 ——它解析出來的數字跟你以為的標籤位址對不上 (Sym 模式裡標籤旁會標示它對應的 ROM 位址, 兩邊一比就現形);

  • 同型 bug:大小寫不一致 (@sum vs @Sum)、 用了 @1 想表達常數 1 卻寫成 D=D&M (讀了 RAM[1])而非 D=D&A (用常數 1)——A 與 M 的混淆是 Hack 第一大 bug 來源。

單步工具:single step(走一步)與 step back(退一步;No animation 模式下不可用)。 501 的路徑(奇數)通常很快分岔, 單步走十幾步就能看到它跳錯地方。修好後 \(501 \to 42\) ✓。

武器三:中斷點(500 案例)。

500 是偶數路徑,若程式用「重複減 2」判斷奇偶, 要繞 250 圈才出結果——單步走到天荒地老。 中斷點讓模擬器全速跑到指定位置才停:

  1. 切到 Sym 模式定位,捲到迴圈尾(例如 ROM[15]) ——右鍵點那格 ROM,它變紅=中斷點設好;

  2. 用 No animation 模式 run——它跑到 ROM[15] 停下;

  3. 把動畫模式調回 “Program flow” (這樣才能 step back),在中斷點附近前後單步, 找「哪一圈開始壞掉」;

  4. 這類 bug 的代表:迴圈邊界/步長錯誤—— 減 2 寫成減 1(奇偶判斷整個歪掉)、 D;JGTD;JGE 之差(最後一圈多跑或 少跑)、離開迴圈時 D 沒重新載入 (殘值 bug:D 還留著上一段的計算結果)。

條件中斷點(工具列旗子圖示)更強: 指定「PCADRAM[i]time 等於某值」時暫停—— 「第 357 圈才壞」的 bug 就設 RAM[counter]\(=357\) 直達案發現場。

最後:回頭修你的 mult.asm, 用 codestuff.online 跑測資、掛了就用上面三件武器。 如果你當初沒寫註解,現在要在一堆 @JMP 裡考古——把這當一次教訓: 註解是寫給兩週後的自己看的

遮罩(Masking)

觀念:把字組當位元圖案而非數字來操作, 就用位元運算(bitwise operations)——它們就像 16 條並排的 一位元閘:\(x \land y\) 的第 \(i\)\(= x_i \land y_i\)。 C 提供 ~&|^;Hack 組語提供 NOT、AND、OR (沒有 XOR,但可以拼出來)。 術語:拿一個固定字組去 AND/OR/XOR 未知字組, 那個固定字組叫遮罩(mask); 用一個位元存布林值時稱它為旗標(flag), 設 1 叫 set、清 0 叫 clear。 三條口訣: AND 清位\(x_i \land 0 = 0\))、 OR 設位\(x_i \lor 1 = 1\))、 XOR 翻位\(x_i \oplus 1 = 1 - x_i\))。

and.asm:數 1 的個數(popcount)

and.asm:數 RAM[0] 中值為 1 的位元數, 寫入 RAM[1]。例:RAM[0]\(=11=\) 0b1011 \(\to\) RAM[1]\(=3\)。 (模擬器右上角 Format 切成 binary 可直接看位元。)

難點:Hack 沒有移位指令。 不能把 \(x\) 右移逐位檢查——但可以反過來讓遮罩左移\(mask\)0x0001 開始,每輪 \(mask = mask + mask\) (自加就是左移一位!),依序掃過 0x0001, 0x0002, …, 0x8000。 終止條件也是免費的:0x8000 再自加, 在 16 位元裡溢位成 \(0\)——「\(mask = 0\)」就是掃完 16 位。

// C 偽代碼
count = 0;
mask = 1;
while (mask != 0) {
    if ((RAM[0] & mask) != 0) count++;
    mask = mask + mask;      // 左移一位
}
RAM[1] = count;
// and.asm -- RAM[1] = RAM[0] 中 1 的個數
    @count
    M=0
    @mask
    M=1
(LOOP)
// if ((RAM[0] & mask) != 0) count++
    @mask
    D=M
    @R0
    D=D&M      // D = RAM[0] & mask
    @SKIP
    D;JEQ      // 該位是 0 → 不加
    @count
    M=M+1
(SKIP)
// mask = mask + mask(左移;0x8000 自加溢位成 0)
    @mask
    D=M
    MD=D+M     // mask 與 D 同時更新為 2*mask
    @LOOP
    D;JNE      // mask != 0 → 繼續掃
// RAM[1] = count
    @count
    D=M
    @R1
    M=D
(END)
    @END
    0;JMP

兩個值得玩味的細節:

  • MD=D+M:一條指令同時把新遮罩寫回記憶體 留在 D 裡給緊接的 D;JNE 判斷—— Hack 的多目的地寫法省一次讀取;

  • 負數也正確:\(mask\) 掃到 0x8000 時, D&M 若非零其值是 \(-32768\), 但 JEQ 只問「是否為 0」,不受符號影響 ✓。 測試:RAM[0]\(=-1=\)0xFFFF \(\to\) 16; RAM[0]\(=11\) \(\to\) 3 ✓。

xor.asm:用 AND/OR/NOT 拼出 XOR

xor.asm:若 RAM[0] 能被 256 整除 (即最低 8 位全為 0),翻轉 RAM[1]第 3 與第 5 高位。 例:RAM[0]\(=512=\)0x0200 時, RAM[1]\(=2831=\)0x0B0F 應變成 \(8975=\)0x230F

第一步:算出兩個遮罩。

  • 整除 256 的判斷:低 8 位全零 \(\Leftrightarrow\) \(\texttt{RAM[0]} \mathbin{\&} \texttt{0x00FF} = 0\)。遮罩 \(= 255\)

  • 要翻的位:16 位元字組的最高位是 「第 1 高位」(bit 15),所以第 3 高位 \(=\) bit 13 (\(2^{13}=8192\))、第 5 高位 \(=\) bit 11(\(2^{11}=2048\))。 遮罩 \(m = 8192 + 2048 = 10240 = \texttt{0x2800}\)。 驗證:\(\texttt{0x0B0F} \oplus \texttt{0x2800} = \texttt{0x230F}\) ✓ (0000 1011… \(\to\) 0010 0011…:bit 13 由 1 翻 0、 bit 11 由 0 翻 1)。

第二步:沒有 XOR?自己拼。 \[x \oplus m \;=\; (x \land \lnot m) \,\lor\, (\lnot x \land m)\] (「\(x\) 中不在 \(m\) 的部分」加上「\(m\) 中不在 \(x\) 的部分」。) Hack 的 comp 欄有 !A!M, 所以 \(\lnot m\) 不用另存變數:@10240 後直接 D=!A 就是 \(\lnot m\)—— 注意 \(\lnot m = \texttt{0xD7FF} = 55295\) 超過 15 位元, 不能@55295 載入, 這正是題目說「@ 指令只吃 15 位元正整數」的實戰場景。

// xor.asm -- 若 RAM[0] % 256 == 0,
//            RAM[1] ^= 0x2800(翻 bit13 與 bit11)

// if ((RAM[0] & 255) != 0) goto END
    @R0
    D=M
    @255
    D=D&A      // 常數用 A,不是 M!
    @END
    D;JNE
// t1 = RAM[1] & ~mask
    @10240
    D=!A       // D = ~0x2800 = 0xD7FF
    @R1
    D=D&M      // D = RAM[1] & ~mask
    @t1
    M=D
// D = ~RAM[1] & mask
    @R1
    D=!M       // D = ~RAM[1]
    @10240
    D=D&A      // D = ~RAM[1] & mask
// RAM[1] = t1 | D
    @t1
    D=D|M
    @R1
    M=D
(END)
    @END
    0;JMP

易錯點整理:

  • 「第 \(k\) 高位」要換成 bit \(16-k\)—— 數錯一位整題就錯(第 3 高位是 bit 13 不是 bit 3!);

  • D=D&A(跟常數 AND)與 D=D&M (跟記憶體 AND)一字之差;

  • 整除判斷只看低 8 位, 跟 RAM[0] 其餘位無關—— \(\texttt{RAM[0]}=0\) 也算整除(\(0 \bmod 256 = 0\)), 本程式行為正確。

Hack 的輸入與輸出(Input and Output)

記憶體映射 I/O 速查(先看熟再動手):

  • 螢幕\(512 \times 256\) 黑白像素, 映射到 RAM[16384..24575](符號 @SCREEN), 每列 32 個字組。像素 \((row, col)\) 住在 RAM[16384 + 32\(\cdot\)row + col/16] 的第 \(col \bmod 16\) 位;位 0(LSB)畫在最左邊 ——字組值 1 只點亮該組 16 像素的最左一顆; 1 = 黑、0 = 白;

  • 鍵盤:單一字組 RAM[24576] (符號 @KBD)。有鍵按住 = 該鍵掃描碼 (c\(=99\),即小寫字母的 ASCII), 沒按 = 0;

  • @ 指令限制@x\(x\) 只能是 變數、標籤或 15 位元正整數\(0 \sim 32767\))。 要寫入像 0x80000xFFFF 這樣的 16 位元圖案,得用遮罩或取負 (M=-1M=-MD=A+1 溢位 等技巧)。

checker1.asm:兩顆角落像素

checker1.asm:沒有鍵按下時, 螢幕左上角像素黑、其餘全白; 按住 c 鍵時,右下角像素黑、其餘全白。

先算地址與值(動手前的功課):

  • 左上角像素 \((0,0)\)RAM[16384] 的 bit 0 \(\Rightarrow\) 寫入值 0x0001,直接 M=1

  • 右下角像素 \((255,511)\): 字組位址 \(16384 + 32 \times 255 + 511/16 = 16384 + 8160 + 31 = 24575\)(螢幕最後一個字組), 位元 \(511 \bmod 16 = 15\)(MSB)\(\Rightarrow\) 寫入值 \(\texttt{0x8000} = -32768\)@32768 不合法!取得它的技巧: @32767D=A+1—— \(32767 + 1\) 在 16 位元中溢位,恰好是 \(-32768 = \texttt{0x8000}\) ✓(這正是題目提示「用遮罩或取負」 的用意;D=-A 給的是 \(-32767 = \texttt{0x8001}\), 多亮一顆像素,不行)。

程式骨架:無窮輪詢(poll)鍵盤, 每圈把兩顆像素都寫一次(一顆設、一顆清), 這樣按下/放開的瞬間畫面自動更新:

// checker1.asm
// 無鍵:左上角黑。按住 c(=99):右下角黑。
(MAIN)
    @KBD
    D=M
    @99
    D=D-A
    @CKEY
    D;JEQ          // 按住 c → CKEY
// 預設狀態:RAM[24575]=0、RAM[16384] bit0=1
    @24575
    M=0
    @SCREEN
    M=1
    @MAIN
    0;JMP
(CKEY)
// c 狀態:RAM[16384]=0、RAM[24575] bit15=1
    @SCREEN
    M=0
    @32767
    D=A+1          // D = 0x8000(溢位技巧)
    @24575
    M=D
    @MAIN
    0;JMP

測試要領:用 No animation 模式 run, 按下螢幕輸出區下方的鍵盤按鈕、點進鍵盤輸入區按住 c。若像素出現在錯的位置: 停下來重看影片想清楚(最常見的誤解: 以為 MSB 在左——其實 LSB 才在左; 或把每列 32 字組算成 16)。别盲目亂試。

checker2.asm:全螢幕棋盤

checker2.asm:沒有鍵按下時, 整個螢幕呈棋盤圖樣(左上角像素黑); 按住 c 時呈相反棋盤(左上角白)。 無論何時按下 c,左上角像素都要即時正確。

先算要寫什麼值(題目再三叮嚀的功課): 單像素棋盤 = 同一列內黑白相間、相鄰列錯開一位。

  • 偶數列(第 0, 2, 4, …列): 最左像素黑 \(\Rightarrow\) bit 0 \(= 1\),之後隔一位一個 1 \(\Rightarrow\) 字組 \(= \texttt{0b0101\,0101\,0101\,0101} = \texttt{0x5555} = 21845\)(15 位元內, @21845 直接合法 ✓);

  • 奇數列:整體錯開一位 \(\Rightarrow\) \(\texttt{0xAAAA} = \lnot\texttt{0x5555}\)—— 一個 NOT 就翻列,不需要第二個常數;

  • 按住 c 的相反棋盤:兩種列值互換 ——起始值從 0x5555 換成 0xAAAA, 其餘邏輯完全相同。

先寫個五行小程式手動把 0x55550xAAAA 塞進 RAM[16384]RAM[16416](第二列開頭) 驗證圖樣方向——理解錯了早點發現,省大痛。

結構:外層無窮迴圈重讀鍵盤決定起始圖樣, 內層雙迴圈掃 256 列 \(\times\) 32 字組,每列結尾把 \(val\) NOT 一次:

// checker2.asm -- 全螢幕棋盤,按住 c 反相
(MAIN)
// val = 0x5555;若按住 c 則 val = 0xAAAA
    @21845
    D=A
    @val
    M=D
    @KBD
    D=M
    @99
    D=D-A
    @DRAW
    D;JNE          // 不是 c → 用預設圖樣
    @val
    M=!M           // c 按住 → 0xAAAA
(DRAW)
// addr = SCREEN;row = 0
    @SCREEN
    D=A
    @addr
    M=D
    @row
    M=0
(ROWLOOP)
    @k
    M=0            // k = 本列已寫字組數
(WORDLOOP)
// RAM[addr] = val(指標間接定址!)
    @val
    D=M
    @addr
    A=M            // A ← addr 的內容
    M=D            // 寫入 RAM[addr]
// addr++
    @addr
    M=M+1
// if (++k < 32) 繼續本列
    @k
    MD=M+1
    @32
    D=D-A
    @WORDLOOP
    D;JLT
// 列結束:val 反相(隔列錯位)、下一列
    @val
    M=!M
    @row
    MD=M+1
    @256
    D=D-A
    @ROWLOOP
    D;JLT
    @MAIN          // 整幅畫完 → 回去重讀鍵盤
    0;JMP

三個關鍵技術點:

  • 記憶體間接定址@addr / A=M / M=D ——「把 D 寫進『addr 所存位址』的格子」。 這是螢幕程式的核心語法,也是本題想逼你練的東西;

  • 每列 NOT 一次\(\lnot\texttt{0x5555} = \texttt{0xAAAA}\)\(\lnot\texttt{0xAAAA} = \texttt{0x5555}\)——256 列後自動回到起點; 「反相棋盤」也只是換個起始值,程式其餘部分零改動;

  • 「隨時按都正確」:整幅畫完才回 MAIN 重讀鍵盤,反應延遲 = 畫一幅的時間 (約 \(8192 \times\) 十餘條指令)。在模擬器的 No animation 模式下這通常夠快;若想更即時, 可在每列結尾檢查 KBD 是否變化、 變了就跳回 MAIN 重畫(軟體版的 「升降緣偵測」——正是第四週 rise/fall detector 的軟體翻版)。

為什麼這題是好考題(原文的話): 螢幕鍵盤程式同時考驗記憶體間接定址、條件、 迴圈與二進位表示——期末通常有一題 15 分的同型題。 把 checker2 獨立寫出來,你就準備好了。

效率討論:能不能少寫一點? 本解每幅重寫全部 8192 個字組。兩個經典優化:

  • 狀態變化才重畫:記住上一圈的鍵盤狀態 prev,只在 \(\texttt{KBD} \ne \texttt{prev}\) 跨越「是否為 c」的邊界時重畫 (軟體版 rise/fall detector);平常螢幕不動, 迴圈只剩幾條指令;

  • 就地反相:兩種棋盤恰好互為 NOT—— 切換時不必重算圖樣,掃一遍螢幕做 M=!M 即可(免載入常數, 每字組省兩三條指令)。

這些技巧在真實的圖形程式裡就是「dirty rectangle」 與「就地位元運算」的雛形。

附錄:速查表

本作業的五支程式

程式 功能 核心技巧
sub.asm R2 = R0 - R1 D 暫存器搬運
mult.asm R2 = R0 * R1 重複加法迴圈
and.asm R1 = popcount(R0) 遮罩自加=左移;溢位終止
xor.asm 條件翻轉 bit 13, 11 \(x \oplus m = (x \land \lnot m) \lor (\lnot x \land m)\)
checker1/2.asm 螢幕棋盤+鍵盤 記憶體映射 I/O、間接定址

除錯工具速查

工具 何時用 在哪裡
Fast translation .asm 拒載(語法錯誤) Assembler 工具列
Asm/Sym 切換 符號解析問題(打錯標籤名) ROM 顯示區上方下拉
Single step/back 逐指令觀察 模擬器工具列
中斷點(右鍵 ROM) 跳過長迴圈 ROM 顯示區
條件中斷點 \(n\) 圈才壞 工具列旗子圖示
Format: binary 看遮罩/位元 視窗右上角

常見 bug 型錄

Bug 症狀
A/M 混淆(D&A vs D&M 常數變成讀記憶體,值不可預期
打錯標籤名(默默變新變數) 跳躍亂飛/變數恆 0
D 殘值 分支後用到上一段的舊 D
迴圈邊界(JGT vs JGE) 多跑或少跑一圈
忘記結尾無窮迴圈 PC 跑進垃圾指令
No animation 下看 RAM 顯示不更新(其實已算完)

關鍵常數

常數 意義
@SCREEN \(=16384\) 螢幕映射起點;每列 32 字組
\(24575\) 螢幕最後一個字組(右下角那 16 顆像素)
@KBD \(=24576\) 鍵盤暫存器;c\(=99\)
0x5555 \(=21845\) 棋盤偶數列(LSB 在左=左上黑)
0xAAAA \(=\lnot\texttt{0x5555}\) 棋盤奇數列
0x8000 \(=\texttt{@32767} + \texttt{D=A+1}\) MSB 遮罩(右下角像素)
0x2800 \(=10240\) 第 3、5 高位遮罩(bit 13, 11)
\(255 = \texttt{0x00FF}\) 低 8 位遮罩(整除 256 判斷)

參考資料

  • COMSM1302 第五週講義與影片 5-2/5-3/5-4 (Hack 組語、流程控制、記憶體映射 I/O), University of Bristol.

  • Nisan & Schocken, The Elements of Computing Systems(nand2tetris),Ch. 4 與 Project 4—— Mult.asmFill.asm 是本作業的近親.

  • nand2tetris CPU Emulator Tutorial—— 中斷點為 \(\langle\)變數, 值\(\rangle\) 配對 (ADPCRAM[i]time).

  • Hack 螢幕映射:像素 \((row, col) \to\) RAM[16384+32row+col/16] 的第 \(col \bmod 16\) 位, LSB 畫在左.