The modem¶
oxinode::modem::Modem is the one place in the firmware that programs a
configuration into the LR1121, senses the channel, transmits and receives. It
takes a ValidConfig and nothing else, and everything it decides without the
chip is decided in oxinode_core::lr1121.
Configuration¶
An RNode host sets five things, one at a time, in any order:
| units on the wire | |
|---|---|
| frequency | Hz |
| bandwidth | Hz |
| spreading factor | 7–12 |
| coding rate | 5–8, the denominator of 4/n |
| transmit power | dBm |
Two consequences shape the design. Intermediate states are invalid and that is normal: a host moving from 915 to 868 MHz passes through a frequency outside the band. Rejecting a set is wrong; refusing to apply an invalid combination is right. And the chip's units are not the wire's units: bandwidth is a code, coding rate is 1–4, and the commanded frequency is not the wanted one. Every one of those conversions is somewhere a wrong answer produces a radio that works perfectly at the wrong settings.
So there are two types in oxinode_core::lr1121::config:
flowchart LR
host["host setter<br/>or panel editor<br/>or stored record"] --> rc["RadioConfig<br/>plain data, no invariants"]
rc -- "check()" --> vc["ValidConfig<br/>the only thing Modem::apply accepts"]
rc -- "check() fails" --> err["ConfigError<br/>FrequencyOutOfBand · UnsupportedBandwidth<br/>SpreadingFactorOutOfRange · CodingRateOutOfRange<br/>PowerAboveModuleRating · PowerUnreachable · PreambleTooShort"]
vc --> chip["LR1121"]
ValidConfig can only be built by passing check(), so "did anybody validate
this?" is a question the compiler answers. check() names each limit
separately because a host on the far end of a serial line has nothing else to
go on. It deliberately does not check whether an RNode host could express the
configuration: SF5 and SF6 are outside the protocol's 7–12 and still valid at
the chip; is_rnode_representable() answers that other question.
Nothing clamps. A host asking for 22 dBm on a module rated for 20 is told 22 dBm and then simply does not get a radio: the state comes back off, the host finds the mismatch, and the specific reason goes to the log port.
The same ValidConfig gate is applied to a host's request over KISS, to the
stored configuration when a TNC-mode board boots, and to a confirmed edit from
the panel.
apply, transmit, receive¶
stateDiagram-v2
[*] --> StandbyXOSC: bring-up
StandbyXOSC --> StandbyXOSC: apply(ValidConfig)
StandbyXOSC --> Rx: start_rx (continuous)
Rx --> StandbyXOSC: leave_receive, before any reconfiguration
Rx --> Sensing: transmit() asked
StandbyXOSC --> Sensing: transmit() asked
Sensing --> ListenSlot: CAD clear
ListenSlot --> Sensing: slot elapsed
ListenSlot --> Rx: packet received while waiting (handed up)
Sensing --> Tx: DIFS + window clear, or budget exhausted
Tx --> Rx: TxDone (returns to receive if it was receiving)
apply programs a configuration and always starts from standby: the LR1121
accepts its configuration commands only in standby, and from receive it
accepts them and reports CMD_FAIL on the next status read, so a modem that
did not leave receive first would keep the old settings while honestly
reporting the new ones. SetPacketType goes first and is not optional.
transmit loads the buffer, listens before it sends (see
On the air), issues SetTx from standby XOSC so the 5 ms oscillator
startup is not charged to the packet, waits for the interrupt line, and
checks that it was TxDone and not something else. A WrongInterrupt is
returned rather than a report, because that is precisely the case a bare
timeout on the interrupt line reports as success. The chip's own transmit
timeout is three airtimes, saturating at the field's 24-bit width.
start_rx puts the chip in continuous receive with no symbol timeout;
receive waits for the interrupt and reads the packet with its RSSI and SNR.
The chip holds one received packet in its buffer, so a packet arriving while
the modem loop is busy is not lost, but a second one behind it would be.
Every await is bounded and named. lr11xx waits on BUSY with no timeout, so a
command that leaves BUSY high hangs the driver, and ModemError::Timeout
carries which step expired. Every sequence ends by asking for a status,
because the chip returns the status of the previous command.
The default¶
With no stored configuration and no host, the radio is off and the
configuration is 915.000 MHz, 125 kHz, SF8, 4/5, 14 dBm, 8 symbols of
preamble, sync word 0x12, CRC on, explicit header, which comes to 3125 bps.
That bitrate is pinned by a host test, and it is the number Reticulum's own
arithmetic reports when it brings the interface up.
Console¶
The radio image holds a RadioConfig instead of constants and lets every
field be cycled from a key, which is how a configuration is exercised on the
bench without a rebuild. See Firmware images.