Files
viktor 6d49ba3cba 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).
2026-07-24 02:52:51 +09:00

250 строки
7.9 KiB
Python

#!/usr/bin/env python3
"""Интерактивный агент в прошивке: управление FM100B через COM-порт с ПК, без перешивки.
Протокол (ПК -> рация по USART6):
0xAA <len> <len байт «сырого» FM100B-кадра>
агент блитит эти len байт в USART3 (модулю). Ответы модуля идут обратно в порт
через снифер-хук on_frame (кадр: F1 F2 <len> <кадр>). Так я с ПК шлю любой кадр
и сразу вижу реакцию — перебор команд (AMBE, record и пр.) вживую.
Приём с порта — по байту за тик в главном цикле (не блокирует UI, в отличие от зонда).
CPS одновременно не подключать. Основа rt4d_ru_batt.bin, откат — она же.
python3 build_agent.py
python3 tools/agent.py send 25 01 01 # спросить версию модуля
python3 tools/agent.py raw 68 25 01 01 00 00 00 00 10
"""
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_agent.bin"
ON_FRAME = 0x08006348
FRAME_CALL = 0x08018DD8
UI_HOOK = 0x08020806
TICK_FUNC = 0x0801DFD0
TX_BYTE = 0x08018BFC # usart6_tx_byte(u8)
USART3_TXBUF = 0x08006C9C # usart3_tx_buf(buf,len)
# приём CPS идёт через USART6-ISR, который складывает байты в это кольцо.
# Читаем кольцо по своему указателю — порт/ISR не трогаем, CPS не ломаем.
RING_BUF = 0x200082EF # 4K кольцо RX (маска 0xFFF)
RING_HEAD = 0x20000C60 # u32, растущий индекс записи (из ISR)
MAGIC_ADDR = 0x2000B000
MAGIC_VAL = 0x41474E54 # 'AGNT'
ST_STATE = 0x2000B040 # 0=ждём AA, 1=ждём len, 2=собираем
ST_LEN = 0x2000B041
ST_IDX = 0x2000B042
ST_BUF = 0x2000B044 # до 64 байт -> 0x2000B084
MY_TAIL = 0x2000B088 # u32, наш указатель чтения кольца
EQ, NE, HS, LS, LO = 0, 1, 2, 9, 3
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:
def __init__(self, ks, base): self.ks=ks; self.base=base; self.items=[]
def asm(self,c): self.items.append(("a",c))
def label(self,n): self.items.append(("l",n))
def b(self,t,cond=None): self.items.append(("b",cond,t))
def _sz(self,it):
if it[0]=="a": return len(bytes(self.ks.asm(it[1],0)[0]))
if it[0]=="b": return 2
return 0
def build(self):
pos={}; off=0
for it in self.items:
if it[0]=="l": pos[it[1]]=off
off+=self._sz(it)
out=bytearray(); off=0
for it in self.items:
if it[0]=="a": out+=bytes(self.ks.asm(it[1],self.base+off)[0])
elif it[0]=="b":
_,cond,tgt=it; d=(self.base+pos[tgt])-(self.base+off+4)
out+= struct.pack("<H",0xE000|((d>>1)&0x7FF)) if cond is None \
else struct.pack("<H",0xD000|(cond<<8)|((d>>1)&0xFF))
off+=self._sz(it)
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)
ks=Ks(KS_ARCH_ARM,KS_MODE_THUMB)
def lo(a): return a&0xFFFF
def hi(a): return (a>>16)&0xFFFF
# ---- agent_poll: читать кольцо RX по своему указателю ----
# r7 = my_tail (u32). Пока my_tail != head — брать ring[my_tail & 0xFFF].
poll_at=code_at
a=Asm(ks,poll_at)
a.asm(f"""
push {{r4, r5, r6, r7, lr}}
movw r7, #{lo(MY_TAIL)}
movt r7, #{hi(MY_TAIL)}
ldr r7, [r7]
""")
a.label("ploop")
# head?
a.asm(f"""
movw r0, #{lo(RING_HEAD)}
movt r0, #{hi(RING_HEAD)}
ldr r0, [r0]
cmp r7, r0
""")
a.b("pret", EQ)
# byte = ring[my_tail & 0xFFF]; my_tail++
a.asm(f"""
movw r0, #{lo(RING_BUF)}
movt r0, #{hi(RING_BUF)}
ubfx r1, r7, #0, #12
ldrb r6, [r0, r1]
adds r7, #1
movw r4, #{lo(ST_STATE)}
movt r4, #{hi(ST_STATE)}
ldrb r5, [r4]
cmp r5, #0
""")
a.b("s_wait_aa", EQ)
a.asm("cmp r5, #1")
a.b("s_len", EQ)
# state 2: собираем в буфер
a.asm(f"""
movw r0, #{lo(ST_IDX)}
movt r0, #{hi(ST_IDX)}
ldrb r1, [r0]
movw r2, #{lo(ST_BUF)}
movt r2, #{hi(ST_BUF)}
strb r6, [r2, r1]
adds r1, #1
strb r1, [r0]
movw r0, #{lo(ST_LEN)}
movt r0, #{hi(ST_LEN)}
ldrb r0, [r0]
cmp r1, r0
""")
a.b("ploop", LO) # idx<len? продолжаем; когда idx==len — отправляем
# idx==len -> отправить в USART3
a.asm(f"""
movw r0, #{lo(ST_BUF)}
movt r0, #{hi(ST_BUF)}
movw r1, #{lo(ST_LEN)}
movt r1, #{hi(ST_LEN)}
ldrb r1, [r1]
bl #{USART3_TXBUF}
movs r0, #0
strb r0, [r4]
""")
a.b("ploop")
# state 0: ждём 0xAA
a.label("s_wait_aa")
a.asm("cmp r6, #0xAA")
a.b("ploop", NE)
a.asm("movs r0, #1\nstrb r0, [r4]")
a.b("ploop")
# state 1: принять len
a.label("s_len")
a.asm(f"""
movw r0, #{lo(ST_LEN)}
movt r0, #{hi(ST_LEN)}
strb r6, [r0]
movw r0, #{lo(ST_IDX)}
movt r0, #{hi(ST_IDX)}
movs r1, #0
strb r1, [r0]
movs r0, #2
strb r0, [r4]
""")
a.b("ploop")
a.label("pret")
a.asm(f"""
movw r0, #{lo(MY_TAIL)}
movt r0, #{hi(MY_TAIL)}
str r7, [r0]
pop {{r4, r5, r6, r7, pc}}
""")
poll=a.build()
# ---- tick_router: init магии один раз + agent_poll каждый тик ----
tick_at=poll_at+len(poll)
r=Asm(ks,tick_at)
r.asm(f"""
push {{lr}}
bl #{TICK_FUNC}
movw r0, #{lo(MAGIC_ADDR)}
movt r0, #{hi(MAGIC_ADDR)}
ldr r1, [r0]
movw r2, #{lo(MAGIC_VAL)}
movt r2, #{hi(MAGIC_VAL)}
cmp r1, r2
""")
r.b("inited", EQ)
r.asm(f"""
str r2, [r0]
movw r0, #{lo(ST_STATE)}
movt r0, #{hi(ST_STATE)}
movs r1, #0
strb r1, [r0]
movw r0, #{lo(RING_HEAD)}
movt r0, #{hi(RING_HEAD)}
ldr r1, [r0]
movw r0, #{lo(MY_TAIL)}
movt r0, #{hi(MY_TAIL)}
str r1, [r0]
""")
r.label("inited")
r.asm(f"bl #{poll_at | 1}\npop {{pc}}")
tick=r.build()
# ---- снифер on_frame ----
hook_at=tick_at+len(tick)
h=Asm(ks,hook_at)
h.asm(f"""
push {{r4, r5, r6, lr}}
mov r4, r0
ldrb r5, [r4, #6]
ldrb r6, [r4, #7]
orr.w r5, r6, r5, lsl #8
adds r5, #9
uxtb r5, r5
movs r0, #0xF1
bl #{TX_BYTE}
movs r0, #0xF2
bl #{TX_BYTE}
mov r0, r5
bl #{TX_BYTE}
movs r6, #0
""")
h.label("sloop")
h.asm("cmp r6, r5")
h.b("sdone", HS)
h.asm(f"ldrb r0, [r4, r6]\nbl #{TX_BYTE}\nadds r6, #1")
h.b("sloop")
h.label("sdone")
h.asm(f"mov r0, r4\nbl #{ON_FRAME}\npop {{r4, r5, r6, pc}}")
hook=h.build()
code=poll+tick+hook
img+=code
while len(img)%4: img.append(0xFF)
print(f"poll 0x{poll_at:08X} ({len(poll)}Б), tick 0x{tick_at:08X}, hook 0x{hook_at:08X}")
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)
open(OUT,"wb").write(bytes(img))
print(f"готово: {OUT}, {len(img)} Б")
if __name__=="__main__":
main()