/* * pog422_inject.c — low-latency POG422 UART burst on Linux (Pi + ttyXRUSB*). * Fixture uses wide/preciseflags mode only (XRIOC_SET_PRECISE_FLAGS + INJECT_TX). */ #include #include #include #include #include #include #include #include #include #include #include #include "pog422_inject.h" #include "pog422_ipc.h" #include "pog422_hw_debug.h" #define XR_USB_SERIAL_GET_REG 0xC0047601u #define XR_USB_SERIAL_SET_REG 0xC0047602u #define GPIO_STAT_REG 0x10u #define GPIO1_STAT_MASK 0x02u #define GPIO_MODE_REG 0x0Cu #define GPIO_DIR_REG 0x0Du #define UART_FLOW_REG 0x06u /* FLOW_CONTROL bit3 = UART Half-Duplex Mode (mute RX while TX). Must stay clear. */ #define UART_FLOW_HALF_DUPLEX_BIT 0x08u #define RX_FIFO_COUNT_REG 0x044u #define TX_FIFO_COUNT_REG 0x041u #define GPIO_SET_REG 0x0Eu #define GPIO_CLR_REG 0x0Fu #define UART_GPIO_SET_DTR_RTS (0x08u | 0x20u) #define POG422_GPIO_MODE_RS485_AUTO 0x0339u /* Match driver: GPIO7 manual DE + GPIO5 RS485_EN_ACT sense (USB INT TX-done). */ #define POG422_GPIO_MODE_MANUAL_SLV 0x030Bu #define POG422_GPIO_STD_DIR 0x28u #define POG422_GPIO7_DIR (POG422_GPIO_STD_DIR | POG422_GPIO7_BIT) #define POG422_GPIO7_BIT 0x80u /* XR21B1422: UART blocks 0 → ttyXRUSB0 (MST/OIS), 2 → ttyXRUSB1 (machine/SLV). */ #define POG422_B142X_CHANNELS 0, 2 #define POG422_BAUD 57600u #define POG422_BITS_PER_BYTE 11u /* 9N1 on wire */ /* Wire margin after theoretical air when opts->post_hold_us is 0. */ #define POG422_DE_AIR_MARGIN_DEFAULT_US 400u /* Match xr_usb_serial_common.h RAMCTL_BUFFER_* (wide-mode TX flag byte). */ #define POG422_RAMCTL_BUFFER_PARITY 0x01u /* Match POG422_INJECT_FRAME_MAX (ioctl / driver). Forensics wide_buf may still truncate. */ #define POG422_INJECT_WIDE_MAX_FRAME POG422_INJECT_FRAME_MAX static int g_slv_in_gpio1 = -1; /* EG 3.0.2.4: after first confirm, skip GET_RX_STATS on every inject (~ms USB). */ static int g_wide_on_cached = 0; static int env_force_register_poll(void) { const char *v = getenv("POG422_SLV_IN_POLL"); return v && v[0] == '1' && v[1] == '\0'; } /* * Soft pre/post FIFO+regs forensics. Default off — soak 1549 still had * pre_bulk≈6 ms when this returned true under HW_DEBUG builds; copper timeline * is already compile-gated in copper_push. Opt-in: POG422_INJECT_FORENSICS=1. */ static int inject_forensics_full(void) { static int cached = -1; if (cached < 0) { const char *v = getenv("POG422_INJECT_FORENSICS"); cached = (v && v[0] == '1' && v[1] == '\0') ? 1 : 0; } return cached; } static int preciseflags_get(int fd, int *on) { struct xr_pog422_rx_stats stats; if (!on) { return -EINVAL; } if (ioctl(fd, XR_USB_SERIAL_GET_RX_STATS, &stats) != 0) { return -errno; } *on = stats.preciseflags ? 1 : 0; return 0; } static int preciseflags_set(int fd, int on) { if (ioctl(fd, XRIOC_SET_PRECISE_FLAGS, on ? 1 : 0) != 0) { return -errno; } return 0; } static size_t build_wide_frame(const uint8_t *frame, size_t len, uint8_t *wide, size_t wide_cap) { size_t i; if (!frame || !wide || len == 0u || len > POG422_INJECT_WIDE_MAX_FRAME || len * 2u > wide_cap) { return 0; } for (i = 0; i < len; i++) { wide[i * 2u] = frame[i]; wide[i * 2u + 1u] = (i == 0u) ? POG422_RAMCTL_BUFFER_PARITY : 0u; } return len * 2u; } static int xr_get_reg(int fd, unsigned reg, unsigned *out) { int buf[3] = {-1, (int)reg, 0}; if (ioctl(fd, XR_USB_SERIAL_GET_REG, buf) < 0) { return -errno; } *out = (unsigned)buf[2]; return 0; } static int xr_set_reg_ch(int fd, int channel, unsigned reg, unsigned value) { int buf[3] = {channel, (int)reg, (int)value}; if (ioctl(fd, XR_USB_SERIAL_SET_REG, buf) < 0) { return -errno; } return 0; } static int xr_get_reg_ch(int fd, int channel, unsigned reg, unsigned *out) { int buf[3] = {channel, (int)reg, 0}; if (ioctl(fd, XR_USB_SERIAL_GET_REG, buf) < 0) { return -errno; } *out = (unsigned)buf[2]; return 0; } static int rs485_auto_on_channel(int fd, int channel) { int rc; rc = xr_set_reg_ch(fd, channel, UART_FLOW_REG, 0); if (rc != 0) { return rc; } rc = xr_set_reg_ch(fd, channel, GPIO_MODE_REG, POG422_GPIO_MODE_RS485_AUTO); if (rc != 0) { return rc; } return xr_set_reg_ch(fd, channel, GPIO_SET_REG, UART_GPIO_SET_DTR_RTS); } static int manual_gpio_slv_on_channel(int fd, int channel) { int rc; rc = xr_set_reg_ch(fd, channel, UART_FLOW_REG, 0); if (rc != 0) { return rc; } rc = xr_set_reg_ch(fd, channel, GPIO_MODE_REG, POG422_GPIO_MODE_MANUAL_SLV); if (rc != 0) { return rc; } rc = xr_set_reg_ch(fd, channel, GPIO_DIR_REG, POG422_GPIO7_DIR); if (rc != 0) { return rc; } rc = xr_set_reg_ch(fd, channel, GPIO_SET_REG, UART_GPIO_SET_DTR_RTS); if (rc != 0) { return rc; } return xr_set_reg_ch(fd, channel, GPIO_CLR_REG, POG422_GPIO7_BIT); } static int gpio7_sl_out(int fd, int tx_enable) { int val = tx_enable ? 1 : 0; if (ioctl(fd, XR_USB_SERIAL_SET_GPIO7, &val) != 0) { return -errno; } return 0; } static int slv_in_from_register(int fd, uint8_t *active) { unsigned stat = 0; int rc = xr_get_reg(fd, GPIO_STAT_REG, &stat); if (rc != 0) { return rc; } *active = (stat & GPIO1_STAT_MASK) ? 0u : 1u; return 0; } static int slv_in_from_gpio1_event(int fd, uint8_t *active) { struct xr_pog422_gpio1_event ev; memset(&ev, 0, sizeof(ev)); if (ioctl(fd, XR_USB_SERIAL_GET_GPIO1_EVENT, &ev) != 0) { return -errno; } *active = ev.slv_in_active ? 1u : 0u; return 0; } static void slv_in_probe_mode(int fd) { if (g_slv_in_gpio1 >= 0) { return; } if (env_force_register_poll()) { g_slv_in_gpio1 = 0; return; } { uint8_t dummy = 0; if (slv_in_from_gpio1_event(fd, &dummy) == 0) { g_slv_in_gpio1 = 1; return; } } g_slv_in_gpio1 = 0; } static uint64_t monotonic_ns(void) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return (uint64_t)ts.tv_sec * 1000000000ull + (uint64_t)ts.tv_nsec; } static void sleep_us(uint32_t us) { if (us == 0u) { return; } struct timespec ts; ts.tv_sec = (time_t)(us / 1000000u); ts.tv_nsec = (long)(us % 1000000u) * 1000L; while (nanosleep(&ts, &ts) != 0 && errno == EINTR) { } } static void inject_poll(const pog422_inject_opts_t *opts) { if (opts && opts->poll_fn) { opts->poll_fn(opts->poll_ctx); } } typedef struct { uint64_t tx0_ns; uint64_t last_event_ts; uint8_t last_active; uint8_t edge_count; uint32_t first_edge_us; uint32_t last_edge_us; } gpio1_inject_trace_t; static void inject_poll_trace(const pog422_inject_opts_t *opts, int fd, gpio1_inject_trace_t *tr); /* Sleep in slices while harvesting RX / GPIO1 edges (DE-high pre_tx). */ static void sleep_us_poll( uint32_t us, const pog422_inject_opts_t *opts, int fd, gpio1_inject_trace_t *tr ) { const uint64_t deadline = monotonic_ns() + (uint64_t)us * 1000ull; /* EG 3.0.2.6: pre_tx is always 0 in product — do not run poll_fn here. */ if (us == 0u) { (void)opts; (void)fd; (void)tr; return; } while (monotonic_ns() < deadline) { inject_poll_trace(opts, fd, tr); sleep_us(100u); } inject_poll_trace(opts, fd, tr); } static int wait_tx_empty( int fd, uint32_t timeout_ms, struct xr_pog422_tx_empty *out ) { if (fd < 0 || !out) { return -EINVAL; } memset(out, 0, sizeof(*out)); out->timeout_ms = timeout_ms ? timeout_ms : 2u; if (ioctl(fd, XR_USB_SERIAL_WAIT_TX_EMPTY, out) != 0) { return -errno; } return 0; } static void gpio1_trace_init(gpio1_inject_trace_t *tr, int fd, uint64_t tx0_ns); static int slv_in_active(int fd, uint8_t *active) { slv_in_probe_mode(fd); if (g_slv_in_gpio1 == 1) { int rc = slv_in_from_gpio1_event(fd, active); if (rc == 0) { return 0; } g_slv_in_gpio1 = 0; } return slv_in_from_register(fd, active); } static int slv_in_phase_active(int fd, uint8_t *active) { /* EG 3.0.2.6: prefer GPIO1 event cache — GET_REG (old path) was ~ms USB * on every inject pre_bulk (soak 1606 residual pre_bulk ≈3.7 ms). */ return slv_in_active(fd, active); } static void gpio1_trace_init(gpio1_inject_trace_t *tr, int fd, uint64_t tx0_ns) { memset(tr, 0, sizeof(*tr)); tr->tx0_ns = tx0_ns; (void)slv_in_active(fd, &tr->last_active); } /* EG 3.0.2.4: seed from require_idle sample — avoid a second SLV-IN ioctl. */ static void gpio1_trace_init_seed( gpio1_inject_trace_t *tr, uint64_t tx0_ns, uint8_t last_active ) { memset(tr, 0, sizeof(*tr)); tr->tx0_ns = tx0_ns; tr->last_active = last_active ? 1u : 0u; } static void gpio1_trace_note_edge(gpio1_inject_trace_t *tr, uint64_t event_ns) { uint64_t off_ns = event_ns >= tr->tx0_ns ? event_ns - tr->tx0_ns : 0u; uint32_t off_us = (uint32_t)(off_ns / 1000ull); if (tr->edge_count == 0u) { tr->first_edge_us = off_us; } tr->last_edge_us = off_us; if (tr->edge_count < 255u) { tr->edge_count++; } } static void gpio1_trace_poll(int fd, gpio1_inject_trace_t *tr) { if (g_slv_in_gpio1 == 1) { struct xr_pog422_gpio1_event ev; memset(&ev, 0, sizeof(ev)); if (ioctl(fd, XR_USB_SERIAL_GET_GPIO1_EVENT, &ev) == 0 && ev.timestamp_ns != 0u && ev.timestamp_ns != tr->last_event_ts) { tr->last_event_ts = ev.timestamp_ns; gpio1_trace_note_edge(tr, ev.timestamp_ns); tr->last_active = ev.slv_in_active ? 1u : 0u; return; } } { uint8_t active = 0; uint64_t now; (void)slv_in_active(fd, &active); if (active == tr->last_active) { return; } now = monotonic_ns(); gpio1_trace_note_edge(tr, now); tr->last_active = active; } } /* * GPIO1-event-only poll — safe while DE is high. * * GET_GPIO1_EVENT copies the driver's cached interrupt-endpoint state; unlike * gpio1_trace_poll() it never falls back to GET_REG, so it costs no USB control * transfer. Returns 1 when the cache was readable. */ static int gpio1_trace_poll_event(int fd, gpio1_inject_trace_t *tr) { struct xr_pog422_gpio1_event ev; if (g_slv_in_gpio1 != 1) { return 0; } memset(&ev, 0, sizeof(ev)); if (ioctl(fd, XR_USB_SERIAL_GET_GPIO1_EVENT, &ev) != 0) { return 0; } if (ev.timestamp_ns != 0u && ev.timestamp_ns != tr->last_event_ts) { tr->last_event_ts = ev.timestamp_ns; gpio1_trace_note_edge(tr, ev.timestamp_ns); } tr->last_active = ev.slv_in_active ? 1u : 0u; return 1; } /* DE-high SLV-IN sample: cached GPIO1 state, or UNKNOWN if that path is off. */ static uint8_t gpio1_de_sample(int fd, gpio1_inject_trace_t *tr) { if (!gpio1_trace_poll_event(fd, tr)) { return POG422_SLV_IN_UNKNOWN; } return tr->last_active ? POG422_SLV_IN_ACTIVE : POG422_SLV_IN_INACTIVE; } static void gpio1_trace_to_result(const gpio1_inject_trace_t *tr, pog422_inject_result_t *result) { result->gpio1_edges = tr->edge_count; result->gpio1_first_edge_us = tr->first_edge_us; result->gpio1_last_edge_us = tr->last_edge_us; } static void inject_poll_trace(const pog422_inject_opts_t *opts, int fd, gpio1_inject_trace_t *tr) { inject_poll(opts); if (tr) { gpio1_trace_poll(fd, tr); } } int pog422_slv_in_active(int fd, uint8_t *active) { return slv_in_active(fd, active); } int pog422_slv_in_live(int fd, uint8_t *active) { return slv_in_from_register(fd, active); } int pog422_slv_in_wait_edge(int fd, struct xr_pog422_gpio1_event *ev) { if (!ev) { return -EINVAL; } slv_in_probe_mode(fd); if (g_slv_in_gpio1 != 1) { return -EOPNOTSUPP; } if (ioctl(fd, XR_USB_SERIAL_WAIT_GPIO1, ev) != 0) { return -errno; } return 0; } int pog422_timing_trace_set(int fd, int enable) { int val = enable ? 1 : 0; if (fd < 0) { return -EINVAL; } if (ioctl(fd, XR_USB_SERIAL_SET_TIMING_TRACE, &val) != 0) { return -errno; } return 0; } int pog422_timing_trace_status(int fd, struct xr_pog422_timing_status *st) { if (fd < 0 || !st) { return -EINVAL; } memset(st, 0, sizeof(*st)); if (ioctl(fd, XR_USB_SERIAL_GET_TIMING_STATUS, st) != 0) { return -errno; } return 0; } int pog422_timing_trace_reset(int fd) { if (fd < 0) { return -EINVAL; } if (ioctl(fd, XR_USB_SERIAL_RESET_TIMING_TRACE) != 0) { return -errno; } return 0; } int pog422_timing_trace_drain( int fd, struct xr_pog422_timing_event *out, uint32_t max_out, uint32_t *out_n, uint32_t *dropped ) { struct xr_pog422_timing_drain req; if (fd < 0 || !out || max_out == 0u) { return -EINVAL; } memset(&req, 0, sizeof(req)); req.user_buf = (uint64_t)(uintptr_t)out; req.max_out = max_out; if (ioctl(fd, XR_USB_SERIAL_DRAIN_TIMING_TRACE, &req) != 0) { return -errno; } if (out_n) { *out_n = req.out_n; } if (dropped) { *dropped = req.dropped; } return 0; } const char *pog422_slv_in_mode(void) { if (g_slv_in_gpio1 == 1) { return "gpio1-interrupt"; } if (g_slv_in_gpio1 == 0) { return "register-poll"; } return "unknown"; } static int maybe_rt(int use_rt, uint8_t priority) { if (!use_rt) { return 0; } struct sched_param sp; memset(&sp, 0, sizeof(sp)); sp.sched_priority = priority ? priority : 50; if (sched_setscheduler(0, SCHED_FIFO, &sp) != 0) { return -errno; } return 0; } static int inject_tx_driver(int fd, const uint8_t *frame, size_t len, uint32_t post_margin_us, uint32_t *tx_ns, uint32_t *drain_wait_ns, uint32_t *tx_fifo_before, uint32_t *tx_fifo_after_bulk, uint32_t *tx_fifo_after_drain, uint32_t *usb_actual) { struct xr_pog422_inject_tx req; if (len == 0u || len > POG422_INJECT_WIDE_MAX_FRAME) { return -EINVAL; } memset(&req, 0, sizeof(req)); req.user_frame = (uint64_t)(uintptr_t)frame; req.frame_len = (uint32_t)len; /* Air @57600 9-bit + slack; MTC 1170 needs ~223 ms on the wire alone. */ { uint32_t air_ms = (uint32_t)((len * 11u * 1000u) / POG422_BAUD) + 500u; req.timeout_ms = air_ms > 500u ? air_ms : 500u; } req.post_margin_us = post_margin_us; if (ioctl(fd, XR_USB_SERIAL_INJECT_TX, &req) != 0) { /* Still harvest diag outs if driver 1S+ filled them before failing. */ if (tx_ns) { *tx_ns = req.tx_ns; } if (drain_wait_ns) { *drain_wait_ns = req.drain_wait_ns; } if (tx_fifo_before) { *tx_fifo_before = req.tx_fifo_before; } if (tx_fifo_after_bulk) { *tx_fifo_after_bulk = req.tx_fifo_after_bulk; } if (tx_fifo_after_drain) { *tx_fifo_after_drain = req.tx_fifo_after_drain; } if (usb_actual) { *usb_actual = req.usb_actual; } return -errno; } if (tx_ns) { *tx_ns = req.tx_ns; } if (drain_wait_ns) { *drain_wait_ns = req.drain_wait_ns; } if (tx_fifo_before) { *tx_fifo_before = req.tx_fifo_before; } if (tx_fifo_after_bulk) { *tx_fifo_after_bulk = req.tx_fifo_after_bulk; } if (tx_fifo_after_drain) { *tx_fifo_after_drain = req.tx_fifo_after_drain; } if (usb_actual) { *usb_actual = req.usb_actual; } return 0; } const char *pog422_inject_version(void) { return POG422_EG_VERSION; } static void inject_forensics_rx_before(pog422_inject_result_t *result, int fd) { uint32_t fifo = 0; if (!result) { return; } if (pog422_hat_read_rx_fifo_count(fd, 2, &fifo) == 0) { result->rx_fifo_before = fifo; } } static void copper_push( pog422_inject_result_t *result, int fd, uint8_t phase, uint64_t t_de_on_ns ) { #if !POG422_HW_DEBUG (void)result; (void)fd; (void)phase; (void)t_de_on_ns; return; /* prod: no GPIO_STAT/FIFO control-transfer sampling */ #else pog422_copper_sample_t *s; unsigned gpio_stat = 0; uint32_t tx_fifo = 0xffffu; uint32_t rx_fifo = 0xffffu; uint64_t now; if (!result || result->copper_n >= POG422_COPPER_PHASE_MAX) { return; } s = &result->copper[result->copper_n]; memset(s, 0, sizeof(*s)); now = monotonic_ns(); s->phase = phase; if (t_de_on_ns != 0u && now >= t_de_on_ns) { s->t_us = (uint32_t)((now - t_de_on_ns) / 1000ull); } if (xr_get_reg_ch(fd, 2, GPIO_STAT_REG, &gpio_stat) == 0) { s->gpio_stat = (uint16_t)(gpio_stat & 0xffffu); /* Match slv_in_from_register: GPIOB1 active-low in GPIO_STAT. */ s->gpio1 = (gpio_stat & GPIO1_STAT_MASK) ? 0u : 1u; s->gpio7 = (gpio_stat & POG422_GPIO7_BIT) ? 1u : 0u; } if (pog422_hat_read_tx_fifo_count(fd, 2, &tx_fifo) == 0) { s->tx_fifo = (uint16_t)(tx_fifo & 0x3ffu); } if (pog422_hat_read_rx_fifo_count(fd, 2, &rx_fifo) == 0) { s->rx_fifo = (uint16_t)(rx_fifo & 0x3ffu); } result->copper_n++; #endif } static void inject_forensics_finish( pog422_inject_result_t *result, int fd, const uint8_t *frame, size_t len, uint8_t method, uint8_t wide_is_wire, int16_t driver_errno ) { uint32_t fifo = 0; size_t built; if (!result || !frame || len == 0u) { return; } /* Soft post-DE GET_REG only when full forensics requested (not hot path). */ if (inject_forensics_full() && pog422_hat_read_rx_fifo_count(fd, 2, &fifo) == 0) { result->rx_fifo_after = fifo; } result->inject_method = method; result->wide_is_wire = wide_is_wire; result->driver_errno = driver_errno; built = build_wide_frame(frame, len, result->wide_buf, sizeof(result->wide_buf)); result->wide_len = built > 0xffffu ? 0u : (uint16_t)built; } int pog422_gpio7_tx(int fd) { return gpio7_sl_out(fd, 1); } int pog422_gpio7_rx(int fd) { return gpio7_sl_out(fd, 0); } int pog422_port_enable_wide(int fd) { int on = 0; int rc; if (fd < 0) { return -EINVAL; } rc = preciseflags_set(fd, 1); if (rc == 0) { return 0; } /* * Driver 1Q: SET_PRECISE_FLAGS enables wide but returns ENOTTY (missing rv=0). * Driver 1R fixes the ioctl return; until then verify via GET_RX_STATS (1H+). */ if (rc == -ENOTTY && preciseflags_get(fd, &on) == 0 && on) { return 0; } if (preciseflags_get(fd, &on) == 0 && on) { return 0; } return rc; } int pog422_hat_init(int fd) { static const int b142x_channels[] = {POG422_B142X_CHANNELS}; size_t i; int rc; if (fd < 0) { return -EINVAL; } for (i = 0; i < sizeof(b142x_channels) / sizeof(b142x_channels[0]); i++) { int ch = b142x_channels[i]; unsigned uf = 0; rc = (ch == 2) ? manual_gpio_slv_on_channel(fd, ch) : rs485_auto_on_channel(fd, ch); if (rc != 0) { fprintf(stderr, "pog422_hat_init: ch%d GPIO setup failed: %s\n", ch, strerror(-rc)); return rc; } /* Guard: FLOW_CONTROL bit3 must stay clear (UART Half-Duplex mutes RX). */ if (xr_get_reg_ch(fd, ch, UART_FLOW_REG, &uf) == 0 && (uf & UART_FLOW_HALF_DUPLEX_BIT) != 0u) { fprintf(stderr, "pog422_hat_init: ch%d FLOW_CONTROL=0x%x has Half-Duplex bit — clearing\n", ch, uf); rc = xr_set_reg_ch(fd, ch, UART_FLOW_REG, uf & ~UART_FLOW_HALF_DUPLEX_BIT); if (rc != 0) { return rc; } } } rc = pog422_port_enable_wide(fd); if (rc != 0) { fprintf(stderr, "pog422_hat_init: SET_PRECISE_FLAGS failed: %s " "(driver 1Q: rebuild inject-shim; 1R+ fixes ioctl return)\n", strerror(-rc)); return rc; } slv_in_probe_mode(fd); return 0; } int pog422_hat_read_regs(int fd, uint32_t *gpio_mode, uint32_t *uart_flow) { unsigned gm = 0; unsigned uf = 0; int rc; if (fd < 0 || !gpio_mode || !uart_flow) { return -EINVAL; } rc = xr_get_reg_ch(fd, 2, GPIO_MODE_REG, &gm); if (rc != 0) { return rc; } rc = xr_get_reg_ch(fd, 2, UART_FLOW_REG, &uf); if (rc != 0) { return rc; } *gpio_mode = gm; *uart_flow = uf; return 0; } int pog422_hat_read_regs_ch(int fd, int channel, uint32_t *gpio_mode, uint32_t *uart_flow) { unsigned gm = 0; unsigned uf = 0; int rc; if (fd < 0 || !gpio_mode || !uart_flow) { return -EINVAL; } rc = xr_get_reg_ch(fd, channel, GPIO_MODE_REG, &gm); if (rc != 0) { return rc; } rc = xr_get_reg_ch(fd, channel, UART_FLOW_REG, &uf); if (rc != 0) { return rc; } *gpio_mode = gm; *uart_flow = uf; return 0; } int pog422_hat_read_rx_fifo_count(int fd, int channel, uint32_t *count) { unsigned raw = 0; int rc; if (fd < 0 || !count) { return -EINVAL; } rc = xr_get_reg_ch(fd, channel, RX_FIFO_COUNT_REG, &raw); if (rc != 0) { return rc; } *count = raw & 0x3ffu; /* XR21 RX_FIFO_COUNT[9:0] */ return 0; } int pog422_hat_read_tx_fifo_count(int fd, int channel, uint32_t *count) { unsigned raw = 0; int rc; if (fd < 0 || !count) { return -EINVAL; } rc = xr_get_reg_ch(fd, channel, TX_FIFO_COUNT_REG, &raw); if (rc != 0) { return rc; } *count = raw & 0x3ffu; /* XR21 TX_FIFO_COUNT[9:0] */ return 0; } int pog422_hat_read_rx_stats(int fd, struct xr_pog422_rx_stats *stats) { if (fd < 0 || !stats) { return -EINVAL; } if (ioctl(fd, XR_USB_SERIAL_GET_RX_STATS, stats) != 0) { return -errno; } return 0; } int pog422_hat_reset_rx_stats(int fd) { if (fd < 0) { return -EINVAL; } if (ioctl(fd, XR_USB_SERIAL_RESET_RX_STATS, NULL) != 0) { return -errno; } return 0; } int pog422_dttty_enable(int fd, int enable) { if (fd < 0) { return -EINVAL; } if (ioctl(fd, XR_USB_SERIAL_SET_DTTTY_RX, (unsigned long)(enable ? 1 : 0)) != 0) { return -errno; } return 0; } int pog422_dttty_is_enabled(int fd) { struct xr_pog422_dttty_avail info; if (fd < 0) { return -EINVAL; } if (ioctl(fd, XR_USB_SERIAL_GET_DTTTY_RX_AVAIL, &info) != 0) { return -errno; } return info.dttty_rx ? 1 : 0; } int pog422_dttty_avail(int fd, uint32_t *avail) { struct xr_pog422_dttty_avail info; if (fd < 0 || !avail) { return -EINVAL; } if (ioctl(fd, XR_USB_SERIAL_GET_DTTTY_RX_AVAIL, &info) != 0) { return -errno; } if (!info.dttty_rx) { return -ENOTTY; } *avail = info.avail; return 0; } int pog422_dttty_flush(int fd) { if (fd < 0) { return -EINVAL; } if (ioctl(fd, XR_USB_SERIAL_FLUSH_DTTTY_RX, NULL) != 0) { return -errno; } return 0; } ssize_t pog422_dttty_read(int fd, void *buf, size_t max_len) { struct xr_pog422_dttty_read req; if (fd < 0 || !buf || max_len == 0) { return -EINVAL; } if (max_len > 4096u) { max_len = 4096u; } memset(&req, 0, sizeof(req)); req.user_buf = (uint64_t)(uintptr_t)buf; req.max_len = (uint32_t)max_len; if (ioctl(fd, XR_USB_SERIAL_READ_DTTTY_RX, &req) != 0) { return -errno; } return (ssize_t)req.out_len; } int pog422_wide_rx_enable(int fd, int enable) { if (fd < 0) { return -EINVAL; } if (ioctl(fd, XR_USB_SERIAL_SET_WIDE_RX, (unsigned long)(enable ? 1 : 0)) != 0) { return -errno; } return 0; } int pog422_wide_rx_is_enabled(int fd) { struct xr_pog422_wide_rx_avail info; if (fd < 0) { return -EINVAL; } if (ioctl(fd, XR_USB_SERIAL_GET_WIDE_RX_AVAIL, &info) != 0) { return -errno; } return info.wide_rx ? 1 : 0; } int pog422_wide_rx_avail(int fd, uint32_t *avail) { struct xr_pog422_wide_rx_avail info; if (fd < 0 || !avail) { return -EINVAL; } if (ioctl(fd, XR_USB_SERIAL_GET_WIDE_RX_AVAIL, &info) != 0) { return -errno; } if (!info.wide_rx) { return -ENOTTY; } *avail = info.avail; return 0; } int pog422_wide_rx_flush(int fd) { if (fd < 0) { return -EINVAL; } if (ioctl(fd, XR_USB_SERIAL_FLUSH_WIDE_RX, NULL) != 0) { return -errno; } return 0; } ssize_t pog422_wide_rx_read(int fd, void *buf, size_t max_len) { struct xr_pog422_wide_rx_read req; if (fd < 0 || !buf || max_len == 0) { return -EINVAL; } if (max_len > 8192u) { max_len = 8192u; } max_len &= ~(size_t)1u; if (max_len == 0) { return -EINVAL; } memset(&req, 0, sizeof(req)); req.user_buf = (uint64_t)(uintptr_t)buf; req.max_len = (uint32_t)max_len; if (ioctl(fd, XR_USB_SERIAL_READ_WIDE_RX, &req) != 0) { return -errno; } return (ssize_t)req.out_len; } int pog422_inject_burst( int fd, const uint8_t *frame, size_t len, const pog422_inject_opts_t *opts, pog422_inject_result_t *result ) { if (!result) { return -EINVAL; } memset(result, 0, sizeof(*result)); result->air_us = (uint32_t)((len * POG422_BITS_PER_BYTE * 1000000ull) / POG422_BAUD); if (fd < 0 || !frame || len == 0) { result->err = EINVAL; return -EINVAL; } /* EG 2.2.1.5: ignore opts->pre_tx_us — GPIO7 on then immediate INJECT_TX. */ uint32_t pre_us = 0u; int rc = 0; int wide_on = 0; const uint64_t t0 = monotonic_ns(); /* * EG 3.0.2.4: hat_init already enables wide; re-probe only until cached * (GET_RX_STATS is a multi-ms USB control transfer on every inject). */ if (g_wide_on_cached) { wide_on = 1; } else if (preciseflags_get(fd, &wide_on) != 0) { /* GET_RX_STATS needs driver 1H+; wide enabled at hat_init. */ wide_on = 1; g_wide_on_cached = 1; } if (!wide_on && preciseflags_set(fd, 1) != 0) { result->err = EINVAL; result->total_ns = monotonic_ns() - t0; return -EINVAL; } g_wide_on_cached = 1; result->require_idle = opts ? opts->require_idle : 0u; result->idle_wait_budget_ms = opts ? (uint16_t)opts->idle_wait_ms : 0u; result->idle_wait_elapsed_us = 0u; result->idle_saw_busy = 0u; result->pre_bulk_us = 0u; result->bulk_wall_us = 0u; (void)slv_in_phase_active(fd, &result->slv_in_before); if (result->slv_in_before) { uint8_t require_idle = opts ? opts->require_idle : 0u; uint32_t idle_wait_ms = opts ? (uint32_t)opts->idle_wait_ms : 0u; if (!require_idle || idle_wait_ms == 0u) { result->err = EBUSY; result->total_ns = monotonic_ns() - t0; return -EBUSY; } { const uint64_t idle_t0 = monotonic_ns(); const uint64_t idle_deadline = idle_t0 + ((uint64_t)idle_wait_ms * 1000000ull); uint8_t live = 1u; result->idle_saw_busy = 1u; while (monotonic_ns() < idle_deadline) { if (opts && opts->poll_fn) { opts->poll_fn(opts->poll_ctx); } live = 0u; (void)slv_in_phase_active(fd, &live); if (!live) { break; } usleep(100); } result->idle_wait_elapsed_us = (uint32_t)((monotonic_ns() - idle_t0) / 1000ull); (void)slv_in_phase_active(fd, &result->slv_in_before); if (result->slv_in_before) { result->err = EBUSY; result->total_ns = monotonic_ns() - t0; return -EBUSY; } } } if (opts && opts->flush_rx_before_tx) { if (tcflush(fd, TCIFLUSH) != 0) { result->err = errno; result->total_ns = monotonic_ns() - t0; return -errno; } } if (opts && opts->use_realtime) { (void)maybe_rt(1, opts->rt_priority); } /* Soft forensics default: do not pad bulk with USB GET_REG pre-TX. */ if (inject_forensics_full()) { inject_forensics_rx_before(result, fd); (void)pog422_hat_read_regs(fd, &result->gpio_mode, &result->uart_flow); /* PRE_DE copper only when full forensics — multi GET_REG pads pre_bulk. */ copper_push(result, fd, POG422_COPPER_PHASE_PRE_DE, 0); } /* * EG 2.2.1.13: do NOT assert GPIO7 here. Driver MANUAL_DE asserts after * mutex/pm, immediately before bulk — so ioctl wait no longer holds DE. */ gpio1_inject_trace_t gpio1_tr; const uint64_t t_ioctl0 = monotonic_ns(); gpio1_trace_init_seed(&gpio1_tr, t_ioctl0, result->slv_in_before); sleep_us_poll(pre_us, opts, fd, &gpio1_tr); const uint64_t t_tx0 = monotonic_ns(); result->pre_bulk_us = (uint32_t)((t_tx0 - t0) / 1000ull); uint32_t drv_tx_ns = 0; uint32_t drv_drain_wait_ns = 0; rc = inject_tx_driver(fd, frame, len, POG422_INJECT_POST_MANUAL_DE, &drv_tx_ns, &drv_drain_wait_ns, &result->tx_fifo_before, &result->tx_fifo_after_bulk, &result->tx_fifo_after_drain, &result->usb_actual); { /* Approximate DE assert ≈ bulk start (driver raises GPIO7 then usb_*_msg). */ const uint64_t t_ioctl_end = monotonic_ns(); result->bulk_wall_us = (uint32_t)((t_ioctl_end - t_tx0) / 1000ull); const uint64_t t_de_on = (drv_tx_ns > 0u && (uint64_t)drv_tx_ns < t_ioctl_end) ? (t_ioctl_end - (uint64_t)drv_tx_ns) : t_tx0; if (rc != 0) { /* Driver drops DE on bulk failure; belt-and-suspenders release. */ (void)gpio7_sl_out(fd, 0); if (inject_forensics_full()) { copper_push(result, fd, POG422_COPPER_PHASE_POST_OFF, t_de_on); } gpio1_trace_to_result(&gpio1_tr, result); result->tx_ns = monotonic_ns() - t_tx0; result->de_hold_us = (uint32_t)((monotonic_ns() - t_de_on) / 1000ull); result->de_release_reason = POG422_DE_RELEASE_ERROR; result->total_ns = monotonic_ns() - t0; result->err = (uint32_t)(rc < 0 ? -rc : errno); inject_forensics_finish( result, fd, frame, len, POG422_INJECT_METHOD_NONE, 1u, (int16_t)(rc < 0 ? -rc : errno)); return rc; } result->tx_ns = drv_tx_ns ? drv_tx_ns : (uint64_t)(monotonic_ns() - t_tx0); result->drain_wait_ns = drv_drain_wait_ns; /* GPIO1-event snapshot only — never GET_REG while DE is high. */ { const uint8_t hs_now = gpio1_de_sample(fd, &gpio1_tr); result->slv_in_after_on = hs_now; result->slv_in_after_body = hs_now; } /* * Hybrid DE release from estimated DE assert: min(TEMT, air+margin). * EG 3.0.1.15: do NOT poll capture/GET_REG while DE is high — generic * USB control timeout is 5000 ms and pinned SLV-OUT for five seconds * after a healthy bulk (pos5 22:25 / 22:44 UTC HS soak). */ { uint32_t margin_us = (opts && opts->post_hold_us) ? opts->post_hold_us : POG422_DE_AIR_MARGIN_DEFAULT_US; const uint64_t de_deadline_ns = t_de_on + ((uint64_t)result->air_us + (uint64_t)margin_us) * 1000ull; const uint64_t hang_guard_ns = t_de_on + ((uint64_t)result->air_us + (uint64_t)margin_us + 50000ull) * 1000ull; uint32_t wait_accum_ns = 0u; uint8_t got_temt = 0u; result->de_release_reason = POG422_DE_RELEASE_NONE; while (monotonic_ns() < de_deadline_ns && monotonic_ns() < hang_guard_ns) { struct xr_pog422_tx_empty te; int wrc; uint64_t now = monotonic_ns(); uint64_t rem_ns = de_deadline_ns - now; uint32_t slice_ms; if (rem_ns == 0ull) { break; } slice_ms = (uint32_t)((rem_ns + 999999ull) / 1000000ull); if (slice_ms == 0u) { slice_ms = 1u; } if (slice_ms > 2u) { slice_ms = 2u; } /* Cached GPIO1 only — keeps the HS trace live without USB. */ (void)gpio1_trace_poll_event(fd, &gpio1_tr); wrc = wait_tx_empty(fd, slice_ms, &te); wait_accum_ns += te.wait_ns; result->tx_fifo_after_drain = te.tx_fifo; result->tx_empty_reason = te.reason; if (te.temt) { got_temt = 1u; result->de_release_reason = POG422_DE_RELEASE_TEMT; break; } if (wrc != 0 && wrc != -ETIMEDOUT && wrc != -EAGAIN) { result->de_release_reason = POG422_DE_RELEASE_ERROR; result->err = (uint32_t)(-wrc); break; } } if (result->de_release_reason == POG422_DE_RELEASE_NONE) { if (got_temt) { result->de_release_reason = POG422_DE_RELEASE_TEMT; } else if (monotonic_ns() >= de_deadline_ns) { result->de_release_reason = POG422_DE_RELEASE_DEADLINE; } else { result->de_release_reason = POG422_DE_RELEASE_TIMEOUT; } } result->drain_wait_ns = wait_accum_ns ? wait_accum_ns : result->drain_wait_ns; } /* Snapshot HS from GPIO1 trace only — no GET_REG while DE still high. */ result->slv_in_after_drain = gpio1_de_sample(fd, &gpio1_tr); (void)gpio7_sl_out(fd, 0); result->de_hold_us = (uint32_t)((monotonic_ns() - t_de_on) / 1000ull); /* Soft: copper POST_OFF is luxury (control transfers) — after DE is free. */ if (inject_forensics_full()) { copper_push(result, fd, POG422_COPPER_PHASE_POST_OFF, t_de_on); } result->total_ns = monotonic_ns() - t0; /* Prefer GPIO1; last_active from drain sample avoids an extra ioctl. */ result->slv_in_after_off = result->slv_in_after_drain; if (result->slv_in_after_off == POG422_SLV_IN_UNKNOWN) { (void)slv_in_phase_active(fd, &result->slv_in_after_off); } gpio1_trace_to_result(&gpio1_tr, result); inject_forensics_finish( result, fd, frame, len, POG422_INJECT_METHOD_DRIVER_WIDE, 1u, 0); if (result->de_release_reason == POG422_DE_RELEASE_TEMT || result->de_release_reason == POG422_DE_RELEASE_DEADLINE || result->de_release_reason == POG422_DE_RELEASE_TIMEOUT) { result->err = 0; return 0; } return result->err ? -(int)result->err : -EIO; } }