From 13a124c86d3c6944d4fb27462c4a98a2bc553881 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 10 Feb 2026 10:10:27 -0500 Subject: [PATCH] [pulse_counter] Migrate from legacy PCNT API to new ESP-IDF 5.x API (#13904) Co-authored-by: Claude Opus 4.6 --- esphome/components/esp32/__init__.py | 1 + esphome/components/hlw8012/sensor.py | 5 +- .../pulse_counter/pulse_counter_sensor.cpp | 113 ++++++++++-------- .../pulse_counter/pulse_counter_sensor.h | 21 ++-- esphome/components/pulse_counter/sensor.py | 5 +- 5 files changed, 72 insertions(+), 73 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 6f5011246cb..8c052acc87a 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -135,6 +135,7 @@ DEFAULT_EXCLUDED_IDF_COMPONENTS = ( "esp_driver_dac", # DAC driver - only needed by esp32_dac component "esp_driver_i2s", # I2S driver - only needed by i2s_audio component "esp_driver_mcpwm", # MCPWM driver - ESPHome doesn't use motor control PWM + "esp_driver_pcnt", # PCNT driver - only needed by pulse_counter, hlw8012 components "esp_driver_rmt", # RMT driver - only needed by remote_transmitter/receiver, neopixelbus "esp_driver_touch_sens", # Touch sensor driver - only needed by esp32_touch "esp_driver_twai", # TWAI/CAN driver - only needed by esp32_can component diff --git a/esphome/components/hlw8012/sensor.py b/esphome/components/hlw8012/sensor.py index 4727877633c..1d793ac6b17 100644 --- a/esphome/components/hlw8012/sensor.py +++ b/esphome/components/hlw8012/sensor.py @@ -94,10 +94,7 @@ CONFIG_SCHEMA = cv.Schema( async def to_code(config): if CORE.is_esp32: - # Re-enable ESP-IDF's legacy driver component (excluded by default to save compile time) - # HLW8012 uses pulse_counter's PCNT storage which requires driver/pcnt.h - # TODO: Remove this once pulse_counter migrates to new PCNT API (driver/pulse_cnt.h) - include_builtin_idf_component("driver") + include_builtin_idf_component("esp_driver_pcnt") var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) diff --git a/esphome/components/pulse_counter/pulse_counter_sensor.cpp b/esphome/components/pulse_counter/pulse_counter_sensor.cpp index c0d74cef4a8..ef4cc980f66 100644 --- a/esphome/components/pulse_counter/pulse_counter_sensor.cpp +++ b/esphome/components/pulse_counter/pulse_counter_sensor.cpp @@ -1,6 +1,11 @@ #include "pulse_counter_sensor.h" #include "esphome/core/log.h" +#ifdef HAS_PCNT +#include +#include +#endif + namespace esphome { namespace pulse_counter { @@ -56,103 +61,107 @@ pulse_counter_t BasicPulseCounterStorage::read_raw_value() { #ifdef HAS_PCNT bool HwPulseCounterStorage::pulse_counter_setup(InternalGPIOPin *pin) { - static pcnt_unit_t next_pcnt_unit = PCNT_UNIT_0; - static pcnt_channel_t next_pcnt_channel = PCNT_CHANNEL_0; this->pin = pin; this->pin->setup(); - this->pcnt_unit = next_pcnt_unit; - this->pcnt_channel = next_pcnt_channel; - next_pcnt_unit = pcnt_unit_t(int(next_pcnt_unit) + 1); - if (int(next_pcnt_unit) >= PCNT_UNIT_0 + PCNT_UNIT_MAX) { - next_pcnt_unit = PCNT_UNIT_0; - next_pcnt_channel = pcnt_channel_t(int(next_pcnt_channel) + 1); + + pcnt_unit_config_t unit_config = { + .low_limit = INT16_MIN, + .high_limit = INT16_MAX, + .flags = {.accum_count = true}, + }; + esp_err_t error = pcnt_new_unit(&unit_config, &this->pcnt_unit); + if (error != ESP_OK) { + ESP_LOGE(TAG, "Creating PCNT unit failed: %s", esp_err_to_name(error)); + return false; } - ESP_LOGCONFIG(TAG, - " PCNT Unit Number: %u\n" - " PCNT Channel Number: %u", - this->pcnt_unit, this->pcnt_channel); + pcnt_chan_config_t chan_config = { + .edge_gpio_num = this->pin->get_pin(), + .level_gpio_num = -1, + }; + error = pcnt_new_channel(this->pcnt_unit, &chan_config, &this->pcnt_channel); + if (error != ESP_OK) { + ESP_LOGE(TAG, "Creating PCNT channel failed: %s", esp_err_to_name(error)); + return false; + } - pcnt_count_mode_t rising = PCNT_COUNT_DIS, falling = PCNT_COUNT_DIS; + pcnt_channel_edge_action_t rising = PCNT_CHANNEL_EDGE_ACTION_HOLD; + pcnt_channel_edge_action_t falling = PCNT_CHANNEL_EDGE_ACTION_HOLD; switch (this->rising_edge_mode) { case PULSE_COUNTER_DISABLE: - rising = PCNT_COUNT_DIS; + rising = PCNT_CHANNEL_EDGE_ACTION_HOLD; break; case PULSE_COUNTER_INCREMENT: - rising = PCNT_COUNT_INC; + rising = PCNT_CHANNEL_EDGE_ACTION_INCREASE; break; case PULSE_COUNTER_DECREMENT: - rising = PCNT_COUNT_DEC; + rising = PCNT_CHANNEL_EDGE_ACTION_DECREASE; break; } switch (this->falling_edge_mode) { case PULSE_COUNTER_DISABLE: - falling = PCNT_COUNT_DIS; + falling = PCNT_CHANNEL_EDGE_ACTION_HOLD; break; case PULSE_COUNTER_INCREMENT: - falling = PCNT_COUNT_INC; + falling = PCNT_CHANNEL_EDGE_ACTION_INCREASE; break; case PULSE_COUNTER_DECREMENT: - falling = PCNT_COUNT_DEC; + falling = PCNT_CHANNEL_EDGE_ACTION_DECREASE; break; } - pcnt_config_t pcnt_config = { - .pulse_gpio_num = this->pin->get_pin(), - .ctrl_gpio_num = PCNT_PIN_NOT_USED, - .lctrl_mode = PCNT_MODE_KEEP, - .hctrl_mode = PCNT_MODE_KEEP, - .pos_mode = rising, - .neg_mode = falling, - .counter_h_lim = 0, - .counter_l_lim = 0, - .unit = this->pcnt_unit, - .channel = this->pcnt_channel, - }; - esp_err_t error = pcnt_unit_config(&pcnt_config); + error = pcnt_channel_set_edge_action(this->pcnt_channel, rising, falling); if (error != ESP_OK) { - ESP_LOGE(TAG, "Configuring Pulse Counter failed: %s", esp_err_to_name(error)); + ESP_LOGE(TAG, "Setting PCNT edge action failed: %s", esp_err_to_name(error)); return false; } if (this->filter_us != 0) { - uint16_t filter_val = std::min(static_cast(this->filter_us * 80u), 1023u); - ESP_LOGCONFIG(TAG, " Filter Value: %" PRIu32 "us (val=%u)", this->filter_us, filter_val); - error = pcnt_set_filter_value(this->pcnt_unit, filter_val); + uint32_t max_glitch_ns = PCNT_LL_MAX_GLITCH_WIDTH * 1000000u / (uint32_t) esp_clk_apb_freq(); + pcnt_glitch_filter_config_t filter_config = { + .max_glitch_ns = std::min(this->filter_us * 1000u, max_glitch_ns), + }; + error = pcnt_unit_set_glitch_filter(this->pcnt_unit, &filter_config); if (error != ESP_OK) { - ESP_LOGE(TAG, "Setting filter value failed: %s", esp_err_to_name(error)); - return false; - } - error = pcnt_filter_enable(this->pcnt_unit); - if (error != ESP_OK) { - ESP_LOGE(TAG, "Enabling filter failed: %s", esp_err_to_name(error)); + ESP_LOGE(TAG, "Setting PCNT glitch filter failed: %s", esp_err_to_name(error)); return false; } } - error = pcnt_counter_pause(this->pcnt_unit); + error = pcnt_unit_add_watch_point(this->pcnt_unit, INT16_MIN); if (error != ESP_OK) { - ESP_LOGE(TAG, "Pausing pulse counter failed: %s", esp_err_to_name(error)); + ESP_LOGE(TAG, "Adding PCNT low limit watch point failed: %s", esp_err_to_name(error)); return false; } - error = pcnt_counter_clear(this->pcnt_unit); + error = pcnt_unit_add_watch_point(this->pcnt_unit, INT16_MAX); if (error != ESP_OK) { - ESP_LOGE(TAG, "Clearing pulse counter failed: %s", esp_err_to_name(error)); + ESP_LOGE(TAG, "Adding PCNT high limit watch point failed: %s", esp_err_to_name(error)); return false; } - error = pcnt_counter_resume(this->pcnt_unit); + + error = pcnt_unit_enable(this->pcnt_unit); if (error != ESP_OK) { - ESP_LOGE(TAG, "Resuming pulse counter failed: %s", esp_err_to_name(error)); + ESP_LOGE(TAG, "Enabling PCNT unit failed: %s", esp_err_to_name(error)); + return false; + } + error = pcnt_unit_clear_count(this->pcnt_unit); + if (error != ESP_OK) { + ESP_LOGE(TAG, "Clearing PCNT unit failed: %s", esp_err_to_name(error)); + return false; + } + error = pcnt_unit_start(this->pcnt_unit); + if (error != ESP_OK) { + ESP_LOGE(TAG, "Starting PCNT unit failed: %s", esp_err_to_name(error)); return false; } return true; } pulse_counter_t HwPulseCounterStorage::read_raw_value() { - pulse_counter_t counter; - pcnt_get_counter_value(this->pcnt_unit, &counter); - pulse_counter_t ret = counter - this->last_value; - this->last_value = counter; + int count; + pcnt_unit_get_count(this->pcnt_unit, &count); + pulse_counter_t ret = count - this->last_value; + this->last_value = count; return ret; } #endif // HAS_PCNT diff --git a/esphome/components/pulse_counter/pulse_counter_sensor.h b/esphome/components/pulse_counter/pulse_counter_sensor.h index f906e9e5cb3..a7913d5d66b 100644 --- a/esphome/components/pulse_counter/pulse_counter_sensor.h +++ b/esphome/components/pulse_counter/pulse_counter_sensor.h @@ -6,14 +6,13 @@ #include -// TODO: Migrate from legacy PCNT API (driver/pcnt.h) to new PCNT API (driver/pulse_cnt.h) -// The legacy PCNT API is deprecated in ESP-IDF 5.x. Migration would allow removing the -// "driver" IDF component dependency. See: -// https://docs.espressif.com/projects/esp-idf/en/latest/esp32/migration-guides/release-5.x/5.0/peripherals.html#id6 -#if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3) -#include +#if defined(USE_ESP32) +#include +#ifdef SOC_PCNT_SUPPORTED +#include #define HAS_PCNT -#endif // defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3) +#endif // SOC_PCNT_SUPPORTED +#endif // USE_ESP32 namespace esphome { namespace pulse_counter { @@ -24,11 +23,7 @@ enum PulseCounterCountMode { PULSE_COUNTER_DECREMENT, }; -#ifdef HAS_PCNT -using pulse_counter_t = int16_t; -#else // HAS_PCNT using pulse_counter_t = int32_t; -#endif // HAS_PCNT struct PulseCounterStorageBase { virtual bool pulse_counter_setup(InternalGPIOPin *pin) = 0; @@ -58,8 +53,8 @@ struct HwPulseCounterStorage : public PulseCounterStorageBase { bool pulse_counter_setup(InternalGPIOPin *pin) override; pulse_counter_t read_raw_value() override; - pcnt_unit_t pcnt_unit; - pcnt_channel_t pcnt_channel; + pcnt_unit_handle_t pcnt_unit{nullptr}; + pcnt_channel_handle_t pcnt_channel{nullptr}; }; #endif // HAS_PCNT diff --git a/esphome/components/pulse_counter/sensor.py b/esphome/components/pulse_counter/sensor.py index 65be5ee7936..01244635679 100644 --- a/esphome/components/pulse_counter/sensor.py +++ b/esphome/components/pulse_counter/sensor.py @@ -129,10 +129,7 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): use_pcnt = config.get(CONF_USE_PCNT) if CORE.is_esp32 and use_pcnt: - # Re-enable ESP-IDF's legacy driver component (excluded by default to save compile time) - # Provides driver/pcnt.h header for hardware pulse counter API - # TODO: Remove this once pulse_counter migrates to new PCNT API (driver/pulse_cnt.h) - include_builtin_idf_component("driver") + include_builtin_idf_component("esp_driver_pcnt") var = await sensor.new_sensor(config, use_pcnt) await cg.register_component(var, config)