/*
    ChibiOS - Copyright (C) 2006..2017 Giovanni Di Sirio
              Copyright (C) 2015..2019 Diego Ismirlian, (dismirlian(at)google's mail)

    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at

        http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.
*/

#include "hal.h"

#if HAL_USE_USBH
#include "usbh/internal.h"
#include <string.h>

#if STM32_USBH_USE_OTG1
#if !defined(STM32_OTG_FS_CHANNELS_NUMBER)
#error "STM32_OTG_FS_CHANNELS_NUMBER must be defined"
#endif
#if !defined(STM32_OTG1_USE_HS)
#define STM32_OTG1_USE_HS FALSE
#endif
#if !defined(STM32_OTG1_USE_ULPI)
#define STM32_OTG1_USE_ULPI FALSE
#endif
#if !defined(STM32_OTG1_USE_ULPI_VBUS)
#define STM32_OTG1_USE_ULPI_VBUS FALSE
#endif
#if !defined(STM32_OTG_FS_RXFIFO_SIZE)
#define STM32_OTG_FS_RXFIFO_SIZE 1024
#endif
#if !defined(STM32_OTG_FS_PTXFIFO_SIZE)
#define STM32_OTG_FS_PTXFIFO_SIZE 128
#endif
#if !defined(STM32_OTG_FS_NPTXFIFO_SIZE)
#define STM32_OTG_FS_NPTXFIFO_SIZE 128
#endif
#if !defined(STM32_OTG_FS_FIFO_MEM_SIZE)
#define STM32_OTG_FS_FIFO_MEM_SIZE 320
#endif
#if defined(STM32H7XX)
#define rccEnableOTG1(lp) rccEnableUSB1_OTG_HS(lp)
#define rccDisableOTG1() rccDisableUSB1_OTG_HS()
#define rccResetOTG1() rccResetUSB1_OTG_HS()
#define rccEnableOTG1_HSULPI(lp) rccEnableUSB1_HSULPI(lp)
#define rccDisableOTG1_HSULPI() rccDisableUSB1_HSULPI()
#define rccResetOTG1_HSULPI() rccResetUSB1_HSULPI()
#define OTG1 OTG_HS
#define OTG1_CHANNELS_NUMBER STM32_OTG_HS_CHANNELS_NUMBER
#else
#define rccEnableOTG1(lp) rccEnableOTG_FS(lp)
#define rccDisableOTG1() rccDisableOTG_FS()
#define rccResetOTG1() rccResetOTG_FS()
#define OTG1 OTG_FS
#define OTG1_CHANNELS_NUMBER STM32_OTG_FS_CHANNELS_NUMBER
#if STM32_OTG1_USE_ULPI
#error "OTG1 has no ULPI on this platform"
#endif
#endif
#if (STM32_OTG_FS_RXFIFO_SIZE + STM32_OTG_FS_PTXFIFO_SIZE + STM32_OTG_FS_NPTXFIFO_SIZE) > (STM32_OTG_FS_FIFO_MEM_SIZE * 4)
#error "Not enough memory in OTG_FS implementation"
#elif (STM32_OTG_FS_RXFIFO_SIZE + STM32_OTG_FS_PTXFIFO_SIZE + STM32_OTG_FS_NPTXFIFO_SIZE) < (STM32_OTG1_FIFO_MEM_SIZE * 4)
#warning "Spare memory in OTG_FS; could enlarge RX, PTX or NPTX FIFO sizes"
#endif
#if (STM32_OTG_FS_RXFIFO_SIZE % 4) || (STM32_OTG_FS_PTXFIFO_SIZE % 4) || (STM32_OTG_FS_NPTXFIFO_SIZE % 4)
#error "FIFO sizes must be a multiple of 32-bit words"
#endif
#endif

#if STM32_USBH_USE_OTG2
#if !defined(STM32_OTG_HS_CHANNELS_NUMBER)
#error "STM32_OTG_HS_CHANNELS_NUMBER must be defined"
#endif
#if !defined(STM32_OTG2_USE_HS)
#define STM32_OTG2_USE_HS FALSE
#endif
#if !defined(STM32_OTG1_USE_ULPI)
#define STM32_OTG1_USE_ULPI FALSE
#endif
#if !defined(STM32_OTG2_USE_ULPI)
#define STM32_OTG2_USE_ULPI FALSE
#endif
#if !defined(STM32_OTG2_USE_ULPI_VBUS)
#define STM32_OTG2_USE_ULPI_VBUS FALSE
#endif
#if !defined(STM32_OTG_HS_RXFIFO_SIZE)
#define STM32_OTG_HS_RXFIFO_SIZE		2048
#endif
#if !defined(STM32_OTG_HS_PTXFIFO_SIZE)
#define STM32_OTG_HS_PTXFIFO_SIZE		1024
#endif
#if !defined(STM32_OTG_HS_NPTXFIFO_SIZE)
#define STM32_OTG_HS_NPTXFIFO_SIZE	1024
#endif
#if !defined(STM32_OTG_HS_FIFO_MEM_SIZE)
#define STM32_OTG_HS_FIFO_MEM_SIZE 1024
#endif
#if !defined(STM32_OTG2_USE_ULPI)
#define STM32_OTG2_USE_ULPI FALSE
#endif
#if defined(STM32H7XX)
#define rccEnableOTG2(lp) rccEnableUSB2_OTG_FS(lp)
#define rccDisableOTG2() rccDisableUSB2_OTG_FS()
#define rccResetOTG2() rccResetUSB2_OTG_FS()
#define OTG2 OTG_FS
#define OTG2_CHANNELS_NUMBER STM32_OTG_FS_CHANNELS_NUMBER
#if STM32_OTG2_USE_ULPI
#error "OTG2 has no ULPI on this platform"
#endif
#else          
#define rccEnableOTG2(lp) rccEnableOTG_HS(lp)
#define rccDisableOTG2() rccDisableOTG_HS()
#define rccResetOTG2() rccResetOTG_HS()                                                                                                                                                         
#define rccEnableOTG2_HSULPI(x) rccEnableUSB2_HSULPI(x)
#define rccDisableOTG2_HSULPI() rccDisableUSB2_HSULPI()
#define rccResetOTG2_HSULPI() rccResetUSB2_HSULPI()
#define OTG2 OTG_HS
#define OTG2_CHANNELS_NUMBER STM32_OTG_HS_CHANNELS_NUMBER
#endif
#if (STM32_OTG_HS_RXFIFO_SIZE + STM32_OTG_HS_PTXFIFO_SIZE + STM32_OTG_HS_NPTXFIFO_SIZE) > (STM32_OTG_HS_FIFO_MEM_SIZE * 4)
#error "Not enough memory in OTG_HS implementation"
#elif (STM32_OTG_HS_RXFIFO_SIZE + STM32_OTG_HS_PTXFIFO_SIZE + STM32_OTG_HS_NPTXFIFO_SIZE) < (STM32_OTG_HS_FIFO_MEM_SIZE * 4)
#warning "Spare memory in OTG_HS; could enlarge RX, PTX or NPTX FIFO sizes"
#endif
#if (STM32_OTG_HS_RXFIFO_SIZE % 4) || (STM32_OTG_HS_PTXFIFO_SIZE % 4) || (STM32_OTG_HS_NPTXFIFO_SIZE % 4)
#error "FIFO sizes must be a multiple of 32-bit words"
#endif
#endif

#define TRDT_VALUE_FS 5
#define TRDT_VALUE_HS 9

#define _USBH_DEBUG_HELPER_ENABLE_TRACE		USBH_LLD_DEBUG_ENABLE_TRACE
#define _USBH_DEBUG_HELPER_ENABLE_INFO		USBH_LLD_DEBUG_ENABLE_INFO
#define _USBH_DEBUG_HELPER_ENABLE_WARNINGS	USBH_LLD_DEBUG_ENABLE_WARNINGS
#define _USBH_DEBUG_HELPER_ENABLE_ERRORS	USBH_LLD_DEBUG_ENABLE_ERRORS
#include "usbh/debug_helpers.h"

static void _transfer_completedI(usbh_ep_t *ep, usbh_urb_t *urb, usbh_urbstatus_t status);
static void _try_commit_np(USBHDriver *host);
static void otg_rxfifo_flush(USBHDriver *usbp);
static void otg_txfifo_flush(USBHDriver *usbp, uint32_t fifo);

#if STM32_USBH_USE_OTG1
USBHDriver USBHD1;
#endif
#if STM32_USBH_USE_OTG2
USBHDriver USBHD2;
#endif

/*===========================================================================*/
/* Little helper functions.                                                  */
/*===========================================================================*/
static inline void _move_to_pending_queue(usbh_ep_t *ep) {
	list_move_tail(&ep->node, ep->pending_list);
}

static inline usbh_urb_t *_active_urb(usbh_ep_t *ep) {
	return list_first_entry(&ep->urb_list, usbh_urb_t, node);
}

static inline void _save_dt_mask(usbh_ep_t *ep, uint32_t hctsiz) {
	ep->dt_mask = hctsiz & HCTSIZ_DPID_MASK;
}

/*===========================================================================*/
/* Functions called from many places.                                        */
/*===========================================================================*/
static void _transfer_completedI(usbh_ep_t *ep, usbh_urb_t *urb, usbh_urbstatus_t status) {
	osalDbgCheckClassI();

	urb->queued = FALSE;

	/* remove URB from EP's queue */
	list_del_init(&urb->node);

	/* Call the callback function now, so that if it calls usbhURBSubmitI,
	 * the list_empty check below will be false. Also, note that the
	 *   if (list_empty(&ep->node)) {
	 *     ...
	 *   }
	 * in usbh_lld_urb_submit will be false, since the endpoint is
	 * still in the active queue.
	 */
	_usbh_urb_completeI(urb, status);

	if (list_empty(&ep->urb_list)) {
		/* no more URBs to process in this EP, remove EP from the host's queue */
		list_del_init(&ep->node);
	} else {
		/* more URBs to process */
		_move_to_pending_queue(ep);
	}
}

static void _halt_channel(USBHDriver *host, stm32_hc_management_t *hcm, usbh_lld_halt_reason_t reason) {
	(void)host;

	if (hcm->halt_reason != USBH_LLD_HALTREASON_NONE) {
#if USBH_DEBUG_ENABLE && USBH_LLD_DEBUG_ENABLE_WARNINGS
		usbh_ep_t *const ep = hcm->ep;
		uepwarnf("Repeated halt (original=%d, new=%d)", hcm->halt_reason, reason);
#endif
		return;
	}

#if CH_DBG_ENABLE_CHECKS
	if (usbhEPIsPeriodic(hcm->ep)) {
		osalDbgCheck(host->otg->HPTXSTS & HPTXSTS_PTXQSAV_MASK);
	} else {
		osalDbgCheck(host->otg->HNPTXSTS & HPTXSTS_PTXQSAV_MASK);
	}
#endif

	hcm->halt_reason = reason;
	hcm->hc->HCCHAR |= HCCHAR_CHENA | HCCHAR_CHDIS;
}

static void _release_channel(USBHDriver *host, stm32_hc_management_t *hcm) {
	usbh_ep_t *const ep = hcm->ep;
#if USBH_DEBUG_ENABLE && USBH_LLD_DEBUG_ENABLE_TRACE
	static const char *reason[] =  {"XFRC",	"XFRC",	"NAK", "STALL",	"ERROR", "ABORT"};
	uepdbgf("release (%s)", reason[hcm->halt_reason]);
#endif
	hcm->hc->HCINTMSK = 0;
	host->otg->HAINTMSK &= ~hcm->haintmsk;
	hcm->halt_reason = USBH_LLD_HALTREASON_NONE;
	if (usbhEPIsPeriodic(ep)) {
		list_add(&hcm->node, &host->ch_free[0]);
	} else {
		list_add(&hcm->node, &host->ch_free[1]);
	}
	ep->xfer.hcm = 0;
	hcm->ep = 0;
}

static bool _activate_ep(USBHDriver *host, usbh_ep_t *ep) {
	struct list_head *list;
	uint16_t spc;

	osalDbgCheck(ep->xfer.hcm == NULL);

	if (usbhEPIsPeriodic(ep)) {
		list = &host->ch_free[0];
		spc = (host->otg->HPTXSTS >> 16) & 0xff;
	} else {
		list = &host->ch_free[1];
		spc = (host->otg->HNPTXSTS >> 16) & 0xff;
	}

	if (list_empty(list)) {
		uepwarnf("No free %s channels", usbhEPIsPeriodic(ep) ? "P" : "NP");
		return FALSE;
	}

	if (spc <= STM32_USBH_MIN_QSPACE) {
		uepwarnf("No space in %s Queue (spc=%d)", usbhEPIsPeriodic(ep) ? "P" : "NP", spc);
		return FALSE;
	}

	/* get the first channel */
	stm32_hc_management_t *hcm = list_first_entry(list, stm32_hc_management_t, node);
	osalDbgCheck((hcm->halt_reason == USBH_LLD_HALTREASON_NONE) && (hcm->ep == NULL));

	usbh_urb_t *const urb = _active_urb(ep);
	uint32_t hcintmsk = ep->hcintmsk;
	uint32_t hcchar = ep->hcchar;
	uint16_t mps = ep->wMaxPacketSize;

	uint32_t xfer_packets;
	uint32_t xfer_len = 0;	//Initialize just to shut up a compiler warning

	osalDbgCheck(urb->status == USBH_URBSTATUS_PENDING);

	/* check if the URB is a new one, or we must continue a previously started URB */
	if (urb->queued == FALSE) {
		/* prepare EP for a new URB */
		if (ep->type == USBH_EPTYPE_CTRL) {
			xfer_len = 8;
			ep->xfer.buf = (uint8_t *)urb->setup_buff;
			ep->dt_mask = HCTSIZ_DPID_SETUP;
			ep->in = FALSE;
			ep->xfer.u.ctrl_phase = USBH_LLD_CTRLPHASE_SETUP;
		} else {
			xfer_len = urb->requestedLength;
			ep->xfer.buf = urb->buff;
		}
		ep->xfer.error_count = 0;
	} else {
		osalDbgCheck(urb->requestedLength >= urb->actualLength);

		if (ep->type == USBH_EPTYPE_CTRL) {
			switch (ep->xfer.u.ctrl_phase) {
			case USBH_LLD_CTRLPHASE_SETUP:
				xfer_len = 8;
				ep->xfer.buf = (uint8_t *)urb->setup_buff;
				ep->dt_mask = HCTSIZ_DPID_SETUP;
				break;
			case USBH_LLD_CTRLPHASE_DATA:
				xfer_len = urb->requestedLength - urb->actualLength;
				ep->xfer.buf = (uint8_t *) urb->buff + urb->actualLength;
				break;
			case USBH_LLD_CTRLPHASE_STATUS:
				xfer_len = 0;
				ep->dt_mask = HCTSIZ_DPID_DATA1;
				ep->xfer.error_count = 0;
				break;
			default:
				osalDbgCheck(0);
			}
			if (ep->in) {
				hcintmsk |= HCINTMSK_DTERRM | HCINTMSK_BBERRM;
				hcchar |= HCCHAR_EPDIR;
			}
		} else {
			xfer_len = urb->requestedLength - urb->actualLength;
			ep->xfer.buf = (uint8_t *) urb->buff + urb->actualLength;
		}

		if (ep->xfer.error_count)
			hcintmsk |= HCINTMSK_ACKM;

	}
	ep->xfer.partial = 0;

	if (ep->type == USBH_EPTYPE_ISO) {
		ep->dt_mask = HCTSIZ_DPID_DATA0;

		/* [USB 2.0 spec, 5.6.4]: A host must not issue more than 1
		 * transaction in a (micro)frame for an isochronous endpoint
		 * unless the endpoint is high-speed, high-bandwidth.
		 */
		if (xfer_len > mps)
			xfer_len = mps;
	} else if (xfer_len > 0x7FFFF) {
		xfer_len = 0x7FFFF - mps + 1;
	}

	/* calculate required packets */
	if (xfer_len) {
		xfer_packets = (xfer_len + mps - 1) / mps;

		if (xfer_packets > 0x3FF) {
			xfer_packets = 0x3FF;
			xfer_len = xfer_packets * mps;
		}
	} else {
		xfer_packets = 1;	/* Need 1 packet for transfer length of 0 */
	}

	if (ep->in)
		xfer_len = xfer_packets * mps;

	/* Clear old interrupt conditions,
	 * configure transfer size,
	 * enable required interrupts */
	stm32_otg_host_chn_t *const hc = hcm->hc;
	hc->HCINT = 0xffffffff;
	hc->HCTSIZ = ep->dt_mask
					| HCTSIZ_PKTCNT(xfer_packets)
					| HCTSIZ_XFRSIZ(xfer_len);
	hc->HCINTMSK = hcintmsk;

	/* Queue the transfer for the next frame (no effect for non-periodic transfers) */
	if (!(host->otg->HFNUM & 1))
		hcchar |= HCCHAR_ODDFRM;

	/* configure channel characteristics and queue a request */
	hc->HCCHAR = hcchar;
	if (ep->in && (xfer_packets > 1)) {
		/* For IN transfers, try to queue two back-to-back packets.
		 * This results in a 1% performance gain for Full Speed transfers
		 */
		if (--spc > STM32_USBH_MIN_QSPACE) {
			hc->HCCHAR |= HCCHAR_CHENA;
		} else {
			uepwarnf("Could not queue back-to-back packets");
		}
	}

	if (urb->queued == FALSE) {
		urb->queued = TRUE;
		uepdbgf("Start (%dB)", xfer_len);
	} else {
		uepdbgf("Restart (%dB)", xfer_len);
	}

	ep->xfer.len = xfer_len;
	ep->xfer.packets = (uint16_t)xfer_packets;

	/* remove the channel from the free list, link endpoint <-> channel and move to the active queue*/
	list_del(&hcm->node);
	ep->xfer.hcm = hcm;
	hcm->ep = ep;
	list_move_tail(&ep->node, ep->active_list);


	stm32_otg_t *const otg = host->otg;

	/* enable this channel's interrupt and global channel interrupt */
	otg->HAINTMSK |= hcm->haintmsk;
	if (ep->in) {
		otg->GINTMSK |= GINTMSK_HCM;
	} else if (usbhEPIsPeriodic(ep)) {
		otg->GINTMSK |= GINTMSK_HCM | GINTMSK_PTXFEM;
	} else {
		//TODO: write to the FIFO now
		otg->GINTMSK |= GINTMSK_HCM | GINTMSK_NPTXFEM;
	}

	return TRUE;
}

static bool _update_urb(usbh_ep_t *ep, uint32_t hctsiz, usbh_urb_t *urb, bool completed) {
	uint32_t len;

	if (!completed) {
		len = ep->wMaxPacketSize * (ep->xfer.packets - ((hctsiz & HCTSIZ_PKTCNT_MASK) >> 19));
	} else {
		if (ep->in) {
			len = ep->xfer.len - ((hctsiz & HCTSIZ_XFRSIZ_MASK) >> 0);
		} else {
			len = ep->xfer.len;
		}
		osalDbgCheck(len == ep->xfer.partial);	//TODO: if len == ep->xfer.partial, use this instead of the above code
	}

#if 0
	osalDbgAssert(urb->actualLength + len <= urb->requestedLength, "what happened?");
#else
	if (urb->actualLength + len > urb->requestedLength) {
		ueperrf("Trimming actualLength %u -> %u", urb->actualLength + len, urb->requestedLength);
		urb->actualLength = urb->requestedLength;
		return TRUE;
	}
#endif

	urb->actualLength += len;
	if ((urb->actualLength == urb->requestedLength)
			|| (ep->in && completed && (hctsiz & HCTSIZ_XFRSIZ_MASK)))
		return TRUE;

	return FALSE;
}

static void _try_commit_np(USBHDriver *host) {
	usbh_ep_t *item, *tmp;

	list_for_each_entry_safe(item, usbh_ep_t, tmp, &host->ep_pending_lists[USBH_EPTYPE_CTRL], node) {
		if (!_activate_ep(host, item))
			return;
	}

	list_for_each_entry_safe(item, usbh_ep_t, tmp, &host->ep_pending_lists[USBH_EPTYPE_BULK], node) {
		if (!_activate_ep(host, item))
			return;
	}
}

static void _try_commit_p(USBHDriver *host, bool sof) {
	usbh_ep_t *item, *tmp;

	list_for_each_entry_safe(item, usbh_ep_t, tmp, &host->ep_pending_lists[USBH_EPTYPE_ISO], node) {
		if (!_activate_ep(host, item))
			return;
	}

	list_for_each_entry_safe(item, usbh_ep_t, tmp, &host->ep_pending_lists[USBH_EPTYPE_INT], node) {
		osalDbgCheck(item);
		/* TODO: improve this */
		if (sof && item->xfer.u.frame_counter)
			--item->xfer.u.frame_counter;

		if (item->xfer.u.frame_counter == 0) {
			if (!_activate_ep(host, item))
				return;
			item->xfer.u.frame_counter = item->bInterval;
		}
	}

	if (list_empty(&host->ep_pending_lists[USBH_EPTYPE_ISO])
		&& list_empty(&host->ep_pending_lists[USBH_EPTYPE_INT])) {
		host->otg->GINTMSK &= ~GINTMSK_SOFM;
	} else {
		host->otg->GINTMSK |= GINTMSK_SOFM;
	}
}

static void _purge_queue(USBHDriver *host, struct list_head *list) {
	usbh_ep_t *ep, *tmp;
	list_for_each_entry_safe(ep, usbh_ep_t, tmp, list, node) {
		usbh_urb_t *const urb = _active_urb(ep);
		stm32_hc_management_t *const hcm = ep->xfer.hcm;
		uepwarnf("Abort URB, USBH_URBSTATUS_DISCONNECTED");
		if (hcm) {
			uepwarnf("URB had channel %d assigned, halt_reason = %d", hcm - host->channels, hcm->halt_reason);
			_release_channel(host, hcm);
			_update_urb(ep, hcm->hc->HCTSIZ, urb, FALSE);
		}
		_transfer_completedI(ep, urb, USBH_URBSTATUS_DISCONNECTED);
	}
}

static void _purge_active(USBHDriver *host) {
	_purge_queue(host, &host->ep_active_lists[0]);
	_purge_queue(host, &host->ep_active_lists[1]);
	_purge_queue(host, &host->ep_active_lists[2]);
	_purge_queue(host, &host->ep_active_lists[3]);
}

static void _purge_pending(USBHDriver *host) {
	_purge_queue(host, &host->ep_pending_lists[0]);
	_purge_queue(host, &host->ep_pending_lists[1]);
	_purge_queue(host, &host->ep_pending_lists[2]);
	_purge_queue(host, &host->ep_pending_lists[3]);
}

static uint32_t _write_packet(struct list_head *list, uint32_t space_available) {
	usbh_ep_t *ep;

	uint32_t remaining = 0;

	list_for_each_entry(ep, usbh_ep_t, list, node) {
		if (ep->in || (ep->xfer.hcm->halt_reason != USBH_LLD_HALTREASON_NONE))
			continue;

		int32_t rem = ep->xfer.len - ep->xfer.partial;
		osalDbgCheck(rem >= 0);
		if (rem <= 0)
			continue;

		remaining += rem;

		if (!space_available) {
			if (remaining)
				break;

			continue;
		}

		/* write one packet only */
		if (rem > ep->wMaxPacketSize)
			rem = ep->wMaxPacketSize;

		/* round up to dwords */
		uint32_t words = (rem + 3) / 4;

		if (words > space_available)
			words = space_available;

		space_available -= words;

		uint32_t written = words * 4;
		if ((int32_t)written > rem)
			written = rem;

		volatile uint32_t *dest = ep->xfer.hcm->fifo;
		uint32_t *src = (uint32_t *)ep->xfer.buf;
		uepdbgf("write %d words (%dB), partial=%d", words, written, ep->xfer.partial);
		while (words--) {
			*dest = *src++;
		}

		ep->xfer.buf += written;
		ep->xfer.partial += written;

		remaining -= written;
	}

	return remaining;
}


/*===========================================================================*/
/* API.                                                                      */
/*===========================================================================*/

void usbh_lld_ep_object_init(usbh_ep_t *ep) {
/*			CTRL(IN)	CTRL(OUT)	INT(IN)		INT(OUT)	BULK(IN)	BULK(OUT)	ISO(IN)		ISO(OUT)
 * STALL		si		solo DAT/STAT	si			si			si			si			no			no		ep->type != ISO && (ep->type != CTRL || ctrlphase != SETUP)
 * ACK			si			si			si			si			si			si			no			no		ep->type != ISO
 * NAK			si			si			si			si			si			si			no			no		ep->type != ISO
 * BBERR		si			no			si			no			si			no			si			no		ep->in
 * TRERR		si			si			si			si			si			si			si			no		ep->type != ISO || ep->in
 * DTERR		si			no			si			no			si			no			no			no		ep->type != ISO && ep->in
 * FRMOR		no			no			si			si			no			no			si			si		ep->type = PERIODIC
 */
	USBHDriver *host = ep->device->host;
	uint32_t hcintmsk = HCINTMSK_CHHM | HCINTMSK_XFRCM | HCINTMSK_AHBERRM;

	switch (ep->type) {
	case USBH_EPTYPE_ISO:
		hcintmsk |= HCINTMSK_FRMORM;
		if (ep->in) {
			hcintmsk |= HCINTMSK_TRERRM | HCINTMSK_BBERRM;
		}
		break;
	case USBH_EPTYPE_INT:
		hcintmsk |= HCINTMSK_TRERRM | HCINTMSK_FRMORM | HCINTMSK_STALLM | HCINTMSK_NAKM;
		if (ep->in) {
			hcintmsk |= HCINTMSK_DTERRM | HCINTMSK_BBERRM;
		}
		ep->xfer.u.frame_counter = 1;
		break;
	case USBH_EPTYPE_CTRL:
		hcintmsk |= HCINTMSK_TRERRM | HCINTMSK_STALLM | HCINTMSK_NAKM;
		break;
	case USBH_EPTYPE_BULK:
		hcintmsk |= HCINTMSK_TRERRM | HCINTMSK_STALLM | HCINTMSK_NAKM;
		if (ep->in) {
			hcintmsk |= HCINTMSK_DTERRM | HCINTMSK_BBERRM;
		}
		break;
	default:
		chDbgCheck(0);
	}
	ep->active_list = &host->ep_active_lists[ep->type];
	ep->pending_list = &host->ep_pending_lists[ep->type];
	INIT_LIST_HEAD(&ep->urb_list);
	INIT_LIST_HEAD(&ep->node);

	ep->hcintmsk = hcintmsk;
	ep->hcchar = HCCHAR_CHENA
			| HCCHAR_DAD(ep->device->address)
			| HCCHAR_MCNT(1)
			| HCCHAR_EPTYP(ep->type)
			| ((ep->device->speed == USBH_DEVSPEED_LOW) ? HCCHAR_LSDEV : 0)
			| (ep->in ? HCCHAR_EPDIR : 0)
			| HCCHAR_EPNUM(ep->address)
			| HCCHAR_MPS(ep->wMaxPacketSize);
}

void usbh_lld_ep_open(usbh_ep_t *ep) {
	uepinfof("Open EP");
	ep->status = USBH_EPSTATUS_OPEN;
}

void usbh_lld_ep_close(usbh_ep_t *ep) {
	usbh_urb_t *urb;
	uepinfof("Closing EP...");
	while (!list_empty(&ep->urb_list)) {
		urb = list_first_entry(&ep->urb_list, usbh_urb_t, node);
		uepinfof("Abort URB, USBH_URBSTATUS_DISCONNECTED");
		_usbh_urb_abort_and_waitS(urb, USBH_URBSTATUS_DISCONNECTED);
	}
	uepinfof("Closed");
	ep->status = USBH_EPSTATUS_CLOSED;
}

bool usbh_lld_ep_reset(usbh_ep_t *ep) {
	ep->dt_mask = HCTSIZ_DPID_DATA0;
	return TRUE;
}

void usbh_lld_urb_submit(usbh_urb_t *urb) {
	usbh_ep_t *const ep = urb->ep;
	USBHDriver *const host = ep->device->host;

	if (!(host->otg->HPRT & HPRT_PENA)) {
		uepwarnf("Can't submit URB, port disabled");
		_usbh_urb_completeI(urb, USBH_URBSTATUS_DISCONNECTED);
		return;
	}

	/* add the URB to the EP's queue */
	list_add_tail(&urb->node, &ep->urb_list);

	/* check if the EP wasn't in any queue (pending nor active) */
	if (list_empty(&ep->node)) {

		/* add the EP to the pending queue */
		_move_to_pending_queue(ep);

		if (usbhEPIsPeriodic(ep)) {
			host->otg->GINTMSK |= GINTMSK_SOFM;
		} else {
			/* try to queue non-periodic transfers */
			_try_commit_np(ep->device->host);
		}
	}
}

/* usbh_lld_urb_abort may require a reschedule if called from a S-locked state */
bool usbh_lld_urb_abort(usbh_urb_t *urb, usbh_urbstatus_t status) {
	osalDbgCheck(usbhURBIsBusy(urb));

	usbh_ep_t *const ep = urb->ep;
	osalDbgCheck(ep);
	stm32_hc_management_t *const hcm = ep->xfer.hcm;

	if ((hcm != NULL) && (urb == _active_urb(ep))) {
		/* This URB is active (channel assigned, top of the EP's URB list) */

		if (hcm->halt_reason == USBH_LLD_HALTREASON_NONE) {
			/* The channel is not being halted */
			uepinfof("usbh_lld_urb_abort: channel is not being halted");
			urb->status = status;
			_halt_channel(ep->device->host, hcm, USBH_LLD_HALTREASON_ABORT);
		} else {
			/* The channel is being halted, so we can't re-halt it. The CHH interrupt will
			 * be in charge of completing the transfer, but the URB will not have the specified status.
			 */
			uepinfof("usbh_lld_urb_abort: channel is being halted");
		}
		return FALSE;
	}

	/* This URB is inactive, we can cancel it now */
	uepinfof("usbh_lld_urb_abort: URB is not active");
	_transfer_completedI(ep, urb, status);

	return TRUE;
}


/*===========================================================================*/
/* Channel Interrupts.                                                       */
/*===========================================================================*/

//CTRL(IN)	CTRL(OUT)	INT(IN)		INT(OUT)	BULK(IN)	BULK(OUT)	ISO(IN)		ISO(OUT)
//	si			si			si			si			si			si			no			no		ep->type != ISO && !ep->in
static inline void _ack_int(USBHDriver *host, stm32_hc_management_t *hcm, stm32_otg_host_chn_t *hc) {
	(void)host;
	usbh_ep_t *const ep = hcm->ep;
	osalDbgAssert(ep->type != USBH_EPTYPE_ISO, "ACK should not happen in ISO endpoints");
	ep->xfer.error_count = 0;
	hc->HCINTMSK &= ~HCINTMSK_ACKM;
	uepdbgf("ACK");
}

//CTRL(IN)	CTRL(OUT)	INT(IN)		INT(OUT)	BULK(IN)	BULK(OUT)	ISO(IN)		ISO(OUT)
//	si			no			si			no			si			no			no			no		ep->type != ISO && ep->in
static inline void _dterr_int(USBHDriver *host, stm32_hc_management_t *hcm, stm32_otg_host_chn_t *hc) {
	(void)host;
	usbh_ep_t *const ep = hcm->ep;
	osalDbgAssert(ep->in && (ep->type != USBH_EPTYPE_ISO), "DTERR should not happen in OUT or ISO endpoints");
#if 0
	hc->HCINTMSK &= ~(HCINTMSK_DTERRM | HCINTMSK_ACKM);
	ep->xfer.error_count = 0;
	_halt_channel(host, hcm, USBH_LLD_HALTREASON_ERROR);
#else
	/* restart directly, no need to halt it in this case */
	ep->xfer.error_count = 0;
	hc->HCINTMSK &= ~HCINTMSK_ACKM;
	hc->HCCHAR |= HCCHAR_CHENA;
#endif
	ueperrf("DTERR");
}

//CTRL(IN)	CTRL(OUT)	INT(IN)		INT(OUT)	BULK(IN)	BULK(OUT)	ISO(IN)		ISO(OUT)
//	si			no			si			no			si			no			si			no		ep->in
static inline void _bberr_int(USBHDriver *host, stm32_hc_management_t *hcm, stm32_otg_host_chn_t *hc) {
	usbh_ep_t *const ep = hcm->ep;
	osalDbgAssert(ep->in, "BBERR should not happen in OUT endpoints");
	hc->HCINTMSK &= ~HCINTMSK_BBERRM;
	ep->xfer.error_count = 3;
	_halt_channel(host, hcm, USBH_LLD_HALTREASON_ERROR);
	ueperrf("BBERR");
}

///CTRL(IN)	CTRL(OUT)	INT(IN)		INT(OUT)	BULK(IN)	BULK(OUT)	ISO(IN)		ISO(OUT)
//	si			si			si			si			si			si			si			no		ep->type != ISO || ep->in
static inline void _trerr_int(USBHDriver *host, stm32_hc_management_t *hcm, stm32_otg_host_chn_t *hc) {
	usbh_ep_t *const ep = hcm->ep;
	osalDbgAssert(ep->in || (ep->type != USBH_EPTYPE_ISO), "TRERR should not happen in ISO OUT endpoints");
	hc->HCINTMSK &= ~HCINTMSK_TRERRM;
	++ep->xfer.error_count;
	_halt_channel(host, hcm, USBH_LLD_HALTREASON_ERROR);
	ueperrf("TRERR");
}

//CTRL(IN)	CTRL(OUT)	INT(IN)		INT(OUT)	BULK(IN)	BULK(OUT)	ISO(IN)		ISO(OUT)
//	no			no			si			si			no			no			si			si		ep->type = PERIODIC
static inline void _frmor_int(USBHDriver *host, stm32_hc_management_t *hcm, stm32_otg_host_chn_t *hc) {
	usbh_ep_t *const ep = hcm->ep;
	osalDbgAssert(usbhEPIsPeriodic(ep), "FRMOR should not happen in non-periodic endpoints");
	hc->HCINTMSK &= ~HCINTMSK_FRMORM;
	ep->xfer.error_count = 3;
	_halt_channel(host, hcm, USBH_LLD_HALTREASON_ERROR);
	ueperrf("FRMOR");
}

//CTRL(IN)	CTRL(OUT)	INT(IN)		INT(OUT)	BULK(IN)	BULK(OUT)	ISO(IN)		ISO(OUT)
//	si			si			si			si			si			si			no			no		ep->type != ISO
static inline void _nak_int(USBHDriver *host, stm32_hc_management_t *hcm, stm32_otg_host_chn_t *hc) {
	usbh_ep_t *const ep = hcm->ep;
	osalDbgAssert(hcm->ep->type != USBH_EPTYPE_ISO, "NAK should not happen in ISO endpoints");
	if (!ep->in || (ep->type == USBH_EPTYPE_INT)) {
		hc->HCINTMSK &= ~HCINTMSK_NAKM;
		_halt_channel(host, hcm, USBH_LLD_HALTREASON_NAK);
	} else {
		/* restart directly, no need to halt it in this case */
		ep->xfer.error_count = 0;
		hc->HCINTMSK &= ~HCINTMSK_ACKM;
		hc->HCCHAR |= HCCHAR_CHENA;
	}
	uepdbgf("NAK");
}

//CTRL(IN)	CTRL(OUT)	INT(IN)		INT(OUT)	BULK(IN)	BULK(OUT)	ISO(IN)		ISO(OUT)
//	si		solo DAT/STAT	si			si			si			si			no			no		ep->type != ISO && (ep->type != CTRL || ctrlphase != SETUP)
static inline void _stall_int(USBHDriver *host, stm32_hc_management_t *hcm, stm32_otg_host_chn_t *hc) {
	osalDbgAssert(hcm->ep->type != USBH_EPTYPE_ISO, "STALL should not happen in ISO endpoints");
	hc->HCINTMSK &= ~HCINTMSK_STALLM;
	_halt_channel(host, hcm, USBH_LLD_HALTREASON_STALL);
#if USBH_DEBUG_ENABLE && USBH_LLD_DEBUG_ENABLE_WARNINGS
	usbh_ep_t *const ep = hcm->ep;
	uepwarnf("STALL");
#endif
}

static void _complete_bulk_int(USBHDriver *host, stm32_hc_management_t *hcm, usbh_ep_t *ep, usbh_urb_t *urb, uint32_t hctsiz) {
	_release_channel(host, hcm);
	_save_dt_mask(ep, hctsiz);
	if (_update_urb(ep, hctsiz, urb, TRUE)) {
		uepdbgf("done");
		_transfer_completedI(ep, urb, USBH_URBSTATUS_OK);
	} else {
		osalDbgCheck(urb->requestedLength > 0x7FFFF);
		uepwarnf("incomplete");
		_move_to_pending_queue(ep);
	}
	if (usbhEPIsPeriodic(ep)) {
		_try_commit_p(host, FALSE);
	} else {
		_try_commit_np(host);
	}
}

static void _complete_control(USBHDriver *host, stm32_hc_management_t *hcm, usbh_ep_t *ep, usbh_urb_t *urb, uint32_t hctsiz) {
	osalDbgCheck(ep->xfer.u.ctrl_phase != USBH_LLD_CTRLPHASE_SETUP);

	_release_channel(host, hcm);
	if (ep->xfer.u.ctrl_phase == USBH_LLD_CTRLPHASE_DATA) {
		if (_update_urb(ep, hctsiz, urb, TRUE)) {
			uepdbgf("DATA done");
			ep->xfer.u.ctrl_phase = USBH_LLD_CTRLPHASE_STATUS;
			ep->in = !ep->in;
		} else {
			osalDbgCheck(urb->requestedLength > 0x7FFFF);
			uepwarnf("DATA incomplete");
			_save_dt_mask(ep, hctsiz);
		}
		_move_to_pending_queue(ep);
	} else {
		osalDbgCheck(ep->xfer.u.ctrl_phase == USBH_LLD_CTRLPHASE_STATUS);
		uepdbgf("STATUS done");
		_transfer_completedI(ep, urb, USBH_URBSTATUS_OK);
	}
	_try_commit_np(host);
}

static void _complete_control_setup(USBHDriver *host, stm32_hc_management_t *hcm, usbh_ep_t *ep, usbh_urb_t *urb) {
	_release_channel(host, hcm);
	if (urb->requestedLength) {
		uepdbgf("SETUP done -> DATA");
		ep->xfer.u.ctrl_phase = USBH_LLD_CTRLPHASE_DATA;
		ep->in = *((uint8_t *)urb->setup_buff) & 0x80 ? TRUE : FALSE;
		ep->dt_mask = HCTSIZ_DPID_DATA1;
		ep->xfer.error_count = 0;
	} else {
		uepdbgf("SETUP done -> STATUS");
		ep->in = TRUE;
		ep->xfer.u.ctrl_phase = USBH_LLD_CTRLPHASE_STATUS;
	}
	_move_to_pending_queue(ep);
	_try_commit_np(host);
}

static void _complete_iso(USBHDriver *host, stm32_hc_management_t *hcm, usbh_ep_t *ep, usbh_urb_t *urb, uint32_t hctsiz) {
	uepdbgf("done");
	_release_channel(host, hcm);
	_update_urb(ep, hctsiz, urb, TRUE);
	_transfer_completedI(ep, urb, USBH_URBSTATUS_OK);
	_try_commit_p(host, FALSE);
}

static inline void _xfrc_int(USBHDriver *host, stm32_hc_management_t *hcm, stm32_otg_host_chn_t *hc) {
	usbh_ep_t *const ep = hcm->ep;
	usbh_urb_t *const urb = _active_urb(ep);
	osalDbgCheck(urb);
	uint32_t hctsiz = hc->HCTSIZ;

	hc->HCINTMSK &= ~HCINTMSK_XFRCM;

	switch (ep->type) {
	case USBH_EPTYPE_CTRL:
		if (ep->xfer.u.ctrl_phase == USBH_LLD_CTRLPHASE_SETUP) {
			_complete_control_setup(host, hcm, ep, urb);
		} else if (ep->in) {
			_halt_channel(host, hcm, USBH_LLD_HALTREASON_XFRC);
		} else {
			_complete_control(host, hcm, ep, urb, hctsiz);
		}
		break;

	case USBH_EPTYPE_BULK:
		if (ep->in) {
			_halt_channel(host, hcm, USBH_LLD_HALTREASON_XFRC);
		} else {
			_complete_bulk_int(host, hcm, ep, urb, hctsiz);
		}
		break;

	case USBH_EPTYPE_INT:
		if (ep->in && (hctsiz & HCTSIZ_PKTCNT_MASK)) {
			_halt_channel(host, hcm, USBH_LLD_HALTREASON_XFRC);
		} else {
			_complete_bulk_int(host, hcm, ep, urb, hctsiz);
		}
		break;

	case USBH_EPTYPE_ISO:
		if (ep->in && (hctsiz & HCTSIZ_PKTCNT_MASK)) {
			_halt_channel(host, hcm, USBH_LLD_HALTREASON_XFRC);
		} else {
			_complete_iso(host, hcm, ep, urb, hctsiz);
		}
		break;
	}
}

static inline void _chh_int(USBHDriver *host, stm32_hc_management_t *hcm, stm32_otg_host_chn_t *hc) {

	usbh_ep_t *const ep = hcm->ep;
	usbh_urb_t *const urb = _active_urb(ep);
	osalDbgCheck(urb);
	uint32_t hctsiz = hc->HCTSIZ;
	usbh_lld_halt_reason_t reason = hcm->halt_reason;

	//osalDbgCheck(reason != USBH_LLD_HALTREASON_NONE);
	if (reason == USBH_LLD_HALTREASON_NONE) {
		uwarnf("\tCHH: ch=%d, USBH_LLD_HALTREASON_NONE", hcm - host->channels);
		return;
	}

	if (reason == USBH_LLD_HALTREASON_XFRC) {
		osalDbgCheck(ep->in);
		switch (ep->type) {
		case USBH_EPTYPE_CTRL:
			_complete_control(host, hcm, ep, urb, hctsiz);
			break;
		case USBH_EPTYPE_BULK:
		case USBH_EPTYPE_INT:
			_complete_bulk_int(host, hcm, ep, urb, hctsiz);
			break;
		case USBH_EPTYPE_ISO:
			_complete_iso(host, hcm, ep, urb, hctsiz);
			break;
		}
	} else {
		_release_channel(host, hcm);
		_save_dt_mask(ep, hctsiz);
		bool done = _update_urb(ep, hctsiz, urb, FALSE);

		switch (reason) {
		case USBH_LLD_HALTREASON_NAK:
			if ((ep->type == USBH_EPTYPE_INT) && ep->in) {
				_transfer_completedI(ep, urb, USBH_URBSTATUS_TIMEOUT);
			} else {
				ep->xfer.error_count = 0;
				_move_to_pending_queue(ep);
			}
			break;

		case USBH_LLD_HALTREASON_STALL:
			if (ep->type == USBH_EPTYPE_CTRL) {
				if (ep->xfer.u.ctrl_phase == USBH_LLD_CTRLPHASE_SETUP) {
					ueperrf("Faulty device: STALLed SETUP phase");
				}
			} else {
				ep->status = USBH_EPSTATUS_HALTED;
			}
			_transfer_completedI(ep, urb, USBH_URBSTATUS_STALL);
			break;

		case USBH_LLD_HALTREASON_ERROR:
			if ((ep->type == USBH_EPTYPE_ISO) || done || (ep->xfer.error_count >= 3)) {
				_transfer_completedI(ep, urb, USBH_URBSTATUS_ERROR);
			} else {
				ueperrf("err=%d, done=%d, retry", ep->xfer.error_count, done);
				_move_to_pending_queue(ep);
			}
			break;

		case USBH_LLD_HALTREASON_ABORT:
			uepwarnf("Abort");
			_transfer_completedI(ep, urb, urb->status);
			break;

		default:
			osalDbgCheck(0);
			break;
		}

		if (usbhEPIsPeriodic(ep)) {
			_try_commit_p(host, FALSE);
		} else {
			_try_commit_np(host);
		}
	}
}

static void _hcint_n_int(USBHDriver *host, uint8_t chn) {

	stm32_hc_management_t *const hcm = &host->channels[chn];
	stm32_otg_host_chn_t *const hc = hcm->hc;

	uint32_t hcint = hc->HCINT;
	hcint &= hc->HCINTMSK;
	hc->HCINT = hcint;

	osalDbgCheck((hcint & HCINTMSK_AHBERRM) == 0);
	osalDbgCheck(hcm->ep);

	if (hcint & HCINTMSK_STALLM)
		_stall_int(host, hcm, hc);
	if (hcint & HCINTMSK_NAKM)
		_nak_int(host, hcm, hc);
	if (hcint & HCINTMSK_ACKM)
		_ack_int(host, hcm, hc);
	if (hcint & HCINTMSK_TRERRM)
		_trerr_int(host, hcm, hc);
	if (hcint & HCINTMSK_BBERRM)
		_bberr_int(host, hcm, hc);
	if (hcint & HCINTMSK_FRMORM)
		_frmor_int(host, hcm, hc);
	if (hcint & HCINTMSK_DTERRM)
		_dterr_int(host, hcm, hc);
	if (hcint & HCINTMSK_XFRCM)
		_xfrc_int(host, hcm, hc);
	if (hcint & HCINTMSK_CHHM)
		_chh_int(host, hcm, hc);
}

static inline void _hcint_int(USBHDriver *host) {
	uint32_t haint;

	haint = host->otg->HAINT;
	haint &= host->otg->HAINTMSK;

#if USBH_DEBUG_ENABLE && USBH_LLD_DEBUG_ENABLE_ERRORS
	if (!haint) {
		uint32_t a, b;
		a = host->otg->HAINT;
		b = host->otg->HAINTMSK;
		uerrf("HAINT=%08x, HAINTMSK=%08x", a, b);
		return;
	}
#endif

#if 1	//channel lookup loop
	uint8_t i;
	for (i = 0; haint && (i < host->channels_number); i++) {
		if (haint & (1 << i)) {
			_hcint_n_int(host, i);
			haint &= ~(1 << i);
		}
	}
#else	//faster calculation, with __CLZ (count leading zeroes)
	while (haint) {
		uint8_t chn = (uint8_t)(31 - __CLZ(haint));
		osalDbgAssert(chn < host->channels_number, "what?");
		haint &= ~host->channels[chn].haintmsk;
		_hcint_n_int(host, chn);
	}
#endif
}


/*===========================================================================*/
/* Host interrupts.                                                          */
/*===========================================================================*/
static inline void _sof_int(USBHDriver *host) {

	/* this is part of the workaround to the LS bug in the OTG core */
#undef HPRT_PLSTS_MASK
#define HPRT_PLSTS_MASK (3U<<10)
	if (host->check_ls_activity) {
		stm32_otg_t *const otg = host->otg;
		uint16_t remaining = otg->HFNUM >> 16;
		if (remaining < 5975) {
			uwarnf("LS: ISR called too late (time=%d)", 6000 - remaining);
			return;
		}
		/* 15us loop during which we check if the core generates an actual keep-alive
		 * (or activity other than idle) on the DP/DM lines. After 15us, we abort
		 * the loop and wait for the next SOF. If no activity is detected, the upper
		 * layer will time-out waiting for the reset to happen, and the port will remain
		 * enabled (though in a dumb state). This will be detected on the next port reset
		 * request and the OTG core will be reset. */
		for (;;) {
			uint32_t line_status = otg->HPRT & HPRT_PLSTS_MASK;
			remaining = otg->HFNUM >> 16;
			if (!(otg->HPRT & HPRT_PENA)) {
				uwarn("LS: Port disabled");
				return;
			}
			if (line_status != HPRT_PLSTS_DM) {
				/* success; report that the port is enabled */
				uinfof("LS: activity detected, line=%d, time=%d", line_status >> 10,  6000 - remaining);
				host->check_ls_activity = FALSE;
				otg->GINTMSK = (otg->GINTMSK & ~GINTMSK_SOFM) | (GINTMSK_HCM | GINTMSK_RXFLVLM);
				host->rootport.lld_status |= USBH_PORTSTATUS_ENABLE;
				host->rootport.lld_c_status |= USBH_PORTSTATUS_C_ENABLE;
				return;
			}
			if (remaining < 5910) {
				udbg("LS: No activity detected");
				return;
			}
		}
	}

	/* real SOF interrupt */
	udbg("SOF");
	_try_commit_p(host, TRUE);
}

