Raspberry Pi Frame buffer

In my previous article I introduced you to VarkOS, my experimental OS for learning about the ARM architecture. In this article I will go over the steps involved in getting to the point where we can display text and graphics on the screen. For source code that implements what we are going to do here, head over to my Github repository for a copy of the complete VarkOS project, or view the framebuffer source code here.

The frame buffer is essentially a block of memory, which gets mapped to the screen coordinates, based on some parameters we configure. But before we can get into the nitty gritty of how this work, we need to understand how our platform, the Raspberry Pi, works.

The Raspberry Pi CPU is actually a high performance graphical processor with an ARM CPU strapped to its back. The CPU, ARM1176JZF-S, basically boots by reading the firmware files from the SD card, loads our kernel image into memory and starts executing it. Communication between the ARM CPU and the graphical processor, is by means of a mailbox mechanism. The mailbox is nothing other than a set hardware registers that are accessible by both processors. From our point of view, we write our message to memory and then pass the memory address of our message to the GPU, via the mailbox.

Mailbox

The mailbox’s hardware register addresses are found at the following addresses:

Register Address
Base 0x2000B880
Poll 0x2000B890
Sender 0x2000B894
Status 0x2000B898
Configuration 0x2000B89C
Write 0x2000B8A0

We are only going to concern ourselves with some of these registers. Writing to the mailbox is done as follows:

  1. Read the status register (0x2000B898).
  2. Check if bit 31 (0x80000000) is set.
  3. If not, go back to step 1 else proceed to next step.
  4. The data to be written, has to be left shifted by 4 bits. This converts our address to 28 bits.
  5. The lower 4 bits will contain the mailbox channel number, combine this with 28bit address by performing a logical OR with the 28 bit address from step 4.
  6. Write the data to the Write register (0x2000B8A0).

We read from the mailbox as follows:

  1. Read the status (0x2000B898) register.
  2. Check if bit 30 (0x40000000) is set.
  3. If not, go back to step 1.
  4. Read the base register (0x2000B880).
  5. The lower 4 bits will contain the mailbox channel number of the mailbox that responded. Check that this matches the mailbox we are interested in. If not, then go back to step 1.
  6. The upper 28 bit should contain the same address that we passed in originally.

It is important to note, that the addresses passed to and from the mailbox are left shifted by 4 bits. This is because the first 4 bits contains the mailbox channel number. There are 10 mailbox channels:

  1. Power management
  2. Framebuffer
  3. Virtual UART
  4. VCHIQ
  5. LEDs
  6. Buttons
  7. Touch screen
  8. NA
  9. Mailbox-property-interface – ARM to GPU
  10. Mailbox-property-interface – GPU to ARM

Although channel 1 is marked as being the Framebuffer channel, we will be using channel 8.

Configuration
Configuring the framebuffer this way, is achieved by querying and setting various tags to the appropriate values. Below is a list of the tags that are relevant to the framebuffer.

Register Address
Allocate buffer 0x00040001
Release buffer 0x00048001
Blank screen 0x00040002
Get physical (display) width/height 0x00040003
Test physical (display) width/height 0x00044003
Set physical (display) width/height 0x00048003
Get virtual (buffer) width/height 0x00040004
Test virtual (buffer) width/height 0x00044004
Set virtual (buffer) width/height 0x00048004
Get depth 0x00040005
Test depth 0x00044005
Set depth 0x00048005
Get pixel order 0x00040006
Test pixel order 0x00044006
Set pixel order 0x00048006
Get alpha mode 0x00040007
Test alpha mode 0x00044007
Set alpha mode 0x00048007
Get pitch 0x00040008
Get virtual offset 0x00040009
Test virtual offset 0x00044009
Set virtual offset 0x00048009
Get overscan 0x0004000a
Test overscan 0x0004400a
Set overscan 0x0004800a
Get palette 0x0004000b
Test palette 0x0004400b
Set palette 0x0004800b

The way these tags are used, is to format a buffer in a way which will be explained soon. The address of this buffer is then sent to the graphics processor via the mailbox mechanism explained above. What is important to understand, is that we are not sending a message and getting response, but rather our buffer is being modified directly to include the response data filled in by the graphics processor. The mailbox analogy could be confusing, by making it seem that we are sending and receiving messages, where in fact we are providing a buffer and some of our original request values get modified with the appropriate response values, by the graphics processor.

Remember that the buffer we are going to use to configure the frame buffer, has to be aligned to a 16 byte boundary, since we will be passing it through the mailbox. It should be declared as an array of unsigned, 32 bit integers. The layout of tags in this buffer is as follows:

Offset Description
0 Total Buffer size (number of bytes)
1 Request/Response indicator
0x00000000 – Request
0x80000000 – Success Response
0x80000001 – Error Response
2 Tag ID
3 Tag value length (number of bytes)
4 Tag Request/Response indicator
0x00000000 – Request
0x80000000 – Success Response
0x80000001 – Error Response
5 Value data
Value data
n 0 – End tag

The steps for configuring basic frame buffer functionality, is as follows:

  1. Query the physical width and height of the frame buffer by using tag 0x00040003. The response should match the supported size of the attached monitor on the HDMI port.
  2. Use the information in step 1 to request a matching frame buffer by setting all the relevant tags IN ONE GO. It is important that concatenated tags are used in a single request, since sending the requests one by one does not provide the correct context for the graphics processor to know what is required, in which case it will just ignore all the parameters you provide. The tags we will set are as follows:
    1. 0x00048003 – Set physical width and height. This was retrieved in step 1.
    2. 0x00048004 – Set virtual width and height. For now, just set it to same values as the physical settings.
    3. 0x00048005 – Set depth. This is how many bits should be used per pixel, to denote colour. I have only tried 16 and 24 so far.
    4. 0x00040001 – Allocate the actual buffer in memory, based on the provide tag values. Provides required alignment in bytes, in our case 16.
  3. Parse the response by checking for errors.
  4. Get the pitch by using tag 0x00040008, which tells us how many bytes are in one row of our framebuffer.

Conclusion
If everything went according to plan, you should now have a frame buffer that you can write to. One thing to note, is the layout of the tag values that allocate the frame buffer, tag 0x00040001. What is interesting here, is that we are actually expecting more values in the response, than what we have populated in the request. As such, we must be sure to leave space for the response value to be filled in. This was not immediately apparent to me and took me longer than I would have liked, to figure out what was going here. We are only populating the required alignment, but in the response we will receive the alignment value as well as the actual frame buffer memory address pointer, that we are after.

References
The information contained in this article is not entirely all my own, but was collected though tireless research from a number os sources and by examining source code of similar projects.

  1. The Bare Metal section in the Raspberry Pi forum is a great source of information.
  2. Tags are documented in the Raspberry Pi firmware wiki
  3. BCM2835 (SoC) Datasheet
  4. ARM1176JZF-S Technical Reference