mirror of
https://github.com/infiniteCable2/panda.git
synced 2026-02-18 17:23:52 +08:00
Merge branch 'upstream/panda/master' into sync-20251114
This commit is contained in:
@@ -23,7 +23,7 @@ __version__ = '0.0.10'
|
||||
CANPACKET_HEAD_SIZE = 0x6
|
||||
DLC_TO_LEN = [0, 1, 2, 3, 4, 5, 6, 7, 8, 12, 16, 20, 24, 32, 48, 64]
|
||||
LEN_TO_DLC = {length: dlc for (dlc, length) in enumerate(DLC_TO_LEN)}
|
||||
PANDA_BUS_CNT = 3
|
||||
PANDA_CAN_CNT = 3
|
||||
|
||||
|
||||
def calculate_checksum(data):
|
||||
@@ -32,16 +32,13 @@ def calculate_checksum(data):
|
||||
res ^= b
|
||||
return res
|
||||
|
||||
def pack_can_buffer(arr, fd=False):
|
||||
snds = [b'']
|
||||
def pack_can_buffer(arr, chunk=False, fd=False):
|
||||
snds = [bytearray(), ]
|
||||
for address, dat, bus in arr:
|
||||
assert len(dat) in LEN_TO_DLC
|
||||
#logger.debug(" W 0x%x: 0x%s", address, dat.hex())
|
||||
|
||||
extended = 1 if address >= 0x800 else 0
|
||||
data_len_code = LEN_TO_DLC[len(dat)]
|
||||
header = bytearray(CANPACKET_HEAD_SIZE)
|
||||
word_4b = address << 3 | extended << 2
|
||||
word_4b = (address << 3) | (extended << 2)
|
||||
header[0] = (data_len_code << 4) | (bus << 1) | int(fd)
|
||||
header[1] = word_4b & 0xFF
|
||||
header[2] = (word_4b >> 8) & 0xFF
|
||||
@@ -49,9 +46,10 @@ def pack_can_buffer(arr, fd=False):
|
||||
header[4] = (word_4b >> 24) & 0xFF
|
||||
header[5] = calculate_checksum(header[:5] + dat)
|
||||
|
||||
snds[-1] += header + dat
|
||||
if len(snds[-1]) > 256: # Limit chunks to 256 bytes
|
||||
snds.append(b'')
|
||||
snds[-1].extend(header)
|
||||
snds[-1].extend(dat)
|
||||
if chunk and len(snds[-1]) > 256:
|
||||
snds.append(bytearray())
|
||||
|
||||
return snds
|
||||
|
||||
@@ -134,7 +132,7 @@ class Panda:
|
||||
|
||||
MAX_FAN_RPMs = {
|
||||
HW_TYPE_TRES: 6600,
|
||||
HW_TYPE_CUATRO: 12500,
|
||||
HW_TYPE_CUATRO: 5000,
|
||||
}
|
||||
|
||||
HARNESS_STATUS_NC = 0
|
||||
@@ -199,9 +197,9 @@ class Panda:
|
||||
self._handle = None
|
||||
while self._handle is None:
|
||||
# try USB first, then SPI
|
||||
self._context, self._handle, serial, self.bootstub, bcd = self.usb_connect(self._connect_serial, claim=claim, no_error=wait)
|
||||
self._context, self._handle, serial, self.bootstub = self.usb_connect(self._connect_serial, claim=claim, no_error=wait)
|
||||
if self._handle is None:
|
||||
self._context, self._handle, serial, self.bootstub, bcd = self.spi_connect(self._connect_serial)
|
||||
self._context, self._handle, serial, self.bootstub = self.spi_connect(self._connect_serial)
|
||||
if not wait:
|
||||
break
|
||||
|
||||
@@ -228,11 +226,11 @@ class Panda:
|
||||
self.can_reset_communications()
|
||||
|
||||
# disable automatic CAN-FD switching
|
||||
for bus in range(PANDA_BUS_CNT):
|
||||
for bus in range(PANDA_CAN_CNT):
|
||||
self.set_canfd_auto(bus, False)
|
||||
|
||||
# set CAN speed
|
||||
for bus in range(PANDA_BUS_CNT):
|
||||
for bus in range(PANDA_CAN_CNT):
|
||||
self.set_can_speed_kbps(bus, self._can_speed_kbps)
|
||||
|
||||
@property
|
||||
@@ -241,49 +239,33 @@ class Panda:
|
||||
|
||||
@classmethod
|
||||
def spi_connect(cls, serial, ignore_version=False):
|
||||
# get UID to confirm slave is present and up
|
||||
handle = None
|
||||
spi_serial = None
|
||||
bootstub = None
|
||||
spi_version = None
|
||||
try:
|
||||
handle = PandaSpiHandle()
|
||||
|
||||
# connect by protcol version
|
||||
try:
|
||||
dat = handle.get_protocol_version()
|
||||
spi_serial = binascii.hexlify(dat[:12]).decode()
|
||||
pid = dat[13]
|
||||
if pid not in (0xcc, 0xee):
|
||||
raise PandaSpiException("invalid bootstub status")
|
||||
bootstub = pid == 0xee
|
||||
spi_version = dat[14]
|
||||
except PandaSpiException:
|
||||
# fallback, we'll raise a protocol mismatch below
|
||||
dat = handle.controlRead(Panda.REQUEST_IN, 0xc3, 0, 0, 12, timeout=100)
|
||||
spi_serial = binascii.hexlify(dat).decode()
|
||||
bootstub = Panda.flasher_present(handle)
|
||||
spi_version = 0
|
||||
dat = handle.get_protocol_version()
|
||||
except PandaSpiException:
|
||||
pass
|
||||
return None, None, None, False
|
||||
|
||||
# no connection or wrong panda
|
||||
if None in (spi_serial, bootstub) or (serial is not None and (spi_serial != serial)):
|
||||
handle = None
|
||||
spi_serial = None
|
||||
bootstub = False
|
||||
spi_serial = binascii.hexlify(dat[:12]).decode()
|
||||
pid = dat[13]
|
||||
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 handle is not None and not ignore_version:
|
||||
if spi_version != handle.PROTOCOL_VERSION:
|
||||
err = f"panda protocol mismatch: expected {handle.PROTOCOL_VERSION}, got {spi_version}. reflash panda"
|
||||
raise PandaProtocolMismatch(err)
|
||||
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")
|
||||
|
||||
return None, handle, spi_serial, bootstub, None
|
||||
# got a device and all good
|
||||
return None, handle, spi_serial, bootstub
|
||||
|
||||
@classmethod
|
||||
def usb_connect(cls, serial, claim=True, no_error=False):
|
||||
handle, usb_serial, bootstub, bcd = None, None, None, None
|
||||
handle, usb_serial, bootstub = None, None, None
|
||||
context = usb1.USBContext()
|
||||
context.open()
|
||||
try:
|
||||
@@ -309,11 +291,6 @@ class Panda:
|
||||
handle.claimInterface(0)
|
||||
# handle.setInterfaceAltSetting(0, 0) # Issue in USB stack
|
||||
|
||||
# bcdDevice wasn't always set to the hw type, ignore if it's the old constant
|
||||
this_bcd = device.getbcdDevice()
|
||||
if this_bcd is not None and this_bcd != 0x2300:
|
||||
bcd = bytearray([this_bcd >> 8, ])
|
||||
|
||||
break
|
||||
except Exception:
|
||||
logger.exception("USB connect error")
|
||||
@@ -324,7 +301,7 @@ class Panda:
|
||||
else:
|
||||
context.close()
|
||||
|
||||
return context, usb_handle, usb_serial, bootstub, bcd
|
||||
return context, usb_handle, usb_serial, bootstub
|
||||
|
||||
def is_connected_spi(self):
|
||||
return isinstance(self._handle, PandaSpiHandle)
|
||||
@@ -360,7 +337,7 @@ class Panda:
|
||||
|
||||
@classmethod
|
||||
def spi_list(cls):
|
||||
_, _, serial, _, _ = cls.spi_connect(None, ignore_version=True)
|
||||
_, _, serial, _ = cls.spi_connect(None, ignore_version=True)
|
||||
if serial is not None:
|
||||
return [serial, ]
|
||||
return []
|
||||
@@ -732,7 +709,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, fd=fd)
|
||||
snds = pack_can_buffer(arr, chunk=(not self.spi), fd=fd)
|
||||
for tx in snds:
|
||||
while len(tx) > 0:
|
||||
bs = self._handle.bulkWrite(3, tx, timeout=timeout)
|
||||
|
||||
133
python/spi.py
133
python/spi.py
@@ -1,5 +1,4 @@
|
||||
import binascii
|
||||
import ctypes
|
||||
import os
|
||||
import fcntl
|
||||
import math
|
||||
@@ -8,7 +7,6 @@ import struct
|
||||
import threading
|
||||
from contextlib import contextmanager
|
||||
from functools import reduce
|
||||
from collections.abc import Callable
|
||||
|
||||
from .base import BaseHandle, BaseSTBootloaderHandle, TIMEOUT
|
||||
from .constants import McuType, MCU_TYPE_BY_IDCODE, USBPACKET_MAX_SIZE
|
||||
@@ -18,6 +16,10 @@ try:
|
||||
import spidev
|
||||
except ImportError:
|
||||
spidev = None
|
||||
try:
|
||||
import spidev2
|
||||
except ImportError:
|
||||
spidev2 = None
|
||||
|
||||
# Constants
|
||||
SYNC = 0x5A
|
||||
@@ -29,7 +31,8 @@ CHECKSUM_START = 0xAB
|
||||
MIN_ACK_TIMEOUT_MS = 100
|
||||
MAX_XFER_RETRY_COUNT = 5
|
||||
|
||||
XFER_SIZE = 0x40*31
|
||||
SPI_BUF_SIZE = 4096 # from panda/board/drivers/spi.h
|
||||
XFER_SIZE = SPI_BUF_SIZE - 0x40 # give some room for SPI protocol overhead
|
||||
|
||||
DEV_PATH = "/dev/spidev0.0"
|
||||
|
||||
@@ -70,18 +73,6 @@ class PandaSpiTransferFailed(PandaSpiException):
|
||||
pass
|
||||
|
||||
|
||||
class PandaSpiTransfer(ctypes.Structure):
|
||||
_fields_ = [
|
||||
('rx_buf', ctypes.c_uint64),
|
||||
('tx_buf', ctypes.c_uint64),
|
||||
('tx_length', ctypes.c_uint32),
|
||||
('rx_length_max', ctypes.c_uint32),
|
||||
('timeout', ctypes.c_uint32),
|
||||
('endpoint', ctypes.c_uint8),
|
||||
('expect_disconnect', ctypes.c_uint8),
|
||||
]
|
||||
|
||||
|
||||
SPI_LOCK = threading.Lock()
|
||||
SPI_DEVICES = {}
|
||||
class SpiDevice:
|
||||
@@ -89,9 +80,7 @@ class SpiDevice:
|
||||
Provides locked, thread-safe access to a panda's SPI interface.
|
||||
"""
|
||||
|
||||
# 50MHz is the max of the 845. older rev comma three
|
||||
# may not support the full 50MHz
|
||||
MAX_SPEED = 50000000
|
||||
MAX_SPEED = 50000000 # max of the SDM845
|
||||
|
||||
def __init__(self, speed=MAX_SPEED):
|
||||
assert speed <= self.MAX_SPEED
|
||||
@@ -128,24 +117,12 @@ class PandaSpiHandle(BaseHandle):
|
||||
"""
|
||||
|
||||
PROTOCOL_VERSION = 2
|
||||
HEADER = struct.Struct("<BBHH")
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.dev = SpiDevice()
|
||||
|
||||
self._transfer_raw: Callable[[SpiDevice, int, bytes, int, int, bool], bytes] = self._transfer_spidev
|
||||
|
||||
if "KERN" in os.environ:
|
||||
self._transfer_raw = self._transfer_kernel_driver
|
||||
|
||||
self.tx_buf = bytearray(1024)
|
||||
self.rx_buf = bytearray(1024)
|
||||
tx_buf_raw = ctypes.c_char.from_buffer(self.tx_buf)
|
||||
rx_buf_raw = ctypes.c_char.from_buffer(self.rx_buf)
|
||||
|
||||
self.ioctl_data = PandaSpiTransfer()
|
||||
self.ioctl_data.tx_buf = ctypes.addressof(tx_buf_raw)
|
||||
self.ioctl_data.rx_buf = ctypes.addressof(rx_buf_raw)
|
||||
self.fileno = self.dev._spidev.fileno()
|
||||
if spidev2 is not None and "SPI2" in os.environ:
|
||||
self._spi2 = spidev2.SPIBus("/dev/spidev0.0", "w+b", bits_per_word=8, speed_hz=50_000_000)
|
||||
|
||||
# helpers
|
||||
def _calc_checksum(self, data: bytes) -> int:
|
||||
@@ -160,10 +137,10 @@ class PandaSpiHandle(BaseHandle):
|
||||
start = time.monotonic()
|
||||
while (timeout == 0) or ((time.monotonic() - start) < timeout_s):
|
||||
dat = spi.xfer2([tx, ] * length)
|
||||
if dat[0] == NACK:
|
||||
raise PandaSpiNackResponse
|
||||
elif dat[0] == ack_val:
|
||||
if dat[0] == ack_val:
|
||||
return bytes(dat)
|
||||
elif dat[0] == NACK:
|
||||
raise PandaSpiNackResponse
|
||||
|
||||
raise PandaSpiMissingAck
|
||||
|
||||
@@ -171,7 +148,7 @@ class PandaSpiHandle(BaseHandle):
|
||||
max_rx_len = max(USBPACKET_MAX_SIZE, max_rx_len)
|
||||
|
||||
logger.debug("- send header")
|
||||
packet = struct.pack("<BBHH", SYNC, endpoint, len(data), max_rx_len)
|
||||
packet = self.HEADER.pack(SYNC, endpoint, len(data), max_rx_len)
|
||||
packet += bytes([self._calc_checksum(packet), ])
|
||||
spi.xfer2(packet)
|
||||
|
||||
@@ -200,29 +177,57 @@ class PandaSpiHandle(BaseHandle):
|
||||
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_kernel_driver(self, spi, endpoint: int, data, timeout: int, max_rx_len: int = 1000, expect_disconnect: bool = False) -> bytes:
|
||||
import spidev2
|
||||
self.tx_buf[:len(data)] = data
|
||||
self.ioctl_data.endpoint = endpoint
|
||||
self.ioctl_data.tx_length = len(data)
|
||||
self.ioctl_data.rx_length_max = max_rx_len
|
||||
self.ioctl_data.expect_disconnect = int(expect_disconnect)
|
||||
def _transfer_spidev2(self, spi, endpoint: int, data, timeout: int, max_rx_len: int = USBPACKET_MAX_SIZE, expect_disconnect: bool = False) -> bytes:
|
||||
max_rx_len = max(USBPACKET_MAX_SIZE, max_rx_len)
|
||||
|
||||
# TODO: use our own ioctl request
|
||||
try:
|
||||
ret = fcntl.ioctl(self.fileno, spidev2.SPI_IOC_RD_LSB_FIRST, self.ioctl_data)
|
||||
except OSError as e:
|
||||
raise PandaSpiException from e
|
||||
if ret < 0:
|
||||
raise PandaSpiException(f"ioctl returned {ret}")
|
||||
return bytes(self.rx_buf[:ret])
|
||||
header = self.HEADER.pack(SYNC, endpoint, len(data), max_rx_len)
|
||||
|
||||
header_ack = bytearray(1)
|
||||
|
||||
# ACK + <2 bytes for response length> + data + checksum
|
||||
data_rx = bytearray(3+max_rx_len+1)
|
||||
|
||||
self._spi2.submitTransferList(spidev2.SPITransferList((
|
||||
# header
|
||||
{'tx_buf': header + bytes([self._calc_checksum(header), ]), 'delay_usecs': 0, 'cs_change': True},
|
||||
{'rx_buf': header_ack, 'delay_usecs': 0, 'cs_change': True},
|
||||
|
||||
# send data
|
||||
{'tx_buf': bytes([*data, self._calc_checksum(data)]), 'delay_usecs': 0, 'cs_change': True},
|
||||
{'rx_buf': data_rx, 'delay_usecs': 0, 'cs_change': True},
|
||||
)))
|
||||
|
||||
if header_ack[0] != HACK:
|
||||
raise PandaSpiMissingAck
|
||||
|
||||
if expect_disconnect:
|
||||
logger.debug("- expecting disconnect, returning")
|
||||
return b""
|
||||
else:
|
||||
dat = bytes(data_rx)
|
||||
if dat[0] != DACK:
|
||||
if dat[0] == NACK:
|
||||
raise PandaSpiNackResponse
|
||||
|
||||
print("trying again")
|
||||
dat = self._wait_for_ack(spi, DACK, timeout, 0x13, length=3 + max_rx_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})")
|
||||
|
||||
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)
|
||||
@@ -236,11 +241,25 @@ class PandaSpiHandle(BaseHandle):
|
||||
logger.debug("\ntry #%d", n)
|
||||
with self.dev.acquire() as spi:
|
||||
try:
|
||||
return self._transfer_raw(spi, endpoint, data, timeout, max_rx_len, expect_disconnect)
|
||||
fn = self._transfer_spidev
|
||||
#fn = self._transfer_spidev2
|
||||
return fn(spi, endpoint, data, timeout, max_rx_len, expect_disconnect)
|
||||
except PandaSpiException as e:
|
||||
exc = e
|
||||
logger.debug("SPI transfer failed, retrying", exc_info=True)
|
||||
|
||||
# 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
|
||||
|
||||
raise exc
|
||||
|
||||
def get_protocol_version(self) -> bytes:
|
||||
@@ -290,8 +309,9 @@ class PandaSpiHandle(BaseHandle):
|
||||
return self._transfer(0, struct.pack("<BHHH", request, value, index, length), timeout, max_rx_len=length)
|
||||
|
||||
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, data[XFER_SIZE*x:XFER_SIZE*(x+1)], timeout)
|
||||
self._transfer(endpoint, mv[XFER_SIZE*x:XFER_SIZE*(x+1)], timeout)
|
||||
return len(data)
|
||||
|
||||
def bulkRead(self, endpoint: int, length: int, timeout: int = TIMEOUT) -> bytes:
|
||||
@@ -308,6 +328,9 @@ class STBootloaderSPIHandle(BaseSTBootloaderHandle):
|
||||
"""
|
||||
Implementation of the STM32 SPI bootloader protocol described in:
|
||||
https://www.st.com/resource/en/application_note/an4286-spi-protocol-used-in-the-stm32-bootloader-stmicroelectronics.pdf
|
||||
|
||||
NOTE: the bootloader's state machine is fragile and immediately gets into a bad state when
|
||||
sending any junk, e.g. when using the panda SPI protocol.
|
||||
"""
|
||||
|
||||
SYNC = 0x5A
|
||||
|
||||
Reference in New Issue
Block a user