RAK4631 Battery Reading never changes

Core: RAK4631
Base: RAK19010
Power: RAK19012
nRF Connect v3.2.1
Zephyr

I am trying to read the current battery voltage on my setup. The problem is that the value being read from AIN2 always seems to float around 4000mv (after conversion).

My relevant devicetree:
zephyr,user
{
io-channels = <&adc 0>, <&adc 1>, <&adc 2>, <&adc 3>, <&adc 4>, <&adc 5>, <&adc 6>, <&adc 7>;
io-channel-names = “ain0”, “ain1”, “ain2”, “ain3”, “ain4”, “ain5”, “ain6”, “ain7”;
};

&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(ADC_ACQ_TIME_MICROSECONDS, 10)>;
	zephyr,input-positive = <(NRF_SAADC_AIN0)>; 
	zephyr,resolution = <12>;
};	

channel@1 
{
	reg = <1>;
	zephyr,gain = "ADC_GAIN_1_6";
	zephyr,reference = "ADC_REF_INTERNAL";
	zephyr,acquisition-time = <ADC_ACQ_TIME(ADC_ACQ_TIME_MICROSECONDS, 10)>;
	zephyr,input-positive = <NRF_SAADC_AIN1>; 
	zephyr,resolution = <12>;
};	

channel@2 
{
	reg = <2>;
	zephyr,gain = "ADC_GAIN_1_6";
	zephyr,reference = "ADC_REF_INTERNAL";
	zephyr,acquisition-time = <ADC_ACQ_TIME(ADC_ACQ_TIME_MICROSECONDS, 10)>;
	zephyr,input-positive = <NRF_SAADC_AIN2>; 
	zephyr,resolution = <12>;
	zephyr,oversampling = <4>;  // Optional: Enable oversampling for better accuracy
};	

channel@3 
{
	reg = <3>;
	zephyr,gain = "ADC_GAIN_1_6";
	zephyr,reference = "ADC_REF_INTERNAL";
	zephyr,acquisition-time = <ADC_ACQ_TIME(ADC_ACQ_TIME_MICROSECONDS, 10)>;
	zephyr,input-positive = <NRF_SAADC_AIN3>; 
	zephyr,resolution = <12>;
};	

channel@4 
{
	reg = <4>;
	zephyr,gain = "ADC_GAIN_1_6";
	zephyr,reference = "ADC_REF_INTERNAL";
	zephyr,acquisition-time = <ADC_ACQ_TIME(ADC_ACQ_TIME_MICROSECONDS, 10)>;
	zephyr,input-positive = <NRF_SAADC_AIN4>; 
	zephyr,resolution = <12>;
};	

channel@5 
{
	reg = <5>;
	zephyr,gain = "ADC_GAIN_1_6";
	zephyr,reference = "ADC_REF_INTERNAL";
	zephyr,acquisition-time = <ADC_ACQ_TIME(ADC_ACQ_TIME_MICROSECONDS, 10)>;
	zephyr,input-positive = <NRF_SAADC_AIN5>; 
	zephyr,resolution = <12>;
};	

channel@6 
{
	reg = <6>;
	zephyr,gain = "ADC_GAIN_1_6";
	zephyr,reference = "ADC_REF_INTERNAL";
	zephyr,acquisition-time = <ADC_ACQ_TIME(ADC_ACQ_TIME_MICROSECONDS, 10)>;
	zephyr,input-positive = <NRF_SAADC_AIN6>; 
	zephyr,resolution = <12>;
};	

channel@7 
{
	reg = <7>;
	zephyr,gain = "ADC_GAIN_1_6";
	zephyr,reference = "ADC_REF_INTERNAL";
	zephyr,acquisition-time = <ADC_ACQ_TIME(ADC_ACQ_TIME_MICROSECONDS, 10)>;
	zephyr,input-positive = <NRF_SAADC_AIN7>; 
	zephyr,resolution = <12>;
};	

};

*I have all the AIN inputs defined as part of a test to read from all channels to see if AIN2 was the correct channel.

C Code:
DataStructs:

static const struct adc_dt_spec adc_channels = {
ADC_DT_SPEC_GET_BY_NAME(DT_PATH(zephyr_user), ain0),
ADC_DT_SPEC_GET_BY_NAME(DT_PATH(zephyr_user), ain1),
ADC_DT_SPEC_GET_BY_NAME(DT_PATH(zephyr_user), ain2),
ADC_DT_SPEC_GET_BY_NAME(DT_PATH(zephyr_user), ain3),
ADC_DT_SPEC_GET_BY_NAME(DT_PATH(zephyr_user), ain4),
ADC_DT_SPEC_GET_BY_NAME(DT_PATH(zephyr_user), ain5),
ADC_DT_SPEC_GET_BY_NAME(DT_PATH(zephyr_user), ain6),
ADC_DT_SPEC_GET_BY_NAME(DT_PATH(zephyr_user), ain7)
};

static int16_t sample_buffer = {0, 0, 0, 0, 0, 0, 0, 0};
static struct adc_sequence sequence = {
{
.buffer = &sample_buffer[0],
.buffer_size = sizeof(sample_buffer[0]),
},
{
.buffer = &sample_buffer[1],
.buffer_size = sizeof(sample_buffer[0]),
},
{
.buffer = &sample_buffer[2],
.buffer_size = sizeof(sample_buffer[0]),
},
{
.buffer = &sample_buffer[3],
.buffer_size = sizeof(sample_buffer[0]),
},
{
.buffer = &sample_buffer[4],
.buffer_size = sizeof(sample_buffer[0]),
},
{
.buffer = &sample_buffer[5],
.buffer_size = sizeof(sample_buffer[0]),
},
{
.buffer = &sample_buffer[6],
.buffer_size = sizeof(sample_buffer[0]),
},
{
.buffer = &sample_buffer[7],
.buffer_size = sizeof(sample_buffer[0]),
}
};

Initialization:
for (int ch=0; ch < sizeof(adc_channels)/sizeof(adc_channels[0]); ch++)
{
if (!adc_is_ready_dt(&adc_channels[ch]))
{
LOG_ERR(“ADC channel %d device not ready\n”, ch);
return -ENODEV;
}

    err = adc_channel_setup_dt(&adc_channels[ch]);
    if (err < 0) 
    {
        LOG_ERR("ADC channel %d setup failed: %d\n", ch, err);
        return err;
    }
    
    err = adc_sequence_init_dt(&adc_channels[ch], &sequence[ch]);
    if (err < 0) 
    {
        LOG_ERR("ADC channel %d sequence init failed: %d\n", ch, err);
        return err;
    }
}

Read:
static int read_adc_channel(int ch, int32_t* out_mv)
{
int err;

err = adc_read(adc_channels[ch].dev, &sequence[ch]);
if (err < 0) 
{
    return err;
}


*out_mv = (int32_t)sample_buffer[ch];
return 0;

}

Conversion:
static int read_battery_millivolts(int32_t* out_mv)
{
int err;
int32_t adc_val_mv;
int32_t val_mv;

// set_3v3_power(false);	
err = read_adc_channel(2, &adc_val_mv); // Assuming channel 2 is the battery voltage channel
// set_3v3_power(true); // re-enable power after reading

if (err < 0) 
{
    LOG_ERR("Read battery failed: %d", err);
    return err;
}

// Manual conversion to ADC Pin Voltage
val_mv = ((int64_t)adc_val_mv * REFERENCE_VOLTAGE_MV) / MAX_ADC_VALUE;

// Apply divider to get to Battery Voltage
int32_t battery_mv = (int64_t)val_mv * VOLTAGE_DIVIDER_RATIO;

LOG_INF("adc_read(): Raw ADC: %d, ADC pin voltage: %d mV, Battery: %d mV", 
    adc_val_mv, 
    val_mv, 
    battery_mv);  

*out_mv = battery_mv;

return 0;

}

