Reading A20 LRADC0 and LRADC1 values


PaddleStroke
 Share

0

Recommended Posts

Hey guys,

 

I'm trying to find a way to read the values of the LRADC0 and LRADC1 of the A20.

 

Those ADC are usually used for android-style buttons (labeled "VOL+", "VOL-", "MENU", "SEARCH", "HOME", "ESC" and "ENTER") which are connected to a low-resolution ADC via a resistor network.

 

But I want to use the ADC for another purpose and I just need to read values between 0 and 63.

 

Now it seems there is already a driver which let you setup the android-style buttons in the DTS (as can be seen in this patch)

But I can't find the code of this driver, which I could use as a template.

 

Any idea how to find the driver ? Or on how to read the LRADC values from a python script directly?

 

Thanks! :)

 

Related, for reference https://www.olimex.com/forum/index.php?topic=4281.0

https://www.olimex.com/forum/index.php?topic=2425.0

 

Edit: I think I found the driver https://github.com/torvalds/linux/blob/master/drivers/input/keyboard/sun4i-lradc-keys.c

To be continued.

 

Link to post
Share on other sites

Donate and support the project!

On 8/27/2020 at 4:11 PM, martinayotte said:

Using /dev/mem, you can poke control register at 0x01C22800 to be in continuous mode, and then peek data register to read ADC values at 0x01C2280C or 0x01C2280D, depending if LRADC0 or LRADC1.

Hi! Thanks for your reply ! 
I had to put that on hold for some time, but I'm back on it.

 

After checking the A20 user manual I find that the offset for LRADC1 is 0x10 and not 0x0D. So I guess you meant 0x01C22810 for LRADC1?


So I should be doing something like : 

 

fd = open("/dev/mem",  "r+")
mem = mmap.mmap(fd.fileno(), 16, 0x01C22800 )
close(fd)

while(we need to get the values):
   mem.seek(0x0C)
   LRADC0_byte = mem.read(1) 
   mem.seek(0x10) 
   LRADC1_byte = mem.read(1)
   
   //further process the values

mem.close()

Does that sounds correct to you?

 

Link to post
Share on other sites

48 minutes ago, PaddleStroke said:

Could you please tell me how to do this? I don't know what is the control register nor how to initialize it.

Download this UserGuide : http://dl.linux-sunxi.org/A20/A20 User Manual 2013-03-22.pdf

Read the LRADC chapter starting at page 192.

You will see the description of the ControlRegister at page 193-194.

You need your /dev/mem to be "O_RDWR", not "R+" to allow writing into ControlRegister.

Link to post
Share on other sites

Quote

You need your /dev/mem to be "O_RDWR", not "R+" to allow writing into ControlRegister.

Thanks!

I can now read the values of the LRADC. Now I have only one issue left is that my python script does not have permission to read /dev/mem when it's started automatically at launch.

 

I had the same permission issue when trying to read AXP209 ADC, which we solved by using a .rule file in etc/udev/rules.

I tried to add one line for /dev/mem but I think I'm missing something here.

ACTION=="add", SUBSYSTEM=="iio", RUN+="/bin/chmod g+w /sys/bus/iio/devices/iio:device0/in_voltage3_scale"
ACTION=="add", SUBSYSTEM=="iio", RUN+="/bin/chmod g+w /sys/bus/iio/devices/iio:device0/in_voltage4_scale"
ACTION=="add", SUBSYSTEM=="iio", RUN+="/bin/chmod g+w /sys/bus/iio/devices/iio:device0/in_voltage3_raw"
ACTION=="add", SUBSYSTEM=="iio", RUN+="/bin/chmod g+w /sys/bus/iio/devices/iio:device0/in_voltage4_raw"
ACTION=="add", RUN+="/bin/chmod g+w /dev/mem"

 

I also tried : 

 

sudo chmod 777 /dev/men
and
sudo usermod -g kmem pi

Then I have a different error "[Errno1] Operation not permitted: '/dev/mem'

 

 

The working python script for reference

#!/usr/bin/env python

from time import sleep

import os, sys, mmap

DZONE2 = 6 # dead zone applied to joystick (mV)
VREF2 = 40 # max value read when moving joystick to max.
JOYOFFVAL2 = 60 # Value reads 63 when not working


#LRADC base register address = 0x01C22800 (cf A20 manual page 193)
MAP_MASK = mmap.PAGESIZE -1
addr = 0x01C22800
fd = os.open("/dev/mem",  os.O_RDWR | os.O_SYNC)
mem = mmap.mmap(fd, mmap.PAGESIZE, mmap.MAP_SHARED, offset= addr & ~MAP_MASK)
os.close(fd)

#using mem.write_byte to write the 4 bytes of the control register one by one does not work. read_byte one by one also do not work. You need to read the 4 at once. So the following does not work : 
#mem.seek(addr & MAP_MASK)
#mem.write_byte(chr(int('01111001', 2)))
#mem.write_byte(chr(int('00100000', 2)))
#mem.write_byte(chr(int('11000000', 2)))
#mem.write_byte(chr(int('00000001', 2)))

#The correct way to do it is to add the 4 bytes together as follow : 
mem.seek(addr & MAP_MASK)
byte_data = [121, 32, 192, 1]
mem.write("".join(map(chr, byte_data)))
#121 = 0111 1001 is for bit 7 to 0 of the first byte (0x01C22800). Bit 6 and 5 '11' are for reference voltage to use, here 11 = 1.6V. Bit 0 is to activate the LRADC.
#32 = 0010 0000 is for bit 7 to 0 of the 2nd byte (0x01C22801). Bit 13 and 12 '10' are for continuous mode.
#192 = 1100 0000 is for bit 7 to 0 of the 3rd byte (0x01C22802). Bit 23 and 22 '11' are to enable both LRADC0 and LRADC1.
#1 = 0000 0001 is default value. 


mem.seek(addr & MAP_MASK)
print("bytes")
ciuy = mem.read(4) 
print(' '.join(format(ord(x), 'b') for x in ciuy))

while True:

	mem.seek((0x01C2280C)& MAP_MASK)	# register offset for LRADC0 = 0x0C
	joystick2_UD = ord(mem.read_byte()) #should be between 0 and 63 (6 bits)

	mem.seek((0x01C22810)& MAP_MASK)	# register offset for LRADC1 = 0x10
	joystick2_LR = ord(mem.read_byte()) #should be between 0 and 63 (6 bits)


	
	if (joystick2_LR > (VREF2/2 + DZONE2)) or (joystick2_LR < (VREF2/2 - DZONE2)):
		#print(' '.join(format(ord(x), 'b') for x in joystick2_LR))
		print("Right Joystick LR : ", joystick2_LR)
		
	if (joystick2_UD > (VREF2/2 + DZONE2)) or (joystick2_UD < (VREF2/2 - DZONE2)):
		#print(' '.join(format(ord(x), 'b') for x in joystick2_UD))
		print("Right Joystick UD : ", joystick2_UD)
		
	
	
	
	sleep(.2)
	
mem.close()

 

Link to post
Share on other sites

Thanks!

I went with

chmod a+s /usr/bin/python

And it's working now!

Thanks again for your support.

 

I think I should make a proper gamepad kernel module rather than this python script.

 

For reference, the python script is launched as a service in systemd.

 

By the way I made a video on the hardware part of prototyping, maybe it will interest some people :) 

 

 

 

Link to post
Share on other sites

Hello,

 

Interesting topic. I played with a pcDuino3 and the pin LRADC0 and 1 are mapped to A0 and A1. There are other ADCs mapped to the A2 to A5 pins (XP_TP, XN_TP, YP_TP and YN_TP), but I did not play with them yet.

 

I slightly changed your code to port it to Python 3:

 

#!/usr/bin/env python3
from time import sleep
import os
import mmap

# Check CPU
a20 = "Allwinner sun7i (A20)" in open("/proc/cpuinfo", "r").read()

