This commit is contained in:
2026-09-13 13:30:21 +08:00
commit a6bbd520cf
2744 changed files with 1598795 additions and 0 deletions
+319
View File
@@ -0,0 +1,319 @@
/* chipstate — read a chip's registers WITHOUT touching it.
*
* Every other entry point into this driver reconfigures the chip on the way in:
* `claim_interface_then_reset` re-enumerates it, `Init`/`InitWrite` re-run the
* power sequence, reload the BB/RF/MAC tables and re-calibrate. That is exactly
* what you must not do when the question is "what state did the previous
* session leave this chip in?" — the act of looking destroys the evidence.
*
* So this tool: open, claim, DO NOT reset, construct the device (chip identity
* resolves from SYS_CFG2 at construction, no bring-up needed), dump, exit. The
* output is the DEVOURER_DUMP_CANARY format, so two dumps diff directly:
*
* sudo build/chipstate --pid 0x8812 > degraded.canary
* # ... VBUS power-cycle the adapter ...
* sudo build/chipstate --pid 0x8812 > healthy.canary
* python3 tests/canary_diff.py degraded.canary healthy.canary --strict
*
* Reading a powered-down chip returns garbage or fails — that is a real answer
* about the chip, not a tool error, so it is reported rather than hidden.
*
* Note --init: it runs a normal bring-up before dumping, which is what you want
* for a healthy-reference dump on a chip that has just been power-cycled and is
* therefore not configured at all.
*/
#include <cstdio>
#include <cstdlib>
#include <cstdint>
#include <cstring>
#include <memory>
#if __has_include(<libusb.h>)
#include <libusb.h>
#else
#include <libusb-1.0/libusb.h>
#endif
#include <vector>
#include "DeviceSession.h"
#include "IRtlRadio.h"
#include "RtlAdapter.h"
#include "UsbOpen.h"
#include "WiFiDriver.h"
#include "logger.h"
#include "SignalStop.h"
namespace {
/* Same list the other demos' open loop iterates; --pid narrows to one. */
const uint16_t kRealtekPids[] = {0x8812, 0x8813, 0x881a, 0x0811, 0xa811,
0x0820, 0x0821, 0x8822, 0x0120, 0x012d,
0xb82c, 0xc811, 0xc812, 0xa81a};
/* One --peek/--poke, kept in argv order so a poke-then-peek verifies the
* write inside a single claim. */
struct RegOp {
bool write = false;
uint16_t addr = 0;
uint16_t end = 0; /* peek range: inclusive last addr (== addr if single) */
uint32_t val = 0; /* poke */
int width = 1; /* poke: 1/2/4 */
};
struct Args {
uint16_t vid = 0x0bda;
int pid = -1;
int channel = 6;
bool init = false;
bool no_claim = false;
std::vector<RegOp> ops;
};
void usage() {
std::fprintf(stderr,
"usage: chipstate [--vid 0xNNNN] [--pid 0xNNNN] [--init] "
"[--channel N]\n"
" [--peek 0xA[-0xB]]... [--poke 0xA=0xV[:W]]...\n"
" default: attach read-only, no USB reset, no bring-up.\n"
" --init : run a full bring-up first (for a healthy reference\n"
" dump on a freshly power-cycled adapter).\n"
" --peek : dump register byte(s) over the vendor-control path\n"
" (range inclusive, 16 bytes/row) instead of the\n"
" canary set. Bypasses chip dispatch — any die.\n"
" --poke : write a register (width W = 1/2/4, default from the\n"
" value magnitude). The bench-bisection intervention\n"
" lever; ops run in argv order, so a trailing --peek\n"
" verifies the write in the same claim.\n"
" --no-claim : (peek/poke only) skip the interface claim —\n"
" vendor control rides EP0 with device recipient, so\n"
" registers stay reachable while another process\n"
" (e.g. an armed rxdemo) owns the interface.\n");
}
/* Range-checked address parse: a silently-wrapped register (0x12345 ->
* 0x2345) on a poke tool is a corruption hazard, so out-of-range is a parse
* error, never a truncation. */
bool parse_reg_addr(const char *s, char **end, uint16_t &out) {
unsigned long v = std::strtoul(s, end, 0);
if (*end == s || v > 0xffff)
return false;
out = static_cast<uint16_t>(v);
return true;
}
bool parse_peek(const char *s, RegOp &op) {
char *end = nullptr;
if (!parse_reg_addr(s, &end, op.addr))
return false;
op.end = op.addr;
if (*end == '-') {
if (!parse_reg_addr(end + 1, &end, op.end) || op.end < op.addr)
return false;
}
return *end == '\0';
}
bool parse_poke(const char *s, RegOp &op) {
char *end = nullptr;
op.write = true;
if (!parse_reg_addr(s, &end, op.addr))
return false;
if (*end != '=')
return false;
const char *vs = end + 1;
unsigned long v = std::strtoul(vs, &end, 0);
if (end == vs || v > 0xfffffffful)
return false;
op.val = static_cast<uint32_t>(v);
if (*end == ':') {
op.width = static_cast<int>(std::strtoul(end + 1, &end, 0));
if (op.width != 1 && op.width != 2 && op.width != 4)
return false;
/* An explicit width the value doesn't fit is a mistake, not a mask. */
if (op.width < 4 && (op.val >> (op.width * 8)) != 0)
return false;
} else {
op.width = op.val <= 0xff ? 1 : op.val <= 0xffff ? 2 : 4;
}
return *end == '\0';
}
/* Raw register client over the transport layer — deliberately below
* CreateRadio so it works on any die, configured or not. */
int run_reg_ops(libusb_device_handle *handle, Logger_t logger,
libusb_context *ctx,
std::shared_ptr<devourer::UsbDeviceLock> lock,
const std::vector<RegOp> &ops) {
RtlAdapter adapter(handle, logger, ctx, lock);
/* A failed vendor-control read throws (UsbTransport::ctrl_read) — on a
* powered-down or wedged chip that is a real answer about the chip, so
* report which op died and exit nonzero instead of terminating. */
try {
for (const RegOp &op : ops) {
if (op.write) {
if (op.width == 4)
adapter.rtw_write32(op.addr, op.val);
else if (op.width == 2)
adapter.rtw_write16(op.addr, static_cast<uint16_t>(op.val));
else
adapter.rtw_write8(op.addr, static_cast<uint8_t>(op.val));
std::printf("poke 0x%04x = 0x%0*x\n", op.addr, op.width * 2, op.val);
} else {
for (uint32_t row = op.addr & ~0xfu; row <= op.end; row += 16) {
std::printf("0x%04x:", row);
for (uint32_t i = row; i < row + 16; ++i) {
if (i < op.addr || i > op.end)
std::printf(" ");
else
std::printf(" %02x", adapter.rtw_read8(static_cast<uint16_t>(i)));
}
std::printf("\n");
}
}
}
} catch (const std::exception &e) {
std::fflush(stdout);
logger->error("register access failed ({}) — chip powered down / wedged? "
"That is the finding, not a tool error.", e.what());
return 4;
}
std::fflush(stdout);
return 0;
}
} // namespace
int main(int argc, char **argv) {
Args a;
for (int i = 1; i < argc; ++i) {
const char *v = (i + 1 < argc) ? argv[i + 1] : nullptr;
if (!std::strcmp(argv[i], "--vid") && v) {
a.vid = static_cast<uint16_t>(std::strtol(v, nullptr, 0));
++i;
} else if (!std::strcmp(argv[i], "--pid") && v) {
a.pid = static_cast<int>(std::strtol(v, nullptr, 0));
++i;
} else if (!std::strcmp(argv[i], "--channel") && v) {
a.channel = static_cast<int>(std::strtol(v, nullptr, 0));
++i;
} else if (!std::strcmp(argv[i], "--init")) {
a.init = true;
} else if (!std::strcmp(argv[i], "--no-claim")) {
a.no_claim = true;
} else if (!std::strcmp(argv[i], "--peek") && v) {
RegOp op;
if (!parse_peek(v, op)) {
usage();
return 2;
}
a.ops.push_back(op);
++i;
} else if (!std::strcmp(argv[i], "--poke") && v) {
RegOp op;
if (!parse_poke(v, op)) {
usage();
return 2;
}
a.ops.push_back(op);
++i;
} else {
usage();
return 2;
}
}
auto logger = std::make_shared<Logger>();
install_devourer_signal_handlers();
devourer::DeviceSession session{logger};
libusb_context *ctx = nullptr;
if (libusb_init(&ctx) < 0) {
logger->error("libusb_init failed");
return 3;
}
session.adopt_context(ctx);
libusb_device_handle *handle = nullptr;
if (a.pid >= 0) {
handle = libusb_open_device_with_vid_pid(ctx, a.vid,
static_cast<uint16_t>(a.pid));
} else {
for (uint16_t pid : kRealtekPids) {
handle = libusb_open_device_with_vid_pid(ctx, a.vid, pid);
if (handle)
break;
}
}
if (!handle) {
logger->error("no adapter found (vid {:04x})", a.vid);
return 3;
}
/* --no-claim + reg ops: EP0 vendor control with device recipient does not
* need the interface, so peeks/pokes work while another process (a live
* armed rxdemo) owns it — the concurrent-intervention mode. */
if (a.no_claim) {
if (a.ops.empty()) {
logger->error("--no-claim is peek/poke-only (the canary dump needs the "
"claimed device)");
session.adopt_handle(handle);
return 2;
}
session.adopt_handle(handle);
return run_reg_ops(handle, logger, ctx, nullptr, a.ops);
}
/* do_reset=false is the whole point: a USB reset re-runs the chip's own boot
* and would wipe the state we came to read. Only --init opts into disturbing
* the chip, and even then the reset stays off so the bring-up starts from
* whatever the chip currently holds. */
std::shared_ptr<devourer::UsbDeviceLock> lock;
if (devourer::claim_interface_then_reset(
handle, devourer::find_wifi_interface(handle), logger,
/*do_reset=*/false, lock) != 0) {
session.adopt_handle(handle);
return 3;
}
session.adopt_handle(handle);
session.adopt_lock(lock);
/* --peek/--poke: raw transport-level register access, no device
* construction at all — the chip is not even identified, let alone
* configured, so this works mid-experiment on any die. */
if (!a.ops.empty())
return run_reg_ops(handle, logger, ctx, lock, a.ops);
devourer::DeviceConfig cfg;
/* Leave the chip exactly as found. Without this the device destructor runs
* the card-disable sequence on the way out, so the tool would power down the
* very state it exists to inspect — one look and the evidence is gone. */
cfg.tuning.teardown_power_down = false;
WiFiDriver driver(logger);
std::unique_ptr<IRadio> owned = driver.CreateRadio(handle, ctx, lock, cfg);
if (!owned) {
logger->error("CreateRadio failed (chip support not built?)");
return 3;
}
session.adopt_device(std::move(owned));
IRadio *const dev = session.device();
if (a.init) {
logger->info("chipstate: --init, running a full bring-up before the dump");
dev->InitWrite(SelectedChannel{.Channel = static_cast<uint8_t>(a.channel),
.ChannelOffset = 0,
.ChannelWidth = CHANNEL_WIDTH_20});
} else {
logger->info("chipstate: read-only attach (no USB reset, no bring-up) — "
"the chip is being read exactly as the last session left it");
}
auto *rtl = dynamic_cast<IRtlRadio *>(dev);
if (!rtl) {
logger->error("chipstate: no canary register dump on this radio (not a "
"Realtek backend)");
return 4;
}
rtl->DumpChipState();
return 0;
}