为树莓派3B+实现HDMI驱动(三)字体渲染

即使一直是一个人努力。

本文相比前两个较为简单,目标是在之前的基础上增加字体显示基础设置,主要添加了font.rs文件,并在gpu.rs中给出了接口。

1. 原理

字体的渲染包括两步

  • 生成字体各个字符的bitmap
  • 渲染

每个字符都在一个长方形的像素区域内进行渲染,字符bitmap中的每一位都表示了一个像素点,若为1则为需要渲染,若为0则为不需要渲染。不同字符的高度都相同,然而宽度则不一定,字体根据宽度是否相同可以分为两种:等宽字体和非等宽字体。

这里使用等宽字体。各个字符的bitmap使用全局不可变变量进行存储。

每个字符宽8像素,高8像素。共64像素的区域,需要64位的bitmap表示,即8字节。每个字节表示需要渲染的像素区域的一行。

例如对于ASCII 21的感叹号第一个字节为0x18,即表示感叹号的第一行,二进制表示为00011000,可以发现感叹号 “!” 的竖线宽度为2像素。

2. 字模库

首先定义每个字符字模的数据结构:Bitmap,即一个由8个u8组成的数组,每一个u8用于渲染字模的一行,每个字模共8行。该字模库对外导出根据要渲染的字符ASCII码返回对应字模的接口。

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
pub type Bitmap = [u8; 8];

pub fn get_char_bitmap(ch: u8) -> &'static Bitmap {
    &FONT_BITMAP[ch as usize]
}

pub const CHAR_WIDTH: u32 = 8;
pub const CHAR_HEIGHT: u32 = 8;

const FONT_BITMAP: [Bitmap; 128] = [
    [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],   // U+0000 (nul)
    [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],   // U+0001
    [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],   // U+0002
    [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],   // U+0003
    [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],   // U+0004
    [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],   // U+0005
    [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],   // U+0006
    [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],   // U+0007
    [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],   // U+0008
    [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],   // U+0009
    [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],   // U+000A
    [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],   // U+000B
    [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],   // U+000C
    ...
];

接着借助该字模库导出putc接口,其中需要在FRAMEBUFFER全局结构中维护当前绘制的横向偏移(voffset_x)和纵向偏移(voffset_y)。主要算法逻辑:

  • CHAR_HEIGHTCHAR_WIDTH分别表示每个字模的高(像素为单位)和宽(像素为单位)
  • 首先判断当前绘制的行是否超过屏幕,如果要超过则将屏幕当前内容整体向上偏移一行
  • 若是回车,则重设voffset_x = 0voffset_y += CHAR_HEIGHT
  • 获取要绘制的字模
  • 对每一行的每一列像素进行渲染
  • 设置(voffset_x += CHAR_WIDTH)位置,若宽超过屏幕,则voffset_x = 0voffset_y += CHAR_HEIGHT
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
41
42
43
44
45
46
47
48
49
50
pub fn gpu_putc(byte: u8, color: &str, back_color: &str) {
    let color = get_color_pixel(color);
    let back_color = get_color_pixel(back_color);
    let bm = get_char_bitmap(byte);
    let mut x = FRAMEBUFFER.get_voffset_x();
    let mut y = FRAMEBUFFER.get_voffset_y();

    // move everything up one row
    if y >= HEIGHT {
        for row in CHAR_HEIGHT..HEIGHT {
            for col in 0..WIDTH {
                let p = FRAMEBUFFER.get_pixel(col, row); 
                FRAMEBUFFER.write_pixel(col, row - CHAR_HEIGHT, p);
            }
        }
        for row in HEIGHT - CHAR_HEIGHT..HEIGHT {
            for col in 0..WIDTH {
                FRAMEBUFFER.write_pixel(col, row, back_color);
            }
        }
        y = HEIGHT - CHAR_HEIGHT;
    }

    if byte == b'\n' {
        FRAMEBUFFER.set_voffset_x(0);
        FRAMEBUFFER.set_voffset_y(y + CHAR_HEIGHT);
        return;
    }

    for (i, row) in bm.iter().enumerate() {
        let row = row.reverse_bits();
        for (j, mask) in MASKS.iter().enumerate() {
            let offx = j as u32;
            let offy = i as u32;
            if row & (*mask) > 0 {
                FRAMEBUFFER.write_pixel(x + offx, y + offy, color)
            } else {
                FRAMEBUFFER.write_pixel(x + offx, y + offy, back_color);
            }
        }
    }

    x += CHAR_WIDTH;
    if x >= WIDTH {
        x = 0;
        y += CHAR_HEIGHT;
    }
    FRAMEBUFFER.set_voffset_x(x);
    FRAMEBUFFER.set_voffset_y(y);
}

最后在全局Consoleio::writefmt::write_str方法中把这个接口用上就大功告成。

3. 系统集成

3.1. 何时能够使用println

之前全局Console上的write方法都是对uart的内存映射io地址的写入因此在系统各个子模块初始化前就能使用。然而现在Console除了写uart还要写framebuffer,因此只有在framebuffer初始化结束后才能使用打印功能。而framebuffer依赖于系统的内存分配器分配消息缓存,所以现在mem_allocator初始化的时候也不能使用打印功能。

3.2. 适配虚拟内存系统

ATAGs返回的可用内存不包括系统分配给GPU的部分。这意味着framebuffer不在全局内存管理器的管辖范围内。这意味着下面的好处和坏处:

  • pro: 内存管理器的内存分配不会对framebuffer产生影响。同时在“实模式”下可以直接使用framebuffer地址进行读写,无需其他配置。
  • con: 为了适配虚拟内存系统,就要一些其他的工作。具体而言,在初始化内核页表的时候,需要对GPU管辖内存空间的页进行映射。我这里直接把0x3C000000以上的页都映射为设备地址,CPU侧就不会对其进行cache缓存。

其实分配给GPU的内存可以通过config.txtgpu_mem参数进行配置。默认情况下,1G的rpi分配了76M给GPU。具体见Memory options in config.txt

4. 效果

finally,可以渲染字体了哈哈

IMG_2823.JPG

5. 实现

这一系列大致就结束了,接口和实现参考了很多Printing to a Real Screen。更高级的绘图基础设施比如画曲线,使用double buffer提高性能等等,就到上图形学的课程之后再说吧。。。最近太忙了,心累。。还有其他的proj要肝。偷偷说一句,截至写完本文,backspace我还没处理hhh

6. 参考资料