Skip to content

esp32: ADC and ULP improvements #16521

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 124 additions & 11 deletions docs/library/esp32.rst
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ with each number, the bitstream is ``0101`` with durations of [100ns, 2000ns,
For more details see Espressif's `ESP-IDF RMT documentation.
<https://docs.espressif.com/projects/esp-idf/en/latest/api-reference/peripherals/rmt.html>`_.

.. Warning::
.. warning::
The current MicroPython RMT implementation lacks some features, most notably
receiving pulses. RMT should be considered a
*beta feature* and the interface may change in the future.
Expand Down Expand Up @@ -300,34 +300,147 @@ Constants
Ultra-Low-Power co-processor
----------------------------

This class gives access to the Ultra Low Power (ULP) co-processor on the ESP32,
ESP32-S2 and ESP32-S3 chips.
This class gives access to the Finite State Machine (FSM) Ultra Low Power (ULP)
co-processor on the ESP32, ESP32-S2 and ESP32-S3 chips. In addition the ESP32-S2
and ESP32-S3 chips contain a RISCV co-processor which can be accessed through this
class.

.. warning::
.. note::
By default RISCV is preferred on supported chips, however, you can configure a custom build
of micropython to revert back to using the FSM ULP.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
of micropython to revert back to using the FSM ULP.
of MicroPython to revert back to using the FSM ULP.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In addition to the nitpick about capitalisation, this is something of a breaking change for anyone who was using the ULP class on S2 and S3 before, yes? Although I agree the RISC-V one is much nicer to work with, we might have to defer swapping defaults until MP 2.0 (or add a build variant for some boards).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's kind of a shame that ESP-IDF doesn't support enabling both ULP types... I'm guessing you looked into this and there's no way to make it work?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, yes I have looked deep into it and it looks like they are mutually exclusive so we only get to pick one or the other at build time :/ Maybe in the future IDF releases there will be an option for runtime selection as I don't know if this is a hardware of software limitation of the ESP32.

Modify your mpconfigboard.cmake by specifying::

set(PREFER_FSM_ULP ON)

You can compile a custom board definition which uses the ULP and embeds the app image into the firmware.
To do this, edit or create a board definition in the ports/esp32/boards folder.
Make sure the mpconfigboard.cmake file has the following definition::

set(ulp_embedded_sources ${MICROPY_BOARD_DIR}/ulp/main_pin.c)
# and optionally
set(ulp_embedded_sources ${MICROPY_BOARD_DIR}/cmodules/somemodule.c)

When any sources are defined like this, the ULP code will be automatically added the the firmware.
Any variables prefixed with *var_* will be automatically added to the ULP class and you can use them to reference a memory location for reading and writing

Example usage with embedded code::

import esp32
u = esp32.ULP()
u.run_embedded()
print(u.read(u.VAR_TEST))


Example usage with external code::

import esp32
u = esp32.ULP()

with open("test.bin","rb") as file:
buf = file.read()

u.run(buf)
print(u.read(0x500000dc))

Example usage with external code (FSM)::

import esp32
u = esp32.ULP()

with open("fsm.bin","rb") as file:
buf = file.read()

u.run(0x1c,buf)
print(u.write(0x500000dc, 42))

Example usage with embedded code using ADC1 channel 1 on ESP32 S3 (GPIO_2)::

import esp32
u = esp32.ULP()

u.adc_init(1)
u.set_wakeup_period(100)
u.run_embedded()
print(u.write(u.VAR_DO_ADC_SAMPLING, 1))

This class does not provide access to the RISCV ULP co-processor available
on the ESP32-S2 and ESP32-S3 chips.
.. note::
Exposed variables are relative address offsets from RTC_SLOW_MEM address,

however you can use full address values also, if loading ULP code from outside the compilation process.

.. class:: ULP()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even keeping the API (and the implementation) mostly the same, what do you think about renaming the class to RVULP if the build is configured for RISC-V?

Otherwise it's quite confusing to take Python code that works on one board and find it's valid Python on another board, but also doesn't work.

This also makes it easier to document some of the differences (like ULP.set_wakeup_period) between the two implementations.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree and this was going through my head after implementing the changes. This change would be very breaking if the RISCV preference was a default on future builds. I agree the RVULP class is a great idea!


This class provides access to the Ultra-Low-Power co-processor.

.. method:: ULP.set_wakeup_period(period_index, period_us)
ULP.set_wakeup_period(period_us)

Set the wake-up period.
Set the wake-up period. Time specified in micro-seconds.
*period_index* only on FSM.
Since only one slot is used by the RISCV ULP, you only provide the time period in this function.

.. method:: ULP.load_binary(load_addr, program_binary)
.. method:: ULP.run(entry_point, program_binary)
ULP.run(program_binary)

Load a *program_binary* into the ULP at the given *load_addr*.
Load a *program_binary* into the ULP and start from the beginning.
*program_binary* should be a bytearray.

.. method:: ULP.run(entry_point)
*entry_point is only used for FSM*
Start the ULP running at the given *entry_point*, if applicable.

.. method:: ULP.pause()

Stop the ULP wakeup timer from triggering. On RISCV, the ULP will also be halted.

.. method:: ULP.resume()

Resume the ULP wakeup timer. On RISCV, the ULP will also be restarted.

.. method:: ULP.read(address)

Read the contents of RTC memory, specifying either an absolute address within the RTC range
or use embedded variable constants, defined during compilation of firmware.

.. method:: ULP.write(address, value)

Write to the contents of RTC memory, specifying either an absolute address within the RTC range
or use embedded variable constants, defined during compilation of firmware.

The function automatically prevents writing to any other location other than the RTC memory.

.. warning:: **Be careful** as you can overwrite the running ULP program instructions, requiring a reload.


.. method:: ULP.rtc_init(pin_number)

Initialize the give GPIO pin number (different RTC_IO number on ESP32).

.. method:: ULP.rtc_deinit(pin_number)

Un-initialize the give GPIO pin number (different RTC_IO number on ESP32).

.. method:: ULP.adc_init(channel)

Prepare the ADC for use by the ULP on the specified RTC_IO *channel*.

.. note:: Only ADC1 is working with the current ESP-IDF used by MicroPython

.. method:: ULP.run_embedded()

This will load and run the embedded binary, exposing any variables onto the class, for reading and writing.

.. note:: Only available if firmware has compiled with a ULP program image
Start the ULP running the embedded ULP app image.

Start the ULP running at the given *entry_point*.


Constants
---------

.. data:: ULP.RESERVE_MEM

The amount of reserved RTC_SLOW_MEM in bytes.

.. data:: esp32.WAKEUP_ALL_LOW
esp32.WAKEUP_ANY_HIGH

Expand Down
86 changes: 34 additions & 52 deletions ports/esp32/adc.c
Original file line number Diff line number Diff line change
Expand Up @@ -27,82 +27,64 @@

#include "py/mphal.h"
#include "adc.h"
#include "driver/adc.h"
#include "esp_adc/adc_oneshot.h"
#include "esp_adc/adc_cali_scheme.h"

#define DEFAULT_VREF 1100

void madcblock_bits_helper(machine_adc_block_obj_t *self, mp_int_t bits) {
if (bits < SOC_ADC_RTC_MIN_BITWIDTH && bits > SOC_ADC_RTC_MAX_BITWIDTH) {
// Invalid value for the current chip, raise exception in the switch below.
bits = -1;
}
switch (bits) {
case 9:
self->width = ADC_BITWIDTH_9;
break;
case 10:
self->width = ADC_BITWIDTH_10;
break;
case 11:
self->width = ADC_BITWIDTH_11;
break;
case 12:
self->width = ADC_BITWIDTH_12;
break;
case 13:
self->width = ADC_BITWIDTH_13;
break;
default:
mp_raise_ValueError(MP_ERROR_TEXT("invalid bits"));
}
self->bits = bits;
static esp_err_t ensure_adc_calibration(machine_adc_block_obj_t *self, adc_atten_t atten);

if (self->unit_id == ADC_UNIT_1) {
adc1_config_width(self->width);
}

esp_err_t apply_self_adc_channel_atten(const machine_adc_obj_t *self, uint8_t atten) {
adc_oneshot_chan_cfg_t config = {
.atten = atten,
.bitwidth = self->block->bitwidth,
};
esp_err_t ret = adc_oneshot_config_channel(self->block->handle, self->channel_id, &config);
return ret;
}

mp_int_t madcblock_read_helper(machine_adc_block_obj_t *self, adc_channel_t channel_id) {
int raw = 0;
if (self->unit_id == ADC_UNIT_1) {
raw = adc1_get_raw(channel_id);
} else {
#if (SOC_ADC_PERIPH_NUM >= 2)
check_esp_err(adc2_get_raw(channel_id, self->width, &raw));
#endif
}
return raw;
int reading = 0;
adc_oneshot_read(self->handle, channel_id, &reading);
return reading;
}

/*
During testing, it turned out that the function `adc_cali_raw_to_voltage` does not account for the lower resolution,
instead it expects the full resolution value as an argument, hence the scaling applied here
*/
mp_int_t madcblock_read_uv_helper(machine_adc_block_obj_t *self, adc_channel_t channel_id, adc_atten_t atten) {
int raw = madcblock_read_helper(self, channel_id);
int uv = 0;

check_esp_err(ensure_adc_calibration(self, atten));
check_esp_err(adc_cali_raw_to_voltage(self->calib[atten], (raw << (ADC_WIDTH_MAX - self->bitwidth)), &uv));
return (mp_int_t)uv * 1000;
}

static esp_err_t ensure_adc_calibration(machine_adc_block_obj_t *self, adc_atten_t atten) {
if (self->handle[atten] != NULL) {
if (self->calib[atten] != NULL) {
return ESP_OK;
}

esp_err_t ret = ESP_OK;

#if ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED
adc_cali_curve_fitting_config_t cali_config = {
.unit_id = self->unit_id,
.atten = atten,
.bitwidth = self->width,
.bitwidth = self->bitwidth,
};
return adc_cali_create_scheme_curve_fitting(&cali_config, &self->handle[atten]);
ret = adc_cali_create_scheme_curve_fitting(&cali_config, &self->calib[atten]);
#else
adc_cali_line_fitting_config_t cali_config = {
.unit_id = self->unit_id,
.atten = atten,
.bitwidth = self->width,
.bitwidth = self->bitwidth,
};
return adc_cali_create_scheme_line_fitting(&cali_config, &self->handle[atten]);
ret = adc_cali_create_scheme_line_fitting(&cali_config, &self->calib[atten]);
#endif
}

mp_int_t madcblock_read_uv_helper(machine_adc_block_obj_t *self, adc_channel_t channel_id, adc_atten_t atten) {
int raw = madcblock_read_helper(self, channel_id);
int uv;

check_esp_err(ensure_adc_calibration(self, atten));
check_esp_err(adc_cali_raw_to_voltage(self->handle[atten], raw, &uv));

return (mp_int_t)uv * 1000;
return ret;
}
47 changes: 41 additions & 6 deletions ports/esp32/adc.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,45 @@
#define MICROPY_INCLUDED_ESP32_ADC_H

#include "py/runtime.h"
#include "esp_adc_cal.h"
#include "esp_adc/adc_oneshot.h"
#include "esp_adc/adc_cali_scheme.h"

#define ADC_ATTEN_MAX SOC_ADC_ATTEN_NUM
#define ADC_ATTEN_COUNT SOC_ADC_ATTEN_NUM
#define ADC_ATTEN_MIN ADC_ATTEN_DB_0
#define ADC_ATTEN_MAX ADC_ATTEN_DB_11

/*
https://github.com/espressif/esp-idf/issues/13128
https://github.com/espressif/esp-idf/blob/release/v5.2/components/soc/esp32s3/include/soc/soc_caps.h
https://docs.espressif.com/projects/esp-chip-errata/en/latest/esp32s2/03-errata-description/index.html
https://docs.espressif.com/projects/esp-idf/en/v4.2/esp32/api-reference/peripherals/adc.html

Looks like only the original esp32 is capable of bitwidth adjustment, all others are stuck at 12 bits,
except the S2, which is locked at 13 bits, otherwise attenuation doesn't work.
*/
#if CONFIG_IDF_TARGET_ESP32S2

#define ADC_WIDTH_MIN ADC_BITWIDTH_13
#define ADC_WIDTH_MAX ADC_BITWIDTH_13

#elif CONFIG_IDF_TARGET_ESP32

#define ADC_WIDTH_MIN ADC_BITWIDTH_9
#define ADC_WIDTH_MAX ADC_BITWIDTH_12

#else

#define ADC_WIDTH_MIN ADC_BITWIDTH_12
#define ADC_WIDTH_MAX ADC_BITWIDTH_12

#endif

typedef struct _machine_adc_block_obj_t {
mp_obj_base_t base;
adc_unit_t unit_id;
mp_int_t bits;
adc_bits_width_t width;
adc_cali_handle_t handle[ADC_ATTEN_MAX];
adc_oneshot_unit_handle_t handle;
adc_cali_handle_t calib[ADC_ATTEN_COUNT];
adc_bitwidth_t bitwidth;
} machine_adc_block_obj_t;

typedef struct _machine_adc_obj_t {
Expand All @@ -51,11 +79,18 @@ typedef struct _machine_adc_obj_t {

extern machine_adc_block_obj_t madcblock_obj[];

void madcblock_bits_helper(machine_adc_block_obj_t *self, mp_int_t bits);
esp_err_t apply_self_adc_channel_atten(const machine_adc_obj_t *self, uint8_t atten);

mp_int_t madcblock_read_helper(machine_adc_block_obj_t *self, adc_channel_t channel_id);
mp_int_t madcblock_read_uv_helper(machine_adc_block_obj_t *self, adc_channel_t channel_id, adc_atten_t atten);

const machine_adc_obj_t *madc_search_helper(machine_adc_block_obj_t *block, adc_channel_t channel_id, gpio_num_t gpio_id);
void madc_init_helper(const machine_adc_obj_t *self, size_t n_pos_args, const mp_obj_t *pos_args, mp_map_t *kw_args);

mp_int_t mp_machine_adc_atten_get_helper(const machine_adc_obj_t *self);
void mp_machine_adc_atten_set_helper(const machine_adc_obj_t *self, mp_int_t atten);

mp_int_t mp_machine_adc_width_get_helper(const machine_adc_obj_t *self);
void mp_machine_adc_block_width_set_helper(machine_adc_block_obj_t *self, mp_int_t width);

#endif // MICROPY_INCLUDED_ESP32_ADC_H
6 changes: 4 additions & 2 deletions ports/esp32/boards/sdkconfig.base
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,9 @@ CONFIG_MBEDTLS_DEFAULT_MEM_ALLOC=y
# Only on: ESP32, ESP32S2, ESP32S3
CONFIG_ULP_COPROC_ENABLED=y
CONFIG_ULP_COPROC_TYPE_FSM=y
CONFIG_ULP_COPROC_RESERVE_MEM=2040
# Allow SoCs that support RISCV to have it enabled by default, has no effect otherwise
CONFIG_ULP_COPROC_TYPE_RISCV=y
CONFIG_ULP_COPROC_RESERVE_MEM=4096

# For cmake build
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
Expand All @@ -115,7 +117,7 @@ CONFIG_ADC_CAL_LUT_ENABLE=y
CONFIG_UART_ISR_IN_IRAM=y

# IDF 5 deprecated
CONFIG_ADC_SUPPRESS_DEPRECATE_WARN=y
CONFIG_ADC_SUPPRESS_DEPRECATE_WARN=n
CONFIG_RMT_SUPPRESS_DEPRECATE_WARN=y

CONFIG_ETH_USE_SPI_ETHERNET=y
Expand Down
2 changes: 2 additions & 0 deletions ports/esp32/boards/sdkconfig.ulp_fsm
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
CONFIG_ULP_COPROC_TYPE_FSM=y
CONFIG_ULP_COPROC_TYPE_RISCV=n
Loading
Loading
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy