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.