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).