diff --git a/include/rcp/mock.hpp b/include/rcp/mock.hpp index bf080ca..986eb8c 100644 --- a/include/rcp/mock.hpp +++ b/include/rcp/mock.hpp @@ -183,7 +183,7 @@ inline acf::WireErrorCode wire_error_code_for(const std::error_code& ec) noexcep if (ec == make_error_code(regmap::RegMapErrc::invalid_parameter)) return acf::WireErrorCode::InvalidParameter; if (ec == avtp::make_error_code(avtp::AvtpErrc::short_buffer)) return acf::WireErrorCode::InvalidParameter; // e.g. GPIO/SPI payload not exactly the required length if (ec == make_error_code(gpio::GpioErrc::pin_index_out_of_range)) return acf::WireErrorCode::InvalidParameter; - if (ec == make_error_code(spi::SpiErrc::channel_out_of_range)) return acf::WireErrorCode::UnsupportedCmd; // reserved evt[2:0] selection, extraction §5.4/Table entries at 110b + if (ec == make_error_code(spi::SpiErrc::bad_channel)) return acf::WireErrorCode::UnsupportedCmd; // reserved evt[2:0] selection, extraction §5.4/Table entries at 110b if (ec == endpoint::make_error_code(endpoint::EndpointErrc::reserved_evt_row2)) return acf::WireErrorCode::UnsupportedCmd; // Table 33 Row 2 evt[2:0] in 001b-110b, extraction §13.5 if (ec == make_error_code(i2c::I2cErrc::config_write_not_supported)) diff --git a/include/rcp/spi.hpp b/include/rcp/spi.hpp index 7f6bcdc..2b883c0 100644 --- a/include/rcp/spi.hpp +++ b/include/rcp/spi.hpp @@ -3,39 +3,159 @@ // fusa:req REQ-SPI-003 // fusa:req REQ-SPI-004 // fusa:req REQ-SPI-005 +// fusa:req REQ-SPI-006 +// fusa:req REQ-SPI-007 +// fusa:req REQ-SPI-008 +// fusa:req REQ-SPI-009 +// fusa:req REQ-SPI-010 +// fusa:req REQ-SPI-011 +// fusa:req REQ-SPI-012 +// fusa:req REQ-SPI-013 +// fusa:req REQ-SPI-014 +// fusa:req REQ-SPI-015 +// fusa:req REQ-SPI-016 +// fusa:req REQ-SPI-017 +// fusa:req REQ-SPI-018 +// fusa:req REQ-SPI-019 +// fusa:req REQ-SPI-020 +// fusa:req REQ-SPI-021 +// fusa:req REQ-SPI-022 +// fusa:req REQ-SPI-023 +// fusa:req REQ-SPI-024 +// fusa:req REQ-SPI-025 +// fusa:req REQ-SPI-026 +// fusa:req REQ-SPI-027 +// fusa:req REQ-SPI-028 +// fusa:req REQ-SPI-029 +// fusa:req REQ-SPI-030 +// fusa:req REQ-SPI-033 +// fusa:req REQ-SPI-034 +// fusa:req REQ-SPI-035 +// fusa:req REQ-SPI-036 +// fusa:req REQ-SPI-037 +// fusa:req REQ-SPI-038 +// fusa:req REQ-SPI-039 +// fusa:req REQ-SPI-040 +// fusa:req REQ-SPI-041 +// fusa:req REQ-SPI-042 +// fusa:req REQ-SPI-043 +// fusa:req REQ-SPI-044 -// SPI endpoint (ep_type 0x03) — up to 6 pre-configured channels selected by -// evt[2:0], raw full-duplex PICO-out/POCI-in byte transfer, the -// compound-wait status-byte truncation rule, and transfer-complete/per-CS -// assert/de-assert trigger signals (extraction §5.4, §4.5 Group A). +// SPI endpoint (ep_type 0x03) — a controller-only endpoint addressing up to +// kMaxChannels (6) independently pre-configured channels, selected via the +// ACF byte_message_info header's evt[2:0] field directly as a channel +// number (0..5; 6/7 select no defined channel and are rejected), a +// full-duplex byte-for-byte transfer request/response pair, per-channel +// functional configuration, and the EP_func addressed-configuration-write +// register block (evt[2:0] == 111b, §12.7.1) (extraction §5.4, §13.5 +// Table 33, §13.7.3). // -// ROADMAP.md milestone 47, "Basic Endpoint Types I — GPIO & SPI (v2.3.0)": -// SPI is the second endpoint type built on rcp/endpoint.hpp's shared -// trigger-signal table, following the request-dispatch pattern -// rcp/gpio.hpp establishes (SpiEndpoint mirrors GpioEndpoint's shape). -// Unlike GPIO, SPI does *not* use endpoint::WriteSemantics at all — it -// repurposes the same evt[2:0] field as a 0-5 channel selector instead -// (extraction §5.4). The compound-wait truncation rule implemented here -// (compound_wait_matches) is forward-looking scaffolding: the surrounding -// compound-wait request kind itself (sequencer gating, the `cs` field's -// conditional-start meaning) is v2.5.0 scope — this milestone only -// implements the byte-comparison rule the roadmap calls out as belonging -// here. +// Phase 3 rewrite (cpp-RCP issue #129, ROADMAP.md "Phase 17"), re-derived +// (not incrementally patched) from c-RCP's include/rcp/ep_spi.h + +// src/ep_spi.c — this project's RC5-spec-conformant reference for this +// module. This header's own pre-rewrite content was a suspiciously thin +// evt[2:0]-channel-decode-plus-a-wrong-compound-wait-rule pilot (kMaxChannels, +// channel_of, SpiEndpoint::transfer, and the trigger-signal scaffolding); the +// per-channel functional config, the EP_func register block, the real +// ACF-level wire codec (transfer request/response encode+decode), and +// Table 41's own trigger-signal numbering were entirely missing and are +// ported below for the first time. +// +// ── Channel selection is evt-bits only, NOT byte_bus_id/Table 26 ─────────── +// c-RCP's ep_spi.h carries an extensive "INVESTIGATED 2026-08-11" file-header +// note (c-RCP-AUDIT-06, task #98) confirming that evt[2:0] (0-5 = channel, +// 6/7 = reserved/invalid) remains the current, conformant channel-selection +// mechanism: TC18 §13.5's own authoritative per-endpoint-type table's SPI row +// is unchanged ("selects channel 0...5"), and a competing BBID-based Table 26 +// "Channel_selection[3:0]" proposal elsewhere in the 0.5.1_RC5 draft is +// explicitly conditional on that row changing, which as of RC5 has not +// happened. This module's channel_of()/decode_transfer_request()/ +// decode_response() below all decode evt[2:0] directly as the channel +// number — there is no BBID-based channel-selection path in this codebase, +// and none should be added absent a future spec revision resolving that +// still-open proposal. +// +// ── spi_nr_cs is a 4-bit "(count-1)" register field, not a plain 8-bit +// count ───────────────────────────────────────────────────────────────────── +// c-RCP's ep_spi.h/.c carry a "FIXED 2026-08-11" note (spec rebaseline to +// TC18 0.5.1_RC5): the EP_func block's spi_nr_cs register (relative offset +// 0x0001) was originally read against the 0.5.1_RC baseline as a plain 8-bit +// channel count; the RC 4 revision narrowed it to a 4-bit "(count - 1)" field in bits +// [3:0], upper nibble reserved (reads 0000b). render_registers() below +// renders (kMaxChannels - 1) & 0x0F (0x05), not a plain 6, in the low +// nibble, and leaves the high nibble 0 — kRegNrCs is read-only, exactly as +// the specification's own R marker requires (a configuration write covering +// it is silently ignored, see apply_reconfig()/detail::reg_offset_read_only +// below). +// +// ── spi_deassert_cs_pauseN (RC5, ticket NXP_100) ──────────────────────────── +// Bit 4 of each channel's own +0x02 cfg octet, new in TC18 spec revision +// 0.5.1_RC5 with no counterpart at all in the baseline this module was +// originally built against: "0b: no de-assertion during break / 1b: +// de-assertion during break" during the pause window +// spi_cs_clk_leadtimeN/spi_pause_minN/spi_clk_cs_trailtimeN define. +// SpiChannelCfg::deassert_cs_pause is the new field this bit round-trips +// through (render_registers()/detail::parse_registers()), added following +// the same "new field, every pre-existing cfg bit left untouched" rule +// c-RCP's own file header documents for this fix. +// +// ── Compound-wait against an SPI endpoint: the wrong SPI-specific +// 4-byte-truncation rule is REMOVED, not ported ───────────────────────────── +// c-RCP's ep_spi.h file header records that v0.111.0 removed +// rcp_ep_spi_compound_wait_status_equal()/RCP_EP_SPI_COMPOUND_WAIT_COMPARE_LEN: +// both modeled the compound-wait comparison length as an SPI-specific, +// hardcoded 4-byte truncation. That was wrong — TC18 §13.5.1's own length +// rule (status is capped to byte_msg_payload's own length, whatever that +// request happens to carry) is universal across every endpoint type, and the +// specification's own worked example (checking only the first four of 20 +// received bytes when byte_msg_payload has only four bytes) illustrates that +// general rule *using* SPI, rather than stating an SPI-specific rule of its +// own. This header's own pre-rewrite content had exactly that wrong, +// SPI-specific hardcoded-4 rule (compound_wait_matches()/ +// kCompoundWaitCompareLen) — both are deleted here, not merely deprecated. +// This codebase already has the correct, endpoint-type-independent +// primitive (rcp/acf.hpp's compound_wait_evt_valid()/compound_wait_match(), +// added during this rewrite's Phase 1, and already the mechanism +// rcp/i2c.hpp's own file header points callers at for the identical +// reason) — a caller evaluating a compound-wait request against this +// endpoint calls those two functions directly, exactly like every other +// endpoint type; there is nothing SPI-specific left for this header to +// provide here. kMaxStatusBytes below is unrelated (it bounds this endpoint +// type's own transfer-done status-report width, not compound-wait's +// comparison length). +// +// ── REQ-SPI-037: genuinely not implemented ────────────────────────────────── +// c-RCP's own .fusa-reqs.json records REQ-SPI-037 ("SPI error state resets +// the EP enable bit; a clamped pin flags every response") as +// NOT-IMPLEMENTED, blocked by real specification silence: TC18 §13.7.3.3 +// gestures at "cs/hs bits" indicating stopped execution without ever +// defining what that means for SPI, and the two plausible readings (the ACF +// header's own cs/hs bits, vs. the SPI bus's own physical CS/HS hardware +// signal lines) lead to entirely different implementations. This is a +// genuine spec defect (c-RCP: TC18_spec_defects_report.md item 56), not a +// local implementation gap — nothing is ported for it here, matching c-RCP's +// own disposition, rather than guessing at a resolution. // // 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 channel-config -// encoding and trigger-signal id scheme chosen in this file are this -// implementation's own, same as the equivalent disclaimers in -// rcp/avtp.hpp, rcp/regmap.hpp, rcp/endpoint.hpp, and rcp/gpio.hpp. +// text from that document is reproduced here. The concrete +// SpiChannelCfg/SpiFunctionalCfg layout, SpiTrigger's collapsed 4-value +// simplification of Table 41's 14 fixed hardware signals, and bit_order +// (which — like `trigger` — has no TC18 register counterpart at all and is +// never rendered onto the wire) are this implementation's own, same as the +// equivalent disclaimers in rcp/avtp.hpp, rcp/endpoint.hpp, rcp/gpio.hpp, +// and rcp/i2c.hpp. #pragma once +#include +#include #include +#include -#include #include #include #include +#include #include #include #include @@ -43,17 +163,533 @@ namespace rcp { namespace spi { -// ── Channels ────────────────────────────────────────────────────────────────── -// SPI exposes up to 6 pre-configured channels, selected by the request's -// evt[2:0] field taking the value of the channel index directly — 0 through -// 5 are valid channels; 6 and 7 have no defined meaning (extraction §5.4). +// ── Channel addressing ──────────────────────────────────────────────────────── +// The largest number of pre-configured SPI channels this endpoint type +// addresses via evt[2:0] (extraction §5.4; TC18 §13.5 Table 33's own SPI +// row, "selects channel 0...5" — see the file header's own channel-selection +// note). constexpr uint8_t kMaxChannels = 6; +// True iff channel is a valid channel index (0..kMaxChannels-1) — one of the +// 6 evt[2:0] values this endpoint type actually assigns a channel to (values +// 6 and 7 select no defined channel). +constexpr bool channel_valid(uint8_t channel) noexcept { return channel < kMaxChannels; } + +// ── Clock mode: the 4 standard CPOL/CPHA combinations ───────────────────────── + +enum class SpiMode : uint8_t { + Mode0 = 0, // CPOL=0, CPHA=0 + Mode1 = 1, // CPOL=0, CPHA=1 + Mode2 = 2, // CPOL=1, CPHA=0 + Mode3 = 3, // CPOL=1, CPHA=1 +}; + +// True iff v (a raw clock-mode value, e.g. as decoded from a register) is +// one of the four defined modes. +constexpr bool mode_valid(uint8_t v) noexcept { return v <= static_cast(SpiMode::Mode3); } + +// The clock-polarity (CPOL) bit implied by mode: false for Mode0/Mode1, true +// for Mode2/Mode3. An invalid mode value is treated as CPOL false (fail-safe +// default — never fabricate a "true" safety-relevant bit for undefined +// input). +constexpr bool mode_cpol(SpiMode mode) noexcept { + switch (mode) { + case SpiMode::Mode2: + case SpiMode::Mode3: return true; + case SpiMode::Mode0: + case SpiMode::Mode1: + default: return false; + } +} + +// The clock-phase (CPHA) bit implied by mode: false for Mode0/Mode2, true +// for Mode1/Mode3. Same fail-safe treatment of an invalid mode value as +// mode_cpol(). +constexpr bool mode_cpha(SpiMode mode) noexcept { + switch (mode) { + case SpiMode::Mode1: + case SpiMode::Mode3: return true; + case SpiMode::Mode0: + case SpiMode::Mode2: + default: return false; + } +} + +// ── Bit order and chip-select active-polarity ───────────────────────────────── +// bit_order has no counterpart in TC18 §13.7.3.2's per-channel register block +// at all — this module's own original addition, never rendered onto the +// wire (see render_registers() below). + +enum class SpiBitOrder : uint8_t { + MsbFirst = 0, + LsbFirst = 1, +}; + +enum class SpiCsPolarity : uint8_t { + ActiveLow = 0, + ActiveHigh = 1, +}; + +// ── Per-channel trigger signals ──────────────────────────────────────────────── +// SpiTrigger names the three asynchronous-event trigger modes a channel's +// functional config may select (transfer-done, CS-assert-edge, +// CS-deassert-edge), plus None. This is this module's own deliberate +// collapse of TC18 §13.7.3.1 Table 41's 14 fixed, always-on, per-CS-channel +// hardware trigger signals (execution-done, plus an assert/de-assert pair +// for each of CS0 through CS5) into 4 generic values with no per-channel +// distinction — Table 42 (the per-channel functional-config register block) +// defines no register field that selects among them, so `trigger` is never +// rendered onto the wire, exactly like bit_order above. + +enum class SpiTrigger : uint8_t { + None = 0, + TransferDone = 1, + CsAssert = 2, + CsDeassert = 3, +}; + +// The three asynchronous events a channel's trigger mode may be evaluated +// against — see trigger_fires(). +enum class SpiEvent : uint8_t { + TransferDone = 0, + CsAssert = 1, + CsDeassert = 2, +}; + +// True iff event satisfies trigger: never for None; for TransferDone iff +// event == SpiEvent::TransferDone; for CsAssert iff event == +// SpiEvent::CsAssert; for CsDeassert iff event == SpiEvent::CsDeassert. +constexpr bool trigger_fires(SpiTrigger trigger, SpiEvent event) noexcept { + switch (trigger) { + case SpiTrigger::TransferDone: return event == SpiEvent::TransferDone; + case SpiTrigger::CsAssert: return event == SpiEvent::CsAssert; + case SpiTrigger::CsDeassert: return event == SpiEvent::CsDeassert; + case SpiTrigger::None: + default: return false; + } +} + +// REQ-SPI-034: TC18 §13.7.3.1's own Table 41 "spi trigger outputs" — signal 0 +// is "SPI execution done" (a whole-endpoint trigger, not modeled by this +// per-channel function), and for chip select CSn: signal 2+2n is CSn +// asserted, signal 3+2n is CSn de-asserted (0 <= n < 16 in Table 41's own +// wording, narrowed here to this module's own kMaxChannels (6), the same way +// rcp/gpio.hpp's trigger_signal_number() narrows Table 43's IOn range to +// kMaxPins). +// +// This is a pure numbering computation, entirely independent of SpiTrigger's +// own deliberately-collapsed, non-wire-rendered per-channel trigger mode +// above — it only lets a caller resolve a Table 41 signal number for a +// (channel, CS-edge) pair that names one. +// +// Returns the signal number iff channel < kMaxChannels and trigger is +// CsAssert or CsDeassert (never TransferDone, which is signal 0's +// whole-endpoint concept and has no per-channel Table 41 entry, nor None, +// which names no trigger event and therefore no signal number); std::nullopt +// otherwise. +inline std::optional trigger_signal_number(uint8_t channel, SpiTrigger trigger) noexcept { + if (channel >= kMaxChannels) return std::nullopt; + switch (trigger) { + case SpiTrigger::CsAssert: return static_cast(2u + 2u * channel); + case SpiTrigger::CsDeassert: return static_cast(3u + 2u * channel); + case SpiTrigger::TransferDone: // signal 0 is whole-endpoint, not per-channel + case SpiTrigger::None: + default: + return std::nullopt; + } +} + +// ── Functional config ───────────────────────────────────────────────────────── +// One channel's runtime-adjustable functional configuration. Timing delays +// (inter_byte_delay_ns/inter_transfer_delay_ns) are this module's own +// nanosecond-denominated original addition, distinct from the wire's +// spi_clk-cycle-denominated cs_clk_leadtime/clk_cs_trailtime/pause_min +// fields (Table 42) below. + +struct SpiChannelCfg { + SpiMode mode = SpiMode::Mode0; + SpiBitOrder bit_order = SpiBitOrder::MsbFirst; + SpiCsPolarity cs_polarity = SpiCsPolarity::ActiveLow; + SpiTrigger trigger = SpiTrigger::None; + uint32_t clock_divider = 0; + uint32_t inter_byte_delay_ns = 0; + uint32_t inter_transfer_delay_ns = 0; + uint16_t baud_rate_kbps = 0; // spi_baud_rateN, Table 42 + bool use_common_cs = false; // spi_use_csN: false = this channel's own + // CSN (the wire's 0b default), true = the + // common CS0 is used instead + uint8_t cs_clk_leadtime = 0; // spi_cs_clk_leadtimeN, spi_clk cycles + uint8_t clk_cs_trailtime = 0; // spi_clk_cs_trailtimeN, spi_clk cycles + uint8_t bits_max = 0; // spi_bits_maxN + uint8_t pause_min = 0; // spi_pause_minN, spi_clk cycles + bool deassert_cs_pause = false; // spi_deassert_cs_pauseN (TC18 + // 0.5.1_RC5, ticket NXP_100 — see + // the file header): false = no + // de-assertion during the pause + // (the wire's 0b default), true = + // CS is de-asserted during the + // pause window +}; + +struct SpiFunctionalCfg { + 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; + + std::array channels{}; + uint16_t ep_status = 0; // spi_ep_status, Table 42 +}; + +// Resets cfg to its zero/default state (every common flag false; every +// channel's mode Mode0, bit_order MsbFirst, cs_polarity ActiveLow, trigger +// None, every numeric timing/rate field 0, use_common_cs/deassert_cs_pause +// false; ep_status 0). +inline void functional_cfg_init(SpiFunctionalCfg& cfg) noexcept { cfg = SpiFunctionalCfg{}; } + +// 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); +} + +// Sets cfg.channels[channel].mode to mode iff channel is channel_valid() and +// functional_cfg_writable() authorizes the write for state/writer; returns +// whether the write was applied. cfg is left entirely unchanged when it +// returns false. +inline bool set_channel_mode(SpiFunctionalCfg& cfg, uint8_t channel, SpiMode mode, + lifecycle::ServerState state, lifecycle::WriterCtx writer) noexcept { + if (!channel_valid(channel)) return false; + if (!functional_cfg_writable(state, writer)) return false; + cfg.channels[channel].mode = mode; + return true; +} + +// Same authorization/validity rule as set_channel_mode(), for +// cfg.channels[channel].bit_order. +inline bool set_channel_bit_order(SpiFunctionalCfg& cfg, uint8_t channel, SpiBitOrder bit_order, + lifecycle::ServerState state, lifecycle::WriterCtx writer) noexcept { + if (!channel_valid(channel)) return false; + if (!functional_cfg_writable(state, writer)) return false; + cfg.channels[channel].bit_order = bit_order; + return true; +} + +// Same authorization/validity rule, for cfg.channels[channel].cs_polarity. +inline bool set_channel_cs_polarity(SpiFunctionalCfg& cfg, uint8_t channel, SpiCsPolarity cs_polarity, + lifecycle::ServerState state, lifecycle::WriterCtx writer) noexcept { + if (!channel_valid(channel)) return false; + if (!functional_cfg_writable(state, writer)) return false; + cfg.channels[channel].cs_polarity = cs_polarity; + return true; +} + +// Same authorization/validity rule, for cfg.channels[channel].clock_divider. +inline bool set_channel_clock_divider(SpiFunctionalCfg& cfg, uint8_t channel, uint32_t clock_divider, + lifecycle::ServerState state, lifecycle::WriterCtx writer) noexcept { + if (!channel_valid(channel)) return false; + if (!functional_cfg_writable(state, writer)) return false; + cfg.channels[channel].clock_divider = clock_divider; + return true; +} + +// Same authorization/validity rule, for cfg.channels[channel]'s +// inter_byte_delay_ns and inter_transfer_delay_ns together (one setter for +// both timing fields, since they are always reconfigured as a pair on the +// wire). +inline bool set_channel_timing(SpiFunctionalCfg& cfg, uint8_t channel, uint32_t inter_byte_delay_ns, + uint32_t inter_transfer_delay_ns, lifecycle::ServerState state, + lifecycle::WriterCtx writer) noexcept { + if (!channel_valid(channel)) return false; + if (!functional_cfg_writable(state, writer)) return false; + cfg.channels[channel].inter_byte_delay_ns = inter_byte_delay_ns; + cfg.channels[channel].inter_transfer_delay_ns = inter_transfer_delay_ns; + return true; +} + +// Same authorization/validity rule, for cfg.channels[channel].trigger. +inline bool set_channel_trigger(SpiFunctionalCfg& cfg, uint8_t channel, SpiTrigger trigger, + lifecycle::ServerState state, lifecycle::WriterCtx writer) noexcept { + if (!channel_valid(channel)) return false; + if (!functional_cfg_writable(state, writer)) return false; + cfg.channels[channel].trigger = trigger; + return true; +} + +// ── The EP_func register block (evt[2:0] == 111b) ──────────────────────────── +// Relative octet offsets of the registers making up the common (non- +// per-channel) prefix of an SPI endpoint's own EP_func block. Every +// multi-octet register is big-endian. Offsets marked R are read-only: a +// configuration write covering them leaves them unchanged (see +// apply_reconfig() below). + +constexpr uint16_t kRegEpLen = 0x0000; // 8 bit, R +constexpr uint16_t kRegNrCs = 0x0001; // 8 bit, R -- only bits [3:0] + // carry spi_nr_cs + // (count - 1); [7:4] + // reserved (TC18 0.5.1_RC5 — + // see the file header) +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 + +// Channel c's own 8-octet block starts at kRegChannelBase + c * +// kRegChannelSpan; the kChReg* offsets below are relative to that channel's +// own base. +constexpr uint16_t kRegChannelBase = 0x0006; +constexpr uint16_t kRegChannelSpan = 0x0008; + +constexpr uint16_t kChRegBaudRate = 0x00; // 16 bit, R/W +constexpr uint16_t kChRegCfg = 0x02; // 8 bit, R/W -- clk_polarity(0)/ + // clk_phase(1)/cs_polarity(2)/ + // use_cs(3)/deassert_cs_pause(4), + // bits 5-7 reserved +constexpr uint16_t kChRegCsLeadtime = 0x03; // 8 bit, R/W +constexpr uint16_t kChRegCsTrailtime = 0x04; // 8 bit, R/W +constexpr uint16_t kChRegBitsMax = 0x05; // 8 bit, R/W +constexpr uint16_t kChRegPauseMin = 0x06; // 8 bit, R/W +constexpr uint16_t kChRegReserved = 0x07; // 8 bit, R + +// Bit masks within a channel's kChRegCfg octet. +constexpr uint8_t kCfgBitClkPolarity = 1u << 0; +constexpr uint8_t kCfgBitClkPhase = 1u << 1; +constexpr uint8_t kCfgBitCsPolarity = 1u << 2; +constexpr uint8_t kCfgBitUseCs = 1u << 3; +// Added TC18 spec revision 0.5.1_RC5, ticket NXP_100 — see the file header. +constexpr uint8_t kCfgBitDeassertCsPause = 1u << 4; + +// The block's own length in octets — one past the last assigned offset: the +// 6-octet common prefix plus kMaxChannels 8-octet per-channel blocks (0x36 = +// 54 octets). +constexpr uint16_t kEpFuncLen = + static_cast(kRegChannelBase + static_cast(kMaxChannels) * kRegChannelSpan); + +// The fixed width (octets) of the relative-start-address prefix every +// configuration request's payload begins with — a 16-bit big-endian field, +// followed by the configuration data octets to write from that address +// onward (§12.7.1). +constexpr size_t kReconfigAddrLen = 2; + +using SpiRegisterBlock = 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; + +// The inverse of mode_cpol()/mode_cpha(): recovers the mode implied by a +// (cpol, cpha) bit pair. The mapping is bijective (each of the four modes +// yields a distinct pair), so this is a lossless round trip. +constexpr SpiMode mode_from_bits(bool cpol, bool cpha) noexcept { + if (!cpol && !cpha) return SpiMode::Mode0; + if (!cpol && cpha) return SpiMode::Mode1; + if (cpol && !cpha) return SpiMode::Mode2; + return SpiMode::Mode3; +} + +// True iff the octet at relative offset addr belongs to a read-only register +// of the block — EP_LEN, NR_CS, or any channel's own reserved octet +// (computed via the channel span's own modulus, so it applies uniformly to +// all kMaxChannels channels). +inline bool reg_offset_read_only(uint16_t addr) noexcept { + if (addr == kRegEpLen || addr == kRegNrCs) return true; + if (addr >= kRegChannelBase) { + const uint16_t rel = static_cast((addr - kRegChannelBase) % kRegChannelSpan); + return rel == kChRegReserved; + } + return false; +} +} // namespace detail + +// render_registers serializes cfg's EP_func registers into out exactly as a +// configuration *read* of the whole block would report them — the inverse +// of apply_reconfig()'s own parse step. mode's CPOL/CPHA bits are rendered +// via mode_cpol()/mode_cpha(); bit_order and trigger have no wire +// counterpart (see the file header) and are not rendered. +inline void render_registers(const SpiFunctionalCfg& cfg, SpiRegisterBlock& out) noexcept { + 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); + // TC18 0.5.1_RC5: spi_nr_cs is a 4-bit "(count - 1)" field in bits + // [3:0], upper nibble reserved — see the file header. kMaxChannels (6) + // renders as 0x05, not a plain 6. + out[kRegNrCs] = static_cast((kMaxChannels - 1u) & 0x0Fu); + out[kRegEpEnableClr] = enable_clr; + out[kRegEpOptions] = options; + avtp::detail::put_u16(&out[kRegEpStatus], cfg.ep_status); + + for (uint8_t c = 0; c < kMaxChannels; ++c) { + const SpiChannelCfg& ch = cfg.channels[c]; + const uint16_t base = static_cast(kRegChannelBase + + static_cast(c) * kRegChannelSpan); + uint8_t cfg_byte = 0; + if (mode_cpol(ch.mode)) cfg_byte |= kCfgBitClkPolarity; + if (mode_cpha(ch.mode)) cfg_byte |= kCfgBitClkPhase; + if (ch.cs_polarity == SpiCsPolarity::ActiveHigh) cfg_byte |= kCfgBitCsPolarity; + if (ch.use_common_cs) cfg_byte |= kCfgBitUseCs; + if (ch.deassert_cs_pause) cfg_byte |= kCfgBitDeassertCsPause; + + avtp::detail::put_u16(&out[base + kChRegBaudRate], ch.baud_rate_kbps); + out[base + kChRegCfg] = cfg_byte; + out[base + kChRegCsLeadtime] = ch.cs_clk_leadtime; + out[base + kChRegCsTrailtime] = ch.clk_cs_trailtime; + out[base + kChRegBitsMax] = ch.bits_max; + out[base + kChRegPauseMin] = ch.pause_min; + out[base + kChRegReserved] = 0; + } +} + +namespace detail { +// The inverse of render: adopts every R/W register from an already-patched +// block image. The read-only offsets (EP_LEN, NR_CS, and each channel's own +// reserved octet) are deliberately not read back — apply_reconfig() +// re-renders them from cfg before patching, so a write covering them is a +// no-op. bit_order and trigger have no wire counterpart and are left +// untouched here, exactly as render leaves them unrendered. +inline void parse_registers(SpiFunctionalCfg& cfg, const SpiRegisterBlock& 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 = avtp::detail::get_u16(&in[kRegEpStatus]); + + for (uint8_t c = 0; c < kMaxChannels; ++c) { + SpiChannelCfg& ch = cfg.channels[c]; + const uint16_t base = static_cast(kRegChannelBase + + static_cast(c) * kRegChannelSpan); + const uint8_t cfg_byte = in[base + kChRegCfg]; + const bool cpol = (cfg_byte & kCfgBitClkPolarity) != 0; + const bool cpha = (cfg_byte & kCfgBitClkPhase) != 0; + + ch.mode = mode_from_bits(cpol, cpha); + ch.cs_polarity = (cfg_byte & kCfgBitCsPolarity) != 0 ? SpiCsPolarity::ActiveHigh + : SpiCsPolarity::ActiveLow; + ch.use_common_cs = (cfg_byte & kCfgBitUseCs) != 0; + ch.deassert_cs_pause = (cfg_byte & kCfgBitDeassertCsPause) != 0; + + ch.baud_rate_kbps = avtp::detail::get_u16(&in[base + kChRegBaudRate]); + ch.cs_clk_leadtime = in[base + kChRegCsLeadtime]; + ch.clk_cs_trailtime = in[base + kChRegCsTrailtime]; + ch.bits_max = in[base + kChRegBitsMax]; + ch.pause_min = in[base + kChRegPauseMin]; + } +} +} // namespace detail + +// ── Reconfig errors ──────────────────────────────────────────────────────────── + +enum class SpiReconfigErrc : int { + // payload carries no address prefix, or an address prefix with no data + // octet after it. + short_payload = 1, + // start_address + data length exceeds kEpFuncLen — the whole write is + // ignored, per the specification's own rule. + out_of_range = 2, +}; + +inline const std::error_category& spi_reconfig_category() noexcept { + struct Cat : std::error_category { + const char* name() const noexcept override { return "rcp.spi.reconfig"; } + std::string message(int ev) const override { + switch (static_cast(ev)) { + case SpiReconfigErrc::short_payload: + return "rcp/spi: SPI configuration write has no address and data"; + case SpiReconfigErrc::out_of_range: + return "rcp/spi: SPI configuration write extends past the EP_func block"; + default: + return "rcp/spi: SPI unknown configuration-write error"; + } + } + }; + static Cat instance; + return instance; +} + +inline std::error_code make_error_code(SpiReconfigErrc e) noexcept { + return {static_cast(e), spi_reconfig_category()}; +} + +// apply_reconfig applies the configuration escape hatch (evt[2:0] == 111b): +// payload is NOT presented at the interface but interpreted as an addressed +// write into this endpoint's own EP_func block — a 16-bit big-endian +// relative start address followed by the configuration data octets to write +// from that address onward (§12.7.1). Returns SpiReconfigErrc::short_payload +// when payload_len is not at least kReconfigAddrLen + 1, and +// SpiReconfigErrc::out_of_range when the addressed span would extend past +// kEpFuncLen; in both cases cfg is left entirely unchanged, per the +// specification's own "such a payload is to be ignored" rule. Octets of the +// addressed span that land on a read-only register (EP_LEN, NR_CS, or a +// channel's own reserved octet) are left at their current values while the +// rest of the span is still applied — the write is applied at octet +// granularity over the block's rendered image, so a partially-covered +// multi-octet register is handled correctly. +inline std::error_code apply_reconfig(SpiFunctionalCfg& cfg, const uint8_t* payload, + size_t payload_len) noexcept { + if (payload_len <= kReconfigAddrLen) return make_error_code(SpiReconfigErrc::short_payload); + + const uint16_t start_address = avtp::detail::get_u16(payload); + const size_t data_len = payload_len - kReconfigAddrLen; + + if (static_cast(start_address) + data_len > static_cast(kEpFuncLen)) + return make_error_code(SpiReconfigErrc::out_of_range); + + SpiRegisterBlock block{}; + render_registers(cfg, block); + for (size_t i = 0; i < data_len; ++i) { + const auto addr = static_cast(start_address + i); + if (detail::reg_offset_read_only(addr)) continue; // write ignored + block[addr] = payload[kReconfigAddrLen + i]; + } + detail::parse_registers(cfg, block); + return {}; +} + +// 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()); + avtp::detail::put_u16(payload.data(), start_address); + 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 -- §12.7.1: the write request's payload is written into EP_func + hdr.evt_op = 0x7u; // evt[2:0] = 111b, TC18 Table 33 SPI row + hdr.transaction_num = transaction_num; + return acf::encode_acf_abb(hdr, payload); +} + // ── Errors ──────────────────────────────────────────────────────────────────── enum class SpiErrc : int { - channel_out_of_range = 1, // evt[2:0] selected a channel >= kMaxChannels + short_frame = 1, + bad_msg_type = 2, + wrong_bus = 3, + wrong_op = 4, + bad_channel = 5, }; inline const std::error_category& spi_category() noexcept { @@ -61,8 +697,12 @@ inline const std::error_category& spi_category() noexcept { const char* name() const noexcept override { return "rcp.spi"; } std::string message(int ev) const override { switch (static_cast(ev)) { - case SpiErrc::channel_out_of_range: return "rcp/spi: channel index out of range (evt[2:0] not in 0..5)"; - default: return "rcp/spi: unknown error"; + case SpiErrc::short_frame: return "rcp/spi: frame too short"; + case SpiErrc::bad_msg_type: return "rcp/spi: unexpected ACF message type"; + case SpiErrc::wrong_bus: return "rcp/spi: wrong byte_bus_id"; + case SpiErrc::wrong_op: return "rcp/spi: wrong ACF op"; + case SpiErrc::bad_channel: return "rcp/spi: invalid channel selector (evt[2:0] not in 0..5)"; + default: return "rcp/spi: unknown error"; } } }; @@ -74,41 +714,195 @@ inline std::error_code make_error_code(SpiErrc e) noexcept { return {static_cast(e), spi_category()}; } -// channel_of decodes a request's evt[2:0] field as an SPI channel selector. -// This is SPI's own decode, distinct from endpoint::write_semantics_of — +// channel_of decodes a request's evt[2:0] field as an SPI channel selector +// (extraction §5.4; TC18 §13.5 Table 33's own SPI row — see the file +// header's channel-selection note). This is SPI's own decode, distinct from +// rcp::endpoint::write_semantics_of and rcp::endpoint::evt_row2_kind_of — // the same 3-bit wire field means something different for SPI than for -// GPIO/PWM_OUT (extraction §5.4). +// GPIO/PWM_OUT or the Table 33 Row 2 endpoint types. inline std::error_code channel_of(uint8_t evt_op, uint8_t& out_channel) noexcept { const uint8_t channel = static_cast(evt_op & 0x07); - if (channel >= kMaxChannels) return make_error_code(SpiErrc::channel_out_of_range); + if (!channel_valid(channel)) return make_error_code(SpiErrc::bad_channel); out_channel = channel; return {}; } -// ── Compound-wait status-byte truncation rule ──────────────────────────────── -// A compound-wait request's condition is evaluated only against the first -// kCompoundWaitCompareLen bytes of an SPI status transfer, regardless of how -// many of the up to kMaxStatusBytes status bytes that transfer actually -// carried (extraction §5.4). compound_wait_matches implements exactly that -// truncated comparison; it takes no position on the rest of compound-wait's -// semantics (sequencer state, sc-field meaning), which are v2.5.0 scope. +// ── Transfer request ────────────────────────────────────────────────────────── +// A transfer request's payload is the PICO-out (controller-to-peripheral) +// bytes to shift out; the matching response's payload is the same-length +// POCI-in (peripheral-to-controller) bytes captured during that same +// transfer. Both halves are encoded as the read direction: a transfer +// request carries the PICO-out bytes *and* asks for the POCI-in bytes back +// (the specification's own worked SPI example — write N bytes, get a +// response with M — carries op=0 with a non-zero read_size), and the +// response carries the POCI-in bytes. -constexpr size_t kMaxStatusBytes = 20; -constexpr size_t kCompoundWaitCompareLen = 4; +// encode_transfer_request encodes an ACF_ABB transfer request addressed to +// byte_bus_id: evt's low three bits carry channel (0..kMaxChannels-1; any +// other bits are left 0), the payload is exactly tx_data (the PICO-out bytes +// to shift out), and read_size carries the ACF header's own +// read_size_or_segment_num field — see transfer_length() for what it means +// for the actual bus transfer length. Returns an empty vector if tx_data +// exceeds acf::kAcfAbbMaxPayload. +inline std::vector encode_transfer_request(avtp::ByteBusId byte_bus_id, uint8_t channel, + const std::vector& tx_data, + uint16_t read_size, uint8_t transaction_num) { + if (tx_data.size() > acf::kAcfAbbMaxPayload) return {}; -inline bool compound_wait_matches(const std::vector& status, - const std::vector& expected) noexcept { - if (status.size() > kMaxStatusBytes) return false; // exceeds this endpoint's own status-byte ceiling - const size_t n = std::min({status.size(), expected.size(), kCompoundWaitCompareLen}); - if (n == 0) return false; // nothing in the truncated window to compare - return std::equal(status.begin(), status.begin() + static_cast(n), expected.begin()); + acf::AcfMessageInfo hdr; + hdr.byte_bus_id = byte_bus_id; + hdr.op = false; // read direction -- see this section's own header comment + hdr.evt_op = static_cast(channel & 0x7u); + hdr.read_size_or_segment_num = read_size; + hdr.transaction_num = transaction_num; + return acf::encode_acf_abb(hdr, tx_data); } -// ── Trigger signals ─────────────────────────────────────────────────────────── -// Transfer-complete and per-CS assert/de-assert signals, one instance of -// each per channel, built on rcp/endpoint.hpp's generic TriggerRegistry — -// the same primitive rcp/gpio.hpp's per-pin signals use (extraction §5.4, -// §4.5 Group A). +// decode_transfer_request decodes and validates an ACF-level SPI transfer +// request. Fails with SpiErrc::short_frame (frame shorter than the ACF_ABB +// fixed header or its declared payload length), SpiErrc::bad_msg_type (not +// an ACF_ABB message), SpiErrc::wrong_bus (byte_bus_id != expected_bus_id), +// SpiErrc::wrong_op (op is not the read direction), or SpiErrc::bad_channel +// (evt[2:0] is not channel_valid()). On success, out_channel, out_tx_data, +// out_read_size, and out_transaction_num are populated. See +// transfer_length() for combining out_tx_data.size() and out_read_size into +// the actual bus transfer length TC18 §13.7.3.3 requires. +inline std::error_code decode_transfer_request(const uint8_t* b, size_t len, avtp::ByteBusId expected_bus_id, + uint8_t& out_channel, std::vector& out_tx_data, + uint16_t& out_read_size, uint8_t& out_transaction_num) { + acf::AcfMessageInfo hdr; + std::vector payload; + 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(SpiErrc::short_frame); + if (ec) return make_error_code(SpiErrc::bad_msg_type); + + if (hdr.byte_bus_id != expected_bus_id) return make_error_code(SpiErrc::wrong_bus); + // Read direction -- see this section's own header comment. + if (hdr.op) return make_error_code(SpiErrc::wrong_op); + + const uint8_t channel = static_cast(hdr.evt_op & 0x7u); + if (!channel_valid(channel)) return make_error_code(SpiErrc::bad_channel); + + out_channel = channel; + out_tx_data = std::move(payload); + out_read_size = hdr.read_size_or_segment_num; + out_transaction_num = hdr.transaction_num; + return {}; +} + +// REQ-SPI-036: computes the actual SPI bus transfer length in octets for a +// transfer request carrying tx_len bytes of PICO-out payload and a +// read_size of read_size, per TC18 §13.7.3.3's own zero-fill rule: a caller +// driving real SPI hardware clocks tx_data[0..tx_len) verbatim, followed by +// (return value - tx_len) zero octets when read_size > tx_len; POCI is +// captured for the same length; the byte_msg_payload is always presented on +// PICO in full, even when read_size is less than tx_len. Equivalently, +// max(tx_len, read_size) — expressed as its own named primitive both for +// readability and because tx_len is a size_t while read_size is the ACF +// header's own 12-bit-wide uint16_t, two different-width types a bare max() +// would silently promote past their own domains' intent. +inline size_t transfer_length(size_t tx_len, uint16_t read_size) noexcept { + return (read_size > tx_len) ? static_cast(read_size) : tx_len; +} + +// ── Response ─────────────────────────────────────────────────────────────────── + +// encode_response encodes an SPI response carrying rx_data (the POCI-in +// bytes captured during the transfer) as its payload, with evt's low three +// bits carrying channel, echoing transaction_num. Encoded as ACF_ABB when +// timed is false; as ACF_GBB (message_timestamp = timestamp, mtv valid) +// when timed is true. +inline std::vector encode_response(avtp::ByteBusId byte_bus_id, uint8_t channel, + const std::vector& rx_data, uint8_t transaction_num, + bool timed, uint64_t timestamp) { + acf::AcfMessageInfo hdr; + hdr.byte_bus_id = byte_bus_id; + hdr.op = false; // read + hdr.rsp = true; + hdr.evt_op = static_cast(channel & 0x7u); + hdr.transaction_num = transaction_num; + + if (timed) { + hdr.mtv = true; + return acf::encode_acf_gbb(hdr, timestamp, rx_data); + } + return acf::encode_acf_abb(hdr, rx_data); +} + +// decode_response decodes an SPI response from either an ACF_ABB or ACF_GBB +// message (peeks the ACF message type itself, since a response's encoding +// depends on the responding endpoint's own timed/untimed choice). Fails with +// SpiErrc::short_frame, SpiErrc::wrong_bus, or SpiErrc::bad_channel +// (evt[2:0] is not channel_valid()). On success, out_channel and +// out_transaction_num are populated; out_rx_data carries the POCI-in +// payload; out_timed/out_timestamp report whether the message was ACF_GBB +// with a valid timestamp, and that timestamp's value (0 when !out_timed). +inline std::error_code decode_response(const uint8_t* b, size_t len, avtp::ByteBusId expected_bus_id, + uint8_t& out_channel, std::vector& out_rx_data, + 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(SpiErrc::short_frame); + + acf::AcfMessageInfo hdr; + std::vector payload; + avtp::ByteBusId bus_id = 0; + uint8_t evt = 0; + uint8_t txn = 0; + 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(SpiErrc::short_frame); + if (ec) return make_error_code(SpiErrc::bad_msg_type); + bus_id = hdr.byte_bus_id; + evt = hdr.evt_op; + txn = hdr.transaction_num; + 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(SpiErrc::short_frame); + if (ec) return make_error_code(SpiErrc::bad_msg_type); + bus_id = hdr.byte_bus_id; + evt = hdr.evt_op; + txn = hdr.transaction_num; + timed = false; + timestamp = 0; + } + + if (bus_id != expected_bus_id) return make_error_code(SpiErrc::wrong_bus); + + const uint8_t channel = static_cast(evt & 0x7u); + if (!channel_valid(channel)) return make_error_code(SpiErrc::bad_channel); + + out_channel = channel; + out_rx_data = std::move(payload); + out_timed = timed; + out_timestamp = timestamp; + out_transaction_num = txn; + return {}; +} + +// ── SPI status-report width ─────────────────────────────────────────────────── +// The largest status-report width this endpoint type's transfer-done status +// may carry (extraction §4.6's "up to 20 status bytes" ceiling). Not itself +// enforced by any encode/decode function above (no status-report codec is in +// this milestone's scope). Unrelated to compound-wait's own comparison +// length — see the file header's own note on why this module carries no +// compound-wait logic of its own. + +constexpr size_t kMaxStatusBytes = 20; + +// ── Trigger-signal bookkeeping ids ──────────────────────────────────────────── +// One instance of each of transfer-complete and per-CS assert/de-assert per +// channel, built on rcp/endpoint.hpp's generic TriggerRegistry — this +// module's own internal SignalId bookkeeping scheme (kept deliberately +// distinct from trigger_signal_number()'s Table 41 wire-visible numbering +// above, the same split rcp/gpio.hpp's gpio_signal_id/trigger_signal_number +// pair establishes). enum class SpiSignal : uint8_t { TransferComplete = 0, CsAssert = 1, CsDeassert = 2 }; @@ -117,21 +911,24 @@ constexpr endpoint::TriggerRegistry::SignalId spi_signal_id(uint8_t channel, Spi } // ── SpiEndpoint ─────────────────────────────────────────────────────────────── -// Mirrors rcp::gpio::GpioEndpoint's shape: one request-dispatch entry point -// per incoming SPI transfer, recording the exchanged bytes and firing the -// channel's trigger signals in the order a client would observe them. +// A per-transfer convenience wrapper — no c-RCP equivalent (c-RCP is a pure +// free-function codec; see the free functions above for the real ACF-level +// wire content this class does not itself encode/decode): records the +// exchanged bytes of one transfer and fires that channel's trigger signals +// in the order a client would observe them. class SpiEndpoint { public: // transfer performs one full-duplex byte exchange on `channel`: CS is // asserted, `pico_out` is the bytes sent out, `poci_in` is this // implementation's record of the bytes received over the same exchange - // (supplied by the caller — this header models the request/response and + // (supplied by the caller — this class models the request/response and // trigger-signal shape of an SPI transfer, not an actual bus), and CS is - // de-asserted again. Fragmentation is deferred (v2.8.0), so this models - // one transfer per request rather than a CS line held across several. + // de-asserted again. Fragmentation is out of scope (v2.8.0 no-go), so + // this models one transfer per request rather than a CS line held + // across several. std::error_code transfer(uint8_t channel, std::vector pico_out, std::vector poci_in) { - if (channel >= kMaxChannels) return make_error_code(SpiErrc::channel_out_of_range); + if (!channel_valid(channel)) return make_error_code(SpiErrc::bad_channel); triggers_.notify(spi_signal_id(channel, SpiSignal::CsAssert)); last_pico_out_[channel] = std::move(pico_out); @@ -146,16 +943,18 @@ class SpiEndpoint { endpoint::TriggerRegistry& triggers() noexcept { return triggers_; } private: - endpoint::TriggerRegistry triggers_; - std::array, kMaxChannels> last_pico_out_; - std::array, kMaxChannels> last_poci_in_; + endpoint::TriggerRegistry triggers_; + std::array, kMaxChannels> last_pico_out_; + std::array, kMaxChannels> last_poci_in_; }; } // namespace spi } // namespace rcp -// Enable std::error_code construction from rcp::spi::SpiErrc. +// Enable std::error_code construction from rcp::spi::SpiErrc / SpiReconfigErrc. namespace std { template <> struct is_error_code_enum : true_type {}; +template <> +struct is_error_code_enum : true_type {}; } // namespace std diff --git a/include/rcp/uart.hpp b/include/rcp/uart.hpp index 845ce96..bc7f207 100644 --- a/include/rcp/uart.hpp +++ b/include/rcp/uart.hpp @@ -8,63 +8,125 @@ // fusa:req REQ-UART-008 // fusa:req REQ-UART-009 -// UART endpoint (ep_type 0x05) — independent TX/RX queues, RX FIFO -// fill/drain semantics, read completion on either a configured `read_size` -// or `uart_timeout` elapsing, payload-less "pure" read requests, and -// sub-octet bit-width padding (extraction §5.8). +// UART endpoint (ep_type 0x05) — independent TX/RX request families sharing +// one functional-config block (baud rate, word format, flow control), its +// Table 51 functional-configuration register block (§13.7.8.2, reachable +// through the generic evt[2:0]=111b configuration-write escape hatch, +// §12.7.1), Table 52's two HW trigger signals (§13.7.8.4), the Table 33 Row +// 2 evt[2:0] plain/reserved/config-write classification every endpoint type +// in that row shares (§13.5), and §13.7.8.1's read-completion race +// (read_size satisfied / uart_timeout expired / fifo-full-so-fragment), +// wired to this project's Phase 20 fragmentation primitive (rcp/fragment.hpp). // -// ROADMAP.md milestone 48, "Basic Endpoint Types II — I2C, UART, ADC, -// PWM_OUT, PWM_IN (v2.4.0)": UART follows the request-dispatch shape -// rcp/gpio.hpp and rcp/spi.hpp establish, but — unlike either — has no -// evt[2:0]/channel *selector* of its own the way SPI's evt[2:0] picks a -// channel; TX and RX are independent queues addressed by separate calls -// (enqueue_tx/handle_read below) rather than by a shared selector field. +// Phase 3 rewrite (cpp-RCP issue #129, ROADMAP.md "Phase 17"), content- +// corrected against c-RCP's include/rcp/ep_uart.h + src/ep_uart.c — this +// project's RC5-spec-conformant reference for this module. This pass is a +// content correction, not a fresh design: it keeps this header's own +// pre-rewrite UartEndpoint TX/RX-queue convenience class (enqueue_tx/ +// drain_tx/rx_fill/handle_read/handle_pure_read/handle_request) unchanged — +// no c-RCP equivalent (c-RCP is a pure free-function codec with no +// persistent per-endpoint "queue" object), already wired into rcp/mock.hpp's +// real dispatch, and re-verified rather than replaced — while porting in, +// for the first time, the real ACF-level wire codec and functional-config +// content this header previously had none of at all: +// - the functional-config block (UartFunctionalCfg: baud_rate, word format +// [uart_nr_bits/parity/stop_bits], ep_rx_buffer_size, uart_timeout_ms, +// plus Table 51's own rts_enable/cts_enable/half_duplex/trail/ +// baud_rate_kbps/wire_timeout_bit_times/ep_status fields) and its +// setters (set_baud_rate/set_frame_format/set_rx_buffer_size/ +// set_timeout/set_trigger), each gated by functional_cfg_writable() +// - Table 51's own EP_func register block (render_registers()/ +// apply_reconfig()/encode_reconfig_request()), the evt[2:0]==111b +// configuration escape hatch this header never implemented at all before +// - REQ-UART-037: rcp_ep_uart_stop_bits_t's real THIRD member. c-RCP's own +// stop_bits enum shipped for a long time with exactly two values (ONE=0, +// TWO=1) before a later, separately-numbered fix (tc18-gap post-backlog +// audit, 2026-08-14, split 2026-08-18 into the now-independent +// REQ-UART-049) added ONE_HALF as a genuine third, appended (not +// inserted) member — Table 51's own uart_stop_bits register is in HALF +// stop-bit units (2/3/4 for one/one-and-a-half/two stop bits), and this +// port carries the exact same three-way enum and the exact same +// stop_bits_to_half_units()/half_units_to_stop_bits() round-trip below, +// including ONE_HALF's deliberately-appended numeric value (2, not 3) so +// TWO's own pre-existing value (1) never moves — see StopBits below. +// - Table 52's two HW trigger signals (UartTrigger/UartEvent/ +// trigger_fires()) — TX_FINALIZED/RX_FINALIZED, off-by-one against the +// table's own signal numbers 0/1 because NONE occupies ordinal 0 here, +// exactly as c-RCP's own header documents (this codec renders neither +// onto the wire — Table 51 has no trigger-mode register of its own). +// - the TX (write) and RX (read) request/response codec pairs +// (encode_write_request/decode_write_request/encode_write_response/ +// decode_write_response, encode_read_request/decode_read_request/ +// encode_read_response/decode_read_response), all built on rcp/acf.hpp's +// existing ACF_ABB/ACF_GBB codec rather than re-deriving frame layout — +// evt is always 0 for a plain request; this endpoint type carries no +// channel/port selector of its own, exactly as c-RCP's own file header +// documents ("like ep_i2c.h, has no channel selector") — so there is no +// max-channel-count constant to port here, unlike SPI's channel array or +// GPIO's pin array. +// - REQ-UART-033/037/048's read-completion arbitration +// (read_completion_decision()) and wire-timeout conversion +// (wire_timeout_us()) — pure, directly-testable functions over +// caller-tracked counters; this module still owns no FIFO or clock of +// its own, matching every other caller-driven primitive in this +// codebase. +// - REQ-UART-034/029/030/031: read_size is the ACF header's own full +// 12-bit width (kMaxReadSize is now 0x0FFF, not an artificial +// single-AVTPDU ceiling — see below), and a genuinely oversized read +// response is now fragmentable via read_response_fragment_count()/ +// encode_read_response_fragmented()/decode_read_response_fragment(), +// rcp/fragment.hpp's Phase 20 primitive wired into this endpoint type +// for the first time (fragment.hpp's own header comment lists +// rcp/uart.hpp among its documented, previously-unwired callers — this +// pass closes that gap). UartEndpoint's own handle_read()/handle_request() +// convenience-class methods are intentionally left with their existing, +// simpler two-way (read_size-reached vs. uart_timeout-elapsed) behavior +// unchanged, for source and behavior stability with rcp/mock.hpp's +// existing dispatch_uart() wiring and tests/test_mock.cpp's own UART +// coverage — a caller that wants the real three-way arbitration or +// fragmentation calls read_completion_decision()/ +// encode_read_response_fragmented() directly, the same division of +// responsibility gpio.hpp/i2c.hpp already draw between their own +// convenience classes and free-function codecs. +// - sub-octet bit-width padding (nr_bits_valid()/bit_pad_mask()/ +// apply_bit_padding(), plus the pre-existing pack_frame_to_octet()/ +// unpack_frame_bits() convenience wrappers, now rebuilt on top of them): +// WIDENED from this header's own pre-rewrite [5,8]-only accepted range +// to c-RCP's real [1,8] (RCP_EP_UART_NR_BITS_MIN..MAX) — a genuinely +// valid 1-4-bit UART word width was silently rejected before this pass; +// c-RCP's own file header is explicit that 1..8 is exactly what this +// one-byte-per-word wire representation can carry. // -// CORRECTION (Table 30/33 Row 2 evt[2:0] validation, sixth endpoint type -// after I2C, ADC, PWM_IN, LIN, and CAN): the "no evt[2:0] decode of its -// own" claim above described only the *absence of a channel/value -// selector*; it did not mean UART is exempt from Table 33's shared -// Plain/Reserved/ConfigWrite classification. Table 33's own text lists -// UART explicitly, by name, in its second row alongside ADC, PWM_IN, I2C, -// LIN, CAN, ISELED, and MDIO (extraction §13.5, TC18.txt L4085-4092) — the -// exact same row every other endpoint type in that list has already had -// this classification wired in for. handle_request below is that wiring -// for UART, 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, -// and rcp/can.hpp's CanEndpoint::handle_request established, adapted for -// UART's own two-entry-point (TX/RX) shape — see handle_request's own -// comment for why that adaptation is needed and how it stays exactly one -// choke point rather than two independent, unvalidated ones. -// -// ACCEPTED LIMITATION, documented explicitly per the roadmap rather than -// left implicit: ROADMAP.md milestone 52 ("Fragmentation — Go/No-Go -// Decision", v2.8.0) has already made its go/no-go call — fragmentation is -// no-go for this development cycle. Because of that already-final decision, -// this header bounds both the RX FIFO's capacity and the largest -// `read_size` it will accept (kMaxReadSize below) so that a UART read -// response can always be carried in a single, unfragmented AVTPDU. This is -// a conservative implementation-chosen ceiling, not a value derived from -// the wire format's own read_size field width (which is a full uint16_t, -// see rcp/acf.hpp's AcfMessageInfo::read_size_or_segment_num) — a real -// single-AVTPDU budget depends on MTU and other-header overhead this header -// does not model, so kMaxReadSize is deliberately conservative rather than -// computed exactly. +// ── Table 33 Row 2 evt[2:0] validation (handle_request) — re-verified ────── +// UartEndpoint::handle_request's own evt_row2_kind_of dispatch (added by an +// earlier pass, the sixth endpoint type after I2C/ADC/PWM_IN/LIN/CAN to wire +// this classification in) is re-verified against c-RCP's *current* +// ep_uart.c and is unchanged by this pass: decode_write_request()/ +// decode_read_request() below independently apply the identical +// acf::evt_row2_is_plain() rule c-RCP's own rcp_ep_uart_decode_write_request()/ +// _decode_read_request() do (RCP_EP_UART_ERR_BAD_EVT), and c-RCP's own evt/ +// channel semantics have not changed since that earlier pass — no delta +// found here. // // 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 queue-capacity -// values and sub-octet padding convention chosen in this file are this -// implementation's own, same as the equivalent disclaimers in rcp/avtp.hpp, -// rcp/regmap.hpp, rcp/endpoint.hpp, rcp/gpio.hpp, rcp/spi.hpp, rcp/i2c.hpp, -// rcp/adc.hpp, rcp/pwm.hpp, rcp/lin.hpp, and rcp/can.hpp. +// text from that document is reproduced here. The concrete struct/enum +// shapes chosen in this file are this implementation's own, same as the +// equivalent disclaimers in rcp/acf.hpp, rcp/avtp.hpp, rcp/endpoint.hpp, +// rcp/lifecycle.hpp, rcp/fragment.hpp, rcp/gpio.hpp, and rcp/i2c.hpp. #pragma once +#include +#include #include +#include +#include +#include #include #include #include +#include #include #include #include @@ -72,40 +134,57 @@ namespace rcp { namespace uart { -// ── Single-AVTPDU response bound (accepted limitation — see header comment) ── - -constexpr size_t kMaxReadSize = 512; // bytes; conservative single-AVTPDU bound -constexpr size_t kRxFifoCapacity = kMaxReadSize; -constexpr size_t kTxQueueCapacity = kMaxReadSize; - -// ── Sub-octet bit-width padding ─────────────────────────────────────────────── -// UART frames narrower than a full octet (5-8 data bits per frame is this -// implementation's own accepted range) are still carried one-per-byte on -// the wire; the frame's data occupies the low `bits_per_frame` bits of that -// byte, left-justified from bit 0, with the remaining high bits zero-padded -// (this implementation's own explicit packing convention — the extraction -// does not itself dictate which end the padding goes on, so it is called -// out here rather than left to reader inference, matching this repo's -// convention of flagging implementation-chosen encodings elsewhere). +// ── Single-AVTPDU-vs-real-wire-width bound ──────────────────────────────────── +// FIXED (REQ-UART-034, matching i2c.hpp's identical kMaxReadSize correction): +// kMaxReadSize is now the ACF header's own real 12-bit read_size_or_segment_num +// width (0-4095), not an artificial, conservative single-AVTPDU ceiling — a +// caller asking for more than kRxFifoCapacity/kTxQueueCapacity octets is no +// longer inexpressible; it is served, per TC18 §13.7.8.1's own third +// read-completion trigger, via read_response_fragment_count()/ +// encode_read_response_fragmented() below. kRxFifoCapacity/kTxQueueCapacity +// remain this convenience class's own, deliberately separate, implementation- +// chosen queue-capacity bound (unrelated to the wire's own read_size limit, +// same as c-RCP's ep_rx_buffer_size functional-config field is a distinct +// concept from a request's own read_size) — decoupled from kMaxReadSize by +// this pass rather than left aliased to it. -constexpr uint8_t kMinBitsPerFrame = 5; -constexpr uint8_t kMaxBitsPerFrame = 8; +constexpr uint16_t kMaxReadSize = 0x0FFFu; // ACF's 12-bit read_size_or_segment_num max +constexpr size_t kRxFifoCapacity = 512; // this convenience class's own queue-capacity bound +constexpr size_t kTxQueueCapacity = 512; // ── Errors ──────────────────────────────────────────────────────────────────── +// Kept as one shared error category/enum for both UartEndpoint's own +// pre-existing convenience-class errors (1-5, unchanged in value — relied on +// by rcp/mock.hpp's dispatch_uart()) and the newly-ported free-function wire +// codec's errors (6-11, additive), the same single-enum consolidation +// i2c.hpp's own I2cErrc already uses for its convenience class + codec. enum class UartErrc : int { - read_size_exceeds_bound = 1, // requested read_size > kMaxReadSize - rx_fifo_overflow = 2, // rx_fill would push the RX FIFO past kRxFifoCapacity - tx_queue_overflow = 3, // enqueue_tx would push the TX queue past kTxQueueCapacity - bits_per_frame_out_of_range = 4, // bits_per_frame outside [kMinBitsPerFrame, kMaxBitsPerFrame] + read_size_exceeds_bound = 1, // requested read_size > kMaxReadSize + rx_fifo_overflow = 2, // rx_fill would push the RX FIFO past kRxFifoCapacity + tx_queue_overflow = 3, // enqueue_tx would push the TX queue past kTxQueueCapacity + bits_per_frame_out_of_range = 4, // nr_bits/bits_per_frame outside [kNrBitsMin, kNrBitsMax] // evt_row2_kind_of classified the request as ConfigWrite (evt[2:0] == - // 111b, §12.7.1). This milestone deliberately does not implement the - // configuration-write shape (relative EP_functional-config start - // address + configuration data) — see UartEndpoint::handle_request's - // own comment. Reported explicitly rather than silently accepted as a - // plain TX/RX operation or silently ignored, same as I2C's, ADC's, - // PWM_IN's, LIN's, and CAN's own config_write_not_supported variants. + // 111b, §12.7.1) via UartEndpoint::handle_request's own convenience + // dispatch — that call's own out_data/out_bytes shape cannot carry a + // reconfig payload; the real mechanism is apply_reconfig()/ + // render_registers()/encode_reconfig_request() below, called directly. config_write_not_supported = 5, + // ── Ported from c-RCP's rcp_ep_uart_errc_t (RCP_EP_UART_ERR_*) ────────── + short_frame = 6, + bad_msg_type = 7, + wrong_bus = 8, + wrong_op = 9, + // A read request carried a payload — nothing meaningful a UART read + // request's payload could carry (read_size already rides the ACF + // header's own field); this endpoint type treats one arriving anyway as + // an unrecognized command, a deliberate asymmetry against GPIO's write + // requests / PWM_OUT, which do accept a payload on some request types. + unknown_cmd = 10, + // evt[2:0] is not 0b000, TC18 §13.5 Table 33's only legal value for a + // plain (non-configuration) request in UART's endpoint-type row — + // caller shall respond with error code UNSUPPORTED_CMD. + bad_evt = 11, }; inline const std::error_category& uart_category() noexcept { @@ -114,15 +193,22 @@ inline const std::error_category& uart_category() noexcept { std::string message(int ev) const override { switch (static_cast(ev)) { case UartErrc::read_size_exceeds_bound: - return "rcp/uart: read_size exceeds the single-AVTPDU accepted bound"; + return "rcp/uart: read_size exceeds kMaxReadSize (the ACF header's own 12-bit width)"; case UartErrc::rx_fifo_overflow: return "rcp/uart: RX FIFO overflow"; case UartErrc::tx_queue_overflow: return "rcp/uart: TX queue overflow"; case UartErrc::bits_per_frame_out_of_range: - return "rcp/uart: bits_per_frame out of accepted range"; + return "rcp/uart: nr_bits/bits_per_frame out of accepted range"; case UartErrc::config_write_not_supported: - return "rcp/uart: evt[2:0]=111b configuration-write requests are not yet implemented"; + return "rcp/uart: evt[2:0]=111b configuration-write requests are not routable through " + "UartEndpoint::handle_request — call apply_reconfig() directly"; + case UartErrc::short_frame: return "rcp/uart: frame too short"; + case UartErrc::bad_msg_type: return "rcp/uart: unexpected ACF message type"; + case UartErrc::wrong_bus: return "rcp/uart: wrong byte_bus_id"; + case UartErrc::wrong_op: return "rcp/uart: wrong ACF op"; + case UartErrc::unknown_cmd: return "rcp/uart: unrecognized command (payload-bearing read request)"; + case UartErrc::bad_evt: return "rcp/uart: evt[2:0] is not 0b000"; default: return "rcp/uart: unknown error"; } @@ -136,35 +222,834 @@ inline std::error_code make_error_code(UartErrc e) noexcept { return {static_cast(e), uart_category()}; } -// pack_frame_to_octet / unpack_frame_bits implement the sub-octet padding -// convention documented above. +// wire_error maps e to its numbered wire error code (acf::WireErrorCode), for +// a caller building an Error Response frame once a request has failed to +// decode — std::nullopt for every UartErrc value with no numbered +// counterpart. Matches rcp/gpio.hpp's identical wire_error() convenience. +inline std::optional wire_error(UartErrc e) noexcept { + switch (e) { + case UartErrc::bad_evt: + case UartErrc::unknown_cmd: + return acf::WireErrorCode::UnsupportedCmd; + default: + return std::nullopt; + } +} + +// ── Word format: data bits, parity, stop bits ────────────────────────────────── +// UART frames narrower than a full octet are still carried one-per-byte on +// the wire; the frame's data occupies the low `nr_bits` bits of that byte, +// with the remaining high bits zero-padded (this implementation's own +// explicit packing convention, ported from c-RCP's rcp_ep_uart_apply_bit_ +// padding()/rcp_ep_uart_bit_pad_mask()). + +constexpr uint8_t kNrBitsMin = 1; // RCP_EP_UART_NR_BITS_MIN +constexpr uint8_t kNrBitsMax = 8; // RCP_EP_UART_NR_BITS_MAX + +// nr_bits_valid: true iff nr_bits is in [kNrBitsMin, kNrBitsMax] (1..8) +// inclusive — WIDENED from this header's own pre-rewrite [5,8]-only range; +// see the file header. +constexpr bool nr_bits_valid(uint8_t nr_bits) noexcept { + return nr_bits >= kNrBitsMin && nr_bits <= kNrBitsMax; +} + +// bit_pad_mask: the mask apply_bit_padding() applies to every payload byte +// for a given nr_bits — (1u << nr_bits) - 1 for nr_bits_valid(nr_bits), e.g. +// 0x7F for nr_bits == 7 and 0xFF for nr_bits == 8. Returns 0 (fail-safe — +// clears every bit) for an nr_bits outside 1..8. +constexpr uint8_t bit_pad_mask(uint8_t nr_bits) noexcept { + if (!nr_bits_valid(nr_bits)) return 0; + if (nr_bits == kNrBitsMax) return 0xFFu; // (1u << 8) would overflow uint8_t's own range + return static_cast((1u << nr_bits) - 1u); +} + +// Applies bit_pad_mask(nr_bits) to every byte of buf[0..len) in place. buf +// may be nullptr iff len == 0. A no-op when nr_bits == 8; every byte is +// zeroed when nr_bits is outside 1..8 (same fail-safe mask as bit_pad_mask()). +inline void apply_bit_padding(uint8_t* buf, size_t len, uint8_t nr_bits) noexcept { + const uint8_t mask = bit_pad_mask(nr_bits); + for (size_t i = 0; i < len; ++i) buf[i] = static_cast(buf[i] & mask); +} + +inline void apply_bit_padding(std::vector& buf, uint8_t nr_bits) noexcept { + apply_bit_padding(buf.data(), buf.size(), nr_bits); +} + +// pack_frame_to_octet / unpack_frame_bits: this header's own pre-rewrite +// single-octet convenience wrappers, kept and rebuilt on bit_pad_mask() above +// (their accepted range is now [kNrBitsMin, kNrBitsMax] == [1,8], widened +// from the pre-rewrite [5,8] — see the file header). inline std::error_code pack_frame_to_octet(uint8_t value, uint8_t bits_per_frame, uint8_t& out) noexcept { - if (bits_per_frame < kMinBitsPerFrame || bits_per_frame > kMaxBitsPerFrame) - return make_error_code(UartErrc::bits_per_frame_out_of_range); - const uint8_t mask = static_cast((1u << bits_per_frame) - 1u); - out = static_cast(value & mask); + if (!nr_bits_valid(bits_per_frame)) return make_error_code(UartErrc::bits_per_frame_out_of_range); + out = static_cast(value & bit_pad_mask(bits_per_frame)); return {}; } inline std::error_code unpack_frame_bits(uint8_t octet, uint8_t bits_per_frame, uint8_t& out) noexcept { - if (bits_per_frame < kMinBitsPerFrame || bits_per_frame > kMaxBitsPerFrame) - return make_error_code(UartErrc::bits_per_frame_out_of_range); - const uint8_t mask = static_cast((1u << bits_per_frame) - 1u); - out = static_cast(octet & mask); + if (!nr_bits_valid(bits_per_frame)) return make_error_code(UartErrc::bits_per_frame_out_of_range); + out = static_cast(octet & bit_pad_mask(bits_per_frame)); + return {}; +} + +enum class Parity : uint8_t { None = 0, Odd = 1, Even = 2 }; + +// StopBits — REQ-UART-037/049: the real, three-value enum. ONE_HALF is +// APPENDED (value 2), not inserted between ONE and TWO, so TWO's own +// pre-existing numeric value (1) never moves for any code that stored it as +// a raw integer before this fix — ported verbatim from c-RCP's +// rcp_ep_uart_stop_bits_t (RCP_EP_UART_STOP_BITS_ONE=0/_TWO=1/_ONE_HALF=2). +enum class StopBits : uint8_t { + One = 0, + Two = 1, + OneHalf = 2, // 1.5 stop bits — Table 51's own uart_stop_bits register value 3 +}; + +// ── HW trigger signals (§13.7.8.4 Table 52) ───────────────────────────────── +// UartTrigger names Table 52's two output signals ("0: Transmit request +// finalized", "1: Read request finalized") plus a NONE member for "no +// trigger selected" at ordinal 0 (this codebase's own enum convention, e.g. +// gpio::GpioTrigger::None). Because NONE occupies slot 0, the enum's own +// ordinals do NOT directly equal Table 52's signal numbers: +// TxFinalized == 1 (Table 52 signal 0), RxFinalized == 2 (Table 52 signal +// 1) — an off-by-one ported verbatim from c-RCP's own +// rcp_ep_uart_trigger_t, whose header comment documents the identical +// correction (c-RCP-AUDIT-28, issue #449). Nothing below renders this +// field's ordinal onto the wire — cfg.trigger, like SPI's/PWM's own +// per-channel trigger fields, has no Table 51 register of its own; a future +// wire-rendering would need an explicit ordinal -> signal-number mapping +// function, matching the pattern this codebase's own SPI trigger encoding +// already establishes. +enum class UartTrigger : uint8_t { + None = 0, + TxFinalized = 1, // Table 52 signal 0: "Transmit request finalized" + RxFinalized = 2, // Table 52 signal 1: "Read request finalized" +}; + +// The two asynchronous events a UART endpoint's trigger mode may be +// evaluated against — see trigger_fires() below. +enum class UartEvent : uint8_t { + TxRequestFinalized = 0, + ReadRequestFinalized = 1, +}; + +// trigger_fires: true iff event satisfies trigger — never for None; for +// TxFinalized iff event == TxRequestFinalized; for RxFinalized iff event == +// ReadRequestFinalized — TC18 §13.7.8.4 Table 52's own two HW trigger +// signals, verbatim. +constexpr bool trigger_fires(UartTrigger trigger, UartEvent event) noexcept { + switch (trigger) { + case UartTrigger::TxFinalized: return event == UartEvent::TxRequestFinalized; + case UartTrigger::RxFinalized: return event == UartEvent::ReadRequestFinalized; + case UartTrigger::None: + default: return false; + } +} + +// ── Functional config (§13.7.8.2 Table 51) ──────────────────────────────────── +// The common EP_func prefix every endpoint type's own Table shares is +// modeled directly as members here, same as i2c.hpp's I2cFunctionalCfg and +// gpio.hpp's GpioFunctionalConfig (rcp/regmap.hpp's EndpointFunctionalConfig +// is still an opaque byte blob pending its own structural port). +// +// Three fields render/parse through a genuinely different representation +// than this endpoint's own pre-existing, differently-scoped fields of a +// similar name — kept as new, separate members rather than reinterpreting +// existing ones, the same "don't silently redefine an existing public +// field's meaning" caution SPI's own baud_rate_kbps-vs-clock_divider split +// already established (REQ-UART-048): +// - baud_rate_kbps (16 bit, kbit/s) is the wire's own uart_baud_rate +// register; baud_rate (uint32_t, unit unspecified) is a separate, +// pre-existing field, not derived from or written to by the register +// block. +// - wire_timeout_bit_times (8 bit) is the wire's own uart_timeout register +// — a receiver idle-timeout in UART bit periods, distinct from +// uart_timeout_ms (a wall-clock read-completion race timeout at a +// different layer entirely — see UartEndpoint::handle_read() below). +// - stop_bits round-trips through uart_stop_bits's half-stop-bit units +// exactly (REQ-UART-049): render emits 2/3/4 for One/OneHalf/Two; parse +// maps register value 2 to One, 3 to OneHalf, anything >= 4 (or < 2) to +// Two/One respectively — see detail::stop_bits_to_half_units()/ +// detail::half_units_to_stop_bits() below. + +struct UartFunctionalCfg { + 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; + + uint32_t baud_rate = 0; // unit unspecified — this module's own pre-existing field + uint8_t uart_nr_bits = kNrBitsMax; // 1..8; the only sane power-on default (0 is not nr_bits_valid()) + uint8_t parity = static_cast(Parity::None); + uint8_t stop_bits = static_cast(StopBits::One); + uint16_t ep_rx_buffer_size = 0; // RX FIFO size, octets + uint32_t uart_timeout_ms = 0; // read-completion race timeout — see UartEndpoint::handle_read() + + uint16_t ep_status = 0; // uart_ep_status, Table 51 + uint16_t baud_rate_kbps = 0; // uart_baud_rate, Table 51 — kbit/s + bool rts_enable = false; // uart_rts_enable, Table 51 + bool cts_enable = false; // uart_cts_enable, Table 51 + bool half_duplex = false; // uart_half_duplex, Table 51 + uint8_t wire_timeout_bit_times = 0; // uart_timeout, Table 51 — bit times + uint8_t trail = 0; // uart_trail, Table 51 — bit times + + UartTrigger trigger = UartTrigger::None; // this module's own field, not part of the EP_func block +}; + +inline void functional_cfg_init(UartFunctionalCfg& cfg) noexcept { cfg = UartFunctionalCfg{}; } + +// 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); +} + +inline bool set_baud_rate(UartFunctionalCfg& cfg, uint32_t baud_rate, lifecycle::ServerState state, + lifecycle::WriterCtx writer) noexcept { + if (!functional_cfg_writable(state, writer)) return false; + cfg.baud_rate = baud_rate; + return true; +} + +// set_frame_format sets cfg.uart_nr_bits/parity/stop_bits together (one +// setter for all three, since they are always reconfigured as a pack on the +// wire) iff nr_bits is nr_bits_valid() and functional_cfg_writable() +// authorizes the write. cfg is left entirely unchanged when it returns false. +inline bool set_frame_format(UartFunctionalCfg& cfg, uint8_t nr_bits, Parity parity, StopBits stop_bits, + lifecycle::ServerState state, lifecycle::WriterCtx writer) noexcept { + if (!nr_bits_valid(nr_bits)) return false; + if (!functional_cfg_writable(state, writer)) return false; + cfg.uart_nr_bits = nr_bits; + cfg.parity = static_cast(parity); + cfg.stop_bits = static_cast(stop_bits); + return true; +} + +inline bool set_rx_buffer_size(UartFunctionalCfg& cfg, uint16_t rx_buffer_size, lifecycle::ServerState state, + lifecycle::WriterCtx writer) noexcept { + if (!functional_cfg_writable(state, writer)) return false; + cfg.ep_rx_buffer_size = rx_buffer_size; + return true; +} + +inline bool set_timeout(UartFunctionalCfg& cfg, uint32_t timeout_ms, lifecycle::ServerState state, + lifecycle::WriterCtx writer) noexcept { + if (!functional_cfg_writable(state, writer)) return false; + cfg.uart_timeout_ms = timeout_ms; + return true; +} + +// Same authorization rule as set_baud_rate(), for cfg.trigger — see the +// "HW trigger signals" section above. Never touches the EP_func register +// block (this field has no wire counterpart). +inline bool set_trigger(UartFunctionalCfg& cfg, UartTrigger trigger, lifecycle::ServerState state, + lifecycle::WriterCtx writer) noexcept { + if (!functional_cfg_writable(state, writer)) return false; + cfg.trigger = trigger; + return true; +} + +// ── The EP_func register block (evt[2:0] == 111b, §13.7.8.2 Table 51) ──────── +// Table 51's own layout (unlike GPIO's/I2C's own source tables, this one has +// no address-collision editorial defect — its printed addresses are +// internally consistent throughout, per c-RCP's own file header). + +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 +constexpr uint16_t kRegBaudRate = 0x0006; // 16 bit, R/W — kbit/s +constexpr uint16_t kRegNrBits = 0x0008; // 8 bit, R/W — number of data bits +constexpr uint16_t kRegFlags = 0x0009; // 8 bit, R/W — parity_enable(0)/parity_pol(1)/ + // rts_enable(2)/cts_enable(3)/half_duplex(4) +constexpr uint16_t kRegStopBits = 0x000A; // 8 bit, R/W — half stop bits +constexpr uint16_t kRegTimeout = 0x000B; // 8 bit, R/W — receiver timeout, bit times +constexpr uint16_t kRegTrail = 0x000C; // 8 bit, R/W — inter-transmission trail time, bit times + +// Bit masks within kRegFlags. +constexpr uint8_t kFlagParityEnable = 1u << 0; +constexpr uint8_t kFlagParityPol = 1u << 1; +constexpr uint8_t kFlagRtsEnable = 1u << 2; +constexpr uint8_t kFlagCtsEnable = 1u << 3; +constexpr uint8_t kFlagHalfDuplex = 1u << 4; + +// The block's own length in octets — one past the last assigned offset, i.e. +// the value reported at kRegEpLen and the bound the "write beyond EP_LEN is +// ignored" rule (§12.7.1) is applied against. +constexpr size_t kEpFuncLen = 0x000D; + +using EpFuncBlock = std::array; + +constexpr size_t kReconfigAddrLen = 2; + +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; + +// uart_stop_bits's own half-stop-bit units <-> StopBits — REQ-UART-049; see +// the "Functional config" section above for the exact three-way mapping. +constexpr uint8_t stop_bits_to_half_units(uint8_t stop_bits) noexcept { + if (stop_bits == static_cast(StopBits::Two)) return 4u; + if (stop_bits == static_cast(StopBits::OneHalf)) return 3u; + return 2u; // StopBits::One, and any other/invalid value +} + +constexpr uint8_t half_units_to_stop_bits(uint8_t half_units) noexcept { + if (half_units == 3u) return static_cast(StopBits::OneHalf); + return half_units >= 4u ? static_cast(StopBits::Two) : static_cast(StopBits::One); +} + +constexpr bool reg_offset_read_only(uint16_t addr) noexcept { + return addr == kRegEpLen || addr == kRegReserved01; +} +} // namespace detail + +// render_registers serializes cfg's EP_func registers into out exactly as a +// configuration *read* of the whole block would report them — the inverse of +// apply_reconfig()'s own parse step. parity_enable/parity_pol are derived +// from cfg.parity; uart_stop_bits is derived from cfg.stop_bits via the +// half-stop-bit mapping documented above. +inline void render_registers(const UartFunctionalCfg& cfg, EpFuncBlock& out) noexcept { + uint8_t enable_clr = 0; + uint8_t options = 0; + uint8_t flags = 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; + + if (cfg.parity != static_cast(Parity::None)) flags |= kFlagParityEnable; + if (cfg.parity == static_cast(Parity::Even)) flags |= kFlagParityPol; + if (cfg.rts_enable) flags |= kFlagRtsEnable; + if (cfg.cts_enable) flags |= kFlagCtsEnable; + if (cfg.half_duplex) flags |= kFlagHalfDuplex; + + out[kRegEpLen] = static_cast(kEpFuncLen); + out[kRegReserved01] = 0; + out[kRegEpEnableClr] = enable_clr; + out[kRegEpOptions] = options; + avtp::detail::put_u16(&out[kRegEpStatus], cfg.ep_status); + avtp::detail::put_u16(&out[kRegBaudRate], cfg.baud_rate_kbps); + out[kRegNrBits] = cfg.uart_nr_bits; + out[kRegFlags] = flags; + out[kRegStopBits] = detail::stop_bits_to_half_units(cfg.stop_bits); + out[kRegTimeout] = cfg.wire_timeout_bit_times; + out[kRegTrail] = cfg.trail; +} + +namespace detail { +inline void parse_registers(UartFunctionalCfg& cfg, const EpFuncBlock& in) noexcept { + const uint8_t flags = in[kRegFlags]; + const bool parity_enable = (flags & kFlagParityEnable) != 0; + const bool parity_pol = (flags & kFlagParityPol) != 0; + 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 = avtp::detail::get_u16(&in[kRegEpStatus]); + cfg.baud_rate_kbps = avtp::detail::get_u16(&in[kRegBaudRate]); + cfg.uart_nr_bits = in[kRegNrBits]; + + if (!parity_enable) { + cfg.parity = static_cast(Parity::None); + } else { + cfg.parity = parity_pol ? static_cast(Parity::Even) : static_cast(Parity::Odd); + } + cfg.rts_enable = (flags & kFlagRtsEnable) != 0; + cfg.cts_enable = (flags & kFlagCtsEnable) != 0; + cfg.half_duplex = (flags & kFlagHalfDuplex) != 0; + + cfg.stop_bits = half_units_to_stop_bits(in[kRegStopBits]); + cfg.wire_timeout_bit_times = in[kRegTimeout]; + cfg.trail = in[kRegTrail]; +} +} // namespace detail + +enum class UartReconfigErrc : int { + short_payload = 1, // payload carries no address prefix, or one with 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& uart_reconfig_category() noexcept { + struct Cat : std::error_category { + const char* name() const noexcept override { return "rcp.uart.reconfig"; } + std::string message(int ev) const override { + switch (static_cast(ev)) { + case UartReconfigErrc::short_payload: + return "rcp/uart: configuration write has no address and data"; + case UartReconfigErrc::out_of_range: + return "rcp/uart: configuration write extends past the EP_func block"; + default: return "rcp/uart: unknown configuration-write error"; + } + } + }; + static Cat instance; + return instance; +} + +inline std::error_code make_error_code(UartReconfigErrc e) noexcept { + return {static_cast(e), uart_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). +// Patches the block's current image at octet granularity, then adopts it +// wholesale; octets landing on a read-only register (EP_LEN, the reserved +// octet) are silently skipped while the rest of the span is still applied. A +// write whose start_address+length exceeds kEpFuncLen is rejected wholesale +// and cfg left entirely unchanged, per §12.7.1's own "such a payload is to +// be ignored" rule. +inline std::error_code apply_reconfig(UartFunctionalCfg& cfg, const uint8_t* payload, + size_t payload_len) { + if (payload_len <= kReconfigAddrLen) return make_error_code(UartReconfigErrc::short_payload); + + const uint16_t start_address = avtp::detail::get_u16(payload); + const size_t data_len = payload_len - kReconfigAddrLen; + + if (static_cast(start_address) + data_len > kEpFuncLen) + return make_error_code(UartReconfigErrc::out_of_range); + + EpFuncBlock block{}; + render_registers(cfg, block); + for (size_t i = 0; i < data_len; ++i) { + const auto addr = static_cast(start_address + i); + if (detail::reg_offset_read_only(addr)) continue; // write ignored + block[addr] = payload[kReconfigAddrLen + i]; + } + detail::parse_registers(cfg, block); + return {}; +} + +// 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()); + avtp::detail::put_u16(payload.data(), start_address); + 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 — §12.7.1: the write request's payload is written into EP_func + hdr.evt_op = 0x7; // evt[2:0] = 111b, the reconfiguration escape hatch + hdr.transaction_num = transaction_num; + return acf::encode_acf_abb(hdr, payload); +} + +// ── TX: write request/response ────────────────────────────────────────────── ── +// evt is always 0 for every request/response this module produces or +// consumes — this endpoint type, like I2C, has no channel selector. + +// encode_write_request encodes an ACF_ABB write (TX) request addressed to +// byte_bus_id: payload is exactly tx_data, the raw bytes to transmit +// (already bit-padded by the caller if applicable — see apply_bit_padding()). +inline std::vector encode_write_request(avtp::ByteBusId byte_bus_id, + const std::vector& tx_data, + uint8_t transaction_num) { + acf::AcfMessageInfo hdr; + hdr.byte_bus_id = byte_bus_id; + hdr.op = true; // write + hdr.evt_op = 0; // no channel selector — see the file header + hdr.transaction_num = transaction_num; + return acf::encode_acf_abb(hdr, tx_data); +} + +// decode_write_request decodes and validates an ACF-level UART write +// request. Fails with UartErrc::short_frame/bad_msg_type/wrong_bus/wrong_op; +// UartErrc::bad_evt if evt[2:0] is not 0b000 (acf::evt_row2_is_plain(), TC18 +// §13.5 Table 33 — caller shall respond with error code UNSUPPORTED_CMD). On +// success, out_tx_data/out_transaction_num are populated. +inline std::error_code decode_write_request(const uint8_t* b, size_t len, avtp::ByteBusId expected_bus_id, + std::vector& out_tx_data, + uint8_t& out_transaction_num) { + acf::AcfMessageInfo hdr; + std::vector payload; + 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(UartErrc::short_frame); + if (ec) return make_error_code(UartErrc::bad_msg_type); + + if (hdr.byte_bus_id != expected_bus_id) return make_error_code(UartErrc::wrong_bus); + if (!hdr.op) return make_error_code(UartErrc::wrong_op); // op=false means read + if (!acf::evt_row2_is_plain(hdr.evt_op)) return make_error_code(UartErrc::bad_evt); + + out_tx_data = std::move(payload); + out_transaction_num = hdr.transaction_num; + return {}; +} + +// encode_write_response encodes a write (TX) response carrying accepted_data +// (the prefix of the original request's tx bytes this endpoint actually +// accepted into its TX path; all of them, in the ordinary case) as its +// payload, echoing transaction_num. Encoded as ACF_ABB when timed is false; +// as ACF_GBB (mtv valid, message_timestamp = timestamp) when timed is true. +inline std::vector encode_write_response(avtp::ByteBusId byte_bus_id, + const std::vector& accepted_data, + uint8_t transaction_num, bool timed, + uint64_t timestamp) { + 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, accepted_data); + } + return acf::encode_acf_abb(hdr, accepted_data); +} + +// decode_write_response decodes a write (TX) response from either an +// ACF_ABB or ACF_GBB message (peeks the ACF message type, since a +// response's encoding depends on the responding endpoint's own +// timed/untimed choice). Fails with UartErrc::short_frame/bad_msg_type/ +// wrong_bus. +inline std::error_code decode_write_response(const uint8_t* b, size_t len, avtp::ByteBusId expected_bus_id, + std::vector& out_accepted_data, 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(UartErrc::short_frame); + + acf::AcfMessageInfo hdr; + std::vector payload; + avtp::ByteBusId bus_id = 0; + uint8_t txn = 0; + bool timed = false; + uint64_t ts = 0; + + if (msg_type == acf::kAcfMsgTypeGbb) { + 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(UartErrc::short_frame); + if (ec) return make_error_code(UartErrc::bad_msg_type); + bus_id = hdr.byte_bus_id; + txn = hdr.transaction_num; + timed = hdr.mtv; + } 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(UartErrc::short_frame); + if (ec) return make_error_code(UartErrc::bad_msg_type); + bus_id = hdr.byte_bus_id; + txn = hdr.transaction_num; + } + + if (bus_id != expected_bus_id) return make_error_code(UartErrc::wrong_bus); + + out_accepted_data = std::move(payload); + out_timed = timed; + out_timestamp = timed ? ts : 0; + out_transaction_num = txn; + return {}; +} + +// ── RX: read request/response ─────────────────────────────────────────────── ── + +// encode_read_request encodes an ACF_ABB read (RX) request addressed to +// byte_bus_id, with no payload: read_size rides the ACF header's own +// read_size_or_segment_num field. REQ-UART-034: read_size is the header's +// full 12-bit width (0-4095); this function does not itself validate the +// range (matching every other endpoint's own read_size parameter). +inline std::vector encode_read_request(avtp::ByteBusId byte_bus_id, uint16_t read_size, + uint8_t transaction_num) { + acf::AcfMessageInfo hdr; + hdr.byte_bus_id = byte_bus_id; + hdr.op = false; // read + hdr.evt_op = 0; // no channel selector — see the file header + hdr.transaction_num = transaction_num; + hdr.read_size_or_segment_num = read_size; + return acf::encode_acf_abb(hdr, {}); +} + +// decode_read_request decodes and validates an ACF-level UART read request. +// Fails with UartErrc::short_frame/bad_msg_type/wrong_bus/wrong_op; +// UartErrc::bad_evt if evt[2:0] is not 0b000; UartErrc::unknown_cmd if it +// carries any payload at all — the deliberate asymmetry documented in the +// file header. On success, out_read_size/out_transaction_num are populated. +inline std::error_code decode_read_request(const uint8_t* b, size_t len, avtp::ByteBusId expected_bus_id, + uint16_t& out_read_size, uint8_t& out_transaction_num) { + acf::AcfMessageInfo hdr; + std::vector payload; + 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(UartErrc::short_frame); + if (ec) return make_error_code(UartErrc::bad_msg_type); + + if (hdr.byte_bus_id != expected_bus_id) return make_error_code(UartErrc::wrong_bus); + if (hdr.op) return make_error_code(UartErrc::wrong_op); // op=true means write + if (!acf::evt_row2_is_plain(hdr.evt_op)) return make_error_code(UartErrc::bad_evt); + if (!payload.empty()) return make_error_code(UartErrc::unknown_cmd); + + out_read_size = hdr.read_size_or_segment_num; + out_transaction_num = hdr.transaction_num; + return {}; +} + +// encode_read_response encodes a read (RX) response carrying rx_data (the +// bytes actually received — possibly fewer than the requesting read +// request's read_size, i.e. a short read) as its payload, echoing +// transaction_num. Encoded as ACF_ABB when timed is false; as ACF_GBB (mtv +// valid, message_timestamp = timestamp) when timed is true. +inline std::vector encode_read_response(avtp::ByteBusId byte_bus_id, + const std::vector& rx_data, + uint8_t transaction_num, bool timed, uint64_t timestamp) { + 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, rx_data); + } + return acf::encode_acf_abb(hdr, rx_data); +} + +// decode_read_response decodes a read (RX) response from either an ACF_ABB +// or ACF_GBB message (peeked, same reasoning as decode_write_response()). +// Fails with UartErrc::short_frame/bad_msg_type/wrong_bus. payload_len may +// legitimately be shorter than the originating read request's read_size (a +// short read, raced against uart_timeout_ms) — this is treated exactly like +// any other payload length, with no segment_num-based reassembly (this +// codec's own read_size width rarely needs +// decode_read_response_fragment()'s reassembly in practice — see the +// "Fragmented read response" section below). +inline std::error_code decode_read_response(const uint8_t* b, size_t len, avtp::ByteBusId expected_bus_id, + std::vector& out_rx_data, 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(UartErrc::short_frame); + + acf::AcfMessageInfo hdr; + std::vector payload; + avtp::ByteBusId bus_id = 0; + uint8_t txn = 0; + bool timed = false; + uint64_t ts = 0; + + if (msg_type == acf::kAcfMsgTypeGbb) { + 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(UartErrc::short_frame); + if (ec) return make_error_code(UartErrc::bad_msg_type); + bus_id = hdr.byte_bus_id; + txn = hdr.transaction_num; + timed = hdr.mtv; + } 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(UartErrc::short_frame); + if (ec) return make_error_code(UartErrc::bad_msg_type); + bus_id = hdr.byte_bus_id; + txn = hdr.transaction_num; + } + + if (bus_id != expected_bus_id) return make_error_code(UartErrc::wrong_bus); + + out_rx_data = std::move(payload); + out_timed = timed; + out_timestamp = timed ? ts : 0; + out_transaction_num = txn; + return {}; +} + +// ── Read-completion arbitration (REQ-UART-033) ────────────────────────────── +// TC18 §13.7.8.1's own three read-completion triggers, verbatim: a read +// request completes as soon as the fifo-rx-buffer holds read_size bytes, OR +// when uart_timeout has expired, OR — when read_size is larger than the +// rx_fifo_size — once the fifo is full, in which case the response is +// fragmented. This is a pure, directly-testable function over caller-tracked +// counters; this module still owns no real FIFO or clock. + +enum class UartReadCompletion : uint8_t { + NotYetComplete = 0, // keep waiting + RespondNormal = 1, // emit a normal (possibly short) response now + RespondFragmented = 2, // emit via encode_read_response_fragmented() now +}; + +// wire_timeout_us (REQ-UART-037/048) converts Table 51's own uart_timeout +// register (a raw UART bit-time count measured from the last received stop +// bit) into a wall-clock microsecond duration: one bit period is +// 1000/baud_rate_kbps microseconds, so wire_timeout_bit_times bit periods is +// wire_timeout_bit_times*1000/baud_rate_kbps microseconds, rounded UP +// (ceiling) so a caller never underestimates the configured timeout. Fails +// open (returns 0) when baud_rate_kbps == 0 — this library never invents a +// clock rate it has no way to know. +constexpr uint32_t wire_timeout_us(uint16_t baud_rate_kbps, uint8_t wire_timeout_bit_times) noexcept { + return baud_rate_kbps == 0 + ? 0u + : (static_cast(wire_timeout_bit_times) * 1000u + baud_rate_kbps - 1u) / baud_rate_kbps; +} + +// read_completion_decision decides which of the three triggers, if any, has +// fired for a read request in progress: bytes_available is the caller- +// tracked count currently held in the fifo-rx-buffer; read_size is the +// request's own requested byte count; elapsed_ms is wall-clock time since +// the request began; uart_timeout_ms/rx_fifo_size are cfg.uart_timeout_ms/ +// cfg.ep_rx_buffer_size (passed explicitly, since neither is mutated and a +// caller may be tracking several in-flight reads against one shared cfg). +// The fragmentation trigger is checked first (the more specific condition — +// ordering only matters for documentation clarity, not correctness, since +// the read_size-satisfied trigger can never itself fire when read_size > +// rx_fifo_size). elapsed_ms >= uart_timeout_ms with uart_timeout_ms == 0 +// completes immediately (no waiting). +constexpr UartReadCompletion read_completion_decision(uint16_t bytes_available, uint16_t read_size, + uint32_t elapsed_ms, uint32_t uart_timeout_ms, + uint16_t rx_fifo_size) noexcept { + // THIRD trigger: read_size larger than the fifo's own capacity, and the + // fifo has filled to that capacity — fragmentation is required because a + // single response can never carry the whole request's worth of data. + if (read_size > rx_fifo_size && bytes_available >= rx_fifo_size) return UartReadCompletion::RespondFragmented; + // FIRST trigger: the fifo already holds everything the request asked for. + if (bytes_available >= read_size) return UartReadCompletion::RespondNormal; + // SECOND trigger: uart_timeout has expired — whatever is in the fifo + // right now (possibly nothing) goes out as a short read. + if (elapsed_ms >= uart_timeout_ms) return UartReadCompletion::RespondNormal; + return UartReadCompletion::NotYetComplete; +} + +// ── Fragmented read response (Phase 20, rcp/fragment.hpp) ──────────────────── +// REQ-UART-029/030/031: wires rcp/fragment.hpp's generic ms/segment_num +// primitive into this endpoint type for the first time — fragment.hpp's own +// header comment lists rcp/uart.hpp among its documented, previously-unwired +// callers; this pass closes that gap, matching rcp/can.hpp's own +// frame_response_fragment_count()/encode_frame_response_fragmented() shape. + +// The number of ACF frames encode_read_response_fragmented() would produce +// for rx_len octets of read-response payload split into fragments of at most +// max_fragment_payload octets each. +inline size_t read_response_fragment_count(size_t rx_len, size_t max_fragment_payload) noexcept { + return fragment::plan_count(rx_len, max_fragment_payload); +} + +// Encodes a UART read (RX) response as one or more ACF frames, fragmenting +// via rcp/fragment.hpp's ms/segment_num mechanism whenever rx_len exceeds +// max_fragment_payload octets. Every fragment shares byte_bus_id/op(read)/ +// transaction_num/timed/timestamp with encode_read_response(); only the ms +// flag, read_size_or_segment_num (meaningful only on an ms=true fragment), +// and each fragment's own payload slice differ. When rx_len already fits in +// one fragment, this produces exactly one frame identical to what +// encode_read_response() itself would have. Returns an empty vector under +// the same conditions read_response_fragment_count() returns 0 for. +inline std::vector> +encode_read_response_fragmented(avtp::ByteBusId byte_bus_id, const std::vector& rx_data, + uint8_t transaction_num, bool timed, uint64_t timestamp, + size_t max_fragment_payload) { + const size_t count = read_response_fragment_count(rx_data.size(), max_fragment_payload); + if (count == 0) return {}; + + std::vector segs(count); + if (fragment::plan(rx_data.size(), max_fragment_payload, segs.data(), count)) return {}; + + std::vector> out_frames; + out_frames.reserve(count); + for (size_t i = 0; i < count; ++i) { + std::vector slice(rx_data.begin() + static_cast(segs[i].offset), + rx_data.begin() + static_cast(segs[i].offset + segs[i].len)); + + 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; + hdr.ms = segs[i].ms; + hdr.read_size_or_segment_num = segs[i].ms ? segs[i].segment_num : uint16_t{0}; + + if (timed) { + hdr.mtv = true; + out_frames.push_back(acf::encode_acf_gbb(hdr, timestamp, slice)); + } else { + out_frames.push_back(acf::encode_acf_abb(hdr, slice)); + } + } + return out_frames; +} + +// Decodes one fragment of a (possibly multi-fragment) UART read response +// from b[0..len) — the same peek-message-type/byte_bus_id validation +// decode_read_response() applies, but surfaces the fragment's own ms bit and +// read_size_or_segment_num (as *out_segment_num, meaningful only when +// *out_ms) alongside the raw payload slice, for a caller to feed straight +// into a rcp::fragment::Reassembler. Once reassembly reports +// ReasmResult::kComplete, the Reassembler's own data()/size() *is* the fully +// reassembled rx_data directly — unlike CAN's fragmented response, this +// endpoint's payload has no further internal structure of its own to parse. +// Fails with the same UartErrc::short_frame/bad_msg_type/wrong_bus +// conditions decode_read_response() does. +inline std::error_code decode_read_response_fragment(const uint8_t* b, size_t len, + avtp::ByteBusId expected_bus_id, bool& out_ms, + uint16_t& out_segment_num, + std::vector& out_payload, 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(UartErrc::short_frame); + + acf::AcfMessageInfo hdr; + std::vector payload; + avtp::ByteBusId bus_id = 0; + uint8_t txn = 0; + bool timed = false; + uint64_t ts = 0; + + if (msg_type == acf::kAcfMsgTypeGbb) { + 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(UartErrc::short_frame); + if (ec) return make_error_code(UartErrc::bad_msg_type); + bus_id = hdr.byte_bus_id; + txn = hdr.transaction_num; + timed = hdr.mtv; + } 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(UartErrc::short_frame); + if (ec) return make_error_code(UartErrc::bad_msg_type); + bus_id = hdr.byte_bus_id; + txn = hdr.transaction_num; + } + + if (bus_id != expected_bus_id) return make_error_code(UartErrc::wrong_bus); + + out_ms = hdr.ms; + out_segment_num = hdr.read_size_or_segment_num; + out_payload = std::move(payload); + out_timed = timed; + out_timestamp = timed ? ts : 0; + out_transaction_num = txn; return {}; } // ── UartEndpoint ────────────────────────────────────────────────────────────── -// TX and RX are modeled as independent queues (extraction §5.8): writes -// enqueue onto the TX queue via enqueue_tx and are later drained by the -// caller's transport/driver layer via drain_tx; bytes arriving from the -// wire are pushed into the RX FIFO via rx_fill (also driven by that same -// external layer — this header does not itself own a UART transceiver) and -// consumed by read requests via handle_read/handle_pure_read. +// This header's own pre-rewrite TX/RX-queue convenience class — no c-RCP +// equivalent (c-RCP is a pure free-function codec with no persistent +// per-endpoint "queue" object) — kept and re-verified unchanged, per the file +// header. TX and RX are modeled as independent queues: writes enqueue onto +// the TX queue via enqueue_tx and are later drained by the caller's own +// transport/driver layer via drain_tx; bytes arriving from the wire are +// pushed into the RX FIFO via rx_fill (also driven by that same external +// layer — this class does not itself own a UART transceiver) and consumed by +// read requests via handle_read/handle_pure_read. class UartEndpoint { public: // enqueue_tx appends to the independent TX queue, bounded by - // kTxQueueCapacity per the single-AVTPDU accepted limitation. + // kTxQueueCapacity. std::error_code enqueue_tx(const std::vector& bytes) { if (tx_queue_.size() + bytes.size() > kTxQueueCapacity) return make_error_code(UartErrc::tx_queue_overflow); @@ -180,8 +1065,7 @@ class UartEndpoint { } // rx_fill pushes bytes that arrived from the wire into the RX FIFO, - // bounded by kRxFifoCapacity per the single-AVTPDU accepted limitation - // (extraction §5.8's RX FIFO fill semantics). + // bounded by kRxFifoCapacity. std::error_code rx_fill(const std::vector& bytes) { if (rx_fifo_.size() + bytes.size() > kRxFifoCapacity) return make_error_code(UartErrc::rx_fifo_overflow); @@ -191,13 +1075,15 @@ class UartEndpoint { size_t rx_available() const noexcept { return rx_fifo_.size(); } - // handle_read is UART's read-completion rule: it drains up to - // `read_size` bytes currently available in the RX FIFO (RX FIFO drain - // semantics), then reports whether `read_size` was fully reached or - // whether `elapsed_ms >= uart_timeout_ms` — the two conditions the - // extraction names as completing a read (extraction §5.8). The caller - // supplies elapsed/timeout explicitly since this header has no clock of - // its own, matching every other endpoint type in this milestone. + // handle_read is this convenience class's own simplified read-completion + // rule: it drains up to `read_size` bytes currently available in the RX + // FIFO, then reports whether `read_size` was fully reached or whether + // `elapsed_ms >= uart_timeout_ms` — the caller supplies elapsed/timeout + // explicitly since this class has no clock of its own. Deliberately + // unchanged by this pass (source/behavior stability with rcp/mock.hpp's + // existing dispatch_uart() wiring) — a caller wanting the real, full + // three-way §13.7.8.1 arbitration (fragmentation included) calls + // read_completion_decision()/encode_read_response_fragmented() directly. std::error_code handle_read(uint16_t read_size, uint32_t elapsed_ms, uint32_t uart_timeout_ms, std::vector& out_data, bool& out_timed_out) noexcept { if (read_size > kMaxReadSize) return make_error_code(UartErrc::read_size_exceeds_bound); @@ -214,70 +1100,39 @@ class UartEndpoint { // handle_pure_read is UART's payload-less "pure" read request: no // read_size is carried at all, so there is nothing to wait on — it // drains and returns whatever is currently buffered in the RX FIFO, - // non-blocking (extraction §5.8). + // non-blocking. std::vector handle_pure_read() { std::vector out(rx_fifo_.begin(), rx_fifo_.end()); rx_fifo_.clear(); return out; } - // handle_request is UART's single request-decode entry point — the - // piece this header previously had none of, mirroring rcp::i2c:: - // I2cEndpoint::handle_request's shape (this repo's sixth Table 33 Row 2 - // endpoint type after I2C, ADC, PWM_IN, LIN, and CAN) with one - // necessary adaptation: every other Row 2 endpoint type funnels its - // Plain request into a single unified transfer()/transmit()/ - // request_reading() call, but UART's TX and RX are genuinely - // independent operations reached via two different existing entry - // points (enqueue_tx, handle_read — extraction §13.7.8.1's "these two - // processes are independent from each other"). Rather than leave two - // separate, individually-unvalidated request-decode entry points (which - // would let a Reserved or ConfigWrite evt reach either one directly), - // handle_request classifies evt[2:0] via rcp::endpoint:: - // evt_row2_kind_of exactly once and then routes on `is_write` (the - // caller's own req.op — this header has no AcfMessageInfo of its own to - // read it from, same reason every out-parameter below is passed - // explicitly rather than pulled from a wire type): - // - Plain (evt[2:0] == 000b) + is_write: delegates straight to - // enqueue_tx(tx_bytes), unchanged — UART's existing TX-queue model - // already IS this row's correct "plain write request" behavior. - // out_data/out_timed_out are left exactly as the caller passed - // them: a write request produces no read data. - // - Plain (evt[2:0] == 000b) + !is_write: delegates straight to + // handle_request is UART's single request-decode entry point, mirroring + // rcp::i2c::I2cEndpoint::handle_request's shape with one necessary + // adaptation: every other Row 2 endpoint type funnels its Plain request + // into a single unified transfer()/transmit()/request_reading() call, + // but UART's TX and RX are genuinely independent operations reached via + // two different existing entry points (enqueue_tx, handle_read). + // Classifies evt[2:0] via rcp::endpoint::evt_row2_kind_of exactly once + // and routes on `is_write`: + // - Plain (evt[2:0] == 000b) + is_write: delegates to + // enqueue_tx(tx_bytes). out_data/out_timed_out are left exactly as + // the caller passed them: a write request produces no read data. + // - Plain (evt[2:0] == 000b) + !is_write: delegates to // handle_read(read_size, elapsed_ms, uart_timeout_ms, out_data, - // out_timed_out), unchanged — UART's existing read-completion rule - // already IS this row's correct "plain read request" behavior. + // out_timed_out). // - Reserved (evt[2:0] in 001b-110b): returns // endpoint::EndpointErrc::reserved_evt_row2 without touching either - // queue (neither enqueue_tx nor handle_read/rx_fifo_ is invoked) — - // TC18 requires this be rejected with error code UNSUPPORTED_CMD. + // queue. // - ConfigWrite (evt[2:0] == 111b): §12.7.1's configuration-write // shape targets the UART EP's own functional-config block (Table - // 51 — baud rate, parity, stop bits, uart_timeout itself, ...), not - // a TX/RX operation 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, and CAN's own - // handle_request comments defer for the identical reason); this - // returns UartErrc::config_write_not_supported rather than - // crashing, silently accepting the request as a TX/RX operation, - // or silently doing nothing. - // - // NOT to be confused with §13.7.8.3's own, entirely separate rules — - // "A read request having a byte_msg_payload will be rejected with - // error code = UNKNOWN_CMD" (the payload-less-read-only rule) and the - // read_size-reached-vs-uart_timeout-elapsed race handle_read already - // implements. Both operate only once a request has already been - // classified Plain by the switch above; neither is folded into - // evt[2:0] decoding here, and reading evt[2:0] as if it also gated or - // combined with either would be exactly the kind of invented, - // non-spec-derived encoding this codebase has had to remove elsewhere - // once discovered (e.g. rcp/iseled.hpp's and rcp/mdio.hpp's own header - // comments, and rcp/lin.hpp's/rcp/can.hpp's own handle_request comments - // on the identical class of mistake for their own endpoint types). The - // payload-less-read-only rule itself remains unimplemented by this - // header — called out explicitly here rather than silently conflated - // with Table 33 classification or silently assumed. + // 51), not a TX/RX operation at all — this convenience call's + // out_data/tx_bytes shape cannot carry that payload; returns + // UartErrc::config_write_not_supported. The real mechanism + // (apply_reconfig()/render_registers()/encode_reconfig_request() + // above) is fully implemented; a caller integrating real EP0/regmap + // dispatch calls those directly with the request's actual raw + // payload. std::error_code handle_request(uint8_t evt_op, bool is_write, const std::vector& tx_bytes, uint16_t read_size, uint32_t elapsed_ms, uint32_t uart_timeout_ms, std::vector& out_data, bool& out_timed_out) { @@ -301,8 +1156,10 @@ class UartEndpoint { } // namespace uart } // namespace rcp -// Enable std::error_code construction from rcp::uart::UartErrc. +// Enable std::error_code construction from rcp::uart::UartErrc / UartReconfigErrc. 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_spi.cpp b/tests/test_spi.cpp index 9dae928..85c4edd 100644 --- a/tests/test_spi.cpp +++ b/tests/test_spi.cpp @@ -3,100 +3,852 @@ // fusa:test REQ-SPI-003 // fusa:test REQ-SPI-004 // fusa:test REQ-SPI-005 +// fusa:test REQ-SPI-006 +// fusa:test REQ-SPI-007 +// fusa:test REQ-SPI-008 +// fusa:test REQ-SPI-009 +// fusa:test REQ-SPI-010 +// fusa:test REQ-SPI-011 +// fusa:test REQ-SPI-012 +// fusa:test REQ-SPI-013 +// fusa:test REQ-SPI-014 +// fusa:test REQ-SPI-015 +// fusa:test REQ-SPI-016 +// fusa:test REQ-SPI-017 +// fusa:test REQ-SPI-018 +// fusa:test REQ-SPI-019 +// fusa:test REQ-SPI-020 +// fusa:test REQ-SPI-021 +// fusa:test REQ-SPI-022 +// fusa:test REQ-SPI-023 +// fusa:test REQ-SPI-024 +// fusa:test REQ-SPI-025 +// fusa:test REQ-SPI-026 +// fusa:test REQ-SPI-027 +// fusa:test REQ-SPI-028 +// fusa:test REQ-SPI-029 +// fusa:test REQ-SPI-030 +// fusa:test REQ-SPI-033 +// fusa:test REQ-SPI-034 +// fusa:test REQ-SPI-035 +// fusa:test REQ-SPI-036 +// fusa:test REQ-SPI-038 +// fusa:test REQ-SPI-039 +// fusa:test REQ-SPI-040 +// fusa:test REQ-SPI-041 +// fusa:test REQ-SPI-042 +// fusa:test REQ-SPI-043 +// fusa:test REQ-SPI-044 -// Tests for rcp/spi.hpp — the SPI endpoint type (ROADMAP.md milestone 47, -// "Basic Endpoint Types I — GPIO & SPI", v2.3.0). +// Tests for rcp/spi.hpp — the SPI endpoint type (ep_type 0x03), ported from +// c-RCP's tests/test_ep_spi.c (this project's RC5-spec-conformant reference) +// as part of Phase 3 of the ground-up rewrite (cpp-RCP issue #129, +// ROADMAP.md "Phase 17"), plus this codebase's own pre-existing +// SpiEndpoint-convenience-class coverage, re-verified rather than dropped. #include +#include +#include #include using namespace rcp::spi; -// ── Channel selection via evt[2:0] ─────────────────────────────────────────── +// ── Channel addressing ──────────────────────────────────────────────────────── -TEST_CASE("kMaxChannels is 6", "[spi][REQ-SPI-001]") { +TEST_CASE("kMaxChannels is 6", "[spi][REQ-SPI-002]") { REQUIRE(kMaxChannels == 6); } -TEST_CASE("channel_of accepts evt[2:0] values 0..5", "[spi][REQ-SPI-001]") { +TEST_CASE("channel_valid bounds channel indices to 0..5", "[spi][REQ-SPI-002]") { + REQUIRE(channel_valid(0)); + REQUIRE(channel_valid(5)); + REQUIRE_FALSE(channel_valid(6)); + REQUIRE_FALSE(channel_valid(255)); +} + +TEST_CASE("channel_of accepts evt[2:0] values 0..5", "[spi][REQ-SPI-033]") { for (uint8_t v = 0; v < kMaxChannels; ++v) { uint8_t out = 0xFF; - auto ec = channel_of(v, out); + auto ec = channel_of(v, out); REQUIRE_FALSE(ec); REQUIRE(out == v); } } -TEST_CASE("channel_of rejects evt[2:0] values 6 and 7", "[spi][REQ-SPI-001]") { +TEST_CASE("channel_of rejects evt[2:0] values 6 and 7", "[spi][REQ-SPI-033]") { uint8_t out = 0xFF; - REQUIRE(channel_of(6, out) == make_error_code(SpiErrc::channel_out_of_range)); - REQUIRE(channel_of(7, out) == make_error_code(SpiErrc::channel_out_of_range)); + REQUIRE(channel_of(6, out) == make_error_code(SpiErrc::bad_channel)); + REQUIRE(channel_of(7, out) == make_error_code(SpiErrc::bad_channel)); } -TEST_CASE("channel_of masks its input down to 3 bits before range-checking", - "[spi][REQ-SPI-001]") { +TEST_CASE("channel_of masks its input down to 3 bits before range-checking", "[spi][REQ-SPI-033]") { uint8_t out = 0xFF; - auto ec = channel_of(0xF9, out); // low 3 bits = 1 + auto ec = channel_of(0xF9, out); // low 3 bits = 1 REQUIRE_FALSE(ec); REQUIRE(out == 1); } -// ── Compound-wait status-byte truncation rule ──────────────────────────────── +// ── Clock mode ───────────────────────────────────────────────────────────────── -TEST_CASE("compound_wait_matches compares only the first 4 bytes", "[spi][REQ-SPI-002]") { - std::vector status{0x01, 0x02, 0x03, 0x04, 0xFF, 0xFF, 0xFF}; - std::vector expected{0x01, 0x02, 0x03, 0x04}; - REQUIRE(compound_wait_matches(status, expected)); +TEST_CASE("mode_valid accepts exactly 0..3", "[spi][REQ-SPI-003]") { + for (uint8_t v = 0; v <= 3; ++v) REQUIRE(mode_valid(v)); + REQUIRE_FALSE(mode_valid(4)); + REQUIRE_FALSE(mode_valid(255)); } -TEST_CASE("compound_wait_matches ignores a mismatch beyond byte 4", "[spi][REQ-SPI-002]") { - std::vector status(20, 0x00); - status[0] = 0x01; - status[1] = 0x02; - status[2] = 0x03; - status[3] = 0x04; - status[19] = 0xAA; // differs from expected, but past the truncation window +TEST_CASE("mode_cpol derives CPOL correctly for all 4 modes", "[spi][REQ-SPI-004]") { + REQUIRE_FALSE(mode_cpol(SpiMode::Mode0)); + REQUIRE_FALSE(mode_cpol(SpiMode::Mode1)); + REQUIRE(mode_cpol(SpiMode::Mode2)); + REQUIRE(mode_cpol(SpiMode::Mode3)); +} + +TEST_CASE("mode_cpha derives CPHA correctly for all 4 modes", "[spi][REQ-SPI-005]") { + REQUIRE_FALSE(mode_cpha(SpiMode::Mode0)); + REQUIRE(mode_cpha(SpiMode::Mode1)); + REQUIRE_FALSE(mode_cpha(SpiMode::Mode2)); + REQUIRE(mode_cpha(SpiMode::Mode3)); +} + +// ── Per-channel trigger signals ──────────────────────────────────────────────── + +TEST_CASE("trigger_fires never fires for SpiTrigger::None", "[spi][REQ-SPI-006]") { + REQUIRE_FALSE(trigger_fires(SpiTrigger::None, SpiEvent::TransferDone)); + REQUIRE_FALSE(trigger_fires(SpiTrigger::None, SpiEvent::CsAssert)); + REQUIRE_FALSE(trigger_fires(SpiTrigger::None, SpiEvent::CsDeassert)); +} + +TEST_CASE("trigger_fires implements TransferDone", "[spi][REQ-SPI-007]") { + REQUIRE(trigger_fires(SpiTrigger::TransferDone, SpiEvent::TransferDone)); + REQUIRE_FALSE(trigger_fires(SpiTrigger::TransferDone, SpiEvent::CsAssert)); + REQUIRE_FALSE(trigger_fires(SpiTrigger::TransferDone, SpiEvent::CsDeassert)); +} + +TEST_CASE("trigger_fires implements CsAssert", "[spi][REQ-SPI-008]") { + REQUIRE(trigger_fires(SpiTrigger::CsAssert, SpiEvent::CsAssert)); + REQUIRE_FALSE(trigger_fires(SpiTrigger::CsAssert, SpiEvent::TransferDone)); + REQUIRE_FALSE(trigger_fires(SpiTrigger::CsAssert, SpiEvent::CsDeassert)); +} + +TEST_CASE("trigger_fires implements CsDeassert", "[spi][REQ-SPI-009]") { + REQUIRE(trigger_fires(SpiTrigger::CsDeassert, SpiEvent::CsDeassert)); + REQUIRE_FALSE(trigger_fires(SpiTrigger::CsDeassert, SpiEvent::TransferDone)); + REQUIRE_FALSE(trigger_fires(SpiTrigger::CsDeassert, SpiEvent::CsAssert)); +} + +// ── Table 41 trigger-signal numbering ───────────────────────────────────────── + +TEST_CASE("trigger_signal_number implements Table 41's 2+2n/3+2n CS numbering", "[spi][REQ-SPI-034]") { + // CSn asserted -> signal 2+2n. + REQUIRE(trigger_signal_number(0, SpiTrigger::CsAssert) == 2); + REQUIRE(trigger_signal_number(3, SpiTrigger::CsAssert) == 8); + REQUIRE(trigger_signal_number(5, SpiTrigger::CsAssert) == 12); + // CSn de-asserted -> signal 3+2n. + REQUIRE(trigger_signal_number(0, SpiTrigger::CsDeassert) == 3); + REQUIRE(trigger_signal_number(3, SpiTrigger::CsDeassert) == 9); + REQUIRE(trigger_signal_number(5, SpiTrigger::CsDeassert) == 13); +} + +TEST_CASE("trigger_signal_number has no signal for TransferDone (signal 0 is whole-endpoint) or None", + "[spi][REQ-SPI-034]") { + REQUIRE_FALSE(trigger_signal_number(0, SpiTrigger::TransferDone).has_value()); + REQUIRE_FALSE(trigger_signal_number(0, SpiTrigger::None).has_value()); +} + +TEST_CASE("trigger_signal_number rejects a channel >= kMaxChannels", "[spi][REQ-SPI-034]") { + REQUIRE_FALSE(trigger_signal_number(6, SpiTrigger::CsAssert).has_value()); + REQUIRE_FALSE(trigger_signal_number(255, SpiTrigger::CsDeassert).has_value()); +} + +// ── Functional config ───────────────────────────────────────────────────────── + +TEST_CASE("functional_cfg_init zeroes every field", "[spi][REQ-SPI-010]") { + SpiFunctionalCfg cfg; + cfg.ep_enable = true; + cfg.ep_status = 0xBEEF; + cfg.channels[0].mode = SpiMode::Mode3; + cfg.channels[0].bit_order = SpiBitOrder::LsbFirst; + cfg.channels[0].cs_polarity = SpiCsPolarity::ActiveHigh; + cfg.channels[0].trigger = SpiTrigger::CsAssert; + cfg.channels[0].clock_divider = 7; + cfg.channels[0].use_common_cs = true; + cfg.channels[0].deassert_cs_pause = true; + + 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); + + for (uint8_t i = 0; i < kMaxChannels; ++i) { + const auto& ch = cfg.channels[i]; + REQUIRE(ch.mode == SpiMode::Mode0); + REQUIRE(ch.bit_order == SpiBitOrder::MsbFirst); + REQUIRE(ch.cs_polarity == SpiCsPolarity::ActiveLow); + REQUIRE(ch.trigger == SpiTrigger::None); + REQUIRE(ch.clock_divider == 0); + REQUIRE(ch.inter_byte_delay_ns == 0); + REQUIRE(ch.inter_transfer_delay_ns == 0); + REQUIRE(ch.baud_rate_kbps == 0); + REQUIRE_FALSE(ch.use_common_cs); + REQUIRE(ch.cs_clk_leadtime == 0); + REQUIRE(ch.clk_cs_trailtime == 0); + REQUIRE(ch.bits_max == 0); + REQUIRE(ch.pause_min == 0); + REQUIRE_FALSE(ch.deassert_cs_pause); + } +} + +TEST_CASE("functional_cfg_writable is false in HwUnconfigured regardless of writer", "[spi][REQ-SPI-011]") { + 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", + "[spi][REQ-SPI-012]") { + 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", + "[spi][REQ-SPI-013]") { + 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)); +} + +// ── Per-channel setters ──────────────────────────────────────────────────────── + +TEST_CASE("set_channel_mode rejects an invalid channel or an unauthorized write without mutating cfg", + "[spi][REQ-SPI-014]") { + SpiFunctionalCfg cfg; + functional_cfg_init(cfg); + rcp::lifecycle::WriterCtx authorized, none; + authorized.via_root_client_ep0 = true; + + REQUIRE_FALSE(set_channel_mode(cfg, 6, SpiMode::Mode3, rcp::lifecycle::ServerState::HwConfigured, authorized)); + REQUIRE(cfg.channels[0].mode == SpiMode::Mode0); + + REQUIRE_FALSE(set_channel_mode(cfg, 0, SpiMode::Mode3, rcp::lifecycle::ServerState::RcpConfigured, none)); + REQUIRE(cfg.channels[0].mode == SpiMode::Mode0); +} + +TEST_CASE("set_channel_mode applies the write when authorized", "[spi][REQ-SPI-015]") { + SpiFunctionalCfg cfg; + functional_cfg_init(cfg); + rcp::lifecycle::WriterCtx writer; + writer.via_owning_stream = true; + + REQUIRE(set_channel_mode(cfg, 3, SpiMode::Mode2, rcp::lifecycle::ServerState::HwConfigured, writer)); + REQUIRE(cfg.channels[3].mode == SpiMode::Mode2); +} + +TEST_CASE("set_channel_bit_order rejects an invalid channel or an unauthorized write without mutating cfg", + "[spi][REQ-SPI-016]") { + SpiFunctionalCfg cfg; + functional_cfg_init(cfg); + rcp::lifecycle::WriterCtx none; + + REQUIRE_FALSE(set_channel_bit_order(cfg, 6, SpiBitOrder::LsbFirst, rcp::lifecycle::ServerState::HwConfigured, + none)); + REQUIRE_FALSE(set_channel_bit_order(cfg, 0, SpiBitOrder::LsbFirst, rcp::lifecycle::ServerState::HwUnconfigured, + none)); + REQUIRE(cfg.channels[0].bit_order == SpiBitOrder::MsbFirst); +} + +TEST_CASE("set_channel_bit_order applies the write when authorized", "[spi][REQ-SPI-017]") { + SpiFunctionalCfg cfg; + functional_cfg_init(cfg); + rcp::lifecycle::WriterCtx writer; + writer.via_owning_stream = true; + + REQUIRE(set_channel_bit_order(cfg, 1, SpiBitOrder::LsbFirst, rcp::lifecycle::ServerState::HwConfigured, writer)); + REQUIRE(cfg.channels[1].bit_order == SpiBitOrder::LsbFirst); +} + +TEST_CASE("set_channel_cs_polarity rejects an invalid channel or an unauthorized write without mutating cfg", + "[spi][REQ-SPI-018]") { + SpiFunctionalCfg cfg; + functional_cfg_init(cfg); + rcp::lifecycle::WriterCtx none; + + REQUIRE_FALSE(set_channel_cs_polarity(cfg, 6, SpiCsPolarity::ActiveHigh, + rcp::lifecycle::ServerState::HwConfigured, none)); + REQUIRE_FALSE(set_channel_cs_polarity(cfg, 0, SpiCsPolarity::ActiveHigh, + rcp::lifecycle::ServerState::HwUnconfigured, none)); + REQUIRE(cfg.channels[0].cs_polarity == SpiCsPolarity::ActiveLow); +} + +TEST_CASE("set_channel_cs_polarity applies the write when authorized", "[spi][REQ-SPI-019]") { + SpiFunctionalCfg cfg; + functional_cfg_init(cfg); + rcp::lifecycle::WriterCtx writer; + writer.via_owning_stream = true; + + REQUIRE(set_channel_cs_polarity(cfg, 2, SpiCsPolarity::ActiveHigh, rcp::lifecycle::ServerState::HwConfigured, + writer)); + REQUIRE(cfg.channels[2].cs_polarity == SpiCsPolarity::ActiveHigh); +} + +TEST_CASE("set_channel_clock_divider rejects an invalid channel or an unauthorized write without mutating cfg", + "[spi][REQ-SPI-020]") { + SpiFunctionalCfg cfg; + functional_cfg_init(cfg); + rcp::lifecycle::WriterCtx none; + + REQUIRE_FALSE(set_channel_clock_divider(cfg, 6, 128, rcp::lifecycle::ServerState::HwConfigured, none)); + REQUIRE_FALSE(set_channel_clock_divider(cfg, 0, 128, rcp::lifecycle::ServerState::HwUnconfigured, none)); + REQUIRE(cfg.channels[0].clock_divider == 0); +} + +TEST_CASE("set_channel_clock_divider applies the write when authorized", "[spi][REQ-SPI-021]") { + SpiFunctionalCfg cfg; + functional_cfg_init(cfg); + rcp::lifecycle::WriterCtx writer; + writer.via_owning_stream = true; + + REQUIRE(set_channel_clock_divider(cfg, 4, 256, rcp::lifecycle::ServerState::HwConfigured, writer)); + REQUIRE(cfg.channels[4].clock_divider == 256); +} + +TEST_CASE("set_channel_timing rejects an invalid channel or an unauthorized write without mutating cfg", + "[spi][REQ-SPI-022]") { + SpiFunctionalCfg cfg; + functional_cfg_init(cfg); + rcp::lifecycle::WriterCtx none; - std::vector expected{0x01, 0x02, 0x03, 0x04, 0x00 /* ... */}; - REQUIRE(compound_wait_matches(status, expected)); + REQUIRE_FALSE(set_channel_timing(cfg, 6, 100, 200, rcp::lifecycle::ServerState::HwConfigured, none)); + REQUIRE_FALSE(set_channel_timing(cfg, 0, 100, 200, rcp::lifecycle::ServerState::HwUnconfigured, none)); + REQUIRE(cfg.channels[0].inter_byte_delay_ns == 0); + REQUIRE(cfg.channels[0].inter_transfer_delay_ns == 0); } -TEST_CASE("compound_wait_matches reports a mismatch within the first 4 bytes", "[spi][REQ-SPI-002]") { - std::vector status{0x01, 0x02, 0x03, 0x04}; - std::vector expected{0x01, 0x02, 0x03, 0x05}; - REQUIRE_FALSE(compound_wait_matches(status, expected)); +TEST_CASE("set_channel_timing applies the write when authorized", "[spi][REQ-SPI-023]") { + SpiFunctionalCfg cfg; + functional_cfg_init(cfg); + rcp::lifecycle::WriterCtx writer; + writer.via_owning_stream = true; + + REQUIRE(set_channel_timing(cfg, 5, 50, 500, rcp::lifecycle::ServerState::HwConfigured, writer)); + REQUIRE(cfg.channels[5].inter_byte_delay_ns == 50); + REQUIRE(cfg.channels[5].inter_transfer_delay_ns == 500); } -TEST_CASE("compound_wait_matches rejects a status transfer beyond kMaxStatusBytes", - "[spi][REQ-SPI-002]") { - std::vector status(kMaxStatusBytes + 1, 0x00); - std::vector expected(kMaxStatusBytes + 1, 0x00); - REQUIRE_FALSE(compound_wait_matches(status, expected)); +TEST_CASE("set_channel_trigger rejects an invalid channel or an unauthorized write without mutating cfg", + "[spi][REQ-SPI-024]") { + SpiFunctionalCfg cfg; + functional_cfg_init(cfg); + rcp::lifecycle::WriterCtx none; + + REQUIRE_FALSE(set_channel_trigger(cfg, 6, SpiTrigger::TransferDone, rcp::lifecycle::ServerState::HwConfigured, + none)); + REQUIRE_FALSE(set_channel_trigger(cfg, 0, SpiTrigger::TransferDone, rcp::lifecycle::ServerState::HwUnconfigured, + none)); + REQUIRE(cfg.channels[0].trigger == SpiTrigger::None); } -TEST_CASE("compound_wait_matches returns false on empty input", "[spi][REQ-SPI-002]") { - REQUIRE_FALSE(compound_wait_matches({}, {})); - REQUIRE_FALSE(compound_wait_matches({0x01}, {})); +TEST_CASE("set_channel_trigger applies the write when authorized", "[spi][REQ-SPI-025]") { + SpiFunctionalCfg cfg; + functional_cfg_init(cfg); + rcp::lifecycle::WriterCtx writer; + writer.via_owning_stream = true; + + REQUIRE(set_channel_trigger(cfg, 0, SpiTrigger::CsAssert, rcp::lifecycle::ServerState::HwConfigured, writer)); + REQUIRE(cfg.channels[0].trigger == SpiTrigger::CsAssert); } -// ── Raw PICO-out/POCI-in transfer ──────────────────────────────────────────── +// ── The EP_func register block ──────────────────────────────────────────────── + +TEST_CASE("render_registers matches Table 42 offsets, including the nr_cs 4-bit (count-1) encoding", + "[spi][REQ-SPI-035][REQ-SPI-038][REQ-SPI-040]") { + SpiFunctionalCfg cfg; + functional_cfg_init(cfg); + cfg.ep_enable = true; + cfg.ep_status = 0x1234; + cfg.channels[0].mode = SpiMode::Mode3; // cpol=1, cpha=1 + cfg.channels[0].cs_polarity = SpiCsPolarity::ActiveHigh; + cfg.channels[0].use_common_cs = true; + cfg.channels[0].baud_rate_kbps = 0x5566; + cfg.channels[0].cs_clk_leadtime = 3; + cfg.channels[0].clk_cs_trailtime = 4; + cfg.channels[0].bits_max = 5; + cfg.channels[0].pause_min = 6; + cfg.channels[0].deassert_cs_pause = true; -TEST_CASE("SpiEndpoint::transfer records sent and received bytes per channel", - "[spi][REQ-SPI-003]") { + SpiRegisterBlock out{}; + render_registers(cfg, out); + + REQUIRE(out[kRegEpLen] == static_cast(kEpFuncLen)); + + // TC18 0.5.1_RC5: spi_nr_cs is a 4-bit "(count - 1)" field, upper + // nibble reserved — kMaxChannels (6) renders as 0x05, not a plain 6. + REQUIRE(out[kRegNrCs] == static_cast(kMaxChannels - 1u)); + REQUIRE((out[kRegNrCs] & 0xF0u) == 0u); // reserved nibble + + REQUIRE((out[kRegEpEnableClr] & 0x01u) != 0u); + REQUIRE(out[kRegEpStatus] == 0x12u); + REQUIRE(out[kRegEpStatus + 1] == 0x34u); + + // Channel 0's own block starts at 0x0006. + REQUIRE(out[0x0006] == 0x55u); + REQUIRE(out[0x0007] == 0x66u); + REQUIRE(out[0x0008] == (kCfgBitClkPolarity | kCfgBitClkPhase | kCfgBitCsPolarity | kCfgBitUseCs | + kCfgBitDeassertCsPause)); + REQUIRE(out[0x0009] == 3); + REQUIRE(out[0x000A] == 4); + REQUIRE(out[0x000B] == 5); + REQUIRE(out[0x000C] == 6); + REQUIRE(out[0x000D] == 0); // channel 0's reserved octet + + // Channel 1's own block starts at 0x0006 + 8 = 0x000E. + REQUIRE(static_cast(kRegChannelBase + 1u * kRegChannelSpan) == 0x000Eu); + REQUIRE(kEpFuncLen == 0x0036u); +} + +TEST_CASE("apply_reconfig writes baud rate", "[spi][REQ-SPI-039]") { + SpiFunctionalCfg cfg; + functional_cfg_init(cfg); + + uint8_t payload[4] = {0x00, 0x0E, 0x12, 0x34}; // channel 1's baud_rate + + REQUIRE_FALSE(apply_reconfig(cfg, payload, sizeof(payload))); + REQUIRE(cfg.channels[1].baud_rate_kbps == 0x1234); + REQUIRE(cfg.channels[0].baud_rate_kbps == 0); // untouched +} + +// TC18 0.5.1_RC5, ticket NXP_100: spi_deassert_cs_pauseN is bit 4 of a +// channel's own +0x02 cfg octet — proves it round-trips through the parse +// path (apply_reconfig(), not just render), and that the other three cfg +// bits are unaffected by setting or clearing it. +TEST_CASE("apply_reconfig writes the deassert_cs_pause bit without disturbing sibling cfg bits", + "[spi][REQ-SPI-039][REQ-SPI-040]") { + SpiFunctionalCfg cfg; + functional_cfg_init(cfg); + + uint8_t payload[3] = {0x00, 0x08, static_cast(kCfgBitDeassertCsPause | kCfgBitClkPhase)}; + + REQUIRE_FALSE(apply_reconfig(cfg, payload, sizeof(payload))); + REQUIRE(cfg.channels[0].deassert_cs_pause); + REQUIRE(cfg.channels[0].mode == SpiMode::Mode1); // cpha only + REQUIRE_FALSE(cfg.channels[0].use_common_cs); + REQUIRE_FALSE(cfg.channels[1].deassert_cs_pause); // untouched + + payload[2] = 0x00; + REQUIRE_FALSE(apply_reconfig(cfg, payload, sizeof(payload))); + REQUIRE_FALSE(cfg.channels[0].deassert_cs_pause); +} + +TEST_CASE("apply_reconfig derives Mode2 and Mode3 from the cpol/cpha bits", "[spi][REQ-SPI-039]") { + SpiFunctionalCfg cfg; + functional_cfg_init(cfg); + + uint8_t payload[3] = {0x00, 0x08, kCfgBitClkPolarity}; // cpol=1, cpha=0 -> Mode2 + REQUIRE_FALSE(apply_reconfig(cfg, payload, sizeof(payload))); + REQUIRE(cfg.channels[0].mode == SpiMode::Mode2); + + payload[2] = static_cast(kCfgBitClkPolarity | kCfgBitClkPhase); // cpol=1, cpha=1 -> Mode3 + REQUIRE_FALSE(apply_reconfig(cfg, payload, sizeof(payload))); + REQUIRE(cfg.channels[0].mode == SpiMode::Mode3); +} + +TEST_CASE("apply_reconfig writes a span covering multiple channels", "[spi][REQ-SPI-039]") { + SpiFunctionalCfg cfg; + functional_cfg_init(cfg); + + uint8_t payload[2 + 16] = { + 0x00, 0x06, // address = channel 0's block base + 0xAA, 0xBB, // baud_rate0 + 0x00, // cfg byte -- all bits clear + 1, 2, 3, 4, 0xFF, // leadtime/trailtime/bits_max/pause_min/reserved(ignored) + 0xCC, 0xDD, // baud_rate1 + 0x00, + 5, 6, 7, 8, 0xFF, + }; + + REQUIRE_FALSE(apply_reconfig(cfg, payload, sizeof(payload))); + REQUIRE(cfg.channels[0].baud_rate_kbps == 0xAABB); + REQUIRE(cfg.channels[0].cs_clk_leadtime == 1); + REQUIRE(cfg.channels[0].clk_cs_trailtime == 2); + REQUIRE(cfg.channels[0].bits_max == 3); + REQUIRE(cfg.channels[0].pause_min == 4); + REQUIRE(cfg.channels[1].baud_rate_kbps == 0xCCDD); + REQUIRE(cfg.channels[1].cs_clk_leadtime == 5); + REQUIRE(cfg.channels[1].clk_cs_trailtime == 6); + REQUIRE(cfg.channels[1].bits_max == 7); + REQUIRE(cfg.channels[1].pause_min == 8); +} + +TEST_CASE("apply_reconfig ignores the read-only EP_LEN/NR_CS registers", "[spi][REQ-SPI-039]") { + SpiFunctionalCfg cfg; + functional_cfg_init(cfg); + + uint8_t payload[2 + 2] = {0x00, 0x00, 0xFF, 0xFF}; // covers EP_LEN(0x00) and NR_CS(0x01) + + REQUIRE_FALSE(apply_reconfig(cfg, payload, sizeof(payload))); + + SpiRegisterBlock out{}; + render_registers(cfg, out); + REQUIRE(out[kRegEpLen] == static_cast(kEpFuncLen)); + REQUIRE(out[kRegNrCs] == static_cast(kMaxChannels - 1u)); +} + +TEST_CASE("apply_reconfig ignores a channel's own reserved octet", "[spi][REQ-SPI-039]") { + SpiFunctionalCfg cfg; + functional_cfg_init(cfg); + + uint8_t payload[3] = {0x00, 0x0D, 0xFF}; // channel 0's own reserved octet + + REQUIRE_FALSE(apply_reconfig(cfg, payload, sizeof(payload))); + + SpiRegisterBlock out{}; + render_registers(cfg, out); + REQUIRE(out[0x000D] == 0); +} + +TEST_CASE("apply_reconfig rejects a write past EP_LEN", "[spi][REQ-SPI-039]") { + SpiFunctionalCfg cfg; + functional_cfg_init(cfg); + + uint8_t payload[3] = {0x00, 0x36, 0xFF}; // == kEpFuncLen, one past the last valid offset + + REQUIRE(apply_reconfig(cfg, payload, sizeof(payload)) == make_error_code(SpiReconfigErrc::out_of_range)); + REQUIRE(cfg.channels[5].baud_rate_kbps == 0); +} + +TEST_CASE("apply_reconfig rejects a payload with no data octet", "[spi][REQ-SPI-039]") { + SpiFunctionalCfg cfg; + functional_cfg_init(cfg); + + uint8_t addr_only[2] = {0x00, 0x06}; + + REQUIRE(apply_reconfig(cfg, addr_only, sizeof(addr_only)) == + make_error_code(SpiReconfigErrc::short_payload)); + REQUIRE(apply_reconfig(cfg, nullptr, 0) == make_error_code(SpiReconfigErrc::short_payload)); +} + +TEST_CASE("reconfig category reports a non-empty message for every known and an unknown code", + "[spi][REQ-SPI-043]") { + for (int code : {1, 2}) { + auto ec = std::error_code(code, spi_reconfig_category()); + REQUIRE_FALSE(ec.message().empty()); + } + auto unknown = std::error_code(99, spi_reconfig_category()); + REQUIRE_FALSE(unknown.message().empty()); +} + +TEST_CASE("encode_reconfig_request round-trips through acf::decode_acf_abb", "[spi][REQ-SPI-042]") { + std::vector data{0xAB, 0xCD}; + auto frame = encode_reconfig_request(0x03, 0x0006, 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); // write + REQUIRE(hdr.evt_op == 0x7u); + REQUIRE(hdr.transaction_num == 7); + REQUIRE(payload.size() == 4); + REQUIRE(payload[0] == 0x00); + REQUIRE(payload[1] == 0x06); + REQUIRE(payload[2] == 0xAB); + REQUIRE(payload[3] == 0xCD); +} + +TEST_CASE("encode_reconfig_request rejects empty data", "[spi][REQ-SPI-042]") { + auto frame = encode_reconfig_request(0x00, 0, {}, 0); + REQUIRE(frame.empty()); +} + +// ── strerror-equivalent category coverage ───────────────────────────────────── + +TEST_CASE("SpiErrc category reports a non-empty, distinct message for every known code", "[spi][REQ-SPI-001]") { + const SpiErrc codes[] = {SpiErrc::short_frame, SpiErrc::bad_msg_type, SpiErrc::wrong_bus, SpiErrc::wrong_op, + SpiErrc::bad_channel}; + for (size_t i = 0; i < std::size(codes); ++i) { + auto ec = make_error_code(codes[i]); + REQUIRE_FALSE(ec.message().empty()); + for (size_t j = 0; j < i; ++j) { + REQUIRE(ec.message() != make_error_code(codes[j]).message()); + } + } + auto unknown = std::error_code(999, spi_category()); + REQUIRE_FALSE(unknown.message().empty()); +} + +// ── Transfer request round trip ─────────────────────────────────────────────── + +// TC18's own worked SPI transfer example ("write N bytes, get a response +// with M") carries op=0 (the read direction) — an SPI transfer request +// sends PICO bytes and expects POCI bytes back. +TEST_CASE("encode_transfer_request uses the read-direction op", "[spi][REQ-SPI-026]") { + std::vector tx{0x55}; + auto frame = encode_transfer_request(4, 3, tx, 0, 3); + + rcp::acf::AcfMessageInfo hdr; + std::vector payload; + REQUIRE_FALSE(rcp::acf::decode_acf_abb(frame.data(), frame.size(), hdr, payload)); + REQUIRE_FALSE(hdr.op); // read + REQUIRE((hdr.evt_op & 0x7u) == 3u); +} + +TEST_CASE("transfer request round-trips channel, PICO-out payload, read_size, and transaction_num", + "[spi][REQ-SPI-027][REQ-SPI-041][REQ-SPI-044]") { + std::vector tx{0x01, 0x02, 0x03}; + auto frame = encode_transfer_request(4, 2, tx, 0x0Au, 9); + + uint8_t channel = 0xFF; + std::vector out_tx; + uint16_t out_read_size = 0; + uint8_t txn = 0; + + REQUIRE_FALSE(decode_transfer_request(frame.data(), frame.size(), 4, channel, out_tx, out_read_size, txn)); + REQUIRE(channel == 2); + REQUIRE(out_tx == tx); + REQUIRE(out_read_size == 0x0Au); + REQUIRE(txn == 9); +} + +TEST_CASE("transfer request round-trips an empty PICO-out payload", "[spi][REQ-SPI-027]") { + auto frame = encode_transfer_request(4, 0, {}, 0, 1); + + uint8_t channel = 0xFF; + std::vector out_tx; + uint16_t out_read_size = 0; + uint8_t txn = 0; + + REQUIRE_FALSE(decode_transfer_request(frame.data(), frame.size(), 4, channel, out_tx, out_read_size, txn)); + REQUIRE(channel == 0); + REQUIRE(out_tx.empty()); +} + +TEST_CASE("decode_transfer_request rejects the wrong byte_bus_id", "[spi][REQ-SPI-027]") { + std::vector tx{0xAB}; + auto frame = encode_transfer_request(4, 1, tx, 0, 0); + + uint8_t channel; + std::vector out_tx; + uint16_t out_read_size; + uint8_t txn; + REQUIRE(decode_transfer_request(frame.data(), frame.size(), 5, channel, out_tx, out_read_size, txn) == + make_error_code(SpiErrc::wrong_bus)); +} + +// The mirror of the read-direction test above: a frame carrying the write +// direction is not an SPI transfer request. +TEST_CASE("decode_transfer_request rejects the wrong op", "[spi][REQ-SPI-027]") { + rcp::acf::AcfMessageInfo hdr; + hdr.byte_bus_id = 4; + hdr.op = true; // write -- not a transfer request + auto frame = rcp::acf::encode_acf_abb(hdr, {}); + + uint8_t channel; + std::vector out_tx; + uint16_t out_read_size; + uint8_t txn; + REQUIRE(decode_transfer_request(frame.data(), frame.size(), 4, channel, out_tx, out_read_size, txn) == + make_error_code(SpiErrc::wrong_op)); +} + +// TC18 Table 33's SPI row: evt[2:0] 000b-101b selects channel 0..5, 110b is +// reserved, 111b is the configuration escape hatch -- neither 6 nor 7 is a +// channel selector. +TEST_CASE("decode_transfer_request rejects a bad channel", "[spi][REQ-SPI-027]") { + rcp::acf::AcfMessageInfo hdr; + hdr.byte_bus_id = 4; + hdr.op = false; // read + hdr.evt_op = 7; // not a valid channel selector + auto frame = rcp::acf::encode_acf_abb(hdr, {}); + + uint8_t channel; + std::vector out_tx; + uint16_t out_read_size; + uint8_t txn; + REQUIRE(decode_transfer_request(frame.data(), frame.size(), 4, channel, out_tx, out_read_size, txn) == + make_error_code(SpiErrc::bad_channel)); +} + +TEST_CASE("decode_transfer_request rejects a non-ACF_ABB frame", "[spi][REQ-SPI-027]") { + rcp::acf::AcfMessageInfo hdr; + hdr.byte_bus_id = 4; + hdr.op = true; + auto frame = rcp::acf::encode_acf_gbb(hdr, 0, {}); + + uint8_t channel; + std::vector out_tx; + uint16_t out_read_size; + uint8_t txn; + REQUIRE(decode_transfer_request(frame.data(), frame.size(), 4, channel, out_tx, out_read_size, txn) == + make_error_code(SpiErrc::bad_msg_type)); +} + +TEST_CASE("decode_transfer_request rejects a short frame", "[spi][REQ-SPI-027]") { + // byte0's top 7 bits must carry acf_msg_type == kAcfMsgTypeAbb (0x0E) -- + // otherwise decode_acf_abb reports bad_msg_type before it even gets to + // check the buffer's length against the fixed header size. + uint8_t too_short[3] = {0x1C, 0x00, 0x00}; + uint8_t channel; + std::vector out_tx; + uint16_t out_read_size; + uint8_t txn; + REQUIRE(decode_transfer_request(too_short, sizeof(too_short), 4, channel, out_tx, out_read_size, txn) == + make_error_code(SpiErrc::short_frame)); +} + +// TC18 §13.7.3.3's own zero-fill rule: verified directly against +// transfer_length(), one case per direction plus the exactly-equal boundary. +TEST_CASE("transfer_length zero-fills when read_size exceeds the payload", "[spi][REQ-SPI-036]") { + REQUIRE(transfer_length(3u, 10u) == 10u); +} + +TEST_CASE("transfer_length presents the full payload when read_size is smaller", "[spi][REQ-SPI-036]") { + REQUIRE(transfer_length(10u, 3u) == 10u); +} + +TEST_CASE("transfer_length handles the exactly-equal boundary", "[spi][REQ-SPI-036]") { + REQUIRE(transfer_length(5u, 5u) == 5u); +} + +// ── Response round trip ─────────────────────────────────────────────────────── + +TEST_CASE("response round-trips untimed", "[spi][REQ-SPI-028]") { + std::vector rx{0xDE, 0xAD, 0xBE, 0xEF}; + auto frame = encode_response(2, 5, rx, 11, false, 0); + + uint8_t channel = 0xFF; + std::vector out_rx; + bool timed = true; + uint64_t ts = 1; + uint8_t txn = 0; + + REQUIRE_FALSE(decode_response(frame.data(), frame.size(), 2, channel, out_rx, timed, ts, txn)); + REQUIRE(channel == 5); + REQUIRE(out_rx == rx); + REQUIRE_FALSE(timed); + REQUIRE(ts == 0); + REQUIRE(txn == 11); +} + +TEST_CASE("response round-trips timed", "[spi][REQ-SPI-029]") { + std::vector rx{0x11, 0x22}; + auto frame = encode_response(2, 3, rx, 200, true, 0x0102030405060708ull); + + uint8_t channel = 0xFF; + std::vector out_rx; + bool timed = false; + uint64_t ts = 0; + uint8_t txn = 0; + + REQUIRE_FALSE(decode_response(frame.data(), frame.size(), 2, channel, out_rx, timed, ts, txn)); + REQUIRE(channel == 3); + REQUIRE(out_rx == rx); + REQUIRE(timed); + REQUIRE(ts == 0x0102030405060708ull); + REQUIRE(txn == 200); +} + +TEST_CASE("decode_response rejects the wrong byte_bus_id", "[spi][REQ-SPI-030]") { + auto frame = encode_response(2, 0, {}, 0, false, 0); + + uint8_t channel; + std::vector out_rx; + bool timed; + uint64_t ts; + uint8_t txn; + REQUIRE(decode_response(frame.data(), frame.size(), 3, channel, out_rx, timed, ts, txn) == + make_error_code(SpiErrc::wrong_bus)); +} + +TEST_CASE("decode_response rejects a bad channel", "[spi][REQ-SPI-030]") { + rcp::acf::AcfMessageInfo hdr; + hdr.byte_bus_id = 2; + hdr.op = false; + hdr.evt_op = 6; // not a valid channel selector + auto frame = rcp::acf::encode_acf_abb(hdr, {}); + + uint8_t channel; + std::vector out_rx; + bool timed; + uint64_t ts; + uint8_t txn; + REQUIRE(decode_response(frame.data(), frame.size(), 2, channel, out_rx, timed, ts, txn) == + make_error_code(SpiErrc::bad_channel)); +} + +TEST_CASE("decode_response rejects a short frame", "[spi][REQ-SPI-030]") { + uint8_t too_short[2] = {rcp::acf::kAcfMsgTypeAbb << 1, 0}; + uint8_t channel; + std::vector out_rx; + bool timed; + uint64_t ts; + uint8_t txn; + REQUIRE(decode_response(too_short, sizeof(too_short), 2, channel, out_rx, timed, ts, txn) == + make_error_code(SpiErrc::short_frame)); +} + +// ── Compound-wait: the wrong SPI-specific 4-byte truncation rule is gone ───── +// c-RCP's v0.111.0 removed rcp_ep_spi_compound_wait_status_equal() and its +// hardcoded 4-byte comparison length as an SPI-specific rule that was simply +// wrong — TC18 §13.5.1's own length rule (status capped to +// byte_msg_payload's own length) is universal across every endpoint type. +// This header carries no compound-wait logic of its own any more; a caller +// evaluating a compound-wait request against this endpoint goes through +// rcp/acf.hpp's own endpoint-type-independent compound_wait_evt_valid()/ +// compound_wait_match() directly, exercised here against the +// specification's own SPI-flavored worked example (checking only the first +// four of 20 received status bytes) to demonstrate the replacement covers +// exactly the case the old, wrong SPI-specific helper existed for. +TEST_CASE("compound-wait against an SPI status transfer goes through acf::compound_wait_match, " + "not an SPI-specific truncation rule", + "[spi]") { + std::vector payload{0x01, 0x02, 0x03, 0x04}; // the request's own 4-byte byte_msg_payload + std::vector status(kMaxStatusBytes, 0x00); // a full 20-byte status transfer + status[0] = 0x01; + status[1] = 0x02; + status[2] = 0x03; + status[3] = 0x04; + status[19] = 0xAA; // differs, but past payload's own 4-byte length -- must not affect the verdict + + REQUIRE(rcp::acf::compound_wait_evt_valid(0x0)); // exact-match mode + REQUIRE(rcp::acf::compound_wait_match(0x0, payload.data(), payload.size(), status.data(), status.size())); + + status[3] = 0x05; // now differs within the first 4 bytes + REQUIRE_FALSE(rcp::acf::compound_wait_match(0x0, payload.data(), payload.size(), status.data(), status.size())); +} + +// ── SpiEndpoint convenience class ───────────────────────────────────────────── + +TEST_CASE("SpiEndpoint::transfer records sent and received bytes per channel", "[spi]") { SpiEndpoint ep; - auto ec = ep.transfer(/*channel=*/2, {0xDE, 0xAD}, {0xBE, 0xEF}); + auto ec = ep.transfer(/*channel=*/2, {0xDE, 0xAD}, {0xBE, 0xEF}); REQUIRE_FALSE(ec); REQUIRE(ep.last_sent(2) == std::vector{0xDE, 0xAD}); REQUIRE(ep.last_received(2) == std::vector{0xBE, 0xEF}); } -TEST_CASE("SpiEndpoint::transfer rejects a channel >= kMaxChannels", "[spi][REQ-SPI-003]") { +TEST_CASE("SpiEndpoint::transfer rejects a channel >= kMaxChannels", "[spi]") { SpiEndpoint ep; - auto ec = ep.transfer(/*channel=*/6, {0x01}, {0x02}); - REQUIRE(ec == make_error_code(SpiErrc::channel_out_of_range)); + auto ec = ep.transfer(/*channel=*/6, {0x01}, {0x02}); + REQUIRE(ec == make_error_code(SpiErrc::bad_channel)); } -TEST_CASE("SpiEndpoint tracks each channel's last transfer independently", "[spi][REQ-SPI-003]") { +TEST_CASE("SpiEndpoint tracks each channel's last transfer independently", "[spi]") { SpiEndpoint ep; REQUIRE_FALSE(ep.transfer(0, {0x01}, {0x11})); REQUIRE_FALSE(ep.transfer(1, {0x02}, {0x22})); @@ -104,10 +856,7 @@ TEST_CASE("SpiEndpoint tracks each channel's last transfer independently", "[spi REQUIRE(ep.last_received(1) == std::vector{0x22}); } -// ── Transfer-complete and per-CS assert/de-assert trigger signals ─────────── - -TEST_CASE("SpiEndpoint::transfer fires CsAssert, TransferComplete, CsDeassert in order when armed", - "[spi][REQ-SPI-004]") { +TEST_CASE("SpiEndpoint::transfer fires CsAssert, TransferComplete, CsDeassert in order when armed", "[spi]") { SpiEndpoint ep; ep.triggers().enable(spi_signal_id(3, SpiSignal::CsAssert)); ep.triggers().enable(spi_signal_id(3, SpiSignal::TransferComplete)); @@ -122,7 +871,7 @@ TEST_CASE("SpiEndpoint::transfer fires CsAssert, TransferComplete, CsDeassert in REQUIRE(drained[2] == spi_signal_id(3, SpiSignal::CsDeassert)); } -TEST_CASE("SpiEndpoint's trigger signals are scoped per channel", "[spi][REQ-SPI-004]") { +TEST_CASE("SpiEndpoint's trigger signals are scoped per channel", "[spi]") { SpiEndpoint ep; ep.triggers().enable(spi_signal_id(0, SpiSignal::TransferComplete)); // Channel 1's TransferComplete signal is deliberately left disabled. @@ -134,15 +883,7 @@ TEST_CASE("SpiEndpoint's trigger signals are scoped per channel", "[spi][REQ-SPI REQUIRE(ep.triggers().has_pending()); } -TEST_CASE("spi_signal_id gives every (channel, signal) pair a distinct id", "[spi][REQ-SPI-004]") { +TEST_CASE("spi_signal_id gives every (channel, signal) pair a distinct id", "[spi]") { REQUIRE(spi_signal_id(0, SpiSignal::TransferComplete) != spi_signal_id(0, SpiSignal::CsAssert)); REQUIRE(spi_signal_id(0, SpiSignal::TransferComplete) != spi_signal_id(1, SpiSignal::TransferComplete)); } - -// ── SpiErrc category sanity ─────────────────────────────────────────────────── - -TEST_CASE("SpiErrc reports a non-empty message in its own category", "[spi][REQ-SPI-005]") { - auto ec = make_error_code(SpiErrc::channel_out_of_range); - REQUIRE(ec.category() == spi_category()); - REQUIRE_FALSE(ec.message().empty()); -} diff --git a/tests/test_uart.cpp b/tests/test_uart.cpp index 9113d27..a2ebefc 100644 --- a/tests/test_uart.cpp +++ b/tests/test_uart.cpp @@ -12,7 +12,10 @@ // "Basic Endpoint Types II — I2C, UART, ADC, PWM_OUT, PWM_IN", v2.4.0). #include +#include #include +#include +#include #include using namespace rcp::uart; @@ -140,10 +143,16 @@ TEST_CASE("unpack_frame_bits round-trips pack_frame_to_octet", "[uart][REQ-UART- REQUIRE(value == (0b01011010 & 0x3F)); } -TEST_CASE("pack_frame_to_octet rejects bits_per_frame outside [5,8]", "[uart][REQ-UART-005]") { +// WIDENED (Phase 3 content correction, matching c-RCP's RCP_EP_UART_NR_BITS_ +// MIN..MAX): this range used to be the pre-rewrite, cpp-RCP-only [5,8]; +// c-RCP's own file header is explicit that 1..8 is exactly what this +// one-byte-per-word wire representation can carry, so a genuinely valid +// 1-4-bit UART word width is no longer rejected here. +TEST_CASE("pack_frame_to_octet rejects bits_per_frame outside [1,8]", "[uart][REQ-UART-005]") { uint8_t out = 0; - REQUIRE(pack_frame_to_octet(0x01, 4, out) == make_error_code(UartErrc::bits_per_frame_out_of_range)); + REQUIRE(pack_frame_to_octet(0x01, 0, out) == make_error_code(UartErrc::bits_per_frame_out_of_range)); REQUIRE(pack_frame_to_octet(0x01, 9, out) == make_error_code(UartErrc::bits_per_frame_out_of_range)); + REQUIRE_FALSE(pack_frame_to_octet(0x01, 4, out)); // now valid — see the file header } TEST_CASE("pack_frame_to_octet with bits_per_frame=8 is a full-octet passthrough", @@ -155,9 +164,14 @@ TEST_CASE("pack_frame_to_octet with bits_per_frame=8 is a full-octet passthrough // ── Single-AVTPDU accepted-limitation bound ────────────────────────────────── -TEST_CASE("kMaxReadSize bounds both the RX FIFO and TX queue capacities", "[uart][REQ-UART-006]") { - REQUIRE(kRxFifoCapacity == kMaxReadSize); - REQUIRE(kTxQueueCapacity == kMaxReadSize); +// FIXED (REQ-UART-034): kMaxReadSize is now the ACF header's own real 12-bit +// read_size_or_segment_num width, decoupled from this convenience class's +// own, separate kRxFifoCapacity/kTxQueueCapacity bound — see the file header. +TEST_CASE("kMaxReadSize is the ACF header's real 12-bit width, decoupled from the queue capacities", + "[uart][REQ-UART-006][REQ-UART-034]") { + REQUIRE(kMaxReadSize == 0x0FFFu); + REQUIRE(kRxFifoCapacity == 512); + REQUIRE(kTxQueueCapacity == 512); } // ── UartErrc category sanity ────────────────────────────────────────────────── @@ -267,3 +281,817 @@ TEST_CASE("UartErrc::config_write_not_supported reports a non-empty message in i REQUIRE(ec.category() == uart_category()); REQUIRE_FALSE(ec.message().empty()); } + +// ═══════════════════════════════════════════════════════════════════════════ +// Phase 3 content-correction pass — ported from c-RCP's tests/test_ep_uart.c +// (this project's RC5-spec-conformant reference), covering the free-function +// ACF wire codec, functional config, Table 51 register block, Table 52 +// triggers, REQ-UART-037's three-value StopBits enum, and the Phase 20 +// fragmentation wiring rcp/uart.hpp previously had none of at all. +// ═══════════════════════════════════════════════════════════════════════════ + +// ── Word format / bit-padding ──────────────────────────────────────────────── + +TEST_CASE("nr_bits_valid accepts 1..8 and rejects everything else", "[uart][REQ-UART-001]") { + REQUIRE_FALSE(nr_bits_valid(0)); + for (uint8_t v = 1; v <= 8; ++v) REQUIRE(nr_bits_valid(v)); + REQUIRE_FALSE(nr_bits_valid(9)); + REQUIRE_FALSE(nr_bits_valid(255)); +} + +TEST_CASE("bit_pad_mask values match c-RCP's rcp_ep_uart_bit_pad_mask", "[uart][REQ-UART-002]") { + REQUIRE(bit_pad_mask(1) == 0x01); + REQUIRE(bit_pad_mask(5) == 0x1F); + REQUIRE(bit_pad_mask(7) == 0x7F); + REQUIRE(bit_pad_mask(8) == 0xFF); + REQUIRE(bit_pad_mask(0) == 0x00); + REQUIRE(bit_pad_mask(9) == 0x00); +} + +TEST_CASE("apply_bit_padding masks every byte in place", "[uart][REQ-UART-003]") { + uint8_t buf[3] = {0xFF, 0xFF, 0xFF}; + apply_bit_padding(buf, sizeof(buf), 7); + REQUIRE(buf[0] == 0x7F); + REQUIRE(buf[1] == 0x7F); + REQUIRE(buf[2] == 0x7F); +} + +TEST_CASE("apply_bit_padding is a no-op for nr_bits == 8", "[uart][REQ-UART-003]") { + uint8_t buf[2] = {0xAB, 0xCD}; + apply_bit_padding(buf, sizeof(buf), 8); + REQUIRE(buf[0] == 0xAB); + REQUIRE(buf[1] == 0xCD); +} + +TEST_CASE("apply_bit_padding zeroes the buffer for an invalid nr_bits", "[uart][REQ-UART-003]") { + uint8_t buf[2] = {0xAB, 0xCD}; + apply_bit_padding(buf, sizeof(buf), 0); + REQUIRE(buf[0] == 0x00); + REQUIRE(buf[1] == 0x00); +} + +// ── HW trigger signals (§13.7.8.4 Table 52) ───────────────────────────────── + +TEST_CASE("trigger_fires: None never fires", "[uart][REQ-UART-041]") { + REQUIRE_FALSE(trigger_fires(UartTrigger::None, UartEvent::TxRequestFinalized)); + REQUIRE_FALSE(trigger_fires(UartTrigger::None, UartEvent::ReadRequestFinalized)); +} + +TEST_CASE("trigger_fires: TxFinalized fires only on its own event", "[uart][REQ-UART-042]") { + REQUIRE(trigger_fires(UartTrigger::TxFinalized, UartEvent::TxRequestFinalized)); + REQUIRE_FALSE(trigger_fires(UartTrigger::TxFinalized, UartEvent::ReadRequestFinalized)); +} + +TEST_CASE("trigger_fires: RxFinalized fires only on its own event", "[uart][REQ-UART-043]") { + REQUIRE(trigger_fires(UartTrigger::RxFinalized, UartEvent::ReadRequestFinalized)); + REQUIRE_FALSE(trigger_fires(UartTrigger::RxFinalized, UartEvent::TxRequestFinalized)); +} + +TEST_CASE("Table 52 off-by-one is preserved: TxFinalized==1 (signal 0), RxFinalized==2 (signal 1)", + "[uart][REQ-UART-041]") { + REQUIRE(static_cast(UartTrigger::None) == 0); + REQUIRE(static_cast(UartTrigger::TxFinalized) == 1); + REQUIRE(static_cast(UartTrigger::RxFinalized) == 2); +} + +// ── Functional config ───────────────────────────────────────────────────────── + +TEST_CASE("functional_cfg_init zeroes every field except uart_nr_bits", "[uart][REQ-UART-004]") { + UartFunctionalCfg cfg; + cfg.ep_enable = cfg.ep_clear_req_storage = cfg.ep_req_crc_enable = true; + cfg.baud_rate = 115200; + cfg.parity = static_cast(Parity::Even); + cfg.stop_bits = static_cast(StopBits::Two); + cfg.ep_rx_buffer_size = 256; + cfg.uart_timeout_ms = 50; + cfg.ep_status = 0xBEEF; + cfg.baud_rate_kbps = 0xAAAA; + cfg.rts_enable = cfg.cts_enable = cfg.half_duplex = true; + cfg.wire_timeout_bit_times = 9; + cfg.trail = 10; + cfg.trigger = UartTrigger::RxFinalized; + + 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.baud_rate == 0); + REQUIRE(cfg.parity == static_cast(Parity::None)); + REQUIRE(cfg.stop_bits == static_cast(StopBits::One)); + REQUIRE(cfg.ep_rx_buffer_size == 0); + REQUIRE(cfg.uart_timeout_ms == 0); + REQUIRE(cfg.uart_nr_bits == kNrBitsMax); + REQUIRE(cfg.ep_status == 0); + REQUIRE(cfg.baud_rate_kbps == 0); + REQUIRE_FALSE(cfg.rts_enable); + REQUIRE_FALSE(cfg.cts_enable); + REQUIRE_FALSE(cfg.half_duplex); + REQUIRE(cfg.wire_timeout_bit_times == 0); + REQUIRE(cfg.trail == 0); + REQUIRE(cfg.trigger == UartTrigger::None); +} + +TEST_CASE("functional_cfg_writable is false in HwUnconfigured regardless of writer", + "[uart][REQ-UART-005]") { + 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", + "[uart][REQ-UART-006]") { + 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", + "[uart][REQ-UART-007]") { + 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)); +} + +TEST_CASE("set_baud_rate rejects an unauthorized writer", "[uart][REQ-UART-008]") { + UartFunctionalCfg cfg; + functional_cfg_init(cfg); + rcp::lifecycle::WriterCtx none; + + REQUIRE_FALSE(set_baud_rate(cfg, 115200, rcp::lifecycle::ServerState::HwUnconfigured, none)); + REQUIRE(cfg.baud_rate == 0); +} + +TEST_CASE("set_baud_rate applies when authorized", "[uart][REQ-UART-009]") { + UartFunctionalCfg cfg; + functional_cfg_init(cfg); + rcp::lifecycle::WriterCtx writer; + writer.via_owning_stream = true; + + REQUIRE(set_baud_rate(cfg, 115200, rcp::lifecycle::ServerState::HwConfigured, writer)); + REQUIRE(cfg.baud_rate == 115200); +} + +TEST_CASE("set_frame_format rejects an invalid nr_bits", "[uart][REQ-UART-010]") { + UartFunctionalCfg cfg; + functional_cfg_init(cfg); + rcp::lifecycle::WriterCtx writer; + + REQUIRE_FALSE(set_frame_format(cfg, 0, Parity::Even, StopBits::Two, + rcp::lifecycle::ServerState::HwConfigured, writer)); + REQUIRE(cfg.uart_nr_bits == kNrBitsMax); + REQUIRE(cfg.parity == static_cast(Parity::None)); +} + +TEST_CASE("set_frame_format rejects an unauthorized writer", "[uart][REQ-UART-011]") { + UartFunctionalCfg cfg; + functional_cfg_init(cfg); + rcp::lifecycle::WriterCtx none; + + REQUIRE_FALSE(set_frame_format(cfg, 7, Parity::Odd, StopBits::One, + rcp::lifecycle::ServerState::HwUnconfigured, none)); + REQUIRE(cfg.uart_nr_bits == kNrBitsMax); +} + +TEST_CASE("set_frame_format applies when valid and authorized", "[uart][REQ-UART-012]") { + UartFunctionalCfg cfg; + functional_cfg_init(cfg); + rcp::lifecycle::WriterCtx writer; + writer.via_owning_stream = true; + + REQUIRE(set_frame_format(cfg, 7, Parity::Even, StopBits::Two, + rcp::lifecycle::ServerState::HwConfigured, writer)); + REQUIRE(cfg.uart_nr_bits == 7); + REQUIRE(cfg.parity == static_cast(Parity::Even)); + REQUIRE(cfg.stop_bits == static_cast(StopBits::Two)); +} + +TEST_CASE("set_rx_buffer_size rejects an unauthorized writer", "[uart][REQ-UART-013]") { + UartFunctionalCfg cfg; + functional_cfg_init(cfg); + rcp::lifecycle::WriterCtx none; + + REQUIRE_FALSE(set_rx_buffer_size(cfg, 256, rcp::lifecycle::ServerState::HwUnconfigured, none)); + REQUIRE(cfg.ep_rx_buffer_size == 0); +} + +TEST_CASE("set_rx_buffer_size applies when authorized", "[uart][REQ-UART-014]") { + UartFunctionalCfg cfg; + functional_cfg_init(cfg); + rcp::lifecycle::WriterCtx writer; + writer.via_owning_stream = true; + + REQUIRE(set_rx_buffer_size(cfg, 256, rcp::lifecycle::ServerState::HwConfigured, writer)); + REQUIRE(cfg.ep_rx_buffer_size == 256); +} + +TEST_CASE("set_timeout rejects an unauthorized writer", "[uart][REQ-UART-015]") { + UartFunctionalCfg cfg; + functional_cfg_init(cfg); + rcp::lifecycle::WriterCtx none; + + REQUIRE_FALSE(set_timeout(cfg, 50, rcp::lifecycle::ServerState::HwUnconfigured, none)); + REQUIRE(cfg.uart_timeout_ms == 0); +} + +TEST_CASE("set_timeout applies when authorized", "[uart][REQ-UART-016]") { + UartFunctionalCfg cfg; + functional_cfg_init(cfg); + rcp::lifecycle::WriterCtx writer; + writer.via_owning_stream = true; + + REQUIRE(set_timeout(cfg, 50, rcp::lifecycle::ServerState::HwConfigured, writer)); + REQUIRE(cfg.uart_timeout_ms == 50); +} + +TEST_CASE("set_trigger rejects an unauthorized writer", "[uart][REQ-UART-044]") { + UartFunctionalCfg cfg; + functional_cfg_init(cfg); + rcp::lifecycle::WriterCtx none; + + REQUIRE_FALSE(set_trigger(cfg, UartTrigger::TxFinalized, rcp::lifecycle::ServerState::HwUnconfigured, none)); + REQUIRE(cfg.trigger == UartTrigger::None); +} + +TEST_CASE("set_trigger applies when authorized", "[uart][REQ-UART-045]") { + UartFunctionalCfg cfg; + functional_cfg_init(cfg); + rcp::lifecycle::WriterCtx writer; + writer.via_owning_stream = true; + + REQUIRE(set_trigger(cfg, UartTrigger::RxFinalized, rcp::lifecycle::ServerState::HwConfigured, writer)); + REQUIRE(cfg.trigger == UartTrigger::RxFinalized); +} + +// ── The EP_func register block (§13.7.8.2 Table 51) ────────────────────────── + +TEST_CASE("render_registers matches Table 51's own offsets", "[uart][REQ-UART-036][REQ-UART-038]") { + UartFunctionalCfg cfg; + functional_cfg_init(cfg); + cfg.ep_enable = true; + cfg.ep_status = 0x1234; + cfg.baud_rate_kbps = 0x5566; + cfg.uart_nr_bits = 7; + cfg.parity = static_cast(Parity::Even); + cfg.rts_enable = true; + cfg.cts_enable = true; + cfg.half_duplex = true; + cfg.stop_bits = static_cast(StopBits::Two); + cfg.wire_timeout_bit_times = 9; + cfg.trail = 10; + + EpFuncBlock out{}; + render_registers(cfg, out); + + 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(out[kRegBaudRate] == 0x55); + REQUIRE(out[kRegBaudRate + 1] == 0x66); + REQUIRE(out[kRegNrBits] == 7); + REQUIRE(out[kRegFlags] == (kFlagParityEnable | kFlagParityPol | kFlagRtsEnable | kFlagCtsEnable | + kFlagHalfDuplex)); + REQUIRE(out[kRegStopBits] == 4); // TWO -> half units 4 + REQUIRE(out[kRegTimeout] == 9); + REQUIRE(out[kRegTrail] == 10); + REQUIRE(kEpFuncLen == 0x000Du); +} + +TEST_CASE("apply_reconfig writes a multi-register span", "[uart][REQ-UART-039][REQ-UART-040]") { + UartFunctionalCfg cfg; + functional_cfg_init(cfg); + const uint8_t payload[9] = { + 0x00, static_cast(kRegBaudRate), + 0xAB, 0xCD, // baud_rate_kbps + 6, // nr_bits + static_cast(kFlagParityEnable | kFlagRtsEnable), // odd parity, RTS + 4, // stop_bits half units -> TWO + 11, // timeout + 12, // trail + }; + + REQUIRE_FALSE(apply_reconfig(cfg, payload, sizeof(payload))); + REQUIRE(cfg.baud_rate_kbps == 0xABCD); + REQUIRE(cfg.uart_nr_bits == 6); + REQUIRE(cfg.parity == static_cast(Parity::Odd)); + REQUIRE(cfg.rts_enable); + REQUIRE_FALSE(cfg.cts_enable); + REQUIRE_FALSE(cfg.half_duplex); + REQUIRE(cfg.stop_bits == static_cast(StopBits::Two)); + REQUIRE(cfg.wire_timeout_bit_times == 11); + REQUIRE(cfg.trail == 12); +} + +// REQ-UART-037/049: the real, three-value StopBits mapping — ONE_HALF (wire +// value 3) is now exact, not rounded up to TWO; only an out-of-range value +// (5) still falls back to the conservative TWO default. +TEST_CASE("apply_reconfig maps all three legal stop_bits register values exactly", + "[uart][REQ-UART-049]") { + UartFunctionalCfg cfg; + + const uint8_t payload_one[3] = {0x00, static_cast(kRegStopBits), 2}; + functional_cfg_init(cfg); + REQUIRE_FALSE(apply_reconfig(cfg, payload_one, sizeof(payload_one))); + REQUIRE(cfg.stop_bits == static_cast(StopBits::One)); + + const uint8_t payload_one_half[3] = {0x00, static_cast(kRegStopBits), 3}; + functional_cfg_init(cfg); + REQUIRE_FALSE(apply_reconfig(cfg, payload_one_half, sizeof(payload_one_half))); + REQUIRE(cfg.stop_bits == static_cast(StopBits::OneHalf)); + + const uint8_t payload_two[3] = {0x00, static_cast(kRegStopBits), 4}; + functional_cfg_init(cfg); + REQUIRE_FALSE(apply_reconfig(cfg, payload_two, sizeof(payload_two))); + REQUIRE(cfg.stop_bits == static_cast(StopBits::Two)); + + const uint8_t payload_out_of_range[3] = {0x00, static_cast(kRegStopBits), 5}; + functional_cfg_init(cfg); + REQUIRE_FALSE(apply_reconfig(cfg, payload_out_of_range, sizeof(payload_out_of_range))); + REQUIRE(cfg.stop_bits == static_cast(StopBits::Two)); +} + +TEST_CASE("render_registers: StopBits::OneHalf renders as the distinct register value 3", + "[uart][REQ-UART-049]") { + UartFunctionalCfg cfg; + functional_cfg_init(cfg); + cfg.stop_bits = static_cast(StopBits::OneHalf); + + EpFuncBlock out{}; + render_registers(cfg, out); + REQUIRE(out[kRegStopBits] == 3); +} + +TEST_CASE("apply_reconfig ignores read-only registers (EP_LEN/reserved)", "[uart][REQ-UART-040]") { + UartFunctionalCfg cfg; + functional_cfg_init(cfg); + const uint8_t payload[4] = {0x00, 0x00, 0xFF, 0xFF}; + + REQUIRE_FALSE(apply_reconfig(cfg, payload, sizeof(payload))); + + EpFuncBlock out{}; + render_registers(cfg, out); + REQUIRE(out[kRegEpLen] == static_cast(kEpFuncLen)); + REQUIRE(out[kRegReserved01] == 0); +} + +TEST_CASE("apply_reconfig rejects a write extending past EP_LEN", "[uart][REQ-UART-040]") { + UartFunctionalCfg cfg; + functional_cfg_init(cfg); + const uint8_t payload[3] = {0x00, static_cast(kEpFuncLen), 0xFF}; + + REQUIRE(apply_reconfig(cfg, payload, sizeof(payload)) == make_error_code(UartReconfigErrc::out_of_range)); + REQUIRE(cfg.trail == 0); +} + +TEST_CASE("apply_reconfig rejects a payload with no data octet", "[uart][REQ-UART-040]") { + UartFunctionalCfg cfg; + functional_cfg_init(cfg); + const uint8_t addr_only[2] = {0x00, 0x06}; + + REQUIRE(apply_reconfig(cfg, addr_only, sizeof(addr_only)) == + make_error_code(UartReconfigErrc::short_payload)); + REQUIRE(apply_reconfig(cfg, nullptr, 0) == make_error_code(UartReconfigErrc::short_payload)); +} + +TEST_CASE("encode_reconfig_request round-trips through acf::decode_acf_abb", "[uart][REQ-UART-039]") { + const std::vector data{0xAB, 0xCD}; + const auto frame = encode_reconfig_request(0x03, 0x0006, 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, 0x06, 0xAB, 0xCD}); +} + +TEST_CASE("encode_reconfig_request rejects empty data", "[uart][REQ-UART-039]") { + REQUIRE(encode_reconfig_request(0x00, 0, {}, 0).empty()); +} + +TEST_CASE("uart reconfig error category reports a distinct, non-empty message per code", + "[uart][REQ-UART-040]") { + auto short_ec = make_error_code(UartReconfigErrc::short_payload); + auto range_ec = make_error_code(UartReconfigErrc::out_of_range); + REQUIRE_FALSE(short_ec.message().empty()); + REQUIRE_FALSE(range_ec.message().empty()); + REQUIRE(short_ec.message() != range_ec.message()); +} + +// ── UartErrc category sanity (full, extended enum) ──────────────────────────── + +TEST_CASE("UartErrc reports a non-empty, distinct message per code", "[uart][REQ-UART-017]") { + const UartErrc codes[] = { + UartErrc::read_size_exceeds_bound, UartErrc::rx_fifo_overflow, + UartErrc::tx_queue_overflow, UartErrc::bits_per_frame_out_of_range, + UartErrc::config_write_not_supported, UartErrc::short_frame, + UartErrc::bad_msg_type, UartErrc::wrong_bus, + UartErrc::wrong_op, UartErrc::unknown_cmd, + UartErrc::bad_evt, + }; + std::vector seen; + for (auto c : codes) { + auto ec = make_error_code(c); + REQUIRE(ec.category() == uart_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()); +} + +TEST_CASE("wire_error maps bad_evt/unknown_cmd to UnsupportedCmd", "[uart][REQ-UART-020]") { + REQUIRE(wire_error(UartErrc::bad_evt) == rcp::acf::WireErrorCode::UnsupportedCmd); + REQUIRE(wire_error(UartErrc::unknown_cmd) == rcp::acf::WireErrorCode::UnsupportedCmd); + REQUIRE_FALSE(wire_error(UartErrc::short_frame).has_value()); +} + +// ── TX: write request/response ────────────────────────────────────────────── ── + +TEST_CASE("encode_write_request / decode_write_request round-trip", "[uart][REQ-UART-018][REQ-UART-019]") { + const std::vector tx{0x01, 0x02, 0x03}; + const auto frame = encode_write_request(4, tx, 9); + REQUIRE_FALSE(frame.empty()); + + std::vector out_tx; + uint8_t txn = 0; + REQUIRE_FALSE(decode_write_request(frame.data(), frame.size(), 4, out_tx, txn)); + REQUIRE(out_tx == tx); + REQUIRE(txn == 9); +} + +TEST_CASE("decode_write_request rejects wrong_bus/wrong_op/bad_msg_type/short_frame", + "[uart][REQ-UART-020]") { + const std::vector tx{0xAB}; + const auto wrong_bus_frame = encode_write_request(4, tx, 0); + std::vector out_tx; + uint8_t txn = 0; + REQUIRE(decode_write_request(wrong_bus_frame.data(), wrong_bus_frame.size(), 5, out_tx, txn) == + make_error_code(UartErrc::wrong_bus)); + + rcp::acf::AcfMessageInfo wrong_op_hdr; + wrong_op_hdr.byte_bus_id = 4; + wrong_op_hdr.op = false; // read, not write + const auto wrong_op_frame = rcp::acf::encode_acf_abb(wrong_op_hdr, {}); + REQUIRE(decode_write_request(wrong_op_frame.data(), wrong_op_frame.size(), 4, out_tx, txn) == + make_error_code(UartErrc::wrong_op)); + + rcp::acf::AcfMessageInfo bad_type_hdr; + bad_type_hdr.byte_bus_id = 4; + bad_type_hdr.op = true; + const auto bad_type_frame = rcp::acf::encode_acf_gbb(bad_type_hdr, 0, {}); + REQUIRE(decode_write_request(bad_type_frame.data(), bad_type_frame.size(), 4, out_tx, txn) == + make_error_code(UartErrc::bad_msg_type)); + + const uint8_t too_short[2] = {0x1C, 0x01}; // valid ACF_ABB type marker, too short for the full header + REQUIRE(decode_write_request(too_short, sizeof(too_short), 4, out_tx, txn) == + make_error_code(UartErrc::short_frame)); +} + +TEST_CASE("decode_write_request rejects a nonzero evt[2:0]", "[uart][REQ-UART-020]") { + rcp::acf::AcfMessageInfo hdr; + hdr.byte_bus_id = 4; + hdr.op = true; + hdr.evt_op = 0x6; // reserved in UART's own Table 33 row + const auto frame = rcp::acf::encode_acf_abb(hdr, {}); + + std::vector out_tx; + uint8_t txn = 0; + REQUIRE(decode_write_request(frame.data(), frame.size(), 4, out_tx, txn) == + make_error_code(UartErrc::bad_evt)); +} + +TEST_CASE("encode_write_response / decode_write_response round-trip untimed and timed", + "[uart][REQ-UART-021][REQ-UART-022]") { + const std::vector accepted{0x55, 0x66}; + const auto untimed = encode_write_response(3, accepted, 4, false, 0); + const auto timed_frame = encode_write_response(3, accepted, 4, true, 0xAABBCCDDull); + + std::vector out_accepted; + bool timed = true; + uint64_t ts = 1; + uint8_t txn = 0; + REQUIRE_FALSE(decode_write_response(untimed.data(), untimed.size(), 3, out_accepted, timed, ts, txn)); + REQUIRE(out_accepted == accepted); + REQUIRE_FALSE(timed); + REQUIRE(ts == 0); + + REQUIRE_FALSE(decode_write_response(timed_frame.data(), timed_frame.size(), 3, out_accepted, timed, ts, txn)); + REQUIRE(timed); + REQUIRE(ts == 0xAABBCCDDull); + REQUIRE(txn == 4); +} + +TEST_CASE("decode_write_response rejects wrong_bus and short_frame", "[uart][REQ-UART-046]") { + const auto frame = encode_write_response(3, {}, 0, false, 0); + std::vector out_accepted; + bool timed = false; + uint64_t ts = 0; + uint8_t txn = 0; + REQUIRE(decode_write_response(frame.data(), frame.size(), 9, out_accepted, timed, ts, txn) == + make_error_code(UartErrc::wrong_bus)); + + const uint8_t too_short[2] = {static_cast(rcp::acf::kAcfMsgTypeAbb << 1), 0}; + REQUIRE(decode_write_response(too_short, sizeof(too_short), 3, out_accepted, timed, ts, txn) == + make_error_code(UartErrc::short_frame)); +} + +// ── RX: read request/response ─────────────────────────────────────────────── ── + +TEST_CASE("encode_read_request / decode_read_request round-trip", "[uart][REQ-UART-023][REQ-UART-024]") { + const auto frame = encode_read_request(6, 64, 3); + REQUIRE_FALSE(frame.empty()); + + uint16_t read_size = 0; + uint8_t txn = 0; + REQUIRE_FALSE(decode_read_request(frame.data(), frame.size(), 6, read_size, txn)); + REQUIRE(read_size == 64); + REQUIRE(txn == 3); +} + +// FIXED (REQ-UART-034): a read_size above 255 — previously inexpressible had +// this parameter been narrowed to uint8_t — round-trips through the ACF +// header's real 12-bit field. +TEST_CASE("encode_read_request / decode_read_request round-trip a read_size above 255", + "[uart][REQ-UART-023][REQ-UART-024][REQ-UART-034]") { + const auto frame = encode_read_request(6, 4000u, 3); + REQUIRE_FALSE(frame.empty()); + + uint16_t read_size = 0; + uint8_t txn = 0; + REQUIRE_FALSE(decode_read_request(frame.data(), frame.size(), 6, read_size, txn)); + REQUIRE(read_size == 4000u); + REQUIRE(txn == 3); +} + +TEST_CASE("decode_read_request rejects a payload-bearing read request with unknown_cmd", + "[uart][REQ-UART-025]") { + rcp::acf::AcfMessageInfo hdr; + hdr.byte_bus_id = 6; + hdr.op = false; + hdr.read_size_or_segment_num = 8; + const auto frame = rcp::acf::encode_acf_abb(hdr, {0x01}); + + uint16_t read_size = 0; + uint8_t txn = 0; + REQUIRE(decode_read_request(frame.data(), frame.size(), 6, read_size, txn) == + make_error_code(UartErrc::unknown_cmd)); +} + +TEST_CASE("decode_read_request rejects wrong_bus/wrong_op/short_frame", "[uart][REQ-UART-025]") { + const auto wrong_bus = encode_read_request(6, 8, 0); + uint16_t read_size = 0; + uint8_t txn = 0; + REQUIRE(decode_read_request(wrong_bus.data(), wrong_bus.size(), 7, read_size, txn) == + make_error_code(UartErrc::wrong_bus)); + + rcp::acf::AcfMessageInfo wrong_op_hdr; + wrong_op_hdr.byte_bus_id = 6; + wrong_op_hdr.op = true; // write, not read + const auto wrong_op_frame = rcp::acf::encode_acf_abb(wrong_op_hdr, {}); + REQUIRE(decode_read_request(wrong_op_frame.data(), wrong_op_frame.size(), 6, read_size, txn) == + make_error_code(UartErrc::wrong_op)); + + const uint8_t too_short[2] = {0x1C, 0x01}; // valid ACF_ABB type marker, too short for the full header + REQUIRE(decode_read_request(too_short, sizeof(too_short), 6, read_size, txn) == + make_error_code(UartErrc::short_frame)); +} + +TEST_CASE("decode_read_request rejects a nonzero evt[2:0]", "[uart][REQ-UART-025]") { + rcp::acf::AcfMessageInfo hdr; + hdr.byte_bus_id = 6; + hdr.op = false; + hdr.evt_op = 0x5; // reserved in UART's own Table 33 row + const auto frame = rcp::acf::encode_acf_abb(hdr, {}); + + uint16_t read_size = 0; + uint8_t txn = 0; + REQUIRE(decode_read_request(frame.data(), frame.size(), 6, read_size, txn) == + make_error_code(UartErrc::bad_evt)); +} + +TEST_CASE("encode_read_response / decode_read_response round-trip full length", + "[uart][REQ-UART-026][REQ-UART-027]") { + const std::vector rx{0xDE, 0xAD, 0xBE, 0xEF}; + const auto frame = encode_read_response(2, rx, 11, false, 0); + REQUIRE_FALSE(frame.empty()); + + std::vector out_rx; + bool timed = true; + uint64_t ts = 1; + uint8_t txn = 0; + REQUIRE_FALSE(decode_read_response(frame.data(), frame.size(), 2, out_rx, timed, ts, txn)); + REQUIRE(out_rx == rx); + REQUIRE_FALSE(timed); + REQUIRE(txn == 11); +} + +// Worst-case single-AVTPDU short read: the read request asks for read_size +// bytes but the uart_timeout_ms race completes first with fewer bytes +// actually captured — still just one ordinary ACF message, no +// segment_num-based reassembly needed. +TEST_CASE("read response round-trips a short read (fewer bytes than requested)", + "[uart][REQ-UART-026][REQ-UART-027]") { + const auto read_req = encode_read_request(2, 32, 5); + const std::vector rx{0x01, 0x02, 0x03}; // far fewer than the requested 32 + const auto frame = encode_read_response(2, rx, 5, true, 0x1122334455667788ull); + + uint16_t requested_read_size = 0; + uint8_t req_txn = 0; + REQUIRE_FALSE(decode_read_request(read_req.data(), read_req.size(), 2, requested_read_size, req_txn)); + REQUIRE(requested_read_size == 32); + + std::vector out_rx; + bool timed = false; + uint64_t ts = 0; + uint8_t txn = 0; + REQUIRE_FALSE(decode_read_response(frame.data(), frame.size(), 2, out_rx, timed, ts, txn)); + REQUIRE(out_rx == rx); + REQUIRE(out_rx.size() < requested_read_size); + REQUIRE(timed); + REQUIRE(ts == 0x1122334455667788ull); + REQUIRE(txn == 5); +} + +TEST_CASE("decode_read_response rejects wrong_bus and short_frame", "[uart][REQ-UART-028]") { + const auto frame = encode_read_response(2, {}, 0, false, 0); + std::vector out_rx; + bool timed = false; + uint64_t ts = 0; + uint8_t txn = 0; + REQUIRE(decode_read_response(frame.data(), frame.size(), 3, out_rx, timed, ts, txn) == + make_error_code(UartErrc::wrong_bus)); + + const uint8_t too_short[2] = {static_cast(rcp::acf::kAcfMsgTypeAbb << 1), 0}; + REQUIRE(decode_read_response(too_short, sizeof(too_short), 2, out_rx, timed, ts, txn) == + make_error_code(UartErrc::short_frame)); +} + +// ── Read-completion arbitration (REQ-UART-033) ────────────────────────────── +// Not covered by c-RCP's own tests/test_ep_uart.c (that coverage lives in +// c-RCP's tests/test_tc18_gaps_ep2.c's test_uart_read_completion_decision()); +// ported here since read_completion_decision() is a real production delta +// this pass introduces to rcp/uart.hpp for the first time. + +TEST_CASE("read_completion_decision: FIRST trigger — read_size satisfied", "[uart][REQ-UART-033]") { + REQUIRE(read_completion_decision(4, 4, 0, 250, 8) == UartReadCompletion::RespondNormal); +} + +TEST_CASE("read_completion_decision: not yet complete while within the timeout window and fifo short", + "[uart][REQ-UART-033]") { + REQUIRE(read_completion_decision(2, 4, 100, 250, 8) == UartReadCompletion::NotYetComplete); +} + +TEST_CASE("read_completion_decision: SECOND trigger — uart_timeout expired with a short fifo", + "[uart][REQ-UART-033]") { + REQUIRE(read_completion_decision(2, 4, 250, 250, 8) == UartReadCompletion::RespondNormal); +} + +TEST_CASE("read_completion_decision: THIRD trigger — read_size exceeds rx_fifo_size and fifo is full", + "[uart][REQ-UART-033]") { + REQUIRE(read_completion_decision(8, 20, 0, 250, 8) == UartReadCompletion::RespondFragmented); +} + +TEST_CASE("read_completion_decision: not yet complete while the oversized read's fifo has not yet filled", + "[uart][REQ-UART-033]") { + REQUIRE(read_completion_decision(3, 20, 50, 250, 8) == UartReadCompletion::NotYetComplete); +} + +TEST_CASE("read_completion_decision: uart_timeout_ms == 0 completes immediately", "[uart][REQ-UART-033]") { + REQUIRE(read_completion_decision(0, 4, 0, 0, 8) == UartReadCompletion::RespondNormal); +} + +// ── Fragmented read response (Phase 20, rcp/fragment.hpp) ──────────────────── + +TEST_CASE("read_response_fragment_count is 1 when the payload already fits", "[uart][REQ-UART-029]") { + REQUIRE(read_response_fragment_count(10, 100) == 1); + REQUIRE(read_response_fragment_count(0, 0) == 1); +} + +TEST_CASE("encode_read_response_fragmented, when unfragmented, matches encode_read_response exactly", + "[uart][REQ-UART-030]") { + const std::vector rx{0x11, 0x22, 0x33}; + const auto plain = encode_read_response(6, rx, 12, false, 0); + REQUIRE_FALSE(plain.empty()); + + const auto fragmented = encode_read_response_fragmented(6, rx, 12, false, 0, 255); + REQUIRE(fragmented.size() == 1); + REQUIRE(fragmented[0] == plain); +} + +// Exercises fragment.hpp's ms/segment_num mechanism against this endpoint's +// own wire codec end-to-end, using a deliberately small max_fragment_payload. +TEST_CASE("encode_read_response_fragmented / decode_read_response_fragment round-trip via a " + "Reassembler with a small cap", + "[uart][REQ-UART-030][REQ-UART-031]") { + std::vector rx(20); + for (size_t i = 0; i < rx.size(); ++i) rx[i] = static_cast(100 + i); + + const size_t max_fragment_payload = 6; + REQUIRE(read_response_fragment_count(rx.size(), max_fragment_payload) == 4); // ceil(20/6) + + const auto frames = + encode_read_response_fragmented(3, rx, 66, true, 0x0102030405060708ull, max_fragment_payload); + REQUIRE(frames.size() == 4); + + rcp::fragment::Reassembler reasm(rx.size()); + for (size_t i = 0; i < frames.size(); ++i) { + bool ms = false; + uint16_t segnum = 0; + std::vector payload; + bool timed = false; + uint64_t ts = 0; + uint8_t txn = 0; + + REQUIRE_FALSE(decode_read_response_fragment(frames[i].data(), frames[i].size(), 3, ms, segnum, + payload, timed, ts, txn)); + REQUIRE(txn == 66); + REQUIRE(timed); + REQUIRE(ts == 0x0102030405060708ull); + + const auto rc = reasm.feed(ms, segnum, payload.empty() ? nullptr : payload.data(), payload.size()); + if (i + 1 < frames.size()) { + REQUIRE(rc == rcp::fragment::ReasmResult::kContinue); + } else { + REQUIRE(rc == rcp::fragment::ReasmResult::kComplete); + } + } + + REQUIRE(reasm.size() == rx.size()); + REQUIRE(std::vector(reasm.data(), reasm.data() + reasm.size()) == rx); +} + +TEST_CASE("decode_read_response_fragment rejects short_frame/bad_msg_type/wrong_bus", + "[uart][REQ-UART-047]") { + bool ms = false; + uint16_t segnum = 0; + std::vector payload; + bool timed = false; + uint64_t ts = 0; + uint8_t txn = 0; + + const uint8_t too_short[1] = {0}; + REQUIRE(decode_read_response_fragment(too_short, 0, 5, ms, segnum, payload, timed, ts, txn) == + make_error_code(UartErrc::short_frame)); + + uint8_t bad_type[rcp::acf::kAcfCommonHeaderLen] = {0}; + bad_type[0] = static_cast(0x01u << 1); // neither ACF_ABB nor ACF_GBB + REQUIRE(decode_read_response_fragment(bad_type, sizeof(bad_type), 5, ms, segnum, payload, timed, ts, + txn) == make_error_code(UartErrc::bad_msg_type)); + + const std::vector rx{0x01, 0x02}; + const auto frame = encode_read_response(2, rx, 1, false, 0); + REQUIRE(decode_read_response_fragment(frame.data(), frame.size(), 9, ms, segnum, payload, timed, ts, + txn) == make_error_code(UartErrc::wrong_bus)); +} + +TEST_CASE("encode_read_response_fragmented is disabled for a zero cap with an oversized payload", + "[uart][REQ-UART-030]") { + const std::vector rx{1, 2, 3, 4}; + const auto frames = encode_read_response_fragmented(3, rx, 1, false, 0, 0); + REQUIRE(frames.empty()); +} + +// ── wire_timeout_us (REQ-UART-037, issue #341 lineage) ─────────────────────── + +// At 3 kbit/s, one bit period is 1000/3 = 333.33...us; 10 bit periods is +// 3333.33...us, which ceilings to 3334 — proves the rounding is genuinely UP. +TEST_CASE("wire_timeout_us computes the ceiling of the bit-period count", "[uart][REQ-UART-037]") { + REQUIRE(wire_timeout_us(3, 10) == 3334u); +} + +TEST_CASE("wire_timeout_us has no off-by-one when the division is exact", "[uart][REQ-UART-037]") { + REQUIRE(wire_timeout_us(1000, 10) == 10u); +} + +TEST_CASE("wire_timeout_us fails open (returns 0) with no configured baud rate", "[uart][REQ-UART-037]") { + REQUIRE(wire_timeout_us(0, 10) == 0u); + REQUIRE(wire_timeout_us(0, 0) == 0u); +} + +TEST_CASE("wire_timeout_us: zero bit times converts to zero microseconds", "[uart][REQ-UART-037]") { + REQUIRE(wire_timeout_us(9600, 0) == 0u); +} + +TEST_CASE("wire_timeout_us: the maximum representable inputs do not overflow uint32_t", + "[uart][REQ-UART-037]") { + REQUIRE(wire_timeout_us(1, 255) == 255000u); // 255 bit periods at 1 kbit/s: exactly 255*1000us +}