Devourer's RTL8733B backend supports the Wi-Fi function of the 1T1R, dual-band RTL8731BU/RTL8733BU family. It is based on the vendor's HALMAC 87xx path rather than any Jaguar or Kestrel implementation.
The supported USB identities are:
0bda:f72b, the Wi-Fi-only identity used by the tested RTL8731BU module.0bda:b733, the combo-module Wi-Fi function listed by the vendor driver.
An opened device must also report RTL8733B chip ID 0x16; a matching PID is
not accepted as sufficient identity.
The backend advertises one spatial stream, 2.4 and 5 GHz, legacy OFDM and HT MCS0–7 at 20 or 40 MHz. It also accepts all four long-preamble CCK rates on 2.4 GHz at 20 MHz; CCK is rejected on 5 GHz and at 40 MHz. Raw HT TX is deliberately forced to BCC. SGI, STBC, LDPC, VHT, 80 MHz, CCK short preamble, Bluetooth/coexistence controls and narrowband are not advertised.
That conservative surface follows both the hardware results and the vendor
HAL. The pinned 2023 HAL declares 802.11b/g/n, one TX and RX spatial stream,
and only BW_CAP_20M | BW_CAP_40M. It sets TX-LDPC true but RX-LDPC false;
devourer still reports both false because independently witnessed raw HT TX
only became reliable after clearing the global HT/VHT LDPC enable bits. The
newer 2024 vendor tree has the same 1SS, HT and 20/40 MHz declarations, and its
Fix VHT flags commit removes the accidentally enabled VHT build flag.
Every claim below rests on one physical unit: a bare unbranded 1T1R
RTL8731BU module, 0bda:f72b, RTL8733B chip ID 0x16, cut D, on USB high
speed. Independent witness: an RTL8812AU in monitor mode. Nothing here was
measured on an RTL8733BU — that chip is recognized from the vendor ID table and
rides the same code path, but no 0bda:b733 combo module was available.
The following was exercised through the normal WiFiDriver / IRtlDevice
factory path:
- USB discovery and endpoint selection; card enable and disable; firmware download, checksum and ready handshakes; HALMAC/MAC, EFUSE, BB and RF setup.
- Stable physical EFUSE reads, permanent MAC extraction, firmware-health and thermal telemetry.
- Ambient monitor RX on channels 6 and 36, including descriptor aggregation, PHY status, CRC reporting and clean shutdown.
- Independently captured raw TX on 2.4 and 5 GHz: legacy OFDM and HT MCS0–7, 20 MHz and HT 40 MHz. BCC was used throughout. On 2.4 GHz, an RTL8812AU witness also decoded long-preamble 1, 2, 5.5 and 11 Mbps CCK with valid FCS.
- Repeated warm initialization, concurrent RX/TX, bounded sustained TX, teardown during active RX/TX, malformed-input rejection and post-test adapter-doctor checks.
Per-rate delivery, witnessed by an RTL8812AU (0bda:8812) in monitor mode on
channel 6, 300 submitted frames per rate. Counted per frame from the witness's
own RX events, so these are exact rather than sampled:
| requested | delivered | decoded as | FCS errors |
|---|---|---|---|
| 1 Mbps CCK | 298 / 300 | 1 Mbps | 0 |
| 2 Mbps CCK | 300 / 300 | 2 Mbps | 0 |
| 5.5 Mbps CCK | 300 / 300 | 5.5 Mbps | 0 |
| 11 Mbps CCK | 294 / 300 | 11 Mbps | 0 |
| 6 Mbps OFDM | 300 / 300 | 6 Mbps | 0 |
| HT MCS0 | 270 / 300 | MCS0 | 0 |
| HT MCS7 | 277 / 300 | MCS7 | 0 |
Every frame decoded at the rate it was requested at, and no captured frame carried an FCS error at any rate. These are single probes, so per this repo's own guidance they are worth about +/-3 points each.
The two HT readings sitting below the legacy ones are not a transmitter
effect, and the witness was qualified rather than assumed
(tests/ground_station_qualify.sh exists for exactly this question). Sweeping
the RTL8812AU witness across the HT ladder, 300 frames each:
| rate | delivered | witness RSSI |
|---|---|---|
| MCS0 | 270 / 300 | 57.8 |
| MCS1 | 300 / 300 | 65.0 |
| MCS3 | 300 / 300 | 65.3 |
| MCS5 | 300 / 300 | 65.2 |
| MCS6 | 296 / 300 | 65.1 |
| MCS7 | 277 / 300 | 60.3 |
The ladder is flat at 100% through MCS5, so the witness is nowhere near its modulation cliff and is fit to measure this rate range — a receiver measuring itself would fall off progressively at the top, not dip at both ends. MCS0 is the most robust rate in the set, so its being the lowest reading rules out a cliff at either end. Both low readings also came with 5-7 units less RSSI than the flat middle, which places the variation in the per-run link conditions rather than in modulation. Treat the whole HT column as one flat band around 90-100% until something moves it by more than the probe noise.
A mixed run alternating all eight rates (1/2/5.5/11 Mbps CCK, 6 Mbps OFDM,
MCS0/4/7) submitted 250 frames, crossing the CCK/OFDM rate-class boundary on
every frame. The witness decoded all eight rate_hw values on air with valid
FCS, every frame at 20 MHz with STBC and LDPC clear, matching the forced-BCC
contract.
USB TX aggregation is ported and is the one host-side lever that matters on
a small SoC. send_packets packs up to three [txdesc][frame] blocks into one
bulk-OUT URB (DEVOURER_TX_USB_AGG, default 0 = off = byte-identical), the
HalMAC 3-descriptor rule that MAC init had already programmed as
BLK_DESC_NUM in DWBCN0_CTRL[7:4]. The block count rides the first
descriptor's dword7[31:24] and is written before the checksum, which folds
over byte 0x1f.
The cost it removes is host CPU, not air time. One bulk submission measured ~248 µs of CPU on the CV610 craft against ~22 µs on x86 — the same transfer, an 11× ratio — and a profile attributes ~87% of it to the kernel USB submit/completion path, with the per-byte term below noise on ARM. Packing 3:1 therefore removes two submissions in three: on the craft, 248 → 148 µs per frame and 43.0% → 26.7% of one core at ~1750 fps, frame rate unchanged (1733 → 1800). On x86 the same A/B reads 21.5 → 10.6 µs.
The failure mode this was checked against is the 8822BU's: wrong packing makes
the TXDMA re-air block 1 agg_num times, which frame counts cannot see. Each
frame was therefore stamped (DEVOURER_TX_QOS_DATA) and the stamps counted at
the witness (DEVOURER_RX_PCTR): 31552 receptions, 31551 distinct counters,
ratio 1.00 where re-airing would read 3.00. Note GetTxStats().submitted
counts URBs rather than frames, so an aggregated session reports about a third
— accounting, not throughput.
The boundary shim is verified separately, because a fixed-payload sweep
never reaches it. When a packed total lands on an exact bulk-MPS multiple the
planner inserts the 8-byte first-block PKT_OFFSET reserve (the sync bulk path
has no ZLP), and on the 8822BU a reserved first block is precisely what made
the TXDMA re-air block 1. For three blocks at USB HS that happens when the MPDU
length is ≡ 472 mod 512 — measured exactly, urb_bytes 1544 / 3080 / 4616 at
payloads 472 / 984 / 1496, all n x 512 + 8. Stamped at the witness with the
shim engaged: 15027 shim URBs, 44032 receptions, 44032 distinct, ratio 1.00
against 1.00 for the no-shim control. This part's block walker does account the
reserve.
RX aggregation is capped at 12 KiB (rtl8733b::kRxAggregateBytes8733b) because
the vendor default of 20 KiB exceeds one bulk-IN URB: a 60-second high-traffic
receive run had xHCI split such an aggregate, leaving a descriptor tail and its
body in separate completions. The RX loop floors its URB size at the same
constant, so DEVOURER_RX_URB_BYTES cannot reintroduce the straddle from the
other side. Five repeated high-traffic channel-36 runs with the cap in place
completed without malformed descriptors, aggregate mismatches or parser aborts.
A separate ten-cycle channel-6/channel-36 warm lifecycle run passed EFUSE,
firmware-ready, initialization, thermal, rollback and card-disable gates.
tests/rtl8733b_lifecycle_soak.sh makes that warm test reproducible and labels
it explicitly as distinct from a true VBUS cycle.
Thermal readings are telemetry, exactly as on every other generation:
GetThermalStatus() exposes the RF 0x42 meter against the EFUSE baseline,
InitWrite logs one bring-up snapshot, and DEVOURER_THERMAL_POLL_MS emits
thermal events at the caller's cadence. Nothing in the send path reads the
meter — a per-frame read would be several USB control transfers per frame, and
the meter is a PA-bias tracking index rather than a calibrated junction
temperature, so it is not sound input for a TX gate
(docs/warm-tx-degradation.md). During the mixed CCK/OFDM witness run the raw
code stayed at 32–33 against baseline 32 across 650 submitted frames; 636 were
captured with valid FCS across all five requested rates. A separate 1,000-frame
run alternating CCK and OFDM submitted 1,000/1,000; the witness captured 660
canonical frames across both rates, all with valid FCS, while the same 32–33
thermal range held. Both runs are rate-coverage and thermal evidence only —
neither says anything about TX-power tracking, and at these deltas (+0 to +1)
the two thermal-compensation curves are the same bits anyway.
On a unit whose EFUSE is in TSSI-offset PG mode — the tested RTL8731BU is —
closed-loop TSSI is the TX-power control, so it runs by default. It is not
optional there: the flat fallback index (kSafeTxAgcIndex8733b) is a
conservative bring-up value, and on air it runs cold enough that HT rates do
not survive the link. Measured with the witness, 300 frames submitted at MCS7
with the flat index: zero captured, twice. A unit whose EFUSE carries no TSSI
calibration has nothing to drive the loop and takes the flat path.
Because the loop is the power control, the runtime knob is the loop's target.
IRtlDevice::SetTxPowerOffsetQdb shifts every per-rate target that survives
the safe ceiling — cap first, then shift, so the calibrated per-rate spread
comes through the move intact — and writes it as the five packed dwords at
0x3a00..0x3a10, in place, with tracking left enabled. That is the same
shape fast_retune uses for its per-channel rewrite (#389); the alternative,
a TSSI disable/re-enable pair, costs ~165 ms and buys nothing here.
The capability report is the dBm model — index_max = 0, one qdB per step —
over [-128, +127]. Offset 0 is kSafeTssiTargetQdbm8733b (16 dBm), a
first-light clip sitting at or below this part's factory targets (18–20 dBm at
2.4 GHz, 16–19 at 5 GHz).
That range is asymmetric, and only one end is a hardware limit. The
0x3a00..0x3a10 bytes hold a signed int8 offset from the 64 qdBm anchor, so
the field itself spans [−128, +127] — targets from −16 dBm to +47.75 dBm.
Both ends are that field, and neither is re-clamped at something softer — the same clamped-only-at-the-hardware-rail answer Jaguar1 (±126) and Jaguar3 (±127) give. Where the chip stops responding is measured and written down below rather than enforced in the setter, because on this part both ends stop responding well inside the field and at points only a bench can find.
The positive half is deliberately not re-clamped at the factory table.
src/TxPower.h states for every family that headroom above the generated
table is the operator's call, and a per-unit EFUSE trimmed too cold is exactly
what a bench calibration exists to correct — clamping here would make this the
one backend where a measured operating point cannot be commanded. What that
costs is measured below.
The offset is sticky: a full SetMonitorChannel re-folds it against the new
channel's targets, and FastRetune carries it into the hop's in-place rewrite.
The flat-index and per-rate-diff knobs are not ported. The flat index has no measured dB-per-step slope on this part and, as above, cannot carry HT.
Measured on air. Two independent 6-point passes at ch36 against an
RTL8812AU witness (chip-RSSI ground station — the SDR saturates at this range,
see tests/txpwr_offset_onair.sh): the lever is monotone and worth
14.2 / 14.8 dB of received power for the full 16 dB of command, overall
slope 0.222 / 0.231 dB per qdB against the 0.25 nominal. Its adversarial
counterpart, in the same breath: the step is not constant. The bottom 12 qdB
delivered 0.125 and 0.126 dB/qdB — the only structure that reproduced
exactly across both passes — while everything above −52 qdB ran 0.233..0.242
and the mid-range scattered 0.219..0.252 between passes. Near the floor the
loop returns about half the dB commanded, and saturated_low does not warn
about it (that clamp fires only at −64). TxPowerCaps::step_measured
therefore stays false, the same call the 8822E gets for the same reason: a
controller should calibrate its own dB-per-qdB, or close the loop on the
ground's RSSI.
The control that makes those numbers readable: the pre-change binary, in the
same session and geometry, measured flat — 0.3 dB across the same 64 qdB of
command, because it had no actuator to move. One unit, one witness, near-field
geometry, integer-quantised RSSI, no SDR. Harnesses:
tests/rtl8733b_txpwr_regcheck.sh (registers, including that null control),
tests/txpwr_offset_onair.sh (slope), tests/rtl8733b_txpwr_selftest.cpp
(the offset math, in ctest).
An earlier cut of this work clamped the offset at −64 qdB, reasoning that a target below 0 dBm was meaningless. Sweeping past it says otherwise (MCS0, ch36):
| offset | target | witness RSSI | witness EVM |
|---|---|---|---|
| −32 | 8.0 dBm | 69.40 | −60.8 |
| −64 | 0.0 dBm | 62.07 | −59.3 |
| −80 | −4.0 dBm | 54.80 | −54.0 |
| −96 | −8.0 dBm | 52.96 | −53.1 |
| −112 | −12.0 dBm | 52.97 | −52.8 |
| −128 | −16.0 dBm | 52.99 | −52.8 |
Power keeps falling past the 0 dBm target — another 7.3 dB between it and −4 dBm — with no sign wrap, and pins from about −96 qdB, where three successive rungs agree within 0.03 dB. EVM softens from −59 to −53 across the whole extension and the link stays decodable throughout. So usable travel is ~23 dB below the clip rather than the ~16 dB the first cut allowed, which matters for near-field bench work and for a link that wants to sit quiet.
Sweeping the other way — up from the clip, MCS0 at ch36, with the witness reporting EVM beside RSSI:
| offset | witness RSSI | witness EVM | witness SNR |
|---|---|---|---|
| 0 | 75.4 | −62.0 | 62.0 |
| +16 (top of the PG table) | 78.2 | −50.2 | 63.7 |
| +32 | 84.1 | −18.0 | 57.9 |
| +48 | 83.7 | −18.0 | 58.1 |
| +64 | 83.8 | −18.0 | 58.2 |
The +16 rung is real gain, +2.8 dB, though the constellation has already given
up 12 dB of EVM to get it. The +32 rung is not gain: the witness hears
8.7 dB more energy while EVM collapses to −18, and +48 and +64 change nothing
at all — RSSI and EVM both pinned. That is the PA in hard compression, and
SNR never saw it (58..64 throughout), which is the failure mode
docs/bench-testing-near-field.md exists to warn about: strong RSSI plus poor
EVM means back power off, the opposite of the weak-link response.
Two things follow. The vendor's PG table lands about where this part stops being linear, so +16 qdB is the edge of usable overdrive even though the API allows +127 — a caller sweeping for its own operating point should watch EVM, not RSSI, and stop where EVM turns. And the negative half is clean by comparison: EVM sits flat at −58..−61 across all 16 dB of backoff.
The chip keeps two thermal-compensation curves, one for CCK and one for
OFDM/HT. The table is chosen once per channel set from the configured TX mode
and then left alone, which is what the vendor driver does —
_halrf_tssi_set_tmeter_tbl_8733b is only ever called from full TSSI setup,
keyed on phydm_get_tx_rate at that instant, and never re-selected at runtime.
There is deliberately no runtime switch, and the reason is worth recording so nobody adds one back.
The two curves are bit-identical for thermal deltas 0..+17 and first differ
at +18 — the swing ramp starts there, and the words either side of the
baseline are zero in both tables. A five-minute max-duty MCS7 soak on the
validation unit plateaued at +8 after two minutes and stopped climbing,
less than half the delta needed for the tables to differ by a single entry. So
on this silicon a runtime switch would spend 84 ms — 136 USB register round
trips, inside send_packet, capping a mixed-rate stream near 11 fps — to
install a table bit-identical to the one already loaded.
An opt-in knob for it was written, measured on air and then deleted on that evidence. Room temperature agreed too: 300 frames of 1 Mbps CCK delivered 300/300 with the OFDM table loaded and 299/300 with the CCK table, zero FCS errors either way. OpenIPC/devourer#389 has the cost breakdown and a validated in-place alternative (13.8 ms, tracking left enabled) — if a future board does reach +18, that is the implementation to revisit, not the teardown/rebuild.
A related property of the closed loop, worth knowing before reading power numbers from a fast rate-switching run: TSSI needs settling time. Alternating CCK and OFDM at roughly 9 ms per frame leaves CCK airing about 5 dB above its settled level, and the same stream paced to 86 ms per frame lands on the settled value. That is the tracking loop converging, not a descriptor or table-programming effect.
SetCcaMode(false) — the universal carrier-sense-enabled default, and the
state this backend's MAC bring-up leaves programmed — succeeds as a no-op.
SetCcaMode(true) (DEVOURER_DIS_CCA) throws: the HALMAC 87xx carrier-sense
gate has not been located and measured here, and the backend refuses rather
than silently pretending. The refusal leaves the session running.
The construction-time form of the same knob — DeviceConfig::tuning::disable_cca,
which every other generation applies during bring-up — cannot be honoured
either, and bring-up emits one warning saying so rather than dropping it
silently or failing to initialise. Silence is the only outcome that would let a
refused knob look like a granted one; a hard failure would be a harsh answer to
a request a caller may be making only through an inherited environment.
These results have not been claimed:
- No SDR was available, so occupied bandwidth, spectral mask, EVM and absolute output power were not measured. The runtime power lever's slope is a relative witness-RSSI measurement for the same reason: it says the lever moves ~14 dB, not what any rung radiates in dBm.
- The TX-power offset's dB-per-qdB is not constant across its advertised range
(0.125 in the bottom 12 qdB vs 0.233..0.242 above −52), so
step_measuredstays false and a controller must calibrate its own slope. RaisingkSafeTssiTargetQdbm8733babove 16 dBm is deferred — that wants a conducted measurement, not a witness receiver. - The experimental 5/10 MHz sequence has register-readback and normal-path RX
evidence only. Narrowband TX and decode by an independent narrowband peer are
deferred;
AdapterCaps::narrowband_okremains false. - No physical
0bda:b733combo module was available. Its USB identity is taken from the vendor driver, but Bluetooth and coexistence are out of scope. - Secure-firmware variants were not exercised. The tested cut-D device used the normal NIC image.
- The test hub cannot switch VBUS, so repeated true power-off cold boots were not automated. Physical unplug/replug and repeated warm bring-up were tested.
- SGI remains disabled: a descriptor with its short-GI bit set submitted successfully, but an independent RTL8812AU decoded both broadcast and unicast probes as long GI. STBC and LDPC remain rejected. ACK/BlockAck response, A-MPDU and throughput were not independently validated and remain unadvertised or unsupported by this backend.
- Fast retune is now ported and independently witnessed (intra-band,
same-width;
src/rtl8733b/CLAUDE.mdhas the measured contract and its counterparts): channel-state readback parity 7/7 hops, a 299/300 post-hop burst decoded by an RTL8812AU witness, ~55 ms call / ~10 ms p50 radio-live on this unit. The channel-bucketed TSSI-DE offsets are rewritten in place across the hop with tracking live (the first cut skipped them — caught in review): DE readback parity 22/22 across ch1→ch13 hops, tracking-enable field held at 7 throughout, settle p50 unchanged at 10.4 ms. Radiated power across a hop remains SDR-gated like every other RF-domain quantity here. - A vendor-kernel-driver cross-test was attempted with the 2024 source on Linux
6.14, but its module did not build: its Makefile omitted required include
paths, and after supplying the main include path compilation stopped on an
incompatible
MODULE_IMPORT_NSuse and a missingplatform_ops.h. No vendor module was loaded, so no vendor-driver parity result is claimed.
A second RTL8731BU f72b unit — the same chip the surviving DUT is, so it
would have covered the same README row — passed early identity, EFUSE and
firmware bring-up checks, then stopped enumerating and overheated while
attached to a custom power supply. It is excluded from all radio-validation
claims, and the RTL8731BU row therefore rests on a single surviving sample: a
one-unit result, not a population.
The experimental 5/10 MHz path ports the later workaround in
rtl8733bu-20230626: program the RF as 20 MHz, then apply the small-bandwidth
BB clock fields after the RF writes. Both 5 and 10 MHz register transitions
read back on the tested device. The vendor's published capability mask remains
20/40 MHz, however, and the workaround itself describes a no-RF-output failure
without its final writes. Consequently the code is retained for further work
but the public capability mask stays at 20/40 MHz until SDR occupied-bandwidth
and independent narrowband TX/RX tests are available.
Firmware and parameter tables are reproducibly extracted from
libc0607/rtl8733bu-20230626
commit 9e5f6845b4e3393207720fc1886668bc80b6af1c, pinned as the
reference/rtl8733bu-20230626 submodule (reference/README.md). That tree is
also the source of the halmac_reg2.h and power-sequence constants transcribed
into src/rtl8733b/, so both the generated artifacts and the hand-carried
constants are re-verifiable from a fresh checkout. The extraction tools pin the
source-file hashes, output sizes and content hashes:
git submodule update --init reference/rtl8733bu-20230626
python3 tools/extract_8733b_fw.py --check
python3 tools/extract_8733b_tables.py --checkBoth accept --input / --source-root to point at a tree elsewhere.
The implementation was also compared with
libc0607/rtl8733bu-20240806
commit 2ec19e154cffbc2abd98d43d59278dffa6e50d49; it was not used to silently
replace the firmware or tables already validated on the device. The review
covered the later active-RF-path TSSI selection (already matched here), SRRC
band-edge setup, WLAN/Bluetooth GPIO pull-up changes, power-saving and C2H/TX
tasklet fixes. None justified replacing the pinned firmware/tables or widening
the raw-monitor capability claims above.
The backend covers the USB-first HALMAC 87xx scope of OpenIPC/devourer#233. It does not adopt that issue's provisional VHT/80 MHz assumptions, because both vendor trees describe this silicon as HT-only and 20/40 MHz.