diff --git a/include/rcp/mdio.hpp b/include/rcp/mdio.hpp index 132f6bf..8eabba9 100644 --- a/include/rcp/mdio.hpp +++ b/include/rcp/mdio.hpp @@ -5,114 +5,156 @@ // fusa:req REQ-MDIO-005 // fusa:req REQ-MDIO-006 // fusa:req REQ-MDIO-007 +// fusa:req REQ-MDIO-008 +// fusa:req REQ-MDIO-009 +// fusa:req REQ-MDIO-010 +// fusa:req REQ-MDIO-011 +// fusa:req REQ-MDIO-012 +// fusa:req REQ-MDIO-013 +// fusa:req REQ-MDIO-014 +// fusa:req REQ-MDIO-015 +// fusa:req REQ-MDIO-016 +// fusa:req REQ-MDIO-017 +// fusa:req REQ-MDIO-018 +// fusa:req REQ-MDIO-019 +// fusa:req REQ-MDIO-020 +// fusa:req REQ-MDIO-021 +// fusa:req REQ-MDIO-022 +// fusa:req REQ-MDIO-023 +// fusa:req REQ-MDIO-024 +// fusa:req REQ-MDIO-025 +// fusa:req REQ-MDIO-026 +// fusa:req REQ-MDIO-027 +// fusa:req REQ-MDIO-028 // MDIO endpoint (ep_type 0x0D) — the OPEN Alliance TC18 Remote Control -// Protocol Specification v0.5.1_RC's mdio_mode-selected register access -// (extraction §5.13). +// Protocol Specification v0.5.1_RC5's Clause-22 (MMD)/Clause-45 (MMS) +// register-access codec, its Table 59 functional-configuration register +// block (§13.7.13.2), and the Table 33 Row 2 evt[2:0] plain/reserved/ +// config-write classification every endpoint type in that row shares +// (§13.5). // -// ROADMAP.md milestone 51, "Remaining Endpoint Types — LIN, CAN (incl. CAN -// XL), ISELED, MDIO, Wakeup Control (v2.7.0)": this endpoint type is fully -// specified in the source document but is not named in that document's own -// informative "ten interfaces" scope-summary list (extraction §1.2) — this -// implementation treats that as an editorial omission in the summary list, -// not as evidence MDIO is actually out of scope, and builds it anyway. +// Phase 3 rewrite (cpp-RCP issue #129, ROADMAP.md "Phase 17"), ported from +// c-RCP's include/rcp/ep_mdio.h + src/ep_mdio.c — this project's RC5-spec- +// conformant reference for this module, itself the single largest endpoint +// source file in c-RCP (1038+1195 lines) and the product of a real fix +// history (c-RCP-AUDIT-06/issue #256 Group I's Table 59 register-block + +// evt[2:0]=111b reconfig work, and the REQ-MDIO-021/022/024 mdio_mode/MMS- +// addressing investigation below). // -// Addressing-model fix (issue #72, cpp-RCP-03): this header previously -// modeled MDIO addressing as an invented IEEE 802.3 Clause 22/Clause -// 45-style PHY-address scheme (a 5-bit PHY address, a Clause22 register / -// Clause45 device-address field, and an MdioClause selector) that has no -// basis in the TC18 spec's own MDIO section. Verified against the OPEN -// Alliance TC18 Remote Control Protocol Specification's "mdio request -// format" figure and Table 57 (§13.7.13.3): the real addressing model is a -// 2-bit mdio_mode selector, an mdio_address field ("as per IEEE & OA SPI -// spec" — i.e. opaque to this header, not decomposed into a PHY/device/ -// register split of this header's own invention), and an mdio_payload whose -// width mdio_mode (and, for one mode, which MMS device is addressed) -// determines. The Clause 22/Clause 45 abstraction (MdioClause, -// phy_address, device_or_reg, register_address, kMaxPhyAddress, -// kMaxDeviceOrRegField) is removed entirely and rebuilt around -// mdio_mode/mdio_address/mdio_payload below. +// ── MAJOR CONTENT-DRIFT FIX: this header's own prior "addressing-model fix" ── +// (issue #72, cpp-RCP-03) was itself wrong against c-RCP's actual design, and +// is reverted here. That prior pass reasoned that a 5-bit PHY address plus a +// Clause-22/Clause-45 register-address split had "no basis in the TC18 spec's +// own MDIO section" and replaced MdioRequest's addressing with an opaque +// mdio_mode/mdio_address/mdio_payload triple. In fact c-RCP's ep_mdio.h/.c — +// this project's own RC5-conformant source of truth — keeps the Clause-22/ +// Clause-45 split as REAL, load-bearing addressing (rcp_ep_mdio_addr_t: a +// 5-bit prtad, plus either nothing further (Clause-22, a 5-bit regad +// directly) or a 5-bit devad + 16-bit regad (Clause-45)) — independently +// public IEEE 802.3 Clause 22/45 knowledge, not spec prose, per ep_mdio.h's +// own file header — and layers a *separate*, additive mdio_mode leading wire +// octet on top of it (REQ-MDIO-021), plus an entirely distinct MMS (Memory +// Map Selector) addressing family (REQ-MDIO-022/024, rcp_ep_mdio_mms_addr_t) +// that the prior pass never modeled at all. This pass restores the real +// Clause-22/Clause-45 (MMD) addressing model below (MdioAddr) alongside the +// new MMS family (MdioMmsAddr) and the complete ACF-level wire codec c-RCP +// actually implements (read/write request/response encode/decode, for both +// families) — none of which existed anywhere in this header before. Given +// the size mismatch this drift left behind (309 lines here vs. c-RCP's 2233), +// this is the largest single content expansion of any Phase 3 batch so far. // -// mdio_mode's four values, per Table 57: -// 00b: MMD, single word access (16-bit payload) -// 01b: MMD, multiple byte access (16-bit payload) -// 10b: MMS, single word access (16-bit payload) -// 11b: MMS, multiple (double) word access (32-bit payload, but only when -// the addressed MMS device is MMS0 or MMS1 — any other MMS device -// number still uses a 16-bit payload even in this mode) -// Table 57 itself prints "01b" for both the first two rows (MMD single word -// and MMD multiple byte access), which cannot both be correct in a 2-bit -// field with four otherwise-unambiguous rows (10b and 11b are printed once -// each, for the two MMS rows) — this is treated as a transcription defect -// in the v0.5.1_RC table, not as evidence of a 3-value field. By -// elimination (00b is the only 2-bit value the table's other three rows -// leave unclaimed), MMD single word access is 00b and MMD multiple byte -// access is 01b, below. +// mdio_mode's four values, per Table 60 (REQ-MDIO-021): +// 00b: MMD, single word access (word_count == 1) +// 01b: MMD, multiple byte access (word_count > 1) +// 10b: MMS, single word access (word_count == 1) +// 11b: MMS, multiple (double) word access (word_count > 1) +// c-RCP's own REQ-MDIO-021 catalog entry documents that Table 60's own +// printed value list assigns `01b` to BOTH MMD rows and leaves `00b` +// unclaimed — a transcription defect, not evidence of a 3-value field — +// resolved the same way this header's own MdioMode enum already did (below): +// 00b is MMD-single, by elimination the only reading giving the field's own +// natural 00/01/10/11 sequence four distinct meanings. // -// mdio_payload's width, per Table 57 ("for MMD, data fields are 16 bits. -// for MMS0 & 1: data fields are 32 bits. For other MMS, data fields are 16 -// bits"): always 16 bits for MMD (either sub-mode) and for MMS single word -// access; 32 bits for MMS multiple (double) word access specifically when -// the addressed MMS device is MMS0 or MMS1, 16 bits otherwise. Which MMS -// device number mdio_address addresses is part of the external IEEE 802.3 / -// OPEN Alliance SPI addressing scheme Table 57 itself defers to (mdio -// address: "As per IEEE & OA SPI spec") and is not decoded by this header; -// MdioRequest::mms_is_0_or_1 below is the caller-supplied fact -// payload_width_bits needs for the MmsMultiWord case. +// ── REQ-MDIO-024: the MMS addressing ambiguity and how it was closed ──────── +// Neither Figure 43 nor Table 60 gives TC18's own `mdio_address` field a bit +// width or internal layout for MMS mode at all (TC18_spec_defects_report.md +// item 55, still open in the specification itself — this port does not +// resolve item 55, it works around it the same way c-RCP does). c-RCP closed +// this with a documented, user-approved, EXTERNALLY-SOURCED assumption (not +// invented from nothing): the OPEN Alliance 10BASE-T1x MAC-PHY Serial +// Interface Specification, V1.1 — a public OPEN Alliance document, NOT the +// confidential TC18 document — whose own control command header (§7.4.1 +// Table 4) shows the real protocol "MMS" terminology is almost certainly +// borrowed from: a 4-bit MMS selector (0-15, its own §9.1 Table 6) followed +// by a 16-bit ADDR field. c-RCP's rcp_ep_mdio_mms_addr_t ASSUMES TC18's own +// mdio_address packs the same two sub-fields in the same order, represented +// on THIS module's own wire as two whole octets (`mms`, then a big-endian +// `addr`) — ported below unchanged as MdioMmsAddr. Per REQ-MDIO-022, MMS0 +// and MMS1 use 32-bit data words and every other MMS uses 16-bit — that part +// IS TC18-literal (Table 60 states it directly), not an assumption. +// REQ-MDIO-024 is catalogued PARTIAL, not IMPLEMENTED, in c-RCP's own +// .fusa-reqs.json for exactly this reason: the code path fully exists and is +// fully tested against its own assumed layout, but a peer built against a +// different real TC18 committee resolution of item 55 (a different field +// order, width, or meaning) would not interoperate. This port carries that +// same PARTIAL characterization forward rather than presenting the MMS +// family as settled TC18 conformance. // -// Table 30/33 Row 2 evt[2:0] validation (post-v2.7.0, EIGHTH and last +// ── Table 33 Row 2 evt[2:0] validation (post-v2.7.0, EIGHTH and last // endpoint type in this row, after I2C, ADC, PWM_IN, LIN, CAN, UART, and // ISELED): MdioEndpoint::handle_request is this header's own wiring of // rcp::endpoint::evt_row2_kind_of — the shared 3-way evt[2:0] classifier for // Table 33's {ADC, PWM_IN, I2C, LIN, CAN, UART, ISELED, MDIO} row — into -// MDIO's request handling, following the exact shape rcp/i2c.hpp's -// I2cEndpoint::handle_request, rcp/adc.hpp's AdcEndpoint::handle_request, -// rcp/pwm.hpp's PwmInEndpoint::handle_request, rcp/lin.hpp's -// LinEndpoint::handle_request, rcp/can.hpp's CanEndpoint::handle_request, -// rcp/uart.hpp's UartEndpoint::handle_request, and rcp/iseled.hpp's -// IseledEndpoint::handle_request established. +// MDIO's request handling, following the exact shape every sibling +// *Endpoint::handle_request already established. This is retained +// unchanged from before this pass, source-compatible with rcp/mock.hpp's +// existing MdioEndpoint wiring (dispatch_mdio()) — see the "MdioEndpoint +// convenience wrapper" section below for exactly what is and is not +// reframed. // -// NAMING: this header's own single-transaction method — the read/write -// register access above, predating this milestone entirely — was previously -// itself named `handle_request(MdioRequest, MdioResponse&)`. That name is -// now reused for the NEW evt[2:0]-classifying entry point below, so the old -// method is renamed to `transact`, matching the name rcp/iseled.hpp's -// IseledEndpoint::transact already established for the identical "existing -// single-call transaction method, now wrapped by a classifying -// handle_request" role. Every call site (tests/test_mdio.cpp) is updated to -// call transact() directly where it exercises the register-access behavior -// itself, unrelated to evt[2:0] classification. -// -// This is a completely separate, TC18-Table-33-derived concern from this -// header's own addressing-model fix directly above — it does not touch, -// reinterpret, or extend MdioRequest's/MdioResponse's field shapes, or the -// mdio_mode/mdio_address/mdio_payload/register_key model, in any way. See -// handle_request's own doc comment below for why a Reserved or ConfigWrite -// evt must never reach transact(). +// ── No trigger-signal table (drift fix) ────────────────────────────────────── +// This header previously gave MdioEndpoint an invented TriggerRegistry/ +// MdioSignal::TransferComplete pair with no c-RCP basis. c-RCP's ep_mdio.h +// own file header states this explicitly and deliberately: "Like ep_can.h +// ... this module defines *no* trigger enumeration ... This mirrors +// ep_can.h's own documented reflection of a gap in the specification itself +// ... rather than an oversight." rcp/can.hpp's own CanEndpoint already +// established the correct pattern for this exact situation (no +// TriggerRegistry member, no signal-id helper) — MdioEndpoint below follows +// it. Nothing outside this header referenced MdioSignal/mdio_signal_id/ +// MdioEndpoint::triggers(), so removing them is source-compatible. // // Field names and behavior below implement TC18's *behavior* as described in // an internal structured extraction of the specification named above; no // text from that document is reproduced here. The concrete register-key -// composition chosen in this file is this implementation's own, same as the -// equivalent disclaimers in rcp/avtp.hpp, rcp/regmap.hpp, rcp/endpoint.hpp, -// rcp/i2c.hpp, rcp/adc.hpp, rcp/pwm.hpp, rcp/lin.hpp, rcp/can.hpp, -// rcp/uart.hpp, and rcp/iseled.hpp. +// composition and wire-layout choices in this file are this implementation's +// own where noted, ported from c-RCP where c-RCP defines the behavior, same +// as the equivalent disclaimers in rcp/avtp.hpp, rcp/regmap.hpp, +// rcp/endpoint.hpp, rcp/i2c.hpp, rcp/adc.hpp, rcp/pwm.hpp, rcp/lin.hpp, +// rcp/can.hpp, rcp/uart.hpp, and rcp/iseled.hpp. #pragma once +#include +#include #include +#include +#include +#include #include #include #include #include #include +#include namespace rcp { namespace mdio { -// ── mdio_mode ───────────────────────────────────────────────────────────────── -// See header comment for the 00b/01b elimination reasoning. - +// ── mdio_mode (Table 60, REQ-MDIO-021) ─────────────────────────────────────── +// See the file header for the 00b/01b transcription-defect elimination +// reasoning. Values and bit patterns unchanged from before this pass. enum class MdioMode : uint8_t { MmdSingleWord = 0b00, MmdMultiWord = 0b01, @@ -120,41 +162,263 @@ enum class MdioMode : uint8_t { MmsMultiWord = 0b11, }; -// payload_width_bits returns the mdio_payload width Table 57 assigns for +constexpr uint8_t kModeOctetMask = 0x03; // bits[1:0] of the leading mdio_mode wire octet + +// mode_for_word_count / mms_mode_for_word_count: which MdioMode value a +// request of word_count words selects, for the MMD family and MMS family +// respectively (REQ-MDIO-021/022) — single (word_count == 1) vs. multiple +// (word_count > 1). +constexpr MdioMode mode_for_word_count(size_t word_count) noexcept { + return word_count > 1 ? MdioMode::MmdMultiWord : MdioMode::MmdSingleWord; +} +constexpr MdioMode mms_mode_for_word_count(size_t word_count) noexcept { + return word_count > 1 ? MdioMode::MmsMultiWord : MdioMode::MmsSingleWord; +} + +// mode_is_unsupported_mms: true iff mode belongs to the MMS family +// (MmsSingleWord/MmsMultiWord) rather than the MMD family. Name kept for +// continuity with c-RCP's rcp_ep_mdio_mode_is_unsupported_mms() (its own +// comment: kept "for source compatibility" even though MMS is no longer +// actually unsupported by this module as a whole) — it means "route to the +// *_mms_* decoder family instead", not "MMS is rejected outright". +constexpr bool mode_is_unsupported_mms(MdioMode mode) noexcept { + return mode == MdioMode::MmsSingleWord || mode == MdioMode::MmsMultiWord; +} + +// payload_width_bits: the width (bits) a single register word carries for // `mode`, given whether the addressed MMS device (relevant only for -// MmsMultiWord) is MMS0 or MMS1. +// MmsMultiWord) is MMS0 or MMS1 (Table 60: MMD is always 16-bit; MMS is +// 16-bit except MMS0/MMS1 multi-word access, which is 32-bit). Used by the +// MdioEndpoint convenience wrapper below, not by the ACF-level codec (which +// always knows its own word width from mms_uses_32bit_words()/the MMD +// family's fixed 16-bit width directly). constexpr uint8_t payload_width_bits(MdioMode mode, bool mms_is_0_or_1) noexcept { if (mode == MdioMode::MmsMultiWord && mms_is_0_or_1) return 32; return 16; } -// ── Request / response shapes ──────────────────────────────────────────────── +// ── Clause-22 (MMD legacy) / Clause-45 (MMD extended) addressing ──────────── +// rcp_ep_mdio_addr_t, ported: independently public IEEE 802.3 Clause 22/45 +// knowledge (5-bit port/PHY address, plus either a 5-bit register address +// directly (Clause-22) or a 5-bit device address + 16-bit register address +// (Clause-45)) — not values TC18 itself defines; see the file header. +enum class MdioClause : uint8_t { + Clause22 = 0, // legacy: 5-bit prtad + 5-bit regad directly; devad must be 0 + Clause45 = 1, // extended: 5-bit prtad + 5-bit devad + full 16-bit regad +}; -struct MdioRequest { - MdioMode mode = MdioMode::MmdSingleWord; - uint16_t mdio_address = 0; // opaque per IEEE 802.3 / OA SPI addressing (Table 57); not decoded here - bool mms_is_0_or_1 = false; // only meaningful when mode == MmsMultiWord; see payload_width_bits - bool is_write = false; - uint32_t mdio_payload = 0; // width per payload_width_bits(mode, mms_is_0_or_1) +constexpr uint8_t kPrtadMax = 0x1F; +constexpr uint8_t kDevadMax = 0x1F; +constexpr uint16_t kClause22RegadMax = 0x1F; + +struct MdioAddr { + MdioClause clause = MdioClause::Clause22; + uint8_t prtad = 0; // 5-bit port/PHY address, 0..kPrtadMax + uint8_t devad = 0; // meaningful (0..kDevadMax) only for Clause45; must be 0 for Clause22 + uint16_t regad = 0; // 0..kClause22RegadMax for Clause22; full 16-bit range for Clause45 }; -struct MdioResponse { - uint32_t mdio_payload = 0; +// addr_valid: false for prtad above kPrtadMax, for a Clause22 address with a +// nonzero devad or a regad above kClause22RegadMax, for a Clause45 address +// with devad above kDevadMax, and for any other clause value; true +// otherwise (REQ-MDIO-001). +constexpr bool addr_valid(MdioAddr addr) noexcept { + if (addr.prtad > kPrtadMax) return false; + switch (addr.clause) { + case MdioClause::Clause22: return addr.devad == 0 && addr.regad <= kClause22RegadMax; + case MdioClause::Clause45: return addr.devad <= kDevadMax; + default: return false; + } +} + +// burst_next_regad: the next register address one step into a burst +// starting at regad, for clause's own addressing width — wraps at +// kClause22RegadMax (Clause22) or at 0xFFFF (Clause45); returns regad +// unchanged for any other clause value (REQ-MDIO-002). +constexpr uint16_t burst_next_regad(MdioClause clause, uint16_t regad) noexcept { + switch (clause) { + case MdioClause::Clause22: return static_cast((regad + 1) & kClause22RegadMax); + case MdioClause::Clause45: return static_cast(regad + 1); // wraps at 16 bits naturally + default: return regad; + } +} + +// ── MMS (Memory Map Selector) addressing (REQ-MDIO-022/024) ───────────────── +// See the file header's own "REQ-MDIO-024" section for the documented, +// externally-sourced assumption this family rests on. +constexpr uint8_t kMmsMax = 0x0F; // 4-bit MMS selector, 0..15, OA-SPI spec Table 6 + +struct MdioMmsAddr { + uint8_t mms = 0; // Memory Map Selector, 0..kMmsMax + uint16_t addr = 0; // register address within the selected memory map }; -// ── Errors ──────────────────────────────────────────────────────────────────── +// mms_addr_valid: true iff addr.mms <= kMmsMax. addr.addr's full 16-bit +// range is always valid — no MMS-specific narrower range is known. +constexpr bool mms_addr_valid(MdioMmsAddr addr) noexcept { return addr.mms <= kMmsMax; } + +// mms_uses_32bit_words: true iff mms is 0 or 1 — REQ-MDIO-022's own +// TC18-literal rule (Table 60): MMS0/MMS1 use 32-bit data fields, every +// other mms (2..15) uses 16-bit. Meaningless (but well-defined: false) for +// mms > kMmsMax — callers should have already validated mms via +// mms_addr_valid() first. +constexpr bool mms_uses_32bit_words(uint8_t mms) noexcept { return mms == 0 || mms == 1; } + +// mms_burst_next_addr: the next register address one step into an MMS +// burst starting at addr, at MMS addressing's own full 16-bit width (wraps +// at 0xFFFF) — this module's own design choice, like its MMD counterpart. +constexpr uint16_t mms_burst_next_addr(uint16_t addr) noexcept { + return static_cast(addr + 1); // wraps at 16 bits naturally +} + +// ── Register-word packing: MMD family, always 16-bit (REQ-MDIO-003..008) ──── + +inline void word_encode(uint16_t word, uint8_t out[2]) noexcept { + out[0] = static_cast((word >> 8) & 0xFF); + out[1] = static_cast(word & 0xFF); +} +inline uint16_t word_decode(const uint8_t in[2]) noexcept { + return static_cast((static_cast(in[0]) << 8) | in[1]); +} + +constexpr size_t pack_len(size_t word_count) noexcept { return word_count * 2; } + +// pack_words: packs words[0..word_count) into a newly built big-endian byte +// vector of pack_len(word_count) octets. Returns an empty vector iff +// word_count == 0. +inline std::vector pack_words(const uint16_t* words, size_t word_count) { + if (word_count == 0) return {}; + std::vector out(pack_len(word_count)); + for (size_t i = 0; i < word_count; ++i) word_encode(words[i], &out[2 * i]); + return out; +} +inline std::vector pack_words(const std::vector& words) { + return pack_words(words.empty() ? nullptr : words.data(), words.size()); +} + +// word_count_of: true (with out_word_count set to byte_len / 2) iff +// byte_len is even — every packed word occupies exactly 2 octets, so an odd +// byte_len can never hold a whole number of words. +inline bool word_count_of(size_t byte_len, size_t& out_word_count) noexcept { + if ((byte_len & size_t{1}) != 0) return false; + out_word_count = byte_len / 2; + return true; +} + +// unpack_word_at: reads the word_index'th packed word out of data. No +// bounds check of its own — caller is responsible for having already +// established (e.g. via word_count_of()) that word_index selects a whole +// word actually present in data. +inline uint16_t unpack_word_at(const uint8_t* data, size_t word_index) noexcept { + return word_decode(&data[2 * word_index]); +} + +// ── Register-word packing: MMS family, 16- or 32-bit per mms (REQ-MDIO-022) ─ +// Every word is represented in memory as a uint32_t regardless of its own +// wire width — one packing family instead of two width-specific ones, since +// the width is always a pure function of the already-known `mms` value. + +inline void word32_encode(uint32_t word, uint8_t out[4]) noexcept { + out[0] = static_cast((word >> 24) & 0xFF); + out[1] = static_cast((word >> 16) & 0xFF); + out[2] = static_cast((word >> 8) & 0xFF); + out[3] = static_cast(word & 0xFF); +} + +inline uint32_t word32_decode(const uint8_t in[4]) noexcept { + return (static_cast(in[0]) << 24) | (static_cast(in[1]) << 16) | + (static_cast(in[2]) << 8) | static_cast(in[3]); +} + +namespace detail { +constexpr size_t mms_word_width(uint8_t mms) noexcept { return mms_uses_32bit_words(mms) ? size_t{4} : size_t{2}; } +} // namespace detail + +inline size_t mms_pack_len(uint8_t mms, size_t word_count) noexcept { + return word_count * detail::mms_word_width(mms); +} + +// mms_pack_words: packs words[0..word_count) into a newly built big-endian +// byte vector of mms_pack_len(mms, word_count) octets, at mms's own word +// width (word32_encode() word by word for a 32-bit mms; the low 16 bits of +// each word via word_encode() otherwise). Returns an empty vector iff +// word_count == 0. +inline std::vector mms_pack_words(uint8_t mms, const uint32_t* words, size_t word_count) { + if (word_count == 0) return {}; + const size_t width = detail::mms_word_width(mms); + std::vector out(word_count * width); + for (size_t i = 0; i < word_count; ++i) { + if (width == 4) { + word32_encode(words[i], &out[width * i]); + } else { + word_encode(static_cast(words[i] & 0xFFFF), &out[width * i]); + } + } + return out; +} +inline std::vector mms_pack_words(uint8_t mms, const std::vector& words) { + return mms_pack_words(mms, words.empty() ? nullptr : words.data(), words.size()); +} + +// mms_word_count_of: true (with out_word_count set) iff byte_len is an exact +// multiple of mms's own word width (4 or 2 octets). +inline bool mms_word_count_of(uint8_t mms, size_t byte_len, size_t& out_word_count) noexcept { + const size_t width = detail::mms_word_width(mms); + if (byte_len % width != 0) return false; + out_word_count = byte_len / width; + return true; +} + +// mms_unpack_word_at: reads the word_index'th packed word out of data at +// mms's own width, zero-extended to uint32_t for a 16-bit mms. No bounds +// check of its own. +inline uint32_t mms_unpack_word_at(uint8_t mms, const uint8_t* data, size_t word_index) noexcept { + const size_t width = detail::mms_word_width(mms); + if (width == 4) return word32_decode(&data[width * word_index]); + return static_cast(word_decode(&data[width * word_index])); +} + +// ── Errors ──────────────────────────────────────────────────────────────────── +// payload_exceeds_mode_width/config_write_not_supported keep their existing +// values (1/2) — rcp/mock.hpp compares std::error_code equality against +// both by name. Every other enumerator is new, added by this pass for the +// ACF-level codec below (ported from c-RCP's rcp_ep_mdio_errc_t). enum class MdioErrc : int { - payload_exceeds_mode_width = 1, // mdio_payload does not fit payload_width_bits(mode, mms_is_0_or_1) + // mdio_payload does not fit payload_width_bits(mode, mms_is_0_or_1) — + // MdioEndpoint convenience-wrapper only, see validate_request() below. + payload_exceeds_mode_width = 1, // evt_row2_kind_of classified the request as ConfigWrite (evt[2:0] == - // 111b, §12.7.1). This milestone's follow-up deliberately does not - // implement the configuration-write shape (relative EP_functional- - // config start address + configuration data) — see handle_request's - // own comment. Reported explicitly rather than silently accepted as a - // plain transaction or silently ignored, same as I2C's, ADC's, - // PWM_IN's, LIN's, CAN's, UART's, and ISELED's own - // config_write_not_supported variants. + // 111b, §12.7.1) at the MdioEndpoint convenience-wrapper's own + // handle_request() entry point. Reported explicitly rather than + // silently accepted or silently ignored, same as every sibling + // *Endpoint::handle_request's own config_write_not_supported variant — + // the real mechanism is apply_reconfig()/render_registers() below. config_write_not_supported = 2, + + // ── ACF-level codec errors (ported from rcp_ep_mdio_errc_t) ────────────── + short_frame = 3, + bad_msg_type = 4, + wrong_bus = 5, + wrong_op = 6, + bad_addr = 7, // MMD MdioAddr fails addr_valid() + bad_word_count = 8, + // evt[2:0] is not 0b000, TC18 §13.5 Table 33's only legal value for a + // plain (non-configuration) request in MDIO's endpoint-type row — + // caller shall respond with error code UNSUPPORTED_CMD. + bad_evt = 9, + // The decoded mdio_mode octet belongs to the MMS family + // (mode_is_unsupported_mms()) but was handed to an MMD decoder — use + // the *_mms_* decoder family instead. Name kept for source continuity + // with c-RCP's RCP_EP_MDIO_ERR_UNSUPPORTED_MMS (its own comment: no + // longer means "MMS is unsupported", just "wrong decoder family"). + unsupported_mms = 10, + // The decoded MdioMmsAddr fails mms_addr_valid(). + bad_mms_addr = 11, + // The decoded mdio_mode octet belongs to the MMD family but was handed + // to an *_mms_* decoder — the mirror image of unsupported_mms; use the + // MMD decoder family instead. + wrong_mdio_mode = 12, }; inline const std::error_category& mdio_category() noexcept { @@ -165,7 +429,20 @@ inline const std::error_category& mdio_category() noexcept { case MdioErrc::payload_exceeds_mode_width: return "rcp/mdio: mdio_payload exceeds the width mdio_mode assigns it"; case MdioErrc::config_write_not_supported: - return "rcp/mdio: evt[2:0]=111b configuration-write requests are not yet implemented"; + return "rcp/mdio: evt[2:0]=111b configuration-write requests are not supported by " + "this convenience wrapper — call apply_reconfig() directly"; + case MdioErrc::short_frame: return "rcp/mdio: frame too short"; + case MdioErrc::bad_msg_type: return "rcp/mdio: unexpected ACF message type"; + case MdioErrc::wrong_bus: return "rcp/mdio: wrong byte_bus_id"; + case MdioErrc::wrong_op: return "rcp/mdio: wrong ACF op"; + case MdioErrc::bad_addr: return "rcp/mdio: invalid MDIO address"; + case MdioErrc::bad_word_count: return "rcp/mdio: invalid register-word count"; + case MdioErrc::bad_evt: return "rcp/mdio: evt[2:0] is not 0b000"; + case MdioErrc::unsupported_mms: + return "rcp/mdio: frame uses MMS mode — use the *_mms_* decoder family"; + case MdioErrc::bad_mms_addr: return "rcp/mdio: invalid MMS address"; + case MdioErrc::wrong_mdio_mode: + return "rcp/mdio: frame uses MMD mode — use the MMD decoder family"; default: return "rcp/mdio: unknown error"; } @@ -179,40 +456,819 @@ inline std::error_code make_error_code(MdioErrc e) noexcept { return {static_cast(e), mdio_category()}; } -inline std::error_code validate_request(const MdioRequest& req) noexcept { - const uint8_t width = payload_width_bits(req.mode, req.mms_is_0_or_1); - const uint64_t max_value = (width == 32) ? 0xFFFFFFFFull : 0xFFFFull; - if (req.mdio_payload > max_value) return make_error_code(MdioErrc::payload_exceeds_mode_width); +// ── Functional config (§13.7.13.2 Table 59, REQ-MDIO-009/010/020) ─────────── +// TC18 §13.7.13.2 opens with "The MDIO EP does not have any configurable +// parameters" — that describes what a *write* can change, not whether the +// block is *readable*: Table 59 still fixes a real register block every +// endpoint type exposes via evt[2:0]=111b. Consequently there is no +// set_ep_status()-shaped mutator here, matching every other endpoint type's +// register-block fields being unreachable except through apply_reconfig(). +struct MdioFunctionalCfg { + bool ep_enable = false; + bool ep_clear_req_storage = false; + bool ep_req_crc_enable = false; + bool ep_response_ts_enable = false; + bool ep_suppress_response = false; + + uint16_t ep_status = 0; // 0x0004, R/W — "to be defined" by the spec itself; round-tripped as-is +}; + +inline void functional_cfg_init(MdioFunctionalCfg& cfg) noexcept { cfg = MdioFunctionalCfg{}; } + +// functional_cfg_writable is a thin, named wrapper over rcp/lifecycle.hpp's +// field_writable() (FieldKind::FunctionalW) — reuses, never duplicates, +// that function's authorization logic. +inline bool functional_cfg_writable(lifecycle::ServerState state, lifecycle::WriterCtx writer) noexcept { + return lifecycle::field_writable(state, lifecycle::FieldKind::FunctionalW, writer); +} + +// ── The EP_func register block (evt[2:0] == 111b, Table 59) ───────────────── +// A genuine address-collision editorial defect (c-RCP-AUDIT-06, the fifth +// this audit found, after PWM/GPIO/I2C/ISELED's own): Table 59 prints +// mdio_ep_status at the same relative address (0x0002) as mdio_ep_enable&clr. +// Unlike every other endpoint type's own table, Table 59 defines NO base_clk +// row at all (consistent with "no configurable parameters" — there is +// genuinely no system clock register here), so the minimal, table-literal +// fix is to place mdio_ep_status at the next unclaimed offset after options: +// 0x0004 — one register width narrower than every other endpoint type's +// common prefix, which reserves 0x0004-0x0005 for a base_clk row Table 59 +// never lists. +constexpr uint16_t kRegEpLen = 0x0000; // 8 bit, R +constexpr uint16_t kRegReserved01 = 0x0001; // 8 bit, R +constexpr uint16_t kRegEpEnableClr = 0x0002; // 8 bit, R/W +constexpr uint16_t kRegEpOptions = 0x0003; // 8 bit, R/W +constexpr uint16_t kRegEpStatus = 0x0004; // 16 bit, R/W + +// The block's own length in octets — one past the last assigned offset. +// Note this is narrower than every other endpoint type's own kEpFuncLen: +// there is no base_clk row here. +constexpr size_t kEpFuncLen = 0x0006; + +using EpFuncBlock = std::array; + +namespace detail { +constexpr uint8_t kEnableClrBitEnable = 1u << 0; +constexpr uint8_t kEnableClrBitClear = 1u << 4; +constexpr uint8_t kOptionsBitReqCrc = 1u << 0; +constexpr uint8_t kOptionsBitRespTs = 1u << 3; +constexpr uint8_t kOptionsBitSuppress = 1u << 7; +} // namespace detail + +// render_registers serializes cfg's whole EP_func register block into the +// corrected offsets above — the inverse of apply_reconfig()'s own parse +// step. +inline EpFuncBlock render_registers(const MdioFunctionalCfg& cfg) noexcept { + EpFuncBlock out{}; + uint8_t enable_clr = 0; + uint8_t options = 0; + if (cfg.ep_enable) enable_clr |= detail::kEnableClrBitEnable; + if (cfg.ep_clear_req_storage) enable_clr |= detail::kEnableClrBitClear; + if (cfg.ep_req_crc_enable) options |= detail::kOptionsBitReqCrc; + if (cfg.ep_response_ts_enable) options |= detail::kOptionsBitRespTs; + if (cfg.ep_suppress_response) options |= detail::kOptionsBitSuppress; + + out[kRegEpLen] = static_cast(kEpFuncLen); + out[kRegReserved01] = 0; + out[kRegEpEnableClr] = enable_clr; + out[kRegEpOptions] = options; + out[kRegEpStatus] = static_cast(cfg.ep_status >> 8); + out[kRegEpStatus + 1] = static_cast(cfg.ep_status & 0xFF); + return out; +} + +namespace detail { +inline void parse_registers(MdioFunctionalCfg& cfg, const EpFuncBlock& in) noexcept { + const uint8_t enable_clr = in[kRegEpEnableClr]; + const uint8_t options = in[kRegEpOptions]; + + cfg.ep_enable = (enable_clr & kEnableClrBitEnable) != 0; + cfg.ep_clear_req_storage = (enable_clr & kEnableClrBitClear) != 0; + cfg.ep_req_crc_enable = (options & kOptionsBitReqCrc) != 0; + cfg.ep_response_ts_enable = (options & kOptionsBitRespTs) != 0; + cfg.ep_suppress_response = (options & kOptionsBitSuppress) != 0; + + cfg.ep_status = static_cast((static_cast(in[kRegEpStatus]) << 8) | in[kRegEpStatus + 1]); +} + +// True iff the octet at relative offset addr belongs to a read-only +// register of the block — EP_LEN and the reserved octet (no base_clk row +// to skip here, unlike every other endpoint type's own common prefix). +constexpr bool reg_offset_read_only(uint16_t addr) noexcept { + return addr == kRegEpLen || addr == kRegReserved01; +} +} // namespace detail + +// The fixed width (octets) of the relative-start-address prefix every +// configuration request's payload begins with. +constexpr size_t kReconfigAddrLen = 2; + +enum class MdioReconfigErrc : int { + short_payload = 1, // payload carries no address prefix, or no data octet after it + out_of_range = 2, // start_address + data length exceeds kEpFuncLen — the whole write is ignored +}; + +inline const std::error_category& mdio_reconfig_category() noexcept { + struct Cat : std::error_category { + const char* name() const noexcept override { return "rcp.mdio.reconfig"; } + std::string message(int ev) const override { + switch (static_cast(ev)) { + case MdioReconfigErrc::short_payload: + return "rcp/mdio: configuration write has no address and data"; + case MdioReconfigErrc::out_of_range: + return "rcp/mdio: configuration write extends past the EP_func block"; + default: return "rcp/mdio: unknown configuration-write error"; + } + } + }; + static Cat instance; + return instance; +} + +inline std::error_code make_error_code(MdioReconfigErrc e) noexcept { + return {static_cast(e), mdio_reconfig_category()}; +} + +// apply_reconfig applies the configuration escape hatch (evt[2:0] == 111b): +// payload is a 16-bit big-endian relative start address followed by the +// configuration data octets to write from that address onward (§12.7.1). +// Same octet-granularity patch, read-only-offset-skip, and +// out-of-range-ignores-the-whole-write rules as every sibling endpoint +// type's own apply_reconfig(). +inline std::error_code apply_reconfig(MdioFunctionalCfg& cfg, const uint8_t* payload, + size_t payload_len) noexcept { + if (payload_len <= kReconfigAddrLen) return make_error_code(MdioReconfigErrc::short_payload); + + const uint16_t start_address = + static_cast((static_cast(payload[0]) << 8) | payload[1]); + const size_t data_len = payload_len - kReconfigAddrLen; + + if (static_cast(start_address) + data_len > kEpFuncLen) + return make_error_code(MdioReconfigErrc::out_of_range); + + EpFuncBlock block = render_registers(cfg); + for (size_t i = 0; i < data_len; ++i) { + const uint16_t addr = static_cast(start_address + i); + if (detail::reg_offset_read_only(addr)) continue; + block[addr] = payload[kReconfigAddrLen + i]; + } + detail::parse_registers(cfg, block); return {}; } -// ── Trigger signals ─────────────────────────────────────────────────────────── -// One TransferComplete signal per MdioEndpoint instance, built on -// rcp/endpoint.hpp's generic TriggerRegistry, same primitive every other -// bus-style endpoint type in this codebase uses. +// encode_reconfig_request encodes an ACF_ABB configuration request +// (evt[2:0] == 111b) addressed to byte_bus_id: payload is start_address +// (16-bit big-endian) followed by data. Returns an empty vector if data is +// empty, or if the encoded payload would exceed acf::kAcfAbbMaxPayload. +inline std::vector encode_reconfig_request(avtp::ByteBusId byte_bus_id, uint16_t start_address, + const std::vector& data, + uint8_t transaction_num) { + if (data.empty()) return {}; + if (kReconfigAddrLen + data.size() > acf::kAcfAbbMaxPayload) return {}; + + std::vector payload(kReconfigAddrLen + data.size()); + payload[0] = static_cast(start_address >> 8); + payload[1] = static_cast(start_address & 0xFF); + std::copy(data.begin(), data.end(), payload.begin() + static_cast(kReconfigAddrLen)); + + acf::AcfMessageInfo hdr; + hdr.byte_bus_id = byte_bus_id; + hdr.op = true; // write + hdr.evt_op = 0x7; // evt[2:0] = 111b, the reconfiguration escape hatch + hdr.transaction_num = transaction_num; + return acf::encode_acf_abb(hdr, payload); +} + +// ── Wire-layout constants (this module's own choice, ported from c-RCP's +// ep_mdio.c) — see the file header's "wire-layout" discussion. ──────────── +constexpr size_t kMaxBurstWords = 512; + +namespace detail { +constexpr size_t kModeOctetLen = 1; // mdio_mode +constexpr size_t kAddrPrefixLen = 5; // clause(1)+prtad(1)+devad(1)+regad(2 BE) +constexpr size_t kReadRequestPayloadLen = kModeOctetLen + kAddrPrefixLen + 2; // + word_count(2 BE) +constexpr size_t kWriteRequestMinPayloadLen = kModeOctetLen + kAddrPrefixLen; + +constexpr size_t kMmsAddrPrefixLen = 3; // mms(1)+addr(2 BE) +constexpr size_t kMmsReadRequestPayloadLen = kModeOctetLen + kMmsAddrPrefixLen + 2; // + word_count(2 BE) +constexpr size_t kMmsWriteRequestMinPayloadLen = kModeOctetLen + kMmsAddrPrefixLen; + +inline void put_be16(uint8_t* p, uint16_t v) noexcept { + p[0] = static_cast((v >> 8) & 0xFF); + p[1] = static_cast(v & 0xFF); +} +inline uint16_t get_be16(const uint8_t* p) noexcept { + return static_cast((static_cast(p[0]) << 8) | p[1]); +} + +inline void put_addr_prefix(uint8_t* p, MdioAddr addr) noexcept { + p[0] = static_cast(addr.clause); + p[1] = addr.prtad; + p[2] = addr.devad; + put_be16(&p[3], addr.regad); +} +inline MdioAddr get_addr_prefix(const uint8_t* p) noexcept { + MdioAddr addr; + addr.clause = static_cast(p[0]); + addr.prtad = p[1]; + addr.devad = p[2]; + addr.regad = get_be16(&p[3]); + return addr; +} + +inline void put_mms_addr_prefix(uint8_t* p, MdioMmsAddr addr) noexcept { + p[0] = addr.mms; + put_be16(&p[1], addr.addr); +} +inline MdioMmsAddr get_mms_addr_prefix(const uint8_t* p) noexcept { + MdioMmsAddr addr; + addr.mms = p[0]; + addr.addr = get_be16(&p[1]); + return addr; +} +} // namespace detail -enum class MdioSignal : uint8_t { TransferComplete = 0 }; +// ── Read request/response (MMD family, REQ-MDIO-012..015/025/026) ─────────── -constexpr endpoint::TriggerRegistry::SignalId mdio_signal_id(MdioSignal sig) noexcept { - return static_cast(sig); +// encode_read_request encodes an ACF_ABB read request addressed to +// byte_bus_id: a leading mdio_mode octet (derived from word_count via +// mode_for_word_count()) followed by addr's own clause/prtad/devad/regad +// fields and word_count. Returns an empty vector if !addr_valid(addr), or +// if word_count is 0 or exceeds kMaxBurstWords. +inline std::vector encode_read_request(avtp::ByteBusId byte_bus_id, MdioAddr addr, size_t word_count, + uint8_t transaction_num) { + if (!addr_valid(addr)) return {}; + if (word_count == 0 || word_count > kMaxBurstWords) return {}; + + std::vector payload(detail::kReadRequestPayloadLen); + payload[0] = static_cast(mode_for_word_count(word_count)); + detail::put_addr_prefix(&payload[detail::kModeOctetLen], addr); + detail::put_be16(&payload[detail::kModeOctetLen + detail::kAddrPrefixLen], static_cast(word_count)); + + acf::AcfMessageInfo hdr; + hdr.byte_bus_id = byte_bus_id; + hdr.op = false; // read + hdr.evt_op = 0; + hdr.transaction_num = transaction_num; + return acf::encode_acf_abb(hdr, payload); +} + +// Decodes and validates an ACF-level MDIO read request from b[0..len). +// Fails with MdioErrc::short_frame/bad_msg_type/wrong_bus/wrong_op/bad_evt/ +// unsupported_mms/bad_addr/bad_word_count — see the enumerator comments +// above for the exact condition each maps to. +inline std::error_code decode_read_request(const uint8_t* b, size_t len, avtp::ByteBusId expected_bus_id, + MdioAddr& out_addr, size_t& out_word_count, + uint8_t& out_transaction_num) { + acf::AcfMessageInfo hdr; + std::vector payload; + auto ec = acf::decode_acf_abb(b, len, hdr, payload); + if (ec == avtp::make_error_code(avtp::AvtpErrc::short_buffer)) return make_error_code(MdioErrc::short_frame); + if (ec) return make_error_code(MdioErrc::bad_msg_type); + + if (hdr.byte_bus_id != expected_bus_id) return make_error_code(MdioErrc::wrong_bus); + if (hdr.op) return make_error_code(MdioErrc::wrong_op); + if (!acf::evt_row2_is_plain(hdr.evt_op)) return make_error_code(MdioErrc::bad_evt); + if (payload.size() < detail::kReadRequestPayloadLen) return make_error_code(MdioErrc::short_frame); + + const auto mode = static_cast(payload[0] & kModeOctetMask); + if (mode_is_unsupported_mms(mode)) return make_error_code(MdioErrc::unsupported_mms); + + const MdioAddr addr = detail::get_addr_prefix(&payload[detail::kModeOctetLen]); + if (!addr_valid(addr)) return make_error_code(MdioErrc::bad_addr); + + const uint16_t word_count = detail::get_be16(&payload[detail::kModeOctetLen + detail::kAddrPrefixLen]); + if (word_count == 0 || static_cast(word_count) > kMaxBurstWords) + return make_error_code(MdioErrc::bad_word_count); + + out_addr = addr; + out_word_count = word_count; + out_transaction_num = hdr.transaction_num; + return {}; +} + +// encode_read_response encodes a read response carrying +// pack_words(words, word_count) as its payload, echoing transaction_num. +// Encoded as ACF_ABB when timed is false; ACF_GBB (message_timestamp = +// timestamp, mtv valid) when timed is true. word_count may be fewer than +// the originating request's own word_count (a short/partial burst read) or +// 0. Returns an empty vector if word_count exceeds kMaxBurstWords. +inline std::vector encode_read_response(avtp::ByteBusId byte_bus_id, const uint16_t* words, + size_t word_count, uint8_t transaction_num, bool timed, + uint64_t timestamp) { + if (word_count > kMaxBurstWords) return {}; + const std::vector payload = pack_words(words, word_count); + + acf::AcfMessageInfo hdr; + hdr.byte_bus_id = byte_bus_id; + hdr.op = false; // read + hdr.rsp = true; + hdr.evt_op = 0; + hdr.transaction_num = transaction_num; + + if (timed) { + hdr.mtv = true; + return acf::encode_acf_gbb(hdr, timestamp, payload); + } + return acf::encode_acf_abb(hdr, payload); +} +inline std::vector encode_read_response(avtp::ByteBusId byte_bus_id, const std::vector& words, + uint8_t transaction_num, bool timed, uint64_t timestamp) { + return encode_read_response(byte_bus_id, words.empty() ? nullptr : words.data(), words.size(), + transaction_num, timed, timestamp); +} + +// decode_read_response decodes a read response from either an ACF_ABB or +// ACF_GBB message (peeks the message type itself, since a response's +// encoding depends on the responding endpoint's own timed/untimed choice). +// out_words_data holds the packed word bytes — unpack_word_at() reads +// individual words out of it. +inline std::error_code decode_read_response(const uint8_t* b, size_t len, avtp::ByteBusId expected_bus_id, + std::vector& out_words_data, size_t& out_word_count, + bool& out_timed, uint64_t& out_timestamp, + uint8_t& out_transaction_num) { + uint8_t msg_type = 0; + if (acf::peek_msg_type(b, len, msg_type)) return make_error_code(MdioErrc::short_frame); + + acf::AcfMessageInfo hdr; + std::vector payload; + bool timed = false; + uint64_t timestamp = 0; + + if (msg_type == acf::kAcfMsgTypeGbb) { + uint64_t ts = 0; + const auto ec = acf::decode_acf_gbb(b, len, hdr, ts, payload); + if (ec == avtp::make_error_code(avtp::AvtpErrc::short_buffer)) return make_error_code(MdioErrc::short_frame); + if (ec) return make_error_code(MdioErrc::bad_msg_type); + timed = hdr.mtv; + timestamp = timed ? ts : 0; + } else { + const auto ec = acf::decode_acf_abb(b, len, hdr, payload); + if (ec == avtp::make_error_code(avtp::AvtpErrc::short_buffer)) return make_error_code(MdioErrc::short_frame); + if (ec) return make_error_code(MdioErrc::bad_msg_type); + } + + if (hdr.byte_bus_id != expected_bus_id) return make_error_code(MdioErrc::wrong_bus); + + size_t word_count = 0; + if (!word_count_of(payload.size(), word_count)) return make_error_code(MdioErrc::bad_word_count); + if (word_count > kMaxBurstWords) return make_error_code(MdioErrc::bad_word_count); + + out_words_data = std::move(payload); + out_word_count = word_count; + out_timed = timed; + out_timestamp = timestamp; + out_transaction_num = hdr.transaction_num; + return {}; +} + +// ── Write request/response (MMD family, REQ-MDIO-016..019/027/028) ────────── + +// encode_write_request encodes an ACF_ABB write request addressed to +// byte_bus_id: a leading mdio_mode octet followed by addr's own address +// prefix and pack_words(words, word_count). Returns an empty vector if +// !addr_valid(addr), or if word_count is 0 or exceeds kMaxBurstWords. +inline std::vector encode_write_request(avtp::ByteBusId byte_bus_id, MdioAddr addr, + const uint16_t* words, size_t word_count, + uint8_t transaction_num) { + if (!addr_valid(addr)) return {}; + if (word_count == 0 || word_count > kMaxBurstWords) return {}; + + const std::vector words_bytes = pack_words(words, word_count); + std::vector payload(detail::kWriteRequestMinPayloadLen + words_bytes.size()); + payload[0] = static_cast(mode_for_word_count(word_count)); + detail::put_addr_prefix(&payload[detail::kModeOctetLen], addr); + std::copy(words_bytes.begin(), words_bytes.end(), + payload.begin() + static_cast(detail::kWriteRequestMinPayloadLen)); + + acf::AcfMessageInfo hdr; + hdr.byte_bus_id = byte_bus_id; + hdr.op = true; // write + hdr.evt_op = 0; + hdr.transaction_num = transaction_num; + return acf::encode_acf_abb(hdr, payload); +} +inline std::vector encode_write_request(avtp::ByteBusId byte_bus_id, MdioAddr addr, + const std::vector& words, uint8_t transaction_num) { + return encode_write_request(byte_bus_id, addr, words.empty() ? nullptr : words.data(), words.size(), + transaction_num); +} + +// Decodes and validates an ACF-level MDIO write request from b[0..len). +// Fails with MdioErrc::short_frame/bad_msg_type/wrong_bus/wrong_op/bad_evt/ +// unsupported_mms/bad_addr/bad_word_count. out_words_data holds the packed +// word bytes following the address prefix — unpack_word_at() reads +// individual words out of it. +inline std::error_code decode_write_request(const uint8_t* b, size_t len, avtp::ByteBusId expected_bus_id, + MdioAddr& out_addr, std::vector& out_words_data, + size_t& out_word_count, uint8_t& out_transaction_num) { + acf::AcfMessageInfo hdr; + std::vector payload; + auto ec = acf::decode_acf_abb(b, len, hdr, payload); + if (ec == avtp::make_error_code(avtp::AvtpErrc::short_buffer)) return make_error_code(MdioErrc::short_frame); + if (ec) return make_error_code(MdioErrc::bad_msg_type); + + if (hdr.byte_bus_id != expected_bus_id) return make_error_code(MdioErrc::wrong_bus); + if (!hdr.op) return make_error_code(MdioErrc::wrong_op); + if (!acf::evt_row2_is_plain(hdr.evt_op)) return make_error_code(MdioErrc::bad_evt); + if (payload.size() < detail::kWriteRequestMinPayloadLen) return make_error_code(MdioErrc::short_frame); + + const auto mode = static_cast(payload[0] & kModeOctetMask); + if (mode_is_unsupported_mms(mode)) return make_error_code(MdioErrc::unsupported_mms); + + const MdioAddr addr = detail::get_addr_prefix(&payload[detail::kModeOctetLen]); + if (!addr_valid(addr)) return make_error_code(MdioErrc::bad_addr); + + const size_t words_len = payload.size() - detail::kWriteRequestMinPayloadLen; + size_t word_count = 0; + if (!word_count_of(words_len, word_count)) return make_error_code(MdioErrc::bad_word_count); + if (word_count == 0 || word_count > kMaxBurstWords) return make_error_code(MdioErrc::bad_word_count); + + out_addr = addr; + out_words_data = std::vector(payload.begin() + static_cast(detail::kWriteRequestMinPayloadLen), + payload.end()); + out_word_count = word_count; + out_transaction_num = hdr.transaction_num; + return {}; +} + +// encode_write_response encodes a write response carrying +// pack_words(accepted_words, accepted_word_count) as its payload, echoing +// transaction_num — the words this endpoint actually accepted (possibly a +// prefix of the originating request's own words on a partial burst, or 0 +// for nothing accepted). Encoded as ACF_ABB when timed is false; ACF_GBB +// when timed is true. +inline std::vector encode_write_response(avtp::ByteBusId byte_bus_id, const uint16_t* accepted_words, + size_t accepted_word_count, uint8_t transaction_num, + bool timed, uint64_t timestamp) { + if (accepted_word_count > kMaxBurstWords) return {}; + const std::vector payload = pack_words(accepted_words, accepted_word_count); + + acf::AcfMessageInfo hdr; + hdr.byte_bus_id = byte_bus_id; + hdr.op = true; // write + hdr.rsp = true; + hdr.evt_op = 0; + hdr.transaction_num = transaction_num; + + if (timed) { + hdr.mtv = true; + return acf::encode_acf_gbb(hdr, timestamp, payload); + } + return acf::encode_acf_abb(hdr, payload); +} +inline std::vector encode_write_response(avtp::ByteBusId byte_bus_id, + const std::vector& accepted_words, + uint8_t transaction_num, bool timed, uint64_t timestamp) { + return encode_write_response(byte_bus_id, accepted_words.empty() ? nullptr : accepted_words.data(), + accepted_words.size(), transaction_num, timed, timestamp); +} + +// decode_write_response decodes a write response from either an ACF_ABB or +// ACF_GBB message (peeked, same reasoning as decode_read_response()). +inline std::error_code decode_write_response(const uint8_t* b, size_t len, avtp::ByteBusId expected_bus_id, + std::vector& out_words_data, size_t& out_word_count, + bool& out_timed, uint64_t& out_timestamp, + uint8_t& out_transaction_num) { + uint8_t msg_type = 0; + if (acf::peek_msg_type(b, len, msg_type)) return make_error_code(MdioErrc::short_frame); + + acf::AcfMessageInfo hdr; + std::vector payload; + bool timed = false; + uint64_t timestamp = 0; + + if (msg_type == acf::kAcfMsgTypeGbb) { + uint64_t ts = 0; + const auto ec = acf::decode_acf_gbb(b, len, hdr, ts, payload); + if (ec == avtp::make_error_code(avtp::AvtpErrc::short_buffer)) return make_error_code(MdioErrc::short_frame); + if (ec) return make_error_code(MdioErrc::bad_msg_type); + timed = hdr.mtv; + timestamp = timed ? ts : 0; + } else { + const auto ec = acf::decode_acf_abb(b, len, hdr, payload); + if (ec == avtp::make_error_code(avtp::AvtpErrc::short_buffer)) return make_error_code(MdioErrc::short_frame); + if (ec) return make_error_code(MdioErrc::bad_msg_type); + } + + if (hdr.byte_bus_id != expected_bus_id) return make_error_code(MdioErrc::wrong_bus); + + size_t word_count = 0; + if (!word_count_of(payload.size(), word_count)) return make_error_code(MdioErrc::bad_word_count); + if (word_count > kMaxBurstWords) return make_error_code(MdioErrc::bad_word_count); + + out_words_data = std::move(payload); + out_word_count = word_count; + out_timed = timed; + out_timestamp = timestamp; + out_transaction_num = hdr.transaction_num; + return {}; +} + +// ── MMS read request/response (REQ-MDIO-022/024) ──────────────────────────── +// The MMS family's own counterpart to the MMD read family above — see the +// file header's "REQ-MDIO-024" section for the wire layout and its +// documented assumption. + +inline std::vector encode_mms_read_request(avtp::ByteBusId byte_bus_id, MdioMmsAddr addr, + size_t word_count, uint8_t transaction_num) { + if (!mms_addr_valid(addr)) return {}; + if (word_count == 0 || word_count > kMaxBurstWords) return {}; + + std::vector payload(detail::kMmsReadRequestPayloadLen); + payload[0] = static_cast(mms_mode_for_word_count(word_count)); + detail::put_mms_addr_prefix(&payload[detail::kModeOctetLen], addr); + detail::put_be16(&payload[detail::kModeOctetLen + detail::kMmsAddrPrefixLen], static_cast(word_count)); + + acf::AcfMessageInfo hdr; + hdr.byte_bus_id = byte_bus_id; + hdr.op = false; // read + hdr.evt_op = 0; + hdr.transaction_num = transaction_num; + return acf::encode_acf_abb(hdr, payload); +} + +// Fails the same way decode_read_request() does; MdioErrc::wrong_mdio_mode +// if the decoded mdio_mode octet belongs to the MMD family instead (use +// decode_read_request()); MdioErrc::bad_mms_addr if the decoded address +// fails mms_addr_valid(). +inline std::error_code decode_mms_read_request(const uint8_t* b, size_t len, avtp::ByteBusId expected_bus_id, + MdioMmsAddr& out_addr, size_t& out_word_count, + uint8_t& out_transaction_num) { + acf::AcfMessageInfo hdr; + std::vector payload; + auto ec = acf::decode_acf_abb(b, len, hdr, payload); + if (ec == avtp::make_error_code(avtp::AvtpErrc::short_buffer)) return make_error_code(MdioErrc::short_frame); + if (ec) return make_error_code(MdioErrc::bad_msg_type); + + if (hdr.byte_bus_id != expected_bus_id) return make_error_code(MdioErrc::wrong_bus); + if (hdr.op) return make_error_code(MdioErrc::wrong_op); + if (!acf::evt_row2_is_plain(hdr.evt_op)) return make_error_code(MdioErrc::bad_evt); + if (payload.size() < detail::kMmsReadRequestPayloadLen) return make_error_code(MdioErrc::short_frame); + + const auto mode = static_cast(payload[0] & kModeOctetMask); + if (!mode_is_unsupported_mms(mode)) return make_error_code(MdioErrc::wrong_mdio_mode); + + const MdioMmsAddr addr = detail::get_mms_addr_prefix(&payload[detail::kModeOctetLen]); + if (!mms_addr_valid(addr)) return make_error_code(MdioErrc::bad_mms_addr); + + const uint16_t word_count = detail::get_be16(&payload[detail::kModeOctetLen + detail::kMmsAddrPrefixLen]); + if (word_count == 0 || static_cast(word_count) > kMaxBurstWords) + return make_error_code(MdioErrc::bad_word_count); + + out_addr = addr; + out_word_count = word_count; + out_transaction_num = hdr.transaction_num; + return {}; +} + +// encode_mms_read_response encodes a read response carrying +// mms_pack_words(mms, words, word_count) as its payload — mms is a +// caller-supplied input (not carried in the response payload itself; the +// caller already knows it from the originating request). Otherwise +// identical to encode_read_response(). +inline std::vector encode_mms_read_response(avtp::ByteBusId byte_bus_id, uint8_t mms, + const uint32_t* words, size_t word_count, + uint8_t transaction_num, bool timed, uint64_t timestamp) { + if (word_count > kMaxBurstWords) return {}; + const std::vector payload = mms_pack_words(mms, words, word_count); + + acf::AcfMessageInfo hdr; + hdr.byte_bus_id = byte_bus_id; + hdr.op = false; // read + hdr.rsp = true; + hdr.evt_op = 0; + hdr.transaction_num = transaction_num; + + if (timed) { + hdr.mtv = true; + return acf::encode_acf_gbb(hdr, timestamp, payload); + } + return acf::encode_acf_abb(hdr, payload); +} + +// decode_mms_read_response: mms is a caller-supplied INPUT used only to +// validate the payload's own byte length against mms's own word width via +// mms_word_count_of() — otherwise identical to decode_read_response(). +inline std::error_code decode_mms_read_response(const uint8_t* b, size_t len, avtp::ByteBusId expected_bus_id, + uint8_t mms, std::vector& out_words_data, + size_t& out_word_count, bool& out_timed, uint64_t& out_timestamp, + uint8_t& out_transaction_num) { + uint8_t msg_type = 0; + if (acf::peek_msg_type(b, len, msg_type)) return make_error_code(MdioErrc::short_frame); + + acf::AcfMessageInfo hdr; + std::vector payload; + bool timed = false; + uint64_t timestamp = 0; + + if (msg_type == acf::kAcfMsgTypeGbb) { + uint64_t ts = 0; + const auto ec = acf::decode_acf_gbb(b, len, hdr, ts, payload); + if (ec == avtp::make_error_code(avtp::AvtpErrc::short_buffer)) return make_error_code(MdioErrc::short_frame); + if (ec) return make_error_code(MdioErrc::bad_msg_type); + timed = hdr.mtv; + timestamp = timed ? ts : 0; + } else { + const auto ec = acf::decode_acf_abb(b, len, hdr, payload); + if (ec == avtp::make_error_code(avtp::AvtpErrc::short_buffer)) return make_error_code(MdioErrc::short_frame); + if (ec) return make_error_code(MdioErrc::bad_msg_type); + } + + if (hdr.byte_bus_id != expected_bus_id) return make_error_code(MdioErrc::wrong_bus); + + size_t word_count = 0; + if (!mms_word_count_of(mms, payload.size(), word_count)) return make_error_code(MdioErrc::bad_word_count); + if (word_count > kMaxBurstWords) return make_error_code(MdioErrc::bad_word_count); + + out_words_data = std::move(payload); + out_word_count = word_count; + out_timed = timed; + out_timestamp = timestamp; + out_transaction_num = hdr.transaction_num; + return {}; +} + +// ── MMS write request/response (REQ-MDIO-022/024) ─────────────────────────── + +inline std::vector encode_mms_write_request(avtp::ByteBusId byte_bus_id, MdioMmsAddr addr, + const uint32_t* words, size_t word_count, + uint8_t transaction_num) { + if (!mms_addr_valid(addr)) return {}; + if (word_count == 0 || word_count > kMaxBurstWords) return {}; + + const std::vector words_bytes = mms_pack_words(addr.mms, words, word_count); + std::vector payload(detail::kMmsWriteRequestMinPayloadLen + words_bytes.size()); + payload[0] = static_cast(mms_mode_for_word_count(word_count)); + detail::put_mms_addr_prefix(&payload[detail::kModeOctetLen], addr); + std::copy(words_bytes.begin(), words_bytes.end(), + payload.begin() + static_cast(detail::kMmsWriteRequestMinPayloadLen)); + + acf::AcfMessageInfo hdr; + hdr.byte_bus_id = byte_bus_id; + hdr.op = true; // write + hdr.evt_op = 0; + hdr.transaction_num = transaction_num; + return acf::encode_acf_abb(hdr, payload); +} + +// Fails the same way decode_mms_read_request() does (with +// MdioErrc::wrong_op instead of a read-op check, matching +// decode_write_request()'s own convention). MdioErrc::bad_word_count covers +// a words-region byte length that is not a whole multiple of the decoded +// addr.mms's own word width, is 0, or represents more than kMaxBurstWords +// words. +inline std::error_code decode_mms_write_request(const uint8_t* b, size_t len, avtp::ByteBusId expected_bus_id, + MdioMmsAddr& out_addr, std::vector& out_words_data, + size_t& out_word_count, uint8_t& out_transaction_num) { + acf::AcfMessageInfo hdr; + std::vector payload; + auto ec = acf::decode_acf_abb(b, len, hdr, payload); + if (ec == avtp::make_error_code(avtp::AvtpErrc::short_buffer)) return make_error_code(MdioErrc::short_frame); + if (ec) return make_error_code(MdioErrc::bad_msg_type); + + if (hdr.byte_bus_id != expected_bus_id) return make_error_code(MdioErrc::wrong_bus); + if (!hdr.op) return make_error_code(MdioErrc::wrong_op); + if (!acf::evt_row2_is_plain(hdr.evt_op)) return make_error_code(MdioErrc::bad_evt); + if (payload.size() < detail::kMmsWriteRequestMinPayloadLen) return make_error_code(MdioErrc::short_frame); + + const auto mode = static_cast(payload[0] & kModeOctetMask); + if (!mode_is_unsupported_mms(mode)) return make_error_code(MdioErrc::wrong_mdio_mode); + + const MdioMmsAddr addr = detail::get_mms_addr_prefix(&payload[detail::kModeOctetLen]); + if (!mms_addr_valid(addr)) return make_error_code(MdioErrc::bad_mms_addr); + + const size_t words_len = payload.size() - detail::kMmsWriteRequestMinPayloadLen; + size_t word_count = 0; + if (!mms_word_count_of(addr.mms, words_len, word_count)) return make_error_code(MdioErrc::bad_word_count); + if (word_count == 0 || word_count > kMaxBurstWords) return make_error_code(MdioErrc::bad_word_count); + + out_addr = addr; + out_words_data = std::vector(payload.begin() + static_cast(detail::kMmsWriteRequestMinPayloadLen), + payload.end()); + out_word_count = word_count; + out_transaction_num = hdr.transaction_num; + return {}; +} + +// encode_mms_write_response: mms is a caller-supplied input, the same +// convention as encode_mms_read_response(). Otherwise identical to +// encode_write_response(). +inline std::vector encode_mms_write_response(avtp::ByteBusId byte_bus_id, uint8_t mms, + const uint32_t* accepted_words, + size_t accepted_word_count, uint8_t transaction_num, + bool timed, uint64_t timestamp) { + if (accepted_word_count > kMaxBurstWords) return {}; + const std::vector payload = mms_pack_words(mms, accepted_words, accepted_word_count); + + acf::AcfMessageInfo hdr; + hdr.byte_bus_id = byte_bus_id; + hdr.op = true; // write + hdr.rsp = true; + hdr.evt_op = 0; + hdr.transaction_num = transaction_num; + + if (timed) { + hdr.mtv = true; + return acf::encode_acf_gbb(hdr, timestamp, payload); + } + return acf::encode_acf_abb(hdr, payload); +} + +// decode_mms_write_response: mms is a caller-supplied input, the same +// convention as decode_mms_read_response(). Otherwise identical to +// decode_write_response(). +inline std::error_code decode_mms_write_response(const uint8_t* b, size_t len, avtp::ByteBusId expected_bus_id, + uint8_t mms, std::vector& out_words_data, + size_t& out_word_count, bool& out_timed, uint64_t& out_timestamp, + uint8_t& out_transaction_num) { + uint8_t msg_type = 0; + if (acf::peek_msg_type(b, len, msg_type)) return make_error_code(MdioErrc::short_frame); + + acf::AcfMessageInfo hdr; + std::vector payload; + bool timed = false; + uint64_t timestamp = 0; + + if (msg_type == acf::kAcfMsgTypeGbb) { + uint64_t ts = 0; + const auto ec = acf::decode_acf_gbb(b, len, hdr, ts, payload); + if (ec == avtp::make_error_code(avtp::AvtpErrc::short_buffer)) return make_error_code(MdioErrc::short_frame); + if (ec) return make_error_code(MdioErrc::bad_msg_type); + timed = hdr.mtv; + timestamp = timed ? ts : 0; + } else { + const auto ec = acf::decode_acf_abb(b, len, hdr, payload); + if (ec == avtp::make_error_code(avtp::AvtpErrc::short_buffer)) return make_error_code(MdioErrc::short_frame); + if (ec) return make_error_code(MdioErrc::bad_msg_type); + } + + if (hdr.byte_bus_id != expected_bus_id) return make_error_code(MdioErrc::wrong_bus); + + size_t word_count = 0; + if (!mms_word_count_of(mms, payload.size(), word_count)) return make_error_code(MdioErrc::bad_word_count); + if (word_count > kMaxBurstWords) return make_error_code(MdioErrc::bad_word_count); + + out_words_data = std::move(payload); + out_word_count = word_count; + out_timed = timed; + out_timestamp = timestamp; + out_transaction_num = hdr.transaction_num; + return {}; +} + +// ── MdioEndpoint convenience wrapper (source-compatible with rcp/mock.hpp) ── +// MdioRequest/MdioResponse/MdioEndpoint below are UNCHANGED in field shape +// and method signature from before this pass — rcp/mock.hpp's dispatch_mdio() +// constructs MdioRequest{mode, mdio_address, is_write, mdio_payload} and +// calls mdio_.handle_request(req.evt_op, request, response) with exactly +// this shape; both keep working unmodified by this port. +// +// REFRAMED (this pass): the header comment that used to accompany this +// section claimed mdio_mode/mdio_address/mdio_payload as "the real +// addressing model" per Table 57/60 — that claim is false against c-RCP's +// actual design (see this file's own top-of-file "MAJOR CONTENT-DRIFT FIX" +// section) and is corrected here. mdio_address below is honestly this +// wrapper's OWN simplified, opaque round-trip key — deliberately NOT +// decomposed into the real Clause-22/Clause-45 prtad/devad/regad split (or +// the real MMS mms/addr split) the ACF-level codec above now implements +// faithfully. This wrapper exists purely as a self-contained (mode, +// mdio_address)-keyed register-map convenience for rcp/mock.hpp's own +// in-process simulator (see mock.hpp's own "No set_mdio_response()" note) — +// a caller that needs TC18-conformant wire encoding uses the free functions +// above instead (encode_read_request()/decode_read_request()/etc.). +// +// TriggerRegistry removed (drift fix): see the file header's own "No +// trigger-signal table" section — c-RCP documents no MDIO trigger table at +// all, mirroring rcp/can.hpp's CanEndpoint, which this class now matches. + +struct MdioRequest { + MdioMode mode = MdioMode::MmdSingleWord; + uint16_t mdio_address = 0; // this wrapper's own opaque round-trip key — see comment above + bool mms_is_0_or_1 = false; // only meaningful when mode == MmsMultiWord; see payload_width_bits + bool is_write = false; + uint32_t mdio_payload = 0; // width per payload_width_bits(mode, mms_is_0_or_1) +}; + +struct MdioResponse { + uint32_t mdio_payload = 0; +}; + +inline std::error_code validate_request(const MdioRequest& req) noexcept { + const uint8_t width = payload_width_bits(req.mode, req.mms_is_0_or_1); + const uint64_t max_value = (width == 32) ? 0xFFFFFFFFull : 0xFFFFull; + if (req.mdio_payload > max_value) return make_error_code(MdioErrc::payload_exceeds_mode_width); + return {}; } -// ── MdioEndpoint ────────────────────────────────────────────────────────────── // register_key folds (mode, mdio_address) into one lookup key, so the four // mdio_mode values never collide with each other even for an identical -// mdio_address bit pattern (mirroring how the register spaces of, e.g., -// MMD vs. MMS access are logically distinct on real MDIO-manageable -// devices, per the spec's own MMD/MMS split in Table 57). +// mdio_address bit pattern. class MdioEndpoint { public: // transact is MDIO's own pre-existing single-call register access — a - // write stores req.mdio_payload under (mode, mdio_address) and echoes it - // back via `out`; a read returns whatever was last stored there, or 0 if - // never written. Renamed from this method's pre-milestone name - // `handle_request` (see this header's own top comment) so that name is - // free for the evt[2:0]-classifying entry point below, matching - // rcp/iseled.hpp's IseledEndpoint::transact naming for the identical - // role. + // write stores req.mdio_payload under (mode, mdio_address) and echoes + // it back via `out`; a read returns whatever was last stored there, or + // 0 if never written. std::error_code transact(MdioRequest req, MdioResponse& out) { auto ec = validate_request(req); if (ec) return ec; @@ -226,54 +1282,25 @@ class MdioEndpoint { const auto it = registers_.find(key); out.mdio_payload = (it != registers_.end()) ? it->second : uint32_t{0}; } - triggers_.notify(mdio_signal_id(MdioSignal::TransferComplete)); return {}; } - // handle_request is MDIO's request-decode entry point — the piece this - // header previously had none of, mirroring rcp::i2c:: - // I2cEndpoint::handle_request's shape (this repo's eighth and LAST - // Table 33 Row 2 endpoint type, after I2C, ADC, PWM_IN, LIN, CAN, UART, - // and ISELED). It classifies the incoming request's evt[2:0] field via + // handle_request classifies the incoming request's evt[2:0] field via // rcp::endpoint::evt_row2_kind_of before doing anything else, so a // Reserved value can never reach transact() and be misread as an // ordinary transaction, and a ConfigWrite value can never be silently // accepted or silently dropped: // - Plain (evt[2:0] == 000b): delegates straight to transact(request, - // out) with both arguments unchanged — MDIO's existing - // mdio_mode-keyed request/response transaction model (extraction - // §13.7.13.3) already IS this row's correct "plain request" - // behavior; evt[2:0] carries no combinable value or channel - // selector for this row. + // out) with both arguments unchanged. // - Reserved (evt[2:0] in 001b-110b): returns // endpoint::EndpointErrc::reserved_evt_row2 without recording // anything (last_request_ is left exactly as it was, no register is - // written or read, and TransferComplete does not fire) — TC18 - // requires this be rejected with error code UNSUPPORTED_CMD. + // written or read). // - ConfigWrite (evt[2:0] == 111b): §12.7.1's configuration-write - // shape targets the MDIO EP's own functional-config block, not an - // mdio_mode-selected register access at all. Full handling is - // deliberately out of scope for this milestone (nontrivial — it - // needs EP_functional-config wiring this header does not yet have, - // the same gap I2C's, ADC's, PWM_IN's, LIN's, CAN's, UART's, and - // ISELED's own handle_request comments defer for the identical - // reason); this returns MdioErrc::config_write_not_supported rather - // than crashing, silently accepting the request as a transaction, - // or silently doing nothing. - // - // NOT to be confused with this header's own addressing-model fix above - // (issue #72's mdio_mode/mdio_address/mdio_payload rebuild replacing the - // invented Clause 22/Clause 45 scheme): that fix concerns what - // MdioRequest's and MdioResponse's *fields* mean and how wide they are; - // Table 33's evt[2:0] classification is an entirely separate, orthogonal - // concern about which *kind* of request evt[2:0] itself selects. Reading - // evt[2:0] as if it also selected or combined with mode/address/payload, - // or inventing any encoding of those fields beyond what - // validate_request/register_key already define, would be exactly the - // kind of invented, non-spec-derived encoding this header's own - // addressing-model-fix comment documents having to remove once already. - // handle_request below calls the same shared evt_row2_kind_of every - // other Row 2 endpoint type uses and invents nothing of its own. + // shape targets the MDIO EP's own functional-config block + // (apply_reconfig()/render_registers() above), not an + // mdio_mode-selected register access at all — returns + // MdioErrc::config_write_not_supported. std::error_code handle_request(uint8_t evt_op, MdioRequest req, MdioResponse& out) { switch (endpoint::evt_row2_kind_of(evt_op)) { case endpoint::EvtRow2Kind::Plain: @@ -287,14 +1314,12 @@ class MdioEndpoint { } const MdioRequest& last_request() const noexcept { return last_request_; } - endpoint::TriggerRegistry& triggers() noexcept { return triggers_; } private: static uint64_t register_key(const MdioRequest& req) noexcept { return (static_cast(req.mode) << 16) | req.mdio_address; } - endpoint::TriggerRegistry triggers_; MdioRequest last_request_; std::unordered_map registers_; }; @@ -302,8 +1327,10 @@ class MdioEndpoint { } // namespace mdio } // namespace rcp -// Enable std::error_code construction from rcp::mdio::MdioErrc. +// Enable std::error_code construction from rcp::mdio::MdioErrc/MdioReconfigErrc. namespace std { template <> struct is_error_code_enum : true_type {}; +template <> +struct is_error_code_enum : true_type {}; } // namespace std diff --git a/tests/test_mdio.cpp b/tests/test_mdio.cpp index 2006f20..2143eea 100644 --- a/tests/test_mdio.cpp +++ b/tests/test_mdio.cpp @@ -5,24 +5,1288 @@ // fusa:test REQ-MDIO-005 // fusa:test REQ-MDIO-006 // fusa:test REQ-MDIO-007 +// fusa:test REQ-MDIO-008 +// fusa:test REQ-MDIO-009 +// fusa:test REQ-MDIO-010 +// fusa:test REQ-MDIO-011 +// fusa:test REQ-MDIO-012 +// fusa:test REQ-MDIO-013 +// fusa:test REQ-MDIO-014 +// fusa:test REQ-MDIO-015 +// fusa:test REQ-MDIO-016 +// fusa:test REQ-MDIO-017 +// fusa:test REQ-MDIO-018 +// fusa:test REQ-MDIO-019 +// fusa:test REQ-MDIO-020 +// fusa:test REQ-MDIO-021 +// fusa:test REQ-MDIO-022 +// fusa:test REQ-MDIO-023 +// fusa:test REQ-MDIO-024 +// fusa:test REQ-MDIO-025 +// fusa:test REQ-MDIO-026 +// fusa:test REQ-MDIO-027 +// fusa:test REQ-MDIO-028 -// Tests for rcp/mdio.hpp — the MDIO endpoint type (ROADMAP.md milestone 51, -// "Remaining Endpoint Types — LIN, CAN (incl. CAN XL), ISELED, MDIO, Wakeup -// Control", v2.7.0). +// Tests for rcp/mdio.hpp — the MDIO endpoint type (ep_type 0x0D), ported +// from c-RCP's tests/test_ep_mdio.c (this project's RC5-spec-conformant +// reference, and the single largest endpoint test file in c-RCP at 2219 +// lines) as part of Phase 3 of the ground-up rewrite (cpp-RCP issue #129, +// ROADMAP.md "Phase 17"). Allocation-failure tests from test_ep_mdio.c +// (rcp_alloc_set_hooks()-based MC/DC coverage of c-RCP's own rcp_malloc() +// failure paths) have no cpp-RCP equivalent — this codec uses std::vector +// throughout, with no allocation-hook seam anywhere else in this codebase +// either (see e.g. rcp/i2c.hpp's/rcp/iseled.hpp's own test files, which +// carry none) — and are therefore not ported. #include +#include #include +#include #include using namespace rcp::mdio; +namespace { +MdioAddr clause22_addr(uint8_t prtad, uint16_t regad) { + MdioAddr addr; + addr.clause = MdioClause::Clause22; + addr.prtad = prtad; + addr.devad = 0; + addr.regad = regad; + return addr; +} +MdioAddr clause45_addr(uint8_t prtad, uint8_t devad, uint16_t regad) { + MdioAddr addr; + addr.clause = MdioClause::Clause45; + addr.prtad = prtad; + addr.devad = devad; + addr.regad = regad; + return addr; +} +MdioMmsAddr mms_addr(uint8_t mms, uint16_t addr_val) { + MdioMmsAddr addr; + addr.mms = mms; + addr.addr = addr_val; + return addr; +} +} // namespace + // ── ep_type id ──────────────────────────────────────────────────────────────── TEST_CASE("MDIO's ep_type id is 0x0D", "[mdio][REQ-MDIO-001]") { REQUIRE(rcp::endpoint::kEndpointTypeMdio == 0x0D); } -// ── mdio_payload width rule (Table 57) ─────────────────────────────────────── +// ── addr_valid (Clause-22/Clause-45), REQ-MDIO-001 ─────────────────────────── + +TEST_CASE("addr_valid accepts Clause-22 addresses in range", "[mdio][REQ-MDIO-001]") { + REQUIRE(addr_valid(clause22_addr(0, 0))); + REQUIRE(addr_valid(clause22_addr(0x1F, 0x1F))); +} + +TEST_CASE("addr_valid rejects a Clause-22 address with a nonzero devad", "[mdio][REQ-MDIO-001]") { + MdioAddr addr = clause22_addr(1, 1); + addr.devad = 1; + REQUIRE_FALSE(addr_valid(addr)); +} + +TEST_CASE("addr_valid rejects a Clause-22 regad above 5 bits", "[mdio][REQ-MDIO-001]") { + REQUIRE_FALSE(addr_valid(clause22_addr(0, 0x20))); +} + +TEST_CASE("addr_valid accepts Clause-45 addresses in range", "[mdio][REQ-MDIO-001]") { + REQUIRE(addr_valid(clause45_addr(0, 0, 0))); + REQUIRE(addr_valid(clause45_addr(0x1F, 0x1F, 0xFFFF))); +} + +TEST_CASE("addr_valid rejects a Clause-45 devad above 5 bits", "[mdio][REQ-MDIO-001]") { + REQUIRE_FALSE(addr_valid(clause45_addr(0, 0x20, 0))); +} + +TEST_CASE("addr_valid rejects prtad above 5 bits for either clause", "[mdio][REQ-MDIO-001]") { + REQUIRE_FALSE(addr_valid(clause22_addr(0x20, 0))); + REQUIRE_FALSE(addr_valid(clause45_addr(0x20, 0, 0))); +} + +TEST_CASE("addr_valid rejects an unknown clause value", "[mdio][REQ-MDIO-001]") { + MdioAddr addr = clause22_addr(0, 0); + addr.clause = static_cast(2); + REQUIRE_FALSE(addr_valid(addr)); +} + +// ── burst_next_regad, REQ-MDIO-002 ─────────────────────────────────────────── + +TEST_CASE("burst_next_regad increments within Clause-22's 5-bit range", "[mdio][REQ-MDIO-002]") { + REQUIRE(burst_next_regad(MdioClause::Clause22, 0) == 1); +} +TEST_CASE("burst_next_regad wraps at Clause-22's 5-bit boundary", "[mdio][REQ-MDIO-002]") { + REQUIRE(burst_next_regad(MdioClause::Clause22, 0x1F) == 0); +} +TEST_CASE("burst_next_regad increments within Clause-45's 16-bit range", "[mdio][REQ-MDIO-002]") { + REQUIRE(burst_next_regad(MdioClause::Clause45, 0x1234) == 0x1235); +} +TEST_CASE("burst_next_regad wraps at Clause-45's 16-bit boundary", "[mdio][REQ-MDIO-002]") { + REQUIRE(burst_next_regad(MdioClause::Clause45, 0xFFFF) == 0); +} +TEST_CASE("burst_next_regad leaves regad unchanged for an unknown clause", "[mdio][REQ-MDIO-002]") { + REQUIRE(burst_next_regad(static_cast(2), 42) == 42); +} + +// ── mdio_mode, REQ-MDIO-021 ─────────────────────────────────────────────────── + +TEST_CASE("mode_for_word_count selects single for word_count==1, multi otherwise", + "[mdio][REQ-MDIO-021]") { + REQUIRE(mode_for_word_count(1) == MdioMode::MmdSingleWord); + REQUIRE(mode_for_word_count(2) == MdioMode::MmdMultiWord); + REQUIRE(mode_for_word_count(kMaxBurstWords) == MdioMode::MmdMultiWord); +} + +TEST_CASE("mode_is_unsupported_mms is true only for the MMS mode values", "[mdio][REQ-MDIO-021]") { + REQUIRE_FALSE(mode_is_unsupported_mms(MdioMode::MmdSingleWord)); + REQUIRE_FALSE(mode_is_unsupported_mms(MdioMode::MmdMultiWord)); + REQUIRE(mode_is_unsupported_mms(MdioMode::MmsSingleWord)); + REQUIRE(mode_is_unsupported_mms(MdioMode::MmsMultiWord)); +} + +TEST_CASE("encode_read_request's leading mdio_mode octet reflects word_count", "[mdio][REQ-MDIO-021]") { + { + const auto frame = encode_read_request(2, clause22_addr(1, 0), 1, 0); + REQUIRE_FALSE(frame.empty()); + rcp::acf::AcfMessageInfo hdr; + std::vector payload; + REQUIRE_FALSE(rcp::acf::decode_acf_abb(frame.data(), frame.size(), hdr, payload)); + REQUIRE(payload[0] == static_cast(MdioMode::MmdSingleWord)); + } + { + const auto frame = encode_read_request(2, clause22_addr(1, 0), 4, 0); + rcp::acf::AcfMessageInfo hdr; + std::vector payload; + REQUIRE_FALSE(rcp::acf::decode_acf_abb(frame.data(), frame.size(), hdr, payload)); + REQUIRE(payload[0] == static_cast(MdioMode::MmdMultiWord)); + } +} + +TEST_CASE("encode_write_request's leading mdio_mode octet reflects word_count", "[mdio][REQ-MDIO-021]") { + { + const uint16_t word = 0x1234; + const auto frame = encode_write_request(2, clause45_addr(1, 2, 0), &word, 1, 0); + rcp::acf::AcfMessageInfo hdr; + std::vector payload; + REQUIRE_FALSE(rcp::acf::decode_acf_abb(frame.data(), frame.size(), hdr, payload)); + REQUIRE(payload[0] == static_cast(MdioMode::MmdSingleWord)); + } + { + const uint16_t words[3] = {1, 2, 3}; + const auto frame = encode_write_request(2, clause45_addr(1, 2, 0), words, 3, 0); + rcp::acf::AcfMessageInfo hdr; + std::vector payload; + REQUIRE_FALSE(rcp::acf::decode_acf_abb(frame.data(), frame.size(), hdr, payload)); + REQUIRE(payload[0] == static_cast(MdioMode::MmdMultiWord)); + } +} + +TEST_CASE("decode_read_request rejects a frame whose mdio_mode octet is an MMS value", + "[mdio][REQ-MDIO-013][REQ-MDIO-021]") { + std::vector payload(8, 0); + payload[0] = static_cast(MdioMode::MmsSingleWord); + payload[7] = 1; // word_count -- otherwise-valid + + rcp::acf::AcfMessageInfo hdr; + hdr.byte_bus_id = 2; + hdr.op = false; // read + const auto frame = rcp::acf::encode_acf_abb(hdr, payload); + + MdioAddr out_addr; + size_t out_word_count = 0; + uint8_t txn = 0; + REQUIRE(decode_read_request(frame.data(), frame.size(), 2, out_addr, out_word_count, txn) == + make_error_code(MdioErrc::unsupported_mms)); +} + +TEST_CASE("decode_write_request rejects a frame whose mdio_mode octet is an MMS value", + "[mdio][REQ-MDIO-017][REQ-MDIO-021]") { + std::vector payload(1 + 5 + 2, 0); + payload[0] = static_cast(MdioMode::MmsMultiWord); + + rcp::acf::AcfMessageInfo hdr; + hdr.byte_bus_id = 2; + hdr.op = true; // write + const auto frame = rcp::acf::encode_acf_abb(hdr, payload); + + MdioAddr out_addr; + std::vector out_words; + size_t out_word_count = 0; + uint8_t txn = 0; + REQUIRE(decode_write_request(frame.data(), frame.size(), 2, out_addr, out_words, out_word_count, txn) == + make_error_code(MdioErrc::unsupported_mms)); +} + +// ── MMS addressing, REQ-MDIO-022/024 ───────────────────────────────────────── + +TEST_CASE("mms_addr_valid accepts mms in range", "[mdio][REQ-MDIO-024]") { + REQUIRE(mms_addr_valid(mms_addr(0, 0))); + REQUIRE(mms_addr_valid(mms_addr(kMmsMax, 0xFFFF))); +} +TEST_CASE("mms_addr_valid rejects mms above kMmsMax", "[mdio][REQ-MDIO-024]") { + REQUIRE_FALSE(mms_addr_valid(mms_addr(static_cast(kMmsMax + 1), 0))); +} +TEST_CASE("mms_uses_32bit_words is true only for MMS0 and MMS1", "[mdio][REQ-MDIO-022]") { + REQUIRE(mms_uses_32bit_words(0)); + REQUIRE(mms_uses_32bit_words(1)); + for (uint8_t mms = 2; mms <= kMmsMax; ++mms) REQUIRE_FALSE(mms_uses_32bit_words(mms)); +} +TEST_CASE("mms_burst_next_addr increments and wraps at 16 bits", "[mdio][REQ-MDIO-024]") { + REQUIRE(mms_burst_next_addr(0x0001) == 0x0002); + REQUIRE(mms_burst_next_addr(0xFFFF) == 0x0000); +} +TEST_CASE("mms_mode_for_word_count selects single for word_count==1, multi otherwise", + "[mdio][REQ-MDIO-022]") { + REQUIRE(mms_mode_for_word_count(1) == MdioMode::MmsSingleWord); + REQUIRE(mms_mode_for_word_count(2) == MdioMode::MmsMultiWord); + REQUIRE(mms_mode_for_word_count(kMaxBurstWords) == MdioMode::MmsMultiWord); +} + +// ── Register-word packing: MMD family (16-bit), REQ-MDIO-003..008 ─────────── + +TEST_CASE("word_encode/word_decode round-trip and are big-endian", "[mdio][REQ-MDIO-003][REQ-MDIO-004]") { + uint8_t buf[2]; + word_encode(0xBEEF, buf); + REQUIRE(word_decode(buf) == 0xBEEF); + + word_encode(0x1234, buf); + REQUIRE(buf[0] == 0x12); + REQUIRE(buf[1] == 0x34); +} + +TEST_CASE("pack_len is word_count * 2", "[mdio][REQ-MDIO-005]") { + REQUIRE(pack_len(0) == 0); + REQUIRE(pack_len(3) == 6); +} + +TEST_CASE("pack_words/word_count_of/unpack_word_at round-trip", "[mdio][REQ-MDIO-006][REQ-MDIO-008]") { + const uint16_t words[3] = {0x0001, 0xBEEF, 0xFFFF}; + const auto packed = pack_words(words, 3); + REQUIRE(packed.size() == 6); + + size_t word_count = 0; + REQUIRE(word_count_of(packed.size(), word_count)); + REQUIRE(word_count == 3); + for (size_t i = 0; i < 3; ++i) REQUIRE(unpack_word_at(packed.data(), i) == words[i]); +} + +TEST_CASE("pack_words returns an empty vector for word_count 0", "[mdio][REQ-MDIO-006]") { + REQUIRE(pack_words(nullptr, 0).empty()); +} + +TEST_CASE("word_count_of rejects an odd byte length", "[mdio][REQ-MDIO-007]") { + size_t word_count = 0; + REQUIRE_FALSE(word_count_of(3, word_count)); +} +TEST_CASE("word_count_of accepts an even byte length", "[mdio][REQ-MDIO-007]") { + size_t word_count = 0; + REQUIRE(word_count_of(8, word_count)); + REQUIRE(word_count == 4); +} + +// ── Register-word packing: MMS family (16- or 32-bit), REQ-MDIO-022 ───────── + +TEST_CASE("word32_encode/word32_decode round-trip and are big-endian", "[mdio][REQ-MDIO-022]") { + uint8_t buf[4]; + word32_encode(0xDEADBEEFu, buf); + REQUIRE(word32_decode(buf) == 0xDEADBEEFu); + + word32_encode(0x12345678u, buf); + REQUIRE(buf[0] == 0x12); + REQUIRE(buf[1] == 0x34); + REQUIRE(buf[2] == 0x56); + REQUIRE(buf[3] == 0x78); +} + +TEST_CASE("mms_pack_len is 4 octets/word for MMS0/1, 2 otherwise", "[mdio][REQ-MDIO-022]") { + REQUIRE(mms_pack_len(0, 3) == 12); + REQUIRE(mms_pack_len(2, 3) == 6); +} + +TEST_CASE("mms_pack_words/mms_unpack_word_at round-trip at 32-bit width", "[mdio][REQ-MDIO-022]") { + const uint32_t words[2] = {0x11223344u, 0xAABBCCDDu}; + const auto packed = mms_pack_words(1 /* MMS1: 32-bit */, words, 2); + REQUIRE(packed.size() == 8); + REQUIRE(mms_unpack_word_at(1, packed.data(), 0) == 0x11223344u); + REQUIRE(mms_unpack_word_at(1, packed.data(), 1) == 0xAABBCCDDu); +} + +TEST_CASE("mms_pack_words/mms_unpack_word_at round-trip at 16-bit width", "[mdio][REQ-MDIO-022]") { + const uint32_t words[2] = {0x1234u, 0xBEEFu}; + const auto packed = mms_pack_words(4 /* not 0/1: 16-bit */, words, 2); + REQUIRE(packed.size() == 4); + REQUIRE(mms_unpack_word_at(4, packed.data(), 0) == 0x1234u); + REQUIRE(mms_unpack_word_at(4, packed.data(), 1) == 0xBEEFu); +} + +TEST_CASE("mms_pack_words returns an empty vector for word_count 0", "[mdio][REQ-MDIO-022]") { + REQUIRE(mms_pack_words(0, nullptr, 0).empty()); +} + +TEST_CASE("mms_word_count_of rejects a byte length not a multiple of the 32-bit width", + "[mdio][REQ-MDIO-022]") { + size_t out = 0; + REQUIRE_FALSE(mms_word_count_of(0, 5, out)); + REQUIRE(mms_word_count_of(0, 8, out)); + REQUIRE(out == 2); +} +TEST_CASE("mms_word_count_of rejects an odd byte length at 16-bit width", "[mdio][REQ-MDIO-022]") { + size_t out = 0; + REQUIRE_FALSE(mms_word_count_of(3, 3, out)); + REQUIRE(mms_word_count_of(3, 4, out)); + REQUIRE(out == 2); +} + +// ── Functional config, REQ-MDIO-009/010 ────────────────────────────────────── + +TEST_CASE("functional_cfg_init zeroes every field", "[mdio][REQ-MDIO-009]") { + MdioFunctionalCfg cfg; + cfg.ep_enable = cfg.ep_clear_req_storage = cfg.ep_req_crc_enable = true; + cfg.ep_status = 0xBEEF; + + functional_cfg_init(cfg); + + REQUIRE_FALSE(cfg.ep_enable); + REQUIRE_FALSE(cfg.ep_clear_req_storage); + REQUIRE_FALSE(cfg.ep_req_crc_enable); + REQUIRE_FALSE(cfg.ep_response_ts_enable); + REQUIRE_FALSE(cfg.ep_suppress_response); + REQUIRE(cfg.ep_status == 0); +} + +TEST_CASE("functional_cfg_writable is false in HwUnconfigured regardless of writer", + "[mdio][REQ-MDIO-010]") { + rcp::lifecycle::WriterCtx writer; + writer.via_root_client_ep0 = true; + writer.via_owning_stream = true; + REQUIRE_FALSE(functional_cfg_writable(rcp::lifecycle::ServerState::HwUnconfigured, writer)); +} + +TEST_CASE("functional_cfg_writable in HwConfigured requires EP0/owning-stream/discovery-stream", + "[mdio][REQ-MDIO-010]") { + rcp::lifecycle::WriterCtx none, via_ep0, via_stream, via_discovery; + via_ep0.via_root_client_ep0 = true; + via_stream.via_owning_stream = true; + via_discovery.via_discovery_stream = true; + + REQUIRE_FALSE(functional_cfg_writable(rcp::lifecycle::ServerState::HwConfigured, none)); + REQUIRE(functional_cfg_writable(rcp::lifecycle::ServerState::HwConfigured, via_ep0)); + REQUIRE(functional_cfg_writable(rcp::lifecycle::ServerState::HwConfigured, via_stream)); + REQUIRE(functional_cfg_writable(rcp::lifecycle::ServerState::HwConfigured, via_discovery)); +} + +TEST_CASE("functional_cfg_writable in RcpConfigured requires EP0/owning-stream, not discovery", + "[mdio][REQ-MDIO-010]") { + rcp::lifecycle::WriterCtx none, via_ep0, via_stream; + via_ep0.via_root_client_ep0 = true; + via_stream.via_owning_stream = true; + + REQUIRE_FALSE(functional_cfg_writable(rcp::lifecycle::ServerState::RcpConfigured, none)); + REQUIRE(functional_cfg_writable(rcp::lifecycle::ServerState::RcpConfigured, via_ep0)); + REQUIRE(functional_cfg_writable(rcp::lifecycle::ServerState::RcpConfigured, via_stream)); +} + +// ── The EP_func register block, REQ-MDIO-020/023 ───────────────────────────── + +TEST_CASE("render_registers matches the corrected Table 59 offsets (no base_clk row)", + "[mdio][REQ-MDIO-020][REQ-MDIO-023]") { + MdioFunctionalCfg cfg; + functional_cfg_init(cfg); + cfg.ep_enable = true; + cfg.ep_status = 0x1234; + + const auto out = render_registers(cfg); + + REQUIRE(out[kRegEpLen] == static_cast(kEpFuncLen)); + REQUIRE(out[kRegReserved01] == 0); + REQUIRE((out[kRegEpEnableClr] & 0x01) != 0); + REQUIRE(out[kRegEpStatus] == 0x12); + REQUIRE(out[kRegEpStatus + 1] == 0x34); + REQUIRE(kEpFuncLen == 0x0006u); +} + +TEST_CASE("apply_reconfig writes ep_status", "[mdio][REQ-MDIO-023]") { + MdioFunctionalCfg cfg; + functional_cfg_init(cfg); + const uint8_t payload[4] = {0x00, static_cast(kRegEpStatus), 0xAB, 0xCD}; + + REQUIRE_FALSE(apply_reconfig(cfg, payload, sizeof(payload))); + REQUIRE(cfg.ep_status == 0xABCD); +} + +TEST_CASE("apply_reconfig ignores EP_LEN and the reserved octet", "[mdio][REQ-MDIO-023]") { + MdioFunctionalCfg cfg; + functional_cfg_init(cfg); + const uint8_t payload[4] = {0x00, 0x00, 0xFF, 0xFF}; + + REQUIRE_FALSE(apply_reconfig(cfg, payload, sizeof(payload))); + + const auto out = render_registers(cfg); + REQUIRE(out[kRegEpLen] == static_cast(kEpFuncLen)); + REQUIRE(out[kRegReserved01] == 0); +} + +TEST_CASE("apply_reconfig rejects a write extending past EP_LEN", "[mdio][REQ-MDIO-023]") { + MdioFunctionalCfg cfg; + functional_cfg_init(cfg); + const uint8_t payload[3] = {0x00, 0x06, 0xFF}; // 0x06 == kEpFuncLen -- one past the last valid offset + + REQUIRE(apply_reconfig(cfg, payload, sizeof(payload)) == make_error_code(MdioReconfigErrc::out_of_range)); + REQUIRE(cfg.ep_status == 0); +} + +TEST_CASE("apply_reconfig rejects a payload with no data octet", "[mdio][REQ-MDIO-023]") { + MdioFunctionalCfg cfg; + functional_cfg_init(cfg); + const uint8_t addr_only[2] = {0x00, 0x04}; + + REQUIRE(apply_reconfig(cfg, addr_only, sizeof(addr_only)) == + make_error_code(MdioReconfigErrc::short_payload)); + REQUIRE(apply_reconfig(cfg, nullptr, 0) == make_error_code(MdioReconfigErrc::short_payload)); +} + +TEST_CASE("encode_reconfig_request round-trips through acf::decode_acf_abb", "[mdio][REQ-MDIO-023]") { + const std::vector data{0xAB, 0xCD}; + const auto frame = encode_reconfig_request(0x03, 0x0004, data, 7); + REQUIRE_FALSE(frame.empty()); + + rcp::acf::AcfMessageInfo hdr; + std::vector payload; + REQUIRE_FALSE(rcp::acf::decode_acf_abb(frame.data(), frame.size(), hdr, payload)); + REQUIRE(hdr.byte_bus_id == 0x03); + REQUIRE(hdr.op); + REQUIRE(hdr.evt_op == 0x7); + REQUIRE(hdr.transaction_num == 7); + REQUIRE(payload == std::vector{0x00, 0x04, 0xAB, 0xCD}); +} + +TEST_CASE("encode_reconfig_request rejects empty data", "[mdio][REQ-MDIO-023]") { + REQUIRE(encode_reconfig_request(0x00, 0, {}, 0).empty()); +} + +TEST_CASE("mdio reconfig error category reports a distinct, non-empty message per code", + "[mdio][REQ-MDIO-023]") { + auto short_ec = make_error_code(MdioReconfigErrc::short_payload); + auto range_ec = make_error_code(MdioReconfigErrc::out_of_range); + REQUIRE_FALSE(short_ec.message().empty()); + REQUIRE_FALSE(range_ec.message().empty()); + REQUIRE(short_ec.message() != range_ec.message()); +} + +// ── MdioErrc category sanity, REQ-MDIO-011 ─────────────────────────────────── + +TEST_CASE("MdioErrc reports a non-empty, distinct message per code", "[mdio][REQ-MDIO-011]") { + const MdioErrc codes[] = { + MdioErrc::payload_exceeds_mode_width, MdioErrc::config_write_not_supported, + MdioErrc::short_frame, MdioErrc::bad_msg_type, + MdioErrc::wrong_bus, MdioErrc::wrong_op, + MdioErrc::bad_addr, MdioErrc::bad_word_count, + MdioErrc::bad_evt, MdioErrc::unsupported_mms, + MdioErrc::bad_mms_addr, MdioErrc::wrong_mdio_mode, + }; + std::vector seen; + for (auto c : codes) { + auto ec = make_error_code(c); + REQUIRE(ec.category() == mdio_category()); + REQUIRE_FALSE(ec.message().empty()); + for (const auto& s : seen) REQUIRE(s != ec.message()); + seen.push_back(ec.message()); + } + REQUIRE_FALSE(make_error_code(static_cast(999)).message().empty()); +} + +// ── Read request round trip (MMD family), REQ-MDIO-012/013 ────────────────── + +TEST_CASE("read request round-trips a single-word Clause-45 address", "[mdio][REQ-MDIO-012][REQ-MDIO-013]") { + const auto addr = clause45_addr(3, 1, 0x1234); + const auto frame = encode_read_request(6, addr, 1, 9); + REQUIRE_FALSE(frame.empty()); + + MdioAddr out_addr; + size_t out_word_count = 0; + uint8_t txn = 0; + REQUIRE_FALSE(decode_read_request(frame.data(), frame.size(), 6, out_addr, out_word_count, txn)); + REQUIRE(out_addr.clause == MdioClause::Clause45); + REQUIRE(out_addr.prtad == 3); + REQUIRE(out_addr.devad == 1); + REQUIRE(out_addr.regad == 0x1234); + REQUIRE(out_word_count == 1); + REQUIRE(txn == 9); +} + +TEST_CASE("read request round-trips a burst", "[mdio][REQ-MDIO-012][REQ-MDIO-013]") { + const auto frame = encode_read_request(4, clause22_addr(2, 5), 16, 1); + MdioAddr out_addr; + size_t out_word_count = 0; + uint8_t txn = 0; + REQUIRE_FALSE(decode_read_request(frame.data(), frame.size(), 4, out_addr, out_word_count, txn)); + REQUIRE(out_word_count == 16); +} + +TEST_CASE("encode_read_request rejects an invalid address", "[mdio][REQ-MDIO-012]") { + MdioAddr addr = clause22_addr(0, 0); + addr.devad = 1; // invalid for Clause-22 + REQUIRE(encode_read_request(1, addr, 1, 0).empty()); +} +TEST_CASE("encode_read_request rejects word_count 0 or above kMaxBurstWords", "[mdio][REQ-MDIO-012]") { + REQUIRE(encode_read_request(1, clause22_addr(0, 0), 0, 0).empty()); + REQUIRE(encode_read_request(1, clause22_addr(0, 0), kMaxBurstWords + 1, 0).empty()); +} + +TEST_CASE("decode_read_request rejects the wrong bus", "[mdio][REQ-MDIO-013]") { + const auto frame = encode_read_request(4, clause22_addr(0, 0), 1, 0); + MdioAddr out_addr; + size_t out_word_count; + uint8_t txn; + REQUIRE(decode_read_request(frame.data(), frame.size(), 5, out_addr, out_word_count, txn) == + make_error_code(MdioErrc::wrong_bus)); +} + +TEST_CASE("decode_read_request rejects a write-op frame", "[mdio][REQ-MDIO-013]") { + rcp::acf::AcfMessageInfo hdr; + hdr.byte_bus_id = 2; + hdr.op = true; // not a read request + const auto frame = rcp::acf::encode_acf_abb(hdr, {}); + + MdioAddr out_addr; + size_t out_word_count; + uint8_t txn; + REQUIRE(decode_read_request(frame.data(), frame.size(), 2, out_addr, out_word_count, txn) == + make_error_code(MdioErrc::wrong_op)); +} + +TEST_CASE("decode_read_request rejects a nonzero evt[2:0]", "[mdio][REQ-MDIO-013]") { + rcp::acf::AcfMessageInfo hdr; + hdr.byte_bus_id = 2; + hdr.op = false; + hdr.evt_op = 0x4; // reserved in MDIO's Table 33 row + const auto frame = rcp::acf::encode_acf_abb(hdr, {}); + + MdioAddr out_addr; + size_t out_word_count; + uint8_t txn; + REQUIRE(decode_read_request(frame.data(), frame.size(), 2, out_addr, out_word_count, txn) == + make_error_code(MdioErrc::bad_evt)); +} + +TEST_CASE("decode_read_request rejects a non-ABB frame", "[mdio][REQ-MDIO-013]") { + rcp::acf::AcfMessageInfo hdr; + hdr.byte_bus_id = 2; + hdr.op = false; + const auto frame = rcp::acf::encode_acf_gbb(hdr, 0, {}); + + MdioAddr out_addr; + size_t out_word_count; + uint8_t txn; + REQUIRE(decode_read_request(frame.data(), frame.size(), 2, out_addr, out_word_count, txn) == + make_error_code(MdioErrc::bad_msg_type)); +} + +TEST_CASE("decode_read_request rejects a frame too short for the mode+address prefix", + "[mdio][REQ-MDIO-013]") { + rcp::acf::AcfMessageInfo hdr; + hdr.byte_bus_id = 2; + hdr.op = false; + const std::vector too_short{0, 0, 0}; + const auto frame = rcp::acf::encode_acf_abb(hdr, too_short); + + MdioAddr out_addr; + size_t out_word_count; + uint8_t txn; + REQUIRE(decode_read_request(frame.data(), frame.size(), 2, out_addr, out_word_count, txn) == + make_error_code(MdioErrc::short_frame)); +} + +TEST_CASE("decode_read_request rejects an invalid decoded address", "[mdio][REQ-MDIO-013]") { + std::vector payload(8, 0); + payload[0] = static_cast(MdioMode::MmdSingleWord); + payload[3] = 1; // devad -- invalid for Clause-22 (payload[1]==clause==0) + payload[7] = 1; // word_count + + rcp::acf::AcfMessageInfo hdr; + hdr.byte_bus_id = 2; + hdr.op = false; + const auto frame = rcp::acf::encode_acf_abb(hdr, payload); + + MdioAddr out_addr; + size_t out_word_count; + uint8_t txn; + REQUIRE(decode_read_request(frame.data(), frame.size(), 2, out_addr, out_word_count, txn) == + make_error_code(MdioErrc::bad_addr)); +} + +TEST_CASE("decode_read_request rejects a zero word_count", "[mdio][REQ-MDIO-013]") { + std::vector payload(8, 0); + payload[0] = static_cast(MdioMode::MmdSingleWord); + + rcp::acf::AcfMessageInfo hdr; + hdr.byte_bus_id = 2; + hdr.op = false; + const auto frame = rcp::acf::encode_acf_abb(hdr, payload); + + MdioAddr out_addr; + size_t out_word_count; + uint8_t txn; + REQUIRE(decode_read_request(frame.data(), frame.size(), 2, out_addr, out_word_count, txn) == + make_error_code(MdioErrc::bad_word_count)); +} + +TEST_CASE("decode_read_request rejects a word_count above kMaxBurstWords", "[mdio][REQ-MDIO-013]") { + std::vector payload(8, 0); + payload[0] = static_cast(MdioMode::MmdMultiWord); + payload[6] = static_cast((kMaxBurstWords + 1) >> 8); + payload[7] = static_cast((kMaxBurstWords + 1) & 0xFF); + + rcp::acf::AcfMessageInfo hdr; + hdr.byte_bus_id = 2; + hdr.op = false; + const auto frame = rcp::acf::encode_acf_abb(hdr, payload); + + MdioAddr out_addr; + size_t out_word_count; + uint8_t txn; + REQUIRE(decode_read_request(frame.data(), frame.size(), 2, out_addr, out_word_count, txn) == + make_error_code(MdioErrc::bad_word_count)); +} + +// ── Read response round trip (MMD family), REQ-MDIO-014/015/025/026 ───────── + +TEST_CASE("read response round-trips untimed", "[mdio][REQ-MDIO-014][REQ-MDIO-015]") { + const uint16_t words[2] = {0x1111, 0x2222}; + const auto frame = encode_read_response(3, words, 2, 5, false, 0); + REQUIRE_FALSE(frame.empty()); + + std::vector out_words; + size_t out_word_count = 0; + bool timed = true; + uint64_t ts = 1; + uint8_t txn = 0; + REQUIRE_FALSE(decode_read_response(frame.data(), frame.size(), 3, out_words, out_word_count, timed, ts, txn)); + REQUIRE(out_word_count == 2); + REQUIRE(unpack_word_at(out_words.data(), 0) == 0x1111); + REQUIRE(unpack_word_at(out_words.data(), 1) == 0x2222); + REQUIRE_FALSE(timed); + REQUIRE(ts == 0); + REQUIRE(txn == 5); +} + +TEST_CASE("read response round-trips timed", "[mdio][REQ-MDIO-025][REQ-MDIO-026]") { + const uint16_t words[1] = {0xABCD}; + const auto frame = encode_read_response(3, words, 1, 2, true, 424242); + + std::vector out_words; + size_t out_word_count = 0; + bool timed = false; + uint64_t ts = 0; + uint8_t txn = 0; + REQUIRE_FALSE(decode_read_response(frame.data(), frame.size(), 3, out_words, out_word_count, timed, ts, txn)); + REQUIRE(timed); + REQUIRE(ts == 424242); +} + +TEST_CASE("read response round-trips an empty word set", "[mdio][REQ-MDIO-014][REQ-MDIO-015]") { + const auto frame = encode_read_response(3, nullptr, 0, 2, false, 0); + REQUIRE_FALSE(frame.empty()); + + std::vector out_words; + size_t out_word_count = 1; + bool timed = false; + uint64_t ts = 0; + uint8_t txn = 0; + REQUIRE_FALSE(decode_read_response(frame.data(), frame.size(), 3, out_words, out_word_count, timed, ts, txn)); + REQUIRE(out_word_count == 0); +} + +TEST_CASE("decode_read_response rejects the wrong bus", "[mdio][REQ-MDIO-015]") { + const uint16_t words[1] = {1}; + const auto frame = encode_read_response(3, words, 1, 0, false, 0); + std::vector out_words; + size_t out_word_count; + bool timed; + uint64_t ts; + uint8_t txn; + REQUIRE(decode_read_response(frame.data(), frame.size(), 4, out_words, out_word_count, timed, ts, txn) == + make_error_code(MdioErrc::wrong_bus)); +} + +TEST_CASE("decode_read_response rejects a short frame", "[mdio][REQ-MDIO-015]") { + const uint8_t too_short[3] = {static_cast(rcp::acf::kAcfMsgTypeAbb << 1), 0, 0}; + std::vector out_words; + size_t out_word_count; + bool timed; + uint64_t ts; + uint8_t txn; + REQUIRE(decode_read_response(too_short, sizeof(too_short), 2, out_words, out_word_count, timed, ts, txn) == + make_error_code(MdioErrc::short_frame)); +} + +TEST_CASE("decode_read_response rejects an odd payload length", "[mdio][REQ-MDIO-015]") { + rcp::acf::AcfMessageInfo hdr; + hdr.byte_bus_id = 2; + hdr.op = false; + const std::vector odd_payload{0, 0, 0}; + const auto frame = rcp::acf::encode_acf_abb(hdr, odd_payload); + + std::vector out_words; + size_t out_word_count; + bool timed; + uint64_t ts; + uint8_t txn; + REQUIRE(decode_read_response(frame.data(), frame.size(), 2, out_words, out_word_count, timed, ts, txn) == + make_error_code(MdioErrc::bad_word_count)); +} + +// ── Write request round trip (MMD family), REQ-MDIO-016/017 ───────────────── + +TEST_CASE("write request round-trips a single word", "[mdio][REQ-MDIO-016][REQ-MDIO-017]") { + const auto addr = clause45_addr(4, 2, 0x0010); + const uint16_t words[1] = {0x9999}; + const auto frame = encode_write_request(7, addr, words, 1, 3); + REQUIRE_FALSE(frame.empty()); + + MdioAddr out_addr; + std::vector out_words; + size_t out_word_count = 0; + uint8_t txn = 0; + REQUIRE_FALSE(decode_write_request(frame.data(), frame.size(), 7, out_addr, out_words, out_word_count, txn)); + REQUIRE(out_addr.clause == MdioClause::Clause45); + REQUIRE(out_addr.prtad == 4); + REQUIRE(out_addr.devad == 2); + REQUIRE(out_addr.regad == 0x0010); + REQUIRE(out_word_count == 1); + REQUIRE(unpack_word_at(out_words.data(), 0) == 0x9999); + REQUIRE(txn == 3); +} + +TEST_CASE("write request round-trips a burst", "[mdio][REQ-MDIO-016][REQ-MDIO-017]") { + const uint16_t words[4] = {1, 2, 3, 4}; + const auto frame = encode_write_request(5, clause22_addr(1, 0), words, 4, 0); + + MdioAddr out_addr; + std::vector out_words; + size_t out_word_count = 0; + uint8_t txn = 0; + REQUIRE_FALSE(decode_write_request(frame.data(), frame.size(), 5, out_addr, out_words, out_word_count, txn)); + REQUIRE(out_word_count == 4); + for (size_t i = 0; i < 4; ++i) REQUIRE(unpack_word_at(out_words.data(), i) == words[i]); +} + +TEST_CASE("encode_write_request rejects an invalid address", "[mdio][REQ-MDIO-016]") { + const uint16_t word = 1; + REQUIRE(encode_write_request(1, clause22_addr(0, 0x20), &word, 1, 0).empty()); +} +TEST_CASE("encode_write_request rejects word_count 0 or above kMaxBurstWords", "[mdio][REQ-MDIO-016]") { + REQUIRE(encode_write_request(1, clause22_addr(0, 0), nullptr, 0, 0).empty()); + REQUIRE(encode_write_request(1, clause22_addr(0, 0), nullptr, kMaxBurstWords + 1, 0).empty()); +} + +TEST_CASE("decode_write_request rejects the wrong bus", "[mdio][REQ-MDIO-017]") { + const uint16_t word = 1; + const auto frame = encode_write_request(4, clause22_addr(0, 0), &word, 1, 0); + + MdioAddr out_addr; + std::vector out_words; + size_t out_word_count; + uint8_t txn; + REQUIRE(decode_write_request(frame.data(), frame.size(), 5, out_addr, out_words, out_word_count, txn) == + make_error_code(MdioErrc::wrong_bus)); +} + +TEST_CASE("decode_write_request rejects a read-op frame", "[mdio][REQ-MDIO-017]") { + rcp::acf::AcfMessageInfo hdr; + hdr.byte_bus_id = 2; + hdr.op = false; // not a write request + const auto frame = rcp::acf::encode_acf_abb(hdr, {}); + + MdioAddr out_addr; + std::vector out_words; + size_t out_word_count; + uint8_t txn; + REQUIRE(decode_write_request(frame.data(), frame.size(), 2, out_addr, out_words, out_word_count, txn) == + make_error_code(MdioErrc::wrong_op)); +} + +TEST_CASE("decode_write_request rejects a nonzero evt[2:0]", "[mdio][REQ-MDIO-017]") { + rcp::acf::AcfMessageInfo hdr; + hdr.byte_bus_id = 2; + hdr.op = true; + hdr.evt_op = 0x1; + const auto frame = rcp::acf::encode_acf_abb(hdr, {}); + + MdioAddr out_addr; + std::vector out_words; + size_t out_word_count; + uint8_t txn; + REQUIRE(decode_write_request(frame.data(), frame.size(), 2, out_addr, out_words, out_word_count, txn) == + make_error_code(MdioErrc::bad_evt)); +} + +TEST_CASE("decode_write_request rejects a frame too short for the mode+address prefix", + "[mdio][REQ-MDIO-017]") { + rcp::acf::AcfMessageInfo hdr; + hdr.byte_bus_id = 2; + hdr.op = true; + const std::vector too_short{0, 0}; + const auto frame = rcp::acf::encode_acf_abb(hdr, too_short); + + MdioAddr out_addr; + std::vector out_words; + size_t out_word_count; + uint8_t txn; + REQUIRE(decode_write_request(frame.data(), frame.size(), 2, out_addr, out_words, out_word_count, txn) == + make_error_code(MdioErrc::short_frame)); +} + +TEST_CASE("decode_write_request rejects an invalid decoded address", "[mdio][REQ-MDIO-017]") { + std::vector payload(1 + 5 + 2, 0); + payload[0] = static_cast(MdioMode::MmdSingleWord); + payload[3] = 1; // devad -- invalid for Clause-22 + + rcp::acf::AcfMessageInfo hdr; + hdr.byte_bus_id = 2; + hdr.op = true; + const auto frame = rcp::acf::encode_acf_abb(hdr, payload); + + MdioAddr out_addr; + std::vector out_words; + size_t out_word_count; + uint8_t txn; + REQUIRE(decode_write_request(frame.data(), frame.size(), 2, out_addr, out_words, out_word_count, txn) == + make_error_code(MdioErrc::bad_addr)); +} + +TEST_CASE("decode_write_request rejects zero words after the address prefix", "[mdio][REQ-MDIO-017]") { + std::vector payload(1 + 5, 0); + payload[0] = static_cast(MdioMode::MmdSingleWord); + + rcp::acf::AcfMessageInfo hdr; + hdr.byte_bus_id = 2; + hdr.op = true; + const auto frame = rcp::acf::encode_acf_abb(hdr, payload); + + MdioAddr out_addr; + std::vector out_words; + size_t out_word_count; + uint8_t txn; + REQUIRE(decode_write_request(frame.data(), frame.size(), 2, out_addr, out_words, out_word_count, txn) == + make_error_code(MdioErrc::bad_word_count)); +} + +TEST_CASE("decode_write_request rejects a word_count above kMaxBurstWords", "[mdio][REQ-MDIO-017]") { + std::vector payload(1 + 5 + 2 * (kMaxBurstWords + 1), 0); + payload[0] = static_cast(MdioMode::MmdMultiWord); + + rcp::acf::AcfMessageInfo hdr; + hdr.byte_bus_id = 2; + hdr.op = true; + const auto frame = rcp::acf::encode_acf_abb(hdr, payload); + + MdioAddr out_addr; + std::vector out_words; + size_t out_word_count; + uint8_t txn; + REQUIRE(decode_write_request(frame.data(), frame.size(), 2, out_addr, out_words, out_word_count, txn) == + make_error_code(MdioErrc::bad_word_count)); +} + +// ── Write response round trip (MMD family), REQ-MDIO-018/019/027/028 ──────── + +TEST_CASE("write response round-trips untimed", "[mdio][REQ-MDIO-018][REQ-MDIO-019]") { + const uint16_t accepted[2] = {0xAAAA, 0xBBBB}; + const auto frame = encode_write_response(3, accepted, 2, 6, false, 0); + REQUIRE_FALSE(frame.empty()); + + std::vector out_words; + size_t out_word_count = 0; + bool timed = true; + uint64_t ts = 1; + uint8_t txn = 0; + REQUIRE_FALSE(decode_write_response(frame.data(), frame.size(), 3, out_words, out_word_count, timed, ts, txn)); + REQUIRE(out_word_count == 2); + REQUIRE(unpack_word_at(out_words.data(), 0) == 0xAAAA); + REQUIRE(unpack_word_at(out_words.data(), 1) == 0xBBBB); + REQUIRE_FALSE(timed); + REQUIRE(txn == 6); +} + +TEST_CASE("write response round-trips timed", "[mdio][REQ-MDIO-027][REQ-MDIO-028]") { + const uint16_t accepted[1] = {0x1}; + const auto frame = encode_write_response(3, accepted, 1, 1, true, 55); + + std::vector out_words; + size_t out_word_count = 0; + bool timed = false; + uint64_t ts = 0; + uint8_t txn = 0; + REQUIRE_FALSE(decode_write_response(frame.data(), frame.size(), 3, out_words, out_word_count, timed, ts, txn)); + REQUIRE(timed); + REQUIRE(ts == 55); +} + +TEST_CASE("write response round-trips nothing accepted", "[mdio][REQ-MDIO-018][REQ-MDIO-019]") { + const auto frame = encode_write_response(3, nullptr, 0, 6, false, 0); + REQUIRE_FALSE(frame.empty()); + + std::vector out_words; + size_t out_word_count = 1; + bool timed = false; + uint64_t ts = 0; + uint8_t txn = 0; + REQUIRE_FALSE(decode_write_response(frame.data(), frame.size(), 3, out_words, out_word_count, timed, ts, txn)); + REQUIRE(out_word_count == 0); +} + +TEST_CASE("decode_write_response rejects the wrong bus", "[mdio][REQ-MDIO-019]") { + const uint16_t accepted[1] = {1}; + const auto frame = encode_write_response(3, accepted, 1, 0, false, 0); + std::vector out_words; + size_t out_word_count; + bool timed; + uint64_t ts; + uint8_t txn; + REQUIRE(decode_write_response(frame.data(), frame.size(), 9, out_words, out_word_count, timed, ts, txn) == + make_error_code(MdioErrc::wrong_bus)); +} + +TEST_CASE("decode_write_response rejects a short frame", "[mdio][REQ-MDIO-019]") { + const uint8_t too_short[3] = {static_cast(rcp::acf::kAcfMsgTypeAbb << 1), 0, 0}; + std::vector out_words; + size_t out_word_count; + bool timed; + uint64_t ts; + uint8_t txn; + REQUIRE(decode_write_response(too_short, sizeof(too_short), 2, out_words, out_word_count, timed, ts, txn) == + make_error_code(MdioErrc::short_frame)); +} + +// ── MMS read request/response, REQ-MDIO-022/024 ───────────────────────────── + +TEST_CASE("MMS read request round-trips a single 32-bit word (MMS0)", "[mdio][REQ-MDIO-022][REQ-MDIO-024]") { + const auto addr = mms_addr(0, 0xBEEF); // MMS0: 32-bit + const auto frame = encode_mms_read_request(0x10, addr, 1, 7); + REQUIRE_FALSE(frame.empty()); + + MdioMmsAddr out_addr; + size_t out_word_count = 0; + uint8_t txn = 0; + REQUIRE_FALSE(decode_mms_read_request(frame.data(), frame.size(), 0x10, out_addr, out_word_count, txn)); + REQUIRE(out_addr.mms == 0); + REQUIRE(out_addr.addr == 0xBEEF); + REQUIRE(out_word_count == 1); + REQUIRE(txn == 7); +} + +TEST_CASE("MMS read request round-trips a 16-bit burst", "[mdio][REQ-MDIO-022][REQ-MDIO-024]") { + const auto addr = mms_addr(3, 0x0100); // not 0/1: 16-bit + const auto frame = encode_mms_read_request(0x10, addr, 5, 9); + + MdioMmsAddr out_addr; + size_t out_word_count = 0; + uint8_t txn = 0; + REQUIRE_FALSE(decode_mms_read_request(frame.data(), frame.size(), 0x10, out_addr, out_word_count, txn)); + REQUIRE(out_addr.mms == 3); + REQUIRE(out_addr.addr == 0x0100); + REQUIRE(out_word_count == 5); +} + +TEST_CASE("encode_mms_read_request rejects an invalid address or bad word_count", + "[mdio][REQ-MDIO-024]") { + REQUIRE(encode_mms_read_request(0x10, mms_addr(static_cast(kMmsMax + 1), 0), 1, 1).empty()); + REQUIRE(encode_mms_read_request(0x10, mms_addr(0, 0), 0, 1).empty()); + REQUIRE(encode_mms_read_request(0x10, mms_addr(0, 0), kMaxBurstWords + 1, 1).empty()); +} + +TEST_CASE("decode_mms_read_request rejects the wrong bus", "[mdio][REQ-MDIO-024]") { + const auto frame = encode_mms_read_request(0x10, mms_addr(0, 0), 1, 1); + MdioMmsAddr out_addr; + size_t out_word_count; + uint8_t txn; + REQUIRE(decode_mms_read_request(frame.data(), frame.size(), 0x11, out_addr, out_word_count, txn) == + make_error_code(MdioErrc::wrong_bus)); +} + +TEST_CASE("decode_mms_read_request rejects an invalid decoded MMS address", "[mdio][REQ-MDIO-024]") { + std::vector payload(6, 0); + payload[0] = static_cast(MdioMode::MmsSingleWord); + payload[1] = static_cast(kMmsMax + 1); // out-of-range mms + payload[5] = 1; // word_count + + rcp::acf::AcfMessageInfo hdr; + hdr.byte_bus_id = 0x10; + hdr.op = false; + const auto frame = rcp::acf::encode_acf_abb(hdr, payload); + + MdioMmsAddr out_addr; + size_t out_word_count; + uint8_t txn; + REQUIRE(decode_mms_read_request(frame.data(), frame.size(), 0x10, out_addr, out_word_count, txn) == + make_error_code(MdioErrc::bad_mms_addr)); +} + +TEST_CASE("decode_mms_read_request rejects a zero word_count", "[mdio][REQ-MDIO-024]") { + std::vector payload(6, 0); + payload[0] = static_cast(MdioMode::MmsSingleWord); + + rcp::acf::AcfMessageInfo hdr; + hdr.byte_bus_id = 0x10; + hdr.op = false; + const auto frame = rcp::acf::encode_acf_abb(hdr, payload); + + MdioMmsAddr out_addr; + size_t out_word_count; + uint8_t txn; + REQUIRE(decode_mms_read_request(frame.data(), frame.size(), 0x10, out_addr, out_word_count, txn) == + make_error_code(MdioErrc::bad_word_count)); +} + +TEST_CASE("decode_mms_read_request rejects a word_count above kMaxBurstWords", "[mdio][REQ-MDIO-024]") { + std::vector payload(6, 0); + payload[0] = static_cast(MdioMode::MmsSingleWord); + payload[4] = static_cast((kMaxBurstWords + 1) >> 8); + payload[5] = static_cast((kMaxBurstWords + 1) & 0xFF); + + rcp::acf::AcfMessageInfo hdr; + hdr.byte_bus_id = 0x10; + hdr.op = false; + const auto frame = rcp::acf::encode_acf_abb(hdr, payload); + + MdioMmsAddr out_addr; + size_t out_word_count; + uint8_t txn; + REQUIRE(decode_mms_read_request(frame.data(), frame.size(), 0x10, out_addr, out_word_count, txn) == + make_error_code(MdioErrc::bad_word_count)); +} + +TEST_CASE("decode_mms_read_request rejects an MMD-mode frame", "[mdio][REQ-MDIO-021][REQ-MDIO-024]") { + std::vector payload(6, 0); + payload[0] = static_cast(MdioMode::MmdSingleWord); + payload[5] = 1; + + rcp::acf::AcfMessageInfo hdr; + hdr.byte_bus_id = 0x10; + hdr.op = false; + const auto frame = rcp::acf::encode_acf_abb(hdr, payload); + + MdioMmsAddr out_addr; + size_t out_word_count; + uint8_t txn; + REQUIRE(decode_mms_read_request(frame.data(), frame.size(), 0x10, out_addr, out_word_count, txn) == + make_error_code(MdioErrc::wrong_mdio_mode)); +} + +TEST_CASE("MMS read response round-trips untimed at 32-bit width", "[mdio][REQ-MDIO-022][REQ-MDIO-024]") { + const uint32_t words[1] = {0xCAFEBABEu}; + const auto frame = encode_mms_read_response(0x10, 0, words, 1, 3, false, 0); + REQUIRE_FALSE(frame.empty()); + + std::vector out_words; + size_t out_word_count; + bool timed; + uint64_t ts; + uint8_t txn; + REQUIRE_FALSE(decode_mms_read_response(frame.data(), frame.size(), 0x10, 0, out_words, out_word_count, timed, ts, txn)); + REQUIRE(out_word_count == 1); + REQUIRE_FALSE(timed); + REQUIRE(txn == 3); + REQUIRE(mms_unpack_word_at(0, out_words.data(), 0) == 0xCAFEBABEu); +} + +TEST_CASE("MMS read response round-trips timed at 16-bit width", "[mdio][REQ-MDIO-022][REQ-MDIO-024]") { + const uint32_t words[1] = {0x4242}; + const auto frame = encode_mms_read_response(0x10, 5, words, 1, 4, true, 999); + + std::vector out_words; + size_t out_word_count; + bool timed; + uint64_t ts; + uint8_t txn; + REQUIRE_FALSE(decode_mms_read_response(frame.data(), frame.size(), 0x10, 5, out_words, out_word_count, timed, ts, txn)); + REQUIRE(timed); + REQUIRE(ts == 999); + REQUIRE(mms_unpack_word_at(5, out_words.data(), 0) == 0x4242u); +} + +TEST_CASE("MMS read response round-trips an empty word set", "[mdio][REQ-MDIO-022][REQ-MDIO-024]") { + const auto frame = encode_mms_read_response(0x10, 0, nullptr, 0, 3, false, 0); + REQUIRE_FALSE(frame.empty()); + + std::vector out_words; + size_t out_word_count; + bool timed; + uint64_t ts; + uint8_t txn; + REQUIRE_FALSE(decode_mms_read_response(frame.data(), frame.size(), 0x10, 0, out_words, out_word_count, timed, ts, txn)); + REQUIRE(out_word_count == 0); +} + +TEST_CASE("decode_mms_read_response rejects the wrong bus", "[mdio][REQ-MDIO-024]") { + const uint32_t words[1] = {1}; + const auto frame = encode_mms_read_response(0x10, 0, words, 1, 1, false, 0); + std::vector out_words; + size_t out_word_count; + bool timed; + uint64_t ts; + uint8_t txn; + REQUIRE(decode_mms_read_response(frame.data(), frame.size(), 0x11, 0, out_words, out_word_count, timed, ts, txn) == + make_error_code(MdioErrc::wrong_bus)); +} + +TEST_CASE("decode_mms_read_response rejects a byte length not a multiple of the 32-bit width", + "[mdio][REQ-MDIO-022]") { + rcp::acf::AcfMessageInfo hdr; + hdr.byte_bus_id = 0x10; + hdr.op = false; + hdr.rsp = true; + const std::vector odd_payload{0x01, 0x02, 0x03}; + const auto frame = rcp::acf::encode_acf_abb(hdr, odd_payload); + + std::vector out_words; + size_t out_word_count; + bool timed; + uint64_t ts; + uint8_t txn; + REQUIRE(decode_mms_read_response(frame.data(), frame.size(), 0x10, 0 /* MMS0: 32-bit */, out_words, + out_word_count, timed, ts, txn) == make_error_code(MdioErrc::bad_word_count)); +} + +// ── MMS write request/response, REQ-MDIO-022/024 ──────────────────────────── + +TEST_CASE("MMS write request round-trips a single 32-bit word (MMS1)", "[mdio][REQ-MDIO-022][REQ-MDIO-024]") { + const auto addr = mms_addr(1, 0x0010); // MMS1: 32-bit + const uint32_t words[1] = {0x11223344u}; + const auto frame = encode_mms_write_request(0x10, addr, words, 1, 6); + REQUIRE_FALSE(frame.empty()); + + MdioMmsAddr out_addr; + std::vector out_words; + size_t out_word_count = 0; + uint8_t txn = 0; + REQUIRE_FALSE(decode_mms_write_request(frame.data(), frame.size(), 0x10, out_addr, out_words, out_word_count, txn)); + REQUIRE(out_addr.mms == 1); + REQUIRE(out_addr.addr == 0x0010); + REQUIRE(out_word_count == 1); + REQUIRE(mms_unpack_word_at(1, out_words.data(), 0) == 0x11223344u); +} + +TEST_CASE("MMS write request round-trips a 16-bit burst", "[mdio][REQ-MDIO-022][REQ-MDIO-024]") { + const auto addr = mms_addr(6, 0x0200); // not 0/1: 16-bit + const uint32_t words[3] = {0x1111, 0x2222, 0x3333}; + const auto frame = encode_mms_write_request(0x10, addr, words, 3, 8); + + MdioMmsAddr out_addr; + std::vector out_words; + size_t out_word_count = 0; + uint8_t txn = 0; + REQUIRE_FALSE(decode_mms_write_request(frame.data(), frame.size(), 0x10, out_addr, out_words, out_word_count, txn)); + REQUIRE(out_word_count == 3); + REQUIRE(mms_unpack_word_at(6, out_words.data(), 1) == 0x2222u); +} + +TEST_CASE("encode_mms_write_request rejects an invalid address or bad word_count", + "[mdio][REQ-MDIO-024]") { + const uint32_t word = 1; + REQUIRE(encode_mms_write_request(0x10, mms_addr(static_cast(kMmsMax + 1), 0), &word, 1, 1).empty()); + REQUIRE(encode_mms_write_request(0x10, mms_addr(0, 0), nullptr, 0, 1).empty()); + REQUIRE(encode_mms_write_request(0x10, mms_addr(0, 0), nullptr, kMaxBurstWords + 1, 1).empty()); +} + +TEST_CASE("decode_mms_write_request rejects a read-op frame", "[mdio][REQ-MDIO-024]") { + const auto frame = encode_mms_read_request(0x10, mms_addr(0, 0), 1, 1); + + MdioMmsAddr out_addr; + std::vector out_words; + size_t out_word_count; + uint8_t txn; + REQUIRE(decode_mms_write_request(frame.data(), frame.size(), 0x10, out_addr, out_words, out_word_count, txn) == + make_error_code(MdioErrc::wrong_op)); +} + +TEST_CASE("decode_mms_write_request rejects zero words after the address prefix", "[mdio][REQ-MDIO-024]") { + std::vector payload(4, 0); + payload[0] = static_cast(MdioMode::MmsSingleWord); + + rcp::acf::AcfMessageInfo hdr; + hdr.byte_bus_id = 0x10; + hdr.op = true; + const auto frame = rcp::acf::encode_acf_abb(hdr, payload); + + MdioMmsAddr out_addr; + std::vector out_words; + size_t out_word_count; + uint8_t txn; + REQUIRE(decode_mms_write_request(frame.data(), frame.size(), 0x10, out_addr, out_words, out_word_count, txn) == + make_error_code(MdioErrc::bad_word_count)); +} + +TEST_CASE("decode_mms_write_request rejects a word_count above kMaxBurstWords at 16-bit width", + "[mdio][REQ-MDIO-024]") { + std::vector payload(1 + 3 + 2 * (kMaxBurstWords + 1), 0); + payload[0] = static_cast(MdioMode::MmsMultiWord); + payload[1] = 2; // mms = 2 -> 16-bit words + + rcp::acf::AcfMessageInfo hdr; + hdr.byte_bus_id = 0x10; + hdr.op = true; + const auto frame = rcp::acf::encode_acf_abb(hdr, payload); + + MdioMmsAddr out_addr; + std::vector out_words; + size_t out_word_count; + uint8_t txn; + REQUIRE(decode_mms_write_request(frame.data(), frame.size(), 0x10, out_addr, out_words, out_word_count, txn) == + make_error_code(MdioErrc::bad_word_count)); +} + +TEST_CASE("decode_mms_write_request rejects an MMD-mode frame", "[mdio][REQ-MDIO-021][REQ-MDIO-024]") { + std::vector payload(6, 0); + payload[0] = static_cast(MdioMode::MmdSingleWord); + + rcp::acf::AcfMessageInfo hdr; + hdr.byte_bus_id = 0x10; + hdr.op = true; + const auto frame = rcp::acf::encode_acf_abb(hdr, payload); + + MdioMmsAddr out_addr; + std::vector out_words; + size_t out_word_count; + uint8_t txn; + REQUIRE(decode_mms_write_request(frame.data(), frame.size(), 0x10, out_addr, out_words, out_word_count, txn) == + make_error_code(MdioErrc::wrong_mdio_mode)); +} + +TEST_CASE("MMS write response round-trips", "[mdio][REQ-MDIO-022][REQ-MDIO-024]") { + const uint32_t accepted[1] = {0xAAAA}; + const auto frame = encode_mms_write_response(0x10, 2, accepted, 1, 5, false, 0); + REQUIRE_FALSE(frame.empty()); + + std::vector out_words; + size_t out_word_count; + bool timed; + uint64_t ts; + uint8_t txn; + REQUIRE_FALSE(decode_mms_write_response(frame.data(), frame.size(), 0x10, 2, out_words, out_word_count, timed, ts, txn)); + REQUIRE(out_word_count == 1); + REQUIRE(mms_unpack_word_at(2, out_words.data(), 0) == 0xAAAAu); +} + +TEST_CASE("MMS write response round-trips nothing accepted", "[mdio][REQ-MDIO-022][REQ-MDIO-024]") { + const auto frame = encode_mms_write_response(0x10, 0, nullptr, 0, 5, false, 0); + REQUIRE_FALSE(frame.empty()); + + std::vector out_words; + size_t out_word_count; + bool timed; + uint64_t ts; + uint8_t txn; + REQUIRE_FALSE(decode_mms_write_response(frame.data(), frame.size(), 0x10, 0, out_words, out_word_count, timed, ts, txn)); + REQUIRE(out_word_count == 0); +} + +// ── MdioEndpoint convenience wrapper (source-compatible with rcp/mock.hpp) ── TEST_CASE("payload_width_bits is 16 for both MMD sub-modes regardless of mms_is_0_or_1", "[mdio][REQ-MDIO-002]") { @@ -49,9 +1313,9 @@ TEST_CASE("validate_request accepts a payload within its mode's width", "[mdio][ req.mdio_payload = 0xFFFF; REQUIRE_FALSE(validate_request(req)); - req.mode = MdioMode::MmsMultiWord; - req.mms_is_0_or_1 = true; - req.mdio_payload = 0xFFFFFFFFu; + req.mode = MdioMode::MmsMultiWord; + req.mms_is_0_or_1 = true; + req.mdio_payload = 0xFFFFFFFFu; REQUIRE_FALSE(validate_request(req)); } @@ -68,12 +1332,9 @@ TEST_CASE("validate_request rejects a payload exceeding its mode's width", "[mdi REQUIRE(validate_request(req2) == make_error_code(MdioErrc::payload_exceeds_mode_width)); } -// ── Register write-then-read round-trip, keyed on (mode, mdio_address) ────── - -TEST_CASE("MdioEndpoint::transact write then read round-trips the value", - "[mdio][REQ-MDIO-003]") { +TEST_CASE("MdioEndpoint::transact write then read round-trips the value", "[mdio][REQ-MDIO-003]") { MdioEndpoint ep; - MdioRequest write_req; + MdioRequest write_req; write_req.mode = MdioMode::MmdSingleWord; write_req.mdio_address = 5; write_req.is_write = true; @@ -92,7 +1353,7 @@ TEST_CASE("MdioEndpoint::transact write then read round-trips the value", TEST_CASE("MdioEndpoint::transact reads an unwritten register as zero", "[mdio][REQ-MDIO-003]") { MdioEndpoint ep; - MdioRequest req; + MdioRequest req; req.mode = MdioMode::MmsSingleWord; req.mdio_address = 1; req.is_write = false; @@ -102,8 +1363,6 @@ TEST_CASE("MdioEndpoint::transact reads an unwritten register as zero", "[mdio][ REQUIRE(out.mdio_payload == 0); } -// ── mdio_mode keeps each mode's register space distinct ───────────────────── - TEST_CASE("MdioEndpoint::transact keys on mdio_mode in addition to mdio_address, " "so different modes at the same address never collide", "[mdio][REQ-MDIO-004]") { @@ -117,8 +1376,6 @@ TEST_CASE("MdioEndpoint::transact keys on mdio_mode in addition to mdio_address, MdioResponse out; REQUIRE_FALSE(ep.transact(mmd_write, out)); - // Same mdio_address, but MmsSingleWord mode — must not read back the - // MMD write above. MdioRequest mms_read; mms_read.mode = MdioMode::MmsSingleWord; mms_read.mdio_address = 4; @@ -132,40 +1389,22 @@ TEST_CASE("MdioEndpoint::transact keys on mdio_mode in addition to mdio_address, REQUIRE_FALSE(ep.transact(mms_write, out)); REQUIRE(out.mdio_payload == 0x2222); - // The earlier MMD write is unaffected. MdioRequest mmd_read = mmd_write; mmd_read.is_write = false; REQUIRE_FALSE(ep.transact(mmd_read, out)); REQUIRE(out.mdio_payload == 0x1111); } -TEST_CASE("MdioEndpoint::transact fires TransferComplete on every request", "[mdio][REQ-MDIO-004]") { - MdioEndpoint ep; - ep.triggers().enable(mdio_signal_id(MdioSignal::TransferComplete)); - - MdioRequest req; - MdioResponse out; - REQUIRE_FALSE(ep.transact(req, out)); - - auto drained = ep.triggers().drain(); - REQUIRE(drained.size() == 1); - REQUIRE(drained[0] == mdio_signal_id(MdioSignal::TransferComplete)); -} - -// ── MdioErrc category sanity ─────────────────────────────────────────────────── - TEST_CASE("MdioErrc reports a non-empty message in its own category", "[mdio][REQ-MDIO-005]") { auto ec = make_error_code(MdioErrc::payload_exceeds_mode_width); REQUIRE(ec.category() == mdio_category()); REQUIRE_FALSE(ec.message().empty()); } -// ── Table 33 Row 2 evt[2:0] validation (handle_request) ───────────────────── - TEST_CASE("MdioEndpoint::handle_request delegates a Plain (evt[2:0]==000b) request to transact()", "[mdio][REQ-MDIO-006]") { MdioEndpoint ep; - MdioRequest req; + MdioRequest req; req.mode = MdioMode::MmdSingleWord; req.mdio_address = 5; req.is_write = true; @@ -183,7 +1422,7 @@ TEST_CASE("MdioEndpoint::handle_request rejects every reserved evt[2:0] value (0 "[mdio][REQ-MDIO-006]") { for (uint8_t evt_op = 1; evt_op <= 6; ++evt_op) { MdioEndpoint ep; - MdioRequest req; + MdioRequest req; req.mode = MdioMode::MmdSingleWord; req.mdio_address = 9; req.is_write = true; @@ -192,15 +1431,8 @@ TEST_CASE("MdioEndpoint::handle_request rejects every reserved evt[2:0] value (0 MdioResponse out; auto ec = ep.handle_request(evt_op, req, out); REQUIRE(ec == rcp::endpoint::make_error_code(rcp::endpoint::EndpointErrc::reserved_evt_row2)); - // A rejected reserved evt must not record anything — last_request() - // stays at its default-constructed value, no register is written, - // and no trigger fires. REQUIRE(ep.last_request().mdio_address == 0); - REQUIRE_FALSE(ep.triggers().has_pending()); - // The register that would have been written must genuinely be - // untouched: a subsequent Plain read at the same address must not - // observe req.mdio_payload above. MdioRequest read_back = req; read_back.is_write = false; read_back.mdio_payload = 0; @@ -213,7 +1445,7 @@ TEST_CASE("MdioEndpoint::handle_request reports config_write_not_supported for e "without crashing or recording anything", "[mdio][REQ-MDIO-007]") { MdioEndpoint ep; - MdioRequest req; + MdioRequest req; req.mode = MdioMode::MmsSingleWord; req.mdio_address = 2; req.is_write = true; @@ -223,13 +1455,12 @@ TEST_CASE("MdioEndpoint::handle_request reports config_write_not_supported for e auto ec = ep.handle_request(/*evt_op=*/7, req, out); REQUIRE(ec == make_error_code(MdioErrc::config_write_not_supported)); REQUIRE(ep.last_request().mdio_address == 0); - REQUIRE_FALSE(ep.triggers().has_pending()); } TEST_CASE("MdioEndpoint::handle_request masks evt_op down to 3 bits before classifying", "[mdio][REQ-MDIO-006]") { MdioEndpoint ep; - MdioRequest req; + MdioRequest req; MdioResponse out; REQUIRE_FALSE(ep.handle_request(/*evt_op=*/0xF8, req, out)); // low 3 bits 000 -> Plain auto ec = ep.handle_request(/*evt_op=*/0xF9, req, out); // low 3 bits 001 -> Reserved @@ -239,12 +1470,8 @@ TEST_CASE("MdioEndpoint::handle_request masks evt_op down to 3 bits before class TEST_CASE("MdioEndpoint::handle_request Reserved/ConfigWrite classification is independent of " "mode/mdio_address/mdio_payload — evt[2:0] carries no field-value selector", "[mdio][REQ-MDIO-006]") { - // Guards against confusing Table 33's evt[2:0] classification with the - // request's own mode/mdio_address/mdio_payload fields (see - // handle_request's own header comment): a Reserved/ConfigWrite evt is - // rejected identically no matter what those fields carry. MdioEndpoint ep; - MdioRequest req; + MdioRequest req; req.mode = MdioMode::MmsMultiWord; req.mms_is_0_or_1 = true; req.mdio_address = 0xFFFF; @@ -254,8 +1481,7 @@ TEST_CASE("MdioEndpoint::handle_request Reserved/ConfigWrite classification is i MdioResponse out; REQUIRE(ep.handle_request(/*evt_op=*/3, req, out) == rcp::endpoint::make_error_code(rcp::endpoint::EndpointErrc::reserved_evt_row2)); - REQUIRE(ep.handle_request(/*evt_op=*/7, req, out) == - make_error_code(MdioErrc::config_write_not_supported)); + REQUIRE(ep.handle_request(/*evt_op=*/7, req, out) == make_error_code(MdioErrc::config_write_not_supported)); } TEST_CASE("MdioErrc::config_write_not_supported reports a non-empty message in its own category",