NRF24L01 Diagnostic Software. Testing

NRF24L01 The Little Module That Finally Does

NRF24L01 Diagnostic Software. Testing
We finally start to learn these 24NRF24L01

There was a LOT of challenges with these modules pre-LLM days.  You could hardly find examples that worked decently, they were ESD sensitive to electrostatic shocks, and they presented a lot of other challenges such as when the transmitted they would spike the power and trip it in to a locked up mode.  Overall people didn't give the greatest reviews.

NRF24L01 Product Specification 2.0

Nordic Semiconductor NRF24L01

NRF24L01 Product Specification 2.0

PLEASE NOTE:

  • You must have your SPI enabled, by default it may show itself as off and you will need to check your /boot/firmware/config.txt and make sure that dtparam=spi=on  Then reboot!
  • We are connecting these to a Raspberry Pi Zero.
# Uncomment some or all of these to enable the optional hardware interfaces
dtparam=i2c_arm=off 
#dtparam=i2s=on
dtparam=spi=on 
  • We did get this working at the VERY end of this long guide, it can be used as a VERY quick checking tool for diagnosing NRF24L01s that might work or not work off Amazon / Ebay. Please note even touching these sensitive modules might zap them!
  • Find a module with a HAT so it's wiring is easier and the capacitor  power-buffer is already done for you.
  • We took a picture of it because this stuff constantly comes and goes.
  • The hat is a game changer, once you are more comfortable working with these you can forgo it.
  • Software.  This is where the LLM saved us DAYS.  I want to mention that you can actually get a completely free and powerful LLM if you look around openrouter.ai and see what is available.  At the time of this writing Google has found 6x compression on their LLM and this will completley change again the capabilities of what they can do.
  • With my Grok 4 here is what it gave me.  Please note Grok 4 at the time of this writing will forget every time to include stdint.h just for your reference we had to add it into the code
#include <stdint.h>
  • To whit here is the complete guide for your benefit. I can recall fumbling around for weeks.  Now I did this in about 30 minutes. Things are changing - fast!

Hardware Connection: Raspberry Pi Zero to nRF24L01+ via SPI

The nRF24L01+ module must be powered exclusively from 3.3 V (never 5 V). The Raspberry Pi Zero uses the same 40-pin GPIO header as other Raspberry Pi models, so the wiring below is fully compatible. The SPI interface uses hardware SPI0 (bus 0, chip-select 0 on GPIO 8).

Raspberry Pi Zero Physical Pin BCM GPIO nRF24L01+ Pin Function Notes
1 or 17 2 (VCC) +3.3 V power Stable 3.3 V supply required
6, 9, 14, 20, 25, or 30 1 (GND) Ground Common ground
19 10 6 (MOSI) SPI Master-Out-Slave-In
21 9 7 (MISO) SPI Master-In-Slave-Out
23 11 5 (SCK) SPI Clock
24 8 4 (CSN) SPI Chip-Select (active low) Handled automatically by /dev/spidev0.0
22 25 3 (CE) Chip Enable (transmit/receive control) Controlled in software
(optional) 18 24 8 (IRQ) Interrupt request Not used in this example; leave unconnected

Important notes

  • Enable the SPI interface once with sudo raspi-config → Interface Options → SPI → Yes, then reboot.
  • A 10 µF electrolytic capacitor placed directly across VCC and GND on the nRF24L01+ module is strongly recommended for stability.
  • The CSN line is managed automatically by the Linux SPI driver; do not manually export or drive GPIO 8.

Complete C Program Example

The program below uses the standard Linux SPI driver (/dev/spidev0.0) and sysfs for the CE GPIO. It demonstrates:

  • Reading registers (CONFIG, STATUS, RF_CH)
  • Basic initialization (power-up, 2.4 GHz channel 76, 1 Mbps, 32-byte payload)
  • Transmit mode (send a short message)
  • Receive mode (listen and print incoming payload)

Save as nrf24_example.c and compile with:

gcc -o nrf24_example nrf24_example.c -Wall -Wextra -O2
#include <stdint.h>  // Grok 4 why do you always forget to add this!
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/spi/spidev.h>
#include <string.h>
#include <errno.h>

// Pin definitions (BCM numbering)
#define CE_GPIO   25          // Physical pin 22
#define SPI_DEVICE "/dev/spidev0.0"
#define SPI_SPEED  1000000    // 1 MHz (safe for nRF24L01+)

// nRF24L01+ commands and registers
#define R_REGISTER    0x00
#define W_REGISTER    0x20
#define R_RX_PAYLOAD  0x61
#define W_TX_PAYLOAD  0xA0
#define FLUSH_TX      0xE1
#define FLUSH_RX      0xE2
#define NOP           0xFF

#define REG_CONFIG    0x00
#define REG_STATUS    0x07
#define REG_RF_CH     0x05
#define REG_SETUP_RETR 0x04
#define REG_RF_SETUP  0x06
#define REG_RX_ADDR_P0 0x0A
#define REG_TX_ADDR   0x10

static int spi_fd = -1;
static int ce_fd = -1;

// ====================== GPIO (sysfs) helpers for CE ======================
static int gpio_export(int pin) {
    char buf[64];
    int fd = open("/sys/class/gpio/export", O_WRONLY);
    if (fd < 0) return -1;
    snprintf(buf, sizeof(buf), "%d", pin);
    write(fd, buf, strlen(buf));
    close(fd);
    return 0;
}

static int gpio_set_direction(int pin, int out) {
    char path[64];
    snprintf(path, sizeof(path), "/sys/class/gpio/gpio%d/direction", pin);
    int fd = open(path, O_WRONLY);
    if (fd < 0) return -1;
    write(fd, out ? "out" : "in", out ? 3 : 2);
    close(fd);
    return 0;
}

static int gpio_write(int pin, int value) {
    char path[64];
    snprintf(path, sizeof(path), "/sys/class/gpio/gpio%d/value", pin);
    int fd = open(path, O_WRONLY);
    if (fd < 0) return -1;
    write(fd, value ? "1" : "0", 1);
    close(fd);
    return 0;
}

// ====================== SPI helpers ======================
static int spi_init(void) {
    spi_fd = open(SPI_DEVICE, O_RDWR);
    if (spi_fd < 0) {
        perror("Failed to open SPI device");
        return -1;
    }
    uint8_t mode = SPI_MODE_0;
    uint8_t bits = 8;
    uint32_t speed = SPI_SPEED;
    ioctl(spi_fd, SPI_IOC_WR_MODE, &mode);
    ioctl(spi_fd, SPI_IOC_WR_BITS_PER_WORD, &bits);
    ioctl(spi_fd, SPI_IOC_WR_MAX_SPEED_HZ, &speed);
    return 0;
}

static void spi_transfer(const uint8_t *tx, uint8_t *rx, size_t len) {
    struct spi_ioc_transfer tr = {
        .tx_buf = (unsigned long)tx,
        .rx_buf = (unsigned long)rx,
        .len = len,
        .speed_hz = SPI_SPEED,
        .bits_per_word = 8,
        .delay_usecs = 0,
        .cs_change = 0,
    };
    ioctl(spi_fd, SPI_IOC_MESSAGE(1), &tr);
}

// ====================== nRF24L01+ low-level functions ======================
static uint8_t nrf_read_reg(uint8_t reg, uint8_t *buf, uint8_t len) {
    uint8_t tx[len + 1];
    uint8_t rx[len + 1];
    tx[0] = R_REGISTER | reg;
    memset(tx + 1, NOP, len);
    spi_transfer(tx, rx, len + 1);
    if (buf) memcpy(buf, rx + 1, len);
    return rx[0];  // status byte
}

static void nrf_write_reg(uint8_t reg, const uint8_t *buf, uint8_t len) {
    uint8_t tx[len + 1];
    uint8_t rx[len + 1];
    tx[0] = W_REGISTER | reg;
    memcpy(tx + 1, buf, len);
    spi_transfer(tx, rx, len + 1);
}

static void nrf_send_command(uint8_t cmd) {
    uint8_t tx[1] = {cmd};
    uint8_t rx[1];
    spi_transfer(tx, rx, 1);
}

// ====================== nRF24L01+ initialization ======================
static void nrf_init(void) {
    // Power down first
    gpio_write(CE_GPIO, 0);

    // CONFIG: PWR_UP = 1, PRIM_RX = 0 (TX mode initially)
    uint8_t config = 0x0E;                // CRC enabled, 2-byte CRC, PWR_UP
    nrf_write_reg(REG_CONFIG, &config, 1);

    usleep(2000);                         // Wait for power-up (≥1.5 ms)

    // RF channel 76 (2.476 GHz)
    uint8_t ch = 76;
    nrf_write_reg(REG_RF_CH, &ch, 1);

    // Data rate 1 Mbps, 0 dBm
    uint8_t rf_setup = 0x06;
    nrf_write_reg(REG_RF_SETUP, &rf_setup, 1);

    // Auto-retransmit
    uint8_t retr = 0x1F;                  // 15 retries, 500 µs delay
    nrf_write_reg(REG_SETUP_RETR, &retr, 1);

    // Set same address for TX and RX pipe 0 (example address)
    uint8_t addr[5] = {0xE7, 0xE7, 0xE7, 0xE7, 0xE7};
    nrf_write_reg(REG_TX_ADDR, addr, 5);
    nrf_write_reg(REG_RX_ADDR_P0, addr, 5);

    // 32-byte payload
    uint8_t payload_width = 32;
    nrf_write_reg(0x11, &payload_width, 1);  // REG_RX_PW_P0

    printf("nRF24L01+ initialized. CONFIG register = 0x%02X\n",
           nrf_read_reg(REG_CONFIG, NULL, 1));
}

// ====================== Transmit ======================
static void nrf_transmit(const uint8_t *data, uint8_t len) {
    gpio_write(CE_GPIO, 0);               // Standby
    nrf_send_command(FLUSH_TX);
    uint8_t tx_buf[len + 1];
    tx_buf[0] = W_TX_PAYLOAD;
    memcpy(tx_buf + 1, data, len);
    uint8_t rx_buf[len + 1];
    spi_transfer(tx_buf, rx_buf, len + 1);

    gpio_write(CE_GPIO, 1);               // Start transmission
    usleep(1000);                         // Pulse CE ≥10 µs
    gpio_write(CE_GPIO, 0);

    // Wait for transmission complete
    uint8_t status;
    do {
        status = nrf_read_reg(REG_STATUS, NULL, 1);
        usleep(1000);
    } while ((status & 0x20) == 0);       // TX_DS bit

    nrf_write_reg(REG_STATUS, (uint8_t[]){0x20}, 1);  // Clear TX_DS
    printf("Message transmitted.\n");
}

