simple spi (#2461)

(cherry picked from commit 835e4c31d9514f9d06c341479237c35349e75136)
This commit is contained in:
Adeeb Shihadeh
2026-10-14 07:58:12 -07:00
committed by firestar5683
parent a0c5371835
commit 0ddd3f254f
10 changed files with 391 additions and 502 deletions
+17 -14
View File
@@ -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;
}
-2
View File
@@ -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);
+158 -207
View File
@@ -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) {
+53 -78
View File
@@ -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);
}
+6
View File
@@ -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;
+3 -8
View File
@@ -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 []
+1 -1
View File
@@ -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):
+97 -156
View File
@@ -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("<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
@@ -57,12 +50,6 @@ class PandaSpiNackResponse(PandaSpiException):
class PandaSpiMissingAck(PandaSpiException):
pass
class PandaSpiBadChecksum(PandaSpiException):
pass
class PandaSpiTransferFailed(PandaSpiException):
pass
SPI_LOCK = threading.Lock()
SPI_DEVICES = {}
@@ -166,140 +153,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):
@@ -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
-2
View File
@@ -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
+56 -34
View File
@@ -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):