From 39a650ee54c1a4e706a121dcbbfb66c4013b1081 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 10 May 2023 16:37:21 +1200 Subject: [PATCH] Add more configuration for microphones - i2s/pdm/adc (#4775) --- esphome/components/adc/__init__.py | 117 +++++++++++++++++ esphome/components/adc/sensor.py | 120 +----------------- .../i2s_audio/microphone/__init__.py | 59 ++++++++- .../microphone/i2s_audio_microphone.cpp | 50 +++++++- .../microphone/i2s_audio_microphone.h | 17 ++- tests/test4.yaml | 11 +- 6 files changed, 245 insertions(+), 129 deletions(-) diff --git a/esphome/components/adc/__init__.py b/esphome/components/adc/__init__.py index f70ffa9520..cceaa594ef 100644 --- a/esphome/components/adc/__init__.py +++ b/esphome/components/adc/__init__.py @@ -1 +1,118 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.const import CONF_INPUT + +from esphome.core import CORE +from esphome.components.esp32 import get_esp32_variant +from esphome.components.esp32.const import ( + VARIANT_ESP32, + VARIANT_ESP32C3, + VARIANT_ESP32H2, + VARIANT_ESP32S2, + VARIANT_ESP32S3, +) + CODEOWNERS = ["@esphome/core"] + +ATTENUATION_MODES = { + "0db": cg.global_ns.ADC_ATTEN_DB_0, + "2.5db": cg.global_ns.ADC_ATTEN_DB_2_5, + "6db": cg.global_ns.ADC_ATTEN_DB_6, + "11db": cg.global_ns.ADC_ATTEN_DB_11, + "auto": "auto", +} + +adc1_channel_t = cg.global_ns.enum("adc1_channel_t") + +# From https://github.com/espressif/esp-idf/blob/master/components/driver/include/driver/adc_common.h +# pin to adc1 channel mapping +ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = { + VARIANT_ESP32: { + 36: adc1_channel_t.ADC1_CHANNEL_0, + 37: adc1_channel_t.ADC1_CHANNEL_1, + 38: adc1_channel_t.ADC1_CHANNEL_2, + 39: adc1_channel_t.ADC1_CHANNEL_3, + 32: adc1_channel_t.ADC1_CHANNEL_4, + 33: adc1_channel_t.ADC1_CHANNEL_5, + 34: adc1_channel_t.ADC1_CHANNEL_6, + 35: adc1_channel_t.ADC1_CHANNEL_7, + }, + VARIANT_ESP32S2: { + 1: adc1_channel_t.ADC1_CHANNEL_0, + 2: adc1_channel_t.ADC1_CHANNEL_1, + 3: adc1_channel_t.ADC1_CHANNEL_2, + 4: adc1_channel_t.ADC1_CHANNEL_3, + 5: adc1_channel_t.ADC1_CHANNEL_4, + 6: adc1_channel_t.ADC1_CHANNEL_5, + 7: adc1_channel_t.ADC1_CHANNEL_6, + 8: adc1_channel_t.ADC1_CHANNEL_7, + 9: adc1_channel_t.ADC1_CHANNEL_8, + 10: adc1_channel_t.ADC1_CHANNEL_9, + }, + VARIANT_ESP32S3: { + 1: adc1_channel_t.ADC1_CHANNEL_0, + 2: adc1_channel_t.ADC1_CHANNEL_1, + 3: adc1_channel_t.ADC1_CHANNEL_2, + 4: adc1_channel_t.ADC1_CHANNEL_3, + 5: adc1_channel_t.ADC1_CHANNEL_4, + 6: adc1_channel_t.ADC1_CHANNEL_5, + 7: adc1_channel_t.ADC1_CHANNEL_6, + 8: adc1_channel_t.ADC1_CHANNEL_7, + 9: adc1_channel_t.ADC1_CHANNEL_8, + 10: adc1_channel_t.ADC1_CHANNEL_9, + }, + VARIANT_ESP32C3: { + 0: adc1_channel_t.ADC1_CHANNEL_0, + 1: adc1_channel_t.ADC1_CHANNEL_1, + 2: adc1_channel_t.ADC1_CHANNEL_2, + 3: adc1_channel_t.ADC1_CHANNEL_3, + 4: adc1_channel_t.ADC1_CHANNEL_4, + }, + VARIANT_ESP32H2: { + 0: adc1_channel_t.ADC1_CHANNEL_0, + 1: adc1_channel_t.ADC1_CHANNEL_1, + 2: adc1_channel_t.ADC1_CHANNEL_2, + 3: adc1_channel_t.ADC1_CHANNEL_3, + 4: adc1_channel_t.ADC1_CHANNEL_4, + }, +} + + +def validate_adc_pin(value): + if str(value).upper() == "VCC": + return cv.only_on_esp8266("VCC") + + if str(value).upper() == "TEMPERATURE": + return cv.only_on_rp2040("TEMPERATURE") + + if CORE.is_esp32: + value = pins.internal_gpio_input_pin_number(value) + variant = get_esp32_variant() + if variant not in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL: + raise cv.Invalid(f"This ESP32 variant ({variant}) is not supported") + + if value not in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant]: + raise cv.Invalid(f"{variant} doesn't support ADC on this pin") + return pins.internal_gpio_input_pin_schema(value) + + if CORE.is_esp8266: + from esphome.components.esp8266.gpio import CONF_ANALOG + + value = pins.internal_gpio_pin_number({CONF_ANALOG: True, CONF_INPUT: True})( + value + ) + + if value != 17: # A0 + raise cv.Invalid("ESP8266: Only pin A0 (GPIO17) supports ADC.") + return pins.gpio_pin_schema( + {CONF_ANALOG: True, CONF_INPUT: True}, internal=True + )(value) + + if CORE.is_rp2040: + value = pins.internal_gpio_input_pin_number(value) + if value not in (26, 27, 28, 29): + raise cv.Invalid("RP2040: Only pins 26, 27, 28 and 29 support ADC.") + return pins.internal_gpio_input_pin_schema(value) + + raise NotImplementedError diff --git a/esphome/components/adc/sensor.py b/esphome/components/adc/sensor.py index 1a519d7506..4695e96570 100644 --- a/esphome/components/adc/sensor.py +++ b/esphome/components/adc/sensor.py @@ -1,133 +1,27 @@ import esphome.codegen as cg import esphome.config_validation as cv -from esphome import pins from esphome.components import sensor, voltage_sampler +from esphome.components.esp32 import get_esp32_variant from esphome.const import ( CONF_ATTENUATION, - CONF_RAW, CONF_ID, - CONF_INPUT, CONF_NUMBER, CONF_PIN, + CONF_RAW, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT, UNIT_VOLT, ) from esphome.core import CORE -from esphome.components.esp32 import get_esp32_variant -from esphome.components.esp32.const import ( - VARIANT_ESP32, - VARIANT_ESP32C3, - VARIANT_ESP32H2, - VARIANT_ESP32S2, - VARIANT_ESP32S3, + +from . import ( + ATTENUATION_MODES, + ESP32_VARIANT_ADC1_PIN_TO_CHANNEL, + validate_adc_pin, ) - AUTO_LOAD = ["voltage_sampler"] -ATTENUATION_MODES = { - "0db": cg.global_ns.ADC_ATTEN_DB_0, - "2.5db": cg.global_ns.ADC_ATTEN_DB_2_5, - "6db": cg.global_ns.ADC_ATTEN_DB_6, - "11db": cg.global_ns.ADC_ATTEN_DB_11, - "auto": "auto", -} - -adc1_channel_t = cg.global_ns.enum("adc1_channel_t") - -# From https://github.com/espressif/esp-idf/blob/master/components/driver/include/driver/adc_common.h -# pin to adc1 channel mapping -ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = { - VARIANT_ESP32: { - 36: adc1_channel_t.ADC1_CHANNEL_0, - 37: adc1_channel_t.ADC1_CHANNEL_1, - 38: adc1_channel_t.ADC1_CHANNEL_2, - 39: adc1_channel_t.ADC1_CHANNEL_3, - 32: adc1_channel_t.ADC1_CHANNEL_4, - 33: adc1_channel_t.ADC1_CHANNEL_5, - 34: adc1_channel_t.ADC1_CHANNEL_6, - 35: adc1_channel_t.ADC1_CHANNEL_7, - }, - VARIANT_ESP32S2: { - 1: adc1_channel_t.ADC1_CHANNEL_0, - 2: adc1_channel_t.ADC1_CHANNEL_1, - 3: adc1_channel_t.ADC1_CHANNEL_2, - 4: adc1_channel_t.ADC1_CHANNEL_3, - 5: adc1_channel_t.ADC1_CHANNEL_4, - 6: adc1_channel_t.ADC1_CHANNEL_5, - 7: adc1_channel_t.ADC1_CHANNEL_6, - 8: adc1_channel_t.ADC1_CHANNEL_7, - 9: adc1_channel_t.ADC1_CHANNEL_8, - 10: adc1_channel_t.ADC1_CHANNEL_9, - }, - VARIANT_ESP32S3: { - 1: adc1_channel_t.ADC1_CHANNEL_0, - 2: adc1_channel_t.ADC1_CHANNEL_1, - 3: adc1_channel_t.ADC1_CHANNEL_2, - 4: adc1_channel_t.ADC1_CHANNEL_3, - 5: adc1_channel_t.ADC1_CHANNEL_4, - 6: adc1_channel_t.ADC1_CHANNEL_5, - 7: adc1_channel_t.ADC1_CHANNEL_6, - 8: adc1_channel_t.ADC1_CHANNEL_7, - 9: adc1_channel_t.ADC1_CHANNEL_8, - 10: adc1_channel_t.ADC1_CHANNEL_9, - }, - VARIANT_ESP32C3: { - 0: adc1_channel_t.ADC1_CHANNEL_0, - 1: adc1_channel_t.ADC1_CHANNEL_1, - 2: adc1_channel_t.ADC1_CHANNEL_2, - 3: adc1_channel_t.ADC1_CHANNEL_3, - 4: adc1_channel_t.ADC1_CHANNEL_4, - }, - VARIANT_ESP32H2: { - 0: adc1_channel_t.ADC1_CHANNEL_0, - 1: adc1_channel_t.ADC1_CHANNEL_1, - 2: adc1_channel_t.ADC1_CHANNEL_2, - 3: adc1_channel_t.ADC1_CHANNEL_3, - 4: adc1_channel_t.ADC1_CHANNEL_4, - }, -} - - -def validate_adc_pin(value): - if str(value).upper() == "VCC": - return cv.only_on_esp8266("VCC") - - if str(value).upper() == "TEMPERATURE": - return cv.only_on_rp2040("TEMPERATURE") - - if CORE.is_esp32: - value = pins.internal_gpio_input_pin_number(value) - variant = get_esp32_variant() - if variant not in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL: - raise cv.Invalid(f"This ESP32 variant ({variant}) is not supported") - - if value not in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant]: - raise cv.Invalid(f"{variant} doesn't support ADC on this pin") - return pins.internal_gpio_input_pin_schema(value) - - if CORE.is_esp8266: - from esphome.components.esp8266.gpio import CONF_ANALOG - - value = pins.internal_gpio_pin_number({CONF_ANALOG: True, CONF_INPUT: True})( - value - ) - - if value != 17: # A0 - raise cv.Invalid("ESP8266: Only pin A0 (GPIO17) supports ADC.") - return pins.gpio_pin_schema( - {CONF_ANALOG: True, CONF_INPUT: True}, internal=True - )(value) - - if CORE.is_rp2040: - value = pins.internal_gpio_input_pin_number(value) - if value not in (26, 27, 28, 29): - raise cv.Invalid("RP2040: Only pins 26, 27, 28 and 29 support ADC.") - return pins.internal_gpio_input_pin_schema(value) - - raise NotImplementedError - def validate_config(config): if config[CONF_RAW] and config.get(CONF_ATTENUATION, None) == "auto": diff --git a/esphome/components/i2s_audio/microphone/__init__.py b/esphome/components/i2s_audio/microphone/__init__.py index d2c73cf0d0..48d4d28f8e 100644 --- a/esphome/components/i2s_audio/microphone/__init__.py +++ b/esphome/components/i2s_audio/microphone/__init__.py @@ -2,8 +2,9 @@ import esphome.config_validation as cv import esphome.codegen as cg from esphome import pins -from esphome.const import CONF_ID -from esphome.components import microphone +from esphome.const import CONF_ID, CONF_NUMBER +from esphome.components import microphone, esp32 +from esphome.components.adc import ESP32_VARIANT_ADC1_PIN_TO_CHANNEL, validate_adc_pin from .. import ( i2s_audio_ns, @@ -16,18 +17,59 @@ from .. import ( CODEOWNERS = ["@jesserockz"] DEPENDENCIES = ["i2s_audio"] +CONF_ADC_PIN = "adc_pin" +CONF_ADC_TYPE = "adc_type" +CONF_PDM = "pdm" + I2SAudioMicrophone = i2s_audio_ns.class_( "I2SAudioMicrophone", I2SAudioIn, microphone.Microphone, cg.Component ) -CONFIG_SCHEMA = microphone.MICROPHONE_SCHEMA.extend( +INTERNAL_ADC_VARIANTS = [esp32.const.VARIANT_ESP32] +PDM_VARIANTS = [esp32.const.VARIANT_ESP32, esp32.const.VARIANT_ESP32S3] + + +def validate_esp32_variant(config): + variant = esp32.get_esp32_variant() + if config[CONF_ADC_TYPE] == "external": + if config[CONF_PDM]: + if variant not in PDM_VARIANTS: + raise cv.Invalid(f"{variant} does not support PDM") + return config + if config[CONF_ADC_TYPE] == "internal": + if variant not in INTERNAL_ADC_VARIANTS: + raise cv.Invalid(f"{variant} does not have an internal ADC") + return config + raise NotImplementedError + + +BASE_SCHEMA = microphone.MICROPHONE_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(I2SAudioMicrophone), cv.GenerateID(CONF_I2S_AUDIO_ID): cv.use_id(I2SAudioComponent), - cv.Required(CONF_I2S_DIN_PIN): pins.internal_gpio_input_pin_number, } ).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = cv.All( + cv.typed_schema( + { + "internal": BASE_SCHEMA.extend( + { + cv.Required(CONF_ADC_PIN): validate_adc_pin, + } + ), + "external": BASE_SCHEMA.extend( + { + cv.Required(CONF_I2S_DIN_PIN): pins.internal_gpio_input_pin_number, + cv.Required(CONF_PDM): cv.boolean, + } + ), + }, + key=CONF_ADC_TYPE, + ), + validate_esp32_variant, +) + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) @@ -35,6 +77,13 @@ async def to_code(config): await cg.register_parented(var, config[CONF_I2S_AUDIO_ID]) - cg.add(var.set_din_pin(config[CONF_I2S_DIN_PIN])) + if config[CONF_ADC_TYPE] == "internal": + variant = esp32.get_esp32_variant() + pin_num = config[CONF_ADC_PIN][CONF_NUMBER] + channel = ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant][pin_num] + cg.add(var.set_adc_channel(channel)) + else: + cg.add(var.set_din_pin(config[CONF_I2S_DIN_PIN])) + cg.add(var.set_pdm(config[CONF_PDM])) await microphone.register_microphone(var, config) diff --git a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp index 8883cdc665..2b38853528 100644 --- a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp +++ b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp @@ -17,15 +17,36 @@ static const char *const TAG = "i2s_audio.microphone"; void I2SAudioMicrophone::setup() { ESP_LOGCONFIG(TAG, "Setting up I2S Audio Microphone..."); this->buffer_.resize(BUFFER_SIZE); + +#if SOC_I2S_SUPPORTS_ADC + if (this->adc_) { + if (this->parent_->get_port() != I2S_NUM_0) { + ESP_LOGE(TAG, "Internal ADC only works on I2S0!"); + this->mark_failed(); + return; + } + } else +#endif + if (this->pdm_) { + if (this->parent_->get_port() != I2S_NUM_0) { + ESP_LOGE(TAG, "PDM only works on I2S0!"); + this->mark_failed(); + return; + } + } } -void I2SAudioMicrophone::start() { this->state_ = microphone::STATE_STARTING; } +void I2SAudioMicrophone::start() { + if (this->is_failed()) + return; + this->state_ = microphone::STATE_STARTING; +} void I2SAudioMicrophone::start_() { if (!this->parent_->try_lock()) { return; // Waiting for another i2s to return lock } i2s_driver_config_t config = { - .mode = (i2s_mode_t) (I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_PDM), + .mode = (i2s_mode_t) (I2S_MODE_MASTER | I2S_MODE_RX), .sample_rate = 16000, .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, .channel_format = I2S_CHANNEL_FMT_ONLY_RIGHT, @@ -40,18 +61,33 @@ void I2SAudioMicrophone::start_() { .bits_per_chan = I2S_BITS_PER_CHAN_DEFAULT, }; - i2s_driver_install(this->parent_->get_port(), &config, 0, nullptr); +#if SOC_I2S_SUPPORTS_ADC + if (this->adc_) { + config.mode = (i2s_mode_t) (config.mode | I2S_MODE_ADC_BUILT_IN); + i2s_driver_install(this->parent_->get_port(), &config, 0, nullptr); - i2s_pin_config_t pin_config = this->parent_->get_pin_config(); - pin_config.data_in_num = this->din_pin_; + i2s_set_adc_mode(ADC_UNIT_1, this->adc_channel_); + i2s_adc_enable(this->parent_->get_port()); + } else { +#endif + if (this->pdm_) + config.mode = (i2s_mode_t) (config.mode | I2S_MODE_PDM); - i2s_set_pin(this->parent_->get_port(), &pin_config); + i2s_driver_install(this->parent_->get_port(), &config, 0, nullptr); + + i2s_pin_config_t pin_config = this->parent_->get_pin_config(); + pin_config.data_in_num = this->din_pin_; + + i2s_set_pin(this->parent_->get_port(), &pin_config); +#if SOC_I2S_SUPPORTS_ADC + } +#endif this->state_ = microphone::STATE_RUNNING; this->high_freq_.start(); } void I2SAudioMicrophone::stop() { - if (this->state_ == microphone::STATE_STOPPED) + if (this->state_ == microphone::STATE_STOPPED || this->is_failed()) return; this->state_ = microphone::STATE_STOPPING; } diff --git a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h index a36cb7340c..e704ed2915 100644 --- a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h +++ b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h @@ -18,14 +18,27 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub void loop() override; - void set_din_pin(uint8_t pin) { this->din_pin_ = pin; } + void set_din_pin(int8_t pin) { this->din_pin_ = pin; } + void set_pdm(bool pdm) { this->pdm_ = pdm; } + +#if SOC_I2S_SUPPORTS_ADC + void set_adc_channel(adc1_channel_t channel) { + this->adc_channel_ = channel; + this->adc_ = true; + } +#endif protected: void start_(); void stop_(); void read_(); - uint8_t din_pin_{0}; + int8_t din_pin_{I2S_PIN_NO_CHANGE}; +#if SOC_I2S_SUPPORTS_ADC + adc1_channel_t adc_channel_{ADC1_CHANNEL_MAX}; + bool adc_{false}; +#endif + bool pdm_{false}; std::vector buffer_; HighFrequencyLoopRequester high_freq_; diff --git a/tests/test4.yaml b/tests/test4.yaml index d253db8f70..c1d49a4349 100644 --- a/tests/test4.yaml +++ b/tests/test4.yaml @@ -700,8 +700,15 @@ prometheus: microphone: - platform: i2s_audio - id: mic_id + id: mic_id_adc + adc_pin: GPIO35 + adc_type: internal + + - platform: i2s_audio + id: mic_id_external i2s_din_pin: GPIO23 + adc_type: external + pdm: false speaker: - platform: i2s_audio @@ -712,7 +719,7 @@ speaker: voice_assistant: - microphone: mic_id + microphone: mic_id_external on_start: - logger.log: "Voice assistant started" on_stt_end: