歡迎來到螢幕系列課程。在本系列中,你將學習在樹莓派中如何使用彙編程式碼控制螢幕,從顯示隨機資料開始,接著學習顯示一個固定的影象和顯示文字,然後格式化數字為文字。假設你已經完成了 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 次冪的倍數,相比真彩色這將極大地降低了獲取資訊的複雜度。
樹莓派和它的圖形處理器有一種特殊而奇怪的關係。在樹莓派上,首先執行的事實上是圖形處理器,它負責啟動主處理器。這是很不常見的。最終它不會有太大的差別,但在許多互動中,它經常給人感覺主處理器是次要的,而圖形處理器才是主要的。在樹莓派上這兩者之間依靠一個叫 “郵箱” 的東西來通訊。它們中的每一個都可以為對方投放郵件,這個郵件將在未來的某個時刻被對方收集並處理。我們將使用這個郵箱去向圖形處理器請求一個地址。這個地址將是一個我們在螢幕上寫入畫素顏色資訊的位置,我們稱為幀緩衝,圖形卡將定期檢查這個位置,然後更新螢幕上相應的畫素。
儲存幀緩衝給計算機帶來了很大的記憶體負擔。基於這種原因,早期計算機經常作弊,比如,儲存一螢幕文字,在每次單獨掃清時,它只繪製掃清了的字母。
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 |
為了給指定的郵箱傳送一個訊息:
Status
欄位的頭一位為 0。Write
,低 4 位是要傳送到的郵箱,高 28 位是要寫入的訊息。為了讀取一個訊息:
Status
欄位的第 30 位為 0。如果你覺得有信心,你現在已經有足夠的資訊去寫出我們所需的兩個方法。如果沒有信心,請繼續往下看。
與以前一樣,我建議你實現的第一個方法是獲取郵箱區域的地址。
-
.globl GetMailboxBase
-
GetMailboxBase:
-
ldr r0,=0x2000B880
-
mov pc,lr
傳送程式相對簡單一些,因此我們將首先去實現它。隨著你的方法越來越複雜,你需要提前去規劃它們。規劃它們的一個好的方式是寫出一個簡單步驟串列,詳細地列出你需要做的事情,像下麵一樣。
r0
),以及寫到什麼郵箱(r1
)。我們必須驗證郵箱的真實性,以及它的低 4 位的值是否為 0。不要忘了驗證輸入。GetMailboxBase
去檢索地址。Status
欄位。Write
。我們來按順序寫出它們中的每一步。
r0
和 r1
的目的。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 進行比較。
GetMailboxBase
。
-
channel .req r1
-
value .req r2
-
mov value,r0
-
push {lr}
-
bl GetMailboxBase
-
mailbox .req r0
-
wait1$:
-
status .req r3
-
ldr status,[mailbox,#0x18]
-
tst status,#0x80000000
-
.unreq status
-
bne wait1$
-
add value,channel
-
.unreq channel
-
str value,[mailbox,#0x20]
-
.unreq value
-
.unreq mailbox
-
pop {pc}
MailboxRead
的程式碼和它非常類似。
r0
)。我們必須要驗證郵箱的真實性。不要忘了驗證輸入。GetMailboxBase
去檢索地址。Status
欄位。Read
欄位。我們來按順序寫出它們中的每一步。
r0
中的值。
-
.globl MailboxRead
-
MailboxRead:
-
cmp r0,#15
-
movhi pc,lr
GetMailboxBase
。
-
channel .req r1
-
mov channel,r0
-
push {lr}
-
bl GetMailboxBase
-
mailbox .req r0
-
rightmail$:
-
wait2$:
-
status .req r2
-
ldr status,[mailbox,#0x18]
-
tst status,#0x40000000
-
.unreq status
-
bne wait2$
-
mail .req r2
-
ldr mail,[mailbox,#0]
-
inchan .req r3
-
and inchan,mail,#0b1111
-
teq inchan,channel
-
.unreq inchan
-
bne rightmail$
-
.unreq mailbox
-
.unreq channel
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 位元組對齊的。
到目前為止,我們已經有了待傳送的訊息,我們可以寫程式碼去傳送它了。通訊將按如下的步驟進行:
FrameBufferInfo + 0x40000000
的地址到郵箱 1。我在步驟 1 中說了一些以前沒有提到的事情。我們在傳送之前,在幀緩衝地址上加了 0x40000000
。這其實是一個給 GPU 的特殊訊號,它告訴 GPU 應該如何寫到結構上。如果我們只是傳送地址,GPU 將寫到它的回覆上,這樣不能保證我們可以透過掃清快取看到它。快取是處理器使用的值在它們被髮送到儲存之前儲存在記憶體中的片段。透過加上 0x40000000
,我們告訴 GPU 不要將寫入到它的快取中,這樣將確保我們能夠看到變化。
因為在那裡發生很多事情,因此最好將它實現為一個函式,而不是將它以程式碼的方式寫入到 main.s
中。我們將要寫一個函式 InitialiseFrameBuffer
,由它來完成所有協調和傳回指向到上面提到的幀緩衝資料的指標。為方便起見,我們還將幀緩衝的寬度、高度、位深作為這個方法的輸入,這樣就很容易地修改 main.s
而不必知道協調的細節了。
再一次,來寫下我們要做的詳細步驟。如果你有信心,可以略過這一步直接嘗試去寫函式。
frame buffer + 0x40000000
的地址到郵箱。現在,我們開始寫更多的方法。以下是上面其中一個實現。
-
.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
-
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
MailboxWrite
方法的輸入是寫入到暫存器 r0
中的值,並將通道寫入到暫存器 r1
中。
-
mov r0,fbInfoAddr
-
add r0,#0x40000000
-
mov r1,#1
-
bl MailboxWrite
MailboxRead
方法的輸入是寫入到暫存器 r0
中的通道,而輸出是值讀數。
-
mov r0,#1
-
bl MailboxRead
MailboxRead
方法的結果是否為 0,如果不為 0,則傳回 0。
-
teq result,#0
-
movne result,#0
-
popne {pc}
-
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[1] 中,我們將學習一個更常用的繪製任務:行。