mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-10-28 00:53:45 +08:00
simple spi (#2461)
(cherry picked from commit 835e4c31d9514f9d06c341479237c35349e75136)
This commit is contained in:
committed by
firestar5683
parent
a0c5371835
commit
0ddd3f254f
+17
-14
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 []
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user