SOLVED: Black screen after flash / micropython install

My oldest had many of the circuitmess kits and built most of them, but moved into other interests. His ByteBoi remained unopened in his closet for a long while. This week, he decided to pass it along to his younger brother and he and I set about building it this weekend. That whole process went great, no issues. His excitement was matched only by his desire to make a game to put on it. That became the next project. We went through laying out a basic game using code blocks, but had to install MicroPython. That’s when the story went bad… after the install, the whole device appeared to be bricked. Black screen, no leds, no sound.

It was still responding to USB connections, so I attempted to restore firmware. That process completed, but the device was exactly the same… black and dead. So I had to start digging in to try and solve the issues.

tl;dr… there are a number of bugs / incompatible code blocks in each of the latest iterations of the ByteBoi firmware for the v1.0 hardware.

Below is the AI Assisted summary of what was found and fixed to get the ByteBoi up and going again:

Starting symptoms

The ByteBoi would enumerate over USB as an ESP32, but the display was black. Early on, the serial console showed boot errors such as:

flash read err, 1000
ets_main.c 371

Later, after flashing a more complete firmware image, the board would boot far enough to print Hello, then crash with a Guru Meditation error.

The device was eventually identified as:

ESP32-D0WDQ6 revision v1.0
4MB flash
PSRAM working
ByteBoi:v1.0

Important early discovery: bad firmware image

One of the first firmware files I flashed was only about 1 MB and did not contain the expected ESP32 boot image magic at the correct offset. A valid ESP32 image should have the 0xe9 magic byte at the app/boot offsets. The bad image caused bootloader-level errors.

The correct recovery path was to use a full 4 MB ByteBoi firmware image and flash it with esptool, for example:

export BB_PORT=/dev/ttyUSB0

esptool --chip esp32 --port "$BB_PORT" erase-flash

esptool \
  --chip esp32 \
  --port "$BB_PORT" \
  --baud 115200 \
  --before default-reset \
  --after hard-reset \
  write-flash \
  --flash-mode dio \
  --flash-freq 40m \
  --flash-size 4MB \
  0x0 ByteBoi-v2.0-with-SD-Test.bin

That got the board past the bootloader issue, but the display was still black and the app crashed.

JIGTEST / factory-test recovery

The stock firmware has a hidden JIGTEST serial path. By spamming JIGTEST over serial during boot, I could trigger the factory test mode.

Python helper:

export BB_PORT=/dev/ttyUSB0

python - <<'PY'
import serial
import time
import os
import sys

port = os.environ.get("BB_PORT", "/dev/ttyUSB0")

print(f"Opening {port}. Power-cycle/reset the ByteBoi now.")
try:
    ser = serial.Serial(port, 115200, timeout=0.01)
except Exception as e:
    print("Could not open serial port:", e)
    sys.exit(1)

end = time.time() + 12
while time.time() < end:
    try:
        ser.write(b"JIGTEST")
        ser.flush()
        data = ser.read(4096)
        if data:
            print(data.decode(errors="replace"), end="")
        time.sleep(0.02)
    except Exception as e:
        print("\nSerial error:", e)
        break

print("\nDone.")
PY

The factory test showed:

PSRAM init: Yes, free: 4194252 B
TEST:startTest:PSRAM
TEST:endTest:pass
TEST:startTest:Battery
TEST:endTest:pass
TEST:startTest:SD
SD:inserted:0
TEST:endTest:fail

So PSRAM and battery were good, but SD detection failed. I temporarily patched the factory test to skip SD/SPIFFS so it could continue and write/confirm the hardware revision.

Display/backlight diagnosis

At this point, the device would make startup sounds but the display remained black. Direct GPIO tests on GPIO18 and GPIO12 did not turn on the backlight.

An I2C scan found an expander:

Scanning I2C bus SDA=23 SCL=22
Found I2C device at 0x74

That was a major clue. On this ByteBoi v1.0 hardware, the backlight is controlled through the I2C expander, not direct ESP32 GPIO.

A direct expander test eventually proved:

expander at 0x74
SDA = 23
SCL = 22
backlight = expander pin 12
active LOW

The command that turned the backlight on was effectively:

expander.pinMode(12, OUTPUT);
expander.pinWrite(12, LOW);

CircuitOS I2cExpander bug / unsafe overload

One big issue was this pattern:

expander.begin(0x74, 23, 22);

That crashed. The overload in the library dereferenced the internal i2c pointer before assigning it.

The safe pattern was:

Wire.begin(23, 22);
Wire.setClock(100000);
expander.begin(0x74, Wire);

I patched the ByteBoi library to use that safe overload.

PinDef.cpp bug

In the installed ByteBoi library, Pins1 had what looked like a typo:

{ Pin::I2C_Sda, 23 },
{ Pin::I2C_Sda, 22 },

The second entry should be SCL:

{ Pin::I2C_Sda, 23 },
{ Pin::I2C_Scl, 22 },

This mattered because v1.0 hardware uses the I2C expander path.

Hardware version forcing

Another confusing discovery: ByteBoi.initVer(1) does not mean v1 hardware in this code path. It forced the newer v2 path:

if(hw == 1){
    // HW v2
    ver = v2_0;
    Pins.set(Pins3);
}

For my board, I had to force:

ByteBoi.initVer(0);

That allowed the firmware to probe the expander at 0x74 and select:

ver = v1_0;
Pins.set(Pins1);

Display recovery confirmation

Once the v1.0 path, I2C pins, and expander init were fixed, the screen was still not immediately usable. I added a simple red-screen test after ByteBoi.begin():

auto display = ByteBoi.getDisplay();
auto sprite = display->getBaseSprite();

sprite->clear(TFT_RED);
display->commit();

while(true){
    delay(1000);
}

