k_timer / Periodic Timer Stops After Single Execution

nRF Connect SDK 3.0.2

Insight SiP ISP2454 module

Using zephyr

mcu boot. 

In my application, I am using a 400 ms timer to measure the battery voltage. However, when the issue occurs, the periodic timer stops after executing only once.

To verify this, I defined another timer with a 1-second interval, and I am observing the same behavior, the timer handler is called only once and then stops.

I have attached my battery handler file for reference.

I am not able to recover the device from this state even after re-flashing the firmware. I also tried increasing the stack size, but that did not resolve the issue.

My observation is that the device can be recovered only after performing Erase All and then power cycling the device.

This issue is observed after executing OTA updates multiple times.

Thanks,

Nirav Patel 

/**
 * @file battery_monitor.c
 * @brief Source file for the battery monitoring module.
 *
 * This module handles ADC measurements, voltage calculation,
 * battery percentage conversion, and hysteresis application for
 * a Li-Po battery connected via a voltage divider, with configuration
 * pulled from Device Tree.
 */

#include "battery_monitor.h"
#include "sys_event.h"

#include <zephyr/kernel.h>
#include <zephyr/drivers/adc.h>
#include <zephyr/devicetree.h>
#include <zephyr/logging/log.h>
#include <math.h> // For round()
#include <zephyr/devicetree/io-channels.h>
#include <zephyr/drivers/gpio.h>


LOG_MODULE_REGISTER(battery_monitor);

static struct k_timer test_timer;

/***************************************************************************/
/* --- Defines and Constants --- */
/** Charging detect node */
#define CHARGING_NODE DT_NODELABEL(charging_status)

/** ADC channel node for battery voltage measurement */
#define ADC_CHANNEL_NODE DT_NODELABEL(adc_ch4)

/** Node label for the battery voltage measurement ADC channel. */
#define VBATT DT_PATH(vbatt)

/** ADC channel ID derived from the 'vbatt' node in devicetree. */
#define ADC_CHANNEL_FROM_DTS (DT_REG_ADDR(DT_NODELABEL(adc_ch4)))

/** Resistor R2 (low side) value in ohms, from 'vbatt' node in devicetree. */
#define R2_DIVIDER_OHM DT_PROP(VBATT, output_ohms)

/** Total resistance (R1 + R2) in ohms, from 'vbatt' node in devicetree. */
#define R_TOTAL_DIVIDER_OHM DT_PROP(VBATT, full_ohms)

/** Resistor R1 (high side) value in ohms, calculated from DTS values. */
#define R1_DIVIDER_OHM (R_TOTAL_DIVIDER_OHM - R2_DIVIDER_OHM)

/** ADC resolution in bits. SAADC supports up to 14 bits. */
#define ADC_RESOLUTION 12

/** ADC gain setting. ADC_GAIN_1_2 allows input up to 1.8V with internal reference. */
#define ADC_GAIN adc_channel_cfg.gain

/** ADC oversampling setting. 4x oversampling for burst mode. */
#define ADC_OVERSAMPLING_4X 4

/** Number of samples to average for each battery measurement. */
#define NUM_SAMPLES 5

/** Interval between individual ADC measurements in milliseconds. */
#define MEASUREMENT_INTERVAL_MS 400

/** Voltage (in mV) considered 100% battery. */
#define BATTERY_FULL_MV 4000

/** Voltage (in mV) considered 0% battery. */
#define BATTERY_EMPTY_MV 3700

/** Internal ADC reference voltage in millivolts (0.6V). */
#define VREF_MV 900

/***************************************************************************/
/* --- Static Variables --- */

/** GPIO device structure for the battery charge status pin. */
static const struct gpio_dt_spec charge_detect = GPIO_DT_SPEC_GET(CHARGING_NODE, gpios);

/** Work structure for ADC processing. */
static struct k_work adc_work;

/** ADC device pointer. */
static const struct device *adc_dev;

/** ADC channel configuration structure, derived from devicetree. */
static const struct adc_channel_cfg adc_channel_cfg = ADC_CHANNEL_CFG_DT(ADC_CHANNEL_NODE);

/** ADC sequence structure for a single sample. */
static struct adc_sequence adc_sequence = {
    .channels    = BIT(ADC_CHANNEL_FROM_DTS),
    .resolution  = ADC_RESOLUTION,
    .oversampling = ADC_OVERSAMPLING_4X, /* Enabled burst mode (4x oversampling) */
    .calibrate   = false,
};

/** Buffer to store a single ADC sample. */
static int16_t adc_buffer;

/** Sum of collected ADC samples for averaging. */
static uint32_t sample_sum = 0;

/** Counter for collected ADC samples. */
static uint8_t sample_count = 0;

/** Stores the last calculated battery percentage. */
static uint8_t current_battery_percentage = 0;

/** Timer count for debugging purposes. */
static uint16_t timer_count = 0;

/***************************************************************************/
/* --- Static Function Prototypes --- */

/**
 * @brief Timer handler for periodic battery measurements.
 *
 * This function is called by the Zephyr kernel timer every
 * MEASUREMENT_INTERVAL_MS. It triggers an ADC conversion, processes
 * the sample, and if enough samples are collected, calculates the
 * battery percentage and invokes the callback.
 *
 * @param timer_id Pointer to the k_timer instance that expired.
 */
static void battery_measurement_timer_handler(struct k_timer *timer_id);

/**
 * @brief Timer handler for testing purposes.
 * 
 * @param timer_id 
 */
static void test_timer_handler(struct k_timer *timer_id);

/**
 * @brief Calculates the battery percentage from raw voltage.
 *
 * This function converts the measured battery voltage (in mV) into
 * a percentage based on defined full and empty voltage levels.
 *
 * @param voltage_mv The measured battery voltage in millivolts.
 * @return The calculated battery percentage (0-100).
 */
static uint8_t calculate_battery_percentage(uint32_t voltage_mv);

/**
 * @brief Applies hysteresis to the calculated battery percentage.
 *
 * This function adjusts the battery percentage to provide a more stable
 * reading, preventing rapid fluctuations around specific thresholds.
 *
 * @param percentage The raw calculated battery percentage.
 * @return The battery percentage after applying hysteresis.
 */
static uint8_t apply_hysteresis(uint8_t percentage);

/***************************************************************************/
/* --- Timer Definition --- */

/** Zephyr kernel timer for periodic battery measurements. */
K_TIMER_DEFINE(battery_measurement_timer, battery_measurement_timer_handler, NULL);

/***************************************************************************/
/* --- Static Function Implementations --- */

static void battery_measurement_timer_handler(struct k_timer *timer_id)
{
    timer_count++;
    LOG_INF("Bat mes timer triggered, count: %u", timer_count);
    int rc = k_work_submit(&adc_work);
    LOG_INF("timer=%u work_submit rc=%d", timer_count, rc);
}

/* Test timer handler */
static void test_timer_handler(struct k_timer *timer_id)
{
    LOG_INF("Test timer triggered");
}

/***************************************************************************/
static void adc_work_handler(struct k_work *work)
{
    int ret;
    int32_t adc_vref_mv = VREF_MV; // Internal reference voltage
    int32_t adc_gain_factor;
    int32_t measured_adc_mv;
    uint32_t battery_voltage_mv;
    uint8_t calculated_percentage;

    /* Set buffer for ADC sequence */
    adc_sequence.buffer = &adc_buffer;
    adc_sequence.buffer_size = sizeof(adc_buffer);

    ret = adc_read(adc_dev, &adc_sequence);
    if (ret < 0) {
        LOG_INF("ADC read failed with error %d", ret);
        return;
    }

    /*
     * Convert raw ADC value to millivolts.
     * The formula for millivolts:
     * millivolts = raw_value * Vref_mv / (2^resolution - 1) * gain_factor
     * The gain_factor depends on the ADC_GAIN setting.
     */
    switch (ADC_GAIN)
    {
        case ADC_GAIN_1_6:
            adc_gain_factor = 6;
            break;
        case ADC_GAIN_1_5:
            adc_gain_factor = 5;
            break;
        case ADC_GAIN_1_4:
            adc_gain_factor = 4;
            break;
        case ADC_GAIN_1_3:
            adc_gain_factor = 3;
            break;
        case ADC_GAIN_1_2:
            adc_gain_factor = 2;
            break;
        case ADC_GAIN_1:
            adc_gain_factor = 1;
            break;
        case ADC_GAIN_2:
            adc_gain_factor = 1; // For gain 2, input is divided by 2, so factor is 1 for compensation
            break;
        case ADC_GAIN_4:
            adc_gain_factor = 1; // For gain 4, input is divided by 4, so factor is 1 for compensation
            break;
        default:
            LOG_INF("Unsupported ADC gain configuration.");
            return;
    }

    // Calculate measured voltage at the ADC input pin
    measured_adc_mv = (int32_t)adc_buffer * adc_vref_mv * adc_gain_factor / ((1 << ADC_RESOLUTION) - 1);

    // Account for voltage divider: V_battery = V_measured * (R1 + R2) / R2
    // R1_DIVIDER_OHM and R2_DIVIDER_OHM are now pulled from DTS.
    battery_voltage_mv = (uint32_t)round((double)measured_adc_mv * R_TOTAL_DIVIDER_OHM / R2_DIVIDER_OHM);

    LOG_INF("Measured ADC raw: %d, Measured ADC mV: %d, Battery mV: %d",
            adc_buffer, measured_adc_mv, battery_voltage_mv);

    sample_sum += battery_voltage_mv;
    sample_count++;

    if (sample_count >= NUM_SAMPLES) {
        /* Stop the timer until explicitly restarted */
        k_timer_stop(&battery_measurement_timer);

        uint32_t average_voltage_mv = sample_sum / NUM_SAMPLES;
        LOG_INF("Average Battery Voltage: %u mV", average_voltage_mv);

        calculated_percentage = calculate_battery_percentage(average_voltage_mv);
        current_battery_percentage = apply_hysteresis(calculated_percentage);

        LOG_INF("Battery Percentage (raw): %u%%, (hysteresis): %u%%",
                calculated_percentage, current_battery_percentage);

        sys_event_t evt = {
            .type = EVENT_BATTERY_LEVEL_UPDATE,
            .data.battery.percentage = current_battery_percentage
        };
        sys_event_post(&evt);

        /* Reset for the next measurement cycle */
        sample_sum = 0;
        sample_count = 0;
    }
}

/***************************************************************************/
static uint8_t calculate_battery_percentage(uint32_t voltage_mv)
{
    if (voltage_mv >= BATTERY_FULL_MV) {
        return 100;
    } else if (voltage_mv <= BATTERY_EMPTY_MV) {
        return 0;
    } else {
        /* Linear interpolation */
        uint32_t range_mv = BATTERY_FULL_MV - BATTERY_EMPTY_MV;
        uint32_t current_mv_offset = voltage_mv - BATTERY_EMPTY_MV;
        uint8_t percentage = (uint8_t)round((double)current_mv_offset * 100.0 / range_mv);
        return percentage;
    }
}

/***************************************************************************/
static uint8_t apply_hysteresis(uint8_t percentage)
{
    if (percentage <= 5) {
        return 0;
    } else if (percentage <= 15) {
        return 10;
    } else if (percentage <= 25) {
        return 20;
    } else if (percentage <= 35) {
        return 30;
    } else if (percentage <= 45) {
        return 40;
    } else if (percentage <= 55) {
        return 50;
    } else if (percentage <= 65) {
        return 60;
    } else if (percentage <= 75) {
        return 70;
    } else if (percentage <= 85) {
        return 80;
    } else if (percentage <= 95) {
        return 90;
    } else { /* percentage > 95 */
        return 100;
    }
}

/***************************************************************************/
/* --- Public Function Implementations --- */

int battery_monitor_start_periodic_measurement(void)
{
    /* Ensure previous measurement cycle is reset before starting a new one */
    sample_sum = 0;
    sample_count = 0;
    timer_count = 0;

    k_timer_stop(&battery_measurement_timer);
    k_timer_start(&battery_measurement_timer, K_NO_WAIT, K_MSEC(MEASUREMENT_INTERVAL_MS));
    LOG_INF("Periodic battery measurement started (every %d ms)", MEASUREMENT_INTERVAL_MS);
    return 0;
}

/***************************************************************************/
uint8_t battery_monitor_get_percentage(void)
{
    return current_battery_percentage;
}

/***************************************************************************/
bool batt_get_charge_status(void)
{
    return (gpio_pin_get_dt(&charge_detect) == 0); // 0 = charging (active low)
}

/***************************************************************************/
int battery_monitor_init(void)
{
    int ret;

    adc_dev = DEVICE_DT_GET(DT_IO_CHANNELS_CTLR(VBATT));
    if (!device_is_ready(adc_dev)) {
        LOG_ERR("ADC device not ready");
        return -ENODEV;
    }

    ret = adc_channel_setup(adc_dev, &adc_channel_cfg);
    if (ret < 0) {
        LOG_ERR("ADC channel setup failed with error %d", ret);
        return ret;
    }

    LOG_INF("Battery monitor initialized on AIN%d using DTS config",
            ADC_CHANNEL_FROM_DTS);

    k_work_init(&adc_work, adc_work_handler);

    if (!device_is_ready(charge_detect.port)) {
        LOG_ERR("Battery charge GPIO port not ready");
        return -ENODEV;
    }

    ret = gpio_pin_configure_dt(&charge_detect, GPIO_INPUT);
    if (ret < 0) {
        LOG_ERR("Failed to configure batt_charge_gpio");
        return ret;
    }

    LOG_INF("Battery is %s", batt_get_charge_status() ? "charging" : "not charging");

    k_timer_init(&test_timer, test_timer_handler, NULL);
    k_timer_start(&test_timer, K_NO_WAIT, K_MSEC(1000));

    return 0;
}
6153.battery_monitor.h

Parents
  • Hi,

    Based on what you write, it may sound like the timer library itself has somehow ended up in a faulty state.

    Are you able to reproduce the same on other devices, and do you have a minimal example for reproducing?

    When the issue has happened, and the device is in the erroneous state: If you read back the application, do you find that it has changed compared to what was put on the device in the first place?

    This issue is observed after executing OTA updates multiple times.

    How many times are we talking here? That is, are we talking about tens, hundreds, or thousands of OTA updates?

    Regards,
    Terje

Reply
  • Hi,

    Based on what you write, it may sound like the timer library itself has somehow ended up in a faulty state.

    Are you able to reproduce the same on other devices, and do you have a minimal example for reproducing?

    When the issue has happened, and the device is in the erroneous state: If you read back the application, do you find that it has changed compared to what was put on the device in the first place?

    This issue is observed after executing OTA updates multiple times.

    How many times are we talking here? That is, are we talking about tens, hundreds, or thousands of OTA updates?

    Regards,
    Terje

Children
Related