Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions examples/simple_repeater/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,14 @@ static unsigned long userBtnDownAt = 0;

void setup() {
Serial.begin(115200);
#ifdef WITH_W5100S_POE
// PoE cold-start: get to board.begin() (which activates the W5100S load)
// ASAP, before the RAK19018/Silvertel converter folds back. Skip the 1 s
// serial-settle delay — there is no operator on the serial port on PoE.
delay(20);
#else
delay(1000);
#endif

board.begin();

Expand Down Expand Up @@ -205,6 +212,12 @@ void loop() {
#ifdef HAS_EXTERNAL_WATCHDOG
external_watchdog.loop();
#endif

#ifdef WITH_W5100S_POE
// PoE-powered (RAK19018/Silvertel): the device must NEVER sleep. CPU sleep
// drops the current draw below the converter's ~125 mA hold threshold,
// making it fold back and reset. Skip the powersaving/sleep path entirely.
#else
if (the_mesh.getNodePrefs()->powersaving_enabled && !the_mesh.hasPendingWork()) {
#if defined(NRF52_PLATFORM)
board.sleep(0); // nrf ignores seconds param, sleeps whenever possible
Expand All @@ -214,4 +227,5 @@ void loop() {
}
#endif
}
#endif // WITH_W5100S_POE
}
7 changes: 7 additions & 0 deletions examples/simple_room_server/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,14 @@ static char ethernet_command[MAX_POST_TEXT_LEN+1];

void setup() {
Serial.begin(115200);
#ifdef WITH_W5100S_POE
// PoE cold-start: get to board.begin() (which activates the W5100S load)
// ASAP, before the RAK19018/Silvertel converter folds back. Skip the 1 s
// serial-settle delay — there is no operator on the serial port on PoE.
delay(20);
#else
delay(1000);
#endif

board.begin();

Expand Down
46 changes: 46 additions & 0 deletions src/helpers/ethernet/RAK13800/RAK13800EthernetInterface.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,37 @@ SPIClass ETHERNET_SPI_PORT(NRF_SPIM1, PIN_SPI1_MISO, PIN_SPI1_SCK, PIN_SPI1_MOSI
#define PIN_ETHERNET_RESET 21
#define PIN_ETHERNET_SS 26

#ifdef WITH_W5100S_POE
// Give the RAK19018 (Silvertel) PoE converter time to latch on the current the
// W5100S is already drawing (see variants/rak4631/W5100SPoE.cpp) before doing the
// disruptive Ethernet-library bring-up (PHY soft-reset + blocking DHCP), which
// otherwise collapses the marginal PoE supply at cold start.
#ifndef ETH_POE_DEFER_MS
#define ETH_POE_DEFER_MS 6000
#endif
// Static fallback if no DHCP server answers (override per network).
#ifndef ETH_STATIC_IP
#define ETH_STATIC_IP 192,168,1,50
#endif
#ifndef ETH_GATEWAY
#define ETH_GATEWAY 192,168,1,1
#endif
#ifndef ETH_SUBNET
#define ETH_SUBNET 255,255,255,0
#endif
#endif

bool RAK13800EthernetInterface::begin() {
#ifdef WITH_W5100S_POE
// Non-disruptive only: the real bring-up runs from loop() after ETH_POE_DEFER_MS.
_startedAt = millis();
return true;
#else
return bringUp();
#endif
}

bool RAK13800EthernetInterface::bringUp() {

// WB_IO2 (power enable) is already driven HIGH by early constructor
// in RAK4631Board.cpp to support POE boot.
Expand Down Expand Up @@ -45,6 +75,14 @@ bool RAK13800EthernetInterface::begin() {
IPAddress subnet(ETHERNET_STATIC_SUBNET);
IPAddress dns(ETHERNET_STATIC_DNS);
Ethernet.begin(mac, ip, dns, gateway, subnet);
#elif defined(WITH_W5100S_POE)
// Bounded DHCP with a static-IP fallback, so the node stays reachable without a
// DHCP server and never blocks indefinitely at cold start.
if (Ethernet.begin(mac, 12000, 4000) == 0) {
ETHERNET_DEBUG_PRINTLN("DHCP failed, using static IP fallback");
IPAddress ip(ETH_STATIC_IP), gw(ETH_GATEWAY), sn(ETH_SUBNET);
Ethernet.begin(mac, ip, gw, gw, sn);
}
#else
if (Ethernet.begin(mac) == 0) {
ETHERNET_DEBUG_PRINTLN("Failed to initialize RAK13800 hardware.");
Expand Down Expand Up @@ -88,6 +126,14 @@ bool RAK13800EthernetInterface::isConnected() const {
}

void RAK13800EthernetInterface::loop() {
#ifdef WITH_W5100S_POE
if (!_hwReady) {
if (millis() - _startedAt > ETH_POE_DEFER_MS) {
_hwReady = bringUp();
}
return;
}
#endif

Ethernet.maintain();

Expand Down
11 changes: 11 additions & 0 deletions src/helpers/ethernet/RAK13800/RAK13800EthernetInterface.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,20 @@ class RAK13800EthernetInterface : public SerialEthernetInterface {
EthernetServer server;
EthernetClient client;

bool bringUp(); // init the W5100S, obtain an IP and start listening

#ifdef WITH_W5100S_POE
bool _hwReady; // true once the (deferred) bring-up has run
unsigned long _startedAt; // millis() at begin(); bring-up is deferred from here
#endif

public:
RAK13800EthernetInterface() : server(EthernetServer(ETHERNET_TCP_PORT)) {
_isConnected = false;
#ifdef WITH_W5100S_POE
_hwReady = false;
_startedAt = 0;
#endif
}

bool begin();
Expand Down
43 changes: 42 additions & 1 deletion src/helpers/nrf52/EthernetCLI.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,26 @@ static SPIClass ETHERNET_SPI_PORT(NRF_SPIM1, PIN_SPI1_MISO, PIN_SPI1_SCK, PIN_SP

#define ETHERNET_RETRY_INTERVAL_MS 30000

#ifdef WITH_W5100S_POE
// Give the RAK19018 (Silvertel) PoE converter time to latch on the current
// the W5100S is already drawing (board.begin()'s early RST release +
// bit-bang soft-reset) before doing the *disruptive* Ethernet-library
// bring-up (another PHY soft-reset + blocking DHCP) — doing that
// immediately reliably collapsed the marginal PoE supply.
#ifndef ETH_POE_DEFER_MS
#define ETH_POE_DEFER_MS 6000
#endif
#ifndef ETH_STATIC_IP
#define ETH_STATIC_IP 192,168,1,50
#endif
#ifndef ETH_GATEWAY
#define ETH_GATEWAY 192,168,1,1
#endif
#ifndef ETH_SUBNET
#define ETH_SUBNET 255,255,255,0
#endif
#endif

static EthernetServer ethernet_server(ETHERNET_TCP_PORT);
static EthernetClient ethernet_client;
static volatile bool ethernet_running = false;
Expand All @@ -35,6 +55,10 @@ static volatile bool ethernet_running = false;
static void ethernet_task(void* param) {
(void)param;

#ifdef WITH_W5100S_POE
vTaskDelay(pdMS_TO_TICKS(ETH_POE_DEFER_MS));
#endif

Serial.println("ETH: Initializing hardware");
// WB_IO2 (power enable) is already driven HIGH by early constructor
// in RAK4631Board.cpp to support POE boot.
Expand All @@ -51,6 +75,22 @@ static void ethernet_task(void* param) {
Serial.printf("ETH: MAC: %02X:%02X:%02X:%02X:%02X:%02X\n",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);

#ifdef WITH_W5100S_POE
// Bounded DHCP with a static-IP fallback, so the node stays reachable even
// without a DHCP server (no infinite retry — this build has no battery and
// must always come up somewhere).
Serial.println("ETH: Attempting DHCP...");
if (Ethernet.begin(mac, 10000, 2000) == 0) {
Serial.println("ETH: DHCP failed -> static IP fallback");
IPAddress ip(ETH_STATIC_IP), gw(ETH_GATEWAY), sn(ETH_SUBNET);
Ethernet.begin(mac, ip, gw, gw, sn);
}
IPAddress ip = Ethernet.localIP();
Serial.printf("ETH: IP: %u.%u.%u.%u\n", ip[0], ip[1], ip[2], ip[3]);
Serial.printf("ETH: Listening on TCP port %d\n", ETHERNET_TCP_PORT);
ethernet_server.begin();
ethernet_running = true;
#else
// Retry loop: keep trying until we get an IP
while (!ethernet_running) {
Serial.println("ETH: Attempting DHCP...");
Expand All @@ -75,8 +115,9 @@ static void ethernet_task(void* param) {
ethernet_server.begin();
ethernet_running = true;
}
#endif

// DHCP succeeded, task is done
// DHCP succeeded (or static fallback applied), task is done
vTaskDelete(NULL);
}

Expand Down
36 changes: 31 additions & 5 deletions variants/rak4631/RAK4631Board.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,24 @@ static void __attribute__((constructor(102))) rak4631_early_poe_power() {
}
#endif

#ifdef WITH_W5100S_POE
#include "W5100SPoE.h"
#endif

#ifdef NRF52_POWER_MANAGEMENT
// Static configuration for power management
// Values set in variant.h defines
const PowerMgtConfig power_config = {
.lpcomp_ain_channel = PWRMGT_LPCOMP_AIN,
.lpcomp_refsel = PWRMGT_LPCOMP_REFSEL,
.voltage_bootlock = PWRMGT_VOLTAGE_BOOTLOCK
.lpcomp_refsel = PWRMGT_LPCOMP_REFSEL,
// WITH_W5100S_POE = PoE operation without a battery.
// isExternalPowered() only detects USB VBUS, not PoE power.
// Without this exception checkBootVoltage() reads the floating
// battery ADC pin (no battery) and triggers the protection
// shutdown -> SYSTEMOFF loop -> red LED flicker.
#ifdef WITH_W5100S_POE
.voltage_bootlock = 0
#else
.voltage_bootlock = PWRMGT_VOLTAGE_BOOTLOCK
#endif
};

void RAK4631Board::initiateShutdown(uint8_t reason) {
Expand Down Expand Up @@ -64,4 +75,19 @@ void RAK4631Board::begin() {
#endif
digitalWrite(SX126X_POWER_EN, HIGH);
delay(10); // give sx1262 some time to power up
}

#ifdef WITH_W5100S_POE
uint8_t w5100s_ver = w5100s_poe_init();
(void)w5100s_ver;
#ifdef MESH_DEBUG
// Wait for USB-CDC to re-enumerate so this line isn't eaten during the
// reconnect, then print it repeatedly to be sure it is seen. Debug only —
// the PoE production build skips this and boots fast.
delay(3000);
for (int i = 0; i < 5; i++) {
MESH_DEBUG_PRINTLN(">>> W5100S VERSIONR = 0x%02X (expect 0x51) <<<", w5100s_ver);
delay(200);
}
#endif
#endif
}
97 changes: 97 additions & 0 deletions variants/rak4631/W5100SPoE.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
#ifdef WITH_W5100S_POE

#include <Arduino.h>
#include <nrf.h>
#include "W5100SPoE.h"

// ── Early RST release (constructor priority 200) ─────────────────────────────
// Runs before setup(), right after SystemInit — earlier than the ETHERNET_ENABLED
// constructor in RAK4631Board.cpp that powers the peripheral rail (WB_IO2,
// priority 102 runs first) but well before setup()/board.begin(), so the
// W5100S starts drawing its full operating current as early as possible,
// before the RAK19018 (Silvertel) converter's foldback timer expires.
//
// Raw registers because the Arduino GPIO layer isn't up this early.
static void __attribute__((constructor(200))) w5100s_early_rst_release() {
// P0.21 = W5100S RST → HIGH (release from reset)
NRF_P0->PIN_CNF[21] =
(GPIO_PIN_CNF_DIR_Output << GPIO_PIN_CNF_DIR_Pos) |
(GPIO_PIN_CNF_INPUT_Disconnect << GPIO_PIN_CNF_INPUT_Pos) |
(GPIO_PIN_CNF_PULL_Disabled << GPIO_PIN_CNF_PULL_Pos) |
(GPIO_PIN_CNF_DRIVE_S0S1 << GPIO_PIN_CNF_DRIVE_Pos) |
(GPIO_PIN_CNF_SENSE_Disabled << GPIO_PIN_CNF_SENSE_Pos);
NRF_P0->OUTSET = (1UL << 21);
}

// ── Bit-banged SPI to the W5100S ────────────────────────────────────────────
// Shares the SPI bus (SCK=3 / MOSI=30 / MISO=29) with the SX1262 radio; only
// the chip-select differs (W5100S CS=26, SX1262 NSS=42). Bit-banged so it
// works regardless of which SPIClass owns the bus and runs before RadioLib.
// W5100S frame: write [0xF0][hi][lo][data], read [0x0F][hi][lo]->[data].
#define ETH_SCK_PIN 3
#define ETH_MOSI_PIN 30
#define ETH_MISO_PIN 29
#define LORA_NSS_PIN 42

static void bb_write_reg(uint16_t addr, uint8_t data) {
const uint8_t frame[4] = { 0xF0, (uint8_t)(addr >> 8), (uint8_t)(addr & 0xFF), data };
digitalWrite(W5100S_CS_PIN, LOW);
for (uint8_t b = 0; b < 4; b++) {
uint8_t v = frame[b];
for (int8_t i = 7; i >= 0; i--) {
digitalWrite(ETH_MOSI_PIN, (v >> i) & 1);
digitalWrite(ETH_SCK_PIN, HIGH);
digitalWrite(ETH_SCK_PIN, LOW);
}
}
digitalWrite(W5100S_CS_PIN, HIGH);
}

static uint8_t bb_read_reg(uint16_t addr) {
const uint8_t frame[3] = { 0x0F, (uint8_t)(addr >> 8), (uint8_t)(addr & 0xFF) };
digitalWrite(W5100S_CS_PIN, LOW);
for (uint8_t b = 0; b < 3; b++) {
uint8_t v = frame[b];
for (int8_t i = 7; i >= 0; i--) {
digitalWrite(ETH_MOSI_PIN, (v >> i) & 1);
digitalWrite(ETH_SCK_PIN, HIGH);
digitalWrite(ETH_SCK_PIN, LOW);
}
}
uint8_t out = 0;
for (int8_t i = 7; i >= 0; i--) {
digitalWrite(ETH_SCK_PIN, HIGH);
out = (out << 1) | (digitalRead(ETH_MISO_PIN) & 1);
digitalWrite(ETH_SCK_PIN, LOW);
}
digitalWrite(W5100S_CS_PIN, HIGH);
return out;
}

// ── Full W5100S bring-up ────────────────────────────────────────────────────
// Called from RAK4631Board::begin(). Confirms RST is driven (Arduino API, in
// case the core re-init touched it), then soft-resets and reads VERSIONR.
// Returns VERSIONR (0x51 = healthy W5100S).
uint8_t w5100s_poe_init() {
delay(20); // let the rail/W5100S settle after the ETHERNET_ENABLED ctor's WB_IO2 enable

// Park both chip-selects HIGH on the shared bus, RST released.
pinMode(LORA_NSS_PIN, OUTPUT); digitalWrite(LORA_NSS_PIN, HIGH);
pinMode(W5100S_CS_PIN, OUTPUT); digitalWrite(W5100S_CS_PIN, HIGH);
pinMode(W5100S_RST_PIN, OUTPUT); digitalWrite(W5100S_RST_PIN, HIGH);

pinMode(ETH_SCK_PIN, OUTPUT); digitalWrite(ETH_SCK_PIN, LOW);
pinMode(ETH_MOSI_PIN, OUTPUT); digitalWrite(ETH_MOSI_PIN, LOW);
pinMode(ETH_MISO_PIN, INPUT);

bb_write_reg(0x0000, 0x80); // MR: software reset
delay(2);

uint8_t ver = bb_read_reg(0x0080); // VERSIONR (0x51 on W5100S)

// Chip stays powered and out of reset; PHY auto-negotiates with the switch
// and the W5100S draws its full operating current.
return ver;
}

#endif // WITH_W5100S_POE
36 changes: 36 additions & 0 deletions variants/rak4631/W5100SPoE.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#pragma once

// W5100S early activation for PoE operation on RAK10720 (RAK4631 + RAK13800 +
// RAK19018).
//
// ROOT CAUSE (confirmed via RAK forum + Meshtastic rak4631_eth_gw variant):
// The RAK19018 PoE module (Silvertel Ag9905MT) enters a non-continuous
// "gated pulse" mode when the load is below ~125-200 mA. A bare MeshCore
// repeater draws only a few mA, so the converter never latches → the supply
// pulses and the device never boots (fade-in / brighten / die / repeat LED).
//
// The fix: bring the W5100S PHY into its active state as early as possible.
// An active W5100S draws ~120 mA — enough to keep the Silvertel converter
// latched in continuous mode. The peripheral power rail (WB_IO2 / P1.02) is
// already driven HIGH by an early constructor in RAK4631Board.cpp whenever
// ETHERNET_ENABLED is set; this file only handles releasing RST (the rail
// alone isn't enough — the chip must actually be brought out of reset and
// made to draw its full current well before the Ethernet library's own
// (later, disruptive) bring-up runs).
//
// Pin mapping (from Meshtastic rak4631_eth_gw variant.h):
// RST → PIN_ETHERNET_RESET = 21 (P0.21 / WB_IO3)
// CS → PIN_ETHERNET_SS = 26 (WB_SPI_CS)
// SPI → SPI1 (SCK=3, MISO=29, MOSI=30) [not used in Layer 1]

#ifndef W5100S_RST_PIN
#define W5100S_RST_PIN 21 // P0.21 / WB_IO3
#endif

#ifndef W5100S_CS_PIN
#define W5100S_CS_PIN 26 // WB_SPI_CS
#endif

// Called from RAK4631Board::begin(); fully brings up the W5100S (soft reset +
// activation) so it draws full operating current. Returns VERSIONR (0x51 = ok).
uint8_t w5100s_poe_init();
Loading