xhci: dbc: detect disabled DbC and disconnects during enumeraton

DbC may be disabled due to xHC host reset or other issues,
and device may be disconnected before its configured.

Detect these issues in the enabled and connected stages
and print a error message and fall back to a proper state
if possible

Add timeout if DbC is in connected state without moving to
configured for too long, as recommended in xhci FIXME

Signed-off-by: Mathias Nyman <mathias.nyman@linux.intel.com>
diff --git a/drivers/usb/host/xhci-dbgcap.c b/drivers/usb/host/xhci-dbgcap.c
index 31f6d62..a5e6bf0 100644
--- a/drivers/usb/host/xhci-dbgcap.c
+++ b/drivers/usb/host/xhci-dbgcap.c
@@ -649,6 +649,7 @@ static void xhci_dbc_stop(struct xhci_dbc *dbc)
 		mutex_unlock(&dbc->enable_mutex);
 		return;
 	case DS_CONFIGURED:
+		dbc->state = DS_DISABLING;
 		xhci_dbc_flush_requests(dbc);
 		spin_unlock(&dbc->lock);
 
@@ -656,6 +657,7 @@ static void xhci_dbc_stop(struct xhci_dbc *dbc)
 			dbc->driver->disconnect(dbc);
 		break;
 	default:
+		dbc->state = DS_DISABLING;
 		spin_unlock(&dbc->lock);
 		break;
 	}
@@ -831,27 +833,46 @@ static enum evtreturn xhci_dbc_do_handle_events(struct xhci_dbc *dbc)
 	/* DbC state machine: */
 	switch (dbc->state) {
 	case DS_DISABLED:
+	case DS_DISABLING:
 	case DS_INITIALIZED:
 
 		return EVT_ERR;
 	case DS_ENABLED:
 		portsc = readl(&dbc->regs->portsc);
+		ctrl = readl(&dbc->regs->control);
+
 		if (portsc & DBC_PORTSC_CONN_STATUS) {
+			dbc->conn_timestamp = jiffies;
 			dbc->state = DS_CONNECTED;
 			dev_info(dbc->dev, "DbC connected\n");
+		} else if (!(ctrl & DBC_CTRL_DBC_ENABLE)) {
+			dev_err(dbc->dev, "unexpected DbC disable, xHC reset?\n");
 		}
 
 		return EVT_DONE;
 	case DS_CONNECTED:
 		ctrl = readl(&dbc->regs->control);
+		portsc = readl(&dbc->regs->portsc);
 		if (ctrl & DBC_CTRL_DBC_RUN) {
 			dbc->state = DS_CONFIGURED;
 			dev_info(dbc->dev, "DbC configured\n");
-			portsc = readl(&dbc->regs->portsc);
 			writel(portsc, &dbc->regs->portsc);
 			return EVT_GSER;
 		}
 
+		if (!(portsc & DBC_PORTSC_CONN_STATUS)) {
+			/* covers DCE == 0 as it also sets CONN_STATUS to 0 */
+			dev_warn(dbc->dev, "DbC lost connection during enum\n");
+			dbc->state = DS_ENABLED;
+			return EVT_DONE;
+		} else if (time_is_before_jiffies(dbc->conn_timestamp +
+						  msecs_to_jiffies(500))) {
+			dev_err(dbc->dev, "DbC enum stuck\n");
+			dev_err(dbc->dev, "CTRL %x, PORTSC %x\n", ctrl, portsc);
+			/* hack to avoid a lot of dev_err() spam */
+			dbc->conn_timestamp = jiffies;
+		}
+
 		return EVT_DONE;
 	case DS_CONFIGURED:
 		/* Handle cable unplug event: */
diff --git a/drivers/usb/host/xhci-dbgcap.h b/drivers/usb/host/xhci-dbgcap.h
index 0522657..100f5f4 100644
--- a/drivers/usb/host/xhci-dbgcap.h
+++ b/drivers/usb/host/xhci-dbgcap.h
@@ -77,6 +77,7 @@ struct dbc_str_descs {
 
 enum dbc_state {
 	DS_DISABLED = 0,
+	DS_DISABLING,
 	DS_INITIALIZED,
 	DS_ENABLED,
 	DS_CONNECTED,
@@ -145,6 +146,7 @@ struct xhci_dbc {
 	struct delayed_work		event_work;
 	unsigned int			poll_interval;	/* ms */
 	unsigned long			xfer_timestamp;
+	unsigned long			conn_timestamp;
 	unsigned			resume_required:1;
 	struct dbc_ep			eps[2];