07-對part5-framebuffer的搬運和翻譯
非黑色字體均為我自己添加,原文見文章末尾
使用屏幕工作
????????就像串口那樣令人興奮,我們終于要將"Hello world"搬上屏幕!現(xiàn)在我們是MMIO的專家了,現(xiàn)在我們將要處理mailbox了.(這里的mailbox雖然字面意思是"郵箱",但是在之前翻譯的文章里推薦的arm的手冊中,mailbox應(yīng)該是指arm的多核通信的方法).這是我們與VideoCore多媒體處理器的通信方式.我們向它發(fā)送信息,它給出回應(yīng).把它當(dāng)成email就行.
????????讓我們創(chuàng)建 mb.c?
#include "io.h"
// The buffer must be 16-byte aligned as only the upper 28 bits of the address can be passed via the mailbox
volatile unsigned int __attribute__((aligned(16))) mbox[36];
enum {
? ? VIDEOCORE_MBOX = (PERIPHERAL_BASE + 0x0000B880),
? ? MBOX_READ? ? ? = (VIDEOCORE_MBOX + 0x0),
? ? MBOX_POLL? ? ? = (VIDEOCORE_MBOX + 0x10),
? ? MBOX_SENDER? ? = (VIDEOCORE_MBOX + 0x14),
? ? MBOX_STATUS? ? = (VIDEOCORE_MBOX + 0x18),
? ? MBOX_CONFIG? ? = (VIDEOCORE_MBOX + 0x1C),
? ? MBOX_WRITE? ? ?= (VIDEOCORE_MBOX + 0x20),
? ? MBOX_RESPONSE? = 0x80000000,
? ? MBOX_FULL? ? ? = 0x80000000,
? ? MBOX_EMPTY? ? ?= 0x40000000
};
unsigned int mbox_call(unsigned char ch)
{
? ? // 28-bit address (MSB) and 4-bit value (LSB)
? ? unsigned int r = ((unsigned int)((long) &mbox) &~ 0xF) | (ch & 0xF);
? ? // Wait until we can write
? ? while (mmio_read(MBOX_STATUS) & MBOX_FULL);
? ??
? ? // Write the address of our buffer to the mailbox with the channel appended
? ? mmio_write(MBOX_WRITE, r);
? ? while (1) {
? ? ? ? // Is there a reply?
? ? ? ? while (mmio_read(MBOX_STATUS) & MBOX_EMPTY);
? ? ? ? // Is it a reply to our message?
? ? ? ? if (r == mmio_read(MBOX_READ)) return mbox[1]==MBOX_RESPONSE; // Is it successful?
? ? ? ? ? ?
? ? }
? ? return 0;
}
????????首先我們包含 io.h ,因為我們需要 PERIPHERAL_BASE 的定義,并且需要 io.c 提供的mmio_read和mmio_write 函數(shù).我們之前的MMIO的經(jīng)驗在這里很有用,因為mailbox的收發(fā),請求/相應(yīng)都是用同樣的技術(shù)達(dá)成的.我們將處理在 PERIPHERAL_BASE 中不同的偏移量,就像你在代碼中看到的那樣.
????????重要的是,我們的mailbox的緩沖區(qū)(信息存儲的地方)需要在內(nèi)存中正確對齊.這是一個我們需要嚴(yán)格要求編譯器而不是讓其自行處理的例子.通過確認(rèn)緩沖區(qū)是16字節(jié)對齊的,我們知道它的內(nèi)存地址是16的倍數(shù),即低4位置零.這很好,因為只有高28位可以被用作地址,剩下的低4位被用來指定通道.
? ? ? ?我推薦閱讀我之前分享的mailbox屬性接口(https://github.com/raspberrypi/firmware/wiki/Mailbox-property-interface)??
????????一個幀緩沖是一個包含驅(qū)動視頻播放的位圖的一塊內(nèi)存區(qū)域.換句話說,我們可以通過向特定的內(nèi)存中寫入數(shù)據(jù)來操縱屏幕上的像素點.我們首先需要了解這個內(nèi)存是如何組織的.
????????在這個例子中,我們向VideoCore要求:
一塊1920*1080(1080p)的緩沖區(qū)
每個像素32bit深度,以RGB方式排列
????????所以我們的像素由 8bit 紅色 8bit 綠色 8bit 藍(lán)色和 8bit Alpha 通道(表示透明/不透明)組成.我們要求像素在內(nèi)存中以紅綠藍(lán)的順序依次排列.實際上Alpha通道總是在最前面,所以實際上是ARGB.
????????我們使用通道8發(fā)出這些信息(MBOX_CH_PROP),并且檢查VideoCore發(fā)回的東西是否是我們要求的.它還應(yīng)該告訴我們幀緩沖區(qū)組織結(jié)構(gòu)中缺失的部分 -- 每行或者每間距字節(jié)數(shù).
????????如果所有東西都向期望的那樣返回了,我們就可以向屏幕中寫入了.
畫一個像素
????????為了保存我們對RGB的組合的記憶,讓我們設(shè)置一個簡單的16色調(diào)色盤.有人記得舊的 EGA/VGA調(diào)色盤(https://en.wikipedia.org/wiki/Enhanced_Graphics_Adapter)嗎?如果你看一下 terminal.h,你將會看到vgapa1數(shù)組設(shè)置了同樣的調(diào)色盤,其中黑色為第0個,白色為第15個,中間由許多陰影.
????????我們的 drawPixel例程可以接受(x,y)坐標(biāo)和顏色.我們可以一次用一個8bit無符號數(shù)來代表兩個調(diào)色盤的位置索引.高四位代表背景顏色,第四位代表前景顏色.你應(yīng)該明白為什么一個僅僅16色的調(diào)色盤是多么有用了.
void drawPixel(int x, int y, unsigned char attr)
{
? ? int offs = (y * pitch) + (x * 4);
? ? *((unsigned int*)(fb + offs)) = vgapal[attr & 0x0f];
}
????????我們首先以字節(jié)為單位計算幀緩沖中的偏移量.(y*pitch)得到坐標(biāo)(0,y) - pitch是每行的字節(jié)數(shù).然后我們加上(x*4)得到坐標(biāo)(x,y) -- 這里每個像素(ARGB)有4字節(jié)(或32位).然后我們就能在幀緩沖區(qū)中設(shè)置前景的顏色(這里我們不需要設(shè)置背景顏色)
畫線,矩形和圓
????????現(xiàn)在檢驗和理解我們的 drawRect , drawLine 和 drawCircle 例程.當(dāng)多邊形被填充時,我們使用背景色作為填充,使用前景色作為輪廓.我推薦閱讀 Bresenham (https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm)?來繪制圖形原語.我的線和圓的算法就是來自于他.它們被設(shè)計為只是用簡單的數(shù)學(xué).他的內(nèi)容讀起來很有趣并且他的算法到今天仍然很重要.(我也不太懂計算機圖形學(xué),感興趣的讀者可以區(qū)看看大名鼎鼎的gemes101)
向屏幕上寫數(shù)字
????????我告訴過你裸機編程上沒有什么是免費的,對嗎?好吧,如果我們要在屏幕上顯示信息,我們需要一種字體.所以,就像我們建立我們的調(diào)色盤,我們需要建立一種字體供我們的代碼使用.幸運的是,字體知識位圖的數(shù)組 -- 用0和1來描述圖片.我們將要定義8*8的字體,就像MS-DOS做到那樣.
想象一個8*8的A:
0 0 0 0 1 1 0 0 = 0x0C
0 0 0 1 1 1 1 0 = 0x1E
0 0 1 1 0 0 1 1 = 0x33
0 0 1 1 0 0 1 1 = 0x33
0 0 1 1 1 1 1 1 = 0x3F
0 0 1 1 0 0 1 1 = 0x33
0 0 1 1 0 0 1 1 = 0x33
0 0 0 0 0 0 0 0 = 0x00
????????這個位圖可以用8字節(jié)來表示(等號后面是16進(jìn)制數(shù)).當(dāng)你看 terminal.h的時候,你將會看到我們實現(xiàn)了code page 437?(https://en.wikipedia.org/wiki/Code_page_437) 中的許多有用的字符.
????????drawChar現(xiàn)在應(yīng)該是不言自明的了.
我們設(shè)置了一個指向位圖中我們想寫的字符的指針 glyph
我們遍歷位圖數(shù)組,從第一行開始,然后是第二行,然后是第三行.
對于每一行中的像素,我們決定它是否應(yīng)該設(shè)置為背景顏色(對應(yīng)的glyph的位為0)或者前景顏色(該位為1)
我們在正確的坐標(biāo)下寫入像素
????????drawString不出意外的調(diào)用了drawChar來打印整個字符串.
更新我們的內(nèi)核使其更加具有藝術(shù)氣息
????????最后,我們可以在屏幕上創(chuàng)造我們的藝術(shù)作品了!我們更新后的kernel.c練習(xí)了所有的圖形例程來畫了下面這張圖.
????????編譯內(nèi)核,拷貝到你的SD卡中.你可能需要再次更新你的 config.txt . 如果你之前設(shè)置了 hdmi_safe 參數(shù)使樹莓派官方操作系統(tǒng)運行,你現(xiàn)在不需要它了.然而,你可能需要去設(shè)置hdmi_mode 和 hdmi_group 來保證我們進(jìn)入1080p模式.
????????是時候?qū)漭傻姆直媛试O(shè)置(https://pimylifeup.com/raspberry-pi-screen-resolution/)進(jìn)行了解了.因為我是用普通的TV,我的 config.txt 目前有三行(包括我們?yōu)榱薝ART添加的那行).
core_freq_min=500
hdmi_group=1
hdmi_mode=16
????????現(xiàn)在啟動樹莓派!
????????我們已經(jīng)在屏幕上做了比“Hello world!”更多的事情了!坐下來,放松,享受你的藝術(shù)作品。在下一個教程中,我們將結(jié)合圖形與鍵盤輸入從UART創(chuàng)建我們的第一個游戲.


Working with the screen
As exciting as the UART is, in this tutorial we're finally going to get "Hello world!" up on the screen! Now that we're experts in MMIO, we're ready to tackle **mailboxes**. This is how we communicate with the VideoCore multimedia processor. We can send it messages, and it can reply. Think of it just like email.
Let's create _mb.c_:
```c
#include "io.h"
// The buffer must be 16-byte aligned as only the upper 28 bits of the address can be passed via the mailbox
volatile unsigned int __attribute__((aligned(16))) mbox[36];
enum {
? ? VIDEOCORE_MBOX = (PERIPHERAL_BASE + 0x0000B880),
? ? MBOX_READ? ? ? = (VIDEOCORE_MBOX + 0x0),
? ? MBOX_POLL? ? ? = (VIDEOCORE_MBOX + 0x10),
? ? MBOX_SENDER? ? = (VIDEOCORE_MBOX + 0x14),
? ? MBOX_STATUS? ? = (VIDEOCORE_MBOX + 0x18),
? ? MBOX_CONFIG? ? = (VIDEOCORE_MBOX + 0x1C),
? ? MBOX_WRITE? ? ?= (VIDEOCORE_MBOX + 0x20),
? ? MBOX_RESPONSE? = 0x80000000,
? ? MBOX_FULL? ? ? = 0x80000000,
? ? MBOX_EMPTY? ? ?= 0x40000000
};
unsigned int mbox_call(unsigned char ch)
{
? ? // 28-bit address (MSB) and 4-bit value (LSB)
? ? unsigned int r = ((unsigned int)((long) &mbox) &~ 0xF) | (ch & 0xF);
? ? // Wait until we can write
? ? while (mmio_read(MBOX_STATUS) & MBOX_FULL);
? ??
? ? // Write the address of our buffer to the mailbox with the channel appended
? ? mmio_write(MBOX_WRITE, r);
? ? while (1) {
? ? ? ? // Is there a reply?
? ? ? ? while (mmio_read(MBOX_STATUS) & MBOX_EMPTY);
? ? ? ? // Is it a reply to our message?
? ? ? ? if (r == mmio_read(MBOX_READ)) return mbox[1]==MBOX_RESPONSE; // Is it successful?
? ? ? ? ? ?
? ? }
? ? return 0;
}
```
First we include _io.h_ as we need access to the `PERIPHERAL_BASE` definition and also to make use of the `mmio_read` and `mmio_write` functions that _io.c_ provides. Our previous MMIO experience is useful here, as sending/receiving mailbox request/responses is achieved using the same technique. We'll just be addressing different offsets from `PERIPHERAL_BASE`, as you see in the code.
Importantly, our mailbox buffer (where messages will be stored) needs to be correctly aligned in memory. This is one example where we need to be strict with the compiler instead of letting it do its thing! By ensuring the buffer is "16-byte aligned", we know that its memory address is a multiple of 16 i.e. the 4 least significant bits are not set. That's good, because only the 28 most significant bits can be used as the address, leaving the 4 least significant bits to specify the **channel**.
I recommend reading up on the [mailbox property interface](https://github.com/raspberrypi/firmware/wiki/Mailbox-property-interface). You'll see that channel 8 is reserved for messages from the ARM for response by the VideoCore, so we'll be using this channel.
`mbox_call` implements all the MMIO we need to send the message (assuming it's been set up in the buffer) and await the reply. The VideoCore will write the reply directly to our original buffer.
The framebuffer
Now take a look at _fb.c_. The `fb_init()` routine makes our very first mailbox call, using some definitions from _mb.h_. Remember the email analogy? Well, since it's possible to ask a person to do more than one thing by email, we can also ask a few things of the VideoCore at once. This message asks for two things:
?* A pointer to the framebuffer start (`MBOX_TAG_GETFB`)
?* The pitch (`MBOX_TAG_GETPITCH`)
You can read more about the message structure on the [mailbox property interface](https://github.com/raspberrypi/firmware/wiki/Mailbox-property-interface) page that I shared before.
A **framebuffer** is simply an area of memory that contains a bitmap which drives a video display. In other words, we can manipulate the pixels on the screen directly by writing to specific memory addresses. We will first need to understand how this memory is organised though.
In this example, we ask the VideoCore for:
?* a simple 1920x1080 (1080p) framebuffer
?* a depth of 32 bits per pixel, with an RGB pixel order
So each pixel is made up of 8-bits for the Red value, 8-bits for Green, 8-bits for Blue and 8-bits for the Alpha channel (representing transparency/opacity). We're asking that the pixels are ordered in memory with the Red byte coming first, then Green, then Blue - RGB. In actual fact, the Alpha byte always comes ahead of all of these, so it's really ARGB.
We then send the message using channel 8 (`MBOX_CH_PROP`) and check that what the VideoCore sends back is what we asked for. It should also tell us the missing piece of the framebuffer organisation puzzle - the number of bytes per line or **pitch**.
If everything comes back as expected, then we're ready to write to the screen!
Drawing a pixel
To save us remembering RGB colour combinations, let's set up a simple 16-colour **palette**. Anyone remember the old [EGA/VGA palette](https://en.wikipedia.org/wiki/Enhanced_Graphics_Adapter)? If you take a look in _terminal.h_, you'll see the `vgapal` array sets up that same palette, with Black as item 0 and White as item 15, and many shades in between!
Our `drawPixel` routine can then take an (x, y) coordinate and a colour. We use an `unsigned char` (8 bits) to represent two palette indexes at once, with the 4 most significant bits representing the background colour and the 4 least significant bits, the foreground colour. You may see why it's helpful to have only a 16-colour palette for now!
```c
void drawPixel(int x, int y, unsigned char attr)
{
? ? int offs = (y * pitch) + (x * 4);
? ? *((unsigned int*)(fb + offs)) = vgapal[attr & 0x0f];
}
```
We first calculate the framebuffer offset in bytes. (y * pitch) gets us to coordinate (0, y) - pitch is the number of bytes per line. We then add (x * 4) to get to (x, y) - there are 4 bytes (or 32 bits!) per pixel (ARGB). We can then set that byte in the framebuffer to our foreground colour (we don't need a background colour here).
Drawing lines, rectangles and circles
Examine and understand our `drawRect`, `drawLine` and `drawCircle` routines now. Where a polygon is filled, we use the background colour for the fill and the foreground colour for the outline.
I recommend reading [Bresenham](https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm) on drawing graphics primitives. My line and circle drawing algorithms come from him, and they're designed to use only simple mathematics. He makes for very interesting reading and his algorithms are still very important today.
Writing characters to the screen
I told you that nothing is for free in bare metal programming, right? Well, if we want to write a message to the screen then we need a font. So, just like we built our palette, we now need to build up a font for our code to use. Luckily, fonts are just arrays of simple **bitmaps** - 1's and 0's used to describe a picture. We're going to define an 8x8 font similar to the one MS-DOS used.
Imagine an 8x8 "A":
```c
0 0 0 0 1 1 0 0 = 0x0C
0 0 0 1 1 1 1 0 = 0x1E
0 0 1 1 0 0 1 1 = 0x33
0 0 1 1 0 0 1 1 = 0x33
0 0 1 1 1 1 1 1 = 0x3F
0 0 1 1 0 0 1 1 = 0x33
0 0 1 1 0 0 1 1 = 0x33
0 0 0 0 0 0 0 0 = 0x00
```
This bitmap can be represented by just 8 bytes (the hexadecimal numbers after the = signs). When you look in _terminal.h_, you'll see that we've done this for many of the useful characters found in [code page 437](https://en.wikipedia.org/wiki/Code_page_437).
`drawChar` should now be fairly self-explanatory.?
?* We set a pointer `glyph` to the bitmap of the character we're looking to draw
?* We iterate over the bitmap array, starting with the first row, then second, then third etc.
?* For each pixel in the row, we determine whether it should be set to the background colour (the corresponding glyph bit is 0) or the foregound colour (the bit is 1)
?* We draw the appropriate pixel at the right coordinates
`drawString` unsurprisingly uses `drawChar` to print a whole string.
Updating our kernel to be more artistic
Finally, we can create a work of art on-screen! Our updated _kernel.c_ exercises all these graphics routines to draw the picture below.
Build the kernel, copy it to your SD card. You may need to update your _config.txt_ once more. If you previously set the `hdmi_safe` parameter to get Raspbian going, you probably won't need it now. You might, however, need to set `hdmi_mode` and `hdmi_group` specifically to ensure we get into 1080p mode.
It's a good time to gain an understanding of the [screen resolution settings](https://pimylifeup.com/raspberry-pi-screen-resolution/) for the RPi4. Because I'm using a regular TV, my _config.txt_ file now contains three lines (including the one we already added for the UART):
```c
core_freq_min=500
hdmi_group=1
hdmi_mode=16
```
Now fire up the RPi4!
We've done so much more than a basic "Hello world!" on-screen already!?Sit back, relax and enjoy your artwork. In the next tutorial, we'll be combining graphics with keyboard input from the UART to create our first game.