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

【深圳 IO 攻略】第 24 关:交通信号

2022-06-08 10:41 作者:ココアお姉ちゃん  | 我要投稿

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

关卡展示

本关要求你控制一个六叉路口的交通信号灯。在没有紧急车辆的时候,按照【灯 0】亮【阶段 0】秒→【灯 1】亮【阶段 1】秒→【灯 2】亮【阶段 2】秒→【灯 0】亮【阶段 0】秒→……的方式循环。而当出现紧急车辆时,关闭所有信号灯,直到紧急车辆离开时,恢复原先的交通模式(不保留之前的计时)。

本关如果没有紧急车辆,我们只需要将三个常数寄存器连到芯片的三个 x 口上,然后用一个 DX-300 连接右侧的三个信号灯输出口,并按照下面的方式设计程序即可:

给 DX-300 赋 100 点亮灯 0,睡 x0 秒;给 DX-300 赋 10 点亮灯 1,睡 x1 秒;给 DX-300 赋 1 点亮灯 2,睡 x2 秒。如此循环即可。

但是现在有紧急车辆,你就不能在点灯过程中一直睡眠。如果在睡眠过程中来了紧急车辆,你总不能告诉我你还没睡醒,信号灯灭不掉呀?

我们现在的思路是:用 dat 寄存器记录当前亮的灯是哪一个(和 DX-300 语言一致,0 都不亮,100 亮灯 0,10 亮灯 1,1 亮灯 2),用 acc 寄存器记录当前灯还要亮多久。当遇到紧急车辆时,将 acc 和 dat 强行清零。当没有紧急车辆时,acc 若未减到 1,则 acc -1,不改变灯的状态;当 acc 减到 1 时,根据当前点灯状态,判断接下来的点灯状态和持续时长,更新 dat 和 acc 寄存器。

这些逻辑看似不复杂,你首先想到的应该是将所有逻辑写在一块 MC6000 里:

结果非常可惜,至少需要 15 行代码,一块 MC6000 写不下。我们来分析一下为什么要占用 15 行,为什么省不掉任何一行代码:

紧急车辆出现时需要关闭所有信号灯,并将计时器重置为 1,这部分的代码占用了 5 行(tcp p0 50, + mov x3 dat, + mov 2 acc, + sub 1, + jmp f)。因为 x3 口连接着 DX-300,进而连接着三个只写 p 口。所以读 x3 口会读到立即数 0,同时清除三个只写 p 口数据。我们在清除三个 p 口数据的同时,可以将读到的这个 0 数字立刻写入 dat 寄存器,一举两得。后两条逻辑是和下一条判断共用的,净占用为 3 行。

没有紧急车辆出现,且计时器尚未减到 1 时,令计时器减 1,这部分的代码占用了 3 行(- tgt acc 1, + sub 1, + jmp f)。因为下方的 +/- 号指令会修改信号灯的状态和计时器。而当计时器尚未减到 1 时,当前信号仍在正常倒计时,信号灯状态不应被改变。所以这里不得不使用 jmp 指令强制跳到最后休眠,以避免错误地执行下方用于修改信号灯状态的 + 号指令。紧急车辆的部分也是一样要强制跳到最后,不能让任意一个信号灯点亮。至此,代码已占用掉了 6 行。

没有紧急车辆出现,且计时器减到 1 时,我们需要用多达 8 行代码来判断下一个信号灯状态,并向 DX-300 写入该值以便修改信号灯的状态。首先检查当前的信号灯状态是否是灯 1 亮的中间状态(tcp dat 10)。我们先假设是中间状态,将下一个状态置为灯 2 亮,持续时长为【阶段 2】秒(mov 1 dat, mov x2 acc)。如果当前状态是端点状态,再撤销之前的设置。如果 dat 是 100,也就是当前状态是灯 0 亮,那么下一个状态为灯 1 亮,持续 x1 秒(+ mov 10 dat, + mov x1 acc)。如果 dat 是 0 或 1,也就是当前状态是灯 2 亮,或紧急车辆刚离开,那么下一个状态为灯 0 亮,持续 x0 秒(- mov 100 dat, - mov x0 acc)。设置完新的信号灯状态后,将该状态值发给 DX-300(mov dat x3)。至此,14 行代码空间已经占用完毕。然而我们还需要最后一行代码:slp 1,休眠一秒,进入下一个时钟周期!很遗憾,即使只多一行代码,我们也无法将所有逻辑写在同一块芯片里。

无奈,只能分工合作。三态判定的部分要占用 8 行,太难受了。

改进思路:将【三态判定】任务分给另一块芯片做

