Опознание БЕЗ разбора рации, по схеме RT4DDLT01 (Radtel v2.1): DMR-модуль «FM100B» = один чип U700 HF6853 — одночиповый DMR-SoC семейства Auctus A6 (внутри RDA8809): CPU + ВЧ-трансивер + DSP + AMBE-вокодер + аудио-кодек в одном кристалле, прошивка во внешнем SPI-flash, кварц 26 МГц. Прежняя версия «Sicomm SCT3258TD» неверна (SCT3258 — baseband-only без ВЧ; и нативный протокол другой). Скрытый ATE/CPS-интерфейс полностью реверснут (jhart99/a6tools, вендорен в research/a6tools). Даёт КОНКРЕТНУЮ починку нашей TX-проблемы: AT+GETFREQERR / AT+DMOFREQERR=N, offset=-2500+10*N, ±2500 Гц — наши ~1100 Гц внутри диапазона. Плюс DMR_ADJTXSYMDEV (девиация), FGU_AFC, DMOSETPOWER, CPS chanInfo, дамп прошивки модуля. - docs/hf6853-auctus-a6.md — идентификация + протокол + план TX-fix - tools/a6_freqfix.py — заготовка тула (probe/read/write, не запускать вслепую) - research/a6tools/ — вендоренный реверс (MIT), atcommands.md/cpecommands.md - docs/dmr-tx-recovery.md, README — путь 0 (AT+DMOFREQERR) как лучший - research/rt4ddlt01-dmr-module-sheet.png — рендер листа схемы DMR-модуля Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NikMGoqQWWk9wy2ww2vJAr
118 строки
5.9 KiB
Python
118 строки
5.9 KiB
Python
#!/usr/bin/env python3
|
|
"""RT-4D DMR-модуль (HF6853 / AUCTUS A6) — починка частотного смещения TX.
|
|
|
|
Модуль RT-4D — это одночиповый Auctus A6 (RDA8809), у которого частотная
|
|
калибровка TCXO хранится в NVRAM и правится СКРЫТЫМ ATE-интерфейсом:
|
|
AT+GETFREQERR -> прочитать N (в десятках Гц)
|
|
AT+DMOFREQERR=<N> -> записать N; offset[Гц] = -2500 + 10*N, N=0..500
|
|
После записи СМЕНИТЬ КАНАЛ на рации, чтобы применилось.
|
|
|
|
Наш случай ([[rt4d-dmr-tx-broken]]): TX смещена ~1100 Гц (в пределах ±2500),
|
|
поэтому один AT+DMOFREQERR должен вернуть декодирование хотспотом.
|
|
|
|
Реверс/библиотека — jhart99/a6tools (копия в research/a6tools). См.
|
|
docs/hf6853-auctus-a6.md.
|
|
|
|
БЕЗОПАСНОСТЬ:
|
|
* По умолчанию РЕЖИМ probe/read (--probe / --read). Запись только с --write И --yes.
|
|
* Точное значение требует ИЗМЕРЕНИЯ реальной TX по SDR (RTL-SDR).
|
|
* Рация боевая — вслепую не писать.
|
|
|
|
Транспорт: два варианта COM-доступа —
|
|
--port /dev/ttyUSBx прямой pyserial (если модуль виден напрямую)
|
|
--bridge через powershell-мост WSL (как tools/fm100b.py) — TODO,
|
|
заготовка ниже; сперва проверить probe напрямую.
|
|
|
|
Скорость Auctus по умолчанию 921600 8N1 XON/XOFF. Режим рации: «Обновление DMR
|
|
чипа» (вкл. с зажатой MENH/MENU) — прозрачный мост PC<->модуль.
|
|
|
|
Примеры:
|
|
python3 a6_freqfix.py --port /dev/ttyUSB0 --probe
|
|
python3 a6_freqfix.py --port /dev/ttyUSB0 --read
|
|
# измерили: рация реально шлёт 438.799100 вместо 438.800000 (=-900 Гц)
|
|
python3 a6_freqfix.py --port /dev/ttyUSB0 --measured 438799100 --nominal 438800000 --write --yes
|
|
"""
|
|
import sys, os, time, argparse
|
|
|
|
# использовать вендоренную библиотеку a6tools
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
sys.path.insert(0, os.path.join(HERE, "..", "research", "a6tools"))
|
|
|
|
|
|
def load_a6(port, baud, verbosity):
|
|
from a6.serialio import (SerialIO, send_ate_command, atecps_resp_read,
|
|
send_uart_setup, get_freq_err, set_freq_err)
|
|
from a6.a6commands import set_uart_to_host
|
|
from a6.serialio import write_flush_pause
|
|
SerialIO(port, baud, verbosity) # singleton init
|
|
return dict(send_ate=send_ate_command, resp=atecps_resp_read,
|
|
setup=send_uart_setup, geterr=get_freq_err, seterr=set_freq_err,
|
|
to_host=set_uart_to_host, wfp=write_flush_pause)
|
|
|
|
|
|
def probe(a6):
|
|
"""Проверить, отвечает ли модуль на RDA-debug (knock). content==0x80 => ок."""
|
|
print("[probe] knock RDA-debug (read reg 0x03)…")
|
|
ok = a6["setup"]()
|
|
print(" ответ:", "МОДУЛЬ ОТВЕЧАЕТ (debug активен)" if ok else "нет ответа")
|
|
if not ok:
|
|
print(" -> модуль не в RDA-debug. Проверь режим рации / скорость (пробуй --baud 115200).")
|
|
return ok
|
|
|
|
|
|
def read_err(a6):
|
|
a6["send_ate"]("AT+DMOCONNECT")
|
|
n = a6["geterr"]()
|
|
print(f"[read] GETFREQERR N (в получаемом виде) -> offset={n} Гц")
|
|
return n
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser(description="HF6853/A6 TX freq-offset fix")
|
|
ap.add_argument("--port", required=True, help="COM/tty (напр. /dev/ttyUSB0)")
|
|
ap.add_argument("--baud", type=int, default=921600)
|
|
ap.add_argument("-v", "--verbose", action="count", default=1)
|
|
ap.add_argument("--probe", action="store_true", help="только проверить связь")
|
|
ap.add_argument("--read", action="store_true", help="прочитать текущий freqerr")
|
|
ap.add_argument("--measured", type=int, help="реально измеренная TX-частота, Гц (по SDR)")
|
|
ap.add_argument("--nominal", type=int, help="номинальная (запрограммированная) частота, Гц")
|
|
ap.add_argument("--write", action="store_true", help="применить новое значение")
|
|
ap.add_argument("--yes", action="store_true", help="подтвердить запись (обязателен с --write)")
|
|
a = ap.parse_args()
|
|
|
|
a6 = load_a6(a.port, a.baud, a.verbose)
|
|
|
|
if not probe(a6):
|
|
if a.probe:
|
|
return
|
|
print("Прерываю: без связи писать нельзя.")
|
|
return
|
|
if a.probe:
|
|
return
|
|
|
|
cur = read_err(a6)
|
|
if a.read and not a.write:
|
|
return
|
|
|
|
if a.write:
|
|
if a.measured is None or a.nominal is None:
|
|
print("Для --write нужны --measured и --nominal (по SDR).")
|
|
return
|
|
delta = a.nominal - a.measured # сколько добавить к текущему offset
|
|
target = cur + delta
|
|
if abs(target) > 2500:
|
|
print(f"Целевой offset {target} Гц вне ±2500 — так фиксить нельзя (нужна другая калибровка).")
|
|
return
|
|
N = int((target + 2500) / 10)
|
|
print(f"[plan] cur={cur} Гц, delta={delta} Гц, target={target} Гц -> N={N}")
|
|
if not a.yes:
|
|
print("Добавь --yes чтобы записать. (ничего не записано)")
|
|
return
|
|
a6["send_ate"]("AT+DMOCONNECT")
|
|
a6["seterr"](N)
|
|
print(f"[write] AT+DMOFREQERR={N} отправлено. ТЕПЕРЬ СМЕНИ КАНАЛ на рации и проверь BER.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|