From 0ddd3f254f2f9778811ed85f3b83a19adeada835 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Wed, 14 Oct 2026 07:58:12 -0700 Subject: [PATCH] simple spi (#2461) (cherry picked from commit 835e4c31d9514f9d06c341479237c35349e75136) --- panda/board/crc.h | 31 +-- panda/board/drivers/drivers.h | 2 - panda/board/drivers/spi.h | 365 ++++++++++++++------------------- panda/board/stm32h7/llspi.h | 131 +++++------- panda/board/sys/power_saving.h | 6 + panda/python/__init__.py | 11 +- panda/python/jungle.py | 2 +- panda/python/spi.py | 253 +++++++++-------------- panda/scripts/spi_test.py | 2 - panda/tests/hitl/test_5_spi.py | 90 +++++--- 10 files changed, 391 insertions(+), 502 deletions(-) diff --git a/panda/board/crc.h b/panda/board/crc.h index 3e20fb0981..ce53984e9e 100644 --- a/panda/board/crc.h +++ b/panda/board/crc.h @@ -1,19 +1,22 @@ #pragma once -uint8_t crc_checksum(const uint8_t *dat, int len, const uint8_t poly) { - uint8_t crc = 0xFFU; - int i; - int j; - for (i = len - 1; i >= 0; i--) { - crc ^= dat[i]; - for (j = 0; j < 8; j++) { - if ((crc & 0x80U) != 0U) { - crc = (uint8_t)((crc << 1) ^ poly); - } - else { - crc <<= 1; - } +// standard CRC-32, same as zlib's crc32() +static uint32_t crc32_table[256]; + +void crc32_init(void) { + for (uint32_t i = 0U; i < 256U; i++) { + uint32_t c = i; + for (uint8_t j = 0U; j < 8U; j++) { + c = ((c & 1U) != 0U) ? (0xEDB88320U ^ (c >> 1U)) : (c >> 1U); } + crc32_table[i] = c; } - return crc; +} + +uint32_t crc32(const uint8_t *dat, uint32_t len) { + uint32_t crc = 0xFFFFFFFFU; + for (uint32_t i = 0U; i < len; i++) { + crc = crc32_table[(crc ^ dat[i]) & 0xFFU] ^ (crc >> 8U); + } + return crc ^ 0xFFFFFFFFU; } diff --git a/panda/board/drivers/drivers.h b/panda/board/drivers/drivers.h index 61fddae1aa..4b187a0e52 100644 --- a/panda/board/drivers/drivers.h +++ b/panda/board/drivers/drivers.h @@ -221,8 +221,6 @@ extern uint16_t spi_error_count; void can_tx_comms_resume_spi(void); void spi_init(void); -void spi_rx_done(void); -void spi_tx_done(bool reset); // ******************** debug ******************** bool debug_get_char(char *elem); diff --git a/panda/board/drivers/spi.h b/panda/board/drivers/spi.h index 08cbbf1313..ccbbac8464 100644 --- a/panda/board/drivers/spi.h +++ b/panda/board/drivers/spi.h @@ -6,240 +6,191 @@ #ifdef STM32H7 __attribute__((section(".sram12"))) uint8_t spi_buf_rx[SPI_BUF_SIZE]; __attribute__((section(".sram12"))) uint8_t spi_buf_tx[SPI_BUF_SIZE]; +__attribute__((section(".sram12"))) static uint8_t spi_buf_ready[1]; #else uint8_t spi_buf_rx[SPI_BUF_SIZE]; uint8_t spi_buf_tx[SPI_BUF_SIZE]; +static uint8_t spi_buf_ready[1]; #endif -#define SPI_CHECKSUM_START 0xABU -#define SPI_SYNC_BYTE 0x5AU -#define SPI_HACK 0x79U -#define SPI_DACK 0x85U -#define SPI_NACK 0x1FU +// SPI protocol, see python/spi.py for the host side. +// +// The host sends each request in one transfer, until the first byte it gets back is SPI_SLAVE_READY: +// MOSI: endpoint, id (u32), tx_len (u16), rx_len (u16), data[tx_len], crc32 (u32) +// then reads rx_len bytes at a time, until the first byte is a response: +// MISO: SPI_ACK or SPI_NACK, id (u32), len (u16), data[len], crc32 (u32) +// The id is random for each new request, and stays the same when the host sends it again. +// The response always fits in rx_len, so the next transfer reads all of it. +// If a read gets SPI_SLAVE_READY instead, the host sends the request again. +// Anything that isn't a valid request is dropped, and we're ready for the next one. +// NACK means a valid request that we refused, e.g. CAN TX buffers are full. +// +// We queue what's next on every CS rising edge, whatever state the bus is in. If a transfer +// doesn't cleanly see what we queued, the host ends up sending its request again, and a +// repeat of the request we last handled (same id) gets the same response again instead +// of running twice. So it never matters how long we take to get to it. +#define SPI_SLAVE_READY 0x3CU +#define SPI_ACK 0xC3U +#define SPI_NACK 0x5AU +#define SPI_IDLE 0x00U // sent on underrun -// SPI states -enum { - SPI_STATE_HEADER, - SPI_STATE_HEADER_ACK, - SPI_STATE_HEADER_NACK, - SPI_STATE_DATA_RX, - SPI_STATE_DATA_RX_ACK, - SPI_STATE_DATA_TX -}; +typedef struct { + uint8_t endpoint; + uint32_t id; + uint16_t tx_len; + uint16_t rx_len; +} __attribute__((packed)) spi_req_header_t; + +typedef struct { + uint8_t status; + uint32_t id; + uint16_t len; +} __attribute__((packed)) spi_resp_header_t; + +#define SPI_CRC_SIZE 4U +#define SPI_RESP_OVERHEAD (sizeof(spi_resp_header_t) + SPI_CRC_SIZE) + +#define SPI_ENDPOINT_INFO 0xF0U uint16_t spi_error_count = 0; -#define SPI_HEADER_SIZE 7U - // low level SPI prototypes void llspi_init(void); -void llspi_mosi_dma(uint8_t *addr, int len); -void llspi_miso_dma(const uint8_t *addr, int len); +void llspi_queue(const uint8_t *tx, uint16_t tx_len); +uint16_t llspi_rx_count(void); -static uint8_t spi_state = SPI_STATE_HEADER; -static uint16_t spi_data_len_mosi; static bool spi_can_tx_ready = false; -static const unsigned char version_text[] = "VERSION"; -static uint16_t spi_version_packet(uint8_t *out) { - // this protocol version request is a stable portion of - // the panda's SPI protocol. its contents match that of the - // panda USB descriptors and are sufficent to list/enumerate - // a panda, determine panda type, and bootstub status. +// Handle one endpoint. Returns whether to ACK, and writes the response data length +static bool spi_handle_endpoint(uint8_t endpoint, const uint8_t *data, uint16_t data_len, uint16_t max_resp_len, + uint8_t *resp, uint16_t *resp_len) { + bool ack = false; + *resp_len = 0U; - // the response is: - // VERSION + 2 byte data length + data + CRC8 + if (endpoint == 0U) { + if (data_len >= sizeof(ControlPacket_t)) { + ControlPacket_t ctrl = {0}; + (void)memcpy((uint8_t*)&ctrl, data, sizeof(ControlPacket_t)); + *resp_len = (uint16_t)comms_control_handler(&ctrl, resp); + ack = true; + } + } else if ((endpoint == 1U) || (endpoint == 0x81U)) { + *resp_len = (uint16_t)comms_can_read(resp, max_resp_len); + ack = true; + } else if (endpoint == 2U) { + comms_endpoint2_write(data, data_len); + ack = true; + } else if (endpoint == 3U) { + // NACK while the CAN TX buffers are full, the host retries + if (spi_can_tx_ready) { + spi_can_tx_ready = false; + comms_can_write(data, data_len); + ack = true; + } + } else if (endpoint == SPI_ENDPOINT_INFO) { + // serial, HW type, bootstub + (void)memcpy(resp, ((uint8_t *)UID_BASE), 12); + resp[12] = hw_type; + resp[13] = USB_PID & 0xFFU; + *resp_len = 14U; + ack = true; + } else if (endpoint == 0xABU) { + // test endpoint: mimics panda -> device transfer + *resp_len = max_resp_len; + ack = true; + } else { + // 0xAC is a test endpoint that always NACKs + } - // echo "VERSION" - (void)memcpy(out, version_text, 7); + return ack; +} - // write response - uint16_t data_len = 0; - uint16_t data_pos = 7U + 2U; +// Handle a request and build the response in spi_buf_tx. Returns the response size the host +// will read, or 0 if it's not a valid request +static uint16_t spi_process_request(uint16_t n) { + static bool handled = false; + static uint32_t handled_id = 0U; + static uint32_t handled_crc = 0U; - // write serial - (void)memcpy(&out[data_pos], ((uint8_t *)UID_BASE), 12); - data_len += 12U; + // n can include filler from the host's reads if we were slow, it's ignored + spi_req_header_t req; + (void)memcpy((uint8_t *)&req, spi_buf_rx, sizeof(req)); + uint32_t req_len = sizeof(req) + (uint32_t)req.tx_len + SPI_CRC_SIZE; + uint32_t crc = 0U; + if (req_len <= n) { + (void)memcpy((uint8_t *)&crc, &spi_buf_rx[req_len - SPI_CRC_SIZE], SPI_CRC_SIZE); + } + bool valid = (req_len <= n) && (crc32(spi_buf_rx, req_len - SPI_CRC_SIZE) == crc) && + (req.rx_len >= SPI_RESP_OVERHEAD) && (req.rx_len <= SPI_BUF_SIZE); - // HW type - out[data_pos + data_len] = hw_type; - data_len += 1U; + if (valid && !(handled && (req.id == handled_id) && (crc == handled_crc))) { + // a new request. a repeat of the last one (same id) gets the same response again + uint16_t max_data_len = req.rx_len - SPI_RESP_OVERHEAD; + uint16_t data_len = 0U; + bool ack = spi_handle_endpoint(req.endpoint, &spi_buf_rx[sizeof(req)], req.tx_len, max_data_len, + &spi_buf_tx[sizeof(spi_resp_header_t)], &data_len); + // like USB, a response is cut off at what the host reads + data_len = ack ? MIN(data_len, max_data_len) : 0U; + spi_resp_header_t resp = { + .status = ack ? SPI_ACK : SPI_NACK, + .id = req.id, + .len = data_len, + }; + (void)memcpy(spi_buf_tx, (uint8_t *)&resp, sizeof(resp)); + uint32_t len = sizeof(resp) + resp.len; + uint32_t resp_crc = crc32(spi_buf_tx, len); + (void)memcpy(&spi_buf_tx[len], (uint8_t *)&resp_crc, SPI_CRC_SIZE); + handled = true; + handled_id = req.id; + handled_crc = crc; + } else if (!valid && (req_len <= n)) { + // looked like a request, but the CRC is wrong. the host's filler doesn't count, its length doesn't fit + spi_error_count += 1U; + } else { + // a repeat, host's filler, or a partial request + } + return valid ? req.rx_len : 0U; +} - // bootstub - out[data_pos + data_len] = USB_PID & 0xFFU; - data_len += 1U; +// Called at the end of every transfer: CS is active low, so it rises when the host is done +void spi_cs_rising(void) { + // what we queued for the transfer that just ended + static bool response_queued = false; + static uint16_t resp_len = 0U; - // SPI protocol version - out[data_pos + data_len] = 0x2; - data_len += 1U; + // *** handle the transfer *** + bool send_response = response_queued; + uint16_t n = llspi_rx_count(); + if (n > 0U) { + if (response_queued) { + // the host read the response, so we're ready for the next request + send_response = false; + } else { + // the host sent a request, we respond if it's valid + resp_len = spi_process_request(n); + send_response = resp_len > 0U; + } + } - // data length - out[7] = data_len & 0xFFU; - out[8] = (data_len >> 8) & 0xFFU; - - // CRC8 - uint16_t resp_len = data_pos + data_len; - out[resp_len] = crc_checksum(out, resp_len, 0xD5U); - resp_len += 1U; - - return resp_len; + // *** setup next transfer with master *** + if (send_response) { + // send response to the master + llspi_queue(spi_buf_tx, resp_len); + response_queued = true; + } else { + // read from the master + llspi_queue(spi_buf_ready, 1U); + response_queued = false; + } } void spi_init(void) { - // platform init + crc32_init(); llspi_init(); - // Start the first packet! - spi_state = SPI_STATE_HEADER; - llspi_mosi_dma(spi_buf_rx, SPI_HEADER_SIZE); -} - -static bool validate_checksum(const uint8_t *data, uint16_t len) { - // TODO: can speed this up by casting the bulk to uint32_t and xor-ing the bytes afterwards - uint8_t checksum = SPI_CHECKSUM_START; - for(uint16_t i = 0U; i < len; i++){ - checksum ^= data[i]; - } - return checksum == 0U; -} - -void spi_rx_done(void) { - uint16_t response_len = 0U; - uint8_t next_rx_state = SPI_STATE_HEADER_NACK; - bool checksum_valid = false; - static uint8_t spi_endpoint; - static uint16_t spi_data_len_miso; - - // parse header - spi_endpoint = spi_buf_rx[1]; - spi_data_len_mosi = (spi_buf_rx[3] << 8) | spi_buf_rx[2]; - spi_data_len_miso = (spi_buf_rx[5] << 8) | spi_buf_rx[4]; - - if (memcmp(spi_buf_rx, version_text, 7) == 0) { - response_len = spi_version_packet(spi_buf_tx); - next_rx_state = SPI_STATE_HEADER_NACK;; - } else if (spi_state == SPI_STATE_HEADER) { - checksum_valid = validate_checksum(spi_buf_rx, SPI_HEADER_SIZE); - if ((spi_buf_rx[0] == SPI_SYNC_BYTE) && checksum_valid) { - // response: ACK and start receiving data portion - spi_buf_tx[0] = SPI_HACK; - next_rx_state = SPI_STATE_HEADER_ACK; - response_len = 1U; - } else { - // response: NACK and reset state machine - spi_buf_tx[0] = SPI_NACK; - next_rx_state = SPI_STATE_HEADER_NACK; - response_len = 1U; - } - } else if (spi_state == SPI_STATE_DATA_RX) { - // We got everything! Based on the endpoint specified, call the appropriate handler - bool response_ack = false; - checksum_valid = validate_checksum(&(spi_buf_rx[SPI_HEADER_SIZE]), spi_data_len_mosi + 1U); - if (checksum_valid) { - if (spi_endpoint == 0U) { - if (spi_data_len_mosi >= sizeof(ControlPacket_t)) { - ControlPacket_t ctrl = {0}; - (void)memcpy((uint8_t*)&ctrl, &spi_buf_rx[SPI_HEADER_SIZE], sizeof(ControlPacket_t)); - response_len = comms_control_handler(&ctrl, &spi_buf_tx[3]); - response_ack = true; - } else { - print("SPI: insufficient data for control handler\n"); - } - } else if ((spi_endpoint == 1U) || (spi_endpoint == 0x81U)) { - if (spi_data_len_mosi == 0U) { - response_len = comms_can_read(&(spi_buf_tx[3]), spi_data_len_miso); - response_ack = true; - } else { - print("SPI: did not expect data for can_read\n"); - } - } else if (spi_endpoint == 2U) { - comms_endpoint2_write(&spi_buf_rx[SPI_HEADER_SIZE], spi_data_len_mosi); - response_ack = true; - } else if (spi_endpoint == 3U) { - if (spi_data_len_mosi > 0U) { - if (spi_can_tx_ready) { - spi_can_tx_ready = false; - comms_can_write(&spi_buf_rx[SPI_HEADER_SIZE], spi_data_len_mosi); - response_ack = true; - } else { - response_ack = false; - print("SPI: CAN NACK\n"); - } - } else { - print("SPI: did expect data for can_write\n"); - } - } else if (spi_endpoint == 0xABU) { - // test endpoint: mimics panda -> device transfer - response_len = spi_data_len_miso; - response_ack = true; - } else if (spi_endpoint == 0xACU) { - // test endpoint: mimics device -> panda transfer (with NACK) - response_ack = false; - } else { - print("SPI: unexpected endpoint"); puth(spi_endpoint); print("\n"); - } - } else { - // Checksum was incorrect - response_ack = false; - } - - if (!response_ack) { - spi_buf_tx[0] = SPI_NACK; - next_rx_state = SPI_STATE_HEADER_NACK; - response_len = 1U; - } else { - // Setup response header - spi_buf_tx[0] = SPI_DACK; - spi_buf_tx[1] = response_len & 0xFFU; - spi_buf_tx[2] = (response_len >> 8) & 0xFFU; - - // Add checksum - uint8_t checksum = SPI_CHECKSUM_START; - for(uint16_t i = 0U; i < (response_len + 3U); i++) { - checksum ^= spi_buf_tx[i]; - } - spi_buf_tx[response_len + 3U] = checksum; - response_len += 4U; - - next_rx_state = SPI_STATE_DATA_TX; - } - } else { - print("SPI: RX unexpected state: "); puth(spi_state); print("\n"); - } - - // send out response - if (response_len == 0U) { - print("SPI: no response\n"); - spi_buf_tx[0] = SPI_NACK; - spi_state = SPI_STATE_HEADER_NACK; - response_len = 1U; - } - llspi_miso_dma(spi_buf_tx, response_len); - - spi_state = next_rx_state; - if (!checksum_valid) { - spi_error_count += 1U; - } -} - -void spi_tx_done(bool reset) { - if ((spi_state == SPI_STATE_HEADER_NACK) || reset) { - // Reset state - spi_state = SPI_STATE_HEADER; - llspi_mosi_dma(spi_buf_rx, SPI_HEADER_SIZE); - } else if (spi_state == SPI_STATE_HEADER_ACK) { - // ACK was sent, queue up the RX buf for the data + checksum - spi_state = SPI_STATE_DATA_RX; - llspi_mosi_dma(&spi_buf_rx[SPI_HEADER_SIZE], spi_data_len_mosi + 1U); - } else if (spi_state == SPI_STATE_DATA_TX) { - // Reset state - spi_state = SPI_STATE_HEADER; - llspi_mosi_dma(spi_buf_rx, SPI_HEADER_SIZE); - } else { - spi_state = SPI_STATE_HEADER; - llspi_mosi_dma(spi_buf_rx, SPI_HEADER_SIZE); - print("SPI: TX unexpected state: "); puth(spi_state); print("\n"); - } + // kick off the first read + spi_buf_ready[0] = SPI_SLAVE_READY; + llspi_queue(spi_buf_ready, 1U); } void can_tx_comms_resume_spi(void) { diff --git a/panda/board/stm32h7/llspi.h b/panda/board/stm32h7/llspi.h index dd28152528..650c62ccab 100644 --- a/panda/board/stm32h7/llspi.h +++ b/panda/board/stm32h7/llspi.h @@ -1,110 +1,85 @@ #pragma once -// master -> panda DMA start -// cppcheck-suppress constParameterPointer ; RX DMA writes through addr after return -void llspi_mosi_dma(uint8_t *addr, int len) { - // disable DMA + SPI - register_clear_bits(&(SPI4->CFG1), SPI_CFG1_RXDMAEN); - DMA2_Stream2->CR &= ~DMA_SxCR_EN; - register_clear_bits(&(SPI4->CR1), SPI_CR1_SPE); +// SPI4 NSS (chip select) is PE11 +#define SPI_CS_PIN 11U - // drain the bus +void spi_cs_rising(void); + +// Queues the next transfer: MOSI into spi_buf_rx, MISO from tx. A transfer that's already in progress is ignored, +// or at worst garbled, which the protocol recovers from +void llspi_queue(const uint8_t *tx, uint16_t tx_len) { + // stop DMA + SPI + register_clear_bits(&(SPI4->CR1), SPI_CR1_SPE); + DMA2_Stream2->CR &= ~DMA_SxCR_EN; + DMA2_Stream3->CR &= ~DMA_SxCR_EN; + register_clear_bits(&(SPI4->CFG1), SPI_CFG1_RXDMAEN | SPI_CFG1_TXDMAEN); + for (uint16_t i = 0U; (i < 1000U) && (((DMA2_Stream2->CR & DMA_SxCR_EN) != 0U) || ((DMA2_Stream3->CR & DMA_SxCR_EN) != 0U)); i++) {} + + // drain the RX FIFO and clear all flags while ((SPI4->SR & SPI_SR_RXP) != 0U) { volatile uint8_t dat = SPI4->RXDR; (void)dat; } + SPI4->IFCR = (0x1FFU << 3U); + DMA2->LIFCR = (0x3DUL << 16U) | (0x3DUL << 22U); // streams 2 + 3 - // clear all pending - SPI4->IFCR |= (0x1FFU << 3U); - register_set(&(SPI4->IER), 0, 0x3FFU); + // setup buffers + register_set(&(DMA2_Stream2->M0AR), (uint32_t)spi_buf_rx, 0xFFFFFFFFU); + DMA2_Stream2->NDTR = SPI_BUF_SIZE; + register_set(&(DMA2_Stream3->M0AR), (uint32_t)tx, 0xFFFFFFFFU); + DMA2_Stream3->NDTR = tx_len; - // setup destination and length - register_set(&(DMA2_Stream2->M0AR), (uint32_t)addr, 0xFFFFFFFFU); - DMA2_Stream2->NDTR = len; - - // enable DMA + SPI - DMA2_Stream2->CR |= DMA_SxCR_EN; + // enable, in the order from the reference manual register_set_bits(&(SPI4->CFG1), SPI_CFG1_RXDMAEN); - register_set_bits(&(SPI4->CR1), SPI_CR1_SPE); -} - -// panda -> master DMA start -void llspi_miso_dma(const uint8_t *addr, int len) { - // disable DMA + SPI - DMA2_Stream3->CR &= ~DMA_SxCR_EN; - register_clear_bits(&(SPI4->CFG1), SPI_CFG1_TXDMAEN); - register_clear_bits(&(SPI4->CR1), SPI_CR1_SPE); - - // setup source and length - register_set(&(DMA2_Stream3->M0AR), (uint32_t)addr, 0xFFFFFFFFU); - DMA2_Stream3->NDTR = len; - - // clear under-run while we were reading - SPI4->IFCR |= (0x1FFU << 3U); - - // setup interrupt on TXC - register_set(&(SPI4->IER), (1U << SPI_IER_EOTIE_Pos), 0x3FFU); - - // enable DMA + SPI - register_set_bits(&(SPI4->CFG1), SPI_CFG1_TXDMAEN); + DMA2_Stream2->CR |= DMA_SxCR_EN; DMA2_Stream3->CR |= DMA_SxCR_EN; + register_set_bits(&(SPI4->CFG1), SPI_CFG1_TXDMAEN); register_set_bits(&(SPI4->CR1), SPI_CR1_SPE); } -static bool spi_tx_dma_done = false; -// master -> panda DMA finished -static void DMA2_Stream2_IRQ_Handler(void) { - // Clear interrupt flag - DMA2->LIFCR = DMA_LIFCR_CTCIF2; - - spi_rx_done(); +// Number of bytes received since we last queued, saturates at SPI_BUF_SIZE +uint16_t llspi_rx_count(void) { + // let the DMA empty the FIFO, unless it's already full + for (uint16_t i = 0U; (i < 1000U) && ((SPI4->SR & SPI_SR_RXP) != 0U) && (DMA2_Stream2->NDTR != 0U); i++) {} + return SPI_BUF_SIZE - DMA2_Stream2->NDTR; } -// panda -> master DMA finished -static void DMA2_Stream3_IRQ_Handler(void) { - ENTER_CRITICAL(); - - DMA2->LIFCR = DMA_LIFCR_CTCIF3; - spi_tx_dma_done = true; - - EXIT_CRITICAL(); -} - -// panda TX finished -static void SPI4_IRQ_Handler(void) { - // clear flag - SPI4->IFCR |= (0x1FFU << 3U); - - if (spi_tx_dma_done && ((SPI4->SR & SPI_SR_TXC) != 0U)) { - spi_tx_dma_done = false; - spi_tx_done(false); +// CS rising edge, the end of a transfer +static void EXTI15_10_IRQ_Handler(void) { + if ((EXTI->PR1 & (1UL << SPI_CS_PIN)) != 0U) { + // clear first, so an edge during processing fires again + EXTI->PR1 = (1UL << SPI_CS_PIN); + // if we're late and the next transfer already started, its own rising edge handles it + if ((GPIOE->IDR & (1UL << SPI_CS_PIN)) != 0U) { + spi_cs_rising(); + } } } - void llspi_init(void) { - REGISTER_INTERRUPT(SPI4_IRQn, SPI4_IRQ_Handler, (SPI_IRQ_RATE * 2U), FAULT_INTERRUPT_RATE_SPI) - REGISTER_INTERRUPT(DMA2_Stream2_IRQn, DMA2_Stream2_IRQ_Handler, SPI_IRQ_RATE, FAULT_INTERRUPT_RATE_SPI_DMA) - REGISTER_INTERRUPT(DMA2_Stream3_IRQn, DMA2_Stream3_IRQ_Handler, SPI_IRQ_RATE, FAULT_INTERRUPT_RATE_SPI_DMA) + // one interrupt per transfer, on CS going high + REGISTER_INTERRUPT(EXTI15_10_IRQn, EXTI15_10_IRQ_Handler, (SPI_IRQ_RATE * 2U), FAULT_INTERRUPT_RATE_SPI) - // Setup MOSI DMA + // MOSI DMA, peripheral -> memory register_set(&(DMAMUX1_Channel10->CCR), 83U, 0xFFFFFFFFU); - register_set(&(DMA2_Stream2->CR), (DMA_SxCR_MINC | DMA_SxCR_TCIE), 0x1E077EFEU); + register_set(&(DMA2_Stream2->CR), DMA_SxCR_MINC, 0x1E077EFEU); register_set(&(DMA2_Stream2->PAR), (uint32_t)&(SPI4->RXDR), 0xFFFFFFFFU); - // Setup MISO DMA, memory -> peripheral + // MISO DMA, memory -> peripheral register_set(&(DMAMUX1_Channel11->CCR), 84U, 0xFFFFFFFFU); - register_set(&(DMA2_Stream3->CR), (DMA_SxCR_MINC | DMA_SxCR_DIR_0 | DMA_SxCR_TCIE), 0x1E077EFEU); + register_set(&(DMA2_Stream3->CR), (DMA_SxCR_MINC | DMA_SxCR_DIR_0), 0x1E077EFEU); register_set(&(DMA2_Stream3->PAR), (uint32_t)&(SPI4->TXDR), 0xFFFFFFFFU); - // Enable SPI + // SPI: 8-bit slave, underrun sends UDRDR register_set(&(SPI4->IER), 0, 0x3FFU); register_set(&(SPI4->CFG1), (7U << SPI_CFG1_DSIZE_Pos), SPI_CFG1_DSIZE_Msk); - register_set(&(SPI4->UDRDR), 0xcd, 0xFFFFU); // set under-run value for debugging - register_set(&(SPI4->CR1), SPI_CR1_SPE, 0xFFFFU); + register_set(&(SPI4->UDRDR), SPI_IDLE, 0xFFFFU); register_set(&(SPI4->CR2), 0, 0xFFFFU); - NVIC_EnableIRQ(DMA2_Stream2_IRQn); - NVIC_EnableIRQ(DMA2_Stream3_IRQn); - NVIC_EnableIRQ(SPI4_IRQn); + // CS rising edge interrupt + register_set(&(SYSCFG->EXTICR[2]), SYSCFG_EXTICR3_EXTI11_PE, SYSCFG_EXTICR3_EXTI11); + register_set_bits(&(EXTI->RTSR1), (1UL << SPI_CS_PIN)); + register_set_bits(&(EXTI->IMR1), (1UL << SPI_CS_PIN)); + EXTI->PR1 = (1UL << SPI_CS_PIN); + NVIC_EnableIRQ(EXTI15_10_IRQn); } diff --git a/panda/board/sys/power_saving.h b/panda/board/sys/power_saving.h index ee66ecf88a..eb97d62818 100644 --- a/panda/board/sys/power_saving.h +++ b/panda/board/sys/power_saving.h @@ -55,6 +55,9 @@ void set_power_save_state(bool enable) { } static void enter_stop_mode(void) { + // the SPI CS interrupt shares an IRQ with the FDCAN3 RX wakeup line, and doesn't clear its pending bit + NVIC_DisableIRQ(EXTI15_10_IRQn); + // set all GPIO to analog mode to reduce power, analog mode also disables pull resistors register_set(&(GPIOA->MODER), 0xFFFFFFFFU, 0xFFFFFFFFU); register_set(&(GPIOB->MODER), 0xFFFFFFFFU, 0xFFFFFFFFU); @@ -118,6 +121,9 @@ static void enter_stop_mode(void) { register_set_bits(&(EXTI->IMR1), can_exti_line); register_set_bits(&(EXTI->FTSR1), can_exti_line); + // don't wake up on SPI chip select (EXTI11, shares an IRQ with FDCAN3 RX) + register_clear_bits(&(EXTI->IMR1), (1UL << 11)); + // clear pending EXTI EXTI->PR1 = (1U << 1) | (1U << 4) | can_exti_line; diff --git a/panda/python/__init__.py b/panda/python/__init__.py index 11a562afc1..73641998c6 100644 --- a/panda/python/__init__.py +++ b/panda/python/__init__.py @@ -251,10 +251,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 +263,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 +345,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 [] diff --git a/panda/python/jungle.py b/panda/python/jungle.py index 72a5a0ff37..7d3047fb14 100644 --- a/panda/python/jungle.py +++ b/panda/python/jungle.py @@ -33,7 +33,7 @@ class PandaJungle(Panda): HARNESS_ORIENTATION_2 = 2 @classmethod - def spi_connect(cls, serial, ignore_version=False): + def spi_connect(cls, serial): return None, None, None, None def flash(self, fn=None, code=None, reconnect=True): diff --git a/panda/python/spi.py b/panda/python/spi.py index 4016b5aa9a..290968221a 100644 --- a/panda/python/spi.py +++ b/panda/python/spi.py @@ -9,39 +9,32 @@ 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 -# Constants -SYNC = 0x5A -HACK = 0x79 -DACK = 0x85 -NACK = 0x1F -CHECKSUM_START = 0xAB +# Protocol, see board/drivers/spi.h +SLAVE_READY = 0x3C +ACK = 0xC3 +NACK = 0x5A +INFO_ENDPOINT = 0xF0 -MIN_ACK_TIMEOUT_MS = 100 -MAX_XFER_RETRY_COUNT = 5 +REQ_HEADER = struct.Struct(" 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(" 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(" 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): @@ -313,16 +253,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 diff --git a/panda/scripts/spi_test.py b/panda/scripts/spi_test.py index d293b91164..9c7390b55f 100755 --- a/panda/scripts/spi_test.py +++ b/panda/scripts/spi_test.py @@ -1,11 +1,9 @@ #!/usr/bin/env python3 -import os import sys import time from datetime import datetime from collections import defaultdict, deque -os.environ['NO_RETRY'] = '1' # no spi retries from panda import Panda, PandaSpiException diff --git a/panda/tests/hitl/test_5_spi.py b/panda/tests/hitl/test_5_spi.py index 0e0eea1625..8ab96f4968 100644 --- a/panda/tests/hitl/test_5_spi.py +++ b/panda/tests/hitl/test_5_spi.py @@ -3,39 +3,26 @@ import random import struct import threading import time -import unittest from unittest.mock import patch from opendbc.car.structs import CarParams from panda.tests.hitl.base import PandaTestCase from panda import Panda -from panda.python.spi import PandaProtocolMismatch, PandaSpiNackResponse +from panda.python.spi import PandaSpiNackResponse, PandaSpiMissingAck, REQ_HEADER, CRC, SLAVE_READY, ACK, NACK, POLL class TestSpi(PandaTestCase): def _ping(self, panda): # should work with no retries - with patch.object(panda._handle, '_wait_for_ack', wraps=panda._handle._wait_for_ack) as spy: + with patch.object(panda._handle, '_transfer_spidev', wraps=panda._handle._transfer_spidev) as spy: panda.health() - assert spy.call_count == 2 + assert spy.call_count == 1 - def test_protocol_version_check(self): + def test_info(self): p = self.p for bootstub in (False, True): p.reset(enter_bootstub=bootstub) - with patch('panda.python.spi.PandaSpiHandle.PROTOCOL_VERSION', return_value="abc"): - # list should still work with wrong version - assert p._serial in Panda.list() - - # connect but raise protocol error - with self.assertRaises(PandaProtocolMismatch): - Panda(p._serial) - - def test_protocol_version_data(self): - p = self.p - for bootstub in (False, True): - p.reset(enter_bootstub=bootstub) - v = p._handle.get_protocol_version() + v = p._handle.get_info() uid = binascii.hexlify(v[:12]).decode() assert uid == p.get_uid() @@ -48,33 +35,69 @@ class TestSpi(PandaTestCase): def test_all_comm_types(self): p = self.p - spy = self.enterContext(patch.object(p._handle, '_wait_for_ack', wraps=p._handle._wait_for_ack)) + spy = self.enterContext(patch.object(p._handle, '_transfer_spidev', wraps=p._handle._transfer_spidev)) # controlRead + controlWrite p.health() p.can_clear(0) - assert spy.call_count == 2*2 + assert spy.call_count == 2 # bulkRead + bulkWrite p.can_recv() p.can_send(0x123, b"somedata", 0) - assert spy.call_count == 2*4 + assert spy.call_count == 4 - def test_bad_header(self): - p = self.p - with patch('panda.python.spi.SYNC', return_value=0): - with self.assertRaises(PandaSpiNackResponse): - p._handle.controlRead(Panda.REQUEST_IN, 0xd2, 0, 0, p.HEALTH_STRUCT.size, timeout=50) - self._ping(p) - - def test_bad_checksum(self): + def test_bad_crc(self): + # dropped, and the panda is ready for the next request p = self.p cnt = p.health()['spi_error_count'] - with patch('panda.python.spi.PandaSpiHandle._calc_checksum', return_value=0): - with self.assertRaises(PandaSpiNackResponse): - p._handle.controlRead(Panda.REQUEST_IN, 0xd2, 0, 0, p.HEALTH_STRUCT.size, timeout=50) + with p._handle.dev.acquire() as spi: + pkt = REQ_HEADER.pack(0, 1, 8, 72) + b"\xd2" + b"\x00"*7 + pkt += CRC.pack(binascii.crc32(pkt) ^ 1) + while spi.xfer(pkt)[0] != SLAVE_READY: + pass + while (dat := spi.xfer(bytes([POLL, ]) * 72))[0] not in (ACK, NACK, SLAVE_READY): + pass + assert dat[0] == SLAVE_READY + self._ping(p) + assert (p.health()['spi_error_count'] - cnt) == 1 + + def test_garbage(self): + # random bytes, of random lengths, at any point in the protocol + p = self.p + for _ in range(100): + with p._handle.dev.acquire() as spi: + for _ in range(random.randint(1, 5)): + spi.xfer(random.randbytes(random.randint(1, 4096))) + self._ping(p) + + def test_abandoned_transfers(self): + # host gives up at every stage of a transfer + p = self.p + with p._handle.dev.acquire() as spi: + pkt = REQ_HEADER.pack(0, 1, 8, 72) + b"\xd2" + b"\x00"*7 + pkt += CRC.pack(binascii.crc32(pkt)) + while spi.xfer(pkt)[0] != SLAVE_READY: + pass + self._ping(p) + + # read only part of the response + with p._handle.dev.acquire() as spi: + while spi.xfer(pkt)[0] != SLAVE_READY: + pass + for _ in range(100): + if spi.xfer(bytes([POLL, ]) * 8)[0] == ACK: + break + else: + raise AssertionError("no response") + self._ping(p) + + def test_timeout(self): + p = self.p + # never equal to a byte, unlike 0xFF which MISO reads while the panda queues the next transfer + with patch('panda.python.spi.SLAVE_READY', 0x100), self.assertRaises(PandaSpiMissingAck): + p._handle.controlRead(Panda.REQUEST_IN, 0xd2, 0, 0, p.HEALTH_STRUCT.size, timeout=50) self._ping(p) - assert (p.health()['spi_error_count'] - cnt) > 0 def test_non_existent_endpoint(self): p = self.p @@ -129,7 +152,6 @@ class TestSpi(PandaTestCase): t.join() assert errors == [] - @unittest.expectedFailure # SPI bulkRead ignores length on the old protocol def test_max_sizes(self): # around the first read size and the max transfer size for n in (1, 1013, 1014, 4032):