Simple sample for measuring coin cell battery voltage with Zephyr and nRF52840

My project uses Zephyr and the nRF52840 with a CR2450 coin cell battery. All is working well, and now I am looking to add in battery level. I've downloaded and read the following samples.

zephyr/samples/boards/nrf/battery
zephyr/samples/drivers/adc

These samples seem to focus on particular hardware (like the Thingy) that use lithium batteries, and so they have a /vbatt voltage divider to bring the voltage down to a range the nRF52840 can read. There are some ifdef and devicetree mechanisms that imply they could work without a voltage divider, but I'm getting confused by the samples.

My board is powered directly by the CR2450, and so I believe I don't need a voltage divider. My board also has P0.02/AIN0 directly connected to the coin cell (3.3V nominal) in anticipation of using this pin for reading the voltage.

If I could read the voltage, I'll do the math to determine battery level.

My questions are as follows.

- From parts of the samples, I'm guessing that the nRF52840 has an internal way to measure VDD. (See "zephyr,input-positive = <NRF_SAADC_VDD>;" in the nRF52840DK devicetree overlay in the "add" sample.) Is this true? If so, how do I access it?

- If the above is not true, could my connection between P0.02/AIN0 and the coin cell be used to measure its voltage?

- Is there a sample, or some guidance within an existing sample, that you can give me to make this clearer?

Thanks,

Steve

  • Hi Steve,

    abvio said:
    Maybe something to do with DT_DRV_COMPAT? 

    It seems so. DT_DRV_COMPAT is supposed to be a defined macro. However, in your case, it is expended into literal DT_DRV_COMPAT, so it looks like the driver was not loaded correctly.

    See: https://developer.nordicsemi.com/nRF_Connect_SDK/doc/2.3.0/zephyr/build/dts/howtos.html#option-1-create-devices-using-instance-numbers

    Hieu

  • Hi Steve,

    Have you found out where the problem is? I am still slowly getting through my backlog. If you still need help, I should be able to get to this tomorrow.

    Hieu

  • Hi Hieu,

    Thank you for your follow up.  I'm happy to report I have it completely solved and am reading the internal VDD using the ADC.  

    It still wasn't clear why my code wasn't compiling, but after iterating on a lot of possibilities, I did change the sample.yaml in my project to match the sample.yaml in the ADC example, which mentioned "adc". After that and a few more iterations, the code started to compile. And after some further debugging, it started to work. 

    In case someone is looking for some simpler sample code, I reduced the devicetree to just one channel that looks like this: 

    / {
    	zephyr,user {
    		io-channels = <&adc 0>;
    	};
    };
    
    &adc {
    	#address-cells = <1>;
    	#size-cells = <0>;
    	status="okay";
    
    	channel@0 {
    		reg = <0>;
    		zephyr,gain = "ADC_GAIN_1_6";
    		zephyr,reference = "ADC_REF_INTERNAL";
    		zephyr,acquisition-time = <ADC_ACQ_TIME_DEFAULT>;
    		zephyr,input-positive = <NRF_SAADC_VDD>;
    		zephyr,resolution = <14>;
    		zephyr,oversampling = <8>;
    	};
    };
    

    The code I use to access it looks like this: 

    #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)
    };
    
    K_MSGQ_DEFINE(battery_msgq, sizeof(battery_event_t), 10, 4);
    
    int32_t battery_level(void) {
    	int err;
    	int32_t val_mv;
    	int16_t buf = 0;
    	uint8_t level = 0; 
    
    	struct adc_sequence sequence = {
    		.buffer = &buf,
    		/* buffer size in bytes, not number of samples */
    		.buffer_size = sizeof(buf),
    	};
    
    	if (!device_is_ready(adc_channels[0].dev)) {
    		printk("ADC controller device not ready\n");
    		return level;
    	}
    	err = adc_channel_setup_dt(&adc_channels[0]);
    	if (err < 0) {
    		printk("Could not setup channel #%d (%d)\n", 0, err);
    		return level;
    	}
    	(void)adc_sequence_init_dt(&adc_channels[0], &sequence);
    
    	err = adc_read(adc_channels[0].dev, &sequence);
    	if (err < 0) {
    		printk("Could not read (%d)\n", err);
    		return level; 
    	}
    
    	val_mv = buf;
    	err = adc_raw_to_millivolts_dt(&adc_channels[0], &val_mv);
    	if (err < 0) {
    		printk(" (value in mV not available)\n");
    		return level; 
    	}
    
    // Voltage to level curve for a coin cell 3V battery. 
    #define BATTERY_FULL_MV 2950
    #define BATTERY_HALF_MV 2900
    #define BATTERY_QUARTER_MV 2800
    #define BATTERY_DEAD_MV 2300
    
    	if (val_mv >= BATTERY_FULL_MV) {
    		level = 100; 
    	} else if (val_mv <= BATTERY_DEAD_MV) {
    		level = 0; 
    	} else if (val_mv >= BATTERY_HALF_MV) {
    		level = (val_mv - BATTERY_HALF_MV) * 50 / (BATTERY_FULL_MV - BATTERY_HALF_MV) + 50;
    	} else if (val_mv >= BATTERY_QUARTER_MV) {
    		level = (val_mv - BATTERY_QUARTER_MV) * 25 / (BATTERY_HALF_MV - BATTERY_QUARTER_MV) + 25;
    	} else {
    		level = (val_mv - BATTERY_DEAD_MV) * 25 / (BATTERY_QUARTER_MV - BATTERY_DEAD_MV) + 0;
    	}
    
    	return level; 
    	// return val_mv / 100; 
    }

    Thanks again for your help,

    Steve

  • Hi Steve,

    It's good to know that you have got it to work. Thank you for sharing your working code with the community. We appreciate it.

    It is curious how sample.yaml  comes into play. I will try to look into this some time.

    Besides the original question, I don't think I have helped with your build issue at all. Thank you for the kind words.

    Happy developing!

    Hieu

Related