Account for ST7789 framebuffer and scanline heap in firmware gates

This commit is contained in:
mikecarper committed 2026-10-18 16:32:49 -07:00
1 parent 1ce3f0cdd7
commit b8f8a1fae1
3 files changed
+163 -1

No files matched your search

@@ -35,6 +35,11 @@ does not require a single guessed factory ID.
- A detected panel retains normal rendering, sleep, wake, and display settings.
- Detection is specific to the T114 automatic-display recipe. Other boards
using the shared ST7789 driver retain their existing behavior.
- The RAM gate budgets a responding screen even when a particular board boots
headless. The T114 display needs two 4,080-byte buffers and one temporary
480-byte RGB565 row: 8,640 bytes of peak heap allocation, in addition to the
normal allocator and transient margin. The same model follows the shared
driver's T190 and M9 panel sizes and its single-buffer configuration.
- Picker consolidation requires evidence from the exact published application.
Historical releases retain their original screen behavior and update IDs.
@@ -46,6 +51,7 @@ does not require a single guessed factory ID.
| Resolved recipe and compatibility-alias checks | PASS, 5 methods; actual PlatformIO configuration also confirms all 5 legacy aliases match their canonical targets |
| Companion headless buttons and related navigation, reader, pairing, startup and deadline suites | PASS, 55 methods, including native headless fault regression and 4 input/reader configurations |
| Picker and release-selection regressions | PASS, 78 Python methods and 3 Node suites; old assets, scoped previews, newest-role defaults and unavailable legacy choices covered |
| Display allocation accounting and firmware RAM gates | PASS, 46 methods, including production allocation tracking across 3 panel sizes and buffer configurations |
| T114 firmware and package checks | Pending |
| Hosted documentation links | PASS, 7 methods |
| Physical T114 with and without a screen | Not performed |
+22 -1
View File
@@ -30,7 +30,7 @@ DISPLAY_HEAP = {
"SSD1306Display": 4096, "SH1106Display": 4096, "U8g2Display": 4096,
"SH1107Display": 4096,
# These drivers render directly or embed their pixel storage in globals.
"ST7789Display": 0, "ST7789LCDDisplay": 0, "NV3001BDisplay": 0,
"ST7789LCDDisplay": 0, "NV3001BDisplay": 0,
"GxEPDDisplay": 0, "E213Display": 8192, "E290Display": 8192,
}
@@ -125,6 +125,25 @@ def wio_tracker_l2_display_heap(defines):
return row_bytes * height + 1024
def st7789_display_heap(defines):
# Match ST7789Display's constructor branches and ST7789Spi's byte-page
# geometry, including the partial last page on the T114 and T190 panels.
if "HELTEC_VISION_MASTER_T190" in defines:
width, height = 320, 170
elif "THINKNODE_M9" in defines:
width, height = 320, 240
else:
width, height = 240, 135
buffers = (1 if "OLEDDISPLAY_REDUCE_MEMORY" in defines
and "OLEDDISPLAY_DOUBLE_BUFFER" not in defines else 2)
# OLEDDisplay mallocs the persistent front/back buffers. ST7789Spi also
# mallocs one RGB565 scanline, freed before allocating the next line.
# The global driver is already linked; allocator overhead remains covered
# by the separate allocation/transient margin. Auto-detection must budget
# a present panel even when a particular board starts without one.
return width * ((height + 7) // 8) * buffers + width * 2
def requirements(platform, defines, target):
if platform not in SUPPORTED:
raise ValueError(f"no memory policy for {platform}")
@@ -143,6 +162,8 @@ def requirements(platform, defines, target):
if platform != "ESP32_PLATFORM":
raise ValueError("Wio Tracker L2 display budget requires ESP32")
display_heap = wio_tracker_l2_display_heap(defines)
elif display == "ST7789Display":
display_heap = st7789_display_heap(defines)
elif display == "ST7735Display":
depth = integer(defines, "ST7735_BUFFER_COLOR_DEPTH", 16)
if depth not in (8, 16):
+135
View File
@@ -18,6 +18,7 @@ import check_firmware_ram as ram
from firmware_elf import FirmwareElf
import firmware_memory_manifest as proof
from audit_esp32_image_ram import EspImage
from test_replay_reset_integration import extract_braced
def elf32(path, symbols, data=b"", address=0x3F400000):
@@ -68,6 +69,140 @@ def esp_fixture(path, modern=False, fragmented=False, internal_length=0x10000):
class FirmwareRamTest(unittest.TestCase):
def test_st7789_budget_matches_production_allocation_peak_and_reuses_buffers(self):
ui = ROOT / "src/helpers/ui"
spi = (ui / "ST7789Spi.h").read_text()
oled = (ui / "OLEDDisplay.cpp").read_text()
header = (ui / "ST7789Display.h").read_text()
start = header.index("#if defined(HELTEC_VISION_MASTER_T190)")
constructors = header[start:header.index("#endif", start) + len("#endif")]
buffering = (ui / "OLEDDisplay.h").read_text()
start = buffering.index("#ifndef OLEDDISPLAY_REDUCE_MEMORY")
buffering = buffering[start:buffering.index("#endif", start) + len("#endif")]
fixture = r'''
#include <algorithm>
#include <cassert>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <utility>
#include <vector>
using std::min; using std::max;
size_t live=0, peak=0, largest=0, calls=0;
std::vector<std::pair<void*,size_t>> allocations;
void* track_malloc(size_t size) {
void* p=std::malloc(size); assert(p); allocations.push_back({p,size});
live+=size; peak=max(peak,live); largest=max(largest,size); ++calls; return p;
}
void track_free(void* p) {
auto at=std::find_if(allocations.begin(),allocations.end(),[&](auto& a){return a.first==p;});
assert(at!=allocations.end()); live-=at->second; allocations.erase(at); std::free(p);
}
#define malloc track_malloc
#define free track_free
#define rtos_malloc track_malloc
#define rtos_free track_free
#define DEBUG_OLEDDISPLAY(...)
#define LOW 0
#define HIGH 1
void yield() {}
enum OLEDDISPLAY_GEOMETRY { GEOMETRY_128_128,GEOMETRY_128_64,GEOMETRY_128_32,
GEOMETRY_64_48,GEOMETRY_64_32,GEOMETRY_RAWMODE };
@BUFFERING@
struct Bus {
unsigned rows=0;
template<class T> void beginTransaction(T) {}
void endTransaction() {}
void transfer(void*,void*,int) { ++rows; }
};
struct Panel {
uint16_t displayWidth=0,displayHeight=0,displayBufferSize=0,_buffheight=0,_RGB=0xffff;
OLEDDISPLAY_GEOMETRY geometry=GEOMETRY_RAWMODE;
unsigned logBufferSize=0,logBufferFilled=0,logBufferLine=0,logBufferMaxLines=0;
uint8_t* buffer=nullptr; uint8_t* buffer_back=nullptr; uint8_t* logBuffer=nullptr;
unsigned BufferOffset=0,_spiSettings=0;
Bus bus; Bus* _spi=&bus;
Panel(int*,uint8_t,uint8_t,uint8_t,OLEDDISPLAY_GEOMETRY g,uint16_t w,uint16_t h,
int=-1,int=-1,int=-1) { setGeometry(g,w,h); }
bool connect() { _buffheight=(displayHeight+7)/8; return true; }
void set_CS(int) {}
void setAddrWindow(uint16_t,uint16_t,uint16_t,uint16_t) {}
@GEOMETRY@
@ALLOCATE@
@FLUSH@
};
int SPI=0,SPI1=0;
constexpr uint8_t PIN_TFT_RST=2,PIN_TFT_DC=12,PIN_TFT_CS=11,PIN_TFT_SDA=41,PIN_TFT_SCL=40;
constexpr uint8_t ST7789_RESET=14,ST7789_RS=15,ST7789_CS=16,ST7789_SDA=13,ST7789_MISO=14,ST7789_SCK=12;
struct DisplayDriver { DisplayDriver(int,int) {} };
struct ST7789Display:DisplayDriver {
Panel display; bool _isOn=false;
@CONSTRUCTORS@
};
int main() {
ST7789Display d; Panel& p=d.display;
assert(p.allocateBuffer());
std::fill(p.buffer,p.buffer+p.displayBufferSize,0xff);
#ifdef OLEDDISPLAY_DOUBLE_BUFFER
std::fill(p.buffer_back,p.buffer_back+p.displayBufferSize,0);
#endif
p.display(); assert(p.bus.rows==p.displayHeight);
const auto count=calls, persistent=live;
assert(p.allocateBuffer()); assert(calls==count && live==persistent);
std::printf("%zu %zu %zu\n",live,peak,largest);
track_free(p.buffer);
#ifdef OLEDDISPLAY_DOUBLE_BUFFER
track_free(p.buffer_back);
#endif
assert(live==0);
}
'''
for marker, value in {
"@BUFFERING@": buffering, "@CONSTRUCTORS@": constructors,
"@GEOMETRY@": extract_braced(spi, "void setGeometry("),
"@ALLOCATE@": extract_braced(oled, "bool OLEDDisplay::allocateBuffer(").replace(
"OLEDDisplay::", ""),
"@FLUSH@": extract_braced(spi, "void display(void)")}.items():
fixture = fixture.replace(marker, value)
with tempfile.TemporaryDirectory(prefix="st7789-ram-") as temporary:
for board in ({"HELTEC_T114": 1, "T114_DISPLAY_AUTODETECT": 1},
{"HELTEC_VISION_MASTER_T190": 1}, {"THINKNODE_M9": 1}):
for buffers in ({}, {"OLEDDISPLAY_REDUCE_MEMORY": 0},
{"OLEDDISPLAY_REDUCE_MEMORY": 1, "OLEDDISPLAY_DOUBLE_BUFFER": 0}):
defines = {**board, **buffers, "DISPLAY_CLASS": "ST7789Display"}
with self.subTest(defines=defines):
binary = Path(temporary) / "allocations"
compiled = subprocess.run([
"c++", "-std=c++17", "-Wall", "-Wextra", "-Werror", "-x", "c++", "-",
*[f"-D{k}={v}" for k,v in {**board, **buffers}.items()],
"-fsanitize=address,undefined", "-fno-sanitize-recover=all", "-fno-pie", "-no-pie",
"-o", str(binary)], input=fixture, text=True, capture_output=True)
self.assertEqual(compiled.returncode, 0, compiled.stderr)
checked = subprocess.run([str(binary)], text=True, capture_output=True, timeout=15)
self.assertEqual(checked.returncode, 0, checked.stderr)
persistent, peak, largest = map(int, checked.stdout.split())
policy = ram.requirements("NRF52_PLATFORM", defines, "display_companion")
self.assertEqual(policy["components"]["display_pixels_and_driver"], peak)
self.assertGreater(peak, persistent) # Scanline is heap, freed after each transfer.
self.assertGreaterEqual(policy["required_contiguous_bytes"], largest)
def test_t114_present_panel_is_budgeted_for_auto_and_legacy_with_exact_admission(self):
flags = {"DISPLAY_CLASS": "ST7789Display", "HELTEC_T114": 1,
"T114_DISPLAY_AUTODETECT": 1, "COMPANION_RADIO_FULL": 1, "BLE_PIN_CODE": 123456}
target = "Heltec_t114_companion_radio_full"
policy = ram.requirements("NRF52_PLATFORM", flags, target)
self.assertEqual(policy["components"]["display_pixels_and_driver"], 8640)
self.assertEqual(policy["components"]["allocation_and_transient_margin"], 4096)
legacy = {key:value for key,value in flags.items() if key != "T114_DISPLAY_AUTODETECT"}
self.assertEqual(ram.requirements("NRF52_PLATFORM", legacy, target), policy)
required = policy["required_heap_bytes"]
with tempfile.TemporaryDirectory() as temp:
path = Path(temp) / "firmware.elf"
for capacity, accepted in ((required-8640, False), (required-1, False), (required, True)):
with self.subTest(capacity=capacity), contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(io.StringIO()):
elf32(path, {"__HeapBase": (0x2003F800-capacity,0), "__HeapLimit": (0x2003F800,0)})
self.assertEqual(ram.check_firmware(path,"NRF52_PLATFORM","nrf52840",flags,target)==0, accepted)
def test_device_encoder_budget_matches_storage_and_role_gates(self):
matrix = [
("ESP32_PLATFORM", {"ENABLE_OTA": 1, "MESHCORE_OTA_DEVICE_DEFLATE": 1}, "v4_repeater", True),