Program Design: Sensor Reading with tight timing AND Arduino BLE

I am writing a program to read a sensor at a fairly rapid rate (100 Hz for example) and I want to send the results over bluetooth to my laptop.

The sensor reading part work fine on it's own. The Bluetooth transfer of data works fine on it's own. When I put the two together the consistent timing of my sensor reading goes out the window! Even if I just add a BLE.begin() the consistent timing is gone.

There are many articles about this problem:
Increase the ADC sample rate - Nano Family / Nano 33 BLE - Arduino Forum

Arduino Nano BLE 33/Sense nRF52840 ADC sampling is interrupted by ArduinoBLE library · Issue #219 · arduino-libraries/ArduinoBLE (github.com)

Delays in code execution when using the ArduinoBLE version 1.1.3 and core for the Arduino Nano 33 BLE version 1.1.6 · Issue #113 · arduino-libraries/ArduinoBLE (github.com)

So my question is, what is the "correct" design pattern I should use to have consistent sensor reads with transmission of the data over bluetooth? I am more than happy to do "batch mode" processing: Read required number of samples from sensor, stop reading and send over bluetooth, then reinitiate the next sensor reading.

I'm at a loss for what to try next to fix the timing consistency problem.

What I'm doing now is shown below. The important parts: Timer Interrupt routine (TimerHandler0 routine) just sets a flag, that the main loop (Section labelled: "4.2 BURST Mode") checks and then uses the flag to take a reading.


*/
#include <Arduino.h>
#include <ArduinoBLE.h>
#include <Wire.h>
#include <Adafruit_Sensor.h>
//#include <Adafruit_BNO055.h>
//#include <utility/quaternion.h>
#include <Arduino_LSM9DS1.h>

#define TIMER_INTERRUPT_DEBUG         0
#define _TIMERINTERRUPT_LOGLEVEL_     3
//#include "NRF52_MBED_TimerInterrupt.h"

#include <mbed.h>

//----------------------------------------------------------------------------------------------------------------------
// BLE UUIDs
//----------------------------------------------------------------------------------------------------------------------
#define BLE_UUID_ENVIRONMENT                      "181C" //Service

#define BLE_UUID_RUNMODE                          "2A56" //Characteristic for string writing and reading.

#define BLE_UUID_CONTMODE_STRUCTDATA              "2A57" //Characteristic for calibration string writing.
                                                          // As a struct.  Most efficient method vs. a string.

#define BLE_UUID_BURSTMODESTRUCTDATA              "2A58"  //Characteristic for data as as struct in burst mode
                                                          // which will return multiple structs in an array

#define BLE_UUID_NUMPTS                           "2A21" // Number of points to record when in burst mode.  TBD

#define BLE_UUID_BURST_OPMODE                    "2A4E"  // Mode of operation.
                                                          // To be defined:  False=Continuous, True=burst mode.

//----------------------------------------------------------------------------------------------------------------------
// BLE
//----------------------------------------------------------------------------------------------------------------------
#define BLE_DEVICE_NAME                           "Arduino Nano 33 BLE"
#define BLE_LOCAL_NAME                            "SensorMonitor"  //The name that Python uses to find device

//----------------------------------------------------------------------------------------------------------------------
// CONSTANTS
//----------------------------------------------------------------------------------------------------------------------
#define LED_BLUE_PIN          24
#define SAMPLE_RATE           20    // ms Delay.  How often do we read and update BLE data.  
#define TIMER0_DURATION_MS    5     //How often the Timer loop will actually go in and check if the TimerFlag
                                    // is set meaning time to take a reading.

#define NUMPTS_BURSTMODE      100  //Numer of points to record when in burst mode
#define NUMPTS_BURST_PERSEND  10 //Number of points to send per write to characteristic.
                                // * because you can't send them all at once with 244 bytes limit per send.
                                // * normally don't need to change this unless the struct size changes


//----------------------------------------------------------------------------------------------------------------------
// STRUCTURE DEFINITION
// Note:  The unpacking in Python needs to match the byte configuration of the struct here.
//----------------------------------------------------------------------------------------------------------------------
    // uint_8 = 1 byte
    // int = 4 bytes
    //unsigned long = 4 bytes
    //unisgned short =u_int16_t = 2 bytes

typedef struct  __attribute__ ((packed)) {
  unsigned long timeread;
  
  int ax;
  int ay;
  int az;
 
}sensordata ;

sensordata d;

//----------------------------------------------------------------------------------------------------------------------
// TIMER SETUP AND BURST MODE CONFIG
//----------------------------------------------------------------------------------------------------------------------

//Define array of structs for BURST MODE
//sensordata burstdata[NUMPTS_BURSTMODE];
sensordata burstdata[NUMPTS_BURSTMODE];

sensordata *burstdataptr=&burstdata[0] ;

bool timerFired=false;  //Flag that the timer has fired and you should read the sensor
bool NRF52Run=false;

// Init NRF52 timer NRF_TIMER3
// NRF52_MBED_Timer ITimer0(NRF_TIMER_3);

// Init MBED Timer
mbed::Ticker mytimer;


volatile uint32_t preMillisTimer0 = 0;
volatile u_int16_t ptsread=0; // Initialize pt counter

bool toggle0 = false;

unsigned long lastTimer=0;

unsigned long previousMillis = 0;  // last time the reading was done, in ms

//----------------------------------------------------------------------------------------------------------------------
//  #########################   FUNCTION DEFINITIONS.  NEED FOR PLATFORMIO  (not Arduino)      #########################
//
//      https://docs.platformio.org/en/latest/faq.html#convert-arduino-file-to-c-manually

void writeHandler(BLEDevice central, BLECharacteristic characteristic);
void updateSensorStructData();
void updateSensorData();
sensordata readSensorDataAsStruct();
sensordata dummyread();
void TimerHandler0();
void setupBluetooth();

//----------------------------------------------------------------------------------------------------------------------


// ********************** INITIALIZATION **************************************

//Setup the BNO055 sensor
//Adafruit_BNO055 myIMU=Adafruit_BNO055();

BLEService mainService(BLE_UUID_ENVIRONMENT);  

// A FLAG to stop and stop reading and updating data.
BLEBoolCharacteristic runmodeChar(BLE_UUID_RUNMODE,BLERead | BLEWrite | BLENotify);

BLECharacteristic contmodestructDataChar (BLE_UUID_CONTMODE_STRUCTDATA,BLERead | BLENotify, sizeof(d)  );

//New Additions to support burst mode
BLEBoolCharacteristic burstoperationmodeChar (BLE_UUID_BURST_OPMODE,BLERead|BLEWrite );
BLEUnsignedShortCharacteristic burstmodenumptsChar (BLE_UUID_NUMPTS,BLERead|BLEWrite );
// Revise to send a subset of points, not total point count.
BLECharacteristic burstmodestructDataChar (BLE_UUID_BURSTMODESTRUCTDATA,BLERead | BLENotify, sizeof(d)*NUMPTS_BURST_PERSEND  );


// *************** DEFINITION OF TIMER HANDLER  ****************

void TimerHandler0(){
  //preMillisTimer0 = millis();
   if (ptsread<NUMPTS_BURSTMODE) {
        //Set flag to take a reading
        timerFired=true;

        
      } //end if
}


// *************** DEFINITION OF BLUETOOTH SETUP ROUTINE ****************

