XIAO BLE Sense battery level and charging status

Dave, you’re a godsend. Have a great week, good luck with your other stuff.

Well, I’m baaaaaaack

Update to mithundotdas (and anyone else out there who is still interested):

I loaded your test program, and a little experimentation indicated that the problem manifests itself with the analogRead() of the ADC value, not the digitalWrite() of the enable signal.

So…
I wired an external voltage divider that used the XIAO BLE analog pin 3 as the ADC input and digital pin 4 as the enable signal.

Result was: Taa-Daa!

Details:

1.
I connected a 1 Megohm resistor from the + terminal of the battery to Pin A3.

2.
I connected a 560K Ohm from Pin A3 to Pin D4. (The schematic shows 510K here, but my spare parts box didn’t have a 510K, so I used something not too different that I did have.)

3.
I made some definitions in the sketch and changed the formula to take into account the changes.

I started the sketch and tested the reported values on the Serial Monitor and on my phone. (Green LED on the XIAO BLE came on, then turned Blue upon successful connection to Central, ADC voltage values were printed out every second, and the phone app could obtain the battery voltage, as expected.)

I used a generic app, LightBlue, instead of the specific app for this program. It’s somewhat awkward, but for debugging, I don’t have to worry about understanding (and debugging) some specific program other than the one I’m working on with the XIAO BLE.

Bottom line:
The existing board is useless for the application you have in mind without a little (very little, actually) external circuit . (Pardon me if you have heard this before, but don’t those Seeed guys actually test this stuff before releasing hardware and/or software and/or the integration?)

There are other discrepancies on the schematic involving various connections, and it just may happen that my selection of pins for the external voltage divider screw the pooch for other applications, but maybe it gets you past top dead center for further testing.

Final note:
Wiring the new connections on my solderless breadboard was quick and easy, but note that it tends to be pretty noisy unless you dress the leads carefully. Adding a small capacitor from the ADC input pin to ground might help a little. A simple running average in the program will make it settle down a lot. (Or so I claim.)

Here’s my modified sketch:


/**
This prorgam is written to collect IMU data from XIAO BLE Sense and send to EI Blue mobile app. 
EI Blue mobile app uploads the data to Edge Impulse Studio.
Visit https://wiki.seeedstudio.com/XIAO-BLE-Sense-Bluetooth-Usage/ to setup Arduino IDE 
Get EI Blue mobile app from https://github.com/just4give/ei-blue 

Some mods by davekw7x
March, 18, 2022
*/

REDIRECT_STDOUT_TO(Serial);

#include <ArduinoBLE.h>
#include <LSM6DS3.h>
#include <Wire.h>

#define BLENAME                       "EIBLUE"
#define SERVICE_UUID                  "4D7D1101-EE27-40B2-836C-17505C1044D7"
#define TX_PRED_CHAR_UUID             "4D7D1106-EE27-40B2-836C-17505C1044D7"
#define TX_BAT_CHAR_UUID              "4D7D1107-EE27-40B2-836C-17505C1044D7"
#define RX_CHAR_UUID                  "4D7D1108-EE27-40B2-836C-17505C1044D7"
#define SAMPLING_RATE                 50   //Hz
#define DURATION                      1 //seconds


BLEService bleService(SERVICE_UUID); // Bluetooth® Low Energy LED Service

// Bluetooth® Low Energy LED Switch Characteristic - custom 128-bit UUID, read and writable by central
BLEStringCharacteristic rxCharacteristic(RX_CHAR_UUID, BLEWrite, 1024);
BLEStringCharacteristic txPredCharacteristic(TX_PRED_CHAR_UUID, BLERead | BLENotify, 1024);
BLEStringCharacteristic txBatCharacteristic(TX_BAT_CHAR_UUID, BLERead | BLENotify, 1024);

LSM6DS3 myIMU(I2C_MODE, 0x6A);    //I2C device address 0x6A
float aX, aY, aZ, gX, gY, gZ;
const float accelerationThreshold = 2.5; // threshold of significant in G's


const double vRef = 3.3; // Assumes 3.3V regulator output is ADC reference voltage
const unsigned int numReadings = 1024; 
#define EXTERNAL_VBAT PIN_A3
#define EXTERNAL_VBAT_READ_ENABLE  D4
const double rhik = 1000.0; // 1000K Ohms = 1 Meg to Vref
const double rlok =  560.0;  // 560K Ohms to Gnd
const double dividerFactor = (rhik + rlok)/rlok; // Multiply ADC voltage by this to get Vbat

