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:
-
Use a valid full ByteBoi firmware image, not a partial/bad
.bin. -
Trigger/use
JIGTESTto confirm PSRAM, battery, hardware path, etc. -
Identify the I2C expander at
0x74on SDA23, SCL22. -
Use the safe expander initialization pattern:
Wire.begin(23, 22); Wire.setClock(100000); expander.begin(0x74, Wire); -
Fix
Pins1I2C typo:{ Pin::I2C_Sda, 23 }, { Pin::I2C_Scl, 22 }, -
Force the correct v1.0 hardware path:
ByteBoi.initVer(0); -
Confirm display operation with a simple red-screen test.
-
Patch
GameImageto allocate icon buffers safely, preferably in PSRAM. -
Build/upload using the correct launcher partition scheme:
cm:esp32:byteboi:PartitionScheme=launcher -
Verify the live partition table contains both
launcherandgameapp 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.