Skip to main content

Displays

Endpoint supports two display categories:

  • Parallel TFT: a panel wired directly to the board's parallel RGB controller. Fastest option; refresh runs off the hardware without any CPU work.
  • Serial (SPI / I2C) displays and character displays: any panel you can drive from a bus. The next sections cover both.

For panels with a touchscreen, see Touch Screen.

The parallel display is driven through GHIElectronics.Endpoint.Devices.Display. Panel timings and backlight pin live in /config/display.json, so they are set once and the application simply opens, enables and flushes.

Parallel display​

One-time configuration​

Write the panel's timings and backlight pin to the config partition. You only have to do this once per board (settings persist across reboots).

using GHIElectronics.Endpoint.Core;
using GHIElectronics.Endpoint.Devices.Display;

var settings = new DisplaySettings {
Type = DisplayType.Parallel,
EnableAtBoot = false, // true = board brings the panel up during boot
Logo = null, // 24-bit BMP filename on /config, or null
Backlight = new BacklightSettings {
Pin = EPDomino24.Backlight, // or -1 to leave the backlight alone
ActivePolarity = SignalPolarity.ActiveHigh,
},
Parallel = new ParallelLcdSettings {
Width = 480, Height = 272,
PixelClockHz = 10_000_000,
HFrontPorch = 2, HBackPorch = 2, HSync = 41,
VFrontPorch = 2, VBackPorch = 2, VSync = 10,
BitsPerPixel = 16, // 16 = RGB565, 24 = RGB888
},
};
DisplayController.SetSettings(settings);

Enable and show pixels​

Once configured, open the controller, enable it, and push a frame. The pixels can come from any .NET graphics library (see Graphics) or from your own code.

using var display = DisplayController.GetDefault(); // null if Type is None
if (display == null) return;

display.Enable();

// Pixel buffer in RGB565 (width * height * 2 bytes)
var pixels = new byte[display.Width * display.Height * 2];
// ... fill the buffer ...

display.Flush(pixels); // defaults to Rgb565

For other pixel formats, pass one explicitly:

using System.Drawing.Imaging;
display.Flush(rgb888Pixels, PixelFormat.Format24bppRgb);
display.Flush(argb32Pixels, PixelFormat.Format32bppArgb);

A partial update is also supported:

display.Flush(regionPixels, PixelFormat.Format16bppRgb565,
x: 100, y: 50, width: 200, height: 100);

Python does not support method overloading, so the binding exposes a single Flush(pixels, format=PixelFormat.Format16bppRgb565, x=0, y=0, width=None, height=None); the three call sites above supply different trailing arguments to that one method.

Set DisplaySettings.Logo to the filename of a 24-bit BMP on the config partition and call ShowLogo() right after Enable() to paint it:

display.Enable();
display.ShowLogo(); // reads /config/<logo-filename>.bmp and flushes it

If EnableAtBoot is also true, the board does this itself during boot, so the user's program sees the panel already lit with the logo.

Serial (SPI / I2C) displays​

SPI and I2C panels (and any other non-parallel panel such as UART or an HDMI bridge) are driven by their own DisplayController subclass. The abstract contract is small:

  • Enable: run the panel's power-up / init sequence
  • Disable: power it down
  • Flush: push pixels
  • Width / Height / IsEnabled: visible panel size and state

Application code uses the same Enable / Flush pattern as the built-in parallel panel, so a graphics stack written against DisplayController works against either. GetDefault() only returns the parallel panel; a serial driver is constructed directly.

Using a ready-made driver​

GHI Electronics publishes a driver for the HiLetgo ILI9341 240x320 SPI TFT in all three languages. Source on GitHub:

All three use the same Initialize + Flush pattern.

using System.Device.Gpio;
using System.Device.Spi;
using GHIElectronics.Endpoint.Core;
using GHIElectronics.Endpoint.Drivers.HiLetgo.ILI9341;

using var gpio = new GpioController(new EPDomino24.Gpio.Driver());
using var spi = SpiDevice.Create(ILI9341Controller.GetConnectionSettings(EPDomino24.Spi.Spi1));

using var tft = new ILI9341Controller(
spi: spi,
cs: EPDomino24.Gpio.Pin.PA14,
dc: EPDomino24.Gpio.Pin.PA10,
reset: EPDomino24.Gpio.Pin.PF4,
gpio: gpio);
tft.SwapRedBlue = true; // set for modules where yellow shows as cyan
tft.Enable();

// Pixel buffer in RGB565 (width * height * 2 bytes)
var pixels = new byte[tft.Width * tft.Height * 2];
// ... fill the buffer ...
tft.Flush(pixels);

:::note Wiring gotchas (worth knowing even with the driver)

  • Chip select is driven by a GPIO, not by the SPI controller's hardware NSS. The driver opens the pin you pass and toggles it around each transaction. On .NET, SpiConnectionSettings needs chipSelectLine: 0 explicitly; Microsoft's Linux SPI stack does not accept -1.
  • Data / Command pin: ILI9341 distinguishes commands from pixel data by a separate GPIO (D/C). Low = command byte, high = data byte.
  • Reset pin: an active-low pulse brings the chip out of reset reliably at startup. Tie to 3.3 V or leave unwired only if your module has an on-board reset circuit.
  • Pixel format on the wire is 16 bpp big-endian, but Canvas writes RGB565 little-endian (ready for /dev/fb0). The driver byte-swaps each pixel in Flush; you never see the raw bytes.
  • Red / Blue swap: some HiLetgo and eBay modules have the physical color filter wired as BGR regardless of what the chip's MADCTL bit says, so yellow renders as cyan and red as blue. Set SwapRedBlue = true for those modules.
  • Backlight on most 2.2"–2.8" ILI9341 modules has a current-limit resistor on board; tie LED to +3.3 V for always-on, or drive from a PWM pin if you want to dim it. :::

Writing your own driver​

To support a panel we do not ship a driver for, subclass DisplayController and drive the hardware through the standard SPI / I2C / GPIO APIs. The skeleton is small:

using System;
using System.Device.Gpio;
using System.Device.Spi;
using GHIElectronics.Endpoint.Devices.Display;
using PixelFormat = System.Drawing.Imaging.PixelFormat;

public sealed class MyPanelController : DisplayController {
private readonly SpiDevice spi;
private readonly GpioController gpio;
private readonly int dcPin, resetPin;
private bool enabled;

public MyPanelController(SpiDevice spi, int dc, int reset, GpioController gpio) {
this.spi = spi;
this.gpio = gpio;
this.dcPin = dc;
this.resetPin = reset;
this.gpio.OpenPin(dc, PinMode.Output);
this.gpio.OpenPin(reset, PinMode.Output);
}

public override int Width => 240;
public override int Height => 320;
public override bool IsEnabled => this.enabled;

public override void Enable() {
// reset pulse + vendor init sequence over SPI ...
this.enabled = true;
}

public override void Disable() {
// put the panel to sleep
this.enabled = false;
}

public override void Flush(ReadOnlySpan<byte> pixels, PixelFormat format,
int x, int y, int width, int height) {
// 1) set the panel's draw window to (x, y, width, height)
// 2) send pixels over SPI in the panel's native format
}

public override void Dispose() {
try { this.Disable(); } catch { }
this.gpio.ClosePin(this.dcPin);
this.gpio.ClosePin(this.resetPin);
this.spi.Dispose();
}
}

The open-source ILI9341 driver source is a complete working example of this pattern.

Character displays​

Character Display

Character-matrix LCDs (2x16, 4x20, etc.) use the standard Iot.Device.CharacterLcd library from .NET IoT. Wire the panel to any GPIO/I2C/SPI pins and drive it with the same code you would use on any .NET IoT board.