diff --git a/.fusa-reqs.json b/.fusa-reqs.json index 9b7b5b0..c792d59 100644 --- a/.fusa-reqs.json +++ b/.fusa-reqs.json @@ -82,8 +82,8 @@ }, { "id": "REQ-MOCK-001", - "title": "Server starts HW_UNCONFIGURED with a representative two-endpoint register map", - "text": "rcp::mock::Server shall construct with rcp::lifecycle::ServerLifecycle in state HwUnconfigured and an rcp::regmap::RegisterMap whose endpoint_count, generic_configs, functional_configs, and ep_id_mapping describe exactly two endpoints: GPIO at endpoint id/byte_bus_id 1 and SPI at endpoint id/byte_bus_id 2.", + "title": "Server starts HW_UNCONFIGURED with a representative three-endpoint register map", + "text": "rcp::mock::Server shall construct with rcp::lifecycle::ServerLifecycle in state HwUnconfigured and an rcp::regmap::RegisterMap whose endpoint_count, generic_configs, functional_configs, and ep_id_mapping describe exactly three endpoints: GPIO at endpoint id/byte_bus_id 1, SPI at endpoint id/byte_bus_id 2, and I2C at endpoint id/byte_bus_id 3.", "standard": "iso26262", "level": "ASIL-B", "asil": "ASIL-B" @@ -160,6 +160,24 @@ "level": "ASIL-B", "asil": "ASIL-B" }, + { + "id": "REQ-MOCK-011", + "title": "I2C dispatch applies Table 33 Row 2 evt[2:0] classification before touching endpoint state", + "text": "rcp::mock::Server::dispatch, for a request addressed to the I2C endpoint while RcpConfigured, shall decode the request's evt[2:0] field via endpoint::evt_row2_kind_of and drive i2c::I2cEndpoint::handle_request with it: a Plain (000b) request shall be answered as a ReadResponse carrying whatever bytes set_i2c_response last scripted, a Reserved (001b-110b) evt shall be rejected with endpoint::EndpointErrc::reserved_evt_row2 (wire error code UNSUPPORTED_CMD) without recording anything as sent/received, and a ConfigWrite (111b) evt shall be rejected with i2c::I2cErrc::config_write_not_supported (wire error code UNSUPPORTED_CMD) rather than crashing or being treated as a plain transfer.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "tc18": "§13.5 Table 33 (\"ADC, PWM_IN, I²C, LIN, CAN, UART, ISELED, MDIO: 000b to 110b reserved – request to be rejected with error code = UNSUPPORTED_CMD\" / \"111b: The byte_msg_payload is not presented to the interface but used to change the configuration of the endpoint (see 12.7.1)\"), TC18.txt L4085-4092; corroborating worked example resolving the evt=000b ambiguity in Table 33's own favor: §13.7.9.3 Figure 34 (\"RC Client sends a standard read request\", ADC endpoint, evt field all-zero), TC18.txt L5590-5607", + "tc18_master_id": "TC18-13.5-001" + }, + { + "id": "REQ-MOCK-012", + "title": "Operational I2C requests are rejected before RCP_CONFIGURED, same gating as GPIO/SPI", + "text": "rcp::mock::Server::dispatch, for a request addressed to the I2C endpoint while the server's lifecycle state is not RcpConfigured, shall return regmap::RegMapErrc::request_rejected and an ErrorResponse without touching the endpoint's live state, the same operational-request gating REQ-MOCK-006 already applies to GPIO/SPI.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B" + }, { "id": "REQ-UDP-001", "title": "Frame round-trips an NTSCF-framed ACF_ABB request", @@ -2500,6 +2518,17 @@ "tc18": "§11.2.2.5 (\"Each endpoint may generate one or more trigger signals.\"), TC18.txt L1689", "tc18_master_id": "TC18-11.2.2.5-002" }, + { + "id": "REQ-ENDPOINT-007", + "title": "evt[2:0] Row 2 (ADC/PWM_IN/I2C/LIN/CAN/UART/ISELED/MDIO) classification", + "text": "rcp::endpoint::evt_row2_kind_of shall classify evt_op values, masked to the low 3 bits first: 000b as EvtRow2Kind::Plain, 111b as EvtRow2Kind::ConfigWrite, and every other value (001b-110b) as EvtRow2Kind::Reserved, per TC18 §13.5 Table 33's shared rule for the {ADC, PWM_IN, I2C, LIN, CAN, UART, ISELED, MDIO} endpoint-type row. EndpointErrc::reserved_evt_row2 shall report a non-empty diagnostic message in its own error_category. RESOLVED AMBIGUITY: Table 33's own literal text groups evt[2:0]=000b together with 001b-110b as a single \"reserved -- reject\" range for this row, which read in isolation would make evt=000b illegal too, leaving every endpoint type in this row permanently unusable for ordinary requests. That reading is contradicted by TC18's own §13.7.9.3 worked example for ADC (a member of this row) showing evt[2:0]=000b used in an ordinary standard read request that succeeds normally. This implementation follows the worked example (evt=000b is the ordinary, successful case), the same resolution c-RCP's rcp_acf_evt_row2_is_plain() already shipped for this identical ambiguity.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "tc18": "§13.5 Table 33 (\"ADC, PWM_IN, I²C, LIN, CAN, UART, ISELED, MDIO: 000b to 110b reserved – request to be rejected with error code = UNSUPPORTED_CMD\" / \"111b: The byte_msg_payload is not presented to the interface but used to change the configuration of the endpoint (see 12.7.1)\"), TC18.txt L4085-4092; corroborating worked example: §13.7.9.3 Figure 34 (\"RC Client sends a standard read request\", ADC endpoint, evt field all-zero), TC18.txt L5590-5607", + "tc18_master_id": "TC18-13.5-001", + "severity": "cybersecurity" + }, { "id": "REQ-GPIO-001", "title": "GPIO payload is a 4-byte big-endian bitmask", @@ -2678,6 +2707,26 @@ "level": "ASIL-B", "asil": "ASIL-B" }, + { + "id": "REQ-I2C-006", + "title": "I2cEndpoint::handle_request applies Table 33 Row 2 evt[2:0] classification before transferring", + "text": "rcp::i2c::I2cEndpoint::handle_request shall classify its evt_op argument via rcp::endpoint::evt_row2_kind_of (masking to the low 3 bits first) and, for EvtRow2Kind::Plain, shall delegate to transfer() with out_bytes/in_bytes/acked unchanged; for EvtRow2Kind::Reserved, shall return endpoint::EndpointErrc::reserved_evt_row2 without recording anything as sent or received.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "tc18": "§13.5 Table 33 (\"ADC, PWM_IN, I²C, LIN, CAN, UART, ISELED, MDIO: 000b to 110b reserved – request to be rejected with error code = UNSUPPORTED_CMD\"), TC18.txt L4085-4092; corroborating worked example: §13.7.9.3 Figure 34 (\"RC Client sends a standard read request\", ADC endpoint, evt field all-zero), TC18.txt L5590-5607", + "tc18_master_id": "TC18-13.5-001" + }, + { + "id": "REQ-I2C-007", + "title": "I2cEndpoint::handle_request reports config-write (evt[2:0]==111b) as not yet supported", + "text": "rcp::i2c::I2cEndpoint::handle_request, for EvtRow2Kind::ConfigWrite (evt[2:0]==111b), shall return I2cErrc::config_write_not_supported without recording anything as sent or received and without crashing, rather than silently accepting the request as an ordinary transfer or silently doing nothing; full §12.7.1 configuration-write handling is deliberately deferred to a follow-up milestone. I2cErrc::config_write_not_supported shall report a non-empty diagnostic message in its own error_category.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "tc18": "§12.7.1 (\"This can be done by setting evt[2:0] = 111b in request as described above.\"; Figure 19 – \"Configuration request\"; \"The data of the byte_msg_payload from a write request is written into the section of the EP that is addressed by the byte_bus_id.\"), TC18.txt L2844-2865", + "tc18_master_id": "TC18-12.7.1-001" + }, { "id": "REQ-UART-001", "title": "UART TX queue is independent of the RX FIFO", diff --git a/include/rcp/endpoint.hpp b/include/rcp/endpoint.hpp index 57b339b..78092a1 100644 --- a/include/rcp/endpoint.hpp +++ b/include/rcp/endpoint.hpp @@ -4,6 +4,7 @@ // fusa:req REQ-ENDPOINT-004 // fusa:req REQ-ENDPOINT-005 // fusa:req REQ-ENDPOINT-006 +// fusa:req REQ-ENDPOINT-007 // Shared endpoint-registration and request-dispatch scaffolding — the pieces // every concrete OPEN Alliance TC18 Remote Control Protocol Specification @@ -114,6 +115,50 @@ constexpr WriteSemantics write_semantics_of(uint8_t evt_op) noexcept { return static_cast(evt_op & 0x07); } +// ── evt[2:0] Row 2 classification (Table 33's ADC/PWM_IN/I2C/LIN/CAN/UART/ ── +// ISELED/MDIO row) ────────────────────────────────────────────────────────── +// The 3-way evt[2:0] classification shared by every endpoint type in Table +// 33's second row (extraction §13.5): I2C (this milestone, rcp/i2c.hpp) and, +// in later milestones, ADC, PWM_IN, LIN, CAN, UART, ISELED, and MDIO. Unlike +// WriteSemantics above (GPIO/PWM_OUT's own row) and spi::channel_of (SPI's +// own dedicated row), this row's evt[2:0] carries no combinable value or +// channel selector at all: 000b is the only value a plain (non- +// configuration) request in this row may carry, 001b-110b are reserved (a +// request carrying one must be rejected with error code UNSUPPORTED_CMD, +// never silently treated as a no-op or as if it were 000b), and 111b +// selects an entirely different, configuration-write-shaped request +// (§12.7.1) that a plain read/write decoder must never accept either. +// +// RESOLVED AMBIGUITY: Table 33's own literal text groups evt[2:0]=000b +// together with 001b-110b as a single "reserved — reject" range for this +// row, which read in isolation would make evt=000b illegal too, leaving +// every endpoint type in this row permanently unusable for ordinary +// requests. That reading is contradicted by TC18's own §13.7.9.3 worked +// example for ADC (a member of this row) showing evt[2:0]=000b used in an +// ordinary standard read request (Figure 34's own bit-level evt field is +// all zero) that succeeds normally, not rejected. This implementation +// follows the worked example — evt=000b is the ordinary, successful case — +// the same resolution c-RCP's rcp_acf_evt_row2_is_plain() already shipped +// for this identical ambiguity. +// +// This function is deliberately not scoped to I2C: every Row 2 endpoint +// type is expected to call it from its own request-decode entry point, +// same as write_semantics_of above is shared by GPIO and PWM_OUT. +enum class EvtRow2Kind : uint8_t { + Plain = 0, // evt[2:0] == 000b — the only valid plain-request value + ConfigWrite = 1, // evt[2:0] == 111b — §12.7.1 configuration-write shape + Reserved = 2, // every other evt[2:0] value (001b-110b) — reject as UNSUPPORTED_CMD +}; + +// evt_row2_kind_of masks its argument down to the low 3 bits before +// classifying, same discipline as write_semantics_of above. +constexpr EvtRow2Kind evt_row2_kind_of(uint8_t evt_op) noexcept { + const uint8_t masked = static_cast(evt_op & 0x07); + if (masked == 0x00) return EvtRow2Kind::Plain; + if (masked == 0x07) return EvtRow2Kind::ConfigWrite; + return EvtRow2Kind::Reserved; +} + // ── Errors ──────────────────────────────────────────────────────────────────── enum class EndpointErrc : int { @@ -125,6 +170,10 @@ enum class EndpointErrc : int { // endpoint-type-specific — see e.g. gpio::apply_gpio_write) was passed // to it directly. non_combinable_write_semantics = 2, + // evt_row2_kind_of classified a Table 33 Row 2 request's evt[2:0] as + // Reserved (001b-110b) — TC18 requires this be rejected with error code + // UNSUPPORTED_CMD, never silently accepted or treated as Plain. + reserved_evt_row2 = 3, }; inline const std::error_category& endpoint_category() noexcept { @@ -136,6 +185,8 @@ inline const std::error_category& endpoint_category() noexcept { return "rcp/endpoint: evt[2:0]=4 (Reserved) is not a valid write semantics"; case EndpointErrc::non_combinable_write_semantics: return "rcp/endpoint: write semantics does not combine via apply_bitmask_write"; + case EndpointErrc::reserved_evt_row2: + return "rcp/endpoint: evt[2:0] is a reserved Table 33 Row 2 value (001b-110b)"; default: return "rcp/endpoint: unknown error"; } diff --git a/include/rcp/i2c.hpp b/include/rcp/i2c.hpp index 2a1719c..191020e 100644 --- a/include/rcp/i2c.hpp +++ b/include/rcp/i2c.hpp @@ -3,6 +3,8 @@ // fusa:req REQ-I2C-003 // fusa:req REQ-I2C-004 // fusa:req REQ-I2C-005 +// fusa:req REQ-I2C-006 +// fusa:req REQ-I2C-007 // I2C endpoint (ep_type 0x04) — controller-only raw byte-stream transfer // (including address bytes, per the OPEN Alliance TC18 Remote Control @@ -19,6 +21,15 @@ // the target device address travels inside the raw byte stream itself // rather than as a separate selector field. // +// Table 30/33 Row 2 evt[2:0] validation pilot (post-v2.4.0): I2cEndpoint's +// handle_request is this repo's first wiring of +// rcp::endpoint::evt_row2_kind_of — the shared 3-way evt[2:0] classifier +// for Table 33's {ADC, PWM_IN, I2C, LIN, CAN, UART, ISELED, MDIO} row — into +// a concrete endpoint type's request decode. The other seven Row 2 endpoint +// types are expected to add their own handle_request-shaped entry point +// calling the same rcp::endpoint::evt_row2_kind_of in their own follow-up +// PRs, matching this shape. +// // OPEN ITEM, called out explicitly per the roadmap rather than guessed at: // the extraction leaves the mapping of I2C's `i2c_mode` field (device // speed grade — Standard/Fast/Fast-mode-Plus/High-Speed) onto wire values @@ -68,6 +79,13 @@ constexpr I2cMode i2c_mode_of(bool hs) noexcept { enum class I2cErrc : int { nack = 1, // the addressed device did not acknowledge (controller-only view) + // evt_row2_kind_of classified the request as ConfigWrite (evt[2:0] == + // 111b, §12.7.1). This milestone's pilot deliberately does not + // implement the configuration-write shape (relative EP_functional- + // config start address + configuration data) — see handle_request's own + // comment. Reported explicitly rather than silently accepted as a plain + // transfer or silently ignored. + config_write_not_supported = 2, }; inline const std::error_category& i2c_category() noexcept { @@ -76,7 +94,9 @@ inline const std::error_category& i2c_category() noexcept { std::string message(int ev) const override { switch (static_cast(ev)) { case I2cErrc::nack: return "rcp/i2c: addressed device did not acknowledge (NACK)"; - default: return "rcp/i2c: unknown error"; + case I2cErrc::config_write_not_supported: + return "rcp/i2c: evt[2:0]=111b configuration-write requests are not yet implemented"; + default: return "rcp/i2c: unknown error"; } } }; @@ -153,6 +173,46 @@ class I2cEndpoint { return {}; } + // handle_request is I2C's request-decode entry point — the piece this + // header previously had none of (issue: cpp-RCP had zero Table 33 Row 2 + // evt[2:0] validation anywhere). It classifies the incoming request's + // evt[2:0] field via rcp::endpoint::evt_row2_kind_of (Table 33's shared + // ADC/PWM_IN/I2C/LIN/CAN/UART/ISELED/MDIO rule) before doing anything + // else, so a Reserved value can never reach transfer() and be misread + // as an ordinary transfer, and a ConfigWrite value can never be + // silently accepted or silently dropped: + // - Plain (evt[2:0] == 000b): delegates straight to transfer() with + // `out_bytes`/`in_bytes`/`acked` unchanged — I2C's existing + // controller-only raw byte-stream transfer model (extraction + // §13.7.7.3) already IS this row's correct "plain request" + // behavior; evt[2:0] carries no combinable value or channel + // selector for this row the way it does for GPIO/PWM_OUT or SPI. + // - Reserved (evt[2:0] in 001b-110b): returns + // endpoint::EndpointErrc::reserved_evt_row2 without touching any + // endpoint state or recording anything as sent/received — TC18 + // requires this be rejected with error code UNSUPPORTED_CMD. + // - ConfigWrite (evt[2:0] == 111b): §12.7.1's configuration-write + // shape targets the I2C EP's own functional-config block (relative + // start address + configuration data), not a bus transfer at all. + // Full handling is deliberately out of scope for this milestone's + // pilot (nontrivial — it needs EP_functional-config wiring this + // header does not yet have); this returns + // I2cErrc::config_write_not_supported rather than crashing, + // silently accepting the request as a transfer, or silently doing + // nothing. + std::error_code handle_request(uint8_t evt_op, std::vector out_bytes, + std::vector in_bytes, bool acked = true) { + switch (endpoint::evt_row2_kind_of(evt_op)) { + case endpoint::EvtRow2Kind::Plain: + return transfer(std::move(out_bytes), std::move(in_bytes), acked); + case endpoint::EvtRow2Kind::Reserved: + return endpoint::make_error_code(endpoint::EndpointErrc::reserved_evt_row2); + case endpoint::EvtRow2Kind::ConfigWrite: + return make_error_code(I2cErrc::config_write_not_supported); + } + return endpoint::make_error_code(endpoint::EndpointErrc::reserved_evt_row2); // unreachable + } + const std::vector& last_sent() const noexcept { return last_out_; } const std::vector& last_received() const noexcept { return last_in_; } endpoint::TriggerRegistry& triggers() noexcept { return triggers_; } diff --git a/include/rcp/mock.hpp b/include/rcp/mock.hpp index 415eafe..0286a0d 100644 --- a/include/rcp/mock.hpp +++ b/include/rcp/mock.hpp @@ -8,6 +8,8 @@ // fusa:req REQ-MOCK-008 // fusa:req REQ-MOCK-009 // fusa:req REQ-MOCK-010 +// fusa:req REQ-MOCK-011 +// fusa:req REQ-MOCK-012 // In-process RC Server simulator — a small, representative OPEN Alliance // TC18 Remote Control Protocol Specification v0.5.1_RC server built @@ -33,8 +35,9 @@ // mock::Server holds a real rcp::lifecycle::ServerLifecycle (v2.1.0), a // real rcp::regmap::RegisterMap plus rcp::regmap::Ep0 (v2.1.0, including // EP0 whole-map-read and root-client write semantics), and one instance -// each of the two simplest fully-built endpoint types — rcp::gpio:: -// GpioEndpoint and rcp::spi::SpiEndpoint (both v2.3.0) — as its +// each of three fully-built endpoint types — rcp::gpio::GpioEndpoint and +// rcp::spi::SpiEndpoint (both v2.3.0), plus rcp::i2c::I2cEndpoint (v2.4.0, +// wired in by the Table 30/33 Row 2 evt[2:0] validation pilot) — as its // representative endpoint set. dispatch() below is the single // request/response entry point a test drives, decoding the standard // request kind's evt[2:0]/op fields (rcp/acf.hpp, v2.0.0) the same way a @@ -55,20 +58,21 @@ // 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 endpoint -// numbering (GPIO at endpoint id / byte_bus_id 1, SPI at 2), access-policy -// choice for operational requests (gated on lifecycle state only, not -// per-endpoint ownership — see dispatch()'s own comment), and EP0 partial- -// read encoding chosen in this file are this implementation's own, purely -// for the purposes of being a usable in-process simulator — full +// numbering (GPIO at endpoint id / byte_bus_id 1, SPI at 2, I2C at 3), +// access-policy choice for operational requests (gated on lifecycle state +// only, not per-endpoint ownership — see dispatch()'s own comment), and EP0 +// partial-read encoding chosen in this file are this implementation's own, +// purely for the purposes of being a usable in-process simulator — full // bit-for-bit conformance against other TC18 implementations is not // claimed, same as the equivalent disclaimers in rcp/regmap.hpp, -// rcp/lifecycle.hpp, rcp/gpio.hpp, and rcp/spi.hpp. +// rcp/lifecycle.hpp, rcp/gpio.hpp, rcp/spi.hpp, and rcp/i2c.hpp. #pragma once #include #include #include #include +#include #include #include #include @@ -94,8 +98,10 @@ using regmap::EndpointId; constexpr EndpointId kGpioEndpointId = 1; constexpr EndpointId kSpiEndpointId = 2; +constexpr EndpointId kI2cEndpointId = 3; constexpr avtp::ByteBusId kGpioByteBusId = static_cast(kGpioEndpointId); constexpr avtp::ByteBusId kSpiByteBusId = static_cast(kSpiEndpointId); +constexpr avtp::ByteBusId kI2cByteBusId = static_cast(kI2cEndpointId); // A discovery-shaped EP0 read only ever answers with the register map's // magic number below — see this header's own scope note above. @@ -121,6 +127,11 @@ inline acf::WireErrorCode wire_error_code_for(const std::error_code& ec) noexcep 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 == 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)) + return acf::WireErrorCode::UnsupportedCmd; // evt[2:0]==111b config-write, not yet implemented by this mock + if (ec == make_error_code(i2c::I2cErrc::nack)) return acf::WireErrorCode::EpError; return acf::WireErrorCode::UnsupportedCmd; } @@ -149,6 +160,7 @@ class Server final { gpio::GpioEndpoint& gpio() noexcept { return gpio_; } spi::SpiEndpoint& spi() noexcept { return spi_; } + i2c::I2cEndpoint& i2c() noexcept { return i2c_; } // set_spi_poci scripts the bytes a subsequent dispatch()/transfer() // call on `channel` reads back as POCI-in data. A real SPI peripheral's @@ -159,6 +171,17 @@ class Server final { if (channel < spi::kMaxChannels) spi_poci_[channel] = std::move(data); } + // set_i2c_response scripts the bytes and ack/nack outcome a subsequent + // dispatch()-driven I2C transfer reads back — the same "test scripts + // the bus, this mock does not model actual hardware" pattern + // set_spi_poci above already establishes. Applies to the next plain + // (evt[2:0]==000b) I2C request only in spirit — like SPI's POCI script, + // it stays in effect until overwritten, there is no auto-clear. + void set_i2c_response(std::vector data, bool acked = true) { + i2c_response_ = std::move(data); + i2c_acked_ = acked; + } + // advance_to_rcp_configured is a convenience for tests/simulators that // don't care about exercising ServerLifecycle's intermediate // plausibility-check gating themselves and just want a fully live @@ -194,6 +217,7 @@ class Server final { if (req.byte_bus_id == regmap::kEp0) return dispatch_ep0(client, req, out_resp, out_resp_payload); if (req.byte_bus_id == kGpioByteBusId) return dispatch_gpio(req, req_payload, out_resp, out_resp_payload); if (req.byte_bus_id == kSpiByteBusId) return dispatch_spi(req, req_payload, out_resp, out_resp_payload); + if (req.byte_bus_id == kI2cByteBusId) return dispatch_i2c(req, req_payload, out_resp, out_resp_payload); return set_error_response(req, make_error_code(regmap::RegMapErrc::invalid_parameter), out_resp, out_resp_payload); @@ -214,12 +238,13 @@ class Server final { static regmap::RegisterMap make_initial_register_map() { regmap::RegisterMap regs; - regs.endpoint_count = 2; - regs.generic_configs.resize(2); - regs.functional_configs.resize(2); + regs.endpoint_count = 3; + regs.generic_configs.resize(3); + regs.functional_configs.resize(3); regs.ep_id_mapping = { {kGpioEndpointId, kGpioByteBusId}, {kSpiEndpointId, kSpiByteBusId}, + {kI2cEndpointId, kI2cByteBusId}, }; return regs; } @@ -295,12 +320,38 @@ class Server final { return {}; } + // I2C, same as SPI (extraction §13.7.7.3), is a raw byte-stream + // transfer rather than a read/write-branched register — this dispatch + // path does not branch on req.op either. Unlike SPI, I2C's evt[2:0] + // does not select a channel; it is Table 33 Row 2's 3-way + // Plain/Reserved/ConfigWrite classification (rcp::endpoint:: + // evt_row2_kind_of), which I2cEndpoint::handle_request checks before + // ever touching `payload` as transfer data — see its own doc comment + // for why a Reserved or ConfigWrite evt must never reach transfer(). + std::error_code dispatch_i2c(const acf::AcfMessageInfo& req, const std::vector& payload, + acf::AcfMessageInfo& out_resp, + std::vector& out_resp_payload) noexcept { + if (!operational_requests_allowed()) { + return set_error_response(req, make_error_code(regmap::RegMapErrc::request_rejected), + out_resp, out_resp_payload); + } + auto ec = i2c_.handle_request(req.evt_op, payload, i2c_response_, i2c_acked_); + if (ec) return set_error_response(req, ec, out_resp, out_resp_payload); + out_resp_payload = i2c_.last_received(); + out_resp = acf::make_response(req, req.evt_ack ? acf::ResponseKind::Acknowledge + : acf::ResponseKind::ReadResponse); + return {}; + } + lifecycle::ServerLifecycle lifecycle_; regmap::RegisterMap regs_; regmap::Ep0 ep0_; gpio::GpioEndpoint gpio_; spi::SpiEndpoint spi_; + i2c::I2cEndpoint i2c_; std::array, spi::kMaxChannels> spi_poci_{}; + std::vector i2c_response_{}; + bool i2c_acked_ = true; }; } // namespace mock diff --git a/tests/test_endpoint.cpp b/tests/test_endpoint.cpp index cae7fad..51925e6 100644 --- a/tests/test_endpoint.cpp +++ b/tests/test_endpoint.cpp @@ -4,6 +4,7 @@ // fusa:test REQ-ENDPOINT-004 // fusa:test REQ-ENDPOINT-005 // fusa:test REQ-ENDPOINT-006 +// fusa:test REQ-ENDPOINT-007 // Tests for rcp/endpoint.hpp — the shared endpoint-registration and // request-dispatch scaffolding (ROADMAP.md milestone 47, "Basic Endpoint @@ -41,6 +42,33 @@ TEST_CASE("write_semantics_of masks its input down to 3 bits", "[endpoint][REQ-E REQUIRE(write_semantics_of(0xF9) == WriteSemantics::Or); // low 3 bits are 001 } +// ── evt[2:0] Row 2 classification (Table 33's ADC/PWM_IN/I2C/LIN/CAN/UART/ ── +// ISELED/MDIO row) ────────────────────────────────────────────────────────── + +TEST_CASE("evt_row2_kind_of classifies all 8 evt[2:0] values", "[endpoint][REQ-ENDPOINT-007]") { + REQUIRE(evt_row2_kind_of(0) == EvtRow2Kind::Plain); + REQUIRE(evt_row2_kind_of(1) == EvtRow2Kind::Reserved); + REQUIRE(evt_row2_kind_of(2) == EvtRow2Kind::Reserved); + REQUIRE(evt_row2_kind_of(3) == EvtRow2Kind::Reserved); + REQUIRE(evt_row2_kind_of(4) == EvtRow2Kind::Reserved); + REQUIRE(evt_row2_kind_of(5) == EvtRow2Kind::Reserved); + REQUIRE(evt_row2_kind_of(6) == EvtRow2Kind::Reserved); + REQUIRE(evt_row2_kind_of(7) == EvtRow2Kind::ConfigWrite); +} + +TEST_CASE("evt_row2_kind_of masks its input down to 3 bits", "[endpoint][REQ-ENDPOINT-007]") { + REQUIRE(evt_row2_kind_of(0xF8) == EvtRow2Kind::Plain); // low 3 bits are 000 + REQUIRE(evt_row2_kind_of(0xF9) == EvtRow2Kind::Reserved); // low 3 bits are 001 + REQUIRE(evt_row2_kind_of(0xFF) == EvtRow2Kind::ConfigWrite); // low 3 bits are 111 +} + +TEST_CASE("EndpointErrc::reserved_evt_row2 reports a non-empty message in its own category", + "[endpoint][REQ-ENDPOINT-007]") { + auto ec = make_error_code(EndpointErrc::reserved_evt_row2); + REQUIRE(ec.category() == endpoint_category()); + REQUIRE_FALSE(ec.message().empty()); +} + // ── Generic bitmask/value write combinator ─────────────────────────────────── TEST_CASE("apply_bitmask_write implements Replace/Or/And/Xor", "[endpoint][REQ-ENDPOINT-003]") { diff --git a/tests/test_i2c.cpp b/tests/test_i2c.cpp index 5050d2d..3eb1cbc 100644 --- a/tests/test_i2c.cpp +++ b/tests/test_i2c.cpp @@ -3,6 +3,8 @@ // fusa:test REQ-I2C-003 // fusa:test REQ-I2C-004 // fusa:test REQ-I2C-005 +// fusa:test REQ-I2C-006 +// fusa:test REQ-I2C-007 // Tests for rcp/i2c.hpp — the I2C endpoint type (ROADMAP.md milestone 48, // "Basic Endpoint Types II — I2C, UART, ADC, PWM_OUT, PWM_IN", v2.4.0). @@ -91,6 +93,51 @@ TEST_CASE("I2cEndpoint::transfer fires only TransferComplete on a normal acked t REQUIRE(drained[0] == i2c_signal_id(I2cSignal::TransferComplete)); } +// ── Table 33 Row 2 evt[2:0] validation (handle_request) ───────────────────── + +TEST_CASE("I2cEndpoint::handle_request delegates a Plain (evt[2:0]==000b) request to transfer()", + "[i2c][REQ-I2C-006]") { + I2cEndpoint ep; + ep.triggers().enable(i2c_signal_id(I2cSignal::TransferComplete)); + + auto ec = ep.handle_request(/*evt_op=*/0, {0xA0, 0x10}, {0xFF}); + REQUIRE_FALSE(ec); + REQUIRE(ep.last_sent() == std::vector{0xA0, 0x10}); + REQUIRE(ep.last_received() == std::vector{0xFF}); + REQUIRE(ep.triggers().drain() == std::vector{ + i2c_signal_id(I2cSignal::TransferComplete)}); +} + +TEST_CASE("I2cEndpoint::handle_request rejects every reserved evt[2:0] value (001b-110b)", + "[i2c][REQ-I2C-006]") { + for (uint8_t evt_op = 1; evt_op <= 6; ++evt_op) { + I2cEndpoint ep; + auto ec = ep.handle_request(evt_op, {0xA0}, {0xFF}); + REQUIRE(ec == rcp::endpoint::make_error_code(rcp::endpoint::EndpointErrc::reserved_evt_row2)); + // A rejected reserved evt must not record anything as sent/received. + REQUIRE(ep.last_sent().empty()); + REQUIRE(ep.last_received().empty()); + } +} + +TEST_CASE("I2cEndpoint::handle_request reports config_write_not_supported for evt[2:0]==111b " + "without crashing or touching transfer state", + "[i2c][REQ-I2C-007]") { + I2cEndpoint ep; + auto ec = ep.handle_request(/*evt_op=*/7, {0x00, 0xAB}, {}); + REQUIRE(ec == make_error_code(I2cErrc::config_write_not_supported)); + REQUIRE(ep.last_sent().empty()); + REQUIRE(ep.last_received().empty()); +} + +TEST_CASE("I2cEndpoint::handle_request masks evt_op down to 3 bits before classifying", + "[i2c][REQ-I2C-006]") { + I2cEndpoint ep; + REQUIRE_FALSE(ep.handle_request(/*evt_op=*/0xF8, {0xA0}, {0xFF})); // low 3 bits 000 -> Plain + auto ec = ep.handle_request(/*evt_op=*/0xF9, {0xA0}, {0xFF}); // low 3 bits 001 -> Reserved + REQUIRE(ec == rcp::endpoint::make_error_code(rcp::endpoint::EndpointErrc::reserved_evt_row2)); +} + // ── I2cErrc category sanity ─────────────────────────────────────────────────── TEST_CASE("I2cErrc reports a non-empty message in its own category", "[i2c][REQ-I2C-005]") { @@ -98,3 +145,10 @@ TEST_CASE("I2cErrc reports a non-empty message in its own category", "[i2c][REQ- REQUIRE(ec.category() == i2c_category()); REQUIRE_FALSE(ec.message().empty()); } + +TEST_CASE("I2cErrc::config_write_not_supported reports a non-empty message in its own category", + "[i2c][REQ-I2C-007]") { + auto ec = make_error_code(I2cErrc::config_write_not_supported); + REQUIRE(ec.category() == i2c_category()); + REQUIRE_FALSE(ec.message().empty()); +} diff --git a/tests/test_mock.cpp b/tests/test_mock.cpp index c6d4699..b4709af 100644 --- a/tests/test_mock.cpp +++ b/tests/test_mock.cpp @@ -8,6 +8,8 @@ // fusa:test REQ-MOCK-008 // fusa:test REQ-MOCK-009 // fusa:test REQ-MOCK-010 +// fusa:test REQ-MOCK-011 +// fusa:test REQ-MOCK-012 // Tests for rcp/mock.hpp — the in-process RC Server simulator (ROADMAP.md // milestone 56, "Test & Simulation Harness Rebuild", v2.12.0). See @@ -34,18 +36,20 @@ acf::AcfMessageInfo standard_request(avtp::ByteBusId bus_id, bool write, uint8_t // ── Construction / register map ────────────────────────────────────────────── -TEST_CASE("Server starts HW_UNCONFIGURED with a two-endpoint register map", +TEST_CASE("Server starts HW_UNCONFIGURED with a three-endpoint register map", "[mock][REQ-MOCK-001]") { mock::Server server; REQUIRE(server.lifecycle().state() == lifecycle::ServerState::HwUnconfigured); - REQUIRE(server.registers().endpoint_count == 2); - REQUIRE(server.registers().generic_configs.size() == 2); - REQUIRE(server.registers().functional_configs.size() == 2); - REQUIRE(server.registers().ep_id_mapping.size() == 2); + REQUIRE(server.registers().endpoint_count == 3); + REQUIRE(server.registers().generic_configs.size() == 3); + REQUIRE(server.registers().functional_configs.size() == 3); + REQUIRE(server.registers().ep_id_mapping.size() == 3); REQUIRE(server.registers().ep_id_mapping[0].ep_id == mock::kGpioEndpointId); REQUIRE(server.registers().ep_id_mapping[0].byte_bus_id == mock::kGpioByteBusId); REQUIRE(server.registers().ep_id_mapping[1].ep_id == mock::kSpiEndpointId); REQUIRE(server.registers().ep_id_mapping[1].byte_bus_id == mock::kSpiByteBusId); + REQUIRE(server.registers().ep_id_mapping[2].ep_id == mock::kI2cEndpointId); + REQUIRE(server.registers().ep_id_mapping[2].byte_bus_id == mock::kI2cByteBusId); } TEST_CASE("advance_to_rcp_configured drives the lifecycle straight to RCP_CONFIGURED", @@ -189,6 +193,76 @@ TEST_CASE("SPI transfer answers with the scripted POCI-in bytes for the addresse REQUIRE(server.spi().last_sent(2) == pico_out); } +// ── I2C ─────────────────────────────────────────────────────────────────────── +// Table 30/33 Row 2 evt[2:0] validation pilot: I2cEndpoint wired into +// dispatch() at byte_bus_id 3. + +TEST_CASE("I2C plain request (evt[2:0]==000b) answers with the scripted response bytes", + "[mock][REQ-MOCK-011]") { + mock::Server server; + REQUIRE_FALSE(server.advance_to_rcp_configured()); + + server.set_i2c_response({0xDE, 0xAD}); + + auto req = standard_request(mock::kI2cByteBusId, /*write=*/true, /*evt_op=*/0); + std::vector out_bytes{0xA0, 0x10}; + acf::AcfMessageInfo resp; + std::vector resp_payload; + REQUIRE_FALSE(server.dispatch(0, req, out_bytes, resp, resp_payload)); + REQUIRE(acf::response_kind_of(resp) == acf::ResponseKind::ReadResponse); + REQUIRE(resp_payload == std::vector{0xDE, 0xAD}); + REQUIRE(server.i2c().last_sent() == out_bytes); +} + +TEST_CASE("I2C request with a reserved evt[2:0] (001b-110b) is rejected with wire error code " + "UNSUPPORTED_CMD and does not touch endpoint state", + "[mock][REQ-MOCK-011]") { + mock::Server server; + REQUIRE_FALSE(server.advance_to_rcp_configured()); + + for (uint8_t evt_op = 1; evt_op <= 6; ++evt_op) { + auto req = standard_request(mock::kI2cByteBusId, /*write=*/true, evt_op); + acf::AcfMessageInfo resp; + std::vector resp_payload; + auto ec = server.dispatch(0, req, {0xA0}, resp, resp_payload); + REQUIRE(ec == endpoint::make_error_code(endpoint::EndpointErrc::reserved_evt_row2)); + REQUIRE(acf::response_kind_of(resp) == acf::ResponseKind::ErrorResponse); + REQUIRE(resp_payload == + std::vector{static_cast(acf::WireErrorCode::UnsupportedCmd)}); + REQUIRE(server.i2c().last_sent().empty()); + } +} + +TEST_CASE("I2C request with evt[2:0]==111b (config-write) is rejected with wire error code " + "UNSUPPORTED_CMD rather than crashing or being treated as a plain transfer", + "[mock][REQ-MOCK-011]") { + mock::Server server; + REQUIRE_FALSE(server.advance_to_rcp_configured()); + + auto req = standard_request(mock::kI2cByteBusId, /*write=*/true, /*evt_op=*/7); + acf::AcfMessageInfo resp; + std::vector resp_payload; + auto ec = server.dispatch(0, req, {0x00, 0xAB}, resp, resp_payload); + REQUIRE(ec == i2c::make_error_code(i2c::I2cErrc::config_write_not_supported)); + REQUIRE(acf::response_kind_of(resp) == acf::ResponseKind::ErrorResponse); + REQUIRE(resp_payload == + std::vector{static_cast(acf::WireErrorCode::UnsupportedCmd)}); + REQUIRE(server.i2c().last_sent().empty()); +} + +TEST_CASE("I2C request is rejected before RCP_CONFIGURED, same operational gating as GPIO/SPI", + "[mock][REQ-MOCK-012]") { + mock::Server server; + REQUIRE(server.lifecycle().state() == lifecycle::ServerState::HwUnconfigured); + + auto req = standard_request(mock::kI2cByteBusId, /*write=*/true, /*evt_op=*/0); + acf::AcfMessageInfo resp; + std::vector resp_payload; + auto ec = server.dispatch(0, req, {0xA0}, resp, resp_payload); + REQUIRE(ec == regmap::make_error_code(regmap::RegMapErrc::request_rejected)); + REQUIRE(acf::response_kind_of(resp) == acf::ResponseKind::ErrorResponse); +} + // ── Unmapped endpoints ──────────────────────────────────────────────────────── TEST_CASE("dispatch to an unmapped byte_bus_id reports invalid_parameter",