robert-hh / ads1x15

Micropython driver for ADS1115 and ADS1015
90 stars 25 forks source link

pin object has no attribute irq #7

Closed brauliobarahona closed 4 years ago

brauliobarahona commented 4 years ago

I am testing your sample code for continuous sampling trigger by the ADC, however, there are a couple of things that are not clear. One of them is the irq attribute of Pin. Is the line

irq_pin.irq(trigger=Pin.IRQ_FALLING, handler=sample_auto)

supposed to work on ESP32 (i.e. LoPy4 + pysense)? In my case, it does not:

AttributeError: 'Pin' object has no attribute 'irq'

That said, I modified a few lines to instantiate the I2C bus, and I can manage to run your example using the callbackmethod in Pin. However, it does not seem to actually change the data rate of the ADC (I get around 34 sps, no matter the rate -- though I am executing this in REPL). You mentioned you reach 252 sps? Were you able to test different rates?

Help appreciated.

robert-hh commented 4 years ago

The code was initially written for an ESP8266 with the genuine micropython from micropython.org. This esp8266 supports true IRQ, when called with the option hard=True, in contrast to a callback, which is way slower. The setup line in my application code, which samples at 250/s, is below.

self.irq_pin.irq(handler=_sample, trigger=Pin.IRQ_FALLING, hard=True)

But this application can also be set to lower sample rates. Since the PyCom variant of Micropython does not support the hard IRQ mode, sample rates are limited by the callback mechanism. This is more convenient for coding, but bad in matters of speed. In that case, polling or using a timer based mechanism using read_rev should be faster, because sampling and starting the callback are then overlapping.

robert-hh commented 4 years ago

I just recalled that I had a test & discussion about Pin interrupt latency at earlier times: https://forum.pycom.io/topic/936/pin-interrupt-latency In that test, made wit a LoPy, the latency was 0.3-0.9ms, which still would be good for a 250 samples/s test loop. So I grab a Lopy4 and see, what's up now. Give me a few hours.

robert-hh commented 4 years ago

So it was faster. It turned out that I had done that before. Test item is LoP4, Firmware 1.20.1.r1 homebrew. That is one without PyBytes. The sample code is below. Sampling rate of 250 is possible, given that the device is not too busy. 512 samples were collected in 2035 ms. The time between the alert pulse and the ack in the wait loop is about 1.5 ms, giving your code an average of 2.5 ms to deal with data. That's not a lot. Timing snapshot: https://hidrive.ionos.com/lnk/UU4mK5Z5 Channel 0 is the ADS1115 trigger, Channel 1 is the ack response by the script.

from machine import I2C, Pin, Timer
import ads1x15
from array import array
import utime

i2c = I2C(baudrate=400000) # Default pins P9 and P10
addr = 72
gain = 1
ads = ads1x15.ADS1115(i2c, addr, gain)
index_put = 0
_BUFFERSIZE = const(512)
data = array("h", [0] * _BUFFERSIZE)

irq_pin = Pin("P11", Pin.IN, Pin.PULL_UP)
ack_pin = Pin("P8", Pin.OUT, value=1)

#
# Interrupt service routine zum messen
# diese wird vom Timer-interrupt aktiviert
#
def sample_auto(x, adc=ads.alert_read, data=data):
    global index_put
    if index_put < _BUFFERSIZE:
        data[index_put] = adc()
        index_put += 1

ads.conversion_start(5, 1) # 250 samples/s, channel 1

irq_pin.callback(handler=sample_auto, trigger=Pin.IRQ_FALLING)

start = utime.ticks_ms()
try:
    index = index_put
    while index_put < _BUFFERSIZE:
        if index != index_put:
            ack_pin(0)
            ack_pin(1)
            index = index_put
except:
    pass

irq_pin.callback(handler=None)
end = utime.ticks_ms()
#
# at that point data contains 512 samples acquired at the given rate
#
print("Time for sampling (ms): ", index_put, end - start)
for _ in range(1, _BUFFERSIZE):
    print(data[_], data[_] - data[_ - 1])
brauliobarahona commented 4 years ago

So, it yet does not work in my setup: Pycom MicroPython 1.20.1.r1 [69dd8b5d-dirty] on 2019-11-22; LoPy4 with ESP32 - interacting through REPL (Atom-pymakr on MacOS). After adjusting the I2Cset up to my own, it simply hangs. Let me try and get further...

robert-hh commented 4 years ago

Maybe the I2C address of your device is different. What is the result to i2c.scan. And yes, the test code will not terminate until 512 samples have been read. P.S.: So you are crafting your own firmware images too?

brauliobarahona commented 4 years ago

yes, as mentioned the setup of the I2C I adjust accordingly ... here:

>>> i2c.scan()
[8, 30, 41, 64, 69, 72, 73, 88, 96]
brauliobarahona commented 4 years ago
#
# Interrupt service routine zum messen
# diese wird vom Timer-interrupt aktiviert
#
def sample_auto(x, adc=ads.alert_read, data=data):
    global index_put
    if index_put < _BUFFERSIZE:
        data[index_put] = adc()
        index_put += 1

This callback seems to be a problem for me ... yesterday I was debugging line by line and I noticed this little issue. However, that should not be the problem. But in any a case index_put (nor index in your new version) update.

 start = utime.ticks_ms()
try:
    index = index_put
    while index_put < _BUFFERSIZE:
        if index != index_put:
            ack_pin(0)
            ack_pin(1)
            index = index_put
except:
    pass
robert-hh commented 4 years ago

This code runs well for me. I used it to create the diagram. But I have the code in a separate script file, while you seem to inject it via pymakr. That makes a difference with respect to BUFFERSIZE. When executed from a script file, it behaves well. When you inject code via pymakr, it actually runs at REPL level, which treats symbols starting with different. You could simply omit the _

brauliobarahona commented 4 years ago

hey, yes, indeed I tried that but the bug in my case is elsewhere, it seems that the callback is simply not calling the handler ... I switched now to Pycom MicroPython 1.20.1 [v1.11-12f4ce0]and start from scratch. I need to understand better the I2C setup and the Pin class ...

robert-hh commented 4 years ago

Why don't you simply copy the script into the file system of your Lopy and call it with import filename?

brauliobarahona commented 4 years ago

In principle, you initialise these pins

irq_pin = Pin("P11", Pin.IN, Pin.PULL_UP)
ack_pin = Pin("P8", Pin.OUT, value=1)

then set up a function to read from the adc sample_auto(), and a callback irq_pin.callback() where the callback function is sample_auto and it is trigger by Pin.IRQ_FALLING ... is this callback supposed to be immediately executed or only when you call ack_pin()?

robert-hh commented 4 years ago

Forget ack_pin. That was just added for signalling. The callback is triggered when the sensor signals that the data is ready by toggling the alert line. For the LoPy to respond, you need in addition to the i2c bus a connection between the alert output of the ADC to P11.

brauliobarahona commented 4 years ago

Why don't you simply copy the script into the file system of your Lopy and call it with import filename?

same result, it sort of hangs ... I say this because I modified the buffer to a small number, and it seems to take too long, when I stop it data has not recorded any values

robert-hh commented 4 years ago

Did you connect P11 to the alert output of the ADS1115?

brauliobarahona commented 4 years ago

Did you connect P11 to the alert output of the ADS1115?

nope, I am trying to figure that out, for now I simply think my I2C set up is fine because I can read what I think are the correct sensor values with ads.alert_read()

robert-hh commented 4 years ago

Without the link between ADS1115 alert and the LoPy P11 the callback function will not be called.

brauliobarahona commented 4 years ago

ok, now P11 is physically connected to my adc ALERT/RDY - but conversion_start and write_register throw an I2C bus error when conversion_start is called ...

robert-hh commented 4 years ago

Check if gnd sda and scl are still connected. You need these for data transfer. Edit: use i2c.scan() to check, whether I2c is still working. Maybe you touched the addr pin at the chip and changed the address.

brauliobarahona commented 4 years ago

indeed, i2cwas broken (had a typo) ... now I am back to the previous state: the callback is not working, but in principle now the P11 is connected to ALERT/RDY :/

robert-hh commented 4 years ago

What kind of board are you using? Does it use an ADS1115? During our last conversion I tested the above script again with by breakouts, and it worked fine.

brauliobarahona commented 4 years ago

huy, yes, I hope so, so the LoPy4 + Pysense V1.1 + ADS1115 + some other stuff ... can you point me to the documentation of i2c.scan()?

robert-hh commented 4 years ago

https://docs.pycom.io/firmwareapi/pycom/machine/i2c/

brauliobarahona commented 4 years ago

at the moment this is how I define the callback

def sample_auto(x, adc=ads.alert_read, data=data):
#    global index_put
#    if index_put < BUFFERSIZE:
        data[index_put] = adc()
        index_put += 1

however, it is never called ... after the program runs, the connection to the ADC seems to be working

>>> ads.alert_read()
4739

however, data has not been populated.

brauliobarahona commented 4 years ago

https://docs.pycom.io/firmwareapi/pycom/machine/i2c/

is there a serious documention? :) - for example, describing in detail the outputs of i2c.scan()?

brauliobarahona commented 4 years ago

Sorry, I know this looks half wrong:

at the moment this is how I define the callback

def sample_auto(x, adc=ads.alert_read, data=data):
#    global index_put
#    if index_put < BUFFERSIZE:
        data[index_put] = adc()
        index_put += 1

but I tried different stuff and to me it is clear that sample_auto is never called.

robert-hh commented 4 years ago

You must not comment index_put, because it is also used by the "main" code to tell, whether the data has been filled. A call to i2c.scan() just returns a list of addresses for the devices on the I2C bus. It should return the address of you ADC adapter.

brauliobarahona commented 4 years ago

the key steps in your snippet, once everything is instantiated are two as far as I understand

(1) establish the callback

irq_pin.callback(handler=sample_auto, trigger=Pin.IRQ_FALLING)

(2) start the conversion

ads.conversion_start(0, 0) # 250 samples/s, channel 1

conversion will then go on, perform the callback (which in principle populates data) until when? in principle until index_put < BUFFERSIZE ? then conversion stops when the callback is removed? i.e.

irq_pin.callback(handler=None)
brauliobarahona commented 4 years ago

You must not comment index_put, because it is also used by the "main" code to tell, whether the data has been filled. A call to i2c.scan() just returns a list of addresses for the devices on the I2C bus. It should return the address of you ADC adapter.

indeed, I thought so, but the callback is simply not called - a print() statement could tell, right?

brauliobarahona commented 4 years ago

so, besides testing how well and how fast can I sample from the ADC I am mainly interested in controlling to some extend the sampling rate and data recording rate, thus I wanted to pass a time object to the call back to have a neat time stamp next to the sensor values ... however, at the moment this seems rather cumbersome ... since I do not really need such a fast sampling rate in any case, I think can live just calling ads.alert_read() - will probably need to move on like this.

robert-hh commented 4 years ago

conversion will then go on, perform the callback (which in principle populates data) until when?

Conversion will go infinite, but putting data into the buffer will stop when index_put == BUFFERSIZE, and that's what the main loop is waiting for. irq_pin.callback(handler=None) stops the callback being called, to avoid trouble when the main code has finished. If you do not use interrupt or timer base conversion, I recommend to use the read() or read_rev() method. The difference: read() starts a conversion, waits until it is finished and then it returns the result. read_alert() assumes that a conversion is finished and just reads the result. read_rev() reads the result and starts the next conversion. So it performs the actions of read(), but in opposite order. It saves some time, because there is not waiting in the call. But is up to the calling code not to call it again toot early.