Ultra Low Power Battery Voltage Measurement

Introduction

The objective of this project is to write a sample code to read battery voltage in low power embedded devices. I am using Silicon Lab’s Thunderboard (EFR32BG22). However, the fundamentals of the firmware design can be used for any microcontroller for any embedded dev kit. Here, Thunderboard has ARM Cortex M33 based target device, with 512 kB Flash, and 32 kB ram.

Let’s get to the point, without mentioning every little details of this board. You can get those from here.

Internally, at GuardRFID, we use starter kit to design and develope the firmware for our BLE Tags. We can get access to following using Mini Simplicity Connector, which is really handy for debugging FW, Power Consumption etc.

  • AEM
  • PTI
  • VCOM
  • SWD

In most ultra low power embedded devices, for which the battery life estimate could range from 2 years to even 6 years, we do not have much flexibility in terms of adding additional passive/active components in the PCBA. Reason being, smaller footprints, and low cost requirements. Also, addition of these components contribute to higher power consumption.

So, how do you read battery voltage without using external components?

We utilize internal voltage reference of the SoC. Also, we have Analog to Digital(ADC) peripheral in almost every microcontroller. We can use internal VRef, and the ADC peripheral to read the connected battery voltage without the need of external components.

In ultra low power embedded systems, you would have a system tick of <x>. This X can range from 100 mS to 60 seconds, or more, depending upon the device and the functionality. In this project, we are going to use 10 minutes system tick.

Why 10 minutes?

Because, generally speaking, you don’t need to read battery voltage much often, as this devices have years of battery life. Whereas, if you were designing a firmware for android device, or any battery operated high power embedded device, then you would have a systick in the range of seconds. Otherwise, battery measurement is not a critical task that needs to addressed every few seconds.

The Concept

We measure the battery voltage against internal reference voltage. In the case of Thunderboard, we have 1.21 V internal reference voltage, which is maintained automatically by internal DC-DC circuits. Siliconlabs makes it easy to measure battery voltage by provides the AVDD pin, which outputs $$ AVDD = VCC / 4 \ \ (volt) $$

Why do we need this? We need this because, if you read say 1.5 volt with respect to internal voltage reference 1.21 V then you will cap it at 1.21 V. You can not read higher voltage level using ADC peripheral than internal voltage reference.

Normally, we have 3.3V coin cell battery powered embedded devices, where we would use this. So, in the case of thunderboard, we have $AVDD = (3.3) / 4 = 0.825, \ \ (< 1.21 \ \ v)$

In the case of full battery voltage, if we were to read AVDD against internal VRef of 1.21V using 12 bit ADC peripheral then, we can successfully read the battery voltage according to following formula,

$$\therefore batteryVoltage = (ADCRegisterData) * 1.21 / 0xFFF$$ $$ \therefore scaledBatteryVoltage = 4 * batteryVoltage $$

This will give us the correct current battery voltage.

Demo Application Code

The demo code below demonstrates the this practically. Here, we are using a onboard push button as an interrupt to read the battery voltage, when the push button is pressed. You can find the github repository here.

#include "em_device.h"
#include "em_chip.h"
#include "em_core.h"
#include "em_cmu.h"
#include "em_emu.h"
#include "em_gpio.h"
#include "em_iadc.h"

/*******************************************************************************
 *******************************   DEFINES   ***********************************
 ******************************************************************************/

// Set CLK_ADC to 10 MHz
#define CLK_SRC_ADC_FREQ        20000000  // CLK_SRC_ADC
#define CLK_ADC_FREQ            10000000  // CLK_ADC - 10 MHz max in normal mode
#define EM2DEBUG                  1

/*******************************************************************************
 ***************************   GLOBAL VARIABLES   ******************************
 ******************************************************************************/

