歡迎光臨
每天分享高質量文章

計算機實驗室之樹莓派:課程 6 屏幕01 | Linux 中國

在本系列中,你將學習在樹莓派中如何使用彙編代碼控制屏幕,從顯示隨機資料開始,接著學習顯示一個固定的圖像和顯示文本,然後格式化數字為文本。

— Alex Chadwick

 

歡迎來到屏幕系列課程。在本系列中,你將學習在樹莓派中如何使用彙編代碼控制屏幕,從顯示隨機資料開始,接著學習顯示一個固定的圖像和顯示文本,然後格式化數字為文本。假設你已經完成了 OK 系列課程的學習,所以在本系列中出現的有些知識將不再重覆。

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

1、入門

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

2、計算機圖形

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

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

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

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

< 如顯示不全,請左右滑動 >
名字 唯一顏色數量 描述 示例
單色 2 每個像素使用 1 位去儲存,其中 1 表示白色,0 表示黑色。
灰度 256 每個像素使用 1 個位元組去儲存,使用 255 表示白色,0 表示黑色,介於這兩個值之間的所有值表示這兩個顏色的一個線性組合。
8 色 8 每個像素使用 3 位去儲存,第一位表示紅色通道,第二位表示綠色通道,第三位表示藍色通道。
低色值 256 每個像素使用 8 位去儲存,前三位表示紅色通道的強度,接下來的三位表示綠色通道的強度,最後兩位表示藍色通道的強度。
高色值 65,536 每個像素使用 16 位去儲存,前五位表示紅色通道的強度,接下來的六位表示綠色通道的強度,最後的五位表示藍色通道的強度。
真彩色 16,777,216 每個像素使用 24 位去儲存,前八位表示紅色通道,第二個八位表示綠色通道,最後八位表示藍色通道。
RGBA32 16,777,216 帶 256 級透明度 每個像素使用 32 位去儲存,前八位表示紅色通道,第二個八位表示綠色通道,第三個八位表示藍色通道。只有一個圖像繪製在另一個圖像的上方時才考慮使用透明通道,值為 0 時表示下麵圖像的顏色,值為 255 時表示上面這個圖像的顏色,介於這兩個值之間的所有值表示這兩個圖像顏色的混合。

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

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

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

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

3、編寫郵差程式

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

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

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

表 3.1 郵箱地址

< 如顯示不全,請左右滑動 >
地址 大小 / 位元組 名字 描述 讀 / 寫
2000B880 4 Read 接收郵件 R
2000B890 4 Poll 不檢索接收 R
2000B894 4 Sender 發送者信息 R
2000B898 4 Status 信息 R
2000B89C 4 Configuration 設置 RW
2000B8A0 4 Write 發送郵件 W

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

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

為了讀取一個訊息:

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

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

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

  1. .globl GetMailboxBase
  2. GetMailboxBase:
  3. ldr r0,=0x2000B880
  4. mov pc,lr

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

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

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

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

  1. .globl MailboxWrite
  2. MailboxWrite:
  3. tst r0,#0b1111
  4. movne pc,lr
  5. cmp r1,#15
  6. movhi pc,lr

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

2. 這段代碼確保我們不會改寫我們的值,或鏈接暫存器,然後呼叫 GetMailboxBase

  1. channel .req r1
  2. value .req r2
  3. mov value,r0
  4. push {lr}
  5. bl GetMailboxBase
  6. mailbox .req r0
