NuttX RTOS for PinePhone: LCD Panel

đź“ť 28 Dec 2022

Our PinePhone LCD Driver renders a Test Pattern on Apache NuttX RTOS

Apache NuttX RTOS now boots on Pine64 PinePhone and renders a Test Pattern! (Pic above)

Let’s find out what’s inside our NuttX Kernel Driver for PinePhone’s LCD Panel…

LCD Display on PinePhone Schematic (Page 2)

LCD Display on PinePhone Schematic (Page 2)

1 Xingbangda XBD599 LCD Panel

The LCD Panel inside PinePhone is Xingbangda XBD599 (兴邦达) with…

(Includes a Capacitive Touch Panel, but we won’t touch it today heh)

The Xingbangda XBD599 LCD Panel is connected to PinePhone’s Allwinner A64 SoC over a MIPI Display Serial Interface (DSI). (Pic above)

Why is there an ST7703 LCD Controller inside the LCD Panel?

Talking over MIPI DSI can get complicated… It runs on packets of data with CRCs and Checksums, over multiple data lanes.

Later we’ll see that ST7703 LCD Controller handles…

Let’s start with something simpler without ST7703…

Backlight on PinePhone Schematic (Page 11)

Backlight on PinePhone Schematic (Page 11)

2 LCD Panel Backlight

First thing we do when booting PinePhone is to turn on the LCD Panel Backlight… Otherwise the LCD Display stays dark!

The PinePhone Schematic (Page 11) says that the LCD Panel Backlight is controlled by two pins (pic above)…

The two pins are connected to Diodes AP3127, which is a PWM Controller. (Pic above)

This is how we turn on the backlight in our NuttX LCD Driver: pinephone_lcd.c

// Turn on the LCD Backlight
int pinephone_lcd_backlight_enable(
  uint32_t percent  // Brightness percentage, typically 90
) {
  // Configure PL10 for PWM
  a64_pio_config(LCD_PWM);  // LCD_PWM is PL10

We begin by configuring PL10 for PWM.

(a64_pio_config comes from our NuttX PIO Driver)

Next we disable PWM through the R_PWM Port on Allwinner A64…

  // R_PWM Control Register (Undocumented)
  // Assume same as PWM Control Register (A64 Page 194)
  // Set SCLK_CH0_GATING (Bit 6) to 0 (Mask)
  modreg32(  // Modify a Register...
    0,                // Set these bits
    SCLK_CH0_GATING,  // Mask these bits
    R_PWM_CTRL_REG    // Register Address
  );

(R_PWM is implemented in the Allwinner A100 Coprocessor)

The R_PWM Port isn’t documented in the Allwinner A64 User Manual.

But thanks to Reverse-Engineering, we figured out how it works: pinephone_lcd.c

  // R_PWM Control Register (Undocumented)
  // Assume same as PWM Control Register (A64 Page 194)
  #define R_PWM_CTRL_REG            (A64_RPWM_ADDR + 0)
  #define PWM_CH0_PRESCAL(n)        ((n) << 0)
  #define PWM_CH0_EN                (1 << 4)
  #define SCLK_CH0_GATING           (1 << 6)

  // R_PWM Channel 0 Period Register (Undocumented)
  // Assume same as PWM Channel 0 Period Register (A64 Page 195)
  #define R_PWM_CH0_PERIOD          (A64_RPWM_ADDR + 4)
  #define PWM_CH0_ENTIRE_ACT_CYS(n) ((n) << 0)
  #define PWM_CH0_ENTIRE_CYS(n)     ((n) << 16)

Then we set the PWM Period and Duty Cycle: pinephone_lcd.c

  // R_PWM Channel 0 Period Register (Undocumented)
  // Assume same as PWM Channel 0 Period Register (A64 Page 195)
  // Set PWM_CH0_ENTIRE_CYS (Bits 16 to 31) to PWM Period
  // Set PWM_CH0_ENTIRE_ACT_CYS (Bits 0 to 15) to PWM Period * Percent / 100
  // `BACKLIGHT_PWM_PERIOD` is 1,199 PWM cycles
  // `percent` (brightness percent) is typically 90
  uint32_t period = 
    PWM_CH0_ENTIRE_CYS(BACKLIGHT_PWM_PERIOD) |
    PWM_CH0_ENTIRE_ACT_CYS(BACKLIGHT_PWM_PERIOD * percent / 100);
  putreg32(           // Write to Register...
    period,           // Register Value
    R_PWM_CH0_PERIOD  // Register Address
  );

Finally we enable PWM…

  // R_PWM Control Register (Undocumented)
  // Assume same as PWM Control Register (A64 Page 194)
  // Set SCLK_CH0_GATING (Bit 6) to 1 (Pass)
  // Set PWM_CH0_EN (Bit 4) to 1 (Enable)
  // Set PWM_CH0_PRESCAL (Bits 0 to 3) to 0b1111 (Prescaler 1)
  uint32_t ctrl = SCLK_CH0_GATING |
    PWM_CH0_EN |
    PWM_CH0_PRESCAL(0b1111);
  putreg32(         // Write to Register...
    ctrl,           // Register Value
    R_PWM_CTRL_REG  // Register Address
  );

One last thing: We configure PH10 for Output and set it to High…

  // Configure PH10 for Output
  a64_pio_config(LCD_BL_EN);  // LCD_BL_EN is PH10

  // Set PH10 to High
  a64_pio_write(LCD_BL_EN, true);
  return OK;
}

(a64_pio_write comes from our NuttX PIO Driver)

This enables the AP3127 PWM Controller. And switches on the LCD Backlight! (Pic above)

Now that the Backlight is on, let’s reset the LCD Panel and prepare for action…

LCD Panel Reset (PD23) on PinePhone Schematic (Page 11)

LCD Panel Reset (PD23) on PinePhone Schematic (Page 11)

3 Reset LCD Panel

At startup, we need to toggle the LCD Reset from Low to High in this specific sequence…

  1. Reset LCD Panel to Low

  2. Power on the LCD Panel’s MIPI Display Serial Interface (DSI)

    (Via the Power Management Integrated Circuit)

  3. Wait 15 milliseconds

  4. Enable MIPI DSI on Allwinner A64 SoC

  5. Enable MIPI D-PHY on Allwinner A64 SoC

  6. Reset LCD Panel to High

Followed by more MIPI DSI and Display Engine operations.

How will we toggle LCD Reset?

The PinePhone Schematic (Page 11) says that LCD Reset is controlled on PD23. (Pic above)

(DLDO2 is powered by the PMIC)

Let’s do it: pinephone_lcd.c

// Reset the LCD Panel
int pinephone_lcd_panel_reset(
  bool val  // Set Reset to High or Low
) {
  // Reset LCD Panel at PD23 (Active Low)
  // Configure PD23 for Output
  a64_pio_config(LCD_RESET);  // LCD_RESET is PD23
  
  // Set PD23 to High or Low
  a64_pio_write(LCD_RESET, val);
  return OK;
}

The code above configures PD23 for Output, and sets PD23 to High or Low.

(a64_pio_config comes from our NuttX PIO Driver)

(a64_pio_write too)

And that’s how we reset the LCD Panel! Now we power on the LCD Panel…

AXP803 PMIC on PinePhone Schematic (Page 3)

AXP803 PMIC on PinePhone Schematic (Page 3)

4 Power On LCD Panel

How do we power on the LCD Panel?

The LCD Panel won’t respond to our MIPI DSI Commands until we power it on.

The PinePhone Schematic (Page 3) says that the MIPI DSI Port of the LCD Panel (DLDO2 / VCC-MIPI) is powered by…

This is how we talk to the AXP803 PMIC: pinephone_pmic.c

