From 1b105f563242fadae0adb5456d31193e998529b4 Mon Sep 17 00:00:00 2001 From: Ryan Malloy Date: Thu, 29 Jan 2026 19:15:49 -0700 Subject: [PATCH] Add firmware internals section documenting RE work New reference pages covering the V2.4a reverse engineering: - Overview with architecture diagram and memory layout - Command dispatch: XOR pattern, two dispatchers, compound commands - Memory map: RAM variables, EEPROM layout, SFR definitions - I2C & synthesizer: RDA1846S communication, frequency programming - Source files: links to annotated assembly, C port, Python tools These document the 19,053-line annotated assembly and 12,020-line readable C port produced by Phases 1-3 of the RE project. --- astro.config.mjs | 11 + .../reference/firmware/command-dispatch.md | 172 +++++++++++++ .../reference/firmware/i2c-synthesizer.md | 226 ++++++++++++++++++ src/content/docs/reference/firmware/index.md | 108 +++++++++ .../docs/reference/firmware/memory-map.md | 197 +++++++++++++++ .../docs/reference/firmware/source-files.md | 207 ++++++++++++++++ 6 files changed, 921 insertions(+) create mode 100644 src/content/docs/reference/firmware/command-dispatch.md create mode 100644 src/content/docs/reference/firmware/i2c-synthesizer.md create mode 100644 src/content/docs/reference/firmware/index.md create mode 100644 src/content/docs/reference/firmware/memory-map.md create mode 100644 src/content/docs/reference/firmware/source-files.md diff --git a/astro.config.mjs b/astro.config.mjs index 2df8a08..b3e3960 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -65,6 +65,17 @@ export default defineConfig({ { label: 'Connectors', slug: 'reference/connectors' }, { label: 'Schematics', slug: 'reference/schematics' }, { label: 'Hardware Revisions', slug: 'reference/hardware-revisions' }, + { + label: 'Firmware Internals', + collapsed: true, + items: [ + { label: 'Overview', slug: 'reference/firmware' }, + { label: 'Command Dispatch', slug: 'reference/firmware/command-dispatch' }, + { label: 'Memory Map', slug: 'reference/firmware/memory-map' }, + { label: 'I2C & Synthesizer', slug: 'reference/firmware/i2c-synthesizer' }, + { label: 'Source Files', slug: 'reference/firmware/source-files' }, + ], + }, ], }, { diff --git a/src/content/docs/reference/firmware/command-dispatch.md b/src/content/docs/reference/firmware/command-dispatch.md new file mode 100644 index 0000000..bad25a9 --- /dev/null +++ b/src/content/docs/reference/firmware/command-dispatch.md @@ -0,0 +1,172 @@ +--- +title: Command Dispatch +description: How the RS-UV3 firmware parses and routes serial commands +--- + +The RS-UV3 uses a two-character command protocol. Every command is exactly two ASCII characters, optionally followed by parameters. The firmware implements this with a simple but elegant XOR-based dispatch pattern. + +## Protocol Basics + +``` +┌──────┬──────┬─────────────────┬────┐ +│ Cmd1 │ Cmd2 │ Parameters │ CR │ +└──────┴──────┴─────────────────┴────┘ + 'F' 'S' '146520000' 0x0D +``` + +- **Two-character command code** — Not case-sensitive (`fs` = `FS`) +- **Optional parameters** — Numeric or string, depending on command +- **Carriage return terminator** — ASCII 13 (0x0D) +- **Baud rate** — 19200 8N1 (default) + +## The XOR Dispatch Pattern + +PIC18 assembly doesn't have a `switch` statement. Instead, the firmware uses XOR to test character values: + +```asm +; Read first character from serial buffer +movff 0x59a, FSR0L ; Load buffer base address +movff 0x59b, FSR0H +movf POSTINC0, W ; Read first byte into W + +; Check if it's 'P' (0x50) +xorlw 0x50 ; XOR with 'P' +btfss STATUS, Z ; Skip if zero (matched) +bra not_P ; Branch if not 'P' + +; It's 'P' — now check second character +movlw 0x01 +addwf 0x9a, W ; Advance buffer pointer by 1 +movwf FSR0L +; ... load second byte ... +xorlw 0x44 ; XOR with 'D' +bz handle_PD ; Branch if zero → PD command +``` + +The key insight: `XOR A, A = 0`. If the character matches the expected value, the result is zero and the Z (zero) flag is set. + +### Why XOR Instead of Compare? + +On PIC18: +- `CPFSEQ` (compare, skip if equal) exists but can't compare with literals +- `SUBLW` (subtract literal) works but affects the carry flag +- `XORLW` (XOR with literal) is single-cycle, only affects Z flag, and the value is preserved + +The XOR pattern is the most efficient way to dispatch on ASCII values. + +## Two Dispatchers + +The firmware splits command handling across two functions: + +### Dispatcher A — `0x0042b0` (5,826 bytes) + +Handles memory, power, and low-level system commands: + +| Command | Description | +|---------|-------------| +| **PD** | Power down transceiver chip | +| **PW** | TX power level (high/low) | +| **RC** | Recall memory channel | +| **RR** | Read RDA1846S register | +| **RS** | Set RDA1846S register | +| **ST** | Store to memory channel | +| **CP** | Copy/report channel parameters | +| **FD** | Factory defaults | +| **BL** | Enter bootloader | + +### Dispatcher B — `0x005972` (10,632 bytes) + +Handles the bulk of radio configuration: + +| First Char | Commands | +|------------|----------| +| **A** | AF, AI, AO | +| **B** | B1, B2, BC, BM, BS, BT, BW | +| **C** | CB, CC, CF, CL, CO, CS, CT, CW | +| **D** | DD, DP, DR, DS | +| **E** | EX | +| **F** | F (compound), FM, FW | +| **G** | GM, GT | +| **H** | HP, HT | +| **I** | ID, IT | +| **L** | LD | +| **M** | MC | +| **S** | SD, SN, SO, SQ, SR, SS | +| **T** | TF, TG, TM, TO, TP, TS, TX | +| **V** | VL, VT, VU, VX | +| **X** | X1 | + +## Compound Commands + +The **F** (frequency) command is special — it uses a third character as a sub-command: + +| Syntax | Meaning | +|--------|---------| +| `FR146520000` | Set RX frequency | +| `FT146520000` | Set TX frequency | +| `FS146520000` | Set simplex (both) | +| `FU005000` | Shift frequency up | +| `FD005000` | Shift frequency down | +| `F?` | Query current frequencies | + +This is handled as a single dispatch entry that checks the third character internally: + +```asm +; First char already matched 'F' +; Check second char for R, T, S, U, D, or ? +xorlw 0x52 ; 'R' +bz freq_rx +xorlw 0x52 ^ 0x54 ; 'T' (XOR trick: 0x52^0x52^0x54 = 0x54) +bz freq_tx +; ... and so on +``` + +## Serial Buffer + +Commands are received into a small buffer: + +| Address | Purpose | +|---------|---------| +| `0x059a` | Buffer pointer (low byte) | +| `0x059b` | Buffer pointer (high byte) | +| `0x059d` | Port selector (1=USB, 2=DE-9) | + +The buffer is only 2 bytes for the command code itself — parameters are parsed directly from the UART as they arrive. + +## Response Routing + +When a command generates a response, the firmware routes it back to whichever port received the command: + +```asm +movff 0x59d, 0x5df ; Copy port selector to output port +movlw 0x74 ; String table index for "PD: " +movwf 0xe0 +call serial_send_string +``` + +This allows both serial ports (USB via JP1 and DE-9) to operate independently. + +## Query Pattern + +Most commands support a `?` suffix to query the current value: + +```asm +; After matching command code, check for '?' +movf POSTINC0, W ; Read third character +xorlw 0x3f ; '?' +bnz set_value ; Not '?' → setting a value +; Fall through to query handler +movff 0x59d, 0x5df ; Set output port +movlw 0x8f ; String table: "SQ: " +call serial_send_string +; ... send current value ... +``` + +## Timing + +Command processing is fast: +- Dispatch lookup: ~20 cycles (2.5 µs at 8 MHz) +- Simple query response: ~100 cycles (12.5 µs) +- Frequency change (with I2C): ~5,000 cycles (625 µs) + +Even the slowest commands complete well under the 52 µs between characters at 19200 baud. diff --git a/src/content/docs/reference/firmware/i2c-synthesizer.md b/src/content/docs/reference/firmware/i2c-synthesizer.md new file mode 100644 index 0000000..260c0e1 --- /dev/null +++ b/src/content/docs/reference/firmware/i2c-synthesizer.md @@ -0,0 +1,226 @@ +--- +title: I2C & Synthesizer +description: Communication with the RDA1846S transceiver chip +--- + +The RS-UV3's RF section is built around the **RDA1846S** — a single-chip transceiver that handles frequency synthesis, modulation, demodulation, and audio processing. The PIC18 communicates with it over I2C. + +## RDA1846S Overview + +The RDA1846S is a highly integrated FM transceiver IC: + +| Feature | Specification | +|---------|---------------| +| **Frequency range** | 134–174 MHz, 200–260 MHz, 400–520 MHz | +| **Channel spacing** | 12.5 kHz or 25 kHz | +| **TX power** | Adjustable, up to +8 dBm at chip | +| **RX sensitivity** | -122 dBm typical | +| **Interface** | I2C (7-bit address 0x2E) | +| **Supply** | 3.3V | + +The chip contains: +- Fractional-N PLL frequency synthesizer +- PA driver (external PA for full power) +- LNA, mixer, IF filter +- Audio codec with pre/de-emphasis +- CTCSS/DCS encoder and decoder +- RSSI measurement + +## I2C Protocol + +The RDA1846S uses standard I2C with 7-bit addressing: + +``` +┌─────┬───────────────┬─────┬──────────────┬─────┬──────────────┬─────┐ +│START│ 0x2E + W (0) │ ACK │ Register Addr│ ACK │ Data (16-bit)│STOP │ +└─────┴───────────────┴─────┴──────────────┴─────┴──────────────┴─────┘ +``` + +All registers are 16 bits. The firmware bit-bangs I2C rather than using the PIC's MSSP hardware in I2C mode: + +```asm +; i2c_write_byte — Send one byte over I2C +i2c_write_byte: + movlw 0x08 ; 8 bits + movwf bit_count +write_loop: + bcf LATC, SDA ; Data low (default) + btfsc i2c_data, 7 ; Check MSB + bsf LATC, SDA ; Set data high if bit=1 + bsf LATC, SCL ; Clock high + rlcf i2c_data, F ; Shift left + bcf LATC, SCL ; Clock low + decfsz bit_count + bra write_loop + ; Read ACK + bsf TRISC, SDA ; SDA as input + bsf LATC, SCL ; Clock high + btfsc PORTC, SDA ; Check ACK (low = ACK) + bsf i2c_status, 0 ; Set error if NACK + bcf LATC, SCL ; Clock low + bcf TRISC, SDA ; SDA as output + return +``` + +## Frequency Programming + +To tune to a new frequency, the firmware must: +1. Calculate PLL divider values from the target frequency +2. Write to the RDA1846S frequency registers +3. Wait for PLL lock + +### Frequency Calculation + +The RDA1846S uses a fractional-N synthesizer. For a target frequency `F`: + +``` +F = F_ref × (N + K/65536) / 2 +``` + +Where: +- `F_ref` = 12.8 MHz (crystal reference) +- `N` = integer divider (11 bits) +- `K` = fractional divider (16 bits) + +The firmware performs this calculation using 32-bit math: + +```c +// Pseudocode for frequency calculation +uint32_t freq_khz = 146520; // 146.52 MHz in kHz +uint32_t divider = (freq_khz * 1000 * 2) / 12800; +uint16_t N = divider >> 16; +uint16_t K = divider & 0xFFFF; +``` + +### Register Writes + +Setting a new RX frequency requires writing several registers: + +| Register | Address | Purpose | +|----------|---------|---------| +| 0x29 | RX frequency high | N divider | +| 0x2A | RX frequency low | K divider | +| 0x30 | Control | PLL enable, band select | + +```asm +; set_rx_frequency — Program new RX frequency +set_rx_frequency: + ; Write N divider to register 0x29 + movlw 0x29 + call i2c_start_write + movff freq_n_hi, i2c_data + call i2c_write_byte + movff freq_n_lo, i2c_data + call i2c_write_byte + call i2c_stop + + ; Write K divider to register 0x2A + movlw 0x2A + call i2c_start_write + movff freq_k_hi, i2c_data + call i2c_write_byte + movff freq_k_lo, i2c_data + call i2c_write_byte + call i2c_stop + + ; Trigger PLL recalibration + ; ... + return +``` + +## Key RDA1846S Registers + +### Frequency Control + +| Reg | Name | Bits | Description | +|-----|------|------|-------------| +| 0x29 | RX_FREQ_H | 15:0 | RX frequency high word | +| 0x2A | RX_FREQ_L | 15:0 | RX frequency low word | +| 0x39 | TX_FREQ_H | 15:0 | TX frequency high word | +| 0x3A | TX_FREQ_L | 15:0 | TX frequency low word | + +### Audio + +| Reg | Name | Description | +|-----|------|-------------| +| 0x44 | VOL | RX audio volume (0–15) | +| 0x45 | AUDIO_DAC | DAC configuration | +| 0x59 | VOICE | Voice scrambler settings | + +### CTCSS/DCS + +| Reg | Name | Description | +|-----|------|-------------| +| 0x4A | CTCSS_FREQ | CTCSS tone frequency | +| 0x4B | CDCSS_CODE | DCS code | +| 0x63 | TONE_SEL | Tone mode select | + +### Status + +| Reg | Name | Description | +|-----|------|-------------| +| 0x5F | RSSI | Received signal strength | +| 0x5C | SQ_OPEN | Squelch status | + +## RR and RS Commands + +The `RR` and `RS` commands provide direct access to RDA1846S registers: + +``` +RR29 → Read register 0x29 +RS2900FF → Write 0x00FF to register 0x29 +``` + +:::caution[Direct Register Access] +Writing arbitrary values to RDA1846S registers can cause RF interference, loss of calibration, or other unintended behavior. Use `RR`/`RS` only if you understand the chip's register map. +::: + +## Initialization Sequence + +At power-on, `init_radio` (`0x000400`) performs a lengthy initialization: + +1. **Soft reset** — Write 0x0001 to register 0x00 +2. **Clock config** — Set reference divider, enable PLL +3. **Band select** — Configure for 2m/1.25m/70cm +4. **Filter setup** — Set IF bandwidth (12.5 or 25 kHz) +5. **Audio setup** — Configure volume, pre/de-emphasis +6. **CTCSS init** — Load default tone (if enabled) +7. **Calibration** — Trigger automatic calibration routines +8. **Squelch** — Set initial squelch level + +The full initialization is about 1,782 bytes of code — roughly 5% of the entire firmware. + +## Band Selection + +The RDA1846S supports three bands: + +| Band | Frequency | Register 0x30 bits | +|------|-----------|-------------------| +| VHF Low | 134–174 MHz | `0x00xx` | +| VHF High | 200–260 MHz | `0x01xx` | +| UHF | 400–520 MHz | `0x02xx` | + +The firmware automatically selects the correct band based on the programmed frequency: + +```c +if (freq_mhz < 200) + band = VHF_LOW; +else if (freq_mhz < 300) + band = VHF_HIGH; +else + band = UHF; +``` + +## Timing Constraints + +I2C transactions have specific timing requirements: + +| Parameter | Value | +|-----------|-------| +| Clock frequency | ~100 kHz | +| Start setup | 4.7 µs minimum | +| Stop setup | 4.0 µs minimum | +| Data hold | 0 µs minimum | +| PLL lock time | ~5 ms after frequency change | + +The firmware inserts appropriate delays between operations. A complete frequency change (command parse → PLL lock → audio enabled) takes approximately 10 ms. diff --git a/src/content/docs/reference/firmware/index.md b/src/content/docs/reference/firmware/index.md new file mode 100644 index 0000000..8eadcac --- /dev/null +++ b/src/content/docs/reference/firmware/index.md @@ -0,0 +1,108 @@ +--- +title: Firmware Internals +description: Reverse-engineered documentation of the RS-UV3 V2.4a firmware +--- + +This section documents the internal architecture of the RS-UV3 V2.4a firmware, derived from reverse engineering the original HEX file using Ghidra and gputils. + +:::caution[For Advanced Users] +This documentation is for developers who want to understand, modify, or learn from the firmware internals. Normal operation doesn't require any of this information. +::: + +## Firmware Overview + +| Property | Value | +|----------|-------| +| **Version** | 2.4a | +| **MCU** | PIC18F46J11-I/PT | +| **Package** | TQFP-44 | +| **Flash** | 64KB (36,296 bytes used) | +| **RAM** | 3,776 bytes | +| **Clock** | Internal oscillator, 8 MHz | +| **Voltage** | 3.3V | + +The firmware handles: +- Serial command parsing (66 commands across two dispatchers) +- I2C communication with the RDA1846S transceiver chip +- EEPROM storage for channel memories and configuration +- CTCSS/DCS tone encoding and decoding +- CW and DTMF generation +- Beacon timing and transmission + +## Architecture + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Main Loop (0x000400) │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │ +│ │ Check UART │→ │ Parse Cmd │→ │ Dispatch A or B │ │ +│ │ RX Buffer │ │ 2-char code │ │ based on 1st char │ │ +│ └─────────────┘ └─────────────┘ └─────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ + │ + ┌───────────────┴───────────────┐ + ▼ ▼ +┌──────────────────────────┐ ┌──────────────────────────┐ +│ Dispatcher A (0x0042b0) │ │ Dispatcher B (0x005972) │ +│ 5,826 bytes │ │ 10,632 bytes │ +│ ────────────────────────│ │ ────────────────────────│ +│ PD, PW, RC, RR, RS │ │ AF, AI, AO, B1, B2... │ +│ ST, CP, FD, BL │ │ (bulk of commands) │ +└──────────────────────────┘ └──────────────────────────┘ + │ │ + └───────────────┬───────────────┘ + ▼ + ┌──────────────────────────────┐ + │ Hardware Abstraction Layer │ + │ ─────────────────────────── │ + │ • I2C to RDA1846S (synth) │ + │ • EEPROM read/write │ + │ • UART TX/RX │ + │ • ADC for RSSI, temp, volts │ + └──────────────────────────────┘ +``` + +## Memory Layout + +| Region | Address | Size | Purpose | +|--------|---------|------|---------| +| Reset vector | `0x000000` | 4B | Jump to init | +| High-priority ISR | `0x000008` | 16B | Serial RX entry | +| Low-priority ISR | `0x000018` | 874B | Timers, buffers | +| Init + Main | `0x000400` | 2,098B | Setup and event loop | +| Dispatcher A | `0x0042b0` | 5,826B | Memory/power commands | +| Dispatcher B | `0x005972` | 10,632B | Radio config commands | +| String tables | `0x008B38` | ~1,800B | Response strings | +| **Total code** | — | 36,296B | 55% of 64KB flash | + +## Tools Used + +The reverse engineering was performed with: + +| Tool | Version | Purpose | +|------|---------|---------| +| **Ghidra** | 12.0.1 | PIC-18 decompilation, cross-reference analysis | +| **GhydraMCP** | — | Ghidra integration for Claude Code | +| **gputils** | 1.5.2 | `gpdasm` disassembly, `gpasm` reassembly | +| **objcopy** | 2.45.1 | Intel HEX to raw binary conversion | +| **Python 3** | — | Annotation scripts (annotate.py, c-port.py) | + +## Verification + +All reverse-engineered assembly was verified byte-identical to the original firmware: + +```bash +# Convert both to raw binary and compare +objcopy -I ihex -O binary reassembled.hex /tmp/verify.bin +cmp original.bin /tmp/verify.bin +# Output: (no output = identical) +``` + +This verification runs automatically via `make verify` in the source directory. + +## Section Contents + +- **[Command Dispatch](./command-dispatch/)** — How the firmware parses and routes serial commands +- **[Memory Map](./memory-map/)** — RAM variables, EEPROM layout, and register definitions +- **[I2C & Synthesizer](./i2c-synthesizer/)** — Communication with the RDA1846S transceiver chip +- **[Source Files](./source-files/)** — Annotated assembly, C port, and analysis tools diff --git a/src/content/docs/reference/firmware/memory-map.md b/src/content/docs/reference/firmware/memory-map.md new file mode 100644 index 0000000..d31bb0b --- /dev/null +++ b/src/content/docs/reference/firmware/memory-map.md @@ -0,0 +1,197 @@ +--- +title: Memory Map +description: RAM variables, EEPROM layout, and register definitions +--- + +The PIC18F46J11 has 3,776 bytes of RAM organized into banks, plus 1KB of internal EEPROM for persistent storage. This page documents the key memory locations used by the RS-UV3 firmware. + +## RAM Organization + +### Serial Communication + +| Address | Name | Description | +|---------|------|-------------| +| `0x059a` | `cmd_char0` | Serial buffer pointer (low byte) | +| `0x059b` | `cmd_char1` | Serial buffer pointer (high byte) | +| `0x059d` | `port_selector` | Which port received command: 1=USB, 2=DE-9 | +| `0x059f` | `cmd_param` | Parsed numeric parameter | +| `0x05a0` | `response_port` | Output port for response | +| `0x05df` | `output_port` | Current serial output destination | + +### String Table Interface + +| Address | Name | Description | +|---------|------|-------------| +| `0x05e0` | `str_tbl_lo` | String table index (low byte) | +| `0x05e1` | `str_tbl_hi` | String table index (high byte) | +| `0x05e2` | `str_length` | String length for transmission | + +### Frequency Storage + +| Address | Name | Description | +|---------|------|-------------| +| `0x045d–0x0460` | `freq_main` | Main frequency (4 bytes, kHz × 100) | +| `0x0459–0x045c` | `freq_sub` | Sub/offset frequency (4 bytes) | +| `0x0461–0x0464` | `freq_tx` | TX frequency when split | +| `0x0467` | `power_level` | TX power: 0=low, 1=high | + +### Configuration Flags + +| Address | Name | Description | +|---------|------|-------------| +| `0x0423` | `io_config` | AI/AO analog I/O configuration | +| `0x0425` | `config_flags` | Audio filter, power, misc flags | +| `0x0454` | `squelch_mode` | Squelch sensitivity: L/M/H | +| `0x046a` | `tone_mode` | CTCSS mode: 0=off, 1=encode, 2=enc+dec | + +### Math Library Workspace + +The firmware implements 32-bit multiply and divide using shift-and-add. These addresses are the working registers: + +| Address | Name | Description | +|---------|------|-------------| +| `0x0000–0x0003` | `Common_RAM` | 32-bit accumulator (bank 0) | +| `0x05a1–0x05a4` | `math_op_a` | First operand | +| `0x05a5–0x05a8` | `math_op_b` | Second operand | +| `0x05a9–0x05ac` | `math_result` | Result storage | + +### I2C Communication + +| Address | Name | Description | +|---------|------|-------------| +| `0x05e3` | `i2c_addr` | Target I2C address | +| `0x05e4` | `i2c_data` | Data byte for transfer | +| `0x05e5` | `i2c_status` | Transfer status/error code | + +## EEPROM Layout + +The PIC18F46J11 has 1KB of internal EEPROM. The RS-UV3 uses it for persistent configuration: + +### Channel Memories (0x00–0x5F) + +Each channel uses 10 bytes: + +| Offset | Size | Content | +|--------|------|---------| +| +0 | 4 | RX frequency (kHz × 100) | +| +4 | 4 | TX frequency (kHz × 100) | +| +8 | 1 | CTCSS tone index | +| +9 | 1 | Flags (power, squelch mode) | + +Channels 1–9 are stored at `0x00`, `0x0A`, `0x14`, etc. +Channel 0 is the power-on default configuration. + +### System Configuration (0x5A–0x6F) + +| Address | Content | +|---------|---------| +| `0x5A` | Squelch level (0–9) | +| `0x5B` | Squelch sensitivity | +| `0x5C` | Volume level (0–39) | +| `0x5D` | Audio filter flags | +| `0x5E–0x5F` | Beacon timer (seconds) | +| `0x60–0x63` | Reserved | +| `0x64` | Bandwidth setting | +| `0x65` | VOX sensitivity | +| `0x66` | Hang time (×10 ms) | +| `0x67` | COR inhibit time | + +### Beacon Message (0x70–0x8F) + +32 bytes for the beacon message text (null-terminated ASCII). + +### Callsign (0x90–0xA3) + +20 bytes for the station callsign (null-terminated ASCII). + +## Special Function Registers + +Key PIC18F46J11 SFRs used by the firmware: + +### UART (Serial) + +| Register | Address | Purpose | +|----------|---------|---------| +| `TXSTA1` | `0xFAC` | UART1 transmit status | +| `RCSTA1` | `0xFAB` | UART1 receive status | +| `TXREG1` | `0xFAD` | UART1 transmit data | +| `RCREG1` | `0xFAE` | UART1 receive data | +| `SPBRG1` | `0xFAF` | UART1 baud rate generator | +| `TXSTA2` | `0xF6C` | UART2 transmit status | +| `RCSTA2` | `0xF6B` | UART2 receive status | + +### I2C (MSSP) + +| Register | Address | Purpose | +|----------|---------|---------| +| `SSPCON1` | `0xFC6` | MSSP control 1 | +| `SSPCON2` | `0xFC5` | MSSP control 2 | +| `SSPSTAT` | `0xFC7` | MSSP status | +| `SSPBUF` | `0xFC9` | MSSP data buffer | +| `SSPADD` | `0xFC8` | I2C address/baud | + +### ADC + +| Register | Address | Purpose | +|----------|---------|---------| +| `ADCON0` | `0xFC2` | ADC control 0 | +| `ADCON1` | `0xFC1` | ADC control 1 | +| `ADRESH` | `0xFC4` | ADC result high | +| `ADRESL` | `0xFC3` | ADC result low | + +### Interrupts + +| Register | Address | Purpose | +|----------|---------|---------| +| `INTCON` | `0xFF2` | Interrupt control | +| `PIR1` | `0xF9E` | Peripheral interrupt flags 1 | +| `PIE1` | `0xF9D` | Peripheral interrupt enable 1 | + +## Memory Access Patterns + +### Reading from String Table + +The firmware stores response strings in program memory. To send a string: + +```asm +movlw 0x74 ; String index low +movwf str_tbl_lo +movlw 0x00 ; String index high +movwf str_tbl_hi +call serial_send_string ; Sends until null terminator +``` + +### EEPROM Read + +```asm +movlw 0x5A ; EEPROM address +movwf EEADR +bcf EECON1, EEPGD ; Select data EEPROM +bcf EECON1, CFGS ; Not configuration +bsf EECON1, RD ; Start read +movf EEDATA, W ; Read result +``` + +### EEPROM Write + +```asm +movlw 0x5A ; Address +movwf EEADR +movlw 0x05 ; Data to write +movwf EEDATA +bcf EECON1, EEPGD +bcf EECON1, CFGS +bsf EECON1, WREN ; Enable writes +; Required unlock sequence +movlw 0x55 +movwf EECON2 +movlw 0xAA +movwf EECON2 +bsf EECON1, WR ; Start write +; Wait for completion +btfsc EECON1, WR +bra $-2 +bcf EECON1, WREN ; Disable writes +``` + +The unlock sequence (0x55, 0xAA) is a hardware requirement to prevent accidental writes. diff --git a/src/content/docs/reference/firmware/source-files.md b/src/content/docs/reference/firmware/source-files.md new file mode 100644 index 0000000..9d61016 --- /dev/null +++ b/src/content/docs/reference/firmware/source-files.md @@ -0,0 +1,207 @@ +--- +title: Source Files +description: Annotated assembly, C port, and analysis tools +--- + +The reverse engineering effort produced several source files that document the RS-UV3 V2.4a firmware internals. All files are available in the project repository. + +## Primary Sources + +### Annotated Assembly + +**`src/rs-uv3-v2.4.asm`** — 19,053 lines + +The complete firmware in PIC18 assembly, annotated for readability: + +- **97 named functions** with descriptive labels +- **34 section headers** dividing the code into logical regions +- **71 RAM variable definitions** with meaningful names +- **247 ASCII character annotations** on XOR comparisons +- **Byte-identical to original** — verified via `make verify` + +```asm +; Example: Command dispatcher entry +cmd_dispatch_a: ; address: 0x0042b0 + movff 0x59a, FSR0L ; Load serial buffer pointer + movff 0x59b, FSR0H + movf POSTINC0, W ; Read first command character + xorlw 0x50 ; 'P' + btfss STATUS, Z + bra not_P_command + ; ... handle P commands (PD, PW) ... +``` + +### Readable C Port + +**`src/rs-uv3-v2.4-readable.c`** — 12,020 lines + +Ghidra's decompiled C output, cleaned up for human readability: + +- **89 function renames** from `FUN_CODE_XXXXXX` to descriptive names +- **130 variable renames** from `DAT_DATA_XXXX` to register names +- **34 section headers** matching the assembly +- **35 algorithm annotations** explaining complex routines + +```c +// Example: Frequency setting function +void set_frequency(void) { + // Calculate PLL divider from frequency in kHz + math_op_a = freq_main; + math_op_b = 0x00001388; // 5000 (scaling factor) + math_multiply_32(); + + // Write to RDA1846S via I2C + i2c_addr = 0x29; // RX frequency register + i2c_data = math_result >> 16; + i2c_write_word(); + // ... +} +``` + +:::note[Not Compilable] +The C port is a reference document, not compilable source. Ghidra's decompilation includes constructs that aren't valid C (like `CONCAT11` macros). Use it to understand algorithms, not to build firmware. +::: + +## Analysis Tools + +### annotate.py + +**`analysis/annotate.py`** — 250 lines + +Transforms raw gpdasm output into annotated assembly: + +- Applies the code/data boundary (code ends at `0x8B38`) +- Replaces misinterpreted data bytes with `db` directives +- Adds ASCII character annotations to XOR/SUB instructions +- Inserts function labels from `label-mapping.py` + +```python +# Key function: annotate_instruction +def annotate_instruction(line, address): + # Add ASCII annotation to xorlw/sublw + if 'xorlw' in line or 'sublw' in line: + match = re.search(r'0x([0-9a-f]{2})', line) + if match: + value = int(match.group(1), 16) + if 0x20 <= value <= 0x7E: + return f"{line.rstrip():<52}; '{chr(value)}'" + return line +``` + +### c-port.py + +**`analysis/c-port.py`** — 690 lines + +Transforms Ghidra's decompiled C into readable code: + +- Renames functions using mappings from `label-mapping.py` +- Renames data references to meaningful variable names +- Handles thunk functions (trampolines to real functions) +- Adds section headers and documentation comments + +```python +# Thunk handling +THUNK_NAMES = { + "thunk_FUN_CODE_001344": "thunk_parse_number", + "thunk_FUN_CODE_002aee": "thunk_set_baud_rate", + # ... +} +``` + +### label-mapping.py + +**`analysis/label-mapping.py`** — 241 lines + +Central database of all named symbols: + +- **Function names** — 97 entries with addresses and descriptions +- **Thunk mappings** — 4 trampolines to their targets +- **RAM variables** — 71 data addresses with names +- **Section boundaries** — 34 code region markers + +```python +FUNCTION_NAMES = { + "function_001": ("reset_vector", "0x000000", "Reset vector - jumps to init"), + "function_002": ("high_isr", "0x000008", "High-priority interrupt"), + "function_003": ("low_isr", "0x000018", "Low-priority interrupt"), + "function_004": ("init_radio", "0x000400", "Main init and event loop"), + "function_005": ("cmd_dispatch_a", "0x0042b0", "Dispatcher A: PD,PW,RC,RR,RS,ST,CP,FD,BL"), + "function_006": ("cmd_dispatch_b", "0x005972", "Dispatcher B: AF through X1"), + # ... 91 more entries +} +``` + +## Build System + +### Makefile + +**`src/Makefile`** + +Builds and verifies the assembly: + +```makefile +TARGET = rs-uv3-v2.4.hex +ORIGBIN = ../downloads/firmware/RS-UV3_V2-4.bin + +build: + gpasm -p p18f46j11 -o $(TARGET) rs-uv3-v2.4.asm + +verify: build + @objcopy -I ihex -O binary $(TARGET) /tmp/rs-uv3-verify.bin + @cmp $(ORIGBIN) /tmp/rs-uv3-verify.bin && \ + echo "MATCH: Output identical to original firmware" || \ + (echo "MISMATCH: Differences found"; exit 1) + +clean: + rm -f $(TARGET) *.lst *.cod +``` + +Run `make verify` to confirm the assembly produces byte-identical output. + +## Supporting Files + +### Analysis Documents + +| File | Description | +|------|-------------| +| `analysis/command-map.md` | Complete command dispatch documentation | +| `analysis/communicator-analysis.md` | Windows control software analysis | +| `analysis/collaboration-post.md` | Reverse engineering project summary | + +### Ghidra Project + +| Path | Contents | +|------|----------| +| `ghidra-project/RS-UV3-firmware.gpr` | Ghidra project file | +| `ghidra-project/RS-UV3-firmware.rep/` | Analysis database | +| `ghidra-project/export/` | Exported listings and decompilation | + +### Original Firmware + +| File | Description | +|------|-------------| +| `downloads/firmware/RS-UV3_V2-4.hex` | Original Intel HEX | +| `downloads/firmware/RS-UV3_V2-4.bin` | Raw binary (for verification) | + +## Verification + +The annotated assembly is verified byte-identical to the original firmware after every modification: + +```bash +$ cd src && make verify +gpasm -p p18f46j11 -o rs-uv3-v2.4.hex rs-uv3-v2.4.asm +MATCH: Output identical to original firmware +``` + +Labels, comments, and section headers don't generate machine code — they're stripped during assembly. This allows aggressive annotation without any risk of changing the firmware behavior. + +## Contributing + +To add annotations or improve the documentation: + +1. Edit `analysis/label-mapping.py` to add/rename symbols +2. Run `python analysis/annotate.py` to regenerate assembly +3. Run `make verify` to confirm no byte changes +4. Run `python analysis/c-port.py` to regenerate C port + +The verification step is critical — it ensures your changes don't accidentally affect the assembled output.