SchOS/kernel/drivers/usb/hub/usb_hub.c
Nexoniarz c65bfa3fa3 boot: driver chatter goes to the log; the console clears its margins
ACPI, DMI, SMP, xHCI, USB, I2C, NVMe, r8169 and ramdisk progress lines are
kinfo now: in dmesg and on a verbose boot, off the screen of a working
machine. Failures stay cprintf.

vt_repaint also paints the strip right of the last column and below the
last row, which kept the bottom of the X desktop after Ctrl+Alt+F-key.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
2026-10-25 01:53:46 +02:00

315 lines
9.9 KiB
C

// USB hub class driver.
//
// A hub is what makes USB a tree rather than a handful of sockets, and
// nothing about driving one is special: every operation is an ordinary
// control request sent to the hub as a device. "Reset port 3" is a
// SET_FEATURE(PORT_RESET, wIndex=3). The hub has an interrupt endpoint too,
// which reports which ports have changed. At boot it is enough to walk every
// port and look; after that, the endpoint is how a device plugged into the
// hub, or pulled out of it, is noticed at all. See usb_hub_poll().
//
// What *is* special is what the host controller has to be told. xHCI does not
// discover topology; it is informed of it. A device three hubs deep is
// addressed by a route string naming the port taken at each tier, and a
// slow device behind a fast hub is scheduled against that hub's transaction
// translator rather than against the device. So this driver's real job is
// less "operate the hub" than "tell the controller what shape the tree is".
#include "core/types.h"
#include "core/defs.h"
#include "core/param.h"
#include "core/spinlock.h"
#include "core/dma.h"
#include "dev/usb.h"
#define MAX_PORTS 15 // per hub; more than any real bus hub has
#define NHUB 8
// Port change bits, the second half of GET_STATUS(port).
#define PORTCHG_CONNECTION (1 << 0)
#define PORTCHG_OTHERS 0x1E // enable, suspend, over-current, reset
struct hub {
int in_use;
struct usb_dev *dev;
int nports;
void *scratch; // DMA, for GET_STATUS
volatile uint32 change; // bit n: port n has changed; 0 is hub
};
static struct hub hubs[NHUB];
static int
hub_request(struct usb_dev *dev, int type, int req, int value, int index,
void *buf, int len)
{
struct usb_setup s;
s.bmRequestType = type;
s.bRequest = req;
s.wValue = value;
s.wIndex = index;
s.wLength = len;
return dev->ops->control(dev, &s, buf, len);
}
static int
port_set_feature(struct usb_dev *dev, int port, int feature)
{
return hub_request(dev, USB_DIR_OUT | USB_TYPE_CLASS | USB_RECIP_OTHER,
USB_REQ_SET_FEATURE, feature, port, 0, 0);
}
static int
port_clear_feature(struct usb_dev *dev, int port, int feature)
{
return hub_request(dev, USB_DIR_OUT | USB_TYPE_CLASS | USB_RECIP_OTHER,
USB_REQ_CLEAR_FEATURE, feature, port, 0, 0);
}
// Returns wPortStatus, or -1. The change bits are in the upper half and are
// deliberately dropped here -- they are acknowledged separately.
static int
port_status(struct usb_dev *dev, int port, void *scratch)
{
uint16 *st = scratch;
if(hub_request(dev, USB_DIR_IN | USB_TYPE_CLASS | USB_RECIP_OTHER,
USB_REQ_GET_STATUS, 0, port, scratch, 4) < 0)
return -1;
return st[0];
}
// The other half: what has changed since the bits were last cleared.
static int
port_change(struct usb_dev *dev, int port, void *scratch, int *status)
{
uint16 *st = scratch;
if(hub_request(dev, USB_DIR_IN | USB_TYPE_CLASS | USB_RECIP_OTHER,
USB_REQ_GET_STATUS, 0, port, scratch, 4) < 0)
return -1;
*status = st[0];
return st[1];
}
// Bring up whatever is plugged into one downstream port.
static void
hub_port(struct usb_dev *hub, int port, void *scratch)
{
int st, speed, i;
st = port_status(hub, port, scratch);
if(st < 0 || !(st & PORTSTAT_CONNECTION))
return;
// Acknowledge the connect-change bit before resetting. Leaving it set is
// harmless here but confuses any later attempt to use these bits for
// hotplug, which is what they are actually for.
port_clear_feature(hub, port, PORT_FEAT_C_CONNECTION);
if(port_set_feature(hub, port, PORT_FEAT_RESET) < 0){
cprintf("usb-hub: port %d would not reset\n", port);
return;
}
// A hub does not report reset completion by clearing the request; it sets
// C_PORT_RESET and enables the port. Poll for the enable rather than
// guessing at a delay, because the time depends on the device.
for(i = 0; i < 200; i++){
microdelay(1000);
st = port_status(hub, port, scratch);
if(st >= 0 && !(st & PORTSTAT_RESET) && (st & PORTSTAT_ENABLE))
break;
}
port_clear_feature(hub, port, PORT_FEAT_C_RESET);
st = port_status(hub, port, scratch);
if(st < 0 || !(st & PORTSTAT_ENABLE)){
cprintf("usb-hub: port %d did not enable after reset\n", port);
return;
}
// Speed comes out of two status bits rather than a field, and the absence
// of both means full speed.
if(st & PORTSTAT_LOWSPEED)
speed = USB_SPEED_LOW;
else if(st & PORTSTAT_HIGHSPEED)
speed = USB_SPEED_HIGH;
else
speed = USB_SPEED_FULL;
// A device needs time to settle after reset before it will answer on
// endpoint 0. The specification calls this TRSTRCY and allows 10 ms.
microdelay(20000);
hub->ops->attach(hub, port, speed);
}
// The status-change endpoint has spoken: a bitmap, one bit per port. From
// interrupt context, so only note it.
static void
hub_complete(struct usb_dev *dev, void *buf, int len)
{
struct hub *h = dev->drvdata;
uchar *b = buf;
uint32 bits = 0;
int i;
if(h == 0)
return;
for(i = 0; i < len && i < 4; i++)
bits |= (uint32)b[i] << (8 * i);
__sync_fetch_and_or(&h->change, bits);
}
static int
hub_probe(struct usb_dev *dev)
{
struct usb_hub_desc *hd;
void *scratch;
int nports, mtt, i;
scratch = dma_alloc(64, 64);
if(scratch == 0)
return -1;
// The hub descriptor is class-specific, so it comes from a class GET
// request rather than the standard one -- same request number, different
// recipient and type.
if(hub_request(dev, USB_DIR_IN | USB_TYPE_CLASS | USB_RECIP_DEVICE,
USB_REQ_GET_DESCRIPTOR, USB_DT_HUB << 8, 0,
scratch, sizeof(struct usb_hub_desc)) < 0){
cprintf("usb-hub: no hub descriptor\n");
dma_free(scratch, 64);
return -1;
}
hd = scratch;
nports = hd->bNbrPorts;
if(nports < 1 || nports > MAX_PORTS){
cprintf("usb-hub: %d ports is not a number of ports\n", nports);
dma_free(scratch, 64);
return -1;
}
mtt = (hd->wHubCharacteristics & HUB_CHAR_TT_MASK) != 0;
kinfo("usb-hub: %d ports on slot %d\n", nports, dev->slot);
// Tell the controller *before* powering anything on. Addressing a device
// behind a slot the controller does not know is a hub fails, and it fails
// at Address Device with a completion code rather than anywhere useful.
if(dev->ops->set_hub(dev, nports, mtt) < 0){
dma_free(scratch, 64);
return -1;
}
// Ports come up unpowered. bPwrOn2PwrGood is how long the hub says it needs
// after switching power on before a device behind it is worth talking to,
// in 2 ms units -- a number the hub knows and we do not.
for(i = 1; i <= nports; i++)
port_set_feature(dev, i, PORT_FEAT_POWER);
microdelay((hd->bPwrOn2PwrGood * 2 + 10) * 1000);
for(i = 1; i <= nports; i++)
hub_port(dev, i, scratch);
// From here the hub is listened to: its status-change endpoint completes
// whenever a port changes, and usb_hub_poll() acts on it. A hub without
// one is unheard of and could still be driven by polling, but is not.
for(i = 0; i < NHUB; i++)
if(!hubs[i].in_use)
break;
if(i == NHUB || !dev->has_intr_in){
if(i == NHUB)
cprintf("usb-hub: too many hubs; this one will not see hot-plug\n");
dma_free(scratch, 64);
return 0;
}
hubs[i].dev = dev;
hubs[i].nports = nports;
hubs[i].scratch = scratch;
hubs[i].change = 0;
hubs[i].in_use = 1;
dev->drvdata = &hubs[i];
dev->complete = hub_complete;
if(dev->ops->open_ep(dev, &dev->intr_in) < 0 ||
dev->ops->queue_in(dev, dev->intr_in.bEndpointAddress, 0, 0) < 0)
cprintf("usb-hub: status-change endpoint would not open; no hot-plug\n");
return 0;
}
// The hub is gone. Whatever was behind it went with it -- the controller
// tears those down first -- so all that is left is to forget it.
static void
hub_disconnect(struct usb_dev *dev)
{
struct hub *h = dev->drvdata;
if(h == 0)
return;
h->in_use = 0;
dma_free(h->scratch, 64);
dev->drvdata = 0;
}
// Act on every port change the hubs have reported. From the hot-plug thread,
// which is the only thing that enumerates once the machine is up.
void
usb_hub_poll(void)
{
int i, port;
for(i = 0; i < NHUB; i++){
struct hub *h = &hubs[i];
uint32 bits;
if(!h->in_use || h->change == 0)
continue;
bits = __sync_fetch_and_and(&h->change, 0);
for(port = 1; port <= h->nports; port++){
int st, chg;
if(!(bits & (1u << port)))
continue;
chg = port_change(h->dev, port, h->scratch, &st);
if(chg < 0)
continue;
if(chg & PORTCHG_OTHERS){
if(chg & (1 << 1)) port_clear_feature(h->dev, port, PORT_FEAT_C_ENABLE);
if(chg & (1 << 2)) port_clear_feature(h->dev, port, 18);
if(chg & (1 << 3)) port_clear_feature(h->dev, port, 19);
if(chg & (1 << 4)) port_clear_feature(h->dev, port, PORT_FEAT_C_RESET);
}
if(!(chg & PORTCHG_CONNECTION))
continue;
// Whatever was there before is not there now, even if something is:
// a connection change with the port still connected is a swap.
port_clear_feature(h->dev, port, PORT_FEAT_C_CONNECTION);
h->dev->ops->detach(h->dev, port);
if(st & PORTSTAT_CONNECTION){
microdelay(100000); // debounce, as on a root port
kinfo("usb-hub: slot %d port %d: plugged in\n", h->dev->slot, port);
hub_port(h->dev, port, h->scratch);
}
// The hub may have been unplugged by what was just done; stop if so.
if(!h->in_use)
break;
}
}
}
// Any hub, whatever its protocol: 0 is a full-speed hub, 1 single-TT, 2
// multi-TT, and they differ only in how transactions are translated, which
// is the controller's problem rather than ours.
const struct usb_driver usb_hub_driver = {
.name = "usb-hub",
.class = USB_CLASS_HUB,
.subclass = 0xFF,
.protocol = 0xFF,
.devclass = DEVCLASS_BUS,
.probe = hub_probe,
.disconnect = hub_disconnect,
};