NRF52840 BLE UART package collisions

61 Views Asked by At

Using to following code on a XIAO Seeed nrf52840, and connecting it to the Bluefruit connect app, the device freezes when a motion is detected and the line "tap detected" is sent out via blueart.print(). There are also other situations where the device freezes (I assume for the same reason), but this one is the only one that can be reproduced reliably.

Here's the code

bool DEBUG = true;  // setting this prints infos over Serial.print()

#include <Arduino.h>
#include <bluefruit.h>

//#include <Adafruit_LittleFS.h>
#include <InternalFileSystem.h>

#include <Wire.h>  // i2c library

// function prototypes
void debugPrint(String message, String end="\n"); // this is needed if optional arguments are present

// BLE Service
BLEDis bledis;       // device information
BLEUart bleuart;     // uart over ble
BLEBas blebas;       // battery level
int bleConn = 0;     // variable to store connection status
long startTime = 0;  // used for delaying sleep mode after BLE disconnect

// string to be received by BLE
String receivedBLEMessage = "";

// Battery Status. Charge controller is TI BQ25100 - Datasheet: https://www.ti.com/lit/ds/symlink/bq25100.pdf
const int batteryCheckInterval = 300000; // 300s = 5min
// const uint16_t batteryCheckInterval = 5000;  // 5s -> DEBUG

float batteryVoltage = -1.0;            // Sentinel value to indicate not initialized state
float batteryPercentage = -100;         // Sentinel value to indicate not initialized state
const float batteryMaxVoltage = 4.2;
long previousBattReadMillis = 0;

// IMU Bosch BMI160
#include <BMI160Gen.h>
const int bmi160_i2c_addr = 0x69;  // i2c address
#define WAKEUP_PIN PIN_A0          // Pin A0 acts as intterupt pin
int GyrXRaw, GyrYRaw, GyrZRaw;
float GyrX, GyrY, GyrZ;
int AccXRaw, AccYRaw, AccZRaw;
float AccX, AccY, AccZ;
float temperature = 0;
int imuRawTemp = 0;

// The sleep mode is entered when there are no sync messages received for "sleepTimeout" duration or the device is disconnected.
uint32_t sleepTimeout = 300000;  // 300s waiting after BLE disconnect prior going to sleep mode

// Magnetometer QMC5883L
#include <QMC5883LCompass.h>
QMC5883LCompass Mag;
int MagX, MagY, MagZ;

// Settings for transmitting of data over BLE using bulk sending
const uint16_t measurementInterval = 40;  // Sampling in ms
uint16_t xfer_batch_cnt = 0;              // Current count of measurments in the transfer batch
const uint16_t xfer_batch_size = 2;       // Size of transfer batch. Results in sending with (25/2)Hz
uint32_t currentTime = 0;
uint32_t previousTime = 0;
String message = "";

// power indicator LED
unsigned long previousLEDOnMillis = 0;
const long onInterval = 250;
unsigned long previousLEDOffMillis = 0;
const long offInterval = 3000;
int indicatorLEDState = LOW;
int battStatus = 0;

// Power saving mode vars
uint32_t lastActivityTimer = currentTime;

// Message Counter
uint32_t messageCounter = 0;

// rssi value
int8_t rssi = -128;  // min. value means no connection

// var to hold device MAC/ID
uint8_t mac_address[6];
char myAdvertisedName[30] = "Seeed nrf52840";
char myURL[20] = "company.org";

void setup() {
 
  if (DEBUG) {
    Serial.begin(115200);
    // wait a max of 2.0 sec for Serial to become ready
    for (int i = 0; i <= 10; i++) {
      if (!Serial) {
        delay(200);
      }
    }
  }

  // ADC settings to read Battery status
  analogReference(AR_INTERNAL_2_4);  //Vref=2.4V
  analogReadResolution(12);          //12bits

  //set the pin to read the battery voltage
  pinMode(PIN_VBAT, INPUT);
  pinMode(VBAT_ENABLE, OUTPUT);
  digitalWrite(VBAT_ENABLE, LOW);
  debugPrint("set the pin to read the battery voltage");

  //set battery charge speed to 50mA
  digitalWrite(PIN_CHARGING_CURRENT, HIGH);  // low for 100mA - not increase for batteries with 50mAh!!
  debugPrint("set battery charging current to 50mA");

  Wire.begin();
  debugPrint("I2C init started");

  // Initialize IMU Bosch BMI160
  BMI160.begin(BMI160GenClass::I2C_MODE, Wire, bmi160_i2c_addr, WAKEUP_PIN);
  BMI160.attachInterrupt(bmi160_intr);
  BMI160.setIntTapEnabled(true);
  debugPrint("BMI160 init completed");

  // Initialize Magnetometer QMC5883L
  Mag.init();
  Mag.setSmoothing(4, 1);  // IMU samples at 100Hz - 4 steps smoothing for 25Hz output rate
  debugPrint("Mag init completed");

  // get device ID
  char buff[6];
  get_id_address(mac_address);
  sprintf(buff, "%02lX%02lX", mac_address[0],mac_address[1]);
  strcat(myAdvertisedName, buff);
  debugPrint("Device ID: " + String(myAdvertisedName));

  debugPrint("Starting BLE init");
  // BLE Service setup start
  Bluefruit.autoConnLed(true);

  // Config the peripheral connection with maximum bandwidth
  Bluefruit.configPrphBandwidth(BANDWIDTH_MAX);

  Bluefruit.begin();
  Bluefruit.setTxPower(0);  // lower value as -8 makes system not reliable -> weak output.
  // remove old data, w/o the following two lines the name is not updated
  Bluefruit.Advertising.clearData();
  Bluefruit.ScanResponse.clearData();
  Bluefruit.setName(myAdvertisedName);
  Bluefruit.Periph.setConnectCallback(connect_callback);
  Bluefruit.Periph.setDisconnectCallback(disconnect_callback);

  // Configure and Start Device Information Service
  bledis.setManufacturer(myURL);
  bledis.setModel(myAdvertisedName);
  bledis.begin();
  debugPrint("BLE init completed");

  // do an initial Battery Level readout
  readBattery();
  debugPrint("read battery in setup()");

  // Configure and Start BLE Uart Service
  bleuart.begin();
  debugPrint("Starting BLE UART Service");

  // Set up and start BLE advertising
  startAdv();

  // initialize LED Pins (turn all LEDs off)
  pinMode(LED_RED, OUTPUT);
  pinMode(LED_GREEN, OUTPUT);
  pinMode(LED_BLUE, OUTPUT);
  debugPrint("Switch all LEDs off at init");

  // do some blinky
  for (int i = 0; i <= 5; i++) {
    digitalWrite(LED_GREEN, LOW);
    delay(100);
    digitalWrite(LED_GREEN, HIGH);
    delay(100);
  }
  debugPrint("Blink green 5x");

  // setup battery service
  blebas.begin();
  updateBatteryLevel();  // push initial battery level
  debugPrint("Started battery service");
} 
/* ****************************************************************************
   end setup
 * ***************************************************************************/

/* ****************************************************************************
   functions to get MAC and device ID

   This can be used as unambiguous name for BLE advertising

   source: https://forum.seeedstudio.com/t/how-can-i-get-xiao-nrf52840-sense-board-info-by-code/267316/4
 * ***************************************************************************/
 void get_id_address(uint8_t mac_address[6]) {
    unsigned int device_addr_0 = NRF_FICR->DEVICEADDR[0];
    unsigned int device_addr_1 = NRF_FICR->DEVICEADDR[1];
    const uint8_t* part_0 = reinterpret_cast<const uint8_t*>(&device_addr_0);
    const uint8_t* part_1 = reinterpret_cast<const uint8_t*>(&device_addr_1);
    mac_address[0] = part_1[3];   //changed from get_mac_address elements 0,1
    mac_address[1] = part_1[2];
}

void get_mac_address(uint8_t mac_address[6]) {
    unsigned int device_addr_0 = NRF_FICR->DEVICEADDR[0];
    unsigned int device_addr_1 = NRF_FICR->DEVICEADDR[1];
    const uint8_t* part_0 = reinterpret_cast<const uint8_t*>(&device_addr_0);
    const uint8_t* part_1 = reinterpret_cast<const uint8_t*>(&device_addr_1);
    mac_address[0] = part_1[1];
    mac_address[1] = part_1[0];
    mac_address[2] = part_0[3];
    mac_address[3] = part_0[2];
    mac_address[4] = part_0[1];
    mac_address[5] = part_0[0];
}
/* ***************************************************************************/
// end: functions to get MAC and device ID 

