【深圳 IO 攻略】番外篇:早期謎題的極致優(yōu)化方案

本文首發(fā)于 B 站《深圳 IO》文集(https://www.bilibili.com/read/readlist/rl569860)。原創(chuàng)不易,轉(zhuǎn)載請注明出處。
當我們接觸了新的元件,以及新的 MC 系列芯片指令后,我們可以再回過頭來,將早期的一些關(guān)卡的三項指標優(yōu)化到極致。
第 2 關(guān):信號放大器,將電量優(yōu)化到 133,代碼行數(shù)減少到 3 行
電路圖和代碼如下:


這里不得不提兩個新的隱藏特性:
1. 邏輯門的輸入電平信號可以是 0~100 里的任意數(shù)字,不一定非得 0 或 100。當輸入電平?<50 時,視為假;當輸入信號 ≥50 時,視為真。


2. 有多個信號同時接在同一個【只寫】p 口時,大的電平信號會覆蓋小的電平信號,最終輸出的電平值是所有輸出信號中的最大值。

說完了這兩個隱藏特性,回到本題。

這道題的輸入量只有 0、25、50 三種,對應的輸出是 0、50、100。
其實如果輸入量只有 0、50 兩種的話,由于 0 < 50,50 ≥ 50,我們只需要一個【或門】將其轉(zhuǎn)成 0、100 信號就 OK 了。
現(xiàn)在因為有第三種信號:25,直接用或門的話,輸出信號是 0。我們的芯片做的就是這樣的事:當信號是 25 時,手動將輸出信號擴大為 50。
首先我們需要把 p1 口清零,然后檢查 p0 口連接的【控制輸入】是否為非 0 值(tcp p0 p1,讀只寫 p1 口會得到立即數(shù) 0,同時清除寫入 p1 口的數(shù)據(jù))。如果是 0 值,什么都不用做,上方的或門以及 p1 口都會輸出 0,兩者的較大值也是 0(slp 1)。只要是非 0 值,我們就需要給 p1 口手動輸出 50,取 50 及或門中的較大值作為最終的輸出(+ mov 50 p1, slp 1)。如果是 25 值,上方的或門會輸出 0,我們輸出的?50 會將較小的 0 值覆蓋。如果是 50 值,上方的或門會輸出 100,會將我們輸出的 50 覆蓋掉。
我們用一個【或門】和一個寫了三行代碼的 MC4000 芯片,完成了“將 0、25、50 信號映射成 0、50、100 信號”的任務。點擊左下角的【模擬】,稍等片刻,便會彈出結(jié)算界面:

貴了 1 塊錢,但是電量由 240 降到了 133,代碼行數(shù)由 4 行減少到了 3 行。
第 3 關(guān):脈沖發(fā)生器,將電量優(yōu)化到 120
這一關(guān)可以使用一個【與非門】和一個【與門】搭出一個【振蕩電路】。電路圖和代碼如下:


左邊的 LC70G08 只用到了下方的輸出,是一個【與非門】;右邊的 LC70G08 只用到了上方輸出,是一個【與門】。下方用【與非門】和【與門】來代指這兩個邏輯門。
當按鈕沒按下時,與門一定會有一個 0 輸入,最終反饋到【脈沖】輸出上的值一定是 0。與此同時,與非門也同樣會有一個 0 輸入,這樣反饋到 p1,進而反饋到 p0(與非門 1 號輸入)的值會保持為 100。如下圖所示:

而當按鈕按下時,精彩的地方就出現(xiàn)了,我們逐秒分析:
①第 1 秒里,信號剛來時,p0 和按鈕的值都為 100,與非門的輸出為 0。但是!不要忘了芯片的存在!這個 0 的輸出量會經(jīng)過芯片的 p1 口,進而刷新 p0 口的值,與非門的 1 號輸入量會在這一秒內(nèi)被改寫成 0!等到芯片執(zhí)行到 slp 1 睡眠過去后,與非門的輸出會穩(wěn)定在 100,作為與門的 1 號輸入。而與門的 2 號輸入和按鈕一致,所以與門的兩路輸入在這一秒里最終都會定格在 100,這一秒的脈沖輸出為 100。如下圖所示:


②第 2 秒里,按鈕信號仍然保持著 100。芯片睡醒后,會像第一秒那樣翻轉(zhuǎn) p0 口的值,改寫【與非門輸出兼與門 1 號輸入】的值,睡過去后,本秒內(nèi)【脈沖】輸出的值會穩(wěn)定在 0。


③只要【按鈕】信號一直保持著在,邏輯就會在①、②間反復橫跳,【脈沖】輸出就會在 100、0 間反復橫跳。
我們再仔細觀察一下這個電路圖,我們每秒鐘都會將【上一秒的與非門 1 號輸入】和【按鈕輸入】的與非結(jié)果作為【本秒的與非門 1 號輸入】。由于振蕩脈沖只在按鈕按下時發(fā)生,此時相當于“每秒鐘都將【上一秒的與非門 1 號輸入】取反后作為【本秒的與非門 1 號輸入】”。每秒鐘都取上一秒鐘的反,這就實現(xiàn)了【振蕩電路】。
點擊左下角的【模擬】,稍等片刻,便會彈出結(jié)算界面:

第 4 關(guān):動畫 ESPORTS 標志,將成本壓縮到 6 塊錢
本關(guān)不使用任何邏輯門,改為用一塊 MC6000 + 一塊 DX-300 的組合,硬編碼出所有輸出口的時序,即可將成本壓縮到 6 塊錢。當然代價是巨大的電量和巨長的代碼行數(shù)。電路圖和代碼如下:


前三行代碼是用于 dat 寄存器的【取反】操作,dat 為 0 時更新為 100,dat 為 100 時更新為 0(tcp dat 50, - mov 100 dat, + mov 0 dat)。因為 dat 寄存器不能像 acc 那樣用 not 指令取反,所以我們只能退而求其次使用這樣的方式取反。
點擊 0 和點擊 1 是互反的,我們這里用 dat 寄存器同時表示【這一秒的點擊 0 信號】和【下一秒的點擊 1 信號】,所以我們在將 dat 取反后,這一秒內(nèi)賦給喝 0(mov dat p1),等睡過一秒后,下一秒里再賦給喝 1(第 13 行,slp 1 后 mov dat p0)。
第 5~9?行的代碼是用于控制喝 0~喝 2 的。任何時候,這三個信號都有且只有一個是 100。所以接到 DX-300 上以后,只可能給 DX-300 賦 1、10、100 中的一個值。我們觀察時序圖,不難發(fā)現(xiàn)以下規(guī)律:
喝 0~喝 2 的時序以 10 秒為周期循環(huán)。具體點哪個燈只跟當前秒數(shù)除以 10 的余數(shù)有關(guān)。
一個周期里,第 0~5 秒時,喝 0 點亮;第 6 或第 9 秒時,喝 1 點亮;第 7~8 秒時,喝 2 點亮。
如果我們先把第 9 秒給排除在外,其實我們不難發(fā)現(xiàn),點亮的燈和當前周期的秒數(shù)呈現(xiàn)一個三態(tài)關(guān)系:秒數(shù) < 6 時點亮喝 0;秒數(shù) = 6 時點亮喝 1;秒數(shù) > 6 時點亮喝 2。那么,我們再把第 9 秒的情況給加上,這就變成了“秒數(shù) > 6 且秒數(shù) < 9 時點亮喝 2”。
這里我們同時將秒數(shù)為 6 和秒數(shù)為 9 作為“共同的中間狀態(tài)”,將其余情況作為端點狀態(tài)。我們先假設(shè)當前處于中間狀態(tài),給 DX-300 賦 10 點亮喝 1(mov 10 x2),然后再細致判斷當前是否處于端點狀態(tài)(tcp acc 6)。如果當前秒數(shù)小于 6,毫無疑問,撤銷喝 1 的點燈狀態(tài),改為點亮喝?0(- mov 1 x2)。而當秒數(shù)大于 6 時,我們需要把秒數(shù)為 9 這樣的“非端點狀態(tài)”排除掉,需要進一步判斷秒數(shù)是否小于 9(+ tlt acc 9)。若秒數(shù)為 9,則“仍視為中間狀態(tài)”,不執(zhí)行任何操作。僅當秒數(shù)大于 6 且同時小于 9 時,才處于端點狀態(tài),才能撤銷喝 1 的點燈狀態(tài),改為點亮喝 2(+ mov 100 x2)。確定了具體點的燈后,我們休眠一秒(slp 1),然后令經(jīng)過的秒數(shù) +1(add 1),并按 10 取余(dgt 0,只取個位數(shù)相當于取除以 10 的余數(shù))。進入下一秒后,不要忘了將點擊 1 更改為上一秒點擊 0 的狀態(tài)值(mov dat p0)。
點擊左下角的【模擬】,稍等片刻,便會彈出結(jié)算界面:

如此,我們便用一塊 MC6000 + 一塊 DX-300,共計 6 塊錢的成本,成功為 5 個 p 口生成了時序圖。
第 5 關(guān):游戲積分器,將代碼行數(shù)縮減到 6 行
本題使用 ROM 打表,可以將代碼行數(shù)縮減到 6 行。電路圖和代碼如下:


我們將【得分】和【犯規(guī)】兩個輸入信號通過 DX-300 轉(zhuǎn)接,合并成一個輸入信號。這樣我們每秒鐘的輸入信號就有 0(無變化)、1(得 1 分)和 10(扣 2 分)三種,對應的分數(shù)增量分別為 0、1、-2。我們使用一塊 ROM,將 0、1、-2 的分數(shù)增量寫在 0、1、10 地址的空間處。
每秒鐘,我們都從 DX-300 中讀取輸入信號的值,然后將 ROM 的地址值置為和該信號值一致(mov x1?x3)。此時我們的指針所指向的數(shù)字對應著本輪的分數(shù)增量,我們將 acc 加上該增量值(add x2)。然后我們進一步判斷分數(shù)是否 <0(tcp acc 0)。分數(shù)不能出現(xiàn)負數(shù),一旦計算出的實時小于 0,就需要將分數(shù)強制置為 0(- sub acc)。計算完畢后,將實際的分數(shù)值發(fā)送給顯示器(mov acc x0)后,休眠一秒,進入下一個時鐘周期。
點擊左下角的【模擬】,稍等片刻,便會彈出結(jié)算界面:

我們使用打表的方法,不將分數(shù)增量寫在代碼中,而是寫在?ROM 中,將實際的代碼行數(shù)由 8 行減少到了 6 行。
第 6 關(guān):調(diào)諧最優(yōu)化引擎,將代碼行數(shù)壓縮到 6 行
我們將代碼做個微調(diào),即可將行數(shù)壓縮到 6 行。電路圖和代碼如下:


我們先不管三七二十一,先把原始信號存到 acc 里再說(mov p0 acc)。然后再判斷是否有最優(yōu)化信號(tcp x0 0),有最優(yōu)化信號時,執(zhí)行 ×4,-150 的操作(+ mul 4, + sub 150)。最后,將 acc 發(fā)給輸出端口(mov acc p1)并休眠(slp 1)。
點擊左下角的【模擬】,稍等片刻,便會彈出結(jié)算界面:

我們來回顧一下前一個方案的代碼:
前一個方案的數(shù)據(jù)流向有兩種可能:p0→p1 或 p0→acc→p1。本方案將數(shù)據(jù)流向統(tǒng)一成了 p0→acc→p1 一種,節(jié)省了一行代碼,但也導致不需要最優(yōu)化處理時會多在 acc 這里停留一步。