NuttX RTOS for PinePhone: MIPI Display Serial Interface

đź“ť 15 Dec 2022

Rendering graphics on PinePhone with Apache NuttX RTOS

Pine64 PinePhone (pic above) will soon support the rendering of graphics on the LCD Display… When we boot the official release of Apache NuttX RTOS!

We’re building the NuttX Display Driver for PinePhone in small chunks, starting with the driver for MIPI Display Serial Interface.

In this article we’ll learn…

Let’s continue the (super looong) journey from our NuttX Porting Journal…

Inside our Complete Display Driver for PinePhone

1 Complete Display Driver for PinePhone

NuttX will render graphics on PinePhone’s LCD Display…

What’s inside the Display Driver for PinePhone?

Through Reverse Engineering (and plenty of experimenting), we discovered that these steps are needed to create a Complete Display Driver for PinePhone (pic above)…

  1. Turn on PinePhone’s Display Backlight

    (Through Programmable I/O and Pulse-Width Modulation)

  2. Initialise Allwinner A64’s Timing Controller (TCON0)

    (Which will pump pixels continuously to the LCD Display)

  3. Initialise PinePhone’s Power Management Integrated Circuit (PMIC) and wait 15 milliseconds

    (To power on PinePhone’s LCD Panel)

  4. Enable Allwinner A64’s MIPI Display Serial Interface (DSI)

    (So we can send MIPI DSI commands to the LCD Panel)

  5. Enable Allwinner A64’s MIPI Display Physical Layer (D-PHY)

    (Which is the communications layer inside MIPI DSI)

  6. Reset PinePhone’s LCD Panel and wait 15 milliseconds

    (Prep it to receive MIPI DSI Commands)

  7. Initialise PinePhone’s LCD Controller (Sitronix ST7703)

    (Send the Initialisation Commands over MIPI DSI)

  8. Start Allwinner A64’s MIPI DSI in HSC and HSD Mode

    (High Speed Clock Mode with High Speed Data Transmission)

  9. Initialise Allwinner A64’s Display Engine (DE)

    (Start pumping pixels from DE to Timing Controller TCON0)

  10. Wait a while

    (160 milliseconds)

  11. Render Graphics with Allwinner A64’s Display Engine (DE)

    (Start pumping pixels from RAM Framebuffers to DE via Direct Memory Access)

Let’s talk about each step and their NuttX Drivers…

LCD Display on PinePhone Schematic (Page 2)

LCD Display on PinePhone Schematic (Page 2)

2 NuttX Driver for MIPI Display Serial Interface

The very first NuttX Driver we’ve implemented is for MIPI Display Serial Interface (DSI).

Why is MIPI DSI needed in PinePhone?

PinePhone talks to its LCD Panel (Xingbangda XBD599) via the MIPI DSI Bus on Allwinner A64 SoC.

That’s why we need a MIPI DSI Driver in the NuttX Kernel.

So our MIPI DSI Driver will render graphics on PinePhone’s LCD Display?

It gets complicated…

Thus our MIPI DSI Driver is called only at startup to initialise the LCD Controller (ST7703).

Why so complicated?

Yeah but this Rendering Pipeline is super efficient!

PinePhone doesn’t need to handle Interrupts while rendering the display… Everything is done in Hardware! (Allwinner A64 SoC)

The pixel data is pumped from RAM Framebuffers via Direct Memory Access (DMA). Which is also done in Hardware.

Let’s dive inside our MIPI DSI Driver…

Composing a MIPI DSI Short Packet

Composing a MIPI DSI Short Packet

3 Send MIPI DSI Packet

How do we send MIPI DSI Commands to PinePhone’s LCD Controller?

Let’s take one MIPI DSI Command that initialises the ST7703 LCD Controller: test_a64_mipi_dsi.c

// Command #1 to init ST7703
const uint8_t cmd1[] = { 
  0xB9,  // SETEXTC (Page 131): Enable USER Command
  0xF1,  // Enable User command
  0x12,  // (Continued)
  0x83   // (Continued)
};

// Send the command to ST7703 over MIPI DSI
write_dcs(cmd1, sizeof(cmd1));

(ST7703 needs 20 Initialisation Commands)

write_dcs sends our command to the MIPI DSI Bus in 3 DCS Formats…

(DCS means Display Command Set)

