diff --git a/CLAUDE.md b/CLAUDE.md index 37a2dc4a..ef0a3cf8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -289,10 +289,21 @@ the rails; flags in `GetTxPowerState`); `SetTxPowerIndexOverride(idx)` forces/clears a flat index. Both apply live and stick across `SetMonitorChannel` (re-folded on the new channel) and `FastRetune` (never rewrites TXAGC). `GetTxPowerCaps` reports the family step: 0.5 dB Jaguar1/2, -0.25 dB Jaguar3. Write-only TXAGC hardware (Jaguar2, the 8814A's packed port) -reports the software shadow (`hw_readback=false`). `txpower` -(examples/txpower/) is the reference consumer; register-level validation: -`tests/txpwr_offset_regcheck.sh`. +0.25 dB Jaguar3. The Jaguar2 TXAGC block and the 8814A's packed port are +write-only, so their `GetTxPowerState` reports the software shadow +(`hw_readback=false`). `SetTxPowerRateDiffs(optional)` is the +8822E-only third knob: a caller-supplied per-rate diff table (cck, legacy, +mcs0..7, signed qdB) that replaces the default `phy_reg_pg` per-rate walk, +folded on top of whichever reference (efuse table or flat override) is +active. It sticks across `SetMonitorChannel`, `FastRetune`, and an +override set/clear round trip (`SetTxPowerIndexOverride`'s clear path does a +full re-apply that re-walks the caller table); `std::nullopt` restores the +default walk, and every other generation's `SetTxPowerRateDiffs` just +returns `false`. `txpower` (examples/txpower/) is the reference consumer — +`--rate-diffs cck,legacy,m0..m7|clear` for this knob, `--offset-start`/ +`--offset-stop` and `--flat` for the other two; register-level validation: +`tests/txpwr_offset_regcheck.sh` (offset/override) and +`tests/txpwr_rate_diffs_regcheck.sh` (diff table). **Per-packet TX power** (a radiotap `DBM_TX_POWER` dB-delta per frame, zero USB cost once armed; see the `per_pkt_txpwr_*` caps): Jaguar2 and the 8814A diff --git a/CMakeLists.txt b/CMakeLists.txt index 09f7a053..636d896c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -791,6 +791,16 @@ target_link_libraries(TxPowerQuantSelftest PRIVATE devourer) add_test(NAME txpower_quant_math COMMAND TxPowerQuantSelftest) +# Headless guard for the per-rate TX-power diff packing (src/TxPower.h) — the +# pack_rate_diff_word helper used to format 0x3a00-table entries, so a packing +# regression fails `ctest` instead of only surfacing as wrong per-rate TXAGC. +add_executable(RateDiffsSelftest + tests/rate_diffs_selftest.cpp +) +target_link_libraries(RateDiffsSelftest PRIVATE devourer) + +add_test(NAME rate_diffs COMMAND RateDiffsSelftest) + # Headless guard for the link-health classifier (src/LinkHealth.h) — the # sensor-tuple -> verdict mapping behind , with cases # drawn from real on-air data (the saturation-knee + AWGN-interference sweeps). diff --git a/examples/txpower/main.cpp b/examples/txpower/main.cpp index b637fe0e..ba3817ef 100644 --- a/examples/txpower/main.cpp +++ b/examples/txpower/main.cpp @@ -2,10 +2,10 @@ * * Opens one adapter, brings it up for TX, prints the family's TxPowerCaps, * then walks the requested knob sequence — a quarter-dB offset ramp - * (SetTxPowerOffsetQdb), a flat index (SetTxPowerIndexOverride), or both — - * echoing the applied qdB and a TxPowerState snapshot after every step. This - * is the shape of an adaptive-link controller's power leg: set, read back, - * observe saturation, react. + * (SetTxPowerOffsetQdb), a flat index (SetTxPowerIndexOverride), per-rate + * diffs (SetTxPowerRateDiffs), or both — echoing the applied qdB and a + * TxPowerState snapshot after every step. This is the shape of an adaptive-link + * controller's power leg: set, read back, observe saturation, react. * * Pure CLI configuration (no environment variables — the API is the point): * @@ -17,6 +17,14 @@ * --offset-stop Q offset ramp stop, qdB (default = start) * --step-qdb Q ramp increment, qdB (default 4 = 1 dB) * --step-ms N dwell per step, ms (default 500) + * --rate-diffs I,I,...,I 10 comma-separated qdB per rate + * (cck,legacy,m0..m7) or 'clear' to nullopt + * --flat-pulse N after --rate-diffs: force flat index N, dump + * state, then clear the override (-1) and dump + * state again — proves a flat override + * temporarily flattens the chip's per-rate + * table and the configured diffs come back once + * the override clears, all in one process * --switch-channel N after the ramp: SetMonitorChannel(N) and re-dump * state — proves the offset is sticky across a * full channel set (and re-folds against the new @@ -30,7 +38,7 @@ * * {"ev":"txpwr.caps","supported":1,"max":63,"step_qdb":2,...} * {"ev":"txpwr.state","flat":-1,"offset_qdb":-24,"steps":-12,"satlo":0, - * "sathi":0,"cck":28,"ofdm":34,"mcs7":30,"rb":1} + * "sathi":0,"cck":28,"ofdm":34,"mcs7":30,"rb":1,"rate_diffs":0} */ #ifdef _WIN32 #define NOMINMAX @@ -47,6 +55,7 @@ #include #include #include +#include #include #include @@ -84,6 +93,11 @@ struct Args { int switch_channel = -1; int retune = -1; bool thermal = false; + bool have_rate_diffs = false; + bool rate_diffs_clear = false; + devourer::TxRateDiffsQdb rate_diffs; + int flat_pulse = 0; + bool have_flat_pulse = false; }; bool parse_int(const char *s, int &out) { @@ -126,6 +140,49 @@ bool parse_args(int argc, char **argv, Args &a) { ; else if (k == "--thermal") a.thermal = true; + else if (k == "--rate-diffs") { + if (i + 1 >= argc) + return false; + const std::string val = argv[++i]; + a.have_rate_diffs = true; + if (val == "clear") { + a.rate_diffs_clear = true; + } else { + /* Parse and range-check the 10 ints here so a typo fails before the + * chip is ever brought up (every other flag validates in parse_args). + * Range is the library's [-64, 63] — a bare strtol->int8_t cast would + * silently truncate (200 -> -56), so reject out-of-range with a real + * error rather than clamp-and-surprise. */ + int v[10] = {0}; + int n = 0; + const char *p = val.c_str(); + char *end = nullptr; + while (n < 10) { + v[n++] = static_cast(std::strtol(p, &end, 10)); + if (*end != ',') + break; + p = end + 1; + } + if (n != 10 || *end != '\0') { + std::fprintf(stderr, "devourer [E] --rate-diffs wants 10 comma ints " + "(cck,legacy,m0..m7) or 'clear'\n"); + return false; + } + for (int j = 0; j < 10; ++j) { + if (v[j] < -64 || v[j] > 63) { + std::fprintf(stderr, "devourer [E] --rate-diffs value %d out of " + "range [-64, 63]\n", + v[j]); + return false; + } + } + a.rate_diffs.cck = static_cast(v[0]); + a.rate_diffs.legacy = static_cast(v[1]); + for (int j = 0; j < 8; ++j) + a.rate_diffs.mcs[j] = static_cast(v[2 + j]); + } + } else if (k == "--flat-pulse" && next(a.flat_pulse)) + a.have_flat_pulse = true; else { std::fprintf(stderr, "devourer [W] unknown/incomplete arg: %s\n", k.c_str()); @@ -159,7 +216,8 @@ void print_state(IRtlDevice *dev, bool with_thermal) { .f("cck", s.cck_index) .f("ofdm", s.ofdm_index) .f("mcs7", s.mcs7_index) - .f("rb", s.hw_readback ? 1 : 0); + .f("rb", s.hw_readback ? 1 : 0) + .f("rate_diffs", s.rate_diffs_custom ? 1 : 0); if (with_thermal) { const devourer::ThermalStatus t = dev->GetThermalStatus(); devourer::Ev ev(*g_ev, "thermal"); @@ -257,6 +315,26 @@ int main(int argc, char **argv) { print_state(dev.get(), a.thermal); } + if (a.have_rate_diffs) { + if (a.rate_diffs_clear) { + const bool ok = dev->SetTxPowerRateDiffs(std::nullopt); + logger->info("rate-diffs clear -> {}", ok ? "ok" : "unsupported"); + } else { + const bool ok = dev->SetTxPowerRateDiffs(a.rate_diffs); + logger->info("rate-diffs -> {}", ok ? "applied" : "unsupported"); + } + print_state(dev.get(), a.thermal); + } + + if (a.have_flat_pulse) { + dev->SetTxPowerIndexOverride(a.flat_pulse); + logger->info("flat pulse -> {}", a.flat_pulse); + print_state(dev.get(), a.thermal); + dev->SetTxPowerIndexOverride(-1); + logger->info("flat pulse cleared"); + print_state(dev.get(), a.thermal); + } + if (a.have_ramp) { const int dir = (a.offset_stop >= a.offset_start) ? 1 : -1; const int inc = (a.step_qdb > 0 ? a.step_qdb : 4) * dir; diff --git a/src/IRtlDevice.h b/src/IRtlDevice.h index 258e7baf..b0b3d4cf 100644 --- a/src/IRtlDevice.h +++ b/src/IRtlDevice.h @@ -4,6 +4,7 @@ #include #include #include +#include #include "AdapterCaps.h" #include "AmpduMode.h" @@ -139,6 +140,17 @@ class IRtlDevice { * without the API wired ignores it (caps.supported=false). */ virtual void SetTxPowerIndexOverride(int idx) { (void)idx; } + /* Program caller-supplied per-rate TXAGC diffs (signed qdB vs the + * reference anchor) in place of the chip's default by-rate table. + * std::nullopt clears back to the default. Applies live; persists across + * SetMonitorChannel / FastRetune and override-set/clear (Runtime TX power + * family contract). Returns false where unsupported (everything except + * the 8822E). */ + virtual bool SetTxPowerRateDiffs(const std::optional& diffs) { + (void)diffs; + return false; + } + /* Re-program the TX-power registers from the current knob state at the * CURRENT channel — the hook tests use to force a re-apply without moving * any knob. Returns false when unsupported or the chip isn't brought up. */ diff --git a/src/TxPower.cpp b/src/TxPower.cpp index bd0e3b01..4b623d27 100644 --- a/src/TxPower.cpp +++ b/src/TxPower.cpp @@ -26,4 +26,12 @@ int quantize_offset_qdb(int qdb, const TxPowerCaps &caps, int *steps_out) { return steps * step; } +uint32_t pack_rate_diff_word(int8_t d0, int8_t d1, int8_t d2, int8_t d3) { + const uint32_t b0 = static_cast(d0) & 0x7fu; + const uint32_t b1 = static_cast(d1) & 0x7fu; + const uint32_t b2 = static_cast(d2) & 0x7fu; + const uint32_t b3 = static_cast(d3) & 0x7fu; + return b0 | (b1 << 8) | (b2 << 16) | (b3 << 24); +} + } // namespace devourer diff --git a/src/TxPower.h b/src/TxPower.h index 7945642c..f806430e 100644 --- a/src/TxPower.h +++ b/src/TxPower.h @@ -62,6 +62,17 @@ struct TxPowerState { int16_t ofdm_index = -1; int16_t mcs7_index = -1; bool hw_readback = false; + bool rate_diffs_custom = false; /* A caller-supplied per-rate diff table is + * CONFIGURED (SetTxPowerRateDiffs), not + * necessarily live on the chip right now: + * a flat override (flat_index >= 0) + * temporarily flattens the chip's per-rate + * table to zero diffs, and re-applies the + * configured table once the override + * clears. The cck/ofdm/mcs7_index summary + * fields always report chip truth for the + * current moment (flat during an override, + * ref+diff otherwise). */ }; /* Quantize a quarter-dB offset request to a family's step size: round to @@ -97,6 +108,23 @@ inline int txpkt_pwr_db_for_step(uint8_t step) { return step < 6 ? db[step] : 0; } +/* Caller-supplied per-rate TXAGC diffs (signed qdB vs the reference anchor). + * Motivating consumer: a wall-equalized rate ladder — each rate parked a + * uniform margin below its measured PA-compression wall. Programmed by + * IRtlDevice::SetTxPowerRateDiffs; 8822E-only. On the 8822E one qdB equals one + * TXAGC index step (step_qdb = 1), so values are used verbatim as index diffs. + * The hardware diff field is 7-bit two's-complement, so the usable range is + * [-64, 63]; SetTxPowerRateDiffs clamps to it before storing. */ +struct TxRateDiffsQdb { + int8_t cck = 0; /* CCK 1..11M rows; [-64, 63] qdB */ + int8_t legacy = 0; /* OFDM 6..54M control frames; [-64, 63] */ + int8_t mcs[8] = {0, 0, 0, 0, 0, 0, 0, 0}; /* HT MCS0..7; [-64, 63] each */ +}; + +/* Pack four signed per-rate diffs into one 0x3a00-table word: byte j = + * (diff_j & 0x7f), the 8822E's 7-bit two's-complement diff field. */ +uint32_t pack_rate_diff_word(int8_t d0, int8_t d1, int8_t d2, int8_t d3); + } // namespace devourer #endif /* DEVOURER_TX_POWER_H */ diff --git a/src/jaguar3/RadioManagementJaguar3.cpp b/src/jaguar3/RadioManagementJaguar3.cpp index 57701472..aad54e61 100644 --- a/src/jaguar3/RadioManagementJaguar3.cpp +++ b/src/jaguar3/RadioManagementJaguar3.cpp @@ -1039,6 +1039,72 @@ void RadioManagementJaguar3::apply_power_by_rate_8822e(uint8_t channel, #endif } +void RadioManagementJaguar3::apply_rate_diffs_8822e( + uint8_t ref_a, uint8_t ref_b, const devourer::TxRateDiffsQdb &d) { + invalidate_fast_caches(); /* writes the 0x1c90 TXAGC gate below */ + /* Clamp to the 7-bit ref fields (masked BB writes truncate mod 128). */ + if (ref_a > 0x7f) + ref_a = 0x7f; + if (ref_b > 0x7f) + ref_b = 0x7f; +#if defined(DEVOURER_HAVE_JAGUAR3_8822E) + auto wr = [this](uint16_t off, uint32_t mask, uint32_t v) { + _device.phy_set_bb_reg(0x1c90, 1u << 15, 0); /* txagc write enable */ + _device.phy_set_bb_reg(off, mask, v); + }; + wr(0x18e8, 0x1fc00, ref_a); /* path A OFDM/HT/VHT ref */ + wr(0x41e8, 0x1fc00, ref_b); /* path B */ + wr(0x18a0, 0x7f0000, ref_a); /* CCK ref (2.4G) */ + wr(0x41a0, 0x7f0000, ref_b); + + /* Diff rows, addressed exactly like apply_power_by_rate_8822e's pass 2: + * addr = 0x3a00 + (hw_rate & 0xfc), hw_rate = MRateToHwRate(first rate of + * the 4-rate group) — same helper/constants as pg_addr_to_rates's callers, + * so the addressing is byte-identical to the proven pg walk. Rows beyond + * the caller-controlled set (HT MCS8..15, all VHT) are written as explicit + * zero words for deterministic table state, covering the full range the + * pg walk can touch (0x3a00..0x3a3c) plus the rest of the 32-dword table + * up to 0x3a7c (matching set_tx_power_ref's zero range). */ + struct Row { + uint8_t first_rate; + int8_t d0, d1, d2, d3; + }; + const Row rows[] = { + /* CCK 1/2/5.5/11M: */ {MGN_1M, d.cck, d.cck, d.cck, d.cck}, + /* OFDM 6/9/12/18M: */ {MGN_6M, d.legacy, d.legacy, d.legacy, d.legacy}, + /* OFDM 24/36/48/54M: */ {MGN_24M, d.legacy, d.legacy, d.legacy, d.legacy}, + /* HT MCS0..3: */ {MGN_MCS0, d.mcs[0], d.mcs[1], d.mcs[2], d.mcs[3]}, + /* HT MCS4..7: */ {MGN_MCS4, d.mcs[4], d.mcs[5], d.mcs[6], d.mcs[7]}, + /* HT MCS8..11 (zeroed, 2SS — not in the caller-supplied set): */ + {MGN_MCS8, 0, 0, 0, 0}, + /* HT MCS12..15 (zeroed, 2SS): */ {MGN_MCS12, 0, 0, 0, 0}, + /* VHT1SS MCS0..3 (zeroed): */ {MGN_VHT1SS_MCS0, 0, 0, 0, 0}, + /* VHT1SS MCS4..7 (zeroed): */ {MGN_VHT1SS_MCS4, 0, 0, 0, 0}, + /* VHT1SS MCS8..9+VHT2SS0..1 (zeroed): */ {MGN_VHT1SS_MCS8, 0, 0, 0, 0}, + /* VHT2SS MCS2..5 (zeroed): */ {MGN_VHT2SS_MCS2, 0, 0, 0, 0}, + /* VHT2SS MCS6..9 (zeroed): */ {MGN_VHT2SS_MCS6, 0, 0, 0, 0}, + }; + for (const Row &r : rows) { + const uint8_t hw = MRateToHwRate(r.first_rate); + const uint16_t addr = static_cast(0x3a00 + (hw & 0xfc)); + wr(addr, 0xffffffff, + devourer::pack_rate_diff_word(r.d0, r.d1, r.d2, r.d3)); + } + /* set_tx_power_ref's zero range runs to 0x3a7c; the pg walk (and the rows + * above) only reach 0x3a3c (VHT2SS MCS6..9, the top of an 8822E's 2SS + * table). Zero the remainder explicitly too, for the same deterministic- + * state reason. */ + for (uint16_t off = 0x3a40; off <= 0x3a7c; off += 4) + wr(off, 0xffffffff, 0x0); + + _logger->info("Jaguar3: custom per-rate diffs applied (cck {} legacy {} " + "mcs0 {} mcs7 {}, ref A=0x{:02x} B=0x{:02x})", + d.cck, d.legacy, d.mcs[0], d.mcs[7], ref_a, ref_b); +#else + (void)d; +#endif +} + void RadioManagementJaguar3::set_bandwidth_dividers(ChannelWidth_t bwmode) { /* Writes 0x808 (8822e shaping) + the BB-reset word — stale-proof the fast * path's composed caches. */ diff --git a/src/jaguar3/RadioManagementJaguar3.h b/src/jaguar3/RadioManagementJaguar3.h index c4983d5a..c98f87f8 100644 --- a/src/jaguar3/RadioManagementJaguar3.h +++ b/src/jaguar3/RadioManagementJaguar3.h @@ -8,6 +8,7 @@ #include "RtlAdapter.h" #include "SelectedChannel.h" #include "ChipVariant.h" +#include "TxPower.h" /* devourer::TxRateDiffsQdb */ namespace jaguar3 { @@ -119,6 +120,14 @@ class RadioManagementJaguar3 { * unclamped over-range ref would wrap to near-zero TX silently. */ void apply_tx_power_refs_8822e(uint8_t ref_a, uint8_t ref_b); + /* Like apply_power_by_rate_8822e, but the per-rate diff table comes from + * the caller instead of phy_reg_pg: refs on both paths, then diff words + * for CCK (0x3a00 word 0), legacy OFDM 6..54M and HT MCS0..7 rows. VHT + * rows are zeroed (HT-only caller set). qdB == index steps on this + * family (step_qdb = 1). */ + void apply_rate_diffs_8822e(uint8_t ref_a, uint8_t ref_b, + const devourer::TxRateDiffsQdb& diffs); + private: /* 8822E channel-switch helpers (straight phydm ports, 8822E-gated): * CCK TX shaping filter + spur elimination (manual NBI / CSI mask). */ diff --git a/src/jaguar3/RtlJaguar3Device.cpp b/src/jaguar3/RtlJaguar3Device.cpp index ac70b376..1fc75bfa 100644 --- a/src/jaguar3/RtlJaguar3Device.cpp +++ b/src/jaguar3/RtlJaguar3Device.cpp @@ -1288,8 +1288,13 @@ void RtlJaguar3Device::apply_tx_power_current(bool full) { const uint8_t rb = clamp127(static_cast(_pwr_ref_b) + off); if (full || _diffs_zeroed) { /* Full apply: refs + the per-rate diff walk (also the path back from - * flat semantics, which zeroed the diffs). */ - _radioManagement.apply_power_by_rate_8822e(_channel.Channel, ra, rb); + * flat semantics, which zeroed the diffs). Route through the + * caller-supplied diff table when SetTxPowerRateDiffs has one set; + * otherwise the default phy_reg_pg walk. */ + if (_rate_diffs) + _radioManagement.apply_rate_diffs_8822e(ra, rb, *_rate_diffs); + else + _radioManagement.apply_power_by_rate_8822e(_channel.Channel, ra, rb); _diffs_zeroed = false; } else { _radioManagement.apply_tx_power_refs_8822e(ra, rb); @@ -1359,6 +1364,38 @@ void RtlJaguar3Device::SetTxPowerIndexOverride(int idx) { } } +bool RtlJaguar3Device::SetTxPowerRateDiffs( + const std::optional &diffs) { + if (_variant != jaguar3::ChipVariant::C8822E) { + _logger->warn("SetTxPowerRateDiffs: 8822E-only"); + return false; + } + if (_cw_active) { + _logger->warn("SetTxPowerRateDiffs refused: CW tone active"); + return false; + } + /* Clamp to the 7-bit two's-complement diff field's usable range [-64, 63] + * before storing. pack_rate_diff_word masks with & 0x7f, which would alias + * the sign of an over-range int8_t: a caller asking for -100 (a big cut) + * would land +28 (a power boost). Same guard the ref path applies against + * its own over-range wrap. */ + std::optional clamped = diffs; + if (clamped) { + auto cl = [](int8_t v) -> int8_t { + return v < -64 ? -64 : (v > 63 ? 63 : v); + }; + clamped->cck = cl(clamped->cck); + clamped->legacy = cl(clamped->legacy); + for (int i = 0; i < 8; ++i) + clamped->mcs[i] = cl(clamped->mcs[i]); + } + std::lock_guard lk(_reg_mu); + _rate_diffs = clamped; + if (_brought_up) + apply_tx_power_current(/*full=*/true); /* re-walk the table now */ + return true; +} + bool RtlJaguar3Device::ReApplyTxPower() { if (!_brought_up || _cw_active) return false; @@ -1561,17 +1598,43 @@ devourer::TxPowerState RtlJaguar3Device::GetTxPowerState() { s.offset_qdb = s.offset_steps; /* 1 qdB per step on Jaguar3 */ s.saturated_low = _txpwr_sat_low; s.saturated_high = _txpwr_sat_high; + /* Under _reg_mu for a coherent snapshot vs the coex tick, and so the + * non-atomic _rate_diffs read below is race-free. */ + std::lock_guard lk(_reg_mu); + /* rate_diffs_custom means "a table is configured" — knowable regardless of + * bring-up / CW state, so it's reported here rather than inside the readback + * block (a pre-bring-up or mid-CW snapshot with a table set must still see + * it flagged). */ + s.rate_diffs_custom = _rate_diffs.has_value(); if (_brought_up && !_cw_active) { /* Reference readback (the Jaguar3 TXAGC refs ARE readable): OFDM ref * 0x18e8[16:10]; MCS7 is the per-rate diff table's zero anchor, so it - * equals the OFDM ref; CCK ref 0x18a0[22:16]. Under _reg_mu for a - * coherent snapshot vs the coex tick. */ - std::lock_guard lk(_reg_mu); + * equals the OFDM ref; CCK ref 0x18a0[22:16]. */ const uint32_t ofdm = (_device.rtw_read32(0x18e8) >> 10) & 0x7f; - s.ofdm_index = static_cast(ofdm); - s.mcs7_index = static_cast(ofdm); - s.cck_index = - static_cast((_device.rtw_read32(0x18a0) >> 16) & 0x7f); + const uint32_t cck = + (_device.rtw_read32(0x18a0) >> 16) & 0x7f; + if (_rate_diffs && s.flat_index < 0) { + /* A custom per-rate diff table is live (8822E SetTxPowerRateDiffs) and + * no flat override is overriding it on the chip: the shared reference + * alone is no longer the effective index for any rate, so report + * ref + diff[rate] instead of the artifact where cck/ofdm/mcs7 all read + * equal to the reference. Clamp to the 7-bit TXAGC index range the same + * way the apply path does. */ + auto clamp127 = [](int v) { + return static_cast(v < 0 ? 0 : (v > 127 ? 127 : v)); + }; + s.ofdm_index = clamp127(static_cast(ofdm) + _rate_diffs->legacy); + s.mcs7_index = clamp127(static_cast(ofdm) + _rate_diffs->mcs[7]); + s.cck_index = clamp127(static_cast(cck) + _rate_diffs->cck); + } else { + /* Either no custom table configured, or a flat override is currently + * live: apply_tx_power_current's flat branch zeroes the chip's + * per-rate diffs while flat >= 0, so the registers already read the + * honest flat truth here — fall through to the plain summary. */ + s.ofdm_index = static_cast(ofdm); + s.mcs7_index = static_cast(ofdm); + s.cck_index = static_cast(cck); + } s.hw_readback = true; } return s; diff --git a/src/jaguar3/RtlJaguar3Device.h b/src/jaguar3/RtlJaguar3Device.h index 6fa5420d..fb0376b9 100644 --- a/src/jaguar3/RtlJaguar3Device.h +++ b/src/jaguar3/RtlJaguar3Device.h @@ -113,6 +113,8 @@ class RtlJaguar3Device : public IRtlDevice { devourer::TxPowerCaps GetTxPowerCaps() override; int SetTxPowerOffsetQdb(int qdb) override; void SetTxPowerIndexOverride(int idx) override; + bool SetTxPowerRateDiffs( + const std::optional &diffs) override; bool ReApplyTxPower() override; /* Per-packet TX-power offset — session default. Programs a * hardware offset BANK (global 0x1e70 reg0/reg1, see TxPktPwrBanks.h) and @@ -307,6 +309,10 @@ class RtlJaguar3Device : public IRtlDevice { /* True while the 0x3a00 per-rate diff table is zeroed (flat semantics / * 8822C default) — repeated flat steps then skip the 32-dword re-zero. */ bool _diffs_zeroed = false; + /* Caller-supplied per-rate TXAGC diffs (SetTxPowerRateDiffs), in place of + * phy_reg_pg's table when set. 8822E-only; std::nullopt = default table. + * Read/written under _reg_mu once brought up. */ + std::optional _rate_diffs; /* Re-program TXAGC from the current knob state. full=true re-derives the * 8822E efuse refs + rewrites the per-rate diff table (bring-up / channel * change / flat<->efuse transitions); full=false is the light offset step. diff --git a/tests/rate_diffs_selftest.cpp b/tests/rate_diffs_selftest.cpp new file mode 100644 index 00000000..23bc59ed --- /dev/null +++ b/tests/rate_diffs_selftest.cpp @@ -0,0 +1,22 @@ +#include +#include "TxPower.h" + +static int fails = 0; +#define CHECK(c) do { if (!(c)) { std::fprintf(stderr, "FAIL: %s\n", #c); ++fails; } } while (0) + +int main() { + using devourer::pack_rate_diff_word; + CHECK(pack_rate_diff_word(0, 0, 0, 0) == 0u); + CHECK(pack_rate_diff_word(1, 2, 3, 4) == 0x04030201u); + /* negative diffs are 7-bit two's complement per byte: -1 -> 0x7f */ + CHECK(pack_rate_diff_word(-1, 0, 0, 0) == 0x0000007fu); + CHECK(pack_rate_diff_word(-24, 24, -1, 63) == // -24=0x68, 24=0x18, -1=0x7f, 63=0x3f + 0x3f7f1868u); + /* bit 7 of every byte must be clear (the field is 7 bits wide) */ + CHECK((pack_rate_diff_word(-64, -64, -64, -64) & 0x80808080u) == 0u); + devourer::TxRateDiffsQdb d; + CHECK(d.cck == 0 && d.legacy == 0 && d.mcs[7] == 0); /* zero-init default */ + if (fails) { std::fprintf(stderr, "%d failure(s)\n", fails); return 1; } + std::puts("rate_diffs_selftest OK"); + return 0; +} diff --git a/tests/txpwr_rate_diffs_regcheck.sh b/tests/txpwr_rate_diffs_regcheck.sh new file mode 100755 index 00000000..517a7339 --- /dev/null +++ b/tests/txpwr_rate_diffs_regcheck.sh @@ -0,0 +1,220 @@ +#!/usr/bin/env bash +# Regression validation of the 8822E per-rate TX-power diff table +# (IRtlDevice::SetTxPowerRateDiffs / GetTxPowerState.rate_diffs_custom), +# exercised end-to-end through the txpower demo's --rate-diffs flag. +# +# Cells per plugged DUT (skip-if-unplugged, PASS/FAIL/SKIP tally like +# txpwr_offset_regcheck.sh): +# +# baseline no --rate-diffs given => txpwr.state reports rate_diffs=0 +# (the default phy_reg_pg per-rate walk, not the caller table). +# apply --rate-diffs 0,0,10,0,0,0,0,-8,-12,-16 => rate_diffs=1 and the +# post-diff readback proves the table landed: mcs7_index == +# ofdm_index - 16 (ofdm reflects legacy=0, mcs7 reflects +# mcs[7]=-16, so the delta is exactly -16 regardless of the +# shared reference the diffs are folded against). +# sticky same diffs + --switch-channel to another channel in the same +# band => post-switch state still rate_diffs=1 with the same +# mcs7-ofdm delta (the table survives a full SetMonitorChannel +# re-fold, not just an offset-only step). +# override same diffs, then one process sequences a flat pulse via +# --flat-pulse 40 (SetTxPowerIndexOverride(40), dump state, +# SetTxPowerIndexOverride(-1), dump state again) => mid-pulse +# state reports rate_diffs=1 (a table is configured) but +# mcs7_index == ofdm_index (the honest flat truth — the override +# zeroes the chip's per-rate diffs, and GetTxPowerState must not +# paper over that), while the post-clear state reports +# rate_diffs=1 with the delta intact (the override clear's full +# re-apply re-walks the caller table, per +# RtlJaguar3Device::apply_tx_power_current's full-apply path). +# +# Only the 8822E (RTL8812EU/RTL8822EU, PID 0xa81a) implements +# SetTxPowerRateDiffs (RtlJaguar3Device::SetTxPowerRateDiffs returns +# false off that variant) — this script targets that DUT only and SKIPs +# cleanly when it isn't plugged in. +# +# Usage: sudo -v && tests/txpwr_rate_diffs_regcheck.sh +set -u +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +OUT="${TXPWR_RATE_DIFFS_REGCHECK_OUT:-/tmp/devourer-txpwr-rate-diffs-regcheck}" +STEP_DEMO="$ROOT/build/txpower" +mkdir -p "$OUT" + +PASS=0; FAIL=0; SKIP=0 +pass() { echo " PASS: $*"; PASS=$((PASS+1)); } +fail() { echo " FAIL: $*"; FAIL=$((FAIL+1)); } +skip() { echo " SKIP: $*"; SKIP=$((SKIP+1)); } + +cleanup() { + pkill -x txpower 2>/dev/null || true +} +trap cleanup EXIT INT TERM + +echo "== building ==" +cmake --build "$ROOT/build" -j --target txpower >/dev/null || exit 1 + +# DUT: only the 8822E implements SetTxPowerRateDiffs. CH_A/CH_B are +# same-band channels in different efuse channel groups (same pair the offset +# regcheck uses for its 8822E sticky cell), so the sticky check proves the +# table survives a full channel-group re-fold, not just an offset step. +PID="0xa81a"; VID="0x0bda"; CH_A="36"; CH_B="149" +DIFFS="0,0,10,0,0,0,0,-8,-12,-16" + +plugged() { lsusb -d "$(printf '%04x:%04x' "$2" "$1")" >/dev/null 2>&1; } + +# --- helpers --------------------------------------------------------------- +# Extract numeric field F from the Nth txpwr.state event line of a log. +state_field() { # $1=log $2=line-index(1-based) $3=field + grep -F '"ev":"txpwr.state"' "$1" | sed -n "$2p" \ + | grep -o "\"$3\":-\?[0-9]*" | cut -d: -f2 | tr -d '\r' +} +# Extract numeric field F from the LAST txpwr.state event line of a log. +state_field_last() { # $1=log $2=field + grep -F '"ev":"txpwr.state"' "$1" | tail -1 \ + | grep -o "\"$2\":-\?[0-9]*" | cut -d: -f2 | tr -d '\r' +} +caps_field() { # $1=log $2=field + grep -F '"ev":"txpwr.caps"' "$1" | head -1 \ + | grep -o "\"$2\":-\?[0-9]*" | cut -d: -f2 | tr -d '\r' +} + +run_step_demo() { # $1=outfile, rest = args + local out="$1"; shift + sudo -n timeout 90 "$STEP_DEMO" "$@" >"$out" 2>&1 +} + +if ! plugged "$PID" "$VID"; then + skip "$PID@$VID (8822E) not plugged" + echo + echo "== txpwr-rate-diffs regcheck: PASS=$PASS FAIL=$FAIL SKIP=$SKIP ==" + exit 0 +fi + +name="$PID@$VID (jaguar3/8822E)" +echo "== DUT $name ==" +tag="${PID#0x}" + +# -- caps sanity ------------------------------------------------------------- +caps_log="$OUT/$tag-caps.log" +run_step_demo "$caps_log" --vid "$VID" --pid "$PID" --channel "$CH_A" +if [ "$(caps_field "$caps_log" supported)" != "1" ]; then + skip "$name: TX-power API not wired for this family yet" + echo + echo "== txpwr-rate-diffs regcheck: PASS=$PASS FAIL=$FAIL SKIP=$SKIP ==" + exit 0 +fi + +# -- (a) baseline: no --rate-diffs => rate_diffs=0 --------------------------- +base_rd="$(state_field "$caps_log" 1 rate_diffs)" +if [ "$base_rd" = "0" ]; then + pass "$name baseline: rate_diffs=0 (default phy_reg_pg walk)" +else + fail "$name baseline: rate_diffs='$base_rd' (want 0)" +fi + +# -- (b) apply: --rate-diffs => rate_diffs=1, mcs7 == ofdm - 16 -------------- +apply_log="$OUT/$tag-apply.log" +run_step_demo "$apply_log" --vid "$VID" --pid "$PID" --channel "$CH_A" \ + --rate-diffs "$DIFFS" +apply_rd="$(state_field_last "$apply_log" rate_diffs)" +apply_ofdm="$(state_field_last "$apply_log" ofdm)" +apply_mcs7="$(state_field_last "$apply_log" mcs7)" +if [ -z "$apply_ofdm" ] || [ "$apply_ofdm" = "-1" ]; then + fail "$name apply: no post-diff state readback" +else + want_mcs7=$((apply_ofdm - 16)) + if [ "$apply_rd" = "1" ] && [ "$apply_mcs7" = "$want_mcs7" ]; then + pass "$name apply: rate_diffs=1, mcs7-ofdm=-16 (ofdm=$apply_ofdm mcs7=$apply_mcs7)" + else + fail "$name apply: rate_diffs=$apply_rd ofdm=$apply_ofdm mcs7=$apply_mcs7 (want rate_diffs=1, mcs7=$want_mcs7)" + fi +fi + +# -- (c) sticky: same diffs + --switch-channel => table survives the re-fold - +sticky_log="$OUT/$tag-sticky.log" +run_step_demo "$sticky_log" --vid "$VID" --pid "$PID" --channel "$CH_A" \ + --rate-diffs "$DIFFS" --switch-channel "$CH_B" +sticky_rd="$(state_field_last "$sticky_log" rate_diffs)" +sticky_ofdm="$(state_field_last "$sticky_log" ofdm)" +sticky_mcs7="$(state_field_last "$sticky_log" mcs7)" +if [ -z "$sticky_ofdm" ] || [ "$sticky_ofdm" = "-1" ]; then + fail "$name sticky: no post-switch state readback" +else + want_mcs7="$((sticky_ofdm - 16))" + if [ "$sticky_rd" = "1" ] && [ "$sticky_mcs7" = "$want_mcs7" ]; then + pass "$name sticky: rate_diffs=1 after SetMonitorChannel ch$CH_A->ch$CH_B, mcs7-ofdm=-16 (ofdm=$sticky_ofdm mcs7=$sticky_mcs7)" + else + fail "$name sticky: rate_diffs=$sticky_rd ofdm=$sticky_ofdm mcs7=$sticky_mcs7 (want rate_diffs=1, mcs7=$want_mcs7)" + fi +fi + +# -- (d) override set-then-clear: sequenced in ONE process via --flat-pulse -- +# main.cpp applies --rate-diffs' SetTxPowerRateDiffs first, then (since +# --flat-pulse's block sits right after it, before the offset ramp) pulses +# SetTxPowerIndexOverride(N) and prints state, then clears it back to -1 and +# prints state again. One invocation now sequences set -> flat -> clear, so +# this is a real within-process persistence proof instead of two independent +# process re-opens (each of which just re-applies the diffs from scratch). +# +# The run emits 4 txpwr.state events total (baseline, post-diffs, mid-pulse, +# post-clear); we read the LAST THREE: +# +# post-diffs (3rd-from-last): rate_diffs=1, mcs7-ofdm=-16 — the table just +# landed (same assertion as check (b), confirms the pulse starts clean). +# mid-pulse (2nd-from-last): rate_diffs=1 (a table is still CONFIGURED) BUT +# mcs7-ofdm=0 (mcs7 == ofdm == the flat index, 40) — the honest flat +# truth: apply_tx_power_current's flat branch zeroed the chip's per-rate +# diffs, and GetTxPowerState must report that chip truth rather than +# ref+diff during the override. +# post-clear (last): rate_diffs=1, mcs7-ofdm=-16 restored — clearing the +# override drove apply_tx_power_current's full-apply path, which re-walks +# the still-configured caller table. This is the actual override-clear +# persistence proof check (d) is named for. +ov_log="$OUT/$tag-override-pulse.log" +run_step_demo "$ov_log" --vid "$VID" --pid "$PID" --channel "$CH_A" \ + --rate-diffs "$DIFFS" --flat-pulse 40 + +n_states="$(grep -cF '"ev":"txpwr.state"' "$ov_log")" +if [ "$n_states" -lt 3 ]; then + fail "$name override(flat-pulse): only $n_states txpwr.state events (want >=3)" +else + post_diffs_idx=$((n_states - 2)) + mid_pulse_idx=$((n_states - 1)) + post_clear_idx=$n_states + + pd_rd="$(state_field "$ov_log" "$post_diffs_idx" rate_diffs)" + pd_ofdm="$(state_field "$ov_log" "$post_diffs_idx" ofdm)" + pd_mcs7="$(state_field "$ov_log" "$post_diffs_idx" mcs7)" + mp_rd="$(state_field "$ov_log" "$mid_pulse_idx" rate_diffs)" + mp_ofdm="$(state_field "$ov_log" "$mid_pulse_idx" ofdm)" + mp_mcs7="$(state_field "$ov_log" "$mid_pulse_idx" mcs7)" + pc_rd="$(state_field "$ov_log" "$post_clear_idx" rate_diffs)" + pc_ofdm="$(state_field "$ov_log" "$post_clear_idx" ofdm)" + pc_mcs7="$(state_field "$ov_log" "$post_clear_idx" mcs7)" + + if [ -z "$pd_ofdm" ] || [ -z "$mp_ofdm" ] || [ -z "$pc_ofdm" ]; then + fail "$name override(flat-pulse): missing state field(s) in last 3 events" + else + pd_want_mcs7=$((pd_ofdm - 16)) + pc_want_mcs7=$((pc_ofdm - 16)) + if [ "$pd_rd" = "1" ] && [ "$pd_mcs7" = "$pd_want_mcs7" ]; then + pass "$name override(flat-pulse) post-diffs: rate_diffs=1, mcs7-ofdm=-16 (ofdm=$pd_ofdm mcs7=$pd_mcs7)" + else + fail "$name override(flat-pulse) post-diffs: rate_diffs=$pd_rd ofdm=$pd_ofdm mcs7=$pd_mcs7 (want rate_diffs=1, mcs7=$pd_want_mcs7)" + fi + if [ "$mp_rd" = "1" ] && [ "$mp_ofdm" = "40" ] && [ "$mp_mcs7" = "40" ]; then + pass "$name override(flat-pulse) mid-pulse: rate_diffs=1 (configured) but mcs7=ofdm=40 (honest flat truth)" + else + fail "$name override(flat-pulse) mid-pulse: rate_diffs=$mp_rd ofdm=$mp_ofdm mcs7=$mp_mcs7 (want rate_diffs=1, ofdm=40, mcs7=40)" + fi + if [ "$pc_rd" = "1" ] && [ "$pc_mcs7" = "$pc_want_mcs7" ]; then + pass "$name override(flat-pulse) post-clear: rate_diffs=1, mcs7-ofdm=-16 restored (ofdm=$pc_ofdm mcs7=$pc_mcs7)" + else + fail "$name override(flat-pulse) post-clear: rate_diffs=$pc_rd ofdm=$pc_ofdm mcs7=$pc_mcs7 (want rate_diffs=1, mcs7=$pc_want_mcs7)" + fi + fi +fi + +echo +echo "== txpwr-rate-diffs regcheck: PASS=$PASS FAIL=$FAIL SKIP=$SKIP ==" +[ "$FAIL" -eq 0 ]