From bfd1e1a2316eaccf4549f720f5bf145cb79390ca Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sun, 23 Aug 2026 11:00:37 +0000 Subject: [PATCH] Add Heltec RC32, RC52 and RCC6 boards, and LC760CA GNSS support (#11572) * refactor(graphics): select Arduino_GFX panels with a capability flag TFTDisplay tested `defined(HACKADAY_COMMUNICATOR)` in a dozen places to mean "this panel is driven by Arduino_GFX rather than LovyanGFX". Every new Arduino_GFX board had to be appended to all of them. Move the decision into the variant as USE_ARDUINO_GFX so the display code stops naming individual boards. No behaviour change: the Hackaday Communicator is still the only board that sets it. * feat(boards): add Heltec RC32, RC52 and RCC6 Three boards around the same 128x220 NV3001B panel: RC32 (ESP32-S3), RCC6 (ESP32-C6) and RC52 (nRF52840). They differ only in how the panel bus is wired, so they share one branch in TFTDisplay behind TFT_NV3001B. RC32 and RC52 also carry a rotary encoder on a TCA6408 I2C expander. That lands as its own input source rather than as board conditionals inside i2cButton, which is the M5Stack UnitC6L button driver and stays untouched. On RC52 and RCC6 the panel is an add-on module, so probe it before reporting a screen. The probe reuses the bit-banged SPI helper that already backs the T114 ST7789 check. Arduino_GFX is pinned to the upstream commit that added the NV3001B driver; it has not shipped in a tagged release yet. Co-Authored-By: Quency-D <55523105+Quency-D@users.noreply.github.com> * feat(gps): detect and configure the LC760CA GNSS module The LC760CA is another Unicore part, so it joins the $PDTINFO probe family and reuses the CM121 message-rate setup. It answers with CC1161W. GNSS_MODEL_LC760CA goes immediately before GNSS_MODEL_GENERIC_NMEA: the sentinel has to stay last because isValidGnssModel() uses it as the exclusive upper bound on values the probe cache may hold. Placing the new model after it would leave LC760CA permanently uncacheable. Co-Authored-By: Quency-D <55523105+Quency-D@users.noreply.github.com> * fix(graphics): re-init the NV3001B after the panel rail comes back DISPLAYOFF de-asserts VTFT_CTRL, which cuts power to the panel, so the controller loses MADCTL, COLMOD and gamma. displayOn() only sends sleep-out and cannot restore them, leaving the panel dark or in the wrong format after wake. Re-run begin() once the rail has settled, and repaint in full since the re-init leaves display RAM undefined. Also stop the TCA6408 rotary polling from two threads at once. Registering as an InputPollable meant InputBroker's pollSoon task could call pollOnce() while runOnce() was mid-transfer on the main thread, with nothing serialising Wire or the decoder state. Drop InputPollable and have the interrupt wake the thread instead, the way ButtonThread does, so the bus and the decode stay on one thread. * fix(graphics): skip the NV3001B wake when re-init fails begin() reports whether the bus came up. Ignoring it meant a failed re-init still lit the backlight and drove a full-screen repaint at a panel that was never initialised. * chore(boards): ship the Heltec RC boards at release level release is the normal level for a variant; the matrix generator still builds each of these in this PR because they add a new platformio.ini. --------- Co-authored-by: Quency-D <55523105+Quency-D@users.noreply.github.com> --- boards/heltec_rc32.json | 43 +++++ boards/heltec_rc52.json | 54 ++++++ boards/heltec_rcc6.json | 29 ++++ src/gps/GPS.cpp | 10 +- src/gps/GPS.h | 3 + src/graphics/TFTDisplay.cpp | 62 +++++-- src/input/InputBroker.cpp | 8 + src/input/TCA6408Rotary.cpp | 156 ++++++++++++++++++ src/input/TCA6408Rotary.h | 40 +++++ src/mesh/NodeDB.cpp | 63 ++++++- src/platform/esp32/architecture.h | 4 + src/platform/nrf52/architecture.h | 2 + variants/esp32c6/heltec_rcc6/pins_arduino.h | 20 +++ variants/esp32c6/heltec_rcc6/platformio.ini | 35 ++++ variants/esp32c6/heltec_rcc6/variant.h | 59 +++++++ .../esp32s3/hackaday-communicator/variant.h | 1 + variants/esp32s3/heltec_rc32/pins_arduino.h | 60 +++++++ variants/esp32s3/heltec_rc32/platformio.ini | 28 ++++ variants/esp32s3/heltec_rc32/sdkconfig.h | 24 +++ variants/esp32s3/heltec_rc32/variant.h | 78 +++++++++ variants/nrf52840/heltec_rc52/platformio.ini | 30 ++++ variants/nrf52840/heltec_rc52/variant.cpp | 103 ++++++++++++ variants/nrf52840/heltec_rc52/variant.h | 136 +++++++++++++++ 23 files changed, 1031 insertions(+), 17 deletions(-) create mode 100644 boards/heltec_rc32.json create mode 100644 boards/heltec_rc52.json create mode 100644 boards/heltec_rcc6.json create mode 100644 src/input/TCA6408Rotary.cpp create mode 100644 src/input/TCA6408Rotary.h create mode 100644 variants/esp32c6/heltec_rcc6/pins_arduino.h create mode 100644 variants/esp32c6/heltec_rcc6/platformio.ini create mode 100644 variants/esp32c6/heltec_rcc6/variant.h create mode 100644 variants/esp32s3/heltec_rc32/pins_arduino.h create mode 100644 variants/esp32s3/heltec_rc32/platformio.ini create mode 100644 variants/esp32s3/heltec_rc32/sdkconfig.h create mode 100644 variants/esp32s3/heltec_rc32/variant.h create mode 100644 variants/nrf52840/heltec_rc52/platformio.ini create mode 100644 variants/nrf52840/heltec_rc52/variant.cpp create mode 100644 variants/nrf52840/heltec_rc52/variant.h diff --git a/boards/heltec_rc32.json b/boards/heltec_rc32.json new file mode 100644 index 000000000..b9bafa265 --- /dev/null +++ b/boards/heltec_rc32.json @@ -0,0 +1,43 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld", + "partitions": "default_16MB.csv", + "memory_type": "qio_opi" + }, + "core": "esp32", + "extra_flags": [ + "-DBOARD_HAS_PSRAM", + "-DARDUINO_USB_CDC_ON_BOOT=1", + "-DARDUINO_USB_MODE=1", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=1" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "psram_type": "opi", + "hwids": [["0x303A", "0x1001"]], + "mcu": "esp32s3", + "variant": "heltec_rc32" + }, + "connectivity": ["wifi", "bluetooth", "lora"], + "debug": { + "default_tool": "esp-builtin", + "onboard_tools": ["esp-builtin"], + "openocd_target": "esp32s3.cfg" + }, + "frameworks": ["arduino", "espidf"], + "name": "Heltec RC32 (16 MB FLASH, 8 MB PSRAM)", + "upload": { + "flash_size": "16MB", + "maximum_ram_size": 327680, + "maximum_size": 16777216, + "use_1200bps_touch": true, + "wait_for_upload_port": true, + "require_upload_port": true, + "speed": 921600 + }, + "url": "https://heltec.org/", + "vendor": "Heltec" +} diff --git a/boards/heltec_rc52.json b/boards/heltec_rc52.json new file mode 100644 index 000000000..2f87529ca --- /dev/null +++ b/boards/heltec_rc52.json @@ -0,0 +1,54 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v6.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [ + ["0x239A", "0x4405"], + ["0x239A", "0x0029"], + ["0x239A", "0x002A"], + ["0x2886", "0x1667"] + ], + "usb_product": "HT-n5262", + "mcu": "nrf52840", + "variant": "heltec_rc52", + "variants_dir": "variants", + "bsp": { + "name": "adafruit" + }, + "softdevice": { + "sd_flags": "-DS140", + "sd_name": "s140", + "sd_version": "6.1.1", + "sd_fwid": "0x00B6" + }, + "bootloader": { + "settings_addr": "0xFF000" + } + }, + "connectivity": ["bluetooth"], + "debug": { + "jlink_device": "nRF52840_xxAA", + "onboard_tools": ["jlink"], + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52840-mdk-rs" + }, + "frameworks": ["arduino"], + "name": "Heltec RC52", + "upload": { + "maximum_ram_size": 248832, + "maximum_size": 815104, + "speed": 115200, + "protocol": "nrfutil", + "protocols": ["jlink", "nrfjprog", "nrfutil", "stlink"], + "use_1200bps_touch": true, + "require_upload_port": true, + "wait_for_upload_port": true + }, + "url": "https://heltec.org/", + "vendor": "Heltec" +} diff --git a/boards/heltec_rcc6.json b/boards/heltec_rcc6.json new file mode 100644 index 000000000..972d2b864 --- /dev/null +++ b/boards/heltec_rcc6.json @@ -0,0 +1,29 @@ +{ + "build": { + "arduino": { + "partitions": "default_16MB.csv" + }, + "core": "esp32", + "f_cpu": "160000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "hwids": [["0x303A", "0x1001"]], + "mcu": "esp32c6", + "variant": "heltec_rcc6" + }, + "connectivity": ["bluetooth", "wifi", "lora"], + "debug": { + "openocd_target": "esp32c6.cfg" + }, + "frameworks": ["arduino", "espidf"], + "name": "Heltec RCC6", + "upload": { + "flash_size": "16MB", + "maximum_ram_size": 327680, + "maximum_size": 16777216, + "require_upload_port": true, + "speed": 921600 + }, + "url": "https://heltec.org/", + "vendor": "Heltec" +} diff --git a/src/gps/GPS.cpp b/src/gps/GPS.cpp index 0bd0f3212..9f29c41f0 100644 --- a/src/gps/GPS.cpp +++ b/src/gps/GPS.cpp @@ -742,6 +742,11 @@ bool GPS::verifyCachedProbePresence() _serial_gps->write("$PDTINFO\r\n"); present = (getACK("CM121", 900) == GNSS_RESPONSE_OK); break; + case GNSS_MODEL_LC760CA: + cachedProbeModelName = "LC760CA"; + _serial_gps->write("$PDTINFO\r\n"); + present = (getACK("CC1161W", 900) == GNSS_RESPONSE_OK); + break; case GNSS_MODEL_UBLOX6: case GNSS_MODEL_UBLOX7: case GNSS_MODEL_UBLOX8: @@ -1115,7 +1120,7 @@ bool GPS::setup() } else { LOG_INFO("GNSS module config saved"); } - } else if (gnssModel == GNSS_MODEL_CM121) { + } else if (IS_ONE_OF(gnssModel, GNSS_MODEL_CM121, GNSS_MODEL_LC760CA)) { // only ask for RMC and GGA // enable GGA _serial_gps->write("$CFGMSG,0,0,1,1*1B\r\n"); @@ -1716,7 +1721,8 @@ GnssModel_t GPS::probe(int serialSpeed) std::vector unicore = {{"UC6580", "UC6580", GNSS_MODEL_UC6580}, {"UM600", "UM600", GNSS_MODEL_UC6580}, {"CM121", "CM121", GNSS_MODEL_CM121}, - {"CC1167Q", "CC1167Q", GNSS_MODEL_CM121}}; + {"CC1167Q", "CC1167Q", GNSS_MODEL_CM121}, + {"LC760CA", "CC1161W", GNSS_MODEL_LC760CA}}; PROBE_FAMILY("Unicore Family", "$PDTINFO", unicore, 500); currentDelay = 20; currentStep = 2; diff --git a/src/gps/GPS.h b/src/gps/GPS.h index a9a82795c..b9bd35717 100644 --- a/src/gps/GPS.h +++ b/src/gps/GPS.h @@ -52,6 +52,9 @@ typedef enum { GNSS_MODEL_AG3352, GNSS_MODEL_LS20031, GNSS_MODEL_CM121, + GNSS_MODEL_LC760CA, + // Keep GNSS_MODEL_GENERIC_NMEA last: isValidGnssModel() uses it as the exclusive upper bound + // for values the probe cache is allowed to hold. GNSS_MODEL_GENERIC_NMEA // generic NMEA source (e.g. gpsd); skips chip-specific probe and init } GnssModel_t; diff --git a/src/graphics/TFTDisplay.cpp b/src/graphics/TFTDisplay.cpp index a3af9e7ff..f5f857797 100644 --- a/src/graphics/TFTDisplay.cpp +++ b/src/graphics/TFTDisplay.cpp @@ -205,7 +205,7 @@ static void rak14014_tpIntHandle(void) _rak14014_touch_int = true; } -#elif defined(HACKADAY_COMMUNICATOR) +#elif defined(USE_ARDUINO_GFX) #include Arduino_GFX *tft = nullptr; @@ -1397,7 +1397,7 @@ void TFTDisplay::display(bool fromBlank) } } } -#if defined(HACKADAY_COMMUNICATOR) +#if defined(USE_ARDUINO_GFX) tft->draw16bitBeRGBBitmap(0, yStart, repaintChunkBuffer, displayWidth, rowsThisChunk); #else tft->pushImage(0, yStart, displayWidth, rowsThisChunk, repaintChunkBuffer); @@ -1564,7 +1564,7 @@ void TFTDisplay::display(bool fromBlank) const uint8_t lines_updated = 1; #endif -#if defined(HACKADAY_COMMUNICATOR) +#if defined(USE_ARDUINO_GFX) tft->draw16bitBeRGBBitmap(x_FirstPixelUpdate, y, &linePixelBuffer[x_FirstPixelUpdate], (x_LastPixelUpdate - x_FirstPixelUpdate + 1), 1); #else @@ -1639,12 +1639,24 @@ void TFTDisplay::sendCommand(uint8_t com) switch (com) { case DISPLAYON: { LOG_DEBUG("Display on"); +#if defined(TFT_NV3001B) + // DISPLAYOFF cuts the panel rail, so the controller loses its configuration and sleep-out + // alone cannot bring it back. Restore the rail, let it settle, then re-run the init sequence. + digitalWrite(VTFT_CTRL, TFT_EN_ON); + delay(10); + if (!tft->begin(SPI_FREQUENCY)) { + // Nothing below this point can reach the panel, so skip the wake instead of lighting + // the backlight and repainting over a bus that did not come up. + LOG_ERROR("NV3001B re-init failed on wake"); + break; + } +#endif backlightEnable->set(true); #if ARCH_PORTDUINO display(true); if (portduino_config.displayBacklight.pin > 0) digitalWrite(portduino_config.displayBacklight.pin, TFT_BACKLIGHT_ON); -#elif defined(HACKADAY_COMMUNICATOR) +#elif defined(USE_ARDUINO_GFX) tft->displayOn(); #elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE) && !defined(HELTEC_MESH_NODE_T096) && \ !defined(HELTEC_MESH_NODE_T1) @@ -1652,7 +1664,13 @@ void TFTDisplay::sendCommand(uint8_t com) tft->powerSaveOff(); #endif -#ifdef VTFT_CTRL +#if defined(TFT_NV3001B) + // Re-init left display RAM undefined, so repaint in full rather than diff against a + // buffer that no longer describes the panel. + display(true); +#endif + +#if defined(VTFT_CTRL) && !defined(TFT_NV3001B) // NV3001B panels already powered the rail above digitalWrite(VTFT_CTRL, LOW); #endif #ifdef UNPHONE @@ -1660,7 +1678,7 @@ void TFTDisplay::sendCommand(uint8_t com) #endif #if defined(RAK14014) || defined(HELTEC_MESH_NODE_T096) || defined(HELTEC_MESH_NODE_T1) #elif !defined(M5STACK) && !defined(ST7789_CS) && \ - !defined(HACKADAY_COMMUNICATOR) // T-Deck gets brightness set in Screen.cpp in the handleSetOn function + !defined(USE_ARDUINO_GFX) // T-Deck gets brightness set in Screen.cpp in the handleSetOn function tft->setBrightness(172); #endif break; @@ -1672,7 +1690,7 @@ void TFTDisplay::sendCommand(uint8_t com) tft->clear(); if (portduino_config.displayBacklight.pin > 0) digitalWrite(portduino_config.displayBacklight.pin, !TFT_BACKLIGHT_ON); -#elif defined(HACKADAY_COMMUNICATOR) +#elif defined(USE_ARDUINO_GFX) tft->displayOff(); #elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE) && !defined(HELTEC_MESH_NODE_T096) && \ !defined(HELTEC_MESH_NODE_T1) @@ -1687,7 +1705,7 @@ void TFTDisplay::sendCommand(uint8_t com) unphone.backlight(false); // using unPhone library #endif #if defined(RAK14014) || defined(HELTEC_MESH_NODE_T096) || defined(HELTEC_MESH_NODE_T1) -#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR) +#elif !defined(M5STACK) && !defined(USE_ARDUINO_GFX) tft->setBrightness(0); #endif break; @@ -1703,7 +1721,7 @@ void TFTDisplay::setDisplayBrightness(uint8_t _brightness) { #if defined(RAK14014) || defined(HELTEC_MESH_NODE_T096) || defined(HELTEC_MESH_NODE_T1) // todo -#elif !defined(HACKADAY_COMMUNICATOR) +#elif !defined(USE_ARDUINO_GFX) tft->setBrightness(_brightness); LOG_DEBUG("Brightness is set to value: %i ", _brightness); #endif @@ -1721,7 +1739,7 @@ bool TFTDisplay::hasTouch(void) { #ifdef RAK14014 return true; -#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR) && !defined(HELTEC_MESH_NODE_T096) && !defined(HELTEC_MESH_NODE_T1) +#elif !defined(M5STACK) && !defined(USE_ARDUINO_GFX) && !defined(HELTEC_MESH_NODE_T096) && !defined(HELTEC_MESH_NODE_T1) return tft->touch() != nullptr; #else return false; @@ -1740,7 +1758,7 @@ bool TFTDisplay::getTouch(int16_t *x, int16_t *y) } else { return false; } -#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR) && !defined(HELTEC_MESH_NODE_T096) && !defined(HELTEC_MESH_NODE_T1) +#elif !defined(M5STACK) && !defined(USE_ARDUINO_GFX) && !defined(HELTEC_MESH_NODE_T096) && !defined(HELTEC_MESH_NODE_T1) return tft->getTouch(x, y); #else return false; @@ -1768,6 +1786,21 @@ bool TFTDisplay::connect() tft = new Arduino_NV3007(bus, 40, 0 /* rotation */, false /* IPS */, 142 /* width */, 428 /* height */, 12 /* col offset 1 */, 0 /* row offset 1 */, 14 /* col offset 2 */, 0 /* row offset 2 */, nv3007_279_init_operations, sizeof(nv3007_279_init_operations)); +#elif defined(TFT_NV3001B) + // The Heltec RC panels all use the same controller and differ only in how the bus is wired. +#if defined(HELTEC_RC52) + // nRF52840: the panel sits on SPI1, clear of the LoRa radio on SPI0. + Arduino_DataBus *bus = new Arduino_HWSPI(TFT_RS, TFT_CS, &SPI1, true /* is_shared_interface */); +#elif defined(HELTEC_RCC6) + // ESP32-C6: the panel shares pins with the LoRa host, so bit-bang it rather than claim the peripheral. + Arduino_DataBus *bus = new Arduino_SWSPI(TFT_RS, TFT_CS, TFT_SCL, TFT_SDA, GFX_NOT_DEFINED /* MISO */); +#else + // ESP32-S3: keep the panel off the LoRa FSPI host, since Arduino_GFX reconfigures whichever bus it is handed. + Arduino_DataBus *bus = + new Arduino_ESP32SPI(TFT_RS, TFT_CS, TFT_SCL, TFT_SDA, GFX_NOT_DEFINED /* MISO */, HSPI /* spi_num */); +#endif + tft = new Arduino_NV3001B(bus, TFT_RST, 3 /* rotation */, true /* IPS */, TFT_WIDTH, TFT_HEIGHT, 0 /* col offset 1 */, + 0 /* row offset 1 */, 0 /* col offset 2 */, 0 /* row offset 2 */); #else tft = new LGFX; #endif @@ -1779,8 +1812,13 @@ bool TFTDisplay::connect() #ifdef UNPHONE unphone.backlight(true); // using unPhone library #endif -#ifdef HACKADAY_COMMUNICATOR +#ifdef USE_ARDUINO_GFX +#if defined(TFT_NV3001B) + // Arduino_SWSPI ignores the clock argument, so this only bites on the hardware-SPI variants. + bool beginStatus = tft->begin(SPI_FREQUENCY); +#else bool beginStatus = tft->begin(); +#endif if (beginStatus) LOG_DEBUG("TFT Success"); else diff --git a/src/input/InputBroker.cpp b/src/input/InputBroker.cpp index 7b0f830c5..8e247bfcc 100644 --- a/src/input/InputBroker.cpp +++ b/src/input/InputBroker.cpp @@ -20,6 +20,7 @@ #include "input/RotaryEncoderImpl.h" #include "input/RotaryEncoderInterruptImpl1.h" #include "input/SerialKeyboardImpl.h" +#include "input/TCA6408Rotary.h" #include "input/UpDownInterruptImpl1.h" #include "input/i2cButton.h" #if HAS_TRACKBALL @@ -468,6 +469,13 @@ void InputBroker::Init() #if defined(M5STACK_UNITC6L) i2cButton = new i2cButtonThread("i2cButtonThread"); #endif +#if defined(HAS_TCA6408_ROTARY) + tca6408Rotary = new TCA6408Rotary("TCA6408Rotary"); + if (!tca6408Rotary->init()) { + delete tca6408Rotary; + tca6408Rotary = nullptr; + } +#endif #ifdef INPUTBROKER_MATRIX_TYPE kbMatrixImpl = new KbMatrixImpl(); kbMatrixImpl->init(); diff --git a/src/input/TCA6408Rotary.cpp b/src/input/TCA6408Rotary.cpp new file mode 100644 index 000000000..3e1f8a9a0 --- /dev/null +++ b/src/input/TCA6408Rotary.cpp @@ -0,0 +1,156 @@ +#include "TCA6408Rotary.h" + +#if defined(HAS_TCA6408_ROTARY) + +#include "Throttle.h" +#include "main.h" +#include + +namespace +{ +constexpr uint8_t TCA6408_ADDR = 0x20; +constexpr uint8_t TCA6408_INPUT_REG = 0x00; +constexpr uint8_t TCA6408_POLARITY_REG = 0x02; +constexpr uint8_t TCA6408_CONFIG_REG = 0x03; +constexpr uint8_t TCA6408_ROTARY_A_MASK = 0x01; +constexpr uint8_t TCA6408_ROTARY_B_MASK = 0x02; +constexpr uint8_t TCA6408_ROTARY_MASK = TCA6408_ROTARY_A_MASK | TCA6408_ROTARY_B_MASK; +constexpr uint32_t TCA6408_DEBOUNCE_MS = 5; +constexpr uint32_t TCA6408_POLL_MS = 100; + +enum class RotaryAction : uint8_t { NONE, UP, DOWN }; +} // namespace + +TCA6408Rotary *tca6408Rotary; +TCA6408Rotary *TCA6408Rotary::instance = nullptr; + +TCA6408Rotary::TCA6408Rotary(const char *name) + : concurrency::OSThread(name, TCA6408_POLL_MS), _originName(name), inputState(TCA6408_ROTARY_MASK) +{ +} + +bool TCA6408Rotary::init() +{ + if (!inputBroker) + return false; + + powerSensorBus(); + pinMode(SENSOR_INT, INPUT_PULLUP); + + // No input inversion, all eight pins configured as inputs. + if (!writeRegister(TCA6408_POLARITY_REG, 0x00) || !writeRegister(TCA6408_CONFIG_REG, 0xFF) || !readInput(inputState)) { + LOG_INFO("TCA6408 rotary not detected"); + concurrency::OSThread::disable(); + return false; + } + + inputBroker->registerSource(this); + instance = this; + attachInterrupt(digitalPinToInterrupt(SENSOR_INT), interruptHandler, FALLING); + ready = true; + LOG_INFO("TCA6408 rotary ready at 0x%02x", TCA6408_ADDR); + return true; +} + +int32_t TCA6408Rotary::runOnce() +{ + if (!ready) + return concurrency::OSThread::disable(); + + uint8_t newState = 0; + if (!readInput(newState)) { + LOG_DEBUG("TCA6408 rotary read failed"); + return TCA6408_POLL_MS; + } + + handleTransition(newState); + inputState = newState; + return TCA6408_POLL_MS; +} + +// Only wakes the thread. Reading the expander here would put an I2C transfer in interrupt +// context and race runOnce() for the bus and the decoder state. +void TCA6408Rotary::interruptHandler() +{ + if (!instance) + return; + + instance->setIntervalFromNow(0); + runASAP = true; + BaseType_t higherWake = 0; + concurrency::mainDelay.interruptFromISR(&higherWake); +} + +void TCA6408Rotary::powerSensorBus() +{ +#ifdef SENSOR_POWER_CTRL_PIN + pinMode(SENSOR_POWER_CTRL_PIN, OUTPUT); + digitalWrite(SENSOR_POWER_CTRL_PIN, SENSOR_POWER_ON); +#ifdef PERIPHERAL_WARMUP_MS + delay(PERIPHERAL_WARMUP_MS); +#else + delay(20); +#endif +#endif +} + +bool TCA6408Rotary::writeRegister(uint8_t reg, uint8_t value) +{ + Wire.beginTransmission(TCA6408_ADDR); + Wire.write(reg); + Wire.write(value); + return Wire.endTransmission() == 0; +} + +bool TCA6408Rotary::readInput(uint8_t &value) +{ + Wire.beginTransmission(TCA6408_ADDR); + Wire.write(TCA6408_INPUT_REG); + if (Wire.endTransmission(false) != 0) + return false; + if (Wire.requestFrom(TCA6408_ADDR, static_cast(1)) != 1) + return false; + + value = Wire.read(); + return true; +} + +// Whichever of A/B falls first decides the direction for the whole detent; activeLowPhase then +// suppresses further events until both inputs come back high. The rising-edge cases below catch +// detents whose falling edge was missed because the poll landed mid-rotation. +void TCA6408Rotary::handleTransition(uint8_t newState) +{ + const uint8_t changed = (inputState ^ newState) & TCA6408_ROTARY_MASK; + const bool aLow = (newState & TCA6408_ROTARY_A_MASK) == 0; + const bool bLow = (newState & TCA6408_ROTARY_B_MASK) == 0; + RotaryAction action = RotaryAction::NONE; + + if (!aLow && !bLow) + activeLowPhase = false; // back at the detent, arm for the next turn + + if (!activeLowPhase) { + if ((changed & TCA6408_ROTARY_A_MASK) && aLow && !bLow) { + action = RotaryAction::UP; + activeLowPhase = true; + } else if ((changed & TCA6408_ROTARY_B_MASK) && bLow && !aLow) { + action = RotaryAction::DOWN; + activeLowPhase = true; + } else if ((changed & TCA6408_ROTARY_A_MASK) && !aLow && bLow) { + action = RotaryAction::UP; + } else if ((changed & TCA6408_ROTARY_B_MASK) && !bLow && aLow) { + action = RotaryAction::DOWN; + } + } + + if (action == RotaryAction::NONE || Throttle::isWithinTimespanMs(lastEventMs, TCA6408_DEBOUNCE_MS)) + return; + + lastEventMs = millis(); + InputEvent event = {}; + event.source = _originName; + event.inputEvent = action == RotaryAction::DOWN ? INPUT_BROKER_DOWN : INPUT_BROKER_UP; + LOG_DEBUG("TCA6408 rotary event %d state=0x%02x", event.inputEvent, newState); + notifyObservers(&event); +} + +#endif diff --git a/src/input/TCA6408Rotary.h b/src/input/TCA6408Rotary.h new file mode 100644 index 000000000..600311bb9 --- /dev/null +++ b/src/input/TCA6408Rotary.h @@ -0,0 +1,40 @@ +#pragma once + +#include "InputBroker.h" +#include "concurrency/OSThread.h" +#include "configuration.h" + +#if defined(HAS_TCA6408_ROTARY) + +/** + * Rotary encoder on the A/B inputs of a TCA6408 I2C GPIO expander. SENSOR_INT goes low on any + * input change; the interrupt only wakes this thread, so all I2C and decoder state stay on it. + */ +class TCA6408Rotary : public Observable, public concurrency::OSThread +{ + public: + explicit TCA6408Rotary(const char *name); + bool init(); + int32_t runOnce() override; + + private: + static void interruptHandler(); + void powerSensorBus(); + bool writeRegister(uint8_t reg, uint8_t value); + bool readInput(uint8_t &value); + void handleTransition(uint8_t newState); + + const char *_originName; + uint8_t inputState; + uint32_t lastEventMs = 0; + bool ready = false; + // Latched between the first falling edge of a detent and both inputs returning high, + // so one detent reports exactly one event. + bool activeLowPhase = false; + + static TCA6408Rotary *instance; +}; + +extern TCA6408Rotary *tca6408Rotary; + +#endif diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index ca4b30155..87aab5ad9 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -107,7 +107,7 @@ __attribute__((noinline)) void variantDefaultConfig() {} __attribute__((noinline)) void variantDefaultModuleConfig() __attribute__((weak)); __attribute__((noinline)) void variantDefaultModuleConfig() {} -#ifdef HELTEC_MESH_NODE_T114 +#if defined(HELTEC_MESH_NODE_T114) || defined(TFT_NV3001B_DETECT) uint32_t read8(uint8_t bits, uint8_t dummy, uint8_t cs, uint8_t sck, uint8_t mosi, uint8_t dc, uint8_t rst) { @@ -157,6 +157,10 @@ uint32_t readwrite8(uint8_t cmd, uint8_t bits, uint8_t dummy, uint8_t cs, uint8_ return ret; } +#endif + +#ifdef HELTEC_MESH_NODE_T114 + uint32_t get_st7789_id(uint8_t cs, uint8_t sck, uint8_t mosi, uint8_t dc, uint8_t rst) { pinMode(cs, OUTPUT); @@ -178,6 +182,57 @@ uint32_t get_st7789_id(uint8_t cs, uint8_t sck, uint8_t mosi, uint8_t dc, uint8_ #endif +#ifdef TFT_NV3001B_DETECT + +// The NV3001B panel is an add-on module on these boards, so probe for it before assuming a screen. +static constexpr uint32_t NV3001B_PANEL_ID = 0x300101; +static constexpr uint32_t NV3001B_RESET_DELAY_MS = 120; // NV3001B_RST_DELAY, per the Arduino_GFX driver + +bool nv3001bPanelPresent(uint8_t cs, uint8_t sck, uint8_t mosi, uint8_t dc, uint8_t rst, uint8_t en, uint8_t bl) +{ + pinMode(en, OUTPUT); + digitalWrite(en, TFT_EN_ON); + pinMode(bl, OUTPUT); + digitalWrite(bl, TFT_BACKLIGHT_ON); + delay(NV3001B_RESET_DELAY_MS); + + pinMode(cs, OUTPUT); + digitalWrite(cs, HIGH); + pinMode(sck, OUTPUT); + digitalWrite(sck, LOW); + pinMode(mosi, OUTPUT); + pinMode(dc, OUTPUT); + pinMode(rst, OUTPUT); + digitalWrite(rst, HIGH); + delay(NV3001B_RESET_DELAY_MS); + digitalWrite(rst, LOW); // Hardware Reset + delay(NV3001B_RESET_DELAY_MS); + digitalWrite(rst, HIGH); + delay(NV3001B_RESET_DELAY_MS); + + // 0x04 reports the whole 24-bit display ID; 0xDA/0xDB/0xDC report it one byte at a time. + // A panel that answers either way is present. + uint32_t rddid = readwrite8(0x04, 24, 1, cs, sck, mosi, dc, rst); + uint32_t rdid = (readwrite8(0xDA, 8, 0, cs, sck, mosi, dc, rst) << 16) | + (readwrite8(0xDB, 8, 0, cs, sck, mosi, dc, rst) << 8) | readwrite8(0xDC, 8, 0, cs, sck, mosi, dc, rst); + LOG_INFO("NV3001B probe RDDID=0x%06x RDID=0x%06x", (unsigned int)rddid, (unsigned int)rdid); + + if (rddid == NV3001B_PANEL_ID || rdid == NV3001B_PANEL_ID) { + LOG_INFO("NV3001B panel detected"); + return true; + } + + // All ones means the data line floated, all zeroes means something held it low; either way no panel + // answered, so drop the rail again rather than leave an empty header powered. + LOG_INFO("NV3001B panel not detected"); + digitalWrite(bl, TFT_BACKLIGHT_OFF); + digitalWrite(en, TFT_EN_OFF); + pinMode(en, INPUT); + return false; +} + +#endif + // When armed by loadFromDisk, the decode callback writes satellite entries // straight into these maps instead of the temp vectors. Nullptr = legacy // push_back-to-vector path for backup/restore and other decoders. @@ -1046,12 +1101,14 @@ void NodeDB::installDefaultConfig(bool preserveKey = false) #if defined(USE_EINK) || defined(HAS_SPI_TFT) || defined(USE_SPISSD1306) bool hasScreen = true; -#ifdef HELTEC_MESH_NODE_T114 +#if defined(TFT_NV3001B_DETECT) + hasScreen = nv3001bPanelPresent(TFT_CS, TFT_SCL, TFT_SDA, TFT_RS, TFT_RST, TFT_EN, TFT_BL); +#elif defined(HELTEC_MESH_NODE_T114) uint32_t st7789_id = get_st7789_id(ST7789_NSS, ST7789_SCK, ST7789_SDA, ST7789_RS, ST7789_RESET); if (st7789_id == 0xFFFFFF) { hasScreen = false; } -#endif // HELTEC_MESH_NODE_T114 +#endif // TFT_NV3001B_DETECT / HELTEC_MESH_NODE_T114 #elif ARCH_PORTDUINO bool hasScreen = false; if (portduino_config.displayPanel) diff --git a/src/platform/esp32/architecture.h b/src/platform/esp32/architecture.h index 48618b641..2c409b0b8 100644 --- a/src/platform/esp32/architecture.h +++ b/src/platform/esp32/architecture.h @@ -221,6 +221,10 @@ #define HW_VENDOR meshtastic_HardwareModel_HELTEC_V4_R8 #elif defined(MINI_EPAPER_S3) #define HW_VENDOR meshtastic_HardwareModel_MINI_EPAPER_S3 +#elif defined(HELTEC_RC32) +#define HW_VENDOR meshtastic_HardwareModel_HELTEC_RC32 +#elif defined(HELTEC_RCC6) +#define HW_VENDOR meshtastic_HardwareModel_HELTEC_RCC6 #else #define HW_VENDOR meshtastic_HardwareModel_PRIVATE_HW #endif diff --git a/src/platform/nrf52/architecture.h b/src/platform/nrf52/architecture.h index 4a6afaeae..279ad7575 100644 --- a/src/platform/nrf52/architecture.h +++ b/src/platform/nrf52/architecture.h @@ -149,6 +149,8 @@ #define HW_VENDOR meshtastic_HardwareModel_HELTEC_MESH_TOWER_V2 #elif defined(HELTEC_MESH_NODE_T096) #define HW_VENDOR meshtastic_HardwareModel_HELTEC_MESH_NODE_T096 +#elif defined(HELTEC_RC52) +#define HW_VENDOR meshtastic_HardwareModel_HELTEC_RC52 #else #define HW_VENDOR meshtastic_HardwareModel_NRF52_UNKNOWN #endif diff --git a/variants/esp32c6/heltec_rcc6/pins_arduino.h b/variants/esp32c6/heltec_rcc6/pins_arduino.h new file mode 100644 index 000000000..91604ac60 --- /dev/null +++ b/variants/esp32c6/heltec_rcc6/pins_arduino.h @@ -0,0 +1,20 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include + +#define USB_VID 0x303A +#define USB_PID 0x1001 + +static const uint8_t TX = 16; +static const uint8_t RX = 17; + +static const int8_t SDA = -1; +static const int8_t SCL = -1; + +static const uint8_t MISO = 20; +static const uint8_t SCK = 21; +static const uint8_t MOSI = 22; +static const uint8_t SS = 23; + +#endif /* Pins_Arduino_h */ diff --git a/variants/esp32c6/heltec_rcc6/platformio.ini b/variants/esp32c6/heltec_rcc6/platformio.ini new file mode 100644 index 000000000..66042e1cf --- /dev/null +++ b/variants/esp32c6/heltec_rcc6/platformio.ini @@ -0,0 +1,35 @@ +[env:heltec-rcc6] +custom_meshtastic_hw_model = 143 +custom_meshtastic_hw_model_slug = HELTEC_RCC6 +custom_meshtastic_architecture = esp32-c6 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = Heltec RCC6 +custom_meshtastic_images = heltec-rcc6.svg +custom_meshtastic_tags = Heltec +custom_meshtastic_partition_scheme = 16MB +custom_meshtastic_has_mui = false + +extends = esp32c6_base +board = heltec_rcc6 +board_level = release +board_build.partitions = default_16MB.csv +upload_protocol = esptool + +build_flags = + ${esp32c6_base.build_flags} + -D HELTEC_RCC6 + -I variants/esp32c6/heltec_rcc6 + -DARDUINO_USB_CDC_ON_BOOT=1 + -DARDUINO_USB_MODE=1 + -DMESHTASTIC_EXCLUDE_AUDIO=1 + +lib_deps = + ${esp32c6_base.lib_deps} + # renovate: datasource=git-refs depName=moononournation-Arduino_GFX packageName=https://github.com/moononournation/Arduino_GFX gitBranch=master + # Pinned to a commit rather than a tag: the NV3001B driver has not shipped in a release yet. + https://github.com/moononournation/Arduino_GFX/archive/4d5afb05ce70a51895fe65efc7838d606b2a95a7.zip + +lib_ignore = + ${esp32c6_base.lib_ignore} + ESP32 Codec2 diff --git a/variants/esp32c6/heltec_rcc6/variant.h b/variants/esp32c6/heltec_rcc6/variant.h new file mode 100644 index 000000000..3436aff05 --- /dev/null +++ b/variants/esp32c6/heltec_rcc6/variant.h @@ -0,0 +1,59 @@ +#pragma once + +#define _VARIANT_HELTEC_RCC6_ + +#define BUTTON_PIN 9 + +#define HAS_SCREEN 1 +#define HAS_SPI_TFT 1 +#define USE_TFTDISPLAY 1 +#define USE_ARDUINO_GFX 1 +#define TFT_NV3001B 1 +#define TFT_NV3001B_DETECT 1 +#define HAS_GPS 0 +#define HAS_WIRE 0 +#undef GPS_RX_PIN +#undef GPS_TX_PIN + +#define USE_SX1262 +#define LORA_SCK 21 +#define LORA_MISO 20 +#define LORA_MOSI 22 +#define LORA_CS 23 +#define LORA_RESET 8 +#define LORA_DIO1 19 +#define LORA_BUSY 10 +#define SX126X_CS LORA_CS +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY LORA_BUSY +#define SX126X_RESET LORA_RESET +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 + +#define BATTERY_PIN 6 +#define ADC_CHANNEL ADC_CHANNEL_6 +#define ADC_CTRL 5 +#define ADC_CTRL_ENABLED HIGH +#define ADC_MULTIPLIER 4.95f + +#define TFT_SCL 4 +#define TFT_SDA 15 +#define TFT_CS 18 +#define TFT_RS 3 +#define TFT_DC TFT_RS +#define TFT_RST 0 +#define TFT_EN 2 +#define TFT_EN_ON LOW +#define TFT_EN_OFF HIGH +#define VTFT_CTRL TFT_EN +#define TFT_BL 1 +#define TFT_BLK TFT_BL +#define TFT_BACKLIGHT_ON HIGH +#define TFT_BACKLIGHT_OFF LOW +#define TFT_WIDTH 128 +#define TFT_HEIGHT 220 +#define TFT_BLACK 0 +#define SPI_FREQUENCY 4000000 +#define SCREEN_ROTATE + +#define SERIAL_PRINT_PORT 1 diff --git a/variants/esp32s3/hackaday-communicator/variant.h b/variants/esp32s3/hackaday-communicator/variant.h index ab15e3e6d..b34ea861d 100644 --- a/variants/esp32s3/hackaday-communicator/variant.h +++ b/variants/esp32s3/hackaday-communicator/variant.h @@ -12,6 +12,7 @@ #define BRIGHTNESS_DEFAULT 130 // Medium Low Brightness #define USE_TFTDISPLAY 1 #define HAS_SPI_TFT 1 +#define USE_ARDUINO_GFX 1 #define USE_POWERSAVE #define SLEEP_TIME 120 diff --git a/variants/esp32s3/heltec_rc32/pins_arduino.h b/variants/esp32s3/heltec_rc32/pins_arduino.h new file mode 100644 index 000000000..1df1080bf --- /dev/null +++ b/variants/esp32s3/heltec_rc32/pins_arduino.h @@ -0,0 +1,60 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include + +#define USB_VID 0x303a +#define USB_PID 0x1001 + +static const uint8_t TX = 43; +static const uint8_t RX = 44; + +static const uint8_t SDA = 21; +static const uint8_t SCL = 18; + +static const uint8_t SS = 10; +static const uint8_t MOSI = 12; +static const uint8_t MISO = 13; +static const uint8_t SCK = 11; + +static const uint8_t A0 = 1; +static const uint8_t A1 = 2; +static const uint8_t A2 = 3; +static const uint8_t A3 = 4; +static const uint8_t A4 = 5; +static const uint8_t A5 = 6; +static const uint8_t A6 = 7; +static const uint8_t A7 = 8; +static const uint8_t A8 = 9; +static const uint8_t A9 = 10; +static const uint8_t A10 = 11; +static const uint8_t A11 = 12; +static const uint8_t A12 = 13; +static const uint8_t A13 = 14; +static const uint8_t A14 = 15; +static const uint8_t A15 = 16; +static const uint8_t A16 = 17; +static const uint8_t A17 = 18; +static const uint8_t A18 = 19; +static const uint8_t A19 = 20; + +static const uint8_t T1 = 1; +static const uint8_t T2 = 2; +static const uint8_t T3 = 3; +static const uint8_t T4 = 4; +static const uint8_t T5 = 5; +static const uint8_t T6 = 6; +static const uint8_t T7 = 7; +static const uint8_t T8 = 8; +static const uint8_t T9 = 9; +static const uint8_t T10 = 10; +static const uint8_t T11 = 11; +static const uint8_t T12 = 12; +static const uint8_t T13 = 13; +static const uint8_t T14 = 14; + +static const uint8_t RST_LoRa = 9; +static const uint8_t BUSY_LoRa = 1; +static const uint8_t DIO1_LoRa = 14; + +#endif /* Pins_Arduino_h */ diff --git a/variants/esp32s3/heltec_rc32/platformio.ini b/variants/esp32s3/heltec_rc32/platformio.ini new file mode 100644 index 000000000..827f7c302 --- /dev/null +++ b/variants/esp32s3/heltec_rc32/platformio.ini @@ -0,0 +1,28 @@ +[env:heltec-rc32] +custom_meshtastic_hw_model = 141 +custom_meshtastic_hw_model_slug = HELTEC_RC32 +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_display_name = Heltec RC32 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_images = heltec-rc32.svg +custom_meshtastic_tags = Heltec +custom_meshtastic_requires_dfu = true +custom_meshtastic_partition_scheme = 16MB +custom_meshtastic_has_mui = false + +extends = esp32s3_base +board = heltec_rc32 +board_level = release +board_build.partitions = default_16MB.csv +build_flags = + ${esp32s3_base.build_flags} + -D HELTEC_RC32 + -D USE_PIN_BUZZER=PIN_BUZZER + -D BOARD_HAS_PSRAM + -I variants/esp32s3/heltec_rc32 +lib_deps = + ${esp32s3_base.lib_deps} + # renovate: datasource=git-refs depName=moononournation-Arduino_GFX packageName=https://github.com/moononournation/Arduino_GFX gitBranch=master + # Pinned to a commit rather than a tag: the NV3001B driver has not shipped in a release yet. + https://github.com/moononournation/Arduino_GFX/archive/4d5afb05ce70a51895fe65efc7838d606b2a95a7.zip diff --git a/variants/esp32s3/heltec_rc32/sdkconfig.h b/variants/esp32s3/heltec_rc32/sdkconfig.h new file mode 100644 index 000000000..d7d875010 --- /dev/null +++ b/variants/esp32s3/heltec_rc32/sdkconfig.h @@ -0,0 +1,24 @@ +#pragma once + +#include_next "sdkconfig.h" + +// qio_opi enables this, but Meshtastic removes the matching IDF component. +#ifdef CONFIG_NETWORK_PROV_NETWORK_TYPE_WIFI +#undef CONFIG_NETWORK_PROV_NETWORK_TYPE_WIFI +#endif +#define CONFIG_NETWORK_PROV_NETWORK_TYPE_WIFI 0 + +#ifndef CONFIG_LITTLEFS_PAGE_SIZE +#define CONFIG_LITTLEFS_MAX_PARTITIONS 3 +#define CONFIG_LITTLEFS_PAGE_SIZE 256 +#define CONFIG_LITTLEFS_OBJ_NAME_LEN 64 +#define CONFIG_LITTLEFS_READ_SIZE 128 +#define CONFIG_LITTLEFS_WRITE_SIZE 128 +#define CONFIG_LITTLEFS_LOOKAHEAD_SIZE 128 +#define CONFIG_LITTLEFS_CACHE_SIZE 512 +#define CONFIG_LITTLEFS_BLOCK_CYCLES 512 +#define CONFIG_LITTLEFS_USE_MTIME 1 +#define CONFIG_LITTLEFS_MTIME_USE_SECONDS 1 +#define CONFIG_LITTLEFS_MALLOC_STRATEGY_DEFAULT 1 +#define CONFIG_LITTLEFS_ASSERTS 1 +#endif diff --git a/variants/esp32s3/heltec_rc32/variant.h b/variants/esp32s3/heltec_rc32/variant.h new file mode 100644 index 000000000..413d39d81 --- /dev/null +++ b/variants/esp32s3/heltec_rc32/variant.h @@ -0,0 +1,78 @@ +#ifndef _VARIANT_HELTEC_RC32_ +#define _VARIANT_HELTEC_RC32_ + +#define HAS_SCREEN 1 +#define HAS_SPI_TFT 1 +#define USE_TFTDISPLAY 1 +#define USE_ARDUINO_GFX 1 +#define TFT_NV3001B 1 +#define TFT_NV3001B_DETECT 1 +#define TFT_SCL 17 +#define TFT_SDA 38 +#define TFT_CS 39 +#define TFT_RS 16 +#define TFT_DC TFT_RS +#define TFT_RST 4 +#define TFT_EN 6 +#define TFT_EN_ON LOW +#define TFT_EN_OFF HIGH +#define VTFT_CTRL TFT_EN +#define TFT_BL 5 +#define TFT_BLK TFT_BL +#define TFT_BACKLIGHT_ON HIGH +#define TFT_BACKLIGHT_OFF LOW +#define TFT_WIDTH 128 +#define TFT_HEIGHT 220 +#define TFT_BLACK 0 +#define SPI_FREQUENCY 8000000 +#define SCREEN_ROTATE + +#define BUTTON_PIN 0 + +#define HAS_GPS 1 +#undef GPS_RX_PIN +#undef GPS_TX_PIN +#define GPS_RX_PIN 44 +#define GPS_TX_PIN 43 +#define PIN_GPS_EN 45 +#define GPS_EN_ACTIVE HIGH +#define PIN_GPS_RESET 40 +#define GPS_RESET_MODE LOW +#define PIN_GPS_PPS 41 + +#define I2C_SCL 18 +#define I2C_SDA 21 +#define SENSOR_INT 42 +#define SENSOR_RST 2 +#define SENSOR_POWER_CTRL_PIN 46 +#define SENSOR_POWER_ON HIGH +#define HAS_TCA6408_ROTARY 1 +#define PERIPHERAL_WARMUP_MS 100 + +#define LED_POWER 47 +#define PIN_BUZZER 48 + +#define USE_SX1262 +#define LORA_SCK 11 +#define LORA_MISO 13 +#define LORA_MOSI 12 +#define LORA_CS 10 +#define LORA_DIO0 RADIOLIB_NC +#define LORA_DIO1 14 +#define LORA_RESET 9 + +#define SX126X_CS LORA_CS +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY 1 +#define SX126X_RESET LORA_RESET +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 + +#define BATTERY_PIN 7 +#define ADC_CHANNEL ADC_CHANNEL_6 +#define ADC_CTRL 15 +#define ADC_CTRL_ENABLED HIGH +#define ADC_MULTIPLIER 4.9 +#define ADC_ATTENUATION ADC_ATTEN_DB_2_5 + +#endif diff --git a/variants/nrf52840/heltec_rc52/platformio.ini b/variants/nrf52840/heltec_rc52/platformio.ini new file mode 100644 index 000000000..f57e5f7f9 --- /dev/null +++ b/variants/nrf52840/heltec_rc52/platformio.ini @@ -0,0 +1,30 @@ +; Heltec RC52 nRF52840/SX1262 device +[env:heltec-rc52] +custom_meshtastic_hw_model = 142 +custom_meshtastic_hw_model_slug = HELTEC_RC52 +custom_meshtastic_architecture = nrf52840 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = Heltec RC52 +custom_meshtastic_images = heltec-rc52.svg +custom_meshtastic_tags = Heltec + +extends = nrf52840_base +board = heltec_rc52 +board_level = release +debug_tool = jlink + +build_flags = ${nrf52840_base.build_flags} + -UOLEDDISPLAY_REDUCE_MEMORY ; TFTDisplay.cpp needs the lib's buffer_back for dirty-window diffing + -Ivariants/nrf52840/heltec_rc52 + -DHELTEC_RC52 + -D PIN_BUZZER=5 + -D USE_PIN_BUZZER=PIN_BUZZER + -DCONFIG_NFCT_PINS_AS_GPIOS=1 + +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/heltec_rc52> +lib_deps = + ${nrf52840_base.lib_deps} + # renovate: datasource=git-refs depName=moononournation-Arduino_GFX packageName=https://github.com/moononournation/Arduino_GFX gitBranch=master + # Pinned to a commit rather than a tag: the NV3001B driver has not shipped in a release yet. + https://github.com/moononournation/Arduino_GFX/archive/4d5afb05ce70a51895fe65efc7838d606b2a95a7.zip diff --git a/variants/nrf52840/heltec_rc52/variant.cpp b/variants/nrf52840/heltec_rc52/variant.cpp new file mode 100644 index 000000000..3af5c4032 --- /dev/null +++ b/variants/nrf52840/heltec_rc52/variant.cpp @@ -0,0 +1,103 @@ +#include "variant.h" +#include "Arduino.h" +#include "nrf.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +const uint32_t g_ADigitalPinMap[] = { + // P0 - pins 0 and 1 are hardwired for xtal and should never be enabled + 0xff, 0xff, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + + // P1 + 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; + +void initVariant() +{ + pinMode(PIN_LED1, OUTPUT); + digitalWrite(PIN_LED1, LED_STATE_OFF); + + pinMode(PIN_GPS_EN, OUTPUT); + digitalWrite(PIN_GPS_EN, !GPS_EN_ACTIVE); + pinMode(PIN_GPS_RESET, OUTPUT); + digitalWrite(PIN_GPS_RESET, !GPS_RESET_MODE); + pinMode(PIN_GPS_PPS, INPUT); + + pinMode(SENSOR_POWER_CTRL_PIN, OUTPUT); + digitalWrite(SENSOR_POWER_CTRL_PIN, SENSOR_POWER_ON); + pinMode(SENSOR_RST, OUTPUT); + digitalWrite(SENSOR_RST, HIGH); + pinMode(SENSOR_INT, INPUT); + + pinMode(PIN_BUZZER, OUTPUT); + digitalWrite(PIN_BUZZER, LOW); +} + +void variant_shutdown() +{ + detachInterrupt(PIN_BUTTON1); + detachInterrupt(PIN_GPS_PPS); + detachInterrupt(SENSOR_INT); + + pinMode(PIN_GPS_EN, OUTPUT); + digitalWrite(PIN_GPS_EN, !GPS_EN_ACTIVE); + + pinMode(SENSOR_POWER_CTRL_PIN, OUTPUT); + digitalWrite(SENSOR_POWER_CTRL_PIN, !SENSOR_POWER_ON); + + pinMode(PIN_BUZZER, OUTPUT); + digitalWrite(PIN_BUZZER, LOW); + pinMode(PIN_LED1, OUTPUT); + digitalWrite(PIN_LED1, LOW); + pinMode(ADC_CTRL, OUTPUT); + digitalWrite(ADC_CTRL, !ADC_CTRL_ENABLED); + pinMode(RADIOCORE_FEM_EN, OUTPUT); + digitalWrite(RADIOCORE_FEM_EN, LOW); + pinMode(RADIOCORE_VFEM_CTRL, OUTPUT); + digitalWrite(RADIOCORE_VFEM_CTRL, LOW); + + pinMode(PIN_WIRE_SDA, OUTPUT); + digitalWrite(PIN_WIRE_SDA, LOW); + pinMode(PIN_WIRE_SCL, OUTPUT); + digitalWrite(PIN_WIRE_SCL, LOW); + nrf_gpio_cfg_default(PIN_WIRE_SDA); + nrf_gpio_cfg_default(PIN_WIRE_SCL); + + nrf_gpio_cfg_default(PIN_GPS_PPS); + nrf_gpio_cfg_default(GPS_RX_PIN); + nrf_gpio_cfg_default(GPS_TX_PIN); + nrf_gpio_cfg_default(PIN_GPS_RESET); + nrf_gpio_cfg_default(SENSOR_POWER_CTRL_PIN); + nrf_gpio_cfg_default(SENSOR_RST); + nrf_gpio_cfg_default(SENSOR_INT); + + nrf_gpio_cfg_default(SX126X_DIO1); + nrf_gpio_cfg_default(SX126X_BUSY); + nrf_gpio_cfg_default(SX126X_RESET); + nrf_gpio_cfg_default(PIN_SPI_MISO); + nrf_gpio_cfg_default(PIN_SPI_MOSI); + nrf_gpio_cfg_default(PIN_SPI_SCK); + nrf_gpio_cfg_default(RADIOCORE_FEM_EN); + nrf_gpio_cfg_default(RADIOCORE_VFEM_CTRL); + + pinMode(TFT_EN, OUTPUT); + digitalWrite(TFT_EN, HIGH); + pinMode(TFT_BL, OUTPUT); + digitalWrite(TFT_BL, LOW); + pinMode(TFT_CS, OUTPUT); + digitalWrite(TFT_CS, LOW); + pinMode(TFT_RS, OUTPUT); + digitalWrite(TFT_RS, LOW); + pinMode(TFT_SCL, OUTPUT); + digitalWrite(TFT_SCL, LOW); + pinMode(TFT_SDA, OUTPUT); + digitalWrite(TFT_SDA, LOW); + pinMode(TFT_RST, OUTPUT); + digitalWrite(TFT_RST, LOW); + nrf_gpio_cfg_default(TFT_EN); + nrf_gpio_cfg_default(TFT_BL); + nrf_gpio_cfg_default(TFT_CS); + nrf_gpio_cfg_default(TFT_RS); + nrf_gpio_cfg_default(TFT_SCL); + nrf_gpio_cfg_default(TFT_SDA); + nrf_gpio_cfg_default(TFT_RST); +} \ No newline at end of file diff --git a/variants/nrf52840/heltec_rc52/variant.h b/variants/nrf52840/heltec_rc52/variant.h new file mode 100644 index 000000000..22b0b9a1a --- /dev/null +++ b/variants/nrf52840/heltec_rc52/variant.h @@ -0,0 +1,136 @@ +#ifndef _VARIANT_HELTEC_RC52_ +#define _VARIANT_HELTEC_RC52_ + +#define VARIANT_MCK (64000000ul) +#define USE_LFXO // Board uses 32 kHz crystal for LF + +#include "WVariant.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define PINS_COUNT (48) +#define NUM_DIGITAL_PINS (48) +#define NUM_ANALOG_INPUTS (1) +#define NUM_ANALOG_OUTPUTS (0) + +#define PIN_LED1 (0 + 15) +#define LED_POWER PIN_LED1 +#define LED_BLUE (-1) +#define LED_GREEN PIN_LED1 +#define LED_STATE_ON 1 +#define LED_STATE_OFF (LED_STATE_ON ^ 1) + +#define HAS_SCREEN 1 +#define HAS_SPI_TFT 1 +#define USE_TFTDISPLAY 1 +#define USE_ARDUINO_GFX 1 +#define TFT_NV3001B 1 +#define TFT_NV3001B_DETECT 1 +#define HAS_GPS 1 +#define HAS_WIRE 1 + +/* + * Optional RS-T108 / NV3001B TFT module on P2 + */ +#define TFT_SCL (0 + 30) +#define TFT_SDA (32 + 2) +#define TFT_CS (32 + 4) +#define TFT_RS (0 + 28) +#define TFT_DC TFT_RS +#define TFT_RST (0 + 10) +#define TFT_EN (32 + 13) +#define TFT_EN_ON LOW +#define TFT_EN_OFF HIGH +#define VTFT_CTRL TFT_EN +#define TFT_BL (0 + 9) +#define TFT_BACKLIGHT_ON HIGH +#define TFT_BACKLIGHT_OFF LOW +#define TFT_WIDTH 128 +#define TFT_HEIGHT 220 +#define SPI_FREQUENCY 8000000 +#define SCREEN_ROTATE + +/* + * Buttons + */ +#define PIN_BUTTON1 (32 + 10) // P1.10, external pull-up, active low + +/* + * Sensor I2C and control pins + */ +#define WIRE_INTERFACES_COUNT 1 +#define PIN_WIRE_SDA (32 + 11) // P1.11 +#define PIN_WIRE_SCL (0 + 2) // P0.02 +#define SENSOR_POWER_CTRL_PIN (0 + 12) +#define SENSOR_POWER_ON HIGH +#define SENSOR_INT (0 + 20) +#define SENSOR_RST (32 + 15) +#define HAS_TCA6408_ROTARY 1 + +/* + * Serial interfaces + */ +#define GPS_RX_PIN (0 + 8) +#define GPS_TX_PIN (0 + 7) +#define PIN_GPS_EN (32 + 9) +#define GPS_EN_ACTIVE HIGH +#define PIN_GPS_PPS (32 + 1) +#define PIN_GPS_RESET (32 + 6) +#define GPS_RESET_MODE LOW +#define GPS_THREAD_INTERVAL 50 +#define PERIPHERAL_WARMUP_MS 100 + +#define PIN_SERIAL1_RX GPS_RX_PIN +#define PIN_SERIAL1_TX GPS_TX_PIN +#define PIN_SERIAL2_RX (-1) +#define PIN_SERIAL2_TX (-1) + +/* + * LoRa radio + */ +#define USE_SX1262 +#define SX126X_CS (0 + 13) // P0.13 +#define LORA_CS SX126X_CS +#define SX126X_DIO1 (0 + 11) // P0.11 +#define SX126X_BUSY (0 + 24) // P0.24 +#define SX126X_RESET (32 + 0) // P1.00 +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 + +#define RADIOCORE_FEM_EN (0 + 26) +#define RADIOCORE_VFEM_CTRL (0 + 16) + +/* + * SPI + */ +#define SPI_INTERFACES_COUNT 2 +#define PIN_SPI_MISO (0 + 14) +#define PIN_SPI_MOSI (0 + 22) +#define PIN_SPI_SCK (0 + 25) + +#define PIN_SPI1_MISO (-1) +#define PIN_SPI1_MOSI TFT_SDA +#define PIN_SPI1_SCK TFT_SCL + +/* + * Battery + */ +#define ADC_CTRL (0 + 4) +#define ADC_CTRL_ENABLED HIGH +#define BATTERY_PIN (0 + 31) // P0.31/AIN7 +#define ADC_RESOLUTION 14 + +#define BATTERY_SENSE_RESOLUTION_BITS 12 +#define BATTERY_SENSE_RESOLUTION 4096.0 +#undef AREF_VOLTAGE +#define AREF_VOLTAGE 3.0 +#define VBAT_AR_INTERNAL AR_INTERNAL_3_0 +#define ADC_MULTIPLIER (4.9F) + +#ifdef __cplusplus +} +#endif + +#endif