Fix LR11x0 and LR2021 packet loss when a stale SPI reply is read as the length - #3261
Fix LR11x0 and LR2021 packet loss when a stale SPI reply is read as the length#3261fkallay1 wants to merge 1 commit into
Conversation
1ca27fe to
16978cc
Compare
On both LR11x0 and LR2021 a "get" command is two SPI transactions: send the
opcode, then read the reply. SPItransferStream() waits 1 us after the first
transaction before it starts polling BUSY, so when BUSY has not risen yet the
wait is skipped and the reply is read before the chip has prepared it. The chip
answers with its default [stat 2B][irq 4B] stream instead, and the length is
parsed straight out of that.
On LR2021 the length is built from two bytes, so the result is irq[31:16] -
exactly 4 with RX_DONE set. A 50 or 133 byte frame was reported as len=4,
readData() read 4 bytes, its clearRxFifo() discarded the rest and the frame was
rejected as corrupt. It is not counted as a receive error either, because
readData() returned success. On LR11x0 the length is a single byte, irq[31:24],
which reads 0, and recvRaw() then skips the packet on its 'len > 0' test.
The chip does say whether a reply is on its way: the command status field of
stat1 is CMD_DAT ("successfully processed, data is being transmitted") for the
read half of a get, and CMD_OK ("nothing to collect") when the status stream
comes back instead. RadioLib consumes that byte internally and only rejects
CMD_FAIL and CMD_PERR, so the distinction is lost. Reading the length here
directly keeps the status byte visible, which makes the check possible - the
same trick LRxxxx::getIrqStatus uses to read the IRQ word. The Rx buffer is
still intact at that point, so re-reading recovers the frame.
Measured on both families in a live mesh at SF7, with the BUSY wait skipped on
purpose on every fourth length read:
- LR2021 (XIAO nRF52840 with a NiceRF LoRa2021F33-2G4): 11 of 11 sabotaged reads
returned 4 and reported CMD_OK; the retry returned the true length and the node
received the same traffic as a second receiver on the same channel.
- LR11x0 (Seeed T1000-E): 4 of 4 came back as 0 with CMD_OK, and the retry
recovered 84, 20 and 196 byte frames.
The status is also what keeps the check honest, and this is where the first
version of this change was wrong. On LR2021, judging by the value alone looks
attractive - compare it against irq[31:16] - but 68 is both a common frame length
and what those bytes read when RX_DONE and CRC_ERROR are set together, so it
fires on genuine frames. On LR11x0 the same idea was used with 'len == 0 &&
RX_DONE', on the assumption that a zero length cannot be genuine there. Hardware
says otherwise: on the T1000-E that state occurs 25 times in three minutes,
roughly one per two and a half received frames, with the IRQ word at 0x38, 0x78
(header error) or 0xB8 (CRC error). A probe doing three extra reads on each of
them recovered nothing - 0 out of 25 - so the zero is usually honest and only the
status can tell it apart from a stale reply. Both chips therefore use the same
check now. The existing 'len == 0 && HEADER_ERR' handling in
CustomLR1110::getPacketLength is left untouched.
Note this only covers the path through recvRaw(). LR11x0::readData() reads the
length and buffer offset again through the two-argument
getPacketLength(bool, uint8_t*), which is not virtual and therefore cannot be
guarded from a subclass. If the race is lost there, the payload comes out empty
or shifted by the bogus offset. That part can only be fixed in the driver, and
has been reported to RadioLib as an issue with the reproduction.
Build tested: t1000e_repeater, MeshTracker_X1_repeater.
16978cc to
152c6d5
Compare
|
Updated after testing this on an LR11x0 board as well, and it changed the LR11x0 half of The first version guarded LR11x0 with Both chips now use the same check, and it holds on both: with the BUSY wait skipped on
|
|
Nice work, have you opened an issue/PR at RadioLib? edit: I see you've already brought it to @jgromes attention (jgromes/RadioLib#1857) and it looks like a fix is already in the works upstream (jgromes/RadioLib#1858). It's probably best to just wait for a fix to land upstream in RadioLib rather than papering over the issue down here. I guess it applies to more than just this command anyway. |
|
Yes — jgromes/RadioLib#1857. The description mentioned it only in words, which was not much use; I have put the link in there as well. jgromes agreed and put the cause more sharply than I had: the read transaction does not wait for BUSY between sending the opcode and reading the reply, which the LR1110 datasheet requires. He has pushed a candidate fix to the What that means for this PR is in the description already: the proper fix belongs in the driver's transport layer, and once it lands this guard becomes redundant and can be dropped — but not before the pinned RadioLib commit is moved past it, and the pin currently sits on 11 July. |
Type: bug
Symptom
On an LR2021 repeater, the raw receive log occasionally reported a length of 4 for
frames that were 38, 50, 126 or 133 bytes long - always exactly 4, regardless of the
real length. The frames were then rejected as corrupt and never reached the mesh
layer. A second repeater on the same channel, an SX1262 board, logged the same frames
with correct lengths at the same moment, so it was not an RF problem.
It came in bursts. When several repeaters re-flood the same packet at once and the
chip is busy, roughly a third of the frames were lost this way. In quiet periods it
did not happen for hours.
The loss is silent:
readData()returns success, so it is not counted as a receiveerror, only as a frame that was received and then thrown away.
Cause
On the LR11x0 and LR2021 families a read command is two SPI transactions
(
LRxxxx::SPIcommand()): send the opcode, then read the reply.Module::SPItransferStream()waitsdelayMicroseconds(1)after the firsttransaction and then polls BUSY for a low level - so if BUSY has not risen within
that microsecond, the wait is skipped and the reply is read before the chip has
prepared it.
The chip then sends its default
[stat 2B][irq 4B]stream, and the length is parsedstraight out of that. The status byte is valid, so nothing reports an error.
On LR2021 the length is built from two bytes, so
getRxPktLength()returnsirq[31:16]. With RX_DONE (bit 18) set that is exactly 4 - the observed value. WithTX_DONE or CRC_ERROR also set it becomes 12 or 68, which is plausible enough to pass
as a real length and produce a garbage packet instead of an obviously short one.
On LR11x0 the length is a single byte, so
getRxBufferStatus()returnsirq[31:24],and every RX-relevant flag of that family lives in the two low bytes - so the length
comes back as 0.
recvRaw()skips the packet on itslen > 0test, and the frameis dropped when Rx is re-armed. No log line, no error counter, nothing but a gap in
the
isrversusrecvcounts.getIrqStatus()cannot be caught by this race, because it is that default stream.That also makes the top bytes of the IRQ word an exact fingerprint of a stale reply,
which is what the fix uses.
Fix
The chip already says whether a reply is on its way: the command status field of stat1
is
CMD_DAT("successfully processed, data is being transmitted") for the read half ofa get, and
CMD_OK("nothing to collect") when the status stream comes back instead.RadioLib consumes that byte internally and only rejects
CMD_FAILandCMD_PERR, sothe distinction is lost. Reading the length here directly keeps the status byte visible
LRxxxx::getIrqStatususes to read the IRQ word - and the length istrusted only when the status says the reply is ours. The Rx FIFO is still intact at that
point, so re-reading recovers the frame; if it never settles, the plain library read is
used so this can never end up worse.
Keeping the status byte is the only part that needs care. Setting the status width to 0
tells the transfer layer there is nothing to strip, so the whole reply lands in our own
buffer:
The guard itself is then just the status test:
An earlier version of this compared the returned length against
irq[31:16]instead,which needs no status byte. That turned out to misfire: 68 is both a common frame length
here and what
irq[31:16]reads when RX_DONE and CRC_ERROR are set together, so itflagged genuine frames. The status reported
CMD_DATon exactly those, which is why itdecides now.
On LR11x0 the same check applies, for the same reason. The first version of this used
len == 0 && RX_DONEthere instead, on the assumption that a zero length is unambiguouson that family. Hardware says otherwise: on a T1000-E that state occurs 25 times in three
minutes, roughly one per two and a half received frames, and re-reading recovers nothing
in any of them - 0 out of 25. So the zero is usually honest, and only the status can tell
it apart from a stale reply. The existing
len == 0 && HEADER_ERRhandling is leftuntouched.
Both overrides use only public API and protected members of the base class, so no
RADIOLIB_GODMODEis needed.What this does not cover
This guards the path through
recvRaw().LR11x0::readData()reads the length andthe buffer offset again through the two-argument
getPacketLength(bool update, uint8_t* offset), which is not virtual and thereforecannot be intercepted from a subclass. If the race is lost there, the payload comes
out empty, or shifted by a bogus offset - which may well be the "packets shifted"
symptom the existing comment in
CustomLR1110::getPacketLength()refers to.The same race also affects every other read on these chips -
getRSSI(),getSNR(),getRssiInst(),getVbat(),getTemp()- so a wrong RSSI or SNR can reachpacketScore()without any indication. Those cannot be guarded from here either,because a wrong RSSI has no fingerprint to test against. A proper fix belongs in the
driver's transport layer.
That part has been reported to RadioLib as an issue
(jgromes/RadioLib#1857), with the reproduction and the
measurements above - not as a patch, since where the check belongs is a decision about
the transport layer of the whole LR11x0/LR2021 family. If the driver starts rejecting a
reply whose status is not
CMD_DAT, every get command is covered at once and this guardbecomes redundant. It would still be harmless - our own read never goes through
SPIcommand()- so it can simply be dropped then, though not before the pinned RadioLibis bumped: the pin currently sits on a commit that predates any such fix, so a driver-side
fix changes nothing here until then. Until that happens this guard is what actually keeps
the frames.
Affected boards
LR2021:
meshtracker_x1,meshnology_w12.LR11x0:
t1000-e,wio_wm1110,thinknode_m3,thinknode_m7,thinknode_m9,minewsemi_me25ls01.Testing
LR2021 on hardware: Seeed XIAO nRF52840 with a NiceRF LoRa2021 module, 869.618 MHz,
SF7, BW 62.5, in a live mesh with several repeaters, against a second receiver (SX1262)
on the same channel as a reference.
The failure is a timing race, so rather than wait for it, it was forced: a debug build
skips the BUSY wait on every fourth length read, in the real receive path.
them reported
CMD_OK.no bogus lengths and no receive errors, although a quarter of its length reads had
been broken on purpose.
irq[31:16], the status reportedCMD_DATand the guard correctly stayed out of the way.LR11x0 verified on hardware as well, on a Seeed T1000-E on the same channel, the same
way - the BUSY wait skipped on every fourth length read:
CMD_OK; the re-read returned thetrue length each time, recovering 84, 20 and 196 byte frames.
at 0x38, 0x78 (header error) or 0xB8 (CRC error) - all reported
CMD_DAT, and a probedoing three extra reads on each recovered nothing. That is what ruled out the
value-based rule for this family.
Builds clean:
t1000e_repeater,wio_wm1110_repeater,MeshTracker_X1_repeater.