That produced a stable red screen. This proved:

Backlight works
LCD panel works
Display init works
Framebuffer/sprite works
display->commit() works

At that point the display problem was solved. The remaining problems were launcher/runtime problems.

Launcher crash: GameImage copy bug / memory issue

After the display worked, the launcher crashed during construction, specifically inside Launcher::load() while adding launcher items.

The crash had:

StoreProhibited
EXCVADDR: 0x00000000
A4: 0x00002000

0x2000 is 8192 bytes, which is exactly:

64 * 64 * 2

That matched the size of a 64x64 RGB565 icon.

The issue was in GameImage. The original code allocated 8192 bytes with malloc() and then copied icon buffers without checking whether allocation succeeded. On this device, some allocations failed in internal heap.

I patched GameImage.cpp to allocate image buffers from PSRAM first:

#include "esp_heap_caps.h"

static constexpr int IMG_W = 64;
static constexpr int IMG_H = 64;
static constexpr int IMG_BYTES = IMG_W * IMG_H * 2;

static Color* allocImageBuffer(){
        void* p = heap_caps_malloc(IMG_BYTES, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);

        if(!p){
                Serial.println("DEBUG: PSRAM image malloc failed; falling back to internal malloc");
                p = malloc(IMG_BYTES);
        }

        if(!p){
                Serial.println("DEBUG: GameImage malloc failed");
                return nullptr;
        }

        memset(p, 0, IMG_BYTES);
        return static_cast<Color*>(p);
}

Then I updated constructors, assignment, and destructor to check null pointers and use heap_caps_free().

This fixed the launcher crash and missing icon crashes.

Game launch crash: wrong partition table

After the launcher was working, launching a game such as Bricks still caused a crash loop. Decoding the backtrace showed:

GameLoader::loadFunc(Task*)
UpdateClass::write(...)
spi_flash_erase_sector
is_safe_write_address
abort()

The ByteBoi launcher loads games by writing the selected game binary into an OTA/app partition using the ESP32 Update library.

The live partition table on my device was wrong:

nvs,data,nvs,0x9000,20K
otadata,data,ota,0xe000,8K
eeprom,data,153,0x10000,4K
spiffs,data,spiffs,0x11000,1020K
app0,app,ota_0,0x110000,3008K

There was only one app partition, so the loader had nowhere safe to write the selected game. Update.write() aborted when it attempted to erase/write flash.

The correct ByteBoi launcher/game layout was:

nvs,data,nvs,0x9000,20K
otadata,data,ota,0xe000,8K
eeprom,data,153,0x10000,4K
spiffs,data,spiffs,0x11000,1020K
launcher,app,ota_0,0x110000,960K
game,app,ota_1,0x200000,2M

The fix was to build/upload using the ByteBoi launcher partition scheme:

cd ~/repos/ByteBoi-Firmware

export FQBN="cm:esp32:byteboi:PartitionScheme=launcher"
export BUILD="/tmp/byteboi-launcher-build"

rm -rf "$BUILD"

arduino-cli compile \
  --clean \
  --fqbn "$FQBN" \
  --build-path "$BUILD" \
  .

export BB_PORT=/dev/ttyUSB0

arduino-cli upload \
  -p "$BB_PORT" \
  --fqbn "$FQBN" \
  --input-dir "$BUILD" \
  .

Then I verified the live partition table:

esptool --chip esp32 --port "$BB_PORT" read-flash 0x8000 0xc00 /tmp/byteboi-partitions-after.bin

GENPART=$(find ~/.arduino15/packages/cm/hardware/esp32/1.8.3 -name gen_esp32part.py | head -n 1)

python "$GENPART" /tmp/byteboi-partitions-after.bin

Once the partition table showed launcher and game, game loading worked again.

Optional: restore stock SPIFFS launcher assets

If launcher icons/stock graphics are missing, the stock SPIFFS image can be restored:

export BB_PORT=/dev/ttyUSB0

esptool --chip esp32 --port "$BB_PORT" write-flash \
  --flash-mode dio \
  --flash-freq 40m \
  --flash-size 4MB \
  0x11000 ~/.arduino15/packages/cm/hardware/esp32/1.8.3/firmwares/byteboi/spiffs.bin

Final working fixes

The final recovery required several fixes:

  1. Use a valid full ByteBoi firmware image, not a partial/bad .bin.

  2. Trigger/use JIGTEST to confirm PSRAM, battery, hardware path, etc.

  3. Identify the I2C expander at 0x74 on SDA 23, SCL 22.

  4. Use the safe expander initialization pattern:

    Wire.begin(23, 22);
    Wire.setClock(100000);
    expander.begin(0x74, Wire);
    
  5. Fix Pins1 I2C typo:

    { Pin::I2C_Sda, 23 },
    { Pin::I2C_Scl, 22 },
    
  6. Force the correct v1.0 hardware path:

    ByteBoi.initVer(0);
    
  7. Confirm display operation with a simple red-screen test.

  8. Patch GameImage to allocate icon buffers safely, preferably in PSRAM.

  9. Build/upload using the correct launcher partition scheme:

    cm:esp32:byteboi:PartitionScheme=launcher
    
  10. Verify the live partition table contains both launcher and game app partitions.

Things I learned

The symptoms looked like a dead display at first, but the panel was fine. The actual issues were layered:

Bad/partial firmware image
Missing/incorrect hardware revision state
Wrong hardware init path
I2C expander init bug
Pin map typo
Launcher icon allocation issue
Wrong partition table for game loading

The biggest lesson: if the ByteBoi launcher is expected to load games dynamically, the partition table matters. A single large app partition may boot the launcher, but game launching requires the dedicated launcher + game layout.

Hopefully this helps someone else avoid the same long debugging journey.