// ====================== Receive ======================
static void nrf_receive(void) {
    gpio_write(CE_GPIO, 0);
    uint8_t config = 0x0F;                // PRIM_RX = 1
    nrf_write_reg(REG_CONFIG, &config, 1);
    nrf_send_command(FLUSH_RX);
    gpio_write(CE_GPIO, 1);               // Enter RX mode

    printf("Listening for data (press Ctrl+C to stop)...\n");
    while (1) {
        uint8_t status = nrf_read_reg(REG_STATUS, NULL, 1);
        if (status & 0x40) {              // RX_DR bit set
            uint8_t payload[32];
            uint8_t tx[33] = {R_RX_PAYLOAD};
            uint8_t rx[33];
            spi_transfer(tx, rx, 33);
            memcpy(payload, rx + 1, 32);

            printf("Received: ");
            for (int i = 0; i < 32; i++) printf("%02X ", payload[i]);
            printf("\n");

            nrf_write_reg(REG_STATUS, (uint8_t[]){0x40}, 1);  // Clear RX_DR
        }
        usleep(10000);
    }
}

int main(int argc, char *argv[]) {
    if (gpio_export(CE_GPIO) < 0 && errno != EBUSY) {
        perror("Failed to export CE GPIO");
        return 1;
    }
    gpio_set_direction(CE_GPIO, 1);
    gpio_write(CE_GPIO, 0);

    if (spi_init() < 0) return 1;

    nrf_init();

    // Demonstrate register reads
    uint8_t status, rf_ch;
    nrf_read_reg(REG_STATUS, &status, 1);
    nrf_read_reg(REG_RF_CH, &rf_ch, 1);
    printf("STATUS = 0x%02X, RF_CH = %d\n", status, rf_ch);

    if (argc > 1 && strcmp(argv[1], "rx") == 0) {
        nrf_receive();                    // Receiver mode
    } else {
        // Transmitter example
        uint8_t msg[32] = "Hello from Pi Zero!";
        nrf_transmit(msg, 32);
        printf("Sent test message. Run again with 'rx' argument to listen.\n");
    }

    close(spi_fd);
    return 0;
}

Usage

  • Transmitter (one-time send): ./nrf24_example
  • Receiver (continuous listen): ./nrf24_example rx

Run the receiver on one Pi Zero and the transmitter on another (or on an Arduino with a matching nRF24L01+ sketch). The program is intentionally minimal yet fully functional for send/receive and register access. For production use, replace sysfs GPIO with libgpiod or pigpio for better performance.

A Diagnostic Module!

This module can let you examine all the registers for the status of your NRF24L01!

'r' - Rerun register read.

'w' - Watch registers every 10 seconds

'q' - Quit!

Hardware Connection
The wiring between the Raspberry Pi Zero and the nRF24L01+ module remains unchanged from the previous example (SPI0 on GPIO 8/9/10/11, CE on GPIO 25, 3.3 V power, common ground). A 10 µF capacitor across VCC–GND on the nRF24L01+ module is still strongly recommended.

Amended C Program

The program has been completely updated as requested:

  • It reads every nRF24L01+ register (0x00–0x1D).
  • All register values are decoded with meaningful bit-field explanations and displayed in clean, formatted tables.
  • Interactive commands are supported:
  • r – Refresh / read all registers again.
  • m – Enter monitor mode (automatically reprints the full table every 10 seconds).
  • q – Quit the program.
  • The original SPI/GPIO helpers, initialization, and low-level functions are retained for full compatibility.
  • Terminal handling uses a lightweight, non-blocking kbhit() implementation so the monitor mode works smoothly without requiring the user to press Enter after every key.

Save the code as nrf24_register_monitor.c and compile with:

gcc -o nrf24_register_monitor nrf24_register_monitor.c -Wall -Wextra -O2
#include <stdint.h>  // Grok always forgets this for some reason.
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/spi/spidev.h>
#include <string.h>
#include <errno.h>
#include <termios.h>
#include <sys/select.h>

// Pin definitions (BCM numbering)
#define CE_GPIO   25          // Physical pin 22
#define SPI_DEVICE "/dev/spidev0.0"
#define SPI_SPEED  1000000    // 1 MHz (safe for nRF24L01+)

// nRF24L01+ commands and registers
#define R_REGISTER    0x00
#define W_REGISTER    0x20
#define NOP           0xFF

#define REG_CONFIG      0x00
#define REG_EN_AA       0x01
#define REG_EN_RXADDR   0x02
#define REG_SETUP_AW    0x03
#define REG_SETUP_RETR  0x04
#define REG_RF_CH       0x05
#define REG_RF_SETUP    0x06
#define REG_STATUS      0x07
#define REG_OBSERVE_TX  0x08
#define REG_RPD         0x09
#define REG_RX_ADDR_P0  0x0A
#define REG_RX_ADDR_P1  0x0B
#define REG_RX_ADDR_P2  0x0C
#define REG_RX_ADDR_P3  0x0D
#define REG_RX_ADDR_P4  0x0E
#define REG_RX_ADDR_P5  0x0F
#define REG_TX_ADDR     0x10
#define REG_RX_PW_P0    0x11
#define REG_RX_PW_P1    0x12
#define REG_RX_PW_P2    0x13
#define REG_RX_PW_P3    0x14
#define REG_RX_PW_P4    0x15
#define REG_RX_PW_P5    0x16
#define REG_FIFO_STATUS 0x17
#define REG_DYNPD       0x1C
#define REG_FEATURE     0x1D

static int spi_fd = -1;

// ====================== GPIO (sysfs) helpers for CE ======================
static int gpio_export(int pin) {
    char buf[64];
    int fd = open("/sys/class/gpio/export", O_WRONLY);
    if (fd < 0) return -1;
    snprintf(buf, sizeof(buf), "%d", pin);
    write(fd, buf, strlen(buf));
    close(fd);
    return 0;
}

static int gpio_set_direction(int pin, int out) {
    char path[64];
    snprintf(path, sizeof(path), "/sys/class/gpio/gpio%d/direction", pin);
    int fd = open(path, O_WRONLY);
    if (fd < 0) return -1;
    write(fd, out ? "out" : "in", out ? 3 : 2);
    close(fd);
    return 0;
}

static int gpio_write(int pin, int value) {
    char path[64];
    snprintf(path, sizeof(path), "/sys/class/gpio/gpio%d/value", pin);
    int fd = open(path, O_WRONLY);
    if (fd < 0) return -1;
    write(fd, value ? "1" : "0", 1);
    close(fd);
    return 0;
}

// ====================== SPI helpers ======================
static int spi_init(void) {
    spi_fd = open(SPI_DEVICE, O_RDWR);
    if (spi_fd < 0) {
        perror("Failed to open SPI device");
        return -1;
    }
    uint8_t mode = SPI_MODE_0;
    uint8_t bits = 8;
    uint32_t speed = SPI_SPEED;
    ioctl(spi_fd, SPI_IOC_WR_MODE, &mode);
    ioctl(spi_fd, SPI_IOC_WR_BITS_PER_WORD, &bits);
    ioctl(spi_fd, SPI_IOC_WR_MAX_SPEED_HZ, &speed);
    return 0;
}

static void spi_transfer(const uint8_t *tx, uint8_t *rx, size_t len) {
    struct spi_ioc_transfer tr = {
        .tx_buf = (unsigned long)tx,
        .rx_buf = (unsigned long)rx,
        .len = len,
        .speed_hz = SPI_SPEED,
        .bits_per_word = 8,
        .delay_usecs = 0,
        .cs_change = 0,
    };
    ioctl(spi_fd, SPI_IOC_MESSAGE(1), &tr);
}

// ====================== nRF24L01+ low-level functions ======================
static uint8_t nrf_read_reg(uint8_t reg, uint8_t *buf, uint8_t len) {
    uint8_t tx[len + 1];
    uint8_t rx[len + 1];
    tx[0] = R_REGISTER | reg;
    memset(tx + 1, NOP, len);
    spi_transfer(tx, rx, len + 1);
    if (buf) memcpy(buf, rx + 1, len);
    return rx[0];  // status byte
}

static void nrf_write_reg(uint8_t reg, const uint8_t *buf, uint8_t len) {
    uint8_t tx[len + 1];
    uint8_t rx[len + 1];
    tx[0] = W_REGISTER | reg;
    memcpy(tx + 1, buf, len);
    spi_transfer(tx, rx, len + 1);
}

// ====================== FULL REGISTER DUMP WITH DECODING ======================
static void dump_all_registers(void) {
    uint8_t val;
    uint8_t addr[5];

    printf("\n=== nRF24L01+ Full Register Dump ===\n\n");

    // CONFIG (0x00)
    nrf_read_reg(REG_CONFIG, &val, 1);
    printf("0x00 CONFIG      : 0x%02X\n", val);
    printf("  PRIM_RX     : %d   (1 = PRX, 0 = PTX)\n", val & 0x01);
    printf("  PWR_UP      : %d   (Power Up)\n", (val >> 1) & 0x01);
    printf("  CRCO        : %d   (CRC length: 1 = 2 bytes, 0 = 1 byte)\n", (val >> 2) & 0x01);
    printf("  EN_CRC      : %d\n", (val >> 3) & 0x01);
    printf("  MASK_MAX_RT : %d\n", (val >> 4) & 0x01);
    printf("  MASK_TX_DS  : %d\n", (val >> 5) & 0x01);
    printf("  MASK_RX_DR  : %d\n\n", (val >> 6) & 0x01);

    // EN_AA (0x01)
    nrf_read_reg(REG_EN_AA, &val, 1);
    printf("0x01 EN_AA       : 0x%02X   ENAA_P5..P0 : %d %d %d %d %d %d\n\n",
           val, (val>>5)&1, (val>>4)&1, (val>>3)&1, (val>>2)&1, (val>>1)&1, val&1);

    // EN_RXADDR (0x02)
    nrf_read_reg(REG_EN_RXADDR, &val, 1);
    printf("0x02 EN_RXADDR   : 0x%02X   ERX_P5..P0  : %d %d %d %d %d %d\n\n",
           val, (val>>5)&1, (val>>4)&1, (val>>3)&1, (val>>2)&1, (val>>1)&1, val&1);

    // SETUP_AW (0x03)
    nrf_read_reg(REG_SETUP_AW, &val, 1);
    printf("0x03 SETUP_AW    : 0x%02X   Address width = %d bytes\n\n", val, ((val & 0x03) + 2));

    // SETUP_RETR (0x04)
    nrf_read_reg(REG_SETUP_RETR, &val, 1);
    printf("0x04 SETUP_RETR  : 0x%02X   ARD = %d µs   ARC = %d\n\n",
           val, 250 * (((val >> 4) & 0x0F) + 1), val & 0x0F);

    // RF_CH (0x05)
    nrf_read_reg(REG_RF_CH, &val, 1);
    printf("0x05 RF_CH       : 0x%02X   Channel %d (%.3f GHz)\n\n", val, val, 2.400 + val / 1000.0);

    // RF_SETUP (0x06)
    nrf_read_reg(REG_RF_SETUP, &val, 1);
    printf("0x06 RF_SETUP    : 0x%02X   Data rate: ", val);
    if ((val & 0x28) == 0x00) printf("1 Mbps");
    else if ((val & 0x28) == 0x08) printf("2 Mbps");
    else if ((val & 0x28) == 0x20) printf("250 kbps");
    printf("   RF_PWR: %d dBm\n\n", ((val >> 1) & 0x03) * 2 - 18);

    // STATUS (0x07)
    nrf_read_reg(REG_STATUS, &val, 1);
    printf("0x07 STATUS      : 0x%02X   TX_FULL:%d  MAX_RT:%d  TX_DS:%d  RX_DR:%d\n\n",
           val, (val >> 0) & 1, (val >> 4) & 1, (val >> 5) & 1, (val >> 6) & 1);

    // OBSERVE_TX (0x08)
    nrf_read_reg(REG_OBSERVE_TX, &val, 1);
    printf("0x08 OBSERVE_TX  : 0x%02X   PLOS_CNT:%d  ARC_CNT:%d\n\n",
           val, (val >> 4) & 0x0F, val & 0x0F);

    // RPD (0x09)
    nrf_read_reg(REG_RPD, &val, 1);
    printf("0x09 RPD         : 0x%02X   Received Power Detected (> –64 dBm): %d\n\n", val, val & 1);

    // Addresses
    printf("--- Addresses ---\n");
    nrf_read_reg(REG_RX_ADDR_P0, addr, 5);
    printf("0x0A RX_ADDR_P0  : %02X %02X %02X %02X %02X\n", addr[0], addr[1], addr[2], addr[3], addr[4]);
    nrf_read_reg(REG_RX_ADDR_P1, addr, 5);
    printf("0x0B RX_ADDR_P1  : %02X %02X %02X %02X %02X\n", addr[0], addr[1], addr[2], addr[3], addr[4]);
    nrf_read_reg(REG_RX_ADDR_P2, &val, 1); printf("0x0C RX_ADDR_P2  : LSB = 0x%02X\n", val);
    nrf_read_reg(REG_RX_ADDR_P3, &val, 1); printf("0x0D RX_ADDR_P3  : LSB = 0x%02X\n", val);
    nrf_read_reg(REG_RX_ADDR_P4, &val, 1); printf("0x0E RX_ADDR_P4  : LSB = 0x%02X\n", val);
    nrf_read_reg(REG_RX_ADDR_P5, &val, 1); printf("0x0F RX_ADDR_P5  : LSB = 0x%02X\n", val);
    nrf_read_reg(REG_TX_ADDR, addr, 5);
    printf("0x10 TX_ADDR     : %02X %02X %02X %02X %02X\n\n", addr[0], addr[1], addr[2], addr[3], addr[4]);

    // RX payload widths
    printf("--- Payload Widths ---\n");
    for (int i = 0; i < 6; i++) {
        nrf_read_reg(REG_RX_PW_P0 + i, &val, 1);
        printf("0x%02X RX_PW_P%d   : %d bytes\n", REG_RX_PW_P0 + i, i, val);
    }
    printf("\n");

    // FIFO_STATUS (0x17)
    nrf_read_reg(REG_FIFO_STATUS, &val, 1);
    printf("0x17 FIFO_STATUS : 0x%02X   TX_FULL:%d  TX_EMPTY:%d  RX_FULL:%d  RX_EMPTY:%d\n\n",
           val, (val >> 5) & 1, (val >> 4) & 1, (val >> 1) & 1, val & 1);

    // DYNPD (0x1C) & FEATURE (0x1D)
    nrf_read_reg(REG_DYNPD, &val, 1);
    printf("0x1C DYNPD       : 0x%02X\n", val);
    nrf_read_reg(REG_FEATURE, &val, 1);
    printf("0x1D FEATURE     : 0x%02X   EN_DPL:%d  EN_ACK_PAY:%d  EN_DYN_ACK:%d\n\n",
           val, (val >> 2) & 1, (val >> 1) & 1, val & 1);

    printf("============================================================\n");
}

// ====================== Non-blocking keyboard check ======================
static int kbhit(void) {
    struct timeval tv = {0L, 0L};
    fd_set fds;
    FD_ZERO(&fds);
    FD_SET(STDIN_FILENO, &fds);
    return select(STDIN_FILENO + 1, &fds, NULL, NULL, &tv);
}

// ====================== Monitor mode ======================
static void monitor_mode(void) {
    printf("\n=== MONITOR MODE (updates every 10 seconds) ===\n");
    printf("Press 'q' (or 'Q') to exit monitor mode...\n\n");

    while (1) {
        dump_all_registers();
        fflush(stdout);

        // Check for 'q' every second (max 1 s latency)
        for (int i = 0; i < 10; i++) {
            sleep(1);
            if (kbhit()) {
                char c = getchar();
                if (c == 'q' || c == 'Q') {
                    printf("\nExiting monitor mode.\n");
                    return;
                }
            }
        }
    }
}

// ====================== MAIN ======================
int main(void) {
    // GPIO & SPI setup
    if (gpio_export(CE_GPIO) < 0 && errno != EBUSY) {
        perror("Failed to export CE GPIO");
        return 1;
    }
    gpio_set_direction(CE_GPIO, 1);
    gpio_write(CE_GPIO, 0);

    if (spi_init() < 0) return 1;

    // Basic power-up (same as original nrf_init)
    uint8_t config = 0x0E;                // PWR_UP = 1, CRC enabled
    nrf_write_reg(REG_CONFIG, &config, 1);
    usleep(2000);

    printf("nRF24L01+ Register Monitor started.\n");

    char cmd;
    while (1) {
        dump_all_registers();

        printf("\nCommand: [r] Refresh   [m] Monitor every 10 s   [q] Quit\n");
        printf("Enter choice: ");
        if (scanf(" %c", &cmd) != 1) break;

        if (cmd == 'q' || cmd == 'Q') break;
        if (cmd == 'm' || cmd == 'M') {
            monitor_mode();
        }
        // 'r' simply loops and reprints the table
    }

    close(spi_fd);
    gpio_write(CE_GPIO, 0);
    printf("\nProgram terminated.\n");
    return 0;
}

Usage

  • Run ./nrf24_register_monitor
  • The program will display the complete decoded register table immediately.
  • Press r + Enter to refresh.
  • Press m + Enter to enter monitor mode (auto-refresh every 10 s).
  • Press q + Enter to quit (works inside monitor mode as well).

The code is fully self-contained, compiles without external libraries beyond the standard Linux headers, and provides the exact functionality you requested.

The first time it worked it made a beautiful diagnostic that showed the module was not configured properly:

=== nRF24L01+ Full Register Dump ===

0x00 CONFIG      : 0x00
  PRIM_RX     : 0   (1 = PRX, 0 = PTX)
  PWR_UP      : 0   (Power Up)
  CRCO        : 0   (CRC length: 1 = 2 bytes, 0 = 1 byte)
  EN_CRC      : 0
  MASK_MAX_RT : 0
  MASK_TX_DS  : 0
  MASK_RX_DR  : 0

0x01 EN_AA       : 0x00   ENAA_P5..P0 : 0 0 0 0 0 0

0x02 EN_RXADDR   : 0x00   ERX_P5..P0  : 0 0 0 0 0 0

0x03 SETUP_AW    : 0x00   Address width = 2 bytes

0x04 SETUP_RETR  : 0x00   ARD = 250 µs   ARC = 0

0x05 RF_CH       : 0x00   Channel 0 (2.400 GHz)

0x06 RF_SETUP    : 0x00   Data rate: 1 Mbps   RF_PWR: -18 dBm

0x07 STATUS      : 0x00   TX_FULL:0  MAX_RT:0  TX_DS:0  RX_DR:0

0x08 OBSERVE_TX  : 0x00   PLOS_CNT:0  ARC_CNT:0

0x09 RPD         : 0x00   Received Power Detected (> –64 dBm): 0

--- Addresses ---
0x0A RX_ADDR_P0  : 00 00 00 00 00
0x0B RX_ADDR_P1  : 00 00 00 00 00
0x0C RX_ADDR_P2  : LSB = 0x00
0x0D RX_ADDR_P3  : LSB = 0x00
0x0E RX_ADDR_P4  : LSB = 0x00
0x0F RX_ADDR_P5  : LSB = 0x00
0x10 TX_ADDR     : 00 00 00 00 00

--- Payload Widths ---
0x11 RX_PW_P0   : 0 bytes
0x12 RX_PW_P1   : 0 bytes
0x13 RX_PW_P2   : 0 bytes
0x14 RX_PW_P3   : 0 bytes
0x15 RX_PW_P4   : 0 bytes
0x16 RX_PW_P5   : 0 bytes

0x17 FIFO_STATUS : 0x00   TX_FULL:0  TX_EMPTY:0  RX_FULL:0  RX_EMPTY:0

0x1C DYNPD       : 0x00
0x1D FEATURE     : 0x00   EN_DPL:0  EN_ACK_PAY:0  EN_DYN_ACK:0

============================================================

Our next guide  - debugging the SPI bus!!!

Further diagnostics continued...

Diagnosis: All Registers Reading 0x00

This symptom almost always indicates a complete failure of SPI communication between the Raspberry Pi Zero and the nRF24L01+ module. The MISO line is effectively stuck at logic low, so every byte returned by the chip is 0x00. The nRF24L01+ STATUS register (returned as the first byte of every SPI transaction) should never read 0x00 after power-up; a typical value is 0x0E (or higher). The fact that you are seeing zeros means the module is not responding at all.

Step 1: Run These Diagnostic Commands on Your Pi Zero (Immediately)

Open a terminal and execute the following commands exactly as shown. Copy and paste the output of each into your next message so I can analyse it.

ls -l /dev/spi*
echo "=== SPI kernel messages ==="
dmesg | grep -i spi
echo "=== SPI interface status ==="
lsmod | grep spi
echo "=== GPIO status (check CE pin) ==="
gpio readall 2>/dev/null || echo "gpio command not installed (run: sudo apt install wiringpi)"
cat /sys/class/gpio/gpio25/value 2>/dev/null || echo "CE GPIO25 not exported"
echo "=== Power check (3.3V rail) ==="
vcgencmd measure_volts core

Also run:

sudo raspi-config nonint get_spi   # should return 0 (enabled)

Step 2: Verify Wiring (Most Common Cause)

Double-check every connection against this exact table. Even one swapped wire (especially MISO/MOSI) will produce all zeros.

Raspberry Pi Zero Pin BCM GPIO nRF24L01+ Pin Signal Must be connected?
1 or 17 2 (VCC) +3.3 V Yes (never 5 V)
6, 9, 14, 20, 25, 30 1 (GND) GND Yes
19 10 6 (MOSI) MOSI Yes
21 9 7 (MISO) MISO Yes
23 11 5 (SCK) SCK Yes
24 8 4 (CSN) CSN Yes
22 25 3 (CE) CE Yes

Critical notes

  • A 10 µF electrolytic capacitor must be soldered directly across VCC and GND pins on the nRF24L01+ module (as close as possible). Lack of this capacitor is a very frequent cause of intermittent or zero-response behaviour.
  • Ensure the Pi is supplying a stable 3.3 V (the module draws up to 13 mA in TX; the Pi’s 3.3 V rail is limited).

Step 3: New Minimal SPI Diagnostic Program

I have prepared a stripped-down test program that only attempts to read the STATUS register (and a NOP command) 20 times. It prints the raw bytes received so we can see exactly what the SPI bus is returning. This will confirm whether the hardware link is alive.

Save the code below as nrf24_spi_test.c and compile it with:

gcc -o nrf24_spi_test nrf24_spi_test.c -Wall -Wextra -O2
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/spi/spidev.h>
#include <string.h>

#define SPI_DEVICE "/dev/spidev0.0"
#define SPI_SPEED  1000000    // 1 MHz – safe default

static int spi_fd = -1;

static int spi_init(void) {
    spi_fd = open(SPI_DEVICE, O_RDWR);
    if (spi_fd < 0) {
        perror("Failed to open SPI device");
        return -1;
    }
    uint8_t mode = SPI_MODE_0;
    uint8_t bits = 8;
    uint32_t speed = SPI_SPEED;
    ioctl(spi_fd, SPI_IOC_WR_MODE, &mode);
    ioctl(spi_fd, SPI_IOC_WR_BITS_PER_WORD, &bits);
    ioctl(spi_fd, SPI_IOC_WR_MAX_SPEED_HZ, &speed);
    return 0;
}

static void spi_transfer(const uint8_t *tx, uint8_t *rx, size_t len) {
    struct spi_ioc_transfer tr = {
        .tx_buf = (unsigned long)tx,
        .rx_buf = (unsigned long)rx,
        .len = len,
        .speed_hz = SPI_SPEED,
        .bits_per_word = 8,
        .delay_usecs = 0,
        .cs_change = 0,
    };
    ioctl(spi_fd, SPI_IOC_MESSAGE(1), &tr);
}

int main(void) {
    if (spi_init() < 0) return 1;

    printf("=== nRF24L01+ SPI Link Diagnostic ===\n");
    printf("Reading STATUS register and NOP 20 times...\n\n");

    for (int i = 0; i < 20; i++) {
        uint8_t tx[2] = {0x07, 0xFF};  // R_REGISTER | STATUS, then NOP
        uint8_t rx[2] = {0};
        spi_transfer(tx, rx, 2);

        printf("Trial %2d:  STATUS = 0x%02X   NOP response = 0x%02X\n",
               i+1, rx[0], rx[1]);
    }

    printf("\nInterpretation:\n");
    printf("• If every STATUS byte is 0x00 → SPI link is completely dead (wiring/power/SPI disabled).\n");
    printf("• If STATUS is 0x0E or higher → SPI works; problem is elsewhere (re-run full monitor).\n");

    close(spi_fd);
    return 0;
}

Run it with:

sudo ./nrf24_spi_test

Please run the commands in Step 1 first, then this test program, and reply with the full output. Once I see the results I will give you the exact fix (it is almost always one of the items above).

In the meantime, also try adding the 10 µF capacitor if it is not already present — this alone resolves the issue for many users.

If SPI0 is failing. Here is How to move it to SPI1

The issue you identified is critical: SCLK (clock signal) is not toggling on the logic analyzer. Without a proper clock, the nRF24L01+ cannot interpret any SPI commands, which explains why MISO remains constantly low (all zeros).

Why SCLK Is Not Working

  • On the Raspberry Pi Zero, hardware SPI0 (the default /dev/spidev0.0) fixes SCLK to GPIO 11 (physical pin 23).
  • You cannot reassign the SCLK pin for SPI0 — the hardware does not allow it.
  • Since you bypassed the wiring on SCLK and still see no activity, the problem is likely one of the following:
  1. The SPI0 interface is not properly enabled in the device tree.
  2. A conflict with another GPIO function or overlay on pin 23.
  3. Hardware damage or a faulty Pi Zero GPIO pin (less common but possible on older boards).

We will move to SPI1, which uses completely different pins. This avoids the problematic GPIO 11 entirely.

New Pin Connections (SPI1)

Update your wiring as follows (all other connections — VCC 3.3V, GND, and CE on GPIO 25 — remain the same):

Raspberry Pi Zero Physical Pin BCM GPIO nRF24L01+ Pin Signal Notes
40 21 5 (SCK) SCLK New clock pin
38 20 6 (MOSI) MOSI New MOSI
35 19 7 (MISO) MISO New MISO
12 18 4 (CSN) CSN New chip select (SPI1 CE0)
22 25 3 (CE) CE Unchanged (GPIO control)
1 or 17 2 (VCC) +3.3 V Unchanged
Any GND pin 1 (GND) GND Unchanged

Important: Keep the 10 µF (or larger) decoupling capacitor directly across the nRF24L01+ VCC and GND pins.

Configuration Steps

Edit /boot/config.txt (use sudo nano /boot/config.txt):

  • Comment out or remove any existing dtparam=spi=on line.
  • Add the following line at the end:
dtoverlay=spi1-1cs
  • Save and reboot the Pi Zero.

After reboot, verify the new SPI device appears:

ls -l /dev/spi*

You should see /dev/spidev1.0.

Updated Diagnostic Program for SPI1

Here is the modified minimal test program using the new SPI1 bus (/dev/spidev1.0). Save as nrf24_spi1_test.c and compile with:

gcc -o nrf24_spi1_test nrf24_spi1_test.c -Wall -Wextra -O2
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/spi/spidev.h>
#include <string.h>

#define SPI_DEVICE "/dev/spidev1.0"
#define SPI_SPEED  1000000    // 1 MHz

static int spi_fd = -1;

static int spi_init(void) {
    spi_fd = open(SPI_DEVICE, O_RDWR);
    if (spi_fd < 0) {
        perror("Failed to open SPI1 device");
        return -1;
    }
    uint8_t mode = SPI_MODE_0;
    uint8_t bits = 8;
    uint32_t speed = SPI_SPEED;
    ioctl(spi_fd, SPI_IOC_WR_MODE, &mode);
    ioctl(spi_fd, SPI_IOC_WR_BITS_PER_WORD, &bits);
    ioctl(spi_fd, SPI_IOC_WR_MAX_SPEED_HZ, &speed);
    return 0;
}

static void spi_transfer(const uint8_t *tx, uint8_t *rx, size_t len) {
    struct spi_ioc_transfer tr = {
        .tx_buf = (unsigned long)tx,
        .rx_buf = (unsigned long)rx,
        .len = len,
        .speed_hz = SPI_SPEED,
        .bits_per_word = 8,
        .delay_usecs = 0,
        .cs_change = 0,
    };
    ioctl(spi_fd, SPI_IOC_MESSAGE(1), &tr);
}

int main(void) {
    if (spi_init() < 0) return 1;

    printf("=== nRF24L01+ SPI1 Link Diagnostic ===\n");
    printf("Reading STATUS register 20 times on SPI1...\n\n");

    for (int i = 0; i < 20; i++) {
        uint8_t tx[2] = {0x07, 0xFF};  // R_REGISTER | STATUS + NOP
        uint8_t rx[2] = {0};
        spi_transfer(tx, rx, 2);

        printf("Trial %2d:  STATUS = 0x%02X   NOP response = 0x%02X\n",
               i+1, rx[0], rx[1]);
    }

    printf("\nIf you now see non-zero values (especially STATUS around 0x0E), the link is working.\n");
    close(spi_fd);
    return 0;
}

Run it with:

sudo ./nrf24_spi1_test

Next Steps After Testing

Re-run your logic analyzer while executing the test program. You should now see clean SCLK pulses on the new pin (GPIO 21, physical pin 40).

If SCLK is now toggling and you get non-zero STATUS values, update the full register monitor program by changing:

#define SPI_DEVICE "/dev/spidev1.0"

Then recompile and run it.

If SCLK is still not toggling on the new pins, provide:

  • Output of ls -l /dev/spi*
  • Output of cat /boot/config.txt
  • Logic analyzer capture on the new SCLK pin during the test.

This change to SPI1 should resolve the clock issue. Once the basic SPI communication works, we can restore the full register decoding program on the new bus. Let me know the results of the test.

Once it all Finally Works!

We came back to this guide on Day 2, and was finally able to get it to work here is a screen shot of the setup:

  • We T-paired every single wire back to a Logic Analyzer. This really helps you see if you are driving pins, or not driving pins.  Very recommended!
  • We soldered a very large cap across the 3.3V as recommended all over (If these things are so problematic why not just build them on the board and add $1..)
Note we soldered a large cap right across the power supply. No more games! + hat used!
The hat reinforced our wiring checks. Big Tank of Cap Juice soldered in 

We can see clear signalling now on the PulseView Analyzer:

nRF24L01+ Register Monitor started.

=== nRF24L01+ Full Register Dump ===

0x00 CONFIG      : 0xC0
  PRIM_RX     : 0   (1 = PRX, 0 = PTX)
  PWR_UP      : 0   (Power Up)
  CRCO        : 0   (CRC length: 1 = 2 bytes, 0 = 1 byte)
  EN_CRC      : 0
  MASK_MAX_RT : 0
  MASK_TX_DS  : 0
  MASK_RX_DR  : 1

0x01 EN_AA       : 0xC0   ENAA_P5..P0 : 0 0 0 0 0 0

0x02 EN_RXADDR   : 0xC0   ERX_P5..P0  : 0 0 0 0 0 0

0x03 SETUP_AW    : 0xC0   Address width = 2 bytes

0x04 SETUP_RETR  : 0xC0   ARD = 3250 µs   ARC = 0

0x05 RF_CH       : 0xC0   Channel 192 (2.592 GHz)

0x06 RF_SETUP    : 0xC0   Data rate: 1 Mbps   RF_PWR: -18 dBm

0x07 STATUS      : 0x80   TX_FULL:0  MAX_RT:0  TX_DS:0  RX_DR:0

0x08 OBSERVE_TX  : 0xC0   PLOS_CNT:12  ARC_CNT:0

0x09 RPD         : 0xC0   Received Power Detected (> –64 dBm): 0

--- Addresses ---
0x0A RX_ADDR_P0  : C0 80 80 80 80
0x0B RX_ADDR_P1  : C0 80 80 80 80
0x0C RX_ADDR_P2  : LSB = 0xC0
0x0D RX_ADDR_P3  : LSB = 0xC0
0x0E RX_ADDR_P4  : LSB = 0xC0
0x0F RX_ADDR_P5  : LSB = 0x80
0x10 TX_ADDR     : C0 80 80 80 80

--- Payload Widths ---
0x11 RX_PW_P0   : 192 bytes
0x12 RX_PW_P1   : 192 bytes
0x13 RX_PW_P2   : 192 bytes
0x14 RX_PW_P3   : 192 bytes
0x15 RX_PW_P4   : 192 bytes
0x16 RX_PW_P5   : 192 bytes

0x17 FIFO_STATUS : 0x80   TX_FULL:0  TX_EMPTY:0  RX_FULL:0  RX_EMPTY:0

0x1C DYNPD       : 0xC0
0x1D FEATURE     : 0xC0   EN_DPL:0  EN_ACK_PAY:0  EN_DYN_ACK:0

============================================================

Command: [r] Refresh   [m] Monitor every 10 s   [q] Quit

What is nice about this is you can set it in monitor then simply plug and unplug each one - a quick method to verify your modules as there are a lot of cheap knockoffs that never really work floating around!

We realized this exact same setup would work for some CC1101's we had shopped, so we were looking at that next!

Linux Rocks Every Day