void setup() {
  Serial.begin(115200);
  while (!Serial);
  
  Serial.println("BLE Test compiled on " __DATE__ " at " __TIME__);
  Serial.println("BLE Test compiled on " __DATE__ " at " __TIME__);
  Serial.print("EXTERNAL_VBAT_READ_ENABLE = ");Serial.println(EXTERNAL_VBAT_READ_ENABLE);
  Serial.print("EXTERNAL_VBAT = "); Serial.println(EXTERNAL_VBAT);
  pinMode(EXTERNAL_VBAT_READ_ENABLE, OUTPUT);
  digitalWrite(EXTERNAL_VBAT_READ_ENABLE,LOW);
  
  // set LED pin to output mode
  pinMode(LEDB, OUTPUT);
  pinMode(LEDR, OUTPUT);
  pinMode(LEDG, OUTPUT);
  
  digitalWrite(LEDR, HIGH);
  digitalWrite(LEDB, HIGH);
  digitalWrite(LEDG, LOW);

  
   
  // begin initialization
  if (!BLE.begin()) {
    Serial.println("starting Bluetooth® Low Energy module failed!");

    while (1);
  }

  // set advertised local name and service UUID:
  BLE.setLocalName(BLENAME);
  BLE.setDeviceName(BLENAME);
  BLE.setAdvertisedService(bleService);

  // add the characteristic to the service
  bleService.addCharacteristic(txBatCharacteristic);
  bleService.addCharacteristic(txPredCharacteristic);
  bleService.addCharacteristic(rxCharacteristic);

  // add service
  BLE.addService(bleService);

  BLE.setEventHandler(BLEConnected, blePeripheralConnectHandler);
  BLE.setEventHandler(BLEDisconnected, blePeripheralDisconnectHandler);

  
  // start advertising
  BLE.advertise();

  Serial.println("BLE Peripheral");

  if (myIMU.begin() != 0) {
    Serial.println("Device error");
  } else {
    Serial.println("aX,aY,aZ,gX,gY,gZ");
  }

}


void loop() {
  
  BLEDevice central = BLE.central();

  if (central) {
  Serial.print("Connected to central: ");
  //unsigned int adcCount = 400;
  unsigned int adcCount = analogRead(EXTERNAL_VBAT);
  double adcVoltage = (adcCount * vRef) / numReadings;
  double vBat = adcVoltage*dividerFactor; // Voltage divider from Vbat to ADC
  printf("adcCount = %3u = 0x%03X, adcVoltage = %.3fV, vBat = %.3f\n",
             adcCount, adcCount, adcVoltage, vBat);


    String data="";
    data =  String(myIMU.readFloatAccelX(), 3)+","+String(myIMU.readFloatAccelY(), 3)+","+String(myIMU.readFloatAccelZ(), 3);

    
    txBatCharacteristic.writeValue(String(vBat));
    txPredCharacteristic.writeValue(data.c_str());
  
    delay(1000);
  }
}

void blePeripheralConnectHandler(BLEDevice central) {
  // central connected event handler
  Serial.print("Connected event, central: ");
  Serial.println(central.address());
  digitalWrite(LEDB, LOW);
  digitalWrite(LEDG, HIGH);
  digitalWrite(LEDR, HIGH);

}

void blePeripheralDisconnectHandler(BLEDevice central) {
  // central disconnected event handler
  Serial.print("Disconnected event, central: ");
  Serial.println(central.address());
  digitalWrite(LEDB, HIGH);
  digitalWrite(LEDG, LOW);
  digitalWrite(LEDR, HIGH);
}

Regards,

Dave

2 Likes

Thanks Dave for checking it out. I assumed external circuit with voltage divider would work but did not want to have external circuit due to small form factor of my POC and also as XIAO already has a charging chip in it, this feature should have been out of the box :slight_smile: Hope Seeed will fix this or document it so that someone can read battery without external circuit.

1 Like

Hi mithundotdas,

After some digging, I found a way to read PIN_VBAT value and keep BLE & IMU working at the same time.

Issue:

As noted by Dave, bluetooth drops connection when calling analogRead. It turns out that the current analogRead implementation blocks the thread until reading is completed. Here is the calling stack:

analogRead()
  -> mbed::AnalogIn::read()
    -> mbed::AnalogIn::read_u16()
      -> mbed::analog_read_u16()
        -> nrfx_saadc_sample_convert() // blocking

Fortunately Nordic provides another non-blocking api to convert adc value. Details here SAADC Drivers.

So the solution becomes clear, replace nrfx_saadc_sample_convert() with nrfx_saadc_buffer_convert() in analogRead.

But we can’t just modify the source code, because the implementation of mbed::AnalogIn is in the static library libmbed.a.

So we need to implement a non-blocking version of analogRead.

Quick hacky solution:

#include <nrf52840.h>
#include <nrfx_saadc.h>
#include <AnalogIn.h>
#include <pinDefinitions.h>

class HackAnalogIn: public mbed::AnalogIn {
  using mbed::AnalogIn::AnalogIn;
  public:
    analogin_t getAnalogIn_t();
};

analogin_t HackAnalogIn::getAnalogIn_t() {
  return this->_adc;
}

void startReadingBatteryLevel (nrf_saadc_value_t* buffer) {
  auto pin = PIN_VBAT;
  PinName name = analogPinToPinName(pin);
  if (name == NC) { return; }
  HackAnalogIn* adc = static_cast<HackAnalogIn*>(analogPinToAdcObj(pin));
  if (adc == NULL) {
    adc = new HackAnalogIn(name);
    analogPinToAdcObj(pin) = static_cast<mbed::AnalogIn*>(adc);
#ifdef ANALOG_CONFIG
    if (isAdcConfigChanged) {
      adc->configure(adcCurrentConfig);
    }
#endif
  }

  nrfx_saadc_buffer_convert(buffer, 1);
  nrfx_err_t ret = nrfx_saadc_sample();
  if (ret == NRFX_ERROR_BUSY) {
    // failed to start sampling
    return;
  }
}

void setup() {
  pinMode(P0_14, OUTPUT);
  digitalWrite(P0_14,LOW);
  // init BLE, IMU and other peripherals here
  ...
}

nrf_saadc_value_t BatteryLevel = { 0 };

void loop() {
  // BLE.poll()
  static unsigned long _lastT = 0;
  unsigned long _t = millis();

  if (_t - _lastT > 1000) {
    // read battery level every 1 second
    startReadingBatteryLevel(&BatteryLevel);
    _lastT = _t;
  }
  
  // check if ADC conversion has completed
  if (nrf_saadc_event_check(NRF_SAADC_EVENT_DONE)) {
    // ADC conversion completed. Reading is stored in BatteryLevel
    nrf_saadc_event_clear(NRF_SAADC_EVENT_DONE);
    float vBat = (float)BatteryLevel / 4096 * 3.3 / 510 * (1000 + 510);
    // write value to characteristic or things you want to do
    ...
  }
  ...
}

here are some test result:

vBat          EXTERNAL_VOLTMETER
4.03          4.13
4.01          4.14
3.96          4.06
3.93          4.04

Although the value is not absolute precise, but a strong linear correlation can be seen. I was able to obtain a pretty good reading by adjusting the conversion function to (float)BatteryLevel / 1340 * 3.3.

I’ll post a proper designed battery reading library later.

Hope it helps.

3 Likes

Nice solution POPOBE97!

Everything works fine with your code when I use it without any modifications.

I have one issue though. When I set the pin mode for the RGB leds to OUTPUT then nrf_saadc_event_check(NRF_SAADC_EVENT_DONE) always returns zero/false. I have no idea why this could cause an issue though. Do you have any ideas?

pinMode(LEDR, OUTPUT);
pinMode(LEDG, OUTPUT);
pinMode(LEDB, OUTPUT);

I will continue to test some more

Edit:

It seems that only LEDG will cause the issue if you set that to pinMode OUTPUT.

Here is the complete sketch/code:

#include <nrf52840.h>
#include <nrfx_saadc.h>
#include <AnalogIn.h>
#include <pinDefinitions.h>

// Reading Battery Level
// https://forum.seeedstudio.com/t/xiao-ble-sense-battery-level-and-charging-status/263248/24

class HackAnalogIn: public mbed::AnalogIn 
{
  using mbed::AnalogIn::AnalogIn;
  public:
    analogin_t getAnalogIn_t();
};

analogin_t HackAnalogIn::getAnalogIn_t() 
{
  return this->_adc;
}

void startReadingBatteryLevel(nrf_saadc_value_t* buffer) 
{
  auto pin = PIN_VBAT;
  PinName name = analogPinToPinName(pin);
  if (name == NC)
  {
    return;
  }
  HackAnalogIn* adc = static_cast<HackAnalogIn*>(analogPinToAdcObj(pin));
  if (adc == NULL)
  {
    adc = new HackAnalogIn(name);
    analogPinToAdcObj(pin) = static_cast<mbed::AnalogIn*>(adc);
#ifdef ANALOG_CONFIG
    if (isAdcConfigChanged)
    {
      adc->configure(adcCurrentConfig);
    }
#endif
  }

  nrfx_saadc_buffer_convert(buffer, 1);
  nrfx_err_t ret = nrfx_saadc_sample();
  if (ret == NRFX_ERROR_BUSY)
  {
    // failed to start sampling
    return;
  }
}

nrf_saadc_value_t BatteryLevel = { 0 };

void setup()
{
  Serial.begin(115200);
  //while (!Serial);

  Serial.println("Battery Level Example!");  

  pinMode(LEDR, OUTPUT);
  pinMode(LEDG, OUTPUT); // Setting LEDG to pinMode OUTPUT casues the monitoring of Battery Level to stop working
  pinMode(LEDB, OUTPUT);
  
  // Battery Level setup
  pinMode(P0_14, OUTPUT);
  digitalWrite(P0_14,LOW);
}