I read from the channel once every 10s. On battery only, the battery mv value drifts ±30mv. When measuring the battery directly on the battery terminals after 8h, it is down 100mv. After 24h it is down 200mv. The reading from the AIN2 seems to float around 4.000mv.

ALSO, in v3.2.1 of the SDK, the value of (devicetree binding) NRF_SAADC_AIN2 == 2, but the value NRF_SAADC_INPUT_AIN2 == 3, the enum is off by 1. I don’t know if that is an issue or not, but it is confusing.

This looks like a h/w config issue to me, I just cannot find where.

jk

Additional Info:
If I read the voltage (using multimeter) from the BAT pin on J11 (using GND on J10), I get the correct voltage. Something at the SAADC configuration/driver is wonky.

I decided to run a test. Read each ADC channel several times, every 10s. First on battery alone, then on USB. Again, i tested the voltage directly on the BAT header on J11 to verify the actual value.

before USB
ADC Channel 0: 42 mV, 56 mV, 77 mV
ADC Channel 1: 17 mV, 43 mV, 64 mV
ADC Channel 2: 2058 mV, 2069 mV, 2063 mV
ADC Channel 3: 2551 mV, 2575 mV, 2392 mV
ADC Channel 4: 375 mV, 357 mV, 347 mV
ADC Channel 5: 128 mV, 142 mV, 165 mV
ADC Channel 6: 100 mV, 120 mV, 136 mV
ADC Channel 7: 127 mV, 143 mV, 157 mV

after USB
ADC Channel 0: 46 mV, 60 mV, 80 mV
ADC Channel 1: 0 mV, 20 mV, 39 mV
ADC Channel 2: 2095 mV, 2105 mV, 2083 mV
ADC Channel 3: 2597 mV, 2602 mV, 2588 mV
ADC Channel 4: 409 mV, 392 mV, 377 mV
ADC Channel 5: 114 mV, 133 mV, 140 mV
ADC Channel 6: 60 mV, 83 mV, 102 mV
ADC Channel 7: 77 mV, 103 mV, 126 mV

At this point I have zero confidence in my ADC readings. Successive reads of certain channels vary by upwards of 100 (unscaled). Readings at J11 on the base board (RAK19010) indicate that the correct voltage is being presented to the base board (assuming it is a direct bus connect). By the time it gets through the RAK4631 ADC (SAADC) and resulting nRF Connect 3.2.1 zephyr logic, it is all over the place.

I tried to re-seat the RAK19012 board on the RAK19010 connector and now I am getting a whole new set of wild floating values. It looks like having the power board separate was a bad idea. Giving up on this combo.

I think I have a promising solution to this issue.

For reference, I am using nRF Connect SDK 3.2.1, RAK4631 core on RAK19007 base. (I was using RAK19010 base but moved to try to rule out any issues with power module connector on the RAK19010).

The devicetree for reading the correct channel looks as follows:

/ {
    vbatt {
        compatible = "voltage-divider";
        io-channels = <&adc 3>;          /* channel 3, NOT channel 2 */
        output-ohms = <100000>;
        full-ohms   = <165275>;       /* 100000 × 1.65275, derived from measurement */
    };
};

&adc {
    #address-cells = <1>;
    #size-cells = <0>;
    status = "okay";

	channel@3 {
		reg = <3>;
		zephyr,gain             = "ADC_GAIN_1_6";
		zephyr,reference        = "ADC_REF_INTERNAL";
		zephyr,acquisition-time = <ADC_ACQ_TIME(ADC_ACQ_TIME_MICROSECONDS, 40)>;
		zephyr,input-positive   = <NRF_SAADC_AIN3>;
		zephyr,resolution       = <12>;
		zephyr,oversampling     = <8>;   /* hardware averages 8 samples */
	};
};

Note one important thing: the channel definition is using AIN3, not AIN2. I believe with nRF 3.2.0, there is an off by one error on the DT constants.
Also, the acquisition time is high (40 microsec) and the oversampling is relatively high. Both of these settings seemed to stabilize the readings coming from the ADC.

The code for reading:

int vbatt_read_mv(int32_t *mv_out)
{
    int     rc;
    int32_t mv;

    /* Trigger a single conversion */
    rc = adc_read(vbatt_adc.dev, &sequence);

    if (rc < 0) 
    {
        LOG_ERR("vbatt: adc_read failed (%d)\n", rc);
        return rc;
    }

    /* Convert raw counts → millivolts at the ADC pin, then the mv value must be scaled to the full range */
    mv = (int32_t)sample_buf;
    rc = adc_raw_to_millivolts_dt(&vbatt_adc, &mv);
// printk("raw=%d  pin_mv=%d\n", sample_buf, mv);
    if (rc < 0) 
    {
        LOG_ERR("vbatt: raw-to-mV conversion failed (%d)\n", rc);
        return rc;
    }

    /* Scale back through the voltage divider to get battery voltage:
       full-ohms / output-ohms 
    */

    *mv_out = (int32_t)((int64_t)mv
              * DT_PROP(DT_PATH(vbatt), full_ohms)
              / DT_PROP(DT_PATH(vbatt), output_ohms));
    return 0;
}

When reading the ADC value, the raw value is a “counts” value. the call to adc_raw_to_millivolts_dt converts that counts value to a mv value (unscaled). To get to the mv value that is scaled for the full range, a little bit of math is required (like the sample code). The values I am using (in the vbatt definition) are based on my multimeter readings on the BAT pin to get the correct calibration.

I am still testing over the full range to see if the scaling is linear over the range. More burn-in testing on this before I am satisfied it is correct.

@sercan
Can you check?

Hello @jkeane,

I checked your overlay file and it seems correct to me. AIN3 is the correct channel. AIN3 is P0.05 analog pin, it is used for battery measurement. Why do you think that there is a bug?

You can check our adc sample example to get more idea: rak-zephyr-app/app/basic/adc at main · srcnert/rak-zephyr-app · GitHub

If you want to get more idea about how to use vbatt node, please go to your Zephyr RTOS repo and take a look: zephyr/samples/boards/nordic/battery/README.rst file.

Please do not hesitate to ask if you have any other question.

Thanks.

Every sample code I could find, every technical document I could find indicates the pin for battery measurement is AIN2/P0.04. The Arduino example lists WB_A0 as the correct analog pin. Read_Battery_Level.ino

The sample you point to is buried as a generic ADC read sample. The battery level reading purpose is pretty obscured and is not discoverable by search. Try a google search for something like “RAK4631 Read Batter Level Zephyr”. At least it doesnt find this sample code for me. The point is, your sample and the Arduino sample use two different pin assignments. AIN2/P0.04 in Arduino and AIN3/P0.05 in Zephyr. That doesnt make sense.

If you look at the constants in the nRF Connect SDK pre-v3.2.0 and post-v3.2.0, the DT constants changed by one. i.e. in one version AIN0 starts at 0 and in the other AIN0 starts at 1 with no corresponding adjustment in the code for the different offset.
Once I get my login sorted with the nordic forum, I’ll be engaging with them to explore further.

@jkeane I am checking this confusion. But, in my opinion, you can read battery level over P0.05 pin. Again, I am not sure about nordic side but there is no bug on Zephyr RTOS for RAK4631. If you can setup and test adc with my test example, I can help you easily. If possible, please try to setup this repo on your side: GitHub - srcnert/rak-zephyr-app: Example projects for Zephyr RTOS on RAK3172/RAK4631/RAK5010/RAK11720 · GitHub

I will inform you after more investigation. I asked this internally to address this confusion.

@jkeane I double-checked internally, and there is a typo error in the sample examples. Thanks for bringing this up. I informed related people to prepare a fix.

Shortly, the correct pin is AIN3/P0.05 to measure battery voltage on RAK19007 base board.

You can also check schematic to get broad information.

Thanks.
Sercan.