diff --git a/build.sh b/build.sh index ba338d9..2cebc7c 100755 --- a/build.sh +++ b/build.sh @@ -35,7 +35,7 @@ CXXFLAGS="-std=c++17 -O2 -Iinclude -Isrc $WARN ${CXXFLAGS}" CFLAGS_C="-std=c11 -O2 -Iinclude -Wall -Wextra -Wpedantic ${CFLAGS}" [ -n "$STRICT" ] && CFLAGS_C="$CFLAGS_C -Werror" -SRC="src/VirtualSX1262.cpp src/spi.cpp src/abi.cpp" +SRC="src/VirtualSX1262.cpp src/cad.cpp src/commands.cpp src/spi.cpp src/abi.cpp" # SHARED_RUNTIME is the toolchain runtime the shared build links against. # @@ -115,7 +115,7 @@ check_shared_imports() { # One binary per subject rather than one big one, because the house limit on # file length applies to tests too and a single file had already outgrown it. # They share test/harness.h and nothing else, so each counts its own failures. -TESTS="test_reception test_interrupts test_spi" +TESTS="test_reception test_interrupts test_spi test_cad test_modem test_commands" build_tests() { for t in $TESTS; do diff --git a/include/virtual_sx1262.h b/include/virtual_sx1262.h index a84df19..d412f97 100644 --- a/include/virtual_sx1262.h +++ b/include/virtual_sx1262.h @@ -93,6 +93,35 @@ void vsx_set_channel_busy(vsx_chip* chip, int busy); * packet cannot arrive after the signal that carried it has gone. */ void vsx_deliver_frame(vsx_chip* chip, const uint8_t* frame, size_t len); +/* The same, from a transmitter whose sync word the caller knows. + * + * A LoRa receiver uses the sync word to decide a frame was not meant for it, + * and a frame whose word does not match this chip's is refused and counted + * rather than delivered. The firmware sets its own word through two registers + * rather than a command, so nothing above the chip can see it except through + * vsx_state.sync_word. + * + * Separate from vsx_deliver_frame rather than an extra argument to it, because + * a host that does not model sync words must go on working unchanged: making no + * claim about the transmitter is not the same as claiming it matched, and the + * older entry point makes no claim. */ +void vsx_deliver_frame_from(vsx_chip* chip, const uint8_t* frame, size_t len, + uint16_t sync_word); + +/* A frame the receiver got and could not trust. `failure` is 1 for a CRC error + * and 2 for a header error. + * + * PLACEHOLDER INTERFACE, and the only way CrcErr or HeaderErr can be raised at + * all. The chip cannot decide this: whether a frame's CRC checks depends on + * what arrived at the antenna, which the simulator owns. Nothing calls this + * yet, so both flags stay unreachable in practice and the firmware's error + * paths stay unexercised: under calculated RF a frame arrives whole or not at + * all and nothing ever will, and under waveform RF the demodulator genuinely + * can fail a CRC but nothing is wired to say so. A CRC failure delivers the + * corrupt payload and raises RxDone beside CrcErr, as the part does. */ +void vsx_deliver_frame_failed(vsx_chip* chip, const uint8_t* frame, size_t len, + uint8_t failure); + /* The waveform the chip started has finished on the air. The chip cannot know * this: how long a transmission occupied the channel is a property of the * samples the simulator generated. */ @@ -128,6 +157,49 @@ typedef struct { * than irq_mask: that one says what reaches the status register. Appended at * the end, per the ABI rule, so a host built against 1.0 is unaffected. */ uint16_t dio1_mask; + /* What SetCadParams programmed, reported because a scan's cost and its answer + * both come from here and neither is visible any other way. cad_symbol_num is + * the register value, so the dwell is 1 << it symbols; cad_exit_mode is the + * programmed byte, recorded only: every scan ends in standby. The two + * thresholds are recorded and not applied: + * they threshold a correlator peak, and this model is told whether a carrier + * is present rather than how strong it is, so a host that does know the level + * is the only thing that can honestly act on them. */ + uint8_t cad_symbol_num; + uint8_t cad_det_peak; + uint8_t cad_det_min; + uint8_t cad_exit_mode; + /* The LoRa sync word the firmware programmed, as the two registers hold it: + * 0x1424 is the private network default this chip comes up on and 0x3444 is + * the public one. Two meshes sharing a frequency are told apart by this and + * by nothing else the chip can see. */ + uint16_t sync_word; + /* Whether the modem is running low data rate optimisation, from the field + * SetModulationParams programmed. Reported because it costs two bits a symbol + * and so changes every airtime this chip quotes, and because a firmware that + * never programs it is a different case from one that programs it off. */ + uint8_t low_data_rate_optimize; + /* The rest of the command set, as the firmware programmed it. Most of these + * are recorded rather than acted on; src/commands.cpp says which is which for + * each one, and a host reading a value here is reading what was asked for, + * not a claim that the model obeyed it. */ + uint8_t packet_type; /* 0 GFSK, 1 LoRa, 3 LR-FHSS; only LoRa runs */ + uint8_t fallback_mode; /* 0x20 STDBY_RC, 0x30 STDBY_XOSC, 0x40 FS */ + uint8_t regulator_mode; /* recorded only; this model has no current */ + uint8_t dio2_as_rf_switch; /* recorded; the FEM line is host-driven here */ + uint8_t dio3_as_tcxo; /* recorded only */ + uint8_t pa_duty_cycle; /* recorded; with pa_hp_max this is half of */ + uint8_t pa_hp_max; /* what sets output power on silicon, and the */ + uint8_t pa_device_sel; /* board owns the other half */ + uint8_t symb_num_timeout; /* a receive deadline counted in symbols */ + uint8_t stop_timer_on_preamble; + uint8_t tx_continuous_wave; /* a bare carrier; the host must put it on air */ + uint8_t tx_infinite_preamble; + uint8_t header_implicit; /* the three SetPacketParams fields that were */ + uint8_t crc_on; /* discarded, all of which change airtime */ + uint8_t invert_iq; + uint32_t rx_duty_rx_period; /* recorded; the receiver does not sleep */ + uint32_t rx_duty_sleep_period; } vsx_state; void vsx_get_state(const vsx_chip* chip, vsx_state* out); @@ -139,6 +211,28 @@ typedef struct { uint32_t spurious_raises; uint32_t preamble_raises; uint32_t frames_dropped; /* handed over while deaf, past the grace */ + /* Events the chip had and the firmware never saw, because IrqMask had them + * masked off. A firmware waiting on an interrupt it did not enable looks + * exactly like a quiet mesh, and this is the only thing that tells the two + * apart. */ + uint32_t irq_suppressed; + /* Scans run, and scans that found a carrier. A host comparing a firmware that + * scans against one that does not needs to know the scans happened and what + * they cost, which is cad_runs multiplied by the dwell in vsx_state. */ + uint32_t cad_runs; + uint32_t cad_detections; + /* Frames refused because they were sent under another sync word, and + * modulation fields refused because the part does not define them. Both are + * faults that otherwise present as a quiet network: a node on the wrong sync + * word receives nothing and reports nothing wrong. */ + uint32_t sync_mismatches; + uint32_t params_rejected; + /* GetStats' three counters, as the firmware reads them. rx_packets is real; + * the other two can only move through vsx_deliver_frame_failed, which nothing + * calls, so a firmware polling them sees zero however bad the channel is. */ + uint16_t stat_rx_packets; + uint16_t stat_crc_errors; + uint16_t stat_header_errors; } vsx_counters; void vsx_get_counters(const vsx_chip* chip, vsx_counters* out); diff --git a/src/VirtualSX1262.cpp b/src/VirtualSX1262.cpp index e34b476..e9bd58d 100644 --- a/src/VirtualSX1262.cpp +++ b/src/VirtualSX1262.cpp @@ -30,6 +30,13 @@ VirtualSX1262::VirtualSX1262() { // on. Power-on default is the non-boosted value. regs_[0x08AC] = 0x94; + // The sync word the part comes up holding. A firmware that never writes one + // is on the private network word, and a model that came up on zero would + // have every node agreeing with every other node about a value none of them + // chose. + regs_[kRegSyncWordMsb] = (uint8_t)(kSyncWordPrivate >> 8); + regs_[kRegSyncWordLsb] = (uint8_t)(kSyncWordPrivate & 0xFF); + #ifdef VIRTUAL_SX1262_STUCK_IRQ_MS // A deliberately misbehaving chip, built as its own firmware variant. // @@ -44,6 +51,23 @@ VirtualSX1262::VirtualSX1262() { #endif } +void VirtualSX1262::raiseIrq(uint16_t bits) { + const uint16_t allowed = (uint16_t)(bits & irqMask_); + if (allowed != bits) { + irqSuppressed_++; + } + irq_ |= allowed; +} + +// One symbol, in milliseconds, at whatever the firmware has programmed. +// +// Shared rather than recomputed because three things now time themselves in +// symbols - preamble detection, header detection and a CAD scan - and a second +// copy of this is a second place for the bandwidth to be forgotten. +double VirtualSX1262::symbolMs() const { + return (double)(1u << sf_) / (bwKHz_ > 0 ? bwKHz_ : 250.0); +} + void VirtualSX1262::tick(uint64_t nowMs) { // Time with the busy flags up, which is what the firmware is reacting to. if ((irq_ & (kIrqPreambleDetected | kIrqHeaderValid)) && nowMs > lastBusyTickMs_) { @@ -70,33 +94,95 @@ void VirtualSX1262::tick(uint64_t nowMs) { // trusts it believes the channel is busy for ever and stops transmitting. // That is the difference between MeshCore 1.16 and 1.17, and it cannot be // seen on a chip that never lies. - if (stuckIrqMs_ > 0 && mode_ == 1 && nowMs_ >= nextSpuriousMs_) { - irq_ |= kIrqPreambleDetected; + if (stuckIrqMs_ > 0 && mode_ == kModeRx && nowMs_ >= nextSpuriousMs_) { + raiseIrq(kIrqPreambleDetected); spuriousRaises_++; nextSpuriousMs_ = nowMs_ + stuckIrqMs_; } settleInbox(); + // A scan in progress is the chip's own business and runs before the receive + // path below bows out, because CAD is not receive mode and would otherwise + // never be looked at again once it started. + tickCad(); + + // The receive deadline, which exists only for a firmware that asked for one. + // Reaching it is a Timeout and a return to standby, exactly as a completed + // reception would be, because a receiver that stops listening has stopped + // listening whichever way it got there. + if (mode_ == kModeRx && rxDeadlineArmed_ && nowMs_ >= rxDeadlineMs_) { + rxDeadlineArmed_ = false; + mode_ = kModeStandby; + raiseIrq(kIrqTimeout); + } + + // The transmit deadline, which the part enforces against a waveform that + // never finished. The frame stays queued: the chip gave up on it, and whether + // any of it reached the air is the simulator's to say, not ours. + // A timeout goes to STDBY_RC whatever SetRxTxFallbackMode asked for: the + // fallback is where a transmission that finished goes, and this one did not. + if (mode_ == kModeTx && txDeadlineArmed_ && nowMs_ >= txDeadlineMs_) { + txDeadlineArmed_ = false; + mode_ = kModeStandby; + raiseIrq(kIrqTimeout); + } + // Preamble and header detection, in receive mode only. A node cannot hear // anything while its own transmitter is keyed - that is half duplex, and the // engine already reports the channel as clear to a node that is transmitting. - if (mode_ != 1) return; + if (mode_ != kModeRx) return; - const double symbolMs = (double)(1u << sf_) / (bwKHz_ > 0 ? bwKHz_ : 250.0); + const double symMs = symbolMs(); if (channelBusy_) { const double sinceMs = (double)(nowMs_ - busySinceMs_); - if (!preambleRaised_ && sinceMs >= kPreambleSymbols * symbolMs) { - irq_ |= kIrqPreambleDetected; + if (!preambleRaised_ && sinceMs >= kPreambleSymbols * symMs) { + raiseIrq(kIrqPreambleDetected); preambleRaised_ = true; preambleRaises_++; + // StopTimerOnPreamble: a receiver that has heard something is no longer + // on a deadline, so a bounded receive cannot cut off the frame it is in + // the middle of. + if (stopTimerOnPreamble_) { + rxDeadlineArmed_ = false; + } } - if (!headerRaised_ && sinceMs >= kHeaderSymbols * symbolMs) { - irq_ |= kIrqHeaderValid | kIrqSyncWordValid; + // The receiver's own preamble length stands in for the transmitter's: a + // mesh agrees on one, and the chip is told nothing about who is sending. + // An implicit-header receiver has no header to find. + const double headerEnd = preambleSyms_ + kSyncAndSfdSymbols + kHeaderBlockSymbols; + if (!headerImplicit_ && !headerRaised_ && sinceMs >= headerEnd * symMs) { + raiseIrq(kIrqHeaderValid | kIrqSyncWordValid); headerRaised_ = true; } } } +uint16_t VirtualSX1262::syncWord() const { + return (uint16_t)(((uint16_t)regs_[kRegSyncWordMsb] << 8) | regs_[kRegSyncWordLsb]); +} + +// A frame arriving from a transmitter whose sync word the caller knows. +// +// The sync word is what a LoRa receiver uses to decide a frame is not meant for +// it, and it was the one piece of the header this model could not act on: the +// firmware writes it through two registers rather than a command, so it landed +// in the register array and nothing ever read it back. Two meshes on one +// frequency with different words therefore heard each other perfectly, which is +// the opposite of the property the word exists to provide. +// +// Kept separate from deliverFrame() rather than folded into it, because a host +// that does not model sync words at all must go on working: no claim about the +// transmitter means no grounds to refuse, and the old entry point makes no +// claim. A host that does know says so here. +void VirtualSX1262::deliverFrameFrom(const uint8_t* frame, size_t len, + uint16_t syncWordSent) { + if (syncWordSent != syncWord()) { + syncMismatches_++; + return; + } + deliverFrame(frame, len); +} + void VirtualSX1262::setChannelBusy(bool busy) { if (busy && !channelBusy_) busySinceMs_ = nowMs_; if (!busy) { @@ -124,9 +210,12 @@ void VirtualSX1262::setChannelBusy(bool busy) { } void VirtualSX1262::transmitFinished() { - if (mode_ == 2) { - irq_ |= kIrqTxDone; - mode_ = 0; + if (mode_ == kModeTx) { + txDeadlineArmed_ = false; + txContinuousWave_ = false; + txInfinitePreamble_ = false; + raiseIrq(kIrqTxDone); + applyFallback(); } } @@ -168,7 +257,7 @@ uint64_t VirtualSX1262::inboxGraceMs() const { // deafness and the grace exists to cover exactly that. void VirtualSX1262::settleInbox() { if (inbox.empty()) return; - if (mode_ == 1) { + if (mode_ == kModeRx) { deliverPending(); return; } @@ -184,7 +273,8 @@ void VirtualSX1262::deliverPending() { rxLen_ = (uint8_t)(f.size() > 255 ? 255 : f.size()); memcpy(&buffer_[rxBase_], f.data(), rxLen_); inbox.pop_front(); - irq_ |= kIrqRxDone; + statRxPackets_++; + raiseIrq(kIrqRxDone); // Deliberately only RxDone. The detection flags say "a carrier is present", // and by the time a frame is delivered the carrier has ended - raising them // here asserted the presence of a signal at the moment it stopped, which is @@ -213,8 +303,27 @@ void VirtualSX1262::refreshNoise() { noiseNow_ = (uint32_t)z; } -void VirtualSX1262::startRx() { +void VirtualSX1262::startRx(uint32_t timeoutRaw) { mode_ = 1; + + // SetRx carries a 24-bit timeout, and two of its values are not durations. + // All ones is continuous receive and zero is single-shot with no deadline, so + // only what lies between them arms anything. A driver asking for continuous + // and a driver asking for the longest finite timeout write different things + // and mean different things, and collapsing the two is how a receiver that + // should never stop acquires a deadline. + rxDeadlineArmed_ = timeoutRaw != 0 && timeoutRaw != kRxContinuous; + if (rxDeadlineArmed_) { + rxDeadlineMs_ = nowMs_ + (uint64_t)((double)timeoutRaw * kTimeoutStepMs + 0.5); + } else if (timeoutRaw == 0 && symbNumTimeout_ > 0) { + // SetLoRaSymbNumTimeout, which is the same deadline counted in symbols. It + // only gets a look in when SetRx did not bring one of its own: the part + // runs a single timer, and which of the two wins is the placeholder noted + // in commands.cpp. + rxDeadlineArmed_ = true; + rxDeadlineMs_ = nowMs_ + (uint64_t)((double)symbNumTimeout_ * symbolMs() + 0.5); + } + // Arming the receiver is itself a delivery point, not just a change of mode. // // Delivery used to happen only on a tick, which quietly made every reception @@ -227,8 +336,19 @@ void VirtualSX1262::startRx() { settleInbox(); } -void VirtualSX1262::startTx() { +void VirtualSX1262::startTx(uint32_t timeoutRaw) { mode_ = 2; + + // SetTx's timeout, which unlike SetRx's has no continuous case: a + // transmission ends. Zero disables it and is what a driver content to wait + // for TxDone sends; anything else is the part promising to give up. Reaching + // it means the waveform never finished, which from inside the chip is + // indistinguishable from a host that forgot to say it had. + txDeadlineArmed_ = timeoutRaw != 0; + if (txDeadlineArmed_) { + txDeadlineMs_ = nowMs_ + (uint64_t)((double)timeoutRaw * kTimeoutStepMs + 0.5); + } + // 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". @@ -238,20 +358,15 @@ void VirtualSX1262::startTx() { hasPendingTx = true; } -void VirtualSX1262::startCad() { - mode_ = 3; - // CAD answers in one go: the chip listens for a couple of symbols and reports. - irq_ |= kIrqCadDone; - if (channelBusy_) irq_ |= kIrqCadDetected; - mode_ = 0; -} - uint32_t VirtualSX1262::estAirtimeMs(int lenBytes) const { // Semtech's own airtime formula, from the parameters the firmware programmed. const double bwHz = bwKHz_ * 1000.0; const double tSym = (double)(1u << sf_) / bwHz; - const int de = (sf_ >= 11) ? 1 : 0; - const int crc = 1, header = 0, crDen = cr_; + const int de = lowDataRateOptimize() ? 1 : 0; + // IH is 1 when the header is implicit, which is the header being absent, so + // it subtracts its twenty symbols. Both of these were constants standing in + // for fields SetPacketParams carries. + const int crc = crcOn_ ? 1 : 0, header = headerImplicit_ ? 1 : 0, crDen = cr_; double num = 8.0 * lenBytes - 4.0 * sf_ + 28 + 16 * crc - 20 * header; double den = 4.0 * (sf_ - 2 * de); double payloadSyms = 8 + fmax(ceil(num / den) * crDen, 0.0); @@ -259,49 +374,99 @@ uint32_t VirtualSX1262::estAirtimeMs(int lenBytes) const { return (uint32_t)(t * 1000.0 + 0.5); } -void VirtualSX1262::applyModulation(const uint8_t* p) { - sf_ = p[0]; - // Bandwidth is an index in the datasheet's table; only the values MeshCore - // uses are mapped, and anything else keeps the current setting rather than - // silently becoming zero and making airtime infinite. - switch (p[1]) { +// The datasheet's LoRa bandwidth table, whole. Returns 0 for a code the part +// does not define, which is not the same as a bandwidth of zero and must not be +// adopted as one. +float VirtualSX1262::bandwidthForCode(uint8_t code) { + switch (code) { case 0x00: - bwKHz_ = 7.81f; - break; + return 7.81f; case 0x08: - bwKHz_ = 10.42f; - break; + return 10.42f; case 0x01: - bwKHz_ = 15.63f; - break; + return 15.63f; case 0x09: - bwKHz_ = 20.83f; - break; + return 20.83f; case 0x02: - bwKHz_ = 31.25f; - break; + return 31.25f; case 0x0A: - bwKHz_ = 41.67f; - break; + return 41.67f; case 0x03: - bwKHz_ = 62.5f; - break; + return 62.5f; case 0x04: - bwKHz_ = 125.0f; - break; + return 125.0f; case 0x05: - bwKHz_ = 250.0f; - break; + return 250.0f; case 0x06: - bwKHz_ = 500.0f; - break; + return 500.0f; default: - break; + return 0.0f; } - cr_ = p[2] ? (4 + p[2]) : cr_; } +// SetModulationParams: [sf][bw][cr][ldro]. +// +// Every field is checked, and a field the part does not define is counted and +// left alone rather than adopted. Keeping the old value is the safe half of +// that and was already here; the counter is the half that was missing, because +// a rejected parameter used to be indistinguishable from one that was never +// sent. This model's own test harness had been programming bandwidth 0x1A, +// which is not a code, and quietly running every timing assertion at the +// default 250 kHz while its comment said 62.5. +void VirtualSX1262::applyModulation(const uint8_t* p, size_t n) { + if (p[0] >= kSfMin && p[0] <= kSfMax) { + sf_ = p[0]; + } else { + paramsRejected_++; + } + + const float bw = bandwidthForCode(p[1]); + if (bw > 0.0f) { + bwKHz_ = bw; + } else { + paramsRejected_++; + } + + if (p[2] >= kCrMin && p[2] <= kCrMax) { + cr_ = 4 + p[2]; + } else { + paramsRejected_++; + } + + // Low data rate optimisation is a field the firmware programs, not something + // to infer. Inferring it was wrong at every bandwidth but 125 kHz, and wrong + // in the figure the firmware asks this chip for to time its own CSMA: at + // SF11 and 250 kHz it added two bits per symbol the part would not have + // added, and quoted an airtime about an eighth too long. + if (n >= 4) { + ldroProgrammed_ = true; + ldro_ = p[3] != 0; + } +} + +// Whether the modem is spending two bits a symbol on low data rate +// optimisation. +// +// The programmed field wins, because that is the one the part obeys. Only a +// firmware that never sent the field falls back to the symbol duration rule, +// which is the rule the chip would have applied to itself. +bool VirtualSX1262::lowDataRateOptimize() const { + return ldroProgrammed_ ? ldro_ : (symbolMs() >= kLowDataRateSymbolMs); +} + +// SetPacketParams: [preamble 15:8][preamble 7:0][headerType][payloadLen][crcOn] +// [invertIq]. +// +// Three of the six used to be discarded, and estAirtimeMs assumed values for +// two of them. That is the same shape as the low data rate optimisation bug: +// a field the firmware programs, replaced by an assumption, in the figure the +// firmware times its own CSMA on. An implicit header is twenty symbols of +// payload the model was charging for and the part was not, and CRC off is +// sixteen bits it was charging for too. void VirtualSX1262::applyPacketParams(const uint8_t* p) { preambleSyms_ = ((uint32_t)p[0] << 8) | p[1]; + headerImplicit_ = p[2] != 0; txLenForSend_ = p[3]; + crcOn_ = p[4] != 0; + invertIq_ = p[5] != 0; } diff --git a/src/VirtualSX1262.h b/src/VirtualSX1262.h index dea84c8..bfaeab4 100644 --- a/src/VirtualSX1262.h +++ b/src/VirtualSX1262.h @@ -179,6 +179,109 @@ class VirtualSX1262 { uint32_t preambleRaises() const { return preambleRaises_; } uint32_t spuriousRaises() const { return spuriousRaises_; } + // Events the chip had and the firmware never saw, because IrqMask had them + // masked off. + // + // A masked event is silence, and silence is the one symptom that looks the + // same as nothing having happened. A firmware that forgets to enable the + // interrupt it waits on is indistinguishable from a mesh with nothing on it, + // which is the kind of fault that gets diagnosed as the air being quiet. This + // counter is the difference between those two, and it is the reason enforcing + // the mask is safe to do at all. + uint32_t irqSuppressed() const { return irqSuppressed_; } + + // Channel activity detection, as the firmware asked for it and as it turned + // out. Counted because CAD is a decision the firmware pays for in dwell time, + // and a host comparing a run with it against a run without it needs to know + // the scans actually happened. + uint32_t cadRuns() const { return cadRuns_; } + uint32_t cadDetections() const { return cadDetections_; } + + // Frames refused because they were sent under a different sync word, and + // modulation fields refused because the part does not define them. + // + // Both are silent otherwise, and both are firmware faults that look like a + // quiet network: a node on the wrong sync word receives nothing and reports + // nothing wrong, and a bandwidth code that is not a code leaves the modem on + // whatever it was set to last. + uint32_t syncMismatches() const { return syncMismatches_; } + uint32_t paramsRejected() const { return paramsRejected_; } + + // ---- what the rest of the command set was told ---- + // + // Most of these are recorded rather than acted on, and each says which it is + // where it is applied. Recording is not a lesser answer: the fault this model + // exists to catch is a firmware configured differently from how its operator + // believes, and a value nobody can read back is a value nobody can check. + + uint8_t packetType() const { return packetType_; } + uint8_t fallbackMode() const { return fallbackMode_; } + uint8_t regulatorMode() const { return regulatorMode_; } + bool dio2AsRfSwitch() const { return dio2AsRfSwitch_; } + bool dio3AsTcxo() const { return dio3AsTcxo_; } + uint8_t paDutyCycle() const { return paDutyCycle_; } + uint8_t paHpMax() const { return paHpMax_; } + uint8_t paDeviceSel() const { return paDeviceSel_; } + uint8_t symbNumTimeout() const { return symbNumTimeout_; } + bool stopTimerOnPreamble() const { return stopTimerOnPreamble_; } + uint32_t rxDutyRxPeriod() const { return rxDutyRxPeriod_; } + uint32_t rxDutySleepPeriod() const { return rxDutySleepPeriod_; } + bool txContinuousWave() const { return txContinuousWave_; } + bool txInfinitePreamble() const { return txInfinitePreamble_; } + + // Packet parameters beyond the two that were already read. All three change + // airtime, and all three were assumed rather than read. + bool headerImplicit() const { return headerImplicit_; } + bool crcOn() const { return crcOn_; } + bool invertIq() const { return invertIq_; } + + // GetStats' three counters. Received is real; the other two can only move + // through deliverFrameFailed(), which nothing calls yet. + uint16_t statRxPackets() const { return statRxPackets_; } + uint16_t statCrcErrors() const { return statCrcErrors_; } + uint16_t statHeaderErrors() const { return statHeaderErrors_; } + + // The LoRa sync word, from the two registers the firmware writes it into. + // + // Read out of the register array rather than shadowed, because the firmware + // sets it with WriteRegister and a shadow copy is a second thing to keep in + // step with the array that is already the answer. Defined next door, where + // the datasheet's addresses live. + uint16_t syncWord() const; + + // Hand over a frame whose transmitter's sync word is known, so the chip can + // refuse one that was not meant for this network. + void deliverFrameFrom(const uint8_t* frame, size_t len, uint16_t syncWordSent); + + // Hand over a frame the receiver got but could not trust. + // + // PLACEHOLDER INTERFACE. The chip cannot decide this: whether a frame's CRC + // checks depends on what actually arrived at the antenna, which is the + // engine's business. So the caller says, and this is the entry point that was + // missing for CrcErr and HeaderErr to be raisable at all. What is not settled + // is who calls it: under calculated RF a frame arrives whole or not at all + // and nothing ever will, and under waveform RF the demodulator genuinely can + // fail a CRC but nothing in MeshBench is wired to say so. Until a host calls + // it, these two flags remain unreachable in practice and the firmware's error + // paths stay unexercised. + void deliverFrameFailed(const uint8_t* frame, size_t len, uint8_t failure); + + // Every path a frame reaches the chip by, in one place. + // + // inbox is still public because hosts push into it directly and making it + // private breaks them. That is the hole: a host that pushes walks around the + // sync-word check and will walk around the next one too. New callers use this. + void deliverFrame(const uint8_t* frame, size_t len); + + // Whether the modem is spending two bits a symbol on low data rate + // optimisation, from what the firmware programmed rather than from the + // spreading factor alone. + bool lowDataRateOptimize() const; + uint8_t cadSymbolNum() const { return cadSymbolNum_; } + uint8_t cadDetPeak() const { return cadDetPeak_; } + uint8_t cadDetMin() const { return cadDetMin_; } + uint8_t cadExitMode() const { return cadExitMode_; } + // Latch a flag once raised, as a misbehaving chip does. // // This is the fault MeshCore 1.17 exists to survive: a preamble or header @@ -213,11 +316,33 @@ class VirtualSX1262 { private: void runCommand(const uint8_t* out, size_t len, uint8_t* in); - void applyModulation(const uint8_t* p); + static float bandwidthForCode(uint8_t code); + void applyModulation(const uint8_t* p, size_t n); void applyPacketParams(const uint8_t* p); - void startRx(); - void startTx(); + void applyCadParams(const uint8_t* p); + // The commands that were missing, decoded in commands.cpp. + bool runExtraCommand(uint8_t op, const uint8_t* out, size_t len, uint8_t* in); + void applyPaConfig(const uint8_t* p); + void applyRxDutyCycle(const uint8_t* p); + void applyFallback(); + uint8_t statusByte() const; + void startRx(uint32_t timeoutRaw); + void startTx(uint32_t timeoutRaw); void startCad(); + void tickCad(); + void finishCad(); + double symbolMs() const; + + // Record an event in the IRQ status register, if the firmware asked to be + // told about it. + // + // Every flag goes through here rather than touching irq_ directly, because + // IrqMask is not advisory: SetDioIrqParams says what reaches the status + // register, and a chip that records an event the firmware masked off is + // answering a question nobody asked. Modelled because the alternative hides a + // real firmware fault - a driver that waits on an interrupt it never enabled + // waits for ever on silicon and works perfectly here. + void raiseIrq(uint16_t bits); uint64_t inboxGraceMs() const; void settleInbox(); void deliverPending(); @@ -288,11 +413,95 @@ class VirtualSX1262 { uint8_t noiseBits(int bits) const { return (uint8_t)(noiseNow_ & ((1u << bits) - 1)); } uint64_t nowMs_ = 0; + // Receive timeout, from SetRx. + // + // Zero is not a short timeout, it is single-shot receive with no deadline at + // all, and all ones is continuous. Only the values between the two are a + // deadline, which is why this is a flag and an instant rather than a duration. + bool rxDeadlineArmed_ = false; + uint64_t rxDeadlineMs_ = 0; + + // Transmit timeout, from SetTx, on the same 15.625 us unit. + // + // Zero disables it and is what a driver waiting for TxDone sends. Anything + // else is a deadline the part enforces against its own transmission, so + // reaching it means the waveform never finished: Timeout, and back to + // standby. There is no continuous case here, because a transmission ends. + bool txDeadlineArmed_ = false; + uint64_t txDeadlineMs_ = 0; + + // Low data rate optimisation as the firmware programmed it, and whether it + // ever did. Two fields rather than one, because "not programmed" is a real + // state with its own answer and is not the same as "programmed off". + bool ldro_ = false; + bool ldroProgrammed_ = false; + + // Channel activity detection, as SetCadParams programmed it. + // + // The defaults are the reset values: one symbol, CAD_ONLY, and thresholds of + // zero. A firmware that runs CAD without programming it first gets the + // shortest scan the part offers, which is what the silicon does and is not a + // kindness worth inventing. + uint8_t cadSymbolNum_ = 0; + uint8_t cadDetPeak_ = 0; + uint8_t cadDetMin_ = 0; + uint8_t cadExitMode_ = 0; + uint64_t cadEndMs_ = 0; + bool cadSawCarrier_ = false; + // Instrumentation. uint32_t irqReads_ = 0; uint32_t busyReads_ = 0; uint32_t busyMs_ = 0; uint32_t preambleRaises_ = 0; + uint32_t irqSuppressed_ = 0; + uint32_t cadRuns_ = 0; + uint32_t cadDetections_ = 0; + uint32_t syncMismatches_ = 0; + uint32_t paramsRejected_ = 0; + + // ---- the rest of the command set ---- + // + // Reset values throughout, so a chip nobody has configured reads as the part + // does rather than as zero. Zero is a legitimate setting for several of these + // and inventing it hides a firmware that never set one. + uint8_t packetType_ = 0x01; // LoRa; the only type with a data path here + uint8_t fallbackMode_ = 0x20; // STDBY_RC, the part's reset value + uint8_t regulatorMode_ = 0x00; // LDO + bool dio2AsRfSwitch_ = false; + bool dio3AsTcxo_ = false; + uint8_t paDutyCycle_ = 0; + uint8_t paHpMax_ = 0; + uint8_t paDeviceSel_ = 0; + uint8_t paLut_ = 1; + uint8_t symbNumTimeout_ = 0; + bool stopTimerOnPreamble_ = false; + uint32_t rxDutyRxPeriod_ = 0; + uint32_t rxDutySleepPeriod_ = 0; + bool txContinuousWave_ = false; + bool txInfinitePreamble_ = false; + + // SetPacketParams' remaining fields. Defaults are RadioLib's: explicit + // header, CRC on, IQ not inverted. + bool headerImplicit_ = false; + bool crcOn_ = true; + bool invertIq_ = false; + + // GetStats. uint16 because that is the width the command answers in, and a + // counter that wraps the way the part's does is a counter a firmware can be + // tested against. + uint16_t statRxPackets_ = 0; + uint16_t statCrcErrors_ = 0; + uint16_t statHeaderErrors_ = 0; + + // GetDeviceErrors' word. + // + // PLACEHOLDER. Nothing sets it, because every error it can report is an + // analogue or calibration fault this model does not have: PLL lock, image + // calibration, XOSC start, PA ramp. A host that wants to exercise the + // firmware's handling of one needs a way to inject it, and that entry point + // is not here because nothing has asked for it yet. + uint16_t deviceErrors_ = 0; uint64_t lastBusyTickMs_ = 0; // Fault injection: how long a raised flag refuses to clear. 0 is a chip that diff --git a/src/abi.cpp b/src/abi.cpp index c140de3..dea66bb 100644 --- a/src/abi.cpp +++ b/src/abi.cpp @@ -19,7 +19,7 @@ #include #define VSX_ABI_MAJOR 1 -#define VSX_ABI_MINOR 3 +#define VSX_ABI_MINOR 6 struct vsx_chip { VirtualSX1262 chip; @@ -149,6 +149,22 @@ void vsx_deliver_frame(vsx_chip* chip, const uint8_t* frame, size_t len) { * on arrival would let a packet in while the chip was transmitting. */ } +void vsx_deliver_frame_from(vsx_chip* chip, const uint8_t* frame, size_t len, + uint16_t sync_word) { + if (chip && frame) { + chip->chip.deliverFrameFrom(frame, len, sync_word); + settle_dio1(chip); + } +} + +void vsx_deliver_frame_failed(vsx_chip* chip, const uint8_t* frame, size_t len, + uint8_t failure) { + if (chip && frame) { + chip->chip.deliverFrameFailed(frame, len, failure); + settle_dio1(chip); + } +} + void vsx_transmit_finished(vsx_chip* chip) { if (!chip) { return; @@ -201,6 +217,29 @@ void vsx_get_state(const vsx_chip* chip, vsx_state* out) { /* Three states, because "has not transmitted" is not "transmitted with the * module out". */ out->fem_at_tx = !c.hasTransmitted() ? 0 : (c.femAtTx() ? 2 : 1); + out->cad_symbol_num = c.cadSymbolNum(); + out->cad_det_peak = c.cadDetPeak(); + out->cad_det_min = c.cadDetMin(); + out->cad_exit_mode = c.cadExitMode(); + out->sync_word = c.syncWord(); + out->low_data_rate_optimize = c.lowDataRateOptimize() ? 1 : 0; + out->packet_type = c.packetType(); + out->fallback_mode = c.fallbackMode(); + out->regulator_mode = c.regulatorMode(); + out->dio2_as_rf_switch = c.dio2AsRfSwitch() ? 1 : 0; + out->dio3_as_tcxo = c.dio3AsTcxo() ? 1 : 0; + out->pa_duty_cycle = c.paDutyCycle(); + out->pa_hp_max = c.paHpMax(); + out->pa_device_sel = c.paDeviceSel(); + out->symb_num_timeout = c.symbNumTimeout(); + out->stop_timer_on_preamble = c.stopTimerOnPreamble() ? 1 : 0; + out->tx_continuous_wave = c.txContinuousWave() ? 1 : 0; + out->tx_infinite_preamble = c.txInfinitePreamble() ? 1 : 0; + out->header_implicit = c.headerImplicit() ? 1 : 0; + out->crc_on = c.crcOn() ? 1 : 0; + out->invert_iq = c.invertIq() ? 1 : 0; + out->rx_duty_rx_period = c.rxDutyRxPeriod(); + out->rx_duty_sleep_period = c.rxDutySleepPeriod(); } void vsx_get_counters(const vsx_chip* chip, vsx_counters* out) { @@ -214,6 +253,14 @@ void vsx_get_counters(const vsx_chip* chip, vsx_counters* out) { out->spurious_raises = c.spuriousRaises(); out->preamble_raises = c.preambleRaises(); out->frames_dropped = c.framesDropped(); + out->irq_suppressed = c.irqSuppressed(); + out->cad_runs = c.cadRuns(); + out->cad_detections = c.cadDetections(); + out->sync_mismatches = c.syncMismatches(); + out->params_rejected = c.paramsRejected(); + out->stat_rx_packets = c.statRxPackets(); + out->stat_crc_errors = c.statCrcErrors(); + out->stat_header_errors = c.statHeaderErrors(); } uint32_t vsx_est_airtime_ms(const vsx_chip* chip, int len_bytes) { diff --git a/src/cad.cpp b/src/cad.cpp new file mode 100644 index 0000000..b052822 --- /dev/null +++ b/src/cad.cpp @@ -0,0 +1,75 @@ +/* Channel activity detection: the other half of listen before talk. + * + * Its own file rather than another few functions in VirtualSX1262.cpp, for the + * reason that file and spi.cpp are already apart: a scan is a thing the part + * does over time, with its own programmed parameters and its own way out, and + * it is changed for different reasons from the receive path it sits beside. + * + * What makes CAD worth modelling properly is that it costs time. A driver runs + * it to find out whether to transmit, and what it pays for the answer is the + * dwell: 1 to 16 symbols, which at SF12 is most of a second and at SF7 is + * nothing. A scan that answers instantly is not a cheaper CAD, it is a + * different radio, and a comparison between a firmware that scans and one that + * does not then measures nothing at all. + */ +#include "VirtualSX1262.h" + +#include "registers.h" + +// SetCadParams: [symbolNum][detPeak][detMin][exitMode][timeout 23:16..7:0]. +// +// exitMode and the timeout are recorded or ignored, not acted on: every scan +// ends in standby. +// +// detPeak and detMin are recorded and not acted on, which is a deliberate stop +// rather than an unfinished one. They are thresholds against a correlator peak, +// and this model is not handed a correlator peak: the simulator tells it +// whether a carrier is present, full stop. Thresholding a boolean would be the +// chip inventing a signal level and then deciding reception from it, which is +// the simulator's job and not ours. Reported instead, so a host that does know +// the signal level can apply them, and so a firmware setting a threshold no +// real part would detect at is visible rather than silently fine. +void VirtualSX1262::applyCadParams(const uint8_t* p) { + cadSymbolNum_ = p[0] > kCadSymbolNumMax ? kCadSymbolNumMax : p[0]; + cadDetPeak_ = p[1]; + cadDetMin_ = p[2]; + cadExitMode_ = p[3]; +} + +// SetCad. The scan starts here and finishes on a later tick. +// +// Nothing is reported at this point, because at this point nothing is known. +// The old model answered inside the command, which meant a firmware could scan +// the channel, get its answer and transmit without a millisecond having passed +// - so listen before talk cost exactly nothing and every arm that used it was +// flattered by however long the scan should have taken. +void VirtualSX1262::startCad() { + mode_ = 3; + cadRuns_++; + // The carrier is sampled across the whole dwell rather than at either end of + // it, so a transmission that starts or stops mid scan is still found. Seeded + // with the state at the instant the scan begins, which is the one sample a + // zero-length dwell would get. + cadSawCarrier_ = channelBusy_; + cadEndMs_ = nowMs_ + (uint64_t)((double)(1u << cadSymbolNum_) * symbolMs() + 0.5); +} + +void VirtualSX1262::tickCad() { + if (mode_ != 3) return; + if (channelBusy_) cadSawCarrier_ = true; + if (nowMs_ < cadEndMs_) return; + finishCad(); +} + +// The end of a scan, and where it leaves the chip. +// +// CadDone always; CadDetected only if a carrier was there to find. Then standby, +// whatever the answer and whatever exit mode was programmed. +void VirtualSX1262::finishCad() { + raiseIrq(kIrqCadDone); + if (cadSawCarrier_) { + cadDetections_++; + raiseIrq(kIrqCadDetected); + } + mode_ = kModeStandby; +} diff --git a/src/commands.cpp b/src/commands.cpp new file mode 100644 index 0000000..9868faf --- /dev/null +++ b/src/commands.cpp @@ -0,0 +1,263 @@ +/* The rest of the command set: the commands MeshCore never issues. + * + * Its own file because the reason all of these were missing is the same, and it + * is not a good reason: the model grew to the shape of one firmware's driver, + * so a command nothing in that path sends was a command that silently did + * nothing. A host would issue it, get a status byte back, and carry on with the + * chip in a state it had not asked for. + * + * Three kinds of thing live here, and each one says which it is: + * + * ACTED ON the command changes what the model does. + * RECORDED the command's parameters are stored and reported, and nothing + * here acts on them, because acting would mean inventing physics + * this model is not given. Recording is still the point: the + * fault worth catching is a firmware configured differently from + * how its operator believes, and a value nobody can read back is + * a value nobody can check. + * PLACEHOLDER the command is decoded but its effect is not modelled, and the + * comment says what would have to exist first. + * + * Nothing here is a stub that pretends. A command that is RECORDED does not + * report success at something it did not do. + */ +#include "VirtualSX1262.h" + +#include "registers.h" + +// SetPaConfig. RECORDED. +// +// This is half of what sets output power on silicon, the other half being +// SetTxParams, and the two are a pair: the datasheet gives four configurations +// for +14, +17, +20 and +22 dBm and the duty cycle must not be raised beyond +// the one for the power asked for. Turning the pair into an actual figure at +// the antenna is a board question as much as a chip one, so it belongs to +// whatever knows the board. Reported so that it can be. +void VirtualSX1262::applyPaConfig(const uint8_t* p) { + paDutyCycle_ = p[0]; + paHpMax_ = p[1]; + paDeviceSel_ = p[2]; + paLut_ = p[3]; +} + +// SetRxDutyCycle. PLACEHOLDER. +// +// The part alternates receive and sleep on its own timers, waking to listen for +// rxPeriod and sleeping for sleepPeriod, and a preamble seen during a listening +// window holds it awake. Modelling it means a receiver that is deaf for a +// scheduled fraction of the time, which changes which frames arrive at all, and +// that is a decision about what a node hears: the engine already owns that, and +// two things deciding it would be worse than one thing deciding it badly. +// +// So the periods are recorded and the chip stays in continuous receive. A +// firmware using duty-cycled receive will therefore appear to hear everything, +// which is the optimistic direction and must not be read as a result about +// power saving. +void VirtualSX1262::applyRxDutyCycle(const uint8_t* p) { + rxDutyRxPeriod_ = ((uint32_t)p[0] << 16) | ((uint32_t)p[1] << 8) | p[2]; + rxDutySleepPeriod_ = ((uint32_t)p[3] << 16) | ((uint32_t)p[4] << 8) | p[5]; + startRx(0); +} + +// Where the part goes when a transmission or reception finishes. ACTED ON, as +// far as this model has modes to go to. +// +// PLACEHOLDER for FS. The part distinguishes STDBY_RC, STDBY_XOSC and FS, and +// this model has one standby: the difference between them is start-up latency +// and current draw, neither of which it represents. A firmware that asks to +// fall back to FS gets standby, and the mode it asked for is readable through +// fallbackMode() so a host can tell that is what happened. +void VirtualSX1262::applyFallback() { + mode_ = kModeStandby; +} + +// GetStatus' byte. ACTED ON, where it used to be the constant 0x22. +// +// Bits 6:4 are the chip mode and 3:1 the command status. It answered STBY_RC +// whatever the part was doing, so a firmware polling this to find out whether +// its receiver was still armed was told "standby" by a chip in receive. The +// command status is the weaker half: this model has no failing commands, so it +// reports data available when there is a frame waiting and completed otherwise, +// which is true but narrower than the five values the part can return. +uint8_t VirtualSX1262::statusByte() const { + uint8_t chipMode = kStatusModeStandbyRc; + switch (mode_) { + case kModeRx: + chipMode = kStatusModeRx; + break; + case kModeTx: + chipMode = kStatusModeTx; + break; + case kModeCad: + // CAD is not one of the modes the status byte names. The part is + // receiving while it scans, and that is the honest answer of the ones + // available. + chipMode = kStatusModeRx; + break; + default: + chipMode = kStatusModeStandbyRc; + break; + } + const uint8_t cmd = rxLen_ > 0 ? kStatusCmdDataAvailable : kStatusCmdCompleted; + return (uint8_t)((chipMode << 4) | (cmd << 1)); +} + +// A frame the receiver got and could not trust. See the header for why the +// interface is a placeholder: the chip cannot decide this, and nothing calls it +// yet. +void VirtualSX1262::deliverFrameFailed(const uint8_t* frame, size_t len, + uint8_t failure) { + (void)frame; + (void)len; + if (mode_ != kModeRx) { + return; + } + if (failure == kReceiveHeaderError) { + statHeaderErrors_++; + raiseIrq(kIrqHeaderErr); + return; + } + // A CRC failure is a frame that arrived: the payload is in the buffer and + // RxDone is raised beside CrcErr, because that is what the part does and a + // driver that reads the buffer on RxDone must find the corrupt bytes there + // rather than the last good frame's. + statCrcErrors_++; + rxLen_ = (uint8_t)(len > 255 ? 255 : len); + memcpy(&buffer_[rxBase_], frame, rxLen_); + raiseIrq(kIrqRxDone | kIrqCrcErr); +} + +void VirtualSX1262::deliverFrame(const uint8_t* frame, size_t len) { + inbox.emplace_back(frame, frame + len); +} + +// The decoder for everything above, returning false for an opcode that is still +// nobody's. Split from runCommand()'s switch so that the commands a real driver +// issues stay readable as one table. +bool VirtualSX1262::runExtraCommand(uint8_t op, const uint8_t* out, size_t len, + uint8_t* in) { + switch (op) { + // ACTED ON. The chip is one packet type at a time, and GetPacketType now + // answers what was set rather than always LoRa. Only LoRa has a data path + // here: a firmware that switches to GFSK or LR-FHSS will find the type + // reported back correctly and the modem still behaving as LoRa, which is + // recorded in packetType() so a host can refuse to believe the run. + case kSetPacketType: + if (len >= 2) packetType_ = out[1]; + return true; + + // RECORDED. + case kSetPaConfig: + if (len >= 5) applyPaConfig(&out[1]); + return true; + + // ACTED ON at the end of a transmission or reception, with FS collapsed + // into standby. See applyFallback(). + case kSetRxTxFallbackMode: + if (len >= 2) fallbackMode_ = out[1]; + return true; + + // ACTED ON. A symbol-count timeout is the same deadline SetRx's millisecond + // one is, measured in the unit the firmware chose to think in. Zero + // disables it, which is the reset value. + // + // PLACEHOLDER for the interaction with SetRx's own timeout: the part runs + // one timer, and which of the two wins when a firmware sets both is not + // something this model has been able to check against silicon. Here the + // millisecond timeout wins if it is armed, because that is the one + // RadioLib actually sends. + case kSetLoRaSymbNumTimeout: + if (len >= 2) symbNumTimeout_ = out[1]; + return true; + + // ACTED ON. With the timer stopped on preamble, a receiver that has heard + // something is no longer on a deadline: it waits for the frame. This is the + // setting that stops a bounded receive from cutting off a packet it is in + // the middle of. + case kStopTimerOnPreamble: + if (len >= 2) stopTimerOnPreamble_ = out[1] != 0; + return true; + + // PLACEHOLDER. See applyRxDutyCycle(). + case kSetRxDutyCycle: + if (len >= 7) applyRxDutyCycle(&out[1]); + return true; + + // ACTED ON, as far as there is anything to act on: frequency synthesis is + // not receiving and not transmitting, so it is this model's standby. The + // part distinguishes them by start-up latency, which is not represented. + case kSetFs: + mode_ = kModeStandby; + return true; + + // RECORDED, and deliberately not acted on. + // + // Both of these key the transmitter with no frame behind it: a bare carrier + // and an endless preamble, which exist for regulatory measurement. They are + // a transmission this model has no way to hand to the engine, because + // pendingTx carries frames and there is no frame. A host wanting to model an + // interferer reads txContinuousWave() and puts a carrier on the air itself. + case kSetTxContinuousWave: + txContinuousWave_ = true; + mode_ = kModeTx; + return true; + case kSetTxInfinitePreamble: + txInfinitePreamble_ = true; + mode_ = kModeTx; + return true; + + // RECORDED. DC-DC against LDO is a current-draw decision, and this model + // has no current. + case kSetRegulatorMode: + if (len >= 2) regulatorMode_ = out[1]; + return true; + + // RECORDED, and the first one matters more than it looks. + // + // With DIO2 driving the RF switch, the chip raises the transmit line itself + // rather than the firmware doing it as a GPIO. This model has the line the + // other way round, through setFemEnabled(), because that is how MeshCore + // drives it. A firmware that hands the job to DIO2 would therefore look like + // one that never raises the line at all, and femAtTx() would dock it for a + // fault it does not have. + // + // PLACEHOLDER: nothing reconciles the two. dio2AsRfSwitch() is reported so a + // host can notice the case, and the honest fix is for the chip to drive + // femAtTx_ itself when this is set, which needs a host that does not also + // drive it. + case kSetDio2AsRfSwitchCtrl: + if (len >= 2) dio2AsRfSwitch_ = out[1] != 0; + return true; + case kSetDio3AsTcxoCtrl: + dio3AsTcxo_ = true; + return true; + + // RECORDED as a no-op. Image calibration is an analogue procedure with no + // register this model keeps, unlike Calibrate, which has one. + case kCalibrateImage: + return true; + + // ACTED ON. [op][nop][status][pktRx 15:8][pktRx 7:0][crcErr..][hdrErr..] + // + // Two of the three can only move through deliverFrameFailed(), which + // nothing calls, so a firmware reading this will see received packets climb + // and both error counts sit at zero however bad the channel is. + case kGetStats: + if (len >= 4) in[2] = (uint8_t)(statRxPackets_ >> 8); + if (len >= 5) in[3] = (uint8_t)(statRxPackets_ & 0xFF); + if (len >= 6) in[4] = (uint8_t)(statCrcErrors_ >> 8); + if (len >= 7) in[5] = (uint8_t)(statCrcErrors_ & 0xFF); + if (len >= 8) in[6] = (uint8_t)(statHeaderErrors_ >> 8); + if (len >= 9) in[7] = (uint8_t)(statHeaderErrors_ & 0xFF); + return true; + + case kResetStats: + statRxPackets_ = 0; + statCrcErrors_ = 0; + statHeaderErrors_ = 0; + return true; + + default: + return false; + } +} diff --git a/src/registers.h b/src/registers.h index 0e40a84..73e0210 100644 --- a/src/registers.h +++ b/src/registers.h @@ -57,14 +57,119 @@ namespace { [[maybe_unused]] constexpr uint16_t kIrqCadDetected = 1 << 8; [[maybe_unused]] constexpr uint16_t kIrqTimeout = 1 << 9; -// How far into a transmission a receiver locks onto the preamble, and how much -// later the header is demodulated. Both are in symbols and become milliseconds -// through the current modem settings, because that is what makes them behave -// like a radio rather than like a constant: at SF12 a preamble takes an age and -// at SF7 it is gone in a blink, and MeshCore's listen-before-talk times exactly -// that. +// The rest of the command set. Declared together because the reason each one +// was missing is the same: nothing in MeshCore's path issues it, so the model +// grew only what it was asked for. What each one now does is in commands.cpp, +// and several of them record rather than act. That is marked there. +[[maybe_unused]] constexpr uint8_t kSetFs = 0xC1; +[[maybe_unused]] constexpr uint8_t kSetTxContinuousWave = 0xD1; +[[maybe_unused]] constexpr uint8_t kSetTxInfinitePreamble = 0xD2; +[[maybe_unused]] constexpr uint8_t kSetRegulatorMode = 0x96; +[[maybe_unused]] constexpr uint8_t kCalibrateImage = 0x98; +[[maybe_unused]] constexpr uint8_t kSetPaConfig = 0x95; +[[maybe_unused]] constexpr uint8_t kSetRxTxFallbackMode = 0x93; +[[maybe_unused]] constexpr uint8_t kSetRxDutyCycle = 0x94; +[[maybe_unused]] constexpr uint8_t kStopTimerOnPreamble = 0x9F; +[[maybe_unused]] constexpr uint8_t kSetLoRaSymbNumTimeout = 0xA0; +[[maybe_unused]] constexpr uint8_t kSetDio2AsRfSwitchCtrl = 0x9D; +[[maybe_unused]] constexpr uint8_t kSetDio3AsTcxoCtrl = 0x97; +[[maybe_unused]] constexpr uint8_t kGetStats = 0x10; +[[maybe_unused]] constexpr uint8_t kResetStats = 0x00; + +// Packet types, from SetPacketType. Only LoRa has a data path in this model. +[[maybe_unused]] constexpr uint8_t kPacketTypeGfsk = 0x00; +[[maybe_unused]] constexpr uint8_t kPacketTypeLora = 0x01; +[[maybe_unused]] constexpr uint8_t kPacketTypeLrFhss = 0x03; + +// Where the part goes when a transmission or reception ends, from +// SetRxTxFallbackMode. +[[maybe_unused]] constexpr uint8_t kFallbackFs = 0x40; +[[maybe_unused]] constexpr uint8_t kFallbackStandbyXosc = 0x30; +[[maybe_unused]] constexpr uint8_t kFallbackStandbyRc = 0x20; + +// The chip modes this model distinguishes, which are fewer than the part has. +// See the note on kModeFs in VirtualSX1262.h. +[[maybe_unused]] constexpr uint8_t kModeStandby = 0; +[[maybe_unused]] constexpr uint8_t kModeRx = 1; +[[maybe_unused]] constexpr uint8_t kModeTx = 2; +[[maybe_unused]] constexpr uint8_t kModeCad = 3; + +// GetStatus packs the chip mode into bits 6:4 and the command status into 3:1. +[[maybe_unused]] constexpr uint8_t kStatusModeStandbyRc = 0x2; +[[maybe_unused]] constexpr uint8_t kStatusModeStandbyXosc = 0x3; +[[maybe_unused]] constexpr uint8_t kStatusModeFs = 0x4; +[[maybe_unused]] constexpr uint8_t kStatusModeRx = 0x5; +[[maybe_unused]] constexpr uint8_t kStatusModeTx = 0x6; +[[maybe_unused]] constexpr uint8_t kStatusCmdDataAvailable = 0x2; +[[maybe_unused]] constexpr uint8_t kStatusCmdCompleted = 0x6; + +// How a reception failed, for the deliberately corrupt delivery path. +[[maybe_unused]] constexpr uint8_t kReceiveOk = 0; +[[maybe_unused]] constexpr uint8_t kReceiveCrcError = 1; +[[maybe_unused]] constexpr uint8_t kReceiveHeaderError = 2; + +// The LoRa sync word, which lives in two registers rather than a command. +// +// RadioLib writes it as a pair through WriteRegister, so nothing in the opcode +// table names it and a model that only watches commands never sees it change. +// The reset value is the private network word, 0x1424, which is what the part +// comes up holding and what a firmware that never sets one is using. +[[maybe_unused]] constexpr uint16_t kRegSyncWordMsb = 0x0740; +[[maybe_unused]] constexpr uint16_t kRegSyncWordLsb = 0x0741; +[[maybe_unused]] constexpr uint16_t kSyncWordPrivate = 0x1424; +[[maybe_unused]] constexpr uint16_t kSyncWordPublic = 0x3444; + +// What SetModulationParams is allowed to say. Outside these the firmware has +// programmed something the part does not define, which is a fault to report +// rather than a value to adopt. +[[maybe_unused]] constexpr uint8_t kSfMin = 5; +[[maybe_unused]] constexpr uint8_t kSfMax = 12; +[[maybe_unused]] constexpr uint8_t kCrMin = 1; +[[maybe_unused]] constexpr uint8_t kCrMax = 4; + +// The symbol duration past which the part wants low data rate optimisation, in +// milliseconds. It is a property of the symbol and not of the spreading factor: +// SF11 crosses it at 125 kHz and does not at 250 kHz, so a rule written in SF +// alone is wrong at every bandwidth but one. +[[maybe_unused]] constexpr double kLowDataRateSymbolMs = 16.0; + +// Untested. +// // CAD exit modes, from SetCadParams. CAD_ONLY drops back to standby whatever it +// // found; CAD_RX goes straight on into receive when it found something, which is +// // the mode a driver uses to avoid re-arming between the scan and the packet it +// // scanned for. +// [[maybe_unused]] constexpr uint8_t kCadExitOnly = 0x00; +// [[maybe_unused]] constexpr uint8_t kCadExitRx = 0x01; + +// The symbol counts SetCadParams selects between, as 1 << cadSymbolNum: the +// register holds 0 to 4 and the scan lasts 1, 2, 4, 8 or 16 symbols. Anything +// above 4 is not a longer scan, it is a value the part does not define, so it is +// clamped rather than shifted with. +[[maybe_unused]] constexpr uint8_t kCadSymbolNumMax = 4; + +// The timeout register's unit, shared by SetRx, SetTx and SetCadParams: one +// count is 15.625 us, which is 1/64 of a millisecond. +[[maybe_unused]] constexpr double kTimeoutStepMs = 1.0 / 64.0; + +// A 24-bit timeout of all ones is not a very long timeout, it is continuous +// receive, and a driver that means "listen until I say otherwise" writes this. +[[maybe_unused]] constexpr uint32_t kRxContinuous = 0xFFFFFF; + +// How far into a transmission a receiver locks onto the preamble, and where the +// header ends. Both are in symbols and become milliseconds through the current +// modem settings, because that is what makes them behave like a radio rather +// than like a constant: at SF12 a preamble takes an age and at SF7 it is gone in +// a blink, and MeshCore's listen-before-talk times exactly that. +// +// The header cannot be read until the whole preamble, the two sync symbols and +// the 2.25-symbol SFD have gone by, and it is the first interleaver block after +// them: eight symbols, always at CR 4/8. A fixed twelve symbols raised +// HeaderValid 32 symbols early for MeshCore's 32-symbol preamble at SF8 and +// below (16 at SF9 and up), which shrank the stretch where a firmware that can see +// preambles answers differently from one that cannot to a third of its real width. [[maybe_unused]] constexpr double kPreambleSymbols = 4.0; -[[maybe_unused]] constexpr double kHeaderSymbols = 12.0; +[[maybe_unused]] constexpr double kSyncAndSfdSymbols = 4.25; +[[maybe_unused]] constexpr double kHeaderBlockSymbols = 8.0; } // namespace #endif /* VIRTUAL_SX1262_REGISTERS_H */ diff --git a/src/spi.cpp b/src/spi.cpp index e779281..d34b32a 100644 --- a/src/spi.cpp +++ b/src/spi.cpp @@ -39,6 +39,7 @@ static bool returnsData(uint8_t op) { case kGetStatus: case kGetDeviceErrors: case kGetPacketType: + case kGetStats: return true; default: return false; @@ -75,7 +76,7 @@ uint8_t VirtualSX1262::transferByte(uint8_t out) { } // Everything else answers with the status byte in position 1 and nothing // else, exactly as the buffer path does. - return i == 1 ? 0x22 : 0x00; + return i == 1 ? statusByte() : 0x00; } void VirtualSX1262::endTransaction() { @@ -106,7 +107,7 @@ void VirtualSX1262::runCommand(const uint8_t* out, size_t len, uint8_t* in) { auto status = [&](uint8_t v) { if (len > 1) in[1] = v; }; - status(0x22); // standby, command completed + status(statusByte()); switch (op) { case kSetStandby: @@ -116,10 +117,18 @@ void VirtualSX1262::runCommand(const uint8_t* out, size_t len, uint8_t* in) { mode_ = 0; break; case kSetRx: - startRx(); + // The timeout is the three bytes after the opcode. A command that arrives + // without them is a firmware asking for single-shot receive, which is what + // all-zero means, so a short command and an explicit zero agree. + startRx(len >= 4 ? (((uint32_t)out[1] << 16) | ((uint32_t)out[2] << 8) | out[3]) + : 0); break; case kSetTx: - startTx(); + // The same three timeout bytes SetRx carries, on the same unit. All-zero + // is a driver that will wait for TxDone however long it takes, which is + // also what a command too short to carry the field means. + startTx(len >= 4 ? (((uint32_t)out[1] << 16) | ((uint32_t)out[2] << 8) | out[3]) + : 0); break; case kSetCad: startCad(); @@ -162,11 +171,18 @@ void VirtualSX1262::runCommand(const uint8_t* out, size_t len, uint8_t* in) { break; case kSetModulationParams: - if (len >= 4) applyModulation(&out[1]); + if (len >= 4) applyModulation(&out[1], len - 1); break; case kSetPacketParams: if (len >= 7) applyPacketParams(&out[1]); break; + + // The scan's parameters, which decide what a later SetCad costs and where + // it leaves the chip. Ignored until now, so every scan ran with whatever + // the reset defaults were however the firmware had configured it. + case kSetCadParams: + if (len >= 8) applyCadParams(&out[1]); + break; case kSetBufferBase: if (len >= 3) { txBase_ = out[1]; @@ -293,15 +309,23 @@ void VirtualSX1262::runCommand(const uint8_t* out, size_t len, uint8_t* in) { break; case kGetStatus: + // The status byte is the reply, and status() above has already put it in + // in[1]; a bare GetStatus is the one command whose whole answer it is. break; case kGetPacketType: - if (len >= 3) in[2] = 0x01; - break; // LoRa + if (len >= 3) in[2] = packetType_; + break; case kGetDeviceErrors: + if (len >= 4) in[2] = (uint8_t)(deviceErrors_ >> 8); + if (len >= 5) in[3] = (uint8_t)(deviceErrors_ & 0xFF); break; case kClearDeviceErrors: break; default: - break; // acknowledged and ignored + // Everything the driver this model grew around never sends. A command + // that is still nobody's lands here and is acknowledged, which is what + // silicon does with an opcode it does not implement. + runExtraCommand(op, out, len, in); + break; } } diff --git a/test/harness.h b/test/harness.h index 996f8b8..267eb51 100644 --- a/test/harness.h +++ b/test/harness.h @@ -41,6 +41,11 @@ enum { IRQ_PREAMBLE = 1u << 2, IRQ_SYNC_WORD = 1u << 3, IRQ_HEADER = 1u << 4, + IRQ_HEADER_ERR = 1u << 5, + IRQ_CRC_ERR = 1u << 6, + IRQ_CAD_DONE = 1u << 7, + IRQ_CAD_DETECTED = 1u << 8, + IRQ_TIMEOUT = 1u << 9, }; /* Put the chip in a state a driver would recognise: LoRa at SF8/62.5 kHz, every @@ -52,8 +57,15 @@ enum { * in SetDioIrqParams passed every test here for months, so the case named "only * what is routed to DIO1 raises DIO1" sends the real pair instead. */ static inline void bring_up(vsx_chip* c, uint64_t* now) { - /* SetModulationParams: sf, bw, cr, ldro. 0x1A is the 62.5 kHz code. */ - const uint8_t mod[] = {0x8B, 8, 0x1A, 4, 0}; + /* SetModulationParams: sf, bw, cr, ldro. 0x03 is the 62.5 kHz code. + * + * It said 0x1A for a long time, which is not a code at all: the model kept + * whatever bandwidth it already had, so every case here that talks about + * symbols was silently timed at the 250 kHz default while the comment said + * 62.5. Nothing failed, because nothing asserted a number that only holds at + * one of the two. The model now counts a parameter it had to refuse, which is + * what makes the next one of these visible. */ + const uint8_t mod[] = {0x8B, 8, 0x03, 4, 0}; vsx_spi_transaction(c, mod, nullptr, sizeof(mod)); /* SetDioIrqParams: mask, dio1, dio2, dio3 - unmask everything onto DIO1. */ diff --git a/test/test_c_abi.c b/test/test_c_abi.c index 14078af..7f5608e 100644 --- a/test/test_c_abi.c +++ b/test/test_c_abi.c @@ -166,6 +166,23 @@ int main(void) { vsx_transmit_finished(chip); check(1, "vsx_transmit_finished"); + /* The sync word, and a frame from a network this chip is not on. Checked + * from C as well as C++ because the refusal is the whole point of the entry + * point, and a host that gets an uint16_t argument wrong here would deliver + * every frame instead of none. */ + { + vsx_state s; + vsx_get_state(chip, &s); + check(s.sync_word == 0x1424, "vsx_get_state reports the sync word"); + } + dio1_edges = 0; + vsx_deliver_frame_from(chip, frame, sizeof(frame), 0x3444); + { + vsx_counters k; + vsx_get_counters(chip, &k); + check(k.sync_mismatches == 1, "vsx_deliver_frame_from refuses another network"); + } + check(vsx_take_tx(chip, tx, sizeof(tx)) == 0, "vsx_take_tx with nothing to send"); vsx_set_fem_enabled(chip, 1); @@ -195,6 +212,8 @@ int main(void) { vsx_tick(NULL, 1); vsx_set_channel_busy(NULL, 1); vsx_deliver_frame(NULL, frame, sizeof(frame)); + vsx_deliver_frame_from(NULL, frame, sizeof(frame), 0x1424); + vsx_deliver_frame_failed(NULL, frame, sizeof(frame), 1); vsx_transmit_finished(NULL); vsx_set_fem_enabled(NULL, 1); vsx_set_last_signal(NULL, 0.0f, 0.0f); diff --git a/test/test_cad.cpp b/test/test_cad.cpp new file mode 100644 index 0000000..64e4507 --- /dev/null +++ b/test/test_cad.cpp @@ -0,0 +1,195 @@ +/* Channel activity detection: what a scan costs, what it finds, and where it + * leaves the chip. + * + * Every case here failed before CAD was modelled, and most of them failed the + * same way: the old scan answered inside the SPI command, so it cost nothing, + * held the chip for no time and always came back to standby. A firmware doing + * listen before talk with it paid none of the dwell a real part charges, which + * is the entire price of the technique. + * + * Timed at SF12 and 125 kHz throughout, where one symbol is 32.768 ms. That is + * slow enough that a dwell cannot be confused with a rounding error, which at + * SF7 it can. + */ +#include "harness.h" + +namespace { + +/* SF12, 125 kHz, CR 4/8, every interrupt unmasked onto DIO1. Not bring_up(): + * that one picks its own modulation, and every assertion below is a time in + * symbols. */ +void bring_up_slow(vsx_chip* c) { + const uint8_t mod[] = {0x8B, 12, 0x04, 4, 0}; + vsx_spi_transaction(c, mod, nullptr, sizeof(mod)); + const uint8_t dio[] = {0x08, 0xFF, 0xFF, 0xFF, 0xFF, 0, 0, 0, 0}; + vsx_spi_transaction(c, dio, nullptr, sizeof(dio)); +} + +/* SetCadParams: symbolNum, detPeak, detMin, exitMode, timeout 23:16..7:0. */ +void set_cad_params(vsx_chip* c, uint8_t symbols, uint8_t exit_mode) { + const uint8_t p[] = {0x88, symbols, 22, 10, exit_mode, 0, 0, 0}; + vsx_spi_transaction(c, p, nullptr, sizeof(p)); +} + +void set_cad(vsx_chip* c) { + const uint8_t cad[] = {0xC5}; + vsx_spi_transaction(c, cad, nullptr, sizeof(cad)); +} + +uint32_t cad_runs(vsx_chip* c) { + vsx_counters k; + vsx_get_counters(c, &k); + return k.cad_runs; +} + +} // namespace + +int main() { + std::printf("virtual-sx1262"); + + /* ---------------------------------------------------------------- */ + CASE("a scan holds the chip for the dwell its symbol count asks for"); + /* The old model set CadDone inside the command and returned to standby + * before it came back, so a firmware could scan and transmit in the same + * millisecond. Four symbols at SF12/125 kHz is 131 ms, and the answer is not + * available until they have passed. */ + { + vsx_chip* c = vsx_create(); + uint64_t now = 100; + bring_up_slow(c); + set_cad_params(c, 2, 0x00); /* 1 << 2 symbols */ + vsx_tick(c, now); + set_cad(c); + + check(mode_of(c) == 3, "the chip is in CAD as soon as the command lands"); + check((irq_flags(c) & (IRQ_CAD_DONE | IRQ_CAD_DETECTED)) == 0, + "and has answered nothing yet"); + + vsx_tick(c, now + 130); + check(mode_of(c) == 3, "one millisecond short of the dwell it is still scanning"); + check((irq_flags(c) & IRQ_CAD_DONE) == 0, "and still silent"); + + vsx_tick(c, now + 131); + check((irq_flags(c) & IRQ_CAD_DONE) != 0, "at the end of the dwell it reports"); + check(cad_runs(c) == 1, "and the scan is counted"); + vsx_destroy(c); + } + + /* ---------------------------------------------------------------- */ + CASE("a longer symbol count is a longer scan"); + /* The parameter was discarded entirely, so every scan cost the same nothing + * whatever the firmware asked for. */ + { + vsx_chip* c = vsx_create(); + bring_up_slow(c); + set_cad_params(c, 4, 0x00); /* 16 symbols, 524 ms */ + vsx_tick(c, 100); + set_cad(c); + + vsx_tick(c, 100 + 131); + check(mode_of(c) == 3, "four symbols in, a sixteen symbol scan is not finished"); + vsx_tick(c, 100 + 524); + check((irq_flags(c) & IRQ_CAD_DONE) != 0, "sixteen symbols in, it is"); + vsx_destroy(c); + } + + /* ---------------------------------------------------------------- */ + CASE("a carrier that comes and goes inside the dwell is still found"); + /* Sampling at either end of the scan instead of across it would miss exactly + * the transmission CAD exists to catch: a short one, starting after the scan + * began and ending before it finished. */ + { + vsx_chip* c = vsx_create(); + bring_up_slow(c); + set_cad_params(c, 2, 0x00); + vsx_tick(c, 100); + set_cad(c); + + vsx_set_channel_busy(c, 1); + vsx_tick(c, 150); + vsx_set_channel_busy(c, 0); + vsx_tick(c, 180); + vsx_tick(c, 231); + + check((irq_flags(c) & IRQ_CAD_DONE) != 0, "the scan finished"); + check((irq_flags(c) & IRQ_CAD_DETECTED) != 0, + "and it found the carrier it overlapped"); + vsx_destroy(c); + } + + /* ---------------------------------------------------------------- */ + CASE("a quiet channel is scanned and reported quiet"); + { + vsx_chip* c = vsx_create(); + bring_up_slow(c); + set_cad_params(c, 2, 0x00); + vsx_tick(c, 100); + set_cad(c); + vsx_tick(c, 231); + + check((irq_flags(c) & IRQ_CAD_DONE) != 0, "CadDone is raised either way"); + check((irq_flags(c) & IRQ_CAD_DETECTED) == 0, "CadDetected is not"); + check(mode_of(c) == 0, "and CAD_ONLY leaves the chip in standby"); + vsx_destroy(c); + } + + /* ---------------------------------------------------------------- */ + CASE("the thresholds the firmware programmed are reported, not applied"); + /* detPeak and detMin threshold a correlator peak, and this model is told + * whether a carrier is present rather than how strong it is. Recording them + * is what lets a host that does know the level act on them, and what makes a + * firmware setting an implausible threshold visible at all. */ + { + vsx_chip* c = vsx_create(); + bring_up_slow(c); + set_cad_params(c, 3, 0x01); + + vsx_state s; + vsx_get_state(c, &s); + check(s.cad_symbol_num == 3, "the symbol count is readable"); + check(s.cad_det_peak == 22 && s.cad_det_min == 10, "so are both thresholds"); + check(s.cad_exit_mode == 0x01, "and the exit mode"); + vsx_destroy(c); + } + + /* ---------------------------------------------------------------- */ + CASE("a symbol count the part does not define is clamped, not shifted with"); + /* 1 << 9 symbols is not a scan, it is undefined behaviour in the dwell + * arithmetic and a dwell no firmware asked for. */ + { + vsx_chip* c = vsx_create(); + bring_up_slow(c); + set_cad_params(c, 9, 0x00); + + vsx_state s; + vsx_get_state(c, &s); + check(s.cad_symbol_num == 4, "clamped to the longest scan the part defines"); + vsx_destroy(c); + } + + /* ---------------------------------------------------------------- */ + CASE("a scan the firmware masked off runs, and is not reported"); + /* The mask governs the status register, not the radio. The scan still costs + * its dwell and the host still sees it happened; the firmware does not. */ + { + vsx_chip* c = vsx_create(); + const uint8_t mod[] = {0x8B, 12, 0x04, 4, 0}; + vsx_spi_transaction(c, mod, nullptr, sizeof(mod)); + /* Everything except the two CAD bits. */ + const uint8_t dio[] = {0x08, 0xFE, 0x7F, 0xFF, 0xFF, 0, 0, 0, 0}; + vsx_spi_transaction(c, dio, nullptr, sizeof(dio)); + set_cad_params(c, 2, 0x00); + vsx_tick(c, 100); + vsx_set_channel_busy(c, 1); + set_cad(c); + vsx_tick(c, 231); + + check((irq_flags(c) & (IRQ_CAD_DONE | IRQ_CAD_DETECTED)) == 0, + "neither CAD flag reaches the status register"); + check(cad_runs(c) == 1, "but the scan ran"); + vsx_destroy(c); + } + + std::printf("\n%s: %d failure(s)\n", g_failures ? "FAILED" : "PASSED", g_failures); + return g_failures ? 1 : 0; +} diff --git a/test/test_commands.cpp b/test/test_commands.cpp new file mode 100644 index 0000000..a48fb84 --- /dev/null +++ b/test/test_commands.cpp @@ -0,0 +1,289 @@ +/* The rest of the command set. + * + * Every case here was previously an opcode that fell through to "acknowledged + * and ignored": the firmware issued it, got a status byte, and carried on with + * the chip in a state it had not asked for. + * + * Several of these commands are recorded rather than acted on, and the cases + * say so. A test that a value can be read back is not a weaker test than one + * that it changed behaviour: the fault this model exists to catch is a node + * configured differently from how its operator believes, and a value nobody can + * read back is a value nobody can check. + */ +#include "harness.h" + +namespace { + +void tx(vsx_chip* c, const uint8_t* cmd, size_t n) { + vsx_spi_transaction(c, cmd, nullptr, n); +} + +vsx_state state_of(vsx_chip* c) { + vsx_state s; + vsx_get_state(c, &s); + return s; +} + +vsx_counters counters_of(vsx_chip* c) { + vsx_counters k; + vsx_get_counters(c, &k); + return k; +} + +} // namespace + +int main() { + std::printf("virtual-sx1262 commands"); + + /* ---------------------------------------------------------------- */ + CASE("GetStatus says what the chip is actually doing"); + /* It answered the constant 0x22 - standby - whatever the part was doing, so a + * firmware polling it to find out whether its receiver was still armed was + * told no by a chip that was receiving. */ + { + vsx_chip* c = vsx_create(); + uint64_t now = 0; + bring_up(c, &now); + + uint8_t in[3] = {0}; + const uint8_t get[] = {0xC0, 0x00, 0x00}; + vsx_spi_transaction(c, get, in, sizeof(get)); + check(((in[1] >> 4) & 0x7) == 0x5, "receiving reads as RX"); + + const uint8_t stby[] = {0x80, 0x00}; + tx(c, stby, sizeof(stby)); + vsx_spi_transaction(c, get, in, sizeof(get)); + check(((in[1] >> 4) & 0x7) == 0x2, "standby reads as STBY_RC"); + + const uint8_t txc[] = {0x83, 0x00, 0x00, 0x00}; + tx(c, txc, sizeof(txc)); + vsx_spi_transaction(c, get, in, sizeof(get)); + check(((in[1] >> 4) & 0x7) == 0x6, "transmitting reads as TX"); + vsx_destroy(c); + } + + /* ---------------------------------------------------------------- */ + CASE("SetPacketType is remembered, and GetPacketType answers it"); + /* GetPacketType returned a hardcoded LoRa, so a firmware that had switched + * the part to GFSK was told it had not. */ + { + vsx_chip* c = vsx_create(); + check(state_of(c).packet_type == 0x01, "LoRa to begin with"); + + const uint8_t gfsk[] = {0x8A, 0x00}; + tx(c, gfsk, sizeof(gfsk)); + uint8_t in[3] = {0}; + const uint8_t get[] = {0x11, 0x00, 0x00}; + vsx_spi_transaction(c, get, in, sizeof(get)); + check(in[2] == 0x00, "GFSK is answered as GFSK"); + check(state_of(c).packet_type == 0x00, "and reported to the host, which is"); + check(1, "how a host can refuse to believe a run that switched modem"); + vsx_destroy(c); + } + + /* ---------------------------------------------------------------- */ + CASE("the three SetPacketParams fields that were assumed now count"); + /* estAirtimeMs hardcoded an explicit header and CRC on. An implicit header is + * twenty symbols of payload the model charged for and the part did not. */ + { + vsx_chip* c = vsx_create(); + const uint8_t mod[] = {0x8B, 9, 0x04, 1, 0}; + tx(c, mod, sizeof(mod)); + + /* preamble 8, explicit header, 32 bytes, CRC on, IQ normal */ + const uint8_t explicit_crc[] = {0x8C, 0x00, 0x08, 0x00, 32, 0x01, 0x00}; + tx(c, explicit_crc, sizeof(explicit_crc)); + const uint32_t full = vsx_est_airtime_ms(c, 32); + check(state_of(c).header_implicit == 0 && state_of(c).crc_on == 1, + "explicit header and CRC on are read, not assumed"); + + const uint8_t implicit_nocrc[] = {0x8C, 0x00, 0x08, 0x01, 32, 0x00, 0x01}; + tx(c, implicit_nocrc, sizeof(implicit_nocrc)); + const uint32_t lean = vsx_est_airtime_ms(c, 32); + check(state_of(c).header_implicit == 1, "an implicit header is read"); + check(state_of(c).crc_on == 0, "so is CRC off"); + check(state_of(c).invert_iq == 1, "so is inverted IQ"); + check(lean < full, "and dropping the header and the CRC costs less airtime"); + vsx_destroy(c); + } + + /* ---------------------------------------------------------------- */ + CASE("StopTimerOnPreamble keeps a bounded receive from cutting off a frame"); + /* Without it a receiver on a deadline gives up mid packet. This is the whole + * reason the command exists. */ + { + vsx_chip* c = vsx_create(); + uint64_t now = 0; + bring_up(c, &now); + const uint8_t stop[] = {0x9F, 0x01}; + tx(c, stop, sizeof(stop)); + /* 6400 counts of 15.625 us is 100 ms. */ + const uint8_t rx[] = {0x82, 0x00, 0x19, 0x00}; + tx(c, rx, sizeof(rx)); + + vsx_set_channel_busy(c, 1); + for (uint64_t t = now + 1; t <= now + 200; ++t) { + vsx_tick(c, t); + } + check((irq_flags(c) & IRQ_TIMEOUT) == 0, "a preamble stopped the timer"); + check(mode_of(c) == 1, "and the receiver is still listening past the deadline"); + vsx_destroy(c); + } + + /* ---------------------------------------------------------------- */ + CASE("SetLoRaSymbNumTimeout is a deadline counted in symbols"); + { + vsx_chip* c = vsx_create(); + uint64_t now = 100; + const uint8_t mod[] = {0x8B, 12, 0x04, 4, 0}; /* 32.768 ms a symbol */ + tx(c, mod, sizeof(mod)); + const uint8_t dio[] = {0x08, 0xFF, 0xFF, 0xFF, 0xFF, 0, 0, 0, 0}; + tx(c, dio, sizeof(dio)); + const uint8_t symb[] = {0xA0, 4}; + tx(c, symb, sizeof(symb)); + vsx_tick(c, now); + const uint8_t rx[] = {0x82, 0x00, 0x00, 0x00}; /* no millisecond timeout */ + tx(c, rx, sizeof(rx)); + + check(state_of(c).symb_num_timeout == 4, "four symbols is what was asked for"); + vsx_tick(c, now + 130); + check((irq_flags(c) & IRQ_TIMEOUT) == 0, "a symbol short, still listening"); + vsx_tick(c, now + 132); + check((irq_flags(c) & IRQ_TIMEOUT) != 0, "four symbols on, Timeout"); + vsx_destroy(c); + } + + /* ---------------------------------------------------------------- */ + CASE("SetRxTxFallbackMode is recorded, and FS is not a mode this model has"); + /* PLACEHOLDER, and the test pins the placeholder rather than pretending: a + * firmware asking to fall back to FS gets standby, and what it asked for is + * readable so a host can tell that is what happened. */ + { + vsx_chip* c = vsx_create(); + uint64_t now = 0; + bring_up(c, &now); + const uint8_t fb[] = {0x93, 0x40}; /* FS */ + tx(c, fb, sizeof(fb)); + check(state_of(c).fallback_mode == 0x40, "the mode asked for is readable"); + + const uint8_t txc[] = {0x83, 0x00, 0x00, 0x00}; + tx(c, txc, sizeof(txc)); + vsx_transmit_finished(c); + check(mode_of(c) == 0, "and a finished transmission still lands in standby"); + vsx_destroy(c); + } + + /* ---------------------------------------------------------------- */ + CASE("SetPaConfig is recorded, because the board owns the other half"); + { + vsx_chip* c = vsx_create(); + const uint8_t pa[] = {0x95, 0x04, 0x07, 0x00, 0x01}; /* the +22 dBm row */ + tx(c, pa, sizeof(pa)); + const vsx_state s = state_of(c); + check(s.pa_duty_cycle == 0x04 && s.pa_hp_max == 0x07, "both halves readable"); + check(s.pa_device_sel == 0x00, "and which device it selected"); + vsx_destroy(c); + } + + /* ---------------------------------------------------------------- */ + CASE("the recorded-only commands are all readable"); + /* One case for the lot of them, because the assertion is the same: the + * firmware said something and the host can find out what. */ + { + vsx_chip* c = vsx_create(); + const uint8_t reg[] = {0x96, 0x01}; /* DC-DC */ + const uint8_t sw[] = {0x9D, 0x01}; /* DIO2 drives the RF switch */ + const uint8_t tcxo[] = {0x97, 0x02, 0x00, 0x00, 0x64}; + tx(c, reg, sizeof(reg)); + tx(c, sw, sizeof(sw)); + tx(c, tcxo, sizeof(tcxo)); + + const vsx_state s = state_of(c); + check(s.regulator_mode == 0x01, "the regulator mode"); + check(s.dio2_as_rf_switch == 1, "that the chip was told to drive the RF switch"); + check(s.dio3_as_tcxo == 1, "and that DIO3 feeds a TCXO"); + vsx_destroy(c); + } + + /* ---------------------------------------------------------------- */ + CASE("SetRxDutyCycle records its periods and leaves the receiver on"); + /* PLACEHOLDER. A duty-cycled receiver is deaf for a scheduled fraction of the + * time, which changes which frames arrive at all, and that is the engine's + * decision rather than the chip's. Optimistic, and the test says so. */ + { + vsx_chip* c = vsx_create(); + uint64_t now = 0; + bring_up(c, &now); + const uint8_t duty[] = {0x94, 0x00, 0x19, 0x00, 0x00, 0x32, 0x00}; + tx(c, duty, sizeof(duty)); + + const vsx_state s = state_of(c); + check(s.rx_duty_rx_period == 0x001900, "the listening period is readable"); + check(s.rx_duty_sleep_period == 0x003200, "so is the sleeping one"); + check(mode_of(c) == 1, "and the receiver stays on, which is the optimistic case"); + vsx_destroy(c); + } + + /* ---------------------------------------------------------------- */ + CASE("the test modes key the transmitter and say so"); + /* RECORDED: a bare carrier is a transmission with no frame behind it, and + * there is no way to hand that to the engine. A host modelling an interferer + * reads the flag and puts the carrier on the air itself. */ + { + vsx_chip* c = vsx_create(); + const uint8_t cw[] = {0xD1}; + tx(c, cw, sizeof(cw)); + check(state_of(c).tx_continuous_wave == 1, "a continuous wave is flagged"); + check(mode_of(c) == 2, "and the chip is transmitting"); + vsx_transmit_finished(c); + check(state_of(c).tx_continuous_wave == 0, "and stops when the carrier does"); + vsx_destroy(c); + } + + /* ---------------------------------------------------------------- */ + CASE("GetStats counts what arrived"); + { + vsx_chip* c = vsx_create(); + uint64_t now = 0; + bring_up(c, &now); + const uint8_t frame[] = {1, 2, 3}; + vsx_deliver_frame(c, frame, sizeof(frame)); + vsx_tick(c, ++now); + + uint8_t in[9] = {0}; + const uint8_t get[] = {0x10, 0, 0, 0, 0, 0, 0, 0, 0}; + vsx_spi_transaction(c, get, in, sizeof(get)); + check(((in[2] << 8) | in[3]) == 1, "one packet received"); + check(counters_of(c).stat_rx_packets == 1, "and the host sees the same"); + + const uint8_t reset[] = {0x00, 0, 0, 0, 0, 0, 0}; + tx(c, reset, sizeof(reset)); + check(counters_of(c).stat_rx_packets == 0, "ResetStats clears it"); + vsx_destroy(c); + } + + /* ---------------------------------------------------------------- */ + CASE("a frame that failed its CRC can be delivered, once something says so"); + /* PLACEHOLDER INTERFACE. Nothing calls this: the chip cannot know a CRC + * failed, and no host is wired to tell it. The case exists so that the path + * is real rather than hypothetical when one is. */ + { + vsx_chip* c = vsx_create(); + uint64_t now = 0; + bring_up(c, &now); + const uint8_t frame[] = {0xDE, 0xAD}; + vsx_deliver_frame_failed(c, frame, sizeof(frame), 1); + + check((irq_flags(c) & IRQ_RX_DONE) != 0, "RxDone, because a frame did arrive"); + check((irq_flags(c) & IRQ_CRC_ERR) != 0, "and CrcErr beside it"); + check(counters_of(c).stat_crc_errors == 1, "GetStats counts it"); + + vsx_deliver_frame_failed(c, frame, sizeof(frame), 2); + check((irq_flags(c) & IRQ_HEADER_ERR) != 0, "a header error raises HeaderErr"); + check(counters_of(c).stat_header_errors == 1, "and is counted separately"); + vsx_destroy(c); + } + + std::printf("\n%s: %d failure(s)\n", g_failures ? "FAILED" : "PASSED", g_failures); + return g_failures ? 1 : 0; +} diff --git a/test/test_interrupts.cpp b/test/test_interrupts.cpp index 01e593b..4e97abe 100644 --- a/test/test_interrupts.cpp +++ b/test/test_interrupts.cpp @@ -138,5 +138,160 @@ int main() { vsx_destroy(c); } + /* ---------------------------------------------------------------- */ + CASE("an interrupt the firmware masked off is not in the status register"); + /* IrqMask was parsed and then never used, so the register answered every + * event whatever the firmware had enabled. That is forgiving in the direction + * that hides a fault: a driver waiting on an interrupt it forgot to enable + * waits for ever on silicon and works perfectly here, and the difference only + * shows up on a board. + * + * The pair sent here is RadioLib's own for a receive that wants only RxDone: + * enabled in the register, routed to the pin, and nothing else in either. */ + { + vsx_chip* c = vsx_create(); + uint64_t now = 0; + const uint8_t mod[] = {0x8B, 8, 0x1A, 4, 0}; + vsx_spi_transaction(c, mod, nullptr, sizeof(mod)); + const uint8_t dio[] = {0x08, 0x00, 0x02, 0x00, 0x02, 0, 0, 0, 0}; + vsx_spi_transaction(c, dio, nullptr, sizeof(dio)); + const uint8_t rx[] = {0x82, 0xFF, 0xFF, 0xFF}; + vsx_spi_transaction(c, rx, nullptr, sizeof(rx)); + + vsx_set_channel_busy(c, 1); + for (int i = 0; i < 100; ++i) { + vsx_tick(c, ++now); + } + check((irq_flags(c) & (IRQ_PREAMBLE | IRQ_HEADER)) == 0, + "the detection flags are masked off, so the register does not carry them"); + + vsx_counters k; + vsx_get_counters(c, &k); + check(k.preamble_raises > 0, "the carrier was detected all the same"); + check(k.irq_suppressed > 0, "and the chip counts what it did not report"); + + const uint8_t frame[] = {1, 2, 3, 4}; + vsx_deliver_frame(c, frame, sizeof(frame)); + vsx_tick(c, ++now); + check((irq_flags(c) & IRQ_RX_DONE) != 0, "the one interrupt it did enable arrives"); + vsx_destroy(c); + } + + /* ---------------------------------------------------------------- */ + CASE("a masked interrupt cannot raise DIO1 either"); + /* DIO1 selects from what the register carries. Routing a bit to the pin that + * the mask keeps out of the register is a driver asking for something the + * part cannot do, and answering it would put a level on the pin with nothing + * behind it to read. */ + { + vsx_chip* c = vsx_create(); + uint64_t now = 0; + const uint8_t mod[] = {0x8B, 8, 0x1A, 4, 0}; + vsx_spi_transaction(c, mod, nullptr, sizeof(mod)); + /* Nothing enabled, everything routed. */ + const uint8_t dio[] = {0x08, 0x00, 0x00, 0xFF, 0xFF, 0, 0, 0, 0}; + vsx_spi_transaction(c, dio, nullptr, sizeof(dio)); + const uint8_t rx[] = {0x82, 0xFF, 0xFF, 0xFF}; + vsx_spi_transaction(c, rx, nullptr, sizeof(rx)); + + vsx_set_channel_busy(c, 1); + for (int i = 0; i < 100; ++i) { + vsx_tick(c, ++now); + } + check(vsx_dio1_asserted(c) == 0, "the pin stays low"); + vsx_destroy(c); + } + + /* ---------------------------------------------------------------- */ + CASE("a receive timeout expires into Timeout and standby"); + /* SetRx carried a timeout that was discarded, so the bit could never be set + * and a bounded receive never ended. Continuous receive is all ones and is + * the case that must keep working, because it is the one MeshCore uses. */ + { + vsx_chip* c = vsx_create(); + uint64_t now = 0; + bring_up(c, &now); + /* 6400 counts of 15.625 us is 100 ms. */ + const uint8_t rx[] = {0x82, 0x00, 0x19, 0x00}; + vsx_spi_transaction(c, rx, nullptr, sizeof(rx)); + + vsx_tick(c, now + 99); + check(mode_of(c) == 1, "before the deadline it is still listening"); + check((irq_flags(c) & IRQ_TIMEOUT) == 0, "and has not timed out"); + + vsx_tick(c, now + 100); + check((irq_flags(c) & IRQ_TIMEOUT) != 0, "at the deadline Timeout is raised"); + check(mode_of(c) == 0, "and the receiver has stopped"); + vsx_destroy(c); + + c = vsx_create(); + now = 0; + bring_up(c, &now); + for (int i = 0; i < 500; ++i) { + vsx_tick(c, ++now); + } + check(mode_of(c) == 1, "continuous receive has no deadline to reach"); + check((irq_flags(c) & IRQ_TIMEOUT) == 0, "and never times out"); + vsx_destroy(c); + } + + /* ---------------------------------------------------------------- */ + CASE("a transmit timeout expires into Timeout and standby"); + /* SetTx carries the same three bytes SetRx does, and they were discarded the + * same way. Unlike receive there is no continuous case: a transmission ends, + * so a deadline reached means the waveform never finished. */ + { + vsx_chip* c = vsx_create(); + uint64_t now = 0; + bring_up(c, &now); + /* 6400 counts of 15.625 us is 100 ms. */ + const uint8_t tx[] = {0x83, 0x00, 0x19, 0x00}; + vsx_spi_transaction(c, tx, nullptr, sizeof(tx)); + check(mode_of(c) == 2, "the chip is transmitting"); + + vsx_tick(c, now + 99); + check((irq_flags(c) & IRQ_TIMEOUT) == 0, "before the deadline, nothing"); + vsx_tick(c, now + 100); + check((irq_flags(c) & IRQ_TIMEOUT) != 0, "at it, Timeout"); + check(mode_of(c) == 0, "and the transmitter has stopped"); + check((irq_flags(c) & IRQ_TX_DONE) == 0, "a transmission that timed out is not done"); + vsx_destroy(c); + } + + /* ---------------------------------------------------------------- */ + CASE("a transmission that finishes in time does not also time out"); + /* The deadline has to be disarmed by the thing it was waiting for, or a chip + * that transmitted perfectly reports a fault a hundred milliseconds later. */ + { + vsx_chip* c = vsx_create(); + uint64_t now = 0; + bring_up(c, &now); + const uint8_t tx[] = {0x83, 0x00, 0x19, 0x00}; + vsx_spi_transaction(c, tx, nullptr, sizeof(tx)); + + vsx_tick(c, now + 10); + vsx_transmit_finished(c); + check((irq_flags(c) & IRQ_TX_DONE) != 0, "TxDone arrives"); + + vsx_tick(c, now + 500); + check((irq_flags(c) & IRQ_TIMEOUT) == 0, "and the deadline it beat never fires"); + vsx_destroy(c); + } + + /* ---------------------------------------------------------------- */ + CASE("a transmit timeout of zero is a driver that will wait"); + { + vsx_chip* c = vsx_create(); + uint64_t now = 0; + bring_up(c, &now); + const uint8_t tx[] = {0x83, 0x00, 0x00, 0x00}; + vsx_spi_transaction(c, tx, nullptr, sizeof(tx)); + + vsx_tick(c, now + 5000); + check(mode_of(c) == 2, "still transmitting five seconds later"); + check((irq_flags(c) & IRQ_TIMEOUT) == 0, "with no deadline to reach"); + vsx_destroy(c); + } + return report(""); } diff --git a/test/test_modem.cpp b/test/test_modem.cpp new file mode 100644 index 0000000..bbac62f --- /dev/null +++ b/test/test_modem.cpp @@ -0,0 +1,213 @@ +/* What SetModulationParams programmed, and who the chip will listen to. + * + * Two kinds of fault share this file because they present the same way: a node + * that is configured wrong and reports nothing wrong. A bandwidth code the part + * does not define used to leave the modem on its previous setting in silence, + * and a sync word the firmware set was written into a register that nothing + * ever read back, so two meshes on one frequency heard each other perfectly. + */ +#include "harness.h" + +namespace { + +void set_modulation(vsx_chip* c, uint8_t sf, uint8_t bw, uint8_t cr, uint8_t ldro) { + const uint8_t mod[] = {0x8B, sf, bw, cr, ldro}; + vsx_spi_transaction(c, mod, nullptr, sizeof(mod)); +} + +void write_sync_word(vsx_chip* c, uint16_t word) { + const uint8_t w[] = {0x0D, 0x07, 0x40, (uint8_t)(word >> 8), (uint8_t)(word & 0xFF)}; + vsx_spi_transaction(c, w, nullptr, sizeof(w)); +} + +vsx_state state_of(vsx_chip* c) { + vsx_state s; + vsx_get_state(c, &s); + return s; +} + +vsx_counters counters_of(vsx_chip* c) { + vsx_counters k; + vsx_get_counters(c, &k); + return k; +} + +void arm_receiver(vsx_chip* c) { + const uint8_t dio[] = {0x08, 0xFF, 0xFF, 0xFF, 0xFF, 0, 0, 0, 0}; + vsx_spi_transaction(c, dio, nullptr, sizeof(dio)); + const uint8_t rx[] = {0x82, 0xFF, 0xFF, 0xFF}; + vsx_spi_transaction(c, rx, nullptr, sizeof(rx)); +} + +} // namespace + +int main() { + std::printf("virtual-sx1262 modem"); + + /* ---------------------------------------------------------------- */ + CASE("every bandwidth in the table is a bandwidth"); + { + vsx_chip* c = vsx_create(); + const uint8_t codes[] = {0x00, 0x08, 0x01, 0x09, 0x02, 0x0A, 0x03, 0x04, 0x05, 0x06}; + const uint32_t hz[] = {7810, 10420, 15630, 20830, 31250, + 41670, 62500, 125000, 250000, 500000}; + bool all = true; + for (unsigned i = 0; i < sizeof(codes); ++i) { + set_modulation(c, 9, codes[i], 1, 0); + if (state_of(c).bandwidth_hz != hz[i]) { + all = false; + } + } + check(all, "all ten codes decode to the datasheet's bandwidths"); + check(counters_of(c).params_rejected == 0, "and none of them is refused"); + vsx_destroy(c); + } + + /* ---------------------------------------------------------------- */ + CASE("a bandwidth code the part does not define is refused and counted"); + /* This file's own harness had been sending 0x1A, which is not a code. The + * modem kept its previous setting, so every timing assertion ran at a + * bandwidth nobody had chosen and nothing said so. */ + { + vsx_chip* c = vsx_create(); + set_modulation(c, 9, 0x04, 1, 0); + check(state_of(c).bandwidth_hz == 125000, "125 kHz to begin with"); + + set_modulation(c, 9, 0x1A, 1, 0); + check(state_of(c).bandwidth_hz == 125000, + "a code that is not a code changes nothing"); + check(counters_of(c).params_rejected == 1, "and is counted as refused"); + vsx_destroy(c); + } + + /* ---------------------------------------------------------------- */ + CASE("a spreading factor or coding rate outside the part is refused too"); + { + vsx_chip* c = vsx_create(); + set_modulation(c, 9, 0x04, 1, 0); + + set_modulation(c, 20, 0x04, 1, 0); + check(state_of(c).spreading_factor == 9, "SF20 does not become the setting"); + set_modulation(c, 9, 0x04, 7, 0); + check(state_of(c).coding_rate == 5, "nor does a coding rate of 4/11"); + check(counters_of(c).params_rejected == 2, "both are counted"); + vsx_destroy(c); + } + + /* ---------------------------------------------------------------- */ + CASE("low data rate optimisation is the field the firmware programmed"); + /* It used to be inferred from the spreading factor alone, which is the one + * thing it does not depend on: the threshold is a symbol duration, so SF11 + * crosses it at 125 kHz and does not at 250. The airtime this chip quotes is + * the figure the firmware times its own CSMA on, so inferring it wrong + * desynchronised the chip from the air it was supposed to be describing. */ + { + vsx_chip* c = vsx_create(); + + set_modulation(c, 11, 0x05, 1, 0); /* SF11, 250 kHz, 4/5, LDRO off */ + check(state_of(c).low_data_rate_optimize == 0, "off is reported off"); + const uint32_t without = vsx_est_airtime_ms(c, 50); + check(without >= 640 && without <= 642, "a 50 byte frame is about 641 ms"); + + set_modulation(c, 11, 0x05, 1, 1); /* the same, LDRO on */ + check(state_of(c).low_data_rate_optimize == 1, "on is reported on"); + const uint32_t with = vsx_est_airtime_ms(c, 50); + check(with >= 722 && with <= 724, "with it on the same frame is about 723 ms"); + check(with > without, "two bits a symbol is not free"); + vsx_destroy(c); + } + + /* ---------------------------------------------------------------- */ + CASE("a firmware that never programmed it falls back to the symbol duration"); + /* Not to the spreading factor. SF11 is over the threshold at 125 kHz and + * under it at 250, and the fallback has to say so. */ + { + vsx_chip* c = vsx_create(); + const uint8_t narrow[] = {0x8B, 11, 0x04, 1}; /* no LDRO byte at all */ + vsx_spi_transaction(c, narrow, nullptr, sizeof(narrow)); + check(state_of(c).low_data_rate_optimize == 1, + "SF11 at 125 kHz is over the threshold"); + + vsx_chip* d = vsx_create(); + const uint8_t wide[] = {0x8B, 11, 0x05, 1}; + vsx_spi_transaction(d, wide, nullptr, sizeof(wide)); + check(state_of(d).low_data_rate_optimize == 0, "SF11 at 250 kHz is under it"); + vsx_destroy(c); + vsx_destroy(d); + } + + /* ---------------------------------------------------------------- */ + CASE("the chip comes up on the private sync word"); + /* Zero would have every node agreeing with every other node about a value + * none of them chose, which is the same bug as the shared keypair: a default + * that is not the part's default is a default that hides a missing setting. */ + { + vsx_chip* c = vsx_create(); + check(state_of(c).sync_word == 0x1424, "0x1424, as the part resets to"); + write_sync_word(c, 0x3444); + check(state_of(c).sync_word == 0x3444, "and the firmware can set the public one"); + vsx_destroy(c); + } + + /* ---------------------------------------------------------------- */ + CASE("a frame sent under another sync word is not this node's to hear"); + /* The word is written through two registers rather than a command, so it + * landed in the register array and nothing read it back: two meshes sharing a + * frequency heard each other perfectly, which is the opposite of what the + * word is for. */ + { + vsx_chip* c = vsx_create(); + uint64_t now = 0; + bring_up(c, &now); + write_sync_word(c, 0x1424); + + const uint8_t frame[] = {1, 2, 3, 4}; + vsx_deliver_frame_from(c, frame, sizeof(frame), 0x3444); + vsx_tick(c, ++now); + check((irq_flags(c) & IRQ_RX_DONE) == 0, "the other network's frame is refused"); + check(counters_of(c).sync_mismatches == 1, "and counted, not silently lost"); + + vsx_deliver_frame_from(c, frame, sizeof(frame), 0x1424); + vsx_tick(c, ++now); + check((irq_flags(c) & IRQ_RX_DONE) != 0, "its own network's frame arrives"); + vsx_destroy(c); + } + + /* ---------------------------------------------------------------- */ + CASE("a host that does not model sync words is unaffected"); + /* vsx_deliver_frame makes no claim about the transmitter, and no claim is not + * a claim that it matched. Refusing on it would break every host that has not + * been taught about the word yet. */ + { + vsx_chip* c = vsx_create(); + uint64_t now = 0; + bring_up(c, &now); + write_sync_word(c, 0x3444); + + const uint8_t frame[] = {9, 9}; + vsx_deliver_frame(c, frame, sizeof(frame)); + vsx_tick(c, ++now); + check((irq_flags(c) & IRQ_RX_DONE) != 0, "the older entry point still delivers"); + check(counters_of(c).sync_mismatches == 0, "and refuses nothing"); + vsx_destroy(c); + } + + /* ---------------------------------------------------------------- */ + CASE("a refused frame does not occupy the receiver either"); + /* Dropping it into the inbox and then refusing it later would make it a frame + * the chip was deaf to, which is a different counter and a different fault. */ + { + vsx_chip* c = vsx_create(); + uint64_t now = 0; + arm_receiver(c); + const uint8_t frame[] = {1}; + vsx_deliver_frame_from(c, frame, sizeof(frame), 0x3444); + vsx_tick(c, ++now); + check(counters_of(c).frames_dropped == 0, "it was never handed over to be dropped"); + check(counters_of(c).sync_mismatches == 1, "it was refused at the door"); + vsx_destroy(c); + } + + std::printf("\n%s: %d failure(s)\n", g_failures ? "FAILED" : "PASSED", g_failures); + return g_failures ? 1 : 0; +} diff --git a/test/test_reception.cpp b/test/test_reception.cpp index e4bf302..d125a7d 100644 --- a/test/test_reception.cpp +++ b/test/test_reception.cpp @@ -217,5 +217,49 @@ int main() { vsx_destroy(c); } + /* ---------------------------------------------------------------- */ + CASE("HeaderValid waits for the preamble, the SFD and the header block"); + /* It was raised twelve symbols into the carrier whatever the preamble, so for + * MeshCore's sixteen-symbol preamble it came sixteen symbols early. A + * firmware that sees preambles and one that does not answer differently only + * between PreambleDetected and HeaderValid, and that stretch was a third of + * its real width. At SF8 and 62.5 kHz a symbol is 4.096 ms, so the header ends + * (16 + 4.25 + 8) symbols, about 115.7 ms, into the frame. */ + { + vsx_chip* c = vsx_create(); + uint64_t now = 0; + bring_up(c, &now); + vsx_set_channel_busy(c, 1); + for (int i = 0; i < 100; ++i) { + vsx_tick(c, ++now); + } + check((irq_flags(c) & IRQ_PREAMBLE) != 0, "the preamble is detected early on"); + check((irq_flags(c) & IRQ_HEADER) == 0, + "but 100 ms in, the header has not ended yet"); + for (int i = 0; i < 20; ++i) { + vsx_tick(c, ++now); + } + check((irq_flags(c) & IRQ_HEADER) != 0, "and 120 ms in, it has"); + vsx_destroy(c); + } + + /* ---------------------------------------------------------------- */ + CASE("an implicit-header receiver never raises HeaderValid"); + { + vsx_chip* c = vsx_create(); + uint64_t now = 0; + bring_up(c, &now); + /* SetPacketParams: preamble 16, implicit header, length 32, CRC on. */ + const uint8_t pp[] = {0x8C, 0x00, 0x10, 0x01, 0x20, 0x01, 0x00, 0x00, 0x00}; + vsx_spi_transaction(c, pp, nullptr, sizeof(pp)); + vsx_set_channel_busy(c, 1); + for (int i = 0; i < 300; ++i) { + vsx_tick(c, ++now); + } + check((irq_flags(c) & IRQ_PREAMBLE) != 0, "the preamble is still detected"); + check((irq_flags(c) & IRQ_HEADER) == 0, "but there is no header to validate"); + vsx_destroy(c); + } + return report(""); }