PROP Time Division Duplex¶
PROP — proprietary 2.4 GHz FSK link layer. TDD half-duplex: both peers share one channel, alternating TX/RX. No fixed slot assignment; listen timeout with random jitter resolves contention. Idle peer transmits keepalive or queued payload on timeout.
Implementation reference: firmware/src/prop/
radio_core.c— authoritative for radio timing and DMA ownership. Entirely Timer/DPPI and ISR based.session.c— authoritative for service state, app queues, payload sequencing, and loss accounting.
Protocol¶
- Both peers run identical half-duplex ping-pong logic.
- No role-specific slot timing exists.
- Device waits for RX to be done then sends itself, then listens again.
- A TX turn always sends either one queued payload packet or one keepalive.
- Listen timeouts include random jitter; the first peer to time out transmits, breaking deadlock.
Radio Turn State¶
Active protocol states:
| State | Meaning |
|---|---|
LISTEN |
Radio waits for peer ADDRESS or listen deadline. |
IN_RX |
Peer packet has reached ADDRESS; RX deadline is armed. |
IN_TX |
Local TX has been committed; late peer ADDRESS is ignored. |
Implementation sentinel:
| State | Meaning |
|---|---|
DISABLED |
Radio path stopped; not a protocol turn. |
RADIO Peripheral Events¶
Reference: nRF RADIO peripheral docs
| Event | Meaning |
|---|---|
ADDRESS |
Address field sent or matched |
PHYEND |
Full packet (including CRC) transmitted or received |
CRCOK |
Received packet CRC matched |
CRCERROR |
Received packet CRC failed |
Handled inside radio_isr.
Session Events¶
radio_core emits one ordered event stream to session.c.
| Event | Tick | Notes |
|---|---|---|
RX_OK |
RX PHYEND |
RSSI valid. |
RX_BAD |
RX PHYEND |
RSSI valid. |
TX_END |
TX PHYEND |
TX already ended. |
RX_INCOMPLETE |
deadline | RX packet did not finish in time. |
LISTEN_TIMEOUT |
deadline | No peer ADDRESS before deadline. |
TX_TRIGGER_FAILED |
deadline | Timeout TX could not be triggered. |
Packet Format¶
struct prop_packet {
uint8_t length;
uint16_t seq;
uint8_t payload[PROP_PAYLOAD_MAX_LEN];
} __packed __aligned(4);
- packets never use
seq = PROP_KEEPALIVE_SEQ = UINT16_MAXunless they are keepalive packets. - zero-length app TX frames are invalid
Constants:
| Constant | Value |
|---|---|
PROP_PACKET_METADATA_LEN |
2 |
PROP_PAYLOAD_MAX_LEN |
252 |
PROP_KEEPALIVE_PAYLOAD_LEN |
16 |
PROP_KEEPALIVE_SEQ |
UINT16_MAX |
Warning
Do not confuse payload inside struct prop_packet with PAYLOAD field inside nRF RADIO Peripheral docs → Packet configuration
Buffers¶
radio_core.c owns the packet buffers. PACKETPTR always points to the
current read/write slot in these rings. The RADIO peripheral DMA transfers
directly to/from the pointed-to ring entry.
Ring ownership:
| Ring index | Writer |
|---|---|
| TX write | session thread |
| TX read | radio ISR |
| RX write | radio ISR |
| RX read | session thread |
Rings are Single Producer Single Consumer (SPSC). One slot remains unused to distinguish full from empty.
TX ring rules:
- TX ring contains payload packets only.
- Keepalives are never queued in the TX ring.
- Session gets a pointer to the next TX slot via
prop_radio_tx_get_wr_ptr(), copies into it, then commits withprop_radio_tx_advance_wr_idx(). - Session-thread does not touch
PACKETPTR.
RX ring rules:
- RADIO DMA writes to
rx_ring[rx_wr_idx]. CRCOK/CRCERRORqueues a session event.- RX write index advances only if the RX ring has room and event queueing succeeds.
- Session releases a consumed RX slot with
prop_radio_rx_advance_rd_idx().
RADIO Shortcuts¶
Reference: nRF RADIO peripheral docs → SHORTS register
Base shorts (always active):
READY_START— READY → START: begin TX/RX as soon as ramp-up completes.PHYEND_DISABLE— PHYEND → DISABLE: shut down radio after every packet.ADDRESS_RSSISTART— ADDRESS → RSSISTART: sample signal strength on peer packet arrival.
Turn-selection shorts (one active at a time):
DISABLED_RXEN— DISABLED → RXEN: after disable, ramp back into RX.DISABLED_TXEN— DISABLED → TXEN: after disable, ramp into TX.
PHYEND_DISABLE + a turn-selection short chains the whole turnaround in hardware:
packet ends → radio disables → radio re-enables in the next direction, no ISR needed.
The active shortcut set prepares the next direction:
- while receiving:
BASE | DISABLED_TXEN(turnaround into TX) - while transmitting:
BASE | DISABLED_RXEN(turnaround into RX)
PACKETPTR and Shortcut Programming¶
PACKETPTR and shortcuts are always set together. Programming an RX pointer selects DISABLED_TXEN, programming a TX pointer selects DISABLED_RXEN.
RX programming:
TX programming:
Programming points:
| Point | Action |
|---|---|
prop_radio_start() |
Program initial RX pointer. |
ADDRESS while LISTEN |
Program next TX pointer. |
LISTEN/IN_RX timeout |
Program TX pointer directly before switchover to TX. |
ADDRESS while IN_TX |
Program next RX pointer. |
Deadlines¶
Timer:
- TIMER10 runs at 1 MHz.
- CC[2] captures
ADDRESS. - CC[3] is the semantic deadline compare.
- CC[4] captures
PHYEND.
Constants:
| Constant | Meaning |
|---|---|
PROP_RADIO_LISTEN_TIMEOUT_BASE_US |
Fixed listen window before timeout. |
PROP_RADIO_LISTEN_TIMEOUT_JITTER_US |
Max random offset added to listen timeout. Determined by xorshift PRNG seeded from device ID and cycle counter. |
PROP_RADIO_MAX_PACKET_AIRTIME_US |
Worst-case packet duration on air. |
PROP_RADIO_DEADLINE_MIN_LEAD_US |
Minimum time a deadline must be in the future. |
Deadline rules:
| Entry | Deadline |
|---|---|
start → LISTEN |
now + BASE_US + jitter |
ADDRESS → IN_RX |
address_tick + MAX_PACKET_AIRTIME_US |
TX PHYEND → LISTEN |
tx_phyend_tick + MAX_PACKET_AIRTIME_US + jitter |
If a deadline is too close, radio_core clamps it forward by
DEADLINE_MIN_LEAD_US and increments deadline_late_count.
TX ring publication never changes deadlines.
State Transitions¶
LISTEN → IN_RX¶
Trigger: RADIO ADDRESS.
Actions:
- set
turn_state = IN_RX - arm RX deadline
- program TX
PACKETPTRto TX ring head or keepalive
LISTEN → IN_TX¶
Trigger: listen timeout deadline.
Actions:
- queue
LISTEN_TIMEOUT - program TX
PACKETPTRto TX ring head or keepalive - set shorts to
DISABLED_TXEN - clear pending
ADDRESS, RX ADDRESS could arrive between timeout andDISABLE - set
turn_state = IN_TX - trigger RADIO
DISABLE - if trigger fails, queue
TX_TRIGGER_FAILED
IN_RX → IN_TX¶
Triggers:
- RX
PHYEND, afterCRCOK/CRCERROR - RX timeout deadline
RX PHYEND actions:
CRCOKqueuesRX_OK;CRCERRORqueuesRX_BAD- RX write index advances if event queueing succeeds
PHYENDhandler setsturn_state = IN_TX- hardware short starts TX
RX timeout deadline actions:
- queue
RX_INCOMPLETE - program TX
PACKETPTRto TX ring head or keepalive - set shorts to
DISABLED_TXEN - clear pending
ADDRESS, RX ADDRESS could arrive between timeout andDISABLE - set
turn_state = IN_TX - trigger RADIO
DISABLE - if trigger fails, queue
TX_TRIGGER_FAILED
IN_TX → LISTEN¶
Trigger: TX PHYEND.
Actions:
- advance TX read index only if the transmitted packet came from TX ring buffer
- set
turn_state = LISTEN - arm post-TX listen deadline
- increment TX stats
- queue
TX_END
TX_END is queued from PHYEND, not from DISABLED. The next RX pointer was
already programmed by TX ADDRESS.
Session Handling¶
RX_OK:
- read RX packet from RX ring
- reject malformed length and advance RX read index (early return)
- reset
consecutive_rx_misses - enter
IN_SERVICE - if keepalive marker: no app frame, no seq accounting
- if payload: queue app RX frame and update seq/loss accounting
- advance RX read index
RX_BAD:
- reset
consecutive_rx_misses - advance RX ring buffer read index
- do not enter service from
NO_SERVICE
RX_INCOMPLETE:
- increment
rx_incomplete_count - reset
consecutive_rx_misses
LISTEN_TIMEOUT:
- if
IN_SERVICE, incrementconsecutive_rx_misses - if threshold reached, mark
NO_SERVICE - if already
NO_SERVICE, log rate-limited waiting warning
TX_END:
- refill TX ring with queued payloads until full or app TX queue empty
TX_TRIGGER_FAILED:
- increment
tx_trigger_fail_count - mark
NO_SERVICE - log error
- stop radio path
Service State¶
States:
NO_SERVICEIN_SERVICE
Enter service:
- valid
RX_OKpacket, including keepalive
Stay in service:
RX_OK,RX_BAD, andRX_INCOMPLETEall reset missesRX_BADandRX_INCOMPLETEreset misses but do not enter service on their own
Leave service:
- only consecutive
LISTEN_TIMEOUTevents count as missed peer activity - threshold is
PROP_SESSION_SYNC_LOSS_TURNS
On service loss:
- increment outage counter if previously in service
- reset miss counter
- clear in-service timestamp
- clear RX sequence continuity
- do not reset radio turn state
Responsibilities¶
radio_core owns:
- RADIO/TIMER configuration
- turn state
- deadlines and jitter
PACKETPTRand shorts- RX/TX rings
- keepalive fallback packet
- session event queue
- hardware stats
session.c owns:
- service state
- app TX/RX queues
- payload copy into TX ring
- payload sequence/loss accounting
- user-visible session stats/logs
Invariants¶
- Both roles use identical timing logic.
PHYENDanchors packet end.ADDRESSis the only peer packet start gate.FRAMESTARTandSYNCare not protocol inputs.session.cdoes not own radio turn state.- Session events do not carry packet payloads.
- TX ring entries are payload-only.
- Empty TX ring means transmit
keepalive_packet. - Keepalive does not consume payload sequence numbers.
- Payload sequence generation skips
PROP_KEEPALIVE_SEQ. - Session-thread TX publication does not program
PACKETPTR. PACKETPTRand turn-selection shortcut are always programmed together.ADDRESSmust program the next-directionPACKETPTRbeforePHYENDfires.- Only
RX_OKcan enter service;RX_BADandRX_INCOMPLETEcannot. - Listen timeout is a hard turn boundary.
- Stop purges stale session events and leaves radio state disabled.