Skip to content

Conversation

@ArcaneNibble
Copy link
Collaborator

This reads the values from the ADC and scales them according to the ideal scaling factors.

Voltage was tested and reads in the correct order of magnitude, slightly lower (~100 mV) when compared against a cheap DMM without traceable calibration.

Current appears reasonable (116 mA) and increases when I squeeze a running motor (up to 500 mA).

Fixes pybricks/support#2311

@coveralls
Copy link

coveralls commented Aug 6, 2025

Coverage Status

coverage: 58.535% (-0.03%) from 58.563%
when pulling b7f9b1e on ArcaneNibble:ev3-bat
into 19d921b on pybricks:master.

@ArcaneNibble
Copy link
Collaborator Author

Done. Also added a workaround for detecting low battery on bootup (it's added in the ADC driver, unlike the REVISIT in the STM32 implementation).

// Battery current is read across a 0.05 ohm equivalent shunt resistor
// which is then connected to an op-amp configured with a gain of 16
// (non-inverting). This yields 1 A = 0.8 V.
*value = ((uint32_t)(value_raw) * 5000 * 10) / (1023 * 8);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice to move scaling to mV to the ADC driver on all platforms. Can be a different PR though. I think David might be working on ADC at the moment.

@dlech
Copy link
Member

dlech commented Aug 8, 2025

I tried this with 3 EV3s so far and the error is kind of all over the place.

image

// Battery voltage is read through a voltage divider consisting of two
// 100 K resistors, which halves the voltage. The ADC returns 10 LSBs,
// where full scale is 5 V.
*value = ((uint32_t)(value_raw) * 2 * 5000) / 1023;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the Linux kernel, for the battery voltage, I also took into account the VCE of Q19B that I somehow figured to be 50 mV (don't remember how I came up with that number). And I also added a bit more to account for the shunt resistor which depends on the current.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After lowering the SCLK rate to 1MHz, this 50 mV offset seems to go away, so I might have figured that wrong in the Linux driver.

As far as the shunt resistor goes, it would probably be better to include that in the low battery shutdown battery level, but not in the battery voltage that we use for motor control. So for now, I guess we should just leave that out.

@dlech
Copy link
Member

dlech commented Aug 8, 2025

I also think we need to first slow down the SPI on the ADC a bit more to get more accurate readings.

Below are some data points I took (by enabling debug prints in the DCM). As we can see, the {W,C2T,T2C}DELAYs are already helping quite a bit, but the values are still skewed a bit compared to running at a slower SCLK rate.

It looks like 1MHz is the magic value to let us actually read a saturated 5V on the ADC. (Note: the values are what I set ADC_SPI_CLK_SPEED to, not the actual rounded rate).

No *DELAY

56::: p1: 4878mv p2:1 p5:1 p6:0 (254 mv)                                        
56::: p1: 4883mv p2:1 p5:1 p6:0 (249 mv)                                        
56::: p1: 4863mv p2:1 p5:1 p6:0 (229 mv)  

master (~20 MHz)

56::: p1: 4946mv p2:1 p5:1 p6:0 (234 mv)                                        
56::: p1: 4946mv p2:1 p5:1 p6:0 (229 mv)                                        
56::: p1: 4931mv p2:1 p5:1 p6:0 (200 mv)  

10 MHz 

56::: p1: 4951mv p2:1 p5:1 p6:0 (229 mv)                                        
56::: p1: 4956mv p2:1 p5:1 p6:0 (229 mv)                                        
56::: p1: 4941mv p2:1 p5:1 p6:0 (200 mv) 

5 MHz

56::: p1: 4961mv p2:1 p5:1 p6:0 (224 mv)                                        
56::: p1: 4966mv p2:1 p5:1 p6:0 (224 mv)                                        
56::: p1: 4951mv p2:1 p5:1 p6:0 (195 mv)  

1 MHz

56::: p1: 5000mv p2:1 p5:1 p6:0 (190 mv)                                        
56::: p1: 5000mv p2:1 p5:1 p6:0 (190 mv)                                        
56::: p1: 5000mv p2:1 p5:1 p6:0 (171 mv)  

100 kHz

56::: p1: 5000mv p2:1 p5:1 p6:0 (175 mv)                                        
56::: p1: 5000mv p2:1 p5:1 p6:0 (175 mv)                                        
56::: p1: 5000mv p2:1 p5:1 p6:0 (161 mv) 

10 kHz

56::: p1: 5000mv p2:1 p5:1 p6:0 (190 mv)                                        
56::: p1: 5000mv p2:1 p5:1 p6:0 (190 mv)                                        
56::: p1: 5000mv p2:1 p5:1 p6:0 (171 mv) 

@laurensvalk
Copy link
Member

@dlech , what is the status on this one? Would you like to make the ADC fixes first before we merge this?

@dlech
Copy link
Member

dlech commented Sep 1, 2025

I'm still doing testing on this.

@dlech
Copy link
Member

dlech commented Sep 1, 2025

Last weekend, I did some tests on the current measurement using this PR + changing SCLK rate to 1 MHz. The results show that it is not very accurate.

Methodology:

from pybricks.ev3devices import Motor
from pybricks.hubs import EV3Brick
from pybricks.parameters import Port
from pybricks.tools import multitask, run_task, wait

hub = EV3Brick()

motors = [Motor(p) for p in (Port.A, Port.B, Port.C, Port.D)]

for m in motors:
    m.dc(20)

avg_current = hub.battery.current()
RATIO = 0.98


async def average():
    global avg_current

    while True:
        await wait(10)
        avg_current = int(RATIO * avg_current + (1 - RATIO) * hub.battery.current())


async def print_info():
    while True:
        print(hub.battery.voltage(), "mv", avg_current, "mA")
        await wait(1000)


run_task(multitask(average(), print_info()))

Used this program to compare current measured with a digital multi-meter (DMM) compared to the on-board ADC. The duty cycle was adjusted manually to get data points for various current usages. The X axis of the graphs is duty cycle percent. The Y axis is milliamps. This was repeated with 3 different EV3 bricks.

image

It can't be seen in the graph, but at higher currents, the ADC signal for the current measurement is quite noisy even with the averaging filter applied.

There is some clear non-linearity in the ADC measurement, but it doesn't seem to be consistent between EV3 bricks. So I'm not really sure what we could do to make this better, if anything.

ArcaneNibble and others added 3 commits September 13, 2025 16:17
This reads the values from the ADC and scales them according to
the ideal scaling factors.
Without this, the hub can falsely detect low battery when booting.
Using the max SPI SCLK speed of 20 MHz, the ADC is not able to read up
to the full 5V but rather reading somewhere around 50 mV less.

By slowing down the SCLK rate, we give the ADC more time to settle after
switching the mux and can get more accurate readings as a result.
Testing has shown that 1 MHz slow enough to achieve this.
@dlech
Copy link
Member

dlech commented Sep 13, 2025

I rebased and added a couple of extra commits. One to slow down the ADC SPI SCLK to get more accurate measurements. (I suspect this may help with the long standing issue of large motors being detected as medium motors). Another is some more battery monitoring code we had from ev3dev and is also in the official LEGO firmware for a high temperature shutdown.

Copy the EV3 battery monitor code from [1]. This is something that comes
from the original EV3 firmware.

[1]: https://github.com/ev3dev/brickd/blob/master/src/power_monitor.vala
@dlech dlech merged commit 3e8cb1e into pybricks:master Sep 13, 2025
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] [EV3] Implement battery voltage sensing

4 participants