莫忘少年凌云志,曾许天下第一流。
本文主要介绍我为树莓派3B+硬件平台实现HDMI驱动的动机以及必要的图形学和硬件基础。
1. 动机
最近毕设想最后加一个显示屏作图的功能,最初以为通过gpio接口在LCD屏作图会比实现HDMI驱动然后在HDMI显示屏作图要简单。做了一些技术预研才发现如果要自己写LCD驱动会涉及大量和lcd硬件相关的数电细节(是的,我又想起了EECS151的fpga实验,实验5最终结果就是能够在hdmi显示屏上显示图片,各种vsync、hsync之类的信号,然而我没有示波器也不知道如果出问题要怎么调试,况且lcd显示屏各种式样纷繁,没有我能看得懂的文档,自己太菜。。最近一看到芯片手册里面各种数电、模电符号就头疼)。
但幸运的是,HDMI屏作图可能没有我最初想的那么复杂。原因在于,LCD屏需要CPU通过gpio或是SPI和屏上的外部控制器通信并作图,接口规范要遵循外部控制器的来,不同设备不一样。而HDMI屏是CPU和自身集成的GPU通信,GPU在HDMI屏上渲染作图。CPU和GPU都属于博通BCM2837的本地外设(区别于类似外部定时器、gpio、uart、spi、usb等外设),它们之间的硬件交互由博通公司定义,软件接口由GPU侧的实现者:树莓派公司定义。更重要的是,网络上相关资料相对多一些,甚至有教程。
最终,我决定为树莓派3B+实现HDMI屏的作图。
2. 图形学基础
我还没有上过图形学相关的课程,这里记录一些笔记。
- 为了让计算机能够处理图像数据,需要构建一个计数系统表示颜色和图像。为此,考虑一个三维坐标系。使用x,y,z三个坐标分别表示待渲染原子对象在2维屏幕上的x轴位置、y轴位置和颜色值。这个原子渲染对象被称为像素点(pixel)。每个像素点的值表示要显示的颜色。所以存储图像就是存储了图像每个像素点的值。有了每个像素点的值,显示屏的控制器和GPU/CPU就能根据它们进行渲染。
- 像素点有很多表示格式,不同的格式中每个像素存储占用的字节数大小不同,因此能够表示的颜色的数量也不同。
- 黑白:使用1位存储像素点,0表示黑,1表示白
- greyscale:使用1字节存储像素点,每一个数字表示一种颜色,共能表示256种颜色。
- 8-clour:使用3位表示像素,1位标识一个颜色的信道(分别对应红色、绿色和蓝色)
- RGB24:使用24位表示像素,三个字节,每个字节分别表示红色、绿色和蓝色的强烈程度。
- 等等等
为了显示图像,需要将其对应的数据存放在内存中,这块内存称为framebuffer。
3. 硬件原理1:Framebuffer
由于图像渲染工作需要强大的并行处理能力,现代计算机系统均使用单独的GPU芯片负责图像渲染,树莓派3B+也不例外,它使用的GPU是Video Core IV。在GPU能开始渲染工作前,CPU需要将图像数据装载到一块被称为FrameBuffer的内存空间并通知GPU。GPU随后会定期检查framebuffer并依据其中的值更新屏幕上的图像。这就是GPU渲染的基本原理。
具体而言,FrameBuffer是CPU和GPU共享的一块内存区域。CPU负责将RGB像素点的数值数据写入该buffer。GPU通过从HDMI端口发送framebuffer中的像素点不断刷新屏幕。和framebuffer相关的有下面这些元数据:
- physical size:物理屏幕的宽和高。
- virtual size:内存framebuffer中的宽和高。
- depth: 用于描述每个像素点的位数。
- pitch: 用于描述屏幕上每一行的字节数。
通过depth和pitch两个维度的数据,可以计算得到屏幕每一行的像素数为$pitch / (depth / 8)$。同时像素点(x, y)
在framebuffer中的偏移量为$pitch * y + (depth / 8) * x$。
CPU获取framebuffer的唯一方式是向GPU发起请求,并由GPU分配,因此下面介绍CPU和GPU间的通信机制。
4. 硬件原理2: Mailbox
rpi3上CPU和GPU通过Mailbox通信。Mailbox本质上是一个核间通信协议。为了搞明白这个机制,我花了不少时间,在internet各个角落查找资料。。以下是我的心路例程,不想看可以直接跳到4.1。
最初,为了查找mailbox以及相关寄存器信息,因为树莓派3B+使用的是Quad-A7架构的处理器系统。我重点看了Quad-A7手册。具体在树莓派硬件驱动中也有提到,树莓派的设备地址空间布局如下:
Address | Device(s) |
---|---|
0x0000_0000 .. 0x3FFF_FFFF | GPU access |
0x3E00_0000 .. 0x3FFF_FFFF | GPU peripheral access1 |
0x4000_0000 .. 0xFFFF_FFFF | Local peripherals |
0x4000_0000 .. 0x4001_FFFF | ARM timer, IRQs, mailboxes |
0x4002_0000 .. 0x4002_FFFF | Debug ROM |
0x4003_0000 .. 0x4003_FFFF | DAP |
0x4004_0000 .. 0xFFFF_FFFF | <Unused> |
很好,看起来Mailbox属于CPU的本地外设。
具体而言,根据Quad-A7的文档,一个Mailbox由32位宽的写设置(write-bits-high-to-set)寄存器和写清除(write-bits-high-to-clear)寄存器组成。使用set和clear寄存器的目的在于保证操作的原子性。Quad-A7系统内部共有16个Mailbox,分配给每个核4个。例如,Mailbox0-3分配给核0,Mailbox4-7分配给核1。Mailbox0的基地址是0x4000_0080。此外,每个Mailbox有两个中断路由位。然而不幸的是,我发现无论是官方的uboot,还是各个实现教程里使用的mailbox基地址都是0xB880,令人匪夷所思。
后来,看到rpi forum里的讨论才恍然。Quad-A7文档里给出的mailbox用于4个核之间的通信,所以称为core mailboxes。CPU和GPU间的通信需要另一组mailbox,这一组的定义需要参考rpi wiki。
Mailbox | Read/Write | Peek | Sender | Status | Config |
---|---|---|---|---|---|
0 | 0x00(read) | 0x10 | 0x14 | 0x18 | 0x1c |
1 | 0x20(write) | 0x30 | 0x34 | 0x38 | 0x3c |
但其实rpi wiki中的定义没有明确给出基地址,只是给出相对偏移。后来在rpi3-tutoril中找到了比较详尽的布局:
地址 | 设备 |
---|---|
0x3F003000 | - System Timer |
0x3F00B000 | - Interrupt controller |
0x3F00B880 | - VideoCore mailbox |
0x3F100000 | - Power management |
0x3F104000 | - Random Number Generator |
0x3F200000 | - General Purpose IO controller |
0x3F201000 | - UART0 (serial port, PL011) |
0x3F215000 | - UART1 (serial port, AUX mini UART) |
0x3F300000 | - External Mass Media Controller (SD card reader) |
0x3F980000 | - Universal Serial Bus controller |
ok,确切的地址为0x3F00B880。下面来看看Mailbox通信机制为异构系统提供的调用抽象吧。
4.1. Mailbox调用抽象
根据rpi文档。CPU和GPU通信定义了两个Mailbox,0和1。
- Mailbox 0的状态能够触发ARM的中断,因而mailbox 0用于Videocore到ARM的通信
- Mailbox 1用于ARM到Videocore的通信
- Mailgox 0是只读的,Mailbox 1是只写的。
每个mailbox有一系列定义好的虚拟channel,通过这些channel对每一种分类的mailbox调用进行进一步划分。
Mailbox定义了下面这些channel:
- power management
- framebuffer(过时)
- 虚拟UART
- VCHIQ
- LEDs
- Buttons
- Touch screen
- null
- Property tags(ARM->VC)
- Property tags(VC->ARM) - not used
我们这里主要用到channel 8。不过,从上面的列表也可以看出如果想点亮版上的led灯,很不幸CPU没有引脚和led灯相连,需要通过mailbox调用点亮,(虚拟uart也是,但是CPU有gpio引脚对应mini-uart的功能,我写的os就是用的mini-uart)。
- 从开发者的角度,mailbox实际上是一种 inter-processor 通信/调用机制 (类似的概念:进程间通信,远程系统调用)
- 调用者: ARM/GPU
- 被调者: ARM/GPU
- 调用参数: 存放在Mailbox寄存器中。
- 指向通信信息(buffer缓冲区)的指针
- channel号
4.2. 驱动访问Mailbox
上面从开发者角度给出了Mailbox接口,下面看一下在ARM侧从硬件角度如何配置Mailbox寄存器。
为了从mailbox寄存器中读取数据:
- 读status reg直到empty flag被设置
- 从read reg读取数据
- 如果低4位和预期的channel号不匹配,则重复1
- msb的28位是返回的数据
为了向mailbox寄存器中写入数据:
- 读status reg直到full flag没被设置
- 写buffer地址和channel号到write reg
注:除了property tags channel,传递的buffer的地址应当是VC侧可见的总线地址。若L2 cache使能,则对VC的MMU而言物理地址空间被映射到从0x4000_0000开始,若L2 cache没有使能,则物理地址空间被映射到从0xC000_0000开始。当使用property tag则使用正常的物理地址即可。
下面是后来搜到的斯坦福cs107e的一页ppt,给出了mailbox各个寄存器的格式(太良心了):
4.3. 使用Property Channel通信
property channel允许设置、获取和测试GPU上各种属性。该接口是从ARM侧访问Videocore上用户数据的主要方式。
- 来自GPU的响应会覆写请求的buffer
- buffer本身需要16字节对齐,因为地址的前28位会被通过mailbox寄存器传递
- 所有u64/u32/u16都遵循宿主CPU endian
- tag应当按序被处理,除非单个操作需要多个tags
4.3.1. Mailbox寄存器设置
- mailbox接口中,mailbox寄存器内使用高28位(msb)作为值,低4位(lsb)作为channel号
- 请求消息:28位 buffer addr
- 响应消息:28位 buffer addr
- channel8和9被使用
- 8: ARM向VC发出请求
- 9: VC向ARM发出请求(当前没有定义,ARM不需要服务VC的请求)
4.3.2. Mailbox消息缓存设置
下图给出了buffer的格式:
1 |
|
- u32: buffer大小(包含所有部分的总和)
- u32:buffer请求/响应码
- 请求码
- 0x0:
- 响应码
- 0x8000_0000: successful
- 0x8000_0001: error parsing request buffer
- 请求码
- 字节序列:相连的tags
- u32:0x0结束tag
- 字节序列:填充满足16字节对齐
下图给出了tag的格式
1 |
|
- u32: tag id
- u32: value buffer的大小
- u32
- 请求码
- b31 清除:请求
- b30-b0:保留
- 响应码
- b31 设置:响应
- b30-b0:值的长度
- 请求码
- 字节序列:buffer
- 字节序列:填充tag满足32位
而具体的各个tag的格式定义在mailbox 官方doc中。到真正实现的时候再去查好了。
终于,弄清楚了基本的硬件原理,下面就可以开始hacking了!下一篇传送门
5. 参考资料
下面是按推荐程度列出的参考资料链接,打*号的几乎是必看的。