Tone output frequency impacts performance

I'm just doubling checking something I noticed and I could not find a topic related to this.

On the RP2040 Connect, with MBED OS, I am trying to sense an analog input at the highest possible sample rate I can. To test is I set tone(8,4000) and feed this output (D8) into the analog sense pin. What I noticed is that as I increase the frequency past about 8KHz, the overall performance of my sense loop drops dramatically. At 4KHz tone output frequency, I was getting about 100ksps (not bad!). If I increase the tone to 8Kz the sample rate drops to about 80-90ksps. If I increase to 16Khz tone output frequency, the sample rate has dropped to about 33ksps.

I'm guessing this is because the RP2040 emulates the tone output with software (running through the MBED OS) which is slowing down execution of my sample loop.
Can someone confirm this? I did not notice this on the Arduino Nano IOT 33, but the sample rates that are much lower, so perhaps it was not a factor.

Not a crushing problem, just curious. I can also use another Arduino to generate the tone output if I need to.

Curious too. Sounds like a small enough sketch, woyncha post it here for us to take a peek and/or replicate your experiments?

I was tots confused until you mentioned using another signal source. Presumably doing would mean no change in sampling rate achievement?

Or…

So.

a7

I don't know but that makes sense. Every instruction takes processor cycles and that will limit how fast you can loop and sample. One the regular Arduino (ATmega chip) PWM is built-into the chip and it doesn't slow-down processing so I wouldn't be surprised if tone() works similarly on that chip.

Unfortunately my sketch is pretty complicated because I have to dump the resulting samples to my PC and display them through another program. It also includes custom libraries for the I/O to the other PC. Its impossible to see the resulting sample rate without that. I will post the sketch as it is, but it probably won't help you much.

I'm guessing the tone is done with some interrupt-driven timer setup by the OS.

Here is the code:

#define FIRMWARE_VERSION "0.5.0"

#include <TimeLib.h>
#include <WiFiNINA.h>
#include <WiFiUdp.h>
#include <Scheduler.h>
#include <adc.h>

#include "IOTDev.h"
#include "wifi_wrapper.h"

#define DATAWINDOWSIZE 256

const char IP_BYTES[4] = {192, 168, 0, 99}; //<<<<<<<<<<<<<<<<<

//pin number definitions
const uint8_t probe0 = A2; //1 Analog Probe
const uint8_t dprobe[4] = {2,3,4,5}; //4 Digital Probes

//Define device parameters here.  Allowable Formats: int32_t, float and String

const int32_t OFF=0;
const int32_t ONE_SHOT=2;
const int32_t CONT_CAPTURE=1;
const int32_t TRIGGERED_CAPTURE=3;
const int32_t RISING_EDGE=0;
const int32_t FALLING_EDGE=1;
const bool ANALOG=0;
const bool DIGITAL=1;


int32_t enable;
//int32_t dataWindowSize=256; //Must be a power of 2
int32_t trigger_level=0x01ff;  //Default trigger value is half-way
int32_t trigger_edge=RISING_EDGE;
bool displayMode=ANALOG;

bool enable_level=0;
int32_t trigger_event=-1;
int32_t trigger_count=0;
int32_t pre_trigger_count=0;


uint16_t raw_data_buffer[DATAWINDOWSIZE];
uint8_t raw_data_ptr;

DataLog <uint16_t> probe0_log;
DataLog <uint16_t> dprobe_log;

//End device parameters

//Declare UDP Message Port
DeviceUDP Udp0;

//Declare IOT Device Instance
Device device;

//Declare a list of possible statuses for the IOT device
const String devStatus_list[4]={"IDLE","RUNNING","STARTING",""};

inline uint8_t bus4(const uint8_t pins[])
    {
      uint8_t b=0;
      b=((uint8_t)digitalRead(2)<<3)+((uint8_t)digitalRead(3)<<2)+((uint8_t)digitalRead(4)<<1)+((uint8_t)digitalRead(5));

      return(b);
    }

