Close

07/24/19 - Maximum transmission throughput of ESP-NOW

A project log for My attempt at an ESP-NOW Mesh

Designing a custom mesh for sensors, home automation and remote controlling and using Espressif's ESP-NOW

David M.David M. 07/31/2019 at 21:010 Comments

To optimize the code, I let the ScanForSlave function run only once because as I discovered earlier, it took up a significant amount of time. Now, the only two functions which up take the rest amount of the time between transmissions is the manageSlave function and the sendData function itself. I decided to see how much time they take, so again, I toggled a pin for each to see how much time each function takes.

Fig. 11 - The amount of time each function takes and the time between transmissions.
            Captured with Five Wire.

It appears that the manageSlave function is taking the majority of the time between transmissions. The time between transmissions is about 13 milliseconds on average. I believe it is pretty unnecessary to check if the slave still exists every transmission, so I will make it run only once a second.

void loop() {
  // In the loop we scan for slave
  digitalWrite(4, HIGH);
  ScanForSlave();
  digitalWrite(4, LOW);
  // If Slave is found, it would be populate in `slave` variable
  // We will check if `slave` is defined and then we proceed further
  while (slave.channel == CHANNEL) { // check if slave channel is defined
    // `slave` is defined
    // Add slave as peer if it has not been added already
    digitalWrite(16, HIGH);
    bool isPaired = manageSlave();
    digitalWrite(16, LOW);
    if (isPaired) {
      // pair success or already paired
      // Send data to device
      digitalWrite(19, HIGH);
      sendData();
      digitalWrite(19, LOW);
    } else {
      // slave pair failed
      Serial.println("Slave pair failed!");
    }
  }
}
 Fig. 12 - Modified sketch which runs ScanForSlave only once.

I also wanted to see the current consumption of such a setup, so I hooked up a current meter to an oscilloscope and observed that the transmission took about 650 microseconds.

Fig. 13 - Tektronix oscilloscope displaying the packet transfer duration and frequency.

It consumed around 120mA during run-time and about 300mA during transmission. This reading, however, included all of the extra peripherals and internal functions, such as LEDs and TTL-to-USB converter. I expect to be able to drop the run-time current down to 80mA. Also, my hope is that I will be able to make use of the ULP core to drop the current consumption down to less than 5mA by the end of the project.

My second step to optimize the code is to make the manageSlave function run only once a second because we don’t really need to know if the receiver is available all the time, just every once in a while.

unsigned long nextManage;

void loop() {
  // In the loop we scan for slave
  digitalWrite(4, HIGH);
  ScanForSlave();
  digitalWrite(4, LOW);
  // If Slave is found, it would be populate in `slave` variable
  // We will check if `slave` is defined and then we proceed further
  if (slave.channel == CHANNEL) { // check if slave channel is defined
    // `slave` is defined
    // Add slave as peer if it has not been added already
    digitalWrite(16, HIGH);
    bool isPaired = manageSlave();
    digitalWrite(16, LOW);
    nextManage = millis() + 10000;
    while (isPaired) {
      // pair success or already paired
      // Send data to device
      digitalWrite(19, HIGH);
      sendData();
      digitalWrite(19, LOW);

      unsigned long t = millis();
      if (t > nextManage) {
        nextManage = t + 10000;
        digitalWrite(16, HIGH);
        isPaired = manageSlave();
        digitalWrite(16, LOW);
      }
    }
    Serial.println("Slave pair failed!");
  }
}
 Fig. 14 - Modified code which runs manageSlave only once every second.
Fig. 15 - The ScanForSlave and manageSlave functions run only once throughout this 
              transaction. Captured with Five Wire.

This change, however, didn't improve the transmission rate by much, meaning that it could be run multiple times if needed. The time between transmissions is about 10 milliseconds, meaning that the rate is at about 100Hz. This rate, however, includes the serial debugging code on both master and slave ESP's. Let's disable the serial initializer on both, and see what difference it makes. I will connect an additional probe on the receiving ESP and one for the sending ESP which triggers when it confirms the packet has been received.

Fig. 16 - Five Wire logic analyzer displaying transmission behavior where data is sent in chunks.

With serial disabled, the transmission now appears to be sent in chunks. Overall, the transmission rate is much faster, averaging out at 1 millisecond per transmission. The only signal that appears to be consistent is the confirmation of the packet sent. Let's not send another packet until the previous one has been confirmed received.

Fig. 17 - Five Wire logic analyzer displaying the new transmission behavior where data is
             sent at regular intervals.

The time between transmissions is only slightly longer, but with the advantage of being sent at regular intervals. This is all fine and dandy, but what we have tested so far is transmission of packets containing only one byte of data. Let's increase the data up to 250 bytes.

Fig. 18 - The transmission rate has decreased due to a bigger packet size.
             Captured with Five Wire.

This time, the transmission takes about 3 milliseconds per packet which sets the frequency of transmission at 330Hz at its best. This is, however, without any additional code to even do anything with said data.

What I'm not sure about is whether this ESP-NOW library is using the secondary core in the background during WiFi transmissions in an aim to allow the sketch to run on a separate core. If it is, then this is as far as we can go performance wise. If not, then we can use the secondary core to do the rest of our work while keeping the transmission rate at 330Hz.

Discussions