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

Parents
  • Hi Steve,

    - 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?

    Yes. You can find out the details in the SAADC section of the nRF52840 Product Specification.
    In particular, the first subsection, Input Configuration, is what you are looking for.

    (But since you are going to work with ADC, I recommend readding the entire SAADC section to have a full understanding of how it works anyhow).

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

    Using AIN0 also works, and the same documentations I recommended above would explain it.

    This option must be used if you have a regulator between your coin cell and the SoC. In such a setup, VDD is not the coin cell voltage.

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

    The samples you found are exactly the samples for this topic.

    I looked into them and agree that more than a few parts of them don't look very easy to understand. 

    Besides those, in the nRF Desktop sample application, there is a battery measurement module. It may or may not just make you more confused though, since it does more things than what you are looking for.

    If you have some specific points you want to ask about, I can help you with them.

    Hieu

  • Thanks Hieu,

    I've decided to use the "ADC" sample. I've been able to compile it and have it work on my board, showing the 2970 millivolts of the battery using channel 1, which seems to be the internal measure. So now I just need to integrated it into my project. My app currently successfully uses GPIO, SPI and Bluetooth, as well as does DFU, and I would think something like ADC would be simple.

    Here's a snippet of the code I've added. 

    #if !DT_NODE_EXISTS(DT_PATH(zephyr_user)) || \
        !DT_NODE_HAS_PROP(DT_PATH(zephyr_user), io_channels)
    #error "No suitable device tree overlay specified"
    #endif
    
    #define DT_SPEC_AND_COMMA(node_id, prop, idx) \
        ADC_DT_SPEC_INST_GET_BY_IDX(node_id, idx),
    
    static const struct adc_dt_spec adc_channels[] = {
    	DT_FOREACH_PROP_ELEM(DT_PATH(zephyr_user), io_channels,
    			     DT_SPEC_AND_COMMA)
    };

    I'm integrating the code in pieces. I added the following to my device tree. 

    / {
    	zephyr,user {
    		io-channels = <&adc 1>;
    	};
    };
    
    &adc {
    	#address-cells = <1>;
    	#size-cells = <0>;
    	status = "okay";
    
    	channel@1 {
    		reg = <1>;
    		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>;
    	};
    };

    I've added CONFIG_ADC=y to the prj.conf.

    However, I spent today trying to get it to compile without success. Here is the beginning of a long list of errors.  Something seems to be wrong with my device tree. In particular, the errors are triggered by the DT_FOREACH_PROP_ELEM macro. 

    In file included from C:\Users\steve\ncs\v2.2.0\zephyr\include\zephyr\toolchain\gcc.h:88,
                     from C:\Users\steve\ncs\v2.2.0\zephyr\include\zephyr\toolchain.h:50,
                     from C:\Users\steve\ncs\v2.2.0\zephyr\include\zephyr\kernel_includes.h:19,
                     from C:\Users\steve\ncs\v2.2.0\zephyr\include\zephyr\kernel.h:17,
                     from c:\Users\steve\Documents\Projects\display-firmware\src\battery.h:5,
                     from c:\Users\steve\Documents\Projects\display-firmware\src\battery.c:5:
    C:\Users\steve\ncs\v2.2.0\zephyr\include\zephyr\device.h:83:41: error: '__device_dts_ord_DT_N_INST_DT_N_S_zephyr_user_DT_DRV_COMPAT_P_io_channels_IDX_0_PH_ORD' undeclared here (not in a function)
       83 | #define DEVICE_NAME_GET(dev_id) _CONCAT(__device_, dev_id)
          |                                         ^~~~~~~~~
    C:\Users\steve\ncs\v2.2.0\zephyr\include\zephyr\toolchain\common.h:132:26: note: in definition of macro '_DO_CONCAT'
      132 | #define _DO_CONCAT(x, y) x ## y
          |                          ^
    C:\Users\steve\ncs\v2.2.0\zephyr\include\zephyr\device.h:83:33: note: in expansion of macro '_CONCAT'
       83 | #define DEVICE_NAME_GET(dev_id) _CONCAT(__device_, dev_id)

    Devicetree macro debugging seems quite challenging. Any thoughts at what steps I should take to solve this?

    Best,

    Steve

  • Hi Steve,

    I am a bit tied up for the rest of the week. I will try to start looking into this around Monday and Tuesday and get back to you.

    If that's not OK please let me know.

    Regards,

    Hieu

Reply Children
  • Hi Hieu,

    Thanks for letting me know you're busy. 

    In the meantime, if you have any canned advice on debugging devicetree compile errors, that would be helpful. The crucial issue is why is the following not defined using my device tree, which copies the sample program.  Maybe something to do with DT_DRV_COMPAT? 

    __device_dts_ord_DT_N_INST_DT_N_S_zephyr_user_DT_DRV_COMPAT_P_io_channels_IDX_0_PH_ORD

    Thanks again for your help,

    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