NuttX RTOS for PinePhone: Render Graphics in Zig

đź“ť 15 Nov 2022

Our Zig Driver rendering Colour Blocks on Pine64 PinePhone

UPDATE: PinePhone is now officially supported by Apache NuttX RTOS (See this)

What happens when we render graphics on PinePhone’s LCD Display?

Plenty happens when we render graphics on Pine64 PinePhone (pic above)… Because PinePhone’s Display Hardware is so complex!

To understand the internals of PinePhone, let’s build a Display Driver that will talk directly to PinePhone’s Display Hardware. (“Bare Metal”)

We’ll do this with the Zig Programming Language, running on Apache NuttX RTOS.

Why Zig? Why not C?

We could have done it in C… But our driver code in Zig looks neater, more concise and (hopefully) easier to understand.

So instead of writing this in C…

// In C: Get the framebuffer length
int len = sizeof(framebuffer)
  / sizeof(framebuffer[0]);

We use the shorter readable form in Zig…

// In Zig: Get the framebuffer length
const len = framebuffer.len;

Zig looks highly similar to C. If we ever need to convert the driver code to C… Easy peasy!

(In this article we’ll explain the tricky Zig parts with C)

Why NuttX on PinePhone?

Apache NuttX RTOS gives us direct access to PinePhone’s Hardware Registers, so nothing gets in our way. (Like Memory Protection)

(NuttX boots from microSD, so it won’t affect the Linux Distro installed on PinePhone)

The code that we discuss today will soon become the PinePhone Display Driver for NuttX RTOS.

Let’s continue the journey from our NuttX Porting Journal…

PinePhone Framebuffer

§1 Graphics Framebuffer

We begin with a Graphics Framebuffer that we’ll render on PinePhone’s 720 x 1440 display (pic above): render.zig

// Framebuffer of 720 x 1440 pixels
var fb0 = std.mem.zeroes(  // Init to zeroes...
  [720 * 1440] u32         // 720 x 1440 pixels
);                         // (4 bytes per pixel: XRGB 8888)

Each pixel is u32, equivalent to uint32_t in C.

std.mem.zeroes allocates an array of 720 x 1440 pixels, filled with zeroes.

Each pixel has the format ARGB 8888 (32 bits)…

So 0x8080 0000 is Semi-Transparent Red. (Alpha: 0x80, Red: 0x80)

Let’s describe the Framebuffer with a NuttX Struct: render.zig

/// NuttX Color Plane for PinePhone (Base UI Channel):
/// Fullscreen 720 x 1440 (4 bytes per XRGB 8888 pixel)
const planeInfo = c.fb_planeinfo_s {
  .fbmem   = &fb0,     // Start of frame buffer memory
  .fblen   = @sizeOf( @TypeOf(fb0) ),  // Length of frame buffer memory in bytes
  .stride  = 720 * 4,  // Length of a line in bytes (4 bytes per pixel)
  .display = 0,        // Display number (Unused)
  .bpp     = 32,       // Bits per pixel (XRGB 8888)
  .xres_virtual = 720,   // Virtual Horizontal resolution in pixel columns
  .yres_virtual = 1440,  // Virtual Vertical resolution in pixel rows
  .xoffset      = 0,     // Offset from virtual to visible resolution
  .yoffset      = 0,     // Offset from virtual to visible resolution
};

(fb_planeinfo_s comes from NuttX RTOS)

Later we’ll pass the above values to render the Framebuffer: render.zig

// Init the Base UI Channel with the Framebuffer
initUiChannel(
  1,  // UI Channel Number (1 for Base UI Channel)
  planeInfo.fbmem,    // Start of frame buffer memory
  planeInfo.fblen,    // Length of frame buffer memory in bytes
  planeInfo.stride,   // Length of a line in bytes (4 bytes per pixel)
  planeInfo.xres_virtual,  // Horizontal resolution in pixel columns
  planeInfo.yres_virtual,  // Vertical resolution in pixel rows
  planeInfo.xoffset,  // Horizontal offset in pixel columns
  planeInfo.yoffset,  // Vertical offset in pixel rows
);

But first we paint some colours…

Blue, Green, Red Blocks on PinePhone

§2 Fill Framebuffer

This is how we fill the Framebuffer with Blue, Green and Red (pic above): render.zig

// Fill Framebuffer with Blue, Green and Red
var i: usize = 0;  // usize is similar to size_t
while (i < fb0.len) : (i += 1) {

  // Colours are in XRGB 8888 format
  if (i < fb0.len / 4) {
    // Blue for top quarter
    fb0[i] = 0x8000_0080;
  } else if (i < fb0.len / 2) {
    // Green for next quarter
    fb0[i] = 0x8000_8000;
  } else {
    // Red for lower half
    fb0[i] = 0x8080_0000;
  }
}

(Yeah Zig’s while loop looks rather odd, but there’s a simpler way to iterate over arrays: for loop)

Remember that pixels are in 32-bit ARGB 8888 format. So 0x8080 0000 means…

(Or Semi-Transparent Red)

We’re now ready to render our Framebuffer!

Does PinePhone support multiple Framebuffers?

Yep PinePhone supports 3 Framebuffers: One Base Framebuffer plus 2 Overlay Framebuffers: render.zig

/// NuttX Video Controller for PinePhone (3 UI Channels)
const videoInfo = c.fb_videoinfo_s {
  .fmt       = c.FB_FMT_RGBA32,  // Pixel format (XRGB 8888)
  .xres      = 720,   // Horizontal resolution in pixel columns
  .yres      = 1440,  // Vertical resolution in pixel rows
  .nplanes   = 1,     // Number of color planes supported (Base UI Channel)
  .noverlays = 2,     // Number of overlays supported (2 Overlay UI Channels)
};

(fb_videoinfo_s comes from NuttX RTOS)

We’ll test the Overlay Framebuffers later.

TODO

§3 Configure Framebuffer

How do we render the Framebuffer on PinePhone?

Remember that we’re talking directly to PinePhone’s Display Hardware (“Bare Metal”), without any Display Driver. So this part might sound a little more complicated than we expect…

To control PinePhone’s Display Hardware, we’ll set the Hardware Registers for the Allwinner A64 Display Engine inside PinePhone. (Pic above)

In a while we’ll do the following through the Hardware Registers…

  1. Set Framebuffer Address

    (To activate DMA: Direct Memory Access)

  2. Set Framebuffer Pitch

    (Number of bytes per row: 720 * 4)

  3. Set Framebuffer Size

    (Width and Height are 720 x 1440)

  4. Set Framebuffer Coordinates

    (X and Y Offsets are 0)

  5. Set Framebuffer Attributes

    (Global Alpha Values)

  6. Disable Framebuffer Scaler

    (Because we’re not scaling the graphics)

This sounds really low level… But hopefully we’ll learn more about PinePhone’s Internals!

How do we get the above Framebuffer values?

Our program calls initUiChannel, passing the Framebuffer Settings: render.zig

// Init the Base UI Channel with the Framebuffer
initUiChannel(
  1,  // UI Channel Number (1 for Base UI Channel)
  planeInfo.fbmem,    // Start of frame buffer memory
  planeInfo.fblen,    // Length of frame buffer memory in bytes
  planeInfo.stride,   // Length of a line in bytes (4 bytes per pixel)
  planeInfo.xres_virtual,  // Horizontal resolution in pixel columns
  planeInfo.yres_virtual,  // Vertical resolution in pixel rows
  planeInfo.xoffset,  // Horizontal offset in pixel columns
  planeInfo.yoffset,  // Vertical offset in pixel rows
);

(We’ve seen planeInfo earlier)

Our function initUiChannel is defined in render.zig

