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:
- Read the status register (0x2000B898).
- Check if bit 31 (0x80000000) is set.
- If not, go back to step 1 else proceed to next step.
- The data to be written, has to be left shifted by 4 bits. This converts our address to 28 bits.
- 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.
- Write the data to the Write register (0x2000B8A0).
We read from the mailbox as follows:
- Read the status (0x2000B898) register.
- Check if bit 30 (0x40000000) is set.
- If not, go back to step 1.
- Read the base register (0x2000B880).
- 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.
- 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:
- Power management
- Framebuffer
- Virtual UART
- VCHIQ
- LEDs
- Buttons
- Touch screen
- NA
- Mailbox-property-interface – ARM to GPU
- 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:
- 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.
- 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:
- 0x00048003 – Set physical width and height. This was retrieved in step 1.
- 0x00048004 – Set virtual width and height. For now, just set it to same values as the physical settings.
- 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.
- 0x00040001 – Allocate the actual buffer in memory, based on the provide tag values. Provides required alignment in bytes, in our case 16.
- Parse the response by checking for errors.
- 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.
- The Bare Metal section in the Raspberry Pi forum is a great source of information.
- Tags are documented in the Raspberry Pi firmware wiki
- BCM2835 (SoC) Datasheet
- ARM1176JZF-S Technical Reference
Thanks this is awesome. Hope you dont mind if I add it to the South African support forums here http://goo.gl/y4ZtZ2
Not at all