All Posts

62 posts so far.

Searching the instrument cluster for a drawing route

An exact-image search for custom pixels in the J285 cluster closed one promising callback and found a conditional drawing-buffer route with a harder timing problem.

Getting out of the reversing camera's way

ClutchOS covered the reversing camera the first time I selected reverse with Home open. The fix is a small window manager that follows the factory camera request.

A Home screen for ClutchOS

Holding HOME on the pad now opens a Home screen instead of dropping into ES-DE. Built and tested on the Mac; the car has not seen it yet.

Choosing emulator cores by running every game on the card

Five of the head unit's emulator cores came from a microcontroller project. I replaced four of them, chosen by running every game on the card and timing the winners on the car's own Cortex-A9.

Battery saves that survive the emulator

A shared save module restores exact-size battery RAM, writes it only when it changes, and keeps the previous image as a recovery copy.

A one-millisecond sleep that rounded every frame late

Replacing a millisecond sleep with a microsecond deadline removed frame overruns in three hosts. Master System's fast Z80 had never reached the QNX build.

The 92-second return to the game library

The library took 92 seconds to come back after every game. The first plausible fix did nothing; two metadata walks and an eager collection were the cause.

Game audio on a MIB2

Finding the route that puts SNES audio through the dashboard speakers, and the 92-second library load it exposed along the way.

Why the game library ran at 15 fps on the dashboard

I blamed the display, then the textures, and both times I was wrong. Isolating one variable at a time found eight bits of shader arithmetic.

Porting ES-DE to ARM QNX

Cross-compiling the ES-DE frontend for the MIB2 head unit, and what the first native runs got wrong.

Eleven emulators on the head unit

Rusty Nail became a QNX launcher for eleven emulator paths on the MIB2. N64 failed on performance and then on a graphics plugin abort.

A safe route into the factory menu, and reading a gamepad over USB

A one-word menu retarget froze the MIB2 twice before revealing the rule for a safe route. Nineteen USB runs later SMB3 accepted controller reports.

NES on the dashboard, and mirroring the screen to the P4

A native NES core reaches nominal cadence on the MIB2, and the factory screen reaches the P4 panel as live video at 54.59 fps.

Getting inside a MIB2

Three days from a locked Volkswagen head unit to a root Telnet shell and the first native frame on its display.

A phone app for the dashboard panel

A phone app that talks to the dashboard panel rather than the car, and the fault codes a battery disconnect wrote into eight modules.

Folding mirrors on lock

Reading the driver door module's memory, backing it up twice, and changing three bytes so the mirrors fold when I lock the car. Every refusal and dead end is in the record.

The car ignores UDS and answers TP2.0

The first bytes the adapter ever sent me, and the discovery that the car ignores UDS and answers an older Volkswagen transport instead.

Pairing an ESP32 with an OBD-II adapter over BLE

Two days getting an ESP32 to talk to an OBD-II adapter over Bluetooth Low Energy. Everything negotiated correctly and not one byte arrived.

Reading a Game Boy cartridge through a GBFlash

The console's USB host port meets a cartridge reader that pretends to be a CH340 serial chip. By the end of the day the firmware read and validated a real cartridge header.

Decrypting an EMOTIV EPOC headset without the vendor software

Getting a 2013 EEG headset to emit packets and then decrypting them, once I stopped trusting the documentation and scored candidate keys against real data.

Speculative execution against the SNES sound chip

The SNES core was losing four million cycles a frame waiting on a sound chip on another board. Predicting the answer lifted Super Mario World from 35 to about 50 fps.

A flight recorder for firmware

Building a recorder that captures a good run and a bad run of the same firmware workload so a tool can find the first difference that matters.

Proving where the SNES bottleneck was

Days optimising the HDMI coprocessor because every symptom said it was holding the SNES back. The measurements pointed at the wire instead.

The RP2040 as a SNES sound chip, at 97 per cent of realtime

Two bodge wires taught the HDMI coprocessor to talk back and run the SNES sound chip with real Super Mario World music. It reached 97 per cent of realtime.

Profiling the SNES core on the H7

The from-scratch SNES core matches the reference emulator to the cycle. Five optimisations measured on hardware, and a profiler that says the problem is memory.

The Amiga gets a mouse and boots Turrican II

Mouse input for the emulated Amiga, then a commercial game whose custom trackloader refused to work until the floppy drive learned to spin.

The emulated Amiga boots Workbench 1.3

The Amiga core on the STM32H753 boots a real Workbench 1.3 floppy to the desktop, and what it took to get the floppy controller right.

A desktop flasher for the cartridge

A host tool that turns one ROM into the matching H7 runtime, RP2040 receiver and cartridge image, and refuses to declare success until the cart reads back the exact bytes.

Kickstart 1.3 runs under budget on the STM32H753

First hardware numbers for the Amiga core: a real Kickstart 1.3 ROM executing deterministically at a p99 of 2.51M cycles per PAL frame, against a gate of 3.35M.

Mega Drive emulation over an 8 MHz jumper-wire link

A Mega Drive emulator on a 480 MHz Cortex-M7 with perfect music beside a screen that tore in half. The fix was to make a slow frame arrive atomically.