void setupBluetooth(){
  
// begin initialization
  if (!BLE.begin()) {
    Serial.println("starting BLE failed!");

    while (1);
  }

  /* Set a local name for the BLE device
     This name will appear in advertising packets
     and can be used by remote devices to identify this BLE device
     The name can be changed but maybe be truncated based on space left in advertisement packet
  */

  BLE.setDeviceName( BLE_DEVICE_NAME );
  BLE.setLocalName( BLE_LOCAL_NAME );
  BLE.setAdvertisedService( mainService ); // add the service UUID
  

// ********************** EVENT HANDLERS  **************************************
  runmodeChar.setEventHandler(BLEWritten, writeHandler);
    //Should expand to more handles and less inline code.  TBD

// ********************** ADD CHARACTERISTICS **********************************    
  mainService.addCharacteristic(runmodeChar); //Add characteristic to test writing 

  mainService.addCharacteristic(contmodestructDataChar); //Characteristic that will send struct.

  mainService.addCharacteristic(burstoperationmodeChar); //Characteristic that will allow user to set operating mode.
  
  mainService.addCharacteristic(burstmodenumptsChar); //Characteristic that will allow user to set operating mode.
  
  mainService.addCharacteristic(burstmodestructDataChar); //Characteristic that will allow user to set operating mode.

 
 // ********************** DESCRIPTORS TO IMPROVE READABILITY*******************
  BLEDescriptor runmodeDescriptor("2901", "Run Flag:  True=Run, False=Stop.  Needs to be true to read sensor");
  runmodeChar.addDescriptor(runmodeDescriptor);

  BLEDescriptor structdataDescriptor("2901", "CONTINUOUS MODE:  Data as a C Struct");
  contmodestructDataChar.addDescriptor(structdataDescriptor);
  

  BLEDescriptor modeDescriptor("2901", "BURST MODE FLAG: True=Burst Mode, False=Continuous");
  burstoperationmodeChar.addDescriptor(modeDescriptor);
    
  BLEDescriptor burstnumptsDescriptor("2901", "BURST MODE:  Set Number of Pts to collect in burst mode");
  burstmodenumptsChar.addDescriptor(burstnumptsDescriptor);

  BLEDescriptor burstmodestructdataDescriptor("2901", "BURST MODE:  Data as an array of C Structs, numpts long");
  burstmodestructDataChar.addDescriptor(burstmodestructdataDescriptor);

  // *********************** SET DEFAULT VALUES *********************************

  burstmodenumptsChar.setValue(NUMPTS_BURSTMODE); //Set default number of points for burst mode.
  burstoperationmodeChar.setValue(false);  //default to continuous mode

  runmodeChar.setValue(false); //Don't start running immediately...alow for config
  

  //Add the Service
  BLE.addService(mainService);

  /* Start advertising BLE.  It will start continuously transmitting BLE
    advertising packets and will be visible to remote BLE central devices
    until it receives a new connection */

  BLE.advertise();
  Serial.println("Bluetooth device advertising, waiting for connections...");

} //End Setup Bluetooth


// ********************** SETUP **************************************
void setup() {

  Serial.begin(115200);    // initialize serial communication
  
  while (!Serial );
 
  delay(1000);

 
  setupBluetooth();

  //Setup Onboard IMU
  if (!IMU.begin()) {
    Serial.println("Failed to initialize onboard IMU!");
    while (1);
  }

  //IMU Continuous reading mode..Always get the latest sample
  IMU.setContinuousMode();

  pinMode(LED_BUILTIN, OUTPUT); // initialize the built-in LED pin to indicate when a central is connected
  pinMode(LED_BLUE_PIN, OUTPUT);

 

} //End Setup


