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
Этот коммит содержится в:
@@ -0,0 +1,117 @@
|
||||
#!/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()
|
||||
Ссылка в новой задаче
Block a user