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
35 changes: 23 additions & 12 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -307,18 +307,29 @@ rewrites TXAGC). `GetTxPowerCaps` reports the family step: 0.5 dB Jaguar1/2,
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).
third knob, on every generation (check `GetTxPowerCaps().rate_diffs`): a
caller-supplied per-rate table (cck, legacy, mcs0..7, signed qdB) that
**replaces** the chip's calibrated per-rate shape — every rate's level =
**anchor** + its diff (quantized to the family step) + the runtime offset,
clamped to the hardware rail, where the anchor is the level the chip's own
default walk produces for HT MCS7. So a zero table is a no-op at MCS7 and
flattens every other rate onto it; rates the struct doesn't describe (MCS8+,
VHT/HE, 2SS+) sit at the anchor. Diffs are not regulatory-clamped — the same
operator's call the other two knobs make. It sticks across
`SetMonitorChannel`, `FastRetune`, and an override set/clear round trip
(the clear path re-walks the caller table); `std::nullopt` restores the
calibrated shape. **Kestrel is the one software fold**
(`rate_diffs_hw_table=false`): with no per-rate TXAGC table under its
fixed-dBm model the diff for the frame's own rate goes into the BB target at
send time, so a fixed-rate stream costs one extra write and a rate ladder
pays one per change — and a hardware-timed beacon airing between host frames
inherits the last frame's level. `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; `txdemo`
maps it as `DEVOURER_TX_RATE_DIFFS`. Validation: `tests/txpwr_offset_regcheck.sh`
(offset/override), `tests/txpwr_rate_diffs_regcheck.sh` (diff table registers /
shadow — state management, NOT radiated power on a write-only family) and
`tests/txpwr_rate_diffs_onair.sh` (the antenna, per rate).

**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
2 changes: 1 addition & 1 deletion docs/adaptive-link-building-blocks.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ milliseconds, so a controller can retune between frames.
| **Modulation / MCS** (time-on-air) | strong | **strong** — less airtime, fewer Joules/bit | per-packet radiotap rate, or the device TX-mode default (`DEVOURER_TX_RATE`); immediate |
| **FEC strength** | strong | strong | application-layer — the outer code + the per-temporal-layer MCS/FEC ladder (`DEVOURER_SVC_LADDER`, [`fused-fec.md`](fused-fec.md)), not a PHY register |
| **Channel / bandwidth** | strong | moderate | per-packet retune (~1–2 ms intra-band fast retune / `DEVOURER_HOP_*`, longer on a band change); 20/40/80 MHz plus a 5/10 MHz **narrowband re-clock** that trades throughput for link budget |
| **Transmit power** | **strongest** | **weak** — the always-on baseline draw dominates | the runtime TX-power API on every generation: `SetTxPowerOffsetQdb` (quarter-dB offset relative to the efuse per-rate table — the closed-loop knob, sticky across retunes, with saturation flags), `SetTxPowerIndexOverride` (flat index), `GetTxPowerState`/`GetTxPowerCaps` readback, `GetThermalStatus` (the PA-heating budget input); applied live, no channel switch |
| **Transmit power** | **strongest** | **weak** — the always-on baseline draw dominates | the runtime TX-power API on every generation: `SetTxPowerOffsetQdb` (quarter-dB offset relative to the efuse per-rate table — the closed-loop knob, sticky across retunes, with saturation flags), `SetTxPowerIndexOverride` (flat index), `SetTxPowerRateDiffs` (a caller per-rate shape replacing the calibrated one — the wall-equalized rate ladder, and the lever for spending margin where a fixed-rate link needs it), `GetTxPowerState`/`GetTxPowerCaps` readback, `GetThermalStatus` (the PA-heating budget input); applied live, no channel switch |
| **Active receive chains** | conditional | **conditional** — pays only when the antennas decorrelate (motion) | the RX-path enable mask (`DEVOURER_RX_PATHS`); a fade-state lever, not a range lever |
| **Duty cycle** | — | direct | inter-frame gap; back-to-back for maximum airtime, idle to save it |

Expand Down
4 changes: 2 additions & 2 deletions docs/logging.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ Emitters: L = library, RX/TX/... = demo. Optional fields in [brackets];
| ev | emitter | fields |
|---|---|---|
| `init.timing` | L (`src/InitTimer.h`) + demos | stage ("scope.stage", e.g. "demo.first_rx_frame", "txdemo.first_tx_submit"), ms |
| `adapter.caps` | RX, TX, doctor, txpower (`examples/common/caps_event.h`) | supported, chip, names, chip_id "0x..", gen, variant, transport, tx_chains, rx_chains, n_ss, stbc, ldpc, sgi, bw_max, bw[] (MHz), txpwr_max, txpwr_step_qdb, txpwr_step_measured, txpwr_min_qdb, txpwr_max_qdb, tune_2g4[]\|null, tune_5g[]\|null, char_2g4[]\|null, char_5g[]\|null, ldpc_rx_ht, ldpc_rx_vht, ldpc_rx_flag, per_pkt_txpwr, narrowband, fastretune, he_er_su, per_chain_rssi |
| `adapter.caps` | RX, TX, doctor, txpower (`examples/common/caps_event.h`) | supported, chip, names, chip_id "0x..", gen, variant, transport, tx_chains, rx_chains, n_ss, stbc, ldpc, sgi, bw_max, bw[] (MHz), txpwr_max, txpwr_step_qdb, txpwr_step_measured, txpwr_min_qdb, txpwr_max_qdb, txpwr_rate_diffs, txpwr_rate_diffs_hw, txpwr_rate_diffs_measured, tune_2g4[]\|null, tune_5g[]\|null, char_2g4[]\|null, char_5g[]\|null, ldpc_rx_ht, ldpc_rx_vht, ldpc_rx_flag, per_pkt_txpwr, narrowband, fastretune, he_er_su, per_chain_rssi |
| `debug.wreg` | L (`DEVOURER_LOG_WRITES`) | addr "0x0nnn", width, val "0x…" |
| `hop.prof` | L (`DEVOURER_HOP_PROF`) | gen, ch, `<stage>_us`…, total_us |
| `tx.fail` | L (send failure; regress.py keys on it) | {status, actual_len, timeout} or {rc, timeout} |
Expand Down Expand Up @@ -120,7 +120,7 @@ Emitters: L = library, RX/TX/... = demo. Optional fields in [brackets];
| `txpwr.set` | TX | index, t_ms |
| `txpwr.readback` | TX | index, cck1m, ofdm6m, mcs7, rb |
| `txpwr.state` | txpower | flat, offset_qdb, steps, satlo, sathi, cck, ofdm, mcs7, rb |
| `txpwr.caps` | txpower | supported, max, step_qdb, step_measured, min_qdb, max_qdb |
| `txpwr.caps` | txpower | supported, max, step_qdb, step_measured, min_qdb, max_qdb, rate_diffs, rate_diffs_hw (0 = software send-time fold), rate_diffs_measured |
| `txpwr.offset` | txpower | requested, applied |
| `thermal` | RX, TX, txpower | t, raw, baseline\|null, [delta], status |

Expand Down
5 changes: 4 additions & 1 deletion examples/common/caps_event.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,10 @@ inline void emit_adapter_caps(EventSink &sink, IRtlDevice *dev) {
.f("txpwr_step_qdb", c.txpwr.step_qdb)
.f("txpwr_step_measured", c.txpwr.step_measured ? 1 : 0)
.f("txpwr_min_qdb", c.txpwr.offset_min_qdb)
.f("txpwr_max_qdb", c.txpwr.offset_max_qdb);
.f("txpwr_max_qdb", c.txpwr.offset_max_qdb)
.f("txpwr_rate_diffs", c.txpwr.rate_diffs ? 1 : 0)
.f("txpwr_rate_diffs_hw", c.txpwr.rate_diffs_hw_table ? 1 : 0)
.f("txpwr_rate_diffs_measured", c.txpwr.rate_diffs_measured ? 1 : 0);

auto band = [&ev](const char *k, const BandRange &b) {
if (b.valid) {
Expand Down
41 changes: 41 additions & 0 deletions examples/tx/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -829,6 +829,47 @@ int main(int argc, char **argv) {
rtlDevice->SetTxPowerOffsetQdb(
static_cast<int>(std::strtol(p, nullptr, 0)));

/* DEVOURER_TX_RATE_DIFFS=cck,legacy,m0,...,m7 — ten signed qdB replacing the
* chip's calibrated per-rate shape (SetTxPowerRateDiffs, src/TxPower.h):
* every rate sits at the HT MCS7 anchor plus its own diff. The range lever
* for a fixed-rate link — boost CCK / low-MCS, trim high-MCS for PA
* headroom — independent of the descriptor rate. Composes with
* DEVOURER_TX_PWR_OFFSET_QDB above; unset leaves the calibrated shape.
* Quantized to the family step (0.25 dB Jaguar3/Kestrel, 0.5 dB
* Jaguar1/Jaguar2), so an odd qdB rounds. */
if (const char *p = std::getenv("DEVOURER_TX_RATE_DIFFS")) {
devourer::TxRateDiffsQdb d;
int v[10] = {0};
int n = 0;
const char *cur = p;
char *end = nullptr;
while (n < 10) {
v[n++] = static_cast<int>(std::strtol(cur, &end, 10));
if (*end != ',')
break;
cur = end + 1;
}
bool in_range = true;
for (int j = 0; j < n; ++j)
if (v[j] < -64 || v[j] > 63)
in_range = false; /* the int8_t cast below would wrap the sign */
if (n != 10 || (end && *end != '\0')) {
logger->error("DEVOURER_TX_RATE_DIFFS wants 10 comma-separated qdB "
"(cck,legacy,m0..m7) — ignoring '{}'", p);
} else if (!in_range) {
logger->error("DEVOURER_TX_RATE_DIFFS values must be in [-64, 63] qdB "
"— ignoring '{}'", p);
} else {
d.cck = static_cast<int8_t>(v[0]);
d.legacy = static_cast<int8_t>(v[1]);
for (int j = 0; j < 8; ++j)
d.mcs[j] = static_cast<int8_t>(v[2 + j]);
if (!rtlDevice->SetTxPowerRateDiffs(d))
logger->warn("DEVOURER_TX_RATE_DIFFS: per-rate diffs unsupported on "
"this chip");
}
}

/* DEVOURER_TX_PKT_OFSET=N — default per-packet TX-power LUT step written
* into every TX descriptor: 0=none, 1=-3dB, 2=-7dB, 3=-11dB, 4=+3dB,
* 5=+6dB. One env var, four families: Jaguar2 8822B/8821C
Expand Down
10 changes: 8 additions & 2 deletions examples/txpower/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@
* --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
* (cck,legacy,m0..m7) or 'clear' to nullopt.
* Honoured where txpwr.caps reports
* rate_diffs=1; rate_diffs_hw=0 marks the
* software send-time fold (Kestrel)
* --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
Expand Down Expand Up @@ -303,7 +306,10 @@ int main(int argc, char **argv) {
.f("step_qdb", caps.step_qdb)
.f("step_measured", caps.step_measured ? 1 : 0)
.f("min_qdb", caps.offset_min_qdb)
.f("max_qdb", caps.offset_max_qdb);
.f("max_qdb", caps.offset_max_qdb)
.f("rate_diffs", caps.rate_diffs ? 1 : 0)
.f("rate_diffs_hw", caps.rate_diffs_hw_table ? 1 : 0)
.f("rate_diffs_measured", caps.rate_diffs_measured ? 1 : 0);
if (!caps.supported) {
logger->error("TX-power API not wired for this family yet");
dev->Stop();
Expand Down
50 changes: 50 additions & 0 deletions src/TxPower.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,56 @@ int quantize_offset_qdb(int qdb, const TxPowerCaps &caps, int *steps_out) {
return steps * step;
}

int rate_diff_steps(int qdb, uint8_t step_qdb) {
if (step_qdb == 0)
return 0;
const int step = step_qdb;
if (qdb >= 0)
return (qdb + step / 2) / step;
return -((-qdb + step / 2) / step);
}

int rate_diff_qdb_for_rate(uint8_t mgn_rate, const TxRateDiffsQdb &d) {
/* MGN_* values from RateDefinitions.h, spelled numerically so this stays a
* neutral translation unit: CCK 1M/2M/5.5M/11M = 0x02/0x04/0x0b/0x16, the
* eight OFDM rates 6M..54M, HT MCS0..7 = 0x80..0x87. */
switch (mgn_rate) {
case 0x02:
case 0x04:
case 0x0b:
case 0x16:
return d.cck;
case 0x0c:
case 0x12:
case 0x18:
case 0x24:
case 0x30:
case 0x48:
case 0x60:
case 0x6c:
return d.legacy;
default:
break;
}
if (mgn_rate >= 0x80 && mgn_rate <= 0x87)
return d.mcs[mgn_rate - 0x80];
/* HT MCS8+, all VHT/HE, every 2SS+ stream: no diff, i.e. the anchor. */
return 0;
}

std::optional<TxRateDiffsQdb>
clamp_rate_diffs(const std::optional<TxRateDiffsQdb> &d) {
if (!d)
return std::nullopt;
auto cl = [](int8_t v) -> int8_t { return v < -64 ? -64 : (v > 63 ? 63 : v); };
TxRateDiffsQdb out = *d;
out.cck = cl(out.cck);
out.legacy = cl(out.legacy);
for (int i = 0; i < 8; ++i)
out.mcs[i] = cl(out.mcs[i]);
return out;
}

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;
Expand Down
Loading
Loading