Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: GlasgowEmbedded/glasgow
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 4d745342d0bd
Choose a base ref
...
head repository: GlasgowEmbedded/glasgow
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 4feb9cd35136
Choose a head ref
  • 2 commits
  • 2 files changed
  • 1 contributor

Commits on Jan 26, 2020

  1. applet.radio.nrf24l01: refactor RX/TX polling loop.

    To fix several race conditions, especially in RX half.
    whitequark committed Jan 26, 2020
    Copy the full SHA
    9168528 View commit details
  2. Copy the full SHA
    4feb9cd View commit details
Showing with 197 additions and 52 deletions.
  1. +177 −52 software/glasgow/applet/radio/nrf24l01/__init__.py
  2. +20 −0 software/glasgow/arch/nrf24l/__init__.py
229 changes: 177 additions & 52 deletions software/glasgow/applet/radio/nrf24l01/__init__.py
Original file line number Diff line number Diff line change
@@ -8,6 +8,8 @@
from nmigen.compat import *

from ....support.logging import *
from ....support.bits import *
from ....arch.nrf24l import crc_nrf24l
from ....arch.nrf24l.rf import *
from ...interface.spi_master import SPIMasterSubtarget, SPIMasterInterface
from ... import *
@@ -66,6 +68,17 @@ async def read_register(self, address):
async def write_register(self, address, value):
await self.write_register_wide(address, [value])

async def poll_rx_status(self, delay=0.010):
poll_bits = clear_bits = REG_STATUS(RX_DR=1).to_int()
while True:
status_bits, _ = await self.lower.transfer([OP_W_REGISTER|ADDR_STATUS, clear_bits])
status = REG_STATUS.from_int(status_bits)
self._log("poll rx status %s", status.bits_repr(omit_zero=True))
if status_bits & poll_bits:
break
await asyncio.sleep(delay)
return status

async def read_rx_payload_length(self):
await self.lower.write([OP_R_RX_PL_WID], hold_ss=True)
length, = await self.lower.read(1)
@@ -82,6 +95,27 @@ async def flush_rx(self):
self._log("flush rx")
await self.lower.write([OP_FLUSH_RX])

async def flush_rx_all(self):
while True:
fifo_status = REG_FIFO_STATUS.from_int(
await self.read_register(ADDR_FIFO_STATUS))
if fifo_status.RX_EMPTY:
break
await self.flush_rx()

async def poll_tx_status(self, delay=0.010):
# Don't clear MAX_RT, since it prevents REUSE_TX_PL and clears ARC_CNT.
poll_bits = REG_STATUS(TX_DS=1, MAX_RT=1).to_int()
clear_bits = REG_STATUS(TX_DS=1).to_int()
while True:
status_bits, _ = await self.lower.transfer([OP_W_REGISTER|ADDR_STATUS, clear_bits])
status = REG_STATUS.from_int(status_bits)
self._log("poll rx status %s", status.bits_repr(omit_zero=True))
if status_bits & poll_bits:
break
await asyncio.sleep(delay)
return status

async def write_tx_payload(self, payload, *, ack=True):
self._log("write tx payload=<%s> ack=%s", dump_hex(payload), "yes" if ack else "no")
if ack:
@@ -97,6 +131,14 @@ async def flush_tx(self):
self._log("flush tx")
await self.lower.write([OP_FLUSH_TX])

async def flush_tx_all(self):
while True:
fifo_status = REG_FIFO_STATUS.from_int(
await self.read_register(ADDR_FIFO_STATUS))
if fifo_status.TX_EMPTY:
break
await self.flush_tx()


class RadioNRF24L01Applet(GlasgowApplet, name="radio-nrf24l"):
preview = True
@@ -106,13 +148,18 @@ class RadioNRF24L01Applet(GlasgowApplet, name="radio-nrf24l"):
Transmit and receive packets using the nRF24L01/nRF24L01+ RF PHY.
This applet allows configuring all channel and packet parameters, and provides basic transmit
and receive workflow. It supports Enhanced ShockBurst (new packet framing with automatic
transaction handling) with one pipe, as well as ShockBurst (old packet framing). It does not
support multiple pipes or acknowledgement payloads.
and receive workflow, as well as monitor mode. It supports Enhanced ShockBurst (new packet
framing with automatic transaction handling) with one pipe, as well as ShockBurst (old packet
framing). It does not support multiple pipes or acknowledgement payloads.
Note that in the CLI, the addresses are most significant byte first (the same as on-air order,
and reversed with regards to register access order.)
The `monitor` subcommand is functionally identical to the `receive` subcommand, except that
it will never attempt to acknowledge packets; this way, it is possible to watch a transaction
started by a node with a known address without disturbing either party. It is not natively
supported by nRF24L01(+), and is emulated in an imperfect way.
The pinout of a common 8-pin nRF24L01+ module is as follows (live bug view):
::
@@ -225,7 +272,7 @@ def payload(value):
p_operation = parser.add_subparsers(dest="operation", metavar="OPERATION")

p_transmit = p_operation.add_parser(
"transmit", help="transmit a packet")
"transmit", help="transmit packets")
p_transmit.add_argument(
"address", metavar="ADDRESS", type=address,
help="transmit packet with hex address ADDRESS")
@@ -245,17 +292,25 @@ def payload(value):
"-N", "--no-ack", default=False, action="store_true",
help="do not request acknowledgement (L01+ only)")

def add_rx_arguments(parser):
parser.add_argument(
"address", metavar="ADDRESS", type=address,
help="receive packet with hex address ADDRESS")
parser.add_argument(
"-l", "--length", metavar="LENGTH", type=length,
help="receive packet with length LENGTH "
"(mutually exclusive with --dynamic-length)")
parser.add_argument(
"-R", "--repeat", default=False, action="store_true",
help="keep receiving packets until interrupted")

p_receive = p_operation.add_parser(
"receive", help="receive a packet")
p_receive.add_argument(
"address", metavar="ADDRESS", type=address,
help="receive packet with hex address ADDRESS")
p_receive.add_argument(
"-l", "--length", metavar="LENGTH", type=length,
help="receive packet with length LENGTH (mutually exclusive with --dynamic-length)")
p_receive.add_argument(
"-R", "--repeat", default=False, action="store_true",
help="keep receiving packets until interrupted")
"receive", help="receive packets")
add_rx_arguments(p_receive)

p_monitor = p_operation.add_parser(
"monitor", help="monitor packets")
add_rx_arguments(p_monitor)

async def interact(self, device, args, nrf24l01_iface):
if args.crc_width == 0 and not args.compat_framing:
@@ -333,28 +388,16 @@ async def interact(self, device, args, nrf24l01_iface):
await nrf24l01_iface.write_register(ADDR_DYNPD,
REG_DYNPD(DPL_P0=1).to_int())

while True:
fifo_status = REG_FIFO_STATUS.from_int(
await nrf24l01_iface.read_register(ADDR_FIFO_STATUS))
if fifo_status.TX_EMPTY:
break
await nrf24l01_iface.flush_tx()
await nrf24l01_iface.flush_tx_all()
await nrf24l01_iface.write_register(ADDR_CONFIG,
REG_CONFIG(PRIM_RX=0, PWR_UP=1, CRCO=crco, EN_CRC=en_crc).to_int())

await nrf24l01_iface.write_tx_payload(args.payload,
ack=not args.compat_framing and not args.no_ack)

await nrf24l01_iface.write_register(ADDR_STATUS,
REG_STATUS(TX_DS=1, MAX_RT=1).to_int())
await nrf24l01_iface.enable()
try:
while True:
status = REG_STATUS.from_int(
await nrf24l01_iface.read_register(ADDR_STATUS))
if status.TX_DS or status.MAX_RT:
break
await asyncio.sleep(0.010)
status = await nrf24l01_iface.poll_tx_status()
finally:
await nrf24l01_iface.disable()

@@ -370,10 +413,22 @@ async def interact(self, device, args, nrf24l01_iface):
self.logger.error("packet lost after %d retransmits", observe_tx.ARC_CNT)
else:
self.logger.error("packet lost")
await nrf24l01_iface.write_register(ADDR_STATUS,
REG_STATUS(MAX_RT=1).to_int())

if args.operation == "receive":
if args.operation in ("receive", "monitor"):
if len(args.address) != args.address_width:
raise RadioNRF24L01Error("Length of address does not match address width")
if en_dpl:
if args.length is not None:
raise RadioNRF24L01Error(
"Either --dynamic-length or --length may be specified")
else:
if args.length is None:
raise RadioNRF24L01Error(
"One of --dynamic-length or --length must be specified")

if args.operation == "receive":
if en_aa:
await nrf24l01_iface.write_register(ADDR_EN_AA,
REG_EN_AA(ENAA_P0=1).to_int())
@@ -384,53 +439,123 @@ async def interact(self, device, args, nrf24l01_iface):
REG_EN_RXADDR(ERX_P0=1).to_int())
await nrf24l01_iface.write_register_wide(ADDR_RX_ADDR_Pn(0), args.address)
if en_dpl:
if args.length is not None:
raise RadioNRF24L01Error(
"Either --dynamic-length or --length may be specified")
await nrf24l01_iface.write_register(ADDR_DYNPD,
REG_DYNPD(DPL_P0=1).to_int())
else:
if args.length is None:
raise RadioNRF24L01Error(
"One of --dynamic-length or --length must be specified")
await nrf24l01_iface.write_register(ADDR_RX_PW_Pn(0), args.length)

while True:
fifo_status = REG_FIFO_STATUS.from_int(
await nrf24l01_iface.read_register(ADDR_FIFO_STATUS))
if fifo_status.RX_EMPTY:
break
await nrf24l01_iface.flush_rx()
await nrf24l01_iface.flush_rx_all()
await nrf24l01_iface.write_register(ADDR_CONFIG,
REG_CONFIG(PRIM_RX=1, PWR_UP=1, CRCO=crco, EN_CRC=en_crc).to_int())

await nrf24l01_iface.enable()
try:
while True:
await nrf24l01_iface.write_register(ADDR_STATUS,
REG_STATUS(RX_DR=1).to_int())
while True:
status = REG_STATUS.from_int(
await nrf24l01_iface.read_register(ADDR_STATUS))
if status.RX_DR:
assert status.RX_P_NO == 0
break
await asyncio.sleep(0.010)
status = REG_STATUS.from_int(
await nrf24l01_iface.read_register(ADDR_STATUS))
if status.RX_P_NO == 0b111:
await nrf24l01_iface.poll_rx_status()
continue

if en_dpl:
length = await nrf24l01_iface.read_rx_payload_length()
if length > 32:
self.logger.warn("corrupted packet received with length %d",
length)
await nrf24l01_iface.flush_rx()
continue
else:
length = args.length
payload = await nrf24l01_iface.read_rx_payload(length)
await nrf24l01_iface.flush_rx()

payload = await nrf24l01_iface.read_rx_payload(length)
self.logger.info("packet received: %s", dump_hex(payload))

if not args.repeat:
break
finally:
await nrf24l01_iface.disable()

if args.operation == "monitor":
if en_aa:
overhead = 2 + args.crc_width
if en_dpl:
length = 32 + overhead
else:
length = args.length + overhead
else:
length = args.length
if length > 32:
self.logger.warn("packets may be up to %d bytes long, but only %d bytes will "
"be captured", length, 32)

await nrf24l01_iface.write_register(ADDR_FEATURE,
REG_FEATURE(EN_DPL=0, EN_DYN_ACK=0).to_int())
await nrf24l01_iface.write_register(ADDR_EN_AA,
REG_EN_AA().to_int()) # disable on all pipes to release EN_CRC
await nrf24l01_iface.write_register(ADDR_EN_RXADDR,
REG_EN_RXADDR(ERX_P0=1).to_int())
await nrf24l01_iface.write_register_wide(ADDR_RX_ADDR_Pn(0), args.address)
await nrf24l01_iface.write_register(ADDR_RX_PW_Pn(0), min(32, length))

await nrf24l01_iface.flush_rx_all()
if en_aa:
await nrf24l01_iface.write_register(ADDR_CONFIG,
REG_CONFIG(PRIM_RX=1, PWR_UP=1, CRCO=CRCO._1_BYTE, EN_CRC=0).to_int())
else:
await nrf24l01_iface.write_register(ADDR_CONFIG,
REG_CONFIG(PRIM_RX=1, PWR_UP=1, CRCO=crco, EN_CRC=en_crc).to_int())

await nrf24l01_iface.enable()
try:
while True:
status = REG_STATUS.from_int(
await nrf24l01_iface.read_register(ADDR_STATUS))
if status.RX_P_NO == 0b111:
await nrf24l01_iface.poll_rx_status()
continue

payload = await nrf24l01_iface.read_rx_payload(length)
if en_aa:
dyn_length = payload[0] >> 2
packet_id = payload[0] & 0b11
no_ack = payload[1] >> 7
data_crc = bytes([
((payload[1 + n + 0] << 1) & 0b1111111_0) |
((payload[1 + n + 1] >> 7) & 0b0000000_1)
for n in range(len(payload) - 2)
])

if dyn_length == 0:
data, crc = b"", data_crc
payload_msg = "(ACK)"
elif en_dpl:
data, crc = data_crc[:dyn_length], data_crc[dyn_length:]
payload_msg = data.hex()
else:
data, crc = data_crc[:args.length], data_crc[args.length:]
payload_msg = data.hex()

if len(crc) < args.crc_width:
crc_msg = " (CRC?)"
else:
crc_actual = int.from_bytes(crc[:args.crc_width], "big")
crc_expected = crc_nrf24l(bytes(reversed(args.address)) + payload,
bits=len(args.address) * 8 + 9 + len(data) * 8)
if crc_actual != crc_expected:
crc_msg = " (CRC!)"
else:
crc_msg = ""

self.logger.info("packet received: PID=%s %s%s",
"{:02b}".format(packet_id), payload_msg, crc_msg)
else:
self.logger.info("packet received: %s", dump_hex(payload))

if not args.repeat:
break
finally:
await nrf24l01_iface.disable()

# -------------------------------------------------------------------------------------------------

class RadioNRF24L01AppletTestCase(GlasgowAppletTestCase, applet=RadioNRF24L01Applet):
20 changes: 20 additions & 0 deletions software/glasgow/arch/nrf24l/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import crcmod


# CRC for the on-air format, where packets may not be a multiple of 8 bit.
def crc_nrf24l(data, *, bits):
rem = 0xffff
for i, byte in enumerate(data):
if (i + 1) * 8 > bits:
byte &= ~((1 << (8 - bits % 8)) - 1)
rem = rem ^ (byte << 8)
for j in range(8):
if i * 8 + j == bits:
return rem
if rem & 0x8000:
rem = (rem << 1) ^ 0x1021
else:
rem = rem << 1
rem &= 0xffff
return rem