本文首发于 B 站《深圳 IO》文集( https://www.bilibili.com/read/readlist/rl569860 )。原创不易,转载请注明出处。
关卡展示
本关要求在时间到达【开启时间】时开启报警器,在到达【关闭时间】时关闭报警器。同时,在报警器开启的状态下,当传感器的值 ≥20 时,启动报警输出(将【闹钟】端口置为 100),其余时刻关闭报警输出(将【闹钟】端口置为 0)。这里的“闹钟”应该属于翻译错误,英文原文是 alarm,这里根据上下文语境应当翻译为“报警”。
这道题很明显是一个【与】逻辑,仅当“报警器开启”及“传感器值 ≥20”两个条件同时成立时,【报警】端口才为 100。我们首先想到的是:用两块芯片分别计算并输出两个条件的逻辑值,并使用【与门】处理后将最终的值送入【报警】端口。
元件面板中,【LC70G08】这个元件为【与门】。仅当其输入端的两个值都为 100 时,输出端上方的值才为 100,其余情况输出端上方的值都为 0。输出端下方的值则为【与非门】,与输出端上方的值互反。
我们向开发板中拖入两块【MC4000】芯片和一块【LC70G08】(与门),并按下图所示放置好各元件,书写好代码:
如上图所示,【开启时间】和【关闭时间】分别接在上方 MC4000 的 x0 和 x1 口上,而实时的 RTC 时间接在了 p0 口上。所以上方的芯片里,我们需要随时判定 p0 的值是否等于 x0 或 x1。一旦等于两者之一,就需要改变 p1 口的值(用于打开/关闭报警器)。
下方的 MC4000 芯片里的代码,相信看到这一节的你已经很熟悉了,一个最基本的判断,当【传感器】的值 <20 时向 p1 输出 0,否则输出 100。
将以上两块芯片输出的逻辑值做与运算,最终结果输出给【报警】端口。
点击左下角的【模拟】,稍等片刻,便会弹出结算界面:
优化三项指标
上一个方案的结算界面里,我们可以发现三项指标都没有达到最佳。如果你仔细观察,可以发现我们的 MC4000 芯片没有得到充分利用,两块芯片都没有用到 acc 寄存器。那我们可不可以用 acc 寄存器代替一块单独的芯片来存储报警器的开/关值,然后再使用测试指令来实现与门呢?答案是可以的!
这时候,我们要介绍一块功能更强大的芯片:MC6000。它是 MC4000 的老大哥,各项功能特性均完爆小弟 MC4000,不过成本上贵 2 块钱,占用的体积稍微大一些:
MC6000 比 MC4000 多了两个用于传输的 x 口、额外的五行代码空间、一个额外的寄存器 dat。这里要说明一下,虽然 MC6000 有一个额外的寄存器 dat,但是所有的数学运算仍然只能在 acc 中进行。类似 add dat 1 这样的指令是不存在的。dat 寄存器除了不能进行数学运算外,其余方面和 acc 寄存器无异。
这道题共有两个简单输入(时间、传感器)、两个 x 输入(开启时间、关闭时间)和一个简单输出(报警),所以需要连接五个接口。考虑到【报警】这个输出只有 0 和 100 两种值,所以可以经由 DX-300 中转,由 p 口输出改为 x 口输出。这样我们一共需要连接 2 个 p 口和 3 个 x 口,一块 MC6000 芯片足矣。如果我们改用 MC6000 + DX-300,可以省掉一块钱的成本。后面我们会发现,采用此套方案后,不仅仅是成本,三项指标都会比上一个方案更优!
电路图及代码如下图所示:
我们用 acc 寄存器来表示报警器的开/关状态。当 p1(时钟)的值到达 x0(开启时间)或 x1(关闭时间)时,相应地更改 acc 寄存器的值。然后我们检测 p0(传感器)的值是否 <20(tlt p0 20)。若 <20,则无视报警器的开关状态,强制输出 0(+ mov 0 x2);否则输出的值跟报警器的开关状态一致(- mov acc x2)。由于 DX-300 的 p2 口和报警输出相连接,所以我们给 DX-300 传的三位数里,百位为 0 时报警输出关闭,百位为 1 时报警输出开启。和直接通过 p 口传 0/100 的效果完全一致。
此时我们再运行程序,观察结算界面:
可以发现三项指标相比前一个方案都有所提升!
极致优化电量
我们观察时序图,可以发现以下两条规律:
-
实时时钟在下一秒钟的值总是当前时钟值 +1。特殊地,如果当前的时钟值是 95,那么下一秒的时钟值会归零重新计时。或者可以这么说,下一秒钟的时钟值 = (当前时钟值 +1) mod 96。
-
时钟跨度是 0~96,而一个样例只有 60 秒周期,所以报警器最多只会开启、关闭一次。同时我们可以发现,题目中给的每个样例的 60 秒周期里,报警器都是先开启后关闭。
根据以上两条规律,我们可以在报警器开启前提前计算好需要等待的秒数,在此期间令芯片一直休眠,不去检测传感器的状态。休眠结束后再进入正常的检测环节。同样地,一旦到达关闭时间,就令芯片执行 slp 999,让芯片“永久睡眠”下去,和开启前一样,不再检测传感器的状态。如此做,即可省下大量电量。代码如下:
前 5 行代码用来计算当前时间距离【开启时间】有多远。我们用开启时间减去当前时间(@ mov x0 acc, @ sub p1),并检测差值是否为负数(@ tlt acc 0)。如果差值为负数,说明开启时间在下一个 0~95 的循环周期里(例如:当前时间 95,开启时间 0,那么需要等待 1 秒钟),那么就要把 acc 加上 96 得到正确的等待时间(+ add 96)。计算好了等待时间后,睡眠这么长时间(@ slp acc)后,报警器自动开启。
第 6~9 行的代码完成的任务是【已知报警器打开的状态下】,检测传感器的值是否 <20,以此决定是否需要报警。
第 10~12 行的代码用于实时监测当前时间是否已到达【关闭时间】。尚未到达时,由于激活的是 - 前缀的指令,所以会跳回第 6 行(第一条非 + 非 @ 前缀指令)执行。一旦到达,则向报警口输出 0(关闭报警),然后 slp 999 永久睡眠。
点击左下角的【模拟】,稍等片刻,进入结算界面:
可以看到此时电量消耗骤降到了 230,因为在报警器关闭的状态下,芯片便不再探测传感器的值,也不再改变报警输出的值,因此电量被大幅节省。
2022 年 9 月 6 日更新:更极致的省电方案——预计算睡眠时间 + 使用与门
上一版方案里,我们预先计算好了当前时间距离开启时间有多远,以便在到达开始时间前保持睡眠状态。可开始工作后,我们却使用了反复判断是否到达关闭时间的做法,效率上有所浪费。
此时我们再回看初版使用了与门的那个方案,那个方案里,两块芯片分工合作,一个控制总开关,一个控制传感器子开关,两个开关量用与门串联作为报警器的输出。这里,我们只需要把控制总开关的芯片由【每秒判断是否打开或关闭总开关】改为【预先计算好睡眠时间,定时打开或关闭总开关】,即可形成一个完美的省电方案。电路图和代码如下:
这次,为了布线上的方便,我们将控制总开关的芯片挪到了下方,同时因为代码行数到达了 10 行,芯片也升级为了 MC6000。
第 1~4 行计算的是 (start - now + 96) mod 96 的值,即当前时间距离开始时间有多远。计算完成后,执行第 5 行的 slp 指令,令芯片睡眠这么长时间。
第 6~9 行计算的是 (end - start + 96) mod 96 的值,即结束时间距离开始时间有多远。计算完成后,执行第 10 行的 gen 指令,令 p1 口输出这么长时间的 100 信号,然后当到达关闭时间后,清除 p1 口信号并永久睡眠。
点击左下角的【模拟】,稍等片刻,便会弹出结算界面:
电量由 230 进一步降低到了 192。