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
34 changes: 33 additions & 1 deletion bridge/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -291,13 +291,45 @@ int main(int argc, char** argv) {
// What the chip has seen, alongside the acknowledgement. Cheap, and
// it turns "the mesh went quiet" into a question with an answer:
// was the channel busy, or did our chip only say so?
//
// The same payload radioserver sends, in the same order. Two writers
// of one wire format is the arrangement RadioServerSX1262 was written
// to avoid, and it is only tolerable while they are kept identical:
// an emulated node and a native one reporting different shapes would
// make every comparison between them a comparison of our own code.
auto& c = sim_hal.chip();
uint32_t st[4] = {c.irqReads(), c.busyReads(), c.busyMs(), c.spuriousRaises()};
uint8_t sb[16];
uint8_t sb[37];
for (int k = 0; k < 4; k++) {
sb[k*4+0] = (uint8_t)(st[k] >> 24); sb[k*4+1] = (uint8_t)(st[k] >> 16);
sb[k*4+2] = (uint8_t)(st[k] >> 8); sb[k*4+3] = (uint8_t)st[k];
}
auto put32 = [&](int at, uint32_t v) {
sb[at+0] = (uint8_t)(v >> 24); sb[at+1] = (uint8_t)(v >> 16);
sb[at+2] = (uint8_t)(v >> 8); sb[at+3] = (uint8_t)v;
};
auto put16 = [&](int at, uint16_t v) {
sb[at+0] = (uint8_t)(v >> 8); sb[at+1] = (uint8_t)v;
};
sb[16] = c.rxGainReg();
sb[17] = (uint8_t)c.txPowerDbm();
sb[18] = c.femEnabled() ? 1 : 0;
sb[19] = c.mode();
sb[20] = (uint8_t)c.sf();
sb[21] = (uint8_t)c.cr();
put32(22, c.freqHz());
put32(26, (uint32_t)(c.bwKHz() * 1000.0f + 0.5f));
put16(30, (uint16_t)c.preambleSyms());
put16(32, c.irqMask());
put16(34, c.irqFlags());
// Always "not answered" here, never "the module was out".
//
// A native node has no front-end module wired: SimHal owns an array
// of pins and nothing drives an enable line into it. Reporting the
// line as low would be true of the pin and false about the board, and
// the engine would dock every native node on a board that has a
// module for a fault it did not have.
sb[36] = 0;
writeMsg(gFd, kRadioStats, sb, sizeof(sb));
}
if (!writeMsg(gFd, kAck, payload.data(), 4)) goto done;
Expand Down
69 changes: 59 additions & 10 deletions bridge/radioserver.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ enum : uint8_t {
// and a pin nothing drives is a node that configures its radio and then
// sits there for ever.
kReadIrq = 0x05,
// The board's front-end module enable line, with its level in the next
// byte. It arrives here rather than over SPI because the firmware drives it
// as an ordinary GPIO - the module is beside the radio, not inside it, and
// the chip has no idea whether its output reaches an antenna.
kSetFem = 0x06,
};

// The engine side, shared with the simulator's Go half and with bridge/main.cpp.
Expand Down Expand Up @@ -136,6 +141,52 @@ bool writeMsg(sock_t fd, uint8_t kind, const uint8_t* p, size_t n) {
return n == 0 || writeAll(fd, p, n);
}

void put32(uint8_t* p, uint32_t v) {
p[0] = (uint8_t)(v >> 24); p[1] = (uint8_t)(v >> 16);
p[2] = (uint8_t)(v >> 8); p[3] = (uint8_t)v;
}
void put16(uint8_t* p, uint16_t v) {
p[0] = (uint8_t)(v >> 8); p[1] = (uint8_t)v;
}

// Sixteen bytes of counters, then everything this radio has been configured to
// be. The engine reads it on length, so a peer that stops at sixteen still
// parses - and bridge/main.cpp writes the same payload in the same order,
// because an emulated node and a native one reporting different shapes would
// make every comparison between them a comparison of our own code.
//
// It exists because a board profile is a datasheet claim about hardware and not
// a claim about the firmware running on it. Until the chip reported its own
// state there was no way here to tell a node configured correctly from one that
// was not.
void writeRadioStats(sock_t fd) {
uint8_t sb[37];
put32(&sb[0], gChip.irqReads());
put32(&sb[4], gChip.busyReads());
put32(&sb[8], gChip.busyMs());
put32(&sb[12], gChip.spuriousRaises());

sb[16] = gChip.rxGainReg();
sb[17] = (uint8_t)gChip.txPowerDbm();
sb[18] = gChip.femEnabled() ? 1 : 0;
sb[19] = gChip.mode();
sb[20] = (uint8_t)gChip.sf();
sb[21] = (uint8_t)gChip.cr();
put32(&sb[22], gChip.freqHz());
// Bandwidth in whole hertz. The chip holds it in kHz as a float because the
// datasheet's table is fractional - 7.81, 10.42 - and rounding it to kHz here
// would make two distinct settings look like one.
put32(&sb[26], (uint32_t)(gChip.bwKHz() * 1000.0f + 0.5f));
put16(&sb[30], (uint16_t)gChip.preambleSyms());
put16(&sb[32], gChip.irqMask());
put16(&sb[34], gChip.irqFlags());
// Three states, because "has not transmitted" is not "transmitted with the
// module out": 0 no transmission yet, 1 module out, 2 module in.
sb[36] = !gChip.hasTransmitted() ? 0 : (gChip.femAtTx() ? 2 : 1);

writeMsg(fd, kRadioStats, sb, sizeof(sb));
}

