[PATCH] sdhci_fsl: add ACPI front-end for NXP Layerscape eSDHC (NXP0003)

yarshure <[email protected]> Thu, 9 Jul 2026 23:22:35 +0000
Newsgroups gmane.os.freebsd.devel.arm
Message-ID <[email protected]>
Refactor the FDT-only sdhci_fsl driver into a bus-agnostic base class
(DEFINE_CLASS_0 "sdhci_fsl") plus FDT and ACPI subclasses, mirroring
sdhci_xenon. A shared sdhci_fsl_attach_common() does the controller
bring-up; each front-end supplies the bus-specific bits (soc_data, base
clock, endianness, mmc host properties, card-detect).

The new ACPI front-end (sdhci_fsl_acpi.c) matches _HID NXP0003, reads
the base clock from the _DSD "clock-frequency" property (there is no FDT
clock framework under ACPI), and brings up the eSDHC controllers
described by UEFI/EDK2 firmware. On a SolidRun LX2160A CEX7 booting via
EDK2 this exposes the SD/TF card (/dev/mmcsd0) and eMMC (/dev/mmcsd1)
that were previously invisible because no driver matched NXP0003.

- New sdhci_fsl.h: shared softc, soc_data and DECLARE_CLASS(sdhci_fsl_driver).
- sdhci_fsl_fdt.c: split attach into an FDT prologue + bus-agnostic
  sdhci_fsl_attach_common(); base class + FDT subclass; declare the mmc
  bridge on the shared "sdhci_fsl" devclass; guard get_ro for the
  no-GPIO (ACPI) case.
- sdhci_fsl_acpi.c: NXP0003 ACPI binding; base clock from _DSD; generic
  card-present; mark the slot non-removable (eSDHC present-state card
  detect is unreliable); cap to high speed pending UHS tuning support.

MFC after:	2 weeks
Signed-off-by: yarshure <[email protected]>
---
Notes for review (not part of the commit message):

* Verification.  Hardware-tested on a SolidRun HoneyComb / CEX7 (LX2160A)
  booting via EDK2/UEFI (ACPI).  Before this change nothing matched _HID
  NXP0003, so the eSDHC controllers were unattached; after it both slots
  probe and come up:

      mmc0: <MMC/SD bus> ... on sdhci_fsl_acpi0
      mmcsd0: <MMCHC ...> ... (SD/TF card)
      mmc1: <MMC/SD bus> ... on sdhci_fsl_acpi1
      mmcsd1: <MMCHC ...> ... (eMMC)

  /dev/mmcsd0 and /dev/mmcsd1 read and write correctly.

* The FDT path is preserved unchanged -- the refactor only splits the
  existing sdhci_fsl_attach() into an FDT prologue plus a bus-agnostic
  sdhci_fsl_attach_common(), mirroring sdhci_xenon's base/FDT/ACPI split,
  so existing FDT (device-tree) boots behave exactly as before.  It has
  not been re-tested on an FDT-booted board; a smoke test there is welcome.

* Two ACPI-specific choices worth a look: the base clock is read from the
  _DSD "clock-frequency" property (no FDT clock framework under ACPI), and
  the slot is marked non-removable + capped to high speed (eSDHC
  present-state card detect is unreliable; UHS tuning is future work).

* Disclosure: developed with AI assistance; the hardware bring-up above
  was done by hand.

 sys/conf/files                 |   1 +
 sys/dev/sdhci/sdhci_fsl.h      |  61 +++++++++
 sys/dev/sdhci/sdhci_fsl_acpi.c | 124 +++++++++++++++++
 sys/dev/sdhci/sdhci_fsl_fdt.c  | 237 +++++++++++++++++++--------------
 4 files changed, 321 insertions(+), 102 deletions(-)
 create mode 100644 sys/dev/sdhci/sdhci_fsl.h
 create mode 100644 sys/dev/sdhci/sdhci_fsl_acpi.c

diff --git a/sys/conf/files b/sys/conf/files
index cc18ecbb9..face1c207 100644
--- a/sys/conf/files
+++ b/sys/conf/files
@@ -3138,6 +3138,7 @@ dev/sdhci/sdhci.c		optional sdhci
 dev/sdhci/sdhci_fdt.c		optional sdhci fdt regulator clk
 dev/sdhci/sdhci_fdt_gpio.c	optional sdhci fdt gpio
 dev/sdhci/sdhci_fsl_fdt.c	optional sdhci fdt gpio regulator clk
+dev/sdhci/sdhci_fsl_acpi.c	optional sdhci fdt gpio regulator clk acpi
 dev/sdhci/sdhci_if.m		optional sdhci
 dev/sdhci/sdhci_acpi.c		optional sdhci acpi
 dev/sdhci/sdhci_pci.c		optional sdhci pci
diff --git a/sys/dev/sdhci/sdhci_fsl.h b/sys/dev/sdhci/sdhci_fsl.h
new file mode 100644
index 000000000..a623b3b96
--- /dev/null
+++ b/sys/dev/sdhci/sdhci_fsl.h
@@ -0,0 +1,61 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Shared definitions for the NXP QorIQ Layerscape eSDHC controller, used by
+ * both the FDT (sdhci_fsl_fdt.c) and ACPI (sdhci_fsl_acpi.c) bus front-ends.
+ */
+
+#ifndef _SDHCI_FSL_H_
+#define _SDHCI_FSL_H_
+
+#include <dev/mmc/mmc_helpers.h>
+#include <dev/sdhci/sdhci.h>
+
+struct sdhci_fdt_gpio;
+
+struct sdhci_fsl_fdt_soc_data {
+	int quirks;
+	int baseclk_div;
+	uint8_t errata;
+	char *syscon_compat;
+};
+
+extern const struct sdhci_fsl_fdt_soc_data sdhci_fsl_fdt_lx2160a_soc_data;
+
+struct sdhci_fsl_fdt_softc {
+	device_t				dev;
+	const struct sdhci_fsl_fdt_soc_data	*soc_data;
+	struct resource				*mem_res;
+	struct resource				*irq_res;
+	void					*irq_cookie;
+	uint32_t				baseclk_hz;
+	uint32_t				maxclk_hz;
+	struct sdhci_fdt_gpio			*gpio;
+	struct sdhci_slot			slot;
+	bool					slot_init_done;
+	uint32_t				cmd_and_mode;
+	uint16_t				sdclk_bits;
+	struct mmc_helper			fdt_helper;
+	uint32_t				div_ratio;
+	uint8_t					vendor_ver;
+	uint32_t				flags;
+
+	/* Set by the bus front-end before calling sdhci_fsl_attach_common(). */
+	bool					little_endian;
+	bool					acpi;	/* ACPI front-end (no FDT node) */
+
+	uint32_t (* read)(struct sdhci_fsl_fdt_softc *, bus_size_t);
+	void (* write)(struct sdhci_fsl_fdt_softc *, bus_size_t, uint32_t);
+};
+
+DECLARE_CLASS(sdhci_fsl_driver);
+
+/*
+ * Bus-agnostic attach: allocate resources, configure the eSDHC block and bring
+ * up the slot. The caller must first fill soc_data, baseclk_hz, little_endian,
+ * acpi and the parsed mmc host properties (slot.host) in the softc.
+ */
+int	sdhci_fsl_attach_common(device_t dev);
+int	sdhci_fsl_detach(device_t dev);
+
+#endif /* _SDHCI_FSL_H_ */
diff --git a/sys/dev/sdhci/sdhci_fsl_acpi.c b/sys/dev/sdhci/sdhci_fsl_acpi.c
new file mode 100644
index 000000000..b9abfdfa9
--- /dev/null
+++ b/sys/dev/sdhci/sdhci_fsl_acpi.c
@@ -0,0 +1,124 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * ACPI front-end for the NXP QorIQ Layerscape eSDHC controller (_HID NXP0003).
+ *
+ * Under UEFI/ACPI the eSDHC is described in the DSDT (MMIO + IRQ in _CRS, base
+ * clock and bus properties in _DSD) but there is no FDT clock framework. This
+ * binding reads the firmware-provided properties and hands off to the shared
+ * sdhci_fsl_attach_common(), letting the OS access the SD/TF card (e.g. to
+ * rewrite the boot firmware) without booting under u-boot/FDT.
+ */
+
+#include <sys/param.h>
+#include <sys/bus.h>
+#include <sys/kernel.h>
+#include <sys/module.h>
+#include <sys/rman.h>
+#include <sys/taskqueue.h>
+
+#include <machine/bus.h>
+#include <machine/resource.h>
+
+#include <dev/mmc/bridge.h>
+#include <dev/mmc/mmcbrvar.h>
+#include <dev/mmc/mmc_helpers.h>
+
+#include <contrib/dev/acpica/include/acpi.h>
+#include <contrib/dev/acpica/include/accommon.h>
+#include <dev/acpica/acpivar.h>
+
+#include <dev/sdhci/sdhci.h>
+#include <dev/sdhci/sdhci_fsl.h>
+
+#include "mmcbr_if.h"
+#include "sdhci_if.h"
+
+#include "opt_mmccam.h"
+
+static char *sdhci_fsl_acpi_hids[] = {
+	"NXP0003",
+	NULL
+};
+
+static int
+sdhci_fsl_acpi_probe(device_t dev)
+{
+	int err;
+
+	err = ACPI_ID_PROBE(device_get_parent(dev), dev, sdhci_fsl_acpi_hids,
+	    NULL);
+	if (err <= 0) {
+		device_set_desc(dev,
+		    "NXP QorIQ Layerscape eSDHC controller");
+		return (err);
+	}
+
+	return (ENXIO);
+}
+
+static int
+sdhci_fsl_acpi_attach(device_t dev)
+{
+	struct sdhci_fsl_fdt_softc *sc;
+	struct mmc_helper mmc_helper;
+	uint32_t clk_hz;
+
+	sc = device_get_softc(dev);
+
+	/*
+	 * NXP0003 identifies the LX2160A-class eSDHC; use its SoC data for the
+	 * quirks/errata and the base-clock divider.
+	 */
+	sc->soc_data = &sdhci_fsl_fdt_lx2160a_soc_data;
+	sc->acpi = true;
+	sc->little_endian = true;	/* _DSD "little-endian"; LX2160A eSDHC */
+
+	/* Base (peripheral) clock from the _DSD "clock-frequency" property. */
+	clk_hz = 0;
+	if (device_get_property(dev, "clock-frequency", &clk_hz, sizeof(clk_hz),
+	    DEVICE_PROP_UINT32) <= 0 || clk_hz == 0) {
+		device_printf(dev, "missing 'clock-frequency' property\n");
+		return (ENXIO);
+	}
+	sc->baseclk_hz = clk_hz / sc->soc_data->baseclk_div;
+
+	/* mmc host properties (bus-width, max-frequency, ...) from _DSD. */
+	memset(&mmc_helper, 0, sizeof(mmc_helper));
+	if (mmc_parse(dev, &mmc_helper, &sc->slot.host) != 0)
+		return (ENXIO);
+
+	/*
+	 * Cap to high speed (25 MHz). UHS modes (SDR50/SDR104) require a tuning
+	 * sequence with voltage switching that is not yet wired up on the ACPI
+	 * front-end, so restrict to a non-tuned speed for reliable enumeration.
+	 */
+	if (sc->slot.host.f_max > 25000000)
+		sc->slot.host.f_max = 25000000;
+
+	return (sdhci_fsl_attach_common(dev));
+}
+
+static const device_method_t sdhci_fsl_acpi_methods[] = {
+	DEVMETHOD(device_probe,			sdhci_fsl_acpi_probe),
+	DEVMETHOD(device_attach,		sdhci_fsl_acpi_attach),
+	DEVMETHOD(device_detach,		sdhci_fsl_detach),
+
+	/*
+	 * No FDT GPIO line under ACPI; use the controller's own card-detect
+	 * (present-state register) instead.
+	 */
+	DEVMETHOD(sdhci_get_card_present,	sdhci_generic_get_card_present),
+
+	DEVMETHOD_END
+};
+
+DEFINE_CLASS_1(sdhci_fsl, sdhci_fsl_acpi_driver, sdhci_fsl_acpi_methods,
+    sizeof(struct sdhci_fsl_fdt_softc), sdhci_fsl_driver);
+
+DRIVER_MODULE(sdhci_fsl_acpi, acpi, sdhci_fsl_acpi_driver, NULL, NULL);
+SDHCI_DEPEND(sdhci_fsl_acpi);
+
+#ifndef MMCCAM
+MMC_DECLARE_BRIDGE(sdhci_fsl_acpi);
+#endif
diff --git a/sys/dev/sdhci/sdhci_fsl_fdt.c b/sys/dev/sdhci/sdhci_fsl_fdt.c
index 185b53a6d..185c31e50 100644
--- a/sys/dev/sdhci/sdhci_fsl_fdt.c
+++ b/sys/dev/sdhci/sdhci_fsl_fdt.c
@@ -48,6 +48,7 @@
 #include <dev/ofw/ofw_bus_subr.h>
 #include <dev/sdhci/sdhci.h>
 #include <dev/sdhci/sdhci_fdt_gpio.h>
+#include <dev/sdhci/sdhci_fsl.h>
 
 #include "mmcbr_if.h"
 #include "sdhci_if.h"
@@ -183,34 +184,10 @@
 
 #define SDHCI_FSL_MAX_RETRIES		20000	/* DELAY(10) * this = 200ms */
 
-struct sdhci_fsl_fdt_softc {
-	device_t				dev;
-	const struct sdhci_fsl_fdt_soc_data	*soc_data;
-	struct resource				*mem_res;
-	struct resource				*irq_res;
-	void					*irq_cookie;
-	uint32_t				baseclk_hz;
-	uint32_t				maxclk_hz;
-	struct sdhci_fdt_gpio			*gpio;
-	struct sdhci_slot			slot;
-	bool					slot_init_done;
-	uint32_t				cmd_and_mode;
-	uint16_t				sdclk_bits;
-	struct mmc_helper			fdt_helper;
-	uint32_t				div_ratio;
-	uint8_t					vendor_ver;
-	uint32_t				flags;
-
-	uint32_t (* read)(struct sdhci_fsl_fdt_softc *, bus_size_t);
-	void (* write)(struct sdhci_fsl_fdt_softc *, bus_size_t, uint32_t);
-};
-
-struct sdhci_fsl_fdt_soc_data {
-	int quirks;
-	int baseclk_div;
-	uint8_t errata;
-	char *syscon_compat;
-};
+/*
+ * struct sdhci_fsl_fdt_softc and struct sdhci_fsl_fdt_soc_data now live in
+ * <dev/sdhci/sdhci_fsl.h> so they can be shared with the ACPI front-end.
+ */
 
 static const struct sdhci_fsl_fdt_soc_data sdhci_fsl_fdt_ls1012a_soc_data = {
 	.quirks = 0,
@@ -234,7 +211,8 @@ static const struct sdhci_fsl_fdt_soc_data sdhci_fsl_fdt_ls1046a_soc_data = {
 	.syscon_compat = "fsl,ls1046a-scfg",
 };
 
-static const struct sdhci_fsl_fdt_soc_data sdhci_fsl_fdt_lx2160a_soc_data = {
+/* Shared with the ACPI front-end (declared in sdhci_fsl.h). */
+const struct sdhci_fsl_fdt_soc_data sdhci_fsl_fdt_lx2160a_soc_data = {
 	.quirks = 0,
 	.baseclk_div = 2,
 	.errata = SDHCI_FSL_UNRELIABLE_PULSE_DET |
@@ -755,6 +733,9 @@ sdhci_fsl_fdt_get_ro(device_t bus, device_t child)
 	struct sdhci_fsl_fdt_softc *sc;
 
 	sc = device_get_softc(bus);
+	/* No FDT write-protect GPIO (e.g. ACPI front-end): use the std reg. */
+	if (sc->gpio == NULL)
+		return (sdhci_generic_get_ro(bus, child));
 	return (sdhci_fdt_gpio_get_readonly(sc->gpio));
 }
 
@@ -803,8 +784,12 @@ sdhci_fsl_fdt_vddrange_to_mask(device_t dev, uint32_t *vdd_ranges, int len)
 	return (vdd_mask);
 }
 
+/*
+ * Apply the FDT "voltage-ranges" property over the capabilities that
+ * sdhci_fsl_attach_common() already read from hardware. FDT front-end only.
+ */
 static void
-sdhci_fsl_fdt_of_parse(device_t dev)
+sdhci_fsl_fdt_voltage_fixup(device_t dev)
 {
 	struct sdhci_fsl_fdt_softc *sc;
 	phandle_t node;
@@ -815,16 +800,6 @@ sdhci_fsl_fdt_of_parse(device_t dev)
 	sc = device_get_softc(dev);
 	node = ofw_bus_get_node(dev);
 
-	/* Call mmc_fdt_parse in order to get mmc related properties. */
-	mmc_fdt_parse(dev, node, &sc->fdt_helper, &sc->slot.host);
-
-	sc->slot.quirks |= SDHCI_QUIRK_MISSING_CAPS;
-	sc->slot.caps = sdhci_fsl_fdt_read_4(dev, &sc->slot,
-	    SDHCI_CAPABILITIES) & ~(SDHCI_CAN_DO_SUSPEND);
-	sc->slot.caps2 = sdhci_fsl_fdt_read_4(dev, &sc->slot,
-	    SDHCI_CAPABILITIES2);
-
-	/* Parse the "voltage-ranges" dts property. */
 	num_ranges = OF_getencprop_alloc(node, "voltage-ranges",
 	    (void **) &voltage_ranges);
 	if (num_ranges <= 0)
@@ -859,39 +834,37 @@ sdhci_fsl_poll_register(struct sdhci_fsl_fdt_softc *sc,
 	return (0);
 }
 
-static int
-sdhci_fsl_fdt_attach(device_t dev)
+/*
+ * Bus-agnostic attach. The bus front-end must have filled soc_data,
+ * baseclk_hz, little_endian, acpi and the parsed mmc host properties
+ * (slot.host) in the softc before calling this.
+ */
+int
+sdhci_fsl_attach_common(device_t dev)
 {
 	struct sdhci_fsl_fdt_softc *sc;
 	struct mmc_host *host;
 	uint32_t val, buf_order;
-	uintptr_t ocd_data;
-	uint64_t clk_hz;
-	phandle_t node;
 	int rid, ret;
-	clk_t clk;
 
-	node = ofw_bus_get_node(dev);
 	sc = device_get_softc(dev);
-	ocd_data = ofw_bus_search_compatible(dev,
-	    sdhci_fsl_fdt_compat_data)->ocd_data;
 	sc->dev = dev;
 	sc->flags = 0;
 	host = &sc->slot.host;
-	rid = 0;
-
-	/*
-	 * LX2160A needs its own soc_data in order to apply SoC
-	 * specific quriks. Since the controller is identified
-	 * only with a generic compatible string we need to do this dance here.
-	 */
-	if (ofw_bus_node_is_compatible(OF_finddevice("/"), "fsl,lx2160a"))
-		sc->soc_data = &sdhci_fsl_fdt_lx2160a_soc_data;
-	else
-		sc->soc_data = (struct sdhci_fsl_fdt_soc_data *)ocd_data;
-
 	sc->slot.quirks = sc->soc_data->quirks;
 
+	/* eSDHC block endianness; chosen by the bus front-end. */
+	if (sc->little_endian) {
+		sc->read = read_le;
+		sc->write = write_le;
+		buf_order = SDHCI_FSL_PROT_CTRL_BYTE_NATIVE;
+	} else {
+		sc->read = read_be;
+		sc->write = write_be;
+		buf_order = SDHCI_FSL_PROT_CTRL_BYTE_SWAP;
+	}
+
+	rid = 0;
 	sc->mem_res = bus_alloc_resource_any(dev, SYS_RES_MEMORY, &rid,
 	    RF_ACTIVE);
 	if (sc->mem_res == NULL) {
@@ -917,36 +890,20 @@ sdhci_fsl_fdt_attach(device_t dev)
 		goto err_free_irq_res;
 	}
 
-	ret = clk_get_by_ofw_index(dev, node, 0, &clk);
-	if (ret != 0) {
-		device_printf(dev, "Parent clock not found\n");
-		goto err_free_irq;
-	}
-
-	ret = clk_get_freq(clk, &clk_hz);
-	if (ret != 0) {
-		device_printf(dev,
-		    "Could not get parent clock frequency\n");
-		goto err_free_irq;
-	}
-
-	sc->baseclk_hz = clk_hz / sc->soc_data->baseclk_div;
-
-	/* Figure out eSDHC block endianness before we touch any HW regs. */
-	if (OF_hasprop(node, "little-endian")) {
-		sc->read = read_le;
-		sc->write = write_le;
-		buf_order = SDHCI_FSL_PROT_CTRL_BYTE_NATIVE;
-	} else {
-		sc->read = read_be;
-		sc->write = write_be;
-		buf_order = SDHCI_FSL_PROT_CTRL_BYTE_SWAP;
-	}
-
 	sc->vendor_ver = (RD4(sc, SDHCI_FSL_HOST_VERSION) &
 	    SDHCI_VENDOR_VER_MASK) >> SDHCI_VENDOR_VER_SHIFT;
 
-	sdhci_fsl_fdt_of_parse(dev);
+	/* Capabilities are missing from the standard registers; read them. */
+	sc->slot.quirks |= SDHCI_QUIRK_MISSING_CAPS;
+	sc->slot.caps = sdhci_fsl_fdt_read_4(dev, &sc->slot,
+	    SDHCI_CAPABILITIES) & ~(SDHCI_CAN_DO_SUSPEND);
+	sc->slot.caps2 = sdhci_fsl_fdt_read_4(dev, &sc->slot,
+	    SDHCI_CAPABILITIES2);
+
+	/* FDT may override the voltage caps via "voltage-ranges". */
+	if (!sc->acpi)
+		sdhci_fsl_fdt_voltage_fixup(dev);
+
 	sc->maxclk_hz = host->f_max ? host->f_max : sc->baseclk_hz;
 
 	/*
@@ -971,7 +928,10 @@ sdhci_fsl_fdt_attach(device_t dev)
 	val = RD4(sc, SDHCI_FSL_ESDHC_CTRL);
 	WR4(sc, SDHCI_FSL_ESDHC_CTRL, val | SDHCI_FSL_ESDHC_CTRL_CLK_DIV2);
 	sc->slot.max_clk = sc->maxclk_hz;
-	sc->gpio = sdhci_fdt_gpio_setup(dev, &sc->slot);
+
+	/* GPIO card-detect is an FDT-only facility. */
+	if (!sc->acpi)
+		sc->gpio = sdhci_fdt_gpio_setup(dev, &sc->slot);
 
 	/*
 	 * Set the buffer watermark level to 128 words (512 bytes) for both
@@ -992,6 +952,16 @@ sdhci_fsl_fdt_attach(device_t dev)
 	ret = sdhci_init_slot(dev, &sc->slot, 0);
 	if (ret != 0)
 		goto err_free_gpio;
+
+	/*
+	 * The eSDHC's SDHCI present-state card-detect bits are unreliable; the
+	 * FDT path works around this with a GPIO card-detect line, which is not
+	 * available under ACPI. Mark the slot non-removable so sdhci attaches
+	 * the mmc bus without waiting for a (never-stable) card-detect.
+	 */
+	if (sc->acpi)
+		sc->slot.opt |= SDHCI_NON_REMOVABLE;
+
 	sc->slot_init_done = true;
 	sdhci_start_slot(&sc->slot);
 
@@ -999,8 +969,8 @@ sdhci_fsl_fdt_attach(device_t dev)
 	return (0);
 
 err_free_gpio:
-	sdhci_fdt_gpio_teardown(sc->gpio);
-err_free_irq:
+	if (sc->gpio != NULL)
+		sdhci_fdt_gpio_teardown(sc->gpio);
 	bus_teardown_intr(dev, sc->irq_res, sc->irq_cookie);
 err_free_irq_res:
 	bus_free_resource(dev, SYS_RES_IRQ, sc->irq_res);
@@ -1010,7 +980,54 @@ sdhci_fsl_fdt_attach(device_t dev)
 }
 
 static int
-sdhci_fsl_fdt_detach(device_t dev)
+sdhci_fsl_fdt_attach(device_t dev)
+{
+	struct sdhci_fsl_fdt_softc *sc;
+	uintptr_t ocd_data;
+	uint64_t clk_hz;
+	phandle_t node;
+	clk_t clk;
+	int ret;
+
+	node = ofw_bus_get_node(dev);
+	sc = device_get_softc(dev);
+	ocd_data = ofw_bus_search_compatible(dev,
+	    sdhci_fsl_fdt_compat_data)->ocd_data;
+
+	/*
+	 * LX2160A needs its own soc_data in order to apply SoC
+	 * specific quriks. Since the controller is identified
+	 * only with a generic compatible string we need to do this dance here.
+	 */
+	if (ofw_bus_node_is_compatible(OF_finddevice("/"), "fsl,lx2160a"))
+		sc->soc_data = &sdhci_fsl_fdt_lx2160a_soc_data;
+	else
+		sc->soc_data = (const struct sdhci_fsl_fdt_soc_data *)ocd_data;
+
+	/* Parent (peripheral) clock comes from the FDT clock framework. */
+	ret = clk_get_by_ofw_index(dev, node, 0, &clk);
+	if (ret != 0) {
+		device_printf(dev, "Parent clock not found\n");
+		return (ret);
+	}
+	ret = clk_get_freq(clk, &clk_hz);
+	if (ret != 0) {
+		device_printf(dev, "Could not get parent clock frequency\n");
+		return (ret);
+	}
+	sc->baseclk_hz = clk_hz / sc->soc_data->baseclk_div;
+
+	sc->little_endian = OF_hasprop(node, "little-endian");
+	sc->acpi = false;
+
+	/* mmc host properties from the FDT node. */
+	mmc_fdt_parse(dev, node, &sc->fdt_helper, &sc->slot.host);
+
+	return (sdhci_fsl_attach_common(dev));
+}
+
+int
+sdhci_fsl_detach(device_t dev)
 {
 	struct sdhci_fsl_fdt_softc *sc;
 
@@ -1514,12 +1531,11 @@ sdhci_fsl_fdt_set_uhs_timing(device_t dev, struct sdhci_slot *slot)
 	}
 }
 
-static const device_method_t sdhci_fsl_fdt_methods[] = {
-	/* Device interface. */
-	DEVMETHOD(device_probe,			sdhci_fsl_fdt_probe),
-	DEVMETHOD(device_attach,		sdhci_fsl_fdt_attach),
-	DEVMETHOD(device_detach,		sdhci_fsl_fdt_detach),
-
+/*
+ * Bus-agnostic base class. The FDT and ACPI front-ends subclass this and
+ * override only device_probe/attach/detach (and, for ACPI, card-present).
+ */
+static const device_method_t sdhci_fsl_methods[] = {
 	/* Bus interface. */
 	DEVMETHOD(bus_read_ivar,		sdhci_fsl_fdt_read_ivar),
 	DEVMETHOD(bus_write_ivar,		sdhci_fsl_fdt_write_ivar),
@@ -1549,12 +1565,29 @@ static const device_method_t sdhci_fsl_fdt_methods[] = {
 	DEVMETHOD_END
 };
 
-static driver_t sdhci_fsl_fdt_driver = {
-	"sdhci_fsl_fdt",
-	sdhci_fsl_fdt_methods,
-	sizeof(struct sdhci_fsl_fdt_softc),
+DEFINE_CLASS_0(sdhci_fsl, sdhci_fsl_driver, sdhci_fsl_methods,
+    sizeof(struct sdhci_fsl_fdt_softc));
+
+/*
+ * Attach the mmc bus to the shared "sdhci_fsl" devclass (both the FDT and ACPI
+ * subclasses create devices of this class). Without this the controller
+ * attaches but the mmc bridge never does, so no card enumerates.
+ */
+#ifndef MMCCAM
+MMC_DECLARE_BRIDGE(sdhci_fsl);
+#endif
+
+/* FDT front-end. */
+static const device_method_t sdhci_fsl_fdt_methods[] = {
+	DEVMETHOD(device_probe,			sdhci_fsl_fdt_probe),
+	DEVMETHOD(device_attach,		sdhci_fsl_fdt_attach),
+	DEVMETHOD(device_detach,		sdhci_fsl_detach),
+	DEVMETHOD_END
 };
 
+DEFINE_CLASS_1(sdhci_fsl, sdhci_fsl_fdt_driver, sdhci_fsl_fdt_methods,
+    sizeof(struct sdhci_fsl_fdt_softc), sdhci_fsl_driver);
+
 DRIVER_MODULE(sdhci_fsl_fdt, simplebus, sdhci_fsl_fdt_driver, NULL, NULL);
 SDHCI_DEPEND(sdhci_fsl_fdt);
 
-- 
2.39.3 (Apple Git-146)