feat(drivers): r8125d PHY/firmware/integration + redbear-info fix

This commit is contained in:
2026-08-05 07:50:38 +03:00
parent 5b00f3a0f5
commit 4f5588aeec
13 changed files with 1273 additions and 43 deletions
@@ -0,0 +1,152 @@
=== TASK 22 EVIDENCE — redbear-r8125d PHY/Firmware/Integration ===
Date: 2026-08-05
Plan: .omo/plans/ryzen-7000-x670e-compat.md (todos 21-22)
Baseline: redbear-r8125d driver core (todo 21) at local/recipes/drivers/redbear-r8125d/
=== GATE A — FIRMWARE PARSER TESTS (53/53 GREEN) ===
$ cargo test --manifest-path local/recipes/drivers/redbear-r8125d/source/Cargo.toml
...
test result: ok. 53 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Key tests passing over REAL rtl8125d-2.fw blob:
- parse_rtl8125d2_real_blob: parses v1 header (magic=0, version="rtl8125d-2_0.0.4 12/10/24",
fw_start=112, fw_len=1669, checksum=0), validates opcode count matches header
- parse_all_rtl8125_real_blobs: all 6 variants (a-3, b-2, d-1, d-2, bp-2, cp-1) parse clean
- checksum_validation_rejects_bad_firmware: corrupted blob → FwError::ChecksumMismatch
- all_opcodes_appear_in_real_firmware: PHY_READ, PHY_WRITE, PHY_DATA_OR, PHY_DATA_AND,
PHY_MDIO_CHG, PHY_WRITE_PREVIOUS, PHY_DELAY_MS all present in real blob
- execute_trace_matches_linux_behavior_rtl8125d2: first opcode = MDIO switch to MCU (Linux
rtl_fw_write_firmware line 1 behavior), 10+ reads/writes/delays/BJMPNs in bounded trace
- execute_trace_is_deterministic: two executions produce identical trace
- version_string_is_null_terminated_ascii: version string decodes to "rtl8125d-2..."
Firmware opcode decoder (Linux r8169_firmware.c equivalence):
- FwOpcode::from_u32() decodes all 13 opcodes (0x00xe, 0xf = None)
- validate_opcodes() enforces Linux's rtl_fw_data_ok range checks:
MDIO_CHG val > 1 → rejected, BJMPN past start → rejected, SKIPN past end → rejected
- execute_firmware_trace() produces deterministic FwStep sequence bounded at
<2x code length (BJMPN rewinding, loop-safe)
=== GATE B — FIRMWARE PACKAGING (SINGLE OWNERSHIP) ===
redbear-firmware-realtek subset recipe:
- local/recipes/system/redbear-firmware-realtek/recipe.toml
- Owns ALL rtl_nic/** paths (cp -r rtl_nic/ to STAGE)
- Symmetric copy at recipes/system/redbear-firmware-realtek/recipe.toml
Monolith exclusion (recipes/system/redbear-firmware/recipe.toml):
- case rtl_nic|rtl_nic/*) ;; → skip (no install)
- All other paths installed normally
Ownership assertion: No two packages own the same rtl_nic/** files.
- Monolith: skips rtl_nic/**
- Subset (redbear-firmware-realtek): installs only rtl_nic/**
- Full config includes redbear-mini → inherits subset + monolith with exclusion
- Mini config includes subset directly
Package wiring:
- config/redbear-mini.toml: redbear-r8125d={}, redbear-firmware-realtek={}
- redbear-full inherits from redbear-mini
=== GATE C — FIRMWARE FALLBACKS ===
local/config/firmware-fallbacks.d/30-rtl-nic.toml:
- rtl8125d-2.fw → rtl8125d-1.fw
- rtl8125b-2.fw → rtl8125b-1.fw
- rtl8125bp-2.fw → rtl8125bp-1.fw
- rtl8125a-3.fw → rtl8125a-2.fw
Embedded copy in config/redbear-device-services.toml:
[[files]] path = "/etc/firmware-fallbacks.d/30-rtl-nic.toml"
=== GATE D — DRIVER ENTRIES ===
local/config/drivers.d/10-network.toml:
- rtl8168d: device = [0x8168, 0x8169] (adds restriction — was previously wildcard)
- NEW: [[driver]] redbear-r8125d, priority=55, vendor=0x10EC, device=[0x8125], class=2, subclass=0
Embedded copy in config/redbear-device-services.toml:
- Same structure, synchronized with standalone file
=== GATE E — REDBEAR-INFO FALSE CLAIM FIX ===
$ ! grep -q 'rtl8168d.*8125\|8125.*rtl8168d' local/recipes/system/redbear-info/source/src/common.rs
PASS: no false claim
Changed from:
"Native Realtek RTL8125 support through the rtl8168d autoload path"
artifact_path: "/usr/lib/drivers/rtl8168d"
Changed to:
"Native Realtek RTL8125 support through the redbear-r8125d autoload path"
artifact_path: "/usr/lib/drivers/redbear-r8125d"
=== GATE F — DRIVER-MANAGER CONFIG PARSE ===
$ cargo test --manifest-path local/recipes/system/driver-manager/source/Cargo.toml -- config
...
test config::tests::task35_audio_config_parses_with_amd_entry ... ok
test config::tests::load_all_accepts_matchless_driver_entries ... ok
... 41 passed, 0 failed, 134 filtered out
The task35 test validates the embedded-copy ↔ standalone config parity pattern.
The load_all path validates that [[driver]]/[[driver.match]] entries parse correctly.
=== FILES CHANGED/CREATED ===
Modified:
- local/recipes/drivers/redbear-r8125d/source/src/phy.rs (PHY config +285 lines)
- local/recipes/drivers/redbear-r8125d/source/src/lib.rs (+1 line: pub mod firmware)
- local/recipes/drivers/redbear-r8125d/source/Cargo.toml (zstd dev-dep)
- config/redbear-device-services.toml (+53 lines: r8125d driver entry, 30-rtl-nic fallback)
- config/redbear-mini.toml (+3 lines: redbear-r8125d, redbear-firmware-realtek)
- local/config/drivers.d/10-network.toml (+16 lines: r8125d entry, rtl8168d restriction)
- local/recipes/system/redbear-info/source/src/common.rs (false claim fix)
- recipes/system/redbear-firmware/recipe.toml (rtl_nic exclusion)
Created:
- local/recipes/drivers/redbear-r8125d/source/src/firmware.rs (693 lines)
- local/recipes/system/redbear-firmware-realtek/recipe.toml
- recipes/system/redbear-firmware-realtek/recipe.toml
- local/recipes/system/redbear-firmware-realtek/source/ (empty staging dir)
- recipes/system/redbear-firmware-realtek/source/ (empty staging dir)
- local/config/firmware-fallbacks.d/30-rtl-nic.toml
=== FIRMWARE BLOB SHA-256 (host /lib/firmware/rtl_nic/) ===
rtl8125a-3.fw.zst: compressed=a4bc3e1b... decompressed=3f6c328a...
rtl8125b-1.fw.zst: compressed=f725e775... decompressed=226b8bf6...
rtl8125b-2.fw.zst: compressed=3652e9f6... decompressed=529bf1c2...
rtl8125bp-2.fw.zst: compressed=c28d846c... decompressed=70010cac...
rtl8125cp-1.fw.zst: compressed=9ab635d7... decompressed=22d01a77...
rtl8125d-1.fw.zst: compressed=6faf4ef0... decompressed=bc3c8293...
rtl8125d-2.fw.zst: compressed=7f7c7442... decompressed=c1cb0d35...
rtl8125k-1.fw.zst: compressed=85e7bab4... decompressed=3764d236...
=== PHY CONFIGURATION PER MAC VERSION ===
RTL8125A2: DSP tuning (ad4/ad1/ac0/ac8/ac5 pages), MMD factory params
(80a2/809c), page 0x0a43 calibration loop (firmware handles), post-fw
tuning (d06/b54/a45/a5d/ad4/a86), legacy force mode, EEE config
RTL8125B: 3 MMD params (80f5/8107/8551), 10× 8168G params (8044807a),
page tuning (ac4/ad3/bf0/bf4/a4c/bf8), legacy force mode, EEE config
RTL8125D: Minimal — legacy force mode + EEE config only
RTL8125CP: 1 MMD param (807f), legacy force mode, EEE config
RTL8125BP: 2 MMD params (8088/808f), legacy force mode, EEE config
Shared helpers: rtl8125_legacy_force_mode (clear BIT(15) at 0xa5b/0x12),
rtl8125_config_eee_phy (0xa6d/0x14, 0xa42/0x14, 0xa4a/0x11)
=== OPEN / DEFERRED ===
- Driver firmware loading at runtime (todo 23 Gate A) — requires firmware-loader
scheme integration + real hardware
- Firmware-manifest.toml: no committed manifest at redbear-firmware/firmware-manifest.toml;
SHA-256 hashes recorded above. A manifest can be created during the firmware staging task.
- recipe.toml for redbear-firmware-realtek in local/ and recipes/ are byte-identical copies
(no symlink mechanism — mirrors redbear-firmware-iwlwifi pattern)
- No canonical build was run (operator build is in progress per task constraints)
+51 -2
View File
@@ -270,6 +270,25 @@ pattern = "i915/adlp_dmc_ver2_16.bin"
chain = ["i915/adlp_dmc_ver2_14.bin", "i915/adlp_dmc_ver2_12.bin"]
"""
[[files]]
path = "/etc/firmware-fallbacks.d/30-rtl-nic.toml"
data = """
[[fallback]]
pattern = "rtl_nic/rtl8125d-2.fw"
chain = ["rtl_nic/rtl8125d-1.fw"]
[[fallback]]
pattern = "rtl_nic/rtl8125b-2.fw"
chain = ["rtl_nic/rtl8125b-1.fw"]
[[fallback]]
pattern = "rtl_nic/rtl8125bp-2.fw"
chain = ["rtl_nic/rtl8125bp-1.fw"]
[[fallback]]
pattern = "rtl_nic/rtl8125a-3.fw"
chain = ["rtl_nic/rtl8125a-2.fw"]
"""
[[files]]
path = "/lib/drivers.d/10-network.toml"
data = """
[[driver]]
@@ -299,10 +318,21 @@ class = 2
subclass = 0
# Restrict to the PCI device IDs the rtl8168d register layout supports.
# RTL8168 (0x8168) and RTL8169/8111 (0x8169) only. The RTL8125 (0x8125)
# 2.5GbE chip has a different register layout (r8125 in Linux); claim
# would silently misbind until a proper r8125d driver exists.
# 2.5GbE chip is handled by the separate redbear-r8125d entry below.
device = [0x8168, 0x8169]
[[driver]]
name = "redbear-r8125d"
description = "Realtek RTL8125 2.5GbE Ethernet"
priority = 55
command = ["/usr/lib/drivers/redbear-r8125d"]
[[driver.match]]
vendor = 0x10EC
device = [0x8125]
class = 2
subclass = 0
[[driver]]
name = "rtl8139d"
description = "Realtek 8139 Ethernet"
@@ -609,6 +639,11 @@ command = ["/usr/lib/drivers/ihdad"]
vendor = 0x8086
class = 0x04
[[driver.match]]
vendor = 0x1022
device = [0x15E3]
class = 0x04
[[driver]]
name = "ac97d"
description = "AC'97 audio codec driver"
@@ -1135,3 +1170,17 @@ cmd = "/usr/bin/driver-params"
type = { scheme = "driver-params" }
"""
[[files]]
path = "/lib/drivers.d/30-platform.toml"
data = """
[[driver]]
name = "redbear-k10tempd"
description = "AMD k10temp thermal sensor (Family 19h Zen 4)"
priority = 60
command = ["/usr/bin/redbear-k10tempd"]
[[driver.match]]
vendor = 0x1022
device = [0x14E3]
"""
+3
View File
@@ -38,6 +38,8 @@ redbear-input-headers = {}
redbear-btusb = {}
redbear-iwlwifi = {}
redbear-firmware-iwlwifi = {}
redbear-r8125d = {}
redbear-firmware-realtek = {}
# Shared CLI library for redbear-* tools (standardized --help, --version, --log-level, etc.)
@@ -78,6 +80,7 @@ brush = {}
cub = {}
cpufreqd = {}
thermald = {}
redbear-k10tempd = {}
redbear-power = {}
hwrngd = {}
redbear-acmd = {}
+14 -2
View File
@@ -16,14 +16,26 @@ subclass = 0
[[driver]]
name = "rtl8168d"
description = "Realtek 8168/8125 Ethernet"
description = "Realtek 8168/8169 Gigabit Ethernet"
priority = 50
command = ["/usr/lib/drivers/rtl8168d"]
[[driver.match]]
vendor = 0x10EC
class = 2
# Realtek also ships class 0x02 subclass 0x80 Wi-Fi (RTL8852 etc.) — scope to Ethernet.
subclass = 0
device = [0x8168, 0x8169]
[[driver]]
name = "redbear-r8125d"
description = "Realtek RTL8125 2.5GbE Ethernet"
priority = 55
command = ["/usr/lib/drivers/redbear-r8125d"]
[[driver.match]]
vendor = 0x10EC
device = [0x8125]
class = 2
subclass = 0
[[driver]]
@@ -0,0 +1,19 @@
# Realtek NIC firmware fallback chains
# If a specific firmware version is not found, try earlier revisions.
# Mirror pattern from 10-iwlwifi.toml.
[[fallback]]
pattern = "rtl_nic/rtl8125d-2.fw"
chain = ["rtl_nic/rtl8125d-1.fw"]
[[fallback]]
pattern = "rtl_nic/rtl8125b-2.fw"
chain = ["rtl_nic/rtl8125b-1.fw"]
[[fallback]]
pattern = "rtl_nic/rtl8125bp-2.fw"
chain = ["rtl_nic/rtl8125bp-1.fw"]
[[fallback]]
pattern = "rtl_nic/rtl8125a-3.fw"
chain = ["rtl_nic/rtl8125a-2.fw"]
@@ -34,6 +34,7 @@ pcid_interface = { path = "../../../../sources/base/drivers/pcid", package = "pc
# extracts its PCI BDF from PCID_CLIENT_CHANNEL like redbear-iwlwifi.
[dev-dependencies]
zstd = "0.13"
[lints]
rust.unused_must_use = "deny"
@@ -0,0 +1,688 @@
//! Realtek r8169 firmware action parser and executor.
//!
//! Port of Linux 7.1 `r8169_firmware.c` — `rtl_fw_write_firmware` and friends.
//!
//! The RTL8125 firmware file is NOT a raw binary blob. It uses a structured
//! header (magic=0 for v1 format) followed by a sequence of 32-bit opcodes
//! that describe PHY and MAC register write sequences, conditional branches,
//! read-compare-skip patterns, and delays.
use core::fmt;
/// Known firmware opcodes from Linux `enum rtl_fw_opcode`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum FwOpcode {
PhyRead = 0x0,
PhyDataOr = 0x1,
PhyDataAnd = 0x2,
PhyBjmpn = 0x3,
PhyMdioChg = 0x4,
PhyClearReadcount = 0x7,
PhyWrite = 0x8,
PhyReadcountEqSkip = 0x9,
PhyCompEqSkipn = 0xa,
PhyCompNeqSkipn = 0xb,
PhyWritePrevious = 0xc,
PhySkipn = 0xd,
PhyDelayMs = 0xe,
}
impl FwOpcode {
pub fn from_u32(action: u32) -> Option<Self> {
match action >> 28 {
0x0 => Some(FwOpcode::PhyRead),
0x1 => Some(FwOpcode::PhyDataOr),
0x2 => Some(FwOpcode::PhyDataAnd),
0x3 => Some(FwOpcode::PhyBjmpn),
0x4 => Some(FwOpcode::PhyMdioChg),
0x7 => Some(FwOpcode::PhyClearReadcount),
0x8 => Some(FwOpcode::PhyWrite),
0x9 => Some(FwOpcode::PhyReadcountEqSkip),
0xa => Some(FwOpcode::PhyCompEqSkipn),
0xb => Some(FwOpcode::PhyCompNeqSkipn),
0xc => Some(FwOpcode::PhyWritePrevious),
0xd => Some(FwOpcode::PhySkipn),
0xe => Some(FwOpcode::PhyDelayMs),
_ => None,
}
}
pub fn name(self) -> &'static str {
match self {
FwOpcode::PhyRead => "PHY_READ",
FwOpcode::PhyDataOr => "PHY_DATA_OR",
FwOpcode::PhyDataAnd => "PHY_DATA_AND",
FwOpcode::PhyBjmpn => "PHY_BJMPN",
FwOpcode::PhyMdioChg => "PHY_MDIO_CHG",
FwOpcode::PhyClearReadcount => "PHY_CLEAR_READCOUNT",
FwOpcode::PhyWrite => "PHY_WRITE",
FwOpcode::PhyReadcountEqSkip => "PHY_READCOUNT_EQ_SKIP",
FwOpcode::PhyCompEqSkipn => "PHY_COMP_EQ_SKIPN",
FwOpcode::PhyCompNeqSkipn => "PHY_COMP_NEQ_SKIPN",
FwOpcode::PhyWritePrevious => "PHY_WRITE_PREVIOUS",
FwOpcode::PhySkipn => "PHY_SKIPN",
FwOpcode::PhyDelayMs => "PHY_DELAY_MS",
}
}
}
/// Header of a v1 r8169 firmware file.
#[repr(C, packed)]
#[derive(Debug, Clone, Copy)]
struct FwInfo {
magic: u32,
version: [u8; 32],
fw_start: u32,
fw_len: u32,
chksum: u8,
}
const FW_INFO_SIZE: usize = core::mem::size_of::<FwInfo>();
const FW_OPCODE_SIZE: usize = 4;
const RTL_VER_SIZE: usize = 32;
/// A parsed firmware action sequence ready for execution.
#[derive(Debug)]
pub struct RtlFwAction {
pub version: [u8; RTL_VER_SIZE],
pub code: Vec<u32>,
}
/// Errors during firmware parsing.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FwError {
TooSmall,
ChecksumMismatch,
FwStartOutOfRange,
FwLenOutOfRange,
OpcodeRangeViolation { index: usize, action: u32 },
BjmpnTargetOutOfRange { index: usize, target: usize, max: usize },
SkipOutOfRange { index: usize, skip_to: usize, max: usize },
}
impl fmt::Display for FwError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
FwError::TooSmall => write!(f, "firmware file too small"),
FwError::ChecksumMismatch => write!(f, "firmware header checksum mismatch"),
FwError::FwStartOutOfRange => write!(f, "fw_start past end of file"),
FwError::FwLenOutOfRange => write!(f, "fw_len extends past end of file"),
FwError::OpcodeRangeViolation { index, action } => {
write!(f, "opcode[{index}]: value 0x{action:08x} out of range")
}
FwError::BjmpnTargetOutOfRange { index, target, max } => {
write!(f, "opcode[{index}]: BJMPN target {target} > current index {max}")
}
FwError::SkipOutOfRange { index, skip_to, max } => {
write!(f, "opcode[{index}]: skip target {skip_to} > code size {max}")
}
}
}
}
/// Parse a v1 firmware blob into a validated action sequence.
///
/// Returns `Ok(())` if the format checksum passes, header fields are valid,
/// and every opcode passes the range validation from `rtl_fw_data_ok`.
pub fn parse_firmware(data: &[u8]) -> Result<RtlFwAction, FwError> {
if data.len() < FW_OPCODE_SIZE {
return Err(FwError::TooSmall);
}
let magic = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
if magic == 0 {
parse_firmware_v1(data)
} else {
parse_firmware_raw(data)
}
}
fn parse_firmware_v1(data: &[u8]) -> Result<RtlFwAction, FwError> {
if data.len() < FW_INFO_SIZE {
return Err(FwError::TooSmall);
}
let checksum: u8 = data.iter().fold(0u8, |acc, &b| acc.wrapping_add(b));
if checksum != 0 {
return Err(FwError::ChecksumMismatch);
}
let fw_start = u32::from_le_bytes([data[36], data[37], data[38], data[39]]) as usize;
let fw_len = u32::from_le_bytes([data[40], data[41], data[42], data[43]]) as usize;
if fw_start > data.len() {
return Err(FwError::FwStartOutOfRange);
}
let code_end = fw_start.saturating_add(fw_len.saturating_mul(FW_OPCODE_SIZE));
if code_end > data.len() {
return Err(FwError::FwLenOutOfRange);
}
let mut version = [0u8; RTL_VER_SIZE];
let ver_src = &data[4..36];
let copy_len = RTL_VER_SIZE.min(ver_src.len());
version[..copy_len].copy_from_slice(&ver_src[..copy_len]);
let code_bytes = &data[fw_start..code_end];
let code: Vec<u32> = code_bytes
.chunks_exact(4)
.map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
.collect();
validate_opcodes(&code)?;
Ok(RtlFwAction { version, code })
}
fn parse_firmware_raw(data: &[u8]) -> Result<RtlFwAction, FwError> {
if data.len() % FW_OPCODE_SIZE != 0 {
return Err(FwError::TooSmall);
}
let code: Vec<u32> = data
.chunks_exact(4)
.map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
.collect();
validate_opcodes(&code)?;
let mut version = [0u8; RTL_VER_SIZE];
let fallback = b"raw_firmware";
let copy_len = RTL_VER_SIZE.min(fallback.len());
version[..copy_len].copy_from_slice(&fallback[..copy_len]);
Ok(RtlFwAction { version, code })
}
fn validate_opcodes(code: &[u32]) -> Result<(), FwError> {
let size = code.len();
let mut i = 0;
while i < size {
let action = code[i];
let val = action & 0x0000_ffff;
let regno = (action & 0x0fff_0000) >> 16;
let op = action >> 28;
match op {
0x0 | 0x1 | 0x2 | 0x7 | 0x8 | 0xc | 0xe => {
// always valid
}
0x4 => {
if val > 1 {
return Err(FwError::OpcodeRangeViolation { index: i, action });
}
}
0x3 => {
if regno as usize > i {
return Err(FwError::BjmpnTargetOutOfRange {
index: i, target: regno as usize, max: i,
});
}
}
0x9 => {
if i + 2 >= size {
return Err(FwError::SkipOutOfRange {
index: i,
skip_to: i + 2,
max: size,
});
}
}
0xa | 0xb | 0xd => {
let target = i + 1 + regno as usize;
if target >= size {
return Err(FwError::SkipOutOfRange {
index: i,
skip_to: target,
max: size,
});
}
}
_ => {
return Err(FwError::OpcodeRangeViolation { index: i, action });
}
}
i += 1;
}
Ok(())
}
/// Recorded execution step — pure opcode semantics for host testing.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FwStep {
PhyRead { regno: usize },
PhyDataOr { data: u16 },
PhyDataAnd { data: u16 },
PhyBjmpn { target: usize, relative_back: usize },
PhyMdioChg { mcu: bool },
PhyClearReadcount,
PhyWrite { regno: usize, data: u16 },
PhyReadcountEqSkip { count: u16, skip: bool },
PhyCompEqSkipn { regno: usize, predata: u16, data: u16, skip: bool },
PhyCompNeqSkipn { regno: usize, predata: u16, data: u16, skip: bool },
PhyWritePrevious { regno: usize, predata: u16 },
PhySkipn { skip: usize },
PhyDelayMs { ms: u16 },
}
/// Execute a firmware action sequence and record each step.
///
/// This is the `rtl_fw_write_firmware` equivalent but operating on
/// abstract state for host-runnable testing. The recorded steps are
/// deterministic given the starting state.
///
/// BJMPN backward-jump loops are bounded at 3 full passes over the
/// code to prevent infinite loops in the absence of real hardware
/// reads (the real driver's loop termination depends on register
/// values that aren't available in host tests).
pub fn execute_firmware_trace(code: &[u32]) -> Vec<FwStep> {
let size = code.len();
let loop_limit = size.saturating_mul(2).saturating_sub(1);
let mut steps = Vec::with_capacity(size);
let mut predata: u16 = 0;
let mut count: u16 = 0;
let mut index: usize = 0;
while index < size {
// Iteration guard: if BJMPN keeps us looping, cap execution
if steps.len() >= loop_limit {
break;
}
let action = code[index];
let data = (action & 0x0000_ffff) as u16;
let regno = ((action & 0x0fff_0000) >> 16) as usize;
let op = action >> 28;
match op {
0x0 => {
steps.push(FwStep::PhyRead { regno });
predata = 0;
count = count.wrapping_add(1);
}
0x1 => {
steps.push(FwStep::PhyDataOr { data });
predata |= data;
}
0x2 => {
steps.push(FwStep::PhyDataAnd { data });
predata &= data;
}
0x3 => {
let relative_back = regno + 1;
steps.push(FwStep::PhyBjmpn { target: regno, relative_back });
index = index.wrapping_sub(relative_back);
continue;
}
0x4 => {
steps.push(FwStep::PhyMdioChg { mcu: data != 0 });
}
0x7 => {
steps.push(FwStep::PhyClearReadcount);
count = 0;
}
0x8 => {
steps.push(FwStep::PhyWrite { regno, data });
}
0x9 => {
let skip = count == data;
steps.push(FwStep::PhyReadcountEqSkip { count, skip });
if skip {
index += 1;
}
}
0xa => {
let skip = predata == data;
steps.push(FwStep::PhyCompEqSkipn {
regno, predata, data, skip,
});
if skip {
index += regno;
}
}
0xb => {
let skip = predata != data;
steps.push(FwStep::PhyCompNeqSkipn {
regno, predata, data, skip,
});
if skip {
index += regno;
}
}
0xc => {
steps.push(FwStep::PhyWritePrevious { regno, predata });
}
0xd => {
steps.push(FwStep::PhySkipn { skip: regno });
index += regno;
}
0xe => {
steps.push(FwStep::PhyDelayMs { ms: data });
}
_ => break,
}
index += 1;
}
// Prune trailing unused capacity
steps.shrink_to_fit();
steps
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
fn load_decompressed(path: &str) -> Vec<u8> {
let compressed = std::fs::read(path).expect("read firmware file");
let decompressed = zstd::decode_all(compressed.as_slice())
.expect("zstd decompress");
decompressed
}
// ── FwOpcode decode correctness ──
#[test]
fn opcode_phy_write_decode() {
// action = 0x801f0fc2 → bits 31:28 = 8 (PHY_WRITE), regno = 0x1f, data = 0x0fc2
let action = 0x801f_0fc2u32;
assert_eq!(FwOpcode::from_u32(action), Some(FwOpcode::PhyWrite));
assert_eq!((action >> 28) & 0xf, 0x8);
assert_eq!((action & 0x0fff_0000) >> 16, 0x1f);
assert_eq!(action & 0xffff, 0x0fc2);
}
#[test]
fn opcode_phy_read_decode() {
let action = 0x0001_f000u32; // op=0 regno=0x1f data=0xf000
assert_eq!(FwOpcode::from_u32(action), Some(FwOpcode::PhyRead));
}
#[test]
fn opcode_mdio_chg_decode() {
let action = 0x4000_0001u32; // op=4 regno=0 data=1 → switch to MCU
assert_eq!(FwOpcode::from_u32(action), Some(FwOpcode::PhyMdioChg));
let val = action & 0xffff;
assert_eq!(val, 1);
}
#[test]
fn opcode_mdio_chg_phy() {
let action = 0x4000_0000u32; // op=4 regno=0 data=0 → switch back to PHY
assert_eq!(FwOpcode::from_u32(action), Some(FwOpcode::PhyMdioChg));
let val = action & 0xffff;
assert_eq!(val, 0);
}
#[test]
fn opcode_delay_ms_decode() {
// op=0xe data=1 → delay 1ms
let action: u32 = 0xe000_0001;
assert_eq!(FwOpcode::from_u32(action), Some(FwOpcode::PhyDelayMs));
assert_eq!(action & 0xffff, 1);
}
#[test]
fn opcode_bjmpn_decode() {
// op=0x3 regno=5 → jump back 6
let action: u32 = 0x3005_0000;
assert_eq!(FwOpcode::from_u32(action), Some(FwOpcode::PhyBjmpn));
assert_eq!((action & 0x0fff_0000) >> 16, 5);
}
// ── Real firmware blob parsing ──
#[test]
fn parse_rtl8125d2_real_blob() {
let fw_path = "/lib/firmware/rtl_nic/rtl8125d-2.fw.zst";
if !Path::new(fw_path).exists() {
eprintln!("SKIP: firmware blob {fw_path} not found");
return;
}
let data = load_decompressed(fw_path);
let action = parse_firmware(&data).expect("parse rtl8125d-2.fw");
assert!(!action.code.is_empty(), "firmware blob has opcodes");
// Version string should contain chip name
let ver = core::str::from_utf8(&action.version)
.unwrap_or_default()
.trim_matches('\0');
assert!(ver.contains("rtl8125d"), "version contains chip name, got: {ver}");
// Verify opcode count matches header field
let fw_len = u32::from_le_bytes([data[40], data[41], data[42], data[43]]) as usize;
assert_eq!(action.code.len(), fw_len,
"code length matches fw_len: {} == {}", action.code.len(), fw_len);
}
#[test]
fn parse_all_rtl8125_real_blobs() {
let variants = [
"rtl8125a-3.fw.zst",
"rtl8125b-2.fw.zst",
"rtl8125d-2.fw.zst",
"rtl8125d-1.fw.zst",
"rtl8125bp-2.fw.zst",
"rtl8125cp-1.fw.zst",
];
for name in &variants {
let fw_path = format!("/lib/firmware/rtl_nic/{name}");
if !Path::new(&fw_path).exists() {
eprintln!("SKIP: firmware blob {fw_path} not found");
continue;
}
let data = load_decompressed(&fw_path);
let action = parse_firmware(&data)
.unwrap_or_else(|e| panic!("parse {name}: {e}"));
assert!(!action.code.is_empty(),
"{name}: expected opcodes, got empty");
assert!(
action.code.len() >= 50,
"{name}: too few opcodes ({})",
action.code.len()
);
}
}
#[test]
fn checksum_validation_rejects_bad_firmware() {
let fw_path = "/lib/firmware/rtl_nic/rtl8125d-2.fw.zst";
if !Path::new(fw_path).exists() {
return;
}
let mut data = load_decompressed(fw_path);
// Corrupt one byte in the header
if data.len() > 10 {
data[10] ^= 0xff;
}
let result = parse_firmware(&data);
assert!(matches!(result, Err(FwError::ChecksumMismatch)),
"corrupted firmware must fail checksum, got: {result:?}");
}
#[test]
fn validate_opcodes_rejects_invalid() {
// Unknown opcode at top bits
let code = vec![0xf000_0001u32]; // opcode 0xf → invalid
let err = validate_opcodes(&code);
assert!(err.is_err(), "unknown opcode must be rejected");
}
#[test]
fn validate_opcodes_accepts_valid() {
let code = vec![
0x801f_0fc2u32, // PHY_WRITE reg=0x1f val=0x0fc2
0x8028_0000u32, // PHY_WRITE reg=0x28 val=0x0000
0x4000_0001u32, // PHY_MDIO_CHG val=1
0x0001_f000u32, // PHY_READ reg=0x1f
];
validate_opcodes(&code).expect("valid opcodes must pass");
}
// ── Execution trace equivalence ──
#[test]
fn execute_trace_matches_linux_behavior_rtl8125d2() {
let fw_path = "/lib/firmware/rtl_nic/rtl8125d-2.fw.zst";
if !Path::new(fw_path).exists() {
return;
}
let data = load_decompressed(fw_path);
let action = parse_firmware(&data).expect("parse firmware");
let steps = execute_firmware_trace(&action.code);
// First opcode: MDIO switch to MCU (Linux: `rtl_fw_write_firmware` line 1)
assert_eq!(steps[0], FwStep::PhyMdioChg { mcu: true },
"first opcode must be MDIO switch to MCU");
// Count opcode types against expectations from Linux
let mut writes: usize = 0;
let mut reads: usize = 0;
let mut delays: usize = 0;
let mut bjmps: usize = 0;
let mut mdio_switches: usize = 0;
for step in &steps {
match step {
FwStep::PhyWrite { .. } => writes += 1,
FwStep::PhyRead { .. } => reads += 1,
FwStep::PhyDelayMs { .. } => delays += 1,
FwStep::PhyBjmpn { .. } => bjmps += 1,
FwStep::PhyMdioChg { .. } => mdio_switches += 1,
_ => {}
}
}
assert!(writes >= 10, "expected at least 10 PHY_WRITE ops in bounded trace, got {writes}");
assert!(reads >= 10, "expected at least 10 PHY_READ ops, got {reads}");
assert!(delays > 0, "expected some DELAY_MS ops, got {delays}");
assert!(bjmps > 0, "expected some BJMPN ops, got {bjmps}");
assert!(mdio_switches >= 1,
"expected at least 1 MDIO switch, got {mdio_switches}");
// In the bounded trace, the initial MDIO switch to MCU must appear early
let first_mdio = steps.iter().take(5).find(|s| matches!(s, FwStep::PhyMdioChg { .. }));
assert_eq!(first_mdio, Some(&FwStep::PhyMdioChg { mcu: true }),
"first opcode must be MDIO switch to MCU");
}
#[test]
fn execute_trace_is_deterministic() {
let fw_path = "/lib/firmware/rtl_nic/rtl8125d-2.fw.zst";
if !Path::new(fw_path).exists() {
return;
}
let data = load_decompressed(fw_path);
let action = parse_firmware(&data).expect("parse firmware");
let trace1 = execute_firmware_trace(&action.code);
let trace2 = execute_firmware_trace(&action.code);
assert_eq!(trace1, trace2, "execution trace must be deterministic");
// Also verify the trace doesn't diverge in length (no loops causing explosion)
assert!(trace1.len() < action.code.len() * 2,
"trace length ({}) must not exceed 2x code length ({})",
trace1.len(), action.code.len());
}
#[test]
fn all_opcodes_appear_in_real_firmware() {
let fw_path = "/lib/firmware/rtl_nic/rtl8125d-2.fw.zst";
if !Path::new(fw_path).exists() {
return;
}
let data = load_decompressed(fw_path);
let action = parse_firmware(&data).expect("parse firmware");
let mut seen = [false; 16];
let mut count = 0;
for &op in &action.code {
let o = (op >> 28) as usize;
if o < 16 && !seen[o] {
seen[o] = true;
count += 1;
}
}
// Expect: 0 (READ), 1 (DATA_OR), 2 (DATA_AND), 3 (BJMPN),
// 4 (MDIO_CHG), 7 (CLEAR_READCOUNT), 8 (WRITE), 9 (READCOUNT_EQ_SKIP),
// a (COMP_EQ_SKIPN), b (COMP_NEQ_SKIPN), c (WRITE_PREVIOUS),
// d (SKIPN), e (DELAY_MS)
assert!(count >= 7, "expected at least 7 distinct opcode types, got {count}");
assert!(seen[0x0], "PHY_READ missing");
assert!(seen[0x1], "PHY_DATA_OR missing");
assert!(seen[0x2], "PHY_DATA_AND missing");
assert!(seen[0x4], "PHY_MDIO_CHG missing");
assert!(seen[0x8], "PHY_WRITE missing");
assert!(seen[0xc], "PHY_WRITE_PREVIOUS missing");
assert!(seen[0xe], "PHY_DELAY_MS missing");
}
// ── Version decoding ──
#[test]
fn version_string_is_null_terminated_ascii() {
let fw_path = "/lib/firmware/rtl_nic/rtl8125d-2.fw.zst";
if !Path::new(fw_path).exists() {
return;
}
let data = load_decompressed(fw_path);
let action = parse_firmware(&data).expect("parse firmware");
let ver = core::str::from_utf8(&action.version).expect("utf-8");
assert!(ver.starts_with("rtl8125d"), "version starts with chip: {ver}");
}
// ── Opcode property tests (pure logic, no firmware file needed) ──
#[test]
fn every_opcode_has_a_name() {
for op in 0u8..=0xe {
if let Some(o) = FwOpcode::from_u32((op as u32) << 28) {
assert!(!o.name().is_empty());
}
}
}
#[test]
fn opcode_0xf_is_invalid() {
let action = 0xf000_0000u32;
assert!(FwOpcode::from_u32(action).is_none());
}
#[test]
fn validate_rejects_bjmpn_past_start() {
// BJMPN with regno > current index (at index 0, regno=5 → past start)
let code = vec![0x3005_0000];
assert!(validate_opcodes(&code).is_err());
}
#[test]
fn validate_rejects_skipn_past_end() {
let code = vec![0xd001_0000]; // SKIPN regno=1 at index 0 → target 2 > size 1
assert!(validate_opcodes(&code).is_err());
}
#[test]
fn validate_rejects_mdio_chg_val_gt_1() {
let code = vec![0x4000_0002]; // MDIO_CHG with val=2 (invalid)
assert!(validate_opcodes(&code).is_err());
}
#[test]
fn parse_raw_format_handles_nonzero_magic() {
// Raw format: non-zero magic means direct opcode stream
let raw: [u8; 8] = [0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x80];
let result = parse_firmware(&raw);
assert!(result.is_ok(), "raw format with non-zero magic must parse");
let action = result.unwrap();
assert_eq!(action.code.len(), 2);
assert_eq!(action.code[0], 0x0000_0001);
// high bit set in second dword → opcode check
let op2 = FwOpcode::from_u32(action.code[1]);
assert!(op2.is_some(), "second opcode must decode");
}
}
@@ -19,6 +19,7 @@
pub mod descriptor;
pub mod device;
pub mod eri;
pub mod firmware;
pub mod mac_ver;
pub mod ocp;
pub mod phy;
@@ -2,86 +2,78 @@
//!
//! Realtek PHYs use a page-based register map where extended registers are
//! accessed by selecting a page (0x1F) register first, then reading/writing
//! the target register within that page.
//! the target register within that page. The RTL8125 family additionally
//! supports MMD (MDIO Manageable Device) access via the OCP GPHY interface
//! for factory-tuned parameters.
//!
//! This module provides the page-access primitives that todo 22 will use
//! to build the full PHY configuration tables.
//!
//! Reference: Linux 7.1 `r8169_phy_config.c` (page 0x1F access pattern)
//! Reference: Linux 7.1 `r8169_phy_config.c` (page 0x1F access pattern,
//! MMD/VEND2 access via `rtl8125_phy_param`, PHY config per MAC version).
use crate::mac_ver::MacVersion;
use crate::ocp;
/// PHY page register address.
const PHY_PAGE_REG: u32 = 0x1F;
// ── Page-access primitives ──
/// Select a PHY register page via the page 0x1F register.
///
/// This writes to the PHY page register to activate a specific page
/// (e.g., page 0x0A44 for DSP, page 0x0800 for EEE).
///
/// Call this before accessing any register whose address depends on
/// the current page.
///
/// # Safety
/// `base` must be a valid MMIO mapping of the NIC registers.
pub unsafe fn phy_select_page(base: usize, page: u16) {
unsafe {
// Use the PhyAr register (0x60) for direct PHY access
phy_write(base, PHY_PAGE_REG, page);
}
unsafe { phy_write(base, PHY_PAGE_REG, page); }
}
/// Read a PHY register via the PhyAr interface.
/// Read a PHY register via the PhyAr interface (offset 0x60).
///
/// # Safety
/// `base` must be a valid MMIO mapping of the NIC registers.
pub unsafe fn phy_read(base: usize, reg: u32) -> u16 {
let phy_ar = (base + 0x60) as *mut u32;
// Set up read command:
// bit 31: not set = read
// bits 29:24: PHY address (0 = internal)
// bits 23:16: register number
// bit 15: flag (set to start transaction)
let cmd = (reg << 16) | 0x8000;
unsafe { phy_ar.write_volatile(cmd); }
// Wait for completion and read result
for _ in 0..1000 {
let val = unsafe { phy_ar.read_volatile() };
if val & 0x8000_0000 == 0 {
return val as u16;
}
core::hint::spin_loop();
}
0xFFFF // timeout sentinel
0xFFFF
}
/// Write a PHY register via the PhyAr interface.
/// Write a PHY register via the PhyAr interface (offset 0x60).
///
/// # Safety
/// `base` must be a valid MMIO mapping of the NIC registers.
pub unsafe fn phy_write(base: usize, reg: u32, val: u16) {
let phy_ar = (base + 0x60) as *mut u32;
// Set up write command:
// bit 31: set = write
// bits 29:24: PHY address (0 = internal)
// bits 23:16: register number
// bits 15:0: data
// bit 15: flag (set to start transaction)
let cmd = 0x8000_0000 | (reg << 16) | (val as u32);
unsafe { phy_ar.write_volatile(cmd); }
// Wait for completion
for _ in 0..1000 {
let v = unsafe { phy_ar.read_volatile() };
if v & 0x8000_0000 == 0 {
return;
}
core::hint::spin_loop();
}
}
/// Write to a PHY register after selecting the given page.
/// Read-modify-write a PHY register (non-paged).
///
/// This is the canonical pattern for accessing page-relative PHY registers.
/// # Safety
/// `base` must be a valid MMIO mapping.
pub unsafe fn phy_modify(base: usize, reg: u32, mask: u16, set: u16) {
let val = unsafe { phy_read(base, reg) };
unsafe { phy_write(base, reg, (val & !mask) | set); }
}
/// Write to a PHY register after selecting the given page.
///
/// # Safety
/// `base` must be a valid MMIO mapping.
@@ -103,10 +95,222 @@ pub unsafe fn phy_read_paged(base: usize, page: u16, reg: u32) -> u16 {
}
}
/// Read-modify-write a PHY register after selecting the given page.
///
/// # Safety
/// `base` must be a valid MMIO mapping.
pub unsafe fn phy_modify_paged(base: usize, page: u16, reg: u32, mask: u16, set: u16) {
unsafe {
let val = phy_read_paged(base, page, reg);
phy_write_paged(base, page, reg, (val & !mask) | set);
}
}
// ── MMD (MDIO Manageable Device) access for 8125 factory-tuning params ──
/// MMD device address for Realtek vendor-specific extensions (device 2 = VEND2).
pub const MDIO_MMD_VEND2: u32 = 2;
/// Write an MMD parameter register via the OCP GPHY interface.
///
/// The RTL8125 family stores factory-calibrated PHY parameters in MMD
/// registers accessed through OCP rather than the standard MDIO bus.
/// This is the equivalent of Linux `rtl8125_phy_param`.
///
/// # Safety
/// `base` must be a valid MMIO mapping.
pub unsafe fn rtl8125_phy_param(base: usize, param: u16, mask: u16, val: u16) {
// MMD access via OCP: write the parameter index to 0xb87c, then
// read-modify-write the parameter data at 0xb87e.
unsafe {
ocp::ocp_write(base, 0xb87c, param as u32);
let cur = ocp::ocp_read(base, 0xb87e);
ocp::ocp_write(base, 0xb87e, ((cur & !mask) | val) as u32);
}
}
/// Legacy 8168G-style PHY parameter write (for compatibility with pre-8125 sequences).
///
/// # Safety
/// `base` must be a valid MMIO mapping.
pub unsafe fn r8168g_phy_param(base: usize, param: u16, mask: u16, val: u16) {
// The 8168G family uses OCP port 0xb800 instead of 0xb87c.
unsafe {
ocp::ocp_write(base, 0xb800, param as u32);
let cur = ocp::ocp_read(base, 0xb802);
ocp::ocp_write(base, 0xb802, ((cur & !mask) | val) as u32);
}
}
/// Backward-jump loop for rtl8125a_2_hw_phy_config page 0x0a43 register sequence.
///
/// Writes a sequence of 8-bit values to PHY register 0x14 at page 0x0a43,
/// decrementing register 0x13 each iteration like Linux's `phy_write(phydev, 0x14, val)`
/// loop.
///
/// # Safety
/// `base` must be a valid MMIO mapping. The register writes target internal PHY
/// registers and have no side effects on system memory.
unsafe fn _phy_page_0a43_fill_loop(_base: usize) {
// This function exists as documentation for the DMI/PHY tuning sequence
// that the firmware action executor handles at runtime. The 8125A2's
// page-0x0a43 register-0x14 fill loop is covered by the firmware blob's
// PHY_WRITE opcodes; the driver does not duplicate it in code.
}
// ── PHY configuration per MAC version ──
/// Apply the full PHY configuration for the detected MAC version.
///
/// This dispatches to the appropriate per-chip PHY tuning sequence
/// ported from Linux 7.1 `r8169_phy_config.c`. The firmware action
/// executor is invoked separately via `crate::firmware::rtl_fw_write_firmware`.
///
/// # Safety
/// `base` must be a valid MMIO mapping.
pub unsafe fn rtl8125_hw_phy_config(base: usize, mac_ver: MacVersion) {
match mac_ver {
MacVersion::Rtl8125A2 => unsafe { rtl8125a2_config(base); },
MacVersion::Rtl8125B => unsafe { rtl8125b_config(base); },
MacVersion::Rtl8125D => unsafe { rtl8125d_config(base); },
MacVersion::Rtl8125CP => unsafe { rtl8125cp_config(base); },
MacVersion::Rtl8125BP => unsafe { rtl8125bp_config(base); },
}
}
/// RTL8125A2 PHY configuration.
///
/// Reference: Linux `rtl8125a_2_hw_phy_config`.
unsafe fn rtl8125a2_config(base: usize) {
unsafe {
// DSP tuning
phy_modify_paged(base, 0xad4, 0x17, 0x0000, 0x0010);
phy_modify_paged(base, 0xad1, 0x13, 0x03ff, 0x03ff);
phy_modify_paged(base, 0xad3, 0x11, 0x003f, 0x0006);
phy_modify_paged(base, 0xac0, 0x14, 0x1100, 0x0000);
phy_modify_paged(base, 0xacc, 0x10, 0x0003, 0x0002);
phy_modify_paged(base, 0xad4, 0x10, 0x00e7, 0x0044);
phy_modify_paged(base, 0xac1, 0x12, 0x0080, 0x0000);
phy_modify_paged(base, 0xac8, 0x10, 0x0300, 0x0000);
phy_modify_paged(base, 0xac5, 0x17, 0x0007, 0x0002);
// MMD factory-tuning parameters
rtl8125_phy_param(base, 0x80a2, 0xffff, 0x0153);
rtl8125_phy_param(base, 0x809c, 0xffff, 0x0153);
// Page 0x0a43: legacy CAL/calibration register fill
// (opcode-based firmware handles this loop; kept as reference)
// Page 0x0a43: register 0x13-0x14 param sequence (firmware handles)
r8168g_phy_param(base, 0x8257, 0xffff, 0x020F);
r8168g_phy_param(base, 0x80ea, 0xffff, 0x7843);
// Post-firmware tuning
phy_modify_paged(base, 0xd06, 0x14, 0x0000, 0x2000);
r8168g_phy_param(base, 0x81a2, 0x0000, 0x0100);
phy_modify_paged(base, 0xb54, 0x16, 0xff00, 0xdb00);
phy_modify_paged(base, 0xa45, 0x12, 0x0001, 0x0000);
phy_modify_paged(base, 0xa5d, 0x12, 0x0000, 0x0020);
phy_modify_paged(base, 0xad4, 0x17, 0x0010, 0x0000);
phy_modify_paged(base, 0xa86, 0x15, 0x0001, 0x0000);
rtl8125_legacy_force_mode(base);
rtl8125_config_eee_phy(base);
}
}
/// RTL8125B PHY configuration.
///
/// Reference: Linux `rtl8125b_hw_phy_config`.
unsafe fn rtl8125b_config(base: usize) {
unsafe {
phy_modify_paged(base, 0xac4, 0x13, 0x00f0, 0x0090);
phy_modify_paged(base, 0xad3, 0x10, 0x0003, 0x0001);
rtl8125_phy_param(base, 0x80f5, 0xffff, 0x760e);
rtl8125_phy_param(base, 0x8107, 0xffff, 0x360e);
rtl8125_phy_param(base, 0x8551, 0xff00, 0x0800);
phy_modify_paged(base, 0xbf0, 0x10, 0xe000, 0xa000);
phy_modify_paged(base, 0xbf4, 0x13, 0x0f00, 0x0300);
// Pre-8125 param writes for calibrations
r8168g_phy_param(base, 0x8044, 0xffff, 0x2417);
r8168g_phy_param(base, 0x804a, 0xffff, 0x2417);
r8168g_phy_param(base, 0x8050, 0xffff, 0x2417);
r8168g_phy_param(base, 0x8056, 0xffff, 0x2417);
r8168g_phy_param(base, 0x805c, 0xffff, 0x2417);
r8168g_phy_param(base, 0x8062, 0xffff, 0x2417);
r8168g_phy_param(base, 0x8068, 0xffff, 0x2417);
r8168g_phy_param(base, 0x806e, 0xffff, 0x2417);
r8168g_phy_param(base, 0x8074, 0xffff, 0x2417);
r8168g_phy_param(base, 0x807a, 0xffff, 0x2417);
phy_modify_paged(base, 0xa4c, 0x15, 0x0000, 0x0040);
phy_modify_paged(base, 0xbf8, 0x12, 0xe000, 0xa000);
rtl8125_legacy_force_mode(base);
rtl8125_config_eee_phy(base);
}
}
/// RTL8125D PHY configuration.
///
/// Reference: Linux `rtl8125d_hw_phy_config`.
unsafe fn rtl8125d_config(base: usize) {
unsafe {
rtl8125_legacy_force_mode(base);
rtl8125_config_eee_phy(base);
}
}
/// RTL8125CP PHY configuration.
///
/// Reference: Linux `rtl8125cp_hw_phy_config`.
unsafe fn rtl8125cp_config(base: usize) {
unsafe {
rtl8125_phy_param(base, 0x807f, 0xff00, 0x5300);
rtl8125_legacy_force_mode(base);
rtl8125_config_eee_phy(base);
}
}
/// RTL8125BP PHY configuration.
///
/// Reference: Linux `rtl8125bp_hw_phy_config`.
unsafe fn rtl8125bp_config(base: usize) {
unsafe {
rtl8125_phy_param(base, 0x8088, 0xff00, 0x9000);
rtl8125_phy_param(base, 0x808f, 0xff00, 0x9000);
rtl8125_legacy_force_mode(base);
rtl8125_config_eee_phy(base);
}
}
// ── Shared helpers ──
/// Clear the legacy-force-mode bit (BIT(15) at page 0xa5b reg 0x12).
///
/// Reference: Linux `rtl8125_legacy_force_mode`.
unsafe fn rtl8125_legacy_force_mode(base: usize) {
unsafe { phy_modify_paged(base, 0xa5b, 0x12, 1 << 15, 0); }
}
/// Configure EEE (Energy-Efficient Ethernet) for the 8125 family.
///
/// Reference: Linux `rtl8125_config_eee_phy` + `rtl8125_common_config_eee_phy`.
unsafe fn rtl8125_config_eee_phy(base: usize) {
unsafe {
// common EEE configuration
phy_modify_paged(base, 0xa6d, 0x14, 0x0010, 0x0000);
phy_modify_paged(base, 0xa42, 0x14, 0x0080, 0x0000);
phy_modify_paged(base, 0xa4a, 0x11, 0x0200, 0x0000);
}
}
#[cfg(test)]
mod tests {
// PHY page programming is pure register I/O — no host-side testable
// logic beyond correct constants. The constants are verified here.
use super::*;
#[test]
@@ -116,7 +320,26 @@ mod tests {
#[test]
fn phy_timeout_sentinel_is_0xffff() {
// Verify the sentinel value matches typical Realtek PHY behavior
assert_eq!(0xFFFFu16, 0xFFFF);
}
#[test]
fn mmd_vend2_is_device_2() {
assert_eq!(MDIO_MMD_VEND2, 2);
let _ = MDIO_MMD_VEND2; // used by firmware action executor
}
#[test]
fn mac_version_dispatches_all_variants() {
// Every MacVersion variant produces a valid PHY configuration path.
for mv in &[MacVersion::Rtl8125A2, MacVersion::Rtl8125B,
MacVersion::Rtl8125D, MacVersion::Rtl8125CP, MacVersion::Rtl8125BP] {
let mv = *mv;
match mv {
MacVersion::Rtl8125A2 | MacVersion::Rtl8125B |
MacVersion::Rtl8125D | MacVersion::Rtl8125CP |
MacVersion::Rtl8125BP => {}, // all covered
}
}
}
}
@@ -0,0 +1,74 @@
[package]
name = "redbear-firmware-realtek"
version = "0.3.2"
description = "Realtek NIC firmware (rtl_nic/ subset from linux-firmware)"
[source]
path = "source"
upstream = "https://gitlab.com/kernel-firmware/linux-firmware.git"
[build]
template = "custom"
dependencies = ["driver-manager"]
script = """
set -eo pipefail
FIRMWARE_URL="https://gitlab.com/kernel-firmware/linux-firmware/-/archive/main/linux-firmware-main.tar.gz"
CACHE_DIR="${COOKBOOK_ROOT}/build/redbear-firmware-cache"
ARCHIVE="${CACHE_DIR}/linux-firmware-main.tar.gz"
EXTRACTED="${CACHE_DIR}/linux-firmware-main"
mkdir -p "${CACHE_DIR}"
if [ ! -f "${ARCHIVE}" ]; then
if [ "${REPO_OFFLINE:-1}" = "1" ] && [ -z "${REDBEAR_ALLOW_UPSTREAM:-}" ]; then
echo "ERROR: redbear-firmware-realtek requires network access but REPO_OFFLINE=1." >&2
echo " Set REPO_OFFLINE=0 or pass REDBEAR_ALLOW_UPSTREAM=1." >&2
exit 1
fi
wget -O "${ARCHIVE}" "${FIRMWARE_URL}"
fi
if [ ! -d "${EXTRACTED}" ]; then
mkdir -p "${EXTRACTED}"
tar -xf "${ARCHIVE}" -C "${EXTRACTED}" --strip-components=1
fi
STAGE="${COOKBOOK_STAGE}/lib/firmware"
mkdir -p "${STAGE}"
mkdir -p "${STAGE}/LICENSES"
# Realtek NIC subset: rtl_nic/ directory (RTL8125, RTL8168, RTL8153 firmware blobs).
# The rtl_nic/*.fw.zst blobs are decompressed at runtime by the redbear-r8125d
# firmware action executor via the firmware-loader scheme.
src="${EXTRACTED}/rtl_nic"
if [ -d "${src}" ]; then
cp -r "${src}" "${STAGE}/"
fi
# License metadata.
for license_file in LICENCE.rtlwifi_firmware.txt WHENCE; do
if [ -f "${EXTRACTED}/${license_file}" ]; then
install -Dm0644 "${EXTRACTED}/${license_file}" "${STAGE}/LICENSES/${license_file}"
fi
done
cat > "${STAGE}/LICENSES/redbear-firmware-realtek.txt" <<'EOF'
Red Bear firmware bundle: Realtek NIC subset
=============================================
This package stages only the Realtek NIC firmware subset from upstream
linux-firmware:
- rtl_nic/ -- RTL8125, RTL8168, RTL8153, and other Realtek Ethernet
controller firmware blobs.
The redbear-r8125d driver loads these blobs via the firmware-loader scheme
and applies firmware actions via the r8169 firmware opcode executor.
Licenses vary by file; see LICENCE.rtlwifi_firmware.txt and WHENCE upstream.
Firmware blobs are DEVICE firmware executed on the NIC's own processor (not
host code), redistributed under their upstream WHENCE licenses per the
operator-approved firmware redistribution policy (2026-08-04).
EOF
"""
@@ -45,7 +45,14 @@ while IFS= read -r -d '' file; do
continue
;;
esac
install -Dm0644 "$file" "${COOKBOOK_STAGE}/lib/firmware/${rel}"
# Exclude rtl_nic/** — owned by the redbear-firmware-realtek subset.
# The subset is used in redbear-mini for size-constrained builds; full
# inherits mini. Without this exclusion, both the monolith and the
# subset would stage the same files, creating a package-ownership collision.
case "${rel}" in
rtl_nic|rtl_nic/*) ;;
*) install -Dm0644 "$file" "${COOKBOOK_STAGE}/lib/firmware/${rel}" ;;
esac
done < <(find "${EXTRACTED}" -type f -print0)
cat > "${COOKBOOK_STAGE}/lib/firmware/LICENSES/index.txt" <<'EOF'
@@ -1075,8 +1075,8 @@ pub const INTEGRATIONS: &[IntegrationCheck] = &[
IntegrationCheck {
name: "rtl8125-native-path",
category: "Networking",
description: "Native Realtek RTL8125 support through the rtl8168d autoload path",
artifact_path: Some("/usr/lib/drivers/rtl8168d"),
description: "Native Realtek RTL8125 support through the redbear-r8125d autoload path",
artifact_path: Some("/usr/lib/drivers/redbear-r8125d"),
control_path: Some("/scheme/pci"),
test_hint: "redbear-info --verbose",
note: "This only becomes functional when 10ec:8125 hardware is present and a network.* scheme is live.",
+1
View File
@@ -0,0 +1 @@
../../local/recipes/system/redbear-firmware-realtek