mirror of
https://jihulab.com/mr-one/openpilot.git
synced 2026-10-28 05:23:46 +08:00
test: switch to upstream SPI protocol (stage 2a)
This commit is contained in:
@@ -148,6 +148,8 @@ class Panda:
|
||||
HEALTH_FLAG_HEARTBEAT_LOST = 1 << 4
|
||||
HEALTH_FLAG_SAFETY_RX_CHECKS_INVALID = 1 << 5
|
||||
HEALTH_FLAG_SOM_RESET_TRIGGERED = 1 << 6
|
||||
HEALTH_FLAG_NMI_RESET = 1 << 7
|
||||
HEALTH_FLAG_HARDFAULT_RESET = 1 << 8
|
||||
|
||||
H7_DEVICES = [HW_TYPE_RED_PANDA, HW_TYPE_TRES, HW_TYPE_CUATRO, HW_TYPE_BODY]
|
||||
SUPPORTED_DEVICES = H7_DEVICES
|
||||
@@ -251,10 +253,10 @@ class Panda:
|
||||
return isinstance(self._handle, PandaSpiHandle)
|
||||
|
||||
@classmethod
|
||||
def spi_connect(cls, serial, ignore_version=False):
|
||||
def spi_connect(cls, serial):
|
||||
try:
|
||||
handle = PandaSpiHandle()
|
||||
dat = handle.get_protocol_version()
|
||||
dat = handle.get_info()
|
||||
except PandaSpiException:
|
||||
return None, None, None, False
|
||||
|
||||
@@ -263,16 +265,11 @@ class Panda:
|
||||
if pid not in (0xcc, 0xee):
|
||||
raise PandaProtocolMismatch(f"invalid bootstub status ({pid=}). reflash panda")
|
||||
bootstub = pid == 0xee
|
||||
spi_version = dat[14]
|
||||
|
||||
# did we get the right panda?
|
||||
if serial is not None and spi_serial != serial:
|
||||
return None, None, None, False
|
||||
|
||||
# ensure our protocol version matches the panda
|
||||
if (not ignore_version) and spi_version != handle.PROTOCOL_VERSION:
|
||||
raise PandaProtocolMismatch(f"panda protocol mismatch: expected {handle.PROTOCOL_VERSION}, got {spi_version}. reflash panda")
|
||||
|
||||
# got a device and all good
|
||||
return None, handle, spi_serial, bootstub
|
||||
|
||||
@@ -350,7 +347,7 @@ class Panda:
|
||||
|
||||
@classmethod
|
||||
def spi_list(cls):
|
||||
_, _, serial, _ = cls.spi_connect(None, ignore_version=True)
|
||||
_, _, serial, _ = cls.spi_connect(None)
|
||||
if serial is not None:
|
||||
return [serial, ]
|
||||
return []
|
||||
@@ -551,6 +548,8 @@ class Panda:
|
||||
"sbu1_voltage_mV": a[17],
|
||||
"sbu2_voltage_mV": a[18],
|
||||
"som_reset_triggered": bool(flags & self.HEALTH_FLAG_SOM_RESET_TRIGGERED),
|
||||
"nmi_reset": bool(flags & self.HEALTH_FLAG_NMI_RESET),
|
||||
"hardfault_reset": bool(flags & self.HEALTH_FLAG_HARDFAULT_RESET),
|
||||
"sound_output_level": a[19],
|
||||
"temperature": a[20] - 40.0,
|
||||
"controls_allowed_lateral": a[21] & 1,
|
||||
@@ -720,7 +719,7 @@ class Panda:
|
||||
|
||||
@ensure_can_packet_version
|
||||
def can_send_many(self, arr, *, fd=False, timeout=CAN_SEND_TIMEOUT_MS):
|
||||
snds = pack_can_buffer(arr, chunk=(not self.spi), fd=fd)
|
||||
snds = pack_can_buffer(arr, chunk=True, fd=fd)
|
||||
for tx in snds:
|
||||
while len(tx) > 0:
|
||||
bs = self._handle.bulkWrite(3, tx, timeout=timeout)
|
||||
|
||||
+92
-150
@@ -8,7 +8,7 @@ from contextlib import contextmanager
|
||||
from functools import reduce
|
||||
|
||||
from .base import BaseHandle, BaseSTBootloaderHandle, TIMEOUT
|
||||
from .constants import McuType, MCU_TYPE_BY_IDCODE, USBPACKET_MAX_SIZE
|
||||
from .constants import McuType, MCU_TYPE_BY_IDCODE
|
||||
from .utils import logger
|
||||
|
||||
# No fcntl on Windows
|
||||
@@ -30,29 +30,23 @@ DACK = 0x85
|
||||
NACK = 0x1F
|
||||
CHECKSUM_START = 0xAB
|
||||
|
||||
MIN_ACK_TIMEOUT_MS = 100
|
||||
MAX_XFER_RETRY_COUNT = 5
|
||||
REQ_HEADER = struct.Struct("<BIHH") # endpoint, id, tx_len, rx_len
|
||||
RESP_HEADER = struct.Struct("<BIH") # status, id, len
|
||||
CRC = struct.Struct("<I")
|
||||
RESP_OVERHEAD = RESP_HEADER.size + CRC.size
|
||||
POLL = 0xFF # filler while reading the response, never a valid request
|
||||
|
||||
SPI_BUF_SIZE = 4096 # from panda/board/drivers/spi.h
|
||||
MIN_TIMEOUT_MS = 100
|
||||
SILENT_TIMEOUT = 1.0 # seconds, longer than the slowest request takes on the panda
|
||||
MAX_XFER_RETRY_COUNT = 5 # ST bootloader
|
||||
|
||||
SPI_BUF_SIZE = 4096 # from panda/board/drivers/drivers.h
|
||||
XFER_SIZE = SPI_BUF_SIZE - 0x40 # give some room for SPI protocol overhead
|
||||
FIRST_READ_SIZE = 1024 - RESP_OVERHEAD # reads are usually small, start small
|
||||
|
||||
DEV_PATH = "/dev/spidev0.0"
|
||||
|
||||
|
||||
def crc8(data):
|
||||
crc = 0xFF # standard init value
|
||||
poly = 0xD5 # standard crc8: x8+x7+x6+x4+x2+1
|
||||
size = len(data)
|
||||
for i in range(size - 1, -1, -1):
|
||||
crc ^= data[i]
|
||||
for _ in range(8):
|
||||
if ((crc & 0x80) != 0):
|
||||
crc = ((crc << 1) ^ poly) & 0xFF
|
||||
else:
|
||||
crc <<= 1
|
||||
return crc
|
||||
|
||||
|
||||
class PandaSpiException(Exception):
|
||||
pass
|
||||
|
||||
@@ -68,12 +62,6 @@ class PandaSpiNackResponse(PandaSpiException):
|
||||
class PandaSpiMissingAck(PandaSpiException):
|
||||
pass
|
||||
|
||||
class PandaSpiBadChecksum(PandaSpiException):
|
||||
pass
|
||||
|
||||
class PandaSpiTransferFailed(PandaSpiException):
|
||||
pass
|
||||
|
||||
|
||||
SPI_LOCK = threading.Lock()
|
||||
SPI_DEVICES = {}
|
||||
@@ -118,140 +106,93 @@ class PandaSpiHandle(BaseHandle):
|
||||
A class that mimics a libusb1 handle for panda SPI communications.
|
||||
"""
|
||||
|
||||
PROTOCOL_VERSION = 2
|
||||
HEADER = struct.Struct("<BBHH")
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.dev = SpiDevice()
|
||||
self.no_retry = "NO_RETRY" in os.environ
|
||||
|
||||
# helpers
|
||||
def _calc_checksum(self, data: bytes) -> int:
|
||||
cksum = CHECKSUM_START
|
||||
for b in data:
|
||||
cksum ^= b
|
||||
return cksum
|
||||
def _transfer_spidev(self, spi, endpoint: int, req_id: int, data, deadline: float, max_rx_len: int, expect_disconnect: bool) -> bytes | None:
|
||||
"""One request and its response. Retries until the panda got the request and we got its response, or the deadline.
|
||||
Returns None if the panda went quiet, so the caller can let go of the bus for a bit."""
|
||||
# the response is always read in one transfer of rx_len
|
||||
rx_len = max_rx_len + RESP_OVERHEAD
|
||||
packet = REQ_HEADER.pack(endpoint, req_id, len(data), rx_len) + bytes(data)
|
||||
packet += CRC.pack(binascii.crc32(packet))
|
||||
poll = bytes([POLL, ]) * rx_len
|
||||
|
||||
def _wait_for_ack(self, spi, ack_val: int, timeout: int, tx: int, length: int = 1) -> bytes:
|
||||
timeout_s = max(MIN_ACK_TIMEOUT_MS, timeout) * 1e-3
|
||||
# when the panda last showed it's there
|
||||
alive = time.monotonic()
|
||||
while True:
|
||||
# send the request, until the panda was ready for it
|
||||
if spi.xfer2(packet)[0] == SLAVE_READY:
|
||||
alive = time.monotonic()
|
||||
if expect_disconnect:
|
||||
return b""
|
||||
|
||||
start = time.monotonic()
|
||||
while (timeout == 0) or ((time.monotonic() - start) < timeout_s):
|
||||
dat = spi.xfer2([tx, ] * length)
|
||||
if dat[0] == ack_val:
|
||||
return bytes(dat)
|
||||
elif dat[0] == NACK:
|
||||
raise PandaSpiNackResponse
|
||||
|
||||
raise PandaSpiMissingAck
|
||||
|
||||
def _transfer_spidev(self, spi, endpoint: int, data, timeout: int, max_rx_len: int = 1000, expect_disconnect: bool = False) -> bytes:
|
||||
max_rx_len = max(USBPACKET_MAX_SIZE, max_rx_len)
|
||||
|
||||
logger.debug("- send header")
|
||||
packet = self.HEADER.pack(SYNC, endpoint, len(data), max_rx_len)
|
||||
packet += bytes([self._calc_checksum(packet), ])
|
||||
spi.xfer2(packet)
|
||||
|
||||
logger.debug("- waiting for header ACK")
|
||||
self._wait_for_ack(spi, HACK, MIN_ACK_TIMEOUT_MS, 0x11)
|
||||
|
||||
logger.debug("- sending data")
|
||||
packet = bytes([*data, self._calc_checksum(data)])
|
||||
spi.xfer2(packet)
|
||||
|
||||
if expect_disconnect:
|
||||
logger.debug("- expecting disconnect, returning")
|
||||
return b""
|
||||
else:
|
||||
logger.debug("- waiting for data ACK")
|
||||
preread_len = USBPACKET_MAX_SIZE + 1 # read enough for a controlRead
|
||||
dat = self._wait_for_ack(spi, DACK, timeout, 0x13, length=3 + preread_len)
|
||||
|
||||
# get response length, then response
|
||||
response_len = struct.unpack("<H", dat[1:3])[0]
|
||||
if response_len > max_rx_len:
|
||||
raise PandaSpiException(f"response length greater than max ({max_rx_len} {response_len})")
|
||||
|
||||
# read rest
|
||||
remaining = (response_len + 1) - preread_len
|
||||
if remaining > 0:
|
||||
dat += bytes(spi.readbytes(remaining))
|
||||
|
||||
dat = dat[:3 + response_len + 1]
|
||||
if self._calc_checksum(dat) != 0:
|
||||
raise PandaSpiBadChecksum
|
||||
|
||||
return dat[3:-1]
|
||||
|
||||
def _transfer(self, endpoint: int, data, timeout: int, max_rx_len: int = 1000, expect_disconnect: bool = False) -> bytes:
|
||||
logger.debug("starting transfer: endpoint=%d, max_rx_len=%d", endpoint, max_rx_len)
|
||||
logger.debug("==============================================")
|
||||
|
||||
n = 0
|
||||
start_time = time.monotonic()
|
||||
exc = PandaSpiException()
|
||||
while (timeout == 0) or (time.monotonic() - start_time) < timeout*1e-3:
|
||||
n += 1
|
||||
logger.debug("\ntry #%d", n)
|
||||
with self.dev.acquire() as spi:
|
||||
try:
|
||||
return self._transfer_spidev(spi, endpoint, data, timeout, max_rx_len, expect_disconnect)
|
||||
except PandaSpiException as e:
|
||||
exc = e
|
||||
logger.debug("SPI transfer failed, retrying", exc_info=True)
|
||||
if self.no_retry:
|
||||
# read until the response. if the panda is ready again, it never got the request
|
||||
while True:
|
||||
dat = spi.xfer(poll)
|
||||
if dat[0] == SLAVE_READY:
|
||||
alive = time.monotonic()
|
||||
break
|
||||
|
||||
# ensure slave is in a consistent state and ready for the next transfer
|
||||
# (e.g. slave TX buffer isn't stuck full)
|
||||
nack_cnt = 0
|
||||
attempts = 5
|
||||
while (nack_cnt <= 3) and (attempts > 0):
|
||||
attempts -= 1
|
||||
try:
|
||||
self._wait_for_ack(spi, NACK, MIN_ACK_TIMEOUT_MS, 0x11, length=XFER_SIZE//2)
|
||||
nack_cnt += 1
|
||||
except PandaSpiException:
|
||||
nack_cnt = 0
|
||||
# a response that got cut off or corrupted isn't one, keep reading
|
||||
status, resp_id, response_len = RESP_HEADER.unpack_from(dat)
|
||||
end = RESP_HEADER.size + response_len
|
||||
if status in (ACK, NACK) and response_len <= max_rx_len and CRC.unpack_from(dat, end)[0] == binascii.crc32(dat[:end]):
|
||||
# otherwise it's the response to an old request, and we send ours again
|
||||
if resp_id == req_id:
|
||||
if status == NACK:
|
||||
raise PandaSpiNackResponse
|
||||
return dat[RESP_HEADER.size:end]
|
||||
alive = time.monotonic()
|
||||
break
|
||||
|
||||
raise exc
|
||||
if time.monotonic() > min(deadline, alive + SILENT_TIMEOUT):
|
||||
break
|
||||
|
||||
def get_protocol_version(self) -> bytes:
|
||||
vers_str = b"VERSION"
|
||||
def _get_version(spi) -> bytes:
|
||||
spi.writebytes(vers_str)
|
||||
if time.monotonic() > deadline:
|
||||
raise PandaSpiMissingAck
|
||||
if time.monotonic() > alive + SILENT_TIMEOUT:
|
||||
return None
|
||||
|
||||
logger.debug("- waiting for echo")
|
||||
start = time.monotonic()
|
||||
while True:
|
||||
version_bytes = spi.readbytes(len(vers_str) + 2)
|
||||
if bytes(version_bytes).startswith(vers_str):
|
||||
break
|
||||
if (time.monotonic() - start) > 0.001:
|
||||
raise PandaSpiMissingAck
|
||||
def _transfer(self, endpoint: int, data, timeout: int, max_rx_len: int = 0, expect_disconnect: bool = False) -> bytes:
|
||||
logger.debug("transfer: endpoint=%d, max_rx_len=%d", endpoint, max_rx_len)
|
||||
|
||||
rlen = struct.unpack("<H", bytes(version_bytes[-2:]))[0]
|
||||
if rlen > 1000:
|
||||
raise PandaSpiException("response length greater than max")
|
||||
# timeout of 0 means forever
|
||||
deadline = time.monotonic() + (max(MIN_TIMEOUT_MS, timeout) * 1e-3 if timeout != 0 else math.inf)
|
||||
nack = None
|
||||
nack_count = 0
|
||||
# random, so it's never the same as another request the panda just handled, even from another client.
|
||||
# it stays the same when we send the request again, so the panda never runs it twice
|
||||
req_id = int.from_bytes(os.urandom(4), "little")
|
||||
while True:
|
||||
try:
|
||||
with self.dev.acquire() as spi:
|
||||
dat = self._transfer_spidev(spi, endpoint, req_id, data, deadline, max_rx_len, expect_disconnect)
|
||||
if dat is not None:
|
||||
return dat
|
||||
# the panda went quiet. let go of the bus for a bit, someone else might need it
|
||||
logger.warning("SPI: panda isn't responding")
|
||||
time.sleep(0.01)
|
||||
except PandaSpiNackResponse as e:
|
||||
# the panda NACKs while its CAN TX buffers are full, don't busy wait on it
|
||||
if time.monotonic() > deadline:
|
||||
raise
|
||||
# a new id, since the panda would answer the same id with the same NACK
|
||||
req_id = int.from_bytes(os.urandom(4), "little")
|
||||
nack = e
|
||||
nack_count += 1
|
||||
if nack_count > 3:
|
||||
time.sleep(min(max(nack_count * 10, 200), 2000) * 1e-6)
|
||||
except PandaSpiMissingAck:
|
||||
# only raised once the deadline passed. if the panda was refusing, that's why
|
||||
if nack is not None:
|
||||
raise nack from None
|
||||
raise
|
||||
|
||||
# get response
|
||||
dat = spi.readbytes(rlen + 1)
|
||||
resp = dat[:-1]
|
||||
calculated_crc = crc8(bytes(version_bytes + resp))
|
||||
if calculated_crc != dat[-1]:
|
||||
raise PandaSpiBadChecksum
|
||||
return bytes(resp)
|
||||
|
||||
exc = PandaSpiException()
|
||||
with self.dev.acquire() as spi:
|
||||
for _ in range(10):
|
||||
try:
|
||||
return _get_version(spi)
|
||||
except PandaSpiException as e:
|
||||
exc = e
|
||||
logger.debug("SPI get protocol version failed, retrying", exc_info=True)
|
||||
raise exc
|
||||
def get_info(self) -> bytes:
|
||||
# serial, HW type, bootstub
|
||||
return self._transfer(INFO_ENDPOINT, b"", MIN_TIMEOUT_MS, max_rx_len=14)
|
||||
|
||||
# libusb1 functions
|
||||
def close(self):
|
||||
@@ -265,16 +206,17 @@ class PandaSpiHandle(BaseHandle):
|
||||
|
||||
def bulkWrite(self, endpoint: int, data: bytes, timeout: int = TIMEOUT) -> int:
|
||||
mv = memoryview(data)
|
||||
for x in range(math.ceil(len(data) / XFER_SIZE)):
|
||||
self._transfer(endpoint, mv[XFER_SIZE*x:XFER_SIZE*(x+1)], timeout)
|
||||
for x in range(0, len(data), XFER_SIZE):
|
||||
self._transfer(endpoint, mv[x:x+XFER_SIZE], timeout)
|
||||
return len(data)
|
||||
|
||||
def bulkRead(self, endpoint: int, length: int, timeout: int = TIMEOUT) -> bytes:
|
||||
ret = b""
|
||||
for _ in range(math.ceil(length / XFER_SIZE)):
|
||||
d = self._transfer(endpoint, [], timeout, max_rx_len=XFER_SIZE)
|
||||
while len(ret) < length:
|
||||
to_read = min(XFER_SIZE if len(ret) else FIRST_READ_SIZE, length - len(ret))
|
||||
d = self._transfer(endpoint, b"", timeout, max_rx_len=to_read)
|
||||
ret += d
|
||||
if len(d) < XFER_SIZE:
|
||||
if len(d) < to_read:
|
||||
break
|
||||
return ret
|
||||
|
||||
|
||||
Reference in New Issue
Block a user