【深圳 IO 攻略】阿瓦隆城第 1 關(guān):冷庫(kù)機(jī)器人

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

歡迎進(jìn)入地獄模式。當(dāng)你開啟了阿瓦隆城的大門后,相信你對(duì)深圳 IO 這款“單片機(jī)模擬器”已經(jīng)有了較為深入的了解。
本關(guān)要求你控制一個(gè)電機(jī),隨時(shí)將新的食物放入冷庫(kù),或者將已經(jīng)在冷庫(kù)中的食物拿出。C2S-RF901 會(huì)不定期地發(fā)送一些長(zhǎng)度為 2 的數(shù)據(jù)包。當(dāng)你收到以 1 開頭的數(shù)據(jù)包時(shí),控制電機(jī)將對(duì)應(yīng)編號(hào)的食物放入冷庫(kù);當(dāng)你收到以 2 開頭的數(shù)據(jù)包時(shí),控制電機(jī)將冷庫(kù)中對(duì)應(yīng)編號(hào)的食物取出。
電機(jī)的控制規(guī)則如下:
①【伸出】信號(hào)為 0 表示電機(jī)的爪子處于收縮狀態(tài),100 表示電機(jī)的爪子處于伸出狀態(tài)。
②【抓握】信號(hào)為 0 表示電機(jī)的爪子處于放開狀態(tài),100 表示電機(jī)的爪子處于抓握狀態(tài)。
③【電機(jī)】信號(hào)用于控制電機(jī)的水平移動(dòng)。平時(shí)是 50 信號(hào),給電機(jī)發(fā)送 a 秒的 0 信號(hào)可令電機(jī)左移 a 格,給電機(jī)發(fā)送 b 秒的 100 信號(hào)可令電機(jī)右移 b 格。移動(dòng)完畢后需要將電機(jī)信號(hào)還原成 50。
④電機(jī)初始在第 0 格,新食物也總會(huì)在第 0 格出現(xiàn)。當(dāng)你收到 1 開頭的數(shù)據(jù)包時(shí),需要將新食物放入冷庫(kù)。首先令電機(jī)【伸出】1 秒,然后讓電機(jī)【抓握】住食物,并【向右移動(dòng)】,找到冷庫(kù)中的第一個(gè)空位后,令電機(jī)【伸出】1 秒,把爪子【放開】,將食物送入冷庫(kù)。最后,令電機(jī)【向左移動(dòng)】回到原點(diǎn)待命。
⑤當(dāng)你收到 2 開頭的數(shù)據(jù)包時(shí),令電機(jī)【向右移動(dòng)】,找到對(duì)應(yīng)的食物后,令電機(jī)【伸出】1 秒,然后讓電機(jī)【抓握】住食物,并【向左移動(dòng)】到原點(diǎn),將食物運(yùn)送到出口。到達(dá)原點(diǎn)后,令電機(jī)【伸出】1 秒,把爪子【放開】,釋放食物。
這個(gè)題目要實(shí)現(xiàn)的程序邏輯相當(dāng)復(fù)雜,至少需要 4 塊芯片分工合作。
第一塊芯片是總監(jiān),用于監(jiān)測(cè)數(shù)據(jù)包,然后給其余芯片發(fā)送調(diào)度指令;
第二塊芯片是一個(gè)數(shù)據(jù)庫(kù)管理員,它的工作是不停地修改冷庫(kù)中的食物存放狀態(tài)。當(dāng)它收到第一塊芯片發(fā)來(lái)的調(diào)度任務(wù)后,根據(jù)任務(wù)的種類執(zhí)行不同的操作:當(dāng)收到了【存】任務(wù)時(shí),在 RAM 中尋找第一個(gè)空位,并將對(duì)應(yīng)的空位置為新的食物編號(hào);當(dāng)收到了【取】任務(wù)時(shí),在 RAM 中尋找存著對(duì)應(yīng)食物的位置,并將對(duì)應(yīng)位置重置為空。做完這些后,把要到達(dá)的目標(biāo)位置發(fā)送給后面的芯片,由后面的芯片來(lái)控制電機(jī)的移動(dòng),它自己不負(fù)責(zé)這些工作;
第三塊芯片在收到第二塊芯片告知的目標(biāo)位置時(shí),控制電機(jī)的移動(dòng);
第四塊芯片用于控制電機(jī)的伸縮和抓握。這兩塊芯片是勤勞的工人。
電路圖和代碼如下:

上方居中的芯片是和不斷提供數(shù)據(jù)包的 C2S-RF901 直接連接的芯片,它是任務(wù)的調(diào)度者,也就是我們所說(shuō)的第一塊芯片。我們先來(lái)分析它的代碼:

C2S-RF901 的首數(shù)字有三種可能性:-999、1、2。因此首先肯定是一個(gè)三態(tài)判定(tcp x1 1)。當(dāng)首數(shù)字是 -999 時(shí),什么事都不用做,直接跳到最后睡覺(- jmp 9)。當(dāng)首數(shù)字是 1 或 2 時(shí),說(shuō)明收到了新任務(wù),我們首先要給 p0 口設(shè)置狀態(tài)值,0 為存,100 為??;其次要給左邊的芯片發(fā)兩個(gè)數(shù)字,第一個(gè)數(shù)字是“要找的食物的編號(hào)是多少”,第二個(gè)數(shù)字是“要將對(duì)應(yīng)位置的編號(hào)置為多少”。我們依次來(lái)分析。
首數(shù)字是 2 時(shí),說(shuō)明收到的是【取】任務(wù),我們首先要將 p0 口的值置為 100 供下方芯片使用(+ mov 100 p0),然后我們需要找的食物編號(hào)是和數(shù)據(jù)包的第二個(gè)數(shù)字一致的,所以我們往左邊芯片發(fā)送的第一個(gè)數(shù)字即為該數(shù)字(+ mov x1 x0)。找到對(duì)應(yīng)的食物后,需要將對(duì)應(yīng)的格子清零,所以我們往左邊芯片發(fā)送的第二個(gè)數(shù)字為 0(+ mov 0 x0, + jmp 9)。
首數(shù)字是 1 時(shí),說(shuō)明收到的是【存】任務(wù),我們首先要將 p0 口的值置為 0 供下方使用(mov 0 p0),同時(shí)我們要找的食物編號(hào)也為 0(尋找冷庫(kù)中的第一個(gè)空位),往左邊芯片發(fā)送的第一個(gè)數(shù)字是 0(mov 0 x0)。這里就可以用我們之前提到的“多處清零”技巧,將 mov 0 p0 和 mov 0 x0 兩條指令合并成一條(mov p0 x0)。找到空位后,我們將對(duì)應(yīng)的格子置為新的食物編號(hào),所以往左邊芯片發(fā)送的第二個(gè)數(shù)字為數(shù)據(jù)包里的第二個(gè)數(shù)字(mov x1 x0)。做完以上操作后,休眠一秒,進(jìn)入下一個(gè)時(shí)鐘周期(slp 1)。
當(dāng)你讀只寫的 p0 時(shí),會(huì)讀到 0,同時(shí)之前寫入 p0 口的數(shù)據(jù)也會(huì)被清除。與此同時(shí),你將讀到的 0 賦給 acc,這樣就成功用一條指令將兩處存儲(chǔ)置零了(節(jié)省了電量,甚至還可能因此省下關(guān)鍵的一行代碼,正好夠放在一塊 4000 或 6000 里)。 作者:ココアお姉ちゃん https://www.bilibili.com/read/cv16899049?出處:bilibili
這是我從第 10 關(guān)開始提的,后續(xù)也反復(fù)提到的“多處清零,一舉兩得”的技巧。但是,曾經(jīng)使用這樣的技巧只是為了省電和省行數(shù)。而在這一關(guān)里,我們使用了該技巧,將原先的 10 行代碼壓縮到了 9 行,使得代碼正好能夠裝在一塊 MC4000 里,連成本也省了下來(lái),屬于是這條技巧的高光時(shí)刻了。
我們?yōu)槭裁匆獙⒋?取的狀態(tài)值存入 p0 口呢?我們觀察電路圖可以發(fā)現(xiàn),第一塊芯片的 p0 口是和其正下方芯片的 p0 口相接的,后面我們會(huì)說(shuō)到,正下方的這塊芯片是用于控制電機(jī)移動(dòng)的“第三塊芯片”。觀察時(shí)序圖我們發(fā)現(xiàn),【存】任務(wù)里我們需要“先抓取食物,再移動(dòng)電機(jī)”;【取】任務(wù)里我們需要“先移動(dòng)電機(jī),再抓取食物”。為了確保電機(jī)的正常工作,這一塊芯片有必要知道當(dāng)前的任務(wù)類型是什么。所以我們需要將存/取狀態(tài)值提前放入 p0 口。
第一塊芯片將調(diào)度指令發(fā)給第二塊芯片,將任務(wù)類型通過(guò) p0 口告知第三塊芯片后,它的任務(wù)就完成了。接下來(lái)我們看位于左上方的第二塊芯片——數(shù)據(jù)庫(kù)管理員——的代碼:

雖然只有 9 行代碼,但因?yàn)橛玫搅?dat 寄存器,所以用 MC6000 不虧。去掉 dat 的難度比去掉一行代碼的難度大多了,甚至很多情況下根本不可能。
首先這塊芯片需要等待其右側(cè)的調(diào)度芯片發(fā)送調(diào)度任務(wù)(slx x3)。收到調(diào)度任務(wù)后,我們將發(fā)來(lái)的第一個(gè)“要找的食物”的數(shù)字暫存到 dat 中(mov x3 dat),然后開始遍歷 RAM,尋找值為 dat 的格子(mov x1 acc, teq x0 dat, - jmp 3)。找到后,acc 存儲(chǔ)的是對(duì)應(yīng)格子的地址,我們將指針定位到該地址(mov acc x1),然后接收調(diào)度芯片發(fā)來(lái)的第二個(gè)數(shù)字,將對(duì)應(yīng)位置處的數(shù)字改寫成收到的新數(shù)字(mov x3 x0)。
由于 RAM 中的地址是以 0 起始的,而冷庫(kù)中的位置編號(hào)是以 1 起始的,所以冷庫(kù)地址 = RAM 地址 +1。而恰好,我們?cè)谧x/寫 RAM 后,地址會(huì)自增 1。因此我們可以直接將自增后的 RAM 地址作為“冷庫(kù)目標(biāo)地址”,發(fā)給右邊的第三塊芯片,讓第三塊芯片開始控制電機(jī)(mov x1 x2)。操作完畢后,我們將 RAM 地址歸零,等待下一次調(diào)度(mov 0 x1)。
做完這些工作后,第二塊芯片的任務(wù)也結(jié)束了。接下來(lái)我們看第三塊和第四塊芯片——勤勞的工人們——的代碼:

先看左邊的,控制電機(jī)移動(dòng)的芯片。首先,我們需要將電機(jī)的初始電平信號(hào)置為 50(mov 50 p1)。設(shè)置完畢后,等待數(shù)據(jù)庫(kù)管理員發(fā)送目標(biāo)位置信號(hào)(slx x0)。收到位置信號(hào)后,我們將位置值暫存到 acc 中(mov x0 acc)。這時(shí)候,我們需要判定調(diào)度芯片告知我們的任務(wù)種類是什么(tcp p0 50)。
如果任務(wù)類型是【存】,也就是 p0 的值為 0,那激活的是 - 號(hào)指令。我們需要先在 0 位置完成伸出、抓握的動(dòng)作后再向右移動(dòng),此時(shí)需要直接通知右邊的芯片開始伸出、抓握。同時(shí)因?yàn)殡姍C(jī)移動(dòng)的過(guò)程中,電機(jī)要保持抓握狀態(tài)不能松開,所以我們需要將移動(dòng)時(shí)長(zhǎng)(也就是目標(biāo)位置)發(fā)送給右邊的芯片,讓右邊的芯片“保持抓握”這么長(zhǎng)時(shí)間后再松開(- mov acc x3)。初始的伸出、抓握動(dòng)作需要耗費(fèi)兩秒鐘(- slp 2),兩秒鐘過(guò)后,我們令電機(jī)持續(xù)右移 acc 秒(mov 100 p1, slp acc)。到達(dá)目標(biāo)位置后,將電機(jī)的電平信號(hào)還原成 50(mov 50 p1)并休眠兩秒(slp 2),等待右邊的芯片完成伸出、放手動(dòng)作后,我們?cè)倭铍姍C(jī)持續(xù)左移 acc 秒回到原點(diǎn)(mov 0 p1, slp acc)。做完這些后,回到第一行,將電機(jī)的電平信號(hào)還原成 50(mov 50 p1)后就完成了任務(wù),耐心等待下一次任務(wù)調(diào)度即可(slx x0)。
如果任務(wù)類型是【取】,也就是 p0 的值為 100,那激活的是 + 號(hào)指令。我們需要先向右移動(dòng)到目標(biāo)位置后(mov 100 p1, slp acc)后再通知右邊的芯片伸出、抓握(+ mov acc x3)。接下來(lái)的動(dòng)作和上面完全一樣,耗費(fèi)兩秒鐘等待伸出、抓握的動(dòng)作完成(mov 50 p1, slp 2),向左移動(dòng) acc 秒回到原點(diǎn)(mov 0 p1, slp acc),完成任務(wù)等待下一次調(diào)度(mov 50 p1, slx x0)。
再看最右邊的控制伸出、抓握的芯片。收到左邊芯片的信號(hào)后(slx x0),將“保持抓握”的時(shí)長(zhǎng)存入 acc(mov x0 acc),然后令爪子伸出 1 秒(gen p1 1 0),抓握 1 秒(mov 100 p0, slp 1)后,電機(jī)會(huì)(存時(shí)向右,取時(shí)向左)移動(dòng) acc 秒,在此期間電機(jī)要保持抓握的姿勢(shì)不變(slp acc)。到達(dá)目的地后,令爪子伸出 1 秒(mov 100 p1, slp 1)后放手(mov p0 p1,將伸出和抓握信號(hào)同時(shí)清除)。至此,本次的抓、放任務(wù)就完成了。
這是一個(gè)“大家分工合作,一起共同創(chuàng)造美好生活”的典型案例。
點(diǎn)擊左下角的【模擬】,運(yùn)行程序。注意觀察屏幕右下角的動(dòng)圖,看清楚電機(jī)的工作過(guò)程。稍等片刻,便會(huì)彈出結(jié)算界面:
