Linux中國

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

歡迎來到屏幕系列課程。在本系列中,你將學習在樹莓派中如何使用彙編代碼控制屏幕,從顯示隨機數據開始,接著學習顯示一個固定的圖像和顯示文本,然後格式化數字為文本。假設你已經完成了 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
RGBA32 16,777,216 帶 256 級透明度 每個像素使用 32 位去保存,前八位表示紅色通道,第二個八位表示綠色通道,第三個八位表示藍色通道。只有一個圖像繪製在另一個圖像的上方時才考慮使用透明通道,值為 0 時表示下面圖像的顏色,值為 255 時表示上面這個圖像的顏色,介於這兩個值之間的所有值表示這兩個圖像顏色的混合。

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

在本教程中,我們將從使用高色值開始。這樣你就可以看到圖像的構成,它的形成過程清楚,圖像質量好,又不像真彩色那樣佔用太多的空間。也就是說,顯示一個比較小的 800x600 像素的圖像,它只需要小於 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. 接收者確認消息來自正確的郵箱,否則再次重試。

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

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

.globl GetMailboxBase
GetMailboxBase:
ldr r0,=0x2000B880
mov 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 MailboxWrite
MailboxWrite:
tst r0,#0b1111
movne pc,lr
cmp r1,#15
movhi pc,lr

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

2、這段代碼確保我們不會覆蓋我們的值,或鏈接寄存器,然後調用 GetMailboxBase

channel .req r1
value .req r2
mov value,r0
push {lr}
bl GetMailboxBase
mailbox .req r0

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

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

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

tst status,#0x80000000
.unreq status
bne wait1$

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

add value,channel
.unreq channel

6、這段代碼保存結果到寫入欄位。

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

MailboxRead 的代碼和它非常類似。

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

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

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

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

2、這段代碼確保我們不會覆蓋掉我們的值,或鏈接寄存器,然後調用 GetMailboxBase

channel .req r1
mov channel,r0
push {lr}
bl GetMailboxBase
mailbox .req r0

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

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

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

tst status,#0x40000000
.unreq status
bne wait2$

5、這段代碼從郵箱中讀取下一條消息。

mail .req r2
ldr mail,[mailbox,#0]

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

inchan .req r3
and inchan,mail,#0b1111
teq inchan,channel
.unreq inchan
bne rightmail$
.unreq mailbox
.unreq channel

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

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

4、我心愛的圖形處理器

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

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

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

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

.section .data
.align 4
.globl FrameBufferInfo
FrameBufferInfo:
.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 InitialiseFrameBuffer
InitialiseFrameBuffer:
width .req r0
height .req r1
bitDepth .req r2
cmp width,#4096
cmpls height,#4096
cmpls bitDepth,#32
result .req r0
movhi result,#0
movhi pc,lr

2、這段代碼寫入到我們上面定義的幀緩衝結構中。我也趁機將鏈接寄存器推入到棧上。

fbInfoAddr .req r3
push {lr}
ldr fbInfoAddr,=FrameBufferInfo
str 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,fbInfoAddr
add r0,#0x40000000
mov r1,#1
bl MailboxWrite

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

mov r0,#1
bl MailboxRead

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

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

6、這是代碼結束,並返回幀緩衝信息地址。

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

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

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

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

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

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

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

teq r0,#0
bne noError$

mov r0,#16
mov r1,#1
bl SetGpioFunction
mov r0,#16
mov r1,#0
bl SetGpio

error$:
b error$

noError$:
fbInfoAddr .req r4
mov 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 中,我們將學習一個更常用的繪製任務:行。

via: https://www.cl.cam.ac.uk/projects/raspberrypi/tutorials/os/screen01.html

作者:Alex Chadwick 選題:lujun9972 譯者:qhwdw 校對:wxy

本文由 LCTT 原創編譯,Linux中國 榮譽推出


本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive

對這篇文章感覺如何?

太棒了
0
不錯
0
愛死了
0
不太好
0
感覺很糟
0
雨落清風。心向陽

    You may also like

    Leave a reply

    您的郵箱地址不會被公開。 必填項已用 * 標註

    此站點使用Akismet來減少垃圾評論。了解我們如何處理您的評論數據

    More in:Linux中國