Skip to content

rtl8733b: CCK<->OFDM TSSI table switch costs 84 ms (136 USB register ops) inside send_packet #389

Closed

Description

@snokvist

Summary

On the RTL8733B backend, switching the TSSI thermal table between the CCK and OFDM/HT variants costs 84 ms. select_tssi_rate_table runs inside send_packet, so a stream that alternates CCK and OFDM rates is capped at ~11.5 fps.

This is latent, not a live regression. select_tssi_rate_table early-returns when the rate class is unchanged, so a single-rate stream — the normal FPV/video case — pays nothing. It predates #388 and is not caused by it.

Where the 84 ms goes

Measured on the DUT (bare unbranded RTL8731BU module, 0bda:f72b cut D, USB high speed) with an instrumented build counting every BB register access and timing each stage. 60 consecutive switches, steady state:

stage ms reads writes ops
enable_tssi_tracking 44.99 35 35 70
disable_tssi_tracking 25.77 13 34 47
prepare_tssi_thermal 13.50 2 17 19
total 84.26 50 86 136

There are no sleeps in the path — it is 136 USB control transfers. build/reglat on the same adapter measures a register round trip at mean 375 us / p99 407 us, and the mixed read/write traffic here averages ~620 us/op.

The structural cause: to change 16 BB dwords, the backend tears down and rebuilds the entire TSSI front-end. 32 of the 136 ops are the 16-command ANAPAR sequence (write_tssi_anapar), written out by restore_tssi_analog and written straight back by apply_tssi_analog. The channel and band do not change across a rate-table switch, so that analog state is restored only to be re-applied microseconds later.

The premise looks wrong

Rtl8733bDevice::select_tssi_rate_table says the table "cannot be changed while closed-loop tracking is enabled". The vendor's own writer disagrees: _halrf_tssi_set_tmeter_tbl_8733b (hal/phydm/halrf/rtl8733b/halrf_tssi_8733b.c in the pinned reference/rtl8733bu-20230626) writes R_0x4380 and the 16 dwords at 0x4200..0x423c directly and touches no enable bit.

Caveat, stated plainly: the vendor only ever calls it from the full _halrf_tssi_8733b setup sequence, so there is no vendor precedent for a runtime swap in either direction. The vendor picks a table once at TSSI setup from whatever phydm_get_tx_rate returns and leaves it.

The hardware accepts an in-place rewrite

Throwaway build, writing the table with tracking live and reading it back:

  • 101/101 swaps readback-verified across all 16 words against the rate-specific plan.
  • 0x4318 stayed at 7 (tracking enabled) across every swap.
  • 13.77 ms / 20 ops, against 84.26 ms / 136 ops — a 6.1x reduction.

The two plans genuinely differ (8 of the 16 words), so the readback was tracking a real change rather than a no-op.

Why this is not already a PR

TSSI is closed-loop PA control. Register readback proves the write lands; it does not prove the tracking loop behaves correctly across a live table change. Validating that needs an independent witness and ideally an SDR — neither was available — and the failure mode of getting it wrong is transmitting at an uncalibrated power setting.

Suggested work

  1. Add a table-swap-only path to select_tssi_rate_table that writes 0x4380 + 0x4200..0x423c in place and readback-verifies, skipping the disable/enable pair.
  2. Gate it behind a DeviceConfig field until validated, defaulting to the current conservative path.
  3. Validate with a witness at both rate classes across repeated swaps, and an SDR power check either side of a swap to confirm the closed loop settles to the same output power as the teardown path.
  4. If it holds, make it the default and drop the comment claiming the table cannot be changed while tracking is enabled.

Separately: write_tssi_anapar is 16 dwords written twice per switch. If the in-place path lands, that cost disappears with it; if it does not, the restore/re-apply pair is worth revisiting on its own.

Reproducing

# alternating CCK/OFDM so every frame forces a table switch
sudo DEVOURER_PID=0xf72b DEVOURER_CHANNEL=6 DEVOURER_TX_RATE=1M \
     DEVOURER_TX_MCS_SWEEP="1M,6M,2M,MCS0,5.5M,MCS4,11M,MCS7" \
     DEVOURER_TX_MCS_STEP_MS=17 DEVOURER_TX_GAP_US=2000 \
     DEVOURER_TX_FRAMES=500 ./build/txdemo

500 frames takes ~45 s (~87 ms/frame). The single-rate control — same command without DEVOURER_TX_MCS_SWEEP — takes ~3.5 s (~2.6 ms/frame).

Note that on master the per-switch rtl8733b.tssi_thermal_table log cannot show whether the swap took effect: it prints words 0 and 15, which are identical in both tables. #388 changes it to report the words that differ plus a fold over all 16.

Activity

  1. snokvist commented on Aug 13, 2026

    @snokvist
    CollaboratorAuthor

    Validated on air — the in-place path produces the same output power

    The blocker in the issue body was "needs an independent witness and ideally an SDR". A witness is now available (RTL8812AU 0bda:8812, monitor mode, channel 6), so here is the A/B.

    Method: one binary, the in-place path selected by env var, so the only difference is the code path. Alternating 1M,6M so every frame forces a table switch, 200 frames per arm, arms interleaved to cancel drift. Per-frame RSSI from the witness (DEVOURER_RX_DUMP_ALL=1, filtered to the injected frames), which is a relative power proxy, not calibrated power.

    First pass — a 5 dB discrepancy on CCK:

    arm ms/frame 1M CCK rssi 6M OFDM rssi
    teardown-1 86.00 52.10 59.54
    in-place-1 9.00 57.07 58.88
    teardown-2 86.01 50.40 57.82
    in-place-2 9.01 55.29 57.32

    Reproducible, same direction both reps, CCK only. Delivery was ~100% in all four arms with zero FCS errors, so nothing was being lost — but ~5 dB is a lot of transmit power to move, and it is exactly the failure this issue said register readback could not rule out.

    It is not the path — it is settling time. The two arms do not just differ in code, they differ in cadence: 86 ms/frame versus 9 ms/frame. Re-running the in-place path paced to the same 86 ms frame period:

    arm ms/frame 1M CCK rssi 6M OFDM rssi
    teardown-1 / -2 86.00 / 86.01 52.10 / 50.40 59.54 / 57.82
    in-place-slow-1 / -2 86 52.06 / 50.40 60.22 / 59.14

    CCK matches the teardown path to within 0.04 on one rep and exactly on the other. So given equal settling time the two paths land on the same output power, and the earlier gap was the TSSI closed loop not having converged at 9 ms/frame.

    That is a useful finding in its own right, and it is a property of the loop rather than of the swap method — the current path simply cannot switch fast enough to expose it. Any implementation should document it, and anyone reading power off a fast rate-switching run should know about it.

    Where that leaves the issue

    Evidence now in hand for the in-place path:

    • Register readback: 101/101 swaps verified across all 16 words, 0x4318 steady at 7.
    • On-air delivery: ~100%, zero FCS errors, both rate classes.
    • Output power: matches the teardown path at equal cadence, two repetitions, both rates.
    • Cost: 13.8 ms / 20 ops against 84.3 ms / 136 ops.

    Still missing, and still needing an SDR:

    • Absolute power correctness. RSSI shows the two paths agree; it cannot show both are right. Agreeing with the previously validated path is the right bar for a refactor, but it is not an absolute measurement.
    • Occupied bandwidth and spectral mask across a live swap. Low risk — a thermal table drives power tracking, not modulation — but unmeasured.
    • Long-run thermal tracking: whether the loop still tracks temperature correctly over a long session of in-place swaps.

    So I would not make it the default yet. Implementing it behind a DeviceConfig field defaulting to the current conservative path — step 2 of the plan in the issue body — is now justified by evidence, and steps 3 and 4 stay blocked on SDR access.

    Not closing this: the change is not implemented, and the SDR checks remain outstanding.

  2. josephnef commented on Aug 13, 2026

    @josephnef
    Collaborator

    A fix for this exists but is not pushed yet — reported on #388 after the merge.

    Root cause is upstream of the cost: _halrf_tssi_set_tmeter_tbl_8733b is only ever reachable from full TSSI setup in the vendor tree, keyed on phydm_get_tx_rate at that instant, and is never re-selected at runtime. The 84 ms was paying for a mechanism that does not exist upstream. Choosing the table once per channel set and leaving it alone takes an alternating CCK/OFDM stream from 19.69 s to 2.82 s per 200 frames, with 200 switches becoming 0.

    Two things to carry into the PR rather than lose here:

    • The first attempt made TSSI itself opt-in with a flat-AGC fallback, and that path cannot carry HT at all — MCS7 at 0/300, twice, on air. On a TSSI-offset PG unit the closed loop is the power control, so only the table switching can be optional. A register-readback test would have called that path green.
    • Fixing the table appears free at room temperature (300/300 vs 299/300 for 1 Mbps CCK), because the CCK and OFDM curves differ only in the swing ramp away from baseline. Whether they diverge at elevated thermal delta is unmeasured — which is the only thing that would justify keeping runtime switching behind a knob.

    Leaving open until the change lands.

  3. snokvist commented on Aug 13, 2026

    @snokvist
    CollaboratorAuthor

    #396 removes the cost from the default path: the thermal table is now chosen once per channel set instead of on every rate-class crossing, matching what the vendor actually does. Alternating CCK/OFDM drops from 19.69 s to 2.82 s per 200 frames, 200 switches to 0.

    Two things that survive this and are worth keeping the record straight on, since the PR closes the issue:

    The 84 ms is still real on the opt-in path. DEVOURER_TSSI_RATE_TABLE=1 pays exactly the measured cost. If that knob survives the heat-soak question in #390, the validated in-place alternative in the comment above (13.8 ms / 20 ops, 101/101 readback-verified, equal output power at equal pacing) is what makes it affordable. If the knob is deleted instead, the in-place work goes with it.

    Channel changes are untouched by #396 and still pay. SetMonitorChannel tears the loop down and runs a full configure_tx_power rebuild, which is a superset of the table swap, not the same thing — the in-place trick does not help there because it is a full TSSI re-setup, not a 16-dword rewrite. Nobody has measured that path; the 84 ms figure here is the table switch only, so do not quote it for retune cost.

    Happy for this to close on merge, but if you would rather keep it open scoped to the channel-change path, that is a defensible read too.

  4. added a commit that references this issue on Aug 15, 2026
    4b66e62
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions