Files
proxmark3/armsrc/cmd.c
T
Tom HarknessandClaude Sonnet 5 8faafcebb0 PM5 CEP: recover from AT32 SPI1 errata, bound all SPI waits
Problem

With PR #3672/#3674 landed, CEP handshakes reliably but the SPI transport
hangs PM5 hard (needs a physical reset, no watchdog on this platform)
after some number of attach/reconnect cycles - sometimes the first
cycle, sometimes dozens of pings in. Logic-analyzer capture of a hang in
progress showed a ~1.2s burst of rapid CS-gated retries, and in one
capture, three periodic transactions after that burst got progressively
slower (2.1ms -> 3.9ms -> 3.2ms -> 5.2ms) instead of recovering.

Root causes (two, compounding)

1. cep_spi_write_sync() had seven bare `while (flag == RESET);` spins
   waiting on SPI1's TDBE/BF flags, with no timeout. PM5 is the SPI
   slave - TDBE only clears when the Flipper is actively clocking, on
   its own ~20-120ms schedule, completely async to when PM5's reply is
   ready. Queue a reply between two master polls and the wait never
   returns - full main-loop death.
2. Artery's own errata sheet for the AT32F435/437 (ES0003) documents
   that in SPI slave hardware-CS mode, the internal clock sync is not
   guaranteed to reset on every CS falling edge - a bad sync can persist
   across transactions with no self-correction. This is PM5's exact CEP
   configuration (SPI_CS_HARDWARE_MODE, slave mode).

Change

armsrc/pm5_cep.c:
- cep_spi_write_sync() rewritten around a bounded cep_spi_wait_tdbe()
  helper (same spin-iteration-count pattern already used for reads) -
  returns PM3_EIO on timeout instead of hanging.
- New cep_spi_resync(): hardware-resets SPI1 via crm_periph_reset()
  (a register re-init alone doesn't clear the errata's bad internal
  state) and brings it back up. This is Artery's own documented recovery
  for ES0003 ("reset SPI and restart communication"), triggered by our
  existing NG-frame CRC/length checks instead of the SPI peripheral's
  hardware CRC feature.
