为树莓派4编写裸机操作系统--part5

本文最后更新于:2022年3月23日 晚上

本文档所有内容均翻译自 https://github.com/isometimes/rpi4-osdev,作为自己学习树莓派及操作系统的记录。由于本人英文水平有限,如果出现翻译错误请指出,本人会及时改正。如果出现无法理解的内容,请参考原文学习。


为树莓派编写操作系统 - framebuffer

使用显示屏

与使用 UART 一样令人兴奋,在本教程中,我们将最终在屏幕上获得 “Hello World!”! 现在我们是使用 MMIO 的专家,我们已经准备好使用邮箱技术了。这就是我们与 VideoCore 多媒体处理器进行通信的方式、我们可以给它发送消息,它也可以给我们回复。只需要把它想象成电子邮件就可以。

让我们创建 mb.c 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include "io.h"

// 缓冲区是16字节对齐的,因此只有缓冲区地址中的高28字节可以传递给 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 box_call(unsigned char ch)
{
// 高28位是缓冲区的地址,第4位是mailbox的通道号
unsigned int r = ((unsigned int)((long) &mbox) &~ 0xF) | (ch & 0xF);

// 等待直到mailbox可写
while (mmio_read(MBOX_STATUS) & MBOX_FULL);

// 将缓冲区写入通道号对应的mailbox中
mmio_write(MBOX_WRITE, r);

while (1) {
// mailbox是否为空
while (mmio_read(MBOX_STATUS) & MBOX_EMPTY);

// 是否有VC回复的消息
if (r == mmio_read(MBOX_READ))
return mbox[1] == MBOX_RESPONSE; // 检查是否回复成功
}
return 0;
}

上段代码中,我们首先包含 io.h 头文件,因为我们需要使用 PERIPHRAL_BASE 的定义并使用 io.c 提供的 mmio_read 和 mmio_write 函数。我们之前使用 MMIO 的经验在这里很有用,因为发送邮箱请求和接收邮箱响应是使用相同的技术实现的。正如你在代码中看到的那样,我们只需要使用 PERIPHERAL_BASE 的不同偏移量。

重要的是,我们的邮箱缓冲区(将要存储消息的地方)需要在内存中正确对齐。这是一个需要我们严格定义编译器行为而不是使用它的默认行为的一个例子。通过确保缓冲区按照“16 字节对齐”,我们知道缓冲区的地址是 16 的倍数,即地址的最低 4 位为 0。这很好,因为只有最高 28 位有效位可以用作地址,剩下的最低 4 位有效位用来指定邮箱通道。

我建议你阅读有关邮箱属性的网页资料 mailbox property interface。你会看到通道 8 保留用于 ARM 向 VideoCore发送消息并获得响应消息。因此我们将使用该通道。

mbox_call 函数实现了我们发送消息(假设它已在缓冲区中设置)所需的所有MMIO 并等待回复。VideoCore 直接将回复写入我们的原始缓冲区中。

framebuffer

现在看看 fb.c 文件。fb.init() 函数使用 mb.h 中的一些定义进行我们的第一个邮箱调用。还记得电子邮件的类比吗?好吧,既然可以通过电子邮件要求对方做不止一件事,我们也可以一次询问 VideoCore 一批事情。这个消息要求两件事:

  • 指向帧缓冲区开始地址的指针(MBOX_TAG_GETFB)
  • 像素间距(MBOX_TAG_GETPITCH)

你可以在我之前分享的邮箱属性网页 mailbox property interface 中阅读更多关于消息结构的信息。

帧缓冲区只是一个内存区域,其中包含驱动视频显示的位图。换句话说,我们可以通过向特定地址写入位图来直接操纵屏幕上的像素显示。不过,我们首先需要了解该内存是如何组织的。

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

  • 一个简单的 1920 * 1080(1080p) 帧缓冲区
  • 每个像素 32位的深度,采用 RGB 像素顺序

所以每个像素由 8 位红色,8 位绿色,8位蓝色和 8 位 Alpha 通道(标识透明/非透明)值组成。我们要求像素在内存中排序,首先是红色,然后是绿色,然后是蓝色 - RGB。事实上,Alpha值总是在表示 RGB 的字节之前,所以真正的顺序是 ARGB。

