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
15 changes: 15 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -652,6 +652,21 @@ if(DEVOURER_PCIE)
)
target_link_libraries(beacon_phase_observer PUBLIC devourer)
endif()
# M0 contract probes (docs/scheduled-mac.md), USB or PCIe through
# examples/common/radio_open.h (DEVOURER_PCIE_BDF selects the vfio radio).
# POSIX bench tools (unistd.h / _exit), so not on Windows.
if(NOT WIN32)
foreach(m0probe dl_departure_tx txegress_witness beacon_update_probe ue_rx_probe)
add_executable(${m0probe}
tests/${m0probe}.cpp
examples/common/env_config.cpp
)
target_include_directories(${m0probe} PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/examples/common
${CMAKE_CURRENT_SOURCE_DIR}/examples/tdma)
target_link_libraries(${m0probe} PUBLIC devourer)
endforeach()
endif()

# kestrelprobe — staged USB bring-up prober for the Kestrel (RTL8852B/C):
# AX identity (die-id/cut) -> power-on + EFUSE -> firmware download, the
Expand Down
51 changes: 48 additions & 3 deletions docs/scheduled-mac.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,19 @@ the PCIe cell ships the probe to the remote rig, vfio-binds, runs, restores).
| Jaguar2 8812BU (sync USB3) | 14 µs | 64 µs | 1.7 ms | 3.1 ms | 3.3 ms |
| Jaguar3 8822CU (sync USB3) | 16 µs | 61 µs | 2.2 ms | 3.2 ms | 3.3 ms |
| 8821CE (PCIe, vfio) | 11 µs | 54 µs | 2.4 ms | 3.2 ms | 3.3 ms |
| Kestrel 8852CE (PCIe, vfio), ch36 | 2.0 µs | 4.1 µs | 0.25 ms | 2.7 ms | 5.1 ms |
| Kestrel 8852CE (PCIe, vfio), ch149 | 1.3 µs | 2.3 µs | 34 µs | 0.46 ms | 4.8 ms |

The Kestrel rows are the epic-254 rig (TX and an 8852CE witness in two
J6412 boxes, ~2400 frames per cell, `DEVOURER_PCIE_BDF` through
`examples/common/radio_open.h`). ch36 carries a foreign AP (two BSSIDs at
~−83 dBm); ch149 had no decodable traffic in a channel survey. On ch36 about
60% of the frames later than 50 µs fell in two 10 ms bins of a 102.4 ms
period, i.e. phase-locked to that AP's beacons — even with carrier sense off
(`SetCcaMode(true)`). Keeping carrier sense on (`DL_CSMA=1`) did not change the
picture on either channel (ch149: p99 41 µs, p99.9 1.6 ms, max 4.3 ms); the
few ms-class frames remain on the clean channel and their cause is not
established.

Crystal ppm sane (−17 ppm, same reference witness) on every cell — the robust
fits locked. The tail is run-to-run ambient-dependent: the same J1 cell has
Expand Down Expand Up @@ -109,6 +122,13 @@ separating update-caused missing slots from background witness loss.
| Jaguar1 8812AU | 30/30 | 0.0 | 0 | 0 | 40 ms | 67 ms |
| Jaguar2 8812BU | 30/30 | 0.0 | 0 | 0 | 63 ms | 174 ms |
| Jaguar3 8822CU | 30/30 | 0.0 | 0 | 0 | 72 ms | 99 ms |
| Kestrel 8852CE (PCIe), ch149 | 30/30 | 0.0 | 0 | 0 | 13 ms | 16 ms |

Kestrel row: TX and witness are the two 8852CEs of one J6412 (shared host
clock). The update is the same `BCN_UPD_V1` H2C `StartBeacon` sends, with no
port or TBTT reprogramming. The probe's updates come at a fixed spacing of 10
intervals, so their phase against the TBTT barely moves and the latency here
samples one point of the grid: every update aired on the next beacon.

### The contract

Expand Down Expand Up @@ -294,9 +314,14 @@ nonzero limit for absolute numbers; 8821AU row re-measured ch6):
| 8812EU | 98% | 0% | works |
| 8812CU | 69% | 0% | works |
| 8733B | unmeasured | 0% | works (closed-loop 1725/1725 at retry 12; single-shot cell never run) |
| 8852CU (Kestrel) | 0% | 0% | not implemented on the AX generation |

Unmeasured for lack of plugged hardware: 8821CU / PCIe 8821CE (recipe-shared
| 8852CU (Kestrel) | 0% | 0% | measured before the AX responder existed; see [kestrel-ack-responder.md](kestrel-ack-responder.md) |
| 8852CE (Kestrel, PCIe) | 100% (closed loop, retry 8) | 0% (9 copies) | works |

8852CE row (epic-254 rig, ch149, MCS3, 200-byte QoS-Data at a 20 ms gap): Kestrel has
no CCX report, so a third 8852CE counted on-air copies per payload counter
(the `tests/kestrel_retry_witness.sh` method). Responder armed: 1036/1036
frames aired once. Not armed: 184 of 185 aired 9 times (limit 8 = 9
attempts). Unmeasured for lack of plugged hardware: 8821CU / PCIe 8821CE (recipe-shared
with the 8822B; their `AdapterCaps.ack_responder_ok` stays false-as-unmeasured
until a cell runs). The `ack_responder_ok` / `tx_retry_limit_ok` caps flags
carry this table per die.
Expand Down Expand Up @@ -401,6 +426,26 @@ TX1 2955 frames at −51 dBm mean, TX2 741 frames at −44 dBm mean, a 4.0×
count ratio exactly matching the 4× cadence ratio — confirming per-UE frame
counts and per-UE signal statistics don't bleed between transmitters.

**Kestrel (8852CE PCIe DU, two 8852CE UEs, ch149, 10 ms / 40 ms gaps):** the
counts separate (ratio 3.90 against 4), but the first runs showed the signal
statistics *did* bleed. With UE2 attenuated by `DEVOURER_TX_PWR_OFFSET_QDB=-40`,
UE1's window read −58.6 dBm / 32.7 dB SNR and UE2's −55.0 dBm / 38.2 dB, the
opposite of the change made. Cause: the Kestrel RX loop attached each PPDU-status
report to the WIFI frames that followed it, but the MAC delivers a PPDU's
frames *first* and its status after them (traced on the 8852CE: the status
with the same `ppdu_cnt` arrives in the next RX buffer, ~140 µs later). So
every frame carried the previous PPDU's RSSI/SNR/EVM. The RX loop now pairs them
the vendor way (`phl_rx.c` `sts_ent` queues): it holds the PPDU's frames until the status
with the same `ppdu_cnt`, rate, bandwidth, GI/LTF and PPDU type arrives. Frames whose
status never comes are delivered without one (RSSI 0 = no sample) on the next
PPDU, after 2 ms idle, or at loop exit. After the fix, the same sweep reads
UE1 −55.0 dBm / 38 dB in every arm and UE2 −55.0 → −71.5 dBm (SNR 38.9 → 13.4
dB) at −40 qdB. At −80 qdB UE2 was not decoded at all. Two observations are not
explained: the drop is 16.5 dB for a nominal 10 dB offset, and both full-power UEs read
exactly −55 dBm at desk range (possibly a reporting ceiling). The same fix
corrects `GetRxQuality` and `GetActiveRxPaths` on Kestrel, which folded the
same misattributed values.

### Tooling

- `tests/ue_rx_probe.cpp` — on-air probe: feeds every decoded frame into a
Expand Down
75 changes: 75 additions & 0 deletions examples/common/radio_open.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// radio_open.h — open the radio a bench tool names in its environment, over
// USB or PCIe, so one probe binary serves both transports:
// DEVOURER_PCIE_BDF=0000:05:00.0 -> vfio PCIe (DEVOURER_PCIE=ON builds; the
// device bound by tests/pcie_vfio_bind.sh)
// otherwise -> USB by DEVOURER_VID / DEVOURER_PID, with
// the tool's own defaults.
// The radio gets devourer_config_from_env() either way. The USB context and
// handle are deliberately never released: these are one-shot bench tools that
// end in _exit/return from main, so the OS reclaims them.
#ifndef DEVOURER_RADIO_OPEN_H
#define DEVOURER_RADIO_OPEN_H

#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <memory>
#include <string>

#include <libusb.h>

#include "UsbOpen.h"
#include "WiFiDriver.h"
#include "env_config.h"
#include "logger.h"
#if defined(DEVOURER_HAVE_PCIE)
#include "PcieTransport.h"
#endif

struct OpenedRadio {
std::unique_ptr<IRadio> dev; /* null on failure (the reason is on stderr) */
std::string label; /* "pcie 0000:05:00.0" / "usb 0bda:8812" */
};

inline OpenedRadio open_radio_from_env(const std::shared_ptr<Logger> &logger,
uint16_t def_vid, uint16_t def_pid) {
OpenedRadio r;
if (const char *bdf = std::getenv("DEVOURER_PCIE_BDF")) {
#if defined(DEVOURER_HAVE_PCIE)
r.label = std::string("pcie ") + bdf;
auto t = devourer::PcieTransport::Open(bdf, logger);
if (!t) {
fprintf(stderr, "pcie open failed for %s (vfio-bound?)\n", bdf);
return r;
}
WiFiDriver wifi(logger);
r.dev = wifi.CreateRadioPcie(std::move(t), devourer_config_from_env());
if (!r.dev) fprintf(stderr, "no driver for the PCIe chip at %s\n", bdf);
#else
fprintf(stderr, "DEVOURER_PCIE_BDF=%s but this build has no PCIe "
"(configure with -DDEVOURER_PCIE=ON)\n", bdf);
#endif
return r;
}
uint16_t vid = def_vid, pid = def_pid;
if (const char *v = std::getenv("DEVOURER_VID")) vid = (uint16_t)strtoul(v, 0, 0);
if (const char *p = std::getenv("DEVOURER_PID")) pid = (uint16_t)strtoul(p, 0, 0);
char buf[24];
snprintf(buf, sizeof buf, "usb %04x:%04x", vid, pid);
r.label = buf;
libusb_context *ctx = nullptr;
if (libusb_init(&ctx) < 0) { fprintf(stderr, "libusb_init failed\n"); return r; }
libusb_set_option(ctx, LIBUSB_OPTION_LOG_LEVEL, LIBUSB_LOG_LEVEL_WARNING);
auto *h = libusb_open_device_with_vid_pid(ctx, vid, pid);
if (!h) { fprintf(stderr, "open fail %04x:%04x\n", vid, pid); return r; }
std::shared_ptr<devourer::UsbDeviceLock> lk;
if (devourer::claim_interface_then_reset(h, devourer::find_wifi_interface(h),
logger, true, lk) != 0)
return r;
WiFiDriver wifi(logger);
r.dev = wifi.CreateRadio(h, ctx, lk, devourer_config_from_env());
if (!r.dev) fprintf(stderr, "no driver for %04x:%04x\n", vid, pid);
return r;
}

#endif /* DEVOURER_RADIO_OPEN_H */
1 change: 1 addition & 0 deletions src/RtlAdapter.h
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ class RtlAdapter {
void hci_setup() { _transport->hci_setup(); }
/* PCIe LTR permission (ITransport::ltr_allowed); true on USB. */
bool ltr_allowed() { return _transport->ltr_allowed(); }
void set_rx_max_wait_us(unsigned us) { _transport->set_rx_max_wait_us(us); }
/* Pipelined register writes — see ITransport::write_batch_begin. */
void write_batch_begin() { _transport->write_batch_begin(); }
bool write_batch_end() { return _transport->write_batch_end(); }
Expand Down
5 changes: 5 additions & 0 deletions src/Transport.h
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,11 @@ class ITransport {
(void)buf; (void)len; (void)timeout_ms;
return -1;
}
/* Cap on how long rx_loop may wait for data between should_stop() polls
* (us; 0 = the transport default). A caller whose stop predicate does
* time-based work (Kestrel's held-frame flush) sets it; PCIe already polls
* every few hundred us, so only USB implements it. */
virtual void set_rx_max_wait_us(unsigned us) { (void)us; }
virtual void clear_halt(uint8_t ep) { (void)ep; }
/* Stop the TX engine and return only once nothing is in flight: cancel every
* outstanding transfer, reap the completions, and refuse further sends. The
Expand Down
7 changes: 6 additions & 1 deletion src/UsbTransport.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -873,7 +873,10 @@ void UsbTransport::rx_loop(
auto last_ring = std::chrono::steady_clock::now();
while (!should_stop() && sh.active > 0 &&
!sh.dead.load(std::memory_order_relaxed)) {
const unsigned cap = _rx_max_wait_us.load(std::memory_order_relaxed);
struct timeval tv {0, 100000};
if (cap > 0 && cap < 100000) /* suseconds_t is int on macOS: no narrowing init */
tv.tv_usec = static_cast<decltype(tv.tv_usec)>(cap);
libusb_handle_events_timeout_completed(_ctx, &tv, nullptr);
if (sh.telemetry) {
const auto now = std::chrono::steady_clock::now();
Expand Down Expand Up @@ -974,7 +977,9 @@ void UsbTransport::rx_loop_sync(
auto last_ring = std::chrono::steady_clock::now();
long long cb_max_us = 0;
while (!should_stop()) {
int n = rx_raw(buf.data(), buf_size, 200);
const unsigned cap = _rx_max_wait_us.load(std::memory_order_relaxed);
const int wait_ms = cap > 0 && cap < 200000 ? static_cast<int>(cap / 1000 > 0 ? cap / 1000 : 1) : 200;
int n = rx_raw(buf.data(), buf_size, wait_ms);
if (n > 0) {
if (telemetry) {
const auto t0 = std::chrono::steady_clock::now();
Expand Down
2 changes: 2 additions & 0 deletions src/UsbTransport.h
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ class UsbTransport final : public ITransport {
const std::function<void(const uint8_t *, int)> &on_data,
const std::function<bool()> &should_stop) override;
int rx_raw(uint8_t *buf, int len, int timeout_ms) override;
void set_rx_max_wait_us(unsigned us) override { _rx_max_wait_us = us; }
void clear_halt(uint8_t ep) override { libusb_clear_halt(_dev_handle, ep); }
void quiesce_tx() override;

Expand Down Expand Up @@ -272,6 +273,7 @@ class UsbTransport final : public ITransport {
RxMode _rx_mode = RxMode::Async;
int _pool_spare = 0;
int _ring_ms = 0;
std::atomic<unsigned> _rx_max_wait_us{0}; /* set_rx_max_wait_us; 0 = default */
PoolExhaust _pool_exhaust = PoolExhaust::Backpressure;

/* rx_loop helpers for the servicing strategies dispatched off _rx_mode. */
Expand Down
20 changes: 20 additions & 0 deletions src/kestrel/FrameParserKestrel.h
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,26 @@ inline bool parse_rx_8852b(const uint8_t *buf, size_t buflen,
return true;
}

/* Do two sub-packet descriptors describe the same PPDU? The vendor pairing
* check (phl_rx.c): the 3-bit ppdu_cnt wraps every 8 PPDUs, so rate, bw,
* GI/LTF and PPDU type must match too. Used both to grow a held batch of WIFI
* frames and to pair a PPDU-status report with it. */
inline bool same_ppdu(const KestrelRxFrame &a, const KestrelRxFrame &b) {
return a.ppdu_cnt == b.ppdu_cnt && a.rx_rate == b.rx_rate && a.bw == b.bw &&
a.gi_ltf == b.gi_ltf && a.ppdu_type == b.ppdu_type;
}

/* Length of a leading radiotap header on a beacon buffer, or 0 (IRadio beacon
* contract: StartBeacon / UpdateBeaconPayload strip one). A beacon MPDU starts
* with FC byte 0x80; radiotap starts with version 0 and its little-endian
* it_len at bytes 2..3, which must leave at least one byte of MPDU. */
inline size_t beacon_radiotap_len(const uint8_t *b, size_t len) {
if (b == nullptr || len < 8 || b[0] != 0)
return 0;
const size_t rt = static_cast<size_t>(b[2] | (b[3] << 8));
return (rt >= 8 && rt < len) ? rt : 0;
}

/* --- PPDU-status (halbb physts) parse: per-path RSSI / SNR / EVM ---
*
* A PPDU-status payload is the bare halbb physts blob (HalKestrel clears the
Expand Down
18 changes: 18 additions & 0 deletions src/kestrel/HalKestrel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1749,6 +1749,24 @@ bool HalKestrel::start_beacon(const uint8_t *body, uint32_t len,
return true;
}

bool HalKestrel::update_beacon(const uint8_t *body, uint32_t len,
uint16_t rate_ax) {
return _fw.fw_send_beacon(body, len, /*macid=*/0, rate_ax, /*ntx_path_en=*/1,
/*path_map_a=*/0);
}

bool HalKestrel::stop_beacon() {
namespace r = kestrel::reg;
/* Beacon TX off, and the port back to the NO_LINK injection identity
* start_beacon moved it from (IRadio StopBeacon contract). The port stays
* FUNC-enabled, as the injection setup leaves it. */
uint32_t p = _device.rtw_read32(r::R_AX_PORT_CFG_P0);
p = r::set_clr_word(p, r::MAC_AX_NET_TYPE_NO_LINK, r::B_AX_NET_TYPE_P0_MSK,
r::B_AX_NET_TYPE_P0_SH);
p &= ~r::B_AX_BCNTX_EN_P0;
return _device.rtw_write32(r::R_AX_PORT_CFG_P0, p);
}

void HalKestrel::mac_enable_imr() {
/* mac_enable_imr(DMAC)+(CMAC0) + mac_err_imr_ctrl(EN): the System-Error-
* Recovery interrupt masks (ser_imr_config_8852b auto-gen). Each entry does
Expand Down
7 changes: 7 additions & 0 deletions src/kestrel/HalKestrel.h
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,13 @@ class HalKestrel {
* datarate for the beacon (e.g. MAC_AX_OFDM6). */
bool start_beacon(const uint8_t *body, uint32_t len, uint16_t interval_tu,
uint8_t bss_color, uint16_t rate_ax);
/* Replace the armed beacon's content: the same BCN_UPD_V1 H2C as
* start_beacon, without touching the port or the TBTT timing. */
bool update_beacon(const uint8_t *body, uint32_t len, uint16_t rate_ax);
/* Stop the HW beacon: clear BCNTX_EN in PORT_CFG_P0 (mac_port_cfg
* MAC_AX_PCFG_TX_SW = 0, the vendor rtw_hal_beacon_stop path) and return
* NET_TYPE to NO_LINK. The port stays enabled and the TSF keeps running. */
bool stop_beacon();

/* PHY bring-up: apply the halbb BB register + gain tables and the
* halrf radio-A/B tables (via the vendored halbb/halrf loaders). `rfe_type`
Expand Down
Loading
Loading