Skip to content

Add software-controlled PoE for RTL8238B boards (register-based, generic per-machine layer)#265

Open
tobik312 wants to merge 5 commits into
logicog:mainfrom
tobik312:poe-rtl8238b
Open

Add software-controlled PoE for RTL8238B boards (register-based, generic per-machine layer)#265
tobik312 wants to merge 5 commits into
logicog:mainfrom
tobik312:poe-rtl8238b

Conversation

@tobik312

Copy link
Copy Markdown

Adds Power-over-Ethernet support for RTL8373 boards whose PSE is an RTL8238B on I2C,
with a generic, opt-in per-machine PoE layer. Tested on keepLink KP-9000-9XHPML-X V3.1
(8× 2.5G PoE+ + 1× 10G SFP+).

How it works — and how it differs from existing drivers

Other open PoE drivers (realtek-poe, the OpenWrt realtek-pse-* driver) talk to these
PSEs with a 12-byte host-command protocol answered by a companion management MCU. On these
boards that MCU layer does not respond — the RTL8238B is reachable only as a raw I2C register
slave
.

So this driver does everything through direct register I/O, exactly as the OEM web UI does:

  • uploads the volatile PSE application firmware at boot (a bit-banged I2C stream on GPIO46/47,
    because the image far exceeds the SoC I2C engine's 16-byte/transfer limit),
  • enables ports through the chip's admin registers,
  • reads telemetry (power-on / class / voltage / current) straight from the register window.

Per-port enable was verified on hardware to be real auto-detect (no running MCU app required):
the chip powers only genuine PDs — a Class 4 PD read 51 V / 153 mA, and an enabled-but-empty
port stays unpowered.

The register interface (reverse-engineered)

The RTL8238B register file is undocumented; the layout below was reverse-engineered from the OEM
software — the stock firmware's own PoE bring-up code, and the register accesses its web UI makes —
then verified on hardware. Access is plain little-endian I2C; each controller drives four port
channels (local 0–3):

  • Bring-up: 0x1a = whole-chip reset; arm download mode on 0xec (payload 03 00 00 00,
    read-back bit 2 = armed) with an alternate arm on 0x18 (00 00 20 39); stream the image to
    data port 0xf4; 0xec bit 5 = image accepted.
  • Control: per-port admin mode is a 2-bit field in 0x12 (byte 2 of the 0x10 block) —
    3 = on (auto-detect), 0 = off — plus a 1-bit enable in 0x14.
  • Telemetry: Power-On status is 0x10 byte 0 bit 4+local; PD class is the high nibble of
    0x0c+local; per-port voltage/current is 0x30+4*local (volts = top byte × 0.5 V,
    mA = low 16 bits × 125 / 4096).

Full register map + decode in doc/poe-rtl8238b.md.

Generic & opt-in

PoE is described in the machine descriptor (struct machine.poe = chip / I2C addresses / port
count) and gated behind POE_PRESENT: non-PoE machines compile no PoE code at all (~6 KB of
BANK2). Adding another RTL8238B board is descriptor-only; a different PSE chip is a new
poe_<chip>.c behind the same chip-agnostic interface (poe.h).

Interfaces

  • Console: poe load | port <n> <on|off> | global <on|off> (chip-agnostic).
  • Web: a PoE page showing total consumption and a per-port table (state / class / W / V / mA)
    with per-row and global enable/disable.
  • API: GET /poe.json returns the driver-normalized per-port status
    (port, admin, on, class, v, ma); it takes no query parameters — the controller is never
    exposed over HTTP.
image

Firmware image

The RTL8238B's PoE application firmware is volatile and Realtek/OEM-proprietary, so it is not
committed. You extract it from your own device's OEM firmware with
tools/poe/extract_rtl8238b_image.py, and the Makefile embeds it. An OEM dump can bundle PSE
firmware for several chips; the tool writes the RTL8238B image (identified by size) to
tools/poe/pse_image.bin. See doc/poe-rtl8238b.md.

Notes

  • The PoE page and nav link live in the shared html_data, so they're present in every build;
    on a non-PoE machine the page just shows a "no PoE controller" notice (the /poe.json route is
    compiled out). HTML assets are machine-agnostic by design.
  • The bring-up internals (the 0x20/0x21 two-controller topology, the per-port register
    tables) are hardcoded in the driver as the chip's standard layout; machine.poe parameterizes
    the I2C addresses and port count. A board with a non-standard RTL8238B topology would extend the
    driver.

Testing

  • Builds clean for the PoE machine (KP_9000_9XHPML_X_V3_1) and for a non-PoE machine (PoE fully
    excluded).
  • On hardware: PoE comes up automatically at boot, PDs power up, per-port and global
    enable/disable work, and the web page shows live telemetry (verified Class 4 @ 51 V / 153 mA).

Tobiasz added 5 commits June 21, 2026 10:56
Add the generic PoE interface (poe.h) and the RTL8238B driver that
implements it (poe_rtl8238b.c). Unlike the host-command PoE projects
(poemgr / realtek-poe / OpenWrt PSE), this chip answers only as a raw
I2C register slave on these boards, so the driver does the bring-up
(volatile firmware upload over a bit-banged I2C stream, then per-port
enable) and all control and telemetry via register reads/writes - the
same register window the OEM web UI uses. A different PSE chip is a new
poe_<chip>.c implementing the same interface.
Describe PoE per board in struct machine (poe = chip, I2C addresses,
port count). A board's POE_CHIP_<chip> macro selects its driver and
implies POE_PRESENT, which gates the chip-agnostic consumers; non-PoE
machines compile none of it. The boot bring-up is data-driven
(machine.poe.chip).

Add the KP-9000-9XHPML-X V3.1 PoE machine by reusing the base V3.1
block: machine.h chains MACHINE_KP_9000_9XHPML_X_V3_1 to
MACHINE_KP_9000_9XHML_X_V3_1 + POE_CHIP_RTL8238B, and the only
board-config diffs (.poe, .machine_name) are #ifdef POE_PRESENT in the
single block - no duplicated config.

Conditionally embed the user-supplied PSE image in the Makefile via the
RTL8238B-specific RTL8238B_MACHINES / RTL8238B_IMAGE_LOCATION (the
latter must match PSE_IMG_ADDR in the driver). The Makefile diff is
kept minimal.
Add poe load | port <n> <on|off> | global <on|off>, gated on
POE_PRESENT. Deliberately chip-agnostic - only the operations any PoE
driver provides; status and telemetry are served at GET /poe.json.
/poe.json serves the driver-normalized per-port status (port, admin,
on, class, v, ma); the OEM-style PoE page renders it directly (per-row
and global enable/disable, total consumption) with no chip-specific
decoding in JS. Both are gated on POE_PRESENT; the page degrades to a
"no PoE controller" notice on non-PoE machines. The endpoint takes no
query parameters - the controller is never exposed over HTTP.
Add PoE to the README (feature bullet + doc link) and document the
generic PoE framework (doc/poe.md - machine descriptor, gating,
interface, console/web) and the RTL8238B driver specifics, including
the register-based (vs host-command) approach and the empirically
reverse-engineered register map (doc/poe-rtl8238b.md), plus the board
(doc/devices/KP-9000-9XHPML-X-V3.1.md).

Add tools/poe/extract_rtl8238b_image.py to carve the PSE image from an
OEM firmware dump. The image is Realtek/OEM proprietary and user-
supplied, not committed.
@logicog

logicog commented Jun 21, 2026

Copy link
Copy Markdown
Owner

The RTL8238B register file is undocumented; the layout below was reverse-engineered from the OEM
software — the stock firmware's own PoE bring-up code, and the register accesses its web UI makes —
then verified on hardware. Access is plain little-endian I2C; each controller drives four port
channels (local 0–3):

This was probably an enormous amount of work. Amazing! I am just browsing though the code, it is great! It will take a bit to go through it, so please bear with all of us a bit.

I so far went through the commits of this PR and I think you are splitting it absolutely the right way, so that is already cool.

@logicog logicog requested review from feelfree69 and vDorst June 21, 2026 20:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants