From 237dad7c583fcb24d8bd9973d9a935411512cf00 Mon Sep 17 00:00:00 2001 From: Tomasz Duda Date: Wed, 10 Jan 2024 13:34:37 +0100 Subject: [PATCH] init commit for nrf52 --- esphome/__main__.py | 38 ++++++++- esphome/components/logger/__init__.py | 9 ++- esphome/components/logger/logger.cpp | 22 +++-- esphome/components/logger/logger.h | 11 ++- esphome/components/nrf52/gpio.cpp | 112 ++++++++++++++++++++++++++ esphome/components/nrf52/gpio.h | 38 +++++++++ esphome/components/nrf52/gpio.py | 58 +++++++++++++ esphome/config_validation.py | 4 + esphome/const.py | 2 + esphome/core/__init__.py | 5 ++ esphome/core/helpers.h | 4 +- 11 files changed, 289 insertions(+), 14 deletions(-) create mode 100644 esphome/components/nrf52/gpio.cpp create mode 100644 esphome/components/nrf52/gpio.h create mode 100644 esphome/components/nrf52/gpio.py diff --git a/esphome/__main__.py b/esphome/__main__.py index baa5ecde47..97460e289a 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -35,6 +35,7 @@ from esphome.const import ( PLATFORM_ESP8266, PLATFORM_RP2040, SECRETS_FILES, + PLATFORM_NRF52, ) from esphome.core import CORE, EsphomeError, coroutine from esphome.helpers import indent, is_ip_address @@ -297,6 +298,38 @@ def upload_using_platformio(config, port): return platformio_api.run_platformio_cli_run(config, CORE.verbose, *upload_args) +def update_progress(progress=0, done=False, log_message=""): + import click + + del done, log_message # Unused parameters + if progress == 0: + return + + if progress % 40 == 0: + click.echo("#", nl=True) + else: + click.echo("#", nl=False) + + +def upload_adafruit_nrfutil(config, port): + from esphome import platformio_api + from pathlib import Path + from nordicsemi.dfu.dfu_transport_serial import DfuTransportSerial + from nordicsemi.dfu.dfu_transport import DfuEvent + from nordicsemi.dfu.dfu import Dfu + + idedata = platformio_api.get_idedata(config) + dfu_package = str(Path(idedata.firmware_elf_path).with_suffix(".zip")) + serial_backend = DfuTransportSerial(port) + serial_backend.register_events_callback(DfuEvent.PROGRESS_EVENT, update_progress) + dfu = Dfu(dfu_package, dfu_transport=serial_backend) + + try: + dfu.dfu_send_images() + except Exception as e: + raise EsphomeError(f"Unable to send image: {e}") + + def upload_program(config, args, host): if get_port_type(host) == "SERIAL": if CORE.target_platform in (PLATFORM_ESP32, PLATFORM_ESP8266): @@ -309,7 +342,10 @@ def upload_program(config, args, host): if CORE.target_platform in (PLATFORM_BK72XX, PLATFORM_RTL87XX): return upload_using_platformio(config, host) - return 1 # Unknown target platform + if CORE.target_platform in (PLATFORM_NRF52): + return upload_adafruit_nrfutil(config, host) + + raise EsphomeError(f"Unknown target platform: {CORE.target_platform}") if CONF_OTA not in config: raise EsphomeError( diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index fd64c65c77..c9249bc892 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -22,6 +22,7 @@ from esphome.const import ( PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040, + PLATFORM_NRF52, ) from esphome.core import CORE, EsphomeError, Lambda, coroutine_with_priority from esphome.components.esp32 import add_idf_sdkconfig_option, get_esp32_variant @@ -101,6 +102,8 @@ ESP_ARDUINO_UNSUPPORTED_USB_UARTS = [USB_SERIAL_JTAG] UART_SELECTION_RP2040 = [USB_CDC, UART0, UART1] +UART_SELECTION_NRF52 = [USB_CDC] + HARDWARE_UART_TO_UART_SELECTION = { UART0: logger_ns.UART_SELECTION_UART0, UART0_SWAP: logger_ns.UART_SELECTION_UART0_SWAP, @@ -140,6 +143,8 @@ def uart_selection(value): component = get_libretiny_component() if component in UART_SELECTION_LIBRETINY: return cv.one_of(*UART_SELECTION_LIBRETINY[component], upper=True)(value) + if CORE.is_nrf52: + return cv.one_of(*UART_SELECTION_NRF52, upper=True)(value) raise NotImplementedError @@ -179,6 +184,7 @@ CONFIG_SCHEMA = cv.All( rp2040=USB_CDC, bk72xx=DEFAULT, rtl87xx=DEFAULT, + nrf52=USB_CDC, ): cv.All( cv.only_on( [ @@ -187,6 +193,7 @@ CONFIG_SCHEMA = cv.All( PLATFORM_RP2040, PLATFORM_BK72XX, PLATFORM_RTL87XX, + PLATFORM_NRF52, ] ), uart_selection, @@ -263,7 +270,7 @@ async def to_code(config): if config.get(CONF_ESP8266_STORE_LOG_STRINGS_IN_FLASH): cg.add_build_flag("-DUSE_STORE_LOG_STR_IN_FLASH") - if CORE.using_arduino: + if CORE.using_arduino and not CORE.is_nrf52: if config[CONF_HARDWARE_UART] == USB_CDC: cg.add_build_flag("-DARDUINO_USB_CDC_ON_BOOT=1") if CORE.is_esp32 and get_esp32_variant() == VARIANT_ESP32C3: diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index d5f5c275eb..66b7b669d1 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -26,6 +26,10 @@ #include "esphome/core/hal.h" #include "esphome/core/log.h" +#ifdef USE_NRF52 +#include // for Serial +#endif + namespace esphome { namespace logger { @@ -228,10 +232,11 @@ void Logger::pre_setup() { if (this->baud_rate_ > 0) { #ifdef USE_ARDUINO switch (this->uart_) { +#ifndef USE_NRF52 case UART_SELECTION_UART0: #ifdef USE_ESP8266 case UART_SELECTION_UART0_SWAP: -#endif +#endif // USE_ESP8266 #ifdef USE_RP2040 this->hw_serial_ = &Serial1; Serial1.begin(this->baud_rate_); @@ -242,14 +247,15 @@ void Logger::pre_setup() { #else this->hw_serial_ = &Serial; Serial.begin(this->baud_rate_); -#endif -#endif +#endif // ARDUINO_USB_CDC_ON_BOOT +#endif // USE_RP2040 +#endif // USE_NRF52 #ifdef USE_ESP8266 if (this->uart_ == UART_SELECTION_UART0_SWAP) { Serial.swap(); } Serial.setDebugOutput(ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE); -#endif +#endif // USE_ESP8266 break; case UART_SELECTION_UART1: #ifdef USE_RP2040 @@ -258,10 +264,10 @@ void Logger::pre_setup() { #else this->hw_serial_ = &Serial1; Serial1.begin(this->baud_rate_); -#endif +#endif // USE_RP2040 #ifdef USE_ESP8266 Serial1.setDebugOutput(ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE); -#endif +#endif // USE_ESP8266 break; #if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32C6) && \ !defined(USE_ESP32_VARIANT_ESP32S2) && !defined(USE_ESP32_VARIANT_ESP32S3) @@ -290,7 +296,7 @@ void Logger::pre_setup() { #endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32C3 break; #endif // USE_ESP32 && (USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32C3) -#ifdef USE_RP2040 +#if defined(USE_RP2040) || defined(USE_NRF52) case UART_SELECTION_USB_CDC: this->hw_serial_ = &Serial; Serial.begin(this->baud_rate_); @@ -401,7 +407,7 @@ void Logger::set_log_level(const std::string &tag, int log_level) { this->log_levels_.push_back(LogLevelOverride{tag, log_level}); } -#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) +#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_NRF52) UARTSelection Logger::get_uart() const { return this->uart_; } #endif diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index c7f0fe4139..5123a31add 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -25,7 +25,7 @@ namespace esphome { namespace logger { -#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) +#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_NRF52) /** Enum for logging UART selection * * Advanced configuration (pin selection, etc) is not supported. @@ -37,7 +37,9 @@ enum UARTSelection { UART_SELECTION_UART1, UART_SELECTION_UART2, #else +#ifndef USE_NRF52 UART_SELECTION_UART0 = 0, +#endif UART_SELECTION_UART1, #if defined(USE_ESP32) #if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32C6) && \ @@ -58,7 +60,7 @@ enum UARTSelection { #ifdef USE_ESP8266 UART_SELECTION_UART0_SWAP, #endif // USE_ESP8266 -#ifdef USE_RP2040 +#if defined(USE_RP2040) || defined(USE_NRF52) UART_SELECTION_USB_CDC, #endif // USE_RP2040 #endif // USE_LIBRETINY @@ -78,7 +80,7 @@ class Logger : public Component { #ifdef USE_ESP_IDF uart_port_t get_uart_num() const { return uart_num_; } #endif -#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) +#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_NRF52) void set_uart_selection(UARTSelection uart_selection) { uart_ = uart_selection; } /// Get the UART used by the logger. UARTSelection get_uart() const; @@ -165,6 +167,9 @@ class Logger : public Component { #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) UARTSelection uart_{UART_SELECTION_UART0}; #endif +#ifdef USE_NRF52 + UARTSelection uart_{UART_SELECTION_USB_CDC}; +#endif #ifdef USE_LIBRETINY UARTSelection uart_{UART_SELECTION_DEFAULT}; #endif diff --git a/esphome/components/nrf52/gpio.cpp b/esphome/components/nrf52/gpio.cpp new file mode 100644 index 0000000000..3af7234e36 --- /dev/null +++ b/esphome/components/nrf52/gpio.cpp @@ -0,0 +1,112 @@ +#ifdef USE_NRF52 + +#include "gpio.h" +#include "esphome/core/log.h" +#include +#include + +namespace esphome { +namespace nrf52 { + +static const char *const TAG = "nrf52"; + +static int IRAM_ATTR flags_to_mode(gpio::Flags flags, uint8_t pin) { +// For nRF52 extra modes are available. +// Standard drive is typically 2mA (min 1mA) '0' sink (low) or '1' source (high). High drive (VDD > 2.7V) is typically 10mA low, 9mA high (min 6mA) +// OUTPUT_S0S1 Standard '0', standard '1' same as OUTPUT +// OUTPUT_H0S1 High drive '0', standard '1' +// OUTPUT_S0H1 Standard '0', high drive '1' +// OUTPUT_H0H1 High drive '0', high 'drive '1'' +// OUTPUT_D0S1 Disconnect '0' standard '1' (normally used for wired-or connections) +// OUTPUT_D0H1 Disconnect '0', high drive '1' (normally used for wired-or connections) +// OUTPUT_S0D1 Standard '0'. disconnect '1' (normally used for wired-and connections) +// OUTPUT_H0D1 High drive '0', disconnect '1' (normally used for wired-and connections) +// NOTE P0.27 should be only low (standard) drive, low frequency + if (flags == gpio::FLAG_INPUT) { // NOLINT(bugprone-branch-clone) + return INPUT; + } else if (flags == gpio::FLAG_OUTPUT) { + return OUTPUT; + } else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_PULLUP)) { + return INPUT_PULLUP; + } else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_PULLDOWN)) { + return INPUT_PULLDOWN; + } else if (flags == (gpio::FLAG_OUTPUT | gpio::FLAG_OPEN_DRAIN)) { + return OUTPUT_S0D1; + } else { + return INPUT; + } +} + +struct ISRPinArg { + uint8_t pin; + bool inverted; +}; + +//TODO implement +//TODO test +void (*irq_cb)(void *); +void* irq_arg; +static void pin_irq(void){ + irq_cb(irq_arg); +} + +ISRInternalGPIOPin NRF52GPIOPin::to_isr() const { + auto *arg = new ISRPinArg{}; // NOLINT(cppcoreguidelines-owning-memory) + arg->pin = pin_; + arg->inverted = inverted_; + return ISRInternalGPIOPin((void *) arg); +} + +void NRF52GPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const { + uint32_t mode = ISR_DEFERRED; + switch (type) { + case gpio::INTERRUPT_RISING_EDGE: + mode |= inverted_ ? FALLING : RISING; + break; + case gpio::INTERRUPT_FALLING_EDGE: + mode |= inverted_ ? RISING : FALLING; + break; + case gpio::INTERRUPT_ANY_EDGE: + mode |= CHANGE; + break; + default: + return; + } + + irq_cb = func; + irq_arg = arg; + attachInterrupt(pin_, pin_irq, mode); +} +void NRF52GPIOPin::pin_mode(gpio::Flags flags) { + pinMode(pin_, flags_to_mode(flags, pin_)); // NOLINT +} + +std::string NRF52GPIOPin::dump_summary() const { + char buffer[32]; + snprintf(buffer, sizeof(buffer), "GPIO%u", pin_); + return buffer; +} + +bool NRF52GPIOPin::digital_read() { + return bool(digitalRead(pin_)) != inverted_; // NOLINT +} +void NRF52GPIOPin::digital_write(bool value) { + digitalWrite(pin_, value != inverted_ ? 1 : 0); // NOLINT +} +void NRF52GPIOPin::detach_interrupt() const { + detachInterrupt(pin_); +} + +} // namespace nrf52 + +// using namespace nrf52; + +// TODO seems to not work??? +bool IRAM_ATTR ISRInternalGPIOPin::digital_read() { + auto *arg = reinterpret_cast(arg_); + return bool(digitalRead(arg->pin)) != arg->inverted; // NOLINT +} + +} // namespace esphome + +#endif // USE_NRF52 diff --git a/esphome/components/nrf52/gpio.h b/esphome/components/nrf52/gpio.h new file mode 100644 index 0000000000..363883b130 --- /dev/null +++ b/esphome/components/nrf52/gpio.h @@ -0,0 +1,38 @@ +#pragma once + +#ifdef USE_NRF52 + +#include +#include "esphome/core/hal.h" + +namespace esphome { +namespace nrf52 { + +class NRF52GPIOPin : public InternalGPIOPin { + public: + void set_pin(uint8_t pin) { pin_ = pin; } + void set_inverted(bool inverted) { inverted_ = inverted; } + void set_flags(gpio::Flags flags) { flags_ = flags; } + + void setup() override { pin_mode(flags_); } + void pin_mode(gpio::Flags flags) override; + bool digital_read() override; + void digital_write(bool value) override; + std::string dump_summary() const override; + void detach_interrupt() const override; + ISRInternalGPIOPin to_isr() const override; + uint8_t get_pin() const override { return pin_; } + bool is_inverted() const override { return inverted_; } + + protected: + void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override; + + uint8_t pin_; + bool inverted_; + gpio::Flags flags_; +}; + +} // namespace nrf52 +} // namespace esphome + +#endif // USE_NRF52 diff --git a/esphome/components/nrf52/gpio.py b/esphome/components/nrf52/gpio.py new file mode 100644 index 0000000000..5270ab6adb --- /dev/null +++ b/esphome/components/nrf52/gpio.py @@ -0,0 +1,58 @@ +from esphome import pins + +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import ( + CONF_ID, + CONF_MODE, + CONF_INVERTED, + CONF_NUMBER, +) + +nrf52_ns = cg.esphome_ns.namespace("nrf52") +NRF52GPIOPin = nrf52_ns.class_("NRF52GPIOPin", cg.InternalGPIOPin) + + +def _translate_pin(value): + if isinstance(value, dict) or value is None: + raise cv.Invalid( + "This variable only supports pin numbers, not full pin schemas " + "(with inverted and mode)." + ) + if isinstance(value, int): + return value + try: + return int(value) + except ValueError: + pass + # e.g. P0.27 + if len(value) >= len("P0.0") and value[0] == "P" and value[2] == ".": + return cv.int_(value[len("P")].strip()) * 32 + cv.int_( + value[len("P0.") :].strip() + ) + raise cv.Invalid(f"Invalid pin: {value}") + + +def validate_gpio_pin(value): + value = _translate_pin(value) + if value < 0 or value > (32 + 16): + raise cv.Invalid(f"NRF52: Invalid pin number: {value}") + return value + + +NRF52_PIN_SCHEMA = cv.All( + pins.gpio_base_schema( + NRF52GPIOPin, + validate_gpio_pin, + ), +) + + +@pins.PIN_SCHEMA_REGISTRY.register("nrf52", NRF52_PIN_SCHEMA) +async def nrf52_pin_to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + num = config[CONF_NUMBER] + cg.add(var.set_pin(num)) + cg.add(var.set_inverted(config[CONF_INVERTED])) + cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) + return var diff --git a/esphome/config_validation.py b/esphome/config_validation.py index fa1170fb93..95088cba4e 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -1548,6 +1548,7 @@ class SplitDefault(Optional): bk72xx=vol.UNDEFINED, rtl87xx=vol.UNDEFINED, host=vol.UNDEFINED, + nrf52=vol.UNDEFINED, ): super().__init__(key) self._esp8266_default = vol.default_factory(esp8266) @@ -1579,6 +1580,7 @@ class SplitDefault(Optional): self._bk72xx_default = vol.default_factory(bk72xx) self._rtl87xx_default = vol.default_factory(rtl87xx) self._host_default = vol.default_factory(host) + self._nrf52 = vol.default_factory(nrf52) @property def default(self): @@ -1621,6 +1623,8 @@ class SplitDefault(Optional): return self._rtl87xx_default if CORE.is_host: return self._host_default + if CORE.is_nrf52: + return self._nrf52 raise NotImplementedError @default.setter diff --git a/esphome/const.py b/esphome/const.py index 8f9606c5fd..b962575aaf 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -14,6 +14,7 @@ PLATFORM_HOST = "host" PLATFORM_BK72XX = "bk72xx" PLATFORM_RTL87XX = "rtl87xx" PLATFORM_LIBRETINY_OLDSTYLE = "libretiny" +PLATFORM_NRF52 = "nrf52" TARGET_PLATFORMS = [ PLATFORM_ESP32, @@ -23,6 +24,7 @@ TARGET_PLATFORMS = [ PLATFORM_BK72XX, PLATFORM_RTL87XX, PLATFORM_LIBRETINY_OLDSTYLE, + PLATFORM_NRF52, ] SOURCE_FILE_EXTENSIONS = {".cpp", ".hpp", ".h", ".c", ".tcc", ".ino"} diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index 58ae23e139..f10f4f147b 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -21,6 +21,7 @@ from esphome.const import ( PLATFORM_RTL87XX, PLATFORM_RP2040, PLATFORM_HOST, + PLATFORM_NRF52, ) from esphome.coroutine import FakeAwaitable as _FakeAwaitable from esphome.coroutine import FakeEventLoop as _FakeEventLoop @@ -659,6 +660,10 @@ class EsphomeCore: def is_host(self): return self.target_platform == PLATFORM_HOST + @property + def is_nrf52(self): + return self.target_platform == PLATFORM_NRF52 + @property def target_framework(self): return self.data[KEY_CORE][KEY_TARGET_FRAMEWORK] diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index c3ed443bf0..d67a6cfcb2 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -20,6 +20,8 @@ #elif defined(USE_LIBRETINY) #include #include +#elif defined(USE_NRF52) +#include #endif #define HOT __attribute__((hot)) @@ -546,7 +548,7 @@ class Mutex { Mutex &operator=(const Mutex &) = delete; private: -#if defined(USE_ESP32) || defined(USE_LIBRETINY) +#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_NRF52) SemaphoreHandle_t handle_; #endif };