/* SPDX-License-Identifier: AGPL-3.0-or-later */ /* Copyright (C) 2026 David (Corey) Herrera */ /** * @file test_udrv_arc_msg.c * @brief Gate for ft_tt_udrv_arc_msg.c — message encoding, ring arithmetic, * and the synchronous exchange, driven against a SIMULATED ARC. * * The fake firmware in this file implements the PEER side of the protocol * from the driver's semantics — it maintains its own pointers with its own * arithmetic and never calls a function under test. That is deliberate: a * ring exerciser built out of the ring code proves the code is * self-consistent and nothing else (DEV_NOTES §1, "never instrument a * mechanism with itself"). * * WHAT THIS CANNOT PROVE: that a CSM write lands, that the doorbell is at * 0x800B0000, that the QCB pointer is in RESET_SCRATCH(11), or that real * firmware answers any of these messages. Those need silicon. What it does * prove is that every word this driver would put on the wire is the word the * kernel would have put there, and that the ring bookkeeping survives wrap, * fullness, staleness and a dead device. * * @status Off-hardware. No transport here performs I/O of any kind. * @issues None * @todo None */ #include #include #include #include "ft_tt_test_report.h" #include "ft_tt_udrv.h" #include "ft_tt_udrv_arc_msg.h" /** Where the fake queue lives inside the fake CSM. */ #define FAKE_QUEUE_BASE (FT_TT_UDRV_ARC_CSM_BASE + 0x1000u) #define FAKE_ENTRIES 4u /** Enough CSM to hold the header plus both rings, with room to spare. */ #define FAKE_CSM_WORDS 4096u /** * A simulated ARC. * * `memory` is word-addressed from FT_TT_UDRV_ARC_CSM_BASE. `respond` decides * whether the doorbell produces a reply, and `status` is the header the reply * carries — zero for success, which is the firmware's convention. */ typedef struct fake_arc { uint32_t memory[FAKE_CSM_WORDS]; bool respond; uint32_t status; bool dead; /**< every read returns 0xFFFFFFFF */ uint32_t reads; uint32_t writes; uint32_t triggers; uint32_t waits; uint32_t responses_pushed; ft_tt_udrv_arc_msg last_request; /**< what the firmware saw */ } fake_arc; /** Word index of a CSM address, or FAKE_CSM_WORDS when out of range. */ static uint32_t fake_word_index(uint32_t address) { uint32_t offset; if (address < FT_TT_UDRV_ARC_CSM_BASE || (address & 3u) != 0u) return FAKE_CSM_WORDS; offset = (address - FT_TT_UDRV_ARC_CSM_BASE) / 4u; return (offset < FAKE_CSM_WORDS) ? offset : FAKE_CSM_WORDS; } static int fake_read32(void *context, uint32_t address, uint32_t *out_value) { fake_arc *arc = context; uint32_t index = fake_word_index(address); arc->reads++; if (arc->dead) { *out_value = 0xFFFFFFFFu; return 0; } if (index >= FAKE_CSM_WORDS) return -1; *out_value = arc->memory[index]; return 0; } static int fake_write32(void *context, uint32_t address, uint32_t value) { fake_arc *arc = context; uint32_t index = fake_word_index(address); arc->writes++; if (index >= FAKE_CSM_WORDS) return -1; arc->memory[index] = value; return 0; } /** * The doorbell: consume one request and produce one response. * * Written from the protocol description, not from the code under test — its * pointers, slot arithmetic and modulus are all its own. */ static int fake_trigger(void *context) { fake_arc *arc = context; uint32_t header_word = (FAKE_QUEUE_BASE - FT_TT_UDRV_ARC_CSM_BASE) / 4u; uint32_t req_wptr = arc->memory[header_word + 0u]; uint32_t res_rptr = arc->memory[header_word + 1u]; uint32_t req_rptr = arc->memory[header_word + 4u]; uint32_t res_wptr = arc->memory[header_word + 5u]; uint32_t request_word_base; uint32_t response_word_base; uint32_t slot; uint32_t i; arc->triggers++; if (!arc->respond) return 0; /* Nothing to do if the request ring is empty. */ if (req_wptr == req_rptr) return 0; request_word_base = header_word + (FT_TT_UDRV_ARC_QUEUE_HEADER_BYTES / 4u); response_word_base = request_word_base + (FAKE_ENTRIES * (FT_TT_UDRV_ARC_MSG_BYTES / 4u)); slot = req_rptr % FAKE_ENTRIES; arc->last_request.header = arc->memory[request_word_base + (slot * 8u)]; for (i = 0u; i < FT_TT_UDRV_ARC_PAYLOAD_WORDS; i++) arc->last_request.payload[i] = arc->memory[request_word_base + (slot * 8u) + 1u + i]; req_rptr = (req_rptr + 1u) % (2u * FAKE_ENTRIES); arc->memory[header_word + 4u] = req_rptr; /* Echo the payload back with the firmware's status in the header word, * which is what makes "the response is the request's answer" checkable. */ slot = res_wptr % FAKE_ENTRIES; arc->memory[response_word_base + (slot * 8u)] = arc->status; for (i = 0u; i < FT_TT_UDRV_ARC_PAYLOAD_WORDS; i++) arc->memory[response_word_base + (slot * 8u) + 1u + i] = arc->last_request.payload[i] ^ 0xA5A5A5A5u; res_wptr = (res_wptr + 1u) % (2u * FAKE_ENTRIES); arc->memory[header_word + 5u] = res_wptr; arc->responses_pushed++; (void)res_rptr; return 0; } static void fake_wait(void *context) { fake_arc *arc = context; arc->waits++; } static void fake_init(fake_arc *arc, ft_tt_udrv_arc_transport *transport) { memset(arc, 0, sizeof(*arc)); arc->respond = true; arc->status = 0u; transport->csm_read32 = fake_read32; transport->csm_write32 = fake_write32; transport->trigger = fake_trigger; transport->wait = fake_wait; transport->context = arc; } /* ------------------------------------------------------------------------ * * Stages * ------------------------------------------------------------------------ */ static void stage_queue_location(void) { ft_tt_udrv_arc_queue queue; ft_tt_test_step("Stage 1 — locating the queue, and the addresses that follow"); ft_tt_test_check_fmt("QCB pointer is RESET_SCRATCH(11) = 0x8003042C", FT_TT_UDRV_ARC_MSG_QCB_PTR == 0x8003042Cu, "0x%08x", FT_TT_UDRV_ARC_MSG_QCB_PTR); ft_tt_test_check_fmt("boot status is RESET_SCRATCH(2) = 0x80030408", FT_TT_UDRV_ARC_BOOT_STATUS == 0x80030408u, "0x%08x", FT_TT_UDRV_ARC_BOOT_STATUS); ft_tt_test_check("a queue with 4 entries at CSM+0x1000 is accepted", ft_tt_udrv_arc_queue_init(FAKE_QUEUE_BASE, FAKE_ENTRIES, &queue) == FT_TT_UDRV_OK, ""); ft_tt_test_check_fmt("entry count is the low byte of QCB word 1", queue.num_entries == FAKE_ENTRIES, "%u", queue.num_entries); ft_tt_test_check_fmt("requests start 32 bytes past the base", queue.request_base == FAKE_QUEUE_BASE + 32u, "0x%08x", queue.request_base); ft_tt_test_check_fmt("responses start after all 4 requests", queue.response_base == FAKE_QUEUE_BASE + 32u + (4u * 32u), "0x%08x", queue.response_base); ft_tt_test_check_fmt("the four pointer addresses are +0, +4, +0x10, +0x14", queue.req_wptr_addr == FAKE_QUEUE_BASE + 0x00u && queue.res_rptr_addr == FAKE_QUEUE_BASE + 0x04u && queue.req_rptr_addr == FAKE_QUEUE_BASE + 0x10u && queue.res_wptr_addr == FAKE_QUEUE_BASE + 0x14u, "%08x %08x %08x %08x", queue.req_wptr_addr, queue.res_rptr_addr, queue.req_rptr_addr, queue.res_wptr_addr); /* The low byte only: firmware puts other things in the upper bits. */ ft_tt_test_check_fmt("only the low byte of QCB word 1 is the entry count", ft_tt_udrv_arc_queue_init(FAKE_QUEUE_BASE, 0xDEAD0004u, &queue) == FT_TT_UDRV_OK && queue.num_entries == 4u, "%u", queue.num_entries); ft_tt_test_check("a zero-entry queue is refused", ft_tt_udrv_arc_queue_init(FAKE_QUEUE_BASE, 0u, &queue) == FT_TT_UDRV_EINVAL, "would divide by zero"); ft_tt_test_check("a queue below the CSM is refused", ft_tt_udrv_arc_queue_init(0x1000u, 4u, &queue) == FT_TT_UDRV_EINVAL, "outside 0x10000000"); ft_tt_test_check("a queue running off the end of the CSM is refused", ft_tt_udrv_arc_queue_init(FT_TT_UDRV_ARC_CSM_BASE + FT_TT_UDRV_ARC_CSM_SIZE - 64u, 4u, &queue) == FT_TT_UDRV_EINVAL, "the ring would not fit"); ft_tt_test_check("a 255-entry queue that does not fit is refused", ft_tt_udrv_arc_queue_init(FT_TT_UDRV_ARC_CSM_BASE + FT_TT_UDRV_ARC_CSM_SIZE - 0x2000u, 255u, &queue) == FT_TT_UDRV_EINVAL, "255 * 2 * 32 = 16320 bytes"); } static void stage_ring_arithmetic(void) { uint32_t occupancy = 0u; uint32_t pointer = 0u; ft_tt_test_step("Stage 2 — ring arithmetic, including a non-power-of-two ring"); ft_tt_test_check_fmt("empty ring", ft_tt_udrv_arc_ring_occupancy(0u, 0u, 4u, &occupancy) == FT_TT_UDRV_OK && occupancy == 0u, "%u", occupancy); ft_tt_test_check_fmt("three in flight", ft_tt_udrv_arc_ring_occupancy(3u, 0u, 4u, &occupancy) == FT_TT_UDRV_OK && occupancy == 3u, "%u", occupancy); ft_tt_test_check_fmt("full ring (wptr has lapped into the second half)", ft_tt_udrv_arc_ring_occupancy(4u, 0u, 4u, &occupancy) == FT_TT_UDRV_OK && occupancy == 4u, "%u", occupancy); ft_tt_test_check_fmt("wrapped: wptr 1, rptr 7, 4 entries", ft_tt_udrv_arc_ring_occupancy(1u, 7u, 4u, &occupancy) == FT_TT_UDRV_OK && occupancy == 2u, "%u", occupancy); ft_tt_test_check("a pointer at or past 2N is refused", ft_tt_udrv_arc_ring_occupancy(8u, 0u, 4u, &occupancy) == FT_TT_UDRV_EINVAL, "a wild pointer must not read as a full ring"); /* N not a power of two: a single u32 modular subtraction would get this wrong. */ occupancy = 0xFFFFFFFFu; ft_tt_test_check_fmt("wrapped with N=3 (wptr 1, rptr 5): occupancy 2", ft_tt_udrv_arc_ring_occupancy(1u, 5u, 3u, &occupancy) == FT_TT_UDRV_OK && occupancy == 2u, "%u", occupancy); ft_tt_test_check_fmt("advance wraps at 2N", ft_tt_udrv_arc_ring_advance(7u, 4u, &pointer) == FT_TT_UDRV_OK && pointer == 0u, "%u", pointer); ft_tt_test_check_fmt("advance is ordinary in the middle", ft_tt_udrv_arc_ring_advance(3u, 4u, &pointer) == FT_TT_UDRV_OK && pointer == 4u, "%u", pointer); } static void stage_slot_addresses(void) { ft_tt_udrv_arc_queue queue; uint32_t address = 0u; ft_tt_test_step("Stage 3 — slot addresses fold the pointer modulo N, not 2N"); (void)ft_tt_udrv_arc_queue_init(FAKE_QUEUE_BASE, FAKE_ENTRIES, &queue); ft_tt_test_check_fmt("request slot 0 word 0", ft_tt_udrv_arc_request_word_address(&queue, 0u, 0u, &address) == FT_TT_UDRV_OK && address == queue.request_base, "0x%08x", address); ft_tt_test_check_fmt("request slot 0 word 7 is 28 bytes in", ft_tt_udrv_arc_request_word_address(&queue, 0u, 7u, &address) == FT_TT_UDRV_OK && address == queue.request_base + 28u, "0x%08x", address); /* wptr 4 and wptr 0 are the same SLOT — the extra bit only distinguishes * "full" from "empty", which is the whole reason the pointers run to 2N. */ ft_tt_test_check_fmt("wptr 4 addresses the same slot as wptr 0", ft_tt_udrv_arc_request_word_address(&queue, 4u, 0u, &address) == FT_TT_UDRV_OK && address == queue.request_base, "0x%08x", address); ft_tt_test_check_fmt("response slot 3 word 0", ft_tt_udrv_arc_response_word_address(&queue, 3u, 0u, &address) == FT_TT_UDRV_OK && address == queue.response_base + (3u * 32u), "0x%08x", address); ft_tt_test_check("word 8 is refused (a message is 8 words)", ft_tt_udrv_arc_request_word_address(&queue, 0u, 8u, &address) == FT_TT_UDRV_EINVAL, ""); ft_tt_test_check("a pointer at 2N is refused", ft_tt_udrv_arc_request_word_address(&queue, 8u, 0u, &address) == FT_TT_UDRV_EINVAL, ""); ft_tt_test_check_fmt("request and response rings do not overlap", queue.response_base >= queue.request_base + (FAKE_ENTRIES * 32u), "0x%08x vs 0x%08x", queue.response_base, queue.request_base + (FAKE_ENTRIES * 32u)); } static void stage_message_encodings(void) { ft_tt_udrv_arc_msg msg; uint16_t settings[14]; unsigned i; ft_tt_test_step("Stage 4 — the exact words tt-kmd puts on the wire"); (void)ft_tt_udrv_arc_msg_asic_state0(&msg); ft_tt_test_check_fmt("ASIC_STATE0 is header 0xA0 with an empty payload", msg.header == 0xA0u && msg.payload[0] == 0u && msg.payload[6] == 0u, "0x%08x", msg.header); (void)ft_tt_udrv_arc_msg_asic_state3(&msg); ft_tt_test_check_fmt("ASIC_STATE3 is header 0xA3", msg.header == 0xA3u, "0x%08x", msg.header); (void)ft_tt_udrv_arc_msg_test(&msg); ft_tt_test_check_fmt("TEST is header 0x90", msg.header == 0x90u, "0x%08x", msg.header); (void)ft_tt_udrv_arc_msg_set_wdt_timeout(10000u, &msg); ft_tt_test_check_fmt("SET_WDT_TIMEOUT is 0xC1 with the timeout in payload[0]", msg.header == 0xC1u && msg.payload[0] == 10000u, "0x%08x / %u", msg.header, msg.payload[0]); ft_tt_test_check_fmt("...and 10000 ms is the driver's own default (10 s)", msg.payload[0] == 10u * 1000u, "%u", msg.payload[0]); (void)ft_tt_udrv_arc_msg_trigger_reset(3u, &msg); ft_tt_test_check_fmt("TRIGGER_RESET is 0x56 with argument 3 (ASIC + M3)", msg.header == 0x56u && msg.payload[0] == 3u, "0x%08x / %u", msg.header, msg.payload[0]); /* THE ONE THAT MATTERS MOST: this is what sets the clock at open. */ (void)ft_tt_udrv_arc_msg_power_setting_open_default(&msg); ft_tt_test_check_fmt("POWER_SETTING at open is header 0x7FFE0F21", msg.header == 0x7FFE0F21u, "0x%08x", msg.header); ft_tt_test_check_fmt("...i.e. every feature ON except MAX_AI_CLK", ((msg.header >> 16) & 0xFFFFu) == (FT_TT_UDRV_ARC_POWER_FLAG_ALL & ~FT_TT_UDRV_ARC_POWER_FLAG_MAX_AI_CLK), "flags=0x%04x", (msg.header >> 16) & 0xFFFFu); ft_tt_test_check_fmt("...with validity 0x0F (15 flags, 0 settings)", ((msg.header >> 8) & 0xFFu) == 0x0Fu, "0x%02x", (msg.header >> 8) & 0xFFu); (void)ft_tt_udrv_arc_msg_power_setting_last_close(&msg); ft_tt_test_check_fmt("POWER_SETTING at last close is header 0x00000F21 — everything off", msg.header == 0x00000F21u, "0x%08x", msg.header); for (i = 0u; i < 14u; i++) settings[i] = (uint16_t)(0x1000u + i); (void)ft_tt_udrv_arc_msg_power_setting(0x2Fu, 0x0005u, settings, &msg); ft_tt_test_check_fmt("a settings payload packs two u16 per word, little-endian first", msg.payload[0] == 0x10011000u && msg.payload[6] == 0x100D100Cu, "%08x .. %08x", msg.payload[0], msg.payload[6]); ft_tt_test_check_fmt("...and validity/flags land in bytes 1 and 2-3", msg.header == (0x21u | (0x2Fu << 8) | (0x0005u << 16)), "0x%08x", msg.header); ft_tt_test_check("a NULL output is refused", ft_tt_udrv_arc_msg_asic_state0(NULL) == FT_TT_UDRV_EINVAL, ""); } static void stage_push_pop(void) { fake_arc arc; ft_tt_udrv_arc_transport transport; ft_tt_udrv_arc_queue queue; ft_tt_udrv_arc_msg msg; uint32_t index; ft_tt_test_step("Stage 5 — push and pop against the simulated ARC"); fake_init(&arc, &transport); (void)ft_tt_udrv_arc_queue_init(FAKE_QUEUE_BASE, FAKE_ENTRIES, &queue); arc.respond = false; /* the doorbell does nothing, so the ring just fills */ (void)ft_tt_udrv_arc_msg_asic_state0(&msg); ft_tt_test_check("the first push succeeds", ft_tt_udrv_arc_try_push(&transport, &queue, &msg) == FT_TT_UDRV_OK, ""); ft_tt_test_check_fmt("the request landed in slot 0", arc.memory[fake_word_index(queue.request_base)] == 0xA0u, "0x%08x", arc.memory[fake_word_index(queue.request_base)]); ft_tt_test_check_fmt("...and the write pointer advanced to 1", arc.memory[fake_word_index(queue.req_wptr_addr)] == 1u, "%u", arc.memory[fake_word_index(queue.req_wptr_addr)]); ft_tt_test_check("popping an empty response ring is EAGAIN, not an error", ft_tt_udrv_arc_try_pop(&transport, &queue, &msg) == FT_TT_UDRV_EAGAIN, ""); for (index = 1u; index < FAKE_ENTRIES; index++) (void)ft_tt_udrv_arc_try_push(&transport, &queue, &msg); ft_tt_test_check("a full request ring refuses with EAGAIN", ft_tt_udrv_arc_try_push(&transport, &queue, &msg) == FT_TT_UDRV_EAGAIN, "4 of 4 slots occupied"); /* A dead device must be diagnosed as one, not treated as a wild pointer. */ arc.dead = true; ft_tt_test_check("an all-ones pointer read is EIO, not arithmetic", ft_tt_udrv_arc_try_push(&transport, &queue, &msg) == FT_TT_UDRV_EIO, "0xFFFFFFFF means the NoC is gone"); ft_tt_test_check("...and the same on the pop side", ft_tt_udrv_arc_try_pop(&transport, &queue, &msg) == FT_TT_UDRV_EIO, ""); } static void stage_send_sync(void) { fake_arc arc; ft_tt_udrv_arc_transport transport; ft_tt_udrv_arc_queue queue; ft_tt_udrv_arc_msg msg; uint32_t round; ft_tt_test_step("Stage 6 — the synchronous exchange"); fake_init(&arc, &transport); (void)ft_tt_udrv_arc_queue_init(FAKE_QUEUE_BASE, FAKE_ENTRIES, &queue); (void)ft_tt_udrv_arc_msg_set_wdt_timeout(10000u, &msg); ft_tt_test_check("a well-formed exchange succeeds", ft_tt_udrv_arc_send_sync(&transport, &queue, &msg, 8u) == FT_TT_UDRV_OK, ""); ft_tt_test_check_fmt("the firmware saw the message we built", arc.last_request.header == 0xC1u && arc.last_request.payload[0] == 10000u, "0x%08x / %u", arc.last_request.header, arc.last_request.payload[0]); ft_tt_test_check_fmt("the doorbell was rung exactly once", arc.triggers == 1u, "%u", arc.triggers); ft_tt_test_check_fmt("the response overwrote the request, as the driver does", msg.header == 0u && msg.payload[0] == (10000u ^ 0xA5A5A5A5u), "0x%08x / 0x%08x", msg.header, msg.payload[0]); /* Round-trip the ring several times so a wrap bug cannot hide behind a * single exchange. */ for (round = 0u; round < 11u; round++) { (void)ft_tt_udrv_arc_msg_asic_state0(&msg); if (ft_tt_udrv_arc_send_sync(&transport, &queue, &msg, 8u) != FT_TT_UDRV_OK) break; } ft_tt_test_check_fmt("11 more exchanges wrap both rings without wedging", round == 11u, "completed %u of 11", round); /* A firmware error must be surfaced AND its status preserved. */ fake_init(&arc, &transport); arc.status = 0xDEADu; (void)ft_tt_udrv_arc_msg_asic_state0(&msg); ft_tt_test_check("a nonzero response header is a failure", ft_tt_udrv_arc_send_sync(&transport, &queue, &msg, 8u) == FT_TT_UDRV_EIO, ""); ft_tt_test_check_fmt("...and the firmware's status is left where the caller can read it", msg.header == 0xDEADu, "0x%08x", msg.header); /* No response: bounded, and it says EAGAIN rather than hanging. */ fake_init(&arc, &transport); arc.respond = false; (void)ft_tt_udrv_arc_msg_asic_state0(&msg); ft_tt_test_check("a silent firmware exhausts the poll budget", ft_tt_udrv_arc_send_sync(&transport, &queue, &msg, 5u) == FT_TT_UDRV_EAGAIN, ""); ft_tt_test_check_fmt("...having waited exactly the budget", arc.waits == 5u, "%u waits", arc.waits); /* A stale response from an abandoned exchange must be drained, not * mistaken for this exchange's answer. */ fake_init(&arc, &transport); arc.respond = false; (void)ft_tt_udrv_arc_msg_asic_state0(&msg); (void)ft_tt_udrv_arc_try_push(&transport, &queue, &msg); arc.respond = true; (void)fake_trigger(&arc); /* firmware answers the ABANDONED request */ arc.responses_pushed = 0u; (void)ft_tt_udrv_arc_msg_set_wdt_timeout(1234u, &msg); ft_tt_test_check("a stale response is drained before the new request", ft_tt_udrv_arc_send_sync(&transport, &queue, &msg, 8u) == FT_TT_UDRV_OK, ""); ft_tt_test_check_fmt("...and the answer belongs to the NEW request", msg.payload[0] == (1234u ^ 0xA5A5A5A5u), "0x%08x", msg.payload[0]); ft_tt_test_check("a transport missing an operation is refused", ft_tt_udrv_arc_send_sync(NULL, &queue, &msg, 1u) == FT_TT_UDRV_EINVAL, ""); } int main(void) { stage_queue_location(); stage_ring_arithmetic(); stage_slot_addresses(); stage_message_encodings(); stage_push_pop(); stage_send_sync(); return ft_tt_test_finish(); }