最美情侣中文字幕电影,在线麻豆精品传媒,在线网站高清黄,久久黄色视频

歡迎光臨散文網(wǎng) 會員登陸 & 注冊

【深圳 IO 攻略】第 16 關(guān):幽靈娃娃

2022-06-03 15:00 作者:ココアお姉ちゃん  | 我要投稿

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

關(guān)卡展示

這一關(guān)提供了一個沾滿灰塵的隨機數(shù)發(fā)生器。我們要做的是:當(dāng)隨機數(shù)發(fā)生器生成?1 時,令揚聲器播放【邪惡地笑】音效;而當(dāng)隨機數(shù)發(fā)生器生成 2 時,令揚聲器播放【令人毛骨悚然的尖叫】音效。

這兩種音效的波形我們可以在數(shù)據(jù)手冊中找到:

這些音效的波形數(shù)據(jù)由大量的數(shù)字組成,僅靠 MC 系列芯片里的 acc 和 dat 寄存器已經(jīng)無法存儲。這里我們必須要使用到元件面板里的擴展存儲元件。

擴展存儲元件有兩種:一種是 100P-14,黃底,有 14 格額外的存儲空間,數(shù)據(jù)可讀可寫,但初值只能是全 0,任何寫操作都必須在運行時完成,不能在運行前寫好初始數(shù)據(jù)。相當(dāng)于現(xiàn)實世界里的隨機存儲器(RAM)。

另一種是 200P-14,黑底,同樣有 14 格額外的存儲空間。但是里面的數(shù)據(jù)必須在設(shè)計電路板時就確定好,運行時只能讀,不能寫。相當(dāng)于現(xiàn)實世界里的只讀存儲器(ROM)。

這兩個擴展存儲元件有以下共同點:

1. 都帶有兩個指針。讀取 a0/a1 口可以獲得當(dāng)前的左/右指針的位置,范圍 0~13;而向 a0/a1 口寫數(shù)據(jù)則可以更改左/右指針的位置。當(dāng)寫入的地址值在 0~13 范圍之外時,會自動把地址值設(shè)置為傳入的數(shù)取除以 14 的余數(shù)。例如,向 a0 口傳 15?時,會將左指針置為 1;向 a1?口傳 -1 時,會將右指針置為 13。

如圖所示,當(dāng)執(zhí)行完 mov 15 x0 后,下方 ROM 的左指針指向了地址 1
如圖所示,當(dāng)執(zhí)行完 mov -1 x1 后,下方 ROM 的右指針指向了地址 13

2. 讀 d0 口會讀取到左指針?biāo)赶虻臄?shù)字,同時左指針自動向后移動一格。讀 d1 口會讀取到右指針?biāo)赶虻臄?shù)字,同時右指針自動向后移動一格。指針自增的這個特性非常優(yōu)秀,可以讓我們在讀連續(xù)數(shù)據(jù)時不需要反復(fù)操作地址口,只要連續(xù)讀數(shù)據(jù)口就完事了。下面舉一個連續(xù)從 ROM 中讀取三個數(shù),并將 acc 的百位、十位、個位依次置為這三個數(shù)的例子:

執(zhí)行完第一行的 dst 2 x1 后,ROM 的右指針讀到 3,將 acc 的百位置為 3。同時右指針向后移動一格,現(xiàn)在右指針位于地址 1 處。
執(zhí)行完第二行的 dst 1 x1 后,ROM 的右指針讀到 1,將 acc 的十位置為 1。同時右指針向后移動一格,現(xiàn)在右指針位于地址 2 處。
執(zhí)行完第三行的 dst 0 x1 后,ROM 的右指針讀到 4,將 acc 的個位置為 4。同時右指針向后移動一格,現(xiàn)在右指針位于地址 3 處。至此 acc 的三位數(shù)均已設(shè)置完畢,acc 的值為 314。
推理可得,執(zhí)行完全部 8 行代碼后,acc 的值為 159,ROM 的右指針指向了地址 6

3. 對于 RAM 而言,d0/d1 口不僅可讀,而且可寫。向 RAM 的 d0/d1 口寫數(shù)據(jù)時,左/右指針指向的空間里的內(nèi)容會被覆蓋為新內(nèi)容,同時左/右指針自動向后移動一格(指針自增這一點無論讀寫都一樣)。下面給出一個將 ROM 中的內(nèi)容復(fù)制到 RAM 中的示例程序:

首先我們從 ROM 的數(shù)據(jù)口(x0)讀入數(shù)據(jù),并將讀入的數(shù)據(jù)寫入 RAM 的數(shù)據(jù)口(x2)(mov x0 x2)。執(zhí)行完本次操作后,ROM 的右指針以及 RAM 的左指針都會自增 1。此時我們判斷任意一個地址值,如果指向了 0(teq x1 0,或 teq x3 0),說明前一次讀/寫操作是在 13 地址處進(jìn)行的,那么就說明所有的數(shù)據(jù)都復(fù)制完畢了,直接結(jié)束程序(+ slp 999)。如果自增后的地址值沒有指向 0,說明前一次讀寫并不是在 13 地址處進(jìn)行的,那么就要跳轉(zhuǎn)回去循環(huán)執(zhí)行復(fù)制操作(- jmp 1),直到將 13 地址處的數(shù)據(jù)復(fù)制完成為止。

運行程序,我們發(fā)現(xiàn) RAM?最終會被寫入和 ROM 一模一樣的數(shù)據(jù):

前面我們花了很大的篇幅介紹 ROM 和 RAM 這兩個擴展存儲元件,現(xiàn)在我們回到題目。這道題因為要在特定的觸發(fā)條件下播放兩種可能的聲音波形,且這兩種波形數(shù)據(jù)的長度均為 13。所以我們肯定是需要兩個 ROM 來存儲兩種聲音的波形數(shù)據(jù)的。

這道題我們發(fā)現(xiàn)可以將一塊 MC6000 物盡其用,4 個 x 口,正好接在兩個 ROM 的地址口和數(shù)據(jù)口上;剩下兩個 p 口,一個接隨機數(shù)生成器的輸入信號,一個接揚聲器的輸出信號。一點不浪費!我們先二話不說,搭出如下的電路圖:

左邊的 ROM 記錄的是《邪惡地笑》的波形數(shù)據(jù),而右邊的 ROM 記錄的是《令人毛骨悚然的尖叫》的波形數(shù)據(jù)。這兩個 ROM 都以 50 結(jié)尾,是因為播放完對應(yīng)音效后,需要將揚聲器的波形值重設(shè)為 50,而不能一直停留在音效的最終波形值上。

我們現(xiàn)在的思路就是:當(dāng)隨機數(shù)發(fā)生器生成 1 時,我們在接下來的 14 個時鐘周期里不斷讀左邊 ROM 的數(shù)據(jù)口,并將相應(yīng)的波形發(fā)送給揚聲器;同理,當(dāng)隨機數(shù)發(fā)生器生成 2 時,我們在接下來的 14 個時鐘周期里不斷讀右邊 ROM 的數(shù)據(jù)口,并將相應(yīng)的波形發(fā)送給揚聲器。

根據(jù)一開始舉的例子,我們已經(jīng)知道了讀完整個 ROM 的條件是:讀取數(shù)據(jù)后的地址值為 0。因此我們寫出如下的具有循環(huán)結(jié)構(gòu)的代碼:

首先,我們要給揚聲器賦上 50 的初始聲波(@ mov 50 p1)。接下來,我們判斷當(dāng)前時鐘周期里的隨機數(shù)是否為 1(teq p0 1)。如果是 1,我們不斷從左邊的 ROM 中讀取數(shù)據(jù)發(fā)給揚聲器(+ mov x0 p1, + slp 1),并判斷讀取后的地址值是否大于 0(+ tcp x1 0)。如果地址值大于 0,說明當(dāng)前聲波還沒輸出完畢,需要跳到第 3 行繼續(xù)發(fā)送(+ jmp 3),直到地址值為 0 為止,關(guān)閉所有的 + - 號指令,跳到最后一行,休眠一秒,等待下一次信號(slp 1)。如果一開始的隨機數(shù)不是 1,而是 2(- teq p0 2),則改為從右邊的 ROM 循環(huán)讀?。? mov x2 p1, + slp 1, + tcp x3 0, + jmp 8)。等到讀取完畢,右邊的 ROM 的地址值為 0?時,跳到最后一行,休眠一秒,等待下一次信號(slp 1)。

點擊左下角的【模擬】,稍等片刻,便會彈出結(jié)算界面:

優(yōu)化電量

播放特定音效時,我們需要讀取連續(xù)的 14 個波形數(shù)據(jù)。14 除自己外的因數(shù)有 1、2、7,也就是說,我們在循環(huán)的過程中,每次循環(huán)讀取 1/2/7 個波形數(shù)據(jù)時,都能保證循環(huán)正常結(jié)束。為什么每次讀取的數(shù)據(jù)長度必須要是總長度的因數(shù)呢?假如一次讀取 3 個波形數(shù)據(jù),那么在第 5 次循環(huán)時,就會錯誤地觸發(fā)“讀取第 15 個波形數(shù)據(jù)”這樣的事件。

現(xiàn)在我們的 MC6000 里還有兩行代碼空間。我們可以將其中一個音效的循環(huán)改為“每次讀取兩個波形數(shù)據(jù)”,從而減少判斷循環(huán)是否結(jié)束的次數(shù)。如下圖所示,播放音效 2 的循環(huán)改成了“每次讀取兩個波形數(shù)據(jù)”。

最終的三項指標(biāo)為:成本 ¥9,電量 192(比歷史最佳少了 14 格電,少執(zhí)行了?7 次是否終止循環(huán)的判斷,和 7 次跳轉(zhuǎn)),代碼行數(shù) 14

優(yōu)化代碼行數(shù)

這個沾滿灰塵的隨機數(shù)發(fā)生器不會生成值為 0 的隨機數(shù),因此我們可以將讀到的隨機數(shù)分成三類:小于 2(即 1),等于 2,大于 2。沒錯,一次 tcp 三態(tài)判定就可以一勞永逸。

第一行將揚聲器初始化為 50,所有設(shè)計方案都是一樣的(@ mov 50 p1)。然后我們將 p0 的值存入 acc,并對 acc 做三態(tài)判定(mov p0 acc, tcp acc 2)。這時候可能有讀者會問了:p 口數(shù)據(jù)又不像 x 口那樣只能讀一次,明明可以反復(fù)讀,為啥還得存到 acc 里再做三態(tài)判定呢?因為:①p 口數(shù)據(jù)只能在當(dāng)前時鐘里反復(fù)讀,不代表在后續(xù)的時鐘周期里還能反復(fù)讀當(dāng)前這一秒的數(shù)字,“一旦錯過就不再”;②播放音效時,循環(huán)里的跳轉(zhuǎn)指令會破壞 + - 號狀態(tài),每次循環(huán)回來都要重新判定。重新判定的時候,我們不能以實時的隨機數(shù)作為依據(jù),只能以進(jìn)入音效播放流程前的隨機數(shù)作為依據(jù)。所以我們在進(jìn)入音效播放流程前,必須要將隨機數(shù)存入 acc,以方便后續(xù)的判斷。

那為什么前一個方案就不需要將 p0 的值存入 acc 呢?因為前一個方案里,兩個音效的播放流程是各自獨立,不共享代碼的。在各自的流程里,循環(huán)部分全部是用 + 號串聯(lián)起來的,只要跳轉(zhuǎn)指令也使用 + 號,就可以保證不破壞 + - 號狀態(tài)。只有當(dāng)循環(huán)的過程中使用了 + - 號分離了代碼塊時,循環(huán)最后的跳轉(zhuǎn)指令才會起到破壞 + - 號狀態(tài)的副作用。只有這種時候,才需要在循環(huán)的每個周期里都重新執(zhí)行測試指令恢復(fù) + - 號狀態(tài)。