然后我们使用通道 8(MBOX_CH_PROP)发送消息,并检查 VideoCore回复的内容是否是我们要求的。它还应该告诉我们帧缓冲区组织难题缺失的部分 – 每行或每个像素间距的字节数。

如果一切都按预期返回,我们就可以向屏幕写入内容了!

绘制一个像素

为了让我们记住 RGB 颜色组合,让我们设置一个简单的 16 色调色板。有人记得旧的 EGA/VGA 调色板吗?如果你查看 terminal.h 头文件,你会看到 vgapal 数组设置了相同的调色板,黑色作为第 0 项目,白色作为第 15 项,以及介于两者之间的其他颜色。

我们的 drawPixel 函数可以获取 (x, y)的坐标和颜色。我们使用一个 unsigned char (8 位)同时表示两个调色板索引,其中高 4 位表示背景色,第 4 位表示前景色。你现在可能明白了为什么调色板只使用 16 颜色。

1
2
3
4
void drawPixel(int x, int y, unsigned char attr) {
int offs = (y * pitch) + (x * 4);
*((unsigned int *)(fb + offs)) = vgapal[attr & 0xF];
}

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

绘制线条、矩形和圆形

现在检查并理解代码中 drawRect, drawLine 和 drawCircle 的函数实现。在填充多边形的地方,我们使用背景色填充,前景色作为轮廓。

我建议阅读有关绘制基本图元的 Bresenham 算法。程序中的画线和画圆算法的基础是该算法。该算法被设计为只是用简单的数学运算。阅读它将会非常有趣,该算法在今天任然非常重要。

在屏幕上显示字符

我告诉过你,在裸机中编程没有什么是免费的,对吧? 好吧,如果我们想要在屏幕上显示一条消息,那么我们需要一个字体。因此,就像我们构建我们的调色板一样,我们现在需要构建一种字体以供我们的代码使用。幸运的是,字体知识简单的位图数组– 使用 1 和 0 表示我们的图片。我们将定义一种类似于 MS-DOS 使用的 8x8 字体。

想象以下 8x8 位图表示的字母 “A”:

1
2
3
4
5
6
7
8
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 个字节(= 符号后面的十六进制数字)表示。当你查看 terminal.h 头文件时,你会发现我们已经对 code page 437 中的许多有用的字符按照这种方法进行了表示。

drawChar 函数的功能现在是不言自明的了:

  • 我们为要绘制的字符的位图设置一个 无符号字符型指针
  • 我们遍历位图数组,从第一行开始,然后是第二行,然后是第三行,以此类推
  • 对于每一行中的每一个像素,我们判断应该将其设置为背景色(对应的 glyph 位为 0)还是前景色(glyph 位为1)
  • 我们在正确的坐标处绘制适当的像素值

drawString 不出所料的使用 drawChar 函数显示整个字符串。

更新我们的内核使其更具艺术性

最后,我们可以在屏幕上创作一件艺术品!我们的更新的 kernel.c 文件使用这些绘图函数绘制下面的图片。

构建内核,将其复制到你的 SD 卡上。你可能需要再次更新 config.txt。如果你之前设置了 hdmi_safe 参数来让 Raspbian 运行,那么你现在可能不再需要它。但是,你可能需要根据你使用的显示器属性专门设置 hdmi_mode 和 hdmi_group,以确保显示器我们进入 1080p 模式。(需要根据自己使用的显示器进行配置)

现在是了解 RPi4 的屏幕分辨率设置的好时机。因为作者使用的是普通电视屏幕,所以在 config.txt 文件中添加如下三行配置(包含我们为使用 UART 添加的那一行配置)。

1
2
3
core_freq_min = 500
hdmi_group = 1
hdmi_mode = 16

现在启动 RPi4!

我们所做的不止在屏幕上显示基本的 “Hello world!”字符。坐下来,放松,享受你创造的艺术品。在下一个教程中,我们将结合图形显示和 UART 键盘输入来创建我们的第一个游戏。

screen


为树莓派4编写裸机操作系统--part5
http://yoursite.com/2022/03/22/为树莓派4编写裸机操作系统-part5/
作者
BinGoo
发布于
2022年3月22日
许可协议