Browse Source

Add RAK4631 PoE and Ethernet companion support (W5100S / RAK13800)

Adds support for running a RAK4631 over PoE using the RAK13800 (W5100S)
Ethernet module on a RAK19018 base, in two roles:

- PoE repeater: powers up reliably on the RAK19018 (Silvertel) converter.
  The cold-start path is shortened and CPU sleep is disabled, because
  dropping below the converter's hold current makes it fold back and
  reset. Boot-voltage protection is bypassed when battery-less on PoE.

- Ethernet companion: exposes the MeshCore companion protocol as a TCP
  server (default port 5000) over the W5100S, so Home Assistant connects
  to the device's IP. Uses a static IP to avoid blocking DHCP at cold
  start. W5100S PHY bring-up is deferred out of setup() into loop() to
  avoid collapsing the marginal PoE supply during the cold-start window.

New files:
- src/helpers/SerialEthernetInterface.{cpp,h}: TCP transport for the
  companion serial frame protocol.
- variants/rak4631/W5100SPoE.{cpp,h}: W5100S power/reset + init helper.

New platformio envs:
- RAK_4631_repeater_poe (+ _debug)
- RAK_4631_companion_radio_eth

All additions are guarded by WITH_W5100S_POE / WITH_ETHERNET_COMPANION,
so existing RAK4631 builds are unaffected.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
pull/2679/head
1sthandy 2 months ago
parent
commit
7ecbdd2bfc
  1. 102
      examples/companion_radio/main.cpp
  2. 14
      examples/simple_repeater/main.cpp
  3. 151
      src/helpers/SerialEthernetInterface.cpp
  4. 78
      src/helpers/SerialEthernetInterface.h
  5. 36
      variants/rak4631/RAK4631Board.cpp
  6. 113
      variants/rak4631/W5100SPoE.cpp
  7. 42
      variants/rak4631/W5100SPoE.h
  8. 4
      variants/rak4631/target.cpp

102
examples/companion_radio/main.cpp

@ -71,7 +71,32 @@ static uint32_t _atoi(const char* sp) {
ArduinoSerialInterface serial_interface;
#endif
#elif defined(NRF52_PLATFORM)
#ifdef BLE_PIN_CODE
#if defined(WITH_ETHERNET_COMPANION)
#include <SPI.h>
#include <helpers/SerialEthernetInterface.h>
SerialEthernetInterface serial_interface;
// Dedicated SPI for the W5100S on its own pins (SCK=3, MISO=29, MOSI=30).
// The radio remaps the global `SPI` to the LoRa pins (43/44/45) in
// std_init(), so the W5100S needs its own SPIM peripheral. SPIM2 is free
// (radio uses SPIM3, Wire uses TWIM0/1).
SPIClass eth_spi(NRF_SPIM2, 29, 3, 30); // (SPIM, MISO=29, SCK=3, MOSI=30)
uint8_t g_eth_mac[6] = {0}; // set in setup(), used in loop()
#ifndef TCP_PORT
#define TCP_PORT 5000
#endif
// Static IP (no DHCP): predictable address for Home Assistant AND avoids
// the multi-second blocking DHCP that reboot-loops the device on PoE.
// Override per network if needed (octets are comma-separated).
#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
#elif defined(BLE_PIN_CODE)
#include <helpers/nrf52/SerialBLEInterface.h>
SerialBLEInterface serial_interface;
#elif defined(ETHERNET_ENABLED)
@ -114,6 +139,27 @@ void halt() {
unsigned long last_wifi_reconnect_attempt = 0;
#endif
#if defined(WITH_ETHERNET_COMPANION)
// Direct W5100S register write via eth_spi (proven path). Common-register
// block addresses are fixed: GAR=0x0001, SUBR=0x0005, SHAR=0x0009, SIPR=0x000F.
static void eth_wr(uint16_t a, uint8_t v) {
eth_spi.beginTransaction(SPISettings(4000000, MSBFIRST, SPI_MODE0));
digitalWrite(26, LOW);
eth_spi.transfer(0xF0); eth_spi.transfer(a >> 8); eth_spi.transfer(a & 0xFF); eth_spi.transfer(v);
digitalWrite(26, HIGH);
eth_spi.endTransaction();
}
static void eth_write_netcfg(const uint8_t* mac) {
const uint8_t ip[4] = { ETH_STATIC_IP };
const uint8_t gw[4] = { ETH_GATEWAY };
const uint8_t sn[4] = { ETH_SUBNET };
for (int i = 0; i < 4; i++) eth_wr(0x0001 + i, gw[i]); // GAR
for (int i = 0; i < 4; i++) eth_wr(0x0005 + i, sn[i]); // SUBR
for (int i = 0; i < 6; i++) eth_wr(0x0009 + i, mac[i]); // SHAR
for (int i = 0; i < 4; i++) eth_wr(0x000F + i, ip[i]); // SIPR
}
#endif
void setup() {
Serial.begin(115200);
board.begin();
@ -162,7 +208,39 @@ void setup() {
#endif
);
#ifdef BLE_PIN_CODE
#if defined(WITH_ETHERNET_COMPANION)
{
// Bring up the W5100S (RAK13800) TCP/IP stack so the companion protocol is
// reachable over Ethernet (Home Assistant connects to this IP : TCP_PORT).
// Chip power + reset is handled in board.begin() (WITH_W5100S_POE: 3V3_EN +
// RST). The W5100S has its OWN SPI peripheral (eth_spi on SPIM2, pins
// SCK=3/MISO=29/MOSI=30, CS=26) — separate from the radio, which uses the
// global SPI on SPIM3 remapped to the LoRa pins. Derive a stable
// locally-administered MAC from the nRF52 device ID.
// Compute a stable locally-administered MAC from the nRF52 device ID.
// IMPORTANT: the W5100S/Ethernet library bring-up (W5100.init does a PHY
// soft-reset) is DEFERRED to loop() — see below. Doing it here in setup
// dipped the W5100S current during the marginal PoE cold-start window and
// collapsed the RAK19018 (Silvertel) converter → reboot loop. board.begin
// already has the W5100S drawing current (3V3_EN + RST + bit-bang reset),
// which latches the PoE converter just like the plain repeater build.
g_eth_mac[0] = 0x02; // locally administered, unicast
uint32_t id0 = NRF_FICR->DEVICEID[0];
uint32_t id1 = NRF_FICR->DEVICEID[1];
g_eth_mac[1] = (id0 >> 24) & 0xFF;
g_eth_mac[2] = (id0 >> 16) & 0xFF;
g_eth_mac[3] = (id0 >> 8) & 0xFF;
g_eth_mac[4] = (id0) & 0xFF;
g_eth_mac[5] = (id1) & 0xFF;
// Non-disruptive SPI setup here (no chip reset); the disruptive part — the
// lib's Ethernet.begin() / W5100.init() PHY soft-reset — is deferred to
// loop() (~6 s) so it can't collapse the marginal PoE supply at cold start.
eth_spi.begin();
Ethernet.init(eth_spi, 26);
Serial.println("Ethernet companion: bring-up deferred to loop()");
}
#elif defined(BLE_PIN_CODE)
serial_interface.begin(BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name, the_mesh.getBLEPin());
the_mesh.startInterface(serial_interface);
#elif defined(ETHERNET_ENABLED)
@ -291,4 +369,24 @@ void loop() {
last_wifi_reconnect_attempt = millis();
}
#endif
#if defined(WITH_ETHERNET_COMPANION)
// Deferred Ethernet bring-up: only AFTER the device has booted and the PoE
// converter is solidly latched (~6 s). The W5100.init() PHY soft-reset would
// collapse the marginal PoE supply if done during setup() (reboot loop).
static bool _eth_up = false;
if (!_eth_up && millis() > 6000) {
IPAddress sip(ETH_STATIC_IP), sgw(ETH_GATEWAY), ssn(ETH_SUBNET);
Ethernet.begin(g_eth_mac, sip, sgw, sgw, ssn); // inits chip mode/sockets (PHY soft-reset)
serial_interface.begin(TCP_PORT); // start TCP server
delay(50);
eth_write_netcfg(g_eth_mac); // force IP/GW/SN/MAC (reliable here)
_eth_up = true;
IPAddress ip = Ethernet.localIP();
Serial.print("Ethernet up (deferred): ");
Serial.print(ip[0]); Serial.print('.'); Serial.print(ip[1]); Serial.print('.');
Serial.print(ip[2]); Serial.print('.'); Serial.print(ip[3]);
Serial.print(":"); Serial.println(TCP_PORT);
}
#endif
}

14
examples/simple_repeater/main.cpp

@ -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();
@ -195,6 +202,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
@ -204,4 +217,5 @@ void loop() {
}
#endif
}
#endif // WITH_W5100S_POE
}

