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.
This commit is contained in:
Ryan Malloy 2026-01-29 19:15:49 -07:00
parent 5d468cd86a
commit 1b105f5632
6 changed files with 921 additions and 0 deletions

View File

@ -65,6 +65,17 @@ export default defineConfig({
{ label: 'Connectors', slug: 'reference/connectors' }, { label: 'Connectors', slug: 'reference/connectors' },
{ label: 'Schematics', slug: 'reference/schematics' }, { label: 'Schematics', slug: 'reference/schematics' },
{ label: 'Hardware Revisions', slug: 'reference/hardware-revisions' }, { 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' },
],
},
], ],
}, },
{ {

View File

@ -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.

View File

@ -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** | 134174 MHz, 200260 MHz, 400520 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 (015) |
| 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 | 134174 MHz | `0x00xx` |
| VHF High | 200260 MHz | `0x01xx` |
| UHF | 400520 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.

View File

@ -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

View File

@ -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 |
|---------|------|-------------|
| `0x045d0x0460` | `freq_main` | Main frequency (4 bytes, kHz × 100) |
| `0x04590x045c` | `freq_sub` | Sub/offset frequency (4 bytes) |
| `0x04610x0464` | `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 |
|---------|------|-------------|
| `0x00000x0003` | `Common_RAM` | 32-bit accumulator (bank 0) |
| `0x05a10x05a4` | `math_op_a` | First operand |
| `0x05a50x05a8` | `math_op_b` | Second operand |
| `0x05a90x05ac` | `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 (0x000x5F)
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 19 are stored at `0x00`, `0x0A`, `0x14`, etc.
Channel 0 is the power-on default configuration.
### System Configuration (0x5A0x6F)
| Address | Content |
|---------|---------|
| `0x5A` | Squelch level (09) |
| `0x5B` | Squelch sensitivity |
| `0x5C` | Volume level (039) |
| `0x5D` | Audio filter flags |
| `0x5E0x5F` | Beacon timer (seconds) |
| `0x600x63` | Reserved |
| `0x64` | Bandwidth setting |
| `0x65` | VOX sensitivity |
| `0x66` | Hang time (×10 ms) |
| `0x67` | COR inhibit time |
### Beacon Message (0x700x8F)
32 bytes for the beacon message text (null-terminated ASCII).
### Callsign (0x900xA3)
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.

View File

@ -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.