RISC-V Star64 JH7110: Inside the Display Controller

đź“ť 23 Aug 2023

Display Driver for StarFive JH7110 SoC

Today we look deep inside the Display Controller of the RISC-V StarFive JH7110 SoC and figure out how it works.

But the JH7110 Display Controller is NOT documented!

Indeed! The Official JH7110 Doc only points to the Linux Driver Source Code.

(Plus a smattering of Display Registers)

Sounds a little disturbing?

Yeah goodbye olden days of Documented Display Registers! (Like for Allwinner A64)

But no worries! We’re here to decipher the Driver Source Code and document everything ourselves…

Why are we doing this?

We’re building a HDMI Display Driver for Apache NuttX Real-Time Operating System (RTOS) on the Pine64 Star64 SBC. (Based on JH7110, just like VisionFive2)

Our analysis today will be super useful for creating our HDMI Driver for NuttX on Star64. (Pic below)

And hopefully this article will be helpful for porting other Operating Systems to JH7110!

Pine64 Star64 RISC-V SBC

§1 JH7110 Docs and Source Code

What exactly is documented for JH7110 Display Controller?

Officially these are the JH7110 Display Controller docs…

But the crucial docs are confidential. (Sigh)

What about the Driver Source Code?

We have the official Linux Drivers for the Display Controller…

Phew so many Source Files!

Yeah but they’re super helpful for understanding the Inner Workings of our Display Controller!

We’ll decipher the Driver Source Code in a while.

JH7110 Display Subsystem Block Diagram

JH7110 Display Subsystem Block Diagram

§2 DC8200 Display Controller

What’s this DC8200?

From the pic above, we see that JH7110 uses a VeriSilicon DC8200 Dual Display Controller to drive these displays…

The Display Output Ports are named DPI0 and DPI1.

(NoC means Network-on-Chip)

(AXI is the Advanced eXtensible Interface)

These are the Clock and Reset Signals for the Display Subsystem (VOUT)…

JH7110 Clock Structure

JH7110 Clock Structure

And for the Display Controller (DC8200)…

JH7110 Display Subsystem Clock and Reset

JH7110 Display Subsystem Clock and Reset

(Remember to Enable the Clocks and Deassert the Resets!)

The DC8200 Display Controller outputs to 2 displays simultaneously (like MIPI DSI + HDMI)…

Block Diagram of DC8200 Display Controller

Block Diagram of DC8200 Display Controller

(AXI is the Advanced eXtensible Interface)

With support for…

We’ll explain the Display Layers (Overlays) and Display Pipelines in a while.

How are the Display Outputs mapped to MIPI DSI and HDMI?

The Linux Device Tree configures the mapping of Display Outputs to the Display Devices…

