PM5 CEP: widen reply-header timeout, resync SPI on desync evidence

The Flipper master only clocks the bus in bursts roughly every ~120ms
(its own idle poll cadence). CEP_SPI_BYTE_TIMEOUT (~3-10ms real time)
is ~20x shorter than that gap, so cep_spi_write_sync()'s first reply
byte - the one racing against the master's next poll - was giving up
long before the master actually showed up. Measured CEP reply success
at ~22% before this fix, ~100% after, across repeated LA-captured test
runs.

Fix: a separate, ~20x longer timeout (CEP_SPI_HEADER_BYTE_TIMEOUT) used
only for the first two length-header bytes of a reply - the ones that
can land in the gap. Every byte after that stays on the short timeout,
since once the master starts clocking it stays mid-CS and keeps going.

Also makes cep_spi_data_available() call cep_spi_resync() on its two
existing desync-evidence paths (sync-preamble discard overflow, an
implausible length header) instead of just returning false - those are
exactly the AT32 errata ES0003 corruption cep_spi_resync() already
exists to recover from; silently returning false left SPI1 in that bad
state until a later CRC/length check happened to catch it.

Stacked on fix-cep-spi-reliability (#3686, open) - this builds directly
on cep_spi_resync()/cep_spi_wait_tdbe()'s bounded-wait rewrite from that
PR and can't be tested standalone without it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Tom HarknessandClaude Sonnet 5 authored and Philippe Teuwen committed 2026-10-04 14:39:54 +02:00
1 parent f2df3eba9d
commit 7bd39effbd
1 file changed
+26 -7
+26 -7
View File
@@ -45,6 +45,14 @@
#define CEP_HANDSHAKE_STRING "iamf0rupm5"
#define CEP_HANDSHAKE_STX 0x02
#define CEP_SPI_BYTE_TIMEOUT 100000 // spin-wait iterations per byte (matches source)
// The Flipper master only clocks the bus in bursts roughly every ~120ms
// (its own idle poll cadence) - CEP_SPI_BYTE_TIMEOUT alone (~3-10ms real
// time) is far shorter than that gap, so the first reply byte - the one
// racing against the master's next poll - needs a budget that actually
// spans it. Once that byte clears, the master is mid-CS and keeps
// clocking continuously, so every later byte stays on the short timeout.
// Took measured CEP reply success from ~22% to ~100% in testing.
#define CEP_SPI_HEADER_BYTE_TIMEOUT 2000000
#define CEP_SPI_RESPONSE_TIMEOUT_US (1000 * 1000) // 1s, matches source
#define CEP_HANDSHAKE_RX_TIMEOUT_MS 2000 // bail out of the receive loop rather than ever hang the main loop
#define CEP_HANDSHAKE_REPLY_MAX_RETRIES 3 // bound the stall-recovery loop below - still-attached isn't still-working forever
@@ -365,6 +373,12 @@ bool cep_spi_data_available(void) {
break;
}
if (++discarded > CEP_SYNC_MAX_DISCARD) {
// More sync bytes than the FAP's real preamble (500) can only
// mean a desynced read (AT32 errata ES0003, see
// cep_spi_resync()) or bus noise - resync now rather than
// leaving SPI1 in that state until a later CRC/length check
// catches it, which may be a full frame away.
cep_spi_resync();
return false;
}
}
@@ -374,6 +388,9 @@ bool cep_spi_data_available(void) {
s_last_spi_activity_tick = GetTickCount(); // FAP is still polling, valid frame or not
uint16_t data_len = (len_header[1] << 8) | len_header[0];
if (data_len > PM3_CMD_DATA_SIZE * 2) {
// Same reasoning: an implausible length is itself evidence of a
// desynced read, not just "no frame yet."
cep_spi_resync();
return false;
}
return true;
@@ -398,10 +415,10 @@ uint32_t cep_spi_read_ng(uint8_t *data, size_t len) {
// hangs the main loop forever (no recovery, not even a watchdog reset) the
// moment a reply is queued between two master polls - bound every wait so
// a slow/absent master degrades to a dropped reply, not a dead device.
static bool cep_spi_wait_tdbe(void) {
static bool cep_spi_wait_tdbe(uint64_t timeout_limit) {
uint64_t timeout = 0;
while (spi_i2s_flag_get(SPI1, SPI_I2S_TDBE_FLAG) == RESET) {
if (timeout++ > CEP_SPI_BYTE_TIMEOUT) {
if (timeout++ > timeout_limit) {
return false;
}
}
@@ -409,29 +426,31 @@ static bool cep_spi_wait_tdbe(void) {
}
int cep_spi_write_sync(uint8_t *data, size_t len) {
if (!cep_spi_wait_tdbe()) {
// These two length-header bytes are the ones racing against the
// master's next poll - see CEP_SPI_HEADER_BYTE_TIMEOUT's comment.
if (!cep_spi_wait_tdbe(CEP_SPI_HEADER_BYTE_TIMEOUT)) {
return PM3_EIO;
}
spi_i2s_data_transmit(SPI1, len & 0xFF);
if (!cep_spi_wait_tdbe()) {
if (!cep_spi_wait_tdbe(CEP_SPI_HEADER_BYTE_TIMEOUT)) {
return PM3_EIO;
}
spi_i2s_data_transmit(SPI1, (len >> 8) & 0xFF);
for (size_t i = 0; i < len; ++i) {
if (!cep_spi_wait_tdbe()) {
if (!cep_spi_wait_tdbe(CEP_SPI_BYTE_TIMEOUT)) {
return PM3_EIO;
}
spi_i2s_data_transmit(SPI1, data[i]);
}
// Two trailing zero bytes: the agreed "no more data" idle marker.
if (!cep_spi_wait_tdbe()) {
if (!cep_spi_wait_tdbe(CEP_SPI_BYTE_TIMEOUT)) {
return PM3_EIO;
}
spi_i2s_data_transmit(SPI1, 0x00);
if (!cep_spi_wait_tdbe()) {
if (!cep_spi_wait_tdbe(CEP_SPI_BYTE_TIMEOUT)) {
return PM3_EIO;
}
spi_i2s_data_transmit(SPI1, 0x00);