static inline void _rxflvl_int(USBHDriver *host) {

	stm32_otg_t *const otg = host->otg;

	otg->GINTMSK &= ~GINTMSK_RXFLVLM;
	while (otg->GINTSTS & GINTSTS_RXFLVL) {
		uint32_t grxstsp = otg->GRXSTSP;
		osalDbgCheck((grxstsp & GRXSTSP_CHNUM_MASK) < host->channels_number);
		stm32_hc_management_t *const hcm = &host->channels[grxstsp & GRXSTSP_CHNUM_MASK];
		uint32_t hctsiz = hcm->hc->HCTSIZ;

		if ((grxstsp & GRXSTSP_PKTSTS_MASK) == GRXSTSP_PKTSTS(2)) {
			/* 0010: IN data packet received */
			usbh_ep_t *const ep = hcm->ep;
			osalDbgCheck(ep);

			/* restart the channel ASAP */
			if (hctsiz & HCTSIZ_PKTCNT_MASK) {
#if CH_DBG_ENABLE_CHECKS
				if (usbhEPIsPeriodic(ep)) {
					osalDbgCheck(host->otg->HPTXSTS & HPTXSTS_PTXQSAV_MASK);
				} else {
					osalDbgCheck(host->otg->HNPTXSTS & HPTXSTS_PTXQSAV_MASK);
				}
#endif
				hcm->hc->HCCHAR |= HCCHAR_CHENA;
			}

			uepdbgf("RXFLVL rx=%dB, rem=%dB (%dpkts)",
					(grxstsp & GRXSTSP_BCNT_MASK) >> 4,
					(hctsiz & HCTSIZ_XFRSIZ_MASK),
					(hctsiz & HCTSIZ_PKTCNT_MASK) >> 19);

			/* Read */
			uint32_t *dest = (uint32_t *)ep->xfer.buf;
			volatile uint32_t *const src = hcm->fifo;

			uint32_t bcnt = (grxstsp & GRXSTSP_BCNT_MASK) >> GRXSTSP_BCNT_OFF;
			osalDbgCheck(bcnt + ep->xfer.partial <= ep->xfer.len);

			//TODO: optimize this
			uint32_t words = bcnt / 4;
			uint8_t bytes = bcnt & 3;
			while (words--) {
				*dest++ = *src;
			}
			if (bytes) {
				uint32_t r = *src;
				uint8_t *bsrc = (uint8_t *)&r;
				uint8_t *bdest = (uint8_t *)dest;
				do {
					*bdest++ = *bsrc++;
				} while (--bytes);
			}

			ep->xfer.buf += bcnt;
			ep->xfer.partial += bcnt;

#if 0 //STM32_USBH_CHANNELS_NP > 1
			/* check bug */
			if (hctsiz & HCTSIZ_PKTCNT_MASK) {
				uint32_t pkt = (hctsiz & HCTSIZ_PKTCNT_MASK) >> 19;
				uint32_t siz = (hctsiz & HCTSIZ_XFRSIZ_MASK);
				if (pkt * ep->wMaxPacketSize != siz) {
					ueperrf("whatttt???");
				}
			}
#endif

#if USBH_DEBUG_ENABLE && USBH_LLD_DEBUG_ENABLE_ERRORS
		} else {
			/* 0011: IN transfer completed (triggers an interrupt)
			 * 0101: Data toggle error (triggers an interrupt)
			 * 0111: Channel halted (triggers an interrupt)
			 */
			switch (grxstsp & GRXSTSP_PKTSTS_MASK) {
			case GRXSTSP_PKTSTS(3):
			case GRXSTSP_PKTSTS(5):
			case GRXSTSP_PKTSTS(7):
				break;
			default:
				uerrf("\tRXFLVL: ch=%d, UNK=%d", grxstsp & GRXSTSP_CHNUM_MASK, (grxstsp & GRXSTSP_PKTSTS_MASK) >> 17);
				break;
			}
#endif
		}
	}
	otg->GINTMSK |= GINTMSK_RXFLVLM;
}

static inline void _nptxfe_int(USBHDriver *host) {
	uint32_t rem;
	stm32_otg_t *const otg = host->otg;

	rem = _write_packet(&host->ep_active_lists[USBH_EPTYPE_CTRL],
			otg->HNPTXSTS & HPTXSTS_PTXFSAVL_MASK);

	rem += _write_packet(&host->ep_active_lists[USBH_EPTYPE_BULK],
			otg->HNPTXSTS & HPTXSTS_PTXFSAVL_MASK);

	if (!rem)
		otg->GINTMSK &= ~GINTMSK_NPTXFEM;

}

static inline void _ptxfe_int(USBHDriver *host) {
	uint32_t rem;
	stm32_otg_t *const otg = host->otg;

	rem = _write_packet(&host->ep_active_lists[USBH_EPTYPE_ISO],
			otg->HPTXSTS & HPTXSTS_PTXFSAVL_MASK);

	rem += _write_packet(&host->ep_active_lists[USBH_EPTYPE_INT],
			otg->HPTXSTS & HPTXSTS_PTXFSAVL_MASK);

	if (!rem)
		otg->GINTMSK &= ~GINTMSK_PTXFEM;
}

static void _disable(USBHDriver *host) {
	host->rootport.lld_status &= ~(USBH_PORTSTATUS_CONNECTION | USBH_PORTSTATUS_ENABLE);
	host->rootport.lld_c_status |= USBH_PORTSTATUS_C_CONNECTION | USBH_PORTSTATUS_C_ENABLE;

	_purge_active(host);
	_purge_pending(host);

	host->otg->GINTMSK &= ~(GINTMSK_HCM | GINTMSK_RXFLVLM);
}

static inline void _discint_int(USBHDriver *host) {
	uinfo("DISCINT: Port disconnection detected");
	_disable(host);
}

static inline void _hprtint_int(USBHDriver *host) {
	stm32_otg_t *const otg = host->otg;
	uint32_t hprt = otg->HPRT;

	/* note: writing PENA = 1 actually disables the port */
	uint32_t hprt_clr = hprt & ~(HPRT_PENA | HPRT_PCDET | HPRT_PENCHNG | HPRT_POCCHNG);

	if (hprt & HPRT_PCDET) {
		hprt_clr |= HPRT_PCDET;
		if (hprt & HPRT_PCSTS) {
			uinfo("\tHPRT: Port connection detected");
			host->rootport.lld_status |= USBH_PORTSTATUS_CONNECTION;
			host->rootport.lld_c_status |= USBH_PORTSTATUS_C_CONNECTION;
		}
	}

	if (hprt & HPRT_PENCHNG) {
		hprt_clr |= HPRT_PENCHNG;
		if (hprt & HPRT_PENA) {
			uinfo("\tHPRT: Port enabled");
			host->rootport.lld_status &= ~(USBH_PORTSTATUS_HIGH_SPEED | USBH_PORTSTATUS_LOW_SPEED);

			/* configure FIFOs */
#define HNPTXFSIZ						DIEPTXF0
#if STM32_USBH_USE_OTG1
#if STM32_USBH_USE_OTG2
			if (&USBHD1 == host)
#endif
			{
				otg->GRXFSIZ = GRXFSIZ_RXFD(STM32_OTG_FS_RXFIFO_SIZE / 4);
				otg->HNPTXFSIZ = HPTXFSIZ_PTXSA(STM32_OTG_FS_RXFIFO_SIZE / 4) | HPTXFSIZ_PTXFD(STM32_OTG_FS_NPTXFIFO_SIZE / 4);
				otg->HPTXFSIZ = HPTXFSIZ_PTXSA((STM32_OTG_FS_RXFIFO_SIZE / 4) + (STM32_OTG_FS_NPTXFIFO_SIZE / 4)) | HPTXFSIZ_PTXFD(STM32_OTG_FS_PTXFIFO_SIZE / 4);
			}
#endif
#if STM32_USBH_USE_OTG2
#if STM32_USBH_USE_OTG1
			if (&USBHD2 == host)
#endif
			{
				otg->GRXFSIZ = GRXFSIZ_RXFD(STM32_OTG_HS_RXFIFO_SIZE / 4);
				otg->HNPTXFSIZ = HPTXFSIZ_PTXSA(STM32_OTG_HS_RXFIFO_SIZE / 4) | HPTXFSIZ_PTXFD(STM32_OTG_HS_NPTXFIFO_SIZE / 4);
				otg->HPTXFSIZ = HPTXFSIZ_PTXSA((STM32_OTG_HS_RXFIFO_SIZE / 4) + (STM32_OTG_HS_NPTXFIFO_SIZE / 4)) | HPTXFSIZ_PTXFD(STM32_OTG_HS_PTXFIFO_SIZE / 4);
			}
#endif
#undef HNPTXFSIZ

			/* Make sure the FIFOs are flushed. */
		    otg_txfifo_flush(host, 0x10);
		    otg_rxfifo_flush(host);

		    /* Clear all pending HC Interrupts */
			uint8_t i;
			for (i = 0; i < host->channels_number; i++) {
				otg->hc[i].HCINTMSK = 0;
				otg->hc[i].HCINT = 0xFFFFFFFF;
			}

			/* configure speed */
			if ((hprt & HPRT_PSPD_MASK) == HPRT_PSPD_LS) {
				host->rootport.lld_status |= USBH_PORTSTATUS_LOW_SPEED;
				otg->HFIR = 6000;
				otg->HCFG = (otg->HCFG & ~HCFG_FSLSPCS_MASK) | HCFG_FSLSPCS_6;

				/* Low speed devices connected to the STM32's internal transceiver sometimes
				 * don't behave correctly. Although HPRT reports a port enable, really
				 * no traffic is generated, and the core is non-functional. To avoid
				 * this we won't report the port enable until we are sure that the
				 * port is working. */
				host->check_ls_activity = TRUE;
				otg->GINTMSK |= GINTMSK_SOFM;
			} else {
				otg->HFIR = 48000;
				otg->HCFG = (otg->HCFG & ~HCFG_FSLSPCS_MASK) | HCFG_FSLSPCS_48;
				host->check_ls_activity = FALSE;

				/* enable channel and rx interrupts */
				otg->GINTMSK |= GINTMSK_HCM | GINTMSK_RXFLVLM;
				host->rootport.lld_status |= USBH_PORTSTATUS_ENABLE;
				host->rootport.lld_c_status |= USBH_PORTSTATUS_C_ENABLE;
			}
		} else {
			if (hprt & HPRT_PCSTS) {
				if (hprt & HPRT_POCA) {
					uerr("\tHPRT: Port disabled due to overcurrent");
				} else {
					uerr("\tHPRT: Port disabled due to port babble");
				}
			} else {
				uerr("\tHPRT: Port disabled due to disconnect");
			}
			_disable(host);
		}
	}

	if (hprt & HPRT_POCCHNG) {
		hprt_clr |= HPRT_POCCHNG;
		if (hprt & HPRT_POCA) {
			uerr("\tHPRT: Overcurrent");
			host->rootport.lld_status |= USBH_PORTSTATUS_OVERCURRENT;
		} else {
			udbg("\tHPRT: Clear overcurrent");
			host->rootport.lld_status &= ~USBH_PORTSTATUS_OVERCURRENT;
		}
		host->rootport.lld_c_status |= USBH_PORTSTATUS_C_OVERCURRENT;
	}

	otg->HPRT = hprt_clr;
}

static void usb_lld_serve_interrupt(USBHDriver *host) {
	osalDbgCheck(host && (host->status != USBH_STATUS_STOPPED));

	stm32_otg_t *const otg = host->otg;
	uint32_t gintsts = otg->GINTSTS;

	/* check host mode */
	if (!(gintsts & GINTSTS_CMOD)) {
		uerr("Device mode");
		otg->GINTSTS = gintsts;
		return;
	}

	/* check mismatch */
	osalDbgAssert((gintsts & GINTSTS_MMIS) == 0, "mode mismatch");

	gintsts &= otg->GINTMSK;
	if (!gintsts) {
#if USBH_DEBUG_ENABLE && USBH_DEBUG_ENABLE_WARNINGS
		uint32_t a, b;
		a = otg->GINTSTS;
		b = otg->GINTMSK;
		uwarnf("Masked bits caused an ISR: GINTSTS=%08x, GINTMSK=%08x (unhandled bits=%08x)", a, b, a & ~b);
#endif
		return;
	}

	otg->GINTSTS = gintsts;

	if (gintsts & GINTSTS_SOF)
		_sof_int(host);
	if (gintsts & GINTSTS_RXFLVL)
		_rxflvl_int(host);
	if (gintsts & GINTSTS_HPRTINT)
		_hprtint_int(host);
	if (gintsts & GINTSTS_DISCINT)
		_discint_int(host);
	if (gintsts & GINTSTS_HCINT)
		_hcint_int(host);
	if (gintsts & GINTSTS_NPTXFE)
		_nptxfe_int(host);
	if (gintsts & GINTSTS_PTXFE)
		_ptxfe_int(host);
	if (gintsts & GINTSTS_IPXFR) {
		uerr("IPXFRM");
	}
}


/*===========================================================================*/
/* Interrupt handlers.                                                       */
/*===========================================================================*/

#if STM32_USBH_USE_OTG1
OSAL_IRQ_HANDLER(STM32_OTG1_HANDLER) {
	OSAL_IRQ_PROLOGUE();
	osalSysLockFromISR();
	usb_lld_serve_interrupt(&USBHD1);
	osalSysUnlockFromISR();
	OSAL_IRQ_EPILOGUE();
}
#endif

#if STM32_USBH_USE_OTG2
OSAL_IRQ_HANDLER(STM32_OTG2_HANDLER) {
	OSAL_IRQ_PROLOGUE();
	osalSysLockFromISR();
	usb_lld_serve_interrupt(&USBHD2);
	osalSysUnlockFromISR();
	OSAL_IRQ_EPILOGUE();
}
#endif


/*===========================================================================*/
/* Initialization functions.                                                 */
/*===========================================================================*/
static void otg_core_reset(stm32_otg_t *const otgp) {

  /* Wait AHB idle condition.*/
  while ((otgp->GRSTCTL & GRSTCTL_AHBIDL) == 0)
    ;

  /* Core reset and delay of at least 3 PHY cycles.*/
  otgp->GRSTCTL = GRSTCTL_CSRST;
  osalSysPolledDelayX(12);
  while ((otgp->GRSTCTL & GRSTCTL_CSRST) != 0)
    ;

  osalSysPolledDelayX(18);

  /* Wait AHB idle condition again.*/
  while ((otgp->GRSTCTL & GRSTCTL_AHBIDL) == 0)
    ;
}

static void otg_rxfifo_flush(USBHDriver *usbp) {
  stm32_otg_t *const otgp = usbp->otg;

  otgp->GRSTCTL = GRSTCTL_RXFFLSH;
  while ((otgp->GRSTCTL & GRSTCTL_RXFFLSH) != 0)
    ;
  /* Wait for 3 PHY Clocks.*/
  osalSysPolledDelayX(18);
}

static void otg_txfifo_flush(USBHDriver *usbp, uint32_t fifo) {
  stm32_otg_t *const otgp = usbp->otg;

  otgp->GRSTCTL = GRSTCTL_TXFNUM(fifo) | GRSTCTL_TXFFLSH;
  while ((otgp->GRSTCTL & GRSTCTL_TXFFLSH) != 0)
    ;
  /* Wait for 3 PHY Clocks.*/
  osalSysPolledDelayX(18);
}

static void _init(USBHDriver *host) {
	int i;

	usbhObjectInit(host);

#if STM32_USBH_USE_OTG1
#if STM32_USBH_USE_OTG2
	if (&USBHD1 == host)
#endif
	{
		host->otg = OTG1;
		host->channels_number = OTG1_CHANNELS_NUMBER;
	}
#endif

#if STM32_USBH_USE_OTG2
#if STM32_USBH_USE_OTG1
	if (&USBHD2 == host)
#endif
	{
		host->otg = OTG2;
		host->channels_number = OTG2_CHANNELS_NUMBER;
	}
#endif
	INIT_LIST_HEAD(&host->ch_free[0]);
	INIT_LIST_HEAD(&host->ch_free[1]);
	for (i = 0; i < host->channels_number; i++) {
		host->channels[i].haintmsk = 1 << i;
		host->channels[i].hc = &host->otg->hc[i];
		host->channels[i].fifo = host->otg->FIFO[i];
		if (i < STM32_USBH_CHANNELS_NP) {
			list_add_tail(&host->channels[i].node, &host->ch_free[1]);
		} else {
			list_add_tail(&host->channels[i].node, &host->ch_free[0]);
		}
	}
	for (i = 0; i < 4; i++) {
		INIT_LIST_HEAD(&host->ep_active_lists[i]);
		INIT_LIST_HEAD(&host->ep_pending_lists[i]);
	}
}

void usbh_lld_init(void) {
#if STM32_USBH_USE_OTG1
	_init(&USBHD1);
#endif
#if STM32_USBH_USE_OTG2
	_init(&USBHD2);
#endif
}

void usbh_lld_start(USBHDriver *host) {
	stm32_otg_t *const otgp = host->otg;

	/* Clock activation.*/
#if STM32_USBH_USE_OTG1
	if (&USBHD1 == host)
	{
#if STM32_OTG1_USE_ULPI
		rccEnableOTG1_HSULPI(FALSE);
#endif
		/* OTG FS clock enable and reset.*/
		rccEnableOTG1(FALSE);
		rccResetOTG1();

#if STM32_OTG1_USE_HS
		otgp->GUSBCFG = GUSBCFG_TRDT(TRDT_VALUE_HS);
#else
		otgp->GUSBCFG = GUSBCFG_TRDT(TRDT_VALUE_FS);
#endif

#if STM32_OTG1_USE_ULPI
		otgp->GUSBCFG |= GUSBCFG_SRPCAP | GUSBCFG_HNPCAP;
#if STM32_OTG1_USE_ULPI_VBUS
		otgp->GUSBCFG |= USB_OTG_GUSBCFG_ULPIEVBUSD | USB_OTG_GUSBCFG_ULPIEVBUSI;
#endif
#else
		otgp->GUSBCFG |= GUSBCFG_PHYSEL;
#endif
		otgp->GINTMSK = 0;

		/* Enables IRQ vector.*/
		nvicEnableVector(STM32_OTG1_NUMBER, STM32_USB_OTG1_IRQ_PRIORITY);
	}
#endif

#if STM32_USBH_USE_OTG2
	if (&USBHD2 == host)
	{
#if STM32_OTG2_USE_ULPI
		rccEnableOTG2_HSULPI(FALSE);
#endif

		/* OTG HS clock enable and reset.*/
		rccEnableOTG2(FALSE); // Disable HS clock when cpu is in sleep mode
		rccResetOTG2();

#if STM32_OTG2_USE_HS
		otgp->GUSBCFG = GUSBCFG_TRDT(TRDT_VALUE_HS);
#else
		otgp->GUSBCFG = GUSBCFG_TRDT(TRDT_VALUE_FS);
#endif

#if STM32_OTG2_USE_ULPI
		otgp->GUSBCFG |= GUSBCFG_SRPCAP | GUSBCFG_HNPCAP;
#if STM32_OTG2_USE_ULPI_VBUS
		otgp->GUSBCFG |= USB_OTG_GUSBCFG_ULPIEVBUSD | USB_OTG_GUSBCFG_ULPIEVBUSI;
#endif
#else
		otgp->GUSBCFG |= GUSBCFG_PHYSEL;
#endif
		otgp->GINTMSK = 0;

		/* Enables IRQ vector.*/
		nvicEnableVector(STM32_OTG2_NUMBER, STM32_USB_OTG2_IRQ_PRIORITY);
	}
#endif

	/* Reset after a PHY change */
   	otg_core_reset(otgp);

	otgp->GCCFG = GCCFG_PWRDWN;

	/* Forced host mode. */
	otgp->GUSBCFG |= GUSBCFG_FHMOD;

	/* PHY enabled.*/
	otgp->PCGCCTL = 0;

#if !STM32_OTG1_USE_ULPI && !STM32_OTG2_USE_ULPI
	/* Internal FS PHY activation.*/
#if STM32_OTG_STEPPING == 1
#if defined(BOARD_OTG_NOVBUSSENS)
	otgp->GCCFG = GCCFG_NOVBUSSENS | GCCFG_PWRDWN;
#else
	otgp->GCCFG = GCCFG_PWRDWN;
#endif
#elif STM32_OTG_STEPPING == 2
#if defined(BOARD_OTG_NOVBUSSENS)
	otgp->GCCFG = GCCFG_PWRDWN;
#else
	otgp->GCCFG = (GCCFG_VBDEN | GCCFG_PWRDWN);
#endif
#endif
	/* 48MHz 1.1 PHY.*/
	otgp->HCFG = HCFG_FSLSS | HCFG_FSLSPCS_48;
#endif

	/* Interrupts on FIFOs half empty.*/
	otgp->GAHBCFG = 0;

	otgp->GOTGINT = 0xFFFFFFFF;

	otgp->HPRT |= HPRT_PPWR;

	otg_txfifo_flush(host, 0x10);
	otg_rxfifo_flush(host);

	otgp->GINTSTS = 0xffffffff;
	otgp->GINTMSK = GINTMSK_DISCM | GINTMSK_HPRTM | GINTMSK_MMISM;

	host->rootport.lld_status = USBH_PORTSTATUS_POWER;
	host->rootport.lld_c_status = 0;

	/* Global interrupts enable.*/
	otgp->GAHBCFG |= GAHBCFG_GINTMSK;
}

void usbh_lld_stop(USBHDriver *host) {

	stm32_otg_t *const otgp = host->otg;

	otg_txfifo_flush(host, 0x10);
	otg_rxfifo_flush(host);

	/* OTG power down */
	otgp->GCCFG = GCCFG_PWRDWN;

	/* Clock activation.*/
#if STM32_USBH_USE_OTG1
	if (&USBHD1 == host)
	{
		/* Disable IRQ vector.*/
		nvicDisableVector(STM32_OTG1_NUMBER);

		/* OTG FS clock disable.*/
		rccDisableOTG1();
#if defined(STM32H7XX) && STM32_OTG1_USE_ULPI
		rccDisableOTG1_HSULPI();
#endif
	}
#endif

#if STM32_USBH_USE_OTG2
	if (&USBHD2 == host)
	{
		/* Enables IRQ vector.*/
		nvicDisableVector(STM32_OTG2_NUMBER);

		/* OTG HS clock disable.*/
		rccDisableOTG2(); // Disable HS clock when cpu is in sleep mode
#if ! defined(STM32H7XX) && STM32_OTG2_USE_ULPI
		rccDisableOTG2_HSULPI();
#endif
	}
#endif

}

/*===========================================================================*/
/* Root Hub request handler.                                                 */
/*===========================================================================*/
usbh_urbstatus_t usbh_lld_root_hub_request(USBHDriver *host, uint8_t bmRequestType, uint8_t bRequest,
		uint16_t wvalue, uint16_t windex, uint16_t wlength, uint8_t *buf) {

	uint16_t typereq = (bmRequestType << 8) | bRequest;

	switch (typereq) {
	case ClearHubFeature:
		switch (wvalue) {
		case USBH_HUB_FEAT_C_HUB_LOCAL_POWER:
		case USBH_HUB_FEAT_C_HUB_OVER_CURRENT:
			break;
		default:
			osalDbgAssert(0, "invalid wvalue");
		}
		break;

	case ClearPortFeature:
		osalDbgAssert(windex == 1, "invalid windex");

		osalSysLock();
		switch (wvalue) {
		case USBH_PORT_FEAT_ENABLE:
		case USBH_PORT_FEAT_SUSPEND:
		case USBH_PORT_FEAT_POWER:
			osalDbgAssert(0, "unimplemented");	/* TODO */
			break;

		case USBH_PORT_FEAT_INDICATOR:
			osalDbgAssert(0, "unsupported");
			break;

		case USBH_PORT_FEAT_C_CONNECTION:
			host->rootport.lld_c_status &= ~USBH_PORTSTATUS_C_CONNECTION;
			break;

		case USBH_PORT_FEAT_C_RESET:
			host->rootport.lld_c_status &= ~USBH_PORTSTATUS_C_RESET;
			break;

		case USBH_PORT_FEAT_C_ENABLE:
			host->rootport.lld_c_status &= ~USBH_PORTSTATUS_C_ENABLE;
			break;

		case USBH_PORT_FEAT_C_SUSPEND:
			host->rootport.lld_c_status &= ~USBH_PORTSTATUS_C_SUSPEND;
			break;

		case USBH_PORT_FEAT_C_OVERCURRENT:
			host->rootport.lld_c_status &= ~USBH_PORTSTATUS_C_OVERCURRENT;
			break;

		default:
			osalDbgAssert(0, "invalid wvalue");
			break;
		}
		osalSysUnlock();
		break;

	case GetHubDescriptor:
		osalDbgAssert(0, "unsupported");
		break;

	case GetHubStatus:
		osalDbgCheck(wlength >= 4);
		*(uint32_t *)buf = 0;
		break;

	case GetPortStatus:
		osalDbgAssert(windex == 1, "invalid windex");
		osalDbgCheck(wlength >= 4);
		osalSysLock();
		*(uint32_t *)buf = host->rootport.lld_status | (host->rootport.lld_c_status << 16);
		osalSysUnlock();
		break;

	case SetHubFeature:
		osalDbgAssert(0, "unsupported");
		break;

	case SetPortFeature:
		osalDbgAssert(windex == 1, "invalid windex");

		switch (wvalue) {
		case USBH_PORT_FEAT_TEST:
		case USBH_PORT_FEAT_SUSPEND:
		case USBH_PORT_FEAT_POWER:
			osalDbgAssert(0, "unimplemented");	/* TODO */
			break;

		case USBH_PORT_FEAT_RESET: {
			osalSysLock();
			stm32_otg_t *const otg = host->otg;
			uint32_t hprt;
			otg->PCGCCTL = 0;
			hprt = otg->HPRT;
			if (hprt & HPRT_PENA) {
				/* This can occur when the OTG core doesn't generate traffic
				 * despite reporting a successful por enable. */
				uerr("Detected enabled port; resetting OTG core");
				otg->GAHBCFG = 0;
				osalThreadSleepS(OSAL_MS2I(20));
				usbh_lld_start(host);				/* this effectively resets the core */
				osalThreadSleepS(OSAL_MS2I(100));	/* during this delay, the core generates connect ISR */
				uinfo("OTG reset ended");
				if (otg->HPRT & HPRT_PCSTS) {
					/* if the device is still connected, don't report a C_CONNECTION flag, which would cause
					 * the upper layer to abort enumeration */
					uinfo("Clear connection change flag");
					host->rootport.lld_c_status &= ~USBH_PORTSTATUS_C_CONNECTION;
				}
			}
			/* note: writing PENA = 1 actually disables the port */
			hprt &= ~(HPRT_PSUSP | HPRT_PENA | HPRT_PCDET | HPRT_PENCHNG | HPRT_POCCHNG);
			while ((otg->GRSTCTL & GRSTCTL_AHBIDL) == 0);
			otg->HPRT = hprt | HPRT_PRST;
			osalThreadSleepS(OSAL_MS2I(15));
			otg->HPRT = hprt;
			osalThreadSleepS(OSAL_MS2I(10));
			host->rootport.lld_c_status |= USBH_PORTSTATUS_C_RESET;
			osalSysUnlock();
		} 	break;

		case USBH_PORT_FEAT_INDICATOR:
			osalDbgAssert(0, "unsupported");
			break;

		default:
			osalDbgAssert(0, "invalid wvalue");
			break;
		}
		break;

	default:
		osalDbgAssert(0, "invalid typereq");
		break;
	}

	return USBH_URBSTATUS_OK;
}

uint8_t usbh_lld_roothub_get_statuschange_bitmap(USBHDriver *host) {
	return host->rootport.lld_c_status ? (1 << 1) : 0;
}

#endif
