ADC Peripheral Power Management on NRF52840 (BMD-340)

OS: Windows 11 Pro

OS Version: 10.0.22631

Toolchain: nRF Connect SDK (VS Code Extension) version 2.9.0

Hardware: BMD-340

Our Implementation requires disabling the ADC peripheral while not in use to conserve power.

Issue: While attempting to disable the peripheral through the function call void ADC_Disable(void) or enable the peripheral through the function call void ADC_Enable(void), the peripheral is not able to change it's power state. The error output shows the following:

[00:00:59.810,058] <err> battery_monitor: Failed to resume ADC: -88

[00:01:05.235,168] <err> battery_monitor: Failed to suspend ADC: -88

Looking up the error shows the following:
ENOSYS 88
Function not implemented.

Question: What configuration or code needs to be added to be able to manage the ADC peripheral power?

The current device tree configuration is provided below:

&adc {
    status = "okay";
    #address-cells = <1>;
    #size-cells = <0>;
    zephyr,pm-device-runtime-auto;
    channel@0 {
        reg = <0>;
        zephyr,gain = "ADC_GAIN_1_6";
        zephyr,reference = "ADC_REF_INTERNAL";
        zephyr,resolution = <12>;
        zephyr,acquisition-time = <ADC_ACQ_TIME(ADC_ACQ_TIME_MICROSECONDS, 20)>;
        zephyr,input-positive = <NRF_SAADC_VDD>;
    };
};
We have written a function for monitoring the battery voltage of the device which enables the peripheral, takes a sample, then disables the peripheral. I have provided the Implementation Below:
AdcBattery.h
#ifndef ADC_BATTERY_H
#define ADC_BATTERY_H

#include <stdint.h>
#include <zephyr/device.h>
#include <zephyr/drivers/adc.h>
#include "SystemConfig.h"

#ifdef DEBUG_BATTERY_MONITORING
#define BATTERY_VOLTAGE_READ_PERIOD_SECONDS 10
#else
#define BATTERY_VOLTAGE_READ_PERIOD_SECONDS 43200   /** Period in which the battery voltage will be read in seconds. 43200 = 12 hours (twice per day) */
#endif

/**
 * @brief Initializes the ADC peripheral for battery voltage measurement.
 */
void ADC_Init(void);

/**
 * @brief Disables the ADC peripheral to conserve power.
 */
void ADC_Disable(void);

/**
 * @brief Enables the ADC peripheral to obtain a sample.
 */
void ADC_Enable(void);

/**
 * @brief Reads the battery voltage and returns it as millivolts (mV * 1000).
 *
 * @return uint16_t The battery voltage in millivolts x 1000.
 */
uint16_t ADC_GetBatteryVoltage(void);

#endif // ADC_BATTERY_H
AdcBattery.c
#include "AdcBattery.h"
#include <zephyr/kernel.h>
#include <zephyr/sys/printk.h>
#include <zephyr/device.h>
#include <zephyr/drivers/adc.h>
#include <zephyr/devicetree.h>
#include <zephyr/pm/device.h>
#include <zephyr/logging/log.h>

LOG_MODULE_REGISTER(battery_monitor, LOG_LEVEL_INF);

#define ADC_RESOLUTION         12
#define ADC_CHANNEL_ID         0
#define ADC_GAIN               ADC_GAIN_1_6
#define ADC_REFERENCE_VOLTAGE  600  // Internal reference voltage in millivolts
#define ADC_MAX_COUNTS         4096 // 2^12 for 12-bit resolution

// Use adc_dt_spec for defining ADC channel

static const struct adc_dt_spec adc_channel = ADC_DT_SPEC_GET_BY_NAME(DT_PATH(zephyr_user), a0);

static int16_t adc_sample_buffer;
static bool adc_initialized = false;
static bool adc_enabled = false;

static struct adc_sequence sequence = {
    .buffer = &adc_sample_buffer,
    .buffer_size = sizeof(int16_t),
};

void ADC_Init(void) {
    int err = 0;

    if (!adc_is_ready_dt(&adc_channel)) {
        LOG_ERR("ADC controller device %s not ready\n", "Battery");
        return;
    }

    err = adc_channel_setup_dt(&adc_channel);
    if (err < 0) {
        LOG_ERR("Could not setup channel #%d (%d)\n", 0, err);
        return;
    }
    adc_initialized = true;
    LOG_INF("ADC initialized using Zephyr DT\n");
}

void ADC_Disable(void) {
    enum pm_device_state state;
    if (adc_enabled) {
        pm_device_state_get(adc_channel.dev, &state);
        __NOP();
        int ret = pm_device_action_run(adc_channel.dev, PM_DEVICE_ACTION_SUSPEND);
        if (ret == 0) {
            LOG_INF("ADC suspended\n");
        } else {
            LOG_ERR("Failed to suspend ADC: %d\n", ret);
        }
        adc_enabled = false;
    }
   
}

void ADC_Enable(void) {
    enum pm_device_state state;
    if (!adc_initialized) {
        ADC_Init();
    } else {
        pm_device_state_get(adc_channel.dev, &state);
        __NOP();
        int ret = pm_device_action_run(adc_channel.dev, PM_DEVICE_ACTION_RESUME);
        if (ret == 0) {
            LOG_INF("ADC resumed\n");
        } else {
            LOG_ERR("Failed to resume ADC: %d\n", ret);
        }
        adc_enabled = true;
    }
}

uint16_t ADC_GetBatteryVoltage(void) {
    int err = 0;
    if ((!adc_initialized) || (!adc_enabled)) {
        ADC_Enable();
    }

    (void)adc_sequence_init_dt(&adc_channel, &sequence);

    err = adc_read_dt(&adc_channel, &sequence);
    ADC_Disable();
    if (err < 0) {
        LOG_ERR("Could not read (%d)\n", err);
    }

    // Convert ADC counts to millivolts: Voltage = (counts / max_counts) * (reference_voltage / gain)
    uint16_t voltage_mv = ((uint32_t)adc_sample_buffer * ADC_REFERENCE_VOLTAGE * 6) / ADC_MAX_COUNTS;
    return voltage_mv;
}
Parents
  • Hi,

    Do yo make sure to stop ongoin sampling before calling your ADC_Disable() function?

  • There is no "stop" API call, how do you stop the sample sequence? If there is not a way to explicitly stop the sample sequence can i configure for "single sample" or "single shot" mode?

  • Hi,

    I am sorry for the very late reply. You are right, when using the Zephyr blocking API there is no stop. It is also not needed in this case, and you only need to enable device power management in Kconfig, and it will be handled automatically under the holld. That can be demonstrated by using the adc_dt sample from SDK 2.9, with this added to prj.conf:

    CONFIG_PM_DEVICE=y
    CONFIG_PM_DEVICE_RUNTIME=y

    However, the sample will continue to consume a significant amount of current in sleep due to the UART being enabled, so I modified the sample like this to sample for a short time and then also disable the UART. If you measure the current consumption of this, you should see a low sleep current as expected:

    /*
     * Copyright (c) 2020 Libre Solar Technologies GmbH
     *
     * SPDX-License-Identifier: Apache-2.0
     */
    
    #include <inttypes.h>
    #include <stddef.h>
    #include <stdint.h>
    
    #include <zephyr/device.h>
    #include <zephyr/devicetree.h>
    #include <zephyr/drivers/adc.h>
    #include <zephyr/kernel.h>
    #include <zephyr/sys/printk.h>
    #include <zephyr/sys/util.h>
    #include <zephyr/pm/device.h>
    #include <zephyr/pm/device_runtime.h>
    
    #if !DT_NODE_EXISTS(DT_PATH(zephyr_user)) || \
    	!DT_NODE_HAS_PROP(DT_PATH(zephyr_user), io_channels)
    #error "No suitable devicetree overlay specified"
    #endif
    
    #define DT_SPEC_AND_COMMA(node_id, prop, idx) \
    	ADC_DT_SPEC_GET_BY_IDX(node_id, idx),
    
    /* Data of ADC io-channels specified in devicetree. */
    static const struct adc_dt_spec adc_channels[] = {
    	DT_FOREACH_PROP_ELEM(DT_PATH(zephyr_user), io_channels,
    			     DT_SPEC_AND_COMMA)
    };
    
    void uart_suspend(void)
    {
    	static const struct device *const console_dev = DEVICE_DT_GET(DT_CHOSEN(zephyr_console));
    
    	/* Disable console UART */
    	int err = pm_device_action_run(console_dev, PM_DEVICE_ACTION_SUSPEND);
    	if (err < 0)
    	{
    		printk("Unable to suspend console UART. (err: %d)\n", err);
    	}
    }
    
    int main(void)
    {
    	int err;
    	uint32_t count = 0;
    	uint16_t buf;
    	struct adc_sequence sequence = {
    		.buffer = &buf,
    		/* buffer size in bytes, not number of samples */
    		.buffer_size = sizeof(buf),
    	};
    	/* Configure channels individually prior to sampling. */
    	for (size_t i = 0U; i < ARRAY_SIZE(adc_channels); i++) {
    		if (!adc_is_ready_dt(&adc_channels[i])) {
    			printk("ADC controller device %s not ready\n", adc_channels[i].dev->name);
    			return 0;
    		}
    
    		err = adc_channel_setup_dt(&adc_channels[i]);
    		if (err < 0) {
    			printk("Could not setup channel #%d (%d)\n", i, err);
    			return 0;
    		}
    	}
    
    	for (int k = 0; k < 2; k++) {
    		printk("ADC reading[%u]:\n", count++);
    		for (size_t i = 0U; i < ARRAY_SIZE(adc_channels); i++) {
    			int32_t val_mv;
    
    			printk("- %s, channel %d: ",
    			       adc_channels[i].dev->name,
    			       adc_channels[i].channel_id);
    
    			(void)adc_sequence_init_dt(&adc_channels[i], &sequence);
    
    			err = adc_read_dt(&adc_channels[i], &sequence);
    			if (err < 0) {
    				printk("Could not read (%d)\n", err);
    				continue;
    			}
    
    			/*
    			 * If using differential mode, the 16 bit value
    			 * in the ADC sample buffer should be a signed 2's
    			 * complement value.
    			 */
    			if (adc_channels[i].channel_cfg.differential) {
    				val_mv = (int32_t)((int16_t)buf);
    			} else {
    				val_mv = (int32_t)buf;
    			}
    			printk("%"PRId32, val_mv);
    			err = adc_raw_to_millivolts_dt(&adc_channels[i],
    						       &val_mv);
    			/* conversion to mV may not be supported, skip if not */
    			if (err < 0) {
    				printk(" (value in mV not available)\n");
    			} else {
    				printk(" = %"PRId32" mV\n", val_mv);
    			}
    		}
    
    		k_sleep(K_MSEC(1000));
    	}
    
    	uart_suspend();
    
    	// Wait forever
    	while (1) {
    		k_sleep(K_MSEC(1000));
    	}
    
    	return 0;
    }
    

Reply
  • Hi,

    I am sorry for the very late reply. You are right, when using the Zephyr blocking API there is no stop. It is also not needed in this case, and you only need to enable device power management in Kconfig, and it will be handled automatically under the holld. That can be demonstrated by using the adc_dt sample from SDK 2.9, with this added to prj.conf:

    CONFIG_PM_DEVICE=y
    CONFIG_PM_DEVICE_RUNTIME=y

    However, the sample will continue to consume a significant amount of current in sleep due to the UART being enabled, so I modified the sample like this to sample for a short time and then also disable the UART. If you measure the current consumption of this, you should see a low sleep current as expected:

    /*
     * Copyright (c) 2020 Libre Solar Technologies GmbH
     *
     * SPDX-License-Identifier: Apache-2.0
     */
    
    #include <inttypes.h>
    #include <stddef.h>
    #include <stdint.h>
    
    #include <zephyr/device.h>
    #include <zephyr/devicetree.h>
    #include <zephyr/drivers/adc.h>
    #include <zephyr/kernel.h>
    #include <zephyr/sys/printk.h>
    #include <zephyr/sys/util.h>
    #include <zephyr/pm/device.h>
    #include <zephyr/pm/device_runtime.h>
    
    #if !DT_NODE_EXISTS(DT_PATH(zephyr_user)) || \
    	!DT_NODE_HAS_PROP(DT_PATH(zephyr_user), io_channels)
    #error "No suitable devicetree overlay specified"
    #endif
    
    #define DT_SPEC_AND_COMMA(node_id, prop, idx) \
    	ADC_DT_SPEC_GET_BY_IDX(node_id, idx),
    
    /* Data of ADC io-channels specified in devicetree. */
    static const struct adc_dt_spec adc_channels[] = {
    	DT_FOREACH_PROP_ELEM(DT_PATH(zephyr_user), io_channels,
    			     DT_SPEC_AND_COMMA)
    };
    
    void uart_suspend(void)
    {
    	static const struct device *const console_dev = DEVICE_DT_GET(DT_CHOSEN(zephyr_console));
    
    	/* Disable console UART */
    	int err = pm_device_action_run(console_dev, PM_DEVICE_ACTION_SUSPEND);
    	if (err < 0)
    	{
    		printk("Unable to suspend console UART. (err: %d)\n", err);
    	}
    }
    
    int main(void)
    {
    	int err;
    	uint32_t count = 0;
    	uint16_t buf;
    	struct adc_sequence sequence = {
    		.buffer = &buf,
    		/* buffer size in bytes, not number of samples */
    		.buffer_size = sizeof(buf),
    	};
    	/* Configure channels individually prior to sampling. */
    	for (size_t i = 0U; i < ARRAY_SIZE(adc_channels); i++) {
    		if (!adc_is_ready_dt(&adc_channels[i])) {
    			printk("ADC controller device %s not ready\n", adc_channels[i].dev->name);
    			return 0;
    		}
    
    		err = adc_channel_setup_dt(&adc_channels[i]);
    		if (err < 0) {
    			printk("Could not setup channel #%d (%d)\n", i, err);
    			return 0;
    		}
    	}
    
    	for (int k = 0; k < 2; k++) {
    		printk("ADC reading[%u]:\n", count++);
    		for (size_t i = 0U; i < ARRAY_SIZE(adc_channels); i++) {
    			int32_t val_mv;
    
    			printk("- %s, channel %d: ",
    			       adc_channels[i].dev->name,
    			       adc_channels[i].channel_id);
    
    			(void)adc_sequence_init_dt(&adc_channels[i], &sequence);
    
    			err = adc_read_dt(&adc_channels[i], &sequence);
    			if (err < 0) {
    				printk("Could not read (%d)\n", err);
    				continue;
    			}
    
    			/*
    			 * If using differential mode, the 16 bit value
    			 * in the ADC sample buffer should be a signed 2's
    			 * complement value.
    			 */
    			if (adc_channels[i].channel_cfg.differential) {
    				val_mv = (int32_t)((int16_t)buf);
    			} else {
    				val_mv = (int32_t)buf;
    			}
    			printk("%"PRId32, val_mv);
    			err = adc_raw_to_millivolts_dt(&adc_channels[i],
    						       &val_mv);
    			/* conversion to mV may not be supported, skip if not */
    			if (err < 0) {
    				printk(" (value in mV not available)\n");
    			} else {
    				printk(" = %"PRId32" mV\n", val_mv);
    			}
    		}
    
    		k_sleep(K_MSEC(1000));
    	}
    
    	uart_suspend();
    
    	// Wait forever
    	while (1) {
    		k_sleep(K_MSEC(1000));
    	}
    
    	return 0;
    }
    

Children
No Data
Related