計算機實驗室之樹莓派:課程 6 螢幕01

2019-02-16 14:52:00

歡迎來到螢幕系列課程。在本系列中,你將學習在樹莓派中如何使用組合程式碼控制螢幕,從顯示亂數據開始,接著學習顯示一個固定的影象和顯示文字,然後格式化數位為文字。假設你已經完成了 OK 系列課程的學習,所以在本系列中出現的有些知識將不再重複。

第一節的螢幕課程教你一些關於圖形的基礎理論,然後用這些理論在螢幕或電視上顯示一個圖案。

1、入門

預期你已經完成了 OK 系列的課程,以及那個系列課程中在 gpio.ssystemTimer.s 檔案中呼叫的函數。如果你沒有完成這些,或你喜歡完美的實現,可以去下載 OK05.s 解決方案。在這裡也要使用 main.s 檔案中從開始到包含 mov sp,#0x8000 的這一行之前的程式碼。請刪除這一行以後的部分。

2、計算機圖形

正如你所認識到的,從根本上來說,計算機是非常愚蠢的。它們只能執行有限數量的指令,僅僅能做一些數學,但是它們也能以某種方式來做很多很多的事情。而在這些事情中,我們目前想知道的是,計算機是如何將一個影象顯示到螢幕上的。我們如何將這個問題轉換成二進位制?答案相當簡單;我們為每個顏色設計一些編碼方法,然後我們為在螢幕上的每個畫素儲存一個編碼。一個畫素就是你的螢幕上的一個非常小的點。如果你離螢幕足夠近,你或許能夠辨別出你的螢幕上的單個畫素,能夠看到每個影象都是由這些畫素組成的。

將顏色表示為數位有幾種方法。在這裡我們專注於 RGB 方法,但 HSL 也是很常用的另一種方法。

隨著計算機時代的進步,人們希望顯示越來越複雜的圖形,於是發明了圖形卡的概念。圖形卡是你的計算機上用來在螢幕上專門繪製影象的第二個處理器。它的任務就是將畫素值資訊轉換成顯示在螢幕上的亮度級別。在現代計算機中,圖形卡已經能夠做更多更複雜的事情了,比如繪製三維圖形。但是在本系列教學中,我們只專注於圖形卡的基本使用;從記憶體中取得畫素然後把它顯示到螢幕上。

不管使用哪種方法,現在馬上出現的一個問題就是我們使用的顏色編碼。這裡有幾種選擇,每個產生不同的輸出品質。為了完整起見,我在這裡只是簡單概述它們。

名字唯一顏色數量描述範例
單色2每個畫素使用 1 位去儲存,其中 1 表示白色,0 表示黑色。Monochrome image of a bird
灰度256每個畫素使用 1 個位元組去儲存,使用 255 表示白色,0 表示黑色,介於這兩個值之間的所有值表示這兩個顏色的一個線性組合。Geryscale image of a bird
8 色8每個畫素使用 3 位去儲存,第一位表示紅色通道,第二位表示綠色通道,第三位表示藍色通道。8 colour image of a bird
低色值256每個畫素使用 8 位去儲存,前三位表示紅色通道的強度,接下來的三位表示綠色通道的強度,最後兩位表示藍色通道的強度。Low colour image of a bird
高色值65,536每個畫素使用 16 位去儲存,前五位表示紅色通道的強度,接下來的六位表示綠色通道的強度,最後的五位表示藍色通道的強度。High colour image of a bird
真彩色16,777,216每個畫素使用 24 位去儲存,前八位表示紅色通道,第二個八位表示綠色通道,最後八位表示藍色通道。True colour image of a bird
RGBA3216,777,216 帶 256 級透明度每個畫素使用 32 位去儲存,前八位表示紅色通道,第二個八位表示綠色通道,第三個八位表示藍色通道。只有一個影象繪製在另一個影象的上方時才考慮使用透明通道,值為 0 時表示下面影象的顏色,值為 255 時表示上面這個影象的顏色,介於這兩個值之間的所有值表示這兩個影象顏色的混合。

不過這裡的一些影象只用了很少的顏色,因為它們使用了一個叫空間抖動的技術。這允許它們以很少的顏色仍然能表示出非常好的影象。許多早期的作業系統就使用了這種技術。

在本教學中,我們將從使用高色值開始。這樣你就可以看到影象的構成,它的形成過程清楚,影象品質好,又不像真彩色那樣佔用太多的空間。也就是說,顯示一個比較小的 800x600 畫素的影象,它只需要小於 1 MiB 的空間。它另外的好處是它的大小是 2 次冪的倍數,相比真彩色這將極大地降低了獲取資訊的複雜度。

樹莓派和它的圖形處理器有一種特殊而奇怪的關係。在樹莓派上,首先執行的事實上是圖形處理器,它負責啟動主處理器。這是很不常見的。最終它不會有太大的差別,但在許多互動中,它經常給人感覺主處理器是次要的,而圖形處理器才是主要的。在樹莓派上這兩者之間依靠一個叫 “郵箱” 的東西來通訊。它們中的每一個都可以為對方投放郵件,這個郵件將在未來的某個時刻被對方收集並處理。我們將使用這個郵箱去向圖形處理器請求一個地址。這個地址將是一個我們在螢幕上寫入畫素顏色資訊的位置,我們稱為幀緩衝,圖形卡將定期檢查這個位置,然後更新螢幕上相應的畫素。

儲存幀緩衝frame buffer給計算機帶來了很大的記憶體負擔。基於這種原因,早期計算機經常作弊,比如,儲存一螢幕文字,在每次單獨重新整理時,它只繪製重新整理了的字母。

3、編寫郵差程式

接下來我們做的第一件事情就是編寫一個“郵差”程式。它有兩個方法:MailboxRead,從暫存器 r0 中的郵箱通道讀取一個訊息。而 MailboxWrite,將暫存器 r0 中的頭 28 位的值寫到暫存器 r1 中的郵箱通道。樹莓派有 7 個與圖形處理器進行通訊的郵箱通道。但僅第一個對我們有用,因為它用於協調幀緩衝。

訊息傳遞是元件間通訊時使用的常見方法。一些作業系統在程式之間使用虛擬訊息進行通訊。

下列的表和示意圖描述了郵箱的操作。

表 3.1 郵箱地址

地址大小 / 位元組名字描述讀 / 寫
2000B8804Read接收郵件R
2000B8904Poll不檢索接收R
2000B8944Sender傳送者資訊R
2000B8984Status資訊R
2000B89C4Configuration設定RW
2000B8A04Write傳送郵件W

為了給指定的郵箱傳送一個訊息:

  1. 傳送者等待,直到 Status 欄位的頭一位為 0。
  2. 傳送者寫入到 Write,低 4 位是要傳送到的郵箱,高 28 位是要寫入的訊息。

為了讀取一個訊息:

  1. 接收者等待,直到 Status 欄位的第 30 位為 0。
  2. 接收者讀取訊息。
  3. 接收者確認訊息來自正確的郵箱,否則再次重試。

如果你覺得有信心,你現在已經有足夠的資訊去寫出我們所需的兩個方法。如果沒有信心,請繼續往下看。

與以前一樣,我建議你實現的第一個方法是獲取郵箱區域的地址。

.globl GetMailboxBaseGetMailboxBase:ldr r0,=0x2000B880mov pc,lr

傳送程式相對簡單一些,因此我們將首先去實現它。隨著你的方法越來越複雜,你需要提前去規劃它們。規劃它們的一個好的方式是寫出一個簡單步驟列表,詳細地列出你需要做的事情,像下面一樣。

  1. 我們的輸入將要寫什麼(r0),以及寫到什麼郵箱(r1)。我們必須驗證郵箱的真實性,以及它的低 4 位的值是否為 0。不要忘了驗證輸入。
  2. 使用 GetMailboxBase 去檢索地址。
  3. 讀取 Status 欄位。
  4. 檢查頭一位是否為 0。如果不是,回到第 3 步。
  5. 將寫入的值和郵箱通道組合到一起。
  6. 寫入到 Write

我們來按順序寫出它們中的每一步。

1、這將實現我們驗證 r0r1 的目的。tst 是通過計算兩個運算元的邏輯與來比較兩個運算元的函數,然後將結果與 0 進行比較。在本案例中,它將檢查在暫存器 r0 中的輸入的低 4 位是否為全 0。

.globl MailboxWriteMailboxWrite:tst r0,#0b1111movne pc,lrcmp r1,#15movhi pc,lr

tst reg,#val 計算暫存器 reg#val 的邏輯與,然後將計算結果與 0 進行比較。

2、這段程式碼確保我們不會覆蓋我們的值,或連結暫存器,然後呼叫 GetMailboxBase

channel .req r1value .req r2mov value,r0push {lr}bl GetMailboxBasemailbox .req r0

3、這段程式碼載入當前狀態。

wait1$:status .req r3ldr status,[mailbox,#0x18]

4、這段程式碼檢查狀態欄位的頭一位是否為 0,如果不為 0,回圈回到第 3 步。

tst status,#0x80000000.unreq statusbne wait1$

5、這段程式碼將通道和值組合到一起。

add value,channel.unreq channel

6、這段程式碼儲存結果到寫入欄位。

str value,[mailbox,#0x20].unreq value.unreq mailboxpop {pc}

MailboxRead 的程式碼和它非常類似。

  1. 我們的輸入將從哪個郵箱讀取(r0)。我們必須要驗證郵箱的真實性。不要忘了驗證輸入。
  2. 使用 GetMailboxBase 去檢索地址。
  3. 讀取 Status 欄位。
  4. 檢查第 30 位是否為 0。如果不為 0,返回到第 3 步。
  5. 讀取 Read 欄位。
  6. 檢查郵箱是否是我們所要的,如果不是返回到第 3 步。
  7. 返回結果。

我們來按順序寫出它們中的每一步。

1、這一段程式碼來驗證 r0 中的值。

.globl MailboxReadMailboxRead:cmp r0,#15movhi pc,lr

2、這段程式碼確保我們不會覆蓋掉我們的值,或連結暫存器,然後呼叫 GetMailboxBase

channel .req r1mov channel,r0push {lr}bl GetMailboxBasemailbox .req r0

3、這段程式碼載入當前狀態。

rightmail$:wait2$:status .req r2ldr status,[mailbox,#0x18]

4、這段程式碼檢查狀態欄位第 30 位是否為 0,如果不為 0,返回到第 3 步。

tst status,#0x40000000.unreq statusbne wait2$

5、這段程式碼從郵箱中讀取下一條訊息。

mail .req r2ldr mail,[mailbox,#0]

6、這段程式碼檢查我們正在讀取的郵箱通道是否為提供給我們的通道。如果不是,返回到第 3 步。

inchan .req r3and inchan,mail,#0b1111teq inchan,channel.unreq inchanbne rightmail$.unreq mailbox.unreq channel

7、這段程式碼將答案(郵件的前 28 位)移動到暫存器 r0 中。

and r0,mail,#0xfffffff0.unreq mailpop {pc}

4、我心愛的圖形處理器

通過我們新的郵差程式,我們現在已經能夠向圖形卡上傳送訊息了。我們應該傳送些什麼呢?這對我來說可能是個很難找到答案的問題,因為它不是任何線上手冊能夠找到答案的問題。儘管如此,通過查詢有關樹莓派的 GNU/Linux,我們能夠找出我們需要傳送的內容。

訊息很簡單。我們描述我們想要的幀緩衝區,而圖形卡要麼接受我們的請求,給我們返回一個 0,然後用我們寫的一個小的調查問卷來填充螢幕;要麼傳送一個非 0 值,我們知道那表示很遺憾(出錯了)。不幸的是,我並不知道它返回的其它數位是什麼,也不知道它意味著什麼,但我們知道僅當它返回一個 0,才表示一切順利。幸運的是,對於合理的輸入,它總是返回一個 0,因此我們不用過於擔心。

由於在樹莓派的記憶體是在圖形處理器和主處理器之間共用的,我們能夠只傳送可以找到我們資訊的位置即可。這就是 DMA,許多複雜的裝置使用這種技術去加速存取時間。

為簡單起見,我們將提前設計好我們的請求,並將它儲存到 framebuffer.s 檔案的 .data 節中,它的程式碼如下:

.section .data.align 4.globl FrameBufferInfoFrameBufferInfo:.int 1024 /* #0 物理寬度 */.int 768 /* #4 物理高度 */.int 1024 /* #8 虛擬寬度 */.int 768 /* #12 虛擬高度 */.int 0 /* #16 GPU - 間距 */.int 16 /* #20 位深 */.int 0 /* #24 X */.int 0 /* #28 Y */.int 0 /* #32 GPU - 指標 */.int 0 /* #36 GPU - 大小 */

這就是我們傳送到圖形處理器的訊息格式。第一對兩個關鍵字描述了物理寬度和高度。第二對關鍵字描述了虛擬寬度和高度。幀緩衝的寬度和高度就是虛擬的寬度和高度,而 GPU 按需要伸縮幀緩衝去填充物理螢幕。如果 GPU 接受我們的請求,接下來的關鍵字將是 GPU 去填充的引數。它們是幀緩衝每行的位元組數,在本案例中它是 2 × 1024 = 2048。下一個關鍵字是每個畫素分配的位數。使用了一個 16 作為值意味著圖形處理器使用了我們上面所描述的高色值模式。值為 24 是真彩色,而值為 32 則是 RGBA32。接下來的兩個關鍵字是 x 和 y 偏移量,它表示當將幀緩衝複製到螢幕時,從螢幕左上角跳過的畫素數目。最後兩個關鍵字是由圖形處理器填寫的,第一個表示指向幀緩衝的實際指標,第二個是用位元組數表示的幀緩衝大小。

在這裡我非常謹慎地使用了一個 .align 4 指令。正如前面所討論的,這樣確保了下一行地址的低 4 位是 0。所以,我們可以確保將被放到那個地址上的幀緩衝(FrameBufferInfo)是可以傳送到圖形處理器上的,因為我們的郵箱僅傳送低 4 位全為 0 的值。

當裝置使用 DMA 時,對齊約束變得非常重要。GPU 預期該訊息都是 16 位元組對齊的。

到目前為止,我們已經有了待傳送的訊息,我們可以寫程式碼去傳送它了。通訊將按如下的步驟進行:

  1. 寫入 FrameBufferInfo + 0x40000000 的地址到郵箱 1。
  2. 從郵箱 1 上讀取結果。如果它是非 0 值,意味著我們沒有請求一個正確的幀緩衝。
  3. 複製我們的影象到指標,這時影象將出現在螢幕上!

我在步驟 1 中說了一些以前沒有提到的事情。我們在傳送之前,在幀緩衝地址上加了 0x40000000。這其實是一個給 GPU 的特殊信號,它告訴 GPU 應該如何寫到結構上。如果我們只是傳送地址,GPU 將寫到它的回復上,這樣不能保證我們可以通過重新整理快取看到它。快取是處理器使用的值在它們被傳送到儲存之前儲存在記憶體中的片段。通過加上 0x40000000,我們告訴 GPU 不要將寫入到它的快取中,這樣將確保我們能夠看到變化。

因為在那裡發生很多事情,因此最好將它實現為一個函數,而不是將它以程式碼的方式寫入到 main.s 中。我們將要寫一個函數 InitialiseFrameBuffer,由它來完成所有協調和返回指向到上面提到的幀緩衝資料的指標。為方便起見,我們還將幀緩衝的寬度、高度、位深作為這個方法的輸入,這樣就很容易地修改 main.s 而不必知道協調的細節了。

再一次,來寫下我們要做的詳細步驟。如果你有信心,可以略過這一步直接嘗試去寫函數。

  1. 驗證我們的輸入。
  2. 寫輸入到幀緩衝。
  3. 傳送 frame buffer + 0x40000000 的地址到郵箱。
  4. 從郵箱中接收回復。
  5. 如果回復是非 0 值,方法失敗。我們應該返回 0 去表示失敗。
  6. 返回指向幀緩衝資訊的指標。

現在,我們開始寫更多的方法。以下是上面其中一個實現。

1、這段程式碼檢查寬度和高度是小於或等於 4096,位深小於或等於 32。這裡再次使用了條件執行的技巧。相信自己這是可行的。

.section .text.globl InitialiseFrameBufferInitialiseFrameBuffer:width .req r0height .req r1bitDepth .req r2cmp width,#4096cmpls height,#4096cmpls bitDepth,#32result .req r0movhi result,#0movhi pc,lr

2、這段程式碼寫入到我們上面定義的幀緩衝結構中。我也趁機將連結暫存器推入到棧上。

fbInfoAddr .req r3push {lr}ldr fbInfoAddr,=FrameBufferInfostr width,[fbInfoAddr,#0]str height,[fbInfoAddr,#4]str width,[fbInfoAddr,#8]str height,[fbInfoAddr,#12]str bitDepth,[fbInfoAddr,#20].unreq width.unreq height.unreq bitDepth

3、MailboxWrite 方法的輸入是寫入到暫存器 r0 中的值,並將通道寫入到暫存器 r1 中。

mov r0,fbInfoAddradd r0,#0x40000000mov r1,#1bl MailboxWrite

4、MailboxRead 方法的輸入是寫入到暫存器 r0 中的通道,而輸出是值讀數。

mov r0,#1bl MailboxRead

5、這段程式碼檢查 MailboxRead 方法的結果是否為 0,如果不為 0,則返回 0。

teq result,#0movne result,#0popne {pc}

6、這是程式碼結束,並返回幀緩衝資訊地址。

mov result,fbInfoAddrpop {pc}.unreq result.unreq fbInfoAddr

5、在一幀中一行之內的一個畫素

到目前為止,我們已經建立了與圖形處理器通訊的方法。現在它已經能夠給我們返回一個指向到幀緩衝的指標去繪製圖形了。我們現在來繪製一個圖形。

第一範例中,我們將在螢幕上繪製連續的顏色。它看起來並不漂亮,但至少能說明它在工作。我們如何才能在幀緩衝中設定每個畫素為一個連續的數位,並且要持續不斷地這樣做。

將下列程式碼複製到 main.s 檔案中,並放置在 mov sp,#0x8000 行之後。

mov r0,#1024mov r1,#768mov r2,#16bl InitialiseFrameBuffer

這段程式碼使用了我們的 InitialiseFrameBuffer 方法,簡單地建立了一個寬 1024、高 768、位深為 16 的幀緩衝區。在這裡,如果你願意可以嘗試使用不同的值,只要整個程式碼中都一樣就可以。如果圖形處理器沒有給我們建立好一個幀緩衝區,這個方法將返回 0,我們最好檢查一下返回值,如果出現返回值為 0 的情況,我們開啟 OK LED 燈。

teq r0,#0bne noError$mov r0,#16mov r1,#1bl SetGpioFunctionmov r0,#16mov r1,#0bl SetGpioerror$:b error$noError$:fbInfoAddr .req r4mov fbInfoAddr,r0

現在,我們已經有了幀緩衝資訊的地址,我們需要取得幀緩衝資訊的指標,並開始繪製螢幕。我們使用兩個迴圈來做實現,一個走行,一個走列。事實上,樹莓派中的大多數應用程式中,圖片都是以從左到右然後從上到下的順序來儲存的,因此我們也按這個順序來寫迴圈。

render$:    fbAddr .req r3    ldr fbAddr,[fbInfoAddr,#32]        colour .req r0    y .req r1    mov y,#768    drawRow$:            x .req r2        mov x,#1024        drawPixel$:                    strh colour,[fbAddr]            add fbAddr,#2            sub x,#1            teq x,#0            bne drawPixel$                sub y,#1        add colour,#1        teq y,#0        bne drawRow$        b render$.unreq fbAddr.unreq fbInfoAddr

strh reg,[dest] 將暫存器中的低位半個字儲存到給定的 dest 地址上。

這是一個很長的程式碼塊,它巢狀了三層迴圈。為了幫你理清頭緒,我們將迴圈進行縮排處理,這就有點類似於高階程式語言,而組合器會忽略掉這些用於縮排的 tab 字元。我們看到,在這裡它從幀緩衝資訊結構中載入了幀緩衝的地址,然後基於每行來迴圈,接著是每行上的每個畫素。在每個畫素上,我們使用一個 strh(儲存半個字)命令去儲存當前顏色,然後增加地址繼續寫入。每行繪製完成後,我們增加繪製的顏色號。在整個螢幕繪製完成後,我們跳轉到開始位置。

6、看到曙光

現在,你已經準備好在樹莓派上測試這些程式碼了。你應該會看到一個漸變圖案。注意:在第一個訊息被傳送到郵箱之前,樹莓派在它的四個角上一直顯示一個漸變圖案。如果它不能正常工作,請檢視我們的排錯頁面。

如果一切正常,恭喜你!你現在可以控制螢幕了!你可以隨意修改這些程式碼去繪製你想到的任意圖案。你還可以做更精彩的漸變圖案,可以直接計算每個畫素值,因為每個畫素包含了一個 Y 坐標和 X 坐標。在下一個 課程 7:Screen 02 中,我們將學習一個更常用的繪製任務:行。