## Configure DC8200 Display Controller
&dc8200 {

  ## Display Outputs are mapped to...
  dc_out: port {

    ## DPI0: HDMI
    dc_out_dpi0: endpoint@0 {
      reg = <0>;
      remote-endpoint = <&hdmi_input0>;
    };

    ## DPI1: HDMI LCDC
    dc_out_dpi1: endpoint@1 {
      reg = <1>;
      remote-endpoint = <&hdmi_in_lcdc>;
    };

    ## DPI2: MIPI DSI
    dc_out_dpi2: endpoint@2 {
      reg = <2>;
      remote-endpoint = <&mipi_in>;
    };

(Source)

Let’s dive into the Display Driver Code!

Display Driver for StarFive JH7110 SoC

§3 DC8200 Driver for Direct Rendering Manager

What’s this DRM?

Direct Rendering Manager (DRM) is the Linux Subsystem that talks to Display Controllers. (Like the DC8200 Display Controller in JH7110)

The pic above shows the DC8200 Driver for DRM (top left). It works like a façade for the other DC8200 Driver Modules. (Rest of the pic)

(Not to be confused with the other DRM, which is also video-related)

§3.1 DRM Operations

The DRM Driver for DC8200 is named “starfive”. These are the DRM Operations supported by the driver: vs_drv.c

// DRM Driver for DC8200 Display Controller
static struct drm_driver vs_drm_driver = {
  .driver_features    = DRIVER_MODESET | DRIVER_ATOMIC | DRIVER_GEM,
  .lastclose          = drm_fb_helper_lastclose,
  .prime_handle_to_fd = drm_gem_prime_handle_to_fd,
  .prime_fd_to_handle = drm_gem_prime_fd_to_handle,
  .gem_prime_import   = vs_gem_prime_import,
  .gem_prime_import_sg_table = vs_gem_prime_import_sg_table,
  .gem_prime_mmap     = vs_gem_prime_mmap,
  .dumb_create        = vs_gem_dumb_create,
#ifdef CONFIG_DEBUG_FS
  .debugfs_init       = vs_debugfs_init,
#endif
  .fops  = &fops,
  .name  = "starfive",
  .desc  = "VeriSilicon DRM driver",
  .date  = "20191101",
  .major = 1,
  .minor = 0,
};

(Nothing to see here, mostly DRM Boilerplate)

(We’ll come back to fops)

(Graphics Execution Manager “GEM” handles Memory Buffers)

(vs_gem functions are here)

DRM Sub-Drivers

§3.2 DRM Sub-Drivers

Where are the fun bits of our Display Driver?

Remember our DRM Driver is only a façade. Most of the work is done by the Sub-Drivers for DC8200 (pic above): vs_drv.c

// Sub-Drivers for DC8200 Display Controller
static struct platform_driver *drm_sub_drivers[] = {

  // Display Controller Driver
  &dc_platform_driver,

#ifdef CONFIG_STARFIVE_INNO_HDMI
  // HDMI Controller Driver:
  // Inno HDMI 2.0 Transmitter for TSMC28HPC+
  &inno_hdmi_driver,
#endif

  // Display Subsystem (DSS) Encoder Driver
  &simple_encoder_driver,

#ifdef CONFIG_VERISILICON_VIRTUAL_DISPLAY
  // Virtual Display Driver
  &virtual_display_platform_driver,
#endif
};

We’ll see the Display Controller Driver in a while.

Who starts the Sub-Drivers?

At startup, vs_drm_init (in the DRM Driver) registers two things…

That’s how the Sub-Drivers are started.

§3.3 File Operations

What’s inside fops?

fops defines the File Operations supported by our DRM Driver: vs_drv.c

// File Operations for DC8200 Display Controller
static const struct file_operations fops = {
  .owner     = THIS_MODULE,
  .open      = drm_open,
  .release   = drm_release,
  .unlocked_ioctl = drm_ioctl,
  .compat_ioctl   = drm_compat_ioctl,
  .poll      = drm_poll,
  .read      = drm_read,
  .mmap      = vs_gem_mmap,
};

(Looks fairly standard for a DRM Driver)

(vs_gem_mmap is defined here)

Let’s talk about Display Drivers…

Display Driver renders graphics to a Display Device

§4 Inside the Display Driver

What’s inside a typical Display Driver?

How does it talk to the Display Hardware?

Before we dive too deep into our Driver Code, let’s talk about Display Drivers and how they control the Display Hardware.

The pic above shows how a typical Display Driver will render graphics to a Display Device…

What’s a Framebuffer?

A Framebuffer is just a region of RAM that stores pixels in a Colour Format. (Like ARGB 8888)

Framebuffer

Multiple Framebuffers are supported. Framebuffers can be rendered as Opaque or Semi-Transparent Overlays…

Overlays

To do this, we configure the Display Pipeline to Blend the Framebuffers…

Blender

Internally, the Display Driver will manipulate the Display Hardware Registers to…

  1. Configure the Framebuffers (and their RAM Addresses)

  2. Configure the Display Pipelines (for Overlay Blending)

  3. Configure the Display Output (for the Display Device)

  4. Commit the Display Configuration (to the Display Controller)

And that’s how a typical Display Driver works in a Modern SoC!

(Like for Allwinner A64)

Heading back to our scheduled programming…

DC8200 Display Controller Driver

§5 DC8200 Display Controller Driver

The DC8200 Display Controller Driver (pic above) is called by the the DC8200 DRM Driver. The driver exposes the Display Functions for…

The Display Controller Driver is named “vs-dc” (for VeriSilicon Display Controller): vs_dc.c

// DC8200 Display Controller Driver
struct platform_driver dc_platform_driver = {
  .probe  = dc_probe,
  .remove = dc_remove,
  .name   = "vs-dc"

(dc_probe is defined here)

(dc_remove is defined here)

These are the Component Functions exposed by the Display Controller Driver to start the driver: vs_dc.c

// DC8200 Display Component Functions
const struct component_ops dc_component_ops = {
  .bind   = dc_bind,
  .unbind = dc_unbind,
};

What happens at startup?

At startup, the DRM Driver calls our Display Controller Driver at…

Which calls our Display Hardware Driver at…

As we’ll see in the next section.

That’s all for our Display Controller Driver?

There’s more! We’ll come back to our Display Controller Driver for handling the Display Pipeline and Display Plane.

DC8200 Display Hardware Driver

§6 DC8200 Display Hardware Driver

Now we do the exciting bit?

Finally! The Display Hardware Driver (pic above) is called by the Display Controller Driver (previous section) to manipulate the Display Hardware Registers and…

(This is the driver that we’ll reimplement in NuttX!)

Earlier we saw dc_init (from Display Controller Driver) calling the Display Hardware Driver at startup.

Here’s what happens inside dc_hw_init…

  1. Read the Hardware Revision and Chip ID

  2. Initialise every Display Plane (Layer)

  3. Initialise every Display Panel (and Cursor)

Why read the Hardware Revision?

Depending on the Hardware Revision, the DC8200 Display Controller works a little differently. For Star64 SBC…

When we match the Hardware Revision with the Driver Code…

Why are the layers interleaved?

That’s because the Display Planes (Layers) will be rendered to 2 separate displays (HDMI + MIPI DSI)…

Display 0Display 1
PrimaryPrimary_1
OverlayOverlay_2
Overlay_1Overlay_3
CursorCursor_1

Block Diagram of DC8200 Display Controller

Block Diagram of DC8200 Display Controller

Our Display Hardware Driver will do other fun things! Let’s talk about the Display Pipelines and Display Planes…

Setup Display Pipeline

§7 Setup Display Pipeline

Earlier we talked about Display Pipelines and how they…

In Linux Direct Rendering Manager (DRM), the Display Pipeline is implemented as CRTC Functions: vs_dc.c

// Display Pipeline Functions for DC8200 Display Controller
static const struct vs_crtc_funcs dc_crtc_funcs = {
  .enable        = vs_dc_enable,
  .disable       = vs_dc_disable,
  .mode_fixup    = vs_dc_mode_fixup,
  .set_gamma     = vs_dc_set_gamma,
  .enable_gamma  = vs_dc_enable_gamma,
  .enable_vblank = vs_dc_enable_vblank,
  .commit        = vs_dc_commit,
};

How do we create a Display Pipeline?

From above, we see that DRM creates the Display Pipeline (pic above) by calling our Display Controller Driver at…

Which calls…

What’s inside dc_hw_setup_display?

In our Display Hardware Driver, dc_hw_setup_display will…

  1. Copy the Display Struct
  2. Call setup_display_ex

Then setup_display_ex will…

  1. Set the Colour Format
  2. Call setup_display

Finally setup_display will do most of the work…

  1. Set the DPI Config
  2. Disable the Display Panel
  3. Set the Horizontal Resolution and Sync
  4. Set the Vertical Resolution and Sync
  5. Configure the Framebuffer Sync Mode
  6. Set the Framebuffer Background Colour
  7. Set the Display Dither
  8. Enable the Display Panel
  9. Set the Overlay Configuration
  10. Set the Cursor Configuration

Who creates the Display Pipeline?

To create the Display Pipeline, vs_dc_enable (from above) is called by vs_crtc_atomic_enable…

// DC8200 Display Pipeline Helper Functions
static const struct drm_crtc_helper_funcs vs_crtc_helper_funcs = {
  .mode_fixup     = vs_crtc_mode_fixup,
  .atomic_enable  = vs_crtc_atomic_enable,
  .atomic_disable = vs_crtc_atomic_disable,
  .atomic_begin   = vs_crtc_atomic_begin,
  .atomic_flush   = vs_crtc_atomic_flush,
};

(Source)

Which is called by the Linux DRM Atomic Helper.

(We’ll see vs_crtc_atomic_flush in the next section)

(More about DRM Atomic Display)

And that’s how we create a Display Pipeline! Now we commit the Display Pipeline…

Commit Display Pipeline

§8 Commit Display Pipeline

Why will we Commit a Display Pipeline?

A Display Pipeline won’t render any pixels… Until we Commit the Display Pipeline! (Pic above)

To Commit the Display Pipeline, the Linux Direct Rendering Manager (DRM) calls our Display Controller Driver at…

What’s inside dc_hw_commit?

In our Display Hardware Driver, dc_hw_commit will…

  1. Call gamma_ex_commit to commit the Gamma Correction
  2. Call plane_ex_commit to commit the Display Plane
  3. Update the Cursor and QoS

What happens in plane_ex_commit?

plane_ex_commit will…

  1. Set the Colour Space
  2. Set the Gamma Correction
  3. Call plane_commit

Then plane_commit will do these for every Display Plane (Layer)…

  1. Set the Framebuffer YUV Address and Stride
  2. Set the Framebuffer Width and Height
  3. Clear the Framebuffer
  4. Configure the Primary and Non-Primary Framebuffers
  5. Enable Framebuffer Scaling (X and Y)
  6. Set the Framebuffer Offset (X and Y)
  7. Set the Framebuffer Blending
  8. Set the Colour Key (Transparency)
  9. Set the ROI

Who Commits the Display Pipeline?

To Commit the Display Pipeline, vs_dc_commit is called by vs_crtc_atomic_flush…

Which is called by the Linux DRM Atomic Helper.

(More about DRM Atomic Display)

Update Display Plane

§9 Update Display Plane

One last thing for today: How to Update the Display Plane. (Pic above)

(A Display Plane is a Layer of Pixels / Framebuffer that will be blended into the final image by a Display Pipeline)

Our Display Controller Driver exposes these Display Plane Functions: vs_dc.c

// Display Plane Functions for DC8200
static const struct vs_plane_funcs dc_plane_funcs = {
  .update  = vs_dc_update_plane,
  .disable = vs_dc_disable_plane,
  .check   = vs_dc_check_plane,
};

To update the Display Plane, the Linux Direct Rendering Manager (DRM) calls our Display Controller Driver at…

Which calls…

  1. update_plane (Update Plane)
  2. update_qos (Update QoS)
  3. update_cursor_plane (Update Cursor Plane)

What’s inside update_plane?

update_plane (from our Display Controller Driver) will update the Display Plane…

  1. Call update_fb to set the Framebuffer Addresses
  2. Call update_roi to set the ROI
  3. Call update_scale to set the Scaling
  4. Call update_degamma to set the Degamma Correction
  5. Set the Start and End Position (X and Y)
  6. Set the Blending Alpha and Mode
  7. Set the Colour Management
  8. Call dc_hw_update_plane

Then dc_hw_update_plane (from our Display Hardware Driver) will…

  1. Copy the Framebuffer
  2. Copy the Scaling
  3. Copy the Position
  4. Copy the Blending

Who calls our driver to update the Display Plane?

To update the Display Plane, vs_dc_update_plane is called by vs_plane_atomic_update…

// DC8200 Display Plane Helper Functions
const struct drm_plane_helper_funcs vs_plane_helper_funcs = {
  .atomic_check   = vs_plane_atomic_check,
  .atomic_update  = vs_plane_atomic_update,
  .atomic_disable = vs_plane_atomic_disable,
};

(Source)

Which is called by the Linux DRM Atomic Helper.

(More about DRM Atomic Display)

Yocto Linux with KDE Plasma on Star64 HDMI

Yocto Linux with KDE Plasma on Star64 HDMI

§10 Unsolved Mysteries

We’re ready to build our NuttX Display Driver for JH7110?

Not quite. We have a bit more to explore, like the HDMI Controller for JH7110…

Justin (Fishwaldo) suggests that we check out the simpler HDMI Driver in U-Boot Bootloader…

When we port NuttX to the PineTab-V Tablet, we’ll need drivers for MIPI DSI and LCD Panel…

We might also need the Framebuffer Driver and Virtual Display Driver….

Sounds like a lot of work!

Yeah we’ll probably prototype our new driver in Zig before converting to C. Stay tuned for updates…

(Slightly annoying that New Zig won’t run on my Old Mac)

Sketch of Display Driver for StarFive JH7110 SoC

§11 What’s Next

Today we created a Detailed Doc that explains the Inner Workings of the JH7110 Display Controller…

I hope you’ll find this doc helpful too! Check out the next article…

Many Thanks to my GitHub Sponsors (and the awesome NuttX Community) 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/display2.md

DC8200 Framebuffer Driver

§12 Appendix: DC8200 Framebuffer Driver

At startup, vs_drm_bind (from our DRM Driver) calls vs_mode_config_init to register the Framebuffer Driver. (Pic above)

The Framebuffer Driver exposes the following functions: vs_fb.c

// DC8200 Framebuffer Functions
static const struct drm_mode_config_funcs vs_mode_config_funcs = {
  .fb_create       = vs_fb_create,
  .get_format_info = vs_get_format_info,
  .output_poll_changed = drm_fb_helper_output_poll_changed,
  .atomic_check    = drm_atomic_helper_check,
  .atomic_commit   = drm_atomic_helper_commit,
};

Who creates the Framebuffer?

To create the Framebuffer, vs_fb_create is called by drm_framebuffer (from Linux DRM).

To get the Framebuffer Formats, vs_get_format_info is called by drm_fourcc (from Linux DRM).

Framebuffer Formats are defined in vs_formats.

JH7110 Display Subsystem Clock and Reset

JH7110 Display Subsystem Clock and Reset

§13 Appendix: JH7110 Display Clock and Reset

Earlier we talked about our Display Controller Driver preparing the Clock and Reset Signals…

The pic above shows the Clock Signals for the JH7110 / DC8200 Display Controller. The Clock Signals and Reset Registers are defined here…

Let’s walk through dc_bind, dc_init and vs_dc_enable, to understand how they prepare the Clock and Reset Signals.

The Clock and Reset Names below don’t quite match the Official Docs above. Here’s what we tested…

DC8200 Display Controller Driver

§13.1 dc_bind

At startup, our DRM Driver calls our Display Controller Driver at dc_bind to setup the Clock and Reset Signals. (Pic above)

dc_bind will do the following…

  1. Call dc_init

    (Explained here)

  2. Attach the I/O MMU Device

  3. For Each Panel: Create the Display Pipeline

  4. For Each Plane: Create the Display Plane

  5. Update the Pitch Alignment

  6. Disable Clock vout_top_lcd

  7. Assert the DC8200 Reset

  8. Disable the DC8200 Clock

  9. Disable Clock vout_top

  10. Disable Clock vout_clk

DC8200 Display Controller Driver

§13.2 dc_init

At startup, our DRM Driver calls our Display Controller Driver at dc_bind, which calls dc_init to setup the Clock and Reset Signals. (Pic above)

dc_init will do the following…

  1. Enable Clock vout_clk

  2. Deassert the DC8200 Reset

  3. Enable Clock vout_top_lcd

  4. Deassert vout_reset

  5. Update the DSS Registers with this Mystery Code…

    #ifdef CONFIG_DRM_I2C_NXP_TDA998X
      // tda998x-rgb2hdmi. For HDMI only?
      regmap_update_bits(dc->dss_regmap, 0x4, BIT(20), 1<<20);
    #endif
    
    #ifdef CONFIG_STARFIVE_DSI
      // For DSI only?
      regmap_update_bits(dc->dss_regmap, 0x8, BIT(3), 1<<3);
    #endif
    
  6. Call dc_hw_init

    (Explained here)

Setup Display Pipeline

§13.3 vs_dc_enable

DRM creates the Display Pipeline (pic above) by calling our Display Controller Driver at vs_dc_enable, to prepare the Clock and Reset Signals…

vs_dc_enable will do the following…

  1. Enable Clock vout_clk

  2. Deassert the DC8200 Reset

  3. Enable Clock vout_top_lcd

  4. Update the DSS Registers with this Mystery Code…

    regmap_update_bits(dc->dss_regmap, 0x4, BIT(20), 1<<20);
    regmap_update_bits(dc->dss_regmap, 0x8, BIT(3), 1<<3);
    

    (Similar to the Mystery Code in dc_init)

  5. Call dc_hw_init

    (Explained here)

  6. Set the Display Struct: Bus Format, Horizontal Sync, Vertical Sync, Sync Mode, Background Colour, Sync Enable and Dither Enable

  7. If Display is MIPI DSI:

    Set the Clock Rate for dc8200_pix0, dc8200_clk_pix1 and vout_top_lcd

  8. If Display is HDMI:

    Set the Clock hdmitx0_pixelclk

  9. Call dc_hw_setup_display

    (Explained here)