「真實」是什麼?你該如何定義「真實」?如果你談論的是你能感受、聞到、嚐到和看到的事物……那麼「真實」就只是大腦解讀的電氣訊號。

如果處理器是電腦的大腦,它能否也成為某種虛擬實境的一部分?模擬記憶體、軟體定義的外圍設備、人工產生的中斷。

我的第一台電腦是 286,擁有 1 MB 的 RAM 和 50 MB 的硬碟(如果我沒記錯的話)。所以我決定找一顆 286 處理器,並試圖圍繞它模擬電腦的其他部分。或者至少讓它能夠開機並執行一些簡單的組合語言程式碼。

兩年前,我訂購了兩顆(一包就是兩顆)Harris 80C286-12 處理器。我的記憶有點模糊,但我相信它名稱中的 C 很重要,因為這類處理器對時脈精確度較不敏感(結尾的 12 表示它喜歡在 12 MHz 下運行),甚至可以手動步進。

起初,我並沒有太成功,專案最終被擱置。今年我又重新拾起它,試圖找出哪裡出了問題。

處理器安裝在 PLCC-68 插槽中。插槽的針腳不適合直接插入跳線,因此我將插槽安裝在帶有跳線兼容接頭的轉接板 PCB 上。晶片和插槽的針腳圖都包含在資料表中,但轉接板 PCB 讓事情變得有點複雜,於是我創建了一個小型轉換表來讓我的工作更輕鬆。

該表也有助於識別各種輸入和輸出,這對於稍後連接到 Raspberry Pi 非常有用。正如你所見,需要不少於 57 個針腳,這比 Pi 所能提供的要多。MCP23S17 IO 擴展器派上了用場。雖然它無法讓我們以支援的 12 MHz 的驚人速度驅動處理器,但幸運的是,這並非我們的目標。

該晶片包含 16 個 IO 針腳,所以我們需要四個。雖然每個針腳都可以獨立配置為輸入或輸出,但我試圖將它們進行邏輯分組。擴展器有 A 面和 B 面,每面有 8 個針腳,最終結果如下所示:

Pi 透過 SPI 與擴展器通訊。對此有幾種解決方案。我選擇了所有晶片同時啟動,Pi 透過其硬體位址向它們發送訊息。

在此情況下,RESET 針腳(用紫色電纜連接)不需要由 Pi 控制,但在一次除錯過程中,我嘗試了它,希望它能有所幫助,並且就這樣保留了下來。現在我們只需要用一大堆跳線連接所有東西,就可以開始程式設計了。

我們只需要 MCP23S17 的一小部分功能。我們只需要配置 IO 針腳的方向並讀寫相關的暫存器。配置是透過修改暫存器值來完成的。首先,我們需要啟用硬體定址的使用。預設情況下,所有晶片都具有位址 000,因此如果我們將暫存器修改發送到該位址(在 IOCON 暫存器中設定 HAEN 位元),硬體定址將在所有四個晶片上同時啟用。

經過幾個小時(幾天)的絞盡腦汁,結果發現僅僅這樣並不一定足以正常運作。我們還需要將相同的訊息發送到已設定的硬體位址本身以啟用硬體定址(我知道這有點奇怪)。因此,如果我們將硬體位址設定為 101,我們必須將先前發送到 000 的原始暫存器修改訊息也重新發送到 101。

現在硬體定址已解決,我們需要將每個晶片的 IODIRA 和 IODIRB 暫存器設定為適當的方向。由於我們的分組,我們可以一次將整個面配置為讀取(11111111)或寫入(00000000)。更多詳細資訊可以在晶片的資料表中找到。

最初,我開始使用 Pi Zero,但最終決定使用運行 MicroPython 的 Pi Pico。為了管理擴展器晶片,我創建了以下小型類別:

在 init 中,你可以清楚地看到我們將 IOCON 暫存器設定了兩次。我們可以像這樣使用該類別與處理器進行通訊:

起初,我錯過了這裡的 init 調用,當一切都不起作用時感到驚訝。大多數針腳都配置為讀取;只有標誌需要設定為寫入。

在我們做任何事情之前,我們需要重設處理器。為此,RESET 標誌必須保持活躍至少 16 個時脈週期,並且其開關必須與時脈標誌同步。首先,我創建了一些標誌的常數,讓事情更容易:

將此與先前的 MCP23S17 針腳映射進行比較是值得的。我們將每組 8 個針腳視為 8 位元 / 1 位元組的資料。例如,在「misc」晶片的 GPIOB 面位元組中,HLDA 標誌是最低有效位元,而 PEACK 是最高有效位元。

