FM100B = Sicomm SCT3258TD (документирован); Talker Alias, агент, SPI-инструменты, восстановление TX
Главное: - Опознан DMR-чип: FM100B = Sicomm SCT3258TD. Датащит Packet Interface (126 стр) в docs/. Открывает калибровку/AMBE/вокодер по документации. Родня: UV-4R, китайские клоны. Реализовано: - Приём Talker Alias (cmd 0x60) — позывной крупно на экране (build_ta.py) - Интерактивный агент — управление FM100B по COM без перешивки (build_agent.py + agent.py) - Полная карта команд FM100B с живой рации (fm100b-commands.md) - Работа с SPI: чтение/запись/восстановление codeplug и калибровки (spi_read/spi_wr/spi_restore) - Конструктор дисплея + редактор прошивки (tools/display-editor) Документация: - docs/SCT3258_packet_interface.txt/.pdf — спека DMR-чипа - docs/fm100b-commands.md — карта команд - docs/dmr-tx-recovery.md — диагноз/восстановление DMR-передачи Инструменты: flash_ps.py (прошивка через WSL/powershell), sniff.py (снифер кадров), hotspot_rxoffset_sweep.py, apply_patch.py, build_* сборщики. Важно: DMR TX-калибровка сбита в экспериментах со SET-командами модулю; восстановление через offset-компенсацию / .updat Radtel / калибровку по прибору (docs/dmr-tx-recovery.md).
Этот коммит содержится в:
@@ -0,0 +1,292 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Talker Alias — приём: показ позывного вызывающего на экране (шаг 1 из 3).
|
||||
|
||||
Механика (подтверждена снифером на живом вызове):
|
||||
DMR-модуль присылает позывной кадром cmd 0x60:
|
||||
68 60 02 00 CK CK 00 LEN 02 01 <len> <ASCII-позывной> 00.. 10
|
||||
Сток payload этой команды выбрасывает (обработчик = nop; b default).
|
||||
|
||||
Наши патчи (поверх rt4d_ru_batt.bin — там уже вольтметр и роутер отрисовки):
|
||||
• ta_hook перехватывает fm100b_on_frame: на 0x60 копирует позывной в буфер RAM
|
||||
и ставит ta_valid; на 0x59 payload=0 (конец приёма) сбрасывает ta_valid;
|
||||
затем всегда вызывает штатный обработчик.
|
||||
• new_tick_router (последним за кадр): самоинициализация настроек по «магии»,
|
||||
затем на главном экране рисует вольтметр и — если включено и есть свежий
|
||||
позывной — сам позывной внизу экрана.
|
||||
|
||||
Буфер RAM 0x2000C7FC..: [magic4][en][valid][len][txt16] (свободный зазор SRAM).
|
||||
Настройка ta_en инициализируется в 1 при каждом включении (постоянное хранение —
|
||||
шаг 2, вместе с меню). Откат — rt4d_ru_batt.bin.
|
||||
"""
|
||||
import struct
|
||||
from keystone import Ks, KS_ARCH_ARM, KS_MODE_THUMB
|
||||
|
||||
BASE = 0x08002800
|
||||
SRC = "stock-fw/rt4d_ru_batt.bin"
|
||||
OUT = "stock-fw/rt4d_ta.bin"
|
||||
|
||||
DRAW_STRING = 0x08008A50 # void draw_string(page,x,s,len, mode@[sp])
|
||||
BIG_CHAR = 0x08008100 # void big_char(page,x,ch,mode) — крупный шрифт 11x24 частоты
|
||||
ON_FRAME = 0x08006348 # void fm100b_on_frame(u8 *frame)
|
||||
FRAME_CALL = 0x08018DD8 # единственный bl on_frame
|
||||
UI_HOOK = 0x08020806 # последний вызов отрисовки в ui_tick_normal
|
||||
TICK_FUNC = 0x0801DFD0 # оригинал этого вызова
|
||||
BATT_DRAW = 0x08029002 # уже в rt4d_ru_batt.bin
|
||||
G_SCREEN = 0x200008B3
|
||||
|
||||
# Буфер в окне [0x2000AE48 .. 0x2000B108): выше вершины стека (SP=0x2000AE48,
|
||||
# растёт вниз), ниже первого глобала (0x2000B108). Ничья 192-байтовая зона —
|
||||
# в отличие от прошлого 0x2000C7FC, который оказался внутри чужого буфера.
|
||||
MAGIC_ADDR = 0x2000B000
|
||||
MAGIC_VAL = 0x52543444 # 'RT4D'
|
||||
TA_EN = 0x2000B004
|
||||
TA_VAL = 0x2000B005
|
||||
TA_LEN = 0x2000B006
|
||||
TA_TXT = 0x2000B007 # 16 байт -> до 0x2000B017
|
||||
TA_PAGE = 3 # ниже ID, крупный шрифт 24px (страницы 3-5)
|
||||
|
||||
EQ, NE, HS, LS, GE = 0, 1, 2, 9, 10
|
||||
|
||||
|
||||
def thumb_bl(src, dst):
|
||||
off = (dst - (src + 4)) >> 1
|
||||
s = (off >> 23) & 1; i1 = (off >> 22) & 1; i2 = (off >> 21) & 1
|
||||
imm10 = (off >> 11) & 0x3FF; imm11 = off & 0x7FF
|
||||
j1 = (~i1 ^ s) & 1; j2 = (~i2 ^ s) & 1
|
||||
return struct.pack("<HH", 0xF000 | (s << 10) | imm10,
|
||||
0xD000 | (j1 << 13) | (j2 << 11) | imm11)
|
||||
|
||||
|
||||
class Asm:
|
||||
"""Мини-ассемблер с метками: keystone-чанки + ручные ветвления."""
|
||||
def __init__(self, ks, base):
|
||||
self.ks = ks; self.base = base; self.items = []
|
||||
def asm(self, code): self.items.append(("a", code))
|
||||
def label(self, name): self.items.append(("l", name))
|
||||
def b(self, target, cond=None): self.items.append(("b", cond, target))
|
||||
|
||||
def _sizes(self):
|
||||
sz = []
|
||||
for it in self.items:
|
||||
if it[0] == "a":
|
||||
sz.append(len(bytes(self.ks.asm(it[1], 0)[0])))
|
||||
elif it[0] == "b":
|
||||
sz.append(2)
|
||||
else:
|
||||
sz.append(0)
|
||||
return sz
|
||||
|
||||
def build(self):
|
||||
sz = self._sizes()
|
||||
pos = {}; off = 0
|
||||
for it, s in zip(self.items, sz):
|
||||
if it[0] == "l":
|
||||
pos[it[1]] = off
|
||||
off += s
|
||||
out = bytearray(); off = 0
|
||||
for it, s in zip(self.items, sz):
|
||||
if it[0] == "a":
|
||||
out += bytes(self.ks.asm(it[1], self.base + off)[0])
|
||||
elif it[0] == "b":
|
||||
_, cond, tgt = it
|
||||
frm = self.base + off
|
||||
d = (self.base + pos[tgt]) - (frm + 4)
|
||||
imm = (d >> 1) & 0xFF
|
||||
if cond is None:
|
||||
out += struct.pack("<H", 0xE000 | ((d >> 1) & 0x7FF))
|
||||
else:
|
||||
out += struct.pack("<H", 0xD000 | (cond << 8) | imm)
|
||||
off += s
|
||||
return bytes(out)
|
||||
|
||||
|
||||
def main():
|
||||
img = bytearray(open(SRC, "rb").read())
|
||||
end = BASE + len(img)
|
||||
code_at = (end + 3) & ~3
|
||||
img += b"\xff" * (code_at - end)
|
||||
print(f"основа {SRC}: {len(img)} Б; свой код с 0x{code_at:08X}")
|
||||
|
||||
# проверки точек патча
|
||||
assert bytes(img[FRAME_CALL - BASE: FRAME_CALL - BASE + 4]) == thumb_bl(FRAME_CALL, ON_FRAME), \
|
||||
"0x08018DD8 не вызов on_frame"
|
||||
assert bytes(img[UI_HOOK - BASE: UI_HOOK - BASE + 4])[0:1] != b"\xff", "UI hook пуст"
|
||||
print("точки перехвата на месте")
|
||||
|
||||
ks = Ks(KS_ARCH_ARM, KS_MODE_THUMB)
|
||||
|
||||
def hexlo(a): return a & 0xFFFF
|
||||
def hexhi(a): return (a >> 16) & 0xFFFF
|
||||
|
||||
# ---- ta_draw: позывной крупным шрифтом (11x24) по центру экрана ----
|
||||
# ширина символа 12px, высота 24px (страницы 2-4 = вертикальный центр).
|
||||
# x0 = (128 - len*12)/2 = 64 - len*6. Длину зажимаем к 10 (10*12=120<=128).
|
||||
ta_draw_at = code_at
|
||||
a = Asm(ks, ta_draw_at)
|
||||
a.asm(f"""
|
||||
push {{r4, r5, r6, r7, lr}}
|
||||
movw r4, #{hexlo(TA_LEN)}
|
||||
movt r4, #{hexhi(TA_LEN)}
|
||||
ldrb r4, [r4]
|
||||
cmp r4, #10
|
||||
""")
|
||||
a.b("len_ok", LS)
|
||||
a.asm("movs r4, #10")
|
||||
a.label("len_ok")
|
||||
a.asm(f"""
|
||||
movs r0, #6
|
||||
muls r0, r4, r0
|
||||
movs r5, #64
|
||||
subs r5, r5, r0
|
||||
movw r6, #{hexlo(TA_TXT)}
|
||||
movt r6, #{hexhi(TA_TXT)}
|
||||
movs r7, #0
|
||||
""")
|
||||
a.label("cloop")
|
||||
a.asm("cmp r7, r4")
|
||||
a.b("cdone", HS)
|
||||
a.asm(f"""
|
||||
ldrb r2, [r6, r7]
|
||||
mov r1, r5
|
||||
movs r0, #{TA_PAGE}
|
||||
movs r3, #0
|
||||
bl #{BIG_CHAR}
|
||||
adds r5, #12
|
||||
adds r7, #1
|
||||
""")
|
||||
a.b("cloop")
|
||||
a.label("cdone")
|
||||
a.asm("pop {r4, r5, r6, r7, pc}")
|
||||
ta_draw = a.build()
|
||||
|
||||
# ---- new_tick_router ----
|
||||
tick_at = ta_draw_at + len(ta_draw)
|
||||
r = Asm(ks, tick_at)
|
||||
r.asm(f"""
|
||||
push {{r4, lr}}
|
||||
bl #{TICK_FUNC}
|
||||
movw r0, #{hexlo(MAGIC_ADDR)}
|
||||
movt r0, #{hexhi(MAGIC_ADDR)}
|
||||
ldr r1, [r0]
|
||||
movw r2, #{hexlo(MAGIC_VAL)}
|
||||
movt r2, #{hexhi(MAGIC_VAL)}
|
||||
cmp r1, r2
|
||||
""")
|
||||
r.b("skipinit", EQ)
|
||||
r.asm(f"""
|
||||
str r2, [r0]
|
||||
movw r0, #{hexlo(TA_EN)}
|
||||
movt r0, #{hexhi(TA_EN)}
|
||||
movs r3, #1
|
||||
strb r3, [r0]
|
||||
movw r0, #{hexlo(TA_VAL)}
|
||||
movt r0, #{hexhi(TA_VAL)}
|
||||
movs r3, #0
|
||||
strb r3, [r0]
|
||||
""")
|
||||
r.label("skipinit")
|
||||
# вольтметр — только на главном экране (screen==0)
|
||||
r.asm(f"""
|
||||
movw r4, #{hexlo(G_SCREEN)}
|
||||
movt r4, #{hexhi(G_SCREEN)}
|
||||
ldrb r4, [r4]
|
||||
cmp r4, #0
|
||||
""")
|
||||
r.b("no_batt", NE)
|
||||
r.asm(f"bl #{BATT_DRAW}")
|
||||
r.label("no_batt")
|
||||
# ДИАГНОСТИКА: позывной рисуем на ЛЮБОМ экране, если есть свежий TA
|
||||
# (условие en временно убрано — сначала убедимся, что приём работает)
|
||||
r.asm(f"""
|
||||
movw r0, #{hexlo(TA_VAL)}
|
||||
movt r0, #{hexhi(TA_VAL)}
|
||||
ldrb r0, [r0]
|
||||
cmp r0, #0
|
||||
""")
|
||||
r.b("end", EQ)
|
||||
r.asm(f"bl #{ta_draw_at | 1}")
|
||||
r.label("end")
|
||||
r.asm("pop {r4, pc}")
|
||||
tick = r.build()
|
||||
|
||||
# ---- ta_hook ----
|
||||
hook_at = tick_at + len(tick)
|
||||
h = Asm(ks, hook_at)
|
||||
h.asm("""
|
||||
push {r4, r5, r6, lr}
|
||||
mov r4, r0
|
||||
ldrb r5, [r4, #1]
|
||||
cmp r5, #0x60
|
||||
""")
|
||||
h.b("check59", NE)
|
||||
# handle 0x60: len = frame[10], clamp 16
|
||||
h.asm("""
|
||||
ldrb r5, [r4, #10]
|
||||
cmp r5, #16
|
||||
""")
|
||||
h.b("len_ok", LS)
|
||||
h.asm("movs r5, #16")
|
||||
h.label("len_ok")
|
||||
h.asm("movs r6, #0")
|
||||
h.label("loop")
|
||||
h.asm("""
|
||||
cmp r6, r5
|
||||
""")
|
||||
h.b("afterloop", HS)
|
||||
h.asm(f"""
|
||||
adds r0, r4, #11
|
||||
ldrb r0, [r0, r6]
|
||||
movw r1, #{hexlo(TA_TXT)}
|
||||
movt r1, #{hexhi(TA_TXT)}
|
||||
strb r0, [r1, r6]
|
||||
adds r6, #1
|
||||
""")
|
||||
h.b("loop")
|
||||
h.label("afterloop")
|
||||
h.asm(f"""
|
||||
movw r1, #{hexlo(TA_LEN)}
|
||||
movt r1, #{hexhi(TA_LEN)}
|
||||
strb r5, [r1]
|
||||
movw r1, #{hexlo(TA_VAL)}
|
||||
movt r1, #{hexhi(TA_VAL)}
|
||||
movs r0, #1
|
||||
strb r0, [r1]
|
||||
""")
|
||||
h.b("pass")
|
||||
h.label("check59")
|
||||
# кадр 0x06 = начало (sub 0x60) или конец (sub 0x6f) вызова -> сброс позывного.
|
||||
# На начало сброс срабатывает ДО прихода 0x60 (TA), поэтому не мешает показу.
|
||||
h.asm("cmp r5, #0x06")
|
||||
h.b("pass", NE)
|
||||
h.asm(f"""
|
||||
movw r1, #{hexlo(TA_VAL)}
|
||||
movt r1, #{hexhi(TA_VAL)}
|
||||
movs r0, #0
|
||||
strb r0, [r1]
|
||||
""")
|
||||
h.label("pass")
|
||||
h.asm(f"""
|
||||
mov r0, r4
|
||||
bl #{ON_FRAME}
|
||||
pop {{r4, r5, r6, pc}}
|
||||
""")
|
||||
hook = h.build()
|
||||
|
||||
code = ta_draw + tick + hook
|
||||
img += code
|
||||
while len(img) % 4:
|
||||
img.append(0xFF)
|
||||
print(f"код: ta_draw 0x{ta_draw_at:08X} ({len(ta_draw)}Б), "
|
||||
f"tick 0x{tick_at:08X} ({len(tick)}Б), hook 0x{hook_at:08X} ({len(hook)}Б)")
|
||||
|
||||
# репоинты
|
||||
img[UI_HOOK - BASE: UI_HOOK - BASE + 4] = thumb_bl(UI_HOOK, tick_at)
|
||||
img[FRAME_CALL - BASE: FRAME_CALL - BASE + 4] = thumb_bl(FRAME_CALL, hook_at)
|
||||
print("UI-хук -> new_tick_router; on_frame -> ta_hook")
|
||||
|
||||
open(OUT, "wb").write(bytes(img))
|
||||
print(f"готово: {OUT}, {len(img)} Б")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Ссылка в новой задаче
Block a user