This commit is contained in:
1okko
2026-05-10 07:59:33 +08:00
parent 54e6124925
commit 5c0feefabd
482 changed files with 125673 additions and 6849 deletions
+58 -36
View File
@@ -1,23 +1,19 @@
import binascii
import ctypes
import os
import fcntl
import math
import time
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
from .utils import logger
# No fcntl on Windows
try:
import fcntl
except ImportError:
fcntl = None # type: ignore
# No spidev on MacOS/Windows
try:
import spidev
except ImportError:
@@ -33,8 +29,7 @@ CHECKSUM_START = 0xAB
MIN_ACK_TIMEOUT_MS = 100
MAX_XFER_RETRY_COUNT = 5
SPI_BUF_SIZE = 4096 # from panda/board/drivers/spi.h
XFER_SIZE = SPI_BUF_SIZE - 0x40 # give some room for SPI protocol overhead
XFER_SIZE = 0x40*31
DEV_PATH = "/dev/spidev0.0"
@@ -75,6 +70,18 @@ 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:
@@ -82,7 +89,9 @@ class SpiDevice:
Provides locked, thread-safe access to a panda's SPI interface.
"""
MAX_SPEED = 50000000 # max of the SDM845
# 50MHz is the max of the 845. older rev comma three
# may not support the full 50MHz
MAX_SPEED = 50000000
def __init__(self, speed=MAX_SPEED):
assert speed <= self.MAX_SPEED
@@ -119,11 +128,24 @@ class PandaSpiHandle(BaseHandle):
"""
PROTOCOL_VERSION = 2
HEADER = struct.Struct("<BBHH")
def __init__(self) -> None:
self.dev = SpiDevice()
self.no_retry = "NO_RETRY" in os.environ
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()
# helpers
def _calc_checksum(self, data: bytes) -> int:
@@ -138,10 +160,10 @@ class PandaSpiHandle(BaseHandle):
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:
if dat[0] == NACK:
raise PandaSpiNackResponse
elif dat[0] == ack_val:
return bytes(dat)
raise PandaSpiMissingAck
@@ -149,7 +171,7 @@ class PandaSpiHandle(BaseHandle):
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 = struct.pack("<BBHH", SYNC, endpoint, len(data), max_rx_len)
packet += bytes([self._calc_checksum(packet), ])
spi.xfer2(packet)
@@ -178,12 +200,30 @@ 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)
# 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])
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("==============================================")
@@ -196,24 +236,10 @@ class PandaSpiHandle(BaseHandle):
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)
return self._transfer_raw(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:
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
raise exc
@@ -264,9 +290,8 @@ 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, mv[XFER_SIZE*x:XFER_SIZE*(x+1)], timeout)
self._transfer(endpoint, data[XFER_SIZE*x:XFER_SIZE*(x+1)], timeout)
return len(data)
def bulkRead(self, endpoint: int, length: int, timeout: int = TIMEOUT) -> bytes:
@@ -283,9 +308,6 @@ 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