From d841d27d35bda51863668fd60f39b1510d9086a1 Mon Sep 17 00:00:00 2001 From: Gilang Date: Fri, 17 Jul 2026 02:17:53 +0700 Subject: [PATCH 1/8] jaguar3(8822e): TxRateDiffsQdb type + diff-word packing (groundwork) --- CMakeLists.txt | 10 ++++++++++ src/IRtlDevice.h | 12 ++++++++++++ src/TxPower.cpp | 8 ++++++++ src/TxPower.h | 16 ++++++++++++++++ tests/rate_diffs_selftest.cpp | 22 ++++++++++++++++++++++ 5 files changed, 68 insertions(+) create mode 100644 tests/rate_diffs_selftest.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 9dab962d..b2228fdf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -789,6 +789,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/src/IRtlDevice.h b/src/IRtlDevice.h index 258e7baf..740aa55d 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 in v1). */ + 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..ea564ff8 100644 --- a/src/TxPower.h +++ b/src/TxPower.h @@ -97,6 +97,22 @@ 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). + * v1 consumer: mabur's wall-equalized ladder — each rate parked a uniform + * margin below its measured PA-compression wall. Programmed by + * IRtlDevice::SetTxPowerRateDiffs; 8822E-only in v1. On the 8822E one qdB + * equals one TXAGC index step (step_qdb = 1), so values are used verbatim + * as index diffs. */ +struct TxRateDiffsQdb { + int8_t cck = 0; /* CCK 1..11M rows */ + int8_t legacy = 0; /* OFDM 6..54M — control frames */ + int8_t mcs[8] = {0, 0, 0, 0, 0, 0, 0, 0}; /* HT MCS0..7 */ +}; + +/* 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/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; +} From 62ab2aead20cfca98a07db4a38e1cad6947a840c Mon Sep 17 00:00:00 2001 From: Gilang Date: Fri, 17 Jul 2026 02:24:23 +0700 Subject: [PATCH 2/8] jaguar3(8822e): caller-supplied per-rate TXAGC diffs (SetTxPowerRateDiffs) --- src/jaguar3/RadioManagementJaguar3.cpp | 66 ++++++++++++++++++++++++++ src/jaguar3/RadioManagementJaguar3.h | 9 ++++ src/jaguar3/RtlJaguar3Device.cpp | 26 +++++++++- src/jaguar3/RtlJaguar3Device.h | 6 +++ 4 files changed, 105 insertions(+), 2 deletions(-) diff --git a/src/jaguar3/RadioManagementJaguar3.cpp b/src/jaguar3/RadioManagementJaguar3.cpp index 57701472..cb4dba26 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 v1's 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 5f3902a6..4d6b9b01 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 consumer in v1). 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..914d3663 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,23 @@ void RtlJaguar3Device::SetTxPowerIndexOverride(int idx) { } } +bool RtlJaguar3Device::SetTxPowerRateDiffs( + const std::optional &diffs) { + if (_variant != jaguar3::ChipVariant::C8822E) { + _logger->warn("SetTxPowerRateDiffs: 8822E-only in v1"); + return false; + } + if (_cw_active) { + _logger->warn("SetTxPowerRateDiffs refused: CW tone active"); + return false; + } + std::lock_guard lk(_reg_mu); + _rate_diffs = diffs; + 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; 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. From 5436d7c4ae5fa95196ec645870529ac8d295bdf3 Mon Sep 17 00:00:00 2001 From: Gilang Date: Fri, 17 Jul 2026 02:29:20 +0700 Subject: [PATCH 3/8] jaguar3(8822e): GetTxPowerState reports post-diff per-rate indices --- examples/txpower/main.cpp | 3 ++- src/TxPower.h | 2 ++ src/jaguar3/RtlJaguar3Device.cpp | 24 ++++++++++++++++++++---- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/examples/txpower/main.cpp b/examples/txpower/main.cpp index b637fe0e..91a16389 100644 --- a/examples/txpower/main.cpp +++ b/examples/txpower/main.cpp @@ -159,7 +159,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"); diff --git a/src/TxPower.h b/src/TxPower.h index ea564ff8..98325064 100644 --- a/src/TxPower.h +++ b/src/TxPower.h @@ -62,6 +62,8 @@ struct TxPowerState { int16_t ofdm_index = -1; int16_t mcs7_index = -1; bool hw_readback = false; + bool rate_diffs_custom = false; /* caller-supplied per-rate diff table + * active (SetTxPowerRateDiffs) */ }; /* Quantize a quarter-dB offset request to a family's step size: round to diff --git a/src/jaguar3/RtlJaguar3Device.cpp b/src/jaguar3/RtlJaguar3Device.cpp index 914d3663..c125642e 100644 --- a/src/jaguar3/RtlJaguar3Device.cpp +++ b/src/jaguar3/RtlJaguar3Device.cpp @@ -1590,10 +1590,26 @@ devourer::TxPowerState RtlJaguar3Device::GetTxPowerState() { * coherent snapshot vs the coex tick. */ std::lock_guard lk(_reg_mu); 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) { + /* A custom per-rate diff table is live (8822E SetTxPowerRateDiffs): + * 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); + s.rate_diffs_custom = true; + } else { + s.ofdm_index = static_cast(ofdm); + s.mcs7_index = static_cast(ofdm); + s.cck_index = static_cast(cck); + } s.hw_readback = true; } return s; From a75addbeb453c2d124ee8dadf12f9978a49d1da6 Mon Sep 17 00:00:00 2001 From: Gilang Date: Fri, 17 Jul 2026 02:33:28 +0700 Subject: [PATCH 4/8] jaguar3(8822e): txpower --rate-diffs reference consumer --- examples/txpower/main.cpp | 49 ++++++++++++++++++++++++++++++++++----- 1 file changed, 43 insertions(+), 6 deletions(-) diff --git a/examples/txpower/main.cpp b/examples/txpower/main.cpp index 91a16389..846677e6 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,8 @@ * --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 * --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 +32,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 +49,7 @@ #include #include #include +#include #include #include @@ -84,6 +87,7 @@ struct Args { int switch_channel = -1; int retune = -1; bool thermal = false; + std::string rate_diffs; }; bool parse_int(const char *s, int &out) { @@ -126,7 +130,11 @@ bool parse_args(int argc, char **argv, Args &a) { ; else if (k == "--thermal") a.thermal = true; - else { + else if (k == "--rate-diffs") { + if (i + 1 >= argc) + return false; + a.rate_diffs = argv[++i]; + } else { std::fprintf(stderr, "devourer [W] unknown/incomplete arg: %s\n", k.c_str()); return false; @@ -258,6 +266,35 @@ int main(int argc, char **argv) { print_state(dev.get(), a.thermal); } + if (!a.rate_diffs.empty()) { + if (a.rate_diffs == "clear") { + const bool ok = dev->SetTxPowerRateDiffs(std::nullopt); + logger->info("rate-diffs clear -> {}", ok ? "ok" : "unsupported"); + } else { + devourer::TxRateDiffsQdb d; + int v[10] = {0}; + int n = 0; + const char* p = a.rate_diffs.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 2; + } + d.cck = static_cast(v[0]); + d.legacy = static_cast(v[1]); + for (int i2 = 0; i2 < 8; ++i2) d.mcs[i2] = static_cast(v[2 + i2]); + const bool ok = dev->SetTxPowerRateDiffs(d); + logger->info("rate-diffs -> {}", ok ? "applied" : "unsupported"); + } + 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; From eb7429a0994fa60fdadaf645d28ee6d367f8e109 Mon Sep 17 00:00:00 2001 From: Gilang Date: Fri, 17 Jul 2026 02:39:54 +0700 Subject: [PATCH 5/8] jaguar3(8822e): rate-diffs regcheck + Runtime TX power docs --- CLAUDE.md | 19 ++- tests/txpwr_rate_diffs_regcheck.sh | 194 +++++++++++++++++++++++++++++ 2 files changed, 209 insertions(+), 4 deletions(-) create mode 100755 tests/txpwr_rate_diffs_regcheck.sh diff --git a/CLAUDE.md b/CLAUDE.md index 73dadb58..3c6968f5 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/tests/txpwr_rate_diffs_regcheck.sh b/tests/txpwr_rate_diffs_regcheck.sh new file mode 100755 index 00000000..26fb0a8a --- /dev/null +++ b/tests/txpwr_rate_diffs_regcheck.sh @@ -0,0 +1,194 @@ +#!/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 --flat 40 (SetTxPowerIndexOverride forces a +# flat index and zeroes the diffs in hardware) followed by +# --flat -1 (clears the override) in a second invocation => +# final state 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 in v1 (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 in v1. 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: diffs survive the flat-override round trip - +# main.cpp's code order is always --flat's SetTxPowerIndexOverride BEFORE +# --rate-diffs' SetTxPowerRateDiffs, regardless of argv order — and there is +# no flag to re-assert --flat a second time (40, then -1) within one process, +# so the set/clear round trip needs two invocations. Both must re-issue +# --rate-diffs (a fresh device open starts with _rate_diffs unset; state +# does not persist across process exit) so each run's OWN internal sequence +# is what's under test: +# +# run 1: --flat 40 (applied first) then --rate-diffs D (applied second) +# SetTxPowerIndexOverride(40) flattens+zeroes the hw diffs, THEN +# SetTxPowerRateDiffs(D) re-walks the table on top of the flat +# override -> checks rate_diffs=1, delta intact with the override +# still active. +# run 2: --flat -1 (applied first) then --rate-diffs D (applied second) +# SetTxPowerIndexOverride(-1) clears any override (a no-op on a +# freshly-opened device that was never overridden in THIS process, +# but exercises the same idx<0 / full-re-apply branch), THEN +# SetTxPowerRateDiffs(D) walks the table via the override-clear's +# full-apply path (RtlJaguar3Device::apply_tx_power_current's +# idx<0 branch) -> checks rate_diffs=1, delta intact. +# +# Together the two runs bracket the override lever (set / clear) around a +# live diff table and confirm GetTxPowerState reports rate_diffs=1 with the +# same mcs7-ofdm delta on both sides. +for flat_val in 40 -1; do + lbl="flat$flat_val"; lbl="${lbl/-/neg}" + ov_log="$OUT/$tag-override-$lbl.log" + run_step_demo "$ov_log" --vid "$VID" --pid "$PID" --channel "$CH_A" \ + --rate-diffs "$DIFFS" --flat "$flat_val" + ov_rd="$(state_field_last "$ov_log" rate_diffs)" + ov_ofdm="$(state_field_last "$ov_log" ofdm)" + ov_mcs7="$(state_field_last "$ov_log" mcs7)" + if [ -z "$ov_ofdm" ] || [ "$ov_ofdm" = "-1" ]; then + fail "$name override(--flat $flat_val): no post-diff state readback" + continue + fi + want_mcs7="$((ov_ofdm - 16))" + if [ "$ov_rd" = "1" ] && [ "$ov_mcs7" = "$want_mcs7" ]; then + pass "$name override(--flat $flat_val): rate_diffs=1, mcs7-ofdm=-16 intact (ofdm=$ov_ofdm mcs7=$ov_mcs7)" + else + fail "$name override(--flat $flat_val): rate_diffs=$ov_rd ofdm=$ov_ofdm mcs7=$ov_mcs7 (want rate_diffs=1, mcs7=$want_mcs7)" + fi +done + +echo +echo "== txpwr-rate-diffs regcheck: PASS=$PASS FAIL=$FAIL SKIP=$SKIP ==" +[ "$FAIL" -eq 0 ] From f96258d57f54365d59fdd41a3787063e896023fa Mon Sep 17 00:00:00 2001 From: Gilang Date: Fri, 17 Jul 2026 02:45:34 +0700 Subject: [PATCH 6/8] jaguar3(8822e): state reports chip truth during flat override (diffs configured != live) --- src/TxPower.h | 13 +++++++++++-- src/jaguar3/RtlJaguar3Device.cpp | 19 ++++++++++++------- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/src/TxPower.h b/src/TxPower.h index 98325064..aa5f9f71 100644 --- a/src/TxPower.h +++ b/src/TxPower.h @@ -62,8 +62,17 @@ struct TxPowerState { int16_t ofdm_index = -1; int16_t mcs7_index = -1; bool hw_readback = false; - bool rate_diffs_custom = false; /* caller-supplied per-rate diff table - * active (SetTxPowerRateDiffs) */ + 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 diff --git a/src/jaguar3/RtlJaguar3Device.cpp b/src/jaguar3/RtlJaguar3Device.cpp index c125642e..52dae466 100644 --- a/src/jaguar3/RtlJaguar3Device.cpp +++ b/src/jaguar3/RtlJaguar3Device.cpp @@ -1592,24 +1592,29 @@ devourer::TxPowerState RtlJaguar3Device::GetTxPowerState() { const uint32_t ofdm = (_device.rtw_read32(0x18e8) >> 10) & 0x7f; const uint32_t cck = (_device.rtw_read32(0x18a0) >> 16) & 0x7f; - if (_rate_diffs) { - /* A custom per-rate diff table is live (8822E SetTxPowerRateDiffs): - * 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. */ + 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); - s.rate_diffs_custom = true; } 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.rate_diffs_custom = _rate_diffs.has_value(); s.hw_readback = true; } return s; From a94d43d1eabc7b552567e2a1c1264c8fe7ed7edf Mon Sep 17 00:00:00 2001 From: Gilang Date: Fri, 17 Jul 2026 02:45:38 +0700 Subject: [PATCH 7/8] jaguar3(8822e): txpower --flat-pulse; regcheck override-clear in one process --- examples/txpower/main.cpp | 21 ++++- tests/txpwr_rate_diffs_regcheck.sh | 122 ++++++++++++++++++----------- 2 files changed, 97 insertions(+), 46 deletions(-) diff --git a/examples/txpower/main.cpp b/examples/txpower/main.cpp index 846677e6..790b2928 100644 --- a/examples/txpower/main.cpp +++ b/examples/txpower/main.cpp @@ -19,6 +19,12 @@ * --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 @@ -88,6 +94,8 @@ struct Args { int retune = -1; bool thermal = false; std::string rate_diffs; + int flat_pulse = 0; + bool have_flat_pulse = false; }; bool parse_int(const char *s, int &out) { @@ -134,7 +142,9 @@ bool parse_args(int argc, char **argv, Args &a) { if (i + 1 >= argc) return false; a.rate_diffs = argv[++i]; - } else { + } 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()); return false; @@ -295,6 +305,15 @@ int main(int argc, char **argv) { 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/tests/txpwr_rate_diffs_regcheck.sh b/tests/txpwr_rate_diffs_regcheck.sh index 26fb0a8a..c0cd702e 100755 --- a/tests/txpwr_rate_diffs_regcheck.sh +++ b/tests/txpwr_rate_diffs_regcheck.sh @@ -17,11 +17,15 @@ # 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 --flat 40 (SetTxPowerIndexOverride forces a -# flat index and zeroes the diffs in hardware) followed by -# --flat -1 (clears the override) in a second invocation => -# final state rate_diffs=1 with the delta intact (the override -# clear's full re-apply re-walks the caller table, per +# 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 @@ -144,50 +148,78 @@ else fi fi -# -- (d) override set-then-clear: diffs survive the flat-override round trip - -# main.cpp's code order is always --flat's SetTxPowerIndexOverride BEFORE -# --rate-diffs' SetTxPowerRateDiffs, regardless of argv order — and there is -# no flag to re-assert --flat a second time (40, then -1) within one process, -# so the set/clear round trip needs two invocations. Both must re-issue -# --rate-diffs (a fresh device open starts with _rate_diffs unset; state -# does not persist across process exit) so each run's OWN internal sequence -# is what's under test: +# -- (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). # -# run 1: --flat 40 (applied first) then --rate-diffs D (applied second) -# SetTxPowerIndexOverride(40) flattens+zeroes the hw diffs, THEN -# SetTxPowerRateDiffs(D) re-walks the table on top of the flat -# override -> checks rate_diffs=1, delta intact with the override -# still active. -# run 2: --flat -1 (applied first) then --rate-diffs D (applied second) -# SetTxPowerIndexOverride(-1) clears any override (a no-op on a -# freshly-opened device that was never overridden in THIS process, -# but exercises the same idx<0 / full-re-apply branch), THEN -# SetTxPowerRateDiffs(D) walks the table via the override-clear's -# full-apply path (RtlJaguar3Device::apply_tx_power_current's -# idx<0 branch) -> checks rate_diffs=1, delta intact. +# The run emits 4 txpwr.state events total (baseline, post-diffs, mid-pulse, +# post-clear); we read the LAST THREE: # -# Together the two runs bracket the override lever (set / clear) around a -# live diff table and confirm GetTxPowerState reports rate_diffs=1 with the -# same mcs7-ofdm delta on both sides. -for flat_val in 40 -1; do - lbl="flat$flat_val"; lbl="${lbl/-/neg}" - ov_log="$OUT/$tag-override-$lbl.log" - run_step_demo "$ov_log" --vid "$VID" --pid "$PID" --channel "$CH_A" \ - --rate-diffs "$DIFFS" --flat "$flat_val" - ov_rd="$(state_field_last "$ov_log" rate_diffs)" - ov_ofdm="$(state_field_last "$ov_log" ofdm)" - ov_mcs7="$(state_field_last "$ov_log" mcs7)" - if [ -z "$ov_ofdm" ] || [ "$ov_ofdm" = "-1" ]; then - fail "$name override(--flat $flat_val): no post-diff state readback" - continue - fi - want_mcs7="$((ov_ofdm - 16))" - if [ "$ov_rd" = "1" ] && [ "$ov_mcs7" = "$want_mcs7" ]; then - pass "$name override(--flat $flat_val): rate_diffs=1, mcs7-ofdm=-16 intact (ofdm=$ov_ofdm mcs7=$ov_mcs7)" +# 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 (this is the Issue 1 fix under test: a +# stale fix would still show mcs7-ofdm=-16 here even though the hardware +# has no per-rate spread). +# 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 - fail "$name override(--flat $flat_val): rate_diffs=$ov_rd ofdm=$ov_ofdm mcs7=$ov_mcs7 (want rate_diffs=1, mcs7=$want_mcs7)" + pd_want_mcs7=$((pd_ofdm - 16)) + pc_want_mcs7=$((pc_ofdm - 16)) + ok=1 + 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)" + ok=0 + 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)" + ok=0 + 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)" + ok=0 + fi fi -done +fi echo echo "== txpwr-rate-diffs regcheck: PASS=$PASS FAIL=$FAIL SKIP=$SKIP ==" From 3d1687b35fa3d95819d133e3f8f658afcabdedfc Mon Sep 17 00:00:00 2001 From: Gilang Date: Wed, 22 Jul 2026 05:29:29 +0700 Subject: [PATCH 8/8] =?UTF-8?q?jaguar3(8822e):=20address=20rate-diffs=20re?= =?UTF-8?q?view=20=E2=80=94=20clamp,=20flag,=20early=20validation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Code-review follow-ups on SetTxPowerRateDiffs: - Clamp caller diffs to the 7-bit two's-complement field range [-64, 63] in SetTxPowerRateDiffs before storing. pack_rate_diff_word masks with & 0x7f, which would alias the sign of an over-range int8_t (a -100 cut lands +28, a power boost). Range documented on the TxRateDiffsQdb fields. - GetTxPowerState: report rate_diffs_custom regardless of bring-up / CW state (it means "a table is configured", knowable without a readback). Hoisted _reg_mu so the non-atomic _rate_diffs read stays race-free. - txpower --rate-diffs: parse and range-check the 10 ints in parse_args so a typo fails before chip bring-up, with a real error instead of a silent strtol->int8_t truncation. - Drop provenance clutter from the TxPower.h header comment (keep the why); drop "in v1" future-promising phrasing across the feature; remove a dead shell var and a review-thread reference in the regcheck; fix a help-column space. ctest 37/37; register-level regcheck assertions unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) --- examples/txpower/main.cpp | 69 +++++++++++++++++--------- src/IRtlDevice.h | 2 +- src/TxPower.h | 17 ++++--- src/jaguar3/RadioManagementJaguar3.cpp | 2 +- src/jaguar3/RadioManagementJaguar3.h | 2 +- src/jaguar3/RtlJaguar3Device.cpp | 32 +++++++++--- tests/txpwr_rate_diffs_regcheck.sh | 12 ++--- 7 files changed, 86 insertions(+), 50 deletions(-) diff --git a/examples/txpower/main.cpp b/examples/txpower/main.cpp index 790b2928..ba3817ef 100644 --- a/examples/txpower/main.cpp +++ b/examples/txpower/main.cpp @@ -19,7 +19,7 @@ * --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 + * --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 @@ -93,7 +93,9 @@ struct Args { int switch_channel = -1; int retune = -1; bool thermal = false; - std::string rate_diffs; + bool have_rate_diffs = false; + bool rate_diffs_clear = false; + devourer::TxRateDiffsQdb rate_diffs; int flat_pulse = 0; bool have_flat_pulse = false; }; @@ -141,7 +143,44 @@ bool parse_args(int argc, char **argv, Args &a) { else if (k == "--rate-diffs") { if (i + 1 >= argc) return false; - a.rate_diffs = argv[++i]; + 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 { @@ -276,30 +315,12 @@ int main(int argc, char **argv) { print_state(dev.get(), a.thermal); } - if (!a.rate_diffs.empty()) { - if (a.rate_diffs == "clear") { + 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 { - devourer::TxRateDiffsQdb d; - int v[10] = {0}; - int n = 0; - const char* p = a.rate_diffs.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 2; - } - d.cck = static_cast(v[0]); - d.legacy = static_cast(v[1]); - for (int i2 = 0; i2 < 8; ++i2) d.mcs[i2] = static_cast(v[2 + i2]); - const bool ok = dev->SetTxPowerRateDiffs(d); + const bool ok = dev->SetTxPowerRateDiffs(a.rate_diffs); logger->info("rate-diffs -> {}", ok ? "applied" : "unsupported"); } print_state(dev.get(), a.thermal); diff --git a/src/IRtlDevice.h b/src/IRtlDevice.h index 740aa55d..b0b3d4cf 100644 --- a/src/IRtlDevice.h +++ b/src/IRtlDevice.h @@ -145,7 +145,7 @@ class IRtlDevice { * 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 in v1). */ + * the 8822E). */ virtual bool SetTxPowerRateDiffs(const std::optional& diffs) { (void)diffs; return false; diff --git a/src/TxPower.h b/src/TxPower.h index aa5f9f71..f806430e 100644 --- a/src/TxPower.h +++ b/src/TxPower.h @@ -109,15 +109,16 @@ inline int txpkt_pwr_db_for_step(uint8_t step) { } /* Caller-supplied per-rate TXAGC diffs (signed qdB vs the reference anchor). - * v1 consumer: mabur's wall-equalized ladder — each rate parked a uniform - * margin below its measured PA-compression wall. Programmed by - * IRtlDevice::SetTxPowerRateDiffs; 8822E-only in v1. On the 8822E one qdB - * equals one TXAGC index step (step_qdb = 1), so values are used verbatim - * as index diffs. */ + * 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 */ - int8_t legacy = 0; /* OFDM 6..54M — control frames */ - int8_t mcs[8] = {0, 0, 0, 0, 0, 0, 0, 0}; /* HT MCS0..7 */ + 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 --git a/src/jaguar3/RadioManagementJaguar3.cpp b/src/jaguar3/RadioManagementJaguar3.cpp index cb4dba26..aad54e61 100644 --- a/src/jaguar3/RadioManagementJaguar3.cpp +++ b/src/jaguar3/RadioManagementJaguar3.cpp @@ -1075,7 +1075,7 @@ void RadioManagementJaguar3::apply_rate_diffs_8822e( /* 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 v1's caller-supplied set): */ + /* 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}, diff --git a/src/jaguar3/RadioManagementJaguar3.h b/src/jaguar3/RadioManagementJaguar3.h index 4d6b9b01..2aa6240a 100644 --- a/src/jaguar3/RadioManagementJaguar3.h +++ b/src/jaguar3/RadioManagementJaguar3.h @@ -123,7 +123,7 @@ class RadioManagementJaguar3 { /* 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 consumer in v1). qdB == index steps on this + * 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); diff --git a/src/jaguar3/RtlJaguar3Device.cpp b/src/jaguar3/RtlJaguar3Device.cpp index 52dae466..1fc75bfa 100644 --- a/src/jaguar3/RtlJaguar3Device.cpp +++ b/src/jaguar3/RtlJaguar3Device.cpp @@ -1367,15 +1367,30 @@ void RtlJaguar3Device::SetTxPowerIndexOverride(int idx) { bool RtlJaguar3Device::SetTxPowerRateDiffs( const std::optional &diffs) { if (_variant != jaguar3::ChipVariant::C8822E) { - _logger->warn("SetTxPowerRateDiffs: 8822E-only in v1"); + _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 = diffs; + _rate_diffs = clamped; if (_brought_up) apply_tx_power_current(/*full=*/true); /* re-walk the table now */ return true; @@ -1583,12 +1598,18 @@ 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; const uint32_t cck = (_device.rtw_read32(0x18a0) >> 16) & 0x7f; @@ -1614,7 +1635,6 @@ devourer::TxPowerState RtlJaguar3Device::GetTxPowerState() { s.mcs7_index = static_cast(ofdm); s.cck_index = static_cast(cck); } - s.rate_diffs_custom = _rate_diffs.has_value(); s.hw_readback = true; } return s; diff --git a/tests/txpwr_rate_diffs_regcheck.sh b/tests/txpwr_rate_diffs_regcheck.sh index c0cd702e..517a7339 100755 --- a/tests/txpwr_rate_diffs_regcheck.sh +++ b/tests/txpwr_rate_diffs_regcheck.sh @@ -29,7 +29,7 @@ # RtlJaguar3Device::apply_tx_power_current's full-apply path). # # Only the 8822E (RTL8812EU/RTL8822EU, PID 0xa81a) implements -# SetTxPowerRateDiffs in v1 (RtlJaguar3Device::SetTxPowerRateDiffs returns +# SetTxPowerRateDiffs (RtlJaguar3Device::SetTxPowerRateDiffs returns # false off that variant) — this script targets that DUT only and SKIPs # cleanly when it isn't plugged in. # @@ -53,7 +53,7 @@ trap cleanup EXIT INT TERM echo "== building ==" cmake --build "$ROOT/build" -j --target txpower >/dev/null || exit 1 -# DUT: only the 8822E implements SetTxPowerRateDiffs in v1. CH_A/CH_B are +# 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. @@ -165,9 +165,7 @@ fi # 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 (this is the Issue 1 fix under test: a -# stale fix would still show mcs7-ofdm=-16 here even though the hardware -# has no per-rate spread). +# 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 @@ -199,24 +197,20 @@ else else pd_want_mcs7=$((pd_ofdm - 16)) pc_want_mcs7=$((pc_ofdm - 16)) - ok=1 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)" - ok=0 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)" - ok=0 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)" - ok=0 fi fi fi