adafruit / Adafruit_CircuitPython_ADS1x15

CircuitPython drivers for the ADS1x15 series of ADCs.
MIT License
137 stars 59 forks source link

slow reading AnalogIn() #27

Closed tomasinouk closed 5 years ago

tomasinouk commented 5 years ago

Hi, I am using this library on Raspberry Pi Zero with ADS1115 to read ADXL335.

I am reading only one axis at the moment and getting sampling rate avg. 214 samples/sec. I would like to increase a speed to 1000samples/sec.

My sample code is modified example:

import board
import time
import busio

i2c = busio.I2C(board.SCL, board.SDA)

import adafruit_ads1x15.ads1115 as ADS
from adafruit_ads1x15.analog_in import AnalogIn
ads = ADS.ADS1115(i2c, data_rate=860)

# Create analog inputs for each ADXL335 axis.
#x_axis = AnalogIn(ads, ADS.P0)
#y_axis = AnalogIn(ads, ADS.P1)
z_axis = AnalogIn(ads, ADS.P2)

number_samples = 1000

samples = []

start = time.time()
for i in range(number_samples):
   samples.append(z_axis.value)

end = time.time()
total_time = end - start

print("Time of capture: {}s".format(total_time))
print("Sample rate is: ", 1000.0 / total_time)

Which gives me:

Time of capture: 4.6360485553741455s
Sample rate is:  215.7009332529082

Is something like this possible? Thank you

ladyada commented 5 years ago

@caternuson i wonder if its this line https://github.com/adafruit/Adafruit_CircuitPython_ADS1x15/blob/master/adafruit_ads1x15/ads1x15.py#L162 @tomasinouk can you try editing your installed ads1x15.py to change that line to pass?

tomasinouk commented 5 years ago

@caternuson thank you for looking into it. I have already modified that and the above result is with that modification.

I am sorry forgot to mention, was kinda late at night.

I was thinking that the speed looks exactly half of what it would be without differential thought.

caternuson commented 5 years ago

It could just be code execution time or the I2C transaction when trying to read this way. That section of code will probably change when I work a fix for #26. I'll keep track of this issue as part of that as well.

tomasinouk commented 5 years ago

I just post it to complement the information.

I have added baud rate for i2c in /boot/config.txt as well.

dtparam=i2c_arm=on
i2c_arm_baudrate=400000

in file /home/pi/.local/lib/python3.5/site-packages/adafruit_ads1x15/ads1x15.py changed it to pass

        while not self._conversion_complete():
            pass
            # time.sleep(0.01)
caternuson commented 5 years ago

Just an update - I'm setup and testing on a Pi Zero W and have generally recreated the above results. I'm using a version of the library that also removes the time.sleep from the wait loop. I also created a slightly modified version of the above test code.

import time
import board
import busio
import adafruit_ads1x15.ads1115 as ADS
from adafruit_ads1x15.analog_in import AnalogIn

SAMPLES = 1000
RATE = 16

i2c = busio.I2C(board.SCL, board.SDA)
ads = ADS.ADS1115(i2c, data_rate=RATE)
chan = AnalogIn(ads, ADS.P0)

print("Acquiring...")
start = time.time()
for _ in range(SAMPLES):
   foo = chan.value
end = time.time()

total_time = end - start

print("Time of capture: {}s".format(total_time))
print("Sample rate requested={} actual={}".format(RATE, SAMPLES / total_time))

Setting to a slow rate of 16Hz with the expectation that the ADS will be the slowest thing, results are generally as expected:

pi@raspberrypi:~ $ python3 ads_test.py 
Acquiring...
Time of capture: 65.71789121627808s
Sample rate requested=16 actual=15.216556427670396

Setting to the faster 860Hz roughly repeats the same values as in first issue post:

pi@raspberrypi:~ $ python3 ads_test.py 
Acquiring...
Time of capture: 4.6752707958221436s
Sample rate requested=860 actual=213.89135382138878

The library in generally inefficient in that it sets the configuration register with every read. It might help to separate that out somehow. The investigation continues...

caternuson commented 5 years ago

Just dumping more of the story here as a I go.

Looks like changing buadrate does not actually do anything: https://github.com/adafruit/Adafruit_Blinka/blob/a97887fedc5837fbff0a4d7c859d87269c1d3396/src/adafruit_blinka/microcontroller/generic_linux/i2c.py#L16 Which is confirmed by what I'm seeing - a 100kHz clock regardless.

Here's the I2C traffic for chan.value: timing_baseline Over 5ms total. Yikes. I think we can buy back a lot by restructuring all this.

ladyada commented 5 years ago

thats in linux? that's oddly long for delays in between

caternuson commented 5 years ago

Yep, Raspbian Lite. System deets below. Trace is from pressing "Start" in Saleae/Logic and then calling chan.value. So all traffic is from that single call.

pi@raspberrypi:~ $ python3 --version
Python 3.5.3
pi@raspberrypi:~ $ uname -a
Linux raspberrypi 4.14.79+ #1159 Sun Nov 4 17:28:08 GMT 2018 armv6l GNU/Linux
pi@raspberrypi:~ $ cat /proc/cpuinfo
processor   : 0
model name  : ARMv6-compatible processor rev 7 (v6l)
BogoMIPS    : 697.95
Features    : half thumb fastmult vfp edsp java tls 
CPU implementer : 0x41
CPU architecture: 7
CPU variant : 0x0
CPU part    : 0xb76
CPU revision    : 7

Hardware    : BCM2835
Revision    : 9000c1
Serial      : 000000009ead533c
ladyada commented 5 years ago

you may want to try replacing https://github.com/adafruit/Adafruit_CircuitPython_ADS1x15/blob/master/adafruit_ads1x15/ads1x15.py#L191 with a single write_then_readinto call, so you're only calling into the kernel once (we optimize on linux for that)

tomasinouk commented 5 years ago

Just to note. I have got ADS1015 as well, to be able to compare. The results are same.

tomasinouk commented 5 years ago

@caternuson you might try it on normal raspberry pi. I have tried it on Pi 3 B+ and the speeds are completely different.

Maybe this issue might be hardware specific to Pi Zero. I do have v1.1

caternuson commented 5 years ago

@ladyada good tip, that cleaned up the lag in the "register read" between the write and read:

with self.i2c_device as i2c:
    i2c.write(self.buf, end=1, stop=False)
    i2c.readinto(self.buf, end=2)

write_readinto

with self.i2c_device as i2c:
    i2c.write_then_readinto(bytearray([reg]), self.buf, in_end=2, stop=False)

write_then_readinto

The total transaction for chan.value is still O(5ms) though: chan_read but working on that.

@tomasinouk I'd imagine the gaps above between I2C calls would be shorter on a faster Pi, so total time would also be less the 5ms. But hopefully can can get something that'll work even on a Pi Zero.

caternuson commented 5 years ago

OK, some progress. In general, getting this to work is going to require a trade off in simplicity. The current interface:

reading = chan.value   # or chan.voltage

is great, but will always be slow. It requires several I2C transactions to switch the mux to that channel, possibly wait for conversion completion, and then get the actual reading. See annotated scope trace above.

With the faster approach, the idea is to no longer use the AnalogIn to create the chan object. Instead, do things on the ADC directly. Something like this:

# configure
ads.mode = 0
ads.data_rate = 860
ads.gain = 1
ads.mux = 0b100
# get reading
ads.get_last_reading()

Additional speed can be achieved by taking advantage of this feature of the ADS1x15s:

read_wo_pointer

I've implemented this in a new _function _read_fast(). Here are trace comparisons:

ads.get_last_reading() (writes to pointer register, then reads result) get_last_result

ads._read_fast() (just reads) read_fast

Trying this out in an updated test program:

import time
import board
import busio
import adafruit_ads1x15.ads1115 as ADS

SAMPLES = 1000
RATE = 860

i2c = busio.I2C(board.SCL, board.SDA)
ads = ADS.ADS1115(i2c)

ads.mode = 0
ads.gain = 1
ads.data_rate = RATE
ads.mux = 0b100

ads.get_last_result()

print("Acquiring...")
start = time.time()
for _ in range(SAMPLES):
   foo = ads.get_last_result()
   #foo = ads._read_fast()
end = time.time()

total_time = end - start

print("Time of capture: {}s".format(total_time))
print("Sample rate requested={} actual={}".format(RATE, SAMPLES / total_time))

Results are: ads.get_last_result()

pi@raspberrypi:~ $ python3 ads_test.py
Acquiring...
Time of capture: 1.9266817569732666s
Sample rate requested=860 actual=519.0270766724633

ads._read_fast()

pi@raspberrypi:~ $ python3 ads_test.py
fast_read
Acquiring...
Time of capture: 0.9946539402008057s
Sample rate requested=860 actual=1005.3747937680869

What's missing is any kind of synchronization with conversion completion. That's why the last test exceeded the set data rate. It just means code execution is no longer the tent pole. Polling the register bit will likely slow this back down. The other option is to use the ALERT/RDY pin. That would also need to be polled, since interrupts are currently out of scope.

Thoughts? Is this generally seen as something worth polishing up and pursuing?

ladyada commented 5 years ago

i like keeping the AnalogIn interface - could you use registers underneath AnalogIn and then for experts they can twiddle those by hand?

caternuson commented 5 years ago

The AnalogIn interface gives a per channel object to use:

chan0 = AnalogIn(ads, ADS.P0)
chan1 = AnalogIn(ads, ADS.P1)
knob0 = chan0.value
knob1 = chan1.value

Under the hood, it modifies the ADC registers as needed to swap the mux, read value, etc.. But it seems a little odd to directly access the ADC registers through the channel objects? The settings would affect all channels.

Fast reading can really only be done on one channel at a time. So there would need to be some kind of channel-to-channel synchronization.

ladyada commented 5 years ago

yeah i just don't wanna change what we've got now ... we could do some caching so you aren't re-reading the mux?

caternuson commented 5 years ago

How about something like a lock mechanism? Analogous to how the I2C bus is shared with try_lock() and unlock(). A channel could "lock" the ADC into fast mode and then chan.value would operate differently under the hood for that channel. It would no longer worry about changing the mux and simply read current value. And then a context manager could be used to actually do the calls to try_lock() and unlock(), analogous to I2CDevice.

adc.data_rate = 860
adc.mode = Mode.CONTINUOUS
with chan0 as chan:
    # can now do fast readings on chan0
    for i in range(SAMPLES):
        # still no synchronization :(
        data[i] = chan.value
    # this would throw an Error since chan1 does not have the lock
    chan1.value
# lock is now released, so this would work again, but be slow
chan1.value
caternuson commented 5 years ago

I've implemented the above and pushed the working code up here: https://github.com/caternuson/Adafruit_CircuitPython_ADS1x15/tree/fast_read along with an example program: https://github.com/caternuson/Adafruit_CircuitPython_ADS1x15/blob/fast_read/examples/ads1x15_fast_read.py

Testing was done on a Pi Zero W, which I realize is not the fastest Pi. Interestingly, the polling for the ready pin makes it slower than just reading a bunch of single shot conversions. Dealing with synchronization without interrupts is going to be an issue.

pi@raspberrypi:~ $ python3 ads1x15_fast_read.py 
Acquiring normal...
Time of capture: 5.0196709632873535s
Sample rate requested=860 actual=199.21624491201825
Acquiring fast...
Time of capture: 8.566477060317993s
Sample rate requested=860 actual=116.73410118988626
Acquiring fast w/o polling...
Time of capture: 1.148141860961914s
Sample rate requested=860 actual=870.9725113255598

Is this approach worth continuing to pursue? It has the following limitations:

chrisjamesfell commented 5 years ago

Maybe a dumb comment, but isn't the problem related to the fact you're using the ADS1x15 in single-shot mode? According to the specs this is a mode designed for periodic sampling, where the device is woken from a 'sleep' mode, then sampled for a short time (1/8th second for data_rate 8, or 1/860th second for data_rate 860), then put back to sleep. Surely it's unreasonable to expect that 1000 repeat cycles of that would only take 100x the sampling rate?

I'm pretty sure any longer sampling period needs the device to be used in Continuous mode. In that case the power stays fully up and you pick off the latest reading as many times as you like. In that case 1000 samples should take 1000x the sampling rate.

But it seems the python library (at least the new circuitPython incarnation) presently can't be used to set Continuous mode. It would be great to see that fixed :)

Chris.

caternuson commented 5 years ago

Yep. It would be used with continuous. The example linked above shows three ways to take a lot of readings. The first is single shot - which is just there for comparison purposes. Just to prove it's slow. The second two are continuous and more in line with what is trying to be achieved here. The crux of all this the two bullets above, and really, the second bullet.

That example was tested using a local copy of the library that had PR #28 merged in. That's why it was able to use continuous. That PR is still waiting merge here. You can grab that code from the other link above if you want.

chrisjamesfell commented 5 years ago

Cool, thanks. I copied it verbatim and ran it on my 3B+, but got the following error when it tried to apply the second method. I'm running Python 3.5.3.

%Run ads1x15_fast_read.py Acquiring normal... Time of capture: 14.241853952407837s Sample rate requested=860 actual=70.215577504285 Acquiring fast... Traceback (most recent call last): File "/home/pi/POOL_PROJECT/ads1x15_fast_read.py", line 45, in with chan0 as chan: AttributeError: exit

caternuson commented 5 years ago

Did you also get a copy of the library code from the PR branch and switch to using that? https://github.com/caternuson/Adafruit_CircuitPython_ADS1x15/tree/fast_read The example will only run with that version.

chrisjamesfell commented 5 years ago

Ok, that makes sense, but unfortunately my skills don't go far enough to make this happen. I copied the new library, I found where the old version is being stored in my Pi, but after many different approaches I simply can't get the old one renamed or the new one copied there. I think it's a permissions issue.

If I uninstall the CircuitPython package and clone it again from github using the web url, will it pick up the new version of this library?

Sorry for my ignorance. I feel like I'm a little kid found myself in big school.

Chris.

caternuson commented 5 years ago

Since there isn't a pull request yet, easiest would be to just clone the source repo: https://github.com/caternuson/Adafruit_CircuitPython_ADS1x15/ and switch to the fast_read branch. You can then set your PYTHONPATH environonment variable to point to that folder. This will cause that folder to be searched before the system wide folder. So it will end up using that code instead of the one installed via pip. This also means you do not need to mess with your current Python installation. No need to delete the already install library, etc.

chrisjamesfell commented 5 years ago

I'm still a bit out of my depth I'm afraid. After several hours I've figured out how to clone your repo (the key was to fork it to my github profile first). And I can checkout the fast_read branch at the command line using git checkout fast_read. But I can't figure out how to point to that branch in the PYTHONPATH. And even still unclear about how to edit/setup that environment variable on the RPi.

chrisjamesfell commented 5 years ago

Ok, I've ended up using pip to uninstall the original library and then re-install from the fast_read branch (using pip3 install git+https://github.com/chrisjamesfell/Adafruit_CircuitPython_ADS1x15.git@fast_read)

When I ran the test script the second part ("Acquiring fast") hung (endless loop). I deleted those lines and ran it again, so I could try the third part and this time it worked, but it reported an actual sample rate of 2362.05 (supposed to be 860). I presume this means something is wrong?

caternuson commented 5 years ago

What model Pi were you using? The example output a few comments back was on a Pi Zero W, which is one of the slowest Pi's. If you're running on a faster Pi, then the loop w/o polling will run much faster.

chrisjamesfell commented 5 years ago

I'm using a 3B+. I don't need lightning speed (I plan to use 8 samples/sec), but I want Continuous so I can keep sampling a noisy signal for an arbitrary length of time without breaks. I don't really understand what you're doing with the ready pin, but the high apparent speed makes me suspicious that the sampling isn't being done properly. Thoughts?

caternuson commented 5 years ago

The ready pin approach uses the ALRT/READY pin on the breakout. It gets wired to an available digital input on the Pi, in this case BCM 23: https://github.com/caternuson/Adafruit_CircuitPython_ADS1x15/blob/1264e008be7e0557e5fb2a57294e1d6a8789f3f6/examples/ads1x15_fast_read.py#L40 This was really made to be used in conjunction with interrupts. However, CircuitPython does not have interrupts, so the pin is simply polled: https://github.com/caternuson/Adafruit_CircuitPython_ADS1x15/blob/fast_read/examples/ads1x15_fast_read.py#L48 If that pin were not connected, you'd get stuck in the loop. That's probably what happened.

This is all still a work in progress, so hard to advise on best approach. But one idea for right now would be to "tune" a simple delay in this loop: https://github.com/caternuson/Adafruit_CircuitPython_ADS1x15/blob/fast_read/examples/ads1x15_fast_read.py#L71 to slow it down to match what you want.

Or try the same thing with the first approach: https://github.com/caternuson/Adafruit_CircuitPython_ADS1x15/blob/fast_read/examples/ads1x15_fast_read.py#L28

chrisjamesfell commented 5 years ago

From reading the datasheet (https://cdn-shop.adafruit.com/datasheets/ads1115.pdf) (and also some other thread I can no longer find) I believe lower sampling rates incorperate averaging, i.e. there is some native very high sampling rate, and when you ask for 8SPS you actually get averaging of the high rate over 1/8 sec for each delivered sample; so quite a stable and noise-free result.

It isn't clear to me that this would happen if an artificial delay was tuned into a read loop. If the board reports 2300SPS without an artificial delay, then it mustn't be sampling for very long; maybe even just a single point at its internal clock speed. If I insert a wait between calls, won't I just get poor quality data delivered slowly?

wajeehAfridi commented 5 years ago

Hi, I am using this library on Raspberry Pi Zero with ADS1115 to read ADXL335.

I am reading only one axis at the moment and getting sampling rate avg. 214 samples/sec. I would like to increase a speed to 1000samples/sec.

My sample code is modified example:

import board
import time
import busio

i2c = busio.I2C(board.SCL, board.SDA)

import adafruit_ads1x15.ads1115 as ADS
from adafruit_ads1x15.analog_in import AnalogIn
ads = ADS.ADS1115(i2c, data_rate=860)

# Create analog inputs for each ADXL335 axis.
#x_axis = AnalogIn(ads, ADS.P0)
#y_axis = AnalogIn(ads, ADS.P1)
z_axis = AnalogIn(ads, ADS.P2)

number_samples = 1000

samples = []

start = time.time()
for i in range(number_samples):
   samples.append(z_axis.value)

end = time.time()
total_time = end - start

print("Time of capture: {}s".format(total_time))
print("Sample rate is: ", 1000.0 / total_time)

Which gives me:

Time of capture: 4.6360485553741455s
Sample rate is:  215.7009332529082

Is something like this possible? Thank you

i am running this code or similar with this, i get an error that module not found names "board" module not found names "busio" please help

caternuson commented 5 years ago

@wajeehAfridi It sounds like you haven't fully installed Blinka: https://learn.adafruit.com/circuitpython-on-raspberrypi-linux/overview

caternuson commented 5 years ago

@ladyada I bumped up I2C baud rate to 1MHz and ran the tests again, including the one you suggested:

use the library as-is, put it in continuous mode, and read as fast as you can

That is TEST 2 in the results below.

TEST 3 and TEST 4 use the modification I made to the library to additionally reduce the I2C traffic when re-reading the same channel (register). A context manager is used to engage that mode of operation. TEST 3 ignores the conversion ready pin and just sucks in the data - that might be what you were hoping for?

You can generally ignore TEST 1. It is just the single-shot baseline for comparison.

Tests run on a Pi Zero W with an ADS1115.

pi@raspberrypi:~ $ python3 ads1x15_fast_read.py
ADS1x15 Timing Tests
----------------------------------------
TEST 1 - Acquiring normal in single shot mode...
Time of capture: 3.7274725437164307s
Sample rate requested=860 actual=268.278300717666
----------------------------------------
TEST 2 - Acquiring normal in continuous mode...
Time of capture: 2.3309268951416016s
Sample rate requested=860 actual=429.0138837405499
----------------------------------------
TEST 3 - Acquiring fast w/o polling...
Time of capture: 0.8692643642425537s
Sample rate requested=860 actual=1150.398015995243
----------------------------------------
TEST 4 - Acquiring fast with polling...
Time of capture: 5.555660963058472s
Sample rate requested=860 actual=179.99658486170213
ladyada commented 5 years ago

huh! the numbers dont lie, lets go with the mod!

caternuson commented 5 years ago

OK. I cleaned up my experimental fork/branch to just the parts that matter for fast reading. I removed the stuff that was needed for setting up the ALRT pin to signal conversion complete since that's not going to be used.

PR'd #32

caternuson commented 5 years ago

Please try the 2.1.0 release of the library. It should improve the speed at which you can read from the ADC. Note that you'll want to bump up the I2C bus speed and that there is currently no synchronization with conversion complete - so it's a pretty crude and simple optimization.

See new example for general usage: https://github.com/adafruit/Adafruit_CircuitPython_ADS1x15/blob/master/examples/ads1x15_fast_read.py

Closing this issue for now.

Warday commented 4 years ago

Hi, I have notice that it gives the same read several times. In my case is up to 5000 samples per second but it only take 9 diferent values in 18.5 ms guivin 2ms per read 500SPS. I show my results.

Time of capture: 0.01859169099952851s Sample rate requested=3300 actual=5378.746882278542 18.59169099952851 [4656, 4656, 4656, 4656, 4656, 4656, 4416, 4416, 4416, 4416, 4416, 4416, 4416, 4416, 4416, 4416, 4416, 4416, 4320, 4320, 4320, 4320, 4320, 4320, 4320, 4320, 4320, 4320, 4320, 4320, 4576, 4576, 4576, 4576, 4576, 4576, 4576, 4576, 4576, 4576, 4576, 4576, 4576, 4720, 4720, 4720, 4720, 4720, 4720, 4720, 4720, 4720, 4720, 4720, 4720, 4736, 4736, 4736, 4736, 4736, 4736, 4736, 4736, 4736, 4736, 4736, 4736, 5008, 5008, 5008, 5008, 5008, 5008, 5008, 5008, 5008, 5008, 5008, 5008, 4928, 4928, 4928, 4928, 4928, 4928, 4928, 4928, 4928, 4928, 4928, 4928, 4752, 4752, 4752, 4752, 4752, 4752, 4752, 4752, 4752]

If we work with 1115 instead of 1015 we gets 860 SPS 1.16 ms per read. Its seems that 1015 have same problem because of you select 920 it get worse when it should keep at least the same. Show results of 1115. Time of capture: 0.018470353000338946s Sample rate requested=860 actual=5414.081690705366 18.470353000338946 [4695, 4695, 4765, 4765, 4765, 4765, 4765, 4765, 4782, 4782, 4782, 4782, 4782, 4782, 4782, 4791, 4791, 4791, 4791, 4791, 4791, 4919, 4919, 4919, 4919, 4919, 4919, 4919, 5100, 5100, 5100, 5100, 5100, 5100, 5100, 5025, 5025, 5025, 5025, 5025, 5025, 5025, 4941, 4941, 4941, 4941, 4941, 4941, 4834, 4834, 4834, 4834, 4834, 4834, 4834, 4744, 4744, 4744, 4744, 4744, 4744, 4744, 4727, 4727, 4727, 4727, 4727, 4727, 4727, 4703, 4703, 4703, 4703, 4703, 4703, 4703, 4701, 4701, 4701, 4701, 4701, 4701, 4381, 4381, 4381, 4381, 4381, 4381, 4381, 4317, 4317, 4317, 4317, 4317, 4317, 4317, 4427, 4427, 4427, 4427]

caternuson commented 4 years ago

See explanation here: https://github.com/adafruit/Adafruit_CircuitPython_ADS1x15/issues/43#issuecomment-545606646

slaattnes commented 3 years ago

Hi! When I try to use the fast_read script on all 4 channels I only get 77sps. Does anyone know the reason?

If I only read channel 0 I get 6000 "actual sample rate"