// Initialise the Power Mgmt IC
int pinephone_pmic_init(void) {
  // Set DLDO1 Voltage to 3.3V.
  // DLDO1 powers the Front Camera / USB HSIC / I2C Sensors.

  // DLDO1 Voltage Control (AXP803 Page 52)
  // Set Voltage (Bits 0 to 4) to 26 (2.6V + 0.7V = 3.3V)
  pmic_write(               // Write to PMIC Register...
    DLDO1_VOLTAGE_CONTROL,  // PMIC Register
    DLDO1_VOLTAGE(26)       // PMIC Value
  );

  // Power on DLDO1:
  // Output Power On-Off Control 2 (AXP803 Page 51)
  // Set DLDO1 On-Off Control (Bit 3) to 1 (Power On)
  pmic_clrsetbits(  // Clear and set bits in PMIC Register...
    OUTPUT_POWER_ON_OFF_CONTROL2,  // Set these bits
    0,                             // Clear these bits
    DLDO1_ON_OFF_CONTROL           // PMIC Register
  );

DLDO1 Power Output on the PMIC powers the Front Camera, USB HSIC and I2C Sensors on PinePhone.

In the code above, we set DLDO1 Voltage to 3.3V and power it on.

(We’ll talk about pmic_write and pmic_clrsetbits in a while)

Then we set LDO Voltage to 3.3V and power on the Capacitive Touch Panel…

  // Set LDO Voltage to 3.3V.
  // GPIO0LDO powers the Capacitive Touch Panel.

  // GPIO0LDO and GPIO0 High Level Voltage Setting (AXP803 Page 77)
  // Set GPIO0LDO and GPIO0 High Level Voltage (Bits 0 to 4) to 26
  // (2.6V + 0.7V = 3.3V)
  pmic_write(  // Write to PMIC Register...
    GPIO0LDO_HIGH_LEVEL_VOLTAGE_SETTING,  // PMIC Register
    GPIO0LDO_HIGH_LEVEL_VOLTAGE(26)       // PMIC Value
  );

  // Enable LDO Mode on GPIO0:
  // GPIO0 (GPADC) Control (AXP803 Page 76)
  // Set GPIO0 Pin Function Control (Bits 0 to 2) to 0b11 (Low Noise LDO on)
  pmic_write(  // Write to PMIC Register...
    GPIO0_CONTROL,            // PMIC Register
    GPIO0_PIN_FUNCTION(0b11)  // PMIC Value
  );

Next comes the LCD Panel: We set DLDO2 Voltage to 1.8V and power on the MIPI DSI Port of the LCD Panel…

  // Set DLDO2 Voltage to 1.8V.
  // DLDO2 powers the MIPI DSI Interface of Xingbangda XBD599 LCD Panel.

  // DLDO2 Voltage Control (AXP803 Page 52)
  // Set Voltage (Bits 0 to 4) to 11 (1.1V + 0.7V = 1.8V)
  pmic_write(  // Write to PMIC Register...
    DLDO2_VOLTAGE_CONTROL,  // PMIC Register
    DLDO2_VOLTAGE(11)       // PMIC Value
  );

  // Power on DLDO2:
  // Output Power On-Off Control 2 (AXP803 Page 51)
  // Set DLDO2 On-Off Control (Bit 4) to 1 (Power On)
  pmic_clrsetbits(  // Clear and set bits in PMIC Register...
    OUTPUT_POWER_ON_OFF_CONTROL2,  // Set these bits
    0,                             // Clear these bits
    DLDO2_ON_OFF_CONTROL           // PMIC Register
  );
  return OK;
}

Our LCD Panel is powered up and ready to receive MIPI DSI Commands!

(Right after we reset LCD Panel to High)

What are pmic_write and pmic_clrsetbits?

The AXP803 PMIC is connected to Allwinner A64 SoC on the Reduced Serial Bus. Which is a special bus designed for PMICs.

From Allwinner A80 User Manual (Page 918)…

“The RSB (reduced serial bus) Host Controller is designed to communicate with RSB Device using two push-pull wires.”

“It supports a simplified two wire protocol (RSB) on a push-pull bus. The transfer speed can be up to 20MHz and the performance will be improved much.”

(Reduced Serial Bus works like I2C, but specific to PMICs)

Thus to control AXP803 PMIC, pmic_write will talk to the PMIC over the Reduced Serial Bus: pinephone_pmic.c

// Write a byte to an AXP803 PMIC Register
static int pmic_write(
  uint8_t reg,  // AXP803 Register ID
  uint8_t val   // Byte to be written
) {
  //  Write to AXP803 PMIC on Reduced Serial Bus
  a64_rsb_write(
    AXP803_RT_ADDR,  // RSB Address is 0x2D
    reg,             // AXP803 Register ID
    val              // AXP803 Register Value
  );
  return OK;
}