- cep_do_handshake()'s SPI-stall recovery path was itself unbounded
  (retried forever as long as the CC controller reported "still
  attached") and missing the StartTicks()/I2C_init(true) guard every
  other I2C call site in this file has (same bug class as #3672) - now
  bounded (CEP_HANDSHAKE_REPLY_MAX_RETRIES) with the guard added.
- cep_spi_data_available() now discards the Flipper's 0x55 idle-poll
  filler bytes before reading the real length header, instead of ever
  interpreting one as (part of) a frame length.

armsrc/cmd.c: wire cep_spi_resync() into every CEP-reachable error exit
in receive_ng_internal() (short/overflow/CRC-mismatch reads) and both
reply_old()/reply_ng_internal()'s write-failure paths, so any detected
corruption resets SPI1 before the next transaction rather than letting a
bad sync persist. Add a WITH_CEP-gated inline stub for cep_spi_resync()
so receive_ng_internal()'s call sites don't need per-site #ifdef guards
on non-CEP platforms.

Behaviour change

None for USB/FPC/BWM. CEP: a corrupted or stalled SPI1 transaction now
self-heals (one dropped reply, then normal operation) instead of
wedging the device.

Testing

- make clean && make PLATFORM=PM5 PLATFORM_EXTRAS=BWM and
  make clean && make PLATFORM=PM3RDV4 - both zero warnings.
- Hardware: flashed to a real PM5 paired with a real Flipper Zero FAP.
  - 60s continuous-connection capture with zero USB activity (isolates
    CEP from any client-side interaction): clean, no anomalies.
  - 110s capture with 18 real `hw status` calls interspersed: all 18
    succeeded, CEP stayed attached throughout.
  - 5 full attach/detach cycles with `hf search` against a real NTAG213
    card before/during/after each attach (240s, logic-analyzer capture
    throughout): all 15 reads returned the identical UID regardless of
    CEP state, zero hangs, final liveness check succeeded with no reset
    needed.
- Not tested: clang toolchain (firmware-only diff).

Checklist

- [x] Proper style of own code, no unrelated reformatting included
- [x] Builds warning-free with gcc (PM5 and RDV4)
- [ ] clang not tried (firmware-only diff)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-04 14:25:00 +02:00

351 lines
12 KiB
C

//-----------------------------------------------------------------------------
// Copyright (C) Proxmark3 contributors. See AUTHORS.md for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// See LICENSE.txt for the text of the license.
//-----------------------------------------------------------------------------
#include "cmd.h"
#include "usb_cdc_apis.h"
#include "usb_read_ng.h"
#include "usart.h"
#ifdef WITH_BWM_FORWARD
#include "bwm_forward.h"
#endif
#ifdef WITH_CEP
#include "pm5_cep.h"
#else
// receive_ng_internal()'s `cep` parameter is always false without WITH_CEP,
// so this is never actually called - just keeps those call sites free of
// per-site #if defined(WITH_CEP) guards.
static inline void cep_spi_resync(void) {}
#endif
#include "crc16.h"
#include "string.h"
#include "BigBuf.h"
// Flags to tell where to add CRC on sent replies
bool g_reply_with_crc_on_usb = false;
bool g_reply_with_crc_on_fpc = true;
// "Session" flag, to tell via which interface next msgs should be sent: USB or FPC USART
bool g_reply_via_fpc = false;
// Independent of via_fpc - CEP (Flipper Zero, over SPI1) may be live at the same time as BWM/FPC.
bool g_reply_via_cep = false;
// Largest NG payload the device will put on the CURRENT reply link. Over FPC the
// BWM buffers are smaller than a full frame, so cap there; USB uses the full size.
uint16_t reply_ng_max_data_size(void) {
if (g_reply_via_fpc && (PM3_CMD_DATA_SIZE > PM3_FPC_MAX_DATA)) {
return PM3_FPC_MAX_DATA;
}
return PM3_CMD_DATA_SIZE;
}
bool g_reply_via_usb = false;
int reply_old(uint64_t cmd, uint64_t arg0, uint64_t arg1, uint64_t arg2, const void *data, size_t len) {
PacketResponseOLD txcmd = {CMD_UNKNOWN, {0, 0, 0}, {{0}}};
for (size_t i = 0; i < sizeof(PacketResponseOLD); i++) {
((uint8_t *)&txcmd)[i] = 0x00;
}
// Compose the outgoing command frame
txcmd.cmd = cmd;
txcmd.arg[0] = arg0;
txcmd.arg[1] = arg1;
txcmd.arg[2] = arg2;
// Add the (optional) content to the frame, with a maximum size of PM3_CMD_DATA_SIZE_OLD
if (data && len) {
len = MIN(len, PM3_CMD_DATA_SIZE_OLD);
for (size_t i = 0; i < len; i++) {
txcmd.d.asBytes[i] = ((const uint8_t *)data)[i];
}
}
#if defined(WITH_FPC_USART_HOST) || defined(WITH_BWM_FORWARD)
int resultfpc = PM3_EUNDEF;
#endif
#if defined(WITH_CEP)
int resultcep = PM3_EUNDEF;
#endif
int resultusb = PM3_EUNDEF;
// Send frame and make sure all bytes are transmitted
if (g_reply_via_usb) {
resultusb = usb_write((uint8_t *)&txcmd, sizeof(PacketResponseOLD));
}
if (g_reply_via_fpc) {
#if defined(WITH_BWM_FORWARD)
resultfpc = bwm_fwd_writebuffer_sync((uint8_t *)&txcmd, sizeof(PacketResponseOLD));
#elif defined(WITH_FPC_USART_HOST)
resultfpc = usart_writebuffer_sync((uint8_t *)&txcmd, sizeof(PacketResponseOLD));
#else
return PM3_EDEVNOTSUPP;
#endif
}
if (g_reply_via_cep) {
#if defined(WITH_CEP)
resultcep = cep_spi_write_sync((uint8_t *)&txcmd, sizeof(PacketResponseOLD));
if (resultcep != PM3_SUCCESS) {
cep_spi_resync();
}
#else
return PM3_EDEVNOTSUPP;
#endif
}
// we got multiple results, let's prioritize the faulty one and USB over FPC over CEP.
if (g_reply_via_usb && (resultusb != PM3_SUCCESS)) return resultusb;
#if defined(WITH_FPC_USART_HOST) || defined(WITH_BWM_FORWARD)
if (g_reply_via_fpc && (resultfpc != PM3_SUCCESS)) return resultfpc;
#endif
#if defined(WITH_CEP)
if (g_reply_via_cep && (resultcep != PM3_SUCCESS)) return resultcep;
#endif
return PM3_SUCCESS;
}
static int reply_ng_internal(uint16_t cmd, int8_t status, uint8_t reason, const uint8_t *data, size_t len, bool ng) {
// The NG preamble is 10 bytes, so data[] would sit 2 past a word boundary
// and memcpy could never take its word path. Offsetting the whole frame by
// 2 lands data[] on a word boundary. The bytes on the wire are unchanged.
struct {
uint8_t pad[2];
PacketResponseNGRaw raw;
} __attribute__((aligned(4))) frame;
PacketResponseNGRaw *tx = &frame.raw;
size_t txBufferNGLen;
// Compose the outgoing command frame
tx->pre.magic = RESPONSENG_PREAMBLE_MAGIC;
tx->pre.cmd = cmd;
tx->pre.status = status;
tx->pre.reason = reason;
tx->pre.ng = ng;
if (len > PM3_CMD_DATA_SIZE) {
len = PM3_CMD_DATA_SIZE;
// overwrite status
tx->pre.status = PM3_EOVFLOW;
}
// length is only 15bit (32768)
tx->pre.length = (len & 0x7FFF);
// Add the (optional) content to the frame, with a maximum size of PM3_CMD_DATA_SIZE
if (data && len) {
memcpy(tx->data, data, len);
}
PacketResponseNGPostamble *tx_post = (PacketResponseNGPostamble *)((uint8_t *)tx + sizeof(PacketResponseNGPreamble) + len);
// Note: if we send to both FPC & USB, we'll set CRC for both if any of them require CRC
if ((g_reply_via_fpc && g_reply_with_crc_on_fpc) || ((g_reply_via_usb) && g_reply_with_crc_on_usb)) {
uint8_t first, second;
compute_crc(CRC_14443_A, (uint8_t *)tx, sizeof(PacketResponseNGPreamble) + len, &first, &second);
tx_post->crc = ((first << 8) | second);
} else {
tx_post->crc = RESPONSENG_POSTAMBLE_MAGIC;
}
txBufferNGLen = sizeof(PacketResponseNGPreamble) + len + sizeof(PacketResponseNGPostamble);
#if defined(WITH_FPC_USART_HOST) || defined(WITH_BWM_FORWARD)
int resultfpc = PM3_EUNDEF;
#endif
#if defined(WITH_CEP)
int resultcep = PM3_EUNDEF;
#endif
int resultusb = PM3_EUNDEF;
// Send frame and make sure all bytes are transmitted
if (g_reply_via_usb) {
resultusb = usb_write((uint8_t *)tx, txBufferNGLen);
}
if (g_reply_via_fpc) {
#if defined(WITH_BWM_FORWARD)
resultfpc = bwm_fwd_writebuffer_sync((uint8_t *)tx, txBufferNGLen);
#elif defined(WITH_FPC_USART_HOST)
resultfpc = usart_writebuffer_sync((uint8_t *)tx, txBufferNGLen);
#else
return PM3_EDEVNOTSUPP;
#endif
}
if (g_reply_via_cep) {
#if defined(WITH_CEP)
resultcep = cep_spi_write_sync((uint8_t *)tx, txBufferNGLen);
if (resultcep != PM3_SUCCESS) {
cep_spi_resync();
}
#else
return PM3_EDEVNOTSUPP;
#endif
}
// we got multiple results, let's prioritize the faulty one and USB over FPC over CEP.
if (g_reply_via_usb && (resultusb != PM3_SUCCESS)) {
return resultusb;
}
#if defined(WITH_FPC_USART_HOST) || defined(WITH_BWM_FORWARD)
if (g_reply_via_fpc && (resultfpc != PM3_SUCCESS)) {
return resultfpc;
}
#endif
#if defined(WITH_CEP)
if (g_reply_via_cep && (resultcep != PM3_SUCCESS)) {
return resultcep;
}
#endif
return PM3_SUCCESS;
}
int reply_ng(uint16_t cmd, int8_t status, const uint8_t *data, size_t len) {
return reply_ng_internal(cmd, status, PM3_REASON_UNKNOWN, data, len, true);
}
int reply_reason(uint16_t cmd, int8_t status, int8_t reason, const uint8_t *data, size_t len) {
return reply_ng_internal(cmd, status, reason, data, len, true);
}
static int receive_ng_internal(PacketCommandNG *rx, uint32_t read_ng(uint8_t *data, size_t len), bool usb, bool fpc, bool cep) {
PacketCommandNGRaw rx_raw;
size_t bytes = read_ng((uint8_t *)&rx_raw.pre, sizeof(PacketCommandNGPreamble));
if (bytes == 0) {
return PM3_ENODATA;
}
if (bytes != sizeof(PacketCommandNGPreamble)) {
if (cep) cep_spi_resync();
return PM3_EIO;
}
rx->magic = rx_raw.pre.magic;
rx->ng = rx_raw.pre.ng;
rx->cmd = rx_raw.pre.cmd;
uint16_t length = rx_raw.pre.length;
if (rx->magic == COMMANDNG_PREAMBLE_MAGIC) { // New style NG command
if (length > PM3_CMD_DATA_SIZE) {
if (cep) cep_spi_resync();
return PM3_EOVFLOW;
}
// Get the core and variable length payload
bytes = read_ng((uint8_t *)&rx_raw.data, length);
if (bytes != length) {
if (cep) cep_spi_resync();
return PM3_EIO;
}
// Get the postamble
bytes = read_ng((uint8_t *)&rx_raw.foopost, sizeof(PacketCommandNGPostamble));
if (bytes != sizeof(PacketCommandNGPostamble)) {
if (cep) cep_spi_resync();
return PM3_EIO;
}
// Check CRC, accept MAGIC as placeholder - a mismatch here is the
// AT32 errata's own recommended trigger to reset SPI (see
// cep_spi_resync()'s comment).
rx->crc = rx_raw.foopost.crc;
if (rx->crc != COMMANDNG_POSTAMBLE_MAGIC) {
uint8_t first, second;
compute_crc(CRC_14443_A, (uint8_t *)&rx_raw, sizeof(PacketCommandNGPreamble) + length, &first, &second);
if ((first << 8) + second != rx->crc) {
if (cep) cep_spi_resync();
return PM3_EIO;
}
}
// Zero the payload only once a packet is really coming in, and only
// after all wire reads: CEP's SPI receiver has no FIFO, so a stall
// between reads (this memset touches up to PM3_CMD_DATA_SIZE bytes)
// loses data. Both branches leave the tail of data untouched -- an
// NG command fills only its own length, an old style one only
// PM3_CMD_DATA_SIZE_OLD -- so what is not written has to start zero.
memset(&rx->data, 0, sizeof(rx->data));
if (rx->ng) {
memcpy(rx->data.asBytes, rx_raw.data, length);
rx->length = length;
} else {
uint64_t arg[3] = {0};
if (length < sizeof(arg)) {
if (cep) cep_spi_resync();
return PM3_EIO;
}
memcpy(arg, rx_raw.data, sizeof(arg));
rx->oldarg[0] = arg[0];
rx->oldarg[1] = arg[1];
rx->oldarg[2] = arg[2];
memcpy(rx->data.asBytes, rx_raw.data + sizeof(arg), length - sizeof(arg));
rx->length = length - sizeof(arg);
}
g_reply_via_usb = usb;
g_reply_via_fpc = fpc;
g_reply_via_cep = cep;
} else { // Old style command
PacketCommandOLD rx_old;
memcpy(&rx_old, &rx_raw.pre, sizeof(PacketCommandNGPreamble));
bytes = read_ng(((uint8_t *)&rx_old) + sizeof(PacketCommandNGPreamble), sizeof(PacketCommandOLD) - sizeof(PacketCommandNGPreamble));
if (bytes != sizeof(PacketCommandOLD) - sizeof(PacketCommandNGPreamble)) {
return PM3_EIO;
}
g_reply_via_usb = usb;
g_reply_via_fpc = fpc;
g_reply_via_cep = cep;
rx->ng = false;
rx->magic = 0;
rx->crc = 0;
rx->cmd = (rx_old.cmd & 0xFFFF);
rx->oldarg[0] = rx_old.arg[0];
rx->oldarg[1] = rx_old.arg[1];
rx->oldarg[2] = rx_old.arg[2];
rx->length = PM3_CMD_DATA_SIZE_OLD;
memset(&rx->data, 0, sizeof(rx->data));
memcpy(&rx->data, &rx_old.d.asBytes, rx->length);
}
return PM3_SUCCESS;
}
int receive_ng(PacketCommandNG *rx) {
// Check if there is a packet available
if (usb_poll_validate_length()) {
return receive_ng_internal(rx, usb_read_ng, true, false, false);
}
#if defined(WITH_CEP)
// cep_is_active() gates this so an unattached/idle CEP link doesn't cost
// an SPI-timeout poll every single main-loop iteration.
if (cep_is_active() && cep_spi_data_available())
return receive_ng_internal(rx, cep_spi_read_ng, false, false, true);
#endif
#if defined(WITH_BWM_FORWARD)
// De-frame inbound BWM app_com DATA_FORWARD packets into a raw NG stream.
if (bwm_fwd_rxdata_available() > 0)
return receive_ng_internal(rx, bwm_read_ng, false, true, false);
#elif defined(WITH_FPC_USART_HOST)
// Check if there is a FPC packet available
if (usart_rxdata_available() > 0)
return receive_ng_internal(rx, usart_read_ng, false, true, false);
#endif
return PM3_ENODATA;
}