diff --git a/openpilot/selfdrive/pandad/panda_comms.h b/openpilot/selfdrive/pandad/panda_comms.h index cdfb5019b..77c085fdc 100644 --- a/openpilot/selfdrive/pandad/panda_comms.h +++ b/openpilot/selfdrive/pandad/panda_comms.h @@ -8,7 +8,7 @@ #define TIMEOUT 0 -#define SPI_BUF_SIZE 2048 +#define SPI_BUF_SIZE 4096 class PandaSpiHandle { @@ -35,15 +35,18 @@ private: inline static std::recursive_mutex hw_lock; struct __attribute__((packed)) spi_header { - uint8_t sync; uint8_t endpoint; + uint32_t id; uint16_t tx_len; - uint16_t max_rx_len; + uint16_t rx_len; + }; + struct __attribute__((packed)) spi_resp_header { + uint8_t status; + uint32_t id; + uint16_t len; }; - int wait_for_ack(uint8_t ack, uint8_t tx, unsigned int timeout, unsigned int length); - int bulk_transfer(uint8_t endpoint, uint8_t *tx_data, uint16_t tx_len, uint8_t *rx_data, uint16_t rx_len, unsigned int timeout); - int spi_transfer(uint8_t endpoint, uint8_t *tx_data, uint16_t tx_len, uint8_t *rx_data, uint16_t max_rx_len, unsigned int timeout); + int spi_transfer(uint8_t endpoint, uint32_t id, uint8_t *tx_data, uint16_t tx_len, uint8_t *rx_data, uint16_t max_rx_len, double deadline); int spi_transfer_retry(uint8_t endpoint, uint8_t *tx_data, uint16_t tx_len, uint8_t *rx_data, uint16_t max_rx_len, unsigned int timeout); int lltransfer(struct spi_ioc_transfer &t); diff --git a/openpilot/selfdrive/pandad/pandad.cc b/openpilot/selfdrive/pandad/pandad.cc index d89762ba9..8dc81606e 100644 --- a/openpilot/selfdrive/pandad/pandad.cc +++ b/openpilot/selfdrive/pandad/pandad.cc @@ -139,6 +139,8 @@ void fill_panda_state(cereal::PandaState::Builder &ps, cereal::PandaState::Panda ps.setInterruptLoad(health.interrupt_load_pkt / 255.0f); ps.setFanPower(health.fan_power); ps.setSafetyRxChecksInvalid((health.flags_pkt & HEALTH_FLAG_SAFETY_RX_CHECKS_INVALID) != 0U); + ps.setNmiReset((health.flags_pkt & HEALTH_FLAG_NMI_RESET) != 0U); + ps.setHardfaultReset((health.flags_pkt & HEALTH_FLAG_HARDFAULT_RESET) != 0U); ps.setSpiErrorCount(health.spi_error_count_pkt); ps.setSbu1Voltage(health.sbu1_voltage_mV / 1000.0f); ps.setSbu2Voltage(health.sbu2_voltage_mV / 1000.0f); diff --git a/openpilot/selfdrive/pandad/spi.cc b/openpilot/selfdrive/pandad/spi.cc index 369032a53..8636861cb 100644 --- a/openpilot/selfdrive/pandad/spi.cc +++ b/openpilot/selfdrive/pandad/spi.cc @@ -2,10 +2,12 @@ #include #include +#include #include #include #include #include +#include #include #include "common/util.h" @@ -15,26 +17,26 @@ #include "selfdrive/pandad/panda_comms.h" -#define SPI_SYNC 0x5AU -#define SPI_HACK 0x79U -#define SPI_DACK 0x85U -#define SPI_NACK 0x1FU -#define SPI_CHECKSUM_START 0xABU +// protocol, see panda/board/drivers/spi.h +#define SPI_SLAVE_READY 0x3CU +#define SPI_ACK 0xC3U +#define SPI_NACK 0x5AU +#define SPI_POLL 0xFFU // filler while reading the response, never a valid request + +#define SPI_CRC_SIZE 4U +#define SPI_XFER_SIZE (SPI_BUF_SIZE - 0x40) // bulk chunk size, leaves room for the protocol overhead +#define SPI_FIRST_READ_SIZE (1024 - sizeof(spi_resp_header) - SPI_CRC_SIZE) // reads are usually small, start small enum SpiError { NACK = -2, ACK_TIMEOUT = -3, + SILENT = -4, }; -const unsigned int SPI_ACK_TIMEOUT = 500; // milliseconds +const unsigned int SPI_MIN_TIMEOUT = 100; // milliseconds +const double SPI_SILENT_TIMEOUT = 1000; // milliseconds, longer than the slowest request takes on the panda const std::string SPI_DEVICE = "/dev/spidev0.0"; -// TODO: fix SPI turnaround synchronization at the protocol level. -static uint64_t spi_last_bus_activity_ns = 0; // protected by hw_lock - -static void wait_for_spi_turnaround(uint64_t start_ns) { - while ((nanos_since_boot() - start_ns) < 400000) {} -} class LockEx { public: @@ -56,8 +58,8 @@ private: #define SPILOG(fn, fmt, ...) do { \ fn(fmt, ## __VA_ARGS__); \ fn(" %d / 0x%x / %d / %d / tx: %s", \ - xfer_count, header.endpoint, header.tx_len, header.max_rx_len, \ - util::hexdump(tx_buf, std::min((int)header.tx_len, 8)).c_str()); \ + xfer_count, header.endpoint, header.tx_len, header.rx_len, \ + util::hexdump(tx_buf + sizeof(header), std::min((int)header.tx_len, 8)).c_str()); \ } while (0) PandaSpiHandle::PandaSpiHandle(std::string serial) { @@ -88,7 +90,7 @@ PandaSpiHandle::PandaSpiHandle(std::string serial) { util::safe_ioctl(spi_fd, SPI_IOC_WR_BITS_PER_WORD, &spi_bits_per_word, "failed setting SPI bits per word"); // get hw UID/serial - ret = control_read(0xc3, 0, 0, uid, uid_len, 100); + ret = control_read(0xc3, 0, 0, uid, uid_len, 500); if (ret == uid_len) { std::stringstream stream; for (int i = 0; i < uid_len; i++) { @@ -146,40 +148,31 @@ int PandaSpiHandle::control_read(uint8_t request, uint16_t param1, uint16_t para } int PandaSpiHandle::bulk_write(unsigned char endpoint, unsigned char* data, int length, unsigned int timeout) { - return bulk_transfer(endpoint, data, length, NULL, 0, timeout); -} -int PandaSpiHandle::bulk_read(unsigned char endpoint, unsigned char* data, int length, unsigned int timeout) { - return bulk_transfer(endpoint, NULL, 0, data, length, timeout); -} - -int PandaSpiHandle::bulk_transfer(uint8_t endpoint, uint8_t *tx_data, uint16_t tx_len, uint8_t *rx_data, uint16_t rx_len, unsigned int timeout) { - const int xfer_size = SPI_BUF_SIZE - 0x40; - - int ret = 0; - uint16_t length = (tx_data != NULL) ? tx_len : rx_len; - for (int i = 0; i < (int)std::ceil((float)length / xfer_size); i++) { - int d; - if (tx_data != NULL) { - int len = std::min(xfer_size, tx_len - (xfer_size * i)); - d = spi_transfer_retry(endpoint, tx_data + (xfer_size * i), len, NULL, 0, timeout); - } else { - uint16_t to_read = std::min(xfer_size, rx_len - ret); - d = spi_transfer_retry(endpoint, NULL, 0, rx_data + (xfer_size * i), to_read, timeout); - } - - if (d < 0) { - SPILOG(LOGE, "SPI: bulk transfer failed with %d", d); + for (int pos = 0; pos < length; pos += SPI_XFER_SIZE) { + int ret = spi_transfer_retry(endpoint, data + pos, std::min(SPI_XFER_SIZE, length - pos), NULL, 0, timeout); + if (ret < 0) { comms_healthy = false; - return d; + return ret; } + } + return length; +} - ret += d; - if ((rx_data != NULL) && d < xfer_size) { +int PandaSpiHandle::bulk_read(unsigned char endpoint, unsigned char* data, int length, unsigned int timeout) { + int pos = 0; + while (pos < length) { + int to_read = std::min(pos == 0 ? (int)SPI_FIRST_READ_SIZE : SPI_XFER_SIZE, length - pos); + int ret = spi_transfer_retry(endpoint, NULL, 0, data + pos, to_read, timeout); + if (ret < 0) { + comms_healthy = false; + return ret; + } + pos += ret; + if (ret < to_read) { break; } } - - return ret; + return pos; } std::vector PandaSpiHandle::list() { @@ -192,228 +185,202 @@ std::vector PandaSpiHandle::list() { return {}; } -void add_checksum(uint8_t *data, int data_len) { - data[data_len] = SPI_CHECKSUM_START; - for (int i=0; i < data_len; i++) { - data[data_len] ^= data[i]; - } +// random, so it's never the same as another request the panda just handled, even from another client +static uint32_t random_id() { + static thread_local std::mt19937 rng{std::random_device{}()}; + return rng(); } -bool check_checksum(uint8_t *data, int data_len) { - uint8_t checksum = SPI_CHECKSUM_START; - for (uint16_t i = 0U; i < data_len; i++) { - checksum ^= data[i]; +// standard CRC-32, same as zlib's crc32() +static uint32_t crc32(const uint8_t *data, uint32_t len) { + static const auto table = [] { + std::array t{}; + for (uint32_t i = 0; i < 256; i++) { + uint32_t c = i; + for (int j = 0; j < 8; j++) { + c = (c & 1) ? (0xEDB88320U ^ (c >> 1)) : (c >> 1); + } + t[i] = c; + } + return t; + }(); + + uint32_t crc = 0xFFFFFFFFU; + for (uint32_t i = 0; i < len; i++) { + crc = table[(crc ^ data[i]) & 0xFFU] ^ (crc >> 8); } - return checksum == 0U; + return crc ^ 0xFFFFFFFFU; +} + +static void put_crc(uint8_t *data, uint32_t len) { + uint32_t crc = crc32(data, len); + memcpy(&data[len], &crc, sizeof(crc)); +} + +static bool check_crc(const uint8_t *data, uint32_t len) { + uint32_t crc; + memcpy(&crc, &data[len], sizeof(crc)); + return crc == crc32(data, len); } int PandaSpiHandle::spi_transfer_retry(uint8_t endpoint, uint8_t *tx_data, uint16_t tx_len, uint8_t *rx_data, uint16_t max_rx_len, unsigned int timeout) { + // timeout of 0 means forever + const double deadline = (timeout == 0) ? INFINITY : millis_since_boot() + std::max(timeout, SPI_MIN_TIMEOUT); + + // the id stays the same when we send the request again, so the panda never runs it twice + uint32_t id = random_id(); int ret; int nack_count = 0; - int timeout_count = 0; - bool timed_out = false; - double start_time = millis_since_boot(); - - do { - ret = spi_transfer(endpoint, tx_data, tx_len, rx_data, max_rx_len, timeout); - - if (ret < 0) { - timed_out = (timeout != 0) && (timeout_count > 5); - timeout_count += ret == SpiError::ACK_TIMEOUT; - - // give other threads a chance to run - std::this_thread::yield(); - - if (ret == SpiError::NACK) { - // prevent busy waiting while the panda is NACK'ing - // due to full TX buffers - nack_count += 1; - if (nack_count > 3) { - SPILOG(LOGD, "NACK sleep %d", nack_count); - usleep(std::clamp(nack_count*10, 200, 2000)); - } - } + while (true) { + ret = spi_transfer(endpoint, id, tx_data, tx_len, rx_data, max_rx_len, deadline); + if (ret >= 0 || !connected) { + break; } - } while (ret < 0 && connected && !timed_out); + + if (ret == SpiError::NACK) { + // the panda NACKs while its CAN TX buffers are full. that's not a comms + // failure, so keep trying regardless of the timeout, without busy waiting. + // a new id, since the panda would answer the same id with the same NACK + id = random_id(); + nack_count += 1; + if (nack_count > 3) { + usleep(std::clamp(nack_count*10, 200, 2000)); + } + } else if (ret == SpiError::SILENT) { + // the panda went quiet. let go of the bus for a bit, someone else might need it + LOGW("SPI: panda isn't responding"); + usleep(10000); + } else if (millis_since_boot() > deadline) { + break; + } + + // give other threads a chance to run + std::this_thread::yield(); + } if (ret < 0) { - SPILOG(LOGE, "transfer failed, after %d tries, %.2fms", timeout_count, millis_since_boot() - start_time); + SPILOG(LOGE, "SPI: transfer failed with %d", ret); } - return ret; } -int PandaSpiHandle::wait_for_ack(uint8_t ack, uint8_t tx, unsigned int timeout, unsigned int length) { - double start_millis = millis_since_boot(); - if (timeout == 0) { - timeout = SPI_ACK_TIMEOUT; - } - timeout = std::clamp(timeout, 20U, SPI_ACK_TIMEOUT); - - spi_ioc_transfer transfer = { - .tx_buf = (uint64_t)tx_buf, - .rx_buf = (uint64_t)rx_buf, - .len = length, - }; - memset(tx_buf, tx, length); - - while (true) { - int ret = lltransfer(transfer); - if (ret < 0) { - SPILOG(LOGE, "SPI: failed to send ACK request"); - return ret; - } - - if (rx_buf[0] == ack) { - break; - } else if (rx_buf[0] == SPI_NACK) { - SPILOG(LOGD, "SPI: got NACK, waiting for 0x%x", ack); - return SpiError::NACK; - } - - // handle timeout - if (millis_since_boot() - start_millis > timeout) { - SPILOG(LOGW, "SPI: timed out waiting for ACK, waiting for 0x%x", ack); - return SpiError::ACK_TIMEOUT; - } - } - - return 0; -} - int PandaSpiHandle::lltransfer(spi_ioc_transfer &t) { static const double err_prob = std::stod(util::getenv("SPI_ERR_PROB", "-1")); + auto chance = [](double p) { return (static_cast(rand()) / RAND_MAX) < p; }; + auto corrupt = [&](uint64_t buf) { + for (uint32_t i = 0; i < t.len; i++) { + if (chance(0.1)) ((uint8_t*)buf)[i] = (uint8_t)(rand() % 256); + } + }; - if (err_prob > 0) { - if ((static_cast(rand()) / RAND_MAX) < err_prob) { - printf("transfer len error\n"); - t.len = rand() % SPI_BUF_SIZE; - } - if ((static_cast(rand()) / RAND_MAX) < err_prob && t.tx_buf != (uint64_t)NULL) { - printf("corrupting TX\n"); - for (int i = 0; i < t.len; i++) { - if ((static_cast(rand()) / RAND_MAX) > 0.9) { - ((uint8_t*)t.tx_buf)[i] = (uint8_t)(rand() % 256); - } - } - } + if (err_prob > 0 && chance(err_prob)) { + printf("transfer len error\n"); + t.len = rand() % SPI_BUF_SIZE; + } + if (err_prob > 0 && chance(err_prob) && t.tx_buf == (uint64_t)tx_buf) { + printf("corrupting TX\n"); + corrupt(t.tx_buf); } int ret = util::safe_ioctl(spi_fd, SPI_IOC_MESSAGE(1), &t); - - if (err_prob > 0) { - if ((static_cast(rand()) / RAND_MAX) < err_prob && t.rx_buf != (uint64_t)NULL) { - printf("corrupting RX\n"); - for (int i = 0; i < t.len; i++) { - if ((static_cast(rand()) / RAND_MAX) > 0.9) { - ((uint8_t*)t.rx_buf)[i] = (uint8_t)(rand() % 256); - } - } - } + if (err_prob > 0 && chance(err_prob)) { + printf("ioctl error\n"); + ret = -1; } + if (err_prob > 0 && chance(err_prob)) { + printf("corrupting RX\n"); + corrupt(t.rx_buf); + } return ret; } -int PandaSpiHandle::spi_transfer(uint8_t endpoint, uint8_t *tx_data, uint16_t tx_len, uint8_t *rx_data, uint16_t max_rx_len, unsigned int timeout) { - int ret; - uint16_t rx_data_len; +// One request and its response. Resends until the panda got the request and we got its response, +// or the deadline. Every transfer either sees all of what the panda queued from its first byte, or +// nothing it can mistake for a response. Returns the response length, or a negative SpiError +int PandaSpiHandle::spi_transfer(uint8_t endpoint, uint32_t id, uint8_t *tx_data, uint16_t tx_len, uint8_t *rx_data, uint16_t max_rx_len, double deadline) { + static const auto poll = [] { + std::array p; + p.fill(SPI_POLL); + return p; + }(); + LockEx lock(spi_fd, hw_lock); - // needs to be less, since we need to have space for the checksum - assert(tx_len < SPI_BUF_SIZE); - assert(max_rx_len < SPI_BUF_SIZE); - - wait_for_spi_turnaround(spi_last_bus_activity_ns); + // the response is always read in one transfer of rx_len + const uint32_t rx_len = sizeof(spi_resp_header) + max_rx_len + SPI_CRC_SIZE; + const uint32_t req_len = sizeof(header) + tx_len; + assert(req_len + SPI_CRC_SIZE <= SPI_BUF_SIZE); + assert(rx_len <= SPI_BUF_SIZE); xfer_count++; header = { - .sync = SPI_SYNC, .endpoint = endpoint, + .id = id, .tx_len = tx_len, - .max_rx_len = max_rx_len + .rx_len = (uint16_t)rx_len, }; - spi_ioc_transfer transfer = { - .tx_buf = (uint64_t)tx_buf, - .rx_buf = (uint64_t)rx_buf + auto xfer = [&](const uint8_t *tx, uint32_t len) { + spi_ioc_transfer t = {.tx_buf = (uint64_t)tx, .rx_buf = (uint64_t)rx_buf, .len = len}; + int ret = lltransfer(t); + if (ret < 0) { + SPILOG(LOGE, "SPI: transfer failed"); + } + return ret; }; - // Send header - memcpy(tx_buf, &header, sizeof(header)); - add_checksum(tx_buf, sizeof(header)); - transfer.len = sizeof(header) + 1; - ret = lltransfer(transfer); - if (ret < 0) { - SPILOG(LOGE, "SPI: failed to send header"); - goto fail; - } + // when the panda last showed it's there. while it's quiet, we let go of the bus + double alive = millis_since_boot(); + while (true) { + // send the request, until the panda was ready for it. it's rebuilt every time, + // since SPI_ERR_PROB corrupts tx_buf + memcpy(tx_buf, &header, sizeof(header)); + if (tx_data != NULL) { + memcpy(tx_buf + sizeof(header), tx_data, tx_len); + } + put_crc(tx_buf, req_len); + // a failed transfer is sent again like any other, keeping the bus, so nothing can run in between + if (xfer(tx_buf, req_len + SPI_CRC_SIZE) >= 0 && rx_buf[0] == SPI_SLAVE_READY) { + alive = millis_since_boot(); + // read until the response. if the panda is ready again, it never got the request + do { + if (xfer(poll.data(), rx_len) < 0) { + break; + } + if (rx_buf[0] == SPI_SLAVE_READY) { + alive = millis_since_boot(); + break; + } - // Wait for (N)ACK - ret = wait_for_ack(SPI_HACK, 0x11, timeout, 1); - if (ret < 0) { - goto fail; - } - wait_for_spi_turnaround(nanos_since_boot()); + // a response that got cut off or corrupted isn't one, keep reading + spi_resp_header resp; + memcpy(&resp, rx_buf, sizeof(resp)); + if ((resp.status == SPI_ACK || resp.status == SPI_NACK) && resp.len <= max_rx_len && check_crc(rx_buf, sizeof(resp) + resp.len)) { + // otherwise it's the response to an old request, and we send ours again + if (resp.id == header.id) { + if (resp.status == SPI_NACK) { + SPILOG(LOGD, "SPI: got NACK"); + return SpiError::NACK; + } + if (rx_data != NULL) { + memcpy(rx_data, rx_buf + sizeof(resp), resp.len); + } + return resp.len; + } + alive = millis_since_boot(); + break; + } + } while (millis_since_boot() < std::min(deadline, alive + SPI_SILENT_TIMEOUT)); + } - // Send data - if (tx_data != NULL) { - memcpy(tx_buf, tx_data, tx_len); - } - add_checksum(tx_buf, tx_len); - transfer.len = tx_len + 1; - ret = lltransfer(transfer); - if (ret < 0) { - SPILOG(LOGE, "SPI: failed to send data"); - goto fail; - } - - // Wait for (N)ACK - ret = wait_for_ack(SPI_DACK, 0x13, timeout, 3); - if (ret < 0) { - goto fail; - } - - // Read data - rx_data_len = *(uint16_t *)(rx_buf+1); - if (rx_data_len >= SPI_BUF_SIZE) { - SPILOG(LOGE, "SPI: RX data len larger than buf size %d", rx_data_len); - goto fail; - } - - transfer.len = rx_data_len + 1; - transfer.rx_buf = (uint64_t)(rx_buf + 2 + 1); - ret = lltransfer(transfer); - if (ret < 0) { - SPILOG(LOGE, "SPI: failed to read rx data"); - goto fail; - } - if (!check_checksum(rx_buf, rx_data_len + 4)) { - SPILOG(LOGE, "SPI: bad checksum"); - goto fail; - } - - if (rx_data != NULL) { - memcpy(rx_data, rx_buf + 3, rx_data_len); - } - - spi_last_bus_activity_ns = nanos_since_boot(); - return rx_data_len; - -fail: - // ensure slave is in a consistent state - // and ready for the next transfer - int nack_cnt = 0; - while (nack_cnt < 3) { - if (wait_for_ack(SPI_NACK, 0x14, 1, SPI_BUF_SIZE/2) == 0) { - nack_cnt += 1; - } else { - nack_cnt = 0; + if (millis_since_boot() > deadline) { + SPILOG(LOGW, "SPI: timed out"); + return SpiError::ACK_TIMEOUT; + } + if (millis_since_boot() > alive + SPI_SILENT_TIMEOUT) { + return SpiError::SILENT; } } - - spi_last_bus_activity_ns = nanos_since_boot(); - if (ret >= 0) ret = -1; - return ret; } diff --git a/openpilot/selfdrive/pandad/tests/test_pandad_spi.py b/openpilot/selfdrive/pandad/tests/test_pandad_spi.py index 8ca39445a..281865428 100755 --- a/openpilot/selfdrive/pandad/tests/test_pandad_spi.py +++ b/openpilot/selfdrive/pandad/tests/test_pandad_spi.py @@ -45,9 +45,10 @@ class TestBoarddSpi(OpenpilotTestCase): st = time.monotonic() ts = {s: [] for s in socks.keys()} - for _ in range(int(os.getenv("TEST_TIME", "20"))): - # send some CAN messages - if not JUNGLE_SPAM: + test_time = int(os.getenv("TEST_TIME", "20")) + for i in range(test_time + 2): + # send some CAN messages, then give the last ones time to come back + if not JUNGLE_SPAM and i < test_time: sent = send_random_can_messages(sendcan, random.randrange(2, 20)) for k, v in sent.items(): sent_msgs[k].extend(list(v)) @@ -74,8 +75,7 @@ class TestBoarddSpi(OpenpilotTestCase): continue key = (msg.address, msg.dat) assert key in sent_msgs[msg.src], f"got unexpected msg: {msg.src=} {msg.address=} {msg.dat=}" - # TODO: enable this - #sent_msgs[msg.src].remove(key) + sent_msgs[msg.src].remove(key) total_recv_count += 1 elif service == "pandaStates": assert len(m.pandaStates) == 1 @@ -107,6 +107,8 @@ class TestBoarddSpi(OpenpilotTestCase): with subtests.test(msg="CAN traffic"): print(f"Sent {total_sent_count} CAN messages, got {total_recv_count} back. {total_recv_count/(total_sent_count+1e-4):.2%} received") assert total_recv_count > 20 + # every message came back exactly once, despite the injected errors + assert all(len(v) == 0 for v in sent_msgs.values()) if __name__ == "__main__": diff --git a/panda/board/crc.h b/panda/board/crc.h index 3e20fb098..ce53984e9 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 900409fd0..a9cbfeb71 100644 --- a/panda/board/drivers/drivers.h +++ b/panda/board/drivers/drivers.h @@ -231,8 +231,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); // ******************** uart ******************** #ifdef STM32H7 diff --git a/panda/board/drivers/spi.h b/panda/board/drivers/spi.h index 2e992d907..94cb645a7 100644 --- a/panda/board/drivers/spi.h +++ b/panda/board/drivers/spi.h @@ -6,87 +6,186 @@ #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! diff --git a/panda/board/early_init.h b/panda/board/early_init.h index d5131a42c..9f1bc7c41 100644 --- a/panda/board/early_init.h +++ b/panda/board/early_init.h @@ -62,3 +62,19 @@ void early_initialization(void) { jump_to_bootloader(); } } + +// we might hit NMI / HardFault close to brown-out. +// we don't want to infinitely loop, so let's set a logging cookie and reset. +#define NMI_COOKIE 0x4E4D4921U +#define HARDFAULT_COOKIE 0x48464C54U +extern uint32_t fault_cookie; + +void NMI_Handler(void) { + fault_cookie = NMI_COOKIE; + NVIC_SystemReset(); +} + +void HardFault_Handler(void) { + fault_cookie = HARDFAULT_COOKIE; + NVIC_SystemReset(); +} diff --git a/panda/board/health.h b/panda/board/health.h index d18f123bb..180c44fc4 100644 --- a/panda/board/health.h +++ b/panda/board/health.h @@ -7,6 +7,8 @@ #define HEALTH_FLAG_HEARTBEAT_LOST (1U << 4) #define HEALTH_FLAG_SAFETY_RX_CHECKS_INVALID (1U << 5) #define HEALTH_FLAG_SOM_RESET_TRIGGERED (1U << 6) +#define HEALTH_FLAG_NMI_RESET (1U << 7) +#define HEALTH_FLAG_HARDFAULT_RESET ((uint16_t)1U << 8) struct __attribute__((packed)) health_t { uint32_t uptime_pkt; diff --git a/panda/board/main.c b/panda/board/main.c index f269407da..2625f4b6b 100644 --- a/panda/board/main.c +++ b/panda/board/main.c @@ -122,6 +122,13 @@ static void tick_handler(void) { if (TICK_TIMER->SR != 0U) { + heartbeat_tick_counter += 1U; + + // start the siren early on power loss + if ((controls_allowed_countdown > 0U) && (heartbeat_tick_counter > 2U) && (current_board->read_voltage_mV() < 6000U)) { + siren_countdown = 3U; + } + // siren current_board->set_siren((loop_counter & 1U) && (siren_enabled || (siren_countdown > 0U))); @@ -260,7 +267,7 @@ static void tick_handler(void) { ignition_can_cnt += 1U; // synchronous safety check - safety_tick(¤t_safety_config); + safety_tick(); } loop_counter++; @@ -339,6 +346,14 @@ int main(void) { led_set(LED_GREEN, false); led_set(LED_BLUE, false); + if (fault_cookie == NMI_COOKIE) { + reset_flags |= HEALTH_FLAG_NMI_RESET; + } + if (fault_cookie == HARDFAULT_COOKIE) { + reset_flags |= HEALTH_FLAG_HARDFAULT_RESET; + } + fault_cookie = 0U; + print("**** INTERRUPTS ON ****\n"); enable_interrupts(); diff --git a/panda/board/main_comms.h b/panda/board/main_comms.h index b11a99bea..2c277d645 100644 --- a/panda/board/main_comms.h +++ b/panda/board/main_comms.h @@ -20,6 +20,7 @@ static int get_health_pkt(void *dat) { health->flags_pkt |= heartbeat_lost ? HEALTH_FLAG_HEARTBEAT_LOST : 0U; health->flags_pkt |= safety_rx_checks_invalid ? HEALTH_FLAG_SAFETY_RX_CHECKS_INVALID : 0U; health->flags_pkt |= bootkick_reset_triggered ? HEALTH_FLAG_SOM_RESET_TRIGGERED : 0U; + health->flags_pkt |= reset_flags; health->safety_tx_blocked_pkt = safety_tx_blocked; health->safety_rx_invalid_pkt = safety_rx_invalid; @@ -303,6 +304,7 @@ int comms_control_handler(ControlPacket_t *req, uint8_t *resp) { case 0xf3: { heartbeat_counter = 0U; + heartbeat_tick_counter = 0U; heartbeat_lost = false; heartbeat_disabled = false; heartbeat_engaged = (req->param1 == 1U); diff --git a/panda/board/main_declarations.h b/panda/board/main_declarations.h index af2a29937..6f7c1cebb 100644 --- a/panda/board/main_declarations.h +++ b/panda/board/main_declarations.h @@ -12,9 +12,11 @@ void pwm_set(TIM_TypeDef *TIM, uint8_t channel, uint8_t percentage); extern uint8_t hw_type; extern board *current_board; extern uint32_t uptime_cnt; +extern uint16_t reset_flags; // heartbeat state extern uint32_t heartbeat_counter; +extern uint32_t heartbeat_tick_counter; extern bool heartbeat_lost; extern bool heartbeat_disabled; diff --git a/panda/board/main_definitions.h b/panda/board/main_definitions.h index 83c910905..b07788beb 100644 --- a/panda/board/main_definitions.h +++ b/panda/board/main_definitions.h @@ -4,9 +4,11 @@ uint8_t hw_type = 0; board *current_board; uint32_t uptime_cnt = 0; +uint16_t reset_flags = 0U; // heartbeat state uint32_t heartbeat_counter = 0; +uint32_t heartbeat_tick_counter = 0; // 8Hz bool heartbeat_lost = false; bool heartbeat_disabled = false; // set over USB diff --git a/panda/board/stm32h7/llspi.h b/panda/board/stm32h7/llspi.h index 25a7c76e0..ac3119eea 100644 --- a/panda/board/stm32h7/llspi.h +++ b/panda/board/stm32h7/llspi.h @@ -6,103 +6,83 @@ void llspi_mosi_dma(uint8_t *addr, int len) { DMA2_Stream2->CR &= ~DMA_SxCR_EN; register_clear_bits(&(SPI4->CR1), SPI_CR1_SPE); - // 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/stm32h7/sound.h b/panda/board/stm32h7/sound.h index 80c9317fd..f862351ca 100644 --- a/panda/board/stm32h7/sound.h +++ b/panda/board/stm32h7/sound.h @@ -29,6 +29,7 @@ void sound_tick(void) { if (mic_idle_count == 0U) { register_clear_bits(&DFSDM1_Channel0->CHCFGR1, DFSDM_CHCFGR1_DFSDMEN); mic_buffer_count = 0U; + (void)memset(mic_tx_buf, 0, sizeof(mic_tx_buf)); } } } @@ -189,6 +190,9 @@ void sound_init(void) { register_set(&DMAMUX2_Channel0->CCR, 16U, DMAMUX_CxCR_DMAREQ_ID_Msk); // SAI4_B_DMA register_set_bits(&BDMA_Channel0->CCR, BDMA_CCR_EN); + // SRAM4 is not cleared at startup; transmit silence until capture is ready + (void)memset(mic_tx_buf, 0, sizeof(mic_tx_buf)); + // mic output register_set(&SAI4_Block_A->CR1, SAI_xCR1_DMAEN | (0b01UL << SAI_xCR1_SYNCEN_Pos) | (0b100UL << SAI_xCR1_DS_Pos) | (0b10UL << SAI_xCR1_MODE_Pos), 0x0FFB3FEFU); register_set(&SAI4_Block_A->CR2, 0U, 0xFFFBU); diff --git a/panda/board/sys/power_saving.h b/panda/board/sys/power_saving.h index 791353c4e..0b0e518a8 100644 --- a/panda/board/sys/power_saving.h +++ b/panda/board/sys/power_saving.h @@ -53,6 +53,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); @@ -116,6 +119,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 186b5b1a3..754f1fb25 100644 --- a/panda/python/__init__.py +++ b/panda/python/__init__.py @@ -148,6 +148,8 @@ class Panda: HEALTH_FLAG_HEARTBEAT_LOST = 1 << 4 HEALTH_FLAG_SAFETY_RX_CHECKS_INVALID = 1 << 5 HEALTH_FLAG_SOM_RESET_TRIGGERED = 1 << 6 + HEALTH_FLAG_NMI_RESET = 1 << 7 + HEALTH_FLAG_HARDFAULT_RESET = 1 << 8 H7_DEVICES = [HW_TYPE_RED_PANDA, HW_TYPE_TRES, HW_TYPE_CUATRO, HW_TYPE_BODY] SUPPORTED_DEVICES = H7_DEVICES @@ -251,10 +253,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 +265,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 +347,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 [] @@ -551,6 +548,8 @@ class Panda: "sbu1_voltage_mV": a[17], "sbu2_voltage_mV": a[18], "som_reset_triggered": bool(flags & self.HEALTH_FLAG_SOM_RESET_TRIGGERED), + "nmi_reset": bool(flags & self.HEALTH_FLAG_NMI_RESET), + "hardfault_reset": bool(flags & self.HEALTH_FLAG_HARDFAULT_RESET), "sound_output_level": a[19], "temperature": a[20] - 40.0, "controls_allowed_lateral": a[21] & 1, @@ -720,7 +719,7 @@ class Panda: @ensure_can_packet_version def can_send_many(self, arr, *, fd=False, timeout=CAN_SEND_TIMEOUT_MS): - snds = pack_can_buffer(arr, chunk=(not self.spi), fd=fd) + snds = pack_can_buffer(arr, chunk=True, fd=fd) for tx in snds: while len(tx) > 0: bs = self._handle.bulkWrite(3, tx, timeout=timeout) diff --git a/panda/python/spi.py b/panda/python/spi.py index 8d4c18cee..311a8f32b 100644 --- a/panda/python/spi.py +++ b/panda/python/spi.py @@ -8,7 +8,7 @@ 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 # No fcntl on Windows @@ -30,29 +30,23 @@ DACK = 0x85 NACK = 0x1F CHECKSUM_START = 0xAB -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): @@ -265,16 +206,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 14bc6e39e..49980d393 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/5_spi.py b/panda/tests/hitl/5_spi.py index 106a1c7d4..45c4caf76 100644 --- a/panda/tests/hitl/5_spi.py +++ b/panda/tests/hitl/5_spi.py @@ -1,10 +1,13 @@ import binascii import pytest import random +import struct +import threading +import time from unittest.mock import patch 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: @@ -46,12 +49,12 @@ class TestSpi: # 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, mocker, p): with patch('panda.python.spi.SYNC', return_value=0):