/* * Apple S5L8950X Synopsys USB OTG core (minimal SecureROM-facing model) * * This starts with the reset handshake used by SecureROM. More endpoint and * interrupt behavior can be added as the device-mode driver reaches it. */ #include "qemu/osdep.h" #include "qemu/log.h" #include "qemu/timer.h" #include "hw/arm/s5l8950x.h" #include "hw/core/cpu.h" #include "hw/core/irq.h" #include "hw/core/sysbus.h" #include "hw/core/qdev-properties.h" #include "hw/core/qdev-properties-system.h" #include "chardev/char-fe.h" #include "qapi/error.h" #include "qom/object.h" #include "exec/tb-flush.h" #include "exec/translation-block.h" #include "system/address-spaces.h" #include "system/dma.h" #include "target/arm/cpu.h" #include "../../windows/include/qemu_a6_usb_protocol.h" #define S5L8950X_USB_OTG_REGION_SIZE 0x10000 #define S5L8950X_USB_OTG_NUM_REGS \ (S5L8950X_USB_OTG_REGION_SIZE / sizeof(uint32_t)) #define USB_OTG_GRSTCTL 0x10 #define USB_OTG_GRSTCTL_CSFTRST BIT(0) #define USB_OTG_GRSTCTL_RXFFLSH BIT(4) #define USB_OTG_GRSTCTL_TXFFLSH BIT(5) #define USB_OTG_GRSTCTL_AHBIDLE BIT(31) #define USB_OTG_GAHBCFG 0x08 #define USB_OTG_GAHBCFG_GLBLINTRMSK BIT(0) #define USB_OTG_GINTSTS 0x14 #define USB_OTG_GINTMSK 0x18 #define USB_OTG_GINT_USBRST BIT(12) #define USB_OTG_GINT_ENUMDONE BIT(13) #define USB_OTG_GINT_IEPINT BIT(18) #define USB_OTG_GINT_OEPINT BIT(19) #define USB_OTG_DCTL 0x804 #define USB_OTG_DCTL_SFTDISCON BIT(1) #define USB_OTG_DIEPMSK 0x810 #define USB_OTG_DOEPMSK 0x814 #define USB_OTG_DAINT 0x818 #define USB_OTG_DAINTMSK 0x81c #define USB_OTG_DIEPCTL0 0x900 #define USB_OTG_DIEPINT0 0x908 #define USB_OTG_DIEPTSIZ0 0x910 #define USB_OTG_DIEPDMA0 0x914 #define USB_OTG_DOEPCTL0 0xb00 #define USB_OTG_DOEPINT0 0xb08 #define USB_OTG_DOEPTSIZ0 0xb10 #define USB_OTG_DOEPDMA0 0xb14 #define USB_OTG_EP_STRIDE 0x20 #define USB_OTG_EP_COUNT 8 #define USB_OTG_EPCTL_EPENA BIT(31) #define USB_OTG_EPCTL_STALL BIT(21) #define USB_OTG_EPINT_SETUP BIT(3) #define USB_OTG_EPINT_XFERCOMPL BIT(0) #define USB_OTG_EPTSIZ_XFERSIZE_MASK 0x0007ffffu #define USB_OTG_EPTSIZ_PKTCNT_MASK 0x1ff80000u #define USB_DFU_TRANSFER_SIZE 0x800 #define USB_DFU_STATUS_SIZE 6 #define USB_DFU_STATE_IDLE 2 #define USB_DFU_STATE_DNLOAD_IDLE 5 #define USB_DFU_STATE_MANIFEST_SYNC 6 #define USB_DFU_STATE_MANIFEST 7 #define USB_DFU_STATE_WAIT_RESET 8 #define USB_DFU_STATE_ERROR 10 #define USB_DFU_MAX_STATUS_POLLS 20 #define A6_SRAM_BASE 0x10000000u #define A6_SRAM_SIZE 0x00080000u #define A6_DRAM_BASE 0x80000000u #define A6_IBEC_BASE 0xbff00000u #define A6_AIC_MASK_CLR 0x3f204180u #define A6_USB_IRQ 11 #define A6_DFU_CAPTURE_MAX (16u * 1024u * 1024u) #define IMG3_MAGIC 0x496d6733u #define IMG3_TAG_TYPE 0x54595045u #define IMG3_TAG_DATA 0x44415441u #define IMG3_TYPE_IBSS 0x69627373u #define IMG3_TYPE_IBEC 0x69626563u #define IMG3_HEADER_SIZE 20u #define IMG3_ELEMENT_HEADER_SIZE 12u #define IMG3_SEARCH_SIZE 0x00010000u OBJECT_DECLARE_SIMPLE_TYPE(S5L8950XUSBOTGState, S5L8950X_USB_OTG) typedef enum S5L8950XUSBHostStage { USB_HOST_WAIT_DEVICE_SETUP, USB_HOST_WAIT_DEVICE_IN, USB_HOST_WAIT_DEVICE_STATUS_OUT, USB_HOST_WAIT_SET_ADDRESS_SETUP, USB_HOST_WAIT_SET_ADDRESS_STATUS_IN, USB_HOST_ADDRESS_ASSIGNED, USB_HOST_WAIT_SERIAL_IN, USB_HOST_WAIT_SERIAL_STATUS_OUT, USB_HOST_WAIT_CONFIG_HEADER_SETUP, USB_HOST_WAIT_CONFIG_HEADER_IN, USB_HOST_WAIT_CONFIG_HEADER_STATUS_OUT, USB_HOST_WAIT_CONFIG_FULL_SETUP, USB_HOST_WAIT_CONFIG_FULL_IN, USB_HOST_WAIT_CONFIG_FULL_STATUS_OUT, USB_HOST_WAIT_SET_CONFIGURATION_SETUP, USB_HOST_WAIT_SET_CONFIGURATION_STATUS_IN, USB_HOST_DFU_READY, USB_HOST_WAIT_DFU_GETSTATE_IN, USB_HOST_WAIT_DFU_GETSTATE_STATUS_OUT, USB_HOST_DFU_DNLOAD_READY, USB_HOST_WAIT_DFU_DNLOAD_DATA_OUT, USB_HOST_WAIT_DFU_DNLOAD_STATUS_IN, USB_HOST_WAIT_DFU_GETSTATUS_SETUP, USB_HOST_WAIT_DFU_GETSTATUS_IN, USB_HOST_WAIT_DFU_GETSTATUS_STATUS_OUT, USB_HOST_DFU_DONE, USB_HOST_DFU_ERROR, USB_HOST_BRIDGE_WAIT_DATA_IN, USB_HOST_BRIDGE_WAIT_STATUS_OUT, USB_HOST_BRIDGE_WAIT_DATA_OUT, USB_HOST_BRIDGE_WAIT_STATUS_IN, } S5L8950XUSBHostStage; typedef enum S5L8950XDFUPacketKind { USB_DFU_PACKET_PAYLOAD, USB_DFU_PACKET_SUFFIX, USB_DFU_PACKET_NOTIFY, } S5L8950XDFUPacketKind; struct S5L8950XUSBOTGState { SysBusDevice parent_obj; MemoryRegion iomem; qemu_irq irq; uint32_t regs[S5L8950X_USB_OTG_NUM_REGS]; bool usb_reset_sent; bool enum_done_sent; bool setup_sent; bool descriptor_received; uint8_t device_descriptor[18]; uint32_t device_descriptor_len; uint8_t serial_descriptor[256]; uint32_t serial_descriptor_len; uint8_t config_descriptor[256]; uint32_t config_descriptor_len; uint32_t config_total_length; uint32_t enumeration_generation; bool current_device_is_dfu; S5L8950XUSBHostStage host_stage; char *dfu_image_path; uint8_t *dfu_image; size_t dfu_image_size; size_t dfu_image_offset; uint8_t dfu_packet[USB_DFU_TRANSFER_SIZE]; uint32_t dfu_packet_len; uint32_t dfu_packet_offset; uint16_t dfu_packet_block; uint16_t dfu_next_block; uint16_t dfu_notify_block; S5L8950XDFUPacketKind dfu_packet_kind; uint8_t dfu_suffix[16]; bool dfu_suffix_pending; uint8_t dfu_status[USB_DFU_STATUS_SIZE]; uint8_t dfu_state; uint8_t dfu_status_polls; uint8_t dfu_finish_statuses; bool dfu_injection_complete; CharFrontend bridge_chr; bool bridge_open; bool bridge_request_active; Qa6UsbFrameHeader bridge_request; uint8_t bridge_rx[QA6_USB_MAX_FRAME_SIZE]; uint32_t bridge_rx_len; uint8_t bridge_out_data[QA6_USB_MAX_PAYLOAD]; uint32_t bridge_out_offset; uint8_t bridge_in_data[QA6_USB_MAX_PAYLOAD]; uint32_t bridge_in_len; uint8_t bridge_dfu_status; uint8_t bridge_dfu_state; GByteArray *bridge_dfu_image; uint16_t bridge_dfu_next_block; bool bridge_dfu_capture_valid; QEMUTimer *recovery_reset_timer; QEMUTimer *bridge_bulk_timer; bool recovery_reset_wait_for_halt; bool force_debug_uarts; bool native_img3_handoff; }; static void s5l8950x_usb_otg_update_ep_irqs(S5L8950XUSBOTGState *s) { uint32_t daint = 0; int ep; for (ep = 0; ep < USB_OTG_EP_COUNT; ep++) { uint32_t in_int = s->regs[(USB_OTG_DIEPINT0 + ep * USB_OTG_EP_STRIDE) / 4]; uint32_t out_int = s->regs[(USB_OTG_DOEPINT0 + ep * USB_OTG_EP_STRIDE) / 4]; if (in_int & s->regs[USB_OTG_DIEPMSK / 4]) { daint |= BIT(ep); } if (out_int & s->regs[USB_OTG_DOEPMSK / 4]) { daint |= BIT(ep + 16); } } s->regs[USB_OTG_DAINT / 4] = daint; s->regs[USB_OTG_GINTSTS / 4] &= ~(USB_OTG_GINT_IEPINT | USB_OTG_GINT_OEPINT); if (daint & s->regs[USB_OTG_DAINTMSK / 4] & 0xffff) { s->regs[USB_OTG_GINTSTS / 4] |= USB_OTG_GINT_IEPINT; } if (daint & s->regs[USB_OTG_DAINTMSK / 4] & 0xffff0000) { s->regs[USB_OTG_GINTSTS / 4] |= USB_OTG_GINT_OEPINT; } } static void s5l8950x_usb_otg_update_irq(S5L8950XUSBOTGState *s) { s5l8950x_usb_otg_update_ep_irqs(s); bool level = (s->regs[USB_OTG_GAHBCFG / 4] & USB_OTG_GAHBCFG_GLBLINTRMSK) && (s->regs[USB_OTG_GINTSTS / 4] & s->regs[USB_OTG_GINTMSK / 4]); qemu_set_irq(s->irq, level); } static void s5l8950x_usb_otg_recovery_reset(void *opaque) { S5L8950XUSBOTGState *s = opaque; CPUState *cs = first_cpu; int ep; if (s->host_stage != USB_HOST_WAIT_DEVICE_SETUP || s->device_descriptor_len) { return; } /* * A real host cannot reset a device until its reconnect has reached the * bus. Waiting for the bootloader's WFE idle point models that ordering * and, importantly, prevents the reset IRQ from preempting iBSS before it * has installed the USB handler. SecureROM's first enumeration does not * use this delayed path. */ if (s->recovery_reset_wait_for_halt && cs && !cs->halted) { timer_mod(s->recovery_reset_timer, qemu_clock_get_ms(QEMU_CLOCK_VIRTUAL) + 1); return; } /* A USB port reset disables active endpoints but preserves core setup. */ for (ep = 0; ep < USB_OTG_EP_COUNT; ep++) { s->regs[(USB_OTG_DIEPCTL0 + ep * USB_OTG_EP_STRIDE) / 4] &= ~USB_OTG_EPCTL_EPENA; s->regs[(USB_OTG_DOEPCTL0 + ep * USB_OTG_EP_STRIDE) / 4] &= ~USB_OTG_EPCTL_EPENA; s->regs[(USB_OTG_DIEPINT0 + ep * USB_OTG_EP_STRIDE) / 4] = 0; s->regs[(USB_OTG_DOEPINT0 + ep * USB_OTG_EP_STRIDE) / 4] = 0; } s->regs[USB_OTG_DAINT / 4] = 0; /* The last iBSS EP0 completion can leave AIC's auto-mask asserted. */ address_space_stl(&address_space_memory, A6_AIC_MASK_CLR, BIT(A6_USB_IRQ), MEMTXATTRS_UNSPECIFIED, NULL); s->regs[USB_OTG_GINTSTS / 4] |= USB_OTG_GINT_USBRST; s->usb_reset_sent = true; s->recovery_reset_wait_for_halt = false; s5l8950x_usb_otg_update_irq(s); qemu_log_mask(LOG_UNIMP, "s5l8950x.usb-host: delayed bootloader port reset " "delivered\n"); } static bool s5l8950x_usb_otg_is_ep_int(hwaddr offset, hwaddr base) { return offset >= base && offset < base + USB_OTG_EP_COUNT * USB_OTG_EP_STRIDE && (offset - base) % USB_OTG_EP_STRIDE == 0; } static void s5l8950x_usb_otg_send_device_descriptor_setup( S5L8950XUSBOTGState *s) { static const uint8_t setup[] = { 0x80, 0x06, 0x00, 0x01, 0x00, 0x00, 0x12, 0x00, }; hwaddr dma = s->regs[USB_OTG_DOEPDMA0 / 4]; if (dma_memory_write(&address_space_memory, dma, setup, sizeof(setup), MEMTXATTRS_UNSPECIFIED) != MEMTX_OK) { qemu_log_mask(LOG_GUEST_ERROR, "s5l8950x.usb: EP0 SETUP DMA write failed at 0x%08" HWADDR_PRIx "\n", dma); return; } s->regs[USB_OTG_DOEPINT0 / 4] |= USB_OTG_EPINT_SETUP; s->regs[USB_OTG_DOEPCTL0 / 4] &= ~USB_OTG_EPCTL_EPENA; s->setup_sent = true; s->host_stage = USB_HOST_WAIT_DEVICE_IN; qemu_log_mask(LOG_UNIMP, "s5l8950x.usb-host: GET_DESCRIPTOR(Device) -> EP0 DMA " "0x%08" HWADDR_PRIx "\n", dma); s5l8950x_usb_otg_update_irq(s); } static void s5l8950x_usb_otg_send_set_address_setup( S5L8950XUSBOTGState *s) { static const uint8_t setup[] = { 0x00, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, }; hwaddr dma = s->regs[USB_OTG_DOEPDMA0 / 4]; if (dma_memory_write(&address_space_memory, dma, setup, sizeof(setup), MEMTXATTRS_UNSPECIFIED) != MEMTX_OK) { qemu_log_mask(LOG_GUEST_ERROR, "s5l8950x.usb: SET_ADDRESS DMA write failed at 0x%08" HWADDR_PRIx "\n", dma); return; } s->regs[USB_OTG_DOEPINT0 / 4] |= USB_OTG_EPINT_SETUP; s->regs[USB_OTG_DOEPCTL0 / 4] &= ~USB_OTG_EPCTL_EPENA; s->host_stage = USB_HOST_WAIT_SET_ADDRESS_STATUS_IN; qemu_log_mask(LOG_UNIMP, "s5l8950x.usb-host: SET_ADDRESS(1) -> EP0 DMA 0x%08" HWADDR_PRIx "\n", dma); s5l8950x_usb_otg_update_irq(s); } static bool s5l8950x_usb_otg_send_setup(S5L8950XUSBOTGState *s, const uint8_t setup[8], const char *name, S5L8950XUSBHostStage next_stage) { hwaddr dma = s->regs[USB_OTG_DOEPDMA0 / 4]; if (dma_memory_write(&address_space_memory, dma, setup, 8, MEMTXATTRS_UNSPECIFIED) != MEMTX_OK) { qemu_log_mask(LOG_GUEST_ERROR, "s5l8950x.usb: %s DMA write failed at 0x%08" HWADDR_PRIx "\n", name, dma); return false; } s->regs[USB_OTG_DOEPINT0 / 4] |= USB_OTG_EPINT_SETUP; s->regs[USB_OTG_DOEPCTL0 / 4] &= ~USB_OTG_EPCTL_EPENA; s->host_stage = next_stage; qemu_log_mask(LOG_UNIMP, "s5l8950x.usb-host: %s -> EP0 DMA 0x%08" HWADDR_PRIx "\n", name, dma); s5l8950x_usb_otg_update_irq(s); return true; } static void s5l8950x_usb_bridge_write_response( S5L8950XUSBOTGState *s, const Qa6UsbFrameHeader *request, int status, uint32_t transfer_length, const uint8_t *payload, uint32_t payload_length) { Qa6UsbFrameHeader response = { 0 }; g_autofree uint8_t *frame = NULL; uint32_t frame_length; int written; if (payload_length > QA6_USB_MAX_PAYLOAD) { status = QA6_USB_STATUS_PROTOCOL; payload_length = 0; transfer_length = 0; } response.magic = QA6_USB_MAGIC; response.version = QA6_USB_PROTOCOL_VERSION; switch (request->type) { case QA6_USB_MESSAGE_INFO_REQUEST: response.type = QA6_USB_MESSAGE_INFO_RESPONSE; break; case QA6_USB_MESSAGE_BULK_REQUEST: response.type = QA6_USB_MESSAGE_BULK_RESPONSE; break; case QA6_USB_MESSAGE_RESET_REQUEST: response.type = QA6_USB_MESSAGE_RESET_RESPONSE; break; default: response.type = QA6_USB_MESSAGE_RESPONSE; break; } response.request_id = request->request_id; response.status = status; response.payload_length = payload_length; response.transfer_length = transfer_length; memcpy(response.setup, request->setup, sizeof(response.setup)); frame_length = QA6_USB_HEADER_SIZE + payload_length; frame = g_malloc(frame_length); memcpy(frame, &response, QA6_USB_HEADER_SIZE); if (payload_length) { memcpy(frame + QA6_USB_HEADER_SIZE, payload, payload_length); } written = qemu_chr_fe_write_all(&s->bridge_chr, frame, frame_length); if (written != frame_length) { qemu_log_mask(LOG_GUEST_ERROR, "s5l8950x.usb-bridge: response write failed " "(%d/%u bytes)\n", written, frame_length); } } static void s5l8950x_usb_bridge_write_info_response( S5L8950XUSBOTGState *s, const Qa6UsbFrameHeader *request) { Qa6UsbDeviceInfo info = { 0 }; uint32_t input_offset; uint32_t output_offset = 0; if (!s->enumeration_generation || s->device_descriptor_len < 12 || s->serial_descriptor_len < 2) { s5l8950x_usb_bridge_write_response( s, request, QA6_USB_STATUS_DISCONNECTED, 0, NULL, 0); return; } info.generation = s->enumeration_generation; info.vendor_id = s->device_descriptor[8] | (s->device_descriptor[9] << 8); info.product_id = s->device_descriptor[10] | (s->device_descriptor[11] << 8); info.device_class = s->device_descriptor[4]; info.device_subclass = s->device_descriptor[5]; info.device_protocol = s->device_descriptor[6]; info.is_dfu = s->current_device_is_dfu; /* libirecovery recognizes Apple's public DFU PID. */ if (info.is_dfu && info.vendor_id == 0x05ac) { info.product_id = 0x1227; } for (input_offset = 2; input_offset + 1 < s->serial_descriptor_len && output_offset + 1 < sizeof(info.serial); input_offset += 2) { info.serial[output_offset++] = s->serial_descriptor[input_offset + 1] ? '?' : s->serial_descriptor[input_offset]; } s5l8950x_usb_bridge_write_response( s, request, QA6_USB_STATUS_SUCCESS, sizeof(info), (const uint8_t *)&info, sizeof(info)); } static void s5l8950x_usb_bridge_finish(S5L8950XUSBOTGState *s, int status) { bool direction_in = (s->bridge_request.setup[0] & 0x80) != 0; uint32_t transfer_length = direction_in ? s->bridge_in_len : s->bridge_out_offset; timer_del(s->bridge_bulk_timer); if (status == QA6_USB_STATUS_SUCCESS && !direction_in && s->bridge_request.setup[0] == 0x21 && s->bridge_request.setup[1] == 1 && transfer_length) { uint16_t block = s->bridge_request.setup[2] | (s->bridge_request.setup[3] << 8); if (block == 0) { g_byte_array_set_size(s->bridge_dfu_image, 0); s->bridge_dfu_next_block = 0; s->bridge_dfu_capture_valid = true; } if (!s->bridge_dfu_capture_valid || block != s->bridge_dfu_next_block || s->bridge_dfu_image->len > A6_DFU_CAPTURE_MAX - transfer_length) { s->bridge_dfu_capture_valid = false; qemu_log_mask(LOG_GUEST_ERROR, "s5l8950x.usb-bridge: invalid DFU block sequence " "(%u, expected %u)\n", block, s->bridge_dfu_next_block); } else { g_byte_array_append(s->bridge_dfu_image, s->bridge_out_data, transfer_length); s->bridge_dfu_next_block++; } } s5l8950x_usb_bridge_write_response( s, &s->bridge_request, status, transfer_length, direction_in ? s->bridge_in_data : NULL, direction_in && status == QA6_USB_STATUS_SUCCESS ? s->bridge_in_len : 0); if (status == QA6_USB_STATUS_SUCCESS && direction_in && s->bridge_request.setup[0] == 0xa1 && s->bridge_request.setup[1] == 3 && s->bridge_in_len == 6) { s->bridge_dfu_status = s->bridge_in_data[0]; s->bridge_dfu_state = s->bridge_in_data[4]; qemu_log_mask(LOG_UNIMP, "s5l8950x.usb-bridge: DFU GETSTATUS status=%u " "state=%u poll=%u ms\n", s->bridge_in_data[0], s->bridge_in_data[4], s->bridge_in_data[1] | (s->bridge_in_data[2] << 8) | (s->bridge_in_data[3] << 16)); } qemu_log_mask(LOG_UNIMP, "s5l8950x.usb-bridge: %s request %u complete " "(status=%d, bytes=%u)\n", s->bridge_request.type == QA6_USB_MESSAGE_BULK_REQUEST ? "bulk" : "EP0", s->bridge_request.request_id, status, transfer_length); s->bridge_request_active = false; s->bridge_out_offset = 0; s->bridge_in_len = 0; s->host_stage = USB_HOST_DFU_READY; } static void s5l8950x_usb_bridge_bulk_timeout(void *opaque) { S5L8950XUSBOTGState *s = opaque; if (!s->bridge_request_active || s->bridge_request.type != QA6_USB_MESSAGE_BULK_REQUEST || !(s->bridge_request.setup[0] & 0x80)) { return; } qemu_log_mask(LOG_UNIMP, "s5l8950x.usb-bridge: bulk IN timeout, returning %u " "bytes\n", s->bridge_in_len); s5l8950x_usb_bridge_finish(s, QA6_USB_STATUS_SUCCESS); } static void s5l8950x_usb_bridge_try_bulk(S5L8950XUSBOTGState *s) { bool direction_in; unsigned ep; hwaddr ctl_offset; hwaddr int_offset; hwaddr size_offset; hwaddr dma_offset; hwaddr dma; uint32_t capacity; uint32_t remaining; uint32_t length; if (!s->bridge_request_active || s->bridge_request.type != QA6_USB_MESSAGE_BULK_REQUEST) { return; } direction_in = (s->bridge_request.setup[0] & 0x80) != 0; ep = s->bridge_request.setup[0] & 0x7f; if (!ep || ep >= USB_OTG_EP_COUNT) { s5l8950x_usb_bridge_finish(s, QA6_USB_STATUS_PROTOCOL); return; } if (direction_in) { ctl_offset = USB_OTG_DIEPCTL0 + ep * USB_OTG_EP_STRIDE; int_offset = USB_OTG_DIEPINT0 + ep * USB_OTG_EP_STRIDE; size_offset = USB_OTG_DIEPTSIZ0 + ep * USB_OTG_EP_STRIDE; dma_offset = USB_OTG_DIEPDMA0 + ep * USB_OTG_EP_STRIDE; } else { ctl_offset = USB_OTG_DOEPCTL0 + ep * USB_OTG_EP_STRIDE; int_offset = USB_OTG_DOEPINT0 + ep * USB_OTG_EP_STRIDE; size_offset = USB_OTG_DOEPTSIZ0 + ep * USB_OTG_EP_STRIDE; dma_offset = USB_OTG_DOEPDMA0 + ep * USB_OTG_EP_STRIDE; } if (!(s->regs[ctl_offset / 4] & USB_OTG_EPCTL_EPENA)) { return; } capacity = s->regs[size_offset / 4] & USB_OTG_EPTSIZ_XFERSIZE_MASK; dma = s->regs[dma_offset / 4]; if (direction_in) { remaining = s->bridge_request.transfer_length - s->bridge_in_len; length = MIN(capacity, remaining); if (length && dma_memory_read(&address_space_memory, dma, s->bridge_in_data + s->bridge_in_len, length, MEMTXATTRS_UNSPECIFIED) != MEMTX_OK) { qemu_log_mask(LOG_GUEST_ERROR, "s5l8950x.usb-bridge: bulk IN%u DMA read failed " "at 0x%08" HWADDR_PRIx "\n", ep, dma); s5l8950x_usb_bridge_finish( s, QA6_USB_STATUS_DISCONNECTED); return; } s->bridge_in_len += length; } else { remaining = s->bridge_request.payload_length - s->bridge_out_offset; length = MIN(capacity, remaining); if (length && dma_memory_write(&address_space_memory, dma, s->bridge_out_data + s->bridge_out_offset, length, MEMTXATTRS_UNSPECIFIED) != MEMTX_OK) { qemu_log_mask(LOG_GUEST_ERROR, "s5l8950x.usb-bridge: bulk OUT%u DMA write failed " "at 0x%08" HWADDR_PRIx "\n", ep, dma); s5l8950x_usb_bridge_finish( s, QA6_USB_STATUS_DISCONNECTED); return; } s->bridge_out_offset += length; } s->regs[dma_offset / 4] = dma + length; s->regs[size_offset / 4] &= ~(USB_OTG_EPTSIZ_XFERSIZE_MASK | USB_OTG_EPTSIZ_PKTCNT_MASK); if (!direction_in && capacity > length) { s->regs[size_offset / 4] |= capacity - length; } s->regs[ctl_offset / 4] &= ~USB_OTG_EPCTL_EPENA; s->regs[int_offset / 4] |= USB_OTG_EPINT_XFERCOMPL; qemu_log_mask(LOG_UNIMP, "s5l8950x.usb-bridge: bulk %s EP%u DMA=0x%08" HWADDR_PRIx " bytes=%u\n", direction_in ? "IN" : "OUT", ep, dma, length); if (direction_in || s->bridge_out_offset >= s->bridge_request.payload_length) { s5l8950x_usb_bridge_finish(s, QA6_USB_STATUS_SUCCESS); } s5l8950x_usb_otg_update_irq(s); } static void s5l8950x_usb_otg_rearm(S5L8950XUSBOTGState *s, bool cold); typedef struct S5L8950XBootloaderHandoff { S5L8950XUSBOTGState *usb; const uint8_t *payload; uint32_t data_length; uint32_t image_type; hwaddr load_base; bool success; } S5L8950XBootloaderHandoff; /* * RELEASE iBSS does not read NVRAM, and iBEC only reads debug-uarts after its * early platform initialization. On research devices the equivalent early * setting is 3, which enables both UART directions. Locate iBoot's small * debug_enable_uarts() helper by its stable Thumb body and replace only the * ORRS with MOVS r0, #3. The authenticated IMG3 remains untouched until the * real bootloader has accepted it; this is an optional emulator-side debug * aid applied to the copied executable image immediately before handoff. */ static bool s5l8950x_usb_enable_bootloader_uarts( const S5L8950XBootloaderHandoff *handoff) { const uint8_t *image = handoff->payload; static const uint16_t helper_tail[] = { 0x680a, /* ldr r2, [r1] */ 0x4310, /* orrs r0, r2 */ 0x6008, /* str r0, [r1] */ 0x4770, /* bx lr */ }; static const uint8_t movs_r0_3[] = { 0x03, 0x20 }; uint32_t offset; for (offset = 0; offset + 10 <= handoff->data_length; offset += 2) { uint16_t ldr_literal = lduw_le_p(image + offset); unsigned int i; /* Thumb LDR (literal), with Rt == r1. */ if ((ldr_literal & 0xff00) != 0x4900) { continue; } for (i = 0; i < ARRAY_SIZE(helper_tail); i++) { if (lduw_le_p(image + offset + 2 + i * 2) != helper_tail[i]) { break; } } if (i != ARRAY_SIZE(helper_tail)) { continue; } if (address_space_write_rom(&address_space_memory, handoff->load_base + offset + 4, MEMTXATTRS_UNSPECIFIED, movs_r0_3, sizeof(movs_r0_3)) != MEMTX_OK) { return false; } qemu_log_mask(LOG_UNIMP, "s5l8950x.uart: forced debug-uarts=3 for %s at " "0x%08" HWADDR_PRIx "\n", handoff->image_type == IMG3_TYPE_IBSS ? "iBSS" : "iBEC", handoff->load_base + offset + 4); return true; } return false; } static void s5l8950x_usb_bridge_handoff_on_cpu(CPUState *cpu, run_on_cpu_data data) { S5L8950XBootloaderHandoff *handoff = data.host_ptr; S5L8950XUSBOTGState *s = handoff->usb; if (address_space_write_rom(&address_space_memory, handoff->load_base, MEMTXATTRS_UNSPECIFIED, handoff->payload, handoff->data_length) != MEMTX_OK) { return; } if (s->force_debug_uarts && !s5l8950x_usb_enable_bootloader_uarts(handoff)) { qemu_log_mask(LOG_GUEST_ERROR, "s5l8950x.uart: debug_enable_uarts helper not found " "in authenticated bootloader\n"); } /* * iBSS and iBEC occupy the same SRAM window. This callback runs at a * vCPU safe point, so no old translated block can execute concurrently * with the replacement and reset below. */ { RCU_READ_LOCK_GUARD(); hwaddr xlat; hwaddr length = handoff->data_length; MemoryRegion *mr = address_space_translate( &address_space_memory, handoff->load_base, &xlat, &length, false, MEMTXATTRS_UNSPECIFIED); ram_addr_t ram_addr = memory_region_get_ram_addr(mr); if (ram_addr != RAM_ADDR_INVALID) { tb_invalidate_phys_range(cpu, ram_addr + xlat, ram_addr + xlat + length - 1); tcg_flush_jmp_cache(cpu); qemu_log_mask(LOG_UNIMP, "s5l8950x.usb-bridge: invalidated code RAM=" RAM_ADDR_FMT " offset=0x%" HWADDR_PRIx " length=%" HWADDR_PRIu "\n", ram_addr, xlat, length); } } if (handoff->image_type == IMG3_TYPE_IBSS) { s5l8950x_usb_otg_rearm(s, false); s->recovery_reset_wait_for_halt = true; timer_mod(s->recovery_reset_timer, qemu_clock_get_ms(QEMU_CLOCK_VIRTUAL) + 100); qemu_log_mask(LOG_UNIMP, "s5l8950x.usb-host: SecureROM disconnected; " "waiting for iBSS DFU reinitialization\n"); } else { /* * Invalidate iBSS's DFU identity immediately, but preserve the DWC2 * register image inherited by iBEC. iBEC relocates before resetting * the core, so its delayed host reset must not race that relocation. */ s->usb_reset_sent = false; s->enum_done_sent = false; s->setup_sent = false; s->descriptor_received = false; s->device_descriptor_len = 0; s->serial_descriptor_len = 0; s->config_descriptor_len = 0; s->config_total_length = 0; s->current_device_is_dfu = false; s->host_stage = USB_HOST_WAIT_DEVICE_SETUP; s->bridge_dfu_status = 0; s->bridge_dfu_state = 0; s->recovery_reset_wait_for_halt = false; memset(s->device_descriptor, 0, sizeof(s->device_descriptor)); memset(s->serial_descriptor, 0, sizeof(s->serial_descriptor)); memset(s->config_descriptor, 0, sizeof(s->config_descriptor)); /* * iBEC resets and reconnects the DWC core after relocation. Do not * inject a port reset merely because control was transferred: the * DCTL soft-reconnect path below is the point at which a real host * can observe the device and arm the delayed reset. */ timer_del(s->recovery_reset_timer); qemu_log_mask(LOG_UNIMP, "s5l8950x.usb-host: iBSS disconnected; waiting for " "iBEC Recovery reinitialization\n"); } cpu_reset(cpu); ARM_CPU(cpu)->env.regs[0] = 1; cpu->halted = 0; cpu_set_pc(cpu, handoff->load_base); cpu_exit(cpu); handoff->success = true; } static bool s5l8950x_usb_bridge_handoff_captured( S5L8950XUSBOTGState *s) { CPUState *cpu = first_cpu; const uint8_t *image = s->bridge_dfu_image->data; size_t image_length = s->bridge_dfu_image->len; size_t img3_offset; uint32_t full_size = 0; uint32_t offset; uint32_t data_offset = 0; uint32_t data_length = 0; uint32_t image_type = 0; hwaddr load_base; g_autofree uint8_t *payload = NULL; S5L8950XBootloaderHandoff handoff = { 0 }; if (!cpu || !s->bridge_dfu_capture_valid) { return false; } /* A personalized bootloader is the TSS ticket followed by its IMG3. */ for (img3_offset = 0; img3_offset + IMG3_HEADER_SIZE <= image_length; img3_offset += 4) { if (ldl_le_p(image + img3_offset) == IMG3_MAGIC) { uint32_t candidate_size = ldl_le_p(image + img3_offset + 4); if (candidate_size >= IMG3_HEADER_SIZE && candidate_size <= image_length - img3_offset) { full_size = candidate_size; break; } } } if (!full_size) { return false; } offset = IMG3_HEADER_SIZE; while (offset <= full_size - IMG3_ELEMENT_HEADER_SIZE) { const uint8_t *element = image + img3_offset + offset; uint32_t tag = ldl_le_p(element); uint32_t total_length = ldl_le_p(element + 4); uint32_t element_data_length = ldl_le_p(element + 8); if (total_length < IMG3_ELEMENT_HEADER_SIZE || total_length > full_size - offset || element_data_length > total_length - IMG3_ELEMENT_HEADER_SIZE) { return false; } if (tag == IMG3_TAG_TYPE && element_data_length >= 4) { image_type = ldl_le_p(element + IMG3_ELEMENT_HEADER_SIZE); } else if (tag == IMG3_TAG_DATA) { data_offset = offset + IMG3_ELEMENT_HEADER_SIZE; data_length = element_data_length; } offset += total_length; } if ((image_type != IMG3_TYPE_IBSS && image_type != IMG3_TYPE_IBEC) || !data_offset || !data_length || data_offset > full_size || data_length > full_size - data_offset || data_length > A6_SRAM_SIZE) { return false; } load_base = image_type == IMG3_TYPE_IBSS ? A6_SRAM_BASE : A6_IBEC_BASE; payload = g_memdup2(image + img3_offset + data_offset, data_length); handoff.usb = s; handoff.payload = payload; handoff.data_length = data_length; handoff.image_type = image_type; handoff.load_base = load_base; run_on_cpu(cpu, s5l8950x_usb_bridge_handoff_on_cpu, RUN_ON_CPU_HOST_PTR(&handoff)); if (!handoff.success) { return false; } qemu_log_mask(LOG_UNIMP, "s5l8950x.usb-bridge: authenticated %s handoff; " "DATA=%u bytes DFU_IMG3=0x%zx PC=0x%08" HWADDR_PRIx "\n", image_type == IMG3_TYPE_IBSS ? "iBSS" : "iBEC", data_length, img3_offset, load_base); s->bridge_dfu_capture_valid = false; g_byte_array_set_size(s->bridge_dfu_image, 0); return true; } /* * The real host-side reset completes SecureROM's verified IMG3 handoff. * Our minimal DWC2 model has no physical port-reset object, so its USBRST * interrupt takes the generic re-enumeration path instead. Preserve all ROM * parsing and signature checks, then reproduce only the successful final * hardware handoff once the ROM itself reports DFU-MANIFEST-WAIT-RESET. */ static bool s5l8950x_usb_bridge_handoff_bootloader( S5L8950XUSBOTGState *s) { CPUState *cpu = first_cpu; uint8_t img3_header[IMG3_HEADER_SIZE]; uint8_t element_header[IMG3_ELEMENT_HEADER_SIZE]; g_autofree uint8_t *payload = NULL; uint32_t full_size; uint32_t offset = IMG3_HEADER_SIZE; uint32_t data_offset = 0; uint32_t data_length = 0; uint32_t image_type = 0; hwaddr image_base = 0; hwaddr load_base = 0; S5L8950XBootloaderHandoff handoff = { 0 }; static const hwaddr load_bases[] = { A6_SRAM_BASE, A6_DRAM_BASE }; unsigned int region; if (!cpu) { return false; } for (region = 0; region < ARRAY_SIZE(load_bases); region++) { uint32_t search_offset; for (search_offset = 0; search_offset <= IMG3_SEARCH_SIZE - IMG3_HEADER_SIZE; search_offset += 4) { hwaddr candidate = load_bases[region] + search_offset; if (dma_memory_read(&address_space_memory, candidate, img3_header, sizeof(img3_header), MEMTXATTRS_UNSPECIFIED) == MEMTX_OK && ldl_le_p(img3_header) == IMG3_MAGIC) { uint32_t candidate_size = ldl_le_p(img3_header + 4); if (candidate_size >= IMG3_HEADER_SIZE && candidate_size <= A6_SRAM_SIZE - search_offset) { image_base = candidate; load_base = load_bases[region]; break; } } } if (image_base) { break; } } if (!image_base) { return false; } full_size = ldl_le_p(img3_header + 4); if (full_size < IMG3_HEADER_SIZE || full_size > A6_SRAM_SIZE) { return false; } while (offset <= full_size - IMG3_ELEMENT_HEADER_SIZE) { uint32_t tag; uint32_t total_length; uint32_t element_data_length; if (dma_memory_read(&address_space_memory, image_base + offset, element_header, sizeof(element_header), MEMTXATTRS_UNSPECIFIED) != MEMTX_OK) { return false; } tag = ldl_le_p(element_header); total_length = ldl_le_p(element_header + 4); element_data_length = ldl_le_p(element_header + 8); if (total_length < IMG3_ELEMENT_HEADER_SIZE || total_length > full_size - offset || element_data_length > total_length - IMG3_ELEMENT_HEADER_SIZE) { return false; } if (tag == IMG3_TAG_TYPE && element_data_length >= 4) { uint8_t type_data[4]; if (dma_memory_read(&address_space_memory, image_base + offset + IMG3_ELEMENT_HEADER_SIZE, type_data, sizeof(type_data), MEMTXATTRS_UNSPECIFIED) != MEMTX_OK) { return false; } image_type = ldl_le_p(type_data); } else if (tag == IMG3_TAG_DATA) { data_offset = offset + IMG3_ELEMENT_HEADER_SIZE; data_length = element_data_length; } offset += total_length; } if ((image_type != IMG3_TYPE_IBSS && image_type != IMG3_TYPE_IBEC) || !data_offset || !data_length || data_offset > full_size || data_length > full_size - data_offset || data_length > A6_SRAM_SIZE) { return false; } payload = g_malloc(data_length); if (dma_memory_read(&address_space_memory, image_base + data_offset, payload, data_length, MEMTXATTRS_UNSPECIFIED) != MEMTX_OK) { return false; } handoff.usb = s; handoff.payload = payload; handoff.data_length = data_length; handoff.image_type = image_type; handoff.load_base = load_base; run_on_cpu(cpu, s5l8950x_usb_bridge_handoff_on_cpu, RUN_ON_CPU_HOST_PTR(&handoff)); if (!handoff.success) { return false; } qemu_log_mask(LOG_UNIMP, "s5l8950x.usb-bridge: authenticated %s handoff; " "DATA=%u bytes IMG3=0x%08" HWADDR_PRIx " PC=0x%08" HWADDR_PRIx "\n", image_type == IMG3_TYPE_IBSS ? "iBSS" : "iBEC", data_length, image_base, load_base); return true; } static void s5l8950x_usb_bridge_start_request(S5L8950XUSBOTGState *s) { bool direction_in; uint16_t length; S5L8950XUSBHostStage next_stage; if (!s->bridge_request_active || s->host_stage != USB_HOST_DFU_READY) { return; } direction_in = (s->bridge_request.setup[0] & 0x80) != 0; length = s->bridge_request.setup[6] | (s->bridge_request.setup[7] << 8); if (!length) { next_stage = USB_HOST_BRIDGE_WAIT_STATUS_IN; } else if (direction_in) { next_stage = USB_HOST_BRIDGE_WAIT_DATA_IN; } else { next_stage = USB_HOST_BRIDGE_WAIT_DATA_OUT; } /* * USB control endpoints recover from a halt when the host delivers a new * SETUP packet. Without this, one unsupported iBoot command leaves the * emulated EP0 STALL bit set and poisons every later irecovery request. */ s->regs[USB_OTG_DIEPCTL0 / 4] &= ~USB_OTG_EPCTL_STALL; s->regs[USB_OTG_DOEPCTL0 / 4] &= ~USB_OTG_EPCTL_STALL; s5l8950x_usb_otg_send_setup(s, s->bridge_request.setup, "external EP0 request", next_stage); } static void s5l8950x_usb_bridge_complete_data_out( S5L8950XUSBOTGState *s) { hwaddr dma = s->regs[USB_OTG_DOEPDMA0 / 4]; uint32_t remaining = s->bridge_request.payload_length - s->bridge_out_offset; uint32_t capacity = s->regs[USB_OTG_DOEPTSIZ0 / 4] & 0x7f; uint32_t length; if (!capacity) { capacity = 64; } length = MIN(remaining, capacity); if (length && dma_memory_write(&address_space_memory, dma, s->bridge_out_data + s->bridge_out_offset, length, MEMTXATTRS_UNSPECIFIED) != MEMTX_OK) { qemu_log_mask(LOG_GUEST_ERROR, "s5l8950x.usb-bridge: OUT DMA write failed at " "0x%08" HWADDR_PRIx "\n", dma); s5l8950x_usb_bridge_finish(s, QA6_USB_STATUS_DISCONNECTED); return; } s->bridge_out_offset += length; /* DWC2 leaves the unreceived byte count in XFRSIZ on a short packet. */ s->regs[USB_OTG_DOEPTSIZ0 / 4] = (s->regs[USB_OTG_DOEPTSIZ0 / 4] & ~0x7f) | (capacity - length); s->regs[USB_OTG_DOEPCTL0 / 4] &= ~USB_OTG_EPCTL_EPENA; s->regs[USB_OTG_DOEPINT0 / 4] |= USB_OTG_EPINT_XFERCOMPL; if (s->bridge_out_offset >= s->bridge_request.payload_length) { s->host_stage = USB_HOST_BRIDGE_WAIT_STATUS_IN; } s5l8950x_usb_otg_update_irq(s); } static void s5l8950x_usb_bridge_complete_data_in( S5L8950XUSBOTGState *s) { hwaddr dma = s->regs[USB_OTG_DIEPDMA0 / 4]; uint32_t requested = s->bridge_request.transfer_length; uint32_t remaining = requested - s->bridge_in_len; uint32_t length = s->regs[USB_OTG_DIEPTSIZ0 / 4] & 0x7f; length = MIN(length, remaining); if (length && dma_memory_read(&address_space_memory, dma, s->bridge_in_data + s->bridge_in_len, length, MEMTXATTRS_UNSPECIFIED) != MEMTX_OK) { qemu_log_mask(LOG_GUEST_ERROR, "s5l8950x.usb-bridge: IN DMA read failed at " "0x%08" HWADDR_PRIx "\n", dma); s5l8950x_usb_bridge_finish(s, QA6_USB_STATUS_DISCONNECTED); return; } s->bridge_in_len += length; s->regs[USB_OTG_DIEPTSIZ0 / 4] &= ~0x7f; s->regs[USB_OTG_DIEPCTL0 / 4] &= ~USB_OTG_EPCTL_EPENA; s->regs[USB_OTG_DIEPINT0 / 4] |= USB_OTG_EPINT_XFERCOMPL; if (length < 64 || s->bridge_in_len >= requested) { s->host_stage = USB_HOST_BRIDGE_WAIT_STATUS_OUT; } s5l8950x_usb_otg_update_irq(s); } static void s5l8950x_usb_bridge_complete_status_out( S5L8950XUSBOTGState *s) { s->regs[USB_OTG_DOEPINT0 / 4] |= USB_OTG_EPINT_XFERCOMPL; s->regs[USB_OTG_DOEPCTL0 / 4] &= ~USB_OTG_EPCTL_EPENA; s5l8950x_usb_bridge_finish(s, QA6_USB_STATUS_SUCCESS); s5l8950x_usb_otg_update_irq(s); } static void s5l8950x_usb_bridge_complete_status_in( S5L8950XUSBOTGState *s) { s->regs[USB_OTG_DIEPTSIZ0 / 4] &= ~0x7f; s->regs[USB_OTG_DIEPCTL0 / 4] &= ~USB_OTG_EPCTL_EPENA; s->regs[USB_OTG_DIEPINT0 / 4] |= USB_OTG_EPINT_XFERCOMPL; s5l8950x_usb_bridge_finish(s, QA6_USB_STATUS_SUCCESS); s5l8950x_usb_otg_update_irq(s); } static void s5l8950x_usb_bridge_process_frame(S5L8950XUSBOTGState *s) { Qa6UsbFrameHeader request; uint32_t expected_payload; uint16_t setup_length; bool direction_in; memcpy(&request, s->bridge_rx, sizeof(request)); expected_payload = QA6_USB_HEADER_SIZE + request.payload_length; direction_in = (request.setup[0] & 0x80) != 0; setup_length = request.setup[6] | (request.setup[7] << 8); if (request.magic != QA6_USB_MAGIC || request.version != QA6_USB_PROTOCOL_VERSION || request.payload_length > QA6_USB_MAX_PAYLOAD || expected_payload != s->bridge_rx_len) { qemu_log_mask(LOG_GUEST_ERROR, "s5l8950x.usb-bridge: invalid frame header\n"); return; } if (request.type == QA6_USB_MESSAGE_INFO_REQUEST) { if (request.payload_length || request.transfer_length) { s5l8950x_usb_bridge_write_response( s, &request, QA6_USB_STATUS_PROTOCOL, 0, NULL, 0); } else { s5l8950x_usb_bridge_write_info_response(s, &request); } return; } if (request.type == QA6_USB_MESSAGE_RESET_REQUEST) { if (request.payload_length || request.transfer_length) { s5l8950x_usb_bridge_write_response( s, &request, QA6_USB_STATUS_PROTOCOL, 0, NULL, 0); return; } if (s->bridge_dfu_status == 0 && s->bridge_dfu_state == USB_DFU_STATE_WAIT_RESET) { if (s->native_img3_handoff) { s5l8950x_usb_bridge_write_response( s, &request, QA6_USB_STATUS_SUCCESS, 0, NULL, 0); s->regs[USB_OTG_GINTSTS / 4] |= USB_OTG_GINT_USBRST; s->usb_reset_sent = true; s5l8950x_usb_otg_update_irq(s); qemu_log_mask(LOG_UNIMP, "s5l8950x.usb-bridge: native DFU " "manifestation reset delivered to guest\n"); return; } if (!s5l8950x_usb_bridge_handoff_captured(s) && !s5l8950x_usb_bridge_handoff_bootloader(s)) { qemu_log_mask(LOG_GUEST_ERROR, "s5l8950x.usb-bridge: authenticated bootloader " "handoff failed\n"); s5l8950x_usb_bridge_write_response( s, &request, QA6_USB_STATUS_PROTOCOL, 0, NULL, 0); return; } s5l8950x_usb_bridge_write_response( s, &request, QA6_USB_STATUS_SUCCESS, 0, NULL, 0); return; } s5l8950x_usb_bridge_write_response( s, &request, QA6_USB_STATUS_SUCCESS, 0, NULL, 0); qemu_log_mask(LOG_UNIMP, "s5l8950x.usb-bridge: host USB reset request %u\n", request.request_id); s->regs[USB_OTG_GINTSTS / 4] |= USB_OTG_GINT_USBRST; s5l8950x_usb_otg_update_irq(s); return; } if (request.type == QA6_USB_MESSAGE_BULK_REQUEST) { unsigned ep = request.setup[0] & 0x7f; uint32_t timeout_ms = request.setup[1] | (request.setup[2] << 8) | (request.setup[3] << 16) | ((uint32_t)request.setup[4] << 24); if (!ep || ep >= USB_OTG_EP_COUNT || request.transfer_length > QA6_USB_MAX_PAYLOAD || (direction_in && request.payload_length != 0) || (!direction_in && request.payload_length != request.transfer_length)) { s5l8950x_usb_bridge_write_response( s, &request, QA6_USB_STATUS_PROTOCOL, 0, NULL, 0); return; } if (s->bridge_request_active || s->host_stage != USB_HOST_DFU_READY || !s->enumeration_generation || !s->device_descriptor_len) { s5l8950x_usb_bridge_write_response( s, &request, QA6_USB_STATUS_PROTOCOL, 0, NULL, 0); return; } s->bridge_request = request; if (request.payload_length) { memcpy(s->bridge_out_data, s->bridge_rx + QA6_USB_HEADER_SIZE, request.payload_length); } s->bridge_out_offset = 0; s->bridge_in_len = 0; s->bridge_request_active = true; if (direction_in && timeout_ms) { timer_mod(s->bridge_bulk_timer, qemu_clock_get_ms(QEMU_CLOCK_VIRTUAL) + timeout_ms); } qemu_log_mask(LOG_UNIMP, "s5l8950x.usb-bridge: bulk request %u EP=%02x " "len=%u\n", request.request_id, request.setup[0], request.transfer_length); s5l8950x_usb_bridge_try_bulk(s); return; } if (request.type != QA6_USB_MESSAGE_REQUEST || request.transfer_length != setup_length || (direction_in && request.payload_length != 0) || (!direction_in && request.payload_length != setup_length)) { qemu_log_mask(LOG_GUEST_ERROR, "s5l8950x.usb-bridge: invalid request frame\n"); s5l8950x_usb_bridge_write_response( s, &request, QA6_USB_STATUS_PROTOCOL, 0, NULL, 0); return; } if (s->bridge_request_active || s->host_stage != USB_HOST_DFU_READY) { qemu_log_mask(LOG_GUEST_ERROR, "s5l8950x.usb-bridge: device is busy (stage=%d)\n", s->host_stage); s5l8950x_usb_bridge_write_response( s, &request, QA6_USB_STATUS_PROTOCOL, 0, NULL, 0); return; } s->bridge_request = request; if (request.payload_length) { memcpy(s->bridge_out_data, s->bridge_rx + QA6_USB_HEADER_SIZE, request.payload_length); } s->bridge_out_offset = 0; s->bridge_in_len = 0; s->bridge_dfu_status = 0; s->bridge_dfu_state = 0; s->bridge_request_active = true; qemu_log_mask(LOG_UNIMP, "s5l8950x.usb-bridge: EP0 request %u " "bm=%02x req=%02x len=%u\n", request.request_id, request.setup[0], request.setup[1], setup_length); if (s->regs[USB_OTG_DOEPCTL0 / 4] & USB_OTG_EPCTL_EPENA) { s5l8950x_usb_bridge_start_request(s); } } static int s5l8950x_usb_bridge_can_receive(void *opaque) { S5L8950XUSBOTGState *s = opaque; return QA6_USB_MAX_FRAME_SIZE - s->bridge_rx_len; } static void s5l8950x_usb_bridge_receive(void *opaque, const uint8_t *buffer, int size) { S5L8950XUSBOTGState *s = opaque; while (size > 0) { uint32_t target = QA6_USB_HEADER_SIZE; uint32_t amount; if (s->bridge_rx_len >= QA6_USB_HEADER_SIZE) { Qa6UsbFrameHeader header; memcpy(&header, s->bridge_rx, sizeof(header)); if (header.payload_length > QA6_USB_MAX_PAYLOAD) { qemu_log_mask(LOG_GUEST_ERROR, "s5l8950x.usb-bridge: oversized frame\n"); s->bridge_rx_len = 0; return; } target += header.payload_length; } amount = MIN((uint32_t)size, target - s->bridge_rx_len); memcpy(s->bridge_rx + s->bridge_rx_len, buffer, amount); s->bridge_rx_len += amount; buffer += amount; size -= amount; if (s->bridge_rx_len >= QA6_USB_HEADER_SIZE) { Qa6UsbFrameHeader header; memcpy(&header, s->bridge_rx, sizeof(header)); if (header.payload_length <= QA6_USB_MAX_PAYLOAD && s->bridge_rx_len == QA6_USB_HEADER_SIZE + header.payload_length) { s5l8950x_usb_bridge_process_frame(s); s->bridge_rx_len = 0; } } } } static void s5l8950x_usb_bridge_event(void *opaque, QEMUChrEvent event) { S5L8950XUSBOTGState *s = opaque; if (event == CHR_EVENT_OPENED) { s->bridge_open = true; qemu_log_mask(LOG_UNIMP, "s5l8950x.usb-bridge: Windows bridge connected\n"); } else if (event == CHR_EVENT_CLOSED) { s->bridge_open = false; s->bridge_rx_len = 0; s->bridge_request_active = false; timer_del(s->bridge_bulk_timer); if (s->host_stage >= USB_HOST_BRIDGE_WAIT_DATA_IN && s->host_stage <= USB_HOST_BRIDGE_WAIT_STATUS_IN) { s->host_stage = USB_HOST_DFU_READY; } qemu_log_mask(LOG_UNIMP, "s5l8950x.usb-bridge: Windows bridge disconnected\n"); } } /* * Reset the guest-visible DWC2 state and re-arm the small host state machine. * SecureROM and iBSS each initialize this core, so a core reset must allow a * fresh descriptor sequence without replaying an already-consumed DFU image. */ static void s5l8950x_usb_otg_rearm(S5L8950XUSBOTGState *s, bool cold) { timer_del(s->recovery_reset_timer); timer_del(s->bridge_bulk_timer); if (!cold && s->bridge_request_active) { s5l8950x_usb_bridge_finish(s, QA6_USB_STATUS_DISCONNECTED); } memset(s->regs, 0, sizeof(s->regs)); s->regs[USB_OTG_GRSTCTL / sizeof(uint32_t)] = USB_OTG_GRSTCTL_AHBIDLE; s->usb_reset_sent = false; s->enum_done_sent = false; s->setup_sent = false; s->descriptor_received = false; s->device_descriptor_len = 0; s->serial_descriptor_len = 0; s->config_descriptor_len = 0; s->config_total_length = 0; s->current_device_is_dfu = false; s->host_stage = USB_HOST_WAIT_DEVICE_SETUP; s->bridge_request_active = false; s->bridge_rx_len = 0; s->bridge_out_offset = 0; s->bridge_in_len = 0; memset(s->device_descriptor, 0, sizeof(s->device_descriptor)); memset(s->serial_descriptor, 0, sizeof(s->serial_descriptor)); memset(s->config_descriptor, 0, sizeof(s->config_descriptor)); memset(&s->bridge_request, 0, sizeof(s->bridge_request)); memset(s->bridge_out_data, 0, sizeof(s->bridge_out_data)); memset(s->bridge_in_data, 0, sizeof(s->bridge_in_data)); if (cold) { s->enumeration_generation = 0; s->recovery_reset_wait_for_halt = false; g_byte_array_set_size(s->bridge_dfu_image, 0); s->bridge_dfu_next_block = 0; s->bridge_dfu_capture_valid = false; s->dfu_image_offset = 0; s->dfu_packet_len = 0; s->dfu_packet_offset = 0; s->dfu_packet_block = 0; s->dfu_next_block = 0; s->dfu_notify_block = DIV_ROUND_UP(s->dfu_image_size, USB_DFU_TRANSFER_SIZE); s->dfu_packet_kind = USB_DFU_PACKET_PAYLOAD; s->dfu_suffix_pending = false; s->dfu_state = 0; s->dfu_status_polls = 0; s->dfu_finish_statuses = 0; s->dfu_injection_complete = false; memset(s->dfu_packet, 0, sizeof(s->dfu_packet)); memset(s->dfu_status, 0, sizeof(s->dfu_status)); } s5l8950x_usb_otg_update_irq(s); } static uint32_t s5l8950x_usb_dfu_crc32(uint32_t crc, const uint8_t *data, size_t length) { size_t i; for (i = 0; i < length; i++) { int bit; crc ^= data[i]; for (bit = 0; bit < 8; bit++) { crc = (crc >> 1) ^ (0xedb88320U & -(crc & 1)); } } return crc; } static void s5l8950x_usb_dfu_build_suffix(S5L8950XUSBOTGState *s) { static const uint8_t suffix_header[12] = { 0xff, 0xff, 0xff, 0xff, 0xac, 0x05, 0x00, 0x01, 0x55, 0x46, 0x44, 0x10, }; uint32_t crc = 0xffffffffU; crc = s5l8950x_usb_dfu_crc32(crc, s->dfu_image, s->dfu_image_size); crc = s5l8950x_usb_dfu_crc32(crc, suffix_header, sizeof(suffix_header)); memcpy(s->dfu_suffix, suffix_header, sizeof(suffix_header)); s->dfu_suffix[12] = crc; s->dfu_suffix[13] = crc >> 8; s->dfu_suffix[14] = crc >> 16; s->dfu_suffix[15] = crc >> 24; } static void s5l8950x_usb_dfu_prepare_packet(S5L8950XUSBOTGState *s) { size_t remaining; s->dfu_packet_offset = 0; if (s->dfu_image_offset < s->dfu_image_size) { remaining = s->dfu_image_size - s->dfu_image_offset; s->dfu_packet_len = MIN(remaining, (size_t)USB_DFU_TRANSFER_SIZE); memcpy(s->dfu_packet, s->dfu_image + s->dfu_image_offset, s->dfu_packet_len); s->dfu_image_offset += s->dfu_packet_len; s->dfu_packet_block = s->dfu_next_block++; s->dfu_packet_kind = USB_DFU_PACKET_PAYLOAD; if (s->dfu_image_offset == s->dfu_image_size) { if (s->dfu_packet_len + sizeof(s->dfu_suffix) <= USB_DFU_TRANSFER_SIZE) { memcpy(s->dfu_packet + s->dfu_packet_len, s->dfu_suffix, sizeof(s->dfu_suffix)); s->dfu_packet_len += sizeof(s->dfu_suffix); } else { s->dfu_suffix_pending = true; } } return; } if (s->dfu_suffix_pending) { memcpy(s->dfu_packet, s->dfu_suffix, sizeof(s->dfu_suffix)); s->dfu_packet_len = sizeof(s->dfu_suffix); s->dfu_packet_block = s->dfu_next_block - 1; s->dfu_packet_kind = USB_DFU_PACKET_SUFFIX; s->dfu_suffix_pending = false; return; } s->dfu_packet_len = 0; s->dfu_packet_block = s->dfu_notify_block; s->dfu_packet_kind = USB_DFU_PACKET_NOTIFY; } static void s5l8950x_usb_dfu_send_getstate(S5L8950XUSBOTGState *s) { static const uint8_t setup[] = { 0xa1, 0x05, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, }; s5l8950x_usb_otg_send_setup(s, setup, "DFU_GETSTATE", USB_HOST_WAIT_DFU_GETSTATE_IN); } static void s5l8950x_usb_dfu_send_dnload(S5L8950XUSBOTGState *s) { uint8_t setup[] = { 0x21, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, }; s5l8950x_usb_dfu_prepare_packet(s); setup[2] = s->dfu_packet_block; setup[3] = s->dfu_packet_block >> 8; setup[6] = s->dfu_packet_len; setup[7] = s->dfu_packet_len >> 8; s5l8950x_usb_otg_send_setup( s, setup, s->dfu_packet_kind == USB_DFU_PACKET_NOTIFY ? "DFU_DNLOAD(finish)" : "DFU_DNLOAD", s->dfu_packet_len ? USB_HOST_WAIT_DFU_DNLOAD_DATA_OUT : USB_HOST_WAIT_DFU_DNLOAD_STATUS_IN); } static void s5l8950x_usb_dfu_send_getstatus(S5L8950XUSBOTGState *s) { static const uint8_t setup[] = { 0xa1, 0x03, 0x00, 0x00, 0x00, 0x00, USB_DFU_STATUS_SIZE, 0x00, }; s5l8950x_usb_otg_send_setup(s, setup, "DFU_GETSTATUS", USB_HOST_WAIT_DFU_GETSTATUS_IN); } static void s5l8950x_usb_otg_send_config_header_setup( S5L8950XUSBOTGState *s) { static const uint8_t setup[] = { 0x80, 0x06, 0x00, 0x02, 0x00, 0x00, 0x09, 0x00, }; s5l8950x_usb_otg_send_setup(s, setup, "GET_DESCRIPTOR(Config, 9)", USB_HOST_WAIT_CONFIG_HEADER_IN); } static void s5l8950x_usb_otg_send_serial_descriptor_setup( S5L8950XUSBOTGState *s) { static const uint8_t setup[] = { 0x80, 0x06, 0x04, 0x03, 0x09, 0x04, 0xff, 0x00, }; s->serial_descriptor_len = 0; memset(s->serial_descriptor, 0, sizeof(s->serial_descriptor)); s5l8950x_usb_otg_send_setup(s, setup, "GET_DESCRIPTOR(String 4)", USB_HOST_WAIT_SERIAL_IN); } static void s5l8950x_usb_otg_send_config_full_setup( S5L8950XUSBOTGState *s) { uint8_t setup[] = { 0x80, 0x06, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, }; setup[6] = s->config_total_length; setup[7] = s->config_total_length >> 8; s5l8950x_usb_otg_send_setup(s, setup, "GET_DESCRIPTOR(Config, full)", USB_HOST_WAIT_CONFIG_FULL_IN); } static void s5l8950x_usb_otg_send_set_configuration_setup( S5L8950XUSBOTGState *s) { static const uint8_t setup[] = { 0x00, 0x09, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, }; s5l8950x_usb_otg_send_setup(s, setup, "SET_CONFIGURATION(1)", USB_HOST_WAIT_SET_CONFIGURATION_STATUS_IN); } static void s5l8950x_usb_otg_complete_out_status( S5L8950XUSBOTGState *s, S5L8950XUSBHostStage next_stage) { s->regs[USB_OTG_DOEPINT0 / 4] |= USB_OTG_EPINT_XFERCOMPL; s->regs[USB_OTG_DOEPCTL0 / 4] &= ~USB_OTG_EPCTL_EPENA; s->host_stage = next_stage; qemu_log_mask(LOG_UNIMP, "s5l8950x.usb-host: EP0 OUT status complete\n"); s5l8950x_usb_otg_update_irq(s); } static void s5l8950x_usb_otg_complete_in_status(S5L8950XUSBOTGState *s) { s->regs[USB_OTG_DIEPTSIZ0 / 4] &= ~0x7f; s->regs[USB_OTG_DIEPCTL0 / 4] &= ~USB_OTG_EPCTL_EPENA; s->regs[USB_OTG_DIEPINT0 / 4] |= USB_OTG_EPINT_XFERCOMPL; s->host_stage = USB_HOST_ADDRESS_ASSIGNED; qemu_log_mask(LOG_UNIMP, "s5l8950x.usb-host: SET_ADDRESS status complete, DCFG=" "0x%08x\n", s->regs[0x800 / 4]); s5l8950x_usb_otg_update_irq(s); } static void s5l8950x_usb_otg_complete_configuration_status_in( S5L8950XUSBOTGState *s) { uint16_t vendor = 0; uint16_t product = 0; uint32_t offset = 0; bool has_dfu_interface = false; bool has_recovery_interface = false; s->regs[USB_OTG_DIEPTSIZ0 / 4] &= ~0x7f; s->regs[USB_OTG_DIEPCTL0 / 4] &= ~USB_OTG_EPCTL_EPENA; s->regs[USB_OTG_DIEPINT0 / 4] |= USB_OTG_EPINT_XFERCOMPL; s->host_stage = USB_HOST_DFU_READY; if (s->device_descriptor_len >= 12) { vendor = s->device_descriptor[8] | (s->device_descriptor[9] << 8); product = s->device_descriptor[10] | (s->device_descriptor[11] << 8); } while (offset + 2 <= s->config_descriptor_len) { uint8_t length = s->config_descriptor[offset]; uint8_t type = s->config_descriptor[offset + 1]; if (length < 2 || offset + length > s->config_descriptor_len) { break; } if (type == 4 && length >= 9) { uint8_t interface_class = s->config_descriptor[offset + 5]; uint8_t interface_subclass = s->config_descriptor[offset + 6]; if (interface_class == 0xfe && interface_subclass == 0x01) { has_dfu_interface = true; } else if (interface_class == 0xff) { has_recovery_interface = true; } } offset += length; } /* * SecureROM uses an internal PID but a DFU-only configuration, which the * bridge maps to Apple's public 0x1227 PID. Recovery PID 0x1281 keeps a * DFU fallback interface alongside its vendor command/bulk interface and * must retain its real identity. */ s->current_device_is_dfu = has_dfu_interface && !has_recovery_interface; s->enumeration_generation++; qemu_log_mask(LOG_UNIMP, "s5l8950x.usb-host: enumeration complete; " "%04x:%04x, %s interface\n", vendor, product, s->current_device_is_dfu ? "DFU" : "non-DFU"); s5l8950x_usb_otg_update_irq(s); } static void s5l8950x_usb_dfu_complete_dnload_data_out( S5L8950XUSBOTGState *s) { hwaddr dma = s->regs[USB_OTG_DOEPDMA0 / 4]; uint32_t remaining = s->dfu_packet_len - s->dfu_packet_offset; uint32_t capacity = s->regs[USB_OTG_DOEPTSIZ0 / 4] & 0x7f; uint32_t length; if (!capacity) { capacity = 64; } length = MIN(remaining, capacity); if (dma_memory_write(&address_space_memory, dma, s->dfu_packet + s->dfu_packet_offset, length, MEMTXATTRS_UNSPECIFIED) != MEMTX_OK) { qemu_log_mask(LOG_GUEST_ERROR, "s5l8950x.usb: DFU data DMA write failed at 0x%08" HWADDR_PRIx "\n", dma); s->host_stage = USB_HOST_DFU_ERROR; return; } s->dfu_packet_offset += length; /* Preserve the residual XFRSIZ so SecureROM sees the short packet. */ s->regs[USB_OTG_DOEPTSIZ0 / 4] = (s->regs[USB_OTG_DOEPTSIZ0 / 4] & ~0x7f) | (capacity - length); s->regs[USB_OTG_DOEPCTL0 / 4] &= ~USB_OTG_EPCTL_EPENA; s->regs[USB_OTG_DOEPINT0 / 4] |= USB_OTG_EPINT_XFERCOMPL; if (s->dfu_packet_offset == s->dfu_packet_len) { s->host_stage = USB_HOST_WAIT_DFU_DNLOAD_STATUS_IN; qemu_log_mask(LOG_UNIMP, "s5l8950x.usb-host: DFU block %u data complete " "(%u bytes)\n", s->dfu_packet_block, s->dfu_packet_len); } s5l8950x_usb_otg_update_irq(s); } static void s5l8950x_usb_dfu_complete_dnload_status_in( S5L8950XUSBOTGState *s) { s->regs[USB_OTG_DIEPTSIZ0 / 4] &= ~0x7f; s->regs[USB_OTG_DIEPCTL0 / 4] &= ~USB_OTG_EPCTL_EPENA; s->regs[USB_OTG_DIEPINT0 / 4] |= USB_OTG_EPINT_XFERCOMPL; s->host_stage = USB_HOST_WAIT_DFU_GETSTATUS_SETUP; s5l8950x_usb_otg_update_irq(s); } static void s5l8950x_usb_dfu_complete_getstate_in( S5L8950XUSBOTGState *s) { hwaddr dma = s->regs[USB_OTG_DIEPDMA0 / 4]; if (dma_memory_read(&address_space_memory, dma, &s->dfu_state, 1, MEMTXATTRS_UNSPECIFIED) != MEMTX_OK) { qemu_log_mask(LOG_GUEST_ERROR, "s5l8950x.usb: DFU_GETSTATE DMA read failed at 0x%08" HWADDR_PRIx "\n", dma); s->host_stage = USB_HOST_DFU_ERROR; return; } s->regs[USB_OTG_DIEPTSIZ0 / 4] &= ~0x7f; s->regs[USB_OTG_DIEPCTL0 / 4] &= ~USB_OTG_EPCTL_EPENA; s->regs[USB_OTG_DIEPINT0 / 4] |= USB_OTG_EPINT_XFERCOMPL; s->host_stage = USB_HOST_WAIT_DFU_GETSTATE_STATUS_OUT; qemu_log_mask(LOG_UNIMP, "s5l8950x.usb-host: DFU initial state=%u\n", s->dfu_state); s5l8950x_usb_otg_update_irq(s); } static void s5l8950x_usb_dfu_complete_getstatus_in( S5L8950XUSBOTGState *s) { hwaddr dma = s->regs[USB_OTG_DIEPDMA0 / 4]; if (dma_memory_read(&address_space_memory, dma, s->dfu_status, sizeof(s->dfu_status), MEMTXATTRS_UNSPECIFIED) != MEMTX_OK) { qemu_log_mask(LOG_GUEST_ERROR, "s5l8950x.usb: DFU_GETSTATUS DMA read failed at " "0x%08" HWADDR_PRIx "\n", dma); s->host_stage = USB_HOST_DFU_ERROR; return; } s->dfu_state = s->dfu_status[4]; s->regs[USB_OTG_DIEPTSIZ0 / 4] &= ~0x7f; s->regs[USB_OTG_DIEPCTL0 / 4] &= ~USB_OTG_EPCTL_EPENA; s->regs[USB_OTG_DIEPINT0 / 4] |= USB_OTG_EPINT_XFERCOMPL; s->host_stage = USB_HOST_WAIT_DFU_GETSTATUS_STATUS_OUT; qemu_log_mask(LOG_UNIMP, "s5l8950x.usb-host: DFU status=%u state=%u " "poll=%u ms\n", s->dfu_status[0], s->dfu_state, s->dfu_status[1] | (s->dfu_status[2] << 8) | (s->dfu_status[3] << 16)); s5l8950x_usb_otg_update_irq(s); } static S5L8950XUSBHostStage s5l8950x_usb_dfu_stage_after_status( S5L8950XUSBOTGState *s) { if (s->dfu_status[0] || s->dfu_state == USB_DFU_STATE_ERROR) { qemu_log_mask(LOG_GUEST_ERROR, "s5l8950x.usb-host: DFU rejected block %u " "(status=%u state=%u)\n", s->dfu_packet_block, s->dfu_status[0], s->dfu_state); return USB_HOST_DFU_ERROR; } if (s->dfu_packet_kind == USB_DFU_PACKET_NOTIFY) { s->dfu_finish_statuses++; if (s->dfu_state == USB_DFU_STATE_MANIFEST_SYNC || s->dfu_state == USB_DFU_STATE_MANIFEST) { if (s->dfu_finish_statuses >= USB_DFU_MAX_STATUS_POLLS) { qemu_log_mask(LOG_GUEST_ERROR, "s5l8950x.usb-host: DFU manifestation did " "not reach MANIFEST-WAIT-RESET\n"); return USB_HOST_DFU_ERROR; } return USB_HOST_WAIT_DFU_GETSTATUS_SETUP; } if (s->dfu_state != USB_DFU_STATE_WAIT_RESET) { qemu_log_mask(LOG_GUEST_ERROR, "s5l8950x.usb-host: unexpected final DFU state " "%u\n", s->dfu_state); return USB_HOST_DFU_ERROR; } qemu_log_mask(LOG_UNIMP, "s5l8950x.usb-host: SecureROM accepted DFU image " "(%zu bytes, final state=%u)\n", s->dfu_image_size, s->dfu_state); s->dfu_injection_complete = true; return USB_HOST_DFU_DONE; } if (s->dfu_state == USB_DFU_STATE_DNLOAD_IDLE) { s->dfu_status_polls = 0; return USB_HOST_DFU_DNLOAD_READY; } if (++s->dfu_status_polls >= USB_DFU_MAX_STATUS_POLLS) { qemu_log_mask(LOG_GUEST_ERROR, "s5l8950x.usb-host: DFU state %u did not become " "DNLOAD-IDLE\n", s->dfu_state); return USB_HOST_DFU_ERROR; } return USB_HOST_WAIT_DFU_GETSTATUS_SETUP; } static void s5l8950x_usb_otg_complete_descriptor_in( S5L8950XUSBOTGState *s) { hwaddr dma = s->regs[USB_OTG_DIEPDMA0 / 4]; uint32_t length = s->regs[USB_OTG_DIEPTSIZ0 / 4] & 0x7f; uint32_t i; length = MIN(length, (uint32_t)sizeof(s->device_descriptor)); if (dma_memory_read(&address_space_memory, dma, s->device_descriptor, length, MEMTXATTRS_UNSPECIFIED) != MEMTX_OK) { qemu_log_mask(LOG_GUEST_ERROR, "s5l8950x.usb: EP0 IN DMA read failed at 0x%08" HWADDR_PRIx "\n", dma); return; } s->device_descriptor_len = length; s->descriptor_received = true; s->host_stage = USB_HOST_WAIT_DEVICE_STATUS_OUT; s->regs[USB_OTG_DIEPTSIZ0 / 4] &= ~0x7f; s->regs[USB_OTG_DIEPCTL0 / 4] &= ~USB_OTG_EPCTL_EPENA; s->regs[USB_OTG_DIEPINT0 / 4] |= USB_OTG_EPINT_XFERCOMPL; qemu_log_mask(LOG_UNIMP, "s5l8950x.usb-host: device descriptor (%u bytes):", length); for (i = 0; i < length; i++) { qemu_log(" %02x", s->device_descriptor[i]); } qemu_log("\n"); s5l8950x_usb_otg_update_irq(s); } static void s5l8950x_usb_otg_complete_serial_in(S5L8950XUSBOTGState *s) { hwaddr dma = s->regs[USB_OTG_DIEPDMA0 / 4]; uint32_t length = s->regs[USB_OTG_DIEPTSIZ0 / 4] & 0x7f; uint32_t available = sizeof(s->serial_descriptor) - s->serial_descriptor_len; uint32_t descriptor_length; bool complete; length = MIN(length, available); if (dma_memory_read(&address_space_memory, dma, s->serial_descriptor + s->serial_descriptor_len, length, MEMTXATTRS_UNSPECIFIED) != MEMTX_OK) { qemu_log_mask(LOG_GUEST_ERROR, "s5l8950x.usb: serial descriptor DMA read failed at " "0x%08" HWADDR_PRIx "\n", dma); return; } s->serial_descriptor_len += length; descriptor_length = s->serial_descriptor_len ? s->serial_descriptor[0] : 0; complete = length < 64 || (descriptor_length && s->serial_descriptor_len >= descriptor_length) || s->serial_descriptor_len == sizeof(s->serial_descriptor); s->regs[USB_OTG_DIEPTSIZ0 / 4] &= ~0x7f; s->regs[USB_OTG_DIEPCTL0 / 4] &= ~USB_OTG_EPCTL_EPENA; s->regs[USB_OTG_DIEPINT0 / 4] |= USB_OTG_EPINT_XFERCOMPL; if (complete) { char serial[128]; uint32_t i; uint32_t j = 0; for (i = 2; i + 1 < s->serial_descriptor_len && j + 1 < sizeof(serial); i += 2) { serial[j++] = s->serial_descriptor[i + 1] ? '?' : s->serial_descriptor[i]; } serial[j] = '\0'; s->host_stage = USB_HOST_WAIT_SERIAL_STATUS_OUT; qemu_log_mask(LOG_UNIMP, "s5l8950x.usb-host: DFU serial: %s\n", serial); } s5l8950x_usb_otg_update_irq(s); } static void s5l8950x_usb_otg_complete_config_in(S5L8950XUSBOTGState *s, bool header_only) { hwaddr dma = s->regs[USB_OTG_DIEPDMA0 / 4]; uint32_t length = s->regs[USB_OTG_DIEPTSIZ0 / 4] & 0x7f; uint32_t i; length = MIN(length, (uint32_t)sizeof(s->config_descriptor)); if (dma_memory_read(&address_space_memory, dma, s->config_descriptor, length, MEMTXATTRS_UNSPECIFIED) != MEMTX_OK) { qemu_log_mask(LOG_GUEST_ERROR, "s5l8950x.usb: config descriptor DMA read failed at " "0x%08" HWADDR_PRIx "\n", dma); return; } s->config_descriptor_len = length; if (header_only && length >= 4) { s->config_total_length = s->config_descriptor[2] | (s->config_descriptor[3] << 8); if (s->config_total_length < 9 || s->config_total_length > sizeof(s->config_descriptor)) { s->config_total_length = 9; } s->host_stage = USB_HOST_WAIT_CONFIG_HEADER_STATUS_OUT; } else { s->host_stage = USB_HOST_WAIT_CONFIG_FULL_STATUS_OUT; } s->regs[USB_OTG_DIEPTSIZ0 / 4] &= ~0x7f; s->regs[USB_OTG_DIEPCTL0 / 4] &= ~USB_OTG_EPCTL_EPENA; s->regs[USB_OTG_DIEPINT0 / 4] |= USB_OTG_EPINT_XFERCOMPL; qemu_log_mask(LOG_UNIMP, "s5l8950x.usb-host: config descriptor (%u bytes):", length); for (i = 0; i < length; i++) { qemu_log(" %02x", s->config_descriptor[i]); } qemu_log("\n"); s5l8950x_usb_otg_update_irq(s); } static uint64_t s5l8950x_usb_otg_read(void *opaque, hwaddr offset, unsigned size) { S5L8950XUSBOTGState *s = opaque; uint32_t value; if (offset == USB_OTG_GRSTCTL) { value = s->regs[offset / sizeof(uint32_t)] | USB_OTG_GRSTCTL_AHBIDLE; } else { value = s->regs[offset / sizeof(uint32_t)]; } return value; } static void s5l8950x_usb_otg_write(void *opaque, hwaddr offset, uint64_t value, unsigned size) { S5L8950XUSBOTGState *s = opaque; if (s->enumeration_generation && !s->device_descriptor_len && (offset == USB_OTG_GRSTCTL || offset == USB_OTG_GAHBCFG || offset == USB_OTG_GINTSTS || offset == USB_OTG_GINTMSK || offset == USB_OTG_DCTL || offset == USB_OTG_DIEPMSK || offset == USB_OTG_DOEPMSK || offset == USB_OTG_DAINTMSK || offset == USB_OTG_DIEPCTL0 || offset == USB_OTG_DIEPTSIZ0 || offset == USB_OTG_DIEPDMA0 || offset == USB_OTG_DOEPCTL0 || offset == USB_OTG_DOEPTSIZ0 || offset == USB_OTG_DOEPDMA0)) { vaddr pc = current_cpu ? ARM_CPU(current_cpu)->env.regs[15] : 0; qemu_log_mask(LOG_UNIMP, "s5l8950x.usb-trace: PC=0x%08" VADDR_PRIx " write[0x%04" HWADDR_PRIx "]=0x%08" PRIx64 " stage=%u\n", pc, offset, value, s->host_stage); } if (offset == USB_OTG_GRSTCTL && (value & USB_OTG_GRSTCTL_CSFTRST)) { /* iBSS initializes the core again after SecureROM jumps to it. */ s5l8950x_usb_otg_rearm(s, false); qemu_log_mask(LOG_UNIMP, "s5l8950x.usb-host: core soft reset; " "waiting for device re-enumeration\n"); return; } if (offset == USB_OTG_DCTL) { uint32_t old_value = s->regs[USB_OTG_DCTL / 4]; if ((old_value & USB_OTG_DCTL_SFTDISCON) && !(value & USB_OTG_DCTL_SFTDISCON)) { /* * DCTL soft reconnect changes only the bus attachment. The DWC * register image programmed by iBSS must survive it; the host * reset is delivered later, once the interrupt handler is live. */ s->regs[USB_OTG_GINTSTS / 4] &= ~(USB_OTG_GINT_USBRST | USB_OTG_GINT_ENUMDONE); s->usb_reset_sent = false; s->enum_done_sent = false; s->setup_sent = false; s->descriptor_received = false; s->device_descriptor_len = 0; s->serial_descriptor_len = 0; s->config_descriptor_len = 0; s->config_total_length = 0; s->current_device_is_dfu = false; s->host_stage = USB_HOST_WAIT_DEVICE_SETUP; timer_mod(s->recovery_reset_timer, qemu_clock_get_ms(QEMU_CLOCK_VIRTUAL) + 1); qemu_log_mask(LOG_UNIMP, "s5l8950x.usb-host: soft reconnect; " "waiting for device re-enumeration\n"); } s->regs[USB_OTG_DCTL / 4] = value; s5l8950x_usb_otg_update_irq(s); return; } if (offset == USB_OTG_GINTSTS) { uint32_t old_status = s->regs[offset / 4]; s->regs[offset / 4] &= ~value; if ((old_status & value & USB_OTG_GINT_USBRST) && !s->enum_done_sent) { s->regs[offset / 4] |= USB_OTG_GINT_ENUMDONE; s->enum_done_sent = true; } s5l8950x_usb_otg_update_irq(s); return; } if (s5l8950x_usb_otg_is_ep_int(offset, USB_OTG_DIEPINT0) || s5l8950x_usb_otg_is_ep_int(offset, USB_OTG_DOEPINT0)) { s->regs[offset / 4] &= ~value; s5l8950x_usb_otg_update_irq(s); return; } if (offset == USB_OTG_GRSTCTL) { /* Reset/FIFO flush requests complete synchronously in this model. */ value &= ~(USB_OTG_GRSTCTL_CSFTRST | USB_OTG_GRSTCTL_RXFFLSH | USB_OTG_GRSTCTL_TXFFLSH); value |= USB_OTG_GRSTCTL_AHBIDLE; } s->regs[offset / sizeof(uint32_t)] = value; if ((value & USB_OTG_EPCTL_EPENA) && (s5l8950x_usb_otg_is_ep_int(offset, USB_OTG_DIEPCTL0) || s5l8950x_usb_otg_is_ep_int(offset, USB_OTG_DOEPCTL0))) { s5l8950x_usb_bridge_try_bulk(s); } if ((offset == USB_OTG_DIEPCTL0 || offset == USB_OTG_DOEPCTL0) && (value & USB_OTG_EPCTL_STALL) && s->bridge_request_active) { qemu_log_mask(LOG_GUEST_ERROR, "s5l8950x.usb-bridge: guest armed EP0 STALL; " "CPU state follows\n"); if (current_cpu && qemu_loglevel_mask(LOG_GUEST_ERROR)) { FILE *log_file = qemu_log_trylock(); if (log_file) { cpu_dump_state(current_cpu, log_file, 0); qemu_log_unlock(log_file); } } s5l8950x_usb_bridge_finish(s, QA6_USB_STATUS_STALL); s5l8950x_usb_otg_update_irq(s); return; } if (offset == USB_OTG_DIEPCTL0 && (value & USB_OTG_EPCTL_STALL) && s->host_stage == USB_HOST_WAIT_DFU_DNLOAD_STATUS_IN) { s->host_stage = USB_HOST_DFU_ERROR; qemu_log_mask(LOG_GUEST_ERROR, "s5l8950x.usb-host: firmware stalled EP0; " "DFU image rejected after block %u\n", s->dfu_packet_block); s5l8950x_usb_otg_update_irq(s); return; } if (offset == USB_OTG_GINTMSK && (value & USB_OTG_GINT_USBRST) && !s->usb_reset_sent) { if (s->enumeration_generation) { /* * Enabling USBRST while DCTL still holds the soft-disconnect * bit does not expose the device to a physical host. UART * logging makes iBEC's early platform setup long enough for an * incorrectly armed timer to fire here, before its USB handler * is installed. The DCTL reconnect path above will arm the * reset once the guest is actually visible on the bus. */ if (!(s->regs[USB_OTG_DCTL / 4] & USB_OTG_DCTL_SFTDISCON)) { timer_mod(s->recovery_reset_timer, qemu_clock_get_ms(QEMU_CLOCK_VIRTUAL) + 1); } } else { s->regs[USB_OTG_GINTSTS / 4] |= USB_OTG_GINT_USBRST; s->usb_reset_sent = true; } } if (offset == USB_OTG_DOEPCTL0 && (value & USB_OTG_EPCTL_EPENA) && s->enum_done_sent) { if (s->host_stage == USB_HOST_DFU_READY && s->bridge_request_active) { s5l8950x_usb_bridge_start_request(s); } else if (s->host_stage == USB_HOST_BRIDGE_WAIT_DATA_OUT) { s5l8950x_usb_bridge_complete_data_out(s); } else if (s->host_stage == USB_HOST_BRIDGE_WAIT_STATUS_OUT) { s5l8950x_usb_bridge_complete_status_out(s); } else if (s->host_stage == USB_HOST_WAIT_DEVICE_SETUP) { s5l8950x_usb_otg_send_device_descriptor_setup(s); } else if (s->host_stage == USB_HOST_WAIT_DEVICE_STATUS_OUT) { s5l8950x_usb_otg_complete_out_status( s, USB_HOST_WAIT_SET_ADDRESS_SETUP); } else if (s->host_stage == USB_HOST_WAIT_SET_ADDRESS_SETUP) { s5l8950x_usb_otg_send_set_address_setup(s); } else if (s->host_stage == USB_HOST_ADDRESS_ASSIGNED) { s5l8950x_usb_otg_send_serial_descriptor_setup(s); } else if (s->host_stage == USB_HOST_WAIT_SERIAL_STATUS_OUT) { s5l8950x_usb_otg_complete_out_status( s, USB_HOST_WAIT_CONFIG_HEADER_SETUP); } else if (s->host_stage == USB_HOST_WAIT_CONFIG_HEADER_SETUP) { s5l8950x_usb_otg_send_config_header_setup(s); } else if (s->host_stage == USB_HOST_WAIT_CONFIG_HEADER_STATUS_OUT) { s5l8950x_usb_otg_complete_out_status( s, USB_HOST_WAIT_CONFIG_FULL_SETUP); } else if (s->host_stage == USB_HOST_WAIT_CONFIG_FULL_SETUP) { s5l8950x_usb_otg_send_config_full_setup(s); } else if (s->host_stage == USB_HOST_WAIT_CONFIG_FULL_STATUS_OUT) { s5l8950x_usb_otg_complete_out_status( s, USB_HOST_WAIT_SET_CONFIGURATION_SETUP); } else if (s->host_stage == USB_HOST_WAIT_SET_CONFIGURATION_SETUP) { s5l8950x_usb_otg_send_set_configuration_setup(s); } else if (s->host_stage == USB_HOST_DFU_READY && s->dfu_image && s->current_device_is_dfu && !s->dfu_injection_complete) { s5l8950x_usb_dfu_send_getstate(s); } else if (s->host_stage == USB_HOST_WAIT_DFU_GETSTATE_STATUS_OUT) { s5l8950x_usb_otg_complete_out_status( s, s->dfu_state == USB_DFU_STATE_IDLE ? USB_HOST_DFU_DNLOAD_READY : USB_HOST_DFU_ERROR); if (s->dfu_state != USB_DFU_STATE_IDLE) { qemu_log_mask(LOG_GUEST_ERROR, "s5l8950x.usb-host: expected DFU-IDLE, got " "state %u\n", s->dfu_state); } } else if (s->host_stage == USB_HOST_DFU_DNLOAD_READY) { s5l8950x_usb_dfu_send_dnload(s); } else if (s->host_stage == USB_HOST_WAIT_DFU_DNLOAD_DATA_OUT) { s5l8950x_usb_dfu_complete_dnload_data_out(s); } else if (s->host_stage == USB_HOST_WAIT_DFU_GETSTATUS_SETUP) { s5l8950x_usb_dfu_send_getstatus(s); } else if (s->host_stage == USB_HOST_WAIT_DFU_GETSTATUS_STATUS_OUT) { s5l8950x_usb_otg_complete_out_status( s, s5l8950x_usb_dfu_stage_after_status(s)); } /* Device firmware may arm the status IN stage before SETUP/OUT ends. */ if (s->regs[USB_OTG_DIEPCTL0 / 4] & USB_OTG_EPCTL_EPENA) { if (s->host_stage == USB_HOST_BRIDGE_WAIT_STATUS_IN) { s5l8950x_usb_bridge_complete_status_in(s); } else if (s->host_stage == USB_HOST_WAIT_SET_ADDRESS_STATUS_IN) { s5l8950x_usb_otg_complete_in_status(s); } else if (s->host_stage == USB_HOST_WAIT_SET_CONFIGURATION_STATUS_IN) { s5l8950x_usb_otg_complete_configuration_status_in(s); } else if (s->host_stage == USB_HOST_WAIT_DFU_DNLOAD_STATUS_IN) { s5l8950x_usb_dfu_complete_dnload_status_in(s); } } } if (offset == USB_OTG_DIEPCTL0 && (value & USB_OTG_EPCTL_EPENA)) { if (s->host_stage == USB_HOST_BRIDGE_WAIT_DATA_IN) { s5l8950x_usb_bridge_complete_data_in(s); } else if (s->host_stage == USB_HOST_BRIDGE_WAIT_STATUS_IN) { s5l8950x_usb_bridge_complete_status_in(s); } else if (s->host_stage == USB_HOST_WAIT_DEVICE_IN) { s5l8950x_usb_otg_complete_descriptor_in(s); } else if (s->host_stage == USB_HOST_WAIT_SET_ADDRESS_STATUS_IN) { s5l8950x_usb_otg_complete_in_status(s); } else if (s->host_stage == USB_HOST_WAIT_SERIAL_IN) { s5l8950x_usb_otg_complete_serial_in(s); } else if (s->host_stage == USB_HOST_WAIT_CONFIG_HEADER_IN) { s5l8950x_usb_otg_complete_config_in(s, true); } else if (s->host_stage == USB_HOST_WAIT_CONFIG_FULL_IN) { s5l8950x_usb_otg_complete_config_in(s, false); } else if (s->host_stage == USB_HOST_WAIT_SET_CONFIGURATION_STATUS_IN) { s5l8950x_usb_otg_complete_configuration_status_in(s); } else if (s->host_stage == USB_HOST_WAIT_DFU_GETSTATE_IN) { s5l8950x_usb_dfu_complete_getstate_in(s); } else if (s->host_stage == USB_HOST_WAIT_DFU_DNLOAD_STATUS_IN) { s5l8950x_usb_dfu_complete_dnload_status_in(s); } else if (s->host_stage == USB_HOST_WAIT_DFU_GETSTATUS_IN) { s5l8950x_usb_dfu_complete_getstatus_in(s); } /* Device firmware may arm status OUT before the final IN packet. */ if (s->regs[USB_OTG_DOEPCTL0 / 4] & USB_OTG_EPCTL_EPENA) { if (s->host_stage == USB_HOST_BRIDGE_WAIT_STATUS_OUT) { s5l8950x_usb_bridge_complete_status_out(s); } else if (s->host_stage == USB_HOST_WAIT_DEVICE_STATUS_OUT) { s5l8950x_usb_otg_complete_out_status( s, USB_HOST_WAIT_SET_ADDRESS_SETUP); } else if (s->host_stage == USB_HOST_WAIT_SERIAL_STATUS_OUT) { s5l8950x_usb_otg_complete_out_status( s, USB_HOST_WAIT_CONFIG_HEADER_SETUP); } else if (s->host_stage == USB_HOST_WAIT_CONFIG_HEADER_STATUS_OUT) { s5l8950x_usb_otg_complete_out_status( s, USB_HOST_WAIT_CONFIG_FULL_SETUP); } else if (s->host_stage == USB_HOST_WAIT_CONFIG_FULL_STATUS_OUT) { s5l8950x_usb_otg_complete_out_status( s, USB_HOST_WAIT_SET_CONFIGURATION_SETUP); } else if (s->host_stage == USB_HOST_WAIT_DFU_GETSTATE_STATUS_OUT) { s5l8950x_usb_otg_complete_out_status( s, s->dfu_state == USB_DFU_STATE_IDLE ? USB_HOST_DFU_DNLOAD_READY : USB_HOST_DFU_ERROR); } else if (s->host_stage == USB_HOST_WAIT_DFU_GETSTATUS_STATUS_OUT) { s5l8950x_usb_otg_complete_out_status( s, s5l8950x_usb_dfu_stage_after_status(s)); } } } s5l8950x_usb_otg_update_irq(s); } static const MemoryRegionOps s5l8950x_usb_otg_ops = { .read = s5l8950x_usb_otg_read, .write = s5l8950x_usb_otg_write, .endianness = DEVICE_LITTLE_ENDIAN, .valid = { .min_access_size = 4, .max_access_size = 4, }, .impl = { .min_access_size = 4, .max_access_size = 4, }, }; static void s5l8950x_usb_otg_reset(DeviceState *dev) { S5L8950XUSBOTGState *s = S5L8950X_USB_OTG(dev); QEMU_BUILD_BUG_ON(sizeof(Qa6UsbFrameHeader) != 32); QEMU_BUILD_BUG_ON(sizeof(Qa6UsbDeviceInfo) != 268); s5l8950x_usb_otg_rearm(s, true); } static void s5l8950x_usb_otg_realize(DeviceState *dev, Error **errp) { S5L8950XUSBOTGState *s = S5L8950X_USB_OTG(dev); g_autoptr(GError) gerr = NULL; if (qemu_chr_fe_backend_connected(&s->bridge_chr)) { qemu_chr_fe_set_handlers(&s->bridge_chr, s5l8950x_usb_bridge_can_receive, s5l8950x_usb_bridge_receive, s5l8950x_usb_bridge_event, NULL, s, NULL, true); qemu_log_mask(LOG_UNIMP, "s5l8950x.usb-bridge: external USB bridge enabled\n"); } if (!s->dfu_image_path) { return; } if (!g_file_get_contents(s->dfu_image_path, (gchar **)&s->dfu_image, &s->dfu_image_size, &gerr)) { error_setg(errp, "could not load DFU image '%s': %s", s->dfu_image_path, gerr->message); return; } if (!s->dfu_image_size || s->dfu_image_size > (size_t)UINT16_MAX * USB_DFU_TRANSFER_SIZE) { error_setg(errp, "DFU image '%s' has unsupported size %zu", s->dfu_image_path, s->dfu_image_size); g_clear_pointer(&s->dfu_image, g_free); s->dfu_image_size = 0; return; } s5l8950x_usb_dfu_build_suffix(s); s->dfu_notify_block = DIV_ROUND_UP(s->dfu_image_size, USB_DFU_TRANSFER_SIZE); qemu_log_mask(LOG_UNIMP, "s5l8950x.usb-host: loaded DFU image '%s' (%zu bytes)\n", s->dfu_image_path, s->dfu_image_size); } static void s5l8950x_usb_otg_unrealize(DeviceState *dev) { S5L8950XUSBOTGState *s = S5L8950X_USB_OTG(dev); qemu_chr_fe_set_handlers(&s->bridge_chr, NULL, NULL, NULL, NULL, NULL, NULL, false); timer_free(s->recovery_reset_timer); s->recovery_reset_timer = NULL; timer_free(s->bridge_bulk_timer); s->bridge_bulk_timer = NULL; g_clear_pointer(&s->bridge_dfu_image, g_byte_array_unref); g_clear_pointer(&s->dfu_image, g_free); s->dfu_image_size = 0; } static void s5l8950x_usb_otg_init(Object *obj) { S5L8950XUSBOTGState *s = S5L8950X_USB_OTG(obj); SysBusDevice *sbd = SYS_BUS_DEVICE(obj); s->recovery_reset_timer = timer_new_ms( QEMU_CLOCK_VIRTUAL, s5l8950x_usb_otg_recovery_reset, s); s->bridge_bulk_timer = timer_new_ms( QEMU_CLOCK_VIRTUAL, s5l8950x_usb_bridge_bulk_timeout, s); s->bridge_dfu_image = g_byte_array_new(); memory_region_init_io(&s->iomem, obj, &s5l8950x_usb_otg_ops, s, TYPE_S5L8950X_USB_OTG, S5L8950X_USB_OTG_REGION_SIZE); sysbus_init_mmio(sbd, &s->iomem); sysbus_init_irq(sbd, &s->irq); } static const Property s5l8950x_usb_otg_properties[] = { DEFINE_PROP_STRING("dfu-image", S5L8950XUSBOTGState, dfu_image_path), DEFINE_PROP_CHR("usb-bridge", S5L8950XUSBOTGState, bridge_chr), DEFINE_PROP_BOOL("force-debug-uarts", S5L8950XUSBOTGState, force_debug_uarts, false), DEFINE_PROP_BOOL("native-img3-handoff", S5L8950XUSBOTGState, native_img3_handoff, false), }; static void s5l8950x_usb_otg_class_init(ObjectClass *klass, const void *data) { DeviceClass *dc = DEVICE_CLASS(klass); dc->realize = s5l8950x_usb_otg_realize; dc->unrealize = s5l8950x_usb_otg_unrealize; device_class_set_legacy_reset(dc, s5l8950x_usb_otg_reset); device_class_set_props(dc, s5l8950x_usb_otg_properties); } static const TypeInfo s5l8950x_usb_otg_info = { .name = TYPE_S5L8950X_USB_OTG, .parent = TYPE_SYS_BUS_DEVICE, .instance_size = sizeof(S5L8950XUSBOTGState), .instance_init = s5l8950x_usb_otg_init, .class_init = s5l8950x_usb_otg_class_init, }; static void s5l8950x_usb_otg_register_types(void) { type_register_static(&s5l8950x_usb_otg_info); } type_init(s5l8950x_usb_otg_register_types)