From ec42a74ab6285b105ad66c2bde000e6c3464fc3e Mon Sep 17 00:00:00 2001 From: Jakub <106778416+MarekZegare4@users.noreply.github.com> Date: Mon, 12 Oct 2026 19:30:46 +0200 Subject: [PATCH] perf: unused TinyUSB classes off on nRF52, SSD1306 sends only changed blocks, ESP32 loop yields when idle Co-Authored-By: Claude Opus 5.5 --- examples/companion_radio/main.cpp | 5 +++ examples/companion_radio/ui-lvgl/UITask.h | 4 ++ platformio.ini | 8 ++++ src/helpers/ui/SSD1306Display.cpp | 48 ++++++++++++++++++++++- src/helpers/ui/SSD1306Display.h | 22 ++++++++++- 5 files changed, 85 insertions(+), 2 deletions(-) diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index bde1d506..78b0a69a 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -381,6 +381,11 @@ void loop() { if (!the_mesh.hasPendingWork()) { #if defined(NRF52_PLATFORM) board.sleep(0); // nrf ignores seconds param, sleeps whenever possible +#elif defined(ESP32) && !defined(UI_LOOP_IDLES) + // Arduino's loop task never yields: without this it spins the CPU flat + // out. One tick lets the idle task halt the core (WAITI) until the next + // interrupt or tick -- radio, buttons and serial still get a pass a ms. + vTaskDelay(1); #endif } diff --git a/examples/companion_radio/ui-lvgl/UITask.h b/examples/companion_radio/ui-lvgl/UITask.h index dfe11aed..ae109ae0 100644 --- a/examples/companion_radio/ui-lvgl/UITask.h +++ b/examples/companion_radio/ui-lvgl/UITask.h @@ -18,6 +18,10 @@ #include "../ui-core/UiCoreHost.h" #include "../ui-core/ScreenLock.h" +// loop() waits on its own until something is due (lvport::idle, woken by a +// frame from the app): main.cpp adds no wait of its own. +#define UI_LOOP_IDLES 1 + class UiCore; class NearbyModel; struct UiEvent; diff --git a/platformio.ini b/platformio.ini index 57d37a7d..763c3bbe 100644 --- a/platformio.ini +++ b/platformio.ini @@ -93,6 +93,14 @@ build_flags = ${arduino_base.build_flags} -D LFS_NO_ASSERT=1 -D EXTRAFS=1 -D USE_CC310_HW_CRYPTO=1 + ; USB is the serial port only: TinyUSB's other device classes (on by default + ; in the core's tusb_config_nrf.h) cost ~2.4 KB of RAM for nothing. + -D CFG_TUD_MSC=0 + -D CFG_TUD_HID=0 + -D CFG_TUD_MIDI=0 + -D CFG_TUD_VENDOR=0 + -D CFG_TUD_VIDEO=0 + -D CFG_TUD_VIDEO_STREAMING=0 lib_deps = ${arduino_base.lib_deps} https://github.com/oltaco/CustomLFS#0.2.3 diff --git a/src/helpers/ui/SSD1306Display.cpp b/src/helpers/ui/SSD1306Display.cpp index 0bd9fe2a..8a6d4a20 100644 --- a/src/helpers/ui/SSD1306Display.cpp +++ b/src/helpers/ui/SSD1306Display.cpp @@ -17,6 +17,7 @@ bool SSD1306Display::begin() { #ifdef DISPLAY_ROTATION display.setRotation(DISPLAY_ROTATION); #endif + _force_redraw = true; return display.begin(SSD1306_SWITCHCAPVCC, DISPLAY_ADDRESS, true, false) && i2c_probe(Wire, DISPLAY_ADDRESS); } @@ -27,6 +28,7 @@ void SSD1306Display::turnOn() { if (_peripher_power) begin(); // re-init display after power was cut } display.ssd1306_command(SSD1306_DISPLAYON); + _force_redraw = true; // panel was off -- guarantee the next endFrame() flushes } void SSD1306Display::turnOff() { @@ -45,6 +47,7 @@ void SSD1306Display::turnOff() { void SSD1306Display::clear() { display.clearDisplay(); display.display(); + _force_redraw = true; // next endFrame() must flush even if its hash matches a pre-clear frame } void SSD1306Display::startFrame(Color bkg) { @@ -107,6 +110,49 @@ uint16_t SSD1306Display::getTextWidth(const char* str) { #endif } +void SSD1306Paged::sendBlock(uint8_t page, uint8_t col, uint8_t len) { + const uint8_t cmd[] = { SSD1306_PAGEADDR, page, page, SSD1306_COLUMNADDR, col, (uint8_t)(col + len - 1) }; + const uint8_t* ptr = buffer + (uint16_t)page * WIDTH + col; + ssd1306_commandList(cmd, sizeof(cmd)); + while (len) { // 32 data bytes a transfer fit every core's Wire buffer + uint8_t n = len < 32 ? len : 32; + wire->beginTransmission(i2caddr); + wire->write((uint8_t)0x40); + wire->write(ptr, n); + wire->endTransmission(); + ptr += n; len -= n; + } +} + void SSD1306Display::endFrame() { - display.display(); + const uint8_t* buf = display.getBuffer(); + const uint8_t per_page = 128 / BLOCK_W; + bool any = false; + bool dirty[BLOCKS]; + for (uint8_t i = 0; i < BLOCKS; i++) { + const uint8_t* q = buf + (uint16_t)i * BLOCK_W; // blocks run along each page in buffer order + uint32_t h = 2166136261u; + for (uint8_t j = 0; j < BLOCK_W; j++) { h ^= q[j]; h *= 16777619u; } + dirty[i] = _force_redraw || h != _block_hash[i]; + _block_hash[i] = h; + any |= dirty[i]; + } + if (!any) return; + if (_force_redraw || !display.canSendBlocks()) { + _force_redraw = false; + display.display(); // whole frame; also resets the panel's address window + return; + } + display.blocksBegin(); + for (uint8_t page = 0; page < 64 / 8; page++) { + // runs of neighbouring dirty blocks go out as one transfer + for (uint8_t b = 0; b < per_page; ) { + if (!dirty[page * per_page + b]) { b++; continue; } + uint8_t e = b; + while (e + 1 < per_page && dirty[page * per_page + e + 1]) e++; + display.sendBlock(page, b * BLOCK_W, (e - b + 1) * BLOCK_W); + b = e + 1; + } + } + display.blocksEnd(); } diff --git a/src/helpers/ui/SSD1306Display.h b/src/helpers/ui/SSD1306Display.h index 324c76c0..e023cef7 100644 --- a/src/helpers/ui/SSD1306Display.h +++ b/src/helpers/ui/SSD1306Display.h @@ -15,12 +15,32 @@ #define DISPLAY_ADDRESS 0x3C #endif +// Adafruit's display() always sends the whole frame (1 KB, ~25 ms at 400 kHz), +// and startFrame() clears the whole buffer, so every frame went out in full. +// As on the SH1106 (SH1106Display.h): a sender for single 32-column blocks. +class SSD1306Paged : public Adafruit_SSD1306 { +public: + using Adafruit_SSD1306::Adafruit_SSD1306; + bool canSendBlocks() const { return wire != nullptr; } + // A frame's blocks go out between these: the bus clock is set once. + void blocksBegin() { wire->setClock(wireClk); } + void blocksEnd() { wire->setClock(restoreClk); } + void sendBlock(uint8_t page, uint8_t col, uint8_t len); +}; + class SSD1306Display : public DisplayDriver { - Adafruit_SSD1306 display; + SSD1306Paged display; bool _isOn; uint8_t _color; int _text_sz = 1; RefCountedDigitalPin* _peripher_power; + // Frame-skip: endFrame() hashes each 32-column block of each 8-px page and + // sends only the blocks whose hash changed; _force_redraw sends the frame + // after begin()/turnOn()/clear() whole. + static const uint8_t BLOCK_W = 32; + static const uint8_t BLOCKS = (128 / BLOCK_W) * (64 / 8); + uint32_t _block_hash[BLOCKS] = {}; + bool _force_redraw = true; bool i2c_probe(TwoWire& wire, uint8_t addr); #ifdef OLED_MISC_FIXED_FONT