本文首发于 B 站《深圳 IO》文集( https://www.bilibili.com/read/readlist/rl569860 )。原创不易,转载请注明出处。
关卡展示
这一关需要让你在按钮未按下时(对应的输入信号为 0)保持待命,而在按钮按下时(对应的输入信号为 100)生成 100×1s,0×1s 的循环脉冲信号。
这一关我们需要了解【测试指令】这个新概念。
测试指令一共有 4 条:
-
teq I1/R1/P1 I2/R2/P2,测试(test)第一个操作数是否等于(equals)第二个操作数。若两数相等,激活所有带 + 前缀的指令,关闭所有带 - 前缀的指令。若两数不等,激活所有带 - 前缀的指令,关闭所有带 + 前缀的指令。
-
tlt I1/R1/P1 I2/R2/P2,测试第一个操作数是否小于(lower than)第二个操作数。若第一个数小于第二个数,激活所有带 + 前缀的指令,关闭所有带 - 前缀的指令。若第一个数大于或等于第二个数,激活所有带 - 前缀的指令,关闭所有带 + 前缀的指令。
-
tgt I1/R1/P1 I2/R2/P2,测试第一个操作数是否大于(greater than)第二个操作数。若第一个数大于第二个数,激活所有带 + 前缀的指令,关闭所有带 - 前缀的指令。若第一个数小于或等于第二个数,激活所有带 - 前缀的指令,关闭所有带 + 前缀的指令。
-
tcp I1/R1/P1 I2/R2/P2,比较(compare)两个操作数的大小。该指令和 tgt 指令唯一的区别是:两个操作数相等时,tgt 指令会激活带 - 前缀的指令,而 tcp 指令会把带 + - 前缀的指令都关闭。其余情况下,tcp 指令的表现和 tgt 指令一致。
-
带 + - 前缀的指令会一直保持自己的激活或关闭状态,直到遇到下一条测试指令。测试指令前也是可以带 + - 前缀的,这意味着测试指令不是一定会被执行。这属于高阶技巧,以后我们再详细讲解。
举例说明:
首先,我们测试 p0 端口的值是否为 100。如果是 100,那么后续 + 前缀的指令(+ mov 100 p1)将会被执行,而 - 前缀的指令(- mov 0 p1)将不会被执行,那么 p1 口收到的值很明显是 100。反之,p0 端口的值不是 100 时,+ 前缀的指令不执行,- 前缀的指令正常执行,p1 口收到的值就会是 0。
回到这一关,我们需要在按钮按下时(按钮端口的值为 100)时生成脉冲方波(使用 gen 指令生成);而在按钮抬起时(按钮端口的值为 0)时安心休眠待命(使用 slp 指令)。经过以上分析后,这一关的答案已经呼之欲出:
按钮端口只有 0 和 100 两种值,我们首先对和按钮端口相连接的 p0 口执行测试指令 tcp p0 50。当 p0 小于 50 时,按钮处于抬起状态,激活 - 前缀的指令,我们需要执行休眠指令原地待命(- slp 1);当 p0 大于 50 时,按钮处于按下状态,激活 + 前缀的指令,我们需要不断执行 gen 指令生成 100 和 0 各占一秒的方波(+ gen p1 1 1),直到某刻 tcp 测试指令变为 - 生效为止。
点击左下角的【模拟】,稍等片刻,便会弹出结算界面:
优化代码行数
这里要提到一个隐藏特性:slp 里的休眠时间,和 gen 里的两个休眠时间是可以指定为 0 的。
等价于
等价于
我们在同一个时钟周期里可以反复给同一个 p 口赋不同的值,当前的时钟周期过去了之后,确认面板上会显示出对应 p 口【最后一次】赋的值。所以,我们在同一时钟周期里给 p0 依次赋值 100 和 0 后,p0 口这一个时钟周期内的实际值只会是 0。
现在,我们要再了解一个新的元件:DX-300。这个原件最多可以同时对 3 个简单 I/O 口进行读取或写入操作,具体的做法和二进制的【位运算】有点相似。
我们先说 DX-300 怎么同时读三个口的数据。我们将 DX-300 的 p0、p1、p2 分别和三个简单 I/O 口相连,然后另一侧的三个 X 口会实时传送一个三位数表示三个简单 I/O 口的状态:p0 为 0/100 时,个位数为 0/1;p1 口为 0/100 时,十位数为 0/1;p2 口为 0/100 时,百位数为 0/1。那么 DX-300 的 X 口所传出的三位数就有以下 8 种可能:000、001、010、011、100、101、110、111,依次对应三个简单 I/O 的开/关状态的 8 种排列组合。
同时写三个简单 I/O 的原理也很类似,我们往 DX-300 的任意一个 X 口写入以上 8 种三位数中的一种,就达到了用一条指令同时控制三个简单 I/O 的目的。比如你想让 DX-300 的 p0 和 p2 口连接着的端口变为 100,p1 口连接着的端口变为 0,那么你就给 DX-300 传 101 就可以了。
那么这道题,我们怎么优化到 1 条指令呢?我们的第一种方案里,芯片里的代码是这样的:
然后,我们可以推断出来,当执行到【- slp 1】时,p1 口的值一定是 0。由于 gen 指令一定以将对应 p 口赋 0 终结,所以以上 slp 指令也可以替换成 gen 指令,我们的代码可以改写成下面的样子:
这两条 gen 指令只有一个操作数不一样,那么它们能不能合并成一条呢?答案是可以的。我们不难发现:当 p0 是 0 时,执行的是 gen p1 0 1;当 p0 是 100 时,执行的是 gen p1 1 1。如果我们有一种办法能把 p0 的 100 转换成 1,那么只要一条 gen p1 (?) 1 指令就够了,问号处是经过转换后的数字。
这时候,我们的 DX-300 闪亮登场。我们将【按钮】端口接到 DX-300 的 p0 口上,其余两个口空着不接(视为对应端口的值恒为 0)。这样,当【按钮】端口的值为 0 时,经过 DX-300 加工后为 000;反之,当【按钮】端口的值为 100 时,这个值经过 DX-300 加工后会变成 001。这个加工后的值正好是我们需要在 gen 指令中用到的值!
于是,只用一行代码的设计方案顺利完成!