Accurate State of Charge Measurements For Li-Ion Batteries Using a State of Charge vs Open Circuit Voltage Lookup Table

We have had a few questions on the Nordic DevZone related to state of charge estimation of Li-Ion batteries when used in conjucture with an nrf52 DK.

This blog post will show you how to customize your own Thingy:52 Firmware & enable you to do a simple discharge test, where the voltage is recorded at a specific time step (e.g. every 5 seconds). This information can then be used to calculate the State of Charge (SoC) & lets you make a state of charge vs battery voltage lookup table. That way, you can use the ADC on the nrf52DK or on the Thingy:52 to lookup the correct State of Charge value at a given battery voltage level. If you take a look at the Figure at the bottom of this blog post, you notice that the state of charge vs voltage relationship is highly non-linear. Assuming a linear relationship is easier to accomplish, but can lead to great SoC inaccuracies.

First off, you will want to download the Nordic Thingy FW v2.1.0  from this link: . I have currently only tested with this FW.

Then, you want to be sure to place the download close to the C drive. This is because Windows has a Maximum Path Length limitation. You can read more about this limitation here:

Next, follow the prerequisites given in the Github page. Remember to add any required Windows commands to your path. When you run setup_sdk.bat on Windows, you might end up getting a git or make not found error, even if git & make are added to your path. If this is the case, you can try running the .bat file as an Administrator. That could make it work.

If you open up the batch file with Notepad for example, you notice these two lines at the bottom:

pushd external\sdk13\external\micro-ecc\nrf52_armgcc\armgcc && make && popd
pushd external\sdk13\external\micro-ecc\nrf52_keil\armgcc && make && popd

If you receive an error related to this, it could be a good idea to open a command prompt window in the armgcc folder of either nrf52_keil (if you plan on using Keil) or nrf52_armgcc (if you want to use the armgcc compiler). Then, you can run make --debug or make --verbose to get more debugging information. For some unknown reason, when I ran make in a git bash terminal, everything worked fine, whereas when I tried to same in a command prompt window, it did not work. The make command could work better if run in a Git Bash window (right click in the Window that has the Makefile & press "Git BASH here"). Then, you should be able to run the make command & micro-ecc should compile successfully.

Once this has been completed successfully, you can open up the Thingy FW project by going to: project\pca20020_s132\arm5_no_packs\ & opening the ble_app_thingy_s132_pca20020 Keil project. If you want to use armgcc, open the armgcc folder instead of arm5_no_packs. I have only tested the FW on Windows using Keil, but this process should be very similar when using armgcc on macOS or Linux too.

After opening the Keil project, make sure to Select Target "debug_v1_0_0".  An easy option to be able to log the Thingy battery voltage is to use a Segger Real Time Terminal (RTT) client. To enable the RTT client, open the sdk_config.h file & update these defines if need be: NRF_LOG_ENABLED 1 & NRF_LOG_BACKEND_RTT_ENABLED 1. These changes can either be done via the Text Editor or via the Configuration Wizard tab.

In the current default settings, the Thingy will go to sleep after 180 seconds of advertising to conserve power. To change the advertising timeout of the Thingy, open the thingy_config.h header file. In the header file, change APP_ADV_TIMEOUT_IN_SECONDS from 180 to 0. This will enable the Thingy to advertise until the battery runs out of power. 

Leaving the settings like this and measuring the current consumption, the Thingy:52 now uses roughly 5 mA of power when the LED is on constantly. Since the battery has a capacity of roughly 1440 mAh, it would take roughly 288 hours to completely discharge the battery from a full charge. In order to increase the current consumption & decrease the test time, you can turn on the gas_sensor & set the update interval to 1 second. In addition, the LED intensity is increased to max & the LED changed from blue to white. By making this last change, all three RGB LEDs are turned on, instead of only the blue LED. By turning on the gas sensor & changing the LED color & intensity, the current consumption increases to approximately 74 mA.

Turning on the gas sensor & changing the LED to white are shown below:

Turn on gas service: Open up m_environment.c & add this code to the end of the m_environment_init() function at the end of the file right before the code: return NRF_SUCCESS.

err_code = gas_start();

In addition, you'll want to change the default mode in the gas_start() function in m_environment.c. In the switch default case, change mode = DRV_GAS_SENSOR_MODE_1S; This way, the gas sensor will run every second instead of every 10 seconds. There is a quicker mode available, yet this mode is not yet supported unfortunately.

Change LED color & intensity: If you take a look at the end of the thingy_init() function in main.c, you notice that m_ui_led_set_event() is set to M_UI_BLE_DISCONNECTED. If you right click on the m_ui_led_set_event() & press "Go To Definition" & then do the same thing for m_default_config_disconnected, you should end up in m_ui.c:

static const ble_uis_led_t m_default_config_disconnected = UI_CONFIG_DEFAULT_DISCONNECTED;

Right clicking on UI_CONFIG_DEFAULT_DISCONNECTED & going to definition, we can comment out the old definition & change the definition to:

{                                       \
    .mode = BLE_UIS_LED_MODE_CONST,     \
    .data =                             \
    {                                   \
        .mode_const =                   \
        {                               \
            .r= 0xFF,                   \
            .g = 0xFF,                  \
            .b = 0xFF                   \
        }                               \
    }                                   \

Instead of the regular breathe mode, which turns the LED on & off for a given set of time, the const mode keeps the LED on at all times. This is good because we want the discharge current of the battery to be as constant as possible. The .r, .g & .b variables set the intensity of the red, green & blue LEDs. 0xFF is the hexadecimal representation of the decimal value 255, which is the maximum value you can set each respective LED. 

Lastly, let us check to make sure everything works like it should. Compile the Keil Thingy FW code using F7. If you have done everything correctly, you should not receive any errors or warnings. Connect your nrf52DK to the Nordic Thingy via a 10-pin SWD cable. One end can be attached to the P19 Debug Out pin on the DK & the other end to the Thingy:52 10-pin connector. Turn both the DK & the Thingy devices on. The DK will need to be powered via USB.

Then, you can open up the nRFgo Studio application (download from here if needed: Erase the Thingy:52 device & then flash the correct Softdevice to the Thingy:52 (SD v4.0.2). The Softdevice can be found in this folder: external\sdk13\components\softdevice\s132\hex\ in the Thingy FW downloaded from Github.

Next, find the application hex file & program it to the Thingy device. The hex file can be found here: project\pca20020_s132\arm5_no_packs\_debug_v1_0_0\

When everything is done correctly, you should notice that the LED is a constant bright white light. To measure the current consumption, you must first turn off the Thingy device. Then, download the Thingy HW file v1.0.0 from here:

Open up the PDF file located at: Thingy52 - Hardware files 1_0_0\PCA20020-Thingy52 Board 1_0_0\Schematic_Layout pdf files\ & scroll down to sheet 8. Looking at the diagram: "USB- and battery connector and power switch", you notice that SW1 refers to the power switch on the Thingy device. What is interesting is that you can turn off the Thingy device & measure the battery current by connecting a multimeter between TP8 & TP9. Looking at the diagram, you notice that this will correctly measure the battery current. If the switch is on, the current goes to 0, as there is a short circuit between TP8 & TP9. These two points are labeled on the top of the Thingy PCB if you take off the black Thingy plastic cover.

You know you have found the correct points if the Thingy LED turns on. If everything is done correctly, you should notice a current of roughly 73 mA on your multimeter. There are more sophisticated & accurate ways of measuring the current, but this method should suffice for our application.

If you then turn the Thingy device on again, you can open a Segger RTT viewer terminal & you should be able to see the Voltage information that is printed every five seconds or so.

Take a look at this devzone post for saving Segger RTT output data to a text file.  This link shows one possibility of importing the text file into Excel:

Finding the Voltage vs State of Charge Relationship in Excel: I am assuming you have some knowledge of how Excel works. Therefore, from the theory below, you should be able to find the voltage vs state of charge relationship.

 Once you have been able to export the data from the RTT viewer to an Excel file, make sure that you have the timestamp in one column (e.g. column A) & the voltage in another column (e.g. column B). In the next column, calculate the total Ampere-seconds discharged. The first value will be 0. The next values can be calculated by the following function: 

total As discharged (n+1) = total As discharged (n) + current * RTT step time

In my case, the current given in Amperes was 0.0732 & the RTT step time given in seconds was 5. The variables n & n+1 refer to the current & next values of the total As discharged. The current value could for example be in cell C3 & the next value could be in cell C4. Make the same formula apply for all rows in column C.

Next, the depth of discharge (DoD, given as a percentage) at time t (t is given in seconds) can be calculated by the following formula below. The depth of discharge gives a percentage of how much energy has been discharged at a current point in time.

DoD(t) = total As discharged / Q

where Q is the battery capacity given in Ampere-seconds. Q can be found by taking the last value in the column "total As discharged".

Lastly, the State of Charge (SoC) is the inverse of the DoD & can be calculated by:

SoC (t) = 100 - DoD(t)

where t refers to the time in seconds & the units for 100 & DoD(t) are given as a percentage.

For the final application, I assume it is fine to have the state of charge given a battery voltage to the nearest percentage. For example, in the Nordic Thingy application, the state of charge is given as a percentage (no decimal points). Decrementing the next column by 1% with 100 % SoC at the top going down to 0 % SoC (i.e. a column with 101 values starting at 100% down to 0%). Looking at the accurate SoC data, we have too many values that represent each given SoC percentage value (i.e. more than 20 values for 99% SoC). What we will do next is filter out the values not needed by only taking the lowest SoC value for each percentage value. This can be done by using the formula:


Remember to press Ctrl + Shift + Enter when you use this code. Otherwise, you will not get the correct value.

In the last step, we want to find the voltage that corresponds to the minimum SoC value found. This can easily be done by finding the correct measured voltage at the minimum SoC value found using:

=VLOOKUP(K10;$H$10:$N$11477;7; FALSE)

Take a look at my uploaded Excel sheet below for more information on how this column was set up.

Taking a plot of the SoC given as a percentage (no decimals) & the battery voltage gives you a relationship between the SoC & the battery voltage as seen in the Figure below:

Once you have the relationship between State of Charge & Battery Voltage, you can measure the voltage of the Li-Ion battery using the ADC on the nrf52 & use the lookup table relationship that was created to output a State of Charge value. This is exactly what the Nordic Thingy SDK does & you can take a look at the source code for more info related to this.

Also take a look at the uploaded Excel spreadsheet below for a possible solution.

Hope you have enjoyed this detailed, yet long-winded blog post on battery state of charge modelling!