From 5bdc6de0ed29a44f0d3dc382d326ddab32c9f99c Mon Sep 17 00:00:00 2001 From: mikecarper Date: Sat, 17 Oct 2026 11:02:29 -0700 Subject: [PATCH] Sleep ST7735 controllers before releasing display power --- .github/workflows/run-unit-tests.yml | 1 + src/helpers/ui/ST7735Display.cpp | 6 +++ test/test_st7735_display_wake_profile.py | 65 ++++++++++++++++++++++++ 3 files changed, 72 insertions(+) diff --git a/.github/workflows/run-unit-tests.yml b/.github/workflows/run-unit-tests.yml index 08e935e4c..9b7ecd741 100644 --- a/.github/workflows/run-unit-tests.yml +++ b/.github/workflows/run-unit-tests.yml @@ -822,6 +822,7 @@ jobs: python3 -B test/test_backlight_poll_rollover.py -v python3 -B test/test_rgb_led_rollover.py -v python3 -B test/test_display_power.py + python3 -B test/test_st7735_display_wake_profile.py -v python3 -B test/test_legacy_ui_version_bounds.py -v python3 -B test/test_infrastructure_ui_version.py -v python3 -B test/test_ssd1306_power.py -v diff --git a/src/helpers/ui/ST7735Display.cpp b/src/helpers/ui/ST7735Display.cpp index 86965450d..20ad54b3c 100644 --- a/src/helpers/ui/ST7735Display.cpp +++ b/src/helpers/ui/ST7735Display.cpp @@ -581,6 +581,12 @@ void ST7735Display::turnOff() { // Now turn off the backlight digitalWrite(PIN_TFT_LEDA_CTL, !PIN_TFT_LEDA_CTL_ACTIVE); + // DISPOFF only blanks the panel. GPS can retain the T096's shared rail, + // and some boards never gate it: stop the controller oscillator/converter + // too. Let Sleep In settle before releasing the pins or removing power. + sendCommand(ST77XX_SLPIN); + delay(5); + // Prevent back-powering to save 2.8 mA. Keep the SPI object initialized: // begin() restores these GPIO directions explicitly on the next wake, and // retaining the peripheral avoids an unchecked nrfx uninit/reinit cycle. diff --git a/test/test_st7735_display_wake_profile.py b/test/test_st7735_display_wake_profile.py index e03d6a064..c56b00c58 100644 --- a/test/test_st7735_display_wake_profile.py +++ b/test/test_st7735_display_wake_profile.py @@ -1,14 +1,79 @@ #!/usr/bin/env python3 from pathlib import Path +import os +import subprocess +import tempfile import unittest +from cpp_source import body + ROOT = Path(__file__).resolve().parents[1] DRIVER = ROOT / "src" / "helpers" / "ui" / "ST7735Display.cpp" class ST7735DisplayWakeProfileTest(unittest.TestCase): + def test_sleep_command_precedes_pin_release_with_shared_or_ungated_power(self): + source = r''' +#include +#include +#include +constexpr int INPUT = 0, PIN_TFT_CS = 1, PIN_TFT_DC = 2, PIN_TFT_SDA = 3; +constexpr int PIN_TFT_SCL = 4, PIN_TFT_RST = 5, PIN_TFT_LEDA_CTL = 6; +constexpr int PIN_TFT_LEDA_CTL_ACTIVE = 1; +constexpr int ST77XX_DISPOFF = 0x28, ST77XX_SLPIN = 0x10; +static unsigned commands, pins, waits, waited; +static bool sleeping; +void sendCommand(int command) { + assert(command == (commands == 0 ? ST77XX_DISPOFF : ST77XX_SLPIN)); + assert(commands < 2 && pins == 0); + if (command == ST77XX_SLPIN) sleeping = true; + ++commands; +} +void digitalWrite(int pin, int value) { + assert(pin == PIN_TFT_LEDA_CTL && value == !PIN_TFT_LEDA_CTL_ACTIVE); +} +void delay(unsigned ms) { assert(sleeping); ++waits; waited += ms; } +void pinMode(int, int mode) { + assert(mode == INPUT && sleeping && waited >= 5); + ++pins; +} +struct Rail { + unsigned claims; + void release() { assert(sleeping && waited >= 5 && pins == 6); --claims; } +}; +struct ST7735Display { + bool _isOn = true; + Rail* _peripher_power; + void turnOff(); +}; +@METHOD@ +int main() { + // Standalone display, GPS sharing the rail, and boards with no rail control. + for (unsigned users : {1U, 2U, 0U}) { + commands = pins = waits = waited = 0; sleeping = false; + Rail rail{users}; + ST7735Display display{true, users ? &rail : nullptr}; + display.turnOff(); + assert(sleeping && commands == 2 && waits == 1 && pins == 6 && !display._isOn); + assert(rail.claims == (users ? users - 1 : 0)); + display.turnOff(); + assert(commands == 2 && waits == 1 && pins == 6); + } +} +''' + source = source.replace('@METHOD@', body(DRIVER.read_text(), + 'void ST7735Display::turnOff()')) + with tempfile.TemporaryDirectory(prefix='meshcore-tft-sleep-') as directory: + cpp, binary = Path(directory) / 'test.cpp', Path(directory) / 'test' + cpp.write_text(source, encoding='ascii') + subprocess.run([os.environ.get('CXX', 'c++'), '-std=c++17', + '-Wall', '-Wextra', '-Werror', str(cpp), '-o', str(binary)], + check=True, capture_output=True, text=True, timeout=60) + result = subprocess.run([str(binary)], capture_output=True, text=True, timeout=10) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + def test_power_off_releases_bus_without_uninitializing_spi(self): source = DRIVER.read_text(encoding="utf-8") turn_off_start = source.index("void ST7735Display::turnOff()")