Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -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 <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 {
Comment thread
josephnef marked this conversation as resolved.
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
Loading