【TIS-100 攻略】TIS-NET 第 6 关:信号预分频器

本文首发于 B 站《TIS-100》文集(https://www.bilibili.com/read/readlist/rl626023)。原创不易,转载请注明出处。
TIS-NET 第 6 关《信号预分频器》(Signal Prescaler)关卡展示

本关要求将 IN 中的值依次除以 2、4、8,并将结果依次送往 OUT.2、OUT.4 和 OUT.8 端口。
注意到 IN 里的数都是 8 的倍数,所以我们可以先计算 8 分频的值,将得到的结果乘以 2 就得到了 4 分频的值,再乘以 2 就得到了 2 分频的值。
另外,我在第 19 关《除法器》的攻略(传送门:【TIS-100 攻略】第 19 关:除法器)里提过:除法可以使用【线性查找】和【二分查找】两种方式来做。本关也一样,我会用线性查找和二分查找这两种方法各做一遍。
线性查找法(最省节点和代码行数的做法)
核心思想是将 IN 里的值不断减去 8,每减去一个 8 就令商加上 1,直到原值减到 0 为止,此时的商就是 8 分频的值。之后将这个值乘以 2 得到 4 分频的值,再乘以 2 得到 2 分频的值。代码如下:

左侧的三个节点纯传话(mov up down, mov up down, mov up left)。然后看 OUT.2 上方的节点:
将 in 中的值放到 acc 里(mov left acc),
每次循环令 bak 加上 1(swp)
(add 1)
(swp)
以及令 acc 减去 8(sub 8)。
acc 尚未减到 0 时,跳回第 2 行继续循环(jnz 2),
直到 acc 到达 0 为止,bak 中的值就是 8 分频的值,将它发给右边的节点(swp)
(mov acc right)
将 8 分频的值乘以 2 后(add acc)
得到 4 分频的值,同样发给右边的节点(mov acc right)。
将 4 分频的值乘以 2 后(add acc)
得到 2 分频的值,发给自己下方的 OUT.2 端口(mov acc down)。
OUT.4 上方的节点会从左边依次收到 8 分频和 4 分频的值,将前者传给右边(mov left right),后者发到自己下方的 OUT.4 端口(mov left down)。
OUT.8 上方的节点会从左边收到 8 分频的值,将它发到自己下方的 OUT.8 端口(mov left down)。
点击左下角的【RUN】,稍等片刻,便会弹出结算界面:

二分查找法(运行速度最快的做法)
由于 IN 里的值必为 8 的倍数,且最多为 3 位数,所以 IN 里的最大有效值为 8×124=992,最大的 8 分频值为 124,位于 0~127 范围内。因此,使用二分查找,每次将商的取值范围缩小一半,最多经过 7 次计算即可得到答案。步骤如下:
从 IN 里读入一个数,设它为 a。检查 a 是否大于等于 512。若是,则令 a 减去 512,同时令商加上 64(64×8=512,所以此时的 a 至少能商 64);若否,则什么也不做。
检查 a 是否大于等于 256。若是,则令 a 减去 256,同时令商加上 32(32×8=256,所以此时的 a 至少能商 32);若否,则什么也不做。
检查 a 是否大于等于 128。若是,则令 a 减去 128,同时令商加上 16(16×8=128,所以此时的 a 至少能商 16);若否,则什么也不做。
检查 a 是否大于等于 64。若是,则令 a 减去 64,同时令商加上 8(8×8=64,所以此时的 a 至少能商 8);若否,则什么也不做。
检查 a 是否大于等于 32。若是,则令 a 减去 32,同时令商加上 4(4×8=32,所以此时的 a 至少能商 4);若否,则什么也不做。
检查 a 是否大于等于 16。若是,则令 a 减去 16,同时令商加上 2(2×8=16,所以此时的 a 至少能商 2);若否,则什么也不做。
检查 a 是 0 还是 8。若是 8,则令商加上 1,否则商不变。此时的商就是 8 分频的值。
本关的代码如下:

由于用到的节点数量过多,我在图上将用到的节点都编了号,并且用箭头标识出了数据流向。这次的数据是按蛇形流动的。
1~6 号节点用于负责 7 次二分查找。6 个节点要做 7 次运算,根据抽屉原理,至少有一个节点要做其中的 2 次运算。这里我选择了让 1 号节点做前两步运算:
从 IN 里读入一个数,视它为 a。检查 a 是否大于等于 512,即 a-512(mov -512 acc)
(add up)是否大于等于 0。
大于等于 0 时按顺序执行,小于 0 时跳到第 6 行执行(jlz 6)。
a-512 大于等于 0 时,令商加上 64,将增量 64 传给 2 号节点(mov 64 down)
(jmp 8)
否则将增量 0 传给 2 号节点(mov 0 down)
并将 a-512 还原成 a(add 512)。
检查 a 是否大于等于 256,即 a-256(sub 256)是否大于等于 0。
大于等于 0 时按顺序执行,小于 0 时跳到第 12 行执行(jlz c)。
a-256 大于等于 0 时,令商加上 32,将增量 32 传给 2 号节点(mov 32 down)
(jmp e)
否则将增量 0 传给 2 号节点(mov 0 down)
并将 a-256 还原成 a(add 256)。
操作完毕后,将处理后的 a 发给 2 号节点(mov acc down)。
2 号节点执行的是第 3 步运算:
1 号节点会将前两次商的增量 q、dq 发来,我们计算出两者之和(mov up acc)
(add up)
发给 3 号节点(mov acc right)
然后 1 号节点会发来最新的 a 值,我们检查 a 是否大于 128,即 a-128(mov up acc)
(sub 128)是否大于等于 0。
大于等于 0 时按顺序执行,小于 0 时跳到第 9 行执行(jlz 9)。
a-128 大于等于 0 时,令商加上 16,将增量 16 传给 3 号节点(mov 16 right)
(jmp b)
否则将增量 0 传给 3 号节点(mov 0 right),
并将 a-128 还原成 a(add 128)。
操作完毕后,将处理后的 a 发给 3 号节点(mov acc right)。
3 号节点执行的是第 4 步运算:
2 号节点会发来一个老的商值 q 和一个增量值 dq,我们计算出两者之和,作为新的商(mov left acc)
(add left)
发给 4 号节点(mov acc right)。
然后 2 号节点会发来最新的 a 值,我们检查 a 是否大于 64,即 a-64(mov left acc)
(sub 64)是否大于等于 0。
大于等于 0 时按顺序执行,小于 0 时跳到第 9 行执行(jlz 9)。
a-64 大于等于 0 时,令商加上 8,将增量 8 传给 4 号节点(mov 8 right)
(jmp b)
否则将增量 0 传给 4 号节点(mov 0 right),
并将 a-64 还原成 a(add 64)。
操作完毕后,将处理后的 a 发给 4 号节点(mov acc right)。
4 号节点执行的是第 5 步运算:
3 号节点会发来一个老的商值 q 和一个增量值 dq,我们计算出两者之和,作为新的商(mov left acc)
(add left)
发给 5 号节点(mov acc right)。
然后 3 号节点会发来最新的 a 值,我们检查 a 是否大于 32,即 a-32(mov left acc)
(sub 32)是否大于等于 0。
大于等于 0 时按顺序执行,小于 0 时跳到第 9 行执行(jlz 9)。
a-32 大于等于 0 时,令商加上 4,将增量 4 传给 5 号节点(mov 4 right)
(jmp b)
否则将增量 0 传给 5 号节点(mov 0 right),
并将 a-32 还原成 a(add 32)。
操作完毕后,将处理后的 a 发给 5 号节点(mov acc right)。
5 号节点执行的是第 6 步运算:
4 号节点会发来一个老的商值 q 和一个增量值 dq,我们计算出两者之和,作为新的商(mov left acc)
(add left)
发给 6 号节点(mov acc down)。
然后 4 号节点会发来最新的 a 值,我们检查 a 是否大于 16,即 a-16(mov left acc)
(sub 16)是否大于等于 0。
大于等于 0 时按顺序执行,小于 0 时跳到第 9 行执行(jlz 9)。
a-16 大于等于 0 时,令商加上 2,将增量 2 传给 6 号节点(mov 2 down)
(jmp b)
否则将增量 0 传给 6 号节点(mov 0 down),
并将 a-16 还原成 a(add 16)。
操作完毕后,将处理后的 a 发给 6 号节点(mov acc down)。
6 号节点执行的是第 7 步运算。注意这个节点是收尾节点,所以逻辑和前辈们不完全一样:
5 号节点会发来一个老的商值 q 和一个增量值 dq,我们计算出两者之和,作为新的商(mov up acc)
(add up)
放入 bak 中暂存(sav)。
然后 5 号节点会发来最新的 a 值(mov up acc),我们检查 a 是 0 还是 8。
a 是 0 时按顺序执行,a 是 8 时跳到第 8 行执行(jnz 8)。
a 是 0 时,商自身就是 8 分频的值,只需拿出 bak(swp)
准备输出即可(jmp a);
a 是 8 时,需要在原商的基础上加 1(swp)
才能得到正确的 8 分频值(add 1)。
我们将 8 分频值复制一份给 7 号节点后(mov acc left),
再输出到 OUT.8 口(mov acc down)。
7 号节点在得到 6 号节点发来的 8 分频值后,将它乘以 2 得到 4 分频的值(mov right acc, add acc),然后将这个 4 分频的值复制一份给 8 号节点后,再输出到 OUT.4 口(mov acc left, mov acc down)。
8 号节点在得到 7 号节点发来的 4 分频值后,将它乘以 2 得到 2 分频的值(mov right acc, add acc)并输出到 OUT.2 口(mov acc down)。
点击左下角的【RUN】,稍等片刻,便会弹出结算界面:

第 19 关除法器的攻略里我就提到了二分除法,但是效率仅仅提升了 3 倍。那是因为被除数只有两位数,即使用线性查找慢慢找也不会过于费时。但是本关的被除数几乎都是三位数,线性查找和二分查找的效率差异一下就被放大了:线性查找的话,商是多大,就需要进行多少次运算,本题里消耗了 13454 周期;而二分查找的话,只要商在 0~127 范围内,不管商是多少,都是雷打不动的 7 次运算,本题里只消耗了 660 周期,两者的效率相差了两个数量级!