int32_t readBatteryVoltage ()
{

  uint32_t adc_Value;

  IADC_Init_t init = IADC_INIT_DEFAULT;
  IADC_AllConfigs_t initAllConfigs = IADC_ALLCONFIGS_DEFAULT;
  IADC_InitSingle_t initSingle = IADC_INITSINGLE_DEFAULT;
  IADC_SingleInput_t initSingleInput = IADC_SINGLEINPUT_DEFAULT;

  CMU_ClockEnable (cmuClock_IADC0, true);
  IADC_reset (IADC0);
  CMU_ClockSelectSet (cmuClock_IADCCLK, cmuSelect_FSRCO);

  init.warmup = iadcWarmupNormal;
  init.timerCycles = 200;
  init.srcClkPrescale = IADC_calcSrcClkPrescale (IADC0, CLK_SRC_ADC_FREQ, 0);

  // Internal Ref
  initAllConfigs.configs[0].reference = iadcCfgReferenceInt1V2;
  initAllConfigs.configs[0].adcClkPrescale = IADC_calcAdcClkPrescale ( IADC0,
                                                                       CLK_ADC_FREQ,
                                                                       0,
                                                                       iadcCfgModeNormal,
                                                                       init.srcClkPrescale);
  /*
   * Specify the input channel.  When negInput = iadcNegInputGnd, the
   * conversion is single-ended.
   */
  initSingleInput.posInput = iadcPosInputAvdd | 1;
  initSingleInput.negInput = iadcNegInputGnd;

  initSingle.triggerAction = iadcTriggerActionOnce;

  IADC_init (IADC0, &init, &initAllConfigs);
  IADC_initSingle (IADC0, &initSingle, &initSingleInput);
  IADC_command (IADC0, iadcCmdStartSingle);

  // Polling for results
  while ((IADC0->STATUS
      & (_IADC_STATUS_CONVERTING_MASK | _IADC_STATUS_SINGLEFIFODV_MASK))
      != IADC_STATUS_SINGLEFIFODV)
    ;

  adc_Value = IADC_pullSingleFifoResult (IADC0).data;
  double result = ((adc_Value * 4 * 1.21) / (0xFFF)) ;

  // Reset and stop IADC to save power
  IADC_reset (IADC0);
  CMU_ClockEnable (cmuClock_FSRCO, false);
  CMU_ClockEnable (cmuClock_IADC0, false);

  return (((int32_t) adc_Value * 4 * 1.21) / 0xFFF) + 500;

}

/**************************************************************************//**
 * @brief GPIO Interrupt handler for even pins.
 *****************************************************************************/
void GPIO_EVEN_IRQHandler (void)
{
  // Get and clear all pending GPIO interrupts
  uint32_t interruptMask = GPIO_IntGet ();
  GPIO_IntClear (interruptMask);
  int32_t batteryVoltage;

  // Check if button 0 was pressed
  if (interruptMask & (1 << 0))
    {
      batteryVoltage = readBatteryVoltage();
    }

}

static void gpioSetup (void)
{
  // Configure GPIO Clock. Note this is not required for EFR32xG21
  CMU_ClockEnable (cmuClock_GPIO, true);

  // Configure Button PB0 (onboard push button) as input and enable interrupt
  GPIO_PinModeSet (gpioPortB, 0, gpioModeInputPull, 1);
  GPIO_ExtIntConfig (gpioPortB, 0, 0, false, true, true);

  // Enable EVEN interrupt to catch button press that changes slew rate
  NVIC_ClearPendingIRQ (GPIO_EVEN_IRQn);
  NVIC_EnableIRQ (GPIO_EVEN_IRQn);
}

/**************************************************************************//**
 * @brief  Main function
 *****************************************************************************/
int main (void)
{
  CHIP_Init ();
  gpioSetup ();


#ifdef EM2DEBUG
#if (EM2DEBUG == 1)
  // Enable debug connectivity in EM2
  EMU->CTRL_SET = EMU_CTRL_EM2DBGEN;
#endif
#endif

  while (1)
    {
      // Enter EM2 sleep, wait for IADC interrupt
      EMU_EnterEM2 (false);
    }
}
Jenish Rudani
Jenish Rudani
Embedded Software Developer

I love making things