<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[DisplayEngineer]]></title><description><![CDATA[DisplayEngineer]]></description><link>https://displayengineer.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Tue, 27 Oct 2026 01:51:32 GMT</lastBuildDate><atom:link href="https://displayengineer.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Why your ST7701S screen goes black after a reboot in ESPHome]]></title><description><![CDATA[ESPHome 2026.10.0 adds a 50 ms reset delay for ST7701S, but some boards still need a real reset pin, and the old st7701s platform never drives one.]]></description><link>https://displayengineer.hashnode.dev/st7701s-black-after-reboot-esphome-reset-pin</link><guid isPermaLink="true">https://displayengineer.hashnode.dev/st7701s-black-after-reboot-esphome-reset-pin</guid><category><![CDATA[esphome]]></category><category><![CDATA[ESP32]]></category><category><![CDATA[Home Assistant]]></category><category><![CDATA[lvgl]]></category><category><![CDATA[embedded]]></category><dc:creator><![CDATA[DisplayEngineer]]></dc:creator><pubDate>Thu, 22 Oct 2026 06:17:35 GMT</pubDate><content:encoded><![CDATA[<p>The bug report that sent me into ESPHome's reset code keeps showing up in slightly different forms. Someone flashes a Waveshare ESP32-S3-Touch-LCD-4 over USB and the 480×480 ST7701S panel comes up fine. They unplug it, plug it back in, and get a lit backlight over a black screen. Reflash and the picture is back. Power cycle and it's gone again.</p>
<p>That's <a href="https://github.com/esphome/esphome/issues/17636">esphome#17636</a>, opened in July against ESPHome 2026.6.5. Quick disclosure before I go on: I work at a display manufacturer, so I'm not exactly neutral about ST7701S panels. But "works after flashing, black after reboot" is nearly always about reset, not about porches or pixel clock. The controller isn't in a known state when the init commands show up, and SPI writes into a confused chip just vanish.</p>
<p>ESPHome 2026.10.0 came out on 21 October with a fix in exactly this area. I read through it, and on its own it does less than the changelog line makes it sound. You probably need to check one thing in your YAML too.</p>
<p>The fix is <a href="https://github.com/esphome/esphome/pull/17646">esphome#17646</a>, "[mipi_rgb] Fix SWRESET delay for st7701s", from clydebarrow. In his words: "It's also been shown that the published delay (5ms) after reset for an ST7701S appears to be insufficient, and a longer delay is required to ensure proper initialisation." The PR also says an earlier change (<a href="https://github.com/esphome/esphome/pull/17504">#17504</a>) had left a duplicate software reset in the ST7701S init sequence with no delay after it.</p>
<p>The diff matches that. In 2026.9.1 the ST7701S model in <code>mipi_rgb</code> started its init sequence with a bare <code>(0x01,)</code>, the software reset command, and the very next thing sent was the <code>0xFF 0x77 0x01...</code> page select. Nothing in between. In 2026.10.0 that line is gone. Reset now gets added in one place, <code>get_sequence()</code> in <code>esphome/components/mipi/__init__.py</code>: if you haven't configured a <code>reset_pin</code> it inserts SWRESET, and either way it follows with a settle delay. That delay is a per-model <code>reset_delay</code> that defaults to 10 ms, and the ST7701S class sets it to 50. The PR also reorders <code>MipiRgbSpi::setup()</code> so the RGB panel is created first, and the init sequence is only sent if that worked.</p>
<p>I wanted to see it in the generated code rather than take the diff's word for it, so I ran <code>esphome compile --only-generate</code> on 2026.10.0 with the <code>SEEED-INDICATOR-D1</code> model. With a <code>reset_pin</code> set, the <code>mipi_rgb_init_sequence[]</code> array in <code>main.cpp</code> starts with <code>50, 255</code>, which is a 50 ms delay marker. Without one it starts with <code>1, 0, 50, 255</code>: SWRESET with no parameters, then the same 50 ms. So it does what it says.</p>
<p>One thing that tripped me up: <code>reset_delay</code> is a model default in Python, not a YAML option. The <a href="https://esphome.io/components/display/mipi_rgb/">mipi_rgb docs</a> don't list it and you can't set it per device. If 50 ms isn't enough for some odd panel, that needs a code change.</p>
<p>Now the part I found more interesting. The person who opened #17636 tested the PR on their board and wrote back: "Tested PR #17646, clean build, ESPHome 2026.7.0, USB flash. Does not fix it." It actually got a bit worse for them, because after a power cycle only Waveshare's factory firmware would bring the panel back.</p>
<p>What fixed it was the reset pin. Their board is the Rev 4.0 version, and on that one the LCD reset sits behind a CH32V003-based IO expander at 0x24 rather than the TCA9554 older revisions used. clydebarrow posted a Rev 4 config with <code>reset_pin</code> routed through the <code>waveshare_io_ch32v003</code> component, pin 3, and the reply was "We have display working now and surviving power cycling." In 2026.10.0 the <code>WAVESHARE-4-480X480</code> model in <code>models/waveshare.py</code> still has no reset pin of its own (it only requires PSRAM), so you have to add it yourself. The display part of that config looks like this:</p>
<pre><code class="language-yaml">display:
  - platform: mipi_rgb
    model: WAVESHARE-4-480X480
    reset_pin:
      waveshare_io_ch32v003: wave_io_hub
      number: 3