// ********************** MAIN LOOP **************************************
void loop() {

  // wait for a BLE central
  BLEDevice central = BLE.central();
  // *** 1.  ARE YOU CONNECTED TO BLUETOOTH:  if a central is connected to the peripheral:

  if (central) {
    Serial.print("Connected to central: ");
    // print the central's BT address:
    Serial.println(central.address());
    // turn on the LED to indicate the connection:
    digitalWrite(LED_BUILTIN, HIGH);


    Serial.println("Waiting for run mode to be set TRUE.");

    // *** 2. WAIT FOR THE RUN FLAG TO START   
    while(!runmodeChar.value())
    {
      //Do nothing until the run mode is set.
       //Serial.println("Waiting on run mode flag...");
      BLE.poll();  //TODOL  Not sure if this is needed??
    }
    // Serial.print("Current run mode: ");
    // Serial.println(runmodeChar.value());


    // *** 3.  READ THE OPERATING MODE Characteristic so we know which mode to operate in.
    bool currentopmode = burstoperationmodeChar.value();
    Serial.print("Current operating mode: ");
    if(currentopmode==false){
      Serial.println("Continuous");
    } else{
      Serial.println("Burst");
    }

   //BLE.poll();  //TODOL  Not sure if this is needed??

    // *** 4. MAIN LOOP OF RUNNING AND COLLECTING READINGS.
      // ********************************************************************
      // ** 4.1  Continuous Mode **
      // ********************************************************************
    if(currentopmode==false){
       // ** Continuous Mode
        Serial.println("Entered Continuous Run Mode.");
          // while the central is connected:
          while (central.connected() && runmodeChar.value() ) {
              unsigned long currentMillis = millis();

              // ####################################################
              // if time interval have passed, check the value:
              // ####################################################
              if (currentMillis - previousMillis >= SAMPLE_RATE) {
                  previousMillis = currentMillis;
                  
                  //Read the datafrom sensor
                  sensordata data = readSensorDataAsStruct();

                  //Update characteristic
                  contmodestructDataChar.writeValue((uint8_t *)&data, sizeof(data));
              } //end if
          }//end while
      
    }//end Continuous Mode 


      // ********************************************************************
      // ** 4.2  BURST Mode **
      // ********************************************************************
    if (currentopmode==true) {
        // ** Burst Mode **
          Serial.println("Entered Burst Run Mode.");
          //BLE.poll();
          // Start the timer.  Try MBED Ticker
          //Now attach interrupt to MBED so it can start 
          Serial.println("Burst Mode:  Timer Interrupt attached.  looping.");
          mytimer.attach(&TimerHandler0, SAMPLE_RATE/1000.);

                  

          while (true ){

            // if (millis()-lastTimer > TIMER0_DURATION_MS){

              lastTimer=millis();

              // Step 1:  If Flag is true, take a reading
              if(timerFired==true){
                  //take a reading
                  *burstdataptr=readSensorDataAsStruct();
                  
                  //Increment pointer
                  ++burstdataptr;
                  //Increment points read
                  ++ptsread;

                  //ReSet flag to false so we can detect next timer firing event
                  timerFired=false;

              } //End TimerFlag


              //Step 2:  If we have the number of samples required.
              if(ptsread==NUMPTS_BURSTMODE){
                  // Step 2.1:  ** Stop Timer
                  Serial.println("Burst Mode:  Stopping Timer and processing points collected.");
                   //ITimer0.stopTimer();
                   mytimer.detach();
                   //BLE.poll();

                  //Step 2.2:  ** Write array to bluetooth
                      // Break Array into chunks to send that are under the max limit of 244 bytes.
                      int packetbytes=NUMPTS_BURST_PERSEND*sizeof(sensordata);
                      //Initialize pointer
                      sensordata *loopptr=&burstdata[0];
                      sensordata *loopend=loopptr+NUMPTS_BURSTMODE;

                      //Send update to Bluetooth
                      //Original command tried to send all data at once.
                      //  * Exceeds the max allowable of 244 bytes per send.  Look in github
                      //  * or in source code for MTU and maxlength.
                      // burstmodestructDataChar.writeValue( (uint8_t *) &burstdata, sizeof(burstdata) );

                      

                      Serial.println("Burst Mode:  Writing data to bluetooth.");
                      while (loopptr!=loopend){
                      

                        burstmodestructDataChar.writeValue( (uint8_t *) loopptr, packetbytes );
                 
                        //Increment pointer
                        loopptr+=NUMPTS_BURST_PERSEND;
                        
                      }//end while loop writing to bluetooth
                  
                  //BLE.poll();  //TODO:  Not sure if this is needed??
                  
                  //Step 2.3:  ** Reset pointers and counter flags
                  ptsread=0;
                  //loopptr=&burstdata[0];
                  burstdataptr=&burstdata[0]; 
                  
                  //** restart timer
                  //ITimer0.restartTimer();
                  //mytimer.attach(&TimerHandler0, SAMPLE_RATE/1000.);

                  //Step 2.4:  Break out of while loop.
                  break;

              }//End Burst mode point number check


            // } //end Time duration check

          }//end while 

        Serial.println("Exiting Burst Mode loop processing");

    } // end Burst mode 

    
  } //end if Central connected

 
  
}// End Loop() function

// ********************** SUPPORT FUNCTIONS **************************************