/// Write the DCS Command to MIPI DSI
static int write_dcs(const uint8_t *buf, size_t len) {
  // Do DCS Short Write or Long Write depending on command length.
  // A64_MIPI_DSI_VIRTUAL_CHANNEL is 0.
  switch (len) {
    // DCS Short Write (without parameter)
    case 1:
      a64_mipi_dsi_write(A64_MIPI_DSI_VIRTUAL_CHANNEL, 
        MIPI_DSI_DCS_SHORT_WRITE, 
        buf, len);
      break;

    // DCS Short Write (with parameter)
    case 2:
      a64_mipi_dsi_write(A64_MIPI_DSI_VIRTUAL_CHANNEL, 
        MIPI_DSI_DCS_SHORT_WRITE_PARAM, 
        buf, len);
      break;

    // DCS Long Write
    default:
      a64_mipi_dsi_write(A64_MIPI_DSI_VIRTUAL_CHANNEL, 
        MIPI_DSI_DCS_LONG_WRITE, 
        buf, len);
      break;
  };

(Source)

(We talk to MIPI DSI Bus on Virtual Channel 0)

a64_mipi_dsi_write comes from our NuttX MIPI DSI Driver: a64_mipi_dsi.c

// Transmit the payload data to the MIPI DSI Bus as a MIPI DSI Short or
// Long Packet. This function is called to initialize the LCD Controller.
// Assumes that the MIPI DSI Block has been enabled on the SoC.
// Returns the number of bytes transmitted.
ssize_t a64_mipi_dsi_write(
  uint8_t channel,       // Virtual Channel (0)
  enum mipi_dsi_e cmd,   // DCS Command (Data Type)
  const uint8_t *txbuf,  // Payload data for the packet
  size_t txlen)  // Length of payload data (Max 65541 bytes)
{
  ...
  // Compose Short or Long Packet depending on DCS Command
  switch (cmd) {
    // For DCS Long Write:
    // Compose Long Packet
    case MIPI_DSI_DCS_LONG_WRITE:
      pktlen = mipi_dsi_long_packet(pkt, sizeof(pkt), channel, cmd, txbuf, txlen);
      break;

    // For DCS Short Write (with and without parameter):
    // Compose Short Packet
    case MIPI_DSI_DCS_SHORT_WRITE:
      pktlen = mipi_dsi_short_packet(pkt, sizeof(pkt), channel, cmd, txbuf, txlen);
      break;

    case MIPI_DSI_DCS_SHORT_WRITE_PARAM:
      pktlen = mipi_dsi_short_packet(pkt, sizeof(pkt), channel, cmd, txbuf, txlen);
      break;
  };

Our NuttX Driver calls…

Then our NuttX Driver writes the Short or Long Packet to the MIPI DSI Registers of Allwinner A64: a64_mipi_dsi.c

  // Write the packet to DSI Low Power Transmit Package Register
  // at DSI Offset 0x300 (A31 Page 856)
  // A64_DSI_ADDR is the A64 DSI Base Address: 0x01ca0000
  addr = A64_DSI_ADDR + 0x300;
  for (i = 0; i < pktlen; i += 4) {

    // Fetch the next 4 bytes, fill with 0 if not available
    const uint32_t b[4] = {
      pkt[i],
      (i + 1 < pktlen) ? pkt[i + 1] : 0,
      (i + 2 < pktlen) ? pkt[i + 2] : 0,
      (i + 3 < pktlen) ? pkt[i + 3] : 0
    };

    // Merge the next 4 bytes into a 32-bit value
    const uint32_t v = b[0] + (b[1] << 8) + (b[2] << 16) + (b[3] << 24);

    // Write the 32-bit value to DSI Low Power Transmit Package Register
    modreg32(v, 0xffffffff, addr);
    addr += 4;
  }

  // Omitted: Wait for DSI Transmission to complete

And that’s how our MIPI DSI Packet gets transmitted to the ST7703 LCD Controller, over the MIPI DSI Bus!

We do this 20 times, to send 20 Initialisation Commands to the ST7703 LCD Controller…

But wait… We haven’t enabled the MIPI DSI Hardware yet!

4 Enable MIPI DSI and D-PHY

At startup we call the MIPI DSI Driver to send Initialisation Commands to the LCD Controller…

What about other MIPI DSI Operations?

Before sending MIPI DSI Packets, our NuttX Driver needs to enable 2 chunks of hardware on Allwinner A64 SoC…

And after sending the MIPI DSI Packets to initialise our LCD Controller, we need to…

How did we create all this code for our NuttX Driver?

Our NuttX Driver for MIPI DSI (and MIPI D-PHY) lives in the NuttX Kernel as…

We created the above NuttX Source Files by converting our MIPI DSI Driver from Zig to C…

(Why Zig? We’ll come back to this)

We created the Zig Drivers by Reverse-Engineering the logs that we captured from PinePhone’s p-boot Bootloader…

Why Reverse Engineer? Because a lot of details are missing from the official docs for Allwinner A64…

Let’s talk about the Zig-to-C Conversion…

Converting Zig to C

5 Convert Zig to C

Our NuttX Driver MIPI Driver was converted from Zig to C…

Was it difficult to convert Zig to C?

Not at all!

This is the Zig Code for our MIPI DSI Driver: display.zig

// Compose MIPI DSI Short Packet
fn composeShortPacket(
  pkt: []u8,    // Buffer for the returned packet
  channel: u8,  // Virtual Channel
  cmd: u8,      // DCS Command (Data Type)
  buf: [*c]const u8,  // Payload data for the packet
  len: usize          // Length of payload data (1 or 2 bytes)
) []const u8 {        // Returns the Short Packet
  // Data Identifier (DI) (1 byte):
  // - Virtual Channel Identifier (Bits 6 to 7)
  // - Data Type (Bits 0 to 5)
  const vc: u8 = channel;
  const dt: u8 = cmd;
  const di: u8 = (vc << 6) | dt;

  // Data (2 bytes), fill with 0 if Second Byte is missing
  const data = [2]u8 {
    buf[0],                       // First Byte
    if (len == 2) buf[1] else 0,  // Second Byte
  };

  // Data Identifier + Data (3 bytes): For computing Error Correction Code (ECC)
  const di_data = [3]u8 { di, data[0], data[1] };

  // Compute Error Correction Code (ECC) for Data Identifier + Word Count
  const ecc: u8 = computeEcc(di_data);

  // Packet Header (4 bytes):
  // - Data Identifier + Data + Error Correction Code
  const header = [4]u8 { di_data[0], di_data[1], di_data[2], ecc };

  // Packet:
  // - Packet Header (4 bytes)
  const pktlen = header.len;
  std.mem.copy(u8, pkt[0..header.len], &header); // 4 bytes

  // Return the packet
  const result = pkt[0..pktlen];
  return result;
}

We manually converted the Zig code to C like so: mipi_dsi.c

// Compose MIPI DSI Short Packet.
// Returns the Packet Length.
ssize_t mipi_dsi_short_packet(
  uint8_t *pktbuf,       // Buffer for the returned packet
  size_t pktlen,         // Size of the packet buffer
  uint8_t channel,       // Virtual Channel
  enum mipi_dsi_e cmd,   // DCS Command (Data Type)
  const uint8_t *txbuf,  // Payload data for the packet
  size_t txlen)          // Length of payload data (1 or 2 bytes)
{
  // Data Identifier (DI) (1 byte):
  // Virtual Channel Identifier (Bits 6 to 7)
  // Data Type (Bits 0 to 5)
  const uint8_t vc = channel;
  const uint8_t dt = cmd;
  const uint8_t di = (vc << 6) | dt;

  // Data (2 bytes): Fill with 0 if Second Byte is missing
  const uint8_t data[2] = {
    txbuf[0],                     // First Byte
    (txlen == 2) ? txbuf[1] : 0,  // Second Byte
  };

  // Data Identifier + Data (3 bytes):
  // For computing Error Correction Code (ECC)
  const uint8_t di_data[3] = { di, data[0], data[1] };

  // Compute ECC for Data Identifier + Word Count
  const uint8_t ecc = compute_ecc(di_data, sizeof(di_data));

  // Packet Header (4 bytes):
  // Data Identifier + Data + Error Correction Code
  const uint8_t header[4] = { di_data[0], di_data[1], di_data[2], ecc };

  // Packet Length is Packet Header Size (4 bytes)
  const size_t len = sizeof(header);

  // Copy Packet Header to Packet Buffer
  memcpy(pktbuf, header, sizeof(header));  // 4 bytes

  // Return the Packet Length
  return len;
}

The C Code looks highly similar to the original Zig Code! Thus manually converting Zig to C (line by line) is a piece of cake.

(According to Matheus Catarino França, the Zig-to-C Auto-Translation might work too)

Testing MIPI DSI Driver

6 Test MIPI DSI Driver

Our NuttX Display Driver for PinePhone is incomplete…

How do we test the MIPI DSI Driver in the NuttX Kernel?

Right now we have implemented the following in the NuttX Kernel…

But to render graphics on PinePhone we need the following drivers, which are still in Zig (pending conversion to C)…

Running an Integration Test across the C and Zig Drivers will be a little tricky. This is how we run the test…

We created this program in Zig that calls the C and Zig Drivers, in the right sequence: render.zig

/// Main Function that will be called by NuttX
/// when we run the `hello` app
pub export fn hello_main(argc: c_int, argv: [*c]const [*c]u8) c_int {
  // Render graphics on PinePhone in Zig and C...
  // Turn on Display Backlight (in Zig)
  // Init Timing Controller TCON0 (in Zig)
  // Init PMIC (in Zig)

  backlight.backlight_enable(90);
  tcon.tcon0_init();
  pmic.display_board_init();

  // Enable MIPI DSI Block (in C)
  // Enable MIPI Display Physical Layer (in C)

  _ = a64_mipi_dsi_enable();
  _ = a64_mipi_dphy_enable();

  // Reset LCD Panel (in Zig)
  panel.panel_reset();

  // Init LCD Panel (in C)
  // Start MIPI DSI HSC and HSD (in C)

  _ = pinephone_panel_init();
  _ = a64_mipi_dsi_start();

  // Init Display Engine (in Zig)
  // Wait a while
  // Render Graphics with Display Engine (in Zig)

  de2_init();
  _ = c.usleep(160000);
  renderGraphics(3);  // Render 3 UI Channels

(pinephone_panel_init is defined here)

Then we compile our Zig Test Program (targeting PinePhone) and link it with NuttX…

##  Configure NuttX
cd nuttx
./tools/configure.sh pinephone:nsh
make menuconfig

##  Select "System Type > Allwinner A64 Peripheral Selection > MIPI DSI"
##  Select "Build Setup > Debug Options > Graphics Debug Features > Graphics Errors / Warnings / Informational Output"
##  Save and exit menuconfig

##  Build NuttX
make

##  Download the Zig Test Program
pushd $HOME
git clone https://github.com/lupyuen/pinephone-nuttx
cd pinephone-nuttx

##  Compile the Zig App for PinePhone 
##  (armv8-a with cortex-a53)
##  TODO: Change "$HOME/nuttx" to your NuttX Project Directory
zig build-obj \
  --verbose-cimport \
  -target aarch64-freestanding-none \
  -mcpu cortex_a53 \
  -isystem "$HOME/nuttx/nuttx/include" \
  -I "$HOME/nuttx/apps/include" \
  render.zig

##  Copy the compiled app to NuttX and overwrite `hello.o`
##  TODO: Change "$HOME/nuttx" to your NuttX Project Directory
cp render.o \
  $HOME/nuttx/apps/examples/hello/*hello.o  

##  Return to the NuttX Folder
popd

##  Link the Compiled Zig App with NuttX
make

We boot NuttX on PinePhone (via microSD) and run the Zig Test Program (pic above)…

NuttShell (NSH) NuttX-11.0.0-pinephone

nsh> uname -a
NuttX 11.0.0-pinephone 2a1577a-dirty Dec  9 2022 13:57:47 arm64 pinephone

nsh> hello 0

(Source)

Yep our Zig Test Program renders the Test Pattern successfully on PinePhone’s LCD Display! (Like this)

Which means the NuttX Kernel Driver for MIPI DSI is working OK!

Here’s the Test Log for our Zig Test Program running on NuttX and PinePhone…

6.1 Unit Testing

What about Unit Testing? Can we test the MIPI DSI Driver without Zig?

Yep! Our MIPI DSI Driver simply writes values to a bunch of A64 Hardware Registers, like so: a64_mipi_dsi.c

// DSI Configuration Register 1 (A31 Page 846)
// Set Video_Start_Delay (Bits 4 to 16) to 1468 (Line Delay)
// Set Video_Precision_Mode_Align (Bit 2) to 1 (Fill Mode)
// Set Video_Frame_Start (Bit 1) to 1 (Precision Mode)
// Set DSI_Mode (Bit 0) to 1 (Video Mode)
#define DSI_BASIC_CTL1_REG (A64_DSI_ADDR + 0x14)
#define DSI_MODE                   (1 << 0)
#define VIDEO_FRAME_START          (1 << 1)
#define VIDEO_PRECISION_MODE_ALIGN (1 << 2)
#define VIDEO_START_DELAY(n)       ((n) << 4)

dsi_basic_ctl1 = VIDEO_START_DELAY(1468) |
                 VIDEO_PRECISION_MODE_ALIGN |
                 VIDEO_FRAME_START |
                 DSI_MODE;
putreg32(dsi_basic_ctl1, DSI_BASIC_CTL1_REG);

// Include Test Code to verify Register Addresses and Written Values
#include "../../pinephone-nuttx/test/test_a64_mipi_dsi2.c"

So we only need to ensure that the Hardware Register Addresses and the Written Values are correct.

To do that, we use Assertion Checks to verify the Addresses and Values: test_a64_mipi_dsi2.c

// Test Code to verify Register Addresses and Written Values
DEBUGASSERT(DSI_BASIC_CTL1_REG == 0x1ca0014);
DEBUGASSERT(dsi_basic_ctl1 == 0x5bc7);

If the Addresses or Values are incorrect, our MIPI DSI Driver halts with an Assertion Failure.

(We remove the Assertion Checks in the final version of our driver)

What about a smaller, self-contained Unit Test for MIPI DSI?

This is the Unit Test that verifies our NuttX Driver correctly composes MIPI DSI Packets (Long / Short / Short with Parameter)…

We run this Unit Test locally on our computer, here’s how…

6.2 Local Testing

Can we test the MIPI DSI Driver on our Local Computer? Without running on PinePhone?

Most certainly! In fact we test the MIPI DSI Driver on our Local Computer first before testing on PinePhone. Here’s how…

Remember that our MIPI DSI Driver simply writes values to a bunch of A64 Hardware Registers. So we only need to ensure that the Hardware Register Addresses and the Written Values are correct.

To target our Local Computer, we created a Test Scaffold that simulates the NuttX Build Environment: test.c

// Simulate NuttX Build Environment
#include <nuttx/arch.h>
#include "arm64_arch.h"
#include "mipi_dsi.h"
#include "a64_mipi_dsi.h"
#include "a64_mipi_dphy.h"

// Test Scaffold for Local Testing
int main() {

  // Test: Enable MIPI DSI Block
  a64_mipi_dsi_enable();

  // Test: Enable MIPI Display Physical Layer (DPHY)
  a64_mipi_dphy_enable();

  // Test: Initialise LCD Controller (ST7703)
  pinephone_panel_init();

  // Test: Start MIPI DSI HSC and HSD
  a64_mipi_dsi_start();

  // Test: MIPI DSI Packets
  mipi_dsi_test();
}

Then we compile the Test Scaffold and run it on our Local Computer: run.sh

## Compile Test Code for Local Testing
gcc \
  -o test \
  -I . \
  -I ../../nuttx/arch/arm64/src/a64 \
  test.c \
  ../../nuttx/arch/arm64/src/a64/a64_mipi_dphy.c \
  ../../nuttx/arch/arm64/src/a64/a64_mipi_dsi.c \
  ../../nuttx/arch/arm64/src/a64/mipi_dsi.c

## Run the Local Test
./test

## Capture the Actual Test Log
./test >test.log

## Diff the Actual and Expected Test Logs
diff \
  --ignore-all-space \
  expected.log \
  test.log

Note that we capture the Actual Test Log and we diff it with the Expected Test Log.

That’s how we detect discrepancies in the Register Addresses and the Written Values…

Enable MIPI DSI Bus
  *0x1c20060: clear 0x2, set 0x2
  *0x1c202c0: clear 0x2, set 0x2
Enable DSI Block
  *0x1ca0000 = 0x1
  *0x1ca0010 = 0x30000
  *0x1ca0060 = 0xa
  *0x1ca0078 = 0x0
Set Instructions
  *0x1ca0020 = 0x1f
  *0x1ca0024 = 0x10000001
  *0x1ca0028 = 0x20000010
  *0x1ca002c = 0x2000000f
  *0x1ca0030 = 0x30100001
  *0x1ca0034 = 0x40000010
  *0x1ca0038 = 0xf
  *0x1ca003c = 0x5000001f
  ...

(Source)

Let’s talk about the missing parts of our NuttX Driver…

Inside our Complete Display Driver for PinePhone

Inside our Complete Display Driver for PinePhone

7 Upcoming NuttX Drivers

What about the rest of our NuttX Display Driver?

We talked earlier about the Grand Plan for our NuttX Display Driver (pic above) that’s deeply layered like an Onion Kueh Lapis…

Today we’ve implemented the MIPI Display Serial Interface and MIPI Display Physical Layer for our NuttX Display Driver (lower part of pic above)…

We’re now building the NuttX Drivers for the remaining features (upper part of pic above), converting our Zig code to C…

  1. Timing Controller (TCON0): To render PinePhone’s LCD Display, the MIPI DSI Controller on Allwinner A64 needs to receive a continuous stream of pixels…

    Which will be provided by Allwinner A64’s Timing Controller (TCON0).

    (TCON0 will receive the pixel stream from A64’s Display Engine)

    Our NuttX Driver shall program TCON0 to send the stream of pixels to the MIPI DSI Controller.

    This will be implemented in our new Timing Controller (TCON0) Driver for NuttX.

    (Details in the Appendix)

  2. Display Engine (DE): Allwinner A64’s Display Engine (DE) reads the Graphics Framebuffers in RAM (up to 3 Framebuffers)…

    And streams the pixels to the Timing Controller (TCON0).

    Our NuttX Driver shall configure DE to read the Framebuffers via Direct Memory Access (DMA).

    With DMA, updates to the Framebuffers will be instantly visible on PinePhone’s LCD Display.

    This will be implemented in our new Display Engine Driver for NuttX.

    (Details in the Appendix)

  3. Display Backlight: We won’t see anything on PinePhone’s LCD Display… Until we switch on the Display Backlight!

    PinePhone’s Display Backlight is controlled by A64’s…

    To turn on the Display Backlight, we’ll call PIO and PWM in our new Board LCD Driver for NuttX.

    (Details in the Appendix)

  4. LCD Panel: Before sending Initialisation Commands to the ST7703 LCD Controller, we need to reset the LCD Panel.

    We do this with Allwinner A64’s Programmable Input / Output (PIO), implemented in a64_pio.c. (Works like GPIO)

    To reset the LCD Panel, we’ll call PIO in our new Board LCD Driver for NuttX.

    (Details in the Appendix)

  5. Power Management Integrated Circuit (PMIC): To power on the LCD Display, we need to program PinePhone’s Power Management Integrated Circuit (PMIC).

    The AXP803 PMIC is connected on Allwinner A64’s Reduced Serial Bus (RSB). (Works like I2C)

    We’ll control the PMIC over RSB in our new Board LCD Driver for NuttX.

    (Details in the Appendix)

Very soon the official NuttX Kernel will be rendering graphics on PinePhone’s LCD Display. Stay Tuned!

Converting Zig to C

8 Why Zig

Why did we start with Zig? Why not code directly in C?

Building a NuttX Display Driver for PinePhone feels like a risky challenge…

Zig seems to work really well because…

Along the way we created an Executable Specification of Allwinner A64’s Display Interfaces… A huge bunch of Hardware Register Addresses and their Expected Values: display.zig

// Set Video Start Delay
// DSI_BASIC_CTL1_REG: DSI Offset 0x14 (A31 Page 846)
// Set Video_Start_Delay (Bits 4 to 16) to 1468 (Line Delay)
// Set Video_Precision_Mode_Align (Bit 2) to 1 (Fill Mode)
// Set Video_Frame_Start (Bit 1) to 1 (Precision Mode)
// Set DSI_Mode (Bit 0) to 1 (Video Mode)
// Note: Video_Start_Delay is actually 13 bits, not 8 bits as documented in the A31 User Manual

const DSI_BASIC_CTL1_REG = DSI_BASE_ADDRESS + 0x14;
comptime{ assert(DSI_BASIC_CTL1_REG == 0x1ca0014); }

const Video_Start_Delay:          u17 = 1468 << 4;
const Video_Precision_Mode_Align: u3  = 1    << 2;
const Video_Frame_Start:          u2  = 1    << 1;
const DSI_Mode:                   u1  = 1    << 0;
const DSI_BASIC_CTL1 = Video_Start_Delay
  | Video_Precision_Mode_Align
  | Video_Frame_Start
  | DSI_Mode;
comptime{ assert(DSI_BASIC_CTL1 == 0x5bc7); }
putreg32(DSI_BASIC_CTL1, DSI_BASIC_CTL1_REG);  // TODO: DMB

Which is really neat because…

With the Executable Spec, maybe someday we’ll emulate PinePhone Hardware with QEMU or FPGA!

Was it worth the effort? Would you do it again in Zig?

Yes and yes! Zig is excellent for prototyping new Device Drivers for Operating Systems.

Once again… Why are we doing all this?

PinePhone is becoming popular as the Edgy, Alternative Smartphone for folks who love to tinker with their gadgets. (And it’s still in stock!)

The best way to understand what’s really inside PinePhone: Creating our own PinePhone Display Driver.

That’s why we’re doing all this PinePhone Reverse-Engineering… First to Zig, then to C!

What about other cool open-source Allwinner A64 gadgets like TERES-I?

Someday we might! But first let’s uncover all the secrets inside PinePhone.

Testing our PinePhone Display Driver on Apache NuttX RTOS

Testing our PinePhone Display Driver on Apache NuttX RTOS

9 What’s Next

Very soon the official NuttX Kernel will be rendering graphics on PinePhone’s LCD Display!

Stay Tuned for Updates!

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/dsi3.md

Inside our Complete Display Driver for PinePhone

Inside our Complete Display Driver for PinePhone

10 Appendix: Upcoming NuttX Drivers

We talked earlier about our implementation of the MIPI Display Serial Interface and MIPI Display Physical Layer for our NuttX Display Driver (lower part of pic above)…

This section explains how we’re building the NuttX Drivers for the remaining features (upper part of pic above), by converting our Zig Drivers to C…

Allwinner A64 Timing Controller (TCON0)

Allwinner A64 Timing Controller (TCON0)

10.1 Timing Controller (TCON0)

To render PinePhone’s LCD Display, the MIPI DSI Controller on Allwinner A64 needs to receive a continuous stream of pixels…

Which will be provided by Allwinner A64’s Timing Controller (TCON0).

(TCON0 will receive the pixel stream from A64’s Display Engine)

Our NuttX Driver shall program TCON0 to send the stream of pixels to the MIPI DSI Controller.

This will be implemented in our new Timing Controller (TCON0) Driver for NuttX…

We have converted the above TCON0 Driver from Zig to C and added it to NuttX Mainline…

Allwinner A64 Display Engine

Allwinner A64 Display Engine

10.2 Display Engine

Allwinner A64’s Display Engine (DE) reads the Graphics Framebuffers in RAM (up to 3 Framebuffers, pic above)…

And streams the pixels to the ST7703 LCD Controller for display, via the A64 Timing Controller (TCON0).

Our NuttX Driver shall configure DE to read the Framebuffers via Direct Memory Access (DMA). With DMA, updates to the Framebuffers will be instantly visible on PinePhone’s LCD Display.

This will be implemented in our new Display Engine Driver for NuttX in two parts…

UPDATE: We have implemented the Display Engine Driver in NuttX Kernel…

PinePhone Display Backlight

PinePhone Display Backlight

10.3 Display Backlight

We won’t see anything on PinePhone’s LCD Display… Until we switch on the Display Backlight!

PinePhone’s Display Backlight is controlled by A64’s…

To turn on the Display Backlight, we’ll call PIO and PWM in our new Board LCD Driver for NuttX…

We’ll convert the above Backlight Driver from Zig to C. Work-in-progress…

10.4 LCD Panel

Before sending Initialisation Commands to the ST7703 LCD Controller, we need to reset the LCD Panel.

We do this with Allwinner A64’s Programmable Input / Output (PIO), implemented in a64_pio.c. (Works like GPIO)

To reset the LCD Panel, we’ll call PIO in our new Board LCD Driver for NuttX…

Also we’ll add the code to send the Initialisation Commands to the ST7703 LCD Controller (via MIPI DSI)…

We’re converting the above from Zig to C. Work-in-progress…

10.5 Power Management Integrated Circuit

To power on the LCD Display, we need to program PinePhone’s Power Management Integrated Circuit (PMIC).

The AXP803 PMIC is connected on Allwinner A64’s Reduced Serial Bus (RSB). (Works like I2C)

We’ll control the PMIC over RSB in our new Board LCD Driver for NuttX…

We’ll convert the above drivers from Zig to C. Work-in-progress for Reduced Serial Bus Driver and PMIC Driver…