Skip to content

sdiob(4) and SDIO in-band interrupts

Source: sys/dev/sdio/sdiob.c, sdio_subr.c, sdio_if.m, sdiodevs; the host gate in sys/dev/sdhci/sdhci.c; CCB plumbing in sys/cam/cam_ccb.h and cam_xpt.c.

MMCCAM discovers SDIO cards, but WiFi function drivers live in newbus. sdiob is the periph that bridges the two worlds: it walks the card’s CIS, creates one newbus child per function, and translates every register access a function driver makes into a CAM CCB. In the middle sits the part worth documenting: in-band interrupt delivery, where the card yanks DAT1 low and a driver seven layers up has to hear about it without the CPU melting.

The announcement and pnpinfo export are what make autoload work: the pnpinfo string flows through devctl, so devd and devmatch can match a function driver’s MODULE_PNP_INFO and load it. Before this, an unclaimed card was a row of bare “unknown” children. sdiodevs carries the ID database: Broadcom, Realtek, Atheros, MediaTek WLAN chips, in usbdevs format.

The discovery CCB is allocated before taking the periph lock; allocating under it was a sleep-under-non-sleepable-lock that WITNESS flagged and memory pressure could deadlock.

A function driver calls sdio_read_1(); the kobj method on the parent builds an XPT_MMC_IO CCB (a queued CCB) on the single cached per-card CCB, fills in the CMD52 or CMD53 argument, and runs it synchronously through CAM into sdhci. CMD53 requests split into block-mode chunks (when the card supports multiblock) then byte-mode chunks. One CCB per card, serialized by the periph lock: SDIO register traffic is not the fast path, the DMA data it triggers is.

The SDIO card interrupt is level-sensitive. The card holds DAT1 low until the function driver clears the condition at the card. Two consequences:

  • Writing the CARD_INT status bit clears nothing: the source is still asserted. sdhci explicitly never writes it, and excludes it from the unknown-interrupt clear mask.
  • If the host re-enabled the interrupt before the function driver serviced the card, the CPU would spin in an interrupt storm. So the gate must stay closed from the moment the interrupt fires until the bus acknowledges that every handler has run.

That acknowledgment is a CCB of its own: XPT_MMC_SDIO_IRQ, an immediate CCB (no device queue), carrying one of three actions: ENABLE, DISABLE, ACK.

The contract line, straight from the interface definition: returning from all selected callbacks is the bus’s acknowledgment and re-arm point. The function driver clears its chip’s interrupt source inside the callback; only after every callback returns does sdiob issue the ACK, and only then does sdhci re-enable CARD_INT. A MASKED gate refuses ENABLE with EBUSY: you cannot skip the ACK.

Two details keep it honest:

  • The ACK path costs a CMD52 (the INT_PENDING read) plus an immediate CCB. The immediate CCB bypasses the device queue, so it can be issued while a data CMD53 is still outstanding.
  • Registration and unregistration run as config transactions guarded by a condvar, not a mutex held across CCBs. An interrupt that arrives mid-transaction is deferred and replayed when the transaction closes. Unregistering from inside the interrupt task returns EDEADLK, because success implies draining the very task you are standing in.

Across a controller reset, sdhci re-adds CARD_INT to the interrupt mask only when the gate state is ARMED: a reset cannot silently reopen a masked gate.

CCCR INT_ENABLE holds per-function enable bits plus a master bit. Enable sets the function bit and the master; disable clears the function bit and drops the master only when no function bits remain. Detach clears exactly the bits sdiob owns, preserving anything foreign.

Terminal window
camcontrol devlist # the card as a CAM device
devinfo -v | grep sdiob # children with vendor/device/class pnpinfo

This is the plumbing bcfm rides on dory: the BCM43430’s WLAN function is child function 1, and every firmware interrupt takes the full trip: DAT1, gate, two taskqueue hops, CMD52, callback, ACK, re-arm.