我们一旦进入了三态判定,就需要用多达 8 行代码来更新 acc 和 dat 两个寄存器的值。我们现在可以考虑将这么多行代码逻辑写到另一块芯片里,然后用芯片间通讯的办法,由这另一块芯片告知本芯片,下一个信号灯状态是什么,以及持续时长是多少。电路图和代码如下:

这里我留了个彩蛋,将导线绕了几个圈,布置成了【六叉路口信号灯】的样子。不用担心电流会往回流,因为电流和水流类似,只会从高电势的方向往低电势的方向流动,不会在原地转圈,更不会往高电势的起点回流

几乎就是上一版方案的代码“分裂”到两块芯片里的结果。

我们先看上方的芯片,和上一版方案相比,改动如下:

首先,去掉了一个 jmp 指令。因为三态判定移动到了下方芯片里,【误执行改变信号灯状态的 + 号指令】这样的前提已经不存在了。所以我们只需要将不满足 - tgt acc 1 判断时要执行的逻辑全部加上 - 号前缀即可,完全不需要使用 jmp 指令。

然后,当需要改变信号灯状态时,我们将当前状态通过 x2 口发给下面的芯片(- mov dat x2),由下方芯片执行三态判定,计算下一个信号灯状态值和持续时长,并传给上方的芯片。上方的芯片只要依次接收完这两个值后(- mov x2 dat, - mov x2 acc),将新的状态值发送给 DX-300(- mov dat x3)就算完成任务。

然后我们看下方的芯片。第 2~8 行代码和上一版方案的第 7~13 行代码相比,除了三态判定由 tcp dat 10 改成了 tcp x3 10 外,其余的地方完全一致。因为在这版方案里,“当前状态”不是存在下方芯片的 dat 里的,而是存在上方芯片的 dat 里的。上方芯片通过 x3 口将它的 dat 发过来,我们的三态判定需要和上方的 dat,也就是和 x3 口作比较,而不是和自己的 dat 做比较

再然后,加上了第 1 行的等待接收指令(slx x3),以及第 9~10 行的发送指令,依次将本芯片的 dat, acc 寄存器的值发给上方芯片,覆盖上方芯片原始的 dat 和 acc 寄存器(mov dat x3, mov acc x3)。至此,本设计方案相比于上一版已废弃的设计方案的改动就说明完毕了。

点击左下角的【模拟】,运行程序:

紧急车辆出现时,信号灯形状的导线会被点亮,非常漂亮。稍等片刻,便会弹出结算界面:

优化成本

我们的第一个成品方案里,用了两块 MC6000,且都只写了 10 行代码。资源没有得到充分利用。我们如果能将两块芯片的分工做一个微调,让 A 多干点事,B 少干点事,那么 A 可以用足一块大芯片的 14 行代码空间,B 也可以将代码压缩到 9 行以内,进而将芯片由 MC6000 替换成 MC4000X。这样我们就可以物尽其用,节省成本。

节省成本的电路图和代码如下:

同样是 20 行代码,现在我们微调了一下两者的分工。以前是各 10 行代码,现在变成了上方 14 行代码,下方 6 行代码。这就变成了:上方的芯片物尽其用,下方的芯片成功替换成了成本更低的 MC4000X,整体的设计成本就这么降低了。

我们的三态判定是要完成两个任务的:①得到下一个信号灯的状态值;②得到下一个状态值的持续时长。第一个成品方案里,我们将三态判定的两项任务都交给下方芯片来完成,所以下方芯片也需要 10 行代码和 acc、dat 两个寄存器。

这个方案里,我们微调了一下分工。当需要改变信号灯状态时,接下来的状态值由上方芯片自己算,下方芯片只负责计算持续时长。下方芯片的代码改动很简单,就是在上一版方案的基础上,把含有 dat 的指令全部删除了而已。改变了分工后,下方芯片只需要 6 行代码,寄存器也只需要 acc 一个了。因此成功替换成 MC4000X。上方芯片的代码改动也很简单,就是在前一个成品方案的基础上,改为自己推断下一个信号灯的状态值。具体我不再做逻辑推演,请读者自行推演。

本方案的成本降到了 9 块钱,代码行数不变,但是电量稍有增加。原因在于:原先只要一次三态判定就能完成两项任务,现在这两项任务被分配到了两块芯片中,所以变成了两块芯片都要做一次三态判定。资源损耗就来源于此。

使用 RAM 打表,极致优化电量和代码行数

相比于【三态判定法】,我们可以改用【RAM 打表法】,将三路信号灯的状态码和时长都写入 RAM 中。实际执行时,遇到需要切换信号灯的场合,直接读两格 RAM 就切换到了下一个信号灯状态,免去了【对当前状态做三态判定】的过程。如此,便可大幅减少电量和代码行数。而且,由于 RAM 一共有 14 格空间,所以改用 RAM 打表后,何止 6 叉路口,14 叉路口都能给你控制得服服帖帖的!电路图和代码如下:

由于阶段 0~2 的值(三个灯的持续时长)只在同一个样例里保持不变,而不同的样例间,这三个阶段时长是不同的。所以本题我们不能使用 ROM 将三个信号灯的持续时长预先写好,只能【把 RAM 当 ROM 用】,在上电初始化的过程中把三个状态值和三个持续时长填入 RAM 中,后续只读不写。

我们左下角的芯片做的就是这样的事。上电的时候,我们将灯 0 的状态码 100,阶段 0 的时长 x1,灯 1 的状态码 10,阶段 1 的时长 x2,灯 2 的状态码 1,阶段 2 的时长 x3,这六个数写入 RAM。而且只需要写一次,后续就没有任何其他的事情要做了。所以这 6 条指令都加上了 @ 前缀,执行完毕后就永久睡眠了,直到进入下一个样例,重新上电时才会再次执行这六条初始化指令。

因为使用了 RAM,上方的芯片仅用 9 行代码就能完成所有的信号灯控制逻辑。RAM 的右指针永远指向当前阶段计时完毕后,下一个信号灯的状态值。

首先检查是否出现了紧急车辆(tcp p1 50)。当出现紧急车辆时,我们需要将信号灯和计时器都清零(+ mov x3 acc,读连接着若干只写 p 口的 DX-300 时,会读到 0,同时清除这些 p 口的数据,一举两得),然后将 RAM 的右指针也清零,将“下一个信号灯状态”还原成初始状态(+ mov 0 x1)。做完这些后,休眠一秒,进入下一个时钟周期(+ sub 1, slp 1)。这里的 + sub 1 原本是当第五行的 - tgt acc 1 指令成立后执行的,在这里执行只是“省去 jmp 指令所带来的副作用”,会将 acc 的值由 0 减为 -1。当然,由于对 acc 的判断是 tgt acc 1,也就是实际 acc 的值不论是 1、0 还是 -1,最终要执行的操作都是一样的,这里 acc 变为 -1 不会给整个程序带来影响最终输出结果的 bug,甚至可以因此省下关键的一行 jmp 指令。

回到开头,当没有出现紧急车辆时,需要判定 RAM 的地址指针是否指到了 6(- teq x1 6)。当地址指针指到了 6 时,由于我们在初始化时只给 RAM 的前 6 格写上了数据,因此当地址指针指向了有效数据范围之外的地址时,我们需要强制让地址指针归零,确保信号灯正常循环(+ mov 0 x1)。仅当上一秒钟读取了【灯 2 的状态值及阶段 2 的持续时长】时,RAM 的地址指针才会到达 6。所以 RAM 的地址是 6 时,当前一定处在阶段 2 的第 1 秒钟,所以本秒不需要判定计时器是否减到了 1,直接令计时器正常 -1 即可(+ sub 1)。如此,便又省掉了一行 jmp 指令。

没有紧急车辆,RAM 指针也正常时,我们判断计时器是否减到了 1(- tgt acc 1)。尚未减到 1 时,令计时器 -1(+ sub 1)。减到 1 后,我们只需要从 RAM 中连读两格数据,获得下一个信号灯状态及持续时长即可(- mov x0 x3, mov x0 acc)。做完这些操作后,休眠一秒,进入下一个时钟周期(slp 1)。

然后我们发现,上方的 MC6000 芯片没用到 dat 寄存器,且只写了 9 行代码。只是接口方面用了三个 x 口,一个 p 口。那么我们完全可以将这个 p 口用 DX-300 转接一下,变成四个 x 口,用 MC4000X + DX-300 组合代替 MC6000,省下一块钱的成本。最终电路图和代码如下:

上方芯片原先是用 x3 口接输出用的 DX-300 的,现在换到了 x2 口,所以原先代码里的 x3 全部换成 x2。而原先的 p1 信号现在经过 DX-300 的 p2 口转接,输入到了 x3 口里,所以我们将第一行的 p1 改成 x3。其余的代码都不用改变。

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

元器件上,上一版方案是 MC4000X + MC6000 + DX300(¥9),这一版方案是 2MC4000X + 2DX-300 + RAM(¥10),贵了一块钱。但是另两项指标,电量骤降到 250,代码行数骤减到 15 行,都是质的飞跃。用一块钱换来了这么大的性能提升,值!

【深圳 IO 攻略】第 24 关:交通信号的评论 (共 条)

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