3. 這段代碼加載當前狀態。

  1. wait1$:
  2. status .req r3
  3. ldr status,[mailbox,#0x18]
4. 這段代碼檢查狀態欄位的頭一位是否為 0,如果不為 0,迴圈回到第 3 步。

  1. tst status,#0x80000000
  2. .unreq status
  3. bne wait1$
5. 這段代碼將通道和值組合到一起。

  1. add value,channel
  2. .unreq channel
6. 這段代碼儲存結果到寫入欄位。

  1. str value,[mailbox,#0x20]
  2. .unreq value
  3. .unreq mailbox
  4. pop {pc}

MailboxRead 的代碼和它非常類似。

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

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

1. 這一段代碼來驗證 r0 中的值。

  1. .globl MailboxRead
  2. MailboxRead:
  3. cmp r0,#15
  4. movhi pc,lr
2. 這段代碼確保我們不會改寫掉我們的值,或鏈接暫存器,然後呼叫 GetMailboxBase

  1. channel .req r1
  2. mov channel,r0
  3. push {lr}
  4. bl GetMailboxBase
  5. mailbox .req r0
3. 這段代碼加載當前狀態。

  1. rightmail$:
  2. wait2$:
  3. status .req r2
  4. ldr status,[mailbox,#0x18]
4. 這段代碼檢查狀態欄位第 30 位是否為 0,如果不為 0,傳回到第 3 步。

  1. tst status,#0x40000000
  2. .unreq status
  3. bne wait2$
5. 這段代碼從郵箱中讀取下一條訊息。

  1. mail .req r2
  2. ldr mail,[mailbox,#0]
6. 這段代碼檢查我們正在讀取的郵箱通道是否為提供給我們的通道。如果不是,傳回到第 3 步。

  1. inchan .req r3
  2. and inchan,mail,#0b1111
  3. teq inchan,channel
  4. .unreq inchan
  5. bne rightmail$
  6. .unreq mailbox
  7. .unreq channel
7. 這段代碼將答案(郵件的前 28 位)移動到暫存器 r0 中。

  1. and r0,mail,#0xfffffff0
  2. .unreq mail
  3. pop {pc}

4、我心愛的圖形處理器

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

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

由於在樹莓派的記憶體是在圖形處理器和主處理器之間共享的,我們能夠只發送可以找到我們信息的位置即可。這就是 DMA,許多複雜的設備使用這種技術去加速訪問時間。

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

  1. .section .data
  2. .align 4
  3. .globl FrameBufferInfo
  4. FrameBufferInfo:
  5. .int 1024 /* #0 物理寬度 */
  6. .int 768 /* #4 物理高度 */
  7. .int 1024 /* #8 虛擬寬度 */
  8. .int 768 /* #12 虛擬高度 */
  9. .int 0 /* #16 GPU - 間距 */
  10. .int 16 /* #20 位深 */
  11. .int 0 /* #24 X */
  12. .int 0 /* #28 Y */
  13. .int 0 /* #32 GPU - 指標 */
  14. .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。這裡再次使用了條件運行的技巧。相信自己這是可行的。

  1. .section .text
  2. .globl InitialiseFrameBuffer
  3. InitialiseFrameBuffer:
  4. width .req r0
  5. height .req r1
  6. bitDepth .req r2
  7. cmp width,#4096
  8. cmpls height,#4096
  9. cmpls bitDepth,#32
  10. result .req r0
  11. movhi result,#0
  12. movhi pc,lr
2. 這段代碼寫入到我們上面定義的幀緩衝結構中。我也趁機將鏈接暫存器推入到棧上。

  1. fbInfoAddr .req r3
  2. push {lr}
  3. ldr fbInfoAddr,=FrameBufferInfo
  4. str width,[fbInfoAddr,#0]
  5. str height,[fbInfoAddr,#4]
  6. str width,[fbInfoAddr,#8]
  7. str height,[fbInfoAddr,#12]
  8. str bitDepth,[fbInfoAddr,#20]
  9. .unreq width
  10. .unreq height
  11. .unreq bitDepth
3. MailboxWrite 方法的輸入是寫入到暫存器 r0 中的值,並將通道寫入到暫存器 r1中。

  1. mov r0,fbInfoAddr
  2. add r0,#0x40000000
  3. mov r1,#1
  4. bl MailboxWrite
4. MailboxRead 方法的輸入是寫入到暫存器 r0 中的通道,而輸出是值讀數。

  1. mov r0,#1
  2. bl MailboxRead
5. 這段代碼檢查 MailboxRead 方法的結果是否為 0,如果不為 0,則傳回 0。

  1. teq result,#0
  2. movne result,#0
  3. popne {pc}
6. 這是代碼結束,並傳回幀緩衝信息地址。

  1. mov result,fbInfoAddr
  2. pop {pc}
  3. .unreq result
  4. .unreq fbInfoAddr

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

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

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

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

  1. mov r0,#1024
  2. mov r1,#768
  3. mov r2,#16
  4. bl InitialiseFrameBuffer

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

  1. teq r0,#0
  2. bne noError$
  3. mov r0,#16
  4. mov r1,#1
  5. bl SetGpioFunction
  6. mov r0,#16
  7. mov r1,#0
  8. bl SetGpio
  9. error$:
  10. b error$
  11. noError$:
  12. fbInfoAddr .req r4
  13. mov fbInfoAddr,r0

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

  1. render$:
  2.    fbAddr .req r3
  3.    ldr fbAddr,[fbInfoAddr,#32]
  4.    
  5.    colour .req r0
  6.    y .req r1
  7.    mov y,#768
  8.    drawRow$:
  9.    
  10.        x .req r2
  11.        mov x,#1024
  12.        drawPixel$:
  13.        
  14.            strh colour,[fbAddr]
  15.            add fbAddr,#2
  16.            sub x,#1
  17.            teq x,#0
  18.            bne drawPixel$
  19.        
  20.        sub y,#1
  21.        add colour,#1
  22.        teq y,#0
  23.        bne drawRow$
  24.    
  25.    b render$
  26. .unreq fbAddr
  27. .unreq fbInfoAddr

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

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

6、看到曙光

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

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

 

赞(0)

分享創造快樂