(a64_rsb_write comes from our NuttX Driver for Reduced Serial Bus)

pmic_clrsetbits works the same way, it’s defined here: pinephone_pmic.c

Let’s move on to the ST7703 LCD Controller…

MIPI DSI Connector on PinePhone Schematic (Page 11)

MIPI DSI Connector on PinePhone Schematic (Page 11)

5 Initialise LCD Controller

We’ve done quite a bit on our LCD Panel…

  1. Switch on LCD Backlight

  2. Reset LCD Panel to Low

  3. Power on the LCD Panel’s MIPI Display Serial Interface (DSI)

  4. Reset LCD Panel to High

Now it’s time to initialise the Sitronix ST7703 LCD Controller inside the LCD Panel!

We do that by sending 20 Initialisation Commands over MIPI DSI.

What kind of Initialisation Commands?

Here’s a simple Initialisation Command with 4 bytes: pinephone_lcd.c

// Initialization Commands for Sitronix ST7703 LCD Controller:
// Command #1: SETEXTC (ST7703 Page 131)
// Enable USER Command
static const uint8_t g_pinephone_setextc[] = {
  0xb9,  // SETEXTC (ST7703 Page 131): Enable USER Command
  0xf1,  // Enable User command
  0x12,  // (Continued)
  0x83   // (Continued)
};

And here’s a long Initialisation Command with 64 bytes: pinephone_lcd.c

