LPC2103F Flash Dump
After the serial probing session revealed that the LPC2103F is completely silent on UART0 in both OPERATE and UPDATE modes, and the JTAG investigation stalled on the adapter wiring problem, I needed a third path to the 32KB flash. The ISP bootloader turned out to be it — but not through the UPDATE switch.
The JP24 discovery
Section titled “The JP24 discovery”While examining the controller board for JTAG header buzzing points, Cameron spotted a 2-position jumper header that had gone unnoticed during earlier teardowns. It is labeled JP24 on the PCB, with a silkscreen annotation “0P” nearby. The jumper was open — no shorting block installed.
On NXP’s LPC2100 series, the In-System Programming bootloader activates when pin P0.14 is held LOW during a reset event. The “0P” silkscreen is almost certainly shorthand for port 0, pin 14. Installing a jumper on JP24 would pull P0.14 to ground, and the next power-on would enter ISP mode instead of running the application firmware.
ISP bootloader confirmation
Section titled “ISP bootloader confirmation”With JP24 jumpered and no AC power, I connected the mini USB cable. The CP2104 enumerated on /dev/ttyUSB2 as expected — it is bus-powered from USB and does not need the amp’s power rails. Then I ran the ISP detection command:
lpc21isp -detectonly -control /dev/ttyUSB2 115200 12000AC mains were switched on (the front power button was not pressed — the MCU runs on the standby rail, which is active whenever AC is present). Within one second:
lpc21isp version 1.97Synchronizing (ESC to abort). OKRead bootcode version: 21Read part ID: LPC2103, 32 kiB FLASH / 8 kiB SRAM (0x0004FF11)The ISP bootloader was alive and responding. Bootcode version 21.2, part ID 0x0004FF11 — an LPC2103 with 32 KB flash and 8 KB SRAM. The -control flag tells lpc21isp to use DTR for nRESET and RTS for P0.14 entry, but with JP24 hardwired, the critical signal is already there.
Patching lpc21isp for flash reading
Section titled “Patching lpc21isp for flash reading”The standard lpc21isp tool can write and verify flash, but it has no built-in command to read flash contents back to a file. The NXP ISP protocol supports reading — the R <address> <count> command returns flash data as UU-encoded lines — but lpc21isp never implemented that path. Other tools like nxpprog and lpc21isp’s Python equivalents had bugs or DTR-related issues that made them unusable for this hardware.
I patched the lpc21isp C source (from the capiman/lpc21isp GitHub repository) to add a -readfile flag. The patch touches four files:
lpc21isp.h — Added ReadFlash and ReadFileName fields to the ISP_ENVIRONMENT struct. Also disabled the SYSFS_GPIO_SUPPORT define, which the GitHub source enables for Linux but which conflicts with systems that use serial DTR/RTS for reset control (the Arch Linux package has this disabled).
lpc21isp.c — Added -readfile <filename> CLI option that sets ReadFlash=1, ProgramChip=0, and stores the output path. Extended PerformActions() to call the read function after connecting.
lpcprog.c — Added the NxpReadFlash() function (~170 lines). It calculates flash size from the LPC2103 sector table (8 sectors of 4KB = 32KB), reads in 256-byte chunks using the ISP R command, UU-decodes each response line, acknowledges checksums every 20 lines with OK\r\n, and writes the raw binary to disk. Also modified NxpDownload() to skip programming when ReadFlash is set.
lpcprog.h — Added the function declaration and error code define.
The patched source compiles to tools/lpc21isp-read:
cd /tmp/lpc21isp-src && make clean && makecp lpc21isp /path/to/project/tools/lpc21isp-readThe power cycle ritual
Section titled “The power cycle ritual”This was the hardest-won piece of knowledge. The ISP bootloader session is exquisitely sensitive to the state of the DTR line (which lpc21isp -control uses for nRESET). After a successful session, the DTR state persists across serial port close/reopen, and the MCU does not cleanly re-enter the bootloader without a full power cycle. Quick power cycles do not work either — the 1000uF Rubycon capacitors on the ±15V rails (C20, C25, C26, freshly replaced) hold charge for 15–30 seconds.
-
Remove all power: Turn off the rear AC switch. Unplug the USB cable. Wait at least 30 seconds for the 1000uF caps to discharge fully.
-
Reconnect USB first: Plug in the mini USB cable. The CP2104 enumerates on the bus, and
/dev/ttyUSB2(or similar) appears. The MCU has no power yet — only the USB bridge is alive. -
Start the tool: Launch
lpc21isp-readbefore applying AC power. The tool begins sending sync characters and will wait for the bootloader to respond.Terminal window timeout 120 ./tools/lpc21isp-read \-readfile firmware/lpc2103f_flash_dump.bin \-control /dev/ttyUSB2 115200 12000 -
Apply AC power: Flip the rear AC switch on. The standby rail powers the MCU, which enters the ISP bootloader (JP24 is still installed). The bootloader responds to the sync characters, and the flash read begins.
-
Wait for completion: The 32KB read takes approximately 60–90 seconds at 115200 baud with UU encoding overhead. The tool prints progress as it goes.
The 120-second timeout is generous but necessary. An earlier attempt with a 45-second timeout killed the read at 75% (24,832 of 32,768 bytes).
The dump
Section titled “The dump”The complete read finished cleanly:
lpc21isp version 1.97Synchronizing (ESC to abort)......... OKRead bootcode version: 212Read part ID: LPC2103, 32 kiB FLASH / 8 kiB SRAM (0x0004FF11)Reading 32768 bytes of flash to firmware/lpc2103f_flash_dump.binRead: 32768 / 32768 bytes (100%)Flash dump complete: firmware/lpc2103f_flash_dump.bin (32768 bytes)| Property | Value |
|---|---|
| File | firmware/lpc2103f_flash_dump.bin |
| Size | 32,768 bytes (32 KB) |
| SHA-256 | c719740cafea1fc9563aacae80ef2806ec31b937020e9d3660ba2e0774696e7d |
| Format | Raw ARM7TDMI binary, little-endian |
| Entry point | 0x00000000 (reset vector) |
Initial verification
Section titled “Initial verification”A quick look at the first bytes confirms valid ARM7TDMI code. The binary begins with 1c 20 9f e5 00 30 a0 e3 93 00 02 e1 — an LDR R2, [PC, #0x1C] / MOV R3, #0 / MSR CPSR_cxsf, R3 sequence. This is a textbook ARM7 startup: load the stack pointer from a constant pool, zero a register, and write to the CPSR to set the processor mode. The vector table and exception handlers follow, setting up stack pointers for each processor mode (Supervisor, IRQ, FIQ, Abort, Undefined, System/User).
The tail end of the binary (offset 0x7F60 onward) is all zeros — unused flash space. The actual firmware occupies roughly 31 KB of the 32 KB available, leaving about 1 KB of headroom.
What failed along the way
Section titled “What failed along the way”Several approaches were tried and abandoned before the patched lpc21isp succeeded:
Python ISP reader (tools/lpc_isp_dump.py): Multiple iterations using pyserial to implement the ISP protocol from scratch. The fundamental problem was that pyserial’s port opening manipulates DTR, which is wired to nRESET on this board. Opening the port resets the MCU, and by the time the script sends its first sync byte, the MCU has already left the ISP bootloader window. Various workarounds (disabling DTR with dsrdtr=False, waiting for manual power cycle, USB re-enumeration after disconnect) all failed.
nxpprog: A Python package on PyPI that implements the full NXP ISP protocol. Crashed immediately with NameError: name 'a' is not defined in the --read code path — an unfinished implementation.
JTAG via DAP-Link: The NOYITO 2.0mm-to-2.54mm adapter scrambles the pin mapping. A proper direct-wired JTAG connection would work, but the ISP path was faster to get working.
Restoring normal operation
Section titled “Restoring normal operation”After dumping the flash, remove the JP24 jumper and power cycle the amp (with the full 30-second discharge wait). The MCU boots its application firmware normally — VFD display lights up, menu system works, audio output resumes. JP24 is only needed for ISP access.
Firmware analysis
Section titled “Firmware analysis”The 32 KB binary has been completely reverse-engineered. All 121 functions (120 auto-detected plus 1 manually discovered) were decompiled and categorized using mcghidra, an MCP server wrapping headless Ghidra for use by Claude Code.
The headline finding: there is no UART command parser. UART0 is TX-only — the MCU sends a boot message at 9600 baud and never reads the serial port again. The dealer update tool talks to the ROM ISP bootloader, not to the application firmware. Several other assumptions from this page and the architecture section have been corrected.
The full analysis is documented in the Firmware Analysis section:
- Analysis Overview — methodology, clock tree, memory layout, headline discoveries
- Boot Sequence — reset vector, PLL, MAM, stacks, main() init, 100 Hz main loop, GPIO pin assignments
- Audio DSP Pipeline — I2C protocol, all 6 speaker EQ presets decoded, sound modes, volume control, mute-configure-unmute pattern
- Display & Menu System — HD44780 VFD driver, priority compositor, 3-state menu machine, settings persistence, heartbeat LED