Files
rt-4d/tools/a6_freqfix.py
T
viktorиClaude Opus 4.8 c10ab3c6f1 DMR-чип опознан по схеме: HF6853 = AUCTUS A6 (RDA8809), не SCT3258
Опознание БЕЗ разбора рации, по схеме 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
2026-07-24 03:10:24 +09:00

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()