【深圳 IO 攻略】阿瓦隆城第 2 關:精確計時器

本文首發(fā)于 B 站《深圳 IO》文集(https://www.bilibili.com/read/readlist/rl569860)。原創(chuàng)不易,轉載請注明出處。
關卡展示

這一關看上去非常像【深圳龍騰有限公司】系列里的關卡。但實際做了以后才發(fā)現(xiàn),這一關并不簡單,要考慮的因素非常多,難度大致和龍騰第 29 關《變色鞋》相當。
本關要求實現(xiàn)一個精確計時器,這個計時器有【開始停止】和【關閉重置】兩個按鈕,均為多功能按鈕。本關的要求如下:
初始狀態(tài)下,計時器處于關機狀態(tài)。
關機狀態(tài)下按下【開始停止】按鈕時,開機,并將時間置為 0。
開機狀態(tài)下按下【開始停止】按鈕,可來回切換計時/停止計時的狀態(tài)。當處于計時狀態(tài)時,每隔兩個時鐘周期,計時器的值 +1。
在開機且停止計時的狀態(tài)下按下【關閉重置】按鈕時,若計時器時間不為零,則先將計時器時間歸零;若計時器時間已歸零,則向計時器發(fā)送 -999 以關機。其余任何時候,按下【關閉重置】按鈕都維持原狀。
根據(jù)以上要求,我們可以將計時器細分為四種狀態(tài):①關機;②開機且正在計時;③開機且停在 0 時間;④開機且停在非 0 時間。那么,我們只需要用一個芯片來計算計時器的實時狀態(tài),另一個芯片根據(jù)實時狀態(tài)來計算顯示值,就可以完成任務了。電路圖和代碼如下:


由于【開始停止】和【關閉重置】兩個輸入量和下方的芯片直接相連,因此下方的芯片是真正意義上的“第一塊芯片”。我們從這塊芯片開始看起。
我們剛才說過,計時器一共可以細分為四種狀態(tài)。我們將這四種狀態(tài)分別編號 0~3。并存儲到這塊芯片的 acc 寄存器里。具體對應關系如下:
0:關機(且是初始狀態(tài))
1:開機且停在 0 值
2:開機且停在非 0 值
3:開機且處于計時狀態(tài)
第 1~5 行代碼的作用是檢測是否有【關閉重置】信號,以及檢測到信號時的操作。首先我們檢查【關閉重置】信號是否出現(xiàn)(tcp p0 0),未出現(xiàn)時關閉一切 + - 號指令,跳到第 6 行。根據(jù)題目要求,處于計時狀態(tài)時,不響應關閉重置信號。因此我們現(xiàn)在檢測狀態(tài)碼是否小于 3(+ tcp acc 3)。狀態(tài)碼為 3 時,不執(zhí)行任何操作,關閉 + - 號指令,跳到第 6 行。而當狀態(tài)碼小于 3 時,又細分成數(shù)值為 0 和非 0 兩種情況。數(shù)值為非 0,狀態(tài)碼為 2 時(- teq acc 2),將數(shù)值清零但不關機,也就是將狀態(tài)碼變?yōu)?1(+ mov 1 acc)。數(shù)值為 0,或者處于關機狀態(tài)時,狀態(tài)碼為 0、1 中的一個,此時需要將計時器關機,也就是將狀態(tài)碼變?yōu)?0(- mov 0 acc)?!娟P閉重置】按鈕的邏輯就這么多。
第 6~12 行代碼的作用是檢測是否有【開始停止】信號,以及檢測到信號時的操作。首先我們檢查【開始停止】信號是否出現(xiàn)(tcp p1 0),未出現(xiàn)時關閉一切 + - 號指令,跳到第 13 行。【開始停止】按鈕的邏輯較為復雜,因為不論當前處于什么狀態(tài),只要按下了【開始停止】按鈕,狀態(tài)都會改變。首先我們判斷當前是否處于關機狀態(tài)(+ teq acc?0),如果是關機狀態(tài),則開機,且令數(shù)字停在 0 值,也就是將狀態(tài)碼設為 1(+ mov 1 acc)。執(zhí)行完后需要跳到第 13 行,避開第 12 行的 + 號指令(+ jmp d)。如果當前不處于關機狀態(tài),那么又要細分是否正在計時(- teq acc?3)。停在 0 還是非 0 不重要,那個是給【關閉重置】做邏輯判斷的。如果不在計時,也就是狀態(tài)碼不為 3,則啟動計時,將狀態(tài)碼改為 3(- mov 3 acc)。如果正在計時,也就是狀態(tài)碼為 3,則停止計時。停止計時后,時間肯定是處于非 0 狀態(tài),所以此時將狀態(tài)碼改為 2(+?mov 2 acc)。第 12 行的 + mov 2 acc?僅當?shù)?10?行的測試指令 - teq?acc?3 成立時才能執(zhí)行,所以第 8 行的 + mov 1 acc?執(zhí)行完畢后需要強制跳轉下去,避免誤執(zhí)行這條 + 號指令。
操作完成后,將當前時鐘周期的狀態(tài)碼發(fā)給左上角的“第二塊芯片”,由它計算現(xiàn)在的計時器應該顯示什么數(shù)字(mov acc x0)。最后,休眠一秒,進入下一個時鐘周期(slp 1)。

現(xiàn)在開始看左上角的芯片。左上角芯片的 acc 寄存器用于記錄計時器的實時數(shù)字(擴大十倍的數(shù)字),dat 寄存器用于接收下方芯片發(fā)來的狀態(tài)碼。我們首先將下方芯片發(fā)來的實時狀態(tài)碼存入 dat 寄存器(mov x1 dat),然后圍繞著這個狀態(tài)碼做文章。當狀態(tài)碼是 0 時(teq dat 0),說明處于關機狀態(tài),將 acc 置為 -999 表示關機(+ mov -999 acc),然后跳到第 8 行,將 -999 發(fā)送給右上角的芯片(mov acc x3)。我們發(fā)現(xiàn) 7、8 兩行把 acc 的值傳了兩次,這是因為右上角的芯片需要將擴大了 10 倍的時間做除以 10 的運算,在僅有兩個寄存器的情況下,必須要兩次提供原數(shù)才能正確地執(zhí)行除以 10 的運算(這個我們后面再說)。而如果傳的值是 -999 的話,我們不需要去做除以 10 的運算,這個值只需要傳一遍就行了。所以當 acc 是 -999 時,跳過第一個 mov acc x3,直接執(zhí)行第二個 mov acc x3。
繼續(xù)判斷,當狀態(tài)碼是 1 時(teq acc 1),我們需要將 acc 強制歸零(+ mov 0 acc),至此,我們將 acc 的值傳兩遍給右邊的芯片(mov acc x3, mov acc x3),并休眠一秒(slp 1)。
細心的讀者可能會問了:狀態(tài)碼是 3 時你怎么不計時了?其實,我們不是不計時,而是【延遲計時】。狀態(tài)碼是 3 時,當前的時鐘周期不能立刻做累加運算,而是要等到下一個時鐘周期時才能開始累加。想象一下生活中的秒表是什么樣子的:你剛按下秒表的時候,秒數(shù)還是 0,到了下一秒的時候,秒數(shù)才變?yōu)?1。這里我們也是一樣的邏輯,休眠完畢后,檢查上一秒的狀態(tài)值是否是 3(teq dat 3)。若是 3 的話,計時器 +0.5(+ add 5)。

最后我們來看右上角的芯片。由于我們的計時器每 2 秒才能計一個數(shù),所以實際上相當于每秒鐘只能計 0.5 個數(shù)。而我們的游戲又不能使用小數(shù),所以很無奈,上一個芯片只能把時鐘擴大 10 倍,每秒鐘計 5 個數(shù),再交由這塊芯片重新做一次除以 10 并向下取整的運算,得到實際顯示的時間值。
我們之前說過,百位為 0 時,我們可以直接用 dgt 1 取十位來得到除以 10 的結果。而這道題的計時器是完全可以計到 10 以上的,也就是百位不一定為 0。這時候除以 10 就比較麻煩了。
首先我們得到從左邊芯片傳來的 ×10 后的數(shù)值,并放入 acc(mov x1 acc)。當然,因為 -999 只會傳一次,所以我們首先檢查原數(shù)是否是 -999(tcp acc -999)。如果是 -999,就別做什么除以 10 的操作了,直接跳到第 9 行關機就完事。如果不是 -999 的話,我們首先將這個數(shù)字的十位提取出來(+ dgt 1),并放到 dat 里備用(+ mov acc dat)。這時候,再從左邊的芯片獲得一份原數(shù)的副本(+ mov x1 acc),提取出百位(+ dgt 2),乘以 10,以便將這個放在最低位上的【百位數(shù)字】向前移動一位(+ mul 10),原先的最低位改成放之前存在 dat 里的【十位數(shù)字】(+ add dat)。如此,便得到了原數(shù)字除以 10 并向下取整的數(shù)。將這個數(shù)發(fā)送給顯示器后(mov acc x3),休眠一秒,進入下一個時鐘周期(slp 1)。
點擊左下角的【模擬】,稍等片刻,便會彈出結算界面:

優(yōu)化三項指標
我們的這個初版代碼,主要的功耗都浪費在了【除以 10 并向下取整】這個過程里。其實,我們可以改為使用一個 ROM,里面放上 0、1 相間的數(shù)字,表示當前時鐘周期里需要累加的值。這樣相當于“前一秒計 0 個數(shù),后一秒計 1 個數(shù)”,而不是之前的“每秒鐘計 0.5 個數(shù)”。這樣就成功避開了【除以 10 并向下取整】這樣繁瑣的運算。電路圖和代碼如下:


這個方案里,原先的三塊芯片變成了兩塊芯片 + 一塊 ROM,下方芯片也改為使用 x3 口來和上方芯片通訊。所以下方芯片的第 13 行代碼由原先的 mov acc x0 改成了 mov acc x3,而下方芯片其余代碼和上一版方案完全一致。
然后我們重點關注一下上方芯片。上方芯片此時的 acc 寄存器記錄的已經(jīng)是真實的顯示器值了,而不是擴大十倍的值。所以其 x3 口已經(jīng)直接和顯示器相連了。與此同時,它的 x0 和 x1 口連接著一個 ROM 的數(shù)據(jù)口和地址口。
上方芯片的代碼邏輯和上一版方案大同小異,簡單過一下:首先從下方芯片獲得實時狀態(tài)并存入 dat(mov x2 dat)。狀態(tài)值是 0 時關閉顯示器(teq dat 0, + mov -999 acc),狀態(tài)值是 1 時將計時值清零(teq dat 1, + mov 0 x1, + mov 0 acc)。這里注意,ROM 的地址口是和【計時狀態(tài)下已經(jīng)過的秒數(shù)】一致的,所以清零計時值不只是將顯示的數(shù)值清零,經(jīng)過的秒數(shù)也要清零。我們將當前秒數(shù)發(fā)給顯示器并休眠一秒后(mov acc x3, slp 1)【延遲判定】上一秒是否處于計時狀態(tài)(teq dat 3)。若上一秒處于計時狀態(tài),讀一下 ROM 的數(shù)據(jù)口,令實際數(shù)字加上這個增量值(+ add x0,上一秒是偶數(shù)秒時 +0,上一秒是奇數(shù)秒時 +1)。
點擊左下角的【模擬】,稍等片刻,便會彈出結算界面:

成本 ¥15→¥12,電量 1.4K→719,代碼行數(shù) 35→24,三項指標都有質的飛躍。