When developing portable applications using batteries, it is important to keep track of the remaining battery level to inform the user and take action when it drops to a level that might be critical for the correct device functionality.
A common measurement method consists of taking a sample of the current battery voltage and correlate it to a percentage depending on its capacity. Then this value is reported to the user in a visual manner.
MKW40 is a system on chip (SoC) that embeds a processor and a Bluetooth® Low Energy (BLE)/802.15.4 radio for wireless communications. This posts describes how to obtain the current battery level and report it via BLE using this part.
Typically, the battery voltage is regulated so the MCU has a stable power supply across the whole battery life. This causes the ADC supply voltage to be lower than the actual battery voltage. To address this, a voltage divider is used to adequate the battery voltage to levels that can be read by the ADC.
Figure 1 Typical battery level divider circuit
The MKW40 includes a voltage divider on its embedded DC-DC converter removing the need to add this voltage divider externally. It is internally connected to the ADC0 channel 23 so reading this channel obtains the current level of the power source connected to the DC-DC converter in.
Figure 2 DC-DC converter with battery voltage monitor
Software implementation consists in acquiring the ADC value, correlate it with a percentage level and transmit it using BLE. The connectivity software includes functions to perform all those actions.
A voltage divider connected to the battery and internally wired to an ADC channel is embedded in the SoC. For the MKW40Z it is the ADC0 single ended channel 23 (ADC0_CH23) . There are some examples in the Kinetis Software Development Kit (KSDK) documentation explaining how to configure the ADC module. The connectivity software includes a function named BOARD_InitAdc in the file app.c where this is initialized.
void BleApp_Init(void)
{
/* Initialize application support for drivers */
BOARD_InitAdc(); <-- Initialization function
/* Initialize Software-timer */
SwTimer_Init();
/* Status Indicator fade initialization */
status_indicator_fade_init();
/* Initialise ECG Acquisition system */
ecg_acquisition_init();
/* Initialize battery charger pin configuration */
power_manager_init();
/* Create Advertising Timer */
advertisingTimerId = SwTimer_CreateTimer(TimerAdvertisingCallback);
}
Once initialized, this ADC channel must be read to obtain the current voltage present in the divisor. There are also some cool examples in the KSDK documentation on how to do this. The obtained value is a digital representation of the voltage in the divisor and is relative to the VDDA or VREFH voltage (depending on what is used as reference for the ADC).
Since the battery voltage varies over the time (unless you use a voltage regulator or use the DC-DC converter in buck mode), the most accurate way to get the battery voltage is to obtain the actual voltage that is referencing the ADC first. For this, the MKW40Z includes a 1V reference voltage channel wired to another ADC channel: ADC0_CH27. Reading this ADC channel obtains the number of counts that correspond to 1V, and VREF can be calculated using the next formula.
Once the VRef voltage has been obtained, it is possible to determine the voltage present in the battery voltage divisor by using the following formula.
The obtained voltage is still only a portion of the actual battery voltage. To obtain the full voltage, the obtained value must be multiplied by the divisor relation. This relation is selected in the DC-DC register DCDC_REG0:DCDC_VBAT_DIV_CTRL and can be: VBATT, VBATT/2 or VBATT/4.
After the full VBAT voltage is obtained, it must be correlated to a percentage depending on the values set for 0% and 100%. Using the slope method is a good approach to correlate the voltage value. For this, the slope m must be calculated using the formula.
Where V100% is the voltage in the battery when it is fully charged, and V0% is the voltage in the battery when it is empty. Once m has been calculated, the battery percentage can be obtained using the formula:
The Connectivity Software includes a function that obtains the current battery voltage connected to the DC-DC input and returns the battery percentage. It is included in the board.c file.
uint8_t BOARD_GetBatteryLevel(void)
{
uint16_t batVal, bgVal, batLvl, batVolt, bgVolt = 100; /*cV*/
bgVal = ADC16_BgLvl();
DCDC_AdjustVbatDiv4(); /* Bat voltage divided by 4 */
batVal = ADC16_BatLvl() * 4; /* Need to multiply the value by 4 because the measured voltage is divided by 4*/
batVolt = bgVolt * batVal / bgVal;
batLvl = (batVolt - MIN_VOLT_BUCK) * (FULL_BAT - EMPTY_BAT) / (MAX_VOLT_BUCK - MIN_VOLT_BUCK);
return ((batLvl <= 100) ? batLvl:100);
}
After the battery level has been determined it can be now reported via BLE. The Connectivity Software includes predefined profile files to be included in custom applications. Battery profile is included in the files battery_service.c and .h, under the Bluetooth folder structure.
To make use of them, make sure that they are included in your project. Then, include the file battery_interface.h in your BLE application file. An example using the included BLE applications is shown.
In app.c (Connectivity Software examples application file) include the battery service interface
#include "battery_interface.h"
In the variable declaration section, create a new basConfig_t variable. This is needed to configure a new service.
static basConfig_t basServiceConfig = {service_battery, 0};
The new service needs to be created after the BLE stack has been initialized. When this happens, the function BleApp_Config is executed. Inside this function, the battery service is started.
/* Start services */
hrsServiceConfig.sensorContactDetected = mContactStatus;
#if gHrs_EnableRRIntervalMeasurements_d
hrsServiceConfig.pUserData->pStoredRrIntervals = MEM_BufferAlloc(sizeof(uint16_t) * gHrs_NumOfRRIntervalsRecorded_c);
#endif
Hrs_Start(&hrsServiceConfig);
Bas_Start(&basServiceConfig);
/* Allocate application timers */
mAdvTimerId = TMR_AllocateTimer();
mMeasurementTimerId = TMR_AllocateTimer();
mBatteryMeasurementTimerId = TMR_AllocateTimer();
Function Bas_Start is used for this purpose. This function starts the battery service functionality indicating that it needs to be reported by the BLE application.
After a central has successfully connected to the peripheral device, the battery service must be subscribed so it’ measurements can be reported to the central. For this, the function Bas_Suscribe is used inside the connection callback. Following code shows its implementation in the connectivity software. It takes place in the connection callback function BleApp_ConnectionCallback in app.c
switch (pConnectionEvent->eventType)
{
case gConnEvtConnected_c:
{
mPeerDeviceId = peerDeviceId;
/* Advertising stops when connected */
mAdvState.advOn = FALSE;
#if gBondingSupported_d
/* Copy peer device address information */
mPeerDeviceAddressType = pConnectionEvent->eventData.connectedEvent.peerAddressType;
FLib_MemCpy(maPeerDeviceAddress, pConnectionEvent->eventData.connectedEvent.peerAddress, sizeof(bleDeviceAddress_t));
#endif
#if gUseServiceSecurity_d
{
bool_t isBonded = FALSE ;
if (gBleSuccess_c == Gap_CheckIfBonded(peerDeviceId, &isBonded) &&
FALSE == isBonded)
{
Gap_SendSlaveSecurityRequest(peerDeviceId, TRUE, gSecurityMode_1_Level_3_c);
}
}
#endif
/* Subscribe client*/
Bas_Subscribe(peerDeviceId);
Hrs_Subscribe(peerDeviceId);
/* UI */
LED_StopFlashingAllLeds();
/* Stop Advertising Timer*/
mAdvState.advOn = FALSE;
TMR_StopTimer(mAdvTimerId);
/* Start measurements */
TMR_StartLowPowerTimer(mMeasurementTimerId, gTmrLowPowerIntervalMillisTimer_c,
TmrSeconds(mHeartRateReportInterval_c), TimerMeasurementCallback, NULL);
}
Once the service has been subscribed, a new measurements can be registered by using the Bas_RecordBatteryMeasurement function at any time.
Bas_RecordBatteryMeasurement(basServiceConfig.serviceHandle, basServiceConfig.batteryLevel);
This function receives the battery service handler previously defined (static basConfig_t basServiceConfig = {service_battery, 0}) and the current battery level percentage as input parameters.
When a disconnection occurs, the battery service must be unsubscribed so no further updates are performed during disconnection. For this, the Bas_Unsuscribe function must be used after a disconnection event.
case gConnEvtDisconnected_c:
{
/* Unsubscribe client */
Bas_Unsubscribe();
Hrs_Unsubscribe();
mPeerDeviceId = gInvalidDeviceId_c;
Now, updated battery levels can be reported by the BLE device letting the user know when a battery must be replaced or recharged.
In order to obtain battery level is central device supposed to send a request or as far as peripheral is subscribed it sends it periodically by itself? Thanks
Hi Aleh,
The battery service includes the battery level characteristic to report the current battery level of the peripheral. In BLE there are several ways to get the value of a characteristic.
1. The client executes a Read Request to get the characteristic value.
2. The client enables Notifications for the characteristic (when Characteristic Notifications are available) and the peripheral reports the value periodically
3. The client enables Indications for the characteristic (when Characteristic Indications are available) and the peripheral reports the value periodically
It will depend on how the characteristic is implemented. In this particular case, the battery level characteristic requires the client to enable Notifications.
Saludos