Мост к 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
родитель ae36c3b729
Коммит 5f4d207aa5
34 изменённых файлов: 21718 добавлений и 17823 удалений
+50
Просмотреть файл
@@ -0,0 +1,50 @@
#!/usr/bin/env python3
"""Find every GPIO-base literal load in the RT-4D app and show the surrounding
code, so each (port, register-offset, bit-mask) access can be identified."""
import struct, re, sys, 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",
0x40021800: "GPIOG", 0x40021C00: "GPIOH"}
def word(va):
o = va - BASE
return struct.unpack_from("<I", IMG, o)[0] if 0 <= o <= len(IMG) - 4 else None
def litval(ins):
m = re.search(r"\[pc, #(?:0x)?([0-9a-fA-F]+)\]", ins.op_str)
if m and ins.mnemonic.startswith("ldr"):
return word(((ins.address + 4) & ~3) + int(m.group(1), 16))
return None
def main(ctx=6, only=None):
ins_list = list(md.disasm(IMG, BASE))
idx = {i.address: n for n, i in enumerate(ins_list)}
hits = []
for n, i in enumerate(ins_list):
v = litval(i)
if v in PORTS and (only is None or v == only):
hits.append((n, i, v))
print(f"{len(hits)} GPIO-base literal loads\n")
for n, i, v in hits:
print(f"=== {PORTS[v]} (0x{v:08X}) referenced at 0x{i.address:08X} ===")
for k in range(n, min(n + ctx, len(ins_list))):
j = ins_list[k]
extra = ""
lv = litval(j)
if lv is not None:
extra = f" ; =0x{lv:08X}"
print(f" 0x{j.address:08X}: {j.mnemonic:8} {j.op_str}{extra}")
print()
if __name__ == "__main__":
only = int(sys.argv[1], 16) if len(sys.argv) > 1 else None
main(only=only)