add muziworks R1 Neo

This commit is contained in:
liquidraver
2026-07-21 15:04:25 +02:00
parent 3e04c11621
commit 3c62747ca7
22 changed files with 800 additions and 6 deletions
+1
View File
@@ -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
+1
View File
@@ -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
+1 -1
View File
@@ -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.
+5 -1
View File
@@ -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,
}
+48 -1
View File
@@ -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
+1
View File
@@ -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 | - | - | - |
+1 -1
View File
@@ -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")
+1
View File
@@ -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.
@@ -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
* };
* };
*/
@@ -0,0 +1,6 @@
# Copyright (c) 2026 ZephCore
#
# SPDX-License-Identifier: MIT
config BOARD_MUZIWORKS_R1NEO
select SOC_NRF52840_QIAA
@@ -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.
@@ -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)
@@ -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
@@ -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
@@ -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 = <NRF_PSEL(UART_TX, 0, 24)>;
};
group2 {
psels = <NRF_PSEL(UART_RX, 0, 25)>;
bias-pull-up;
};
};
uart0_sleep: uart0_sleep {
group1 {
psels = <NRF_PSEL(UART_TX, 0, 24)>,
<NRF_PSEL(UART_RX, 0, 25)>;
low-power-enable;
};
};
/* I2C0 - RX8130CE RTC (0x32) + optional Grove-style sensors */
i2c0_default: i2c0_default {
group1 {
psels = <NRF_PSEL(TWIM_SDA, 0, 19)>,
<NRF_PSEL(TWIM_SCL, 0, 20)>;
};
};
i2c0_sleep: i2c0_sleep {
group1 {
psels = <NRF_PSEL(TWIM_SDA, 0, 19)>,
<NRF_PSEL(TWIM_SCL, 0, 20)>;
low-power-enable;
};
};
/* PWM0 for buzzer */
pwm0_default: pwm0_default {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 3)>;
};
};
pwm0_sleep: pwm0_sleep {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 3)>;
low-power-enable;
};
};
/* SPI1 for SX1262 - CS controlled manually */
spi1_default: spi1_default {
group1 {
psels = <NRF_PSEL(SPIM_SCK, 1, 11)>,
<NRF_PSEL(SPIM_MOSI, 1, 12)>,
<NRF_PSEL(SPIM_MISO, 1, 13)>;
};
};
spi1_sleep: spi1_sleep {
group1 {
psels = <NRF_PSEL(SPIM_SCK, 1, 11)>,
<NRF_PSEL(SPIM_MOSI, 1, 12)>,
<NRF_PSEL(SPIM_MISO, 1, 13)>;
low-power-enable;
};
};
};
@@ -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 <nordic/nrf52840_qiaa.dtsi>
#include <nordic/nrf52840_partition.dtsi>
#include <zephyr/dt-bindings/lora/sx126x.h>
#include <zephyr/dt-bindings/adc/nrf-saadc.h>
#include <zephyr/dt-bindings/input/input-event-codes.h>
#include <zephyr/dt-bindings/pwm/pwm.h>
#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 = <INPUT_KEY_0>;
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 = <INPUT_KEY_0>;
short-codes = <INPUT_KEY_A>;
long-codes = <INPUT_KEY_F>;
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 = <INPUT_KEY_A>;
tap-codes = <INPUT_KEY_1 INPUT_KEY_B INPUT_KEY_D INPUT_KEY_C INPUT_KEY_E>;
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;
};
};
&reg0 {
status = "okay";
};
&reg1 {
regulator-initial-mode = <NRF5X_REG_MODE_DCDC>;
};
&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 = <ADC_ACQ_TIME(ADC_ACQ_TIME_MICROSECONDS, 10)>;
zephyr,input-positive = <NRF_SAADC_AIN7>;
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 = <I2C_BITRATE_FAST>;
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 = <SX126X_DIO3_TCXO_1V8>;
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"
@@ -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
@@ -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
+1
View File
@@ -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**.
@@ -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
@@ -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.
+33
View File
@@ -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 ==========