欢迎光临散文网 会员登陆 & 注册

【深圳 IO 攻略】第 16 关:幽灵娃娃

2022-06-03 15:00 作者:ココアお姉ちゃん  | 我要投稿

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

关卡展示

这一关提供了一个沾满灰尘的随机数发生器。我们要做的是:当随机数发生器生成 1 时,令扬声器播放【邪恶地笑】音效;而当随机数发生器生成 2 时,令扬声器播放【令人毛骨悚然的尖叫】音效。

这两种音效的波形我们可以在数据手册中找到:

这些音效的波形数据由大量的数字组成,仅靠 MC 系列芯片里的 acc 和 dat 寄存器已经无法存储。这里我们必须要使用到元件面板里的扩展存储元件。

扩展存储元件有两种:一种是 100P-14,黄底,有 14 格额外的存储空间,数据可读可写,但初值只能是全 0,任何写操作都必须在运行时完成,不能在运行前写好初始数据。相当于现实世界里的随机存储器(RAM)。

另一种是 200P-14,黑底,同样有 14 格额外的存储空间。但是里面的数据必须在设计电路板时就确定好,运行时只能读,不能写。相当于现实世界里的只读存储器(ROM)。

这两个扩展存储元件有以下共同点:

1. 都带有两个指针。读取 a0/a1 口可以获得当前的左/右指针的位置,范围 0~13;而向 a0/a1 口写数据则可以更改左/右指针的位置。当写入的地址值在 0~13 范围之外时,会自动把地址值设置为传入的数取除以 14 的余数。例如,向 a0 口传 15 时,会将左指针置为 1;向 a1 口传 -1 时,会将右指针置为 13。

如图所示,当执行完 mov 15 x0 后,下方 ROM 的左指针指向了地址 1
如图所示,当执行完 mov -1 x1 后,下方 ROM 的右指针指向了地址 13

2. 读 d0 口会读取到左指针所指向的数字,同时左指针自动向后移动一格。读 d1 口会读取到右指针所指向的数字,同时右指针自动向后移动一格。指针自增的这个特性非常优秀,可以让我们在读连续数据时不需要反复操作地址口,只要连续读数据口就完事了。下面举一个连续从 ROM 中读取三个数,并将 acc 的百位、十位、个位依次置为这三个数的例子:

执行完第一行的 dst 2 x1 后,ROM 的右指针读到 3,将 acc 的百位置为 3。同时右指针向后移动一格,现在右指针位于地址 1 处。
执行完第二行的 dst 1 x1 后,ROM 的右指针读到 1,将 acc 的十位置为 1。同时右指针向后移动一格,现在右指针位于地址 2 处。
执行完第三行的 dst 0 x1 后,ROM 的右指针读到 4,将 acc 的个位置为 4。同时右指针向后移动一格,现在右指针位于地址 3 处。至此 acc 的三位数均已设置完毕,acc 的值为 314。
推理可得,执行完全部 8 行代码后,acc 的值为 159,ROM 的右指针指向了地址 6

3. 对于 RAM 而言,d0/d1 口不仅可读,而且可写。向 RAM 的 d0/d1 口写数据时,左/右指针指向的空间里的内容会被覆盖为新内容,同时左/右指针自动向后移动一格(指针自增这一点无论读写都一样)。下面给出一个将 ROM 中的内容复制到 RAM 中的示例程序:

首先我们从 ROM 的数据口(x0)读入数据,并将读入的数据写入 RAM 的数据口(x2)(mov x0 x2)。执行完本次操作后,ROM 的右指针以及 RAM 的左指针都会自增 1。此时我们判断任意一个地址值,如果指向了 0(teq x1 0,或 teq x3 0),说明前一次读/写操作是在 13 地址处进行的,那么就说明所有的数据都复制完毕了,直接结束程序(+ slp 999)。如果自增后的地址值没有指向 0,说明前一次读写并不是在 13 地址处进行的,那么就要跳转回去循环执行复制操作(- jmp 1),直到将 13 地址处的数据复制完成为止。

运行程序,我们发现 RAM 最终会被写入和 ROM 一模一样的数据:

前面我们花了很大的篇幅介绍 ROM 和 RAM 这两个扩展存储元件,现在我们回到题目。这道题因为要在特定的触发条件下播放两种可能的声音波形,且这两种波形数据的长度均为 13。所以我们肯定是需要两个 ROM 来存储两种声音的波形数据的。

这道题我们发现可以将一块 MC6000 物尽其用,4 个 x 口,正好接在两个 ROM 的地址口和数据口上;剩下两个 p 口,一个接随机数生成器的输入信号,一个接扬声器的输出信号。一点不浪费!我们先二话不说,搭出如下的电路图:

左边的 ROM 记录的是《邪恶地笑》的波形数据,而右边的 ROM 记录的是《令人毛骨悚然的尖叫》的波形数据。这两个 ROM 都以 50 结尾,是因为播放完对应音效后,需要将扬声器的波形值重设为 50,而不能一直停留在音效的最终波形值上。

我们现在的思路就是:当随机数发生器生成 1 时,我们在接下来的 14 个时钟周期里不断读左边 ROM 的数据口,并将相应的波形发送给扬声器;同理,当随机数发生器生成 2 时,我们在接下来的 14 个时钟周期里不断读右边 ROM 的数据口,并将相应的波形发送给扬声器。

根据一开始举的例子,我们已经知道了读完整个 ROM 的条件是:读取数据后的地址值为 0。因此我们写出如下的具有循环结构的代码:

首先,我们要给扬声器赋上 50 的初始声波(@ mov 50 p1)。接下来,我们判断当前时钟周期里的随机数是否为 1(teq p0 1)。如果是 1,我们不断从左边的 ROM 中读取数据发给扬声器(+ mov x0 p1, + slp 1),并判断读取后的地址值是否大于 0(+ tcp x1 0)。如果地址值大于 0,说明当前声波还没输出完毕,需要跳到第 3 行继续发送(+ jmp 3),直到地址值为 0 为止,关闭所有的 + - 号指令,跳到最后一行,休眠一秒,等待下一次信号(slp 1)。如果一开始的随机数不是 1,而是 2(- teq p0 2),则改为从右边的 ROM 循环读取(+ mov x2 p1, + slp 1, + tcp x3 0, + jmp 8)。等到读取完毕,右边的 ROM 的地址值为 0 时,跳到最后一行,休眠一秒,等待下一次信号(slp 1)。

点击左下角的【模拟】,稍等片刻,便会弹出结算界面:

优化电量

播放特定音效时,我们需要读取连续的 14 个波形数据。14 除自己外的因数有 1、2、7,也就是说,我们在循环的过程中,每次循环读取 1/2/7 个波形数据时,都能保证循环正常结束。为什么每次读取的数据长度必须要是总长度的因数呢?假如一次读取 3 个波形数据,那么在第 5 次循环时,就会错误地触发“读取第 15 个波形数据”这样的事件。

现在我们的 MC6000 里还有两行代码空间。我们可以将其中一个音效的循环改为“每次读取两个波形数据”,从而减少判断循环是否结束的次数。如下图所示,播放音效 2 的循环改成了“每次读取两个波形数据”。

最终的三项指标为:成本 ¥9,电量 192(比历史最佳少了 14 格电,少执行了 7 次是否终止循环的判断,和 7 次跳转),代码行数 14

优化代码行数

这个沾满灰尘的随机数发生器不会生成值为 0 的随机数,因此我们可以将读到的随机数分成三类:小于 2(即 1),等于 2,大于 2。没错,一次 tcp 三态判定就可以一劳永逸。

第一行将扬声器初始化为 50,所有设计方案都是一样的(@ mov 50 p1)。然后我们将 p0 的值存入 acc,并对 acc 做三态判定(mov p0 acc, tcp acc 2)。这时候可能有读者会问了:p 口数据又不像 x 口那样只能读一次,明明可以反复读,为啥还得存到 acc 里再做三态判定呢?因为:①p 口数据只能在当前时钟里反复读,不代表在后续的时钟周期里还能反复读当前这一秒的数字,“一旦错过就不再”;②播放音效时,循环里的跳转指令会破坏 + - 号状态,每次循环回来都要重新判定。重新判定的时候,我们不能以实时的随机数作为依据,只能以进入音效播放流程前的随机数作为依据。所以我们在进入音效播放流程前,必须要将随机数存入 acc,以方便后续的判断。

那为什么前一个方案就不需要将 p0 的值存入 acc 呢?因为前一个方案里,两个音效的播放流程是各自独立,不共享代码的。在各自的流程里,循环部分全部是用 + 号串联起来的,只要跳转指令也使用 + 号,就可以保证不破坏 + - 号状态。只有当循环的过程中使用了 + - 号分离了代码块时,循环最后的跳转指令才会起到破坏 + - 号状态的副作用。只有这种时候,才需要在循环的每个周期里都重新执行测试指令恢复 + - 号状态

回到程序。如果 acc 的值大于 2,那么直接跳到第 7 行休眠一秒(+ jmp 7)。进入下一个时钟周期后,因为两个 ROM 的数据指针都没有动过,所以后续的 tcp x3 0 判断一定是不成立的,会直接跳回第 2 行,重新对新的随机数做三态判定。

如果 acc 的值小于等于 2,那么我们需要分情况讨论是 1 还是 2。我们先假设 acc 是 2,从右边的 ROM 中读一格波形数据发给扬声器(mov x2 p1)。如果 acc 的值是 1,那么再撤销刚才发的波形数据,改为从左边的 ROM 中读取(- mov x0 p1)。读取完毕后,休眠 1 秒,让波形数据作用到扬声器上(slp 1)。这就相当于:当 acc 是 2 时,只读取右边 ROM 的波形;而当 acc 是 1 时,左右两边的 ROM 一起读取,但最终生效的是左边 ROM 的波形。因此,只要进入了音效播放流程,右边的 ROM 指针是一定会动的。此时,我们可以统一以右侧 ROM 的地址值是否为 0,作为判定循环结束的依据(tcp x3 0)。如果该地址值大于 0,说明当前声波还没输出完毕,需要跳到第 3 行,将 + - 号状态还原,并继续发送波形数据(+ jmp 3)。等到读取完毕,右边的 ROM 的地址值为 0 时,再跳回到开头,重新对新的随机数做三态判定。

点击左下角的【模拟】,稍等片刻,便会弹出结算界面:

我们通过三态判定,及巧用 + - 号的方式,合并了初始案例中的相似代码,省去了一行休眠、一行地址判定、一行跳转的代码,用电量消耗骤然增加的代价,把总代码行数减少到了 9 行。当不同条件下需要执行的逻辑相似时,我们可以【将相似的逻辑合并】,以一定程度的电量为代价,换取【相似逻辑间共享代码,节约代码行数,甚至节约成本】的成果。【逻辑合并】是优化代码行数及优化成本时的常用套路,后续关卡中仍然会再度用到。

碎碎念

代码压缩到了 9 行?没用到 dat 寄存器?对不起,因为你的 6 个口都接上导线了,想降成本换成 MC4000,没门。即使左边 ROM 的地址口没用上,那也是接了 5 个口。连接的口数减少不到 4 个,就别想换成 4000。

【深圳 IO 攻略】第 16 关:幽灵娃娃的评论 (共 条)

分享到微博请遵守国家法律