diff --git a/bridge/main.cpp b/bridge/main.cpp index 9fd124c..891fcf1 100644 --- a/bridge/main.cpp +++ b/bridge/main.cpp @@ -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; diff --git a/bridge/radioserver.cpp b/bridge/radioserver.cpp index 367ddbe..b84d1fa 100644 --- a/bridge/radioserver.cpp +++ b/bridge/radioserver.cpp @@ -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. @@ -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 @@ -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; } @@ -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 diff --git a/variants/host/VirtualSX1262.cpp b/variants/host/VirtualSX1262.cpp index 88744b2..d805bf3 100644 --- a/variants/host/VirtualSX1262.cpp +++ b/variants/host/VirtualSX1262.cpp @@ -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; @@ -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; } @@ -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; diff --git a/variants/host/VirtualSX1262.h b/variants/host/VirtualSX1262.h index b7d45ca..2591cc1 100644 --- a/variants/host/VirtualSX1262.h +++ b/variants/host/VirtualSX1262.h @@ -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); @@ -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;