lpc_ip3516: fix isochronous transfer scheduling

- Select one ISO service slot per transfer: the PTD uFrame field holds the
  frame number in [7:3] (UM11126 Table 814), and the microframe goes in
  uframe_active. The next slot keeps the endpoint's interval phase.
- Keep the ISO scheduling fields (S-mask, C-mask, split slot) per PTD and
  restore them on every transfer, since the controller consumes them;
  activate the PTD only afterwards. Each submission covers at most one
  endpoint packet.
- FS endpoints behind a HS hub use split transactions with windows from the
  shared best-effort allocator (hub_iso_split_slot()), keyed by the upstream
  HS hub and port, also through an intervening FS hub. IN is limited to
  564 bytes, OUT to 1023; split ISO intervals are limited to 16 ms and HS to
  32 ms by the five frame bits the controller compares.
- audio_host builds for LPCXpresso55S28/S69. Host audio needs RHPORT_HOST=1
  (IP3516); port 0 (OHCI) does not support ISO.

Signed-off-by: HiFiPhile <admin@hifiphile.com>
diff --git a/examples/host/audio_host/only.txt b/examples/host/audio_host/only.txt
index 8105108..2c02b4b 100644
--- a/examples/host/audio_host/only.txt
+++ b/examples/host/audio_host/only.txt
@@ -30,3 +30,7 @@
 mcu:STM32WBA
 mcu:LPC43XX
 mcu:MIMXRT1XXX
+# LPC55S28/69: switch RHPORT_HOST to 1 in board.cmake/board.mk for host-only audio (IP3516).
+# Port 0 (OHCI) does not support ISO.
+board:lpcxpresso55s28
+board:lpcxpresso55s69
diff --git a/src/portable/nxp/lpc_ip3516/hcd_lpc_ip3516.c b/src/portable/nxp/lpc_ip3516/hcd_lpc_ip3516.c
index b9ad172..01f095d 100644
--- a/src/portable/nxp/lpc_ip3516/hcd_lpc_ip3516.c
+++ b/src/portable/nxp/lpc_ip3516/hcd_lpc_ip3516.c
@@ -16,6 +16,9 @@
 #include "common/tusb_common.h"
 #include "host/hcd.h"
 #include "host/usbh.h"
+#if CFG_TUH_HUB
+#include "host/hub.h"
+#endif
 #include "hcd_lpc_ip3516.h"
 
 #if TU_CHECK_MCU(OPT_MCU_LPC55, OPT_MCU_LPC54)
@@ -64,6 +67,15 @@
 
 CFG_TUH_MEM_SECTION TU_ATTR_ALIGNED(1024) static ip3516_ptd_t _ptd;
 
+// ISO scheduling fields are consumed by hardware and must be restored for each transfer.
+static struct {
+  uint16_t interval;
+  uint16_t max_packet_size;
+  uint8_t uframe_active;
+  uint8_t uframe_complete;
+  uint8_t split_slot;
+} _iso_ep[IP3516_PTL_NUM];
+
 static struct {
   uint32_t uframe_number;
   uint32_t uframe_length;
@@ -82,6 +94,27 @@
   return (xfer_type == TUSB_XFER_CONTROL || xfer_type == TUSB_XFER_BULK);
 }
 
+// Reserve coarse ISO payload windows on the downstream bus. Assume MTT hubs:
+// each (hub address, downstream port) has an independent schedule.
+// OUT grows from slot 0, IN from the end, leaving two extra complete attempts
+// before the frame ends. Windows use maximum packet sizes, not current data.
+// This is best effort: no full bus-time admission or interrupt reservations.
+#if CFG_TUH_HUB
+static bool iso_split_slot(uint8_t hub_addr, uint8_t hub_port, uint16_t packet_size, uint8_t dir, uint8_t *slot) {
+  uint8_t used = 0;
+  for (uint8_t i = 0; i < IP3516_PTL_NUM; i++) {
+    const ip3516_ptl_t *ptd = &_ptd.iso[i];
+    if (!is_ptd_free(ptd->ctrl1) && ptd->ctrl2.split &&
+        ptd->ctrl2.hub_addr == hub_addr && ptd->ctrl2.hub_port == hub_port) {
+      const uint8_t count = (uint8_t)((_iso_ep[i].max_packet_size + 187u) / 188u);
+      used |= ((1u << count) - 1u) << _iso_ep[i].split_slot;
+    }
+  }
+
+  return hub_iso_split_slot(used, packet_size, dir, slot);
+}
+#endif
+
 static inline void ptd_clear_state(ptd_state_t *state) {
   ptd_state_t local = {.value = 0};
   local.ep_type     = state->ep_type;     // preserve ep_type
@@ -279,6 +312,30 @@
   ptd_data_t  *ptd_data  = (ptd_data_t *)(ptd_ptr + offsetof(ip3516_atl_t, data));
   ptd_state_t *ptd_state = (ptd_state_t *)(ptd_ptr + offsetof(ip3516_atl_t, state));
 
+  if (ptd_state->ep_type == TUSB_XFER_ISOCHRONOUS) {
+    ip3516_ptl_t *ptd = (ip3516_ptl_t *)ptd_ptr;
+    const uint8_t index = (uint8_t)(ptd - _ptd.iso);
+    // Each submission covers at most one endpoint packet, including splits.
+    TU_VERIFY(buflen <= _iso_ep[index].max_packet_size);
+    const uint32_t interval = _iso_ep[index].interval;
+    const uint32_t now = (USBHSH->FLADJ_FRINDEX & USBHSH_FLADJ_FRINDEX_FRINDEX_MASK) >>
+                         USBHSH_FLADJ_FRINDEX_FRINDEX_SHIFT;
+    const uint32_t next = (now + interval) & ~(interval - 1u);
+
+    // UM11126, Table 814: ISO uFrame[7:3] is a frame number; bits [2:0]
+    // do not encode a polling interval. Select one service slot per transfer.
+    ptd_ctrl1->uframe = next & 0xf8u;
+    ptd->status.value = 0;
+    ptd->status.uframe_active = ptd_ctrl2->split ? _iso_ep[index].uframe_active : (1u << (next & 7u));
+    if (ptd_ctrl2->split && ep_dir == TUSB_DIR_OUT) {
+      const uint8_t slots = (uint8_t)tu_max32(1, (buflen + 187u) / 188u);
+      ptd->status.uframe_active = ((1u << slots) - 1u) << _iso_ep[index].split_slot;
+    }
+    ptd->iso_in_0.value = _iso_ep[index].uframe_complete;
+    ptd->iso_in_1 = 0;
+    ptd->iso_in_2 = 0;
+  }
+
   // Setup data buffer and length
   ptd_data->data_addr = (uint32_t)(uintptr_t)buffer & IP3516_PTD_DATA_ADDR_MASK;
   ptd_data->xfer_len  = buflen;
@@ -303,7 +360,7 @@
     ptd_state->nak_cnt = 0x0f;
   }
 
-  // Activate PTD
+  // Activate only after the ISO scheduling fields have been restored.
   ptd_ctrl1->valid  = 1;
   ptd_state->active = 1;
 
@@ -326,6 +383,7 @@
   USBHSH->PORTMODE = USBHSH_PORTMODE_SW_CTRL_PDCOM_MASK;
 
   tu_memclr(&_ptd, sizeof(_ptd));
+  tu_memclr(_iso_ep, sizeof(_iso_ep));
   tu_varclr(&_hcd_data);
 
   // Set base addresses
@@ -479,155 +537,127 @@
 
 // Open an endpoint
 bool hcd_edpt_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_endpoint_t *ep_desc) {
-  (void)rhport;
-
   const uint8_t          ep_num    = tu_edpt_number(ep_desc->bEndpointAddress);
+  const tusb_dir_t       ep_dir    = tu_edpt_dir(ep_desc->bEndpointAddress);
   const tusb_xfer_type_t xfer_type = (tusb_xfer_type_t)ep_desc->bmAttributes.xfer;
 
   tuh_bus_info_t bus_info;
   tuh_bus_info_get(dev_addr, &bus_info);
 
-  // Find a free PTD
-  uint8_t ptd_index = ptd_find_free(xfer_type);
-  TU_ASSERT(ptd_index != TUSB_INDEX_INVALID_8);
+  const bool high_speed = bus_info.speed == TUSB_SPEED_HIGH;
+  const bool split = hcd_port_speed_get(rhport) == TUSB_SPEED_HIGH && !high_speed;
+  if (split) {
+    // A full-speed hub may sit between this device and its high-speed TT.
+    while (bus_info.hub_addr != 0) {
+      tuh_bus_info_t hub;
+      TU_VERIFY(tuh_bus_info_get(bus_info.hub_addr, &hub));
+      if (hub.speed == TUSB_SPEED_HIGH) {
+        break;
+      }
+      bus_info.hub_addr = hub.hub_addr;
+      bus_info.hub_port = hub.hub_port;
+    }
+    TU_VERIFY(bus_info.hub_addr != 0);
+  }
 
-  // Configure PTD
-  intptr_t              ptd_ptr = get_ptd_from_index(xfer_type, ptd_index);
-  volatile ptd_ctrl1_t *ctrl1   = (volatile ptd_ctrl1_t *)(ptd_ptr + offsetof(ip3516_atl_t, ctrl1));
-  volatile ptd_ctrl2_t *ctrl2   = (volatile ptd_ctrl2_t *)(ptd_ptr + offsetof(ip3516_atl_t, ctrl2));
-  volatile ptd_data_t  *data    = (volatile ptd_data_t *)(ptd_ptr + offsetof(ip3516_atl_t, data));
-  volatile ptd_state_t *state   = (volatile ptd_state_t *)(ptd_ptr + offsetof(ip3516_atl_t, state));
+  const uint8_t ptd_index = ptd_find_free(xfer_type);
+  TU_VERIFY(ptd_index != TUSB_INDEX_INVALID_8);
 
-  // Initialize PTD fields
-  ctrl1->mps  = ep_desc->wMaxPacketSize;
-  ctrl1->mult = 1;
+  uint16_t mps = ep_desc->wMaxPacketSize;
+  uint8_t uframe = 0;
+  uint8_t uframe_active = 0;
+  uint8_t uframe_complete = 0;
 
+  switch (xfer_type) {
+    case TUSB_XFER_ISOCHRONOUS: {
+      // ISO compares five frame bits, allowing at most 32 ms at HS. FS/split
+      // must target a different frame, so its power-of-two limit is 16 ms.
+      const uint8_t max_binterval = high_speed ? 9 : 5;
+      TU_VERIFY(ep_desc->bInterval > 0 && ep_desc->bInterval <= max_binterval);
+      const uint16_t interval = (uint16_t)(1u << (ep_desc->bInterval - 1u + (high_speed ? 0u : 3u)));
+      uint8_t split_slot = 0;
+      uframe_active = 1; // Non-split ISO gets its actual frame/slot at submission.
+      #if CFG_TUH_HUB
+      if (split) {
+        // Reserve before publishing this endpoint. Existing windows stay put.
+        TU_VERIFY(bus_info.speed == TUSB_SPEED_FULL);
+        TU_VERIFY(mps > 0 && mps <= (ep_dir == TUSB_DIR_IN ? 564 : 1023));
+        TU_VERIFY(iso_split_slot(bus_info.hub_addr, bus_info.hub_port, mps, ep_dir, &split_slot));
+        uframe_active = 1u << split_slot;
+        if (ep_dir == TUSB_DIR_IN) {
+          const uint8_t slots = (uint8_t)((mps + 187u) / 188u);
+          uframe_complete = ((1u << (slots + 2u)) - 1u) << (split_slot + 2u);
+          mps = tu_min16(mps, 192);
+        } else {
+          mps = tu_min16(mps, 188);
+        }
+      }
+      #endif
+      _iso_ep[ptd_index].interval = interval;
+      _iso_ep[ptd_index].max_packet_size = ep_desc->wMaxPacketSize;
+      _iso_ep[ptd_index].split_slot = split_slot;
+      _iso_ep[ptd_index].uframe_active = uframe_active;
+      _iso_ep[ptd_index].uframe_complete = uframe_complete;
+      break;
+    }
+
+    case TUSB_XFER_INTERRUPT: {
+      TU_VERIFY(ep_desc->bInterval > 0);
+      uint32_t interval;
+      if (high_speed) {
+        TU_VERIFY(ep_desc->bInterval <= 16);
+        interval = 1u << (ep_desc->bInterval - 1u);
+      } else {
+        // Full-/low-speed interrupt intervals are linear frame counts.
+        interval = 1u << tu_log2((uint32_t)ep_desc->bInterval << 3);
+      }
+      interval = tu_min32(interval, IP3516_MAX_UFRAME);
+      switch (interval) {
+        case 1: uframe_active = 0xff; break;
+        case 2: uframe_active = 0xaa; break;
+        case 4: uframe_active = 0x11; break;
+        default:
+          uframe_active = 0x01;
+          uframe = (uint8_t)(tu_log2(interval) - 3);
+          break;
+      }
+      if (split) {
+        // Spread starts between slots 0/1, with three complete attempts.
+        const uint8_t slot = ep_num & 1u;
+        uframe_active = 1u << slot;
+        uframe_complete = 0x1c << slot;
+      }
+      break;
+    }
+
+    default:
+      break;
+  }
+
+  // All validation is complete; publish the endpoint configuration.
+  const intptr_t ptd_ptr = get_ptd_from_index(xfer_type, ptd_index);
+  volatile ptd_ctrl1_t *ctrl1 = (volatile ptd_ctrl1_t *)(ptd_ptr + offsetof(ip3516_atl_t, ctrl1));
+  volatile ptd_ctrl2_t *ctrl2 = (volatile ptd_ctrl2_t *)(ptd_ptr + offsetof(ip3516_atl_t, ctrl2));
+  volatile ptd_data_t  *data  = (volatile ptd_data_t *)(ptd_ptr + offsetof(ip3516_atl_t, data));
+  volatile ptd_state_t *state = (volatile ptd_state_t *)(ptd_ptr + offsetof(ip3516_atl_t, state));
+
+  ctrl1->mps    = mps;
+  ctrl1->mult   = 1;
+  ctrl1->uframe = uframe;
   ctrl2->dev_addr = dev_addr;
   ctrl2->ep_num   = ep_num;
   ctrl2->speed    = bus_info.speed == TUSB_SPEED_LOW ? 2 : 0;
   ctrl2->hub_addr = bus_info.hub_addr;
   ctrl2->hub_port = bus_info.hub_port;
-  ctrl2->split    = (hcd_port_speed_get(rhport) == TUSB_SPEED_HIGH) && (bus_info.speed != TUSB_SPEED_HIGH) ? 1 : 0;
-
+  ctrl2->split    = split;
   data->intr = 1;
-
   state->ep_type = (uint32_t)xfer_type;
-  state->token   = tu_edpt_dir(ep_desc->bEndpointAddress) == TUSB_DIR_IN ? IP3516_PTD_TOKEN_IN : IP3516_PTD_TOKEN_OUT;
-
+  state->token   = ep_dir == TUSB_DIR_IN ? IP3516_PTD_TOKEN_IN : IP3516_PTD_TOKEN_OUT;
   if (!is_xfer_async(xfer_type)) {
     ip3516_ptl_t *ptd = (ip3516_ptl_t *)ptd_ptr;
-
-    uint32_t uframe_interval;
-    if (bus_info.speed == TUSB_SPEED_HIGH) {
-      uframe_interval = 1 << (ep_desc->bInterval - 1);
-    } else {
-      uframe_interval = ep_desc->bInterval << 3;
-      // round down to nearest power of 2
-      uframe_interval = 1 << tu_log2(uframe_interval);
-    }
-    uframe_interval = tu_min32(uframe_interval, IP3516_MAX_UFRAME);
-
-    // uframe_active is an 8-bit mask, where each bit corresponds to a micro-frame within a 1ms frame.
-    // A '1' indicates the endpoint should be polled in that micro-frame.
-    // For example:
-    // Interval 1 (poll every u-frame) -> mask is 0b11111111 (0xFF)
-    // Interval 2 (poll every 2nd u-frame, e.g., 0, 2, 4, 6) -> mask is 0b10101010 (0xAA)
-    // Interval 4 (poll every 4th u-frame, e.g., 0, 4) -> mask is 0b10001000 (0x88)
-    // Interval 8 (poll every 8th u-frame, e.g., 0) -> mask is 0b10000000 (0x80)
-    switch (uframe_interval) {
-      case 1:
-        ptd->status.uframe_active = 0xFF;
-        break;
-      case 2:
-        ptd->status.uframe_active = 0xAA;
-        break;
-      case 4:
-        ptd->status.uframe_active = 0x11;
-        break;
-      case 8:
-        ptd->status.uframe_active = 0x01;
-        break;
-      default:
-        // For intervals > 8, we poll once per frame (every 8 u-frames) and use ctrl1.uframe to skip frames.
-        ptd->status.uframe_active = 0x01;
-        if (uframe_interval >= 16) {
-          ctrl1->uframe = tu_log2(uframe_interval) - 3;
-        }
-        break;
-    }
-
-    if (ctrl2->split) {
-      // 11.18.1 Best Case Full-Speed Budget
-      //
-      // A microframe of time allows at most 187.5 raw bytes of signaling on a full-speed bus.
-      // The best case full-speed budget assumes that 188 full-speed bytes occur in each microframe.
-      //
-      // A 1 ms frame subdivided into microframes of budget time:
-      //
-      // Microframes            Y_0   Y_1   Y_2   Y_3   Y_4   Y_5   Y_6   Y_7
-      // Max wire time          187.5 187.5 187.5 187.5 187.5 187.5 32
-      // Best case wire budget  188   188   188   188   188   188   29
-      //
-      // 11.18.4  Host Split Transaction Scheduling Requirements
-      //
-      // 1. The host must never schedule a start-split in microframe Y_6.
-      // 2. For isochronous OUT full-speed transactions, for each microframe in which the transaction is
-      // budgeted, the host must schedule a 188 (or the remaining data size) data byte start-split transaction.
-      // For isochronous IN and interrupt IN/OUT full-/low-speed transactions, a single start-split must be
-      // scheduled in the microframe before the transaction is budgeted to start on the full-/low-speed bus.
-      // 3. For isochronous OUT full-speed transactions, the host must never schedule a complete-split. The
-      // TT response to a complete-split for an isochronous OUT is undefined.
-      //    For interrupt IN/OUT full-/low-speed transactions, the host must schedule a complete-split
-      // transaction in each of the two microframes following the first microframe in which the full-/low-
-      // speed transaction is budgeted.  An additional complete-split must also be scheduled in the third
-      // following microframe unless the full-/low-speed transaction was budgeted to start in microframe Y_6
-      //    For isochronous IN full-speed transactions, for each microframe in which the full-speed transaction
-      // is budgeted, a complete-split must be scheduled for each following microframe.
-      // Also, determine the last microframe in which a complete-split is scheduled, call it L.
-      // If L is less than Y_6, schedule additional complete-splits in microframe L+1 and L+2.
-      // If L is equal to Y_6, schedule one complete-split in microframe Y_7.
-      //
-      // TODO: Implement budget check scheduling
-      // Otherwise, it may cause bus contention with other split transfers
-      // Here we simply start interrupt transfers for Y_0 and Y1 and isochronous transfers for Y_2
-      if (xfer_type == TUSB_XFER_ISOCHRONOUS) {
-        const uint8_t    ss_slot = 2; // Start-split slot
-        const uint8_t    slots   = (ep_desc->wMaxPacketSize + 187) / 188;
-        const tusb_dir_t ep_dir  = tu_edpt_dir(ep_desc->bEndpointAddress);
-        if (ep_dir == TUSB_DIR_IN) {
-          if (ep_desc->wMaxPacketSize > 192) {
-            ctrl1->mps = 192;
-          }
-          ptd->status.uframe_active = 1 << ss_slot;
-          for (uint8_t i = 0; i < slots; i++) {
-            ptd->iso_in_0.uframe_complete |= 1 << (2 + ss_slot + i);
-          }
-          // Schedule additional complete-splits if needed
-          uint8_t last_complete = ss_slot + slots + 1;
-          if (last_complete < 6) {
-            ptd->iso_in_0.uframe_complete |= 1 << (ss_slot + last_complete + 1);
-            ptd->iso_in_0.uframe_complete |= 1 << (ss_slot + last_complete + 2);
-          } else if (last_complete == 6) {
-            ptd->iso_in_0.uframe_complete |= 1 << 7;
-          }
-        } else {
-          if (ep_desc->wMaxPacketSize > 188) {
-            ctrl1->mps = 188;
-          }
-          for (uint8_t i = 0; i < slots; i++) {
-            ptd->status.uframe_active |= 1 << (ss_slot + i);
-          }
-        }
-      } else {
-        // Start-split slot, jigging to avoid bus contention: EP odd -> Y_1, EP even -> Y_0
-        const uint8_t ss_slot     = ep_num & 0x01;
-        ptd->status.uframe_active = 1 << ss_slot;
-        // Complete-split slots: next 3 u-frames
-        ptd->iso_in_0.uframe_complete = 0x1c << ss_slot;
-      }
-    }
+    ptd->status.uframe_active = uframe_active;
+    ptd->iso_in_0.uframe_complete = uframe_complete;
   }
-
   return true;
 }