</code></pre>
<p>That only fits Rev 4. The maintainer warned in the thread that the expander changes between revisions, so check yours before copying it.</p>
<p>So my reading is that the longer delay is necessary but only half the story. A software reset can only reach a chip that's still listening on SPI. A hardware reset pulse doesn't care what state the controller got itself into.</p>
<p>Which brings me to the older <code>platform: st7701s</code>, still in 2026.10.0 and still what a lot of forum configs use. Its <a href="https://esphome.io/components/display/st7701s/">docs page</a> has a SenseCAP Indicator example with <code>reset_pin</code> on a PCA9554, pin 5. The schema accepts it and <code>display.py</code> passes it to <code>set_reset_pin()</code>. But in <a href="https://github.com/esphome/esphome/blob/2026.10.0/esphome/components/st7701s/st7701s.cpp"><code>st7701s.cpp</code></a> the only place <code>reset_pin_</code> appears is the <code>LOG_PIN</code> line in <code>dump_config()</code>. Nothing ever drives it. Your log shows a reset pin, and the pin just sits there.</p>
<p>There is a call to <code>esp_lcd_panel_reset()</code> in <code>setup()</code>, which sounds like a reset, but for an RGB panel ESP-IDF's <code>rgb_panel_reset()</code> in <a href="https://github.com/espressif/esp-idf/blob/v5.5.1/components/esp_lcd/rgb/esp_lcd_panel_rgb.c"><code>esp_lcd_panel_rgb.c</code></a> only calls <code>lcd_ll_fifo_reset()</code> and <code>lcd_ll_reset()</code>. That's the ESP32-S3's own LCD peripheral. The panel never sees it. It also runs after the init sequence has already gone out over SPI. The only reset the old driver gives the ST7701S is the <code>0x01</code> at the top of canned init sequence 1, followed by <code>delay(6)</code>. That's about the same 5-ish ms window the PR now calls insufficient.</p>
<p>I'm not claiming this is why every black-screen-after-reboot thread happens. Plenty of boards pull the panel's reset line high and come up clean from a real cold start, which is probably why the old platform has worked for so many people. On a warm reboot or an OTA restart, though, the panel stays powered and nothing resets it in hardware, and that's when I'd expect trouble.</p>
<p><code>mipi_rgb</code> does pulse the pin. <code>setup_enables_()</code> in <a href="https://github.com/esphome/esphome/blob/2026.10.0/esphome/components/mipi_rgb/mipi_rgb.cpp"><code>mipi_rgb.cpp</code></a> drives it high, waits 5 ms, pulls it low for 5 ms, releases it, and the 50 ms from the init sequence comes after. If you're on <code>platform: st7701s</code> and your board has a named <code>mipi_rgb</code> model, moving over is usually a short job. For the SenseCAP Indicator it comes out like this, with bus and expander values from the <a href="https://github.com/esphome/esphome-devices/blob/main/src/docs/devices/Seeed-SenseCAP/index.md">ESPHome devices page</a> for that board:</p>
<pre><code class="language-yaml"># Not tested on hardware. With an esphome: name block added,
# this passes `esphome config` on ESPHome 2026.10.0.
esp32:
  variant: esp32s3
  framework:
    type: esp-idf

