【深圳 IO 攻略】第 27 關(guān):深海探測網(wǎng)格

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

本關(guān)有【聲納】和【磁】兩個波形輸入,同時 C2S-RF901 會不定期地提供一些長度為 2 的數(shù)據(jù)包。僅當(dāng)收到的數(shù)據(jù)包的首數(shù)字和鎖中的數(shù)字相等時才向 tx 端口輸出數(shù)據(jù)包,要輸出的數(shù)據(jù)包的內(nèi)容和收到的數(shù)據(jù)包的第二個數(shù)字有關(guān):第二個數(shù)字為 1 時,輸出(包括當(dāng)前時間在內(nèi)的)前 6 秒的【聲納】值;第二個數(shù)字為 2 時,輸出前 6 秒的【磁】值。
思路:由于 p 口數(shù)據(jù)“一旦錯過就不再”,所以對于兩種波形數(shù)據(jù),我們只能像下面這樣“錯位存儲”:

當(dāng)我們讀取數(shù)據(jù)的時候,也必須“隔行掃描”錯位讀取。聲納數(shù)據(jù)只放在偶數(shù)地址里,磁數(shù)據(jù)只放在奇數(shù)地址里。如果需要讀取前 6 秒內(nèi)的【聲納】數(shù)據(jù),我們需要將地址指針前移 12 格(或者后移 2 格)后,讀取一格,舍棄一格,如此循環(huán) 6 次,就成功發(fā)送了【聲納】的數(shù)據(jù)包。如果需要讀取前 6 秒內(nèi)的【磁】數(shù)據(jù),則改為將地址指針前移 11?格(后移 3 格),循環(huán)部分同樣讀取一格,舍棄一格,如此循環(huán) 6 次,就成功發(fā)送了【磁】的數(shù)據(jù)包。電路圖和代碼如下:

首先我們將當(dāng)前的聲納和磁數(shù)據(jù)存入 RAM(mov p1 x0, mov p0 x0),然后判定當(dāng)前數(shù)據(jù)包的首數(shù)字是否和鎖中的數(shù)字一致(teq x2 x3)。如果首數(shù)字和鎖中的數(shù)字不一致,直接跳到最后休眠(slp 1)。一致的情況下,根據(jù)數(shù)據(jù)包的第二個數(shù)字決定需要讓地址指針向前前進多少格。
我們之前分析過,第二個數(shù)字是 1 時,需要跳 2 格;第二個數(shù)字是 2 時,需要跳 3 格。但如果我們將循環(huán)節(jié)變?yōu)椤跋壬釛?、后讀取”的話,就變成了這樣:第一個數(shù)字是 1 時跳 1 格,第二個數(shù)字是 2 時跳 2 格。跳的格數(shù)和第二個數(shù)字完全一致了。我們的 4~6?行代碼正是這樣的邏輯:得到現(xiàn)在的地址(+ mov x1 acc),數(shù)據(jù)包的第二個數(shù)字是多少就向前跳多少格(+ add x2),然后將新地址重新放回地址寄存器(+ mov acc x1)。然后我們準(zhǔn)備進入循環(huán),令 acc 作為循環(huán)次數(shù)計數(shù)器,初始值設(shè)為 6,表示要執(zhí)行 6 次循環(huán)(+ mov 6 acc)。
第 8~12?行是個循環(huán),這里我們用到了一個特殊的寄存器:null。它的作用是,如果你從這個寄存器讀數(shù)字,那么會讀到恒 0;如果你往這個寄存器里寫數(shù)字,那么相應(yīng)的數(shù)字會被丟棄。null 寄存器通常都是和 ROM/RAM 配合使用的,我們從 ROM/RAM 的數(shù)據(jù)口里讀一個數(shù)據(jù),但是送往 null 寄存器,直接舍棄掉,這樣可以很方便地讓地址自增。當(dāng)我們只想讓地址自增,不想對獲得的數(shù)據(jù)做處理時,比起“將地址讀入 acc、令 acc +1、將新的 acc 送回地址寄存器”這三步來說,讀一次數(shù)據(jù)口,然后舍棄掉獲得的數(shù)字,是更優(yōu)的做法。
我們的循環(huán)節(jié)正是“先舍棄”(+ mov x0 null),“后讀取”(+ mov x0 x3),“循環(huán) 6 次”(+ tcp acc 1, + sub 1, + jmp 8)。循環(huán)完成后,休眠一秒,進入下一個時鐘周期(slp 1)。
點擊左下角的【模擬】,稍等片刻,便會彈出結(jié)算界面:

更新:以上方案有潛在 bug
經(jīng)評論區(qū) @Zad590 提醒,當(dāng)讀取聲納數(shù)據(jù)時,以上方案中會出現(xiàn)指針錯位的 bug,只是因為本題的測試樣例里,輸出間隔至少為 6 秒,才掩蓋了這樣的 bug。如下所示:


可以很明顯地看到,RAM 指針在讀取前指向 12 號空間,但在讀取了前 6 秒的聲納數(shù)據(jù)后,RAM 指針錯誤地指向了 11 號空間,沒有回歸原位。如果我們在下一秒里再次請求數(shù)據(jù),就會出現(xiàn)錯誤:

官方的謎題里,為了防止【確認】面板上出現(xiàn)數(shù)據(jù)包遮擋現(xiàn)象,x 口的輸入輸出量都精心控制了間隔時長。本題里,輸出的數(shù)據(jù)包之間至少間隔 6 秒鐘,成功掩蓋了這樣的 bug?,F(xiàn)在我來更新一個無 bug 的版本,確保每次讀取后指針歸位。電路圖和代碼如下:

首先我們將當(dāng)前的聲納和磁數(shù)據(jù)存入 RAM(mov p1 x0, mov p0 x0),然后判定當(dāng)前數(shù)據(jù)包的首數(shù)字是否和鎖中的數(shù)字一致(teq x2 x3)。如果首數(shù)字和鎖中的數(shù)字不一致,直接跳到最后休眠(- jmp e)。
接下來,我們將讀取前的指針地址放入 acc(mov x1 acc),將數(shù)據(jù)包中的第二個數(shù)放入 dat(mov x2 dat)。我在文章的開頭說過:
如果需要讀取前 6 秒內(nèi)的【聲納】數(shù)據(jù),我們需要將地址指針前移 12 格(或者后移 2 格)后,讀取一格,舍棄一格,如此循環(huán) 6 次,就成功發(fā)送了【聲納】的數(shù)據(jù)包。如果需要讀取前 6 秒內(nèi)的【磁】數(shù)據(jù),則改為將地址指針前移 11?格(后移 3 格),循環(huán)部分同樣讀取一格,舍棄一格,如此循環(huán) 6 次,就成功發(fā)送了【磁】的數(shù)據(jù)包。
這里我們不妨換一種思路:首先統(tǒng)一將地址指針前移 12 格(后移 2 格),然后判定要讀取的是哪種數(shù)據(jù)。讀取的是聲納數(shù)據(jù)時,先讀取,后舍棄;讀取的是磁數(shù)據(jù)時,先舍棄,后讀取。如此循環(huán)六次后,指針正好回到原位。
第 7 行的代碼在同一個周期里讀一次數(shù)據(jù)口再寫一次數(shù)據(jù)口(mov x0 x0),這樣我們就在一條指令里實現(xiàn)了令指針后移兩格的操作。而且由于操作前,指針指向的數(shù)據(jù)是前 7 秒的聲納和磁數(shù)據(jù),已經(jīng)是垃圾數(shù)據(jù),所以這一步的讀寫操作不會覆蓋關(guān)鍵數(shù)據(jù)。
第 8~13 行是一個循環(huán)。首先我們檢查數(shù)據(jù)包中的第二個數(shù)是否為 1(teq dat 1)。若為 1,則說明要讀取的是聲納數(shù)據(jù),我們在循環(huán)中需要先讀取,后舍棄(+ mov x0 x3, mov x0 null);若不為 1,則說明要讀取的是磁數(shù)據(jù),我們在循環(huán)中需要先舍棄,后讀取(mov x0 null, - mov x0 x3)。每讀取一個數(shù)字,就判斷地址指針是否回到了原位(teq x1 acc)。地址指針尚未回到原位時,跳回到第 8 行繼續(xù)讀?。? jmp 8),直到地址指針回到原位為止,休眠一秒進入下一個周期(slp 1)。
點擊左下角的【模擬】,稍等片刻,便會彈出結(jié)算界面:

比前一個方案多了一行代碼,電量也增加到了 517。但是這是修復(fù)潛在 bug 必須付出的代價。