From c3f3339c87444acec814a150fdad28e650483405 Mon Sep 17 00:00:00 2001 From: Jonathan Hogg Date: Tue, 15 Aug 2023 13:03:45 +0100 Subject: [PATCH 1/5] esp32/modesp32: Add esp32.PCNT class. Add a new `esp32.PCNT` class that provides complete, low-level support to the ESP32 PCNT pulse counting hardware units. This can be used as a building block to implement the higher-level `machine.Counter` and `machine.Encoder` classes. This is enabled by default on all OG, S2, S3, C6 boards, but not on C3 (as the PCNT peripheral is not supported). Original implementation by: Jonathan Hogg Signed-off-by: Jim Mussared Signed-off-by: Angus Gratton --- ports/esp32/boards/sdkconfig.base | 1 + ports/esp32/esp32_common.cmake | 1 + ports/esp32/esp32_pcnt.c | 513 ++++++++++++++++++++++++++++++ ports/esp32/main.c | 5 + ports/esp32/modesp32.c | 3 + ports/esp32/modesp32.h | 6 + ports/esp32/mpconfigport.h | 4 + 7 files changed, 533 insertions(+) create mode 100644 ports/esp32/esp32_pcnt.c diff --git a/ports/esp32/boards/sdkconfig.base b/ports/esp32/boards/sdkconfig.base index 2f1835c9242d4..4bbccf77d17d7 100644 --- a/ports/esp32/boards/sdkconfig.base +++ b/ports/esp32/boards/sdkconfig.base @@ -117,6 +117,7 @@ CONFIG_ADC_CAL_LUT_ENABLE=y CONFIG_UART_ISR_IN_IRAM=y # IDF 5 deprecated +CONFIG_PCNT_SUPPRESS_DEPRECATE_WARN=y CONFIG_RMT_SUPPRESS_DEPRECATE_WARN=y CONFIG_ETH_USE_SPI_ETHERNET=y diff --git a/ports/esp32/esp32_common.cmake b/ports/esp32/esp32_common.cmake index 09b120391305f..9e8acf8897c6a 100644 --- a/ports/esp32/esp32_common.cmake +++ b/ports/esp32/esp32_common.cmake @@ -127,6 +127,7 @@ list(APPEND MICROPY_SOURCE_PORT modesp.c esp32_nvs.c esp32_partition.c + esp32_pcnt.c esp32_rmt.c esp32_ulp.c modesp32.c diff --git a/ports/esp32/esp32_pcnt.c b/ports/esp32/esp32_pcnt.c new file mode 100644 index 0000000000000..36d43ab4559c6 --- /dev/null +++ b/ports/esp32/esp32_pcnt.c @@ -0,0 +1,513 @@ +/* + * This file is part of the MicroPython project, http://micropython.org/ + * + * The MIT License (MIT) + * + * Copyright (c) 2021-22 Jonathan Hogg + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + + +#include "py/runtime.h" +#include "py/mphal.h" +#include "py/obj.h" + +#if MICROPY_PY_ESP32_PCNT + +#include "shared/runtime/mpirq.h" + +#include "modesp32.h" +#include "driver/pcnt.h" + +#if !MICROPY_ENABLE_FINALISER +#error "esp32.PCNT requires MICROPY_ENABLE_FINALISER." +#endif + +typedef struct _esp32_pcnt_irq_obj_t { + mp_irq_obj_t base; + uint32_t flags; + uint32_t trigger; +} esp32_pcnt_irq_obj_t; + +typedef struct _esp32_pcnt_obj_t { + mp_obj_base_t base; + pcnt_unit_t unit; + esp32_pcnt_irq_obj_t *irq; + struct _esp32_pcnt_obj_t *next; +} esp32_pcnt_obj_t; + +// Linked list of PCNT units. +MP_REGISTER_ROOT_POINTER(struct _esp32_pcnt_obj_t *esp32_pcnt_obj_head); + +// Once off installation of the PCNT ISR service (using the default service). +// Persists across soft reset. +static bool pcnt_isr_service_installed = false; + +static mp_obj_t esp32_pcnt_deinit(mp_obj_t self_in); + +void esp32_pcnt_deinit_all(void) { + esp32_pcnt_obj_t **pcnt = &MP_STATE_PORT(esp32_pcnt_obj_head); + while (*pcnt != NULL) { + esp32_pcnt_deinit(MP_OBJ_FROM_PTR(*pcnt)); + *pcnt = (*pcnt)->next; + } +} + +static void esp32_pcnt_init_helper(esp32_pcnt_obj_t *self, size_t n_pos_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + enum { + ARG_channel, + ARG_pin, + ARG_rising, + ARG_falling, + ARG_mode_pin, + ARG_mode_low, + ARG_mode_high, + ARG_min, + ARG_max, + ARG_filter, + ARG_threshold0, + ARG_threshold1, + ARG_value, + }; + + static const mp_arg_t allowed_args[] = { + { MP_QSTR_channel, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 0} }, + // Applies to the channel. + { MP_QSTR_pin, MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL} }, + { MP_QSTR_rising, MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL} }, + { MP_QSTR_falling, MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL} }, + { MP_QSTR_mode_pin, MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL} }, + { MP_QSTR_mode_low, MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL} }, + { MP_QSTR_mode_high, MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL} }, + // Applies to the whole unit. + { MP_QSTR_min, MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL} }, + { MP_QSTR_max, MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL} }, + { MP_QSTR_filter, MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL} }, + { MP_QSTR_threshold0, MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL} }, + { MP_QSTR_threshold1, MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL} }, + // Implicitly zero if min, max, threshold0/1 are set. + { MP_QSTR_value, MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL} }, + }; + + mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; + mp_arg_parse_all(n_pos_args, pos_args, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + + // The pin/mode_pin, rising, falling, mode_low, mode_high args all apply + // to the channel (defaults to channel zero). + mp_uint_t channel = args[ARG_channel].u_int; + if (channel >= PCNT_CHANNEL_MAX) { + mp_raise_ValueError(MP_ERROR_TEXT("channel")); + } + + if (args[ARG_pin].u_obj != MP_OBJ_NULL || args[ARG_mode_pin].u_obj != MP_OBJ_NULL) { + // If you set mode_pin, you must also set pin. + if (args[ARG_pin].u_obj == MP_OBJ_NULL) { + mp_raise_ValueError(MP_ERROR_TEXT("pin")); + } + + mp_hal_pin_obj_t pin = PCNT_PIN_NOT_USED; + mp_hal_pin_obj_t mode_pin = PCNT_PIN_NOT_USED; + + // Set to None to disable pin/mode_pin. + if (args[ARG_pin].u_obj != mp_const_none) { + pin = mp_hal_get_pin_obj(args[ARG_pin].u_obj); + } + if (args[ARG_mode_pin].u_obj != MP_OBJ_NULL && args[ARG_mode_pin].u_obj != mp_const_none) { + mode_pin = mp_hal_get_pin_obj(args[ARG_mode_pin].u_obj); + } + + pcnt_set_pin(self->unit, channel, pin, mode_pin); + } + + if ( + args[ARG_rising].u_obj != MP_OBJ_NULL || args[ARG_falling].u_obj != MP_OBJ_NULL || + args[ARG_mode_low].u_obj != MP_OBJ_NULL || args[ARG_mode_high].u_obj != MP_OBJ_NULL + ) { + mp_uint_t rising = args[ARG_rising].u_obj == MP_OBJ_NULL ? PCNT_COUNT_DIS : mp_obj_get_int(args[ARG_rising].u_obj); + mp_uint_t falling = args[ARG_falling].u_obj == MP_OBJ_NULL ? PCNT_COUNT_DIS : mp_obj_get_int(args[ARG_falling].u_obj); + mp_uint_t mode_low = args[ARG_mode_low].u_obj == MP_OBJ_NULL ? PCNT_MODE_KEEP : mp_obj_get_int(args[ARG_mode_low].u_obj); + mp_uint_t mode_high = args[ARG_mode_high].u_obj == MP_OBJ_NULL ? PCNT_MODE_KEEP : mp_obj_get_int(args[ARG_mode_high].u_obj); + if (rising >= PCNT_COUNT_MAX) { + mp_raise_ValueError(MP_ERROR_TEXT("rising")); + } + if (falling >= PCNT_COUNT_MAX) { + mp_raise_ValueError(MP_ERROR_TEXT("falling")); + } + if (mode_low >= PCNT_MODE_MAX) { + mp_raise_ValueError(MP_ERROR_TEXT("mode_low")); + } + if (mode_high >= PCNT_MODE_MAX) { + mp_raise_ValueError(MP_ERROR_TEXT("mode_high")); + } + pcnt_set_mode(self->unit, channel, rising, falling, mode_high, mode_low); + } + + // The rest of the arguments apply to the whole unit. + + if (args[ARG_filter].u_obj != MP_OBJ_NULL) { + mp_uint_t filter = mp_obj_get_int(args[ARG_filter].u_obj); + if (filter > 1023) { + mp_raise_ValueError(MP_ERROR_TEXT("filter")); + } + if (filter) { + check_esp_err(pcnt_set_filter_value(self->unit, filter)); + check_esp_err(pcnt_filter_enable(self->unit)); + } else { + check_esp_err(pcnt_filter_disable(self->unit)); + } + } + + bool clear = false; + if (args[ARG_value].u_obj != MP_OBJ_NULL) { + mp_int_t value = mp_obj_get_int(args[ARG_value].u_obj); + if (value != 0) { + mp_raise_ValueError(MP_ERROR_TEXT("value")); + } + clear = true; + } + + if (args[ARG_min].u_obj != MP_OBJ_NULL) { + mp_int_t minimum = mp_obj_get_int(args[ARG_min].u_obj); + if (minimum < -32768 || minimum > 0) { + mp_raise_ValueError(MP_ERROR_TEXT("minimum")); + } + check_esp_err(pcnt_set_event_value(self->unit, PCNT_EVT_L_LIM, minimum)); + clear = true; + } + + if (args[ARG_max].u_obj != MP_OBJ_NULL) { + mp_int_t maximum = mp_obj_get_int(args[ARG_max].u_obj); + if (maximum < 0 || maximum > 32767) { + mp_raise_ValueError(MP_ERROR_TEXT("maximum")); + } + check_esp_err(pcnt_set_event_value(self->unit, PCNT_EVT_H_LIM, maximum)); + clear = true; + } + + if (args[ARG_threshold0].u_obj != MP_OBJ_NULL) { + mp_int_t threshold0 = mp_obj_get_int(args[ARG_threshold0].u_obj); + if (threshold0 < -32768 || threshold0 > 32767) { + mp_raise_ValueError(MP_ERROR_TEXT("threshold0")); + } + check_esp_err(pcnt_set_event_value(self->unit, PCNT_EVT_THRES_0, threshold0)); + clear = true; + } + + if (args[ARG_threshold1].u_obj != MP_OBJ_NULL) { + mp_int_t threshold1 = mp_obj_get_int(args[ARG_threshold1].u_obj); + if (threshold1 < -32768 || threshold1 > 32767) { + mp_raise_ValueError(MP_ERROR_TEXT("threshold1")); + } + check_esp_err(pcnt_set_event_value(self->unit, PCNT_EVT_THRES_1, threshold1)); + clear = true; + } + + if (clear) { + check_esp_err(pcnt_counter_clear(self->unit)); + } +} + +// Disable any events, and remove the ISR handler for this unit. +static void esp32_pcnt_disable_events_for_unit(esp32_pcnt_obj_t *self) { + if (!self->irq) { + return; + } + + // Disable all possible events and remove the ISR. + for (pcnt_evt_type_t evt_type = PCNT_EVT_THRES_1; evt_type <= PCNT_EVT_ZERO; evt_type <<= 1) { + check_esp_err(pcnt_event_disable(self->unit, evt_type)); + } + check_esp_err(pcnt_isr_handler_remove(self->unit)); + + // Clear IRQ object state. + self->irq->base.handler = mp_const_none; + self->irq->trigger = 0; +} + +static mp_obj_t esp32_pcnt_make_new(const mp_obj_type_t *type, size_t n_pos_args, size_t n_kw_args, const mp_obj_t *args) { + if (n_pos_args < 1) { + mp_raise_TypeError(MP_ERROR_TEXT("id")); + } + + pcnt_unit_t unit = mp_obj_get_int(args[0]); + if (unit < 0 || unit >= PCNT_UNIT_MAX) { + mp_raise_ValueError(MP_ERROR_TEXT("invalid id")); + } + + // Try and find an existing instance for this unit. + esp32_pcnt_obj_t *self = MP_STATE_PORT(esp32_pcnt_obj_head); + while (self) { + if (self->unit == unit) { + break; + } + self = self->next; + } + + if (!self) { + // Unused unit, create a new esp32_pcnt_obj_t instance and put it at + // the head of the list. + self = mp_obj_malloc(esp32_pcnt_obj_t, &esp32_pcnt_type); + self->unit = unit; + self->irq = NULL; + self->next = MP_STATE_PORT(esp32_pcnt_obj_head); + MP_STATE_PORT(esp32_pcnt_obj_head) = self; + + // Ensure the unit is in a known (deactivated) state. + esp32_pcnt_deinit(MP_OBJ_FROM_PTR(self)); + } + + mp_map_t kw_args; + mp_map_init_fixed_table(&kw_args, n_kw_args, args + n_pos_args); + esp32_pcnt_init_helper(self, 0, args + n_pos_args, &kw_args); + + // Ensure the global PCNT ISR service is installed. + if (!pcnt_isr_service_installed) { + check_esp_err(pcnt_isr_service_install(ESP_INTR_FLAG_IRAM)); + pcnt_isr_service_installed = true; + } + + // And enable for this unit. + check_esp_err(pcnt_intr_enable(self->unit)); + + return MP_OBJ_FROM_PTR(self); +} + +static void esp32_pcnt_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) { + esp32_pcnt_obj_t *self = MP_OBJ_TO_PTR(self_in); + mp_printf(print, "PCNT(%u)", self->unit); +} + +static mp_obj_t esp32_pcnt_init(size_t n_pos_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + esp32_pcnt_obj_t *self = MP_OBJ_TO_PTR(pos_args[0]); + esp32_pcnt_init_helper(self, n_pos_args - 1, pos_args + 1, kw_args); + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_KW(esp32_pcnt_init_obj, 1, esp32_pcnt_init); + +static mp_obj_t esp32_pcnt_deinit(mp_obj_t self_in) { + esp32_pcnt_obj_t *self = MP_OBJ_TO_PTR(self_in); + + // Remove IRQ and events. + esp32_pcnt_disable_events_for_unit(self); + + // Deactivate both channels. + pcnt_config_t channel_config = { + .unit = self->unit, + .pulse_gpio_num = PCNT_PIN_NOT_USED, + .pos_mode = PCNT_COUNT_DIS, + .neg_mode = PCNT_COUNT_DIS, + .ctrl_gpio_num = PCNT_PIN_NOT_USED, + .lctrl_mode = PCNT_MODE_KEEP, + .hctrl_mode = PCNT_MODE_KEEP, + .counter_l_lim = 0, + .counter_h_lim = 0, + }; + for (pcnt_channel_t channel = 0; channel <= 1; ++channel) { + channel_config.channel = channel; + check_esp_err(pcnt_unit_config(&channel_config)); + } + + // Disable filters & thresholds, pause & clear. + check_esp_err(pcnt_filter_disable(self->unit)); + check_esp_err(pcnt_set_event_value(self->unit, PCNT_EVT_THRES_0, 0)); + check_esp_err(pcnt_set_event_value(self->unit, PCNT_EVT_THRES_1, 0)); + check_esp_err(pcnt_counter_pause(self->unit)); + check_esp_err(pcnt_counter_clear(self->unit)); + + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_1(esp32_pcnt_deinit_obj, esp32_pcnt_deinit); + +static mp_obj_t esp32_pcnt_value(size_t n_args, const mp_obj_t *pos_args) { + esp32_pcnt_obj_t *self = MP_OBJ_TO_PTR(pos_args[0]); + + // Optionally use pcnt.value(True) to clear the counter but only support a + // value of zero. Note: This can lead to skipped counts. + if (n_args == 2) { + if (mp_obj_get_int(pos_args[1]) != 0) { + mp_raise_ValueError(MP_ERROR_TEXT("value")); + } + } + + // This loop ensures that the caller's state (as inferred from IRQs, e.g. + // under/overflow) corresponds to the returned value, by synchronously + // flushing all pending IRQs. + int16_t value; + while (true) { + check_esp_err(pcnt_get_counter_value(self->unit, &value)); + if (self->irq && self->irq->flags && self->irq->base.handler != mp_const_none) { + // The handler must call irq.flags() to clear self->irq->base.flags, + // otherwise this will be an infinite loop. + mp_call_function_1(self->irq->base.handler, self->irq->base.parent); + } else { + break; + } + } + + if (n_args == 2) { + // Value was given, and we've already checked it was zero, so clear + // the counter. + check_esp_err(pcnt_counter_clear(self->unit)); + } + + return MP_OBJ_NEW_SMALL_INT(value); +} +static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(esp32_pcnt_value_obj, 1, 2, esp32_pcnt_value); + +static mp_uint_t esp32_pcnt_irq_trigger(mp_obj_t self_in, mp_uint_t new_trigger) { + esp32_pcnt_obj_t *self = MP_OBJ_TO_PTR(self_in); + self->irq->trigger = new_trigger; + for (pcnt_evt_type_t evt_type = PCNT_EVT_THRES_1; evt_type <= PCNT_EVT_ZERO; evt_type <<= 1) { + if (new_trigger & evt_type) { + pcnt_event_enable(self->unit, evt_type); + } else { + pcnt_event_disable(self->unit, evt_type); + } + } + return 0; +} + +static mp_uint_t esp32_pcnt_irq_info(mp_obj_t self_in, mp_uint_t info_type) { + esp32_pcnt_obj_t *self = MP_OBJ_TO_PTR(self_in); + if (info_type == MP_IRQ_INFO_FLAGS) { + // Atomically get-and-clear the flags. + mp_uint_t atomic_state = MICROPY_BEGIN_ATOMIC_SECTION(); + mp_uint_t flags = self->irq->flags; + self->irq->flags = 0; + MICROPY_END_ATOMIC_SECTION(atomic_state); + return flags; + } else if (info_type == MP_IRQ_INFO_TRIGGERS) { + return self->irq->trigger; + } + return 0; +} + +static const mp_irq_methods_t esp32_pcnt_irq_methods = { + .trigger = esp32_pcnt_irq_trigger, + .info = esp32_pcnt_irq_info, +}; + +static IRAM_ATTR void esp32_pcnt_intr_handler(void *arg) { + esp32_pcnt_obj_t *self = (esp32_pcnt_obj_t *)arg; + pcnt_unit_t unit = self->unit; + uint32_t status; + pcnt_get_event_status(unit, &status); + mp_uint_t atomic_state = MICROPY_BEGIN_ATOMIC_SECTION(); + self->irq->flags |= status; + MICROPY_END_ATOMIC_SECTION(atomic_state); + mp_irq_handler(&self->irq->base); +} + +static mp_obj_t esp32_pcnt_irq(size_t n_pos_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + enum { ARG_handler, ARG_trigger }; + static const mp_arg_t allowed_args[] = { + { MP_QSTR_handler, MP_ARG_OBJ, {.u_obj = mp_const_none} }, + { MP_QSTR_trigger, MP_ARG_INT, {.u_int = PCNT_EVT_ZERO} }, + }; + + esp32_pcnt_obj_t *self = pos_args[0]; + mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; + mp_arg_parse_all(n_pos_args - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + + if (!self->irq) { + // Create IRQ object if necessary. This instance persists across a + // de-init. + self->irq = mp_obj_malloc(esp32_pcnt_irq_obj_t, &mp_irq_type); + self->irq->base.methods = (mp_irq_methods_t *)&esp32_pcnt_irq_methods; + self->irq->base.parent = MP_OBJ_FROM_PTR(self); + self->irq->base.ishard = false; + self->irq->base.handler = mp_const_none; + self->irq->trigger = 0; + } + + if (n_pos_args > 1 || kw_args->used != 0) { + // Update IRQ data. + + mp_obj_t handler = args[ARG_handler].u_obj; + mp_uint_t trigger = args[ARG_trigger].u_int; + + if (trigger < PCNT_EVT_THRES_1 || trigger >= (PCNT_EVT_ZERO << 1)) { + mp_raise_ValueError(MP_ERROR_TEXT("trigger")); + } + + if (handler != mp_const_none) { + self->irq->base.handler = handler; + self->irq->trigger = trigger; + pcnt_isr_handler_add(self->unit, esp32_pcnt_intr_handler, (void *)self); + esp32_pcnt_irq_trigger(MP_OBJ_FROM_PTR(self), trigger); + } else { + // Remove the ISR, disable all events, clear the IRQ object state. + esp32_pcnt_disable_events_for_unit(self); + } + } + + return MP_OBJ_FROM_PTR(self->irq); +} +static MP_DEFINE_CONST_FUN_OBJ_KW(esp32_pcnt_irq_obj, 1, esp32_pcnt_irq); + +static mp_obj_t esp32_pcnt_start(mp_obj_t self_in) { + esp32_pcnt_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_esp_err(pcnt_counter_resume(self->unit)); + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_1(esp32_pcnt_start_obj, esp32_pcnt_start); + +static mp_obj_t esp32_pcnt_stop(mp_obj_t self_in) { + esp32_pcnt_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_esp_err(pcnt_counter_pause(self->unit)); + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_1(esp32_pcnt_stop_obj, esp32_pcnt_stop); + +static const mp_rom_map_elem_t esp32_pcnt_locals_dict_table[] = { + // Methods + { MP_ROM_QSTR(MP_QSTR_init), MP_ROM_PTR(&esp32_pcnt_init_obj) }, + { MP_ROM_QSTR(MP_QSTR_value), MP_ROM_PTR(&esp32_pcnt_value_obj) }, + { MP_ROM_QSTR(MP_QSTR_irq), MP_ROM_PTR(&esp32_pcnt_irq_obj) }, + { MP_ROM_QSTR(MP_QSTR_start), MP_ROM_PTR(&esp32_pcnt_start_obj) }, + { MP_ROM_QSTR(MP_QSTR_stop), MP_ROM_PTR(&esp32_pcnt_stop_obj) }, + { MP_ROM_QSTR(MP_QSTR_deinit), MP_ROM_PTR(&esp32_pcnt_deinit_obj) }, + { MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&esp32_pcnt_deinit_obj) }, + + // Constants + { MP_ROM_QSTR(MP_QSTR_IGNORE), MP_ROM_INT(PCNT_COUNT_DIS) }, + { MP_ROM_QSTR(MP_QSTR_INCREMENT), MP_ROM_INT(PCNT_COUNT_INC) }, + { MP_ROM_QSTR(MP_QSTR_DECREMENT), MP_ROM_INT(PCNT_COUNT_DEC) }, + { MP_ROM_QSTR(MP_QSTR_NORMAL), MP_ROM_INT(PCNT_MODE_KEEP) }, + { MP_ROM_QSTR(MP_QSTR_REVERSE), MP_ROM_INT(PCNT_MODE_REVERSE) }, + { MP_ROM_QSTR(MP_QSTR_HOLD), MP_ROM_INT(PCNT_MODE_DISABLE) }, + { MP_ROM_QSTR(MP_QSTR_IRQ_ZERO), MP_ROM_INT(PCNT_EVT_ZERO) }, + { MP_ROM_QSTR(MP_QSTR_IRQ_THRESHOLD0), MP_ROM_INT(PCNT_EVT_THRES_0) }, + { MP_ROM_QSTR(MP_QSTR_IRQ_THRESHOLD1), MP_ROM_INT(PCNT_EVT_THRES_1) }, + { MP_ROM_QSTR(MP_QSTR_IRQ_MIN), MP_ROM_INT(PCNT_EVT_L_LIM) }, + { MP_ROM_QSTR(MP_QSTR_IRQ_MAX), MP_ROM_INT(PCNT_EVT_H_LIM) }, +}; +static MP_DEFINE_CONST_DICT(esp32_pcnt_locals_dict, esp32_pcnt_locals_dict_table); + +MP_DEFINE_CONST_OBJ_TYPE( + esp32_pcnt_type, + MP_QSTR_PCNT, + MP_TYPE_FLAG_NONE, + make_new, esp32_pcnt_make_new, + print, esp32_pcnt_print, + locals_dict, &esp32_pcnt_locals_dict + ); + +#endif // MICROPY_PY_ESP32_PCNT diff --git a/ports/esp32/main.c b/ports/esp32/main.c index f85fb6c084f90..1523e07f916d6 100644 --- a/ports/esp32/main.c +++ b/ports/esp32/main.c @@ -61,6 +61,7 @@ #include "uart.h" #include "usb.h" #include "usb_serial_jtag.h" +#include "modesp32.h" #include "modmachine.h" #include "modnetwork.h" @@ -181,6 +182,10 @@ void mp_task(void *pvParameter) { machine_timer_deinit_all(); + #if MICROPY_PY_ESP32_PCNT + esp32_pcnt_deinit_all(); + #endif + #if MICROPY_PY_THREAD mp_thread_deinit(); #endif diff --git a/ports/esp32/modesp32.c b/ports/esp32/modesp32.c index bf7aec3944228..858be2ed05f81 100644 --- a/ports/esp32/modesp32.c +++ b/ports/esp32/modesp32.c @@ -299,6 +299,9 @@ static const mp_rom_map_elem_t esp32_module_globals_table[] = { { MP_ROM_QSTR(MP_QSTR_NVS), MP_ROM_PTR(&esp32_nvs_type) }, { MP_ROM_QSTR(MP_QSTR_Partition), MP_ROM_PTR(&esp32_partition_type) }, + #if MICROPY_PY_ESP32_PCNT + { MP_ROM_QSTR(MP_QSTR_PCNT), MP_ROM_PTR(&esp32_pcnt_type) }, + #endif #if SOC_RMT_SUPPORTED { MP_ROM_QSTR(MP_QSTR_RMT), MP_ROM_PTR(&esp32_rmt_type) }, #endif diff --git a/ports/esp32/modesp32.h b/ports/esp32/modesp32.h index a685b7b38fe6f..81ab94dc633ae 100644 --- a/ports/esp32/modesp32.h +++ b/ports/esp32/modesp32.h @@ -66,6 +66,12 @@ extern const mp_obj_type_t esp32_partition_type; extern const mp_obj_type_t esp32_rmt_type; extern const mp_obj_type_t esp32_ulp_type; +#if MICROPY_PY_ESP32_PCNT +extern const mp_obj_type_t esp32_pcnt_type; + +void esp32_pcnt_deinit_all(void); +#endif + esp_err_t rmt_driver_install_core1(uint8_t channel_id); #endif // MICROPY_INCLUDED_ESP32_MODESP32_H diff --git a/ports/esp32/mpconfigport.h b/ports/esp32/mpconfigport.h index 1844018030b8b..48ad39ef7ed17 100644 --- a/ports/esp32/mpconfigport.h +++ b/ports/esp32/mpconfigport.h @@ -70,6 +70,7 @@ #define MICROPY_USE_INTERNAL_ERRNO (0) // errno.h from xtensa-esp32-elf/sys-include/sys #define MICROPY_USE_INTERNAL_PRINTF (0) // ESP32 SDK requires its own printf #define MICROPY_SCHEDULER_DEPTH (8) +#define MICROPY_SCHEDULER_STATIC_NODES (1) #define MICROPY_VFS (1) // control over Python builtins @@ -194,6 +195,9 @@ #define MICROPY_PY_ONEWIRE (1) #define MICROPY_PY_SOCKET_EVENTS (MICROPY_PY_WEBREPL) #define MICROPY_PY_BLUETOOTH_RANDOM_ADDR (1) +#ifndef MICROPY_PY_ESP32_PCNT +#define MICROPY_PY_ESP32_PCNT (SOC_PCNT_SUPPORTED) +#endif // fatfs configuration #define MICROPY_FATFS_ENABLE_LFN (1) From e54553c496bf915ed0263c2d2a61d31d61032dea Mon Sep 17 00:00:00 2001 From: Jonathan Hogg Date: Sat, 10 Sep 2022 17:08:27 +0100 Subject: [PATCH 2/5] docs/esp32: Add documentation for esp32.PCNT. Document the new `esp32.PCNT` class for hardware pulse counting. Originally authored by: Jonathan Hogg Signed-off-by: Jim Mussared --- docs/esp32/quickref.rst | 21 ++++++ docs/library/esp32.rst | 142 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 163 insertions(+) diff --git a/docs/esp32/quickref.rst b/docs/esp32/quickref.rst index 49f546a17c03f..7127cc402bd12 100644 --- a/docs/esp32/quickref.rst +++ b/docs/esp32/quickref.rst @@ -566,6 +566,27 @@ ESP32 S2: Provided to deinit the adc driver. +Pulse Counter (pin pulse/edge counting) +--------------------------------------- + +The ESP32 provides up to 8 pulse counter peripherals depending on the hardware, +with id 0..7. These can be configured to count rising and/or falling edges on +any input pin. + +Use the :ref:`esp32.PCNT ` class:: + + from machine import Pin + from esp32 import PCNT + + counter = PCNT(0, pin=Pin(2), rising=PCNT.INCREMENT) # create counter + counter.start() # start counter + count = counter.value() # read count, -32768..32767 + counter.value(0) # reset counter + count = counter.value(0) # read and reset + +The PCNT hardware supports monitoring multiple pins in a single unit to +implement quadrature decoding or up/down signal counters. + Software SPI bus ---------------- diff --git a/docs/library/esp32.rst b/docs/library/esp32.rst index 4be6dc2671c52..0cf0ebc62636d 100644 --- a/docs/library/esp32.rst +++ b/docs/library/esp32.rst @@ -195,6 +195,148 @@ Constants Used in `idf_heap_info`. + +.. _esp32.PCNT: + +PCNT +---- + +This class provides access to the ESP32 hardware support for pulse counting. +There are 8 pulse counter units, with id 0..7. + +See the :ref:`machine.Counter ` and +:ref:`machine.Encoder ` classes for simpler and portable +abstractions of common pulse counting applications. These classes are +implemented as thin Python shims around :class:`PCNT`. + +.. class:: PCNT(id, *, ...) + + Returns the singleton PCNT instance for the given unit ``id``. + + Keyword arguments are passed to the ``init()`` method as described + below. + +.. method:: PCNT.init(*, ...) + + (Re-)initialise a pulse counter unit. Supported keyword arguments are: + + - ``channel``: see description below + - ``pin``: the input Pin to monitor for pulses + - ``rising``: an action to take on a rising edge - one of + ``PCNT.INCREMENT``, ``PCNT.DECREMENT`` or ``PCNT.IGNORE`` (the default) + - ``falling``: an action to take on a falling edge (takes the save values + as the ``rising`` argument). + - ``mode_pin``: ESP32 pulse counters support monitoring a second pin and + altering the behaviour of the counter based on its level - set this + keyword to any input Pin + - ``mode_low``: set to either ``PCNT.HOLD`` or ``PCNT.REVERSE`` to + either suspend counting or reverse the direction of the counter (i.e., + ``PCNT.INCREMENT`` behaves as ``PCNT.DECREMENT`` and vice versa) + when ``mode_pin`` is low + - ``mode_high``: as ``mode_low`` but for the behaviour when ``mode_pin`` + is high + - ``filter``: set to a value 1..1023, in ticks of the 80MHz clock, to + enable the pulse width filter + - ``min``: set to the minimum level of the counter value when + decrementing (-32768..-1) or 0 to disable + - ``max``: set to the maximum level of the counter value when + incrementing (1..32767) or 0 to disable + - ``threshold0``: sets the counter value for the + ``PCNT.IRQ_THRESHOLD0`` event (see ``irq`` method) + - ``threshold1``: sets the counter value for the + ``PCNT.IRQ_THRESHOLD1`` event (see ``irq`` method) + - ``value``: can be set to ``0`` to reset the counter value + + The hardware initialisation is done in stages and so some of the keyword + arguments can be used in groups or in isolation to partially reconfigure a + unit: + + - the ``pin`` keyword (optionally combined with ``mode_pin``) can be used + to change just the bound pin(s) + - ``rising``, ``falling``, ``mode_low`` and ``mode_high`` can be used + (singly or together) to change the counting logic - omitted keywords + use their default (``PCNT.IGNORE`` or ``PCNT.NORMAL``) + - ``filter`` can be used to change only the pulse width filter (with 0 + disabling it) + - each of ``min``, ``max``, ``threshold0`` and ``threshold1`` can + be used to change these limit/event values individually; however, + setting any will reset the counter to zero (i.e., they imply + ``value=0``) + + Each pulse counter unit supports two channels, 0 and 1, each able to + monitor different pins with different counting logic but updating the same + counter value. Use ``channel=1`` with the ``pin``, ``rising``, ``falling``, + ``mode_pin``, ``mode_low`` and ``mode_high`` keywords to configure the + second channel. + + The second channel can be used to configure 4X quadrature decoding with a + single counter unit:: + + pin_a = Pin(2, Pin.INPUT, pull=Pin.PULL_UP) + pin_b = Pin(3, Pin.INPUT, pull=Pin.PULL_UP) + rotary = PCNT(0, min=-32000, max=32000) + rotary.init(channel=0, pin=pin_a, falling=PCNT.INCREMENT, rising=PCNT.DECREMENT, mode_pin=pin_b, mode_low=PCNT.REVERSE) + rotary.init(channel=1, pin=pin_b, falling=PCNT.DECREMENT, rising=PCNT.INCREMENT, mode_pin=pin_a, mode_low=PCNT.REVERSE) + rotary.start() + +.. method:: PCNT.value([value]) + + Call this method with no arguments to return the current counter value. + + If the optional *value* argument is set to ``0`` then the counter is + reset (but the previous value is returned). Read and reset is not atomic and + so it is possible for a pulse to be missed. Any value other than ``0`` will + raise an error. + +.. method:: PCNT.irq(handler=None, trigger=PCNT.IRQ_ZERO) + + ESP32 pulse counters support interrupts on these counter events: + + - ``PCNT.IRQ_ZERO``: the counter has reset to zero + - ``PCNT.IRQ_MIN``: the counter has hit the ``min`` value + - ``PCNT.IRQ_MAX``: the counter has hit the ``max`` value + - ``PCNT.IRQ_THRESHOLD0``: the counter has hit the ``threshold0`` value + - ``PCNT.IRQ_THRESHOLD1``: the counter has hit the ``threshold1`` value + + ``trigger`` should be a bit-mask of the desired events OR'ed together. The + ``handler`` function should take a single argument which is the + :class:`PCNT` instance that raised the event. + + This method returns a callback object. The callback object can be used to + access the bit-mask of events that are outstanding on the PCNT unit.:: + + def pcnt_irq(pcnt): + flags = pcnt.irq().flags() + if flags & PCNT.IRQ_ZERO: + # reset + if flags & PCNT.IRQ_MAX: + # overflow... + ... etc + + pcnt.irq(handler=pcnt_irq, trigger=PCNT.IRQ_ZERO | PCNT.IRQ_MAX | ...) + + **Note:** Accessing ``irq.flags()`` will clear the flags, so only call it + once per invocation of the handler. + + The handler is called with the MicroPython scheduler and so will run at a + point after the interrupt. If another interrupt occurs before the handler + has been called then the events will be coalesced together into a single + call and the bit mask will indicate all events that have occurred. + + To avoid race conditions between a handler being called and retrieving the + current counter value, the ``value()`` method will force execution of any + pending events before returning the current counter value (and potentially + resetting the value). + + Only one handler can be in place per-unit. Set ``handler`` to ``None`` to + disable the event interrupt. + +.. Note:: + ESP32 pulse counters reset to *zero* when reaching the minimum or maximum + value. Thus the ``IRQ_ZERO`` event will also trigger when either of these + events occurs. + + .. _esp32.RMT: RMT From 327655905e9f523070301f2f35459197d46db4fb Mon Sep 17 00:00:00 2001 From: Jonathan Hogg Date: Sat, 10 Sep 2022 16:47:19 +0100 Subject: [PATCH 3/5] esp32/modules/machine.py: Add Counter and Encoder classes. Adds a Python override of the `machine` module, which delegates to the built-in module and adds an implementation of `Counter` and `Encoder`, based on the `esp32.PCNT` class. Original implementation by: Jonathan Hogg Signed-off-by: Jim Mussared --- ports/esp32/modules/machine.py | 192 +++++++++++++++++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 ports/esp32/modules/machine.py diff --git a/ports/esp32/modules/machine.py b/ports/esp32/modules/machine.py new file mode 100644 index 0000000000000..9cfda12f17753 --- /dev/null +++ b/ports/esp32/modules/machine.py @@ -0,0 +1,192 @@ +import sys + +_path = sys.path +sys.path = () +try: + import machine as _machine +finally: + sys.path = _path + del _path + del sys + + +from micropython import const +import esp32 + +if hasattr(esp32, "PCNT"): + _PCNT_RANGE = const(32000) + + class _CounterBase: + _PCNT = esp32.PCNT + # Singletons, keyed by PCNT unit_id (shared by both Counter & Encoder). + _INSTANCES = {} + + # Use __new__ to implement a singleton rather than a factory function, + # because we need to be able to provide class attributes, e.g. + # Counter.RISING, which is not possible if Counter was a function + # (functions cannot have attributes in MicroPython). + def __new__(cls, unit_id, *_args, **_kwargs): + # Find an existing instance for this PCNT unit id. + self = cls._INSTANCES.get(unit_id) + + if self: + # Verify that this PCNT is being used for the same type + # (Encoder or Counter). + if not isinstance(self, cls): + raise ValueError("PCNT in use") + else: + # Previously unused PCNT unit. + self = object.__new__(cls) + cls._INSTANCES[unit_id] = self + + # __init__ will now be called with the same args. + return self + + def __init__(self, unit_id, *args, filter_ns=0, **kwargs): + self._unit_id = unit_id + + if not hasattr(self, "_pcnt"): + # New instance, or previously deinit-ed. + self._pcnt = self._PCNT(unit_id, min=-_PCNT_RANGE, max=_PCNT_RANGE) + elif not (args or kwargs): + # Existing instance, and no args, so accessing the existing + # singleton without reconfiguring. Note: This means that + # Counter/Encoder cannot be partially re-initalised. Either + # you get the existing instance as-is (by passing no arguments + # other than the id), or you must pass all the necessary + # arguments to additionally re-configure it. + return + + # Counter- or Encoder-specific configuration of self._pcnt. + self._configure(*args, **kwargs) + + # Common unit configuration. + self._pcnt.init( + filter=min(max(0, filter_ns * 80 // 1000), 1023), + value=0, + ) + + # Note: We track number-of-overflows rather than the actual count in + # order to avoid the IRQ handler overflowing MicroPython's "small int" + # range. This gives an effective range of 2**30 overflows. User code + # should use counter.value(0) to reset the overflow count. + # The ESP32 PCNT resets to zero on under/overflow (i.e. it does not wrap + # around to the opposite limit), so each overflow corresponds to exactly + # _PCNT_RANGE counts. + + # Reset counter state. + self._overflows = 0 + self._offset = 0 + + # Install IRQ handler to handle under/overflow. + self._pcnt.irq(self._overflow, self._PCNT.IRQ_MIN | self._PCNT.IRQ_MAX) + + # Start counting. + self._pcnt.start() + + # Handle counter under/overflow. + def _overflow(self, pcnt): + mask = pcnt.irq().flags() + if mask & self._PCNT.IRQ_MIN: + self._overflows -= 1 + elif mask & self._PCNT.IRQ_MAX: + self._overflows += 1 + + # Public machine.Counter & machine.Encoder API. + def init(self, *args, **kwargs): + self.__init__(self._unit_id, *args, **kwargs) + + # Public machine.Counter & machine.Encoder API. + def deinit(self): + if hasattr(self, "_pcnt"): + self._pcnt.deinit() + del self._pcnt + + # Public machine.Counter & machine.Encoder API. + def value(self, value=None): + if not hasattr(self, "_pcnt"): + raise RuntimeError("not initialised") + + # This loop deals with the possibility that a PCNT overflow occurs + # between retrieving self._overflows and self._pcnt.value(). + while True: + overflows = self._overflows + current = self._pcnt.value() + # Calling PCNT.value() forces any pending interrupts to run + # for this PCNT unit. So self._overflows must now be the the + # value corresponding to the value we read. + if self._overflows == overflows: + break + + # Compute the result including the number of times we've cycled + # through the range, and any applied offset. + result = overflows * _PCNT_RANGE + current + self._offset + + # If a new value is specified, then zero out the overflows, and set + # self._offset so that it zeros out the current PCNT value. The + # mutation to self._overflows is atomic w.r.t. the overflow IRQ + # handler because the scheduler only runs on branch instructions. + if value is not None: + self._overflows -= overflows + self._offset = value - current + + return result + + class Counter(_CounterBase): + # Public machine.Counter API. + RISING = 1 + FALLING = 2 + UP = _CounterBase._PCNT.INCREMENT + DOWN = _CounterBase._PCNT.DECREMENT + + # Counter-specific configuration. + def _configure(self, src, edge=RISING, direction=UP): + # Only use the first channel. + self._pcnt.init( + channel=0, + pin=src, + rising=direction if edge & Counter.RISING else self._PCNT.IGNORE, + falling=direction if edge & Counter.FALLING else self._PCNT.IGNORE, + ) + + class Encoder(_CounterBase): + # Encoder-specific configuration. + def _configure(self, phase_a, phase_b, phases=1): + if phases not in (1, 2, 4): + raise ValueError("phases") + # Configure the first channel. + self._pcnt.init( + channel=0, + pin=phase_a, + falling=self._PCNT.INCREMENT, + rising=self._PCNT.DECREMENT, + mode_pin=phase_b, + mode_low=self._PCNT.HOLD if phases == 1 else self._PCNT.REVERSE, + ) + if phases == 4: + # For 4x quadrature, enable the second channel. + self._pcnt.init( + channel=1, + pin=phase_b, + falling=self._PCNT.DECREMENT, + rising=self._PCNT.INCREMENT, + mode_pin=phase_a, + mode_low=self._PCNT.REVERSE, + ) + else: + # For 1x and 2x quadrature, disable the second channel. + self._pcnt.init(channel=1, pin=None, rising=self._PCNT.IGNORE) + self._phases = phases + + def phases(self): + return self._phases + + del _CounterBase + + +del esp32 + + +# Delegate to built-in machine module. +def __getattr__(attr): + return getattr(_machine, attr) From 641ca2eb0624fc0df2b358f1a4652525af123fd7 Mon Sep 17 00:00:00 2001 From: Jonathan Hogg Date: Mon, 12 Sep 2022 18:37:22 +0100 Subject: [PATCH 4/5] docs/library/machine: Add docs for Counter and Encoder. Add documentation for `machine.Counter` and `machine.Encoder` as currently implemented by the esp32 port, but intended to be implemented by other ports. Originally authored by: Ihor Nehrutsa and Jonathan Hogg . Signed-off-by: Jim Mussared --- docs/esp32/quickref.rst | 15 ++++++ docs/library/esp32.rst | 3 ++ docs/library/machine.Counter.rst | 93 ++++++++++++++++++++++++++++++++ docs/library/machine.Encoder.rst | 72 +++++++++++++++++++++++++ docs/library/machine.rst | 2 + docs/reference/glossary.rst | 7 +++ 6 files changed, 192 insertions(+) create mode 100644 docs/library/machine.Counter.rst create mode 100644 docs/library/machine.Encoder.rst diff --git a/docs/esp32/quickref.rst b/docs/esp32/quickref.rst index 7127cc402bd12..2c667a0f014db 100644 --- a/docs/esp32/quickref.rst +++ b/docs/esp32/quickref.rst @@ -587,6 +587,21 @@ Use the :ref:`esp32.PCNT ` class:: The PCNT hardware supports monitoring multiple pins in a single unit to implement quadrature decoding or up/down signal counters. +See the :ref:`machine.Counter ` and +:ref:`machine.Encoder ` classes for simpler abstractions of +common pulse counting applications:: + + from machine import Pin, Counter + + counter = Counter(0, Pin(2)) # create a counter as above and start it + count = counter.value() # read the count as an arbitrary precision signed integer + + encoder = Encoder(0, Pin(12), Pin(14)) # create an encoder and begin counting + count = encoder.value() # read the count as an arbitrary precision signed integer + +Note that the id passed to these ``Counter()`` and ``Encoder()`` objects must be +a PCNT id. + Software SPI bus ---------------- diff --git a/docs/library/esp32.rst b/docs/library/esp32.rst index 0cf0ebc62636d..e5f39c7f59aa1 100644 --- a/docs/library/esp32.rst +++ b/docs/library/esp32.rst @@ -336,6 +336,9 @@ implemented as thin Python shims around :class:`PCNT`. value. Thus the ``IRQ_ZERO`` event will also trigger when either of these events occurs. +See the :ref:`machine.Counter ` and +:ref:`machine.Encoder ` classes for simpler abstractions of +common pulse counting applications. .. _esp32.RMT: diff --git a/docs/library/machine.Counter.rst b/docs/library/machine.Counter.rst new file mode 100644 index 0000000000000..f89a6d5b4f17a --- /dev/null +++ b/docs/library/machine.Counter.rst @@ -0,0 +1,93 @@ +.. currentmodule:: machine +.. _machine.Counter: + +class Counter -- pulse counter +============================== + +Counter implements pulse counting by monitoring an input signal and counting +rising or falling edges. + +Minimal example usage:: + + from machine import Pin, Counter + + counter = Counter(0, Pin(0, Pin.IN)) # create Counter for pin 0 and begin counting + value = counter.value() # retrieve current pulse count + +Availability: **ESP32** + +Constructors +------------ + +.. class:: Counter(id, ...) + + Returns the singleton Counter object for the the given *id*. Values of *id* + depend on a particular port and its hardware. Values 0, 1, etc. are commonly + used to select hardware block #0, #1, etc. + + Additional arguments are passed to the :meth:`init` method described below, + and will cause the Counter instance to be re-initialised and reset. + + On ESP32, the *id* corresponds to a :ref:`PCNT unit `. + +Methods +------- + +.. method:: Counter.init(src, *, ...) + + Initialise and reset the Counter with the given parameters: + + - *src* specifies the input pin as a :ref:`machine.Pin ` object. + May be omitted on ports that have a predefined pin for a given hardware + block. + + Additional keyword-only parameters that may be supported by a port are: + + - *edge* specifies the edge to count. Either ``Counter.RISING`` (the default) + or ``Counter.FALLING``. *(Supported on ESP32)* + + - *direction* specifies the direction to count. Either ``Counter.UP`` (the + default) or ``Counter.DOWN``. *(Supported on ESP32)* + + - *filter_ns* specifies a minimum period of time in nanoseconds that the + source signal needs to be stable for a pulse to be counted. Implementations + should use the longest filter supported by the hardware that is less than + or equal to this value. The default is 0 (no filter). *(Supported on ESP32)* + +.. method:: Counter.deinit() + + Stops the Counter, disabling any interrupts and releasing hardware resources. + A Soft Reset should deinitialize all Counter objects. + +.. method:: Counter.value([value]) + + Get, and optionally set, the counter value as a signed integer. + Implementations must aim to do the get and set atomically (i.e. without + leading to skipped counts). + + This counter value could exceed the range of a :term:`small integer`, which + means that calling :meth:`Counter.value` could cause a heap allocation, but + implementations should aim to ensure that internal state only uses small + integers and therefore will not allocate until the user calls + :meth:`Counter.value`. + + For example, on ESP32, the internal state counts overflows of the hardware + counter (every 32000 counts), which means that it will not exceed the small + integer range until ``2**30 * 32000`` counts (slightly over 1 year at 1MHz). + + In general, it is recommended that you should use ``Counter.value(0)`` to reset + the counter (i.e. to measure the counts since the last call), and this will + avoid this problem. + +Constants +--------- + +.. data:: Counter.RISING + Counter.FALLING + + Select the pulse edge. + +.. data:: Counter.UP + Counter.DOWN + + Select the counting direction. diff --git a/docs/library/machine.Encoder.rst b/docs/library/machine.Encoder.rst new file mode 100644 index 0000000000000..fc2de32084867 --- /dev/null +++ b/docs/library/machine.Encoder.rst @@ -0,0 +1,72 @@ +.. currentmodule:: machine +.. _machine.Encoder: + +class Encoder -- quadrature decoding +==================================== + +Encoder implements decoding of quadrature signals as commonly output from +rotary encoders, by counting either up or down depending on the order of two +input pulses. + +Minimal example usage:: + + from machine import Pin, Encoder + + counter = Counter(0, Pin(0, Pin.IN), Pin(1, Pin.IN)) # create Encoder for pins 0, 1 and begin counting + value = counter.value() # retrieve current count + +Availability: **ESP32** + +Constructors +------------ + +.. class:: Encoder(id, ...) + + Returns the singleton Encoder object for the the given *id*. Values of *id* + depend on a particular port and its hardware. Values 0, 1, etc. are commonly + used to select hardware block #0, #1, etc. + + Additional arguments are passed to the :meth:`init` method described below, + and will cause the Encoder instance to be re-initialised and reset. + + On ESP32, the *id* corresponds to a :ref:`PCNT unit `. + +Methods +------- + +.. method:: Encoder.init(phase_a, phase_b, *, ...) + + Initialise and reset the Encoder with the given parameters: + + - *phase_a* specifies the first input pin as a + :ref:`machine.Pin ` object. + + - *phase_b* specifies the second input pin as a + :ref:`machine.Pin ` object. + + These pins may be omitted on ports that have predefined pins for a given + hardware block. + + Additional keyword-only parameters that may be supported by a port are: + + - *filter_ns* specifies a minimum period of time in nanoseconds that the + source signal needs to be stable for a pulse to be counted. Implementations + should use the longest filter supported by the hardware that is less than + or equal to this value. The default is 0 (no filter). *(Supported on ESP32)* + + - *phases* specifies the number of signal edges to count and thus the + granularity of the decoding. e.g. 4 phases corresponds to "4x quadrature + decoding", and will result in four counts per pulse. Ports may support + either 1, 2, or 4 phases and the default is 1 phase. *(Supported on ESP32)* + +.. method:: Encoder.deinit() + + Stops the Encoder, disabling any interrupts and releasing hardware resources. + A Soft Reset should deinitialize all Encoder objects. + +.. method:: Encoder.value([value]) + + Get, and optionally set, the encoder value as a signed integer. + Implementations should aim to do the get and set atomically. + + See :meth:`machine.Counter.value` for details about overflow of this value. diff --git a/docs/library/machine.rst b/docs/library/machine.rst index 3c22aa9af6524..7acaddde815bc 100644 --- a/docs/library/machine.rst +++ b/docs/library/machine.rst @@ -268,6 +268,8 @@ Classes machine.I2S.rst machine.RTC.rst machine.Timer.rst + machine.Counter.rst + machine.Encoder.rst machine.WDT.rst machine.SD.rst machine.SDCard.rst diff --git a/docs/reference/glossary.rst b/docs/reference/glossary.rst index efaa8f607f72a..ab0b22049bd67 100644 --- a/docs/reference/glossary.rst +++ b/docs/reference/glossary.rst @@ -188,6 +188,13 @@ Glossary Most MicroPython boards make a REPL available over a UART, and this is typically accessible on a host PC via USB. + small integer + MicroPython optimises the internal representation of integers such that + "small" values do not take up space on the heap, and calculations with + them do not require heap allocation. On most 32-bit ports, this + corresponds to values in the interval ``-2**30 <= x < 2**30``, but this + should be considered an implementation detail and not relied upon. + stream Also known as a "file-like object". A Python object which provides sequential read-write access to the underlying data. A stream object From 907c5e99769635678e4bfb8dda5c3c0e2aec040f Mon Sep 17 00:00:00 2001 From: Angus Gratton Date: Wed, 30 Jul 2025 17:20:36 +1000 Subject: [PATCH 5/5] tests/extmod_hardware: Add basic tests for machine.Counter and Encoder. These don't test any advanced features, just the basics. This work was funded through GitHub Sponsors. Signed-off-by: Angus Gratton --- tests/extmod_hardware/machine_counter.py | 90 +++++++++++++++++++++ tests/extmod_hardware/machine_encoder.py | 99 ++++++++++++++++++++++++ 2 files changed, 189 insertions(+) create mode 100644 tests/extmod_hardware/machine_counter.py create mode 100644 tests/extmod_hardware/machine_encoder.py diff --git a/tests/extmod_hardware/machine_counter.py b/tests/extmod_hardware/machine_counter.py new file mode 100644 index 0000000000000..62ac1fed47ce7 --- /dev/null +++ b/tests/extmod_hardware/machine_counter.py @@ -0,0 +1,90 @@ +# Test machine.Counter implementation +# +# IMPORTANT: This test requires hardware connections: the out_pin and in_pin +# must be wired together. + +try: + from machine import Counter +except ImportError: + print("SKIP") + raise SystemExit + +import sys +from machine import Pin + +if "esp32" in sys.platform: + id = 0 + out_pin = 4 + in_pin = 5 +else: + print("Please add support for this test on this platform.") + raise SystemExit + +import unittest + +out_pin = Pin(out_pin, mode=Pin.OUT) +in_pin = Pin(in_pin, mode=Pin.IN) + + +def toggle(times): + for _ in range(times): + out_pin(1) + out_pin(0) + + +class TestCounter(unittest.TestCase): + def setUp(self): + out_pin(0) + self.counter = Counter(id, in_pin) + + def tearDown(self): + self.counter.deinit() + + def assertCounter(self, value): + self.assertEqual(self.counter.value(), value) + + def test_connections(self): + # Test the hardware connections are correct. If this test fails, all tests will fail. + out_pin(1) + self.assertEqual(1, in_pin()) + out_pin(0) + self.assertEqual(0, in_pin()) + + def test_count_rising(self): + self.assertCounter(0) + toggle(100) + self.assertCounter(100) + out_pin(1) + self.assertEqual(self.counter.value(0), 101) + self.assertCounter(0) # calling value(0) resets + out_pin(0) + self.assertCounter(0) # no rising edge + out_pin(1) + self.assertCounter(1) + + def test_change_directions(self): + self.assertCounter(0) + toggle(100) + self.assertCounter(100) + self.counter.init(in_pin, direction=Counter.DOWN) + self.assertCounter(0) # calling init() zeroes the counter + self.counter.value(100) # need to manually reset the value + self.assertCounter(100) + toggle(25) + self.assertCounter(75) + + def test_count_falling(self): + self.counter.init(in_pin, direction=Counter.UP, edge=Counter.FALLING) + toggle(20) + self.assertCounter(20) + out_pin(1) + self.assertCounter(20) # no falling edge + out_pin(0) + self.assertCounter(21) + self.counter.value(-(2**24)) + toggle(20) + self.assertCounter(-(2**24 - 20)) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/extmod_hardware/machine_encoder.py b/tests/extmod_hardware/machine_encoder.py new file mode 100644 index 0000000000000..9bd2bb464178c --- /dev/null +++ b/tests/extmod_hardware/machine_encoder.py @@ -0,0 +1,99 @@ +# Test machine.Encoder implementation +# +# IMPORTANT: This test requires hardware connections: +# - out0_pin and in0_pin must be wired together. +# - out1_pin and in1_pin must be wired together. + +try: + from machine import Encoder +except ImportError: + print("SKIP") + raise SystemExit + +import sys +from machine import Pin + +if "esp32" in sys.platform: + id = 0 + out0_pin = 4 + in0_pin = 5 + out1_pin = 12 + in1_pin = 13 +else: + print("Please add support for this test on this platform.") + raise SystemExit + +import unittest + +out0_pin = Pin(out0_pin, mode=Pin.OUT) +in0_pin = Pin(in0_pin, mode=Pin.IN) +out1_pin = Pin(out1_pin, mode=Pin.OUT) +in1_pin = Pin(in1_pin, mode=Pin.IN) + + +class TestEncoder(unittest.TestCase): + def setUp(self): + out0_pin(0) + out1_pin(0) + self.enc = Encoder(id, in0_pin, in1_pin) + self.pulses = 0 # track the expected encoder position in software + + def tearDown(self): + self.enc.deinit() + + def rotate(self, pulses): + for _ in range(abs(pulses)): + self.pulses += 1 if (pulses > 0) else -1 + q = self.pulses % 4 + # Only one pin should change state each "step" so output won't glitch + out0_pin(q in (1, 2)) + out1_pin(q in (2, 3)) + + def assertPosition(self, value): + self.assertEqual(self.enc.value(), value) + + def test_connections(self): + # Test the hardware connections are correct. If this test fails, all tests will fail. + for ch, outp, inp in ((0, out0_pin, in0_pin), (1, out1_pin, in1_pin)): + print("Testing channel ", ch) + outp(1) + self.assertEqual(1, inp()) + outp(0) + self.assertEqual(0, inp()) + + def test_basics(self): + self.assertPosition(0) + self.rotate(100) + self.assertPosition(100 // 4) + self.rotate(-100) + self.assertPosition(0) + + def test_partial(self): + # With phase=1 (default), need 4x pulses to count a rotation + self.assertPosition(0) + self.rotate(1) + self.assertPosition(0) + self.rotate(1) + self.assertPosition(0) + self.rotate(1) + self.assertPosition(1) # only 3 pulses to count first rotation? + self.rotate(1) + self.assertPosition(1) + self.rotate(1) + self.assertPosition(1) + self.rotate(1) + self.assertPosition(1) + self.rotate(1) + self.assertPosition(2) # 4 for next rotation + self.rotate(-1) + self.assertPosition(1) + self.rotate(-4) + self.assertPosition(0) + self.rotate(-4) + self.assertPosition(-1) + self.rotate(-3) + self.assertPosition(-1) + + +if __name__ == "__main__": + unittest.main() 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