psram:
  mode: octal
  speed: 80MHz

spi:
  - id: lcd_spi
    clk_pin: GPIO41
    mosi_pin: GPIO48
    miso_pin: GPIO47

i2c:
  - id: bus_a
    sda: GPIO39
    scl: GPIO40
    scan: false

pca9554:
  - id: pca9554a_device
    address: 0x20
    pin_count: 16

display:
  - platform: mipi_rgb
    model: SEEED-INDICATOR-D1
    id: sensecap_display
    update_interval: 1s
    reset_pin:
      pca9554: pca9554a_device
      number: 5
    lambda: |-
      it.fill(Color(255, 0, 0));
</code></pre>
<p>The <code>reset_pin</code> block is the whole point. The <code>SEEED-INDICATOR-D1</code> model already puts CS on expander pin 4, but like the Waveshare one it doesn't define a reset pin, so if you copy the minimal three-line example from the devices page you get the software reset path instead. A solid red fill with <code>update_interval: 1s</code> is just a test. Swap in your real lambda or LVGL once the panel survives a few power cycles and a couple of OTA restarts.</p>
<p>If your board isn't a named model, there's a generic <code>ST7701S</code> model and a <code>custom</code> one in <code>mipi_rgb</code>, and the docs have a full custom example. While you're there, note that the other <code>mipi_rgb</code> change in this release, software rotation for pure RGB panels (<a href="https://github.com/esphome/esphome/pull/20757">#20757</a>), says outright that ST7701S boards and CUSTOM are unchanged, so your <code>mirror_x</code>/<code>mirror_y</code> transform should keep validating.</p>
<p>If you're wiring an ST7701S panel into your own board rather than buying a dev kit, I keep the pinout and init notes on the <a href="https://www.displayengineer.com/driver-ic/st7701s">ST7701S driver page</a> on our site. The shorter ESPHome version of all this is in <a href="https://github.com/qiren/esphome-tft-display-examples">esphome-tft-display-examples</a> on GitHub.</p>
<p>If you've got an ST7701S board that still goes black after a reboot on 2026.10.0 with a real reset pin wired in, I'd honestly like to hear which one it is.</p>
]]></content:encoded></item><item><title><![CDATA[ESPHome Drops ili9xxx, st7789v and st7735 in 2027.5.0: Migrating SPI TFTs to mipi_spi]]></title><description><![CDATA[ESPHome removes ili9xxx, st7789v, st7735 in 2027.5.0. Map old YAML to mipi_spi and fix white screens, inverted colours, offsets.]]></description><link>https://displayengineer.hashnode.dev/esphome-drops-ili9xxx-st7789v-and-st7735-in-2027-5-0-migrating-spi-tfts-to-mipi-spi</link><guid isPermaLink="true">https://displayengineer.hashnode.dev/esphome-drops-ili9xxx-st7789v-and-st7735-in-2027-5-0-migrating-spi-tfts-to-mipi-spi</guid><category><![CDATA[esphome]]></category><category><![CDATA[ESP32]]></category><category><![CDATA[embedded]]></category><category><![CDATA[iot]]></category><category><![CDATA[Home Assistant]]></category><dc:creator><![CDATA[DisplayEngineer]]></dc:creator><pubDate>Mon, 19 Oct 2026 00:33:02 GMT</pubDate><content:encoded><![CDATA[<p>ESPHome has now put a date on the end of its legacy SPI display drivers. Four pull requests merged on 14 October 2026 (<a href="https://github.com/esphome/esphome/pull/20687">#20687</a> <code>ili9xxx</code>, <a href="https://github.com/esphome/esphome/pull/20688">#20688</a> <code>st7735</code>, <a href="https://github.com/esphome/esphome/pull/20689">#20689</a> <code>st7789v</code>, <a href="https://github.com/esphome/esphome/pull/20690">#20690</a> <code>qspi_dbi</code>) change the deprecation warning to: <em>"The 'ili9xxx' component is deprecated and will be removed in ESPHome 2027.5.0, migrate to 'mipi_spi' instead."</em> The PRs say the components have been deprecated since April 2026 and that the dated warning ships in 2026.11, six months before removal.</p>
<p>If a small SPI TFT (ILI9341, ST7789V, ST7735, ILI9488, ST7796S) runs on one of those drivers, this guide maps your old YAML onto <code>mipi_spi</code> and lists the likely problems.</p>
<blockquote>
<p><strong>Honesty note:</strong> we wrote every option, default and model name below from the current ESPHome docs and the <code>esphome/esphome</code> source on the <code>dev</code> branch (links at the end). We have <strong>not</strong> run these YAML snippets on hardware. Treat them as starting points and check each value on your own panel. <code>qspi_dbi</code> (quad-SPI panels, which move to <code>mipi_spi</code> with <code>bus_mode: quad</code>) is out of scope here.</p>
</blockquote>
<h2>The option map</h2>
<table>
<thead>
<tr>
<th>Legacy option</th>
<th><code>mipi_spi</code> equivalent</th>
<th>Watch out for</th>
</tr>
</thead>
<tbody><tr>
<td><code>platform: ili9xxx</code> / <code>st7789v</code> / <code>st7735</code></td>
<td><code>platform: mipi_spi</code></td>
<td></td>
</tr>
<tr>
<td><code>model: ILI9341</code>, <code>ST7789V</code>, <code>ST7735</code>, <code>ILI9488</code>, <code>ILI9488_A</code>, <code>ST7796</code></td>
<td>Same names</td>
<td>There is no <code>ST7796S</code> model. ST7796S panels use <code>ST7796</code></td>
</tr>
<tr>
<td><code>model: TTGO TDisplay 135x240</code> (st7789v)</td>
<td><code>model: T-DISPLAY</code></td>
<td></td>
</tr>
<tr>
<td><code>model: INITR_*</code> (st7735)</td>
<td><code>model: ST7735</code></td>
<td>One init sequence for all "tab" variants</td>
</tr>
<tr>
<td><code>device_width</code> / <code>device_height</code> / <code>col_start</code> / <code>row_start</code> (st7735)</td>
<td><code>dimensions:</code> <code>width</code> / <code>height</code> / <code>offset_width</code> / <code>offset_height</code></td>
<td></td>
</tr>
<tr>
<td><code>invert_colors</code> (required in ili9xxx)</td>
<td><code>invert_colors</code>, default <code>false</code></td>
<td>The old <code>st7789v</code> driver <strong>always</strong> sent INVON</td>
</tr>
<tr>
<td><code>use_bgr</code> (st7735), <code>color_order</code> (ili9xxx)</td>
<td><code>color_order: bgr</code> / <code>rgb</code></td>
<td>Default is <code>bgr</code>, but the <code>ST7735</code> model defaults to <code>rgb</code></td>
</tr>
<tr>
<td><code>eightbitcolor</code>, <code>eight_bit_color</code>, <code>color_palette: 8BIT</code></td>
<td><code>color_depth: 8</code></td>
<td>No <code>GRAYSCALE</code> or <code>IMAGE_ADAPTIVE</code> palettes</td>
</tr>
<tr>
<td>SPI default 40 MHz (ili9xxx), 20 MHz (st7789v docs)</td>
<td><code>data_rate</code>, default <strong>10 MHz</strong> unless a board preset overrides it</td>
<td>A likely reason a migrated screen feels slower</td>
</tr>
<tr>
<td><code>rotation</code> (software in ili9xxx)</td>
<td><code>rotation</code>, done in hardware when the chip supports it</td>
<td></td>
</tr>
<tr>
<td><code>transform</code> (keys optional)</td>
<td><code>transform</code>: <code>swap_xy</code>, <code>mirror_x</code>, <code>mirror_y</code> all <strong>required</strong>, and <code>dimensions</code> must be set</td>
<td>Use <code>rotation</code> instead unless the image is mirrored</td>
</tr>
<tr>
<td><code>backlight_pin</code> (st7789v)</td>
<td>none</td>
<td>Drive the backlight with a separate <code>output</code>/<code>light</code></td>
</tr>
</tbody></table>
<p><code>dc_pin</code> is required for single-bit SPI; <code>cs_pin</code> and <code>reset_pin</code> stay optional. Without <code>cs_pin</code>, <code>mipi_spi</code> falls back to SPI <code>MODE3</code> and logs a warning; with one it uses <code>MODE0</code>.</p>
<h2>Before / after pairs</h2>
<h3>ILI9341 240×320 (ili9xxx)</h3>
<pre><code class="language-yaml"># BEFORE
display:
  - platform: ili9xxx
    model: ILI9341
    cs_pin: GPIO5
    dc_pin: GPIO4
    reset_pin: GPIO22
    invert_colors: false
    rotation: 90
    lambda: |-
      it.print(10, 10, id(font1), "Hello");