void loop()
{
  // Monitor the Battery Level
  static unsigned long _lastT = 0;
  unsigned long _t = millis();

  if (_t - _lastT > 1000)
  {
    // read battery level every 1 second
    startReadingBatteryLevel(&BatteryLevel);
    _lastT = _t;
    Serial.print("startReadingBatteryLevel at time ");
    Serial.println(_t);
  }

  // check if ADC conversion has completed
  if (nrf_saadc_event_check(NRF_SAADC_EVENT_DONE))
  {
    // ADC conversion completed. Reading is stored in BatteryLevel
    nrf_saadc_event_clear(NRF_SAADC_EVENT_DONE);
    float vBat = (float)BatteryLevel / 4096 * 3.3 / 510 * (1000 + 510);
    // write value to characteristic or things you want to do
    
    Serial.print("BatteryLevel: ");
    Serial.println(vBat);
  }
}
1 Like

Hi MonsterSmurf,

Unfortunately I have no idea why the code is causing this issue. And I’m moving on to the next project so possibly never going to inspect deeper.

I can provide with you some possible guesses for further inspection, although I strongly suggest you not to because the lack of documentation. (TBH by what we have discussed so far, this hardware really is nothing but a black hole)

  1. What I have placed in the code is a hack way of catching ADC event. This means it will effect all ADC related functionalities. Although LED does not depend on ADC, it does require DAC to function correctly. ADC and DAC may share some resources and therefore causes the problem. Possible solution to this is adding lock before you read/write on DAC related registers.
  2. As you described, only LEDG have this issue. Again, according to what we’ve discussed, the circuit diagram they provide is not very credible. So who know where LEDG is attached to. Possible solution to this is use other LEDs. You can combine LEDR with LEDB to generate countless types of color anyways.

Wish you luck with your project

1 Like

Hi, I’m sorry I can’t help you. I’ve been working on a similar project recently and have encountered similar problems. When I find a solution to the problem, I will share it with you.

2 Likes

I am also able to get battery reading without LEDG. So I think @POPOBE97 's code is the solution. I am making that a solution. Thanks a lot @POPOBE97

2 Likes

I do not have the hardware yet, but reading the various fixes. Looks like this solution will only work with the Mbed based 2.6.1 version of the board (ArduinoBLE) not the 1.0 version based on Adafruit’s libraries. What would be a quick fix for the 1.0 version?

@POPOBE97 thanks for sharing your code! Could anyone explain where the various numbers are coming from in float vBat = (float)BatteryLevel / 4096 * 3.3 / 510 * (1000 + 510); - specifically the 4096, 3.3, 510 and 1510.

I’m using a 3.7v 300mAh battery connected to the pads and it seems to be working well and I’m getting readings using the code from @POPOBE97 but I’m looking to convert that into a rough percentage (or even just a high/medium/low)

4096 : this is the range limit of the 12-bit ADC (so 0=0% and 4095=100%) . (Although I think the code should use 4095 not 4096? :thinking: )

3.3 : this is supposed to correspond to the reference voltage being used by the ADC here, i.e. 100% on ADC corresponds to 3.3v on the analog input pin. Although as I understood it the default vref is actually 3.6v . So readings will be wrong if you have the wrong value here.

510 and 1000 correspond (as noted in the comments in the earlier source code) to the voltage divider in the Xiao schematic (built-in), which lies between the battery output, the vbat pin, and ground. According to the schematic, there’s 1 M Ohm (1000 K Ohm) to battery, and 510K Ohm to ground

Hi everyone! I have a question related to the wiring of a LiPo battery to the Xiao BLE. Must the battery be connected to the VIN/GND pads located underneath the Xiao BLE board? Or can it be connected to pin #13 (the so-called “5V” pin) and pin #12 (GND pin)? I am asking because soldering a wire to these flat pads is a little tricky. I was able to do it, but it’s tricky. Whereas soldering a wire to a pin is a lot easier.

I have yet to receive my Xiao BLE, but I experimented a little on a Xiao SAMD21, and it looks like the voltage of the VIN pad and that of the “5V” pin (#13) are identical. I connected my bench DC power supply to the VIN pad and gave it 3.7V or 5.5V, and pin #13 showed the same exact voltage.

Note: As long as I power the board with a voltage > 3.3V, pin #11 is always at 3.3V.

Thanks in advance!

It depends: do you want to use the onboard battery charger and MOSFET circuitry that switches off the battery when USB-C is plugged? use the battery pad.
If not, then the 5V pin works but you won’t be taking full advantage of the battery capacity.