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,365 @@
|
||||
import serial
|
||||
import time
|
||||
import sys
|
||||
import re
|
||||
from .eprint import eprint
|
||||
from .a6commands import CPSFrame, h2p_command
|
||||
from .a6commands import ChanInfoFrame, h2p_command
|
||||
from .a6commands import ate_command
|
||||
from .a6commands import cps_command
|
||||
from .a6commands import read_uart_to_host
|
||||
from .rdadebug import RdaFrame
|
||||
from .rdadebug import read_word
|
||||
|
||||
class Singleton(object):
|
||||
def __new__(cls, *args, **kwargs):
|
||||
""" Singleton class
|
||||
|
||||
@param args: arguments
|
||||
@param kwargs: keyword arguments
|
||||
@return: object
|
||||
"""
|
||||
it = cls.__dict__.get("__it__")
|
||||
if it is not None:
|
||||
return it
|
||||
cls.__it__ = it = object.__new__(cls)
|
||||
it.init(*args, **kwargs)
|
||||
return it
|
||||
|
||||
def init(self, *args, **kwargs):
|
||||
"""
|
||||
"""
|
||||
pass
|
||||
|
||||
class SerialIO(Singleton):
|
||||
def init(self, port, baudrate=921600, verbosity=0, timeout=0.1):
|
||||
""" Initialize the serial port
|
||||
|
||||
@param port: serial port
|
||||
@param baudrate: baud rate
|
||||
@param verbosity: verbosity level
|
||||
"""
|
||||
self.port = port
|
||||
self.sio = serial.Serial(port, baudrate,
|
||||
serial.EIGHTBITS, serial.PARITY_NONE, serial.STOPBITS_ONE,
|
||||
xonxoff=True, rtscts=False, timeout=timeout)
|
||||
self.verbosity = verbosity
|
||||
self._ate_cps_addr = 0
|
||||
self._ate_cps_resp_addr = 0
|
||||
self._ate_cps_resp_length_addr = 0
|
||||
self._uart_resp_addr = 0
|
||||
self.sio.flush()
|
||||
if verbosity > 0:
|
||||
eprint("SerialIO: {} initialized".format(self.port))
|
||||
|
||||
def __del__(self):
|
||||
""" Close the serial port
|
||||
"""
|
||||
self.sio.close()
|
||||
|
||||
def write(self, msg):
|
||||
""" Write a message to the serial port
|
||||
|
||||
@param msg: message
|
||||
"""
|
||||
if self.verbosity > 0:
|
||||
eprint("write : ", msg.hex())
|
||||
self.sio.write(msg)
|
||||
|
||||
def read(self, nbytes):
|
||||
""" Read nbytes from the serial port
|
||||
|
||||
@param nbytes: number of bytes
|
||||
@return: message
|
||||
"""
|
||||
data = self.sio.read(nbytes)
|
||||
if self.verbosity > 0:
|
||||
eprint("read : ", data.hex())
|
||||
return data
|
||||
|
||||
def flush(self):
|
||||
""" Flush the serial port
|
||||
"""
|
||||
self.sio.flush()
|
||||
|
||||
@property
|
||||
def in_waiting(self):
|
||||
""" return the number of bytes in the serial port
|
||||
"""
|
||||
return self.sio.in_waiting
|
||||
|
||||
@property
|
||||
def ate_cps_addr(self):
|
||||
""" return the address of the ate command
|
||||
"""
|
||||
if self._ate_cps_addr == 0:
|
||||
self._ate_cps_addr = fetch_memory_address(0x81c00270)
|
||||
self._ate_cps_addr = int.from_bytes(self._ate_cps_addr, byteorder='little')
|
||||
return self._ate_cps_addr
|
||||
|
||||
@property
|
||||
def ate_cps_resp_addr(self):
|
||||
""" return the address of the ate command response
|
||||
"""
|
||||
if self._ate_cps_resp_addr == 0:
|
||||
self._ate_cps_resp_addr = fetch_memory_address(0x81c00264)
|
||||
self._ate_cps_resp_addr = int.from_bytes(self._ate_cps_resp_addr, byteorder='little')
|
||||
return self._ate_cps_resp_addr
|
||||
|
||||
@property
|
||||
def ate_cps_resp_length_addr(self):
|
||||
""" return the address of the ate command response
|
||||
"""
|
||||
return self.ate_cps_resp_addr - 4
|
||||
|
||||
@property
|
||||
def uart_resp_addr(self):
|
||||
""" return the address of the ate command response
|
||||
"""
|
||||
if self._uart_resp_addr == 0:
|
||||
self._uart_resp_addr = fetch_memory_address(0x81c0026c)
|
||||
self._uart_resp_addr = int.from_bytes(self._uart_resp_addr, byteorder='little')
|
||||
return self._uart_resp_addr
|
||||
|
||||
|
||||
|
||||
def write_flush_pause(msg, sleep = 0.07):
|
||||
""" Write out to serial and wait for the radio to process the command
|
||||
|
||||
@param msg: bytes to write
|
||||
@param sleep: time to sleep after writing in ms
|
||||
|
||||
"""
|
||||
uart = SerialIO()
|
||||
uart.write(msg)
|
||||
uart.flush()
|
||||
time.sleep(0.07)
|
||||
|
||||
|
||||
def send_ate_command(msg):
|
||||
""" Send a command to the ATE/CPS function on the radio
|
||||
|
||||
To send a command to the ATE or CPS software on the radio, it has
|
||||
to be surrounded by these h2p commands which clear the registers
|
||||
and then throw and interupt which causes the command to be
|
||||
executed
|
||||
|
||||
@param msg: bytes to write
|
||||
|
||||
"""
|
||||
uart = SerialIO()
|
||||
write_flush_pause(h2p_command(0))
|
||||
write_flush_pause(ate_command(msg, uart.ate_cps_addr))
|
||||
write_flush_pause(h2p_command(0xa5))
|
||||
|
||||
def send_cps_command(msg):
|
||||
""" Send a command to the ATE/CPS function on the radio
|
||||
|
||||
To send a command to the ATE or CPS software on the radio, it has
|
||||
to be surrounded by these h2p commands which clear the registers
|
||||
and then throw and interupt which causes the command to be
|
||||
executed
|
||||
|
||||
@param msg: bytes to write
|
||||
|
||||
"""
|
||||
|
||||
uart = SerialIO()
|
||||
write_flush_pause(h2p_command(0))
|
||||
write_flush_pause(cps_command(msg, uart.ate_cps_addr))
|
||||
write_flush_pause(h2p_command(0xa5))
|
||||
|
||||
def wait_on_read(retries=256, delay=0):
|
||||
""" Wait until a read happens
|
||||
|
||||
This function waits until something is received from the serial or
|
||||
will abort after a certain number of retries.
|
||||
|
||||
@param retries: number of retries before aborting
|
||||
@param delay: time to sleep between retries
|
||||
"""
|
||||
|
||||
uart = SerialIO()
|
||||
size = uart.in_waiting
|
||||
countdown = retries
|
||||
while size == 0 and countdown > 0:
|
||||
size = uart.in_waiting
|
||||
countdown -= 1
|
||||
if delay: time.sleep(delay)
|
||||
if countdown == 0:
|
||||
# nothing received
|
||||
return b''
|
||||
if size > 0:
|
||||
data = uart.read(size)
|
||||
return data
|
||||
|
||||
def send_uart_setup():
|
||||
""" Replays the initial UART setup sequence
|
||||
|
||||
This sequence and timing is from the CPS software capture.
|
||||
"""
|
||||
uart = SerialIO()
|
||||
knock_worked = False
|
||||
retries = 25
|
||||
while not knock_worked and retries > 0:
|
||||
uart.write(read_uart_to_host())
|
||||
uart.flush()
|
||||
time.sleep(0.001)
|
||||
data = wait_on_read()
|
||||
response = RdaFrame(data)
|
||||
if response.seq == 1 and response.content == b'\x80':
|
||||
knock_worked = True
|
||||
else:
|
||||
time.sleep(0.25)
|
||||
retries -= 1
|
||||
return knock_worked
|
||||
|
||||
def fetch_memory_address(addr, seq=1):
|
||||
""" Attempt to read a memory address and keep trying until it succeeds
|
||||
|
||||
"""
|
||||
uart = SerialIO()
|
||||
read_ok = False
|
||||
retval = b''
|
||||
retries = 25
|
||||
while not read_ok:
|
||||
frame = read_word(addr, seq)
|
||||
uart.write(frame)
|
||||
uart.flush()
|
||||
size = uart.in_waiting
|
||||
i = retries
|
||||
while size == 0 and i > 0:
|
||||
time.sleep(0.001)
|
||||
size = uart.in_waiting
|
||||
i -= 1
|
||||
if retries == 0:
|
||||
continue
|
||||
data = uart.read(size)
|
||||
inbound_frame = RdaFrame(data)
|
||||
read_ok = inbound_frame.seq == seq and not inbound_frame.check_fail
|
||||
retval = inbound_frame.content
|
||||
return retval
|
||||
|
||||
def atecps_resp_read():
|
||||
""" Read the response from an ATECPS command
|
||||
|
||||
@return: response from ATECPS command
|
||||
|
||||
"""
|
||||
uart = SerialIO()
|
||||
length = fetch_memory_address(uart.ate_cps_resp_length_addr)
|
||||
length = int.from_bytes(length, 'little')
|
||||
response = read_mem_range(uart.ate_cps_resp_addr, uart.ate_cps_resp_addr + length)
|
||||
return response
|
||||
|
||||
def uart_resp_read():
|
||||
""" Read the response from an ATECPS command
|
||||
|
||||
@return: response from ATECPS command
|
||||
|
||||
"""
|
||||
uart = SerialIO()
|
||||
length = fetch_memory_address(uart.uart_resp_addr)
|
||||
length = length[1]
|
||||
response = read_mem_range(uart.uart_resp_addr, uart.uart_resp_addr + length)
|
||||
return response
|
||||
|
||||
def read_mem_range(begin, end):
|
||||
""" Read a memory range
|
||||
|
||||
@param begin: start address
|
||||
@param end: end address
|
||||
@return: the data in bytes
|
||||
|
||||
"""
|
||||
addr = begin
|
||||
datalist = []
|
||||
while addr < end:
|
||||
data = fetch_memory_address(addr)
|
||||
if len(data) == 4:
|
||||
datalist.append(data)
|
||||
addr = addr + 4
|
||||
return b''.join(datalist)
|
||||
|
||||
def read_mem_burst(sio, begin, end, offset=0, verbosity=0):
|
||||
""" Read a limited memory range using a burst read
|
||||
|
||||
@param sio: serial object
|
||||
@param begin: start address
|
||||
@param end: end address
|
||||
@param offset: offset of the sequence number
|
||||
@param verbosity: verbosity level
|
||||
@return: the data in bytes
|
||||
"""
|
||||
|
||||
if end - begin > 0x100:
|
||||
raise ValueError('burst read only supports ranges of less than 256 bytes')
|
||||
# the burst is in words of 4 bytes
|
||||
burst = (end - begin) / 4
|
||||
# preallocate the lists
|
||||
recvflags = [False] * burst
|
||||
recvdata = [0] * burst
|
||||
i = 0
|
||||
data = b''
|
||||
while sum(recvflags) != burst:
|
||||
while i < burst:
|
||||
if not recvflags[i]:
|
||||
sio.write(read_word(begin + 4 * i, i + offset + 1))
|
||||
i += 1
|
||||
size = sio.in_waiting
|
||||
if size > 0:
|
||||
data += sio.read(size)
|
||||
i = 0
|
||||
|
||||
return b''.join(recvdata)
|
||||
|
||||
def get_chan_info(channel = 0):
|
||||
""" Get the channel info
|
||||
|
||||
@param channel: channel number
|
||||
@return: the channel info
|
||||
"""
|
||||
cmd = bytes([0, 0x12]) + channel.to_bytes(1, 'little')
|
||||
send_cps_command(cmd)
|
||||
resp = uart_resp_read()
|
||||
print(ChanInfoFrame(resp))
|
||||
# sys.stdout.buffer.write(resp)
|
||||
|
||||
def get_freq_err():
|
||||
""" Get the frequency error from the Radio
|
||||
|
||||
@return: frequency error in Hz
|
||||
"""
|
||||
send_ate_command("AT+DMOCONNECT")
|
||||
send_ate_command("AT+GETFREQERR")
|
||||
resp = atecps_resp_read()
|
||||
resp = resp.split(b'\x00')
|
||||
resp = [x.decode('utf-8') for x in resp]
|
||||
return parse_freq_err_resp(resp[0])
|
||||
|
||||
def parse_freq_err_resp(resp):
|
||||
""" Parse the frequency error response
|
||||
|
||||
@param resp: response from ATECPS
|
||||
@return: frequency error in Hz
|
||||
"""
|
||||
pattern = '\[(.+)\]'
|
||||
freqerr = re.search(pattern, resp)
|
||||
if freqerr:
|
||||
return int(freqerr.group(1))
|
||||
else:
|
||||
return 0
|
||||
|
||||
def set_freq_err(freqerr):
|
||||
""" Set the frequency error on the Radio
|
||||
|
||||
@param freqerr: frequency error parameter which is (-2500 + 10 * freqerr) in Hz
|
||||
|
||||
"""
|
||||
send_ate_command("AT+DMOCONNECT")
|
||||
send_ate_command("AT+DMOFREQERR={}".format(freqerr))
|
||||
resp = atecps_resp_read()
|
||||
resp = resp.split(b'\x00')
|
||||
resp = [x.decode('utf-8') for x in resp]
|
||||
for line in resp:
|
||||
print(line)
|
||||
Ссылка в новой задаче
Block a user