</code></pre>
<pre><code class="language-yaml"># AFTER - UNTESTED starting point
display:
  - platform: mipi_spi
    model: ILI9341        # native 240x320 portrait, mirror_x set by the model
    cs_pin: GPIO5
    dc_pin: GPIO4
    reset_pin: GPIO22
    data_rate: 40MHz      # ili9xxx used 40MHz by default; mipi_spi defaults to 10MHz
    invert_colors: false
    rotation: 90          # don't swap width/height by hand
    lambda: |-
      it.print(10, 10, id(font1), "Hello");
</code></pre>
<h3>ST7789V 240×320 and 240×240</h3>
<p>A full-size 240×320 ST7789V needs only <code>model: ST7789V</code>. Panels that use part of the controller's RAM need a <code>dimensions</code> block:</p>
<pre><code class="language-yaml"># BEFORE (st7789v, Custom 240x240)
display:
  - platform: st7789v
    model: Custom
    width: 240
    height: 240
    offset_width: 0
    offset_height: 0
    backlight_pin: GPIO21
    cs_pin: GPIO5
    dc_pin: GPIO16
    reset_pin: GPIO23
</code></pre>
<pre><code class="language-yaml"># AFTER - UNTESTED starting point
output:
  - platform: ledc
    pin: GPIO21
    id: bl_pwm