有了標誌後,我們就可以執行 RESET:

睡眠間隔的選擇或多或少是任意的;我們不必遵守任何嚴格的時間要求。在 RESET 期間,處理器必須進入定義的狀態。我們可以透過以下程式碼片段進行驗證:

我們期望看到的數值如下所示:

奇怪的是,我卻看到了以下結果:

很難不注意到第二行和第四行的數值是相同的。我檢查了所有連接,拆卸了所有東西,用 LED 進行除錯以確保我寫入的數值被傳送到正確的位置,更換了分配給 A15-8 針腳的晶片,用備用晶片替換了處理器,重複閱讀程式碼上千次,但都無濟於事。

然後我發現了前面提到的 MCP23S17 的硬體定址技巧,一切都開始像魔法一樣奏效。重點是,如果一切順利,我們可以釋放 RESET 標誌,啟動過程就可以開始了。

在此之後,處理器必須在 50 個時脈週期內開始從位址 0xFFFFF0 讀取第一條要執行的指令。COD/INTA、M/IO、S0 和 S1 標誌決定了處理器打算做什麼。

我省略了表中不太有趣的項目;它們可以在資料表中查看。對於我們的小測試,我們只需要這四個:

所以我們開始發送時脈訊號,並等待直到我們遇到第一個「記憶體指令讀取」:

成功到達時,我們可以開始發送,例如 NOP(0x90)指令。我們將資料匯流排設定為寫入模式,將 NOP 指令放在上面,發送時脈訊號,然後將資料匯流排設定回讀取模式。

這一切都很好,但讓我們看看更有趣的東西。一些需要讀寫記憶體的操作。一個簡單的小程式,它從記憶體中讀取兩個數字,將它們相加,然後將結果寫回記憶體。

由於我們從非常接近記憶體末端(0xFFFFF0)開始,所以我們沒有太多空間,因此我們首先需要跳到其他地方。

使用 nasm 程式,我們也可以從它生成二進位檔:

然後,透過一個簡短的 Python 腳本,我們可以將其轉換為 Python 友好的格式,以便我們可以將其載入到我們的虛擬記憶體中:

為了模擬記憶體,我組裝了以下小型類別:

它只是一個簡單的字典,帶有一個輔助函數,允許我們將資料載入到任意位址。然後我們使用 nasm 生成的程式碼來執行此操作:

剩下要處理的就是各種情況。但首先,我們需要談談 BHE 標誌和 A0 針腳。

因此,在涉及資料匯流排的操作期間,我們可以讀寫整個資料匯流排、其上半部分或其下半部分。

在我們的案例中,「記憶體資料讀取」與「記憶體指令讀取」非常相似,因此我們可以用相同的程式碼處理兩者。我們只需要處理上述標誌並使用偽記憶體。

這比我們最初的基於 NOP 的解決方案複雜不了多少,但這裡有一個額外的轉折,很容易讓人絆倒。我們應該按照什麼順序將位元組放到資料匯流排上?GPIOA 暫存器代表資料匯流排的最低有效位元組,而 GPIOB 代表最高有效位元組。因此,例如,我們的初始 JMP 指令(0xea00)將以 0x00ea(小端序)傳輸。

稍微往回滾動一下,注意到 nasm 已經執行了類似的交換。例如,我們用於加法的 0x1234 值在記憶體中儲存為 0x3412。

「記憶體資料寫入」非常直接;我們只需使用偽記憶體:

在這裡也可以觀察到小端序,儘管在執行過程中,我沒有遇到一次它試圖一次寫入兩個位元組到記憶體的情況。

而在「停止/關機」期間,我們只需從記憶體中列印加法的結果並退出:

最後,運行程式應該會產生類似以下的輸出,您可以看到它讀取初始的 JMP 指令,跳到新位址,從那裡繼續讀取指令,從記憶體中讀取要相加的兩個數字,最後將結果寫回記憶體:

第一次看到正確的最終結果時,我感到非常高興。我認為我已經達到了一個里程碑,現在可以暫停休息一下了。

當然,我們只是觸及了皮毛;還有很多東西需要學習。值得仔細閱讀處理器的資料表,或者思考鍵盤或文字顯示器等各種外圍設備實際上是如何實現的。

然而,可以肯定的是,對於處理器來說,這種現實一點也不虛擬。對它來說,電氣訊號來自何處並不重要,只要它們與其自身的內部現實兼容即可。