As part of a series on Zephyr with Adafruit hardware, this guide shows how to configure Zephyr to use an ST7789 TFT display with a Feather RP2350. By connecting the display with a breadboard, we can use a logic analyzer to verify that the Zephyr display driver pin configuration agrees with the CircuitPython display driver. This guide is meant for people interested in adding support for Adafruit displays to Zephyr.
One of my eventual goals for the Zephyr Quest series of guides is to write a board definition for the Adafruit CLUE board. But, that's too much complexity to tackle all at once. For this guide, I'm aiming for three smaller intermediate goals:
- Walk through a specific example of bringing up an ST7798 TFT display on a board that has convenient SWD debug and UART serial connections. This will include making a pin mapping and translating the CircuitPython ST7789 driver's init sequence byte array into Zephyr Devicetree properties.
- Explain Devicetree concepts that you need to know to configure Zephyr's sitronix,st7789v display driver
- Demonstrate tips and tricks for working through problems that can happen when bringing up a new display in Zephyr. (compare logic analyzer captures, etc.)
Optional Stuff
This guide describes using a Raspberry Pi Debug Probe, Saleae logic analyzer, and 3D printed display stand. But, those are all optional:
- I like programming Zephyr firmware with
west flashand a debug probe. But, if you want, you can program firmware by copying UF2 files using the RP2350's built in USB bootloader. - You don't need to use a Saleae logic analyzer to verify Devicetree pin configurations when bringing up a new display. If you want, you can just use trial and error until you get something that works.
- I made a 3D printed stand for the breadboard and display because I like having the display at an angle that's easy to read. If you don't want to make a stand, you can skip it.
3D Printing (optional)
This is optional. But, if you want to hold the TFT display up at an angle so it's easier to read, I designed a 3D printable stand using Blender. You can download the Blender file or printable STL file using the download buttons below. The download files are both zipped, so you will need to unzip them.
Note: Not all breadboards are the same size. I made this stand to fit a random breadboard I had lying around, and I'm not sure which brand it was. The opening for the breadboard is 84.0 mm wide by 55.2 mm deep. If you need more space, you can edit the .blend file.
If you want to experiment with the Blender file, there are Blender screenshots in the zphqst-02/media/ directory of the GitHub repo for this guide. The screenshots may help you to understand how I used the boolean difference and bevel modifiers.
The stand's holes for the breadboard and display were made to have 1mm of clearance on all sides, relative to caliper measurements of a random half-sized breadboard I grabbed from my parts pile. The horizontal alignment between the display and breadboard is a few mm away from ideal, so the cable may twist slightly. You can try to fix it in Blender if you want. For me, it was close enough, so I didn't bother making a second print. Before printing this, I recommend comparing caliper measurements of your breadboard to the .blend or .stl file.
- Solder Feather headers using a breadboard to hold the pins. If you are unfamiliar with soldering headers, you might want to read:
- Solder EYESPI breakout board headers.
- Arrange the feather and EYESPI breakout on a half-size breadboard as shown in the Fritzing diagram above. Note: If you want to connect a logic analyzer, make sure the EYESPI breakout board is shifted one row above the center of the breadboard, leaving a row of holes open for logic analyzer wires.
- Use diagonal flush cutters, wire strippers, and red hookup wire to connect the Feather 3.3V pin to the EYESPI Breakout Vin pin. If you are unfamiliar with breadboard wiring, you might want to read:
- Breadboards for Beginners ("Jumper Wires" section)
- Connect Feather GND to EYESPI Gnd with black wire.
- Connect Feather MO to EYESPI MOSI with a different color of wire (e.g. gray).
- Connect Feather SCK to EYESPI SCK with a different color of wire (e.g. yellow).
- Connect Feather D5 to EYESPI TCS with a different color of wire (e.g. orange).
- Connect Feather D6 to EYESPI DC with a different color of wire (e.g. green).
- Connect Feather D9 to EYESPI RST with a different color of wire (e.g. blue).
- Carefully flip the TFT Display's EYESPI connector clamp to the open position (pointing up) using your fingernail, a spudger, a trimmed chopstick, or whatever. If you are unfamiliar with FPC flex cable connectors, you might want to read:
- Adafruit EYESPI Breakout Board ("Plugging in an EYESPI Cable" section)
- Insert one end of the 50mm EYESPI flex cable in the 18-pin connector with the blue side facing towards you and away from the display's PCB.
- Make sure the cable is centered, straight, and fully inserted, then carefully close the clamp.
- Flip the display over and connect the other end of the EYESPI flex cable to the EYESPI breakout (blue side facing toward you and away from the PCB).
- (optional) If you 3D printed a stand for the display and breadboard, move the breadboard and TFT display to the stand now.
- Look at the top of the Pi Debug Probe. On the enclosure lid, above the 3-pin female connectors, you should see the letters "U" and "D" molded into the plastic. U is over the UART serial port and D is over the SWD debug port.
- Connect the probe's gray cable from its D port to the Feather Debug (SWD) connector.
- Connect the probe's Orange/Black/Yellow male header cable from its U port to the Feather serial pins:
- Orange wire: probe serial TX; connects to Feather RX
- Black wire: probe GND; connects to Feather GND
- Yellow wire: probe serial RX; connects to Feather TX
- Connect the Pi Debug Probe to your computer with a USB A to Micro B data cable, such as the one that came with the Pi Debug Probe (CAUTION: charge-only cables won't work!)
- Connect the Feather RP2350 to your computer with a USB C data cable (CAUTION: charge-only cables won't work!)
Logic Analyzer (optional)
If you want to connect a logic analyzer, begin by making sure the EYESPI breakout and jumper wires are arranged on your breadboard to leave a row of holes open for connecting logic analyzer wires.
Use male-to-male pin headers to connect the Saleae wiring harness to the EYESPI signals like so:
| EYESPI | Saleae Wire Color | Saleae Wire # |
| SCK | black | 0 |
| MOSI | brown | 1 |
| MISO | red | 2 |
| DC | orange | 3 |
| RST | yellow | 4 |
| TCS | green | 5 |
Also connect some of the logic analyzer's black ground wires to the breadboard's ground row.
In Saleae's Logic 2 app, configure channels 0 through 5 for digital input and add an SPI analyzer with SCK on channel 0, MOSI on channel 1, and MISO on channel 2. If you want, edit the channel labels to match the signal names. For more details on using Saleae's Logic 2 software, check out the Using Logic section of their online manual.
CircuitPython Wiring Test
Before moving on to the Zephyr stuff, I suggest testing that your hardware modules and wiring are working as expected. To do that:
- Install CircuitPython
- Install the project bundle code which will print a test message to the display
- (optional) Start a Saleae logic analyzer capture
- Power up the Feather RP2350 board to run the code and verify that the display works
Install CircuitPython 9.2.4
To prepare for running the CircuitPython display test code, you need to install CircuitPython 9.2.4. To download the Feather RP2350 CircuitPython 9.2.4 .UF2 file, you can use this direct link to circuitpython.org:
Adafruit Feather RP2350 (9.2.4 .UF2 file)
If you prefer to try running the latest version of CircuitPython, you can use the .UF2 download button from the board detail page at circuitpython.org:
Feather RP2350 (board page)
Once you have the .UF2 file, follow the instructions on the Install CircuitPython page of the "Adafruit Feather RP2350 with HSTX" Learn Guide to install CircuitPython.
Install Project Bundle
To install the CircuitPython code, copy the project bundle files to your CIRCUITPY drive:
- Download the project bundle .zip file using the "Download Project Bundle" button below.
- Expand the zip file by opening it, or use unzip in a Terminal. The zip archive should expand to a folder. When you open the folder, it should contain a README.txt file and a CircuitPython 9.x folder.
- Open the CircuitPython 9.x folder and copy all of its contents to your CIRCUITPY drive.
To learn more about copying libraries to your CIRCUITPY drive, check out the CircuitPython Libraries section of the Welcome to CircuitPython! learn guide.
After copying the files from the project bundle, your CIRCUITPY drive should contain the files and directories shown in this screenshot:
# SPDX-License-Identifier: MIT
# SPDX-FileCopyrightText: Copyright 2025 Sam Blenny
from board import SPI, D5, D6, D9
from displayio import Bitmap, Group, Palette, TileGrid, release_displays
from fourwire import FourWire
from terminalio import FONT
from time import sleep
from adafruit_display_text.label import Label
from adafruit_st7789 import ST7789
ZOOM = 3
# Initialize 320x240 ST7789 IPS display with scaling for better readability
release_displays()
bus = FourWire(SPI(), chip_select=D5, command=D6, reset=D9)
display = ST7789(bus, width=320, height=240, rotation=270)
grp = Group(scale=ZOOM)
display.root_group = grp
# Set background color
bg = Bitmap((320 // ZOOM), (240 // ZOOM), 1)
pal = Palette(1)
pal[0] = 0x330033
bg.fill(0)
grp.append(TileGrid(bg, pixel_shader=pal))
# Add some text
lbl = Label(font=FONT, color=0xFF5555,
text="CircuitPython\nWiring Test:\n Feather RP2350 \n + ST7789",
anchor_point=(0, 0),
anchored_position=(8, 10))
grp.append(lbl)
# Spin so the supervisor doesn't change the display
while True:
sleep(1)
When you run code.py, the display should show a purple background with large text reading, "CircuitPython Wiring Test: Feather RP2350 + ST7789", like this:
This is how it looked when I took a Saleae logic analyzer capture of powering up the Feather RP2350 with the wiring test firmware:
Here is a zoomed in view showing the sequencing of the data/command (DC) and chip select (TCS) pins relative to SCK and MOSI as the CircuitPython ST7789 driver is initializing the display (note how DC is low for the command and high for the command's data parameter... that will be important later):
The point of taking these captures is to establish a baseline of what the signals should look like for a working display initialization. As I write the Zephyr Devicetree configuration, I will make sure that the Zephyr MIPI display driver is using the pins in the same way (chip select is active low, etc.).
Dev Tool Setup
This guide builds on top of the same Zephyr workspace and developer tools described in my Zephyr Quest: Feather RP2350 Board Def guide. To follow along with this guide, you will need:
- Zephyr workspace with
westand the Zephyr SDK'sarm-zephyr-eabitoolchain - (optional) The Raspberry Pi version of
openocdincluding their patches for RP2350 support. (alternately, you can program the Feather RP2350 with UF2 files and the factory bootloader) - (optional) A git cloned copy of my zphqst-02 GitHub repo with the EYESPI and TFT display shield definitions described in this guide. (alternately, you can copy and paste the code from this guide)
- Feather RP2350 board definition: you can get this from my zphqst-02 repo or from the adafruit/adafruit-zephyr-support GitHub repository
Here's an example of what it might look like to clone the zphqst-02 repo into your Zephyr workspace directory using a terminal and bash shell on Debian Linux:
$ cd ~/code/zephyr-workspace/ $ source .venv/bin/activate (.venv) $ git clone https://github.com/samblenny/zphqst-02.git (.venv) $ ls adafruit-zephyr-support modules tools zphqst-01 bootloader openocd zephyr zphqst-02 (.venv) $ cd zphqst-02 (.venv) $ ls boards bundle_builder.py code.py Makefile README.md boot.py bundle_manifest.cfg LICENSES media (.venv) $
What is a Shield Definition?
Zephyr uses adapted versions of the Kconfig and Devicetree systems from the Linux kernel to manage modular configuration of software and hardware. Both systems allow for building up a single data structure that merges fragments of configuration from different sources. This approach allows software features to be structured into modules, where code and related configuration fragments can exist side by side in the same folder. The advantage is, you don't have to navigate though one gigantic configuration file. The disadvantage is, you may need to cross-reference several configuration fragment files to get a clear picture of the actual configuration being used.
Board definitions include configuration files describing what hardware is soldered onto the board and which software modules are necessary to make that hardware function. Shield definitions add configuration to enable support for hardware add-on modules that can be connected to a board, such as Arduino shields or Adafruit FeatherWings.
Suppose you have a board, like the Feather RP2350, and that you want to use it with a TFT display which uses the ST7789V controller chip. Zephyr has a MIPI driver for the ST7789V. To use the display, you need to make sure the driver gets compiled in, and you need to configure the display driver with a mapping of which GPIO and SPI pins to use for which signals. You can do those things by creating a shield definition. It's also possible to combine more than one shield.
To use a shield, you can provide an option to west build, such as --shield NAME_OF_SHIELD.
To learn more, you can read the Zephyr project's Shields documentation page.
What is a GPIO Nexus Node?
To map pins from a board to a standardized connector such as the Feather header, Zephyr shield definitions use a Devicetree structure called a GPIO nexus node. The Zephyr project's GPIO nexus nodes documentation is brief. To understand how nexus nodes work, I found the Devicetree v0.4 Specification's Nexus Nodes and Specifier Mapping section helpful.
When you get past the confusing syntax, it turns out that GPIO Nexus Nodes are, more or less, just an implementation of an associative array data structure. There's some subtlety because the nexus node "specifiers" sometimes encode pin mode information in their low bits. The #gpio-cells, gpio-map-mask, and gpio-map-pass-thru settings are part of the mechanism for encoding pin mode information. But, approximately, the nexus nodes function similarly to a Python dictionary literal.
For example, consider this Python REPL example with two dictionary literals:
>>> feather_header = {6: (6,0,"SCK"), 7: (7,0,"MOSI"), 8: (8,0,"MISO")}
>>> eyespi = {3: feather_header[6], 4: feather_header[7], 5: feather_header[8]}
>>> print(eyespi)
{3: (6, 0, 'SCK'), 4: (7, 0, 'MOSI'), 5: (8, 0, 'MISO')}
That pin mapping represented by the eyespi dictionary above is expressing a similar meaning as this Devicetree GPIO nexus node:
/ {
eyespi: connector {
compatible = "eyespi";
#gpio-cells = <2>;
gpio-map-mask = <0xffffffff 0xffffffc0>;
gpio-map-pass-thru = <0 0x3f>;
gpio-map =
< 3 0 &feather 6 0>, /* eyespi SCK = feather SCK */
< 4 0 &feather 7 0>, /* eyespi MOSI = feather MOSI */
< 5 0 &feather 8 0>; /* eyespi MISO = feather MISO */
};
};
Feather RP2350 Board Def
This guide uses an updated version of my feather_rp2350 board definition from the Zephyr Quest: Feather RP2350 Board Def guide. The main difference is the improved version includes a feather_header pin mapping, which includes a GPIO nexus node.
This is a copy of the feather_connector.dtsi file that defines the feather_header pin mapping:
/* * Copyright (c) 2020 Richard Osterloh <[email protected]> * SPDX-FileCopyrightText: Copyright 2025 Sam Blenny * * SPDX-License-Identifier: Apache-2.0 * * Derived from boards/adafruit/nrf52_adafruit_feather/feather_connector.dtsi * Original by Richard Osterloh. Adapted for RP2350 by Sam Blenny. * * On the Feather RP2350 silk screen, the last 2 pins are marked as 4 and 7. * The Feather RP2350 Learn Guide pinout chart labels those pins as D12 (GPIO4) * and D13 (GPIO7). In CircuitPython 9.2, they show up as board.D12, board.D13, * board.IO4, and board.IO7. The SDA and SCL pins for the feather header and QT * I2C port are on the same nets. * * The *gpio-cells, gpio-map-mask, and gpio-map-pass-thru values are * boilerplate that allow Zephyr GPIO pin mode information to be passed around * in the low bits of the pin selectors. In this case, the second cell of all * the gpio-map selectors is "0", but stuff like "GPIO_ACTIVE_LOW" is also a * possibility. See: * https://docs.zephyrproject.org/latest/hardware/porting/shields.html#gpio-nexus-nodes */ / { feather_header: connector { compatible = "adafruit-feather-header"; #gpio-cells = <2>; gpio-map-mask = <0xffffffff 0xffffffc0>; gpio-map-pass-thru = <0 0x3f>; gpio-map = <0 0 &gpio0 26 0>, /* A0 */ <1 0 &gpio0 27 0>, /* A1 */ <2 0 &gpio0 28 0>, /* A2 */ <3 0 &gpio0 29 0>, /* A3 */ <4 0 &gpio0 24 0>, /* D24 */ <5 0 &gpio0 25 0>, /* D25 */ <6 0 &gpio0 22 0>, /* SCK */ <7 0 &gpio0 23 0>, /* MOSI */ <8 0 &gpio0 20 0>, /* MISO */ <9 0 &gpio0 1 0>, /* RX */ <10 0 &gpio0 0 0>, /* TX */ <11 0 &gpio0 8 0>, /* PSRAM_CS */ <12 0 &gpio0 2 0>, /* SDA */ <13 0 &gpio0 3 0>, /* SCL */ <14 0 &gpio0 5 0>, /* D5 */ <15 0 &gpio0 6 0>, /* D6 */ <16 0 &gpio0 9 0>, /* D9 */ <17 0 &gpio0 10 0>, /* D10 */ <18 0 &gpio0 11 0>, /* D11 */ <19 0 &gpio0 4 0>, /* D12 */ <20 0 &gpio0 7 0>; /* D13 */ }; }; feather_serial: &uart0 {}; feather_i2c: &i2c1 {}; feather_spi: &spi0 {};
There are several other files involved in the board definition. If you're curious you can check out the full set of board definition files in my GitHub repo at: zphqst-02/boards/adafruit/feather_rp2350.
EYESPI Breakout Shield
The two files below will make a shield called eyespi_mipi if you put them in a boards/shields/eyespi_mipi directory. The point of this shield is to translate Feather header pin specifiers into EYESPI pin specifiers. With that translation in place, we can make a second shield that just refers to EYESPI pin specifiers without worrying about what sort of board or shield those connector pins might be wired up to.
For example, in theory, I could make a shield for the Adafruit Proto Tripler PiCowbell, which includes an EYESPI connector, and reuse the ST7789 display shield from this guide with the Tripler PiCowbell and a Raspberry Pi Pico.
These are the files for the EYESPI Breakout shield:
boards/shields/eyespi_mipi/Kconfig.shield:
# SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright 2025 Sam Blenny config SHIELD_EYESPI_MIPI def_bool $(shields_list_contains,eysepi_mipi)
boards/shields/eyespi_mipi/eyespi_mipi.overlay:
/*
* SPDX-License-Identifier: Apache-2.0
* SPDX-FileCopyrightText: Copyright 2025 Sam Blenny
*
*
* This maps from Feather header to EYESPI MIPI (spi + CS, DC, and RST).
* To use this, build with a feather board, this shield, and an EYESPI MIPI
* display shield.
*
* The pin mapping here is for a minimal SPI MIPI display configuration without
* support for the microSD slot, I2C touch input, etc.
*
* For hardware, there isn't actually a PCB that goes from Feather to EYESPI.
* This mapping is for connecting a Feather board to an Adafruit EYESPI
* Breakout (P/N 5613) with a breadboard and hookup wire, using a subset of
* the pinout in the EYESPI Breakout's Learn Guide.
*
* The boilerplate #gpio-cells, gpio-map-mask, gpio-map-pass-thru sequence
* is necessary because Zephyr encodes pin mode information in the pin
* specifiers. For more detail, read these docs:
* - https://docs.zephyrproject.org/latest/hardware/porting/shields.html#gpio-nexus-nodes
* - https://github.com/devicetree-org/devicetree-specification/blob/v0.4/source/chapter2-devicetree-basics.rst#nexus-nodes-and-specifier-mapping
*
* For EYESPI pinout docs, check out the EYESPI Breakout Learn Guide:
* - https://learn.adafruit.com/adafruit-eyespi-breakout-board
*/
/ {
eyespi_mipi: eyespi_connector {
compatible = "eyespi-mipi";
#gpio-cells = <2>;
gpio-map-mask = <0xffffffff 0xffffffc0>;
gpio-map-pass-thru = <0 0x3f>;
gpio-map =
/* 0 0 */ /* Vin */
/* 1 0 */ /* Lite */
/* 2 0 */ /* Gnd */
< 3 0 &feather_header 6 0>, /* SCK = feather SCK */
< 4 0 &feather_header 7 0>, /* MOSI = feather MOSI */
< 5 0 &feather_header 8 0>, /* MISO = feather MISO */
< 6 0 &feather_header 15 0>, /* DC = feather D6 */
< 7 0 &feather_header 16 0>, /* RST = feather D9 */
< 8 0 &feather_header 14 0>; /* TCS = feather D5 */
/* 9 0 */ /* SDCS */
/* 10 0 */ /* MEMCS */
/* 11 0 */ /* TSCS */
/* 12 0 */ /* SCL */
/* 13 0 */ /* SDA */
/* 14 0 */ /* INT */
/* 15 0 */ /* BUSY */
/* 16 0 */ /* GP1 */
/* 17 0 */ /* GP2 */
};
};
eyespi_spi: &feather_spi {};
CircuitPython Init Sequence
My goal here is to get the Zephyr display driver working as well as the CircuitPython display driver. So, by looking at how the CircuitPython driver initializes the display we can prepare for configuring the Zephyr driver.
CircuitPython's ST7789 driver specifies display initialization using a byte string that holds an encoded sequence of MIPI commands and variable length delays. DisplayIO decodes the byte string and sends the commands to the display with the requested timing.
Instead of hand-decoding the byte string, I just ran the code with a Saleae logic analyzer hooked up to the EYESPI pins. To understand what those MIPI display control commands were doing, I cross-referenced the command and data bytes from Saleae's SPI analyzer against the ST7798V datasheet.
This screenshot shows the CircuitPython MIPI commands to reset and initialize the display:
This table has a transcription of the timing marker annotations from the screenshot, plus some extra notes from the ST7789V datasheet:
| SPI | CMD / Data | Delay | Description |
|---|---|---|---|
| 0x01 | SWRESET | 150ms | software reset |
| 0x11 | SLPOUT | 500ms | sleep out |
| 0x3A | COLMOD | interface pixel format | |
| 0x55 | (data) | 10ms | 65K of RGB data (0b101) at 16bit/pixel (0b101) |
| 0x36 | MADCTL | memory data access control | |
| 0x08 | (data) | 10ms | MY: top to bot, MX: L to R, MV: normal, ML: top to bot, RGB: BGR, MH: L to R |
| 0x21 | INVON | 10ms | display inversion |
| 0x13 | NORON | 10ms | partial off (normal) |
| 0x36 | MADCTL | memory data access control | |
| 0xC0 | (data) | 10ms | MY: bot to top, MX: R to L, MV: normal, ML: top to bot, RGB: RGB, MH: L to R |
| 0x29 | DISPON | 500ms | display on |
Some notes on what those commands are doing:
- COLMOD command specifies framebuffer format is 16-bit RGB565
- First MADCTL command sets top-to-bottom, left-to-right rotation with BGR color order
- Second MADCTL command changes to bottom-to-top, right-to-left rotation with RGB color order
Zephyr sitronix,st7789v Driver
Compared to the CircuitPython ST7789 driver, Zephyr's sitronix,st7789v driver takes a different approach. The Zephyr driver requires many display register values to be specified as Devicetree parameters, even if you don't actually need to change the display's defaults. The driver hardcodes a sequence of delays and MIPI commands, but the data parameter values come from Devicetree properties.
Many of the required Devicetree properties are not set by the CircuitPython driver, so I had to look up default values in the ST7798V datasheet.
To keep things organized and assist with cross-referencing, I created a table of Devicetree properties, C struct field names, header file #define constants, MIPI command hex values, and default values from the datasheet. The struct fields and defined constants come from display_st7789v.c and display_st7789v.h in the Zephyr GitHub repository.
| Devicetree | Struct | Define | Hex | Datasheet Default |
|---|---|---|---|---|
| vcom | .vcom | ST7789V_CMD_VCOMS | 0xbb | 0x20 |
| gctrl | .gctrl | ST7789V_CMD_GCTRL | 0xb7 | 0x35 |
| mdac | .mdac | ST7789V_CMD_MADCTL | 0x36 | use CircuitPython 0x0C |
| gamma | .gamma | ST7789V_CMD_GAMSET | 0x26 | 0x01 (gamma 2.2) |
| colmod | .colmod | ST7789V_CMD_COLMOD | 0x3a | use CircuitPython 0x55 |
| lcm | .lcm | ST7789V_CMD_LCMCTRL | 0xc0 | 0x2C |
| porch-param | .porch_param | ST7789V_CMD_PORCTRL | 0xb2 | 0C 0C 00 33 33 |
| cmd2en-param | .cmd2en_param | ST7789V_CMD_CMD2EN | 0xdf | 5A 69 02 00 |
| pwctrl1-param | .pwctrl1_param | ST7789V_CMD_PWCTRL1 | 0xd0 | A4 81 |
| pvgam-param | .pvgam_param | ST7789V_CMD_PVGAMCTRL | 0xe0 | d0 00 02 07 0b 1a 31 54 40 29 12 12 12 17 |
| nvgam-param | .nvgam_param | ST7789V_CMD_NVGAMCTRL | 0xe1 | d0 00 02 07 05 25 2d 44 44 1c 18 16 1c 1d |
| ram-param | .ram_param | ST7789V_CMD_RAMCTRL | 0xb0 | 00 f0 |
| rgb-param | .rgb_param | ST7789V_CMD_RGBCTRL | 0xb1 | 40 02 14 |
Making the above table was mostly just a matter of copying default values from the datasheet. But, the gamma calibration array values were tricky. The required properties for positive (pvgam-param) and negative (nvgam-param) gamma calibration correspond to the datasheet's PVGAMCTRL and NVGAMCTRL MIPI commands. Each of those takes an array of 14 parameter data bytes.
Unfortunately, the datasheet doesn't list default values for the 14 parameter bytes. Instead, it gives a table of 18 gamma levels and a chart showing how the 18 values get packed as bitfields into 14 bytes. To calculate the array of bytes to use with the Devicetree properties, I wrote two short programs to pack the bitfields.
This Python code calculates the parameter byte array for PVGAMCTRL:
vp0 = 0
vp1 = 0
vp2 = 0x2
vp4 = 0x7
vp6 = 0xb
vp13 = 0xa
vp20 = 0x31
vp27 = 0x4
vp36 = 0x5
vp43 = 0x40
vp50 = 0x9
vp57 = 0x12
vp59 = 0x12
vp61 = 0x12
vp62 = 0x17
vp63 = 0xd
jp0 = 0x1
jp1 = 0x2
params = [
(vp63 << 4) | vp0, vp1, vp2, vp4, vp6, (jp0 << 4) | vp13, vp20,
(vp36 << 4) | vp27, vp43, (jp1 << 4) | vp50, vp57, vp59, vp61, vp62,
]
print("PVGAMCTRL params = [" + " ".join(['%02x' % p for p in params]) + "]")
#
# PVGAMCTRL params = [d0 00 02 07 0b 1a 31 54 40 29 12 12 12 17]
This Python code calculates the parameter byte array for NVGAMCTRL:
vn0 = 0x0
vn1 = 0x0
vn2 = 0x2
vn4 = 0x7
vn6 = 0x5
vn13 = 0x5
vn20 = 0x2d
vn27 = 0x4
vn36 = 0x4
vn43 = 0x44
vn50 = 0xc
vn57 = 0x18
vn59 = 0x16
vn61 = 0x1c
vn62 = 0x1d
vn63 = 0xd
jn0 = 0x2
jn1 = 0x1
params = [
(vn63 << 4) | vn0, vn1, vn2, vn4, vn6, (jn0 << 4) | vn13, vn20,
(vn36 << 4) | vn27, vn43, (jn1 << 4) | vn50, vn57, vn59, vn61, vn62,
]
print("NVGAMCTRL params = [" + " ".join(['%02x' % p for p in params]) + "]")
#
# NVGAMCTRL params = [d0 00 02 07 05 25 2d 44 44 1c 18 16 1c 1d]
Assembling all the previously described devicetree stuff into an adafruit_2in_tft_ips_display shield definition, we end up with these two files:
boards/shields/adafruit_2in_tft_ips_display/Kconfig.shield:
# SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright 2025 Sam Blenny config SHIELD_ADAFRUIT_2IN_TFT_IPS_DISPLAY def_bool $(shields_list_contains,adafruit_2in_tft_ips_display)
boards/shields/adafruit_2in_tft_ips_display/adafruit_2in_tft_ips_display.overlay:
/*
* SPDX-FileCopyrightText: Copyright 2025 Sam Blenny
* SPDX-License-Identifier: Apache-2.0
*
* Related docs & reference code:
* - https://docs.zephyrproject.org/latest/build/dts/api/bindings/mipi-dbi/zephyr%2Cmipi-dbi-spi.html
* - https://docs.zephyrproject.org/4.0.0/build/dts/api/bindings/display/sitronix%2Cst7789v.html
* - https://newhavendisplay.com/content/datasheets/ST7789V.pdf
* - https://github.com/adafruit/Adafruit_CircuitPython_ST7789/blob/1.6.4/adafruit_st7789.py
* - https://github.com/adafruit/circuitpython/blob/main/shared-bindings/busdisplay/BusDisplay.c
* - https://github.com/adafruit/circuitpython/blob/main/shared-module/busdisplay/BusDisplay.c
* - https://docs.circuitpython.org/en/latest/shared-bindings/busdisplay/
* - https://learn.adafruit.com/2-0-inch-320-x-240-color-ips-tft-display/circuitpython-displayio-quickstart
*
*/
#include <zephyr/dt-bindings/mipi_dbi/mipi_dbi.h>
&eyespi_spi {
/* Board def probably has wrong cs-gpios, so set it properly */
cs-gpios = <&eyespi_mipi 8 GPIO_ACTIVE_LOW>;
/* Datasheet: min serial clock write cycle is 66 ns (15.15 MHz) */
clock-frequency = <1000000>; /* 1 MHz */
};
/ {
chosen {
zephyr,display = &st7789v_adafruit_2in_tft_ips_display;
};
mipi_dbi {
compatible = "zephyr,mipi-dbi-spi";
/* Configure spi device with DC and reset pins */
spi-dev = <&eyespi_spi>;
dc-gpios = <&eyespi_mipi 6 GPIO_ACTIVE_HIGH>;
reset-gpios = <&eyespi_mipi 7 GPIO_ACTIVE_LOW>;
write-only;
#address-cells = <1>;
#size-cells = <0>;
st7789v_adafruit_2in_tft_ips_display: st7789v@0 {
compatible = "sitronix,st7789v";
reg = <0>;
mipi-max-frequency = <1000000>; /* 1 MHz */
mipi-mode = "MIPI_DBI_MODE_SPI_4WIRE";
x-offset = <0>;
y-offset = <0>;
height = <320>; /* Y range is 0..319 */
width = <240>; /* X range is 0..239 */
/* These values copied from CircuitPython init sequence */
colmod = <0x55>; /* 16-bit 5-6-5 */
mdac = <0x0C>;
/* Set required properties to defaults from ST7789V datasheet */
vcom = <0x20>;
gctrl = <0x35>;
lcm = <0x2C>;
gamma = <0x01>;
porch-param = [0C 0C 00 33 33];
cmd2en-param = [5A 69 02 00];
pwctrl1-param = [A4 81];
pvgam-param = [d0 00 02 07 0b 1a 31 54 40 29 12 12 12 17];
nvgam-param = [d0 00 02 07 05 25 2d 44 44 1c 18 16 1c 1d];
ram-param = [00 f0];
rgb-param = [40 02 14];
};
};
};
Fixing Data/Command Polarity
It wasn't easy getting the Devicetree configuration for the sitronix,st7789v driver to work. In particular, I spent a long time failing to realize that I had specified the data/command (DC) pin as <&eyespi_mipi 6 GPIO_ACTIVE_LOW> when it was supposed to be <&eyespi_mipi 6 GPIO_ACTIVE_HIGH>.
After a long time reading the datasheet and eliminating other possibilities, I finally sat down with the logic analyzer to carefully look at signal timing and polarity. That's when I found my mistake.
This is a screenshot showing the DC signal before I fixed the polarity (high for the command byte on the left, low for the parameter data bytes on the right):
And this is how the DC signal looked once I fixed my mistake by specifying GPIO_ACTIVE_HIGH:
Once all the Devicetree configuration stuff in the EYESPI and TFT display shields is in good order, building and running Zephyr's samples/drivers/display sample application is straightforward.
This is how I build and flash the sample app in a terminal with the bash shell on Debian 12:
$ cd ~/code/zephyr-workspace
$ source .venv/bin/activate
(.venv) $ cd zphqst-02
(.venv) $ rm -r build
(.venv) $ west build -b feather_rp2350/rp2350a/m33 \
--shield eyespi_mipi \
--shield adafruit_2in_tft_ips_display \
../zephyr/samples/drivers/display \
-- -DBOARD_ROOT=$(pwd) -DOPENOCD=../openocd/build/bin/openocd
(.venv) $ west flash
This is what the code looks like when it runs (display shows white background with red, green, blue, and gray squares in the corners, the tone of the gray square fades from black to white):
This page (Zephyr Quest: ST7789 Display with Feather RP2350) was last updated on March 04, 2025.
Text editor powered by tinymce.