欢迎光临散文网 会员登陆 & 注册

07-对part5-framebuffer的搬运和翻译

2023-08-12 12:09 作者:Xdz-2333  | 我要投稿

非黑色字体均为我自己添加,原文见文章末尾

使用屏幕工作

        就像串口那样令人兴奋,我们终于要将"Hello world"搬上屏幕!现在我们是MMIO的专家了,现在我们将要处理mailbox了.(这里的mailbox虽然字面意思是"邮箱",但是在之前翻译的文章里推荐的arm的手册中,mailbox应该是指arm的多核通信的方法).这是我们与VideoCore多媒体处理器的通信方式.我们向它发送信息,它给出回应.把它当成email就行.

        让我们创建 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 函数.我们之前的MMIO的经验在这里很有用,因为mailbox的收发,请求/相应都是用同样的技术达成的.我们将处理在 PERIPHERAL_BASE 中不同的偏移量,就像你在代码中看到的那样.

        重要的是,我们的mailbox的缓冲区(信息存储的地方)需要在内存中正确对齐.这是一个我们需要严格要求编译器而不是让其自行处理的例子.通过确认缓冲区是16字节对齐的,我们知道它的内存地址是16的倍数,即低4位置零.这很好,因为只有高28位可以被用作地址,剩下的低4位被用来指定通道.

       我推荐阅读我之前分享的mailbox属性接口(https://github.com/raspberrypi/firmware/wiki/Mailbox-property-interface)  

        一个帧缓冲是一个包含驱动视频播放的位图的一块内存区域.换句话说,我们可以通过向特定的内存中写入数据来操纵屏幕上的像素点.我们首先需要了解这个内存是如何组织的.

        在这个例子中,我们向VideoCore要求:

  1. 一块1920*1080(1080p)的缓冲区

  2. 每个像素32bit深度,以RGB方式排列

        所以我们的像素由 8bit 红色 8bit 绿色 8bit 蓝色和 8bit Alpha 通道(表示透明/不透明)组成.我们要求像素在内存中以红绿蓝的顺序依次排列.实际上Alpha通道总是在最前面,所以实际上是ARGB.

        我们使用通道8发出这些信息(MBOX_CH_PROP),并且检查VideoCore发回的东西是否是我们要求的.它还应该告诉我们帧缓冲区组织结构中缺失的部分 -- 每行或者每间距字节数.

        如果所有东西都向期望的那样返回了,我们就可以向屏幕中写入了.

画一个像素

        为了保存我们对RGB的组合的记忆,让我们设置一个简单的16色调色盘.有人记得旧的 EGA/VGA调色盘(https://en.wikipedia.org/wiki/Enhanced_Graphics_Adapter)吗?如果你看一下 terminal.h,你将会看到vgapa1数组设置了同样的调色盘,其中黑色为第0个,白色为第15个,中间由许多阴影.

        我们的 drawPixel例程可以接受(x,y)坐标和颜色.我们可以一次用一个8bit无符号数来代表两个调色盘的位置索引.高四位代表背景颜色,第四位代表前景颜色.你应该明白为什么一个仅仅16色的调色盘是多么有用了.

void drawPixel(int x, int y, unsigned char attr)

{

    int offs = (y * pitch) + (x * 4);

    *((unsigned int*)(fb + offs)) = vgapal[attr & 0x0f];

}

        我们首先以字节为单位计算帧缓冲中的偏移量.(y*pitch)得到坐标(0,y) - pitch是每行的字节数.然后我们加上(x*4)得到坐标(x,y) -- 这里每个像素(ARGB)有4字节(或32位).然后我们就能在帧缓冲区中设置前景的颜色(这里我们不需要设置背景颜色)

画线,矩形和圆

        现在检验和理解我们的 drawRect , drawLine 和 drawCircle 例程.当多边形被填充时,我们使用背景色作为填充,使用前景色作为轮廓.我推荐阅读 Bresenham (https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm) 来绘制图形原语.我的线和圆的算法就是来自于他.它们被设计为只是用简单的数学.他的内容读起来很有趣并且他的算法到今天仍然很重要.(我也不太懂计算机图形学,感兴趣的读者可以区看看大名鼎鼎的gemes101)

向屏幕上写数字

        我告诉过你裸机编程上没有什么是免费的,对吗?好吧,如果我们要在屏幕上显示信息,我们需要一种字体.所以,就像我们建立我们的调色盘,我们需要建立一种字体供我们的代码使用.幸运的是,字体知识位图的数组 -- 用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字节来表示(等号后面是16进制数).当你看 terminal.h的时候,你将会看到我们实现了code page 437 (https://en.wikipedia.org/wiki/Code_page_437) 中的许多有用的字符.

        drawChar现在应该是不言自明的了.

  • 我们设置了一个指向位图中我们想写的字符的指针 glyph

  • 我们遍历位图数组,从第一行开始,然后是第二行,然后是第三行.

  • 对于每一行中的像素,我们决定它是否应该设置为背景颜色(对应的glyph的位为0)或者前景颜色(该位为1)

  • 我们在正确的坐标下写入像素

        drawString不出意外的调用了drawChar来打印整个字符串.

更新我们的内核使其更加具有艺术气息

        最后,我们可以在屏幕上创造我们的艺术作品了!我们更新后的kernel.c练习了所有的图形例程来画了下面这张图.

        编译内核,拷贝到你的SD卡中.你可能需要再次更新你的 config.txt . 如果你之前设置了 hdmi_safe 参数使树莓派官方操作系统运行,你现在不需要它了.然而,你可能需要去设置hdmi_mode 和 hdmi_group 来保证我们进入1080p模式.

        是时候对树莓派的分辨率设置(https://pimylifeup.com/raspberry-pi-screen-resolution/)进行了解了.因为我是用普通的TV,我的 config.txt 目前有三行(包括我们为了UART添加的那行).

core_freq_min=500

hdmi_group=1

hdmi_mode=16

        现在启动树莓派!

        我们已经在屏幕上做了比“Hello world!”更多的事情了!坐下来,放松,享受你的艺术作品。在下一个教程中,我们将结合图形与键盘输入从UART创建我们的第一个游戏.

作者的屏幕显示的效果

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.


07-对part5-framebuffer的搬运和翻译的评论 (共 条)

分享到微博请遵守国家法律