Boot Sequence & System Architecture
The LPC2103F’s reset vector at address 0x00 does not point to a standard ARM7 vector table. Instead, it contains inline startup code that runs directly from the reset entry. This is a space optimization — rather than wasting 32 bytes on a vector table that mostly points to trap loops, the startup code occupies the vector space itself and uses the remaining exception slots for their actual handlers.
Reset and ISP check
Section titled “Reset and ISP check”The first instruction saves the current PINSEL0 and PINSEL1 register values (pin function assignments), then jumps to the boot ROM at 0x7FFFE178. The boot ROM checks whether P0.14 (JP24) is held LOW. If it is, the ROM enters the ISP bootloader and never returns — the application firmware does not run. If P0.14 is HIGH (JP24 open, normal operation), the boot ROM returns to the application code at 0x40, and startup continues.
The exception vectors at 0x40 through 0x58 are minimal. Undefined instruction, software interrupt (SWI), prefetch abort, and data abort all vector to infinite loops — hard traps with no recovery. The IRQ vector at 0x58 contains a return instruction that dispatches through the Vectored Interrupt Controller.
PLL configuration
Section titled “PLL configuration”The PLL setup at 0x5C through 0x98 configures the clock multiplier:
| Register | Value | Meaning |
|---|---|---|
| PLLCFG | 0x23 | MSEL=3 (M=4, multiply by 4), PSEL=1 (P=2) |
| PLLCON | 0x01 | Enable PLL |
| PLLFEED | 0xAA, 0x55 | Feed sequence to latch configuration |
The firmware polls PLLSTAT until the lock bit is set, then connects the PLL as the system clock source with a second PLLCON write (0x03) and another feed sequence. The resulting core clock is 14.7456 MHz × 4 = 58.98 MHz.
Memory Accelerator Module
Section titled “Memory Accelerator Module”At 58.98 MHz, the flash memory cannot keep up with zero-wait-state access. The MAM configuration at 0x9C:
| Register | Value | Meaning |
|---|---|---|
| MAMTIM | 3 | 3 flash access cycles (required above 40 MHz) |
| MAMCR | 2 | Fully enabled — prefetch and data buffering active |
With the MAM fully enabled, sequential instruction fetches are served from the prefetch buffer at full speed. Only branch targets and data accesses incur the 3-cycle penalty.
Stack pointer setup
Section titled “Stack pointer setup”The code at 0xB0 through 0xF8 initializes the stack pointer for all six ARM processor modes. The LPC2103F has 8 KB of SRAM starting at 0x40000000, and the stacks are placed at the top:
| Mode | Stack size | Purpose |
|---|---|---|
| Undefined | 4 bytes | Trap handler (infinite loop, minimal stack needed) |
| Abort | 4 bytes | Trap handler |
| IRQ | 4 bytes | Interrupt dispatch (VIC handles nesting) |
| FIQ | 4 bytes | Fast interrupt (unused in this firmware) |
| Supervisor (SVC) | 256 bytes | ISP bootloader context |
| System | Remaining SRAM | Application code, function calls, local variables |
The top of the stack space is 0x40001FE0, which is 32 bytes below the top of the 8 KB SRAM. Those final 32 bytes are reserved by the ISP bootloader per NXP’s documentation.
Initialized data copy
Section titled “Initialized data copy”The .data section copy at 0x104 through 0x11C transfers 1196 bytes of initialized global data from flash to SRAM:
| Parameter | Value |
|---|---|
| Source (flash) | 0x00007A88 |
| Destination (SRAM) | 0x40000000 |
| End address | 0x400004AC |
| Size | 1196 bytes (0x4AC) |
This region contains the DSP I2C command packets (7-byte and 18-byte sequences for all speaker EQ presets, sound modes, and filter configurations), the volume attenuation lookup table (3 bytes per level × 101 levels), and the VFD display strings. The mapping is straightforward: any initialized data at SRAM address X was copied from flash address (X - 0x40000000) + 0x7A88.
BSS zero-fill
Section titled “BSS zero-fill”Immediately following the .data copy, the .bss section at 0x120 through 0x134 zeroes 948 bytes:
| Parameter | Value |
|---|---|
| Start | 0x400004AC |
| End | 0x40000860 |
| Size | 948 bytes (0x3B4) |
This is standard C runtime initialization — every uninitialized global variable starts at zero. The .bss region holds I2C transmit/receive buffers, the settings array, state flags, debounce accumulators, and the 8-slot I2C command ring buffer.
main() initialization
Section titled “main() initialization”The startup code jumps to main() at 0x1810. Ghidra did not auto-detect this function — the address range was classified as data, likely because the function is reached through a literal pool load rather than a direct branch. The function was identified by tracing cross-references from known init routines and manually disassembling the 680 bytes from 0x1810 to 0x20B7.
The init sequence calls 10 functions in order:
| Call | Function | Purpose |
|---|---|---|
| 1 | FUN_00000f04 | Pin mux, UART0 (9600,8,N,1), GPIO direction |
| 2 | FUN_00000f90 | VIC interrupt controller — assigns slots for Timer2, Timer1, I2C0 |
| 3 | FUN_0000016c | Timer2 system tick — 100 Hz (10 ms period) |
| 4 | FUN_000020c8 | I2C0 init — 480 kHz fast mode, VIC slot 5 |
| 5 | FUN_00002914 | Timer1 / IR receiver — capture on P0.17 (CAP1.2) |
| 6 | FUN_00003804 | Load settings — reads 16 bytes from D2-81431 EEPROM register 0 |
| 7 | FUN_00002ffc | IR debounce — initializes repeat delay state |
| 8 | FUN_00001040 | Watchdog — WDTC = 0x384000, starts countdown |
| 9 | FUN_000076c4 | Settings validation — range-checks every byte, resets out-of-range values |
| 10 | FUN_00003d50 | VFD init — HD44780 sequence (0x03, 0x28, 0x0C, 0x06) |
After the function calls, the init code drives several GPIO pins directly: P0.23 HIGH (amplifier enable), P0.16 HIGH, P0.22 HIGH, P0.25 LOW. Then it calls FUN_00003e24 to display the boot message on the VFD:
SNAPAV EPISODEEA500 AMPLIFIERThis same string is the only text sent over UART0 — the boot message. After that, the serial port is never touched again.
Main loop
Section titled “Main loop”The main loop at 0x198C is a tight poll with a tick-gated slow path:
while (1) { // Fast path: runs every iteration, as fast as the CPU allows ir_decoder(); // poll IR receiver capture register encoder_read(); // poll rotary encoder GPIOs
if (tick_flag) { // set by Timer2 ISR every 10 ms tick_flag = 0; watchdog_feed(); // prevent WDT reset (0xAA, 0x55) debounce_inputs(); // 4 GPIO debounce channels ir_dispatch(); // process decoded IR commands power_mode(); // handle power/standby transitions menu_handler(); // encoder + button state machine vfd_update(); // refresh VFD if display state changed eeprom_save(); // auto-save if dirty (200-tick delay)
// Heartbeat LED on P0.12 // Active mode: XOR toggle every 40 ticks (400 ms period) // Standby mode: XOR toggle every 120 ticks (1.2 s period)
// Periodic timers: // 3000-tick (30 sec): periodic maintenance // 200-tick (2 sec): EEPROM save delay }}The architecture is a classic embedded super-loop with interrupt-driven peripherals. The IR decoder and encoder polling run at full CPU speed to avoid missing edges, while everything else is gated to the 100 Hz tick rate. The watchdog feed in the tick path means the system has a 10 ms window — if the main loop stalls for more than one tick period, the watchdog will not be fed and the MCU will reset.
Interrupt structure
Section titled “Interrupt structure”Three peripherals use vectored interrupts through the VIC:
| VIC Slot | IRQ | Source | ISR | Purpose |
|---|---|---|---|---|
| 1 | 26 | Timer2 | FUN_0000016c | System tick — sets tick_flag every 10 ms |
| 4 | 5 | Timer1 | FUN_00002914 | IR receiver — captures edge timing on P0.17 |
| 5 | 9 | I2C0 | FUN_000020c8 | I2C state machine — handles byte-level protocol |
Timer2 generates the 100 Hz system tick. The calculation: PCLK (14,745,600 Hz) / prescaler (8) / match value (18,432) = 100.0 Hz exactly. Timer1 captures falling edges from the IR demodulator on P0.17, measuring pulse widths for NEC protocol decoding. I2C0 runs the byte-level I2C state machine — start condition, address, data bytes, acknowledge, stop — so the main loop only needs to enqueue transactions and check completion flags.
Timer2 tick calculation
Section titled “Timer2 tick calculation”| Parameter | Value |
|---|---|
| PCLK | 14,745,600 Hz |
| T2PR (prescaler) | 7 (divide by 8) |
| T2MR0 (match) | 0x4800 (18,432) |
| Tick rate | 14,745,600 / 8 / 18,432 = 100.0 Hz |
| Tick period | 10.0 ms |
The prescaler and match values were chosen to produce an exact 100 Hz tick with zero remainder. This is another benefit of the 14.7456 MHz crystal — the math works out cleanly.
Watchdog
Section titled “Watchdog”The watchdog timer is configured with WDTC = 0x384000 (3,670,016). At PCLK/4 watchdog clock rate, this gives a timeout period of approximately 1 second. The feed sequence (write 0xAA then 0x55 to WDFEED at 0xE0000008) must be executed every tick to prevent a system reset. If the main loop hangs — due to a software bug, an infinite loop in a peripheral driver, or a stuck I2C transaction — the watchdog will reset the MCU and the boot sequence will start over.
GPIO pin assignments
Section titled “GPIO pin assignments”Every GPIO pin used by the firmware has been decoded from the IODIR0 register setting (0x02CD9CF0) and cross-referenced with the functions that access each pin:
| Pin | Direction | Function | Details |
|---|---|---|---|
| P0.0 | Alt (TXD0) | UART0 TX | To CP2104 USB-UART bridge |
| P0.1 | Alt (RXD0) | UART0 RX | From CP2104 (not read by firmware) |
| P0.2 | Alt (SCL0) | I2C0 clock | To D2-81431 DSP, 480 kHz |
| P0.3 | Alt (SDA0) | I2C0 data | To D2-81431 DSP |
| P0.4 | Output | VFD DB4 | HD44780 data bit 4 |
| P0.5 | Output | VFD DB5 | HD44780 data bit 5 |
| P0.6 | Output | VFD DB6 | HD44780 data bit 6 |
| P0.7 | Output | VFD DB7 | HD44780 data bit 7 |
| P0.8 | Input | Encoder button | Menu select / confirm |
| P0.11 | Output | VFD RS | Register Select (0=command, 1=data) |
| P0.12 | Output | Heartbeat LED | XOR toggle, rate varies with power state |
| P0.13 | Input | D2-81431 fault | Debounced 8-sample, protection status |
| P0.14 | Special | ISP entry | JP24 pulls LOW for bootloader mode |
| P0.15 | Output | Unknown | Cleared at init |
| P0.17 | Alt (CAP1.2) | IR receiver | Timer1 capture input, NEC decoding |
| P0.18 | Output | VFD E | Enable strobe, data latched on falling edge |
| P0.20 | Input | 12V trigger / audio sense | External power control input |
| P0.21 | Input | Encoder B | Rotary encoder channel B, 4-sample debounce |
| P0.23 | Output | Amplifier enable | HIGH = power on, LOW = standby |
| P0.24 | Input | Encoder A | Rotary encoder channel A, 4-sample debounce |
| P0.30 | Input | Unknown | Debounced 8-sample, active-low |
The pin init function (FUN_00000f04) configures PINSEL0 = 0x55, which assigns P0.0/P0.1 to UART0 and P0.2/P0.3 to I2C0. PINSEL1 = 0x08 assigns P0.17 to Timer1 capture. All other pins operate as standard GPIO with direction set by IODIR0.