BLE Power Consumption Optimizations

Hello All,

Thank you again for helping me conquer the LoRa/LoRaWAN power optimizations in my previous thread. This time, I’m looking to improve power optimizations for the BLE side of things.

To start, I am powering the bare RAK4631 module via 3V3, GND, with RST pulled high from a Nordic PPK2 supplying 3300mV to the board.

I have uploaded the ble_uart example code modified to remove all serial print statements, and with an increased advertising interval to:

Bluefruit.Advertising.setInterval(1952, 1952);

While advertising, I can see the advertisement TX current peaks when expected, but in between the device does not seem to go to sleep, instead hovering around 1.8mA idle, instead of in the XXXuA range that I would expect. (see below)

NOTE: Adding a 5 second delay to the main loop does not appear to affect current consumption either, although doing this helps considerably for other Adafruit devices using the Bluefruit library (tested on ItsyBitsy NRF52840 for comparison).

Does anyone have any advice for reaching the proper sleep current? All advice is welcome.

Hi @cmod
The problem is that the loop runs all the time, checking for data on the BLE UART.

Try the following:

Declare a semaphore

/** Semaphore used by events to wake up loop task */
SemaphoreHandle_t g_task_sem = NULL;

After g_BleUart.begin() add

	// Start the UART service
	g_BleUart.begin();
	g_BleUart.setRxCallback(bleuart_rx_callback);

	// Create the task event semaphore
	g_task_sem = xSemaphoreCreateBinary();
	// Initialize semaphore
	xSemaphoreGive(g_task_sem);
	// Take the semaphore so the loop will go to sleep until an event happens
	xSemaphoreTake(g_task_sem, 10);

Add the callback function

/**
 * Callback if data has been sent from the connected client
 * @param conn_handle
 * 		The connection handle
 */
void bleuart_rx_callback(uint16_t conn_handle)
{
	(void)conn_handle;

	xSemaphoreGiveFromISR(g_task_sem, pdFALSE);
}

and change your loop() to

void loop()
{
	// Sleep until we are woken up by an event
	if (xSemaphoreTake(g_task_sem, portMAX_DELAY) == pdTRUE)
	{
    		// Forward anything received from USB Serial to BLE UART
    		if (Serial.available() && g_BleUartConnected)
    		{
        		g_BleUart.print(Serial.readString());
	    	}

	    	// Forward anything received from BLE UART to USB Serial
	    	if (g_BleUart.available())
	    	{
	        	Serial.print(g_BleUart.readString());
	    	}
	}
}

for the serial data add this callback

/**
 * @brief Callback when data over USB arrived
 * 
 * @param itf unused
 */
void tud_cdc_rx_cb(uint8_t itf)
{
	if (g_task_sem != NULL)
	{
		xSemaphoreGiveFromISR(g_task_sem, pdFALSE);
	}
}

Hope it works, I was just copying the lines from a more complicated code I am working on.

Hi @beegee, thanks again for coming to my rescue.

I folded the code you supplied into the existing sketch, see below:

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

// Forward declarations for functions
void ble_connect_callback(uint16_t conn_handle);
void ble_disconnect_callback(uint16_t conn_handle, uint8_t reason);

BLEUart g_BleUart;

bool g_BleUartConnected = false;

/** Semaphore used by events to wake up loop task */
SemaphoreHandle_t g_task_sem = NULL;

/**
 * @brief Callback when data over USB arrived
 * 
 * @param itf unused
 */
void tud_cdc_rx_cb(uint8_t itf)
{
  if (g_task_sem != NULL)
  {
    xSemaphoreGiveFromISR(g_task_sem, pdFALSE);
  }
}

/**
 * Callback if data has been sent from the connected client
 * @param conn_handle
 *     The connection handle
 */
void bleuart_rx_callback(uint16_t conn_handle)
{
  (void)conn_handle;

  xSemaphoreGiveFromISR(g_task_sem, pdFALSE);
}

void setup() {

  time_t timeout = millis(); // Timeout in case the system runs on its own

  //Bluefruit.configPrphBandwidth(BANDWIDTH_MAX);
  Bluefruit.configPrphConn(92, BLE_GAP_EVENT_LENGTH_MIN, 16, 16);

  Bluefruit.begin(1, 0);
  Bluefruit.autoConnLed(false);

  // Set max power. Accepted values are: -40, -30, -20, -16, -12, -8, -4, 0, 4
  Bluefruit.setTxPower(4);
  // Set the BLE device name
  Bluefruit.setName("RAK4631_UART");

  //Bluefruit.Periph.setConnectCallback(ble_connect_callback);
  //Bluefruit.Periph.setDisconnectCallback(ble_disconnect_callback);
  Bluefruit.Periph.setConnInterval(16, 16); // min = 9*1.25=11.25 ms, max = 16*1.25=20ms

  // Start the UART service
  g_BleUart.begin();
  g_BleUart.setRxCallback(bleuart_rx_callback);

      // Set up and start advertising
  // Advertising packet
  Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE);
  Bluefruit.Advertising.addTxPower();
  Bluefruit.Advertising.addName();

  Bluefruit.Advertising.restartOnDisconnect(true);
  Bluefruit.Advertising.setInterval(1952, 1952); // in unit of 0.625 ms
  Bluefruit.Advertising.setFastTimeout(30);   // number of seconds in fast mode
  Bluefruit.Advertising.start(0);             // 0 = Don't stop advertising after n seconds

  // Create the task event semaphore
  g_task_sem = xSemaphoreCreateBinary();
  // Initialize semaphore
  xSemaphoreGive(g_task_sem);
  // Take the semaphore so the loop will go to sleep until an event happens
  xSemaphoreTake(g_task_sem, 10);



}

void loop()
{
  // Sleep until we are woken up by an event
  if (xSemaphoreTake(g_task_sem, portMAX_DELAY) == pdTRUE)
  {
        // Forward anything received from USB Serial to BLE UART
        if (Serial.available() && g_BleUartConnected)
        {
            g_BleUart.print(Serial.readString());
        }

        // Forward anything received from BLE UART to USB Serial
        if (g_BleUart.available())
        {
            Serial.print(g_BleUart.readString());
        }
  }
}

However, it did not significantly impact the sleep current in between advertisement intervals, I think it actually increased the draw slightly, “sleep” current is ~1.84mA. Any ideas?

Looks like when advertising is active, the softdevice is never sleeping, keeping the power consumption high. I measured around the same current as you (1.6mA). The only way I could get it down was to reduce the TX power to -40 with Bluefruit.setTxPower(-40); which of course limits the range the device can be seen. With TX set to -40, I got the current down to 490uA.

Just some ideas for a strategy:

  1. Limit advertising time to maybe 15 seconds and advertise only every 60 seconds
  2. Keep TX power low during advertising and set it to 4 after a connection and to -40 after a disconnect

I did 1) in some apps I wrote, but what I found best is to have an external trigger to start advertising.

  1. Start advertising after reboot for 30 seconds (give user a chance to connect after reset/power-up)
  2. Use an event (e.g. button, touch-pad) to start advertising manually if user wants to connect over BLE

This makes sense.

I’ve not done much with BLE but I think the principals of 2 way radio stand as I use the nRF24L01 a fair bit - if you are advertising, surely it’s going to have to listen for a response and so many parts of the MCU (again, not completely familiar with the internal architecture of the nRF52840) have to be active to listen for a response from a subscriber/client so it’s not going to be able to do any serious sleeping.

One product I tried out turned things round by listening for new BLE devices and then turned on advertising - a trick the UK government tried by sending tens of millions of GBP to Switzerland because apparently no one in the UK could create a Covid proximity app (not that I’m bitter, but I’m sure I could have written it for half) - but unfortunately iOS devices also go to sleep.

Overall, I suspect @cmod is going to hit this issue with any BLE product - which is pretty much why I tend to avoid it, particularly as the range is too short to be able to phone home to say it needs its batteries changing if it’s a sensor and it kills the batteries even faster. But I concede it’s very good for interacting with a local smart device for configuration etc.

I think the $64,000,000 question again for @cmod is, is what is the likely use case?

Given that BLE is more power hungry and is something designed more around interactivity, why not use LoRa?

Ah it appears I spoke too soon! I forgot to put the all important LoRa radio sleep code in the BLE sketch. With the above code, including the below snipped in setup(), with TX power set to -40 the average current of just the core module including TX peaks over a 1 minute interval is ~23.5uA! Excellent. FYI having TX power set to 4 all the time leads to average current of ~42uA. Connected average current is ~205uA for -40, and ~266uA for +4, Advertisement interval for both is set to ~1.2s.

For the sake of others who might find this thread, here is my test code in its entirety:

/**
   @file ble_uart.ino
   @author Bernd Giesecke ([email protected])
   @brief BLE example shows how to implement UART over BLE
   @version 0.1
   @date 2020-07-27

   @copyright Copyright (c) 2020

   @note RAK4631 GPIO mapping to nRF52840 GPIO ports
   RAK4631    <->  nRF52840
   WB_IO1     <->  P0.17 (GPIO 17)
   WB_IO2     <->  P1.02 (GPIO 34)
   WB_IO3     <->  P0.21 (GPIO 21)
   WB_IO4     <->  P0.04 (GPIO 4)
   WB_IO5     <->  P0.09 (GPIO 9)
   WB_IO6     <->  P0.10 (GPIO 10)
   WB_SW1     <->  P0.01 (GPIO 1)
   WB_A0      <->  P0.04/AIN2 (AnalogIn A2)
   WB_A1      <->  P0.31/AIN7 (AnalogIn A7)
*/
#include <Arduino.h>
#include <bluefruit.h>
#include <SX126x-RAK4630.h>

// Forward declarations for functions
void ble_connect_callback(uint16_t conn_handle);
void ble_disconnect_callback(uint16_t conn_handle, uint8_t reason);

/**
   @brief  BLE UART service
   @note   Used for BLE UART communication
*/
BLEUart g_BleUart;

/** Flag if BLE UART client is connected */
bool g_BleUartConnected = false;

/**
   @brief Arduino setup function. Called once after power on or reset
*/
void setup()
{
  lora_rak4630_init();
  Radio.Sleep();
  // Initialize built in green LED
  //    pinMode(LED_BUILTIN, OUTPUT);
  //    digitalWrite(LED_BUILTIN, HIGH);

  // Initialize Serial for debug output
  Serial.begin(115200);

  // Wait for USB Serial to be ready or terminal to be connected
  time_t timeout = millis(); // Timeout in case the system runs on its own
  // Waiting for Serial
  //    while (!Serial)
  //    {
  //        if ((millis() - timeout) < 5000)
  //        {
  //            // Blink the LED to show that we are alive
  //            delay(100);
  //            digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
  //        }
  //        else
  //        {
  //            // Timeout while waiting for USB Serial
  //            break;
  //        }
  //    }

  Serial.println("================================");
  Serial.println("RAK4631 BLE UART example");
  Serial.println("================================");

  // Config the peripheral connection with maximum bandwidth
  // more SRAM required by SoftDevice
  // Note: All config***() function must be called before begin()
  //Bluefruit.configPrphBandwidth(BANDWIDTH_MAX);
  Bluefruit.configPrphConn(92, BLE_GAP_EVENT_LENGTH_MIN, 16, 16);

  Bluefruit.begin(1, 0);
  Bluefruit.autoConnLed(false);
  // Set max power. Accepted values are: -40, -30, -20, -16, -12, -8, -4, 0, 4
  Bluefruit.setTxPower(4);
  // Set the BLE device name
  Bluefruit.setName("RAK4631_UART1");

  Bluefruit.Periph.setConnectCallback(ble_connect_callback);
  Bluefruit.Periph.setDisconnectCallback(ble_disconnect_callback);

  // Configure and Start BLE Uart Service
  g_BleUart.begin();

  // Set up and start advertising
  // Advertising packet
  Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE);
  Bluefruit.Advertising.addTxPower();
  Bluefruit.Advertising.addName();

  /* 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(true);
  Bluefruit.Advertising.setInterval(1952, 1952); // in unit of 0.625 ms
  Bluefruit.Advertising.setFastTimeout(30);   // number of seconds in fast mode
  Bluefruit.Advertising.start(0);             // 0 = Don't stop advertising after n seconds
}

/**
   @brief  Callback when client connects
   @param  conn_handle: Connection handle id
*/
void ble_connect_callback(uint16_t conn_handle)
{
  (void)conn_handle;
  g_BleUartConnected = true;

  Serial.println("BLE client connected");
}

/**
   @brief  Callback invoked when a connection is dropped
   @param  conn_handle: connection handle id
   @param  reason: disconnect reason
*/
void ble_disconnect_callback(uint16_t conn_handle, uint8_t reason)
{
  (void)conn_handle;
  (void)reason;
  g_BleUartConnected = false;

  Serial.println("BLE client disconnected");
}

/**
   @brief Arduino loop. Runs forever until power off or reset

*/
void loop()
{
  delay(5 * 1000);
  // Forward anything received from USB Serial to BLE UART
  if (Serial.available() && g_BleUartConnected)
  {
    g_BleUart.print(Serial.readString());
  }

  // Forward anything received from BLE UART to USB Serial
  if (g_BleUart.available())
  {
    Serial.print(g_BleUart.readString());
  }
}

I had an issue using the semaphore framework you linked previously and a device being unable to connect using it, but FYI I found the delay() method (while much less graceful), actually has an extremely similar current consumption profile and should do for my purposes.

Most of this investigation is ongoing to evaluate the general usefulness of the RAK4631 core module @nmcc, but more specifically I do want to implement similar functionality to what Beegee has done regarding configuring a LoRa/LoRaWAN node via BLE. Luckily, with only 24uA draw to have BLE advertisements, very glad to say this is possible.

Once setup, you could shut down BLE entirely - and just restart 5 minutes or so of advertising on reboot/reset/power up.

@cmod

I am happy you could solve the problems. Looking forward to your big order now :grin:

Thank you for sharing your code. Wondering what your problem with the semaphores is.