151
src/helpers/SerialEthernetInterface.cpp

@ -0,0 +1,151 @@
#include "SerialEthernetInterface.h"
void SerialEthernetInterface::begin(int port) {
// Ethernet hardware (Ethernet.init/begin) is brought up in setup();
// here we only start the TCP server.
server = new EthernetServer(port);
server->begin();
}
void SerialEthernetInterface::enable() {
if (_isEnabled) return;
_isEnabled = true;
send_queue_len = 0;
}
void SerialEthernetInterface::disable() {
_isEnabled = false;
}
size_t SerialEthernetInterface::writeFrame(const uint8_t src[], size_t len) {
if (len > MAX_FRAME_SIZE) {
ETH_DEBUG_PRINTLN("writeFrame(): frame too big, len=%d", (int)len);
return 0;
}
if (!_connected || len == 0) return 0;
if (send_queue_len >= ETH_FRAME_QUEUE_SIZE) {
ETH_DEBUG_PRINTLN("writeFrame(): send_queue full (dropping code=0x%02x)", src[0]);
return 0;
}
// PUSH codes (>= 0x80) go to all clients; command responses go to the
// client that issued the most recent command.
int8_t target = (src[0] >= 0x80) ? -1 : (int8_t)_last_rx;
ETH_DEBUG_PRINTLN("TX code=0x%02x len=%d -> %s", src[0], (int)len,
target < 0 ? "all" : (target == 0 ? "slot0" : target == 1 ? "slot1" : "slot2"));
send_queue[send_queue_len].target = target;
send_queue[send_queue_len].len = (uint8_t)len;
memcpy(send_queue[send_queue_len].buf, src, len);
send_queue_len++;
return len;
}
size_t SerialEthernetInterface::checkRecvFrame(uint8_t dest[]) {
if (server == NULL) return 0;
// ---- accept a new connection into a free slot --------------------------
// accept() returns each new connection once and maintains the listen socket,
// so it must be called every loop.
EthernetClient nc = server->accept();
if (nc) {
int slot = -1;
for (int i = 0; i < MAX_ETH_CLIENTS; i++) {
if (!clients[i].connected()) { slot = i; break; }
}
if (slot >= 0) {
clients[slot].stop(); // free any lingering socket in this slot
clients[slot] = nc;
rx_header[slot].type = 0;
rx_header[slot].length = 0;
ETH_DEBUG_PRINTLN("Got connection (slot %d)", slot);
} else {
nc.stop(); // all slots busy — reject
ETH_DEBUG_PRINTLN("Rejected connection (all %d slots busy)", MAX_ETH_CLIENTS);
}
}
// ---- refresh connected state, free dropped sockets ---------------------
bool any = false;
for (int i = 0; i < MAX_ETH_CLIENTS; i++) {
if (clients[i].connected()) {
any = true;
} else if (rx_header[i].type || rx_header[i].length) {
// a client that was active just dropped — reset its parse state
rx_header[i].type = 0;
rx_header[i].length = 0;
clients[i].stop();
ETH_DEBUG_PRINTLN("Disconnected (slot %d)", i);
}
}
_connected = any;
// ---- drain the outbound queue ------------------------------------------
while (send_queue_len > 0) {
Frame &f = send_queue[0];
uint8_t pkt[3 + MAX_FRAME_SIZE];
pkt[0] = '>';
pkt[1] = (f.len & 0xFF);
pkt[2] = (f.len >> 8);
memcpy(&pkt[3], f.buf, f.len);
if (f.target < 0) { // broadcast (push)
for (int i = 0; i < MAX_ETH_CLIENTS; i++) {
if (clients[i].connected()) clients[i].write(pkt, 3 + f.len);
}
} else if (f.target < MAX_ETH_CLIENTS && clients[f.target].connected()) {
clients[f.target].write(pkt, 3 + f.len); // response to the requester
}
send_queue_len--;
for (int i = 0; i < send_queue_len; i++) send_queue[i] = send_queue[i + 1];
}
// ---- read ONE inbound frame (round-robin across clients) ---------------
for (int k = 0; k < MAX_ETH_CLIENTS; k++) {
int i = (_rr + k) % MAX_ETH_CLIENTS;
EthernetClient &c = clients[i];
if (!c.connected()) continue;
// frame header = [type][len_lo][len_hi]
if (rx_header[i].type == 0 || rx_header[i].length == 0) {
if (c.available() >= 3) {
c.readBytes(&rx_header[i].type, 1);
c.readBytes((uint8_t *)&rx_header[i].length, 2);
}
}
if (rx_header[i].type != 0 && rx_header[i].length != 0) {
int avail = c.available();
int frame_type = rx_header[i].type;
int frame_length = rx_header[i].length;
if (frame_length > avail) continue; // wait for the rest
if (frame_length > MAX_FRAME_SIZE || frame_type != '<') {
// oversized or unexpected type — discard
while (frame_length > 0) {
uint8_t skip[1];
int n = c.read(skip, 1);
if (n <= 0) break;
frame_length -= n;
}
rx_header[i].type = 0;
rx_header[i].length = 0;
continue;
}
c.readBytes(dest, frame_length);
rx_header[i].type = 0;
rx_header[i].length = 0;
_last_rx = i; // route responses back here
_rr = (i + 1) % MAX_ETH_CLIENTS; // fairness
ETH_DEBUG_PRINTLN("RX[%d] cmd=0x%02x len=%d", i, dest[0], frame_length);
return frame_length;
}
}
return 0;
}

