Account for ST7789 framebuffer and scanline heap in firmware gates
This commit is contained in:
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 |
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in new issue
Block a user