Why Celeste sounded wrong

The cart was byte-identical to the BBS copy, so the fault was in the synth: a music clock cutting bars to a quarter, and a noise fit matched to the wrong metric.

Planning USB floppy support

Four transport dialects from 1998 and a FAT12 layer nobody ships, which is why the first firmware milestone is just reading descriptors.

The console's second shell

Two days building the console's second shell, including a crash hunt that ended eight kilobytes from the stack floor.

Cattle Crisis from 9 fps to 58

One day of hardware A/B tests on the H753. The biggest single win was a compiler flag I should have set weeks ago.

Game Boy first light

Two days from a feasibility study to DMG code on the TV, with the console's SPI video wire carrying two frame geometries at once.

A crash reporter for a bare-metal console

Giving the console a crash reporter that survives the reset and emails me, and the ways an STM32H7 loses data across a warm reset.

A 9 by 9 demoscene square in the launcher header

The launcher's blinking sparkle became a 9 by 9 pixel demoscene with seventeen tiny effects dealt from a shuffled deck.

Redesigning the cartridge shell around the label

The 3D-printed cartridge shell gets a flush sticker pocket and a chamfered corner, test-fitted virtually before printing anything.

What all the wires are for

A tour of Rusty Nail as it stands, from the OS on screen to the four breakout boards and the cartridge shell with a 16 MiB flash chip inside.

A 3x palette scanout for the HDMI picture

The HDMI picture was pixel-perfect and still looked soft. A full-resolution palette scanout fixed that, after an afternoon lost to a heap that only sometimes existed.

Why the speakers started clicking

The morning after deleting the old Lua core, everything played and then the speakers started clicking. A command trace found three lines of channel semantics and a missing crossfade.

The fixed-point Lua core becomes the default

Stage 9 of the runtime migration, ending the night at 59 fps on jumper wires after a one-byte regression that cost 19 carts.

An evening of unrelated fixes

Three unrelated fixes from one evening. The biggest was a cartridge that finally keeps its saves; the strangest was an iterator that duplicated the player.

z8lua lands behind a flag

The new Lua core measured against the old one on identical silicon, with a licensed PICO-8 as the oracle and a per-cart compatibility matrix.

A runtime baseline before the Lua swap

A permanent benchmark and input tapes recorded from real play, so the new Lua VM has numbers to beat before it lands.

Why I didn't replace the PICO-8 runtime

Whether the homegrown PICO-8 runtime should be replaced with an existing emulator. After twenty-one test cartridges the answer was to keep everything and swap the Lua core.

Threading the HDMI funnel by hand

The eight wires into the HDMI connector were the one part of the board I had decided to hand off. A script almost did it, then KiCad's push-and-shove router did.

Getting console frames onto the Feather RP2040 DVI

Two days between the H7 and a real HDMI picture, including a missing ground return and a clock divider that misreported its rate.

First light on the Feather RP2040 DVI

The DVI board had the console's palette on a monitor within 45 minutes of arriving, despite a wrong-variant pinout diagram and a slicer typing G-code into my REPL.

Downloading PICO-8 carts over WiFi

Rusty Nail can download carts from a pop-up launcher. The WiFi reason-15 failure was my own off-by-one, and a GPIO pin really did die on the bench.

Reading the framebuffer out of RAM

The standalone build sends nothing back to a host, so before I could redesign the boot splash I had to work out how to screenshot a screen my Mac cannot see.

Planning a flash cartridge for the console

Picking a W25Q flash chip for a physical PICO-8 cartridge and mapping how the firmware would read it. Nothing is built yet.

The HDMI board I bought before reading the datasheet

I bought a 50-pin RGB888-to-HDMI board, then counted the pins. The Nucleo never had 24 to give, so the fix is probably a second microcontroller whose whole job is HDMI.

A divide per pixel

Profiling the PICO-8 runtime found a stretched-sprite blit doing an integer divide on every pixel, on a chip with no fast divide.

Moving the SPI panel to DMA

The SPI blit was a blocking row-by-row write that stalled the console for about 13 ms every frame. DMA fixed it and the data cache nearly broke it.

Swapping the ESP32 for a Bluetooth gamepad

I picked an ESP32-C6 for the controller bridge and learned that BLE-only is a property of the silicon, so the fix was a different chip and one build flag.

The SPI panel and me

I had a working HDMI path at 12:48am. The next morning I picked up the tiny SPI screen instead and spent nine hours reinventing flicker.

The SPI panel, from a blank screen to 25 MHz

Getting the desktop off the USB stream and onto a real LCD, then climbing from 1 MHz to 25 MHz without losing the audio.

Getting sound out of it

Wiring an I2S DAC to the console over SAI and DMA, and why the audio stayed silent for most of a day.

Running real PICO-8 Lua on a Cortex-M7

Vendoring Lua 5.4 into bare-metal firmware, and why a hardware watchdog beat an instruction-count hook for stopping runaway carts.

How Rusty Nail is put together

Rusty Nail is a windowed pixel desktop running bare metal on a Nucleo-H753ZI. This is the architecture, and why the display is an abstraction from the start.

Starting this blog

A place to write up the things I build, mostly low-level. Short note on why it exists.