【深圳 IO 攻略】第 27 关:深海探测网格

本文首发于 B 站《深圳 IO》文集(https://www.bilibili.com/read/readlist/rl569860)。原创不易,转载请注明出处。
关卡展示

本关有【声纳】和【磁】两个波形输入,同时 C2S-RF901 会不定期地提供一些长度为 2 的数据包。仅当收到的数据包的首数字和锁中的数字相等时才向 tx 端口输出数据包,要输出的数据包的内容和收到的数据包的第二个数字有关:第二个数字为 1 时,输出(包括当前时间在内的)前 6 秒的【声纳】值;第二个数字为 2 时,输出前 6 秒的【磁】值。
思路:由于 p 口数据“一旦错过就不再”,所以对于两种波形数据,我们只能像下面这样“错位存储”:

当我们读取数据的时候,也必须“隔行扫描”错位读取。声纳数据只放在偶数地址里,磁数据只放在奇数地址里。如果需要读取前 6 秒内的【声纳】数据,我们需要将地址指针前移 12 格(或者后移 2 格)后,读取一格,舍弃一格,如此循环 6 次,就成功发送了【声纳】的数据包。如果需要读取前 6 秒内的【磁】数据,则改为将地址指针前移 11 格(后移 3 格),循环部分同样读取一格,舍弃一格,如此循环 6 次,就成功发送了【磁】的数据包。电路图和代码如下:

首先我们将当前的声纳和磁数据存入 RAM(mov p1 x0, mov p0 x0),然后判定当前数据包的首数字是否和锁中的数字一致(teq x2 x3)。如果首数字和锁中的数字不一致,直接跳到最后休眠(slp 1)。一致的情况下,根据数据包的第二个数字决定需要让地址指针向前前进多少格。
我们之前分析过,第二个数字是 1 时,需要跳 2 格;第二个数字是 2 时,需要跳 3 格。但如果我们将循环节变为“先舍弃、后读取”的话,就变成了这样:第一个数字是 1 时跳 1 格,第二个数字是 2 时跳 2 格。跳的格数和第二个数字完全一致了。我们的 4~6 行代码正是这样的逻辑:得到现在的地址(+ mov x1 acc),数据包的第二个数字是多少就向前跳多少格(+ add x2),然后将新地址重新放回地址寄存器(+ mov acc x1)。然后我们准备进入循环,令 acc 作为循环次数计数器,初始值设为 6,表示要执行 6 次循环(+ mov 6 acc)。
第 8~12 行是个循环,这里我们用到了一个特殊的寄存器:null。它的作用是,如果你从这个寄存器读数字,那么会读到恒 0;如果你往这个寄存器里写数字,那么相应的数字会被丢弃。null 寄存器通常都是和 ROM/RAM 配合使用的,我们从 ROM/RAM 的数据口里读一个数据,但是送往 null 寄存器,直接舍弃掉,这样可以很方便地让地址自增。当我们只想让地址自增,不想对获得的数据做处理时,比起“将地址读入 acc、令 acc +1、将新的 acc 送回地址寄存器”这三步来说,读一次数据口,然后舍弃掉获得的数字,是更优的做法。
我们的循环节正是“先舍弃”(+ mov x0 null),“后读取”(+ mov x0 x3),“循环 6 次”(+ tcp acc 1, + sub 1, + jmp 8)。循环完成后,休眠一秒,进入下一个时钟周期(slp 1)。
点击左下角的【模拟】,稍等片刻,便会弹出结算界面:

更新:以上方案有潜在 bug
经评论区 @Zad590 提醒,当读取声纳数据时,以上方案中会出现指针错位的 bug,只是因为本题的测试样例里,输出间隔至少为 6 秒,才掩盖了这样的 bug。如下所示:


可以很明显地看到,RAM 指针在读取前指向 12 号空间,但在读取了前 6 秒的声纳数据后,RAM 指针错误地指向了 11 号空间,没有回归原位。如果我们在下一秒里再次请求数据,就会出现错误:

官方的谜题里,为了防止【确认】面板上出现数据包遮挡现象,x 口的输入输出量都精心控制了间隔时长。本题里,输出的数据包之间至少间隔 6 秒钟,成功掩盖了这样的 bug。现在我来更新一个无 bug 的版本,确保每次读取后指针归位。电路图和代码如下:

首先我们将当前的声纳和磁数据存入 RAM(mov p1 x0, mov p0 x0),然后判定当前数据包的首数字是否和锁中的数字一致(teq x2 x3)。如果首数字和锁中的数字不一致,直接跳到最后休眠(- jmp e)。
接下来,我们将读取前的指针地址放入 acc(mov x1 acc),将数据包中的第二个数放入 dat(mov x2 dat)。我在文章的开头说过:
如果需要读取前 6 秒内的【声纳】数据,我们需要将地址指针前移 12 格(或者后移 2 格)后,读取一格,舍弃一格,如此循环 6 次,就成功发送了【声纳】的数据包。如果需要读取前 6 秒内的【磁】数据,则改为将地址指针前移 11 格(后移 3 格),循环部分同样读取一格,舍弃一格,如此循环 6 次,就成功发送了【磁】的数据包。
这里我们不妨换一种思路:首先统一将地址指针前移 12 格(后移 2 格),然后判定要读取的是哪种数据。读取的是声纳数据时,先读取,后舍弃;读取的是磁数据时,先舍弃,后读取。如此循环六次后,指针正好回到原位。
第 7 行的代码在同一个周期里读一次数据口再写一次数据口(mov x0 x0),这样我们就在一条指令里实现了令指针后移两格的操作。而且由于操作前,指针指向的数据是前 7 秒的声纳和磁数据,已经是垃圾数据,所以这一步的读写操作不会覆盖关键数据。
第 8~13 行是一个循环。首先我们检查数据包中的第二个数是否为 1(teq dat 1)。若为 1,则说明要读取的是声纳数据,我们在循环中需要先读取,后舍弃(+ mov x0 x3, mov x0 null);若不为 1,则说明要读取的是磁数据,我们在循环中需要先舍弃,后读取(mov x0 null, - mov x0 x3)。每读取一个数字,就判断地址指针是否回到了原位(teq x1 acc)。地址指针尚未回到原位时,跳回到第 8 行继续读取(- jmp 8),直到地址指针回到原位为止,休眠一秒进入下一个周期(slp 1)。
点击左下角的【模拟】,稍等片刻,便会弹出结算界面:

比前一个方案多了一行代码,电量也增加到了 517。但是这是修复潜在 bug 必须付出的代价。