78
src/helpers/SerialEthernetInterface.h

@ -0,0 +1,78 @@
#pragma once
#include "BaseSerialInterface.h"
#include <RAK13800_W5100S.h>
// Multi-client TCP companion interface over a W5100S Ethernet module (RAK13800).
// Lets several clients (e.g. Home Assistant AND the phone app) stay connected
// at once — the single-client model had them kicking each other off the one
// socket, causing an endless reconnect loop.
//
// Routing of outbound frames (the companion protocol isn't natively
// multi-client, so we route by frame code):
// - PUSH frames (code >= 0x80, e.g. LoRa-RX log, adverts) -> ALL clients
// - command RESPONSES (code < 0x80) -> the client
// that issued
// the last command
//
// Ethernet hardware (Ethernet.init/begin) is brought up outside this class.
#ifndef MAX_ETH_CLIENTS
#define MAX_ETH_CLIENTS 3 // W5100S has 4 sockets: up to 3 clients + 1 listen
#endif
class SerialEthernetInterface : public BaseSerialInterface {
bool _isEnabled;
bool _connected; // true if at least one client is connected
EthernetServer* server;
EthernetClient clients[MAX_ETH_CLIENTS];
struct FrameHeader { uint8_t type; uint16_t length; };
FrameHeader rx_header[MAX_ETH_CLIENTS]; // per-client inbound parse state
struct Frame {
int8_t target; // -1 = broadcast, else client index
uint8_t len;
uint8_t buf[MAX_FRAME_SIZE];
};
#define ETH_FRAME_QUEUE_SIZE 16
int send_queue_len;
Frame send_queue[ETH_FRAME_QUEUE_SIZE];
int _last_rx; // client index of the most recent inbound command
int _rr; // round-robin cursor for fair inbound polling
public:
SerialEthernetInterface() : server(NULL) {
_isEnabled = false;
_connected = false;
send_queue_len = 0;
_last_rx = -1;
_rr = 0;
for (int i = 0; i < MAX_ETH_CLIENTS; i++) { rx_header[i].type = 0; rx_header[i].length = 0; }
}
void begin(int port);
// BaseSerialInterface methods
void enable() override;
void disable() override;
bool isEnabled() const override { return _isEnabled; }
bool isConnected() const override { return _connected; }
bool isWriteBusy() const override { return false; }
size_t writeFrame(const uint8_t src[], size_t len) override;
size_t checkRecvFrame(uint8_t dest[]) override;
};
#if ETH_DEBUG_LOGGING && ARDUINO
#include <Arduino.h>
#define ETH_DEBUG_PRINT(F, ...) Serial.printf("ETH: " F, ##__VA_ARGS__)
#define ETH_DEBUG_PRINTLN(F, ...) Serial.printf("ETH: " F "\n", ##__VA_ARGS__)
#else
#define ETH_DEBUG_PRINT(...) {}
#define ETH_DEBUG_PRINTLN(...) {}
#endif