// Note: the default value end="\n" is defined in the function prototype at the top of this file
void debugPrint(String message, String end) {
  // abbreviation for printing in debug mode
  // end can be used to mimic functionality of print() and println() with
  // end="" and end="\n", respectively
  if (DEBUG) {
    Serial.print(message + end);

    // NOTE: this freezes the whole device as soon as a BLE connection is established and a motion is detected
    // also send via BLE UART
    uint16_t conn_hdl = Bluefruit.connHandle();
    if (conn_hdl != BLE_CONN_HANDLE_INVALID) {
      bleuart.print(message + end);
    }
  }
}

void startAdv(void) {
  /*
   * Start BLE Advertising if not connected
   */
  // Advertising packet
  Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE);
  debugPrint("advertising: Flags set");
  Bluefruit.Advertising.addTxPower();
  debugPrint("advertising: tx power set");


  // Include bleuart 128-bit uuid
  Bluefruit.Advertising.addService(bleuart);
  debugPrint("advertising: service added");


  // Secondary Scan Response packet (optional)
  // Since there is no room for 'Name' in Advertising packet
  Bluefruit.ScanResponse.addName();
  debugPrint("advertising: name added");


  /* Start Advertising
   * - Enable auto advertising if disconnected
   * - Interval:  fast mode = 20 ms, slow mode = 152.5 ms
   * - Timeout for fast mode is 30 seconds
   * - Start(timeout) with timeout = 0 will advertise forever (until connected)
   * 
   * For recommended advertising interval
   * https://developer.apple.com/library/content/qa/qa1931/_index.html   
   */
  Bluefruit.Advertising.restartOnDisconnect(false);
  debugPrint("advertising: restartOnDisconnect set");
  Bluefruit.Advertising.setInterval(32, 244);  // in unit of 0.625 ms
  debugPrint("advertising: interval set");
  Bluefruit.Advertising.setFastTimeout(10);    // number of seconds in fast mode
  debugPrint("advertising: fast timeout set");
  Bluefruit.Advertising.start(0);              // 0 = Don't stop advertising after n seconds

  debugPrint("advertising started");
  // add small delay to allow for advertising to start before
  // continuing the main loop
  delay(100);
}


void connect_callback(uint16_t conn_handle) {
  /*
  * callback invoked when central connects
  */

  // Get the reference to current connection
  BLEConnection* connection = Bluefruit.Connection(conn_handle);

  // Start monitoring rssi of this connection
  connection->monitorRssi();

  char central_name[32] = { 0 };
  connection->getPeerName(central_name, sizeof(central_name));

  debugPrint("Connected to " + String(central_name));

  // change variable to tell I'm connected!
  bleConn = 1;

  // mark connection event as activity
  lastActivityTimer = currentTime;
}

void disconnect_callback(uint16_t conn_handle, uint8_t reason) {
  /**
 * Callback invoked when a connection is dropped
 * @param conn_handle connection where this event happens
 * @param reason is a BLE_HCI_STATUS_CODE which can be found in ble_hci.h
 * 
 * task: Put device to sleep mode if disconnected
 */
  (void)conn_handle;
  (void)reason;

  debugPrint("Disconnected, reason = 0x", "");
  debugPrint(String(reason, HEX));

  // change variable to tell I'm NOT connected!
  bleConn = 0;

  // store current millis as start time of disconnect event
  startTime = currentTime;

  // mark disconnect event as activity -> prevents shutting down if BLE is interrupted
  lastActivityTimer = currentTime;

  // Bluefruit.Advertising.start(0);
  startAdv();
}

void gotoSleep() {

  debugPrint("power down");

  // this pin (WAKE_LOW_PIN) is pulled up and wakes up the device when externally connected to ground.
  pinMode(WAKEUP_PIN, INPUT_PULLUP_SENSE); 

  // set interrupt (so the device will wake up again on tap detected)
  BMI160.attachInterrupt(bmi160_intr);
  BMI160.setIntSigMotEnabled(true);

  // do some blinky
  for (int i = 0; i <= 5; i++) {
    digitalWrite(LED_RED, LOW);
    delay(100);
    digitalWrite(LED_RED, HIGH);
    delay(100);
  }

  // to reduce power consumption when sleeping, turn off all your LEDs (and other power hungry devices)
  digitalWrite(LED_RED, HIGH);    // Red internal LED
  digitalWrite(LED_GREEN, HIGH);  // Green internal LED
  digitalWrite(LED_BLUE, HIGH);   // Blue internal LED

  // // suspend gyro, mag and put acc in low power mode (in order to still wake up on significant motion)
  debugPrint("sending BMI160 to sleep...", "");
  /* the following line is a custom function added to BMI160.cpp. It incorporates a small change
     to the existing BMI160Class::suspendIMU(), but instead of suspending Acc we put it into
     low power mode so that it can detect taps and wake up the nrf */
  BMI160.goToSleep();
  debugPrint("done");
  delay(100);

  //Before sleep, detach the USB port
  USBDevice.detach();

  // power down nrf52.
  sd_power_system_off();  // this function puts the whole nRF52 to deep sleep (no Bluetooth). If no sense pins are setup (or other hardware interrupts), the nrf52 will not wake up.
}


float convertRawGyro(int gRaw) {
  // default Settings for 250°/s of Gyro
  float g = (gRaw * 250.0) / 32768.0;
  return g;
}


float convertRawAcc(int aRaw) {
  // Default Setting is 8G for Accelerometer
  float g = (aRaw * 8.0) / 32768.0;
  return g;
}


void bmi160_intr(void) {
  /*
   * Function triggered on tap by IMU.
   */
  //  digitalWrite(LED_GREEN, LOW);  delay(100);
  //  digitalWrite(LED_GREEN, HIGH);

  // this line causes the device to freeze
  // when sent via bleuart after a BLE connection was establishe
  debugPrint("tap detected");
}


// linear approximation of battery curve derived from battery discharching
// at around 3.65 the voltage colapses
float getPercentage(float voltage) {
  if (voltage >= batteryMaxVoltage) {
    return 100.0;
  } else if (voltage > 3.7) {
    return (voltage - 3.7) * 2 * 100;
  } else {
    return 0.0;
  }
}


void readBattery() {
  /*
   * switches charging off, reads battery voltage, afterwards switch on charging again
   */
  double vBat;
  // 2.4 V: reference value, 1510 Ohm & 510 Ohm: Resistors of the voltage divider, 0-4095: ADC range, 1.027: calibration factor?
  vBat = ((((float)analogRead(PIN_VBAT)) * 2.4) / 4096.0) * 1510.0 / 510.0 * 1.027;  // Voltage divider from Vbat to ADC

  // perform EMA only when it is not the first read
  if (batteryVoltage == -1.0 || vBat > batteryVoltage + 0.15) {  // sentinel value for not init || "hot-start" as voltage takes some time to converge at beginning
    batteryVoltage = vBat;
  } else {
    // EMA: use 95% of previous values and 5% of new value
    batteryVoltage = batteryVoltage * 0.95 + 0.05 * vBat;
  }

  batteryPercentage = getPercentage(batteryVoltage);

  // push current charging status to BLE BAS
  //blebas.write(batteryPercentage);

  debugPrint("Battery Percentage: " + String(batteryPercentage), "");
  debugPrint("; vBat: ", "");
  debugPrint(String(vBat));
}

void updateBatteryLevel() {
  // Assuming batteryPercentage is a float between 0.0 and 100.0
  // Convert it to an integer between 0 and 100
  uint8_t batteryLevel = static_cast<uint8_t>(batteryPercentage);

  debugPrint("Battery Service updated percentage: " + String(batteryLevel));

  // Update the BLE Battery Service
  blebas.write(batteryLevel);
}


void statusIndication() {
  /*
   * Blinks the internal LED to show that the device is up&running
  */
  if (currentTime - previousLEDOnMillis >= onInterval) {
    digitalWrite(LED_GREEN, HIGH);
    previousLEDOnMillis = currentTime;
    // debugPrint("set status led to OFF");
  }

  if (currentTime - previousLEDOffMillis >= offInterval) {
    digitalWrite(LED_GREEN, LOW);
    previousLEDOffMillis = currentTime;
    // debugPrint("set status led to ON");
  }
}

/****************************************************
 * start main loop                                  *
 ****************************************************/
void loop() {
  // set global variable currentTime to millis()
  currentTime = millis();

  // function to display device status
  statusIndication();

  // on data receive, read all of it and echo it with " - Ok" suffix
  if (bleuart.available()) {
    do {
      char receivedChar = bleuart.read();  // Read byte to char
      receivedBLEMessage += receivedChar;  // Append char to string
    } while (bleuart.available());

    // remove whitespace (" ","\t","\v","\f","\r","\n")
    receivedBLEMessage.trim();

    debugPrint(receivedBLEMessage + " - OK");
    bleuart.print(receivedBLEMessage + " - OK");

    // check if we received a sync message, if so mark it as activity
    if (receivedBLEMessage.equals("SYNC")) {
      lastActivityTimer = currentTime;  // mark actvity

      debugPrint("received sync msg, marked activity");
    }

    // check if user wants to enforce sleep mode
    if (receivedBLEMessage.equals("SLEEP")) {
      debugPrint("user enforced sleep mode");
      gotoSleep();
    }

        receivedBLEMessage = "";
  } // end: check for incoming message

  // check for inactivity, go to sleep
  if ((currentTime - lastActivityTimer) >= sleepTimeout) {
    debugPrint("going to sleep soon due to inactivity");
    gotoSleep();
  }

  // Read Battery Voltage and update percentage var
  if (currentTime - previousBattReadMillis >= batteryCheckInterval) {
    readBattery();
    updateBatteryLevel();
    previousBattReadMillis = currentTime;
  }

  // go to sleep if battery falls below critical level
  if (batteryPercentage <= batteryPercentageCrit) {
    if (!(IGNORE_GOTOSLEEP)){
      debugPrint("Battery voltage reached critical level");
      gotoSleep();
    }
  }

  if (currentTime - previousTime >= measurementInterval) {
    // debugPrint("start reading sensors");

    // read raw Gyro measurements from BMI160
    BMI160.readGyro(GyrXRaw, GyrYRaw, GyrZRaw);

    // convert the raw gyro data to degrees/second
    GyrX = convertRawGyro(GyrXRaw);
    GyrY = convertRawGyro(GyrYRaw);
    GyrZ = convertRawGyro(GyrZRaw);

    // read raw Accelerometer measurements from BMI160
    BMI160.readAccelerometer(AccXRaw, AccYRaw, AccZRaw);

    // convert the raw gyro data to degrees/second
    AccX = convertRawAcc(AccXRaw);
    AccY = convertRawAcc(AccYRaw);
    AccZ = convertRawAcc(AccZRaw);

    // read Temperature + convert
    imuRawTemp = BMI160.getTemperature();
    // for details on the conversion, see: https://github.com/hanyazou/BMI160-Arduino/blob/master/BMI160.cpp#:~:text=getTemperature()
    temperature = ((float)imuRawTemp / 512.0) + 23;

    // Read Magnetometer Values
    Mag.read();
    MagX = Mag.getX();
    MagY = Mag.getY();
    MagZ = Mag.getZ();

    // Read Bluetooth RSSI Value - Connection Quality
    uint16_t conn_hdl = Bluefruit.connHandle();
    BLEConnection* connection = Bluefruit.Connection(conn_hdl);
    // only read RSSI when a connection is established
    if (conn_hdl != BLE_CONN_HANDLE_INVALID) {
      rssi = connection->getRssi();
    } else {
      rssi = -128;
    }

    /* push sensor readings to BLE UART */
    message += String(AccY) + "," + String(AccX) + "," + String(AccZ) + ","
               + String(GyrY) + "," + String(GyrX) + "," + String(GyrZ) + ","
               + String(MagX) + "," + String(MagY) + "," + String(MagZ) + ","
               + String(temperature) + "," + String(int(batteryPercentage)) + ","
               + String(messageCounter) + "," + String(rssi) + String("\n");

    messageCounter += 1;  // increment message counter for each message sent

    // bulk sending logic (send data if #xfer_batch_size measurements)
    if (++xfer_batch_cnt == xfer_batch_size) {
      bleuart.print(message);
      message = "";
      xfer_batch_cnt = 0;
    }

    // update timer
    previousTime = currentTime;

  } // end: read BMI160 values
}

My assumption is that there is some kind of package collision with bleuart.print() in debugPrint() and in the main loop where measurements are sent in batches with bleuart.print(message).

0

There are 0 best solutions below