【深圳 IO 攻略】阿瓦隆城第 8 關(guān):腦機(jī)接口

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

從本關(guān)開始,BGM 都變得深沉了。我原以為進(jìn)了阿瓦隆城就是地獄了,沒想到前面的關(guān)卡還都是練手的,從這關(guān)開始才是地獄。
本關(guān)的要求很簡(jiǎn)單:一共有 6 路傳感器輸入。當(dāng)最近的 2 秒內(nèi)檢測(cè)到至少兩路有脈沖信號(hào)時(shí),向輸出端口報(bào)告近 2 秒內(nèi)哪些端口產(chǎn)生了脈沖信號(hào)。要求按照數(shù)字大小順序報(bào)告,不能亂序報(bào)告。報(bào)告完畢后的下一秒里不會(huì)有任何傳感器發(fā)生脈沖信號(hào),請(qǐng)確保相鄰的數(shù)據(jù)包間至少有 1 秒的間隔,不要重復(fù)報(bào)告上一秒的脈沖。
其實(shí)總共只有兩種情況需要報(bào)告:
同一秒內(nèi)有兩路以上的輸入信號(hào)產(chǎn)生了脈沖。例如:第 1 秒里,傳感器 1~3 同時(shí)變?yōu)?100,傳感器 4~6 保持為 0。此時(shí)向輸出端口發(fā)送【1、2、3】的數(shù)據(jù)包。
前一秒里有且僅有一路輸入信號(hào)產(chǎn)生了脈沖,緊接著的后一秒里有至少一路輸入信號(hào)產(chǎn)生了脈沖。例如:第 1 秒里,傳感器 1 變?yōu)?100;第 2 秒里,傳感器 2、3 變?yōu)?100。此時(shí)需要在第 2 秒時(shí)向輸出端口發(fā)送【1、2、3】的數(shù)據(jù)包。
其余的情況,都不需要報(bào)告。如果在上面的例子里,傳感器 2、3 改為在第 3 秒時(shí)才變?yōu)?100,那么在第 2 秒時(shí),傳感器 1 會(huì)回歸為“未產(chǎn)生脈沖信號(hào)”的狀態(tài),第 3 秒時(shí)我們只需要向輸出端口發(fā)送【2、3】的數(shù)據(jù)包。
本題需要 4 塊芯片分工合作完成任務(wù)。1 號(hào)芯片“兩頭跑”,先從輸入端獲取兩塊 DX-300 轉(zhuǎn)接后的三位數(shù),然后呼叫 2 號(hào)芯片,令它計(jì)算兩秒內(nèi)有多少路輸入產(chǎn)生了脈沖信號(hào)。得到 2 號(hào)芯片的反饋后,判斷產(chǎn)生了脈沖信號(hào)的路數(shù)是否 > 1。滿足條件時(shí),將兩個(gè)三位數(shù)發(fā)給 3 號(hào)芯片,然后由 3、4 號(hào)芯片共同將數(shù)據(jù)包發(fā)給輸出端口。電路圖和代碼如下:

6 路輸入,首先肯定是二話不說,兩塊 DX-300 安排上。最左邊的 MC6000 和兩塊 DX-300 直接連通,因此它是 1 號(hào)芯片。它的 x3 口連接著中央的 2 號(hào)芯片,x2 口連接著底部居中的 3 號(hào)芯片。
我們先來看“兩頭跑”的 1 號(hào)芯片的代碼:

首先把兩路 DX-300 提供的三位數(shù)都發(fā)給 2 號(hào)芯片,委托它計(jì)算近兩秒內(nèi)有多少路輸入產(chǎn)生了脈沖信號(hào)(mov x0 x3, mov x1 x3)。2 號(hào)芯片在計(jì)算完畢后,會(huì)返回一個(gè)值,這個(gè)值的含義就是【近 2 秒內(nèi)產(chǎn)生了脈沖的路數(shù)】。當(dāng)近 2 秒內(nèi)沒有發(fā)生脈沖,或有且僅有一路發(fā)生了脈沖時(shí)(tgt x3 1),僅僅將這兩個(gè)三位數(shù)暫存到 acc 和 dat 里,不通知 3 號(hào)芯片發(fā)送數(shù)據(jù)包(- mov x0 acc, - mov x1 dat)。而當(dāng)路數(shù) > 1 時(shí),我們要將近 2 秒內(nèi)發(fā)生過脈沖的所有傳感器都告知第 3 塊芯片,由它向輸出端口發(fā)送數(shù)據(jù)包。
問題來了:你怎么將當(dāng)前這 1 秒的三位數(shù)和存在 acc/dat 里的前 1 秒的三位數(shù)合并,轉(zhuǎn)換為近 2 秒內(nèi)的三位數(shù)?
答案是使用加法。我們就以前三路輸出為例:第一秒鐘傳感器 0 發(fā)生了脈沖信號(hào),x0?讀到了 001;第二秒鐘傳感器 1、2 發(fā)生了脈沖信號(hào),x0?讀到了 110。由于第一秒鐘只有一路信號(hào)產(chǎn)生了脈沖,因此在上一秒里,我們將 001 這個(gè)值存入了 acc。那么,到了這一秒時(shí),我們將上一秒的 acc 加上本秒的 x0,001 + 110 = 111,就得到了“傳感器 0~2 在近 2 秒內(nèi)都產(chǎn)生了脈沖信號(hào)”的三位數(shù)。后三路輸入也是同理。
我們的代碼也正是使用加法來計(jì)算哪些輸入在近 2 秒內(nèi)產(chǎn)生了脈沖信號(hào)的。首先,令 acc 加上 x0,將近?2 秒內(nèi)【前三個(gè)傳感器】的脈沖狀態(tài)合并后(+?add x0)發(fā)給 3 號(hào)芯片,通知 3 號(hào)芯片發(fā)送數(shù)據(jù)包的前半部分(+ mov acc x2)。然后再令?dat 加上 x1,將近 2 秒內(nèi)【后三個(gè)傳感器】的脈沖狀態(tài)合并后(+ mov dat acc, + add x1)發(fā)給 3 號(hào)芯片,通知 3 號(hào)芯片發(fā)送數(shù)據(jù)包的后半部分(+ mov acc x2)。做完這些后,休眠一秒,進(jìn)入下一個(gè)時(shí)鐘周期(slp 1)。
然后來看用于計(jì)算【近?2?秒內(nèi)產(chǎn)生了脈沖的路數(shù)】的 2 號(hào)芯片的代碼:

首先我來解釋一下右邊的 ROM 是干什么用的。這是一個(gè)用來計(jì)算【三位數(shù)里有多少個(gè) 1】的 ROM。我們將 ROM 的地址值置為八種 DX-300 三位數(shù)中的一種時(shí),其指向的數(shù)字正好等于【這個(gè)三位數(shù)中 1 的個(gè)數(shù)】。
向地址口傳入?000 時(shí),0 mod 14 = 0,地址指針會(huì)指向 0 號(hào)空間。0 號(hào)空間里的值是 0,代表 000 中有 0 個(gè) 1;
向地址口傳入?001?時(shí),1?mod 14 = 1,地址指針會(huì)指向 1?號(hào)空間。1?號(hào)空間里的值是 1,代表 001?中有 1?個(gè) 1;
向地址口傳入?010 時(shí),10 mod 14 = 10,地址指針會(huì)指向 10 號(hào)空間。10 號(hào)空間里的值是 1,代表 010 中有 1?個(gè) 1;
向地址口傳入?011?時(shí),11?mod 14 = 11,地址指針會(huì)指向 11?號(hào)空間。11?號(hào)空間里的值是 2,代表 011?中有 2?個(gè) 1;
向地址口傳入 100 時(shí),100?mod 14 = 2,地址指針會(huì)指向 2?號(hào)空間。2?號(hào)空間里的值是 1,代表?100 中有?1?個(gè) 1;
向地址口傳入?101 時(shí),101 mod 14 = 3,地址指針會(huì)指向 3?號(hào)空間。3?號(hào)空間里的值是 2,代表 101?中有 2?個(gè) 1;
向地址口傳入?110 時(shí),110 mod 14 = 12,地址指針會(huì)指向 12 號(hào)空間。12?號(hào)空間里的值是?2,代表 110 中有 2 個(gè) 1;
向地址口傳入?111 時(shí),111 mod 14 = 13,地址指針會(huì)指向 13 號(hào)空間。13?號(hào)空間里的值是 3,代表 111?中有 3 個(gè) 1。
解釋完了這個(gè) ROM 的作用,我們來看 2 號(hào)芯片的代碼。本芯片中的 acc 寄存器用于記錄前一秒里產(chǎn)生了脈沖信號(hào)的路數(shù)。首先我們接收?1 號(hào)芯片發(fā)來的第一個(gè)三位數(shù),將其置為 ROM 的地址(mov x1 x3),然后讀一格 ROM,獲得前三位數(shù)里 1 的個(gè)數(shù)。此時(shí)我們需要判斷前一秒里是不是有且僅有一路脈沖信號(hào)(teq acc 1)。如果前一秒里有且僅有一路脈沖信號(hào),那么需要將前一秒的脈沖信號(hào)順延到這一秒。我們從 ROM 中讀取到前三位數(shù)里的 1 的個(gè)數(shù)后,要將這么多 1 和前一秒的那一個(gè) 1 做累加運(yùn)算(+ add x2)。其余情況,前一秒的脈沖都不順延到這一秒,都是直接覆蓋,不做累加運(yùn)算(- mov x2?acc)。做完以上操作后,我們?cè)俳邮?1 號(hào)芯片發(fā)來的第二個(gè)三位數(shù),將其置為 ROM 的地址(mov x1 x3),然后讀一格?ROM,獲得后三位數(shù)里 1 的個(gè)數(shù)。將其累加到 acc 上(add x2)。如此,便得到了【近?2?秒內(nèi)產(chǎn)生了脈沖的路數(shù)】。我們將這個(gè)值回傳給 1 號(hào)芯片(mov acc x1)。
此時(shí)要注意,如果前一秒有且僅有一路脈沖信號(hào)(即 teq acc 1 成立),那么到了此處,我們必須要強(qiáng)制將 acc 清零(+ sub acc)。原因如下:
如果本秒內(nèi)沒有出現(xiàn)脈沖信號(hào),acc 的值會(huì)保持為 1。但是我們最多只能將孤立的脈沖信號(hào)向后順延 1 秒,不能向后順延更長(zhǎng)時(shí)間。1 秒過后,如果這個(gè)孤立的脈沖信號(hào)沒有被接收,那就只能被消滅。強(qiáng)制將 acc 清零,不讓這個(gè)孤立的脈沖信號(hào)向后順延更久。
如果本秒內(nèi)出現(xiàn)了其他的至少一路脈沖信號(hào),acc 的值會(huì)變成 2 以上。此時(shí)清零與否效果一致,但為了節(jié)省一行判斷,上一秒的 acc 是 1 時(shí),這一秒就統(tǒng)一清零了。
做完這些后,睡眠,等待下一次被 1 號(hào)芯片喚醒(slx x1)。
最后是下方用于傳輸數(shù)據(jù)的 3 號(hào)、4 號(hào)芯片:

3 號(hào)芯片在收到由 1 號(hào)芯片發(fā)來的三位數(shù)后(slx x0, mov x0 dat),依次提取出這個(gè)三位數(shù)的個(gè)位(dst 0 dat, mov acc x3)、十位(mov dat acc, dgt 1, mov acc x3)和百位數(shù),發(fā)給 4 號(hào)芯片(mov dat acc, dgt 2, mov acc x3)。1 號(hào)芯片每次呼叫 3 號(hào)芯片,都會(huì)發(fā)兩個(gè)三位數(shù),3 號(hào)芯片也相應(yīng)地重復(fù)做兩次這樣的事。
4 號(hào)芯片的 acc 用來記錄當(dāng)前接收到的是第幾位數(shù)字。6 位數(shù)字里,每出現(xiàn)一個(gè) 1,就表示當(dāng)前位數(shù)所指示的傳感器發(fā)生了脈沖信號(hào),我們就需要將對(duì)應(yīng)的編號(hào)發(fā)送給輸出端口。每當(dāng)收到一位數(shù)字時(shí)(slx x0),檢查這位數(shù)字是否大于 0(tcp x0 0)。大于 0 時(shí),將 acc 表示的位數(shù)(即傳感器編號(hào))發(fā)送給輸出端口(+ mov acc x1)。然后檢查剛才接收的是不是第 5 位數(shù)字(teq acc 5)。尚未接收到第 5 位數(shù)字時(shí),說明還有等待接收的數(shù)字,令位數(shù) +1(- add 1)并跳回第一行繼續(xù)接收,直到接收完第 5 位數(shù)字為止,將位數(shù)歸零(+ sub acc),準(zhǔn)備迎接下一次任務(wù)。
點(diǎn)擊左下角的【模擬】,稍等片刻,便會(huì)彈出結(jié)算界面:

優(yōu)化電量
題目已經(jīng)保證了報(bào)告后的下一秒一定不會(huì)有脈沖信號(hào)。所以報(bào)告完畢后的那一秒,我們可以手動(dòng)將 1 號(hào)芯片的 acc 和 dat 的值置零,無需調(diào)用 2 號(hào)芯片計(jì)算后歸零。我們將 1 號(hào)芯片的代碼改成如下的樣子:

我將改動(dòng)過的地方加上了注釋。
當(dāng)近?2 秒內(nèi)沒有發(fā)生脈沖,或有且僅有一路發(fā)生了脈沖時(shí)(tgt x3 1),僅僅將這兩個(gè)三位數(shù)暫存到 acc 和 dat 里,不通知 3 號(hào)芯片發(fā)送數(shù)據(jù)包(- mov x0 acc, - mov x1 dat)。做完這些后,只休眠 1 秒,下一秒里繼續(xù)判定是否有其他路的脈沖信號(hào)(- slp 1)。
而當(dāng)路數(shù) > 1 時(shí),首先,令 acc 加上 x0,將近?2 秒內(nèi)【前三個(gè)傳感器】的脈沖狀態(tài)合并后(+?add x0)發(fā)給 3 號(hào)芯片,通知 3?號(hào)芯片發(fā)送數(shù)據(jù)包的前半部分(+ mov acc x2)。然后再令?dat 加上 x1,將近 2 秒內(nèi)【后三個(gè)傳感器】的脈沖狀態(tài)合并后(+ mov dat acc, + add x1)發(fā)給 3 號(hào)芯片,通知 3 號(hào)芯片發(fā)送數(shù)據(jù)包的后半部分(+ mov acc x2)。做完這些后,我們手動(dòng)將 acc 和 dat 的值清零(+ mov 0 acc, + mov 0 dat),然后休眠 2 秒,跳過下一秒的脈沖檢測(cè)過程(+ slp 2)。
另外,我在龍騰第 16 關(guān)《幽靈娃娃》的攻略里說過:
播放特定音效時(shí),我們需要讀取連續(xù)的 14 個(gè)波形數(shù)據(jù)。14 除自己外的因數(shù)有 1、2、7,也就是說,我們?cè)谘h(huán)的過程中,每次循環(huán)讀取 1/2/7 個(gè)波形數(shù)據(jù)時(shí),都能保證循環(huán)正常結(jié)束。 作者:ココアお姉ちゃん https://www.bilibili.com/read/cv16930488 出處:bilibili
到這一關(guān)里,每次發(fā)送數(shù)據(jù)包時(shí),都需要檢查 6 位數(shù)是否為 1。6 除自己外的因數(shù)有 1、2、3,因此我們?cè)谘h(huán)檢查的過程中,每次檢查 1/2/3 位數(shù),都能保證循環(huán)正常結(jié)束。用于發(fā)送數(shù)據(jù)的 4 號(hào)芯片還有 3 行代碼空間,我們完全可以一次檢查 2 位數(shù),減少循環(huán)結(jié)束的判斷次數(shù)。

選中的部分是我們?cè)黾拥拇a。由于 3 也是 6 的因數(shù),我們甚至還可以一步到位,將這塊 MC4000 換成 MC6000,一次檢查 3 位數(shù)字,進(jìn)一步節(jié)省電量:

當(dāng)然,換成 MC6000 以后,端口的名稱也相應(yīng)地發(fā)生了變化,代碼中的端口名也要相應(yīng)地做修改(x1→x2, x0→x1)。
嗯……再進(jìn)一步,總共就 6 次判斷,有必要循環(huán)嗎,有必要用 acc 嗎?直接暴力大法就好:


我們加了 2 塊錢,多寫了 10?行代碼,讓電量從 1.8K 降到了 1.2K。效率提升了 1/3。