Главное: - Опознан 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).
250 строки
7.9 KiB
Python
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()
|