// Command #16: SETGIP1 (ST7703 Page 163)
// Set forward GIP timing
static const uint8_t g_pinephone_setgip1[] = {
  0xe9,  // SETGIP1: Set forward GIP timing
  0x82,  // SHR0, SHR1, CHR, CHR2 refer to Internal DE (REF_EN = 1); (PANEL_SEL = 2)
  0x10,  // Starting position of GIP STV group 0 = 4102 HSYNC (SHR0 Bits 8-12 = 0x10)
  0x06,  // (SHR0 Bits 0-7 = 0x06)
  0x05,  // Starting position of GIP STV group 1 = 1442 HSYNC (SHR1 Bits 8-12 = 0x05)
  0xa2,  // (SHR1 Bits 0-7 = 0xA2)
  0x0a,  // Distance of STV rising edge and HYSNC = 10*2 Fosc (SPON  Bits 0-7 = 0x0A)
  0xa5,  // Distance of STV falling edge and HYSNC = 165*2 Fosc (SPOFF Bits 0-7 = 0xA5)
  ...

We need to send all 20 Initialisation Commands as documented here…

These commands will configure the ST7703 LCD Controller specifically for our Xingbangda XBD599 LCD Panel.

How will we send the Initialisation Commands?

This is how we send the 20 Initialisation Commands to ST7703 LCD Controller over the MIPI DSI Bus: pinephone_lcd.c

// Send 20 Initialisation Commands to ST7703 LCD Controller
int pinephone_lcd_panel_init(void) {

  // For every ST7703 Initialisation Command...
  const int cmd_len = sizeof(g_pinephone_commands) /
                      sizeof(g_pinephone_commands[0]);
  for (int i = 0; i < cmd_len; i++) {

    // Get the ST7703 command and length
    const uint8_t *cmd = g_pinephone_commands[i].cmd;
    const uint8_t len  = g_pinephone_commands[i].len;

    //  If command is null, wait 120 milliseconds
    if (cmd == NULL) {
      up_mdelay(120);
      continue;
    }

    // Send the command to ST7703 over MIPI DSI
    write_dcs(cmd, len);
  }
  return OK;
}

(write_dcs is defined here)

(How it works)

What’s g_pinephone_commands?

That’s our Consolidated List of 20 Initialisation Commands: pinephone_lcd.c

// 20 Initialization Commands to be sent to ST7703 LCD Controller
static const struct pinephone_cmd_s g_pinephone_commands[] = {
  { g_pinephone_setextc,      sizeof(g_pinephone_setextc) },
  { g_pinephone_setmipi,      sizeof(g_pinephone_setmipi) },
  { g_pinephone_setpower_ext, sizeof(g_pinephone_setpower_ext) },
  ...

We’re done with the initialisation of the ST7703 LCD Controller inside our LCD Panel! Let’s render something…

Complete Display Driver for PinePhone

Complete Display Driver for PinePhone

6 Render LCD Display

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

It gets complicated (pic above)…

Thus our LCD 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. (Pic above)

How do we render graphics with Display Engine and Timing Controller TCON0?

Our NuttX Kernel Drivers for Display Engine and Timing Controller TCON0 are explained here…

Let’s find out how the drivers are called at startup.

Complete Display Driver for PinePhone

Complete Display Driver for PinePhone

7 Complete Display Driver

How is our LCD Driver called at NuttX Startup?

PinePhone needs a super complex Display Driver that will handle 11 steps at startup (pic above)…

We’ve just implemented all 11 steps in the NuttX Kernel… Including the LCD Driver that we saw today.

Here’s how our LCD Driver is called when NuttX boots on PinePhone: pinephone_display.c

// Called by NuttX Kernel at startup
// to start the Display Driver
int up_fbinitialize(int display) {

  // Turn on Display Backlight.
  // BACKLIGHT_BRIGHTNESS_PERCENT is 90
  pinephone_lcd_backlight_enable(BACKLIGHT_BRIGHTNESS_PERCENT);

  // Init Timing Controller TCON0
  a64_tcon0_init(PANEL_WIDTH, PANEL_HEIGHT);

  // Reset LCD Panel to Low
  pinephone_lcd_panel_reset(false);

  // Init PMIC
  pinephone_pmic_init();

  // Wait 15 milliseconds for power supply and power-on init
  up_mdelay(15);

In the code above, we begin with these steps…

The LCD Panel is powered up, ready to receive MIPI DSI Commands.

  // Enable MIPI DSI
  a64_mipi_dsi_enable();

  // Enable MIPI D-PHY
  a64_mipi_dphy_enable();

  // Reset LCD Panel to High
  pinephone_lcd_panel_reset(true);

  // Wait 15 milliseconds for LCD Panel
  up_mdelay(15);

  // Initialise ST7703 LCD Controller
  pinephone_lcd_panel_init();

Next we…

Our LCD Controller is all ready to render graphics!

  // Start MIPI DSI Bus in HSC and HSD modes
  a64_mipi_dsi_start();

  // Init Display Engine
  a64_de_init();

  // Wait 160 milliseconds for Display Engine
  up_mdelay(160);

  // Render Framebuffers with Display Engine
  render_framebuffers();
  return OK;
}

Finally to render graphics we…

When we boot NuttX on PinePhone (via microSD), the Test Pattern appears on PinePhone’s LCD Display! (Pic below)

Here’s the log from our LCD Driver…

Who calls the code above?

In the code above, our function up_fbinitialize executes the 11 steps needed for our PinePhone Display Driver.

At startup, up_fbinitialize is called by fb_register (from the NuttX Framebuffer Driver)…

And fb_register is called by pinephone_bringup, our Startup Function for PinePhone.

Let’s talk about the Framebuffer Driver…

Apache NuttX RTOS boots on PinePhone and renders a Test Pattern

8 Framebuffer Driver

NuttX Kernel calls our LCD Driver to render graphics…

What about NuttX Apps?

NuttX provides a Framebuffer Interface that will be called by NuttX Apps to render graphics…

We’ll talk about the Framebuffer Driver for PinePhone in the next article. (Pic below)

Stay tuned!

Framebuffer Driver for Apache NuttX RTOS on PinePhone

9 What’s Next

Thanks to our new PinePhone LCD Driver, Apache NuttX RTOS now boots with a Test Pattern!

But we’re not done yet! In the next article we’ll talk about the Framebuffer Driver that will allow NuttX Apps to render graphics on PinePhone.

Also we’ll investigate the mystery of the missing pixels. (Pic above)

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

Testing our PinePhone LCD Driver with a Framebuffer App on Apache NuttX RTOS

Testing our PinePhone LCD Driver with a Framebuffer App on Apache NuttX RTOS