add meshnology w12

This commit is contained in:
liquidraver
2026-08-30 22:29:21 +02:00
parent ccb7fc748d
commit efa5240b69
17 changed files with 750 additions and 27 deletions
+1
View File
@@ -70,6 +70,7 @@ ESP32_boards=(
heltec_wifi_lora32_v43/esp32s3/procpu
heltec_wireless_tracker/esp32s3/procpu
heltec_wireless_tracker_v2/esp32s3/procpu
meshnology_w12/esp32s3/procpu
ttgo_tbeam/esp32/procpu
)
+7 -13
View File
@@ -231,18 +231,12 @@ if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/patches/modules/hal-espressif)
)
endif()
# Copy new files into Zephyr tree
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/patches/zephyr-new)
file(GLOB_RECURSE ZEPHCORE_NEW_FILES
RELATIVE ${CMAKE_CURRENT_SOURCE_DIR}/patches/zephyr-new
${CMAKE_CURRENT_SOURCE_DIR}/patches/zephyr-new/*)
foreach(REL_PATH ${ZEPHCORE_NEW_FILES})
set(SRC_FILE ${CMAKE_CURRENT_SOURCE_DIR}/patches/zephyr-new/${REL_PATH})
set(DST_FILE ${ZEPHYR_DIR}/${REL_PATH})
configure_file(${SRC_FILE} ${DST_FILE} COPYONLY)
message(STATUS " [zephyr-new] ${REL_PATH}")
endforeach()
endif()
# Copy new files into Zephyr tree.
# Shared with sysbuild/CMakeLists.txt, which must do the same thing before
# MCUboot's devicetree pass — see the module for why.
set(ZEPHCORE_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR})
set(ZEPHCORE_ZEPHYR_DIR ${ZEPHYR_DIR})
include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/zephyr_new_files.cmake)
# Custom board root: boards/<mcu>/<board>/ discovered via BOARD_ROOT
list(APPEND BOARD_ROOT ${CMAKE_CURRENT_SOURCE_DIR})
@@ -329,7 +323,7 @@ if(BOARD MATCHES ".*nrf52.*" OR BOARD MATCHES "rak4631" OR BOARD MATCHES "rak_wi
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")
elseif(BOARD MATCHES ".*esp32.*" OR BOARD MATCHES "station_g2" OR BOARD MATCHES "thinknode_m9" OR BOARD MATCHES "lilygo_tlora_c6" OR BOARD MATCHES "heltec_wifi_lora32_v3" OR BOARD MATCHES "heltec_wifi_lora32_v4")
elseif(BOARD MATCHES ".*esp32.*" OR BOARD MATCHES "station_g2" OR BOARD MATCHES "thinknode_m9" OR BOARD MATCHES "lilygo_tlora_c6" OR BOARD MATCHES "heltec_wifi_lora32_v3" OR BOARD MATCHES "heltec_wifi_lora32_v4" OR BOARD MATCHES "meshnology_w12")
set(ZEPHCORE_PLATFORM_CONF "${CMAKE_CURRENT_SOURCE_DIR}/boards/common/esp32_common.conf")
elseif(DEFINED BOARD_QUALIFIERS AND BOARD_QUALIFIERS MATCHES ".*esp32.*")
set(ZEPHCORE_PLATFORM_CONF "${CMAKE_CURRENT_SOURCE_DIR}/boards/common/esp32_common.conf")
@@ -0,0 +1,3 @@
config HEAP_MEM_POOL_ADD_SIZE_BOARD
int
default 4096
@@ -0,0 +1,3 @@
config BOARD_MESHNOLOGY_W12
select SOC_ESP32S3_WROOM_N16R8
select SOC_ESP32S3_PROCPU if BOARD_MESHNOLOGY_W12_ESP32S3_PROCPU
@@ -0,0 +1,7 @@
if(NOT "${OPENOCD}" MATCHES "^${ESPRESSIF_TOOLCHAIN_PATH}/.*")
set(OPENOCD OPENOCD-NOTFOUND)
endif()
find_program(OPENOCD openocd PATHS ${ESPRESSIF_TOOLCHAIN_PATH}/openocd-esp32/bin NO_DEFAULT_PATH)
include(${ZEPHYR_BASE}/boards/common/esp32.board.cmake)
include(${ZEPHYR_BASE}/boards/common/openocd.board.cmake)
@@ -0,0 +1,44 @@
# Meshnology W12 "WiFi LoRa 32 V5" — ESP32-S3R8 + LR2021 + GC1109 FEM
#
# Hardware (schematic W12-MB-V0.2, archived in devdocs/w12/vendor/):
# - ESP32-S3R8 (WROOM-1 N16R8): 16 MB QIO flash, 8 MB OPI PSRAM
# - LR2021 on SPI2 (GPIO8 NSS, 9 SCK, 10 MOSI, 11 MISO, 12 RST, 13 BUSY),
# IRQ on GPIO14 = chip DIO8, plain crystal (no TCXO)
# - GC1109 sub-GHz PA/LNA on chip DIO9/10/11, rail enable GPIO4 (active HIGH)
# - RFX2402E 2.4 GHz PA on chip DIO5/6, rail enable GPIO3 (held LOW, unused)
# - SSD1315 128x64 bicolour OLED @ 0x3C on I2C0 (SDA 17, SCL 18, RST 47)
# - Battery ADC GPIO1 = ADC1_CH0, divider enable GPIO2 (active HIGH), ratio 4.90
# - Vext GPIO45 active LOW; user button GPIO0; RGB LED GPIO46 (unused, see below)
# - GNSS header on UART1 (unpopulated from the factory)
#
# Include order: prj.conf → zephcore_common.conf → esp32_common.conf → board.conf
CONFIG_ZEPHCORE_BOARD_NAME="Meshnology W12"
CONFIG_BT_DIS_MODEL_NUMBER_STR="Meshnology W12 LR2021"
CONFIG_ZEPHCORE_RADIO_LR2021=y
# Flash — 16 MB (override the 4 MB esp32_common.conf default)
CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y
# 8 MB OPI PSRAM. Enable is auto-detected from DTS by Kconfig.psram; only the
# mode is set here, same as station_g2 / thinknode_m9 / xiao_esp32s3.
CONFIG_SPIRAM_MODE_OCT=y
# ========== TX power: the number here is CHIP output, not antenna output ==========
#
# The GC1109 sits between the LR2021 and the antenna and adds roughly 26 dB.
# The vendor rates the finished board at 30 +/- 1 dBm conducted, and MeshCore's
# variant notes the part's absolute maximum TX-port input is +5 dBm with full
# saturation already reached around 3-4 dBm. So 4 dBm at the chip is both the
# most that is useful and the most that is safe: beyond it the extra drive turns
# into distortion and heat, not range.
#
# Cap and default are deliberately the same value. Unlike Station G2 (default 15,
# cap 19) there is no useful headroom above saturation to leave a user, and the
# knob still spans -9..4 downward — roughly 17..30 dBm at the antenna — which is
# the range that matters for staying inside a regional ERP limit. Anyone setting
# this for EU 868 should note that even -9 dBm at the chip is well above 14 dBm
# ERP once the FEM has had its way.
CONFIG_ZEPHCORE_DEFAULT_TX_POWER_DBM=4
CONFIG_ZEPHCORE_MAX_TX_POWER_DBM=4
@@ -0,0 +1,72 @@
/*
* SPDX-License-Identifier: MIT
* Meshnology W12 — ZephCore overlay
*
* The board's own DTS (meshnology_w12_procpu.dts) already declares the LR2021,
* the SSD1315, the FEM rails, the battery ADC, the GNSS UART and the button.
* This overlay only adds the ZephCore integration on top: the runtime sensor
* bus, the LittleFS mount, the button gesture chain and the USB companion node.
*/
#include <zephyr/dt-bindings/input/input-event-codes.h>
/ {
/* Button chain — same mapping as RAK4631 / Heltec V3.
* GPIO0 is the only button the firmware sees; the second hole in the
* case is the ESP32 EN/reset key, which never reaches software. */
user_btn_longpress {
compatible = "zephyr,input-longpress";
input = <&buttons>;
input-codes = <INPUT_KEY_0>;
short-codes = <INPUT_KEY_A>;
long-codes = <INPUT_KEY_ENTER>;
long-delay-ms = <1000>;
};
page_btn_multitap {
compatible = "zephcore,input-multi-tap";
input-codes = <INPUT_KEY_A>;
tap-codes = <INPUT_KEY_1 INPUT_KEY_LEFT>;
tap-delay-ms = <400>;
};
chosen {
/* Plain-UART companion backend (CONFIG_ZEPHCORE_COMPANION_SERIAL).
* uart0 is GPIO43/44 on the expansion header — there is no bridge
* chip, so this is a wired-header option rather than the USB-C port.
* Inert unless that conf is built. */
zephcore,companion-uart = &uart0;
};
};
/* Runtime-probed I2C sensors share the OLED bus, which is broken out on the
* display connector and the expansion header. */
&i2c0 {
#include "../../common/sensors-i2c.dtsi"
};
/*
* Flash partitions live in partitions.overlay (this dir) so the identical map
* is fed to both the app and the MCUboot image — see that file and
* sysbuild/CMakeLists.txt.
*/
/* LittleFS auto-mount — standard /lfs mount point */
#include "../../common/filesystem.dtsi"
/* USB OTG companion transport. Including this dtsi is also the signal that
* CMakeLists.txt greps for to auto-enable boards/common/esp32s3_usb.conf on
* companion builds, so the companion speaks its protocol over the USB-C port.
*
* That matters more here than on most boards: the W12 has no USB-UART bridge,
* so the USB-C port is the ONLY wired route in. The cost is the documented one
* — once the port is USB OTG CDC-ACM rather than USB Serial/JTAG, esptool can
* no longer auto-reset the chip into download mode. Use `start dfu`, an
* Arduino-style 1200-baud touch (the vendor's own PlatformIO board JSON sets
* use_1200bps_touch for the same reason), or hold BOOT while plugging in.
*
* The console reroute to uart0 lives in the auto-paired esp32s3_usb.overlay and
* applies only on those companion builds. Do NOT include
* esp32s3_console_uart0.dtsi here — repeater and debug builds keep their USB
* Serial/JTAG console, which is the only console they have. */
#include "../../common/esp32s3_usb_otg.dtsi"
@@ -0,0 +1,6 @@
board:
name: meshnology_w12
full_name: Meshnology W12 LR2021
vendor: meshnology
socs:
- name: esp32s3
@@ -0,0 +1,67 @@
/*
* SPDX-License-Identifier: MIT
* Meshnology W12 — Pin Control Definitions
*
* Verified against vendor schematic W12-MB-V0.2 (devdocs/w12/vendor/).
*
* UART0: TX=GPIO43, RX=GPIO44
* I2C0: SDA=GPIO17, SCL=GPIO18 (SSD1315 OLED + expansion header)
* SPI2: SCLK=GPIO9, MOSI=GPIO10, MISO=GPIO11 (LR2021 LoRa)
* UART1: TX=GPIO38, RX=GPIO39 (GNSS header, unpopulated from factory)
*
* The FEM control pins (GPIO3/GPIO4) and the LoRa NSS/RST/BUSY/IRQ lines are
* plain GPIO — no pinctrl entries needed.
*/
#include <zephyr/dt-bindings/pinctrl/esp-pinctrl-common.h>
#include <zephyr/dt-bindings/pinctrl/esp32s3-pinctrl.h>
#include <zephyr/dt-bindings/pinctrl/esp32s3-gpio-sigmap.h>
&pinctrl {
uart0_default: uart0_default {
group1 {
pinmux = <UART0_TX_GPIO43>;
output-high;
};
group2 {
pinmux = <UART0_RX_GPIO44>;
bias-pull-up;
};
};
/* GNSS header. The vendor pin table names these from the GNSS module's
* point of view — "GNSS_RX" (GPIO38) is the module's RX, i.e. the SoC's
* TX, and "GNSS_TX" (GPIO39) is the SoC's RX. Module-perspective naming is
* the usual trap here (same one as lilygo_timpulse_plus and the RAK3401);
* the schematic net names settle it and this is the wiring that follows
* from them. */
uart1_default: uart1_default {
group1 {
pinmux = <UART1_TX_GPIO38>;
output-high;
};
group2 {
pinmux = <UART1_RX_GPIO39>;
bias-pull-up;
};
};
i2c0_default: i2c0_default {
group1 {
pinmux = <I2C0_SDA_GPIO17>,
<I2C0_SCL_GPIO18>;
drive-open-drain;
};
};
spim2_default: spim2_default {
group1 {
pinmux = <SPIM2_MISO_GPIO11>,
<SPIM2_SCLK_GPIO9>;
};
group2 {
pinmux = <SPIM2_MOSI_GPIO10>;
output-low;
};
};
};
@@ -0,0 +1,341 @@
/*
* SPDX-License-Identifier: MIT
* Meshnology W12 "WiFi LoRa 32 V5" — ZephCore procpu DTS
*
* Every pin and polarity below is taken from the vendor schematic
* W12-MB-V0.2 and the vendor pin-assignment sheet, both archived in
* devdocs/w12/vendor/. Several of them contradict what the Arduino-side
* variants for this board claim; the schematic decides every time — see
* devdocs/w12/HANDOVER_MESHNOLOGY_W12.md for the list.
*
* Hardware:
* MCU: ESP32-S3R8 (WROOM-1 N16R8) — 16 MB QIO flash, 8 MB OPI PSRAM
* Radio: Semtech LR2021 on SPI2, plain 32 MHz crystal (no TCXO)
* FEM: GC1109 sub-GHz PA/LNA (~30 dBm out) + RFX2402E 2.4 GHz PA
* OLED: SSD1315 128x64 (SSD1306-compatible) on I2C0, bicolour glass
* Button: GPIO0 (BOOT doubles as the user key)
* BatADC: GPIO1 = ADC1_CH0, divider enable GPIO2 active-HIGH
* Vext: GPIO45 active-LOW (AO3401 P-FET gating an LDO enable)
* GNSS: header on UART1, unpopulated from the factory
* Console: native USB Serial/JTAG (there is no USB-UART bridge)
*
* LoRa SPI2: SCLK=9, MOSI=10, MISO=11, NSS=8
* LoRa ctrl: RESET=12, BUSY=13, IRQ=14 (chip DIO8)
* FEM rails: sub-GHz enable GPIO4, 2.4 GHz enable GPIO3 (both active-HIGH)
* FEM switch: chip DIO9=CTX, DIO10=CPS, DIO11=CSD, DIO5/6 = 2.4 GHz TX/RX EN
*/
/dts-v1/;
#include <espressif/esp32s3/esp32s3_wroom_n16r8.dtsi>
#include "meshnology_w12-pinctrl.dtsi"
#include <espressif/partitions_0x0_default_16M.dtsi>
#include <zephyr/dt-bindings/input/input-event-codes.h>
#include <zephyr/dt-bindings/adc/adc.h>
/ {
model = "Meshnology W12 PROCPU";
compatible = "meshnology,w12";
aliases {
uart-0 = &uart0;
i2c-0 = &i2c0;
lora0 = &lora0;
sw0 = &button0;
watchdog0 = &wdt0;
};
buttons: buttons {
compatible = "gpio-keys";
button0: button_0 {
gpios = <&gpio0 0 (GPIO_PULL_UP | GPIO_ACTIVE_LOW)>;
label = "USER Key";
zephyr,code = <INPUT_KEY_0>;
};
};
/* Vext rail — GPIO45, ACTIVE LOW.
*
* Schematic sheet 1: GPIO45 drives the gate of Q1 (AO3401, P-channel)
* through R3 1k. Q1's source sits on VDD_3V3 with R2 100k gate-to-source
* holding it off, and its drain feeds the EN pin of U4 (TLV75733 LDO),
* which R5 100k pulls to ground. So GPIO45 LOW turns the P-FET on, pulls
* EN high and brings VEXT_3V3 up; GPIO45 high or floating leaves the rail
* dead. That makes this active-LOW, and it makes the rail OFF by default.
*
* MeshCore's variant declares PIN_VEXT_EN_ACTIVE=HIGH, which is wrong for
* this circuit. Do not "fix" this to active-HIGH to match it without
* re-reading sheet 1 — the whole peripheral rail, OLED included, hangs
* off this pin.
*
* GPIO45 is also an ESP32-S3 strapping pin (VDD_SPI voltage select) with
* an internal pull-down at reset. That default reads as 0, which is the
* same level we drive to enable the rail, so there is no boot conflict —
* but never add a pull-up here, it would flip the strap. */
vext_enable: vext-enable {
compatible = "regulator-fixed";
regulator-name = "vext";
enable-gpios = <&gpio1 13 GPIO_ACTIVE_LOW>; /* GPIO45 */
regulator-boot-on;
regulator-always-on;
};
/* Sub-GHz front end rail — GPIO4 (PA_EN_M), ACTIVE HIGH.
*
* Drives the EN pin of the adjustable regulator that supplies the GC1109
* (net VR_PA). Its pull-up R56 is marked NC on the schematic, so the rail
* is off until firmware asserts this — the radio is deaf and mute without
* it, in both directions, because the GC1109 carries the RX LNA too.
*
* startup-delay-us matches the 100 ms MeshCore waits after enabling the
* FEM before touching the radio. Expressed as a regulator startup delay it
* is paid once at regulator init, which runs at priority 75 — ahead of the
* LoRa driver — instead of being a sleep on the radio init path. */
fem_lf_power: fem-lf-power {
compatible = "regulator-fixed";
regulator-name = "fem-lf";
enable-gpios = <&gpio0 4 GPIO_ACTIVE_HIGH>; /* GPIO4 = PA_EN_M */
startup-delay-us = <100000>;
regulator-boot-on;
regulator-always-on;
};
/* 2.4 GHz front end rail — GPIO3 (PA_EN_G), ACTIVE HIGH, deliberately
* left DISABLED. ZephCore is sub-GHz only, so the RFX2402E never needs
* power, and leaving its rail up would burn current for nothing.
*
* This is a regulator rather than a comment because an undriven GPIO3
* floats: R38 (the EN pull-up) is marked NC, so nothing else defines the
* level. regulator_fixed_init() configures the enable pin
* GPIO_OUTPUT_INACTIVE when the node has neither regulator-boot-on nor
* regulator-always-on, which actively drives GPIO3 LOW at boot. That is
* the entire purpose of this node — do not add boot-on/always-on. */
fem_hf_power: fem-hf-power {
compatible = "regulator-fixed";
regulator-name = "fem-hf";
enable-gpios = <&gpio0 3 GPIO_ACTIVE_HIGH>; /* GPIO3 = PA_EN_G */
};
/* Battery divider enable — GPIO2, ACTIVE HIGH.
* GPIO2 -> R52 1k -> base of Q7 (S8050 NPN); Q7's collector pulls the gate
* of Q6 (AO3401A P-FET) down through R48 1k, switching BAT onto the
* R50 390K / R51 100K divider. Driving GPIO2 high therefore enables the
* divider; undriven, the divider is open and GPIO1 reads a hard 0 mV.
* ZephyrBoard toggles this per reading, so the divider draws nothing
* between samples. */
vbat_enable: vbat-enable {
compatible = "regulator-fixed";
regulator-name = "vbat-enable";
enable-gpios = <&gpio0 2 GPIO_ACTIVE_HIGH>; /* GPIO2 = ADC_CTRL */
};
/* GNSS module power — GPIO48 (GNSS_CTRL), ACTIVE LOW.
* The header is unpopulated on shipping units; the node is unconditional
* so a user-fitted module just works, exactly like the RAK4631's optional
* RAK1910. With nothing fitted the driver simply never sees a fix. */
gps_en: gps-enable {
compatible = "gpio-leds";
gps_enable_pin: gps_enable {
gpios = <&gpio1 16 GPIO_ACTIVE_LOW>; /* GPIO48 */
label = "GNSS Enable";
};
};
/* Battery ADC — ZephCore reads this via zephyr,user { io-channels }.
* Multiplier = internal reference (1100 mV) x divider ratio 4.90
* ((390K + 100K) / 100K, schematic R50/R51) = 5390. */
zephyr,user {
io-channels = <&adc0 0>;
vbat-mv-multiplier = <5390>;
};
aliases {
gps-enable = &gps_enable_pin;
};
chosen {
zephyr,sram = &sram1;
/* There is no USB-UART bridge on this board — GPIO43/44 only reach
* the expansion header — so the stock console is USB Serial/JTAG.
* A companion build pulls in boards/common/esp32s3_usb.conf, which
* hands D+/D- to USB OTG CDC-ACM and reroutes this console to
* uart0 on the header. */
zephyr,console = &usb_serial;
zephyr,shell-uart = &usb_serial;
zephyr,flash = &flash0;
zephyr,code-partition = &slot0_partition;
zephyr,bt-hci = &esp32_bt_hci;
zephyr,display = &ssd1306_128x64;
};
};
/* Battery ADC on adc0 (ADC unit 1 -> GPIO1 = ADC1_CH0). The node *labelled*
* adc0 is unit 1; the node labelled adc1 is unit 2 (GPIO11 = LR2021 MISO).
* The channel@0 node is required — without it adc_channel_setup_dt() fails and
* getBattMilliVolts() returns 0 before ever performing a conversion.
* ADC_GAIN_1 (0 dB) with ADC_REF_INTERNAL (~1100 mV): a 3.0-4.2 V cell lands at
* 612-857 mV after the 4.90 divider, comfortably inside range. */
&adc0 {
status = "okay";
#address-cells = <1>;
#size-cells = <0>;
channel@0 {
reg = <0>;
zephyr,gain = "ADC_GAIN_1";
zephyr,reference = "ADC_REF_INTERNAL";
zephyr,acquisition-time = <ADC_ACQ_TIME_DEFAULT>;
zephyr,resolution = <12>;
};
};
/* ADC unit 2 (label adc1) is unused; keep it disabled so nothing can claim
* GPIO11 (LR2021 MISO) as an ADC pad. */
&adc1 {
status = "disabled";
};
&usb_serial {
status = "okay";
};
&uart0 {
status = "okay";
current-speed = <115200>;
pinctrl-0 = <&uart0_default>;
pinctrl-names = "default";
};
&uart1 {
status = "okay";
current-speed = <9600>;
pinctrl-0 = <&uart1_default>;
pinctrl-names = "default";
gnss: gnss-nmea-generic {
compatible = "gnss-nmea-generic";
};
};
&gpio0 {
status = "okay";
};
&gpio1 {
status = "okay";
};
&i2c0 {
status = "okay";
clock-frequency = <I2C_BITRATE_FAST>;
pinctrl-0 = <&i2c0_default>;
pinctrl-names = "default";
/* SSD1315 — register-compatible with the SSD1306 the Zephyr driver binds
* to; the difference is the panel glass (bicolour: top 16 rows yellow),
* which is invisible to the driver. Same part as lilygo_timpulse_plus.
*
* Reset is GPIO47, not GPIO21. The vendor pin sheet lists IO47 = OLED/LCD
* RST and IO21 = OLED/LCD CS (CS belongs to the SPI-LCD option this
* connector also supports and the shipping I2C panel does not use).
* MeshCore's pins_arduino.h has RST_OLED = 21, which is the CS pin. */
ssd1306_128x64: display-controller@3c {
compatible = "solomon,ssd1306";
reg = <0x3c>;
width = <128>;
height = <64>;
segment-offset = <0>;
page-offset = <0>;
display-offset = <0>;
multiplex-ratio = <63>;
segment-remap;
com-invdir;
inversion-on;
prechargep = <0x22>;
reset-gpios = <&gpio1 15 GPIO_ACTIVE_LOW>; /* GPIO47 */
vin-supply = <&vext_enable>;
};
};
&spi2 {
status = "okay";
#address-cells = <1>;
#size-cells = <0>;
pinctrl-0 = <&spim2_default>;
pinctrl-names = "default";
/* The LR2021 driver takes NSS from here but strips CS out of its own
* spi_dt_spec and toggles the pin itself, because the chip needs NSS held
* across a two-window read. The ESP32 SPI controller therefore never
* drives CS and the two cannot fight.
*
* No overrun-character property, and none is needed: the LR2021 HAL clocks
* NULL TX buffers on its read windows and the chip parses those bytes, so
* MOSI must idle at 0x00 (its NOP) rather than 0xff. On Nordic that takes
* an explicit overrun-character; spi_esp32_spim allocates a zero-filled TX
* buffer for any RX-only transfer, so ESP32 already sends 0x00. */
cs-gpios = <&gpio0 8 GPIO_ACTIVE_LOW>; /* NSS = GPIO8 */
lora0: lora@0 {
compatible = "semtech,lr2021";
reg = <0>;
spi-max-frequency = <8000000>;
reset-gpios = <&gpio0 12 GPIO_ACTIVE_LOW>; /* GPIO12 */
busy-gpios = <&gpio0 13 GPIO_ACTIVE_HIGH>; /* GPIO13 */
dio1-gpios = <&gpio0 14 (GPIO_PULL_DOWN | GPIO_ACTIVE_HIGH)>;/* GPIO14 */
/* Chip DIO8 -> GPIO14 (schematic net IO14_LORA_INT2). DIO7 goes to
* GPIO7 as a second IRQ line (IO7_LORA_INT1) that ZephCore does not
* use. DIO5 is NOT free for interrupts here — it drives the 2.4 GHz
* PA's TX enable — so leaving irq-dio at its default of 9 would also
* be wrong. */
irq-dio = <8>;
/* No tcxo-voltage-mv: schematic sheet 3 shows a plain crystal on
* XTA/XTB (pins 4/5) and pin 6 wired to the VNTC net, not a TCXO
* supply. Setting a TCXO voltage on a board without one wedges the
* chip at HF-XOSC startup — that is what happened on the
* promicro_lr2021 module. */
rx-boosted;
/* RF switch — the LR2021 drives the whole front end itself.
*
* DIO5 (bit 0) = RFX2402E 2.4 GHz TX_EN
* DIO6 (bit 1) = RFX2402E 2.4 GHz RX_EN
* DIO9 (bit 4) = GC1109 CTX (transmit path select)
* DIO10 (bit 5) = GC1109 CPS (PA vs bypass path)
* DIO11 (bit 6) = GC1109 CSD (shutdown, HIGH = awake)
*
* CSD and CPS carry 10k pull-downs (R26/R27), so the GC1109 boots
* into shutdown and stays there until these are configured. That is
* why the driver's DIO5..DIO8-only loop had to be widened to
* DIO5..DIO11 for this board: with DIO9/10/11 unconfigured the node
* looks healthy on every register read and hears nothing.
*
* RX is split per band because the two front ends are mutually
* exclusive: sub-GHz RX wakes the GC1109 (CSD high, CTX low = LNA
* path), while 2.4 GHz RX must leave it in shutdown and enable the
* RFX2402E instead. ZephCore only ever uses the sub-GHz side; the HF
* masks are here so the table is a faithful description of the
* hardware rather than a half-truth. */
rfswitch-enable = <0x73>; /* DIO5,6,9,10,11 */
rfswitch-standby = <0x00>; /* everything off */
rfswitch-rx-lf = <0x40>; /* DIO11: GC1109 out of shutdown, LNA */
rfswitch-rx-hf = <0x02>; /* DIO6: 2.4 GHz LNA, GC1109 shut down */
rfswitch-tx = <0x70>; /* DIO9|10|11: GC1109 full PA */
rfswitch-tx-hp = <0x01>; /* DIO5: 2.4 GHz PA */
};
};
&timer0 { status = "okay"; };
&timer1 { status = "okay"; };
&timer2 { status = "okay"; };
&timer3 { status = "okay"; };
&wdt0 { status = "okay"; };
&trng0 { status = "okay"; };
&esp32_bt_hci { status = "okay"; };
&wifi { status = "okay"; };
@@ -0,0 +1,12 @@
# Meshnology W12 — minimal Zephyr defconfig (procpu)
CONFIG_CONSOLE=y
CONFIG_SERIAL=y
CONFIG_UART_CONSOLE=y
CONFIG_GPIO=y
CONFIG_CLOCK_CONTROL=y
CONFIG_PINCTRL=y
CONFIG_SPI=y
CONFIG_I2C=y
CONFIG_FLASH=y
CONFIG_FLASH_MAP=y
CONFIG_NVS=y
@@ -0,0 +1,53 @@
/*
* Flash partition layout — SHARED by the app image and MCUboot.
*
* Fed to BOTH images (app via EXTRA_DTC_OVERLAY_FILE, MCUboot via
* mcuboot_EXTRA_DTC_OVERLAY_FILE in sysbuild/CMakeLists.txt) so their flash maps
* are identical — otherwise MCUboot stages/looks for the OTA image at a different
* address than the app writes it to and OTA silently reverts on reboot.
*
* slot0 starts at 0x10000 (right after the 64KB MCUboot region), matching the
* Arduino/ESP-IDF app offset (Mesh America "flash-update" compatible). sys_partition
* is reclaimed. Slots are equal size and slot1 ends exactly on the unchanged
* storage/lfs offsets, so a -merged reflash keeps identity/prefs/contacts/BLE-bonds.
*
* 16MB layout.
*/
/delete-node/ &sys_partition;
/delete-node/ &slot0_lpcore_partition;
/delete-node/ &slot1_lpcore_partition;
/delete-node/ &storage_partition;
/delete-node/ &scratch_partition;
/delete-node/ &coredump_partition;
/delete-node/ &slot0_partition;
/delete-node/ &slot1_partition;
/ {
chosen {
zephyr,settings-partition = &storage_partition;
};
};
&flash0 {
partitions {
/* App slots — equal size (swap-mode compatible), slot0 at 0x10000. */
slot0_partition: partition@10000 {
label = "image-0";
reg = <0x10000 0x7C6000>;
};
slot1_partition: partition@7D6000 {
label = "image-1";
reg = <0x7D6000 0x7C6000>;
};
/* BLE bond NVS — offset unchanged so bonds survive the reflash. */
storage_partition: partition@F9C000 {
label = "storage";
reg = <0xF9C000 0x4000>;
};
/* LittleFS (identity/prefs/contacts) — offset unchanged. */
lfs_partition: partition@FA0000 {
label = "lfs";
reg = <0xFA0000 0x60000>;
};
};
};
+8
View File
@@ -51,9 +51,17 @@ SWD flash: `west flash` (requires J-Link, pyocd, or nrfjprog connected).
| Heltec Wireless Tracker V2 | `west build -b heltec_wireless_tracker_v2/esp32s3/procpu zephcore` | `west flash` |
| LilyGo T-Beam v1.2 | `west build -b ttgo_tbeam/esp32/procpu zephcore` | `west flash` |
| ThinkNode M9 | `west build -b thinknode_m9/esp32s3/procpu zephcore` | `west flash` |
| Meshnology W12 (LR2021) | `west build -b meshnology_w12/esp32s3/procpu zephcore` | `west flash` |
**Heltec V3 console:** ZephCore routes console/shell to `uart0` on V3. Use the UART serial port for boot logs and CLI.
**Meshnology W12:** the only board here with an LR2021 *and* an external PA, and
the only ESP32 board whose TX power is capped well below the chip maximum —
4 dBm at the chip is ~30 dBm at the antenna through the GC1109 front end. It has
no USB-UART bridge, so esptool cannot auto-reset a companion build into download
mode; use `start dfu`, a 1200-baud touch, or hold BOOT. Full port notes and the
vendor schematic live in `devdocs/w12/`.
**Heltec V4.2 vs V4.3:** The hardware revision is printed on the PCB silkscreen. If
unclear, check GPIO2's default pull: the V4.2 GC1109 PA has an internal pull-down
(GPIO2 reads LOW at boot), while the V4.3 KCT8103L PA has an internal pull-up
+37
View File
@@ -0,0 +1,37 @@
# Copy patches/zephyr-new/* into the Zephyr tree.
#
# These are whole files ZephCore adds to Zephyr that have no upstream to patch
# against — today the LR11xx and LR20xx LoRa drivers and their devicetree
# bindings. They are copied rather than patched because there is nothing to
# apply a diff to.
#
# This lives in its own module because it has to run from TWO places, and the
# order matters:
#
# - zephcore/CMakeLists.txt, for the application image.
# - zephcore/sysbuild/CMakeLists.txt, BEFORE find_package(Sysbuild).
#
# Under --sysbuild, sysbuild configures the MCUboot image first, and MCUboot
# runs a full devicetree pass over the same board DTS the app uses. If the
# copy only happened in the app's CMakeLists, MCUboot's DTS pass would see
# whatever version of these bindings a previous build happened to leave behind
# — or, on a clean workspace, none at all. Editing a vendored binding then
# fails the MCUboot image with "not declared in 'properties:'" while the app
# image would have been perfectly happy, and the error points at the copy in
# the Zephyr tree rather than at the real source in patches/zephyr-new.
#
# configure_file(... COPYONLY) is a no-op when the contents already match, so
# running this twice per configure costs nothing and never churns timestamps.
#
# Expects ZEPHCORE_SOURCE_DIR and ZEPHCORE_ZEPHYR_DIR to be set by the caller.
if(EXISTS ${ZEPHCORE_SOURCE_DIR}/patches/zephyr-new)
file(GLOB_RECURSE ZEPHCORE_NEW_FILES
RELATIVE ${ZEPHCORE_SOURCE_DIR}/patches/zephyr-new
${ZEPHCORE_SOURCE_DIR}/patches/zephyr-new/*)
foreach(REL_PATH ${ZEPHCORE_NEW_FILES})
configure_file(${ZEPHCORE_SOURCE_DIR}/patches/zephyr-new/${REL_PATH}
${ZEPHCORE_ZEPHYR_DIR}/${REL_PATH} COPYONLY)
message(STATUS " [zephyr-new] ${REL_PATH}")
endforeach()
endif()
@@ -61,7 +61,12 @@ struct lr20xx_config {
/* RF switch DIO bitmasks (bit 0 = DIO5, bit 1 = DIO6, ...) */
uint8_t rfswitch_enable;
uint8_t rfswitch_standby;
uint8_t rfswitch_rx;
/* RX masks are split per band: a front end that shuts its sub-GHz PA
* down during 2.4 GHz RX (Meshnology W12 / GC1109 CSD) needs the two
* to differ. Boards that set only the combined `rfswitch-rx` get the
* same mask in both, which is the pre-split behaviour. */
uint8_t rfswitch_rx_lf;
uint8_t rfswitch_rx_hf;
uint8_t rfswitch_tx;
uint8_t rfswitch_tx_hp;
};
@@ -350,19 +355,45 @@ lr20xx_irq_dio_pull(const struct lr20xx_config *cfg)
/* ── Configure RF switch DIOs ───────────────────────────────────────── */
/* DIO5..DIO11 inclusive — the full RF-switch-capable range on this chip.
* Bit i of every rfswitch-* mask is DIO(5 + i), so the masks stay uint8_t. */
#define LR20XX_RFSWITCH_DIO_COUNT \
(LR20XX_SYSTEM_DIO_11 - LR20XX_SYSTEM_DIO_5 + 1)
static void lr20xx_configure_rfswitch(void *ctx, const struct lr20xx_config *cfg)
{
/* LR20xx RF switch uses per-DIO configuration.
* DIO5..DIO8 map to enable bitmask bits 0..3.
* DIO5..DIO11 map to enable bitmask bits 0..6 (the chip has no RF
* switch DIOs outside 5..11 — see lr20xx_system_dio_t).
* For each enabled DIO, compute which operational modes
* should drive it HIGH by looking at the per-mode bitmasks. */
for (int i = 0; i < 4; i++) {
* should drive it HIGH by looking at the per-mode bitmasks.
*
* This used to stop at bit 3 (DIO5..DIO8), which silently dropped the
* top three DIOs from any board that used them: the enable bit was set
* in devicetree, the loop never reached it, so the DIO kept its default
* function and the front end it drives never switched. The Meshnology
* W12 puts its entire sub-GHz front end there (DIO9 = GC1109 CTX,
* DIO10 = CPS, DIO11 = CSD), and with those three left unconfigured the
* FEM stays in the shutdown its 10k pull-downs assert at reset — the
* node is both deaf and mute while every register readback looks fine. */
for (int i = 0; i < LR20XX_RFSWITCH_DIO_COUNT; i++) {
if (!(cfg->rfswitch_enable & BIT(i))) {
continue;
}
lr20xx_system_dio_t dio = (lr20xx_system_dio_t)(LR20XX_SYSTEM_DIO_5 + i);
/* Never repurpose the interrupt line. A board that sets the IRQ
* DIO's bit in rfswitch-enable by mistake would otherwise have its
* only IRQ pin switched to RF-switch duty here, and the driver
* would then wait forever on TX_DONE/RX_DONE with no way to tell
* why. Skipping it turns a silent hang into a warning. */
if (dio == lr20xx_irq_dio(cfg)) {
LOG_WRN("rfswitch: DIO%d is the IRQ line, not configuring "
"it as an RF switch", (int)dio);
continue;
}
/* Set this DIO function to RF switch control */
lr20xx_system_set_dio_function(ctx, dio,
LR20XX_SYSTEM_DIO_FUNC_RF_SWITCH,
@@ -375,9 +406,11 @@ static void lr20xx_configure_rfswitch(void *ctx, const struct lr20xx_config *cfg
if (cfg->rfswitch_standby & BIT(i)) {
sw_cfg |= LR20XX_SYSTEM_DIO_RF_SWITCH_WHEN_STANDBY;
}
if (cfg->rfswitch_rx & BIT(i)) {
sw_cfg |= LR20XX_SYSTEM_DIO_RF_SWITCH_WHEN_RX_LF |
LR20XX_SYSTEM_DIO_RF_SWITCH_WHEN_RX_HF;
if (cfg->rfswitch_rx_lf & BIT(i)) {
sw_cfg |= LR20XX_SYSTEM_DIO_RF_SWITCH_WHEN_RX_LF;
}
if (cfg->rfswitch_rx_hf & BIT(i)) {
sw_cfg |= LR20XX_SYSTEM_DIO_RF_SWITCH_WHEN_RX_HF;
}
if (cfg->rfswitch_tx & BIT(i)) {
sw_cfg |= LR20XX_SYSTEM_DIO_RF_SWITCH_WHEN_TX_LF;
@@ -3238,8 +3271,10 @@ static int lr20xx_hw_init(struct lr20xx_data *data,
lr20xx_status_t st;
lr20xx_configure_rfswitch(ctx, cfg);
LOG_DBG("RF switch: en=0x%02x stby=0x%02x rx=0x%02x tx=0x%02x txhp=0x%02x",
cfg->rfswitch_enable, cfg->rfswitch_standby, cfg->rfswitch_rx,
LOG_DBG("RF switch: en=0x%02x stby=0x%02x rxlf=0x%02x rxhf=0x%02x "
"tx=0x%02x txhp=0x%02x",
cfg->rfswitch_enable, cfg->rfswitch_standby,
cfg->rfswitch_rx_lf, cfg->rfswitch_rx_hf,
cfg->rfswitch_tx, cfg->rfswitch_tx_hp);
st = lr20xx_system_set_dio_function(ctx, lr20xx_irq_dio(cfg),
@@ -3408,7 +3443,14 @@ static DEVICE_API(lora, lr20xx_lora_api) = {
.irq_dio = DT_INST_PROP_OR(n, irq_dio, 9), \
.rfswitch_enable = DT_INST_PROP_OR(n, rfswitch_enable, 0), \
.rfswitch_standby = DT_INST_PROP_OR(n, rfswitch_standby, 0),\
.rfswitch_rx = DT_INST_PROP_OR(n, rfswitch_rx, 0), \
/* rfswitch-rx-lf / -hf are optional and carry no binding \
* default, so an absent one falls back to the combined \
* rfswitch-rx — which keeps every pre-split board (e.g. \
* promicro_lr2021) byte-identical. */ \
.rfswitch_rx_lf = DT_INST_PROP_OR(n, rfswitch_rx_lf, \
DT_INST_PROP_OR(n, rfswitch_rx, 0)), \
.rfswitch_rx_hf = DT_INST_PROP_OR(n, rfswitch_rx_hf, \
DT_INST_PROP_OR(n, rfswitch_rx, 0)), \
.rfswitch_tx = DT_INST_PROP_OR(n, rfswitch_tx, 0), \
.rfswitch_tx_hp = DT_INST_PROP_OR(n, rfswitch_tx_hp, 0), \
}; \
@@ -60,8 +60,10 @@ properties:
type: int
default: 0
description: |
RF switch DIO enable bitmask (bit 0 = DIO5, bit 1 = DIO6, ...).
Set a bit to include that DIO in RF switch control.
RF switch DIO enable bitmask (bit 0 = DIO5, bit 1 = DIO6, ... bit 6 =
DIO11). DIO5..DIO11 is the chip's entire RF-switch-capable range, so
bit 7 is unused. Set a bit to include that DIO in RF switch control.
The DIO named by irq-dio is skipped even if its bit is set here.
rfswitch-standby:
type: int
@@ -74,8 +76,30 @@ properties:
type: int
default: 0
description: |
Bitmask of RF switch DIOs that are HIGH in RX mode (both LF and HF).
Bit mapping matches rfswitch-enable.
Bitmask of RF switch DIOs that are HIGH in RX mode, in BOTH bands.
Bit mapping matches rfswitch-enable. This is the convenient form for
the common single-band board; set rfswitch-rx-lf / rfswitch-rx-hf
instead when the two bands need different masks.
rfswitch-rx-lf:
type: int
description: |
Bitmask of RF switch DIOs that are HIGH in sub-GHz RX only.
Defaults to rfswitch-rx when omitted. Bit mapping matches
rfswitch-enable.
rfswitch-rx-hf:
type: int
description: |
Bitmask of RF switch DIOs that are HIGH in 2.4 GHz RX only.
Defaults to rfswitch-rx when omitted. Bit mapping matches
rfswitch-enable.
Split from rfswitch-rx for front ends whose two bands are mutually
exclusive: the Meshnology W12 must hold the GC1109 sub-GHz FEM in
shutdown (DIO11 low) during 2.4 GHz RX, which a single shared mask
cannot express. Do NOT give these two a `default:` here — the driver
falls back to rfswitch-rx by testing for the property's absence.
rfswitch-tx:
type: int
+9
View File
@@ -2,6 +2,15 @@
# so the BOARD_ROOT set there never fires. Set it here, before find_package(Sysbuild).
set(BOARD_ROOT "${APP_DIR}" CACHE STRING "" FORCE)
# Same story for the files ZephCore adds to the Zephyr tree: the app copies them
# in its own CMakeLists, but MCUboot is configured first and runs a devicetree
# pass over the same board DTS. A board whose radio node uses a ZephCore-only
# binding (the LR11xx / LR20xx LoRa drivers) needs that binding present before
# MCUboot's DTS pass, not after. See cmake/zephyr_new_files.cmake.
set(ZEPHCORE_SOURCE_DIR "${APP_DIR}")
set(ZEPHCORE_ZEPHYR_DIR "$ENV{ZEPHYR_BASE}")
include("${APP_DIR}/cmake/zephyr_new_files.cmake")
# Auto-propagate flash size from board.conf to MCUboot.
#
# MCUboot is built as a separate image and does NOT inherit the app's board.conf