adafruit / circuitpython

CircuitPython - a Python implementation for teaching coding with microcontrollers
https://circuitpython.org
Other
4.07k stars 1.2k forks source link

ESP32: Document that `AnalogIn` is not available when WiFi is in use. #9079

Closed tyeth closed 6 months ago

tyeth commented 6 months ago

CircuitPython version

9.0.0-stable  ItsyBitsy ESP32

Code/REPL

# SPDX-FileCopyrightText: 2024 Tyeth Gundry
# SPDX-License-Identifier: MIT

# IGNORE PINOUT COMMENTS for another board:
# the dotstar should use SPI pins, they'll be natively better setup for it.
# Neopixel on A3, POT on A2, dotstar on gpio 35+36, tacho on MISO,  4PST on
# A0(GPIO18),A1(GPIO17),SDA (GPIO7),SCL, mosfet on TX (GPIO5)
# NeoPixel on A3 (GPIO8)
# Potentiometer on A2 (GPIO9)
# DotStar on SPI pins:
# Data on GPIO35 (SCK)
# Clock on GPIO36 (MOSI)
# Tachometer Input on MISO (GPIO37)
# 4PST Rotary Switch:
# Pole 1 on A0 (GPIO18)
# Pole 2 on A1 (GPIO17)
# Pole 3 on SDA (GPIO7)
# Pole 4 on SCL (GPIO8)
# MOSFET Output on TX (GPIO5)

import time
import board
from rainbowio import colorwheel
import neopixel
import asyncio
import analogio
import os
import busio
# import adafruit_dotstar as dotstar
import countio

from adafruit_io.adafruit_io import IO_HTTP, AdafruitIO_RequestError
CIRCUITPYTHON_AIO_USERNAME=os.getenv("CIRCUITPY_AIO_USERNAME", "YOUR_USERNAME_HERE")
CIRCUITPYTHON_AIO_KEY=os.getenv("CIRCUITPY_AIO_KEY", 'YOUR_KEY')
ADAFRUIT_IO_USERNAME = os.getenv('ADAFRUIT_IO_USERNAME', CIRCUITPYTHON_AIO_USERNAME)
ADAFRUIT_IO_KEY = os.getenv('ADAFRUIT_IO_KEY', CIRCUITPYTHON_AIO_KEY)

NEOPIXEL_NUMPIXELS = 13  # Update this to match the number of LEDs.
SPEED = 0.05  # Increase to slow down the rainbow. Decrease to speed it up.
NEOPIXEL_BRIGHTNESS = 0.1 # 0.05  # A number between 0.0 and 1.0, where 0.0 is off, and 1.0 is max.
NEOPIXEL_PIN = board.D5  # This is the default pin on the 5x5 NeoPixel Grid BFF.
POTENTIOMETER_PIN = board.A2 # control knob

# DOTSTAR_NUMPIXELS=24
# DOTSTAR_DATA_PIN = board.D35
# DOTSTAR_CLOCK_PIN = board.D36
# DOTSTAR_BRIGHTNESS = 0.5

TACHO_PIN = board.MISO

# Set up tachometer on MISO (GPIO37)
tach_counter = countio.Counter(TACHO_PIN)

fan_speed = 0
neopixels = neopixel.NeoPixel(NEOPIXEL_PIN, NEOPIXEL_NUMPIXELS, brightness=NEOPIXEL_BRIGHTNESS, auto_write=False, pixel_order=neopixel.GRB)

# # DotStar setup
# dots = dotstar.DotStar(clock=DOTSTAR_CLOCK_PIN, data=DOTSTAR_DATA_PIN, n=DOTSTAR_NUMPIXELS, brightness=DOTSTAR_BRIGHTNESS, auto_write=False, pixel_order=dotstar.BGR)

#Input potentiometer for manual speed / noise adjustment
potentiometer = analogio.AnalogIn(POTENTIOMETER_PIN)

async def rainbow_cycle(wait, pixels):
    while True:
        for color in range(255):
            for pixel in range(len(pixels)):  # pylint: disable=consider-using-enumerate
                pixel_index = (pixel * 256 // len(pixels)) + color * 5
                pixels[pixel] = colorwheel(pixel_index & 255)
            pixels.show()
            await asyncio.sleep(wait)

async def monitor_fanspeed(fanspeed_polling_interval=25):
    global fan_speed, ADAFRUIT_IO_USERNAME, ADAFRUIT_IO_KEY
    while True:
        try:
            io = IO_HTTP(ADAFRUIT_IO_USERNAME, ADAFRUIT_IO_KEY)
            while True:
                fan_speed = io.receive('fan_speed').value
                print(f"fan_speed: {fan_speed}")
                io.run()
                await asyncio.sleep(fanspeed_polling_interval)
        except AdafruitIO_RequestError:
            print("Failed to fetch fan_speed")

async def read_tachometer(interval=5):
    global tach_counter
    first_time = True
    while True:
        revolutions = tach_counter.count
        # Reset the counter after reading
        tach_counter.reset()
        if not first_time:
            print("Tachometer count:", revolutions, "RPM:", revolutions * (60 / interval))
        else:
            print("First time reading tachometer, skipping value")
            first_time = False
        # Add appropriate delay based on how often you want to read the tachometer
        await asyncio.sleep(interval)

async def other_tasks(interval=1):
    while True:
        print(".", end="")
        await asyncio.sleep(interval)

async def read_potentiometer(interval=1):
    global potentiometer
    while True:
        print(f" potentiometer.value: {potentiometer.value} ", end="")
        await asyncio.sleep(interval)

async def main_loop():
    global SPEED, rainbow_cycle, other_tasks, neopixels
    while True:
        print("Hello")
        neopixel_task = asyncio.create_task(rainbow_cycle(SPEED, neopixels))
        # dotstar_task = asyncio.create_task(rainbow_cycle(SPEED, dots))
        tachometer_task = asyncio.create_task(read_tachometer())
        other_tasks = asyncio.create_task(other_tasks())
        potentiometer_task = asyncio.create_task(read_potentiometer())
        await asyncio.gather(
            other_tasks,
            tachometer_task,
            # potentiometer_task,
            neopixel_task
        )#, dotstar_task)
        await asyncio.sleep(1)
        print("Shouldnt get here")

# while True:
#     rainbow_cycle(SPEED)
print("Starting")
asyncio.run(main_loop())
print("Done")

Behavior


connected
......Tachometer count: 0 RPM: 0.0
..........Tachometer count: 0 RPM: 0.0
..........Tachometer count: 0 RPM: 0.0
..........Tachometer count: 0 RPM: 0.0
....
Code stopped by auto-reload. Reloading soon.
soft reboot

Auto-reload is on. Simply save files over USB to run them or enter REPL to disable.
code.py output:
Starting
Hello
First time reading tachometer, skipping value
.Traceback (most recent call last):
  File "/lib/asyncio/core.py", line 261, in run_until_complete
  File "code.py", line 109, in read_potentiometer
espidf.IDFError: Requested resource not found
....Tachometer count: 0 RPM: 0.0
.....Tachometer count: 0 RPM: 0.0
.....Tachometer count: 0 RPM: 0.0

Description

Saving a code change causes soft reset, then I'm assuming fails to bind pin presumably due to still being registered in use

Additional information

No response

RetiredWizard commented 6 months ago

I tried to reproduce this on a QT PY ESP32 Pico with the following code.py:

import analogio
import board
import time
potentiometer = analogio.AnalogIn(board.A2)
while True:
    print("Hellorld!",potentiometer.value)
    time.sleep(10)

I got the same error after a soft reboot:

soft reboot

Auto-reload is on. Simply save files over USB to run them or enter REPL to disable.
code.py output:
Traceback (most recent call last):
  File "code.py", line 6, in <module>
espidf.IDFError: Requested resource not found

However the code.py wouldn't run on hard reset but gave a timeout error:

Auto-reload is on. Simply save files over USB to run them or enter REPL to disable.
code.py output:
Traceback (most recent call last):
  File "code.py", line 6, in <module>
espidf.IDFError: Operation timed out

Code done running.

It turns out that at least on the QT PY, the the AnalogIn object for board.A2 doesn't have a "value" function:

>>> dir(analogio.AnalogIn(board.A2))
['__class__', '__enter__', '__exit__', 'deinit', 'reference_voltage']

None of board.A0 - board.A4 (all ADC2 pins) have value methods. board.A5 and board.A6 (associated with ADC1) have value methods but when used with my test code.py don't cause the "resource not found" error. board.A7 doesn't appear to be associated with either ADC1 or ADC2 and returns an "Invalid pin" error when passed to AnalogIn.

I'm not sure what any of this means but on the Itsy Bitsy it looks like A3,A4 and A5 (and D32, D33) use ADC1 so for a test, if possible, you might try connecting the potentiometer up to one of those pins.

tyeth commented 6 months ago

That could explain something. I had it written initially for a QTPY S2, and thought I'd seen valid values, but to be honest can't be sure. I did just realise at the pub I'm probably using analogio or the pin incorrectly.

On Sun, 24 Mar 2024, 18:10 RetiredWizard, @.***> wrote:

I tried to reproduce this on a QT PY ESP32 Pico with the following code.py:

import analogioimport boardimport timepotentiometer = analogio.AnalogIn(board.A2)while True: print("Hellorld!",potentiometer.value) time.sleep(10)

I got the same error after a soft reboot:

soft reboot

Auto-reload is on. Simply save files over USB to run them or enter REPL to disable. code.py output: Traceback (most recent call last): File "code.py", line 6, in espidf.IDFError: Requested resource not found

However the code.py wouldn't run on hard reset but gave a timeout error:

Auto-reload is on. Simply save files over USB to run them or enter REPL to disable. code.py output: Traceback (most recent call last): File "code.py", line 6, in espidf.IDFError: Operation timed out

Code done running.

It turns out that at least on the QT PY, the the AnalogIn object for board.A2 doesn't have a "value" function:

dir(analogio.AnalogIn(board.A2)) ['class', 'enter', 'exit', 'deinit', 'reference_voltage']

None of board.A0 - board.A4 (all ADC2 pins) have value methods but when used with my test code.py don't cause the "resource not found" error. board.A7 doesn't appear to be associated with either ADC1 or ADC2 and returns an "Invalid pin" error when passed to AnalogIn.

I'm not sure what any of this means but on the Itsy Bitsy it looks like A3,A4 and A5 (and D32, D33) use ADC1 so for a test, if possible, you might try connecting the potentiometer up to one of those pins.

— Reply to this email directly, view it on GitHub https://github.com/adafruit/circuitpython/issues/9079#issuecomment-2016890431, or unsubscribe https://github.com/notifications/unsubscribe-auth/ABTBZ47W5OVD5YRDVSAUUH3YZ4JH5AVCNFSM6AAAAABFFZ7UJ6VHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDAMJWHA4TANBTGE . You are receiving this because you authored the thread.Message ID: @.***>

tyeth commented 6 months ago

Possibly related to #9086 but unlikely as probably user error and a reasonable enough error message. -Edit- Originally closed as no longer using that hardware (mcu+potentiometer) so unable to continue.

RetiredWizard commented 6 months ago

I was curious if there was an explanation for why ADC2 association analog pins did not have value attributes. If that's a limitation of the ESP32 module, I wonder if the pins should be renamed (assuming they can be used for digital I/O).

tannewt commented 6 months ago

Possibly related to #9086 but unlikely as probably user error and a reasonable enough error message. Closing

This is probably an issue. A simpler example like the one @RetiredWizard is how to know for sure.

It turns out that at least on the QT PY, the the AnalogIn object for board.A2 doesn't have a "value" function:

I think this is a different bug around how dir() figures out what is on an object.

dhalbert commented 6 months ago

This is strange. I installed 9.0.1 on QT Py ESP32 Pico, and used @RetiredWizard's test program, and it's not breaking for me:

Adafruit CircuitPython 9.0.1 on 2024-03-27; Adafruit QT Py ESP32 PICO with ESP32
>>> 
soft reboot

Auto-reload is on. Simply save files over USB to run them or enter REPL to disable.
code.py output:
Hellorld! 37076
Hellorld! 8797
Traceback (most recent call last):
  File "code.py", line 7, in <module>
KeyboardInterrupt: 

Code done running.

Press any key to enter the REPL. Use CTRL-D to reload.
soft reboot

Auto-reload is on. Simply save files over USB to run them or enter REPL to disable.
code.py output:
Hellorld! 37414
Hellorld! 4150
Traceback (most recent call last):
  File "code.py", line 7, in <module>
KeyboardInterrupt: 

Code done running.

Press any key to enter the REPL. Use CTRL-D to reload.
soft reboot

Auto-reload is on. Simply save files over USB to run them or enter REPL to disable.
code.py output:
Hellorld! 37017
RetiredWizard commented 6 months ago

To create the soft reboots, I was saving files to the device while code.py was running using web workflow.

dhalbert commented 6 months ago

OK, this is a hardware issue, specific to the ESP32. The ESP32 has two ADC's, ADC1 and ADC2. Only ADC2 is connectable to pins . (EDIT: this is wrong) But it is also used when WiFi is in use. So really, you can only use ADC2 when WiFi is off.

Quoting from https://www.espressif.com/sites/default/files/documentation/esp32_technical_reference_manual_en.pdf, section 29.3.1

... If Wi-Fi module is using the SAR ADC2, users can not measure the analog signal from the pins using SAR ADC2. After SAR ADC2 is released by Wi-Fi, users can use SAR ADC2 normally.

In practice, ADC2 is not available.

We should document this in a Limitations: section in AnalogIn. Changing title to reflect that.

dhalbert commented 6 months ago

Also note:

https://esp32.com/viewtopic.php?t=30102#p104184

On the ESP32-S2 and newer chips, ADC can be used together with Wi-Fi, although usage by Wi-Fi has priority over the usage by software. So you may occasionally get an error code while getting an ADC sample, in which case you have to retry sampling.