Zig NEWS

Cover image for NuttX RTOS for PinePhone: Render Graphics in Zig
Lup Yuen Lee
Lup Yuen Lee

Posted on

NuttX RTOS for PinePhone: Render Graphics in Zig

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]);
Enter fullscreen mode Exit fullscreen mode

We use the shorter readable form in Zig...

// In Zig: Get the framebuffer length
const len = framebuffer.len;
Enter fullscreen mode Exit fullscreen mode

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

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)
Enter fullscreen mode Exit fullscreen mode

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)...

  • Alpha: 8 bits

  • Red: 8 bits

  • Green: 8 bits

  • Blue: 8 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
};
Enter fullscreen mode Exit fullscreen mode

(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
);
Enter fullscreen mode Exit fullscreen mode

But first we paint some colours...

Blue, Green, Red Blocks on PinePhone

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

(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...

  • Alpha (8 bits): 0x80

  • Red (8 bits): 0x80

  • Green (8 bits): 0x00

  • Blue (8 bits): 0x00

(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)
};
Enter fullscreen mode Exit fullscreen mode

(fb_videoinfo_s comes from NuttX RTOS)

We'll test the Overlay Framebuffers later.

TODO

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
);
Enter fullscreen mode Exit fullscreen mode

(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 {
  ...
Enter fullscreen mode Exit fullscreen mode

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

  • channel is 1

    (We'll see Channels 2 and 3 later)

  • fbmem is fb0

    (Framebuffer Address)

  • fblen is 720 * 1280 * 4

    (Framebuffer Size in Bytes)

  • stride is 720 * 4

    (Number of Bytes in a Row)

  • xres is 720

    (Framebuffer Width)

  • yres is 1440

    (Framebuffer Height)

  • xoffset is 0

    (Framebuffer X Offset)

  • yoffset is 0

    (Framebuffer Y Offset)

Why is the Framebuffer Address declared as "?*anyopaque"?

That's because...

  • "*anyopaque" is similar to "void *" in C (non-null)

  • "?*anyopaque" is the same, except that null values are allowed

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...

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
);
Enter fullscreen mode Exit fullscreen mode

(Recall that fbmem is the Address of fb0)

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

  • fbmem.? returns the non-null value of fbmem

    (It halts with a Runtime Panic if null)

  • @ptrToInt converts fbmem from 64-bit Pointer to 64-bit Integer

  • @intCast converts the 64-bit Integer to 32-bit

    (It halts if it won't fit)

  • putreg32 writes the 32-bit Integer to the Address of the Hardware Register

    (As defined here)

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;
Enter fullscreen mode Exit fullscreen mode

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
  );
}
Enter fullscreen mode Exit fullscreen mode

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)

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
);
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

Framebuffer Attributes

(OVL_UI_ATTR_CTL, Page 102)

We set the Framebuffer Attributes...

  • Framebuffer is Opaque

    (Non-Transparent)

  • Framebuffer Pixel Format is 32-bit XRGB 8888

    ("X" means Pixel Alpha Value is ignored)

  • Framebuffer Alpha is mixed with Pixel Alpha

    (Effective Alpha Value = Framebuffer Alpha Value * Pixel’s Alpha Value)

  • Enable Framebuffer

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);
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

(Zig also supports Packed Structs with Bit Fields)

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);
Enter fullscreen mode Exit fullscreen mode

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

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)

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)
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

(We've seen height_width earlier)

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);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

Blender Attributes

(BLD_CTL, Page 110)

We set these (mysterious) Blender Attributes...

  • Coefficient for Destination Alpha Data Q[d] is 1-A[s]

  • Coefficient for Source Alpha Data Q[s] is 1

  • Coefficient for Destination Pixel Data F[d] is 1-A[s]

  • Coefficient for Source Pixel Data F[s] is 1

    (Why?)

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);
Enter fullscreen mode Exit fullscreen mode

We're almost done with our Blender Configuration...

TODO

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
Enter fullscreen mode Exit fullscreen mode

(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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

And that's all for rendering our Framebuffer!

Testing our PinePhone Display Driver

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

(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

Multiple Framebuffers

Can we render Multiple Framebuffers?

Yep PinePhone's Display Hardware supports up to 3 Framebuffers (pic above)...

  • One Base Framebuffer

    (Which we've just rendered)

  • Two Overlay Framebuffers

    (Which we'll render now)

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

Allocate Framebuffers

Earlier we have allocated the Base Framebuffer...

  • Framebuffer 0: 720 x 1440 pixels (Fullscreen)

    (With the Blue / Green / Red blocks)

Now we allocate 2 Overlay Framebuffers...

  • Framebuffer 1: 600 x 600 pixels (Square)

  • Framebuffer 2: 720 x 1440 pixels (Fullscreen)

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)
Enter fullscreen mode Exit fullscreen mode

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

Fill Framebuffers

Let's fill the pixels of the 2 Overlay Framebuffers (pic above)...

  • Framebuffer 1: Semi-Transparent Blue Square

  • Framebuffer 2: Semi-Transparent Green Circle

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;
}
Enter fullscreen mode Exit fullscreen mode

(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
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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

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

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);
Enter fullscreen mode Exit fullscreen mode

(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

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

(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...

  • Framebuffer 1: Semi-Transparent Blue Square

    (Sorry the Top Half vanished into the Blue Block)

  • Framebuffer 2: Semi-Transparent Green Circle

    (Pic above)

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

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!

Check out the other articles on NuttX RTOS 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

Multiple Framebuffers

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

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);
Enter fullscreen mode Exit fullscreen mode

Next we configure the Blender...

Multiple Framebuffers

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)...

  • Framebuffer 0 (Channel 1) connects to Blender Pipe 0

  • Framebuffer 1 (Channel 2) connects to Blender Pipe 1

  • Framebuffer 2 (Channel 3) connects to Blender Pipe 2

    (Pic above)

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
Enter fullscreen mode Exit fullscreen mode

(channels is 3 when we render 3 Framebuffers)

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
Enter fullscreen mode Exit fullscreen mode

(channels is 3 when we render 3 Framebuffers)

Appendix: Upcoming Features in PinePhone Display Driver

We have completed in Zig three major chunks of PinePhone's Display Driver...

  • Initialise PinePhone's ST7703 LCD Controller

    nuttx_panel_init

  • Initialise PinePhone's Allwinner A64 Display Engine

    de2_init

  • Render Graphics on PinePhone's Allwinner A64 Display Engine

    renderGraphics

Some features are still missing from our Zig Display Driver...

  • Initialise PinePhone's Allwinner A64 Timing Controller (TCON0)

    tcon0_init

  • Initialise PinePhone's Allwinner A64 MIPI Display Serial Interface (including MIPI DPHY)

    dsi_init

  • Turn on PinePhone's Backlight

    backlight_enable

We hope to implement the missing features and complete the documentation for PinePhone's Display Driver. Stay Tuned!

(The Guitar And The Fish!)

Top comments (2)

Collapse
 
xq profile image
Felix "xq" Queißner

Hell yeah!

That's a damn cool tech demo! I should try that on my on PinePhone, wanted to play around with Zig on it anyways.

Thanks for the detailled writeup!

Collapse
 
lupyuen profile image
Lup Yuen Lee

Thanks for reading! :-)