Skip to content

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.

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.

The PLL setup at 0x5C through 0x98 configures the clock multiplier:

RegisterValueMeaning
PLLCFG0x23MSEL=3 (M=4, multiply by 4), PSEL=1 (P=2)
PLLCON0x01Enable PLL
PLLFEED0xAA, 0x55Feed 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.

At 58.98 MHz, the flash memory cannot keep up with zero-wait-state access. The MAM configuration at 0x9C:

RegisterValueMeaning
MAMTIM33 flash access cycles (required above 40 MHz)
MAMCR2Fully 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.

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:

ModeStack sizePurpose
Undefined4 bytesTrap handler (infinite loop, minimal stack needed)
Abort4 bytesTrap handler
IRQ4 bytesInterrupt dispatch (VIC handles nesting)
FIQ4 bytesFast interrupt (unused in this firmware)
Supervisor (SVC)256 bytesISP bootloader context
SystemRemaining SRAMApplication 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.

The .data section copy at 0x104 through 0x11C transfers 1196 bytes of initialized global data from flash to SRAM:

ParameterValue
Source (flash)0x00007A88
Destination (SRAM)0x40000000
End address0x400004AC
Size1196 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.

Immediately following the .data copy, the .bss section at 0x120 through 0x134 zeroes 948 bytes:

ParameterValue
Start0x400004AC
End0x40000860
Size948 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.

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:

CallFunctionPurpose
1FUN_00000f04Pin mux, UART0 (9600,8,N,1), GPIO direction
2FUN_00000f90VIC interrupt controller — assigns slots for Timer2, Timer1, I2C0
3FUN_0000016cTimer2 system tick — 100 Hz (10 ms period)
4FUN_000020c8I2C0 init — 480 kHz fast mode, VIC slot 5
5FUN_00002914Timer1 / IR receiver — capture on P0.17 (CAP1.2)
6FUN_00003804Load settings — reads 16 bytes from D2-81431 EEPROM register 0
7FUN_00002ffcIR debounce — initializes repeat delay state
8FUN_00001040Watchdog — WDTC = 0x384000, starts countdown
9FUN_000076c4Settings validation — range-checks every byte, resets out-of-range values
10FUN_00003d50VFD 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 EPISODE
EA500 AMPLIFIER

This same string is the only text sent over UART0 — the boot message. After that, the serial port is never touched again.

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.

Three peripherals use vectored interrupts through the VIC:

VIC SlotIRQSourceISRPurpose
126Timer2FUN_0000016cSystem tick — sets tick_flag every 10 ms
45Timer1FUN_00002914IR receiver — captures edge timing on P0.17
59I2C0FUN_000020c8I2C 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.

ParameterValue
PCLK14,745,600 Hz
T2PR (prescaler)7 (divide by 8)
T2MR0 (match)0x4800 (18,432)
Tick rate14,745,600 / 8 / 18,432 = 100.0 Hz
Tick period10.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.

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.

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:

PinDirectionFunctionDetails
P0.0Alt (TXD0)UART0 TXTo CP2104 USB-UART bridge
P0.1Alt (RXD0)UART0 RXFrom CP2104 (not read by firmware)
P0.2Alt (SCL0)I2C0 clockTo D2-81431 DSP, 480 kHz
P0.3Alt (SDA0)I2C0 dataTo D2-81431 DSP
P0.4OutputVFD DB4HD44780 data bit 4
P0.5OutputVFD DB5HD44780 data bit 5
P0.6OutputVFD DB6HD44780 data bit 6
P0.7OutputVFD DB7HD44780 data bit 7
P0.8InputEncoder buttonMenu select / confirm
P0.11OutputVFD RSRegister Select (0=command, 1=data)
P0.12OutputHeartbeat LEDXOR toggle, rate varies with power state
P0.13InputD2-81431 faultDebounced 8-sample, protection status
P0.14SpecialISP entryJP24 pulls LOW for bootloader mode
P0.15OutputUnknownCleared at init
P0.17Alt (CAP1.2)IR receiverTimer1 capture input, NEC decoding
P0.18OutputVFD EEnable strobe, data latched on falling edge
P0.20Input12V trigger / audio senseExternal power control input
P0.21InputEncoder BRotary encoder channel B, 4-sample debounce
P0.23OutputAmplifier enableHIGH = power on, LOW = standby
P0.24InputEncoder ARotary encoder channel A, 4-sample debounce
P0.30InputUnknownDebounced 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.