sensordata readSensorDataAsStruct(){
  /*  Read the current data values.
        Returns a Struct with the data.  */

  
  // Initialize
  static sensordata datareading;
  static float ax,ay,az;

  unsigned long tnow=millis();

  IMU.readAcceleration(ax,ay,az);

  // Serial.print("Sensor read: x,y,z=");
  // Serial.print(ax);
  // Serial.print("|");
  // Serial.print(ay);
  // Serial.print("|");
  // Serial.println(az);

  //Declare struct and populate
  //The scaler 10000 converts a float so it can be sent as an int.
  //It will be decoded back to float on receiving side.
  

  datareading.timeread=tnow;
  datareading.ax=(int) (ax*10000);
  datareading.ay=(int) (ay*10000);
  datareading.az=(int) (az*10000);
  

  return datareading;
  
}

void writeHandler(BLEDevice central, BLECharacteristic characteristic){


  //Event Handler.  Fires when something is written to the string characteristic
  
  // Serial.print("WriteHandler UUID: ");
  // Serial.println(characteristic.uuid());
  u_int8_t setval=0;

  int valuelen = characteristic.valueLength();  //Get Size
  Serial.print("writeHandler:  Size Written=");
  Serial.print(valuelen);
  Serial.print("| Data= ");

  for (int i=0;i<valuelen ;i++) {
    setval=(u_int8_t)characteristic.value()[i];
    Serial.print(setval);
  }
  Serial.println("");

  if (setval==false)
    {
      //If false -> in stop Mode
      Serial.println("runmode set to FALSE");
    } else
    {
      Serial.println("runmode set to TRUE");
    }

} //End Writehandler

sensordata dummyread(){

  unsigned long tnow=millis();

  //The scaler 10000 converts a float so it can be sent as an int.
  //It will be decoded back to float on receiving side.
  sensordata datareading;

  datareading.timeread=tnow;
  datareading.ax=20000;
  datareading.ay=30000;
  datareading.az=40000;
  

  return datareading;
  
}

A sample of the timing inconsistencies when trying to read the onboard LSM9DS1 accelerometer at 100 Hz (10ms) is shown below. These don't happen when Arduino BLE is NOT present in the program.

Confused and hoping for some good troubleshooting / program design tips.

Greatly appreciated.

I think first you should questions whether you should have 100Hz data send over BLE. BLE sensors are typically connected to a mobile phone and then some useful information is shown to the user. Humans cannot process this kind of data directly. So, when you process the data in the sensor instead of sending raw data you might be able to reduce the information to something useful that you can update every second or so.

Next you have to understand how BLE works on the Arduino Nano 33 BLE. Some of the low level stuff is handled by the radio peripheral but other parts run on the same processor as your code. That will interfere with your application.

Since you did not write all of the code you do not know how much of the time and how often the BLE stack runs. You will have to do some experiments and figure out whether you can fit your data collection into the time available. And it is not primarily about the total amount of time. There is plenty of processing time available. The processor is quite fast. It is more about the intervals at which you need to get data and what kind of hardware (timer, easy DMA ...) you can use to remove jitter.
Worst case you may not be able to use BLE and your sensor at the same time.

If you describe what you want to do with the data, maybe I can provide you some more specific hints.

1 Like

Great comments and thank you for sharing.

I don't need to be continually sharing data at 100 Hz over Bluetooth and I should have been more clear in my explanation....apologies. My application is: read accelerometer data at 100Hz for a short period of time.....maybe 1.3 seconds to get 128 samples, then stop collecting data and send those 128 samples to my laptop for FFT. Yes, I am thinking of doing the FFT on the Arduino and only sending the results but the problem remains the same: how can I originally get those 128 samples at 100Hz sampling with no interruptions/jitter? I'd like to do more than 128 but let's use that number for an example for now.

For all the reasons you highlight, which you no doubt understand WAY better than I, it seems getting 1.3 secs of uninterrupted processor attention is my challenge. It feels like I need to so something like this in the program:

  • Not start BLE at all on program startup.
  • Read data from Sensor when there is nothing else going on.
  • Stop data collection.
  • Start BLE, and look for laptop to upload to.
  • Upload data to laptop.
  • Disconnect all BLE connections and shut BLE down completely.
  • Repeat above data collection and send process.

Is that how this is normally handled? I think I can make it work, but am looking for broad design or program structure guidelines for how experts like you would handle this task.

Thanks again....I'm learning a lot!

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