light:
  - platform: monochromatic
    output: bl_pwm
    name: "Display Backlight"
    restore_mode: ALWAYS_ON

display:
  - platform: mipi_spi
    model: ST7789V
    cs_pin: GPIO5
    dc_pin: GPIO16
    reset_pin: GPIO23
    invert_colors: true   # the old st7789v driver hard-coded inversion on
    dimensions:           # use the long form, not "240x240"
      width: 240
      height: 240
      offset_width: 0
      offset_height: 0
</code></pre>
<p>Why the long form? In the source (<code>esphome/components/mipi/__init__.py</code>), the <code>240x240</code> shorthand sets every offset and pad to zero. The long form lets the driver work out <code>pad_height = 320 − 240 − 0 = 80</code> from the ST7789V's native 240×320 size, and the docs say padding is used to support hardware rotation. ESPHome's own <code>GEEKMAGIC-SMALLTV</code> preset, a 240×240 ST7789V, uses 0/0 offsets with <code>invert_colors=True</code>.</p>
<p>We keep a 240×320 ST7789V <code>mipi_spi</code> example for our PJ2801H22 module in <a href="https://github.com/qiren/esphome-tft-display-examples">qiren/esphome-tft-display-examples</a>. It is marked TESTING-NEEDED for the same reason as this post.</p>
<h3>ST7789V 135×240 (TTGO T-Display style)</h3>
<pre><code class="language-yaml"># BEFORE
display:
  - platform: st7789v
    model: TTGO TDisplay 135x240
    backlight_pin: GPIO4
    cs_pin: GPIO5
    dc_pin: GPIO16
    reset_pin: GPIO23
