Files
rt-4d/tools/refs.py
T
viktor 5f4d207aa5 Мост к DMR-модулю, разбор спектра REFV DualTachyon, карта запчастей прошивки
Баузбенд FM100B:
- найден прозрачный мост ПК<->модуль (включение с зажатой МЕНЮ), проверен живьём
- расшифрована контрольная сумма кадров (one's complement, BE), сверена с ответом рации
- инструмент tools/fm100b.py (ping/send/raw/scan)
- cmd 0x25 = запрос версии, модуль отвечает V1.2.0.32 (совпало с офиц. образом)
- разобран диспетчер входящих кадров: 193 записи, реальный код у 10 команд
- cmd 0x59 = индикация приёма, она же управляет гейтом звука PA14
- исправлено: boot-handshake это cmd 0x84, а не 0x64 (research/re/dmr.md)

Спектроанализатор REFV DualTachyon (docs/refw-spectrum.md):
- вызывается как функция горячей клавиши №22 Analog Spectrum
- вход 0x08009CAA -> обычный 0x080139E4 / по зоне 0x08015318
- спектр это экран №11; тик 0x08013BA4 (автомат на 4 состояния), клавиши 0x08013BFC

Декомпозиция (docs/firmware-parts.md):
- два процессора + внешний SPI = три канала внедрения
- карта ресурсов SPI: шрифты, пиньинь, голос, таблица Unicode
- найден штатный загрузчик ресурсов FontVoicePicture (шрифты/голос/картинки)
- дерево меню целиком: MIC/SPK Gain, RX/TX Limit, SMS Format уже в стоке
- аудио двухступенчатое: PA2 питание УНЧ, PA14 гейт от DMR-модуля

Прочее:
- везде исправлен режим прошивки: тангента PTT вместо клавиши "*"
- устаревший Ru-4D_Flasher.exe удалён из репозитория
- добавлены инструменты реверса: xref, refs, gpiomap, gpioscan, schem
2026-07-22 21:17:20 +09:00

115 строки
3.8 KiB
Python

#!/usr/bin/env python3
"""Exhaustive reference finder for the RT-4D app image.
Decodes one instruction at EVERY 2-byte alignment (linear sweep desyncs on
Thumb), so nothing is missed.
python refs.py addr <0xADDR> [ctx] who pc-relative-loads this address/constant
python refs.py gpio full GPIO map: (port, register, mask) per site
"""
import sys, struct, re, capstone
BASE = 0x08002800
IMG = open("/home/viktor/claude/rt-4d/stock-fw/rt4d_stock_v3.25_abs_0x08002800.bin", "rb").read()
md = capstone.Cs(capstone.CS_ARCH_ARM, capstone.CS_MODE_THUMB)
PORTS = {0x40020000: "GPIOA", 0x40020400: "GPIOB", 0x40020800: "GPIOC",
0x40020C00: "GPIOD", 0x40021000: "GPIOE", 0x40021400: "GPIOF"}
# AT32F43x GPIO layout
REG = {0x00: "CFGR", 0x10: "IDT", 0x14: "ODT", 0x18: "SCR(set)",
0x1C: "CLR16", 0x28: "CLR(clear)", 0x2C: "TOGR(toggle)"}
def word(va):
o = va - BASE
return struct.unpack_from("<I", IMG, o)[0] if 0 <= o <= len(IMG) - 4 else None
def decode_at(va):
o = va - BASE
for i in md.disasm(IMG[o:o + 4], va):
return i
return None
def litval(i):
if i is None or not i.mnemonic.startswith("ldr"):
return None
m = re.search(r"\[pc, #(?:0x)?([0-9a-fA-F]+)\]", i.op_str)
if not m:
return None
return word(((i.address + 4) & ~3) + int(m.group(1), 16))
def all_lit_loads():
"""[(va, ins, value)] for every pc-relative ldr at any 2-byte alignment"""
out = []
for off in range(0, len(IMG) - 4, 2):
va = BASE + off
i = decode_at(va)
v = litval(i)
if v is not None:
out.append((va, i, v))
return out
def cmd_addr(target, ctx=8):
hits = [(va, i) for va, i, v in all_lit_loads() if v == target]
print(f"pc-relative loads of 0x{target:08X}: {len(hits)}")
for va, i in hits:
print(f"\n--- 0x{va:08X} ({i.mnemonic} {i.op_str}) ---")
a = va
for _ in range(ctx):
j = decode_at(a)
if j is None:
break
v = litval(j)
e = f" ; =0x{v:08X}" if v is not None else ""
print(f" 0x{j.address:08X}: {j.mnemonic:8} {j.op_str}{e}")
a += j.size
def cmd_gpio():
loads = all_lit_loads()
rows = []
for va, i, v in loads:
if v not in PORTS:
continue
reg = i.op_str.split(",")[0].strip()
a = va + i.size
mask = None
# small forward window: find store to [reg,#off]
for _ in range(6):
j = decode_at(a)
if j is None:
break
m = re.match(r"(r\d+), \[" + reg + r", #(0x[0-9a-fA-F]+|\d+)\]$", j.op_str)
if j.mnemonic.startswith("str") and m:
src, off = m.group(1), int(m.group(2), 0)
# back-window for the immediate loaded into src
b = va
for _ in range(10):
b -= 2
p = decode_at(b)
if p and p.op_str.startswith(src + ", #"):
try:
mask = int(p.op_str.split("#")[1], 0)
except Exception:
pass
break
rows.append((va, PORTS[v], off, mask))
break
a += j.size
print(f"{len(rows)} GPIO store sites\n")
print(f"{'site':<13}{'port':<7}{'register':<14}{'mask':<10}bit")
for va, p, off, m in rows:
bit = f"bit{m.bit_length()-1}" if m and (m & (m - 1)) == 0 else ("?" if m is None else "multi")
print(f"0x{va:08X} {p:<7}{REG.get(off, hex(off)):<14}{(hex(m) if m is not None else '?'):<10}{bit}")
if __name__ == "__main__":
if sys.argv[1] == "addr":
cmd_addr(int(sys.argv[2], 16), int(sys.argv[3]) if len(sys.argv) > 3 else 8)
elif sys.argv[1] == "gpio":
cmd_gpio()