为树莓派3B+实现HDMI驱动(一)硬件原理

莫忘少年凌云志,曾许天下第一流。

本文主要介绍我为树莓派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相关的有下面这些元数据:

fb_config_t.png

  • physical size:物理屏幕的宽和高。
  • virtual size:内存framebuffer中的宽和高。
  • depth: 用于描述每个像素点的位数。
  • pitch: 用于描述屏幕上每一行的字节数。

通过depthpitch两个维度的数据,可以计算得到屏幕每一行的像素数为$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:

  1. power management
  2. framebuffer(过时)
  3. 虚拟UART
  4. VCHIQ
  5. LEDs
  6. Buttons
  7. Touch screen
  8. null
  9. Property tags(ARM->VC)
  10. 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寄存器中。
      1. 指向通信信息(buffer缓冲区)的指针
      2. channel号

4.2. 驱动访问Mailbox

上面从开发者角度给出了Mailbox接口,下面看一下在ARM侧从硬件角度如何配置Mailbox寄存器。

为了从mailbox寄存器中读取数据:

  1. 读status reg直到empty flag被设置
  2. 从read reg读取数据
  3. 如果低4位和预期的channel号不匹配,则重复1
  4. msb的28位是返回的数据

为了向mailbox寄存器中写入数据:

  1. 读status reg直到full flag没被设置
  2. 写buffer地址和channel号到write reg

注:除了property tags channel,传递的buffer的地址应当是VC侧可见的总线地址。若L2 cache使能,则对VC的MMU而言物理地址空间被映射到从0x4000_0000开始,若L2 cache没有使能,则物理地址空间被映射到从0xC000_0000开始。当使用property tag则使用正常的物理地址即可。

下面是后来搜到的斯坦福cs107e的一页ppt,给出了mailbox各个寄存器的格式(太良心了):

mailbox-reg.png

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
2
3
4
5
6
7
AAAABBBBxxxxxxxxCCCCyyyyyyyy
└┬─┘└┬─┘└───┬──┘└┬─┘└───┬──┘
                      Padding to specified length
               End Tag (0x00)
           Sequence of Tags
     Buffer request/response code
  Buffer Size
  • u32: buffer大小(包含所有部分的总和)
  • u32:buffer请求/响应码
    • 请求码
      • 0x0:
    • 响应码
      • 0x8000_0000: successful
      • 0x8000_0001: error parsing request buffer
  • 字节序列:相连的tags
  • u32:0x0结束tag
  • 字节序列:填充满足16字节对齐

下图给出了tag的格式

1
2
3
4
5
6
7
AAAABBBBCCCCxxxxxxxxyyyyyyyy
└┬─┘└┬─┘└┬─┘└──┬───┘└───┬──┘
                      Padding to align to 32bits
             Value buffer
        Request/Response code
     Value buffer size in bytes
  Tag identifier
  • u32: tag id
  • u32: value buffer的大小
  • u32
    • 请求码
      • b31 清除:请求
      • b30-b0:保留
    • 响应码
      • b31 设置:响应
      • b30-b0:值的长度
  • 字节序列:buffer
  • 字节序列:填充tag满足32位

而具体的各个tag的格式定义在mailbox 官方doc中。到真正实现的时候再去查好了。

终于,弄清楚了基本的硬件原理,下面就可以开始hacking了!下一篇传送门

5. 参考资料

下面是按推荐程度列出的参考资料链接,打*号的几乎是必看的。