</code></pre>
<pre><code class="language-yaml"># AFTER - UNTESTED starting point (generic 135x240 module)
display:
  - platform: mipi_spi
    model: ST7789V
    cs_pin: GPIO5
    dc_pin: GPIO16
    reset_pin: GPIO23
    invert_colors: true
    dimensions:
      width: 135
      height: 240
      offset_width: 52
      offset_height: 40
</code></pre>
<p>On the actual LilyGO board, <code>model: T-DISPLAY</code> presets the same 135×240 size, 52/40 offsets, CS 5, DC 16 and inversion. It does not preset a reset pin, so add <code>reset_pin</code> if your board has one wired. Drive the backlight on GPIO4 as in the previous example.</p>
<h3>ST7735 128×160</h3>
<pre><code class="language-yaml"># BEFORE
display:
  - platform: st7735
    model: INITR_18BLACKTAB
    cs_pin: GPIO5
    dc_pin: GPIO4
    reset_pin: GPIO22
    device_width: 128
    device_height: 160
    col_start: 0
    row_start: 0
    eight_bit_color: true
</code></pre>
<pre><code class="language-yaml"># AFTER - UNTESTED starting point
display:
  - platform: mipi_spi
    model: ST7735         # 128x160, color_order defaults to rgb for this model
    cs_pin: GPIO5
    dc_pin: GPIO4
    reset_pin: GPIO22
    color_depth: 8        # replaces eight_bit_color
    # if your old col_start/row_start were non-zero:
    # dimensions: {width: 128, height: 160, offset_width: &lt;col_start&gt;, offset_height: &lt;row_start&gt;}
</code></pre>
<p>Legacy "tab" variants with non-zero <code>col_start</code>/<code>row_start</code> need those values in <code>dimensions</code>. Because <code>mipi_spi</code> has a single ST7735 init sequence, mini 160×80 modules in particular need checking on the bench.</p>
<h3>ILI9488 and ST7796S 320×480</h3>
<pre><code class="language-yaml"># AFTER - UNTESTED starting point
display:
  - platform: mipi_spi
    model: ILI9488        # or ST7796 for ST7796S panels, or ILI9488_A
    cs_pin: GPIO10
    dc_pin: GPIO13
    reset_pin: GPIO41
    data_rate: 40MHz
    rotation: 90          # see note below
</code></pre>
<p>Two things to know. First, the legacy <code>ili9xxx</code> source declares its ILI9488 class as 480×320. The <code>mipi_spi</code> ILI9488 model is 320×480 portrait, so a landscape layout from the old driver probably needs <code>rotation: 90</code> or <code>270</code>. Second, the ILI9488 model defaults to <code>pixel_mode: 18bit</code>, which sends 3 bytes per pixel instead of 2, so expect a slower full refresh than on an ST7796 of the same size.</p>
<h2>Where "Invalid offsets." comes from</h2>
<p>When you give <code>dimensions</code> in long form, the driver computes <code>pad = native − size − offset</code> on each axis. A negative result raises <code>Invalid offsets</code>. On an ILI9341 (native 240×320), writing <code>width: 320, height: 240</code> to get landscape gives <code>pad_width = 240 − 320 = −80</code>, and that fails. A maintainer said it plainly in <a href="https://github.com/esphome/esphome/issues/17074">#17074</a>: keep the native orientation and use <code>rotation</code>.</p>
<h2>Lambdas and LVGL</h2>
<p><code>mipi_spi</code> uses the standard display schema, so <code>lambda:</code> and <code>pages:</code> carry over unchanged. The frame buffer is always RGB whatever <code>color_order</code> you set, so your <code>Color()</code> values stay the same. Without PSRAM, and without an explicit <code>buffer_size</code> (12–100%), the driver picks a partial buffer under about 20 kB and calls your lambda several times per frame. Lambdas must therefore have no side effects. If there is no lambda and no LVGL, the driver shows a test card automatically, which is useful for bring-up.</p>
<p>With LVGL, the display needs no buffer of its own. Configure it with <code>auto_clear_enabled: false</code> and <code>update_interval: never</code>. LVGL's own <code>buffer_size</code> defaults to 100% and falls back to 12% at runtime if the allocation fails. The docs recommend 25% without PSRAM.</p>
<h2>Symptom table</h2>
<table>
<thead>
<tr>
<th>Symptom</th>
<th>Likely cause after migrating</th>
<th>Try</th>
</tr>
</thead>
<tbody><tr>
<td>Black screen, backlight off</td>
<td><code>st7789v</code>'s <code>backlight_pin</code> has no <code>mipi_spi</code> equivalent</td>
<td>Add an <code>output</code> + <code>light</code> (or a GPIO switch)</td>
</tr>
<tr>
<td>White screen, backlight on</td>
<td>No valid init: DC/CS swapped, wrong <code>model</code>, SPI too fast</td>
<td>Check wiring, drop to 10 MHz, try the other likely controller (see below)</td>
</tr>
<tr>
<td>White screen on a 16-bit shift-register board after 2026.9.0</td>
<td>Regression, see next section</td>
<td>Update to 2026.9.1 or later</td>
</tr>
<tr>
<td>Colours look like a negative</td>
<td><code>invert_colors</code> now defaults to <code>false</code></td>
<td><code>invert_colors: true</code> (matches the old <code>st7789v</code> behaviour)</td>
</tr>
<tr>
<td>Red and blue swapped</td>
<td><code>color_order</code></td>
<td>Flip between <code>bgr</code> and <code>rgb</code></td>
</tr>
<tr>
<td>Image shifted, garbage rows or columns at one edge</td>
<td>Missing offsets, or <code>WxH</code> shorthand on a panel smaller than the controller</td>
<td>Long-form <code>dimensions</code> with <code>offset_width</code>/<code>offset_height</code></td>
</tr>
<tr>
<td><code>Invalid offsets</code> at compile</td>
<td>Width and height swapped by hand</td>
<td>Native dimensions + <code>rotation</code></td>
</tr>
<tr>
<td>Mirrored text</td>
<td>Model default <code>mirror_x</code> (ILI9341, ST7796, ILI9488_A) does not match your glass</td>
<td><code>transform:</code> with all three keys plus <code>dimensions</code></td>
</tr>
<tr>
<td>Noticeably slower refresh</td>
<td>10 MHz default instead of 40 MHz; small buffer without PSRAM; 18-bit ILI9488</td>
<td>Set <code>data_rate</code>; enable <code>psram:</code>; raise <code>buffer_size</code></td>
</tr>
<tr>
<td>Out of memory / reboot loop</td>
<td>Full-frame buffers in internal RAM</td>
<td><code>color_depth: 8</code>, smaller <code>buffer_size</code>, LVGL <code>buffer_size: 25%</code>, or PSRAM</td>
</tr>
<tr>
<td>Log: "defaulting to MODE3 due to lack of CS pin"</td>
<td>No <code>cs_pin</code></td>
<td>Add <code>cs_pin</code> or set <code>spi_mode</code> explicitly</td>
</tr>
</tbody></table>
<h2>Known regression: white screen after 2026.9.0</h2>
<p><a href="https://github.com/esphome/esphome/issues/19732">#19732</a> reported a white screen on a Waveshare 4" TFT (<code>model: WAVESHARE-4-TFT</code>) after upgrading from 2026.8.2 to 2026.9.0, with no YAML change. Maintainer clydebarrow attributed it to <a href="https://github.com/esphome/esphome/pull/18529">#18529</a>, which moved the D/C toggling inside the SPI bus lock to fix a shared-pin problem on the M5Stack CoreS3. <a href="https://github.com/esphome/esphome/pull/19745">#19745</a> restored the old sequence for the 16-bit (<code>spi_16</code>) path, and its release notes place it in 2026.9.1. This affects boards that drive the panel through a 16-bit shift register, not ordinary ILI9341/ST7789V breakouts. Still, pin your ESPHome version while migrating and read the <code>mipi_spi</code> changelog before upgrading.</p>
<h2>Is it really an ILI9341?</h2>
<p>Mislabeled modules are common. ESPHome itself ships three presets for one Sunton ESP32-2432S028 board: ILI9341 (micro-USB version), ST7789V and ILI9342 (dual-USB versions). Two ways to check:</p>
<ol>
<li><strong>Try the models.</strong> With no lambda, <code>mipi_spi</code> shows a test card. Flash <code>ST7789V</code> with <code>invert_colors: true</code>, then <code>ILI9341</code> with <code>invert_colors: false</code>, and keep the one with correct colours and geometry.</li>
<li><strong>Read the ID.</strong> ESPHome display drivers only write to the panel, so this needs a short Arduino or ESP-IDF sketch and the module's SDO/MISO line wired. Per the datasheets, the ILI9341 answers <em>Read ID4</em> (<code>0xD3</code>) with <code>0x00 0x93 0x41</code> after a dummy byte. The ST7789V answers <em>RDDID</em> (<code>0x04</code>) with <code>0x85 0x85 0x52</code> by default. The ST7789V's ID bytes live in non-volatile memory that module makers can program, so treat an unexpected value as a hint, not proof.</li>
</ol>
<hr />
<p><em>Written by the DisplayEngineer team. We manufacture small TFT and LCD modules; our <a href="https://www.displayengineer.com/driver-ic/st7789v">ST7789V datasheet, pinout and init-code notes</a> cover the controller used in several of the panels above. Corrections from anyone who has tested these configs on real hardware are very welcome.</em></p>
<h2>References</h2>
<ul>
<li>ESPHome docs, MIPI SPI display driver: <a href="https://esphome.io/components/display/mipi_spi/">https://esphome.io/components/display/mipi_spi/</a></li>
<li>ESPHome docs, ILI9xxx (legacy): <a href="https://esphome.io/components/display/ili9xxx/">https://esphome.io/components/display/ili9xxx/</a></li>
<li>ESPHome docs, ST7789V (legacy): <a href="https://esphome.io/components/display/st7789v/">https://esphome.io/components/display/st7789v/</a></li>
<li>ESPHome docs, ST7735 (legacy): <a href="https://esphome.io/components/display/st7735/">https://esphome.io/components/display/st7735/</a></li>
<li>ESPHome docs, LVGL: <a href="https://esphome.io/components/lvgl/">https://esphome.io/components/lvgl/</a></li>
<li>Source: <code>esphome/components/mipi_spi/display.py</code>, <code>models/ili.py</code>, <code>models/lilygo.py</code>, <code>models/cyd.py</code>, <code>esphome/components/mipi/__init__.py</code>, <code>esphome/components/st7789v/st7789v.cpp</code>, <code>esphome/components/ili9xxx/</code> (<a href="https://github.com/esphome/esphome">https://github.com/esphome/esphome</a>, <code>dev</code> branch)</li>
<li>Removal PRs: <a href="https://github.com/esphome/esphome/pull/20687">https://github.com/esphome/esphome/pull/20687</a>, /20688, /20689, /20690</li>
<li>Issue #17074 ("Invalid offsets"): <a href="https://github.com/esphome/esphome/issues/17074">https://github.com/esphome/esphome/issues/17074</a></li>
<li>Issue #19732, PR #18529, fix PR #19745: <a href="https://github.com/esphome/esphome/issues/19732">https://github.com/esphome/esphome/issues/19732</a>, <a href="https://github.com/esphome/esphome/pull/18529">https://github.com/esphome/esphome/pull/18529</a>, <a href="https://github.com/esphome/esphome/pull/19745">https://github.com/esphome/esphome/pull/19745</a></li>
<li>ESPHome 2026.9.1 release notes: <a href="https://github.com/esphome/esphome/releases/tag/2026.9.1">https://github.com/esphome/esphome/releases/tag/2026.9.1</a></li>
<li>ILI9341 datasheet (Read ID4, D3h): <a href="https://cdn-shop.adafruit.com/datasheets/ILI9341.pdf">https://cdn-shop.adafruit.com/datasheets/ILI9341.pdf</a></li>
<li>ST7789V datasheet v1.3 (RDDID, 04h): <a href="https://www.newhavendisplay.com/appnotes/datasheets/LCDs/ST7789V.pdf">https://www.newhavendisplay.com/appnotes/datasheets/LCDs/ST7789V.pdf</a></li>
</ul>
]]></content:encoded></item></channel></rss>