Solution: Using 12-bit ADCs in the AXP209 PMU for external voltage measurement

4 2048
I like this board more and more :-)
Ok I needed to read the voltage of a large external power source (6S lipo or similar) up to about 30V. I was looking at the various ADC options and here's a nice easy one:

AX209 info
BananaPi schematic:

Ok so... I'll explain this in some detail if you're new to this and interested...

The AXP209 has pins GPIO0 and 1 that can be ADC inputs into a perfectly nice 12-bit ADC, which you can then read over i2c.  
On the BPi, GPIO0 is unconnected (and a bit hard to solder to, though possible) but easier than that;

a) GPIO1 is used to measure VBUS on the USB OTG connector (see schematic, search for VBUS_WK), goes from the OTG connector VBUS line to AXP209 GPIO1 via a voltage divider (R22 100k on top, R26 200k on the bottom)

b) Bless the silkscreen gods, they're easy to locate on the board (yay!) - On the back of the BPi board, right next to the USB OTG connector, you see R26 and R22 right next to each other. Handy!

c) The divider on the board is 100k/200k. If you do the simple math; it's 200k / (100k+200k) = 0.6666, so if the OTG vbus is 5V, should get, 3.3v into GPIO1.  This is actually higher than the ADC can read (and if you plug in an OTG cable to a host you see the ADC reads 0xfff = max value) but that's not important for the original use; they just want to know if a voltage is there or not.

c) I want to measure up to 30V, and the GPIO1 ADC range has two options (see register 0x85) I'll use " 0.7-2.7475v" range.  If I leave R26 on the board and feed in my external voltage via a 2M resistor, the divider will be 200k/(2000k+200k) = 0.0909 , so 30V will be 2.727v.. perfect!   

d) In theory, because it's a 12-bit ADC and I've selected 0.7-2.7475v range, that's 12 bits over 2.0475v so /(1<<12) = 0.0049987 (call it 0.005v or 5mv per count). In practice it won't be that good of course, but that's the idea.

e) Call me paranoid, but if I'm brining 30V anywhere near my BPi, I'd like to be pretty sure things don't get toasted in case of accident. Hence I'm going to put a 3v zener diode to ground (so; 30V comes in, via 2M resistor, then to the anode of a 3v zener (cathode to ground), and then on to R26.  This is optional of course.

f) Then the software side; I'm just going to use "i2cget" and "i2cset" from "i2c-tools" package, like so...
#One time setup...  set gpio1 to adc in mode
i2cset -f -y 0 0x34 0x92 4
#enable ADC on on gpio1
i2cset  -f -y 0 0x34 0x83 0x84      
#set range 0.7-2.7475v
i2cset -f -y 0 0x34 0x85 2      

#..then to read.. read hi byte on 0x66, low 0x67 (low 4 bits)
i2cget -f -y 0 0x34 0x66
i2cget -f -y 0 0x34 0x67

Stick those two values back together into a 12-bit value, divide by 0.0909 and then add 0.7 and that should be your voltage.

FYI you can read stuff like the current draw of the whole board by reading "ACIN Current" registers (0x58/0x59) and voltage (0x56/0x57) and so on.  

I've not done the hw mod yet - I thought I'd come type this up to share while I have a cup of coffee -  but I verified this works on an unmodified board by doing the register setup as above and plugging in power to the OTG port; as expected I get 0 , 0 unplugged and 0xff, 0xf when plugged in (i.e. the divider is turning 5v into 3.3v which is above the 2.7v range so reads as 0xfff).

Oh, nice! Unexpected bonus ADC! Thanks for posting this.

Thanks for this info

Or instead of taking the value through ap, tri using an external ADC. It works out well.
I am attaching the code below.
import time
import os
import RPi.GPIO as GPIO


# change these as desired - they're the pins connected from the GPIO

# set up the SPI interface pins
globtemp = 0

def readadc(clockpin, misopin, cspin):
        # if (adcnum != 0):
                # return -1
    GPIO.output(cspin, True)
    GPIO.output(clockpin, False)  # start clock low
    GPIO.output(cspin, False)     # bring CS low

    adcout = 0
        # read in two empty bits, one null bit and 12 ADC bits
    for i in range(15):
        GPIO.output(clockpin, True)
        GPIO.output(clockpin, False)
        adcout <<= 1
        if (GPIO.input(misopin)):
           # print(GPIO.input(misopin))
            adcout |= 0x1

    GPIO.output(cspin, True)

    adcout >>= 1       # first bit is 'null' so drop it
    print (adcout)
    return adcout

voltage = round(((value*5.25)/4096),3)

I just got around to actually implementing this for a job and it works fine; you do need to remove R22 off the BPi board, then solder a piece of nice thin wire onto R26 (the side nearest the OTG port); fairly obviously you should tack down the wire (e.g. with a dab of hot glue) to the PCB to provide strain relief.
Also to reduce noise you may want to add a capacitor (e.g. 1uf) in parallel with R26 (I had an 0605 cap and it's pretty easy to slap it on top of the existing resistor).  I also average the ADC samples in software to provide some extra filtering because the voltage I'm measuring is quite noisy (spikes from a large buck converter).

Also watch out for a slight gotcha - because it appears you can only read the 12-bit ADC registers in two separate 8-bit reads, hence there's a chance the value will change between reading the MSB and LSB; this can give you wonky values if (for example) the value decreases by one from 0x05 0x00 to 0x04 0xff ; if you read MSB then LSB you might actually read "0x05, 0xff". Getting around this is an exercise for the reader. :-)

Anyway, works fine for my needs.

You have to log in before you can reply Login | Sign Up

Points Rules