【深圳 IO 攻略】阿瓦隆城第 10 關(guān):神經(jīng)處理邏輯主板

本文首發(fā)于 B 站《深圳 IO》文集(https://www.bilibili.com/read/readlist/rl569860)。原創(chuàng)不易,轉(zhuǎn)載請注明出處。
關(guān)卡展示

出現(xiàn)了,長度 25 的數(shù)據(jù)包,所有關(guān)卡里最長的,沒有之一。
本關(guān)里,我們的線路板上已經(jīng)固定好了兩個神經(jīng)處理單元:LOGOS 和 TELOS。這兩個元件占用空間極大,且已被焊死在電路板上,不能移動,非??印?/p>
這兩個元件左側(cè)的 x 口會不定期地發(fā)送一些長度為 25 的數(shù)據(jù)包,設(shè)這些數(shù)字為 a~y。僅當(dāng)收到的數(shù)據(jù)包滿足 a - b + c - d + e - ... - x + y = 0 時,才向【網(wǎng)眼數(shù)據(jù)】端口發(fā)送收到的數(shù)據(jù)包。否則視為數(shù)據(jù)包已損壞,直接丟棄。
與此同時,我們需要每隔 5 秒給?LOGOS 和 TELOS 右側(cè)的泵端口發(fā)送一次心跳信號。特殊地,當(dāng)輸入端生成了新的數(shù)據(jù)包時,立刻啟動心跳,并重置計時器。
本關(guān)需要 5 塊芯片和 2 塊 RAM 在一起分工合作,且需要用到一個我雪藏至今,終于不得不點破的隱藏特性。電路圖如下所示:

放置在 LOGOS 左右兩側(cè)的芯片分別用于接收 LOGOS 的數(shù)據(jù)包及控制 LOGOS 的心跳,分別為 1、2 號芯片;放置在 TELOS?左右兩側(cè)的芯片分別用于接收 TELOS?的數(shù)據(jù)包及控制 TELOS?的心跳,分別為?3、4?號芯片;最左側(cè)的芯片連接著用于存儲數(shù)據(jù)包的兩塊 RAM 以及【網(wǎng)眼輸出】的最終輸出口,為 5 號芯片。
你可能會覺得 1、2 號芯片的連線有點奇怪。1 號芯片的 x0 口,以及 2 號芯片的 x1 口,各有一截導(dǎo)線露在外面,但看上去又沒跟任何端口相接。與此同時,LOGOS 和 TELOS 中間的那一條導(dǎo)線是從哪來的,找不到源頭呀?
其實,這個游戲的導(dǎo)線,不僅可以放置在線路板的空白位置,還可以放在芯片的背面。當(dāng)線路板的空間捉襟見肘時,我們?nèi)匀豢梢岳迷趁娴目捎每臻g,來連接導(dǎo)線。我們按住 TAB 鍵,可以看到電路板的透視圖:

破案了。1 號芯片的 x0 口,以及 3?號芯片的 x1 口的導(dǎo)線的電流匯集到了 LOGOS 和 TELOS 中間的那一條路上,之后又分流到了右側(cè)控制各自心跳的 2、4 號芯片上。這種從元件背面穿過的導(dǎo)線(簡稱【背線】,下同)有兩種畫法:一是按住 TAB 鍵后畫,二是先把導(dǎo)線畫好,再拖動元件到對應(yīng)的位置,讓元件“覆蓋到導(dǎo)線上面”。
以前的關(guān)卡,電路板的可用面積都十分充足,因此我一直沒有提到【背線】這樣的隱藏特性。但是這一關(guān),因為兩塊巨大的神經(jīng)處理單元占用掉了相當(dāng)多的線路板面積,且無法移動,已經(jīng)屬于是不走背線就沒地方放置導(dǎo)線了。我不得已才點破了這個一直以來都被雪藏了的特性。
唉,明明電路板右邊還有那么多的空白。但凡這兩個大東西可以向右挪一格,我都不至于要這么尷尬地走背線。
本關(guān)的 LOGOS 和 TELOS 僅僅是名字不一樣,功能上是完全一樣的。為了降低問題復(fù)雜度,我們先考慮只監(jiān)視其中一塊主板,無視另一塊。觀察時序圖發(fā)現(xiàn),第一個樣例里,【網(wǎng)眼輸出】端口打印出的都是 TELOS 的數(shù)據(jù)包。如果我們先實現(xiàn) LOGOS 的部分,那即使實現(xiàn)了也無法打印出任何數(shù)據(jù)包,也就無法驗證算法的正確性。所以這里我們先實現(xiàn)?TELOS 的部分。我們先寫出和 TELOS 直接相連的 3、4 號芯片的代碼:

3 號芯片每秒鐘都將 TELOS?中的首數(shù)字發(fā)給 4?號芯片,由它判斷是否需要開啟 TELOS?的心跳并重置計時器(mov x3 dat, mov dat?x1)。之后檢查首數(shù)字是否為 -999(tcp dat -999)。首數(shù)字是 -999 時,說明本秒沒有數(shù)據(jù)包,也就不需要計算校驗碼,更不需要向左側(cè)的 5 號芯片發(fā)送數(shù)據(jù),此時直接關(guān)閉所有 + - 號指令跳到最后睡覺即可(slp 1)。首數(shù)字不為 -999 時,說明本秒鐘里會從 TELOS?收到一個長度為 25 的數(shù)據(jù)包。我們將首數(shù)字發(fā)給左側(cè)的 5 號芯片后(+ mov dat x0),接下來的第 5、6、7、8、9、4 行代碼構(gòu)成了一個“先判斷,后執(zhí)行”的循環(huán):若前一個讀入的數(shù)字不為 -999(+ tgt dat -999),我們就將 acc 取反(+ mul -1)并加上這個數(shù)字(+ add dat)。做完這些后再從 TELOS?讀入下一個數(shù)字發(fā)出去(+ mov x3 dat, + jmp 4),如此循環(huán),直到讀入的數(shù)字為 -999 為止。讀到 -999 后,將計算好的校驗碼發(fā)給 5 號芯片(- mov acc x0),并在發(fā)送完畢后,清除殘留在 acc 里的校驗碼數(shù)據(jù),準(zhǔn)備迎接下一次任務(wù)(- sub acc)。至此,本秒的任務(wù)就完成了,可以安心睡覺了(slp 1)。
也就是說,沒有從 x 口接收到數(shù)據(jù)包時(首數(shù)字是 -999),則只給 4?號芯片發(fā)信號,不給 5 號芯片發(fā)信號;一旦從 x 口接收到了數(shù)據(jù)包(首數(shù)字不是 -999),則除了要將首數(shù)字發(fā)給 4?號芯片外,還要不斷從 TELOS 中讀取數(shù)字發(fā)給 5 號芯片,把整個數(shù)據(jù)包都發(fā)給 5 號芯片,直到讀完數(shù)據(jù)包內(nèi)所有的數(shù)字,接收到 -999 為止。同時,因為循環(huán)結(jié)束的條件是【前一個接收到的數(shù)字是 -999】,所以剛接收到最后的 -999 時,也要將這個 -999 發(fā)送給 5 號芯片。這是為了給 5 號芯片發(fā)送一個結(jié)束信號。5 號芯片因為端口數(shù)量有限,只連接了 RAM 的數(shù)據(jù)口,沒有連接地址口,它自己無法方便地判定還剩余多少個數(shù)字要接收。這一點我們在說到 5 號芯片時也會去詳細(xì)解釋。
我們在接收數(shù)據(jù)包的過程中,是這樣迭代計算校驗碼的:
若前一個讀入的數(shù)字不為 -999(+ tgt dat -999),我們就將?acc 取反(+ mul -1)并加上這個數(shù)字(+ add dat)。
每讀入一個數(shù)字就將 acc 取反后再加上這個數(shù)字。而題目要求用第一個數(shù) 減去 第二個數(shù) 加上 第三個數(shù) 減去 第四個數(shù) 加上 第五個數(shù)……這樣的方式來計算校驗碼。這里,我這樣簡單迭代了一下,就規(guī)避掉了原始的“加減號反復(fù)橫跳”的運算。這是為什么呢?我們來分析一下:
當(dāng)收到第一個數(shù) a 時,acc 的值變成了:0 × (-1) + a = a;
當(dāng)收到第二個數(shù) b 時,acc 的值變成了:a × (-1) + b = -a + b;
當(dāng)收到第三個數(shù) c 時,acc 的值變成了:(-a + b) × (-1) + c = a - b + c;
當(dāng)收到第四個數(shù) d 時,acc 的值變成了:(a - b + c) × (-1) + d = -a + b - c + d;
當(dāng)收到第五個數(shù) e 時,acc 的值變成了:(-a + b - c + d) × (-1) + e = a - b + c - d + e;
……
當(dāng)?shù)降谄鏀?shù)項時,所有奇數(shù)項前都是 + 號,所有偶數(shù)項前都是 - 號;當(dāng)?shù)降谂紨?shù)項時,所有奇數(shù)項前都是 - 號,所有偶數(shù)項前都是 + 號。因此,如果數(shù)據(jù)包的長度是偶數(shù),那么迭代后需要整體乘以 -1,將符號翻轉(zhuǎn)成“奇數(shù)項前是 + 號,偶數(shù)項前是 - 號”才能得到正確的校驗碼;而如果數(shù)據(jù)包的長度是奇數(shù)!迭代后就會直接滿足“奇數(shù)項前是 + 號,偶數(shù)項前是?-?號”這樣的條件,此時不用做任何處理,直接得到的就是最終的校驗碼!本題中,數(shù)據(jù)包的長度是 25,是奇數(shù)。因此只要按照這樣的方法迭代,就能得到正確的校驗碼。
然后是控制 TELOS?心跳的 4?號芯片。當(dāng)?3?號芯片發(fā)來的數(shù)字不是 -999 時(tgt?x1 -999),立刻產(chǎn)生心跳信號(+?mov 100 p0, slp 1),1 秒后同時重置心跳信號和計時器(+?mov p0 acc)。而當(dāng) 1 號芯片發(fā)來的首數(shù)字是 -999 時,需要判定 acc 是否到達(dá)了 4,即前一秒是不是計時器重置以來的第 4 秒(- teq acc 4)。前一秒尚未到達(dá)第 4 秒時,令計時器?+1(- add 1);若前一秒到達(dá)了第 4 秒,則本秒是第 5 秒,同樣產(chǎn)生心跳信號(+ mov 100 p0, slp 1),同樣在 1 秒鐘后重置心跳和計時器(+ mov p0 acc)。
最后的麻煩來了,長度為 25 的數(shù)據(jù)包,怎么存儲,并在校驗正確的情況下讀取并發(fā)送呢?請看和【網(wǎng)眼數(shù)據(jù)】端口通信的 5?號芯片的代碼:

每個數(shù)據(jù)包有 25 個數(shù)字,需要兩塊 RAM 才能存得下。這里,同時操作兩塊 RAM 的話,就只能連接數(shù)據(jù)口,而不能連接地址口了,如圖所示。不過,在這一關(guān)里,我們即使不連接地址口,也是可以完成任務(wù)的。
首先我們等待 3 號芯片的喚醒信號(slx x2)。3 號芯片一共會向本芯片發(fā)送(包括 -999 結(jié)束標(biāo)志在內(nèi)的)26 個數(shù)字,我們將第奇數(shù)個數(shù)字放在下方的 RAM 里(mov x2?x1),而將第偶數(shù)個數(shù)字放在上方的 RAM 里(mov x2?acc, mov acc x3)。每接收到一次第偶數(shù)個數(shù)字,就判斷剛才接收的數(shù)字是否是 -999(teq acc -999)。如果接收到的不是 -999,就說明仍有待接收的數(shù)字,此時關(guān)閉所有的 + 號指令,跳回第 1 行繼續(xù)接收,如此循環(huán),直到接收到 -999 為止。接收到 -999 后,3 號芯片還會把校驗碼也發(fā)過來,我們檢查校驗碼是否為 0(+ teq x2?0)。當(dāng)校驗碼不為 0 時,說明我們收到了受損的數(shù)據(jù)包,直接什么都不做,關(guān)閉所有的 + 號指令,跳回第 1 行等待下一個數(shù)據(jù)包。而如果校驗碼為 0,說明收到的數(shù)據(jù)包是有效的,我們需要將這個數(shù)據(jù)包從 RAM 中重新讀出來,并發(fā)給【網(wǎng)眼數(shù)據(jù)】端口。
因為一共接收了(包括 -999 結(jié)束標(biāo)志在內(nèi)的)26 個數(shù)字,所以讀取之前,我們的兩塊 RAM 的地址指針都偏移到了原始地址?+13 的位置。這時候,我們需要同時讓兩塊 RAM 的地址都增加 1,這樣即可讓?RAM 指針偏移到相對原始地址 +14 的位置,即回到原位。這里,我們在一條指令里讀一個 RAM 的值,并寫給另一個 RAM,令兩塊 RAM 的指針都 +1(+ mov x1 x3)。發(fā)送數(shù)據(jù)包時,我們先從下方的 RAM 里讀取第奇數(shù)個數(shù)字直接發(fā)送(+ mov x1 x0),再從上方的 RAM 里讀取第偶數(shù)個數(shù)字暫存(+ mov x3 acc)。讀到第偶數(shù)個數(shù)字時,需要先判斷是否讀取到了結(jié)束標(biāo)志 -999(+ tcp?acc -999)。如果尚未讀到結(jié)束標(biāo)志,則說明讀到的是一個有效的“第偶數(shù)位數(shù)字”,將它發(fā)送給輸出端口后(+ mov acc x0),跳回到第 8 行繼續(xù)讀取下一組數(shù)字(+ jmp 8)。如此循環(huán),直到讀到 -999 為止,當(dāng)前數(shù)據(jù)包就發(fā)送完畢了,跳回到第 1 行等待下一次任務(wù)即可。
點擊左下角的【模擬】,觀察時序圖:

發(fā)現(xiàn)【網(wǎng)眼輸出】和【TELOS 泵】的輸出都正確了?!綥OGOS 泵】因為還沒有寫入控制代碼,所以暫時沒有輸出信號。
現(xiàn)在我們要給 1、2 號芯片寫上代碼,讓?LOGOS 跑起來。1、2 號芯片的代碼和 3、4 號芯片的代碼非常相似,僅僅做了點微調(diào)。如下:

1 號芯片在 3 號芯片代碼的基礎(chǔ)上做了兩處修改:①1 號芯片改為 x0 口和右邊連接,x1 口和左邊連接,和 3 號芯片正好反過來,因此代碼里的 x0、x1 做了互換;②第 2、3 行代碼的順序做了調(diào)整。3 號芯片中是先發(fā)心跳信號再判斷首數(shù)字是不是 -999,而 1 號芯片是先判斷首數(shù)字是不是 -999,激活 + - 號后再發(fā)心跳信號。
2 號芯片的代碼乍一看你可能會覺得很奇怪,為什么在測試指令之前就出現(xiàn)了帶 + 號前綴的指令呢?其實對比一下 2 號和 4 號芯片的代碼:
你會發(fā)現(xiàn) 2 號芯片其實是將 4 號芯片的后兩行代碼挪到了最前方,然后把 slp 指令改為了 slx 指令。由于上電后 +、- 號前綴都處于關(guān)閉狀態(tài),帶 + 號前綴的指令在第一秒里一定不會被執(zhí)行。因此,2 號芯片的代碼等價于以下代碼:
實際上只是將 4 號芯片里的 slp 1(睡一秒后自然醒)改為了 slx x1(等待別人叫醒),并在開頭加了一句?@ slx x1 而已。
以上的改動都是為了時序同步上的需要。LOGOS 和 TELOS 兩塊巨大的神經(jīng)處理單元中間只有短短的一行空白可以讓導(dǎo)線經(jīng)過,右邊的兩塊芯片都通過這同一根導(dǎo)線接收數(shù)據(jù),因此時序上的同步很重要。如果兩塊芯片互相接收到了原本是發(fā)給對方的數(shù)據(jù),就會導(dǎo)致心跳信號的混亂。
3 號芯片先發(fā)送首數(shù)字,再判斷它是不是 -999;1 號芯片先判斷首數(shù)字是不是 -999,激活 + - 號后再發(fā)送它。中間狹窄通路上的導(dǎo)線,每秒鐘都會有兩個數(shù)字在跑。我們經(jīng)過以上同步處理后,可以確保在這條狹窄的通路上,TELOS 的數(shù)字一定在前,LOGOS 的數(shù)字一定在后。右側(cè)的 4 號、2 號芯片則是:數(shù)據(jù)還在狹窄通路上行進(jìn)的時候,4 號芯片就已經(jīng)迫不及待了(tgt x1 -999),而 2 號芯片還在呼呼大睡(slx x1);數(shù)據(jù)到達(dá)后,4 號芯片“快人一步”將第一個數(shù)字(TELOS)拿走的時候,2 號芯片才剛醒,等到撐完懶腰以后,就只能拿走第二個數(shù)字(LOGOS)了。
我們這樣微調(diào)了一下時序以后,就可以確保數(shù)據(jù)的發(fā)送和接收都處于有序狀態(tài),這樣右側(cè)的兩塊芯片收到的都是自己所需要的數(shù)字,不會產(chǎn)生混亂。
點擊左下角的【模擬】,稍等片刻,便會彈出結(jié)算界面:

各位,別急著走,這還不是最終 BOSS 關(guān),最終 BOSS 關(guān)是編號為 3113 的隱藏關(guān)第 3 關(guān)。最終 BOSS 關(guān)就算世界紀(jì)錄,電量也超過了 5K,可想而知復(fù)雜度有多高。