void setup() { 
  Serial.begin(9600);
  delay(1500); 
  
  //Set the Static IP Address and Name for the Device
  
  device=Device("TESTBED",IP_BYTES);

  DB_PRINT("##################\n");
  DB_PRINT("Initializing device"+device.hostName+"\n");

  //Connect to WiFI using static IP Address
  connectToWifi(&(device.staticIPAddress),device.hostName);
  
  //Initialize Message Port
  Udp0=DeviceUDP(localPort);
  device.bindUDP(&Udp0);

  DB_PRINT("Adding Device Instance\n");
  //Register List of Status Values
  device.setStatusList(devStatus_list);
  //Set current status
  device.setStatus("STARTING");
  
  //Initialize params and register them on the IOT device
  enable=0;
  device.ADD_PARAM(enable);
  trigger_level=0x01ff;  //Default trigger value is half-way
  device.ADD_PARAM(trigger_level);
  trigger_edge=RISING_EDGE;
  device.ADD_PARAM(trigger_edge);
  displayMode=ANALOG;
  device.ADD_PARAM(displayMode);

  trigger_event=-1;
  trigger_count=0;

  probe0_log=DataLog<uint16_t>("Probe0",DATALOG_MAX_SIZE);
  device.ADD_LOG(probe0_log);
  dprobe_log=DataLog<uint16_t>("DProbe",DATALOG_MAX_SIZE);

  //Add any additional setup code here*/
  pinMode(LED_BUILTIN, OUTPUT); //For DEBUG Purposes
  digitalWrite(LED_BUILTIN,LOW);
  for(int i=0;i<4;i++)
    pinMode(dprobe[i],INPUT);
  
  tone(8,16000);
  
  //End additional setup
  analogReadResolution(10);
 
  DB_PRINT("Setup Completed.\n");
  DB_PRINT("###################\n");

  //Finally set the device to nominal status
  device.setStatus("IDLE");
  //Start secondary communication loop
  Scheduler.startLoop(loop2);
}

void loop() {
  //######Custom loop code here#######
  if(enable==CONT_CAPTURE or enable==ONE_SHOT)
  {
    digitalWrite(LED_BUILTIN,enable);
    if(displayMode==ANALOG)
    {
      for(int i=0;i<DATAWINDOWSIZE;i++)
      {
        probe0_log.appendDatum((uint16_t)analogRead(probe0));
      }

      device.sendLogData(&(probe0_log),displayMode);

      if(enable==ONE_SHOT)  //Disable After One Sample Window If In One-Shot Mode
        enable=OFF;
    }
  }
  
  else if(enable==OFF)
  {
    digitalWrite(LED_BUILTIN,enable);
    enable_level=0;
    trigger_event=-1;
    trigger_count=0;
    pre_trigger_count=0;
    device.flushUDP();
    probe0_log.clear();
    dprobe_log.clear();
  }
  yield();  //Must yield schedule time or loop2 will never run
 //#######End custom loop code#######
}

void loop2()
{
  //Check for incoming messages
  device.checkIO();  //Using lightweight I/O routine to reduce loop time
  yield();  //Must yield schedule time or main loop will never run
}

The tone is enabled in the setup (tone(8,16000);).

The for loop within the loop routine where you see:

probe0_log.appendDatum((uint16_t)analogRead(probe0));

is where I flash sample 256 samples as fast as possible. There is no additional code
other than pushing the resulting analogRead output into a buffer. No I/O is done
until the whole buffer is full.

The only difference in observed sample rate is when I change the tone command in setup to something like tone(8,4000).

I think I answered my own question. Looking in the MBED OS source code there is a file called Tone.cpp which defines the tone() function and also declares a timer and attached a callback (presumably for an interrupt):

ticker.attach(mbed::callback(this, &Tone::toggle), 500000us / frequency );
        if (duration != 0) {
            start_timeout();

This would create a timer which times out every time the output needs to be toggled to create that frequency of oscillation.

The toggle routine is not complex but the overhead of swapping out registers, storing pointers etc. is probably going to be somewhat expensive. Doing it every ~31.25 us is pretty expensive computationally.

So, at least on MBED OS processors do NOT use tone() if you are worried about real-time performance (unless you want something very slow like 1KHz).

1 Like

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