if not a20:
    exit("This program only works on Allwinner sun7i (A20) cpus.")

# LRADC register base address = 0x01C22800 (cfr. A20 manual page 193)
LRADC_BASE_ADDRESS = 0x01C22800
"""
# LRADC Control Register
LRADC_CTRL = LRADC_BASE_ADDRESS + 0x00
# LRADC Interrupt Control Register
LRADC_INTC = LRADC_BASE_ADDRESS + 0x04
# LRADC Interrupt Status Register
LRADC_INTS = LRADC_BASE_ADDRESS + 0x08
"""
# LRADC Data Register 0
LRADC_DATA0 = LRADC_BASE_ADDRESS + 0x0c
# LRADC Data Register 1
LRADC_DATA1 = LRADC_BASE_ADDRESS + 0x10

MAP_MASK = mmap.PAGESIZE - 1

try:
    mem = mmap.mmap(os.open("/dev/mem", os.O_RDWR | os.O_SYNC), mmap.PAGESIZE,
                    mmap.MAP_SHARED, offset=LRADC_BASE_ADDRESS & ~MAP_MASK)

    # The correct way to do it is to add the 4 bytes together as follow :
    mem.seek(LRADC_BASE_ADDRESS & MAP_MASK)
    REGISTERS_CONFIG = bytearray((
        # 0111 1001 is for bit 7 to 0 of the first byte (0x01C22800).
        # Bit 7 /
        # Bit 6 is LRADC sample hold
        # Bit 5:4 '11' are for reference voltage to use, where:
        #  00 ≈ 1.9 V, 01 ≈ 1.8V, 10 ≈ 1.7V, and 11 ≈ 1.6V
        # Bit 3:2 is the sampling rate, where:
        #  00 = 250 Hz, 01 = 125 Hz, 10 = 62.5 Hz, and 11 = 32.25 Hz.
        # Bit 1 /
        # Bit 0 is to activate the LRADC.
        0b01000001,
        # 0010 0000 is for bit 7 to 0 of the 2nd byte (0x01C22801).
        # Bit 13 and 12 '10' are for continuous mode.
        # 1100 0000 is for bit 7 to 0 of the 3rd byte (0x01C22802).
        0b00100000,
        # Bit 23 and 22 '11' are to enable both LRADC0 and LRADC1.
        0b11000000,
        # 1 = 0000 0001 is default value.
        0b00000001
    )
    )
    mem.write(REGISTERS_CONFIG)

    mem.seek(LRADC_BASE_ADDRESS & MAP_MASK)
    check_register_config = mem.read(4) == REGISTERS_CONFIG
    print(f"Check LRADC config: {'ok' if check_register_config else 'failed'}")

    while True:
        mem.seek(LRADC_DATA0 & MAP_MASK)
        # should be between 0 and 63 (6 bits)
        print(f"LRADC0 (A0): {mem.read_byte()}")

        mem.seek(LRADC_DATA1 & MAP_MASK)
        # should be between 0 and 63 (6 bits)
        print(f"LRADC1 (A1): {mem.read_byte()}")

        sleep(0.2)

finally:
    mem.close()
Edited by Vini
Typo ("et" instead of "and")
Link to post
Share on other sites

I have now had a look at the other ADC. It is quite harder to configure since it has 3 functionalities (CPU temperature sensor, or resistive Touch Panel controller, or single-ended ADC), but I was able to figure it out enough to use it. If you use the ADC, you must interrupt the other functionalities or build a driver to cope with them (temperature sensor and Touch Panel/ADC). Note that I am using Python 3 (because I'm familiar with it) to configure the ADC and read the ADC's values, which is probably very slow. A C/C++ program would be way faster in that regard (e.g., the code used to acquire the CPU temperature: https://github.com/armbian/build/blob/e9fb9542c763a88c8e689c3a14fc135bccea55f1/packages/bsp/sunxi-temp/sunxi_tp_temp.c).

 

Here is the code for those that it could interest.

#!/usr/bin/env python3
"""
To use this script you must remove kernel drivers related to the A20's adc.

Note that this script has been developped on a pcDuino 3, running on Armbian
(Linux pcduino3 5.15.13-sunxi #trunk.0004 SMP Wed Jan 5 17:53:11 UTC 2022 armv7l GNU/Linux)
The values registers have been set using the A20 Allwinner User Manual and by reverse engineering
the previous OS images released by the pcDuino 3 teams in order to find a correct configuration.
(pcDuino3 OS iso used: pcduino3_dd_sdbootable_20141110)

You can list them:
$ sudo lsmod | grep adc
axp20x_adc             16384  0
sun4i_gpadc_iio        16384  0
industrialio           57344  2 sun4i_gpadc_iio,axp20x_adc
sun4i_gpadc            16384  0

And remove them temporaly from the kernel using:
$ sudo rmmod sun4i_gpadc_iio
$ sudo rmmod sun4i_gpadc

Blacklisting the modules would remove the need of runing the rmmod.
WARNING: Note that removing the access to this ADC is probably removing the
access to the temperature sensor.
"""
from time import sleep
import os
import mmap

# Activate or deactivate the debug print
DEBUG = False

# Check CPU
a20 = "sun7i" in open("/proc/cpuinfo", "r").read()

if not a20:
    exit("This program only works on Allwinner sun7i (A20) cpus.")

# TP register base address = 0x01C25000 (cfr. A20 manual page 205)
TP_BASE_ADDRESS = 0x01C25000
# TP Control Register0
TP_CTRL0 = TP_BASE_ADDRESS + 0x00
# TP Control Register1
TP_CTRL1 = TP_BASE_ADDRESS + 0x04
# TP Pressure Measurement and touch sensitive Control Register
TP_CTRL2 = TP_BASE_ADDRESS + 0x08
# Median and averaging filter Controller Register
TP_CTRL3 = TP_BASE_ADDRESS + 0x0c
# TP Interrupt FIFO Control Reg
TP_INT_FIFOC = TP_BASE_ADDRESS + 0x10
# TP Interrupt FIFO Status Register
TP_INT_FIFOS = TP_BASE_ADDRESS + 0x14
# TP Temperature Period Register
TP_TPR = TP_BASE_ADDRESS + 0x18
# TP Common Data
TP_CDAT = TP_BASE_ADDRESS + 0x1c
# Temperature Data Register
TEMP_DATA = TP_BASE_ADDRESS + 0x20
# TP Data Register
TP_DATA = TP_BASE_ADDRESS + 0x24
# TP IO Configuration
TP_IO_CONFIG = TP_BASE_ADDRESS + 0x28
# TP IO Port Data
TP_PORT_DATA = TP_BASE_ADDRESS + 0x2c

MAP_MASK = mmap.PAGESIZE - 1


def write(file, address, content: bytearray):
    """Write the content at a specific address and check success.
    Return the success of the operation.
    """
    file.seek(address)
    file.write(content)
    file.seek(address)
    return file.read(len(content)) == content


def print_all(mem):
    """Print the binary value of all the registers related to the ADC."""
    mem.seek(TP_CTRL0 & MAP_MASK)
    print("TP_CTRL0:", [bin(d) for d in list(mem.read(4))])

    mem.seek(TP_CTRL1 & MAP_MASK)
    print("TP_CTRL1:", [bin(d) for d in list(mem.read(4))])

    mem.seek(TP_CTRL2 & MAP_MASK)
    print("TP_CTRL2:", [bin(d) for d in list(mem.read(4))])

    mem.seek(TP_CTRL3 & MAP_MASK)
    print("TP_CTRL3:", [bin(d) for d in list(mem.read(4))])

    mem.seek(TP_INT_FIFOC & MAP_MASK)
    print("TP_INT_FIFOC", [bin(d) for d in list(mem.read(4))])

    mem.seek(TP_INT_FIFOS & MAP_MASK)
    print("TP_INT_FIFOS", [bin(d) for d in list(mem.read(4))])

    mem.seek(TP_TPR & MAP_MASK)
    print("TP_TPR :", [bin(d) for d in list(mem.read(4))])

    mem.seek(TP_CDAT & MAP_MASK)
    print("TP_CDAT:", [bin(d) for d in list(mem.read(4))])

    mem.seek(TEMP_DATA & MAP_MASK)
    print("TEMP_DATA:", [bin(d) for d in list(mem.read(4))])

    mem.seek(TP_DATA & MAP_MASK)
    print("TP_DATA:", [bin(d) for d in list(mem.read(4))])

    mem.seek(TP_IO_CONFIG & MAP_MASK)
    print("TP_IO_CONFIG:", [bin(d) for d in list(mem.read(4))])

    mem.seek(TP_PORT_DATA & MAP_MASK)
    print("TP_PORT_DATA:", [bin(d) for d in list(mem.read(4))])


try:
    mem = mmap.mmap(os.open("/dev/mem", os.O_RDWR | os.O_SYNC), mmap.PAGESIZE,
                    mmap.MAP_SHARED, offset=TP_BASE_ADDRESS & ~MAP_MASK)

    if DEBUG:
        print("Register initial")
        print_all(mem)

    # The correct way to do it is to add the 4 bytes together as follow :
    REGISTERS_CONFIG0 = bytearray((
        # Bit 15:0 TACQ.
        #  Touch panel ADC acquire time CLK_IN/(16*(N+1))
        0b00111111,
        0b00000000,
        # Bit 23 ADC_FIRST_DLY_MODE.
        #  ADC First Convert Delay Mode Select:
        #  0: CLK_IN/16, and 1: CLK_IN/16*256
        # Bit 22 ADC_CLK_SELECT.
        #  ADC Clock Source Select:
        #  0: HOSC(24MHZ), and 1: Audio PLL
        # Bit 21:20 ADC_CLK_DIVIDER.
        #  ADC Clock Divider(CLK_IN)
        #  00: CLK/2, 01: CLK/3, 10: CLK/6, and 11: CLK/1
        # Bit 19:16 FS_DIV.
        #  ADC Sample Frequency Divider
        #  xxxx: CLK_IN/2^(20-xxxx)
        0b00111111,
        # Bit 31:24 ADC_FIRST_DLY.
        #  ADC First Convert Delay Time(T_FCDT)setting
        #  Based on ADC First Convert Delay Mode select (Bit 23)
        #  T_FCDT = ADC_FIRST_DLY * ADC_FIRST_DLY_MODE
        0b0
    )
    )
    write(mem, TP_CTRL0 & MAP_MASK, REGISTERS_CONFIG0)

    REGISTERS_CONFIG1 = bytearray((
        # Bit 7 CHOP_TEMP_EN
        #  Chop temperature calibration enable
        #  0: Disable, and 1: Enable
        # Bit 6 TOUCH_PAN_CALI_EN.
        #  Touch Panel Calibration
        #  1: start Calibration, it is clear to 0 after calibration
        # Bit 5 TP_DUAL_EN.
        #  Touch Panel Double Point Enable
        #  0: Disable, and 1: Enable
        # Bit 4 TP_MODE_EN.
        #  Tp Mode Function Enable
        #  0: Disable, and 1: Enable
        # Bit 3 TP_ADC_SELECT.
        #  Touch Panel and ADC Select:
        #  0: TP, and 1: ADC
        # Bit 2:0 ADC_CHAN_SELECT.
        #  Analog input channel Select In Normal mode:
        #  000: X1 channel, 001: X2 Channel
        #  010: Y1 Channel, 011: Y2 Channel
        #  1xx : 4-channel robin-round
        #  FIFO Access Mode, based on this setting
        #  Selecting one channel, FIFO will access that channel data;
        #  Selecting four channels FIFO will access each channel data
        #  in successive turn, first is X1 data.
        0b00011000,
        # Bit 19(15):12 STYLUS_UP_DEBOUNCE.
        #  See under
        # Bit 11:10
        # Bit 9 STYLUS_UP_DEBOUCE_EN.
        #  Stylus Up De-bounce Function Select
        #  0: Disable, and 1: Enable
        # Bit 8 /
        0b00000000,
        # Bit 23:20 /
        # Bit 19:(16)12 STYLUS_UP_DEBOUNCE.
        #  Stylus Up De-bounce Time setting
        #  0x00: 0
        #  ….
        #  0xff: 2N*(CLK_IN/16*256)
        0b00000000,
        # Bit 31:24 /
        0b00000000
    )
    )
    write(mem, TP_CTRL1 & MAP_MASK, REGISTERS_CONFIG1)

    REGISTERS_CONFIG2 = bytearray((
        0b00000000,
        0b00000000,
        0b00000000,
        0b11110100
    )
    )
    write(mem, TP_CTRL2 & MAP_MASK, REGISTERS_CONFIG2)

    REGISTERS_CONFIG3 = bytearray((
        # Bit 31(7):3 /
        # Bit 2 FILTER_EN.
        #  Filter Enable
        #  0: Disable, and 1: Enable
        # Bit 1:0 FILTER_TYPE.
        #  Filter Type:
        #  00: 4/2, 01: 5/3, 10: 8/4, and 11: 16/8
        0b00000101,
        # Bit 31:(8)3 /
        0b00000000,
        0b00000000,
        0b00000000
    )
    )
    write(mem, TP_CTRL3 & MAP_MASK, REGISTERS_CONFIG3)

    REGISTERS_CONFIGFIFOC = bytearray((
        0b00000000,
        0b00000000,
        0b00000001,
        0b00000000
    )
    )
    write(mem, TP_INT_FIFOC & MAP_MASK, REGISTERS_CONFIGFIFOC)

    REGISTERS_TPR = bytearray((
        0b00000000,
        0b00000000,
        0b00000000,
        0b00000000
    )
    )
    write(mem, TP_TPR & MAP_MASK, REGISTERS_TPR)

    REGISTERS_CONFIGIO = bytearray((
        # Bit 7 /
        # Bit 6:4 TX_N_SELECT
        #  TX_N Port Function Select:
        #  000:Input,  001:Output, and 010: TP_XN
        # Bit 3 /
        # Bit 2:0 TX_P_SELECT
        #  TX_P Port Function Select:
        #  000:Input,  001:Output, and 010: TP_XP
        0b00100010,
        # 0b00000000, # All inputs
        # 0b00010001, # All inputs
        # Bit 14:12 TY_N_SELECT
        #  TY_N Port Function Select:
        #  000:Input,  001:Output, and 010: TP_YN
        # Bit 11 /
        # Bit 10:8 TY_P_SELECT
        #  TY_P Port Function Select:
        #  000:Input,  001:Output, and 010: TP_YP
        0b00100010,
        # 0b00000000, # All inputs
        # 0b00010001, # All inputs
        # Bit 31:15 /
        0b00000000,
        0b00000000
    )
    )
    write(mem, TP_IO_CONFIG & MAP_MASK, REGISTERS_CONFIGIO)

    if DEBUG:
        print("Final config")
        print_all(mem)

    while True:
        for adc in (0, 1, 2, 3):
            if DEBUG:
                print(f"\nA{adc+2} {45 * '='}")
                print_all(mem)
                print()

            # Select the ADC
            write(mem, TP_CTRL1 & MAP_MASK, bytearray(
                (0b00011000 | adc, 0b00000000, 0b00000000, 0b00000000)))

            # LED some times to the ADC logic to set the value
            sleep(0.01)
            mem.seek(TP_DATA & MAP_MASK)
            LSB, MSB = mem.read(2)
            print(f"A{adc+2}: {(MSB << 8) + LSB}", sep=" ")

        print()

finally:
    mem.close()

 

Link to post
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Loading...
 Share

0