From 3c62747ca7bd21fe04b772ed7e2d1016a4bedc71 Mon Sep 17 00:00:00 2001 From: liquidraver <504870+liquidraver@users.noreply.github.com> Date: Tue, 21 Jul 2026 15:04:25 +0200 Subject: [PATCH] add muziworks R1 Neo --- README.md | 1 + build.sh | 1 + formatter/README.md | 2 +- gen_provider_catalog.py | 6 +- releasenotes/RELEASE_NOTES_1.16.6-zephcore.md | 49 ++- zephcore/ARCHITECTURE.md | 1 + zephcore/CMakeLists.txt | 2 +- zephcore/boards/example_board/README.md | 1 + zephcore/boards/example_board/board.overlay | 30 ++ .../muziworks_r1neo/Kconfig.muziworks_r1neo | 6 + .../boards/nrf52840/muziworks_r1neo/README.md | 111 ++++++ .../nrf52840/muziworks_r1neo/board.cmake | 8 + .../nrf52840/muziworks_r1neo/board.conf | 29 ++ .../boards/nrf52840/muziworks_r1neo/board.yml | 10 + .../muziworks_r1neo_nrf52840-pinctrl.dtsi | 83 +++++ .../muziworks_r1neo_nrf52840.dts | 326 ++++++++++++++++++ .../muziworks_r1neo_nrf52840.yaml | 22 ++ .../muziworks_r1neo_nrf52840_defconfig | 32 ++ zephcore/boards/supported_boards.md | 1 + .../input/zephcore,input-multi-tap.yaml | 4 +- .../power/zephcore,poweroff-gpios.yaml | 48 +++ zephcore/helpers/ui/ui_common.c | 33 ++ 22 files changed, 800 insertions(+), 6 deletions(-) create mode 100644 zephcore/boards/nrf52840/muziworks_r1neo/Kconfig.muziworks_r1neo create mode 100644 zephcore/boards/nrf52840/muziworks_r1neo/README.md create mode 100644 zephcore/boards/nrf52840/muziworks_r1neo/board.cmake create mode 100644 zephcore/boards/nrf52840/muziworks_r1neo/board.conf create mode 100644 zephcore/boards/nrf52840/muziworks_r1neo/board.yml create mode 100644 zephcore/boards/nrf52840/muziworks_r1neo/muziworks_r1neo_nrf52840-pinctrl.dtsi create mode 100644 zephcore/boards/nrf52840/muziworks_r1neo/muziworks_r1neo_nrf52840.dts create mode 100644 zephcore/boards/nrf52840/muziworks_r1neo/muziworks_r1neo_nrf52840.yaml create mode 100644 zephcore/boards/nrf52840/muziworks_r1neo/muziworks_r1neo_nrf52840_defconfig create mode 100644 zephcore/dts/bindings/power/zephcore,poweroff-gpios.yaml diff --git a/README.md b/README.md index a3a0e8e..7e44b70 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ Other benefits: | **SenseCAP Solar** | SX1262 | GPS (L76K), QSPI flash, battery monitor | | **XIAO nRF52840 + Wio-SX1262** | SX1262 | Bare XIAO + Wio-SX1262 expansion | | **ProMicro SX1262** | SX1262 (E22-900M30S) | GPS, battery ADC, button, LED | +| **muzi works R1 Neo** | SX1262 | GPS, RTC, buzzer, button, LEDs, soft power-off | ### ESP32 diff --git a/build.sh b/build.sh index 03d798f..97eefca 100644 --- a/build.sh +++ b/build.sh @@ -23,6 +23,7 @@ nRF_boards=( heltec_t114 heltec_t096 gat562_30s + muziworks_r1neo ) # Native-Linux presets (not Zephyr boards — built with -b native_sim plus an diff --git a/formatter/README.md b/formatter/README.md index 36c6297..57750da 100644 --- a/formatter/README.md +++ b/formatter/README.md @@ -11,7 +11,7 @@ nRF formatter tools | SoftDevice | Boards | |------------|--------| -| **v6** | RAK4631, RAK3401 1W, ThinkNode M1/M3/M6, RAK WisMesh Tag, LilyGo T-Echo, LilyGo T-Impulse Plus, ProMicro SX1262, Heltec T114, Heltec T096, GAT562 30s | +| **v6** | RAK4631, RAK3401 1W, ThinkNode M1/M3/M6, RAK WisMesh Tag, LilyGo T-Echo, LilyGo T-Impulse Plus, ProMicro SX1262, Heltec T114, Heltec T096, GAT562 30s, muzi works R1 Neo | | **v7** | Wio Tracker L1, T1000-E, Ikoka Nano 30dBm, SenseCAP Solar, XIAO nRF52840 | - **`.uf2`** — manual drag-and-drop onto the UF2 mass-storage drive. diff --git a/gen_provider_catalog.py b/gen_provider_catalog.py index c5ae1c1..62d587f 100644 --- a/gen_provider_catalog.py +++ b/gen_provider_catalog.py @@ -53,6 +53,7 @@ MAKERS = { "uniteng": "UnitEng", "Ikoka": "Ikoka", "femtofox": "Femtofox", + "muziworks": "muzi works", } # Manufacturer per device name. @@ -70,6 +71,7 @@ MAKER_BY_DEVICE = { "Elecrow ThinkNode M1": "elecrow", "Elecrow ThinkNode M3": "elecrow", "Elecrow ThinkNode M6": "elecrow", + "Elecrow ThinkNode M9": "elecrow", "Ikoka Nano": "Ikoka", "LilyGo T-Echo": "lilygo", "LilyGo T-Beam (SX1262)": "lilygo", @@ -88,6 +90,7 @@ MAKER_BY_DEVICE = { "Femtofox (Luckfox Pico Mini)": "femtofox", "RAK6421 WisMesh (Raspberry Pi)": "rak", "RAK6421 WisMesh (Raspberry Pi 5)": "rak", + "muzi works R1 Neo": "muziworks", } DESCRIPTION = ( @@ -146,6 +149,7 @@ BOARDS = [ dict(stem="heltec_t114", kind="nrf", device="Heltec T114", variant="noscreen", subtitle="No screen"), dict(stem="gat562_30s", kind="nrf", device="GAT-IoT GAT562 30s"), + dict(stem="muziworks_r1neo", kind="nrf", device="muzi works R1 Neo"), # --- nRF52: new ZephCore-only hardware (own tile) --------------------- dict(stem="lilygo_timpulse_plus", kind="nrf", device="LilyGo T-Impulse Plus", new=True, img="lora.svg"), @@ -185,7 +189,7 @@ SOFTDEVICE = { "rak4631": 6, "rak3401_1watt": 6, "thinknode_m1": 6, "thinknode_m3": 6, "thinknode_m6": 6, "rak_wismesh_tag": 6, "lilygo_techo": 6, "lilygo_timpulse_plus": 6, "promicro_sx1262": 6, "heltec_t114": 6, - "heltec_t096": 6, "gat562_30s": 6, + "heltec_t096": 6, "gat562_30s": 6, "muziworks_r1neo": 6, "wio_tracker_l1": 7, "t1000_e": 7, "ikoka_nano_30dbm": 7, "sensecap_solar": 7, "xiao_nrf52840": 7, } diff --git a/releasenotes/RELEASE_NOTES_1.16.6-zephcore.md b/releasenotes/RELEASE_NOTES_1.16.6-zephcore.md index 4f41c30..373076e 100644 --- a/releasenotes/RELEASE_NOTES_1.16.6-zephcore.md +++ b/releasenotes/RELEASE_NOTES_1.16.6-zephcore.md @@ -24,7 +24,8 @@ --- A maintenance release: an important **admin-password fix**, **GPS standby power savings**, and the -removal of **Adaptive Power Control**. +removal of **Adaptive Power Control** — plus two new boards, **muzi works R1 Neo** and an experimental +first cut at the **ThinkNode M9**. ## Highlights @@ -87,6 +88,38 @@ What changes for you: - **Your saved settings are safe.** APC's two prefs bytes are kept reserved at their original offsets, so no stored configuration shifts and nothing else is misread on upgrade. +## New boards + +### muzi works R1 Neo + +A pocket LoRa node — nRF52840 + SX1262 (RAK4630 stamp on a custom muzi baseboard), with GNSS, buzzer, +battery-backed RTC and a single user button. No display, so the UI is button actions only. It reuses the +RAK4631 flash layout (SoftDevice s140 v6.1.1, app @ `0x26000`), so the stock UF2 bootloader works as-is — +no bootloader reflash needed. The board's soft-power design is handled in devicetree: the DCDC rail latches +on at boot and is released on shutdown, so long-press power-off works properly. + +Two things still want confirming against real hardware: the **battery divider multiplier** is a placeholder +(the two upstreams disagree — check `get batt` against a multimeter), and the **GNSS module identity** is +unconfirmed. Reports welcome. + +### ThinkNode M9 — experimental, first testable release + +**Consider this a preview, not a finished port.** It builds, flashes and runs, and it is worth testing if +you have the hardware and don't mind rough edges — but several things have not been confirmed on a real +device yet, and a few are knowingly missing. + +It is ZephCore's first ESP32-S3 + LR1110 board, so a fair amount of it is new ground rather than a copy of +an existing port. Currently unverified: display init parameters (inherited from another ST7789V board — if +the screen is mirrored or upside down, that's a known one-line fix), GPS output from the CC1167Q module, +battery calibration, and whether the USB-C port reaches the native USB pads at all (BLE is the companion +path meanwhile). Not ported: the I2C keyboard, which is driven by a second MCU. + +Also worth knowing on this board specifically: the **SD slot shares the radio's SPI bus**. That has no +effect on normal operation, but it does matter if you use the LR1110 updater below. + +Feedback from anyone with an M9 is genuinely useful here — the bring-up list in the board's README says +exactly what to look at. + ## Other fixes and improvements - **Fixed: Heltec V3 failed to build from source** once device PM was enabled. The upstream Zephyr board @@ -98,6 +131,20 @@ What changes for you: this only bit source builds. (V4 / V4.3 use ZephCore-local board definitions and were never affected.) - **Air530z GPS driver patch** comments corrected to match the new PM strategy. +## Sidenote: LR1110 radio firmware updater + +Not part of this release and not something you need — but if you have an LR1110 board and feel like it, +you can now update the **radio chip's own firmware** to Semtech's current version (`0x0402`, the H1_2026 +security release). This is the LR1110's internal firmware, not ZephCore. Prebuilt tools for T1000-E and +ThinkNode M9 are in [`LR1110_updater/`](https://github.com/liquidraver/ZephCore/tree/master/LR1110_updater) +— flash one, watch it over serial, then flash ZephCore again afterwards. + +Worth reading the [README](https://github.com/liquidraver/ZephCore/blob/master/LR1110_updater/README.md) +first. Two things in particular: the chip bootloader update it performs is **one-way**, and on the M9 you +must **take the SD card out** (it shares the radio's SPI bus and silently corrupts the write — the tool +checks and refuses to start, but still). Boards on older firmwares work perfectly well with +ZephCore and are fine left alone. + ## Recommended upgrade checklist 1. **Set an admin password on v1.16.5?** Try the all-lowercase form if you are locked out; otherwise diff --git a/zephcore/ARCHITECTURE.md b/zephcore/ARCHITECTURE.md index c980641..4f03848 100644 --- a/zephcore/ARCHITECTURE.md +++ b/zephcore/ARCHITECTURE.md @@ -854,6 +854,7 @@ Build strings and flash methods: `boards/supported_boards.md` and `boards/exampl | SenseCAP Solar | nRF52840 | SX1262 | L76K | - | QSPI, battery monitor | | XIAO nRF52840 + Wio-SX1262 | nRF52840 | SX1262 | - | - | - | | ProMicro SX1262 | nRF52840 | SX1262 (E22-900M30S) | Yes | - | Button, LED, battery ADC | +| muzi works R1 Neo | nRF52840 | SX1262 | Yes | - | Buzzer, button, RX8130CE RTC, latched-rail power-off | | XIAO nRF54L15 | nRF54L15 | SX1262 | - | - | Contacts capped at 450 | | XIAO ESP32-C3 | ESP32-C3 | SX1262 | - | - | Contacts capped at 300 | | XIAO ESP32-C6 | ESP32-C6 | SX1262 | - | - | - | diff --git a/zephcore/CMakeLists.txt b/zephcore/CMakeLists.txt index b033a79..4469684 100644 --- a/zephcore/CMakeLists.txt +++ b/zephcore/CMakeLists.txt @@ -265,7 +265,7 @@ set(ZEPHCORE_COMMON_CONF "${CMAKE_CURRENT_SOURCE_DIR}/boards/common/zephcore_com # Detect platform from board name and add platform-specific common config # For boards with qualifiers like "wio_tracker_l1/nrf52840", check both the full BOARD # string and the BOARD_QUALIFIERS which contains just the qualifier (e.g., "nrf52840") -if(BOARD MATCHES ".*nrf52.*" OR BOARD MATCHES "rak4631" OR BOARD MATCHES "rak_wismesh_tag" OR BOARD MATCHES "rak3401_1watt" OR BOARD MATCHES "wio_tracker" OR BOARD MATCHES "ikoka_nano" OR BOARD MATCHES "t1000_e" OR BOARD MATCHES "thinknode_m1" OR BOARD MATCHES "thinknode_m3" OR BOARD MATCHES "thinknode_m6" OR BOARD MATCHES "promicro_lr2021" OR BOARD MATCHES "promicro_sx1262" OR BOARD MATCHES "sensecap_solar" OR BOARD MATCHES "xiao_nrf52840" OR BOARD MATCHES "lilygo_techo" OR BOARD MATCHES "lilygo_timpulse_plus" OR BOARD MATCHES "heltec_t114" OR BOARD MATCHES "heltec_t096" OR BOARD MATCHES "gat562") +if(BOARD MATCHES ".*nrf52.*" OR BOARD MATCHES "rak4631" OR BOARD MATCHES "rak_wismesh_tag" OR BOARD MATCHES "rak3401_1watt" OR BOARD MATCHES "wio_tracker" OR BOARD MATCHES "ikoka_nano" OR BOARD MATCHES "t1000_e" OR BOARD MATCHES "thinknode_m1" OR BOARD MATCHES "thinknode_m3" OR BOARD MATCHES "thinknode_m6" OR BOARD MATCHES "promicro_lr2021" OR BOARD MATCHES "promicro_sx1262" OR BOARD MATCHES "sensecap_solar" OR BOARD MATCHES "xiao_nrf52840" OR BOARD MATCHES "lilygo_techo" OR BOARD MATCHES "lilygo_timpulse_plus" OR BOARD MATCHES "heltec_t114" OR BOARD MATCHES "heltec_t096" OR BOARD MATCHES "gat562" OR BOARD MATCHES "muziworks_r1neo") set(ZEPHCORE_PLATFORM_CONF "${CMAKE_CURRENT_SOURCE_DIR}/boards/common/nrf52_common.conf") elseif(DEFINED BOARD_QUALIFIERS AND BOARD_QUALIFIERS MATCHES ".*nrf52.*") set(ZEPHCORE_PLATFORM_CONF "${CMAKE_CURRENT_SOURCE_DIR}/boards/common/nrf52_common.conf") diff --git a/zephcore/boards/example_board/README.md b/zephcore/boards/example_board/README.md index 0a9b391..35d35aa 100644 --- a/zephcore/boards/example_board/README.md +++ b/zephcore/boards/example_board/README.md @@ -26,6 +26,7 @@ Supported Boards | LilyGo T-Impulse Plus | `west build -b lilygo_timpulse_plus zephcore` | UF2 drag-drop or `west flash` | | Heltec T114 | `west build -b heltec_t114 zephcore` | UF2 drag-drop or `west flash` | | Heltec Mesh Node T096 | `west build -b heltec_t096 zephcore` | UF2 drag-drop or `west flash` | +| muzi works R1 Neo | `west build -b muziworks_r1neo zephcore` | UF2 drag-drop or `west flash` | **Heltec T114 screenless:** append `boards/nrf52840/heltec_t114/no_display.conf` to `EXTRA_CONF_FILE` for units without the TFT module. diff --git a/zephcore/boards/example_board/board.overlay b/zephcore/boards/example_board/board.overlay index a4e0d92..f6f9183 100644 --- a/zephcore/boards/example_board/board.overlay +++ b/zephcore/boards/example_board/board.overlay @@ -849,3 +849,33 @@ * #include "../../common/sensors-i2c.dtsi" * }; */ + +/* --- Latched supply rail / true power-off (soft-power boards) --- + * + * Only needed if your board has NO mechanical power switch and instead keeps + * its own supply alive by asserting a "hold" GPIO — typically a companion I/O + * controller that gates a DCDC regulator (see muzi works R1 Neo). + * + * On such a board sys_poweroff() alone is not a power-off. The SoC reaches + * System OFF at ~1 uA, but nRF GPIO output latches persist across the + * transition, so the hold line stays asserted and the rail keeps draining the + * battery. Declare the pins here and ui_prepare_for_system_off() releases them + * as its very last step, after the wake button has been observed released. + * + * Order is preserved and matters: downstream loads first, rail latch LAST. + * + * Pins are usually the same ones a gpio-hog asserts at boot. That is fine and + * intended — a hog is init-time configuration, not a runtime reservation, so + * the hog does the early assert and this node does the shutdown release. + * + * Boards without this node are entirely unaffected (the loop compiles away). + * + * / { + * poweroff_gpios: poweroff-gpios { + * compatible = "zephcore,poweroff-gpios"; + * gpios = <&gpio1 5 GPIO_ACTIVE_HIGH>, radio module power + * <&gpio0 29 GPIO_ACTIVE_HIGH>, I/O controller "MCU on" + * <&gpio0 13 GPIO_ACTIVE_HIGH>; rail latch - cuts power + * }; + * }; + */ diff --git a/zephcore/boards/nrf52840/muziworks_r1neo/Kconfig.muziworks_r1neo b/zephcore/boards/nrf52840/muziworks_r1neo/Kconfig.muziworks_r1neo new file mode 100644 index 0000000..26c7b9c --- /dev/null +++ b/zephcore/boards/nrf52840/muziworks_r1neo/Kconfig.muziworks_r1neo @@ -0,0 +1,6 @@ +# Copyright (c) 2026 ZephCore +# +# SPDX-License-Identifier: MIT + +config BOARD_MUZIWORKS_R1NEO + select SOC_NRF52840_QIAA diff --git a/zephcore/boards/nrf52840/muziworks_r1neo/README.md b/zephcore/boards/nrf52840/muziworks_r1neo/README.md new file mode 100644 index 0000000..7a98bc3 --- /dev/null +++ b/zephcore/boards/nrf52840/muziworks_r1neo/README.md @@ -0,0 +1,111 @@ +# muzi works R1 Neo + +A pocket LoRa node built on the **nRF52840 + SX1262** (RAK4630 stamp module on +a custom muzi baseboard), with GNSS, buzzer, battery-backed RTC and a single +user button. No display. + +The port is cross-derived from three sources, because none of them agrees with +the others on every pin: Arduino MeshCore's +[`variants/muziworks_r1_neo`](https://github.com/meshcore-dev/MeshCore/pull/2007), +Meshtastic's +[`variants/nrf52840/r1-neo`](https://github.com/meshtastic/firmware/blob/master/variants/nrf52840/r1-neo/variant.h), +and the vendor's own [product page](https://muzi.works/products/r1-neo-complete-meshtastic-device). +Where they conflict, the reasoning is recorded in the DTS comments and below. + +## Build + +```bash +# Companion (BLE — default role) +west build -b muziworks_r1neo zephcore --pristine + +# Repeater (USB-CDC CLI, no BLE) +west build -b muziworks_r1neo zephcore --pristine -- \ + -DEXTRA_CONF_FILE="boards/common/repeater.conf" + +# Companion + debug logging +west build -b muziworks_r1neo zephcore --pristine -- \ + -DEXTRA_CONF_FILE="boards/common/debug.conf" +``` + +Flash: drag `build/zephyr/zephyr.uf2` onto the UF2 drive, or `west flash` with +a J-Link. Bootloader is Adafruit UF2 / SoftDevice **s140 v6.1.1**, app @ +`0x26000` (`SD_FWID 0x00B6`) — same layout as RAK4631, so the stock bootloader +is reused and no re-flash is needed. + +## Hardware + +| Function | Part | Bus / notes | +|------------|----------------------|----------------------------------------------| +| MCU | nRF52840 (RAK4630) | BLE5, native USB, 1 MB flash / 256 KB RAM | +| LoRa | SX1262 | DIO2 keys the RF switch, DIO3 feeds a 1.8 V TCXO | +| GNSS | unconfirmed (GPS/BDS)| UART0 @ 9600, power gated on P1.01 | +| RTC | Epson RX8130CE | I2C0 @ 0x32, battery-backed | +| Battery | ADC AIN7 (P0.31) | divider ratio unconfirmed — see below | +| Input | 1 user button | P0.26, active-HIGH, external pull-down | +| Buzzer | piezo | P0.03 (PWM0) | +| LEDs | green / blue | P1.04 / P0.28, both active-HIGH | +| Power | I/O controller + DCDC| latch P0.13, "MCU on" P0.29 | + +No QSPI external flash — contacts and channels live in internal flash. + +## Pin map + +| Signal | nRF52840 | Signal | nRF52840 | +|-----------------|----------|---------------------|----------| +| LoRa SCK | P1.11 | LoRa CS | P1.10 | +| LoRa MOSI | P1.12 | LoRa RST | P1.06 | +| LoRa MISO | P1.13 | LoRa BUSY | P1.14 | +| LoRa DIO1 | P1.15 | SX1262 power enable | P1.05 | +| GPS MCU-TX / RX | P0.24 / P0.25 | GPS enable | P1.01 | +| GPS PPS | P0.02 (unused) | RTC SDA / SCL | P0.19 / P0.20 | +| Battery ADC | P0.31 | Battery charge stat | P1.02 (unused) | +| User button | P0.26 | Buzzer | P0.03 | +| LED green/blue | P1.04 / P0.28 | DCDC latch | P0.13 | +| I/O ctrl "MCU on" | P0.29 | MCU_SIGNAL | P0.30 (unused) | + +### Notes worth knowing + +- **GPS TX/RX are swapped relative to the net names.** The schematic labels + `UART_GPS_RX` (P0.24) and `UART_GPS_TX` (P0.25) from the *module's* point of + view, so the MCU's receive pin is P0.25. Meshtastic maps them that way; + Arduino MeshCore takes the labels literally and ends up listening on the + module's own input pin. ZephCore follows Meshtastic. Getting this backwards + is silent — the UART simply never sees a byte. +- **Soft power, not a switch.** The rail is gated by an I/O controller that + only holds the DCDC up while P0.13 is asserted, and P0.29 tells it the MCU + is alive (this also enables button and LED passthrough). Both are `gpio-hog` + at boot and released at shutdown through the `poweroff_gpios` node — without + that release, `sys_poweroff()` would leave the rail latched, because nRF52 + GPIO output latches survive System OFF. +- **Long-press = real power-off.** Hold the button ≥1 s: the buzzer plays the + shutdown melody, the radio and GNSS are parked, then the latch drops and the + board is genuinely off (not a ~1 µA System OFF doze). Press again to boot. +- **Multi-tap actions:** 2 taps = flood advert, 3 = buzzer mute toggle, + 4 = GPS on/off, 5 = LED heartbeat toggle. A single tap maps to "page next", + which is a no-op here since there is no display. +- **NFC pins** (P0.09/P0.10) are freed via `nfct-pins-as-gpios`; they are NC on + this board. Harmless, and consistent with the other RAK-based boards. + +## To verify on real hardware + +1. **Battery multiplier.** `vbat-mv-multiplier = 6146` is a placeholder. The + two upstreams disagree on the divider: Arduino MeshCore's constant implies + ~1.73 (→ 6259, the RAK4631-family value), Meshtastic's variant.h states + 1.667 (→ 6031). Measure the cell with a DMM, compare against `get batt`, + and set the value with ZephCore's convention: + `multiplier = divider_ratio * 1.005 * 3600`. +2. **GNSS lock.** Confirm NMEA arrives after the TX/RX correction above. If it + does not, the next suspect is the module identity — swap + `compatible = "luatos,air530z"` for `"gnss-nmea-generic"` plus a + `gps-enable` alias (pattern in `heltec_t114` / `thinknode_m9`). +3. **Power-off current.** After a long-press shutdown, the board should draw + effectively nothing. If it still drains, the latch release order in + `poweroff_gpios` may need adjusting. +4. **Power-off with USB attached.** The rail may be held up externally, in + which case the shutdown falls through to a plain System OFF instead of a + true power-off. Expected; confirm it still wakes cleanly on button press. +5. **Reboot.** A soft reset floats P0.13 while the SoC restarts, so the I/O + controller may cut the rail and turn the board off instead of rebooting. + Upstream MeshCore reports the same class of problem on this board. If it + reproduces, it is a baseboard behaviour, not something the DTS can fix — + document it rather than chasing it in firmware. diff --git a/zephcore/boards/nrf52840/muziworks_r1neo/board.cmake b/zephcore/boards/nrf52840/muziworks_r1neo/board.cmake new file mode 100644 index 0000000..c338f44 --- /dev/null +++ b/zephcore/boards/nrf52840/muziworks_r1neo/board.cmake @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: MIT + +board_runner_args(jlink "--device=nRF52840_xxAA" "--speed=4000") +board_runner_args(pyocd "--target=nrf52840" "--frequency=4000000") + +include(${ZEPHYR_BASE}/boards/common/uf2.board.cmake) +include(${ZEPHYR_BASE}/boards/common/jlink.board.cmake) +include(${ZEPHYR_BASE}/boards/common/pyocd.board.cmake) diff --git a/zephcore/boards/nrf52840/muziworks_r1neo/board.conf b/zephcore/boards/nrf52840/muziworks_r1neo/board.conf new file mode 100644 index 0000000..ea31c14 --- /dev/null +++ b/zephcore/boards/nrf52840/muziworks_r1neo/board.conf @@ -0,0 +1,29 @@ +# muzi works R1 Neo (nRF52840 + SX1262) +# SPDX-License-Identifier: MIT +# +# RAK4630 stamp module on a custom muzi baseboard. Flash layout matches +# RAK4631 (SoftDevice s140 v6.1.1, app @ 0x26000), so the stock UF2 +# bootloader is reused — no re-flash needed. +# +# The baseboard is a soft-power design: a dedicated I/O controller gates the +# DCDC rail and only keeps it up while the MCU holds P0.13 asserted. That is +# handled entirely in devicetree — gpio-hog to latch on at boot, and a +# zephcore,poweroff-gpios node to release at shutdown. +# +# Most features are auto-detected from devicetree: +# - PWM + buzzer (buzzer nodelabel) -> UI_BUZZER +# - user button + LEDs (gpio-keys / gpio-leds) -> UI_BUTTONS +# - LoRa RX duty cycle (companion + SX1262) +# - GNSS, battery ADC, RX8130CE RTC +# +# There is no display on this board, so ZEPHCORE_UI_DISPLAY stays off and the +# UI reduces to the button actions listed in the DTS. + +# Board identification (matches Arduino MeshCore variant name) +CONFIG_ZEPHCORE_BOARD_NAME="muzi works R1 Neo" + +# Device Information Service model name +CONFIG_BT_DIS_MODEL_NUMBER_STR="muzi works R1 Neo" + +# SoftDevice firmware ID (s140 v6.1.1 — same as RAK4631) +CONFIG_ZEPHCORE_SD_FWID=0x00B6 diff --git a/zephcore/boards/nrf52840/muziworks_r1neo/board.yml b/zephcore/boards/nrf52840/muziworks_r1neo/board.yml new file mode 100644 index 0000000..12c5610 --- /dev/null +++ b/zephcore/boards/nrf52840/muziworks_r1neo/board.yml @@ -0,0 +1,10 @@ +# Copyright (c) 2026 ZephCore +# +# SPDX-License-Identifier: MIT + +board: + name: muziworks_r1neo + full_name: muzi works R1 Neo + vendor: muziworks + socs: + - name: nrf52840 diff --git a/zephcore/boards/nrf52840/muziworks_r1neo/muziworks_r1neo_nrf52840-pinctrl.dtsi b/zephcore/boards/nrf52840/muziworks_r1neo/muziworks_r1neo_nrf52840-pinctrl.dtsi new file mode 100644 index 0000000..cf489d7 --- /dev/null +++ b/zephcore/boards/nrf52840/muziworks_r1neo/muziworks_r1neo_nrf52840-pinctrl.dtsi @@ -0,0 +1,83 @@ +/* + * muzi works R1 Neo - Pin Control + * Copyright (c) 2026 ZephCore + * SPDX-License-Identifier: MIT + */ + +&pinctrl { + /* GNSS UART. + * + * Direction note: the schematic net names UART_GPS_RX (P0.24) and + * UART_GPS_TX (P0.25) are labelled from the GPS module's point of + * view, so UART_GPS_RX is the module's input and therefore the MCU's + * TX. Meshtastic's r1-neo variant.h maps them that way + * (PIN_SERIAL1_RX 25 / PIN_SERIAL1_TX 24); Arduino MeshCore's variant + * takes the net names literally and has them the other way round, + * which leaves the MCU listening on the module's own input pin — i.e. + * silence, and no fix, ever. Follow Meshtastic here. */ + uart0_default: uart0_default { + group1 { + psels = ; + }; + group2 { + psels = ; + bias-pull-up; + }; + }; + + uart0_sleep: uart0_sleep { + group1 { + psels = , + ; + low-power-enable; + }; + }; + + /* I2C0 - RX8130CE RTC (0x32) + optional Grove-style sensors */ + i2c0_default: i2c0_default { + group1 { + psels = , + ; + }; + }; + + i2c0_sleep: i2c0_sleep { + group1 { + psels = , + ; + low-power-enable; + }; + }; + + /* PWM0 for buzzer */ + pwm0_default: pwm0_default { + group1 { + psels = ; + }; + }; + + pwm0_sleep: pwm0_sleep { + group1 { + psels = ; + low-power-enable; + }; + }; + + /* SPI1 for SX1262 - CS controlled manually */ + spi1_default: spi1_default { + group1 { + psels = , + , + ; + }; + }; + + spi1_sleep: spi1_sleep { + group1 { + psels = , + , + ; + low-power-enable; + }; + }; +}; diff --git a/zephcore/boards/nrf52840/muziworks_r1neo/muziworks_r1neo_nrf52840.dts b/zephcore/boards/nrf52840/muziworks_r1neo/muziworks_r1neo_nrf52840.dts new file mode 100644 index 0000000..18c0bf4 --- /dev/null +++ b/zephcore/boards/nrf52840/muziworks_r1neo/muziworks_r1neo_nrf52840.dts @@ -0,0 +1,326 @@ +/* + * muzi works R1 Neo - nRF52840 + SX1262 + * Copyright (c) 2026 ZephCore + * + * Hardware: + * - RAK4630 stamp module (nRF52840 + SX1262), s140 v6.1.1 bootloader + * - Custom baseboard with a dedicated I/O controller gating a DCDC rail: + * the MCU latches its own supply on via P0.13 and signals "MCU is up" on + * P0.29, which also enables button/LED passthrough from the controller. + * - GNSS on UART0 @ 9600 (module identity unconfirmed - see below) + * - RX8130CE battery-backed RTC @ 0x32 on I2C0 + * - Buzzer on P0.03 (PWM0), green LED P1.04, blue LED P0.28 + * - Single user button on P0.26, active HIGH, external pull-down + * - No display, no QSPI external flash + * + * SPDX-License-Identifier: MIT + */ + +/dts-v1/; +#include +#include +#include +#include +#include +#include +#include "muziworks_r1neo_nrf52840-pinctrl.dtsi" + +/* Delete default partitions from nrf52840_partition.dtsi */ +/delete-node/ &boot_partition; +/delete-node/ &slot0_partition; +/delete-node/ &slot1_partition; +/delete-node/ &storage_partition; + +/ { + model = "muzi works R1 Neo"; + compatible = "muziworks,r1-neo"; + + chosen { + zephyr,code-partition = &code_partition; + zephyr,console = &cdc_acm_uart; + zephyr,shell-uart = &cdc_acm_uart; + }; + + leds { + compatible = "gpio-leds"; + + led_green: led_0 { + gpios = <&gpio1 4 GPIO_ACTIVE_HIGH>; + label = "Green LED"; + }; + led_blue: led_1 { + gpios = <&gpio0 28 GPIO_ACTIVE_HIGH>; + label = "Blue LED"; + }; + }; + + buttons: buttons { + compatible = "gpio-keys"; + + user_button: button_0 { + /* Active HIGH (pressed = VCC), no internal pull — the board + * has an external pull-down and the line is passed through + * from the I/O controller. Matches Meshtastic's + * BUTTON_ACTIVE_LOW 0 / BUTTON_ACTIVE_PULLUP 0 / + * BUTTON_SENSE_TYPE INPUT_SENSE_HIGH. The polarity here is + * what ui_prepare_for_system_off() reads to pick the + * System OFF SENSE level, so it must stay correct. */ + gpios = <&gpio0 26 GPIO_ACTIVE_HIGH>; + zephyr,code = ; + label = "User Button"; + }; + }; + + /* Input filter: long-press detection on the single user button. + * Short-press → KEY_A (fed to the multi-tap filter below) + * Long-press → KEY_F = full power-off (see poweroff_gpios). */ + user_btn_longpress { + compatible = "zephyr,input-longpress"; + input = <&buttons>; + input-codes = ; + short-codes = ; + long-codes = ; + long-delay-ms = <1000>; + }; + + /* Input filter: multi-tap on the short-press output. + * 1 tap (400ms wait) → KEY_1 = page next (no-op, board has no display) + * 2 taps (400ms wait) → KEY_B = flood advert + * 3 taps (400ms wait) → KEY_D = buzzer mute toggle + * 4 taps (400ms wait) → KEY_C = GPS on/off + * 5 taps (immediate) → KEY_E = LED heartbeat toggle */ + user_btn_multitap { + compatible = "zephcore,input-multi-tap"; + /* No 'input' phandle — listen to ALL devices. + * KEY_A is emitted by the longpress pseudo-device, + * not by &buttons, so we must not filter by device. */ + input-codes = ; + tap-codes = ; + tap-delay-ms = <400>; + }; + + /* Full power-off sequence, released as the last act before + * sys_poweroff() (helpers/ui/ui_common.c step 6). + * + * Without this the R1 Neo cannot actually switch off: System OFF puts + * the SoC at ~1 uA, but nRF52 GPIO output latches persist across the + * transition, so P0.13 would keep holding the DCDC rail up and the + * baseboard would keep draining the cell. Order mirrors Arduino + * MeshCore's R1NeoBoard::initiateShutdown(): drop the radio rail, tell + * the I/O controller the MCU is going away, then release the latch. */ + poweroff_gpios: poweroff-gpios { + compatible = "zephcore,poweroff-gpios"; + gpios = <&gpio1 5 GPIO_ACTIVE_HIGH>, /* SX1262 module power */ + <&gpio0 29 GPIO_ACTIVE_HIGH>, /* I/O controller "MCU on" */ + <&gpio0 13 GPIO_ACTIVE_HIGH>; /* DCDC latch — cuts the rail */ + }; + + /* Battery ADC: P0.31 (AIN7). + * + * UNVERIFIED — needs a DMM check against a known cell voltage before + * this is trusted. ZephCore's convention is + * multiplier = divider_ratio * 1.005 * 3600 + * (3.6 V full scale from ADC_GAIN_1_6 + ADC_REF_INTERNAL, +0.5% for + * nRF SAADC gain error). The divider ratio itself is disputed between + * the two upstreams: Arduino MeshCore's constant implies ~1.73 + * (→ 6259, the RAK4631-family value), Meshtastic's r1-neo variant.h + * states 1.667 (→ 6031). 6146 splits them and is within ~2% of either; + * correct it once the board has been measured. */ + zephyr,user { + io-channels = <&adc 7>; + vbat-mv-multiplier = <6146>; + }; + + /* Buzzer on PWM0 channel 0 */ + pwmbuzzer { + compatible = "pwm-leds"; + buzzer: buzzer { + pwms = <&pwm0 0 PWM_MSEC(20) PWM_POLARITY_NORMAL>; + }; + }; + + aliases { + led0 = &led_green; + led1 = &led_blue; + sw0 = &user_button; + watchdog0 = &wdt0; + buzzer = &buzzer; + lora0 = &lora; + }; +}; + +®0 { + status = "okay"; +}; + +®1 { + regulator-initial-mode = ; +}; + +&adc { + status = "okay"; + #address-cells = <1>; + #size-cells = <0>; + + /* Battery voltage */ + channel@7 { + reg = <7>; + zephyr,gain = "ADC_GAIN_1_6"; + zephyr,reference = "ADC_REF_INTERNAL"; + zephyr,acquisition-time = ; + zephyr,input-positive = ; + zephyr,resolution = <12>; + }; +}; + +&uicr { + nfct-pins-as-gpios; +}; + +&gpiote { + status = "okay"; +}; + +&gpio0 { + status = "okay"; + + /* Latch the DCDC rail on. Must happen as early as possible during boot — + * gpio-hog runs at PRE_KERNEL, before any driver. Released again at + * shutdown via &poweroff_gpios (a hog is an init-time config, not a + * runtime reservation, so both can reference the same pin). */ + pwr_dcdc_en { + gpio-hog; + gpios = <13 GPIO_ACTIVE_HIGH>; + output-high; + line-name = "pwr-dcdc-en"; + }; + + /* Tell the I/O controller the MCU is up. Enables passthrough of the + * user button and the LEDs; also released at shutdown. */ + pwr_io_en { + gpio-hog; + gpios = <29 GPIO_ACTIVE_HIGH>; + output-high; + line-name = "pwr-io-en"; + }; +}; + +&gpio1 { + status = "okay"; + + /* SX1262 module power-enable: held HIGH. Hogged rather than declared as + * the radio's rx-enable-gpios because it is module power, not a + * per-TX/RX antenna switch — the native sx126x driver must not toggle it + * per direction. Dropped at shutdown via &poweroff_gpios; during normal + * System OFF the radio is instead parked by its reset-gpios (see + * ui_prepare_for_system_off step 4). */ + sx1262_power_en { + gpio-hog; + gpios = <5 GPIO_ACTIVE_HIGH>; + output-high; + line-name = "sx1262-power-en"; + }; + + /* GPS power-enable on P1.01 is NOT hogged here: the pin is driven by + * the luatos,air530z driver, which owns this line via on-off-gpios + * (see &uart0) so ZephyrGPSManager can power the receiver down. */ +}; + +/* PWM0 for buzzer */ +&pwm0 { + status = "okay"; + pinctrl-0 = <&pwm0_default>; + pinctrl-1 = <&pwm0_sleep>; + pinctrl-names = "default", "sleep"; +}; + +/* GNSS on UART0. + * + * Module identity is unconfirmed: muzi advertise "dual GPS/BDS", both + * upstreams default it to 9600 baud, and muzi sell L76K modules separately — + * all consistent with a CASIC part, but nobody has read a part number off the + * PCB. luatos,air530z is safe either way: its init script is + * MODEM_CHAT_SCRIPT_CMD_RESP_NONE under MODEM_CHAT_SCRIPT_NO_ABORT_DEFINE, so + * the CASIC $PCAS03/$PMTK869 sentences are fire-and-forget and a non-CASIC + * module simply ignores them and streams plain NMEA. It also gives us + * on-off-gpios power gating for free. If a teardown ever shows a non-CASIC + * module and the vendor sentences prove harmful, fall back to + * gnss-nmea-generic plus a gps-enable alias (see heltec_t114 / thinknode_m9). + * + * See the pinctrl dtsi for why TX/RX are P0.24/P0.25 and not the reverse. */ +&uart0 { + compatible = "nordic,nrf-uarte"; + status = "okay"; + current-speed = <9600>; + pinctrl-0 = <&uart0_default>; + pinctrl-1 = <&uart0_sleep>; + pinctrl-names = "default", "sleep"; + + gnss: gnss { + compatible = "luatos,air530z"; + on-off-gpios = <&gpio1 1 GPIO_ACTIVE_HIGH>; + }; +}; + +/* I2C0 */ +&i2c0 { + compatible = "nordic,nrf-twim"; + status = "okay"; + clock-frequency = ; + pinctrl-0 = <&i2c0_default>; + pinctrl-1 = <&i2c0_sleep>; + pinctrl-names = "default", "sleep"; + + /* All supported environment & power sensors — auto-detected at runtime */ + #include "../../common/sensors-i2c.dtsi" + /* Battery-backed RX8130CE @ 0x32 — opt in to boot-time time restore + * (auto-detected at runtime among the supported chips). */ + #include "../../common/rtc-i2c.dtsi" +}; + +/* SPI1 for SX1262 */ +&spi1 { + compatible = "nordic,nrf-spim"; + status = "okay"; + cs-gpios = <&gpio1 10 GPIO_ACTIVE_LOW>; + pinctrl-0 = <&spi1_default>; + pinctrl-1 = <&spi1_sleep>; + pinctrl-names = "default", "sleep"; + + lora: lora@0 { + compatible = "semtech,sx1262"; + reg = <0>; + spi-max-frequency = <8000000>; + + /* LoRa control pins. reset-gpios is wired and used — Meshtastic + * drives P1.06 as SX126X_RESET; Arduino MeshCore's RADIOLIB_NC is + * the outlier. ZephCore needs it: ui_prepare_for_system_off() + * parks the radio in HW reset before System OFF. */ + reset-gpios = <&gpio1 6 GPIO_ACTIVE_LOW>; + busy-gpios = <&gpio1 14 GPIO_ACTIVE_HIGH>; + dio1-gpios = <&gpio1 15 (GPIO_PULL_DOWN | GPIO_ACTIVE_HIGH)>; + + /* DIO2 drives RF switch for TX */ + dio2-tx-enable; + + /* TCXO voltage 1.8V via DIO3 */ + dio3-tcxo-voltage = ; + tcxo-power-startup-delay-ms = <10>; + + /* RX boosted mode for better sensitivity */ + rx-boosted; + }; +}; + +zephyr_udc0: &usbd { + compatible = "nordic,nrf-usbd"; + status = "okay"; + + cdc_acm_uart: cdc_acm_uart { + compatible = "zephyr,cdc-acm-uart"; + }; +}; + +/* Arduino MeshCore compatible partition layout (SoftDevice v6) */ +#include "../../common/nrf52_partitions_sdv6.dtsi" +#include "../../common/nrf52_wakeup.dtsi" diff --git a/zephcore/boards/nrf52840/muziworks_r1neo/muziworks_r1neo_nrf52840.yaml b/zephcore/boards/nrf52840/muziworks_r1neo/muziworks_r1neo_nrf52840.yaml new file mode 100644 index 0000000..ad7b7a7 --- /dev/null +++ b/zephcore/boards/nrf52840/muziworks_r1neo/muziworks_r1neo_nrf52840.yaml @@ -0,0 +1,22 @@ +identifier: muziworks_r1neo +name: muzi works R1 Neo +type: mcu +arch: arm +ram: 256 +flash: 1024 +toolchain: + - zephyr + - gnuarmemb + - xtools +supported: + - adc + - ble + - gpio + - gnss + - i2c + - nvs + - pwm + - spi + - usb_device + - watchdog +vendor: muziworks diff --git a/zephcore/boards/nrf52840/muziworks_r1neo/muziworks_r1neo_nrf52840_defconfig b/zephcore/boards/nrf52840/muziworks_r1neo/muziworks_r1neo_nrf52840_defconfig new file mode 100644 index 0000000..fca3081 --- /dev/null +++ b/zephcore/boards/nrf52840/muziworks_r1neo/muziworks_r1neo_nrf52840_defconfig @@ -0,0 +1,32 @@ +# Copyright (c) 2026 ZephCore +# SPDX-License-Identifier: MIT +# muzi works R1 Neo default configuration +# (most subsystems also come from zephcore_common.conf + nrf52_common.conf; +# these are the board essentials, kept self-contained like other custom boards) + +# MPU + hardware stack protection +CONFIG_ARM_MPU=y +CONFIG_HW_STACK_PROTECTION=y + +# GPIO + pin control +CONFIG_GPIO=y +CONFIG_PINCTRL=y + +# Console over USB CDC-ACM (DT node + nrf52_common.conf USB stack) +CONFIG_CONSOLE=y +CONFIG_UART_CONSOLE=y +CONFIG_SERIAL=y +CONFIG_UART_LINE_CTRL=y + +# Flash / NVS (LittleFS app data partition) +CONFIG_FLASH=y +CONFIG_FLASH_MAP=y +CONFIG_NVS=y + +# SPI for LoRa, ADC for battery sense +CONFIG_SPI=y +CONFIG_ADC=y + +# Build UF2 by default +CONFIG_BUILD_OUTPUT_UF2=y +CONFIG_USE_DT_CODE_PARTITION=y diff --git a/zephcore/boards/supported_boards.md b/zephcore/boards/supported_boards.md index eb94e86..2fd0da9 100644 --- a/zephcore/boards/supported_boards.md +++ b/zephcore/boards/supported_boards.md @@ -21,6 +21,7 @@ promicro_sx1262 heltec_t114 heltec_t096 gat562_30s +muziworks_r1neo ``` > **RAK WisMesh Pocket** (WisBlock pocket): use `-b rak4631` — same board string and firmware as **RAK4631**. diff --git a/zephcore/dts/bindings/input/zephcore,input-multi-tap.yaml b/zephcore/dts/bindings/input/zephcore,input-multi-tap.yaml index 65369cb..0fbf94b 100644 --- a/zephcore/dts/bindings/input/zephcore,input-multi-tap.yaml +++ b/zephcore/dts/bindings/input/zephcore,input-multi-tap.yaml @@ -11,7 +11,7 @@ description: | The tap-codes array maps tap counts to output key codes: tap-codes[0] = code emitted for 1 tap tap-codes[1] = code emitted for 2 taps - ...up to 4 entries. + ...up to 5 entries. If the tap count reaches the length of tap-codes, the code is emitted immediately (no need to wait for more taps). Otherwise the tap-delay-ms @@ -47,7 +47,7 @@ properties: description: | Array of output key codes for each tap count (1-based). Entry 0 = 1 tap, entry 1 = 2 taps, etc. - Maximum 4 entries. + Maximum 5 entries. tap-delay-ms: type: int diff --git a/zephcore/dts/bindings/power/zephcore,poweroff-gpios.yaml b/zephcore/dts/bindings/power/zephcore,poweroff-gpios.yaml new file mode 100644 index 0000000..68a8f4b --- /dev/null +++ b/zephcore/dts/bindings/power/zephcore,poweroff-gpios.yaml @@ -0,0 +1,48 @@ +# Copyright 2026 ZephCore +# SPDX-License-Identifier: MIT + +description: | + Board power-latch release list. + + Lists GPIOs that must be driven to their INACTIVE level as the very last + step before sys_poweroff(), after every other peripheral has been shut down + and after the sw0 wake button has been released. + + This exists for boards whose supply rail is latched on by the MCU rather + than by a mechanical switch — typically a soft-power design where a + companion I/O controller holds the regulator enabled only while the MCU + keeps a "hold" line asserted. On such boards sys_poweroff() alone is not a + real power-off: the SoC enters System OFF at ~1 uA, but nRF GPIO output + latches persist across the transition, so the hold line stays asserted and + the rail (plus whatever else it feeds) keeps draining the battery. + + Order matters and is preserved: list downstream loads first and the rail + latch itself last, so the board is quiesced before the supply is cut. + + Boards that do not define a node with this compatible are completely + unaffected — the release loop compiles to nothing. + + Note that these are usually the same pins a board asserts at boot via + gpio-hog. The hog performs the early boot assert (PRE_KERNEL, before any + driver runs); this node performs the shutdown release. Referencing a + hogged pin here is intentional and safe — a gpio-hog is an init-time + configuration, not a runtime reservation. + + Example (muzi works R1 Neo — I/O controller gated DCDC rail): + poweroff_gpios: poweroff-gpios { + compatible = "zephcore,poweroff-gpios"; + gpios = <&gpio1 5 GPIO_ACTIVE_HIGH>, /* SX1262 module power */ + <&gpio0 29 GPIO_ACTIVE_HIGH>, /* I/O controller "MCU on" */ + <&gpio0 13 GPIO_ACTIVE_HIGH>; /* DCDC latch - cuts the rail */ + }; + +compatible: "zephcore,poweroff-gpios" + +properties: + gpios: + type: phandle-array + required: true + description: | + GPIOs to drive INACTIVE (per each entry's own GPIO_ACTIVE_* flag) + immediately before sys_poweroff(). Listed in shutdown order: + downstream loads first, supply latch last. diff --git a/zephcore/helpers/ui/ui_common.c b/zephcore/helpers/ui/ui_common.c index 0144ea0..24ac044 100644 --- a/zephcore/helpers/ui/ui_common.c +++ b/zephcore/helpers/ui/ui_common.c @@ -291,6 +291,11 @@ void ui_invalidate_battery_cache(void) * * Non-nRF platforms skip the SENSE block; they rely on Zephyr's wakeup-source * DT property which is honoured by their respective GPIO drivers. + * + * Boards with a soft-power rail (the MCU latches its own supply on) add a + * "zephcore,poweroff-gpios" node; its pins are released last, which cuts the + * rail outright instead of leaving it latched through System OFF. Boards + * without that node are unaffected — step 6 compiles to nothing. */ void ui_prepare_for_system_off(void) { @@ -353,6 +358,34 @@ void ui_prepare_for_system_off(void) #undef _SW0_FLAGS } #endif /* CONFIG_SOC_FAMILY_NORDIC_NRF && sw0 */ + + /* 6. Release the board power latch — must be dead last. + * + * Only present on soft-power boards (see zephcore,poweroff-gpios). The + * rail is cut here rather than in step 3 because the pins are ordered + * loads-first/latch-last, and because step 5 must have observed the + * button release first: on these boards the button is also the power-on + * input, so dropping the latch while it is still held would let the I/O + * controller re-latch the rail immediately. + * + * On battery this does not return — the supply is gone mid-loop, which + * is the intended outcome. On USB the rail may be held up externally, in + * which case we simply fall through to the caller's sys_poweroff() and + * land in System OFF as before. */ +#if DT_HAS_COMPAT_STATUS_OKAY(zephcore_poweroff_gpios) + { +#define _PWROFF_NODE DT_COMPAT_GET_ANY_STATUS_OKAY(zephcore_poweroff_gpios) + static const struct gpio_dt_spec poweroff_gpios[] = { + DT_FOREACH_PROP_ELEM_SEP(_PWROFF_NODE, gpios, + GPIO_DT_SPEC_GET_BY_IDX, (,)) + }; + + for (size_t i = 0; i < ARRAY_SIZE(poweroff_gpios); i++) { + gpio_pin_configure_dt(&poweroff_gpios[i], GPIO_OUTPUT_INACTIVE); + } +#undef _PWROFF_NODE + } +#endif } /* ========== Low-battery auto-shutdown ==========