/// Initialise a UI Channel for PinePhone's A64 Display Engine.
/// We use 3 UI Channels: Base UI Channel (#1) plus 2 Overlay UI Channels (#2, #3).
/// See https://lupyuen.github.io/articles/de#appendix-programming-the-allwinner-a64-display-engine
fn initUiChannel(
  comptime channel: u8,   // UI Channel Number: 1, 2 or 3
  fbmem: ?*anyopaque,     // Start of frame buffer memory, or null if this channel should be disabled
  comptime fblen: usize,           // Length of frame buffer memory in bytes
  comptime stride:  c.fb_coord_t,  // Length of a line in bytes (4 bytes per pixel)
  comptime xres:    c.fb_coord_t,  // Horizontal resolution in pixel columns
  comptime yres:    c.fb_coord_t,  // Vertical resolution in pixel rows
  comptime xoffset: c.fb_coord_t,  // Horizontal offset in pixel columns
  comptime yoffset: c.fb_coord_t,  // Vertical offset in pixel rows
) void {
  ...

Which means that our function initUiChannel will receive the following values…

Why is the Framebuffer Address declared as “?*anyopaque”?

That’s because…

So the Framebuffer Address can be null.

(Which will disable the Overlay Framebuffers)

What’s comptime?

comptime substitutes the Parameter Values at Compile-Time. (Somewhat like a C Macro)

We’ll explain why in a while.

Let’s look inside our function initUiChannel…

§3.1 Framebuffer Address

(OVL_UI_TOP_LADD, Page 104)

The first Hardware Register we’ll set is the Framebuffer Address: render.zig

// OVL_UI_TOP_LADD (UI Overlay Top Field Memory Block Low Address)
// At OVL_UI Offset 0x10
// Set to Framebuffer Address fb0
// (DE Page 104)

const ptr = @ptrToInt(fbmem.?);
const OVL_UI_TOP_LADD = 
  OVL_UI_BASE_ADDRESS + 0x10;
putreg32(              // Write to Hardware Register...
  @intCast(u32, ptr),  // Value
  OVL_UI_TOP_LADD      // Address
);

(Recall that fbmem is the Address of fb0)

For our safety, Zig gets strict about Null Values and Range Checking…

Huh we’re force-fitting a 64-bit Physical Address into a 32-bit Integer?

That’s perfectly OK because PinePhone only supports up to 3 GB of Physical RAM.

What’s OVL_UI_BASE_ADDRESS?

OVL_UI_BASE_ADDRESS is computed though a chain of Hardware Register addresses: render.zig

// OVL_UI(CH1) (UI Overlay 1) is at MIXER0 Offset 0x3000
// (DE Page 102, 0x110 3000)
// We convert channel to 64-bit to prevent overflow
const OVL_UI_BASE_ADDRESS = OVL_UI_CH1_BASE_ADDRESS
  + @intCast(u64, channel - 1) * 0x1000;

// OVL_UI(CH1) (UI Overlay 1) is at MIXER0 Offset 0x3000
// (DE Page 102, 0x110 3000)
const OVL_UI_CH1_BASE_ADDRESS = MIXER0_BASE_ADDRESS + 0x3000;

// MIXER0 is at DE Offset 0x10 0000
// (DE Page 24, 0x110 0000)
const MIXER0_BASE_ADDRESS = DISPLAY_ENGINE_BASE_ADDRESS + 0x10_0000;

// Display Engine Base Address is 0x0100 0000
// (DE Page 24)
const DISPLAY_ENGINE_BASE_ADDRESS = 0x0100_0000;

Hmmm this looks error-prone…

That’s why we added Assertion Checks to verify that the addresses of Hardware Registers are computed correctly: render.zig

// Verify Register Address at Compile-Time
comptime { 
  // Halt during compilation if verification fails
  assert(
    // Register Address should be this...
    OVL_UI_TOP_LADD == 0x110_3010
  );
}

comptime means that the Assertion Check is performed by the Zig Compiler at Compile-Time. (Instead of Runtime)

This verification is super helpful as we create the new Display Driver for PinePhone.

(We verify both Register Addresses and Values at Compile-Time. This becomes an “Executable Specification” of PinePhone’s Hardware)

§3.2 Framebuffer Pitch

(OVL_UI_PITCH, Page 104)

Next we set the Framebuffer Pitch to the number of bytes per row (720 * 4): render.zig

// OVL_UI_PITCH (UI Overlay Memory Pitch)
// At OVL_UI Offset 0x0C
// Set to (width * 4), number of bytes per row
// (DE Page 104)

const OVL_UI_PITCH = OVL_UI_BASE_ADDRESS + 0x0C;
putreg32(       // Write to Hardware Register...
  xres * 4,     // xres is 720
  OVL_UI_PITCH  // Address of Hardware Register
);

§3.3 Framebuffer Size

(OVL_UI_MBSIZE / OVL_UI_SIZE, Page 104 / 106)

We set the Framebuffer Size with this rather odd formula…

(height - 1) << 16 + (width - 1)

This is how we do it: render.zig

// OVL_UI_MBSIZE (UI Overlay Memory Block Size)
// At OVL_UI Offset 0x04
// Set to (height-1) << 16 + (width-1)
// (DE Page 104)

const height_width: u32 =
  @intCast(u32, yres - 1) << 16  // yres is 1440
  | (xres - 1);                  // xres is 720
const OVL_UI_MBSIZE = OVL_UI_BASE_ADDRESS + 0x04;
putreg32(height_width, OVL_UI_MBSIZE);

We do the same for another Hardware Register: render.zig

// OVL_UI_SIZE (UI Overlay Overlay Window Size)
// At OVL_UI Offset 0x88
// Set to (height-1) << 16 + (width-1)
// (DE Page 106)

const OVL_UI_SIZE = OVL_UI_BASE_ADDRESS + 0x88;
putreg32(height_width, OVL_UI_SIZE);

§3.4 Framebuffer Coordinates

(OVL_UI_COOR, Page 104)

Our Framebuffer will be rendered at X = 0, Y = 0. We set this in the Framebuffer Coordinates: render.zig

// OVL_UI_COOR (UI Overlay Memory Block Coordinate)
// At OVL_UI Offset 0x08
// Set to 0 (Overlay at X=0, Y=0)
// (DE Page 104)

const OVL_UI_COOR = OVL_UI_BASE_ADDRESS + 0x08;
putreg32(0, OVL_UI_COOR);

§3.5 Framebuffer Attributes

(OVL_UI_ATTR_CTL, Page 102)

We set the Framebuffer Attributes…

This is how we set the above attributes as Bit Fields: render.zig

// OVL_UI_ATTR_CTL (UI Overlay Attribute Control)
// At OVL_UI Offset 0x00
// LAY_GLBALPHA   (Bits 24 to 31) = Global Alpha Value
// LAY_FBFMT      (Bits 8  to 12) = Input Data Format
// LAY_ALPHA_MODE (Bits 1  to 2)  = Mix Global Alpha with Pixel Alpha
// LAY_EN         (Bit 0)         = Enable Layer
// (DE Page 102)

// Framebuffer is Opaque
const LAY_GLBALPHA: u32 = 0xFF << 24;

// Framebuffer Pixel Format is XRGB 8888
const LAY_FBFMT: u13 = 4 << 8;

// Framebuffer Alpha is mixed with Pixel Alpha
const LAY_ALPHA_MODE: u3 = 2 << 1;

// Enable Framebuffer
const LAY_EN: u1 = 1 << 0;

// Combine the bits and set the register
const attr = LAY_GLBALPHA
  | LAY_FBFMT
  | LAY_ALPHA_MODE
  | LAY_EN;
const OVL_UI_ATTR_CTL = OVL_UI_BASE_ADDRESS + 0x00;
putreg32(attr, OVL_UI_ATTR_CTL);

Why u3 and u13?

That’s for 3-Bit and 13-Bit Integers. If we make a mistake and specify an invalid value, the Zig Compiler will stop us…

// Zig Compiler won't allow this
// because it needs 4 bits
const LAY_ALPHA_MODE: u3 = 4 << 1;

(Zig also supports Packed Structs with Bit Fields)

§3.6 Disable Scaler

(UIS_CTRL_REG, Page 66)

PinePhone’s A64 Display Engine includes a UI Scaler that will do Hardware Scaling of our Framebuffer. Let’s disable it: render.zig

// UIS_CTRL_REG at Offset 0 of UI_SCALER1(CH1) or UI_SCALER2(CH2) or UI_SCALER3(CH3)
// Set to 0 (Disable UI Scaler)
// EN (Bit 0) = 0 (Disable UI Scaler)
// (DE Page 66)

const UIS_CTRL_REG = UI_SCALER_BASE_ADDRESS + 0;
putreg32(0, UIS_CTRL_REG);

And we’re done configuring the PinePhone Display Engine for our Framebuffer!

Let’s talk about PinePhone’s Blender…

(Will PinePhone Blend? Yep for sure!)

TODO

§4 Configure Blender

What’s the Blender inside PinePhone?

PinePhone’s A64 Display Engine supports 3 Framebuffers (pic above). PinePhone’s Blender combines the 3 Framebuffers into a single image for display.

Our job now is to configure the Blender so that it renders the Framebuffer correctly.

But we’re using only one Framebuffer?

For now. Which makes the Blender Configuration a little simpler.

Up next: We’ll set PinePhone’s Hardware Registers to configure the Blender for a single Framebuffer…

  1. Set Output Size

    (Screen Size is 720 x 1440)

  2. Set Input Size

    (Framebuffer Size is 720 x 1440)

  3. Set Fill Color

    (Background is Opaque Black)

  4. Set Input Offset

    (X and Y Offsets are 0)

  5. Set Blender Attributes

    (For Alpha Blending)

  6. Enable Blender

    (For Blender Pipe 0)

§4.1 Output Size

(BLD_SIZE / GLB_SIZE, Page 110 / 93)

We set the Output Size of our Blender to 720 x 1440 with this odd formula (that we’ve seen earlier)…

(height - 1) << 16 + (width - 1)

This is how we set the Hardware Registers: render.zig

// BLD_SIZE (Blender Output Size Setting)
// At BLD Offset 0x08C
// Set to (height-1) << 16 + (width-1)
// (DE Page 110)

const height_width: u32 =
  @intCast(u32, yres - 1) << 16  // yres is 1440
  | (xres - 1);                  // xres is 720
const BLD_SIZE = BLD_BASE_ADDRESS + 0x08C;
putreg32(height_width, BLD_SIZE);
        
// GLB_SIZE (Global Size)
// At GLB Offset 0x00C
// Set to (height-1) << 16 + (width-1)
// (DE Page 93)

const GLB_SIZE = GLB_BASE_ADDRESS + 0x00C;
putreg32(height_width, GLB_SIZE);

§4.2 Input Size

(BLD_CH_ISIZE, Page 108)

According to the pic above, we’re configuring Blender Pipe 0: render.zig

// Set Blender Input Pipe to Pipe 0
// (For Channel 1)
const pipe: u64 = channel - 1;

This is how we set the Input Size to 720 x 1440 for Blender Pipe 0: render.zig

// BLD_CH_ISIZE (Blender Input Memory Size)
// At BLD Offset 0x008 + N*0x10 (N=0 for Channel 1)
// Set to (height-1) << 16 + (width-1)
// (DE Page 108)

const BLD_CH_ISIZE = BLD_BASE_ADDRESS + 0x008 + pipe * 0x10;
putreg32(height_width, BLD_CH_ISIZE);

(We’ve seen height_width earlier)

§4.3 Fill Color

(BLD_FILL_COLOR, Page 107)

We set the Background Fill Color for the Blender to Opaque Black: render.zig

// BLD_FILL_COLOR (Blender Fill Color)
// At BLD Offset 0x004 + N*0x10 (N=0 for Channel 1)
// ALPHA (Bits 24 to 31) = 0xFF
// RED   (Bits 16 to 23) = 0
// GREEN (Bits 8  to 15) = 0
// BLUE  (Bits 0  to 7)  = 0
// (DE Page 107)

const ALPHA: u32 = 0xFF << 24;  // Opaque
const RED:   u24 = 0    << 16;  // Black
const GREEN: u18 = 0    << 8;
const BLUE:  u8  = 0    << 0;
const color = ALPHA
  | RED
  | GREEN
  | BLUE;

const BLD_FILL_COLOR = BLD_BASE_ADDRESS + 0x004 + pipe * 0x10;
putreg32(color, BLD_FILL_COLOR);

§4.4 Input Offset

(BLD_CH_OFFSET, Page 108)

We set the Input Offset of the Blender to X = 0, Y = 0: render.zig

// BLD_CH_OFFSET (Blender Input Memory Offset)
// At BLD Offset 0x00C + N*0x10 (N=0 for Channel 1)
// (DE Page 108)

const offset = 
  @intCast(u32, yoffset) << 16  // yoffset is 0
  | xoffset;                    // xoffset is 0
const BLD_CH_OFFSET = BLD_BASE_ADDRESS + 0x00C + pipe * 0x10;
putreg32(offset, BLD_CH_OFFSET);

§4.5 Blender Attributes

(BLD_CTL, Page 110)

We set these (mysterious) Blender Attributes…

Like so: render.zig

// BLD_CTL (Blender Control)
// At BLD Offset 0x090 + N*4 (N=0 for Channel 1)
// BLEND_AFD (Bits 24 to 27) = 3
//   (Coefficient for destination alpha data Q[d] is 1-A[s])
// BLEND_AFS (Bits 16 to 19) = 1
//   (Coefficient for source alpha data Q[s] is 1)
// BLEND_PFD (Bits 8 to 11) = 3
//   (Coefficient for destination pixel data F[d] is 1-A[s])
// BLEND_PFS (Bits 0 to 3) = 1
//   (Coefficient for source pixel data F[s] is 1)
// (DE Page 110)

const BLEND_AFD: u28 = 3 << 24;  // Coefficient for destination alpha data Q[d] is 1-A[s]
const BLEND_AFS: u20 = 1 << 16;  // Coefficient for source alpha data Q[s] is 1
const BLEND_PFD: u12 = 3 << 8;   // Coefficient for destination pixel data F[d] is 1-A[s]
const BLEND_PFS: u4  = 1 << 0;   // Coefficient for source pixel data F[s] is 1
const blend = BLEND_AFD
  | BLEND_AFS
  | BLEND_PFD
  | BLEND_PFS;

const BLD_CTL = BLD_BASE_ADDRESS + 0x090 + pipe * 4;
putreg32(blend, BLD_CTL);

We’re almost done with our Blender Configuration…

TODO

§4.6 Enable Blender

(BLD_CH_RTCTL / BLD_FILL_COLOR_CTL / GLB_DBUFFER, Page 108 / 106 / 93)

Finally we enable Blender Pipe 0 (pic above): render.zig

// Set Blender Route
// BLD_CH_RTCTL (Blender Routing Control)
// At BLD Offset 0x080
//   P0_RTCTL (Bits 0 to 3) = 1 (Pipe 0 from Channel 1)
// (DE Page 108)

const P0_RTCTL: u4 = 1 << 0;  // Select Pipe 0 from UI Channel 1
const route = P0_RTCTL;

const BLD_CH_RTCTL = BLD_BASE_ADDRESS + 0x080;
putreg32(route, BLD_CH_RTCTL);  // TODO: DMB

(DMB means Data Memory Barrier)

We disable Pipes 1 and 2 since they’re not used: render.zig

// Enable Blender Pipes
// BLD_FILL_COLOR_CTL (Blender Fill Color Control)
// At BLD Offset 0x000
//   P0_EN   (Bit 8)  = 1 (Enable Pipe 0)
//   P0_FCEN (Bit 0)  = 1 (Enable Pipe 0 Fill Color)
// (DE Page 106)

const P0_EN:   u9 = 1 << 8;  // Enable Pipe 0
const P0_FCEN: u1 = 1 << 0;  // Enable Pipe 0 Fill Color
const fill = P0_EN
    | P0_FCEN;

const BLD_FILL_COLOR_CTL = BLD_BASE_ADDRESS + 0x000;
putreg32(fill, BLD_FILL_COLOR_CTL);  // TODO: DMB

Our Framebuffer appears on PinePhone’s Display when we apply the settings for the Display Engine: render.zig

// Apply Settings
// GLB_DBUFFER (Global Double Buffer Control)
// At GLB Offset 0x008
// DOUBLE_BUFFER_RDY (Bit 0) = 1
// (Register Value is ready for update)
// (DE Page 93)

const DOUBLE_BUFFER_RDY: u1 = 1 << 0;  // Register Value is ready for update
const GLB_DBUFFER = GLB_BASE_ADDRESS + 0x008;
putreg32(DOUBLE_BUFFER_RDY, GLB_DBUFFER);  // TODO: DMB

And that’s all for rendering our Framebuffer!

Testing our PinePhone Display Driver

§5 Test PinePhone Display Driver

We’re ready to test our Zig Display Driver on PinePhone!

Follow these steps to download NuttX RTOS (with our Zig Driver inside) to a microSD Card…

Connect our computer to PinePhone with a USB Serial Debug Cable. (At 115.2 kbps)

Boot PinePhone with NuttX RTOS in the microSD Card.

(NuttX won’t disturb the eMMC Flash Memory)

At the NuttX Shell, enter this command to test our Zig Display Driver…

hello 1

We should see our Zig Driver setting the Hardware Registers of the Allwinner A64 Display Engine (pic above)…

HELLO NUTTX ON PINEPHONE!
Shell (NSH) NuttX-11.0.0-RC2
nsh> hello 1
...
initUiChannel: start
Channel 1: Set Overlay (720 x 1440)
  *0x1103000 = 0xff000405
  *0x1103010 = 0x4010c000
  *0x110300c = 0xb40
  *0x1103004 = 0x59f02cf
  *0x1103088 = 0x59f02cf
  *0x1103008 = 0x0
Channel 1: Set Blender Output
Channel 1: Set Blender Input Pipe 0 (720 x 1440)
Channel 1: Disable Scaler
...
Channel 2: Disable Overlay and Pipe
Channel 2: Disable Scaler
...
Channel 3: Disable Overlay and Pipe
Channel 3: Disable Scaler
...
Set Blender Route
Enable Blender Pipes
Apply Settings

(See the Complete Log)

Why are Channels 2 and 3 disabled?

PinePhone supports 3 Framebuffers, but our demo uses only a single Framebuffer. (On Channel 1)

That’s why we disabled Channels 2 and 3 for the unused Framebuffers.

(Here’s how)

Blue, Green, Red Blocks on PinePhone

On PinePhone we see the Blue, Green and Red colour blocks. (Pic above)

Yep our Zig Display Driver renders graphics correctly on PinePhone! 🎉

We’ve successfully rendered a single Framebuffer. In the next chapter we push PinePhone’s Display Hardware to the max with 3 Framebuffers.

The Blue / Green / Red Blocks look kinda bright. Didn’t we set the Alpha Channel to be Semi-Transparent?

Aha! That’s because we’re rendering a Single Framebuffer, and we disregard the Alpha Channel for the Framebuffer.

That’s why we configured the Framebuffer for XRGB 8888 instead of ARGB 8888. (See this)

In a while we’ll render 2 Overlay Framebuffers configured for ARGB 8888. (To prove that the Alpha Channel really works!)

Multiple Framebuffers

§6 Multiple Framebuffers

Can we render Multiple Framebuffers?

Yep PinePhone’s Display Hardware supports up to 3 Framebuffers (pic above)…

Let’s walk through the steps to…

  1. Allocate the 2 Overlay Framebuffers

  2. Fill the pixels of the 2 Framebuffers

  3. Render the 2 Framebuffers as Overlays

§6.1 Allocate Framebuffers

Earlier we have allocated the Base Framebuffer…

Now we allocate 2 Overlay Framebuffers…

Like so: render.zig

// Framebuffer 1: (First Overlay UI Channel)
// Square 600 x 600 (4 bytes per ARGB 8888 pixel)
var fb1 = std.mem.zeroes(  // Init to zeroes...
  [600 * 600] u32          // 600 x 600 pixels
);                         // (4 bytes per pixel: ARGB 8888)

// Framebuffer 2: (Second Overlay UI Channel)
// Fullscreen 720 x 1440 (4 bytes per ARGB 8888 pixel)
var fb2 = std.mem.zeroes(  // Init to zeroes...
  [720 * 1440] u32         // 720 x 1440 pixels
);                         // (4 bytes per pixel: ARGB 8888)

PinePhone supports Framebuffers that are not Fullscreen?

Yep Framebuffer 1 doesn’t cover the screen completely, and it’s OK!

Later we’ll set the X and Y Offsets of Framebuffer 1, to centre it horizontally.

Blue, Green, Red Blocks with Overlays

§6.2 Fill Framebuffers

Let’s fill the pixels of the 2 Overlay Framebuffers (pic above)…

This is how we fill Framebuffer 1 with a Semi-Transparent Blue Square: render.zig

// Init Framebuffer 1:
// Fill with Semi-Transparent Blue
i = 0;
while (i < fb1.len) : (i += 1) {
  // Colours are in ARGB 8888 format
  fb1[i] = 0x8000_0080;
}

(0x8000_0080 means Alpha = 0x80, Blue = 0x80)

And this is how we fill Framebuffer 2 with a Semi-Transparent Green Circle: render.zig

// Init Framebuffer 2:
// Fill with Semi-Transparent Green Circle
var y: usize = 0;
while (y < 1440) : (y += 1) {
  var x: usize = 0;
  while (x < 720) : (x += 1) {
    // Get pixel index
    const p = (y * 720) + x;
    assert(p < fb2.len);

    // Shift coordinates so that centre of screen is (0,0)
    const x_shift = @intCast(isize, x) - 360;
    const y_shift = @intCast(isize, y) - 720;

    // If x^2 + y^2 < radius^2, set the pixel to Semi-Transparent Green
    if (x_shift*x_shift + y_shift*y_shift < 360*360) {
      fb2[p] = 0x8000_8000;  // Semi-Transparent Green in ARGB 8888 Format
    } else {  // Otherwise set to Transparent Black
      fb2[p] = 0x0000_0000;  // Transparent Black in ARGB 8888 Format
    }
  }
}

(0x8000_8000 means Alpha = 0x80, Green = 0x80)

Note that pixels outside the circle are filled with 0. (Transparent Black)

§6.3 Render Framebuffers

For our final step, we render all 3 Framebuffers: render.zig

// Init the UI Blender for PinePhone's A64 Display Engine
initUiBlender();

// Omitted: Init the Base UI Channel (as seen earlier)
initUiChannel(1, ...);

// Init the 2 Overlay UI Channels
inline for (overlayInfo) | ov, ov_index | {
  initUiChannel(
    @intCast(u8, ov_index + 2),  // UI Channel Number (2 and 3 for Overlay UI Channels)
    ov.fbmem,    // Start of frame buffer memory
    ov.fblen,    // Length of frame buffer memory in bytes
    ov.stride,   // Length of a line in bytes (4 bytes per pixel)
    ov.sarea.w,  // Horizontal resolution in pixel columns
    ov.sarea.h,  // Vertical resolution in pixel rows
    ov.sarea.x,  // Horizontal offset in pixel columns
    ov.sarea.y,  // Vertical offset in pixel rows
  );
}

// Set UI Blender Route, enable Blender Pipes
// and apply the settings
applySettings(channels);

(initUiBlender is defined here)

(applySettings is defined here)

Earlier we’ve seen initUiChannel for rendering a single Framebuffer.

We made some changes to support Multiple Framebuffers…

What’s overlayInfo?

overlayInfo is the array that defines the properties of the 2 Overlay Framebuffers.

(overlayInfo is defined here)

(fb_overlayinfo_s comes from NuttX RTOS)

Why “inline for”?

“inline for” expands (or unrolls) the loop at Compile-Time.

We need this because we’re passing the arguments to initUiChannel as comptime Compile-Time Constants.

(As explained previously)

Blue, Green, Red Blocks with Overlays

§7 Test Multiple Framebuffers

We’re ready for the final demo: Render Multiple Framebuffers with our Zig Driver on PinePhone!

Follow the earlier steps to download Apache NuttX RTOS to a microSD Card and boot it on PinePhone…

At the NuttX Shell, enter this command to render Multiple Framebuffers with our Zig Display Driver…

hello 3

Our Zig Driver sets the Hardware Registers of the Allwinner A64 Display Engine…

HELLO NUTTX ON PINEPHONE!
Shell (NSH) NuttX-11.0.0-RC2
nsh> hello 3
...
initUiChannel: start
Channel 1: Set Overlay (720 x 1440)
  *0x1103000 = 0xff000405
  *0x1103010 = 0x4010c000
  *0x110300c = 0xb40
  *0x1103004 = 0x59f02cf
  *0x1103088 = 0x59f02cf
  *0x1103008 = 0x0
Channel 1: Set Blender Output
Channel 1: Set Blender Input Pipe 0 (720 x 1440)
...
Channel 2: Set Overlay (600 x 600)
Channel 2: Set Blender Input Pipe 1 (600 x 600)
...
Channel 3: Set Overlay (720 x 1440)
Channel 3: Set Blender Input Pipe 2 (720 x 1440)
...
Set Blender Route
Enable Blender Pipes
Apply Settings

(See the Complete Log)

Note that Channels 2 and 3 are now enabled. This means that Framebuffers 1 and 2 will be visible.

On PinePhone we see the Blue, Green and Red colour blocks as before, plus 2 overlays…

Our Zig Display Driver renders all 3 Framebuffers correctly on PinePhone yay!

The Green Circle looks really faint? Compared with the Blue Square?

That’s because we applied a Global Alpha Value to the Green Circle…

This further reduces the opacity of the Semi-Transparent Pixels of the Green Circle, making it look really faint.

When we update the pixels in the Framebuffers, how do we refresh the display?

No refresh necessary, Framebuffer Updates are automatically pushed to PinePhone’s Display!

That’s because PinePhone’s A64 Display Engine is connected to the Framebuffers via Direct Memory Access (DMA).

The pixel data goes directly from the Framebuffers in RAM to PinePhone’s Display.

(Here’s the proof)

PinePhone rendering Mandelbrot Set on Apache NuttX RTOS

§8 What’s Next

Today we’ve shown that it’s indeed possible to write a Zig Display Driver that talks directly to PinePhone’s Hardware to render graphics.

(Bonus: Our Zig Driver includes an “Executable Specification” of PinePhone’s Display Hardware Registers, with their addresses and values!)

The code we’ve seen today will eventually become the PinePhone Display Driver for Apache NuttX RTOS. Though some bits are still missing…

But now it’s time to merge our code into NuttX Mainline! I’ll explain the process in the next couple of articles, stay tuned!

Please check out the other articles on NuttX for PinePhone…

Many Thanks to my GitHub Sponsors for supporting my work! This article wouldn’t have been possible without your support.

Got a question, comment or suggestion? Create an Issue or submit a Pull Request here…

lupyuen.github.io/src/de2.md

§9 Notes

  1. Sorry I took so long to merge the PinePhone code into NuttX Mainline… I wanted to be really sure that PinePhone is sufficiently stable on NuttX.

    Our experiments today proved that PinePhone works well indeed on NuttX!

Multiple Framebuffers

§10 Appendix: Render Multiple Framebuffers

Earlier we’ve seen initUiChannel for rendering a single Framebuffer…

Then we modified initUiChannel to render 3 Framebuffers…

This Appendix explains the changes we made to render 3 Framebuffers. (Pic above)

Blue, Green, Red Blocks with Overlays

§10.1 Set Framebuffer Attributes

(OVL_UI_ATTR_CTL, Page 102)

For Framebuffer Alpha: We configured Framebuffer 2 (Channel 3) to be Globally Semi-Transparent. (Global Alpha = 0x7F)

This means that the Global Alpha will be mixed with the Pixel Alpha. So the pixels will look extra faint for Framebuffer 2. (Green Circle, pic above)

For Pixel Format: We configured Framebuffers 1 and 2 (Channels 2 and 3) for Pixel Format ARGB 8888. (8-bit Alpha, 8-bit Red, 8-bit Green, 8-bit Blue)

This differs from Framebuffer 0, which we configured for XRGB 8888. (Alpha is ignored)

This is how we set the Framebuffer Attributes: render.zig

// Set Overlay (Assume Layer = 0)
// OVL_UI_ATTR_CTL (UI Overlay Attribute Control)
// At OVL_UI Offset 0x00
// LAY_GLBALPHA (Bits 24 to 31) = 0xFF or 0x7F
//   (Global Alpha Value is Opaque or Semi-Transparent)
// LAY_FBFMT (Bits 8 to 12) = 4 or 0
//   (Input Data Format is XRGB 8888 or ARGB 8888)
// LAY_ALPHA_MODE (Bits 1 to 2) = 2
//   (Global Alpha is mixed with Pixel Alpha)
//   (Input Alpha Value = Global Alpha Value * Pixel’s Alpha Value)
// LAY_EN (Bit 0) = 1 (Enable Layer)
// (DE Page 102)

const LAY_GLBALPHA: u32 = switch (channel) {  // For Global Alpha Value...
  1 => 0xFF,  // Channel 1: Opaque
  2 => 0xFF,  // Channel 2: Opaque
  3 => 0x7F,  // Channel 3: Semi-Transparent
  else => unreachable,
} << 24;  // Bits 24 to 31

const LAY_FBFMT: u13 = switch (channel) {  // For Input Data Format...
  1 => 4,  // Channel 1: XRGB 8888
  2 => 0,  // Channel 2: ARGB 8888
  3 => 0,  // Channel 3: ARGB 8888
  else => unreachable,
} << 8;  // Bits 8 to 12

const LAY_ALPHA_MODE: u3 = 2 << 1;  // Global Alpha is mixed with Pixel Alpha
const LAY_EN:         u1 = 1 << 0;  // Enable Layer
const attr = LAY_GLBALPHA
  | LAY_FBFMT
  | LAY_ALPHA_MODE
  | LAY_EN;

const OVL_UI_ATTR_CTL = OVL_UI_BASE_ADDRESS + 0x00;
putreg32(attr, OVL_UI_ATTR_CTL);

Next we configure the Blender…

Multiple Framebuffers

§10.2 Set Blender Route

(BLD_CH_RTCTL, Page 108)

Now that we render 3 Framebuffers instead of 1, we need to connect the Blender Pipes to their respective Framebuffers (Channels)…

Here’s how we connect the pipes: render.zig

// Set Blender Route
// BLD_CH_RTCTL (Blender Routing Control)
// At BLD Offset 0x080
// If Rendering 3 UI Channels:
//   P2_RTCTL (Bits 8 to 11) = 3 (Pipe 2 from Channel 3)
//   P1_RTCTL (Bits 4 to 7)  = 2 (Pipe 1 from Channel 2)
//   P0_RTCTL (Bits 0 to 3)  = 1 (Pipe 0 from Channel 1)
// If Rendering 1 UI Channel:
//   P0_RTCTL (Bits 0 to 3) = 1 (Pipe 0 from Channel 1)
// (DE Page 108)

const P2_RTCTL: u12 = switch (channels) {  // For Pipe 2...
  3 => 3,  // 3 UI Channels: Select Pipe 2 from UI Channel 3
  1 => 0,  // 1 UI Channel:  Unused Pipe 2
  else => unreachable,
} << 8;  // Bits 8 to 11

const P1_RTCTL: u8 = switch (channels) {  // For Pipe 1...
  3 => 2,  // 3 UI Channels: Select Pipe 1 from UI Channel 2
  1 => 0,  // 1 UI Channel:  Unused Pipe 1
  else => unreachable,
} << 4;  // Bits 4 to 7

const P0_RTCTL: u4 = 1 << 0;  // Select Pipe 0 from UI Channel 1
const route = P2_RTCTL
  | P1_RTCTL
  | P0_RTCTL;

const BLD_CH_RTCTL = BLD_BASE_ADDRESS + 0x080;
putreg32(route, BLD_CH_RTCTL);  // TODO: DMB

(channels is 3 when we render 3 Framebuffers)

§10.3 Enable Blender Pipes

(BLD_FILL_COLOR_CTL, Page 106)

After connecting the Blender Pipes, we enable all 3 Blender Pipes: render.zig

// Enable Blender Pipes
// BLD_FILL_COLOR_CTL (Blender Fill Color Control)
// At BLD Offset 0x000
// If Rendering 3 UI Channels:
//   P2_EN   (Bit 10) = 1 (Enable Pipe 2)
//   P1_EN   (Bit 9)  = 1 (Enable Pipe 1)
//   P0_EN   (Bit 8)  = 1 (Enable Pipe 0)
//   P0_FCEN (Bit 0)  = 1 (Enable Pipe 0 Fill Color)
// If Rendering 1 UI Channel:
//   P0_EN   (Bit 8)  = 1 (Enable Pipe 0)
//   P0_FCEN (Bit 0)  = 1 (Enable Pipe 0 Fill Color)
// (DE Page 106)

const P2_EN: u11 = switch (channels) {  // For Pipe 2...
  3 => 1,  // 3 UI Channels: Enable Pipe 2
  1 => 0,  // 1 UI Channel:  Disable Pipe 2
  else => unreachable,
} << 10;  // Bit 10

const P1_EN: u10 = switch (channels) {  // For Pipe 1...
  3 => 1,  // 3 UI Channels: Enable Pipe 1
  1 => 0,  // 1 UI Channel:  Disable Pipe 1
  else => unreachable,
} << 9;  // Bit 9

const P0_EN:   u9 = 1 << 8;  // Enable Pipe 0
const P0_FCEN: u1 = 1 << 0;  // Enable Pipe 0 Fill Color
const fill = P2_EN
  | P1_EN
  | P0_EN
  | P0_FCEN;

const BLD_FILL_COLOR_CTL = BLD_BASE_ADDRESS + 0x000;
putreg32(fill, BLD_FILL_COLOR_CTL);  // TODO: DMB

(channels is 3 when we render 3 Framebuffers)

§11 Appendix: Upcoming Features in PinePhone Display Driver

We have completed in Zig three major chunks of PinePhone’s Display Driver…

We have recently added these missing features…

We hope to complete the documentation for PinePhone’s Display Driver. Stay Tuned!

(The Guitar And The Fish!)