Skip to content
Open
19 changes: 15 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<TxRateDiffsQdb>)` 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
Expand Down
10 changes: 10 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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 <devourer-linkhealth>, with cases
# drawn from real on-air data (the saturation-knee + AWGN-interference sweeps).
Expand Down
90 changes: 84 additions & 6 deletions examples/txpower/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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):
*
Expand All @@ -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
Expand All @@ -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
Expand All @@ -47,6 +55,7 @@
#include <cstdlib>
#include <cstring>
#include <memory>
#include <optional>
#include <string>
#include <thread>

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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<int>(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<int8_t>(v[0]);
a.rate_diffs.legacy = static_cast<int8_t>(v[1]);
for (int j = 0; j < 8; ++j)
a.rate_diffs.mcs[j] = static_cast<int8_t>(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());
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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;
Expand Down
12 changes: 12 additions & 0 deletions src/IRtlDevice.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include <cstddef>
#include <cstdint>
#include <functional>
#include <optional>

#include "AdapterCaps.h"
#include "AmpduMode.h"
Expand Down Expand Up @@ -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<devourer::TxRateDiffsQdb>& 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. */
Expand Down
8 changes: 8 additions & 0 deletions src/TxPower.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<uint32_t>(d0) & 0x7fu;
const uint32_t b1 = static_cast<uint32_t>(d1) & 0x7fu;
const uint32_t b2 = static_cast<uint32_t>(d2) & 0x7fu;
const uint32_t b3 = static_cast<uint32_t>(d3) & 0x7fu;
return b0 | (b1 << 8) | (b2 << 16) | (b3 << 24);
}

} // namespace devourer
28 changes: 28 additions & 0 deletions src/TxPower.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hardware field is 7-bit two's complement, so the usable range is [−64, 63] — but these are int8_t (±127) and pack_rate_diff_word masks with & 0x7f, which silently aliases the sign: a caller asking for −100 gets +28. For the adaptive-controller callers this knob targets, that's a power boost where a big cut was requested.

The codebase already guards the analogous ref wrap ("an unclamped over-range ref would wrap to near-zero TX silently", RadioManagementJaguar3.h). Same treatment here please: clamp to [−64, 63] in SetTxPowerRateDiffs (or reject), and document the range on these fields.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clamped to [-64, 63] in SetTxPowerRateDiffs before the table is stored, and documented the range on the TxRateDiffsQdb fields + struct comment. With in-range input the & 0x7f mask now encodes 7-bit two's-complement without aliasing the sign. 3d1687b

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 */
66 changes: 66 additions & 0 deletions src/jaguar3/RadioManagementJaguar3.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<uint16_t>(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. */
Expand Down
9 changes: 9 additions & 0 deletions src/jaguar3/RadioManagementJaguar3.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include "RtlAdapter.h"
#include "SelectedChannel.h"
#include "ChipVariant.h"
#include "TxPower.h" /* devourer::TxRateDiffsQdb */

namespace jaguar3 {

Expand Down Expand Up @@ -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). */
Expand Down
Loading