Skip to content

Commit b9b80de

Browse files
josephnefclaude
andauthored
mt7612u: make a retune race-free against the live RX ring (#444)
The two data races docs/mt7612u.md listed for a channel change while the RX ring is up, reproduced under ThreadSanitizer on the first MT7612U in the lab (Comfast CF-922AC) and fixed: the per-channel RSSI correction is one atomic word (mt7612u_cal::rx_corr, Mt7612uRxCorr.h), and the sync helpers submit through libusb's async API and wait on the library's own acquire/release flag, with heap-owned transfer state (last party frees), transfers pooled before the event thread exists, and a stalled transfer stranding the device rather than being freed in flight. Measured after: 0 library/libusb TSan reports over ~1000 cross-band retunes and 7000+ frames; retune latency and witnessed delivery identical to the sync-API build. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 parent 09eb26a commit b9b80de

8 files changed

Lines changed: 377 additions & 55 deletions

File tree

‎docs/mt7612u.md‎

Lines changed: 40 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -598,46 +598,54 @@ Stated because the numbers above are uniformly favourable.
598598
metadata and every consumer that trims four bytes are in place - but the
599599
adapter still has to USE it: see Open.
600600

601+
## Retune while the ring is up
602+
603+
A channel change from the caller's thread while the libusb event thread is
604+
delivering frames is the one concurrent operation this backend has, and it is
605+
measured race-free: `rxdemo` under ThreadSanitizer (`DEVOURER_SANITIZE=thread`)
606+
with `DEVOURER_RX_SWEEP=1,6,11,36,40,44,48` at 150 ms dwells — a full,
607+
cross-band tune every dwell — ran 150 s on a Comfast CF-922AC (SuperSpeed,
608+
`40:a5:ef:5f:65:51`), about a thousand retunes and 7221 frames, with **zero
609+
reports inside the library or libusb**. The two reports that remain are in
610+
`rxdemo` itself (its frame counter and the demos' signal-handler stop flag)
611+
and are shared with every generation.
612+
613+
Two facts about the design hold that result up:
614+
615+
- **The per-channel RSSI correction is one word**, published once per tune
616+
and loaded once per frame — the layout and why are on the declaration
617+
(`mt7612u_cal::rx_corr`, `Mt7612uRxCorr.h`). What that bought: three
618+
separate bytes gave a frame parsed mid-retune a wrong RSSI, which is the
619+
class of bug this port cares most about.
620+
- **Synchronous transfers do not use libusb's synchronous API.** Every
621+
register access and MCU exchange is an async submission waited on the
622+
library's own acquire/release flag; the hazard in libusb's own sync layer
623+
and the lifetime rules are documented once, at the helpers in `usb.cpp`. The
624+
replacement is A/B-neutral where it could cost: retune latency on the
625+
7-bin sweep above read p50 791 ms / mean 756 ms on both the sync-API build
626+
and this one (65 dwells each), and a 2000-frame injection witnessed by an
627+
RTL8822BU delivered the same count from both builds at two power back-offs
628+
(1000 at −20 dB, 900 at −10 dB; the ground station sits 20 cm away in
629+
saturation, so those are its ceiling, not the transmitter's).
630+
631+
For scale: the same stress on the shipping RTL8812AU path produces **8** TSan
632+
reports, all in devourer's own Jaguar1 state (`RtlJaguarDevice.cpp`,
633+
`RtlAdapter.h`). Retune-during-RX is race-free here and not there.
634+
601635
## Open list
602636

603637
Ordered, and honest about which are unknowns rather than typing:
604638

605-
1. **Two data races the library has when a channel change runs while the RX
606-
ring is up.** Found with ThreadSanitizer against real hardware, driving
607-
`rxdemo` with `DEVOURER_RX_SWEEP` so the main thread retunes while the
608-
libusb event thread delivers frames. Neither is in `Mt7612uRadio` — that
609-
class's `_mu` discipline held, and TSan reported nothing inside it — and
610-
neither is reachable from the bring-up harness, which sets the channel
611-
before starting a ring and measured 0 warnings over 10667 frames.
612-
613-
- `mt_read_rx_gain()` (`eeprom.cpp`) rewrites the per-channel `lna_gain` and
614-
`rssi_offset[]` from the EEPROM on every tune, while `mt_rx_parse()`
615-
(`rx.cpp`) reads them on the event thread to correct each frame's RSSI. A
616-
frame parsed mid-retune therefore gets a mixed correction, i.e. a wrong
617-
RSSI for that frame. Wrong number, not a crash — which is the class this
618-
port cares most about.
619-
- A synchronous control transfer issued during the tune (`mt_rr_chk` ->
620-
`libusb_control_transfer`) reaches `libusb_free_transfer()`, destroying a
621-
transfer's mutex, while the async ring's event thread locks it. That is
622-
inside libusb, and it is the documented hazard of mixing the synchronous
623-
API with a dedicated event thread on one context.
624-
625-
For scale: the same stress on the shipping RTL8812AU path produces **8**
626-
TSan reports, all in devourer's own Jaguar1 state (`RtlJaguarDevice.cpp`,
627-
`RtlAdapter.h`). Retune-during-RX is not a race-free operation anywhere in
628-
this project today, so this is a shared gap rather than a MediaTek
629-
regression — but the two above are specific and fixable, and the second one
630-
is a use-after-destroy rather than a torn read.
631-
2. `mt76x2_phy_tssi_compensate()` — periodic temperature correction. Without
639+
1. `mt76x2_phy_tssi_compensate()` — periodic temperature correction. Without
632640
it output power drifts with die temperature.
633-
3. Cold-boot verification on a host with switchable USB power.
634-
4. A witness on different silicon. The regression matrix added a second witness
641+
2. Cold-boot verification on a host with switchable USB power.
642+
3. A witness on different silicon. The regression matrix added a second witness
635643
*implementation* (`tcpdump` over `mt76x2u`), and a second *unit* of the part
636644
— but not a second board revision, and not a decoder outside the MediaTek
637645
family.
638-
5. 80 MHz; VHT and NSS=2 on air.
639-
6. Retune tuning — batch registers via `CMD_RANDOM_WRITE`, drop the inter-command
646+
4. 80 MHz; VHT and NSS=2 on air.
647+
5. Retune tuning — batch registers via `CMD_RANDOM_WRITE`, drop the inter-command
640648
sleep, skip the `RXDCOC` on a fast path. Worth doing only if 10–20 ms is
641649
useful to someone.
642-
7. Whether the single MCS6 frame in the rate-LUT control arm (1 of 84) is a
650+
6. Whether the single MCS6 frame in the rate-LUT control arm (1 of 84) is a
643651
witness decode artefact or a real fallback. Unexplained.

‎src/mt7612u/Mt7612uRxCorr.h‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/* SPDX-License-Identifier: BSD-3-Clause-Clear */
2+
/*
3+
* The per-channel RSSI correction as one word.
4+
*
5+
* mt7612u_cal::rx_corr packs the two chain RSSI offsets and the LNA gain
6+
* (mt76x02_mac_get_rssi's three inputs) into a single uint32_t so that a
7+
* retune publishes all three at once and the RX parser reads them in one
8+
* load. Pure integer packing, no device dependency - kept in its own header
9+
* so the headless mapping selftest can pin the sign handling.
10+
*
11+
* Layout: byte 0 = chain-0 offset, byte 1 = chain-1 offset, byte 2 = LNA
12+
* gain, each a signed dB; byte 3 unused.
13+
*/
14+
#ifndef MT7612U_RX_CORR_H
15+
#define MT7612U_RX_CORR_H
16+
17+
#include <stdint.h>
18+
19+
static inline uint32_t mt_rx_corr_pack(int8_t off0, int8_t off1, int8_t lna)
20+
{
21+
return (uint32_t)(uint8_t)off0 | ((uint32_t)(uint8_t)off1 << 8) |
22+
((uint32_t)(uint8_t)lna << 16);
23+
}
24+
25+
static inline void mt_rx_corr_unpack(uint32_t v, int8_t off[2], int8_t *lna)
26+
{
27+
off[0] = (int8_t)(v & 0xff);
28+
off[1] = (int8_t)((v >> 8) & 0xff);
29+
*lna = (int8_t)((v >> 16) & 0xff);
30+
}
31+
32+
#endif

‎src/mt7612u/eeprom.cpp‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ static uint8_t get_5g_rx_gain(struct mt7612u_dev *d, uint8_t chan)
248248

249249
void mt_read_rx_gain(struct mt7612u_dev *d, uint8_t chan, int band)
250250
{
251-
int8_t lna_2g, lna_5g[3];
251+
int8_t lna_2g, lna_5g[3], rssi_offset[2], lna_gain;
252252
uint16_t rssi_off, v;
253253
uint8_t gain, lna = 0;
254254

@@ -273,9 +273,9 @@ void mt_read_rx_gain(struct mt7612u_dev *d, uint8_t chan, int band)
273273

274274
rssi_off = (band == 0) ? mt_ee(d, MT_EE_RSSI_OFFSET_2G_0)
275275
: mt_ee(d, MT_EE_RSSI_OFFSET_5G_0);
276-
d->cal.rssi_offset[0] = field_valid(rssi_off & 0xff)
276+
rssi_offset[0] = field_valid(rssi_off & 0xff)
277277
? (int8_t)sign_extend_optional(rssi_off & 0xff, 7) : 0;
278-
d->cal.rssi_offset[1] = field_valid(rssi_off >> 8)
278+
rssi_offset[1] = field_valid(rssi_off >> 8)
279279
? (int8_t)sign_extend_optional(rssi_off >> 8, 7) : 0;
280280

281281
/* mt76x02_get_lna_gain(): which LNA entry applies to this channel. */
@@ -290,8 +290,13 @@ void mt_read_rx_gain(struct mt7612u_dev *d, uint8_t chan, int band)
290290
uint16_t c1 = mt_ee(d, MT_EE_NIC_CONF_1);
291291
int ext = (band == 0) ? (c1 & MT_EE_NIC_CONF_1_LNA_EXT_2G)
292292
: (c1 & MT_EE_NIC_CONF_1_LNA_EXT_5G);
293-
d->cal.lna_gain = ext ? 0 : (int8_t)sign_extend(lna, 8);
293+
lna_gain = ext ? 0 : (int8_t)sign_extend(lna, 8);
294294
}
295+
/* One store: the RX event thread reads this word per frame, and the
296+
* three values only make sense together (see mt7612u_cal::rx_corr). */
297+
d->cal.rx_corr.store(mt_rx_corr_pack(rssi_offset[0], rssi_offset[1],
298+
lna_gain),
299+
std::memory_order_relaxed);
295300

296301
d->cal.mcu_gain = (uint32_t)(lna_2g & 0xff);
297302
d->cal.mcu_gain |= (uint32_t)(lna_5g[0] & 0xff) << 8;

‎src/mt7612u/internal.h‎

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
* because MSVC has no <pthread.h> and devourer builds Windows first-class.
2121
* Nothing outside this subtree includes this header; the public C ABI in
2222
* include/mt7612u/mt7612u.h is unaffected and stays C-includable. */
23+
#include <atomic>
2324
#include <chrono>
2425
#include <condition_variable>
2526
#include <mutex>
@@ -28,6 +29,7 @@
2829
#include <stddef.h>
2930
#include <stdio.h>
3031
#include "regs.h"
32+
#include "Mt7612uRxCorr.h"
3133
#include "include/mt7612u/mt7612u.h"
3234

3335
/* Per-rate TX power, 0.5 dB units, exactly mt76x02_rate_power's layout. */
@@ -46,8 +48,18 @@ struct mt_tx_power_info {
4648

4749
/* EEPROM-derived values the host computes with (firmware does the rest). */
4850
struct mt7612u_cal {
49-
int8_t rssi_offset[2];
50-
int8_t lna_gain;
51+
/* The per-channel RSSI correction (mt76x02_mac_get_rssi's two chain
52+
* offsets and the LNA gain), packed into one word so that a retune
53+
* publishes all three at once. mt_read_rx_gain() rewrites them on every
54+
* tune from the caller's thread while mt_rx_parse() reads them on the
55+
* libusb event thread for every frame; as three separate bytes a frame
56+
* parsed mid-retune got the new offset with the old LNA gain - a wrong
57+
* RSSI, which ThreadSanitizer reported against real hardware. One
58+
* relaxed atomic word is the whole fix: the reader sees either the old
59+
* triple or the new, never a mix, and the hot path pays one load.
60+
* Encode/decode with mt_rx_corr_pack()/mt_rx_corr_unpack()
61+
* (Mt7612uRxCorr.h). */
62+
std::atomic<uint32_t> rx_corr;
5163
int8_t high_gain[2];
5264
uint8_t init_cal_done;
5365
uint8_t channel_cal_done;
@@ -69,6 +81,7 @@ struct mt7612u_cal {
6981
* RX hot path does no cross-thread write at all. */
7082
};
7183

84+
#define MT_SYNC_POOL 4 /* pooled transfers for the sync helpers */
7285
#define MT_RX_RING 16
7386
/* 16 slots, not 32: the slots now carry a full aggregate, so this is the
7487
* difference between 256 KB and 512 KB of ring. Depth is not what buys
@@ -208,6 +221,16 @@ struct mt7612u_dev {
208221

209222
unsigned io_err; /* EP0 transfers that exhausted their retries */
210223
int transfers_stranded; /* libusb still owns a cancelled ring */
224+
/* libusb_transfer objects for the synchronous helpers (usb.cpp), taken
225+
* from here rather than allocated per call. Allocated in
226+
* mt_dev_state_init(), i.e. before any event thread exists, so the
227+
* thread's first lock of a transfer's mutex is ordered after its
228+
* initialisation by thread creation - a per-call allocation is ordered
229+
* only through the kernel's URB handoff, which ThreadSanitizer cannot
230+
* see and reports. Empty pool = allocate fresh (correct, just noisier). */
231+
std::mutex sync_pool_mu;
232+
struct libusb_transfer *sync_pool[MT_SYNC_POOL];
233+
int sync_pool_n;
211234
uint16_t max_mpdu_rx; /* from MT_MAX_LEN_CFG at init, less the FCS */
212235
uint64_t stats_last_us; /* previous mt7612u_link_stats() mark */
213236
int ch_time_armed; /* channel timers configured and zeroed */

‎src/mt7612u/rx.cpp‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -91,10 +91,18 @@ int mt_rx_parse(struct mt7612u_dev *d, uint8_t *buf, int n,
9191
* the EEPROM (mt76x02_mac_get_rssi); with them at zero these are the
9292
* raw chip values, which is still enough to compare two chains. */
9393
info->n_chains = (uint8_t)((d->chainmask & 0xf) > 1 ? 2 : 1);
94-
for (int c = 0; c < 4; c++)
95-
info->rssi[c] = (int8_t)((int8_t)rxwi[12 + c] +
96-
(c < 2 ? d->cal.rssi_offset[c] : 0) -
97-
d->cal.lna_gain);
94+
{
95+
/* One load of the packed triple: a retune on another thread
96+
* republishes all three at once, so this frame is corrected
97+
* consistently with either the old channel or the new one. */
98+
int8_t rssi_offset[2], lna_gain;
99+
mt_rx_corr_unpack(d->cal.rx_corr.load(std::memory_order_relaxed),
100+
rssi_offset, &lna_gain);
101+
for (int c = 0; c < 4; c++)
102+
info->rssi[c] = (int8_t)((int8_t)rxwi[12 + c] +
103+
(c < 2 ? rssi_offset[c] : 0) -
104+
lna_gain);
105+
}
98106

99107
for (int i = 0; i < 4; i++)
100108
info->bbp[i] = get_le32(rxwi + 16 + 4 * i);

‎src/mt7612u/tools/bringup.cpp‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1812,9 +1812,12 @@ static int gate_caps(uint8_t chan)
18121812
c.max_mpdu_tx, c.max_mpdu_rx,
18131813
mt_rr(&dev, MT_MAX_LEN_CFG) & 0xfff);
18141814

1815+
int8_t rssi_offset[2], lna_gain;
1816+
mt_rx_corr_unpack(dev.cal.rx_corr.load(std::memory_order_relaxed),
1817+
rssi_offset, &lna_gain);
18151818
printf("\nRX gain from EEPROM: rssi_offset=[%d,%d] lna_gain=%d "
18161819
"high_gain=[%d,%d] mcu_gain=0x%08x\n",
1817-
dev.cal.rssi_offset[0], dev.cal.rssi_offset[1], dev.cal.lna_gain,
1820+
rssi_offset[0], rssi_offset[1], lna_gain,
18181821
dev.cal.high_gain[0], dev.cal.high_gain[1], dev.cal.mcu_gain);
18191822
printf(" raw EEPROM: LNA_GAIN=0x%04x RSSI_OFF_5G_0=0x%04x "
18201823
"RSSI_OFF_5G_1=0x%04x GRP4_5_RX_HIGH_GAIN=0x%04x\n",

0 commit comments

Comments
 (0)