Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Pico SDK] using multiple multiple radios on a single RPi Pico board #912

Closed
2bndy5 opened this issue Aug 12, 2023 · 15 comments
Closed

[Pico SDK] using multiple multiple radios on a single RPi Pico board #912

2bndy5 opened this issue Aug 12, 2023 · 15 comments
Labels

Comments

@2bndy5
Copy link
Member

2bndy5 commented Aug 12, 2023

I did a test inspired by your documentation, but that doesn't seem to work properly on my side.

What I do/observe:

  • I compile the code and upload it on 2 rpi pico
  • each rpi pico has a module on spi0 and spi1, one module to send, one to receive
  • on one of them, I toggle the pin1 to "swap" the address for rx/tx and ensure each node can properly send/receive to each other.
  • I see sometime some messages, but like 99% of failure (nothing sent, nothing received)

My idea is to have 2 modules on a single pico to have fully bidirectional communication.

Here is my code, not doing anything smart (sending a unique id + counter to distinguish nodes and dropped packets).

example.zip

is it possible using these modules?
do I miss something with the lib?

Originally posted by @XDeschuyteneer in #892 (comment)

@2bndy5
Copy link
Member Author

2bndy5 commented Aug 12, 2023

After looking at the code, I noticed a few things that weren't mentioned in the OP:

  1. ACK payloads require dynamic payloads. This is a HW mandate not a software stipulation. Use getDynamicPayloadSize() instead of getPayloadSize() for fetching received ACK payloads.
  2. You don't need to pass the radios' CSN & CE pins to begin() if they were already specified to the RF24 c'tor. This doesn't imply any errors, but it would mean less error-prone code if the pins were only set via one RF24 API (c'tor or begin() not both).
  3. Addresses in uint64_t consume 3 bytes more than necessary. It is more memory efficient to use a byte buffer instead. Again, this doesn't imply any error, but the endianess difference between byte buffers and a uint64_t is something to beware.
  4. We haven't tested/explored the multi-processing capabilities of the RP2040. So, I'm not sure how using separate threads/cores with their own SPI bus will (or won't) work. Changing the radio's pipe addresses may require joining threads first. Further more, I'm not sure if there's a mutex API in the Pico SDK to manage thread resources.

@XDeschuyteneer
Copy link

XDeschuyteneer commented Aug 13, 2023

  1. Noted and fixed
  2. Indeed and fixed
  3. I was confused by the documentation and took a wrong shortcut void RF24::openReadingPipe(uint8_t child, uint64_t address). Thanks for pointing that.
  4. good point, not a lot of experience on the rpi pico, tried to refactor it in a single thread/single core, with the retries/overlap, it should be ok, at least for the tests.

But still not OK, here is the refactored code to work on a single core (including remarks):

#include <stdio.h>
#include <pico/stdlib.h>
#include <pico/unique_id.h>
#include "pico/multicore.h"
#include <hardware/adc.h>
#include <tusb.h>
#include <RF24.h>

const int GPIO_MODE = 1;

#define SPI0_MISO 4
#define SPI0_MOSI 3
#define SPI0_SCK 2
#define SPI0_CSN 5
#define SPI0_CE 6

#define SPI1_MISO 12
#define SPI1_MOSI 11
#define SPI1_SCK 10
#define SPI1_CSN 13
#define SPI1_CE 14

int cnt = 1;

char sid[2 * PICO_UNIQUE_BOARD_ID_SIZE_BYTES + 1] = {0};
pico_unique_board_id_t id;

SPI spi_0;
SPI spi_1;
RF24 radio_tx(SPI0_CE, SPI0_CSN);
RF24 radio_rx(SPI1_CE, SPI1_CSN);


struct address_t {
    uint8_t rx[5];
    uint8_t tx[5];
};

struct message_t {
    uint8_t cnt;
    uint8_t payload[32];
};

struct address_t address[] = {
    {.rx = {0xF0, 0xF0, 0xF0, 0xF0, 0xE1}, .tx = {0xF0, 0xF0, 0xF0, 0xF0, 0xD2}},
    {.rx = {0xF0, 0xF0, 0xF0, 0xF0, 0xD2}, .tx = {0xF0, 0xF0, 0xF0, 0xF0, 0xE1}}
};

struct message_t tx_msg = {0};
struct message_t rx_msg = {0};

void tx_action() {
    memset(&tx_msg, 0, sizeof(tx_msg));
    tx_msg.cnt = cnt;
    snprintf((char*)tx_msg.payload, sizeof(tx_msg.payload), "msg from %s", sid);
    if (radio_tx.write(&tx_msg, sizeof(tx_msg))) /* send OK */
    {
        printf("Tx (me:%s):  payload=%s cnt=%d\n", sid, tx_msg.payload, tx_msg.cnt);
        cnt++;
    }
    else {} /* send KO */
}

void tx_thread()
{
    printf("TX Thread started\n");
    while (true)
    {
        tx_action();
    }
}

void rx_action() {
    uint8_t pipe;
    if (radio_rx.available(&pipe))
    {
        memset(&rx_msg, 0, sizeof(rx_msg));
        radio_rx.read(&rx_msg, radio_rx.getDynamicPayloadSize());
        printf("Rx (me:%s): pipe=%d, payload=%s cnt=%d\n", sid, pipe, rx_msg.payload, rx_msg.cnt);
    }
}

void rx_thread()
{
    printf("RX Thread started\n");
    while (true)
    {
        rx_action();
    }
}

void rx_tx_thread()
{
    printf("RX TX Thread started\n");
    while (true)
    {
        rx_action();
        tx_action();
    }
}

void setup()
{
    uint8_t device_id = gpio_get(GPIO_MODE);

    spi_1.begin(spi1, SPI1_SCK, SPI1_MISO, SPI1_MOSI);
    while (!radio_rx.begin(&spi_1))
    {
        printf("radio_rx.begin failed\n");
    }

    spi_0.begin(spi0, SPI0_SCK, SPI0_MISO, SPI0_MOSI);
    while (!radio_tx.begin(&spi_0, SPI0_CE, SPI0_CSN))
    {
        printf("radio_tx.begin failed\n");
    }

    /* RF24_PA_MIN RF24_PA_LOW RF24_PA_HIGH RF24_PA_MAX */
    radio_rx.setPALevel(RF24_PA_LOW);
    radio_tx.setPALevel(RF24_PA_LOW);
    radio_rx.setDataRate(RF24_1MBPS);
    radio_tx.setDataRate(RF24_1MBPS);
    radio_rx.enableDynamicPayloads();
    radio_tx.enableDynamicPayloads();
    radio_rx.setAutoAck(true);
    radio_tx.setAutoAck(true);
    radio_rx.enableAckPayload();
    radio_tx.enableAckPayload();

    radio_rx.openReadingPipe(1 /* pipe */, address[device_id].rx);
    radio_tx.openWritingPipe(address[device_id].tx);

    radio_rx.startListening();
    radio_tx.stopListening();

    printf("device_id=%d\n", device_id);
    printf("tx_address=%llX, rx_address=%llX\n", address[device_id].tx, address[device_id].rx);
}

void wait_usb_read() {
    while (!tud_cdc_connected()) {
        sleep_ms(500);
    }
}

int main()
{
    stdio_init_all();
    pico_get_unique_board_id(&id);
    pico_get_unique_board_id_string(sid, sizeof(sid));
    wait_usb_read();
    printf("--------\n");
    printf("Board ID: %s\n", sid);

    setup();

    // multicore_launch_core1(rx_thread);
    // tx_thread();
    rx_tx_thread();
    // shouldn't reach here
    while (1)
    {
        printf("Main loop\n");
        sleep_ms(1000);
    }
}

Could be related to power as I'm powering my rpi pico with USB and powering 2 NRF using pico.
I checked various status on Tx failure and couldn't detect anything wrong but maybe it's not possible (isChipConnected, printPrettyDetails).

Will provide external power supply to both modules to ensure it's not the issue here.

@2bndy5
Copy link
Member Author

2bndy5 commented Aug 13, 2023

Power supply was going to be my next recourse. The RPi Pico board uses a switching regulator (last time I checked) which inherently produces a lot of noise (AKA instability) in favor of efficiency.

Thanks for eliminating the multi-processing "unknown" (& posting the latest revision).

You could also check RF24::failureDetected for tx failures, but it doesn't include RX behavior (referring to ACK payload transmission).

@XDeschuyteneer
Copy link

I tried to catch Tx/Rx failures and reinit de radio as explained in the documentation, but nothing pops (I mean no failure).

I'll try to use external power supply, but this will require some soldering, will do it later and post updates.

Here is the updated code:

#include <stdio.h>
#include <pico/stdlib.h>
#include <pico/unique_id.h>
#include "pico/multicore.h"
#include <hardware/adc.h>
#include <tusb.h>
#include <RF24.h>

const int GPIO_MODE = 1;

#define SPI0_MISO 4
#define SPI0_MOSI 3
#define SPI0_SCK 2
#define SPI0_CSN 5
#define SPI0_CE 6

#define SPI1_MISO 12
#define SPI1_MOSI 11
#define SPI1_SCK 10
#define SPI1_CSN 13
#define SPI1_CE 14

int cnt = 1;

char sid[2 * PICO_UNIQUE_BOARD_ID_SIZE_BYTES + 1] = {0};
pico_unique_board_id_t id;

SPI spi_0;
SPI spi_1;
RF24 radio_tx(SPI0_CE, SPI0_CSN);
RF24 radio_rx(SPI1_CE, SPI1_CSN);

struct address_t {
    uint8_t rx[5];
    uint8_t tx[5];
};

struct message_t {
    uint8_t cnt;
    uint8_t payload[32];
};

struct address_t address[] = {
    {.rx = {0xF0, 0xF0, 0xF0, 0xF0, 0xE1}, .tx = {0xF0, 0xF0, 0xF0, 0xF0, 0xD2}},
    {.rx = {0xF0, 0xF0, 0xF0, 0xF0, 0xD2}, .tx = {0xF0, 0xF0, 0xF0, 0xF0, 0xE1}}
};

struct message_t tx_msg = {0};
struct message_t rx_msg = {0};

void radio_init(bool receiver, RF24* radio, SPI* spi_nrf, spi_inst_t* spi_pico, uint8_t spi_sck, uint8_t spi_miso, uint8_t spi_mosi, uint8_t spi_ce, uint8_t spi_csn) {
    printf("radio_init(%d, %p, %p, %p, %d, %d, %d, %d, %d)\n", receiver, radio, spi_nrf, spi_pico, spi_sck, spi_miso, spi_mosi, spi_ce, spi_csn);
    uint8_t device_id = gpio_get(GPIO_MODE);
    spi_nrf->begin(spi_pico, spi_sck, spi_miso, spi_mosi);
    while (!radio->begin(spi_nrf, spi_ce, spi_csn))
    {
        printf("radio.begin failed\n");
    }
    radio->failureDetected = 0;
    radio->setPALevel(RF24_PA_LOW);
    radio->setDataRate(RF24_1MBPS);
    radio->enableDynamicPayloads();
    radio->setAutoAck(true);
    radio->enableAckPayload();
    if (receiver) {
        radio->openReadingPipe(1 /* pipe */, address[device_id].rx);
        radio->startListening();
    } else {
        radio->openWritingPipe(address[device_id].tx);
        radio->stopListening();
    }
}

void radio_init_rx() {
    printf("radio_init_rx\n");
    radio_init(true, &radio_rx, &spi_1, spi1, SPI1_SCK, SPI1_MISO, SPI1_MOSI, SPI1_CE, SPI1_CSN);
}

void radio_init_tx() {
    printf("radio_init_tx\n");
    radio_init(false, &radio_tx, &spi_0, spi0, SPI0_SCK, SPI0_MISO, SPI0_MOSI, SPI0_CE, SPI0_CSN);
}

void tx_action() {
    memset(&tx_msg, 0, sizeof(tx_msg));
    tx_msg.cnt = cnt;
    snprintf((char*)tx_msg.payload, sizeof(tx_msg.payload), "msg from %s", sid);
    if (radio_tx.write(&tx_msg, sizeof(tx_msg))) /* send OK */
    {
        printf("Tx (me:%s):  payload=%s cnt=%d\n", sid, tx_msg.payload, tx_msg.cnt);
        cnt++;
    }
    else {
        if (radio_tx.failureDetected) {
            printf("/!\\ radio_tx.failureDetected\n");
            radio_init_tx();
        }
        
    } /* send KO */
}

void tx_thread()
{
    printf("TX Thread started\n");
    while (true)
    {
        tx_action();
    }
}

void rx_action() {
    uint8_t pipe;
    if (radio_rx.available(&pipe))
    {
        memset(&rx_msg, 0, sizeof(rx_msg));
        radio_rx.read(&rx_msg, radio_rx.getDynamicPayloadSize());
        printf("Rx (me:%s): pipe=%d, payload=%s cnt=%d\n", sid, pipe, rx_msg.payload, rx_msg.cnt);
    }
    else {
        if (radio_rx.failureDetected) {
            printf("/!\\ radio_rx.failureDetected\n");
            radio_init_rx();
        } else {} /* all good */
    }
}

void rx_thread()
{
    printf("RX Thread started\n");
    while (true)
    {
        rx_action();
    }
}

void rx_tx_thread()
{
    printf("RX TX Thread started\n");
    while (true)
    {
        rx_action();
        tx_action();
    }
}

void setup()
{
    uint8_t device_id = gpio_get(GPIO_MODE);
    radio_init_tx();
    radio_init_rx();
    printf("device_id=%d\n", device_id);
    printf("tx_address=%llX, rx_address=%llX\n", address[device_id].tx, address[device_id].rx);
}

void wait_usb_read() {
    while (!tud_cdc_connected()) {
        sleep_ms(500);
    }
}

int main()
{
    stdio_init_all();
    pico_get_unique_board_id(&id);
    pico_get_unique_board_id_string(sid, sizeof(sid));
    wait_usb_read();
    printf("--------\n");
    printf("Board ID: %s\n", sid);

    setup();

    // multicore_launch_core1(rx_thread);
    // tx_thread();
    rx_tx_thread();
    // shouldn't reach here
    while (1)
    {
        printf("Main loop\n");
        sleep_ms(1000);
    }
}

@2bndy5
Copy link
Member Author

2bndy5 commented Aug 13, 2023

I think the endianess of your address buffers is backward. The unique byte (MSB) of the address should come first in the buffer:

struct address_t address[] = {
    {.rx = {0xE1, 0xF0, 0xF0, 0xF0, 0xF0}, .tx = {0xD2, 0xF0, 0xF0, 0xF0, 0xF0}},
    {.rx = {0xD2, 0xF0, 0xF0, 0xF0, 0xF0}, .tx = {0xE1, 0xF0, 0xF0, 0xF0, 0xF0}}
};

@2bndy5 2bndy5 changed the title I did a test inspired by your documentation, but that doesn't seem to work properly on my side. [Pico SDK] using multiple multiple radios Aug 13, 2023
@XDeschuyteneer
Copy link

why the unique byte of the address should come first?
I saw that "idea of path" multiple time, I saw in the examples people used "1Node" or "2Node", but I didn't find any explanation for it.
Is it for optimisation purpose? or is there a HW requirement? or a logic behind it?

@2bndy5
Copy link
Member Author

2bndy5 commented Aug 14, 2023

It is an implementation detail about how the address is set to the radio's registers (which must be done in big endian order). Keep in mind that the endianess of the data stored in memory is important when making SPI transfers.

@XDeschuyteneer
Copy link

yes, but here it's an “address”, intuitively, as long as it 'match', endianness shouldn't matter. Will take a look at the code, thanks for pointing that out. I'll fix it in my code then and try external power supply.

@2bndy5
Copy link
Member Author

2bndy5 commented Aug 14, 2023

I added an example snippet to operitingPipe's doc to explain why the unique byte is important. Essentially pipes 2-5 don't actually accept entirely different addresses... More explained in docs.

@XDeschuyteneer
Copy link

crystal clear, thanks. missed that comments and links.

@2bndy5
Copy link
Member Author

2bndy5 commented Aug 14, 2023

If you're using a single pico board with 2 radios, does it help to set the PA level to min for each? RF24_PA_MIN would only apply to this issue's testing while using bare minimum power to transmit. If not, then it would still help to have a more reliable power source.

@XDeschuyteneer
Copy link

I fixed the addresses and the set power to min (instead of low, pretty sure i tested that in the past, but it was probably a combination of errors) and it was working fine after this.
I reenabled my multi core Rx/Tx. And it's working perfectly.

2 minicoms opened on each rpi pico:
image

I assume

here is the code I'm using (2xrpi pico with 2 NRF24 modules each, giving me a real bidirectional communication):

#include <stdio.h>
#include <pico/stdlib.h>
#include <pico/unique_id.h>
#include "pico/multicore.h"
#include <hardware/adc.h>
#include <tusb.h>
#include <RF24.h>

const int GPIO_MODE = 1;

#define SPI0_MISO 4
#define SPI0_MOSI 3
#define SPI0_SCK 2
#define SPI0_CSN 5
#define SPI0_CE 6

#define SPI1_MISO 12
#define SPI1_MOSI 11
#define SPI1_SCK 10
#define SPI1_CSN 13
#define SPI1_CE 14

char sid[2 * PICO_UNIQUE_BOARD_ID_SIZE_BYTES + 1] = {0};
pico_unique_board_id_t id;

SPI spi_0;
SPI spi_1;
RF24 radio_tx(SPI0_CE, SPI0_CSN);
RF24 radio_rx(SPI1_CE, SPI1_CSN);


struct address_t {
    uint8_t rx[5];
    uint8_t tx[5];
};

struct message_t {
    uint32_t cnt;
    uint8_t payload[32];
};

struct address_t address[] = {
    {.rx = {0xE1, 0xF0, 0xF0, 0xF0, 0xF0}, .tx = {0xD2, 0xF0, 0xF0, 0xF0, 0xF0}},
    {.rx = {0xD2, 0xF0, 0xF0, 0xF0, 0xF0}, .tx = {0xE1, 0xF0, 0xF0, 0xF0, 0xF0}}
};

struct message_t tx_msg = {0};
struct message_t rx_msg = {0};

void radio_init(bool receiver, RF24* radio, SPI* spi_nrf, spi_inst_t* spi_pico, uint8_t spi_sck, uint8_t spi_miso, uint8_t spi_mosi, uint8_t spi_ce, uint8_t spi_csn) {
    printf("radio_init(%d, %p, %p, %p, %d, %d, %d, %d, %d)\n", receiver, radio, spi_nrf, spi_pico, spi_sck, spi_miso, spi_mosi, spi_ce, spi_csn);
    uint8_t device_id = gpio_get(GPIO_MODE);
    spi_nrf->begin(spi_pico, spi_sck, spi_miso, spi_mosi);
    while (!radio->begin(spi_nrf, spi_ce, spi_csn))
    {
        printf("radio.begin failed\n");
    }
    radio->failureDetected = 0;
    radio->setPALevel(RF24_PA_MIN);
    radio->setDataRate(RF24_1MBPS);
    radio->enableDynamicPayloads();
    radio->setAutoAck(true);
    radio->enableAckPayload();
    if (receiver) {
        radio->openReadingPipe(1 /* pipe */, address[device_id].rx);
        radio->startListening();
    } else {
        radio->openWritingPipe(address[device_id].tx);
        radio->stopListening();
    }
}

void radio_init_rx() {
    printf("radio_init_rx\n");
    radio_init(true, &radio_rx, &spi_1, spi1, SPI1_SCK, SPI1_MISO, SPI1_MOSI, SPI1_CE, SPI1_CSN);
}

void radio_init_tx() {
    printf("radio_init_tx\n");
    radio_init(false, &radio_tx, &spi_0, spi0, SPI0_SCK, SPI0_MISO, SPI0_MOSI, SPI0_CE, SPI0_CSN);
}

void tx_action() {
    memset(&tx_msg.payload, 0, sizeof(tx_msg.payload));
    tx_msg.cnt++;
    snprintf((char*)tx_msg.payload, sizeof(tx_msg.payload), "msg from %s", sid);
    if (radio_tx.write(&tx_msg, sizeof(tx_msg))) /* send OK */
    {
        printf("Tx (me:%s):  payload=%s cnt=%d\n", sid, tx_msg.payload, tx_msg.cnt);
    }
    else {
        if (radio_tx.failureDetected) {
            printf("/!\\ radio_tx.failureDetected\n");
            radio_init_tx();
        }
        
    } /* send KO */
}

void tx_thread()
{
    printf("TX Thread started\n");
    while (true)
    {
        tx_action();
    }
}

void rx_action() {
    uint8_t pipe;
    if (radio_rx.available(&pipe))
    {
        memset(&rx_msg, 0, sizeof(rx_msg));
        radio_rx.read(&rx_msg, radio_rx.getDynamicPayloadSize());
        printf("Rx (me:%s): pipe=%d, payload=%s cnt=%d\n", sid, pipe, rx_msg.payload, rx_msg.cnt);
    }
    else {
        if (radio_rx.failureDetected) {
            printf("/!\\ radio_rx.failureDetected\n");
            radio_init_rx();
        } else {} /* all good */
    }
}

void rx_thread()
{
    printf("RX Thread started\n");
    while (true)
    {
        rx_action();
    }
}

void setup()
{
    uint8_t device_id = gpio_get(GPIO_MODE);
    radio_init_tx();
    radio_init_rx();
    printf("device_id=%d\n", device_id);
    printf("tx_address=%llX, rx_address=%llX\n", address[device_id].tx, address[device_id].rx);
}

void wait_usb_read() {
    while (!tud_cdc_connected()) {
        sleep_ms(500);
    }
}

int main()
{
    stdio_init_all();
    pico_get_unique_board_id(&id);
    pico_get_unique_board_id_string(sid, sizeof(sid));
    wait_usb_read();
    printf("--------\n");
    printf("Board ID: %s\n", sid);

    setup();

    multicore_launch_core1(rx_thread);
    tx_thread();
    // shouldn't reach here
    while (1)
    {
        printf("Main loop\n");
        sleep_ms(1000);
    }
}

@XDeschuyteneer
Copy link

Thanks for the help! I think we can close this. Except if you want me to push my example to the repo.

@2bndy5
Copy link
Member Author

2bndy5 commented Aug 14, 2023

yeah! 🎉 that does confirm it is a power supply problem. Unfortunately, I foresee soldering in your future 😜.

you want me to push my example to the repo

Thanks, but no thanks... I'd rather not maintain another example; 6 examples per platform is tedious as it is.

This isn't the first time we've seen problems with the RPi Pico board's 3v regulator though. It is probably worth a mention in the doc somewhere.

@2bndy5 2bndy5 changed the title [Pico SDK] using multiple multiple radios [Pico SDK] using multiple multiple radios on a single RPi Pico board Aug 14, 2023
@XDeschuyteneer
Copy link

can close this then :-) Thanks again for the help.

@2bndy5 2bndy5 closed this as completed Aug 14, 2023
2bndy5 added a commit that referenced this issue Aug 14, 2023
Hopefully this can avoid issues like #912
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants