From 50f83919180147cf4d81e751c85b70ca8a0d8f6d Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Tue, 6 Oct 2026 22:57:19 +0000 Subject: [PATCH] SX126x: keep a staged-over frame's RX_DONE across the resume A CAD skipped because a frame was in flight set keepRxIrqsAtResume only when RX_DONE or CRC_ERR was already up. With a header but no RX_DONE yet, the resume cleared RX_DONE, CRC_ERR, HEADER_ERR and TIMEOUT - and on a CH341 host the flag read in stageTxInRx() and that clear are a USB round trip apart, so a frame completing in between lost its RX_DONE. The poll then had no edge to deliver and the frame waited for checkStaleRxFlags() to retire the latched HEADER_VALID, one max packet later. Keep RX_DONE and CRC_ERR for every busy verdict, so the resume's checkRxDoneIrqFlag() drives the readout. HEADER_ERR and TIMEOUT are still cleared unconditionally: no readout waits on them, and a stale one costs checkStaleRxFlags() a re-arm. Only RX_DONE is mapped to DIO1, so leaving the other terminal flag latched holds no interrupt line. Found by CodeRabbit on #12016; the blanket keep it proposed would have left a stale HEADER_ERR or TIMEOUT latched instead. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0112S3QXU8WUGSCcMQHnwRKZ --- src/mesh/SX126xInterface.cpp | 18 +++++++++++------- src/mesh/SX126xInterface.h | 2 +- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/mesh/SX126xInterface.cpp b/src/mesh/SX126xInterface.cpp index c431bcf59a..21677f5f33 100644 --- a/src/mesh/SX126xInterface.cpp +++ b/src/mesh/SX126xInterface.cpp @@ -543,12 +543,15 @@ template bool SX126xInterface::resumeRunningReceive() #endif if (!rxArmedContinuous) return false; - // readData() clears these, but handleReceiveInterrupt()'s early outs do not, and a latched one would hold DIO1 - // high past the re-arm. PREAMBLE/HEADER_VALID stay: they may belong to the next frame, already arriving. A scan - // skipped for an unread frame keeps them all, for its readout. + // readData() clears these, but handleReceiveInterrupt()'s early outs do not. PREAMBLE/HEADER_VALID stay: they + // may belong to the next frame, already arriving. A scan skipped for a frame in flight or unread keeps that + // frame's RX_DONE (the only flag on DIO1) and CRC_ERR for its readout, which checkRxDoneIrqFlag() below drives - + // including an RX_DONE that landed in the USB round trip since the scan read the flags. A stale HEADER_ERR or + // TIMEOUT has no readout waiting and would cost checkStaleRxFlags() a re-arm, so it goes either way. + uint32_t clearMask = RADIOLIB_SX126X_IRQ_HEADER_ERR | RADIOLIB_SX126X_IRQ_TIMEOUT; if (!keepRxIrqs) - lora.clearIrqFlags(RADIOLIB_SX126X_IRQ_RX_DONE | RADIOLIB_SX126X_IRQ_CRC_ERR | RADIOLIB_SX126X_IRQ_HEADER_ERR | - RADIOLIB_SX126X_IRQ_TIMEOUT); + clearMask |= RADIOLIB_SX126X_IRQ_RX_DONE | RADIOLIB_SX126X_IRQ_CRC_ERR; + lora.clearIrqFlags(clearMask); activeReceiveStart = 0; // as the standby this replaces would RadioLibInterface::startReceive(); enableInterrupt(isrRxLevel0); @@ -779,8 +782,9 @@ template bool SX126xInterface::stageTxInRx() // Its readout checks whether our bytes can lie across it if (irq & (RADIOLIB_SX126X_IRQ_HEADER_VALID | doneIrqs)) noteStagedOverFrame(base, numbytes); - // The busy verdict's rearmReceive() resumes the RX: it must leave a finished frame's flags for its readout - keepRxIrqsAtResume = (irq & doneIrqs) != 0; + // The busy verdict's rearmReceive() resumes the RX: it must leave the frame's terminal flags for its + // readout, whether it had already finished or finishes while we get there + keepRxIrqsAtResume = true; LOG_DEBUG("TX stage in RX: frame arriving or unread (irq 0x%04x), scan skipped", (unsigned)irq); return true; } diff --git a/src/mesh/SX126xInterface.h b/src/mesh/SX126xInterface.h index 762934b727..d34c53082d 100644 --- a/src/mesh/SX126xInterface.h +++ b/src/mesh/SX126xInterface.h @@ -120,7 +120,7 @@ template class SX126xInterface : public RadioLibInterface uint8_t rxClobberBase = 0; size_t rxClobberLen = 0; uint8_t rxClobberBytes[256]; - /** That frame had finished: the next resumeRunningReceive() must not clear its IRQ flags before it is read */ + /** A frame was in flight or unread: the next resumeRunningReceive() keeps RX_DONE/CRC_ERR for its readout */ bool keepRxIrqsAtResume = false; /** A payload written during its backoff: its length (0 if none), packet id, offset and bytes */ size_t earlyStagedLen = 0;