36
variants/rak4631/RAK4631Board.cpp

@ -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) {
@ -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
}

113
variants/rak4631/W5100SPoE.cpp

@ -0,0 +1,113 @@
#ifdef WITH_W5100S_POE
#include <Arduino.h>
#include <nrf.h>
#include "W5100SPoE.h"
// ── Early power-rail + RST release (constructor priority 200) ────────────────
// Runs before setup(), right after SystemInit. Two jobs, as early as possible
// so the W5100S draws its full operating current before the RAK19018
// (Silvertel) converter folds back during PoE cold-start:
//
// 1. Drive PIN_3V3_EN (P1.02 / WB_IO2 / Arduino 34) HIGH — this is the
// RAK19007 3.3 V PERIPHERAL POWER ENABLE that feeds the RAK13800/W5100S.
// Meshtastic does this in initVariant(); stock MeshCore never did, so our
// W5100S was only weakly powered via a default path (responds on USB but
// can't pull its full ~130 mA on the marginal PoE rail).
// 2. Drive W5100S RST (P0.21 / WB_IO3 / Arduino 21) HIGH — out of reset.
//
// Raw registers because the Arduino GPIO layer isn't up this early.
static void __attribute__((constructor(200))) w5100s_early_power_init() {
// P1.02 = 3V3_EN → HIGH (power the peripheral rail FIRST)
NRF_P1->PIN_CNF[2] =
(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_P1->OUTSET = (1UL << 2);
// 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 the 3V3 rail + RST are driven
// (Arduino API, in case the core re-init touched them), then soft-resets and
// reads VERSIONR. Returns VERSIONR (0x51 = healthy W5100S).
uint8_t w5100s_poe_init() {
// Make sure the peripheral power rail stays driven HIGH.
pinMode(W5100S_3V3_EN_PIN, OUTPUT); digitalWrite(W5100S_3V3_EN_PIN, HIGH);
delay(20); // let the rail/W5100S settle after 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

42
variants/rak4631/W5100SPoE.h

@ -0,0 +1,42 @@
#pragma once
// W5100S 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 that lets Meshtastic boot on PoE without a battery: bring the
// W5100S PHY into its active state. An active W5100S draws ~120 mA — enough
// to keep the Silvertel converter latched in continuous mode.
//
// The W5100S has no power-enable pin on this board (always powered), but its
// RST must be HIGH for the PHY to run. We release RST as early as possible
// (before setup(), via a constructor) so the PHY draws current and latches
// the converter before its foldback timer expires.
//
// 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
// RAK19007 3.3 V peripheral power enable (feeds the RAK13800/W5100S).
// Must be driven HIGH or the W5100S is only weakly powered — exactly what
// Meshtastic's initVariant() does and stock MeshCore omits.
#ifndef W5100S_3V3_EN_PIN
#define W5100S_3V3_EN_PIN 34 // P1.02 / WB_IO2
#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();

4
variants/rak4631/target.cpp

@ -2,6 +2,10 @@
#include "target.h"
#include <helpers/ArduinoHelpers.h>
#ifdef WITH_W5100S_POE
#include "W5100SPoE.h"
#endif
RAK4631Board board;
#ifndef PIN_USER_BTN

Loading…
Cancel
Save