// Anything the firmware handed its radio goes out to the engine now.
//
// Transmission reaches the channel immediately and is *not* immediately
Expand Down Expand Up @@ -224,16 +275,7 @@ bool serviceBridge(sock_t fd) {
gChip.tick(gSimMillis);
drainTx(fd);

uint32_t st[4] = {gChip.irqReads(), gChip.busyReads(), gChip.busyMs(),
gChip.spuriousRaises()};
uint8_t sb[16];
for (int k = 0; k < 4; k++) {
sb[k * 4 + 0] = (uint8_t)(st[k] >> 24);
sb[k * 4 + 1] = (uint8_t)(st[k] >> 16);
sb[k * 4 + 2] = (uint8_t)(st[k] >> 8);
sb[k * 4 + 3] = (uint8_t)st[k];
}
writeMsg(fd, kRadioStats, sb, sizeof(sb));
writeRadioStats(fd);
if (!writeMsg(fd, kAck, payload.data(), 4)) return false;
break;
}
Expand Down Expand Up @@ -303,6 +345,13 @@ bool serviceQemu(sock_t fd, uint64_t* transactions, uint64_t* bytes) {
return writeAll(fd, &irq, 1);
}

case kSetFem: {
uint8_t level = 0;
if (!readAll(fd, &level, 1)) return false;
gChip.setFemEnabled(level != 0);
return true;
}

case kReadBusy: {
// Always clear, which is what the native path does too: SimHal holds BUSY
// low and VirtualSX1262 does not model the time a real chip spends
Expand Down
29 changes: 29 additions & 0 deletions variants/host/VirtualSX1262.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ constexpr uint8_t kSetTxParams = 0x8E;
constexpr uint8_t kSetModulationParams = 0x8B;
constexpr uint8_t kSetPacketParams = 0x8C;
constexpr uint8_t kSetCadParams = 0x88;
constexpr uint8_t kCalibrate = 0x89;
constexpr uint8_t kSetBufferBase = 0x8F;
constexpr uint8_t kWriteBuffer = 0x0E;
constexpr uint8_t kReadBuffer = 0x1E;
Expand Down Expand Up @@ -168,6 +169,11 @@ void VirtualSX1262::startRx() { mode_ = 1; }

void VirtualSX1262::startTx() {
mode_ = 2;
// Latched here because this is the instant it decides anything. RadioLib
// raises the RF switch into transmit before issuing SetTx, so by now the line
// carries the answer to "does this transmission reach the antenna".
femAtTx_ = femEnabled_;
hasTransmitted_ = true;
pendingTx.assign(&buffer_[txBase_], &buffer_[txBase_] + txLenForSend_);
hasPendingTx = true;
}
Expand Down Expand Up @@ -312,6 +318,29 @@ void VirtualSX1262::runCommand(const uint8_t* out, size_t len, uint8_t* in) {
}
break;

// The PA drive level, in dBm as a signed byte. Stored where it was
// discarded before: the engine used to take transmit power from the board
// profile alone, which is a datasheet figure and not a claim about what
// this firmware asked for.
case kSetTxParams: if (len >= 2) txPowerDbm_ = (int8_t)out[1]; break;

// Calibration returns the receive gain register to its reset default.
//
// Modelled rather than ignored because it is the mechanism behind the fault
// MeshCore 1.17.1 fixed. sx126xResetAGC() runs a full CALIBRATE_ALL and then
// re-applies the compile-time SX126X_RX_BOOSTED_GAIN macro - so a variant
// that does not define the macro, generic-e22 among them, re-applies nothing
// and boosted gain is gone until the node reboots. The firmware's own prefs
// and its CLI go on reporting the setting the operator chose, so there is no
// symptom anywhere except sensitivity.
//
// "May" is doing work in MeshCore's own comment for this - SX126xReset.h
// calls it "RX settings that calibration may reset" - so what this models is
// the chip behaving the way the firmware's authors assumed. Section 9.6 of
// the SX126x datasheet is the authority, and it should be reconciled against
// this before any sensitivity figure derived from it is published.
case kCalibrate: regs_[kRegRxGain] = kRxGainPowerSaving; break;

case kSetModulationParams: if (len >= 4) applyModulation(&out[1]); break;
case kSetPacketParams: if (len >= 7) applyPacketParams(&out[1]); break;
case kSetBufferBase: if (len >= 3) { txBase_ = out[1]; rxBase_ = out[2]; } break;
Expand Down
81 changes: 81 additions & 0 deletions variants/host/VirtualSX1262.h
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,76 @@ class VirtualSX1262 {
// What the engine measured for the last frame it delivered.
void setLastSignal(float rssiDbm, float snrDb) { rssi_ = rssiDbm; snr_ = snrDb; }

// ---- what the firmware has configured this radio to be ----
//
// The board profile says what the hardware can do; these say what the
// firmware actually asked for, and the two diverge whenever the firmware has
// a bug. MeshCore 1.17.1 fixed one of each kind - receive gain reverting
// after an AGC reset, and a transmit-enable line that never went high - and
// neither was visible from outside the chip because nothing here was read.
//
// Reported raw rather than interpreted. Deciding that a given gain register
// value is worth 2 dB is the engine's business; the chip's business is to say
// what the register holds.
uint8_t rxGainReg() const { return regs_[kRegRxGain]; }
int8_t txPowerDbm() const { return txPowerDbm_; }

// The front-end module's transmit-enable line, driven by the firmware as an
// ordinary GPIO rather than through the chip.
//
// Defaults to false because that is what a pin nobody has driven reads as. A
// board with no FEM has no such pin, and the engine knows which boards those
// are from the profile - answering true here to be kind would hide exactly
// the fault this exists to catch.
bool femEnabled() const { return femEnabled_; }
void setFemEnabled(bool on) { femEnabled_ = on; }

// Whether the module was switched in at the moment transmission started.
//
// This, not femEnabled(), is what decides how much power left the board. The
// line is supposed to be low while the node listens - RadioLib drives it from
// its RF-switch table and only raises it just before SetTx - so reading the
// live level would dock a node for the ordinary state of receiving. What the
// T096 fault broke was the line being high at the moment it mattered, and
// that is a property of the transmission rather than of the node.
// Until the node has transmitted once there is no answer, and "the module was
// out" is the wrong one to invent: it would dock a node for not having spoken
// yet. hasTransmitted separates the two.
bool femAtTx() const { return femAtTx_; }
bool hasTransmitted() const { return hasTransmitted_; }

// The rest of what this radio has been configured to be. Nothing above the
// chip can see any of it today, which is how a node can be set to the wrong
// spreading factor, the wrong bandwidth or a gain mode its operator did not
// choose, and look identical from outside to one that is right.
//
// Reported wholesale rather than field by field as each becomes interesting:
// the expensive part is the wire format, and widening it once costs less than
// widening it five times.
uint8_t mode() const { return mode_; } // 0 standby, 1 rx, 2 tx, 3 cad
int sf() const { return sf_; }
float bwKHz() const { return bwKHz_; }
int cr() const { return cr_; }
uint32_t preambleSyms() const { return preambleSyms_; }
uint32_t freqHz() const { return freqHz_; }
uint16_t irqMask() const { return irqMask_; }
uint16_t irqFlags() const { return irq_; }

// Receive gain, at the address the datasheet gives it, with the two values
// RadioLib writes. Power saving is the reset default and what this chip comes
// up holding.
//
// Worth knowing why watching this register is enough. MeshCore does not use
// RadioLib's own resetAGC(), which restores the mode from cached runtime
// state; it has its own in helpers/radiolib/SX126xReset.h, which re-applies
// the compile-time SX126X_RX_BOOSTED_GAIN macro and so discards whatever the
// operator set at runtime. The firmware therefore writes the register itself
// after every AGC reset - we do not have to model what calibration does to
// silicon to see the fault, only to record what was written.
static constexpr uint16_t kRegRxGain = 0x08AC;
static constexpr uint8_t kRxGainBoosted = 0x96;
static constexpr uint8_t kRxGainPowerSaving = 0x94;

// Advance internal timers to this simulated instant.
void tick(uint64_t nowMs);

Expand Down Expand Up @@ -130,6 +200,17 @@ class VirtualSX1262 {
int cr_ = 5;
uint32_t preambleSyms_ = 16;
uint32_t freqHz_ = 869525000;
// What SetTxParams asked the PA for. Not the same as what leaves the
// antenna, which is this plus the board's front end - and the front end only
// contributes if the firmware remembered to switch it on.
//
// INT8_MIN until the firmware has said, because 0 dBm is a level a radio can
// legitimately be set to and a node that has not configured itself yet must
// not be read as one that chose silence.
int8_t txPowerDbm_ = -128;
bool femEnabled_ = false;
bool femAtTx_ = false;
bool hasTransmitted_ = false;

// What the air is doing here, from the engine.
bool channelBusy_ = false;
Expand Down