【深圳 IO 攻略】第 11 關(guān):變色電子煙筆

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

本關(guān)的?C2S-RF901 元件會(huì)不定期地發(fā)送一些長(zhǎng)度為 4 的數(shù)據(jù)包,數(shù)據(jù)包里的前三個(gè)數(shù)表示一個(gè)顏色的 RGB 值(不過(guò)范圍不是 0~255 而是 0~100),第四個(gè)數(shù)表示當(dāng)前顏色的持續(xù)時(shí)長(zhǎng)。如果在當(dāng)前顏色的時(shí)間范圍內(nèi)收到了新的數(shù)據(jù)包,那么立刻停止計(jì)時(shí),切換到新的顏色上。
我們很輕易地就能設(shè)計(jì)出這樣的一個(gè)算法:
從?C2S-RF901 元件讀入當(dāng)前時(shí)鐘周期內(nèi)的首數(shù)字。
首數(shù)字不是 -999 時(shí),說(shuō)明要將變色筆置為新的顏色。將當(dāng)前數(shù)據(jù)包里的前三個(gè)數(shù)分別傳給右側(cè)原件的 R、G、B 通道,然后將第四個(gè)數(shù)表示的持續(xù)時(shí)長(zhǎng)傳入芯片的 acc 寄存器。
判斷剩余時(shí)間是否減到了 0。若未減到 0,則令 acc -1(剩余時(shí)間 -1)。若減到了 0,則將變色筆的顏色清除。
做完以上操作后,休眠一秒,進(jìn)入下一個(gè)時(shí)鐘周期。
由于變色筆由三個(gè) p 口組成,且不是只有 0/100 兩種數(shù)字,所以本例里我們無(wú)法使用 DX-300,我們只能這樣做:其中一塊芯片傳輸 R、B 兩個(gè)顏色通道的值,對(duì)于剩下的 G 通道,我們將值通過(guò) x 口傳給另一塊芯片,委托它用自己的 p 口把收到的數(shù)據(jù)傳給 G 通道。
代碼如下:

這里,我們接觸到了一條新的指令:等待喚醒指令 slx。它的用法如下:
slx P,作用是令芯片在收到端口 P 的信號(hào)前一直保持睡眠。我在第 9 關(guān)的攻略里說(shuō)過(guò),芯片間使用 x 口傳輸數(shù)據(jù)時(shí),必須在同一個(gè)時(shí)鐘周期內(nèi),一方發(fā)送數(shù)據(jù)的同時(shí),另一方接收等長(zhǎng)的數(shù)據(jù),通訊才能完成。一旦不滿足以上任意一個(gè)條件(包括只發(fā)不收、只收不發(fā)、發(fā)送和接收的長(zhǎng)度不一致),就會(huì)導(dǎo)致運(yùn)行時(shí)阻塞。那么問(wèn)題來(lái)了,接收方不知道自己什么時(shí)候會(huì)收到數(shù)據(jù),不知道自己該等待到哪個(gè)時(shí)鐘周期時(shí)接收怎么辦?我又不能主動(dòng)去讀,讀不到數(shù)據(jù)的話一樣會(huì)阻塞。
slx 指令就是為了解決這樣的問(wèn)題而存在的。它的作用是,我來(lái)隨時(shí)監(jiān)視外面有沒(méi)有數(shù)據(jù)進(jìn)來(lái),我叫你讀你再讀,我不叫你的時(shí)候你就給我老老實(shí)實(shí)等著。只要我叫你的時(shí)候你立刻讀就一定沒(méi)有問(wèn)題(除非喚醒了以后又 slp 睡過(guò)去了,或者讀的長(zhǎng)度和發(fā)的長(zhǎng)度不一致,那樣的話仍然會(huì)阻塞)
那么,這道題里,左邊的芯片不會(huì)每秒鐘都告訴右邊芯片,現(xiàn)在的 G 通道值是多少。只有當(dāng)顏色改變的時(shí)候,才會(huì)通知右邊的芯片改寫(xiě)掉 G 通道。因此,右邊的芯片的確不知道自己該什么時(shí)候去讀 x0 口的數(shù)據(jù)。所以右邊的這塊芯片里,我們需要在代碼的開(kāi)頭加入一條 slx x0 指令,相當(dāng)于為 x0 端口安排一個(gè)監(jiān)控哨兵,告訴哨兵:“我現(xiàn)在睡了,啥時(shí)候 x0 口傳過(guò)來(lái)數(shù)據(jù)了就叫醒我,這時(shí)候我讀這個(gè)值肯定不會(huì)阻塞。然后,我把 G 通道改寫(xiě)成這個(gè)值以后就繼續(xù)睡了,等有新數(shù)據(jù)了再重新叫醒我?!?/p>
右邊的芯片雖然只有兩條指令,但這兩條指令里卻有這么多的學(xué)問(wèn)。
現(xiàn)在我們回過(guò)頭來(lái)看左邊的芯片。首先,我們之前提到過(guò),x 口的數(shù)據(jù)只能讀一次,不像 p 口那樣可以在同一秒內(nèi)反復(fù)讀。那么,對(duì)于可能需要多次讀取的數(shù)據(jù),我們就必須先把讀入的數(shù)據(jù)放到寄存器里暫存一下。像這道題里,首數(shù)字是 -999 則無(wú)視(只讀一次),首數(shù)字大于 -999 則將首數(shù)字的值發(fā)給 R 通道(讀了兩次),首數(shù)字是存在讀兩次的可能性的,所以必須將首數(shù)字暫存到?dat 里,然后在 dat 上做文章,僅當(dāng) dat 大于 -999 時(shí)才將 dat 寫(xiě)到 R 通道里。而對(duì)于非首數(shù)字,因?yàn)橹蛔x一次就夠了,所以就沒(méi)必要暫存到寄存器里了,直接將 x0 的值傳走就 OK。
然后我們來(lái)逐行分析左邊芯片里的代碼:
點(diǎn)擊左下角的【模擬】,稍等片刻,便會(huì)彈出結(jié)算界面:

優(yōu)化電量和代碼行數(shù)
我們將“剩余時(shí)間不為 0 時(shí)令剩余時(shí)間 -1”的邏輯改為“首數(shù)字為 -999 時(shí)剩余時(shí)間 -1”,這樣,當(dāng)某一刻顏色清零時(shí),倒計(jì)時(shí)仍然會(huì)繼續(xù)進(jìn)行下去變成負(fù)數(shù),不會(huì)停在 0。之前的方案里,倒計(jì)時(shí)到達(dá) 0 時(shí)就不會(huì)繼續(xù)計(jì)時(shí)了,這就導(dǎo)致長(zhǎng)時(shí)間不來(lái)新的顏色信號(hào)時(shí),teq acc 0 這條判斷始終成立,就會(huì)導(dǎo)致“反復(fù)清零”的負(fù)面效果。經(jīng)過(guò)以上改進(jìn)后,倒計(jì)時(shí)只會(huì)在來(lái)顏色信號(hào)時(shí)重置為某個(gè)數(shù),而永遠(yuǎn)不會(huì)停止,程序也只會(huì)在倒計(jì)時(shí)到達(dá) 0 的那一刻執(zhí)行一次清零效果,而當(dāng)?shù)褂?jì)時(shí)到達(dá)負(fù)數(shù)時(shí),不做任何操作。這樣就節(jié)省了電量。
現(xiàn)在,我們將第 2 行的 tcp dat -999 改為 tcp dat -1,然后將第 8?行的 - sub 1 挪到第 2 行代碼后面。代碼變成了下面的樣子:

而對(duì)于代碼行數(shù)的優(yōu)化,則需要用到我在第 10 關(guān)里提到過(guò)的一個(gè)技巧:【讀一個(gè)只寫(xiě) p 口時(shí)會(huì)讀到恒 0 數(shù)據(jù),同時(shí)之前寫(xiě)入該 p 口的數(shù)據(jù)也會(huì)被清零】。這一關(guān)里,因?yàn)樽筮?MC6000 的 p0 和 p1 口連接的是只寫(xiě)的 R、B 通道,所以
這兩行代碼可以合并為一行:
最終代碼如下:
