nRF24/RF24Audio

measuring ellapsed time for transmission

Ryderizm opened this issue ยท 29 comments

Hello there,

I was currently reviewing the data sheet for the NRF24L01 + version, It has the enhanced shockburst format. Is this supported in this RF24 Audio code or is it only shockburst format? .. I need to know this because I'm trying to evaluate packet timing.

radio.setAutoAck(0); // Disable ACKnowledgement packets

auto ack is disabled to optimize throughput.

radio.setAutoAck(0); // Disable ACKnowledgement packets

auto ack is disabled to optimize throughput.

So if auto ack is disabled does that mean it's using shockburst format?

So if auto ack is disabled does that mean it's using shockburst format?

IIRC, the only difference between ESB vs SB is the use of auto-ack feature, so yes. I read the datasheets too many times to count.

So if auto ack is disabled does that mean it's using shockburst format?

IIRC, the only difference between ESB vs SB is the use of auto-ack feature, so yes. I read the datasheets too many times to count.

yeah, you are right. However, you answered my question very much but I'm struggling to calculate something else as well.. I was wondering if there is an equation where I can calculate total delay from sending a packet from an arduino input to another arduino output using the NRF transceivers (considering parameters such as, sampling rate, audio resolution, and ADC rate for example) .. because in the datasheet it only calculates delay for the transceivers only. When I actually measured the delay it was about 3-4 ms, but otherwise in the datasheet, it is in microseconds instead. I would be happy if you can help me with that or at least direct me to a point where I can start getting digging for an answer. I'm trying to study delay from signal filtering and conversions as well

What you're looking for may not have a definitive answer.

Aside from info in the datasheet about timing (which is specifically measured in a lab under only certain conditions with certain radio settings), there are hardware-specific variables that should also be taken into account. Variables that include

  • measuring the audio input
  • the actual SPI bus speed (which is usually an approximation on behalf of the Arduino Core's libraries - this is specific to the MCU board being used)
  • the execution time of the functions called (eg the time it takes to call Serial.print*() should be elided)

I'm not sure if this helps.

BTW, you don't have to quote my whole comment every time you post a comment in response.

Sorry my bad I usually quote people even if in singular conversation.

  • Well I don't know what do you mean by measuring the audio input.
  • I heard that the SPI speed for the Arduino nano (atmega328p) has 8 MHz clock speed but in the code it is adjusted to 10 MHz which is confusing me a bit.
  • Hence I don't really have a good background in radio communication, for example when you talk about SPI, is it arduino related or nrf related or is it both? (Because there are two different SPI bus speeds)
  • Is there a way to find the execution time of function rfAudio.transmit(); for example? I know it's a loop but is there a way to find delay of that function?

Sorry I'm asking a lot of questions because I'm 3 weeks away from a deadline

I heard that the SPI speed for the Arduino nano (atmega328p) has 8 MHz clock speed but in the code it is adjusted to 10 MHz which is confusing me a bit.

The max speed that the radio (an SPI slave device) can support is 10MHz. That is why the RF24Audio code specifies 10MHz.

There is more complexity in this because the CPU's clock speed is determined with a crystal oscillator. The crystal may oscillate at 8MHz (whose value depends on the crystal chosen by the board's manufacturer), but the SPI speed is physically determined based on the crystal oscillator (using division). CPUs like the ATMega328p have this math "hard wired" into the silicon, and it is changed by writing different values to the ATMega328p's registers.


when you talk about SPI, is it arduino related or nrf related or is it both? (Because there are two different SPI bus speeds)

SPI is the data bus that facilitates communication between your arduino board (the SPI master device) and the radio. There can only be 1 speed used for transactions on this data bus. The Arduino framework is a bit more complex in that it allows changing the SPI speed used when communicating with different SPI slave devices; this is abstracted in the SPI lib's SPIClass::beginTransaction().

Remember that the SPI speed is different from the radio's RF data rate (which describes the oscillation of electromagnetic radiation to transmit data).


Is there a way to find the execution time of function rfAudio.transmit(); for example? I know it's a loop but is there a way to find delay of that function?

Disclaimer: I didn't write this lib, rather I try to help with simple questions like the one you originally posed.

This lib isn't geared toward academic study. You may have to alter the source a bit to measure what you want. With that said, rfAudio.transmit() does not look like a loop to me.

Disclaimer: I didn't write this lib, rather I try to help with simple questions like the one you originally posed.

This lib isn't geared toward academic study. You may have to alter the source a bit to measure what you want. With that said, rfAudio.transmit() does not look like a loop to me.

Oh sorry. I assumed that transmit() function has an implicit loop which streams data from the Arduino to the NRF. The reason why I assumed it as a loop is because the function is called in void setup(); instead of void loop(); .. I might be wrong because I don't have a full understanding of the RF24Audio library.

Anyway you clarified some points for me and I'm happy for that.

You may have to alter the source a bit to measure what you want

Is there any other sources you think might be helpful for me to get the answer?

TIMSK1 = _BV(OCIE1B) | _BV(OCIE1A); //Enable the TIMER1 COMPA and COMPB interrupts

I think this line means that the transmission is done with internal interrupts. Much of this source code relies on writing to the ATMega328p registers. I suspect that the actual transmitting is done by the Interrupt Service Routines (ISR() function like here and here). It would be difficult to use millis() or micros() within the ISR functions because those timing functions return non-volatile data (yielding unreliable measurements from within the ISR() function body).

I guess it actually is difficult to obtain the internal delay as well :/ .. I guess the only way to find delays is through experiments then

Probably the easiest way is to use experiments that involve a short audio input to the transmitter. My initial idea was to make the ISR() that uses radi.startFastWrite() set a volatile bool has_done_tx to true just before exiting, then keep track of the elapsed time between instances where has_done_tx was set to true.

mhmm I like your idea although it might be a little hard to implement but could be also slightly inaccurate. Can you provide me further support with code implementation? do I have to set the volatile bool after the startFastWrite() function?.. As well, how to get the elapsed time between the instances?

I'm not a fan of writing the code for other people's projects (especially since I'm not being paid). But here's a brief psuedo code of my idea.

volatile bool has_done_tx = false;

// ...

ISR(TIMER1_COMPA_vect) {                       // This interrupt vector sends the samples when a buffer is filled
    if (buffEmpty[!whichBuff] == 0) {          // If a buffer is ready to be sent
        a = !whichBuff;                        // Get the buffer # before allowing nested interrupts
        TIMSK1 &= ~(_BV(OCIE1A));              // Disable this interrupt vector
        sei();                                 // Enable global interrupts
        radi.startFastWrite(&buffer[a], 32, 1);// Do an IRQ friendly radio write        
        cli();                                 // Disable global interrupts
        buffEmpty[a] = 1;                      // Mark the buffer as empty
        TIMSK1 |= _BV(OCIE1A); 
        has_done_tx = true;
    }
}

then it would be like edge detection in your ino file.

uint32_t timeStart;

void setup() {
  timerStart = micros();
  // ...
}

void loop {
  if (has_done_tx) {
    uint32_t now = micros();
    Serial.print(F("ellapsed microseconds: "));
    Serial.println(now - timerStart);
    timerStart = now;
    has_done_tx = false;
  }
}

It looks very clear and pretty straight forward. I consider this a big help already, I will try this and I will give you a feedback as soon as I can. Once again, thanks a tons :))

Hey there... I just did the modification you suggested to me a week ago.. It worked with the addition of including extern in the .ino file so It can be accessible from the .cpp file.

I just noticed few things and i have two questions.

  • When i make the sample rate high, eventually the measured delay jumps between 1.5-2.5 ms in a uniform pattern, not randomly. what could explain the variation in the delay? is it audio buffering or is it due to the sampling rate just too high?.. I just can't explain it yet..
  • I would like to do the same thing to receiving operation, which function should i similarly add the volatile boolean? is it "ISR(TIMER1_OVF_vect)" underneath receiving interrupt?

When i make the sample rate high, eventually the measured delay jumps between 1.5-2.5 ms in a uniform pattern, not randomly. what could explain the variation in the delay? is it audio buffering or is it due to the sampling rate just too high?.. I just can't explain it yet..

@TMRh20 would be better equipped to answer this one.

I would like to do the same thing to receiving operation, which function should i similarly add the volatile boolean?

[EDITED]
Insert the volatile bool in handleRadio() after radi.read() is called.

void handleRadio()
{
    if (buffEmpty[!whichBuff] && streaming) { // If in RX mode and a buffer is empty, load it
        if (radi.available()) {
            boolean n=!whichBuff;             // Grab the changing value of which buffer is not being read before enabling nested interrupts
            TIMSK1 &= ~_BV(ICIE1);            // Disable this interrupt so it is not triggered while still running (this may take a while)
            sei();                            // Enable nested interrupts (Other interrupts can interrupt this one)
            radi.read(&buffer[n],32);         // Read the payload from the radio
            buffEmpty[n] = 0;                 // Indicate that a buffer is now full and ready to play
            pauseCntr = 0;                    // For disabling speaker when no data is being received

            TIMSK1 |= _BV(ICIE1);             // Finished, re-enable the interrupt vector that runs this function
+           has_done_rx = true;
        }
        // ...
}

@TMRh20 would be better equipped to answer this one.

Is he active? Does he respond to open issues on RF24Audio?

Insert the volatile bool after handleRadio() is called.

ISR(TIMER1_CAPT_vect) { // This interrupt checks for data at 1/16th the sample rate. Since there are 32 bytes per payload, it gets two chances for every payload

    bufCtr++; visCtr++; // Keep track of how many times the interrupt has been triggered

    if (bufCtr >= 16) { // Every 16 times, do this
        handleRadio();  // Check for incoming radio data if not transmitting

        bufCtr = 0; // Reset this counter

        if (visCtr >= 32 && streaming) {       // Run the visualisation at a much slower speed so we can see the changes better
            OCR0A = buffer[whichBuff][0] << 2; // Adjust the TIMER0 compare match to change the PWM duty and thus the brightess of the LED
            visCtr = 0;                        // Reset the visualization counter
        }
+       has_done_rx = true;
    }
}

Oh.. why is it the CAPT timer interrupt and not the one that says "//Receive Interrupt" / OVF ?

Sorry I modified my suggestion about the measuring RX (please re-read my comment).

Oh.. why is it the CAPT timer interrupt and not the one that says "//Receive Interrupt" / OVF ?

TMRh20's comments weren't always verbose. I think "Receive" is specific to the interrupt getting triggered (not the radio getting data).

He will respond, but it might take a day. He's the "big guns" here; I just try to lighten his load.

What are the sample rates you are using and what exactly is the result? I'm leaning towards it just being too high of a sample rate and there being issues with buffering data etc. fast enough.

I'm not sure exactly what you are trying to measure here, but if you want accurate results, why not use the core RF24 library and test sending of individual packets if you want to evaluate packet timing.

RF24Audio makes extensive use of interrupts, which are fairly precise, but per my tests with high speed signalling even the interrupts on an arduino are jittery, so I had to use assembly code to tighten the signal up. My point is that interrupts make things difficult when it comes to timing. They interrupt the millis and micros functions, so those are not so reliable.

What is it you are trying to do exactly? After reading this all, I'm still not sure...

Hello @TMRh20. Sorry for the late reply. The current sample rate I'm using is 35 kHz. For example setting 35kHz gives a delay like this:

20:28:31.389 -> ellapsed microseconds: 2668
20:28:31.389 -> ellapsed microseconds: 2712
20:28:31.389 -> ellapsed microseconds: 1836
20:28:31.389 -> ellapsed microseconds: 2676
20:28:31.389 -> ellapsed microseconds: 2704
20:28:31.389 -> ellapsed microseconds: 2700
20:28:31.436 -> ellapsed microseconds: 1812
20:28:31.436 -> ellapsed microseconds: 2696
20:28:31.436 -> ellapsed microseconds: 2760
20:28:31.436 -> ellapsed microseconds: 1760
20:28:31.436 -> ellapsed microseconds: 2696
20:28:31.436 -> ellapsed microseconds: 2756
20:28:31.436 -> ellapsed microseconds: 2656
20:28:31.436 -> ellapsed microseconds: 1840
20:28:31.436 -> ellapsed microseconds: 2764
20:28:31.436 -> ellapsed microseconds: 2700
20:28:31.436 -> ellapsed microseconds: 2732
20:28:31.436 -> ellapsed microseconds: 1804
20:28:31.436 -> ellapsed microseconds: 2708
20:28:31.436 -> ellapsed microseconds: 2732
20:28:31.436 -> ellapsed microseconds: 1832
20:28:31.436 -> ellapsed microseconds: 2744
20:28:31.436 -> ellapsed microseconds: 2676
20:28:31.436 -> ellapsed microseconds: 2700
20:28:31.436 -> ellapsed microseconds: 1852
20:28:31.483 -> ellapsed microseconds: 2656
20:28:31.483 -> ellapsed microseconds: 2708

If I use a different sample rate. e.g. 16 kHz gives delay sequence of:

20:32:50.091 -> ellapsed microseconds: 2020
20:32:50.091 -> ellapsed microseconds: 1980
20:32:50.091 -> ellapsed microseconds: 4004
20:32:50.091 -> ellapsed microseconds: 2004
20:32:50.091 -> ellapsed microseconds: 2004
20:32:50.091 -> ellapsed microseconds: 2036
20:32:50.091 -> ellapsed microseconds: 2012
20:32:50.091 -> ellapsed microseconds: 4004
20:32:50.091 -> ellapsed microseconds: 1956
20:32:50.091 -> ellapsed microseconds: 2020
20:32:50.091 -> ellapsed microseconds: 1984
20:32:50.091 -> ellapsed microseconds: 4004
20:32:50.091 -> ellapsed microseconds: 2000
20:32:50.091 -> ellapsed microseconds: 2004
20:32:50.091 -> ellapsed microseconds: 2000
20:32:50.091 -> ellapsed microseconds: 4004
20:32:50.128 -> ellapsed microseconds: 2024
20:32:50.128 -> ellapsed microseconds: 1984
20:32:50.128 -> ellapsed microseconds: 2000

a sample rate of 1 kHz gives delay sequence of:

20:31:05.033 -> ellapsed microseconds: 32000
20:31:05.079 -> ellapsed microseconds: 32036
20:31:05.125 -> ellapsed microseconds: 31968
20:31:05.171 -> ellapsed microseconds: 32004
20:31:05.171 -> ellapsed microseconds: 32000
20:31:05.218 -> ellapsed microseconds: 32036
20:31:05.265 -> ellapsed microseconds: 31968
20:31:05.265 -> ellapsed microseconds: 32004
20:31:05.313 -> ellapsed microseconds: 32000
20:31:05.360 -> ellapsed microseconds: 32036
20:31:05.360 -> ellapsed microseconds: 31968
20:31:05.406 -> ellapsed microseconds: 32004
20:31:05.453 -> ellapsed microseconds: 32000
20:31:05.453 -> ellapsed microseconds: 32032
20:31:05.500 -> ellapsed microseconds: 31972
20:31:05.546 -> ellapsed microseconds: 32004

you can notice the delay becomes consistent when lowering the sampling rate. I'm currently doing a study on total audio stream delay caused by changing in Sampling rates/RFBitrate/ChannelFrequency. Maybe I was not clear enough from the beginning but that is my main study. If I evaluate packet delay from the main RF24 library is it the same as total stream delay?

If I evaluate packet delay from the main RF24 library is it the same as total stream delay?

Timing packets with the core RF24 lib would be a lot easier, but it wouldn't include the buffering of audio data on the MCU. Your currently measuring individual 32 byte payloads (with no auto-ack) and audio buffering, so maybe your study would benefit from subtractive comparison (sample_buffering = ellapsed_RF24Audio - ellapsed_RF24).

interrupts make things difficult when it comes to timing. They interrupt the millis and micros functions, so those are not so reliable.

This may imply that you should write your own tests (with only a pre-buffered audio sample + RF24 lib) that doesn't use interrupts for more definitive results. Such a tactic will be able to run on other platforms (like ATSAMD21 or ARM). At the risk of repeating myself, this RF24Audio lib wasn't designed for academic study.

The total stream delay can probably be calculated to some degree. It should stay fairly consistent regardless of sample rates or channel frequency. The RF bitrate would impact it though. Essentially you would want to measure the time it takes from taking an analog reading to the recipient getting the data? You could model this fairly easily using the RF24 core library, using a simple analog-read, then send the data to a recipient and time how long it takes.

The total stream delay can probably be calculated to some degree. It should stay fairly consistent regardless of sample rates or channel frequency. The RF bitrate would impact it though. Essentially you would want to measure the time it takes from taking an analog reading to the recipient getting the data? You could model this fairly easily using the RF24 core library, using a simple analog-read, then send the data to a recipient and time how long it takes.

That's what I'm exactly looking for which is measuring the time it takes to transmit and receive an audio signal. But in this case isn't the audio buffering delay included as well? Because I'm trying to obtain something that is close to the actual delay. I just don't know how to implement the code for it.

Why not then just connect two arduinos via a digital pin, start a timer, then have the recipient do a digitalWrite() whenever data is received via radio. It could trigger the transmitter who would then have a rough timing for how long it took from when transmit() is called. There are probably a number of ways to do this, but your results will be skewed a bit if using AVR devices to time things. Myself, I would probably use an Arduino Due and the AutoAnalogAudio library to get accurate results.
If you want help implementing the code for it, I would have to suggest the Arduino Forums or somewhere else, since we normally don't help with writing code etc, this area is more for code issues and potential problems with implementation, specific questions etc.

Why not then just connect two arduinos via a digital pin, start a timer, then have the recipient do a digitalWrite() whenever data is received via radio. It could trigger the transmitter who would then have a rough timing for how long it took from when transmit() is called. There are probably a number of ways to do this, but your results will be skewed a bit if using AVR devices to time things. Myself, I would probably use an Arduino Due and the AutoAnalogAudio library to get accurate results. If you want help implementing the code for it, I would have to suggest the Arduino Forums or somewhere else, since we normally don't help with writing code etc, this area is more for code issues and potential problems with implementation, specific questions etc.

Sorry for the long delay I got sick yesterday. I guess the best way is to actually record the signals physically rather than just record the delay internally :/. I have multiple arudino nano v3.0 which is not the best for audio transmission, however, I'm planning to transmit low frequency only, below 300 Hz. I also have an arduino uno but it's only one that I have. Do you recommend me to record the signals instead?

Yes. You can find more help on how to wire this test up and write the code for it over at https://forum.arduino.cc/

We've pretty much reached the limit of the help we can provide you here.

Alright, thanks a lot for the help @TMRh20 @2bndy5 really appreciated :D