DMX NRF24L01 WS2812B latency problem

Hi All,

I've been working on a solution to send DMX data cheaply and reliably using NRF24L01+ modules (RF24 lib). The goal of the project is to control 25 costumes equipped with WS2812B LEDs in a dance show. I was inspired by Bob's work—thanks for your great contribution! @ mcnobby mentioned something about a time-controlled code - unfortunately couldn't find anything :frowning:

Transmitter:

  • Hardware:
    • RP2040-Stamp
    • E01-ML01SP2 (NRF24L01+ Module with PA)
    • Isolated DMX Input
    • 128x64 Pixel OLED
    • 21x WS2812B LEDs (to aid in debugging)
    • Rotary Encoder with Pushbutton
    • Micro-SD Card (optional for recording and sending)
  • Software:
    • DMX data is received using the Pico-DMX library with PIO support on the RP2040. Since the DMX bus runs at 250k baud, I'm expecting up to a 44Hz refresh rate.
    • When data is received, a flag is set, and the data is immediately buffered into a second array.
    • The data is then sent in 18 blocks of 32 bytes each (Byte 1: Packet ID; Bytes 2-31: Data; Byte 32: CRC8).
    • Optionally, I have provisioned for sending the data a second time - if some data are lost in air.

Receiver (currently 25 units):

  • Hardware:
    • RP2040 Zero
    • 18650 Battery
    • Charger
    • 5V 3A Boost Converter (for longer WS2812B strips)
    • E01-ML01SP2 (NRF24L01+ Module with PA)
    • 1x WS2812B Status LED
    • 1x Blue Status LED
  • Software:
    • The receiver accepts the packet, checks the CRC8, and writes the data into the DMX array.
    • It then sends the data to the WS2812B LEDs via PIO (NeoPixelBus).

My Problem:

When I input a slow stroboscope effect via DMX, only one receiver performs exactly as expected (DMX channels 1 to 30). However, the others are slightly delayed—unfortunately, the delay is noticeable.

I'm looking for a way to achieve a more stable broadcast transmission and minimize the delay between receivers. A slight delay between the sender and receivers is acceptable, but the delay between receivers should be minimal or, ideally, not noticeable at all.

Some additional info - to optimize the code for this special case:
Each costume has 30 DMX channels (10 RGB LED groups) => Costume 1: DMX 1 to 30, Costume 2: DMX 31 to 60, ...

Hope someone can help.

I'll be happy to share the plans once everything is running smoothly.

Best Regards, Thomas


Code:

bool sendDMXData() {

  // Paketstruktur
  const uint8_t DATA_SIZE       = 30;
  const uint8_t PACKET_SIZE     = 32;      // 1 Byte Paketnummer + 30 Bytes Daten + 1 Bytes CRC8
  
  uint8_t packet[PACKET_SIZE];
  uint8_t packet_id = 0;
  uint16_t dmx_index = 0;

  bool fifo_sent = false;
  bool all_ok = true;

  unsigned long micros_start = micros();

  #ifdef THOB_DEBUG_TX
    Serial.println("--- Send Block ---");
  #endif

  radio.flush_tx();

  packet_id = 0;
  dmx_index = DMX_CHANNEL_START + packet_id * DATA_SIZE;
  packet[0] = packet_id;
  memcpy(&packet[1], &dmx_data_buffer[dmx_index], DATA_SIZE);
  packet[31] = crc8(packet, 31);
  all_ok = all_ok && radio.writeFast(&packet, PACKET_SIZE);

  packet_id = 1;
  dmx_index = DMX_CHANNEL_START + packet_id * DATA_SIZE;
  packet[0] = packet_id;
  memcpy(&packet[1], &dmx_data_buffer[dmx_index], DATA_SIZE);
  packet[31] = crc8(packet, 31);
  all_ok = all_ok && radio.writeFast(&packet, PACKET_SIZE);

  packet_id = 2;
  dmx_index = DMX_CHANNEL_START + packet_id * DATA_SIZE;
  packet[0] = packet_id;
  memcpy(&packet[1], &dmx_data_buffer[dmx_index], DATA_SIZE);
  packet[31] = crc8(packet, 31);
  all_ok = all_ok && radio.writeFast(&packet, PACKET_SIZE);

  fifo_sent = radio.txStandBy(1);  // Returns 0 if failed after x milliseconds of retries. 1 if success.
  all_ok = all_ok && fifo_sent;
  #ifdef THOB_DEBUG_TX
    Serial.print("Paket: "); Serial.print(packet_id - 2); Serial.print(" - "); Serial.print(packet_id);
    Serial.print(" FIFO sent (1): "); Serial.print(fifo_sent);
    Serial.print(" FIFO empty (1): "); Serial.println(radio.isFifo(true));
  #endif

  packet_id = 3;
  dmx_index = DMX_CHANNEL_START + packet_id * DATA_SIZE;
  packet[0] = packet_id;
  memcpy(&packet[1], &dmx_data_buffer[dmx_index], DATA_SIZE);
  packet[31] = crc8(packet, 31);
  all_ok = all_ok && radio.writeFast(&packet, PACKET_SIZE);

  packet_id = 4;
  dmx_index = DMX_CHANNEL_START + packet_id * DATA_SIZE;
  packet[0] = packet_id;
  memcpy(&packet[1], &dmx_data_buffer[dmx_index], DATA_SIZE);
  packet[31] = crc8(packet, 31);
  all_ok = all_ok && radio.writeFast(&packet, PACKET_SIZE);

  packet_id = 5;
  dmx_index = DMX_CHANNEL_START + packet_id * DATA_SIZE;
  packet[0] = packet_id;
  memcpy(&packet[1], &dmx_data_buffer[dmx_index], DATA_SIZE);
  packet[31] = crc8(packet, 31);
  all_ok = all_ok && radio.writeFast(&packet, PACKET_SIZE);

  fifo_sent = radio.txStandBy(1);  // Returns 0 if failed after x milliseconds of retries. 1 if success.
  all_ok = all_ok && fifo_sent;
  #ifdef THOB_DEBUG_TX
    Serial.print("Paket: "); Serial.print(packet_id - 2); Serial.print(" - "); Serial.print(packet_id);
    Serial.print(" FIFO sent (1): "); Serial.print(fifo_sent);
    Serial.print(" FIFO empty (1): "); Serial.println(radio.isFifo(true));
  #endif

  packet_id = 6;
  dmx_index = DMX_CHANNEL_START + packet_id * DATA_SIZE;
  packet[0] = packet_id;
  memcpy(&packet[1], &dmx_data_buffer[dmx_index], DATA_SIZE);
  packet[31] = crc8(packet, 31);
  all_ok = all_ok && radio.writeFast(&packet, PACKET_SIZE);

  packet_id = 7;
  dmx_index = DMX_CHANNEL_START + packet_id * DATA_SIZE;
  packet[0] = packet_id;
  memcpy(&packet[1], &dmx_data_buffer[dmx_index], DATA_SIZE);
  packet[31] = crc8(packet, 31);
  all_ok = all_ok && radio.writeFast(&packet, PACKET_SIZE);

  packet_id = 8;
  dmx_index = DMX_CHANNEL_START + packet_id * DATA_SIZE;
  packet[0] = packet_id;
  memcpy(&packet[1], &dmx_data_buffer[dmx_index], DATA_SIZE);
  packet[31] = crc8(packet, 31);
  all_ok = all_ok && radio.writeFast(&packet, PACKET_SIZE);

  fifo_sent = radio.txStandBy(1);  // Returns 0 if failed after x milliseconds of retries. 1 if success.
  all_ok = all_ok && fifo_sent;
  #ifdef THOB_DEBUG_TX
    Serial.print("Paket: "); Serial.print(packet_id - 2); Serial.print(" - "); Serial.print(packet_id);
    Serial.print(" FIFO sent (1): "); Serial.print(fifo_sent);
    Serial.print(" FIFO empty (1): "); Serial.println(radio.isFifo(true));
  #endif

  packet_id = 9;
  dmx_index = DMX_CHANNEL_START + packet_id * DATA_SIZE;
  packet[0] = packet_id;
  memcpy(&packet[1], &dmx_data_buffer[dmx_index], DATA_SIZE);
  packet[31] = crc8(packet, 31);
  all_ok = all_ok && radio.writeFast(&packet, PACKET_SIZE);

  packet_id = 10;
  dmx_index = DMX_CHANNEL_START + packet_id * DATA_SIZE;
  packet[0] = packet_id;
  memcpy(&packet[1], &dmx_data_buffer[dmx_index], DATA_SIZE);
  packet[31] = crc8(packet, 31);
  all_ok = all_ok && radio.writeFast(&packet, PACKET_SIZE);

  packet_id = 11;
  dmx_index = DMX_CHANNEL_START + packet_id * DATA_SIZE;
  packet[0] = packet_id;
  memcpy(&packet[1], &dmx_data_buffer[dmx_index], DATA_SIZE);
  packet[31] = crc8(packet, 31);
  all_ok = all_ok && radio.writeFast(&packet, PACKET_SIZE);

  fifo_sent = radio.txStandBy(1);  // Returns 0 if failed after x milliseconds of retries. 1 if success.
  all_ok = all_ok && fifo_sent;
  #ifdef THOB_DEBUG_TX
    Serial.print("Paket: "); Serial.print(packet_id - 2); Serial.print(" - "); Serial.print(packet_id);
    Serial.print(" FIFO sent (1): "); Serial.print(fifo_sent);
    Serial.print(" FIFO empty (1): "); Serial.println(radio.isFifo(true));
  #endif

  packet_id = 12;
  dmx_index = DMX_CHANNEL_START + packet_id * DATA_SIZE;
  packet[0] = packet_id;
  memcpy(&packet[1], &dmx_data_buffer[dmx_index], DATA_SIZE);
  packet[31] = crc8(packet, 31);
  all_ok = all_ok && radio.writeFast(&packet, PACKET_SIZE);

  packet_id = 13;
  dmx_index = DMX_CHANNEL_START + packet_id * DATA_SIZE;
  packet[0] = packet_id;
  memcpy(&packet[1], &dmx_data_buffer[dmx_index], DATA_SIZE);
  packet[31] = crc8(packet, 31);
  all_ok = all_ok && radio.writeFast(&packet, PACKET_SIZE);

  packet_id = 14;
  dmx_index = DMX_CHANNEL_START + packet_id * DATA_SIZE;
  packet[0] = packet_id;
  memcpy(&packet[1], &dmx_data_buffer[dmx_index], DATA_SIZE);
  packet[31] = crc8(packet, 31);
  all_ok = all_ok && radio.writeFast(&packet, PACKET_SIZE);

  fifo_sent = radio.txStandBy(1);  // Returns 0 if failed after x milliseconds of retries. 1 if success.
  all_ok = all_ok && fifo_sent;
  #ifdef THOB_DEBUG_TX
    Serial.print("Paket: "); Serial.print(packet_id - 2); Serial.print(" - "); Serial.print(packet_id);
    Serial.print(" FIFO sent (1): "); Serial.print(fifo_sent);
    Serial.print(" FIFO empty (1): "); Serial.println(radio.isFifo(true));
  #endif

  packet_id = 15;
  dmx_index = DMX_CHANNEL_START + packet_id * DATA_SIZE;
  packet[0] = packet_id;
  memcpy(&packet[1], &dmx_data_buffer[dmx_index], DATA_SIZE);
  packet[31] = crc8(packet, 31);
  all_ok = all_ok && radio.writeFast(&packet, PACKET_SIZE);

  packet_id = 16;
  dmx_index = DMX_CHANNEL_START + packet_id * DATA_SIZE;
  packet[0] = packet_id;
  memcpy(&packet[1], &dmx_data_buffer[dmx_index], DATA_SIZE);
  packet[31] = crc8(packet, 31);
  all_ok = all_ok && radio.writeFast(&packet, PACKET_SIZE);

  packet_id = 17;
  dmx_index = DMX_CHANNEL_START + packet_id * DATA_SIZE;
  packet[0] = packet_id;
  memcpy(&packet[1], &dmx_data_buffer[dmx_index], 2);
  memset(&packet[3], 0x00, 28);  // Setze packet[3] bis packet[30] auf 0x00
  packet[31] = crc8(packet, 31);
  all_ok = all_ok && radio.writeFast(&packet, PACKET_SIZE);

  fifo_sent = radio.txStandBy(1);  // Returns 0 if failed after x milliseconds of retries. 1 if success.
  all_ok = all_ok && fifo_sent;
  #ifdef THOB_DEBUG_TX
    Serial.print("Paket: "); Serial.print(packet_id - 2); Serial.print(" - "); Serial.print(packet_id);
    Serial.print(" FIFO sent (1): "); Serial.print(fifo_sent);
    Serial.print(" FIFO empty (1): "); Serial.println(radio.isFifo(true));
  #endif

  #ifdef THOB_DEBUG_TX
    Serial.print("--- "); Serial.print(micros() - micros_start); Serial.println(" ---");
  #endif

  return all_ok; // Returns 0 if failed, 1 if success.
}

// Paketstruktur
  const uint8_t DATA_SIZE       = 30;
  const uint8_t PACKET_SIZE     = 32;      // 1 Byte Paketnummer + 30 Bytes Daten + 1 Bytes CRC8

  while (radio.available()) {
    uint8_t packet[PACKET_SIZE];
    radio.read(&packet, PACKET_SIZE);

    uint8_t packet_id = packet[0];
    uint8_t* data = &packet[1];
    uint8_t received_crc = packet[31];

    uint16_t dmx_index = DMX_CHANNEL_START + packet_id * DATA_SIZE;

   uint16_t dataLength = 30;
    if (packet_id ==17) {
      dataLength = 2;
    }
    memcpy(&dmx_data_buffer[dmx_index], data, dataLength);
  }

I have no experience with the NRF24, but with DMX.

The main issue with DMX is that the receiver needs to wait for the break before it can start receiving the next frame.

In theory yes, but in practice you tend to get only about half of that if you receive a complete frame. The reception can i theory be done in the background though, so after having received the frame if you wait for break straight away and find it and do all processing after that could work.

It does all depend on the method of reception.

Is that all happening in the background or is that done blocking ?

What method is used for this ? Neopixelbus has full-background methods for some boards, but i am not sure if the RP2040 zero is one of them or if it relies on a bit-banged method. (looks like it uses a similar DMA method ast the esp8266, so if you use that method this is not aan issue)

It seems that mainly sending different packets may be a cause. Could you send all data in a single packet, which you can parse on the receiving end rather than parsing them halfway.

In that case send the whole frame !

I don't want to buzz kill anything, but where are you intending to use this. Wireless transmission are severly impacted by speakers producing sound, just so you know.

What i mean to say is that sending those 25 packets one by one will take as much (in fact slightly more) time as sending the whole thing in one go.

thank you for your answer.

In short - this is actually "only" about the NRF24L01(+) - or rather about the radio transmission.

The DMX itself is not a problem and is completely understood.

As the DMX is read in via PIO and the LEDs are output on the receiver via PIO, this works completely in the background (if I haven't overlooked anything) and does not slow down sending and receiving.

The max. packet size of the NRF24L01(+) is 32 bytes. That's why it's split up.

Well in that case you should receive all packets on the receiver end before you start processing so all data is present in all receivers.

Personally i tend to focus on using Wifi, and for that ESP8266's are a much easier solution. They can all connect to the same network / router, and you can use ArtNet protocol, which is basically UDP packets.
Again hostile environments serious impact WiFi transmissions,

ArtNet is in some way the network version of DMX and is supported in any VJ software, which could omit a conversion.

@Deva_Rishi ,
I appreciate your willingness to help me, but please don't be mad at me if I'm direct.
You yourself write that you have no experience with the NRF24.

It's cool that you're dealing with ESP8266 and ArtNet yourself. But that's not relevant here. My hardware with the NRF24 is ready.
This isn't about DMX itself, about ArtNet with ESP8266, whether speakers could interfere with the radio signal or VJ software.
=> Please leave that, that doesn't belong here. :disguised_face:

I think it's clear that my problem is mainly about setting up a robust send/receive routine for the NRF24.
I'm looking for help with a good approach to:
- time synchronization (timestamp or sync byte)
- time slots (TDMA)
- error correction
- fast sending (better FIFO management)

I look forward to constructive contributions :slight_smile:

No that's cool.

It is something you maybe could have considered earlier, I am trying to help you get you to the end result you desire. I have experience with Ledstrip and since DMX is a lighting protocol that is mainly used in clubs and festivals, i try to provide you with what experience i already have.

Any information i can provide you that might be of use and may be relevant, has a place here. I do not know exactly what is the end goal, but keep in mind that this forum is also used as a knowledge base. Your issue may be similar to what others experience and and such may show up in their google search. I understand it isn't nice to hear that you may have taken a turn in your development that you cannot undo and wasn't the most efficient solution, but chances are we should be able to get it to work as you desire anyway. Still others may benefit from this information, so keep your cool please.

I suggested first to send all data in one packet, which isn't possible, but my next suggestion is. You send all packets and on the receiver end make sure that you don't process until the last packet has been received. Of course you discard any packet that is not intended for that specific unit. (all units anyway do receive all packets and discard what isn't meant for them as it as)
Another option would be to send an extra packet telling all receivers to process and transmit the signal to the ledstrip.

Your bottle neck is the transmission over RF. Have you measured how much time that actually takes per packet ?
Another option could be to multiply your DMX receiver - RF transmitter unit and make dedicated pairs. (or split the whole thing into a few)

The other issue i think i should mention is that the DMX reception, although happening in the background still does require some processing time, although the ARM is really quick. There data does get moved from the UART tx-FIFO to RAM (asynchronously)

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.