回到程序。如果 acc 的值大于 2,那么直接跳到第 7 行休眠一秒(+ jmp 7)。進(jìn)入下一個時鐘周期后,因為兩個 ROM 的數(shù)據(jù)指針都沒有動過,所以后續(xù)的 tcp x3 0 判斷一定是不成立的,會直接跳回第 2 行,重新對新的隨機數(shù)做三態(tài)判定。

如果 acc 的值小于等于 2,那么我們需要分情況討論是 1 還是 2。我們先假設(shè) acc 是 2,從右邊的 ROM 中讀一格波形數(shù)據(jù)發(fā)給揚聲器(mov x2 p1)。如果 acc 的值是 1,那么再撤銷剛才發(fā)的波形數(shù)據(jù),改為從左邊的 ROM 中讀取(- mov x0 p1)。讀取完畢后,休眠 1 秒,讓波形數(shù)據(jù)作用到揚聲器上(slp 1)。這就相當(dāng)于:當(dāng) acc 是 2 時,只讀取右邊 ROM 的波形;而當(dāng) acc 是 1 時,左右兩邊的 ROM 一起讀取,但最終生效的是左邊 ROM 的波形。因此,只要進(jìn)入了音效播放流程,右邊的 ROM 指針是一定會動的。此時,我們可以統(tǒng)一以右側(cè) ROM 的地址值是否為 0,作為判定循環(huán)結(jié)束的依據(jù)(tcp x3 0)。如果該地址值大于 0,說明當(dāng)前聲波還沒輸出完畢,需要跳到第 3 行,將 + - 號狀態(tài)還原,并繼續(xù)發(fā)送波形數(shù)據(jù)(+ jmp 3)。等到讀取完畢,右邊的 ROM 的地址值為 0?時,再跳回到開頭,重新對新的隨機數(shù)做三態(tài)判定。

點擊左下角的【模擬】,稍等片刻,便會彈出結(jié)算界面:

我們通過三態(tài)判定,及巧用 + - 號的方式,合并了初始案例中的相似代碼,省去了一行休眠、一行地址判定、一行跳轉(zhuǎn)的代碼,用電量消耗驟然增加的代價,把總代碼行數(shù)減少到了 9 行。當(dāng)不同條件下需要執(zhí)行的邏輯相似時,我們可以【將相似的邏輯合并】,以一定程度的電量為代價,換取【相似邏輯間共享代碼,節(jié)約代碼行數(shù),甚至節(jié)約成本】的成果。【邏輯合并】是優(yōu)化代碼行數(shù)及優(yōu)化成本時的常用套路,后續(xù)關(guān)卡中仍然會再度用到。

碎碎念

代碼壓縮到了 9 行?沒用到 dat 寄存器?對不起,因為你的 6 個口都接上導(dǎo)線了,想降成本換成 MC4000,沒門。即使左邊 ROM 的地址口沒用上,那也是接了 5 個口。連接的口數(shù)減少不到 4 個,就別想換成 4000。

【深圳 IO 攻略】第 16 關(guān):幽靈娃娃的評論 (共 條)

分享到微博請遵守國家法律
南澳县| 黔南| 涞水县| 屏山县| 安宁市| 新田县| 金湖县| 登封市| 云梦县| 湖州市| 石城县| 南康市| 龙海市| 泰顺县| 德化县| 新密市| 黄梅县| 砀山县| 盐山县| 桦南县| 山东| 黔江区| 大姚县| 耿马| 霍州市| 马龙县| 和平区| 都安| 东莞市| 深水埗区| 赤峰市| 乌兰浩特市| 南康市| 右玉县| 安丘市| 建水县| 恩施市| 辽阳市| 孝义市| 平顺县| 山丹县|