git: 76113680635e - main - QCOM GENI I²C driver

From: Poul-Henning Kamp <phk_at_FreeBSD.org>
Date: Tue, 08 Apr 2025 05:52:17 UTC
The branch main has been updated by phk:

URL: https://cgit.FreeBSD.org/src/commit/?id=76113680635ea0c5972cbe8624a7d0d86270f569

commit 76113680635ea0c5972cbe8624a7d0d86270f569
Author:     Poul-Henning Kamp <phk@FreeBSD.org>
AuthorDate: 2025-04-08 05:50:25 +0000
Commit:     Poul-Henning Kamp <phk@FreeBSD.org>
CommitDate: 2025-04-08 05:51:54 +0000

    QCOM GENI I²C driver
    
    This is a from scratch, minimally viable, I²C driver for QCOM's
    GENI serial engine.
    
    No QCOM documentation is available, so other FOSS device drivers
    were consulted to figure things out
    
    The driver works on T14s G6 "Snapdragon" hardware, to such a degree
    that ACPI clients and HID devices work (polling mode & needs
    modifications to iichid)
    
    Differential Revision: https://reviews.freebsd.org/D49676
    Reviews by: adrian
---
 sys/dev/iicbus/controller/qcom/geni_iic.c      | 608 +++++++++++++++++++++++++
 sys/dev/iicbus/controller/qcom/geni_iic_acpi.c | 189 ++++++++
 sys/dev/iicbus/controller/qcom/geni_iic_var.h  |  80 ++++
 3 files changed, 877 insertions(+)

diff --git a/sys/dev/iicbus/controller/qcom/geni_iic.c b/sys/dev/iicbus/controller/qcom/geni_iic.c
new file mode 100644
index 000000000000..f53fc1d3f1cd
--- /dev/null
+++ b/sys/dev/iicbus/controller/qcom/geni_iic.c
@@ -0,0 +1,608 @@
+/*
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2025 Poul-Henning Kamp <phk@FreeBSD.org>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ *
+ * QualComm GENI I2C controller
+ *
+ * The GENI is actually a multi-protocol serial controller, so a lot of
+ * this can probably be shared if we ever get to those protocols.
+ *
+ * The best open "documentation" of the hardware is the Linux device driver
+ * from which much was learned, and we tip our hat to the authors of it.
+ */
+
+#include <sys/cdefs.h>
+
+#include "opt_acpi.h"
+
+#include <sys/param.h>
+#include <sys/kernel.h>
+#include <sys/module.h>
+#include <sys/endian.h>
+#include <sys/time.h>
+#include <sys/lock.h>
+#include <sys/mutex.h>
+#include <sys/sysctl.h>
+#include <sys/sx.h>
+#include <sys/bus.h>
+
+#include <machine/bus.h>
+#include <sys/rman.h>
+
+#include <dev/iicbus/iicbus.h>
+#include <dev/iicbus/iiconf.h>
+
+#include <dev/iicbus/controller/qcom/geni_iic_var.h>
+
+#define GENI_ALL_REGISTERS(THIS_MACRO) \
+	THIS_MACRO(GENI_FORCE_DEFAULT_REG,	0x020) \
+	THIS_MACRO(GENI_OUTPUT_CTRL,		0x024) \
+	THIS_MACRO(GENI_STATUS,			0x040) \
+	THIS_MACRO(GENI_SER_M_CLK_CFG,		0x048) \
+	THIS_MACRO(GENI_SER_S_CLK_CFG,		0x04c) \
+	THIS_MACRO(GENI_IF_DISABLE_RO,		0x064) \
+	THIS_MACRO(GENI_FW_REVISION_RO,		0x068) \
+	THIS_MACRO(GENI_CLK_SEL,		0x07c) \
+	THIS_MACRO(GENI_CFG_SEQ_START,		0x084) \
+	THIS_MACRO(GENI_BYTE_GRANULARITY,	0x254) \
+	THIS_MACRO(GENI_DMA_MODE_EN,		0x258) \
+	THIS_MACRO(GENI_TX_PACKING_CFG0,	0x260) \
+	THIS_MACRO(GENI_TX_PACKING_CFG1,	0x264) \
+	THIS_MACRO(GENI_I2C_TX_TRANS_LEN,	0x26c) \
+	THIS_MACRO(GENI_I2C_RX_TRANS_LEN,	0x270) \
+	THIS_MACRO(GENI_I2C_SCL_COUNTERS,	0x278) \
+	THIS_MACRO(GENI_RX_PACKING_CFG0,	0x284) \
+	THIS_MACRO(GENI_RX_PACKING_CFG1,	0x288) \
+	THIS_MACRO(GENI_M_CMD0,			0x600) \
+	THIS_MACRO(GENI_M_CMD_CTRL_REG,		0x604) \
+	THIS_MACRO(GENI_M_IRQ_STATUS,		0x610) \
+	THIS_MACRO(GENI_M_IRQ_EN,		0x614) \
+	THIS_MACRO(GENI_M_IRQ_CLEAR,		0x618) \
+	THIS_MACRO(GENI_M_IRQ_EN_SET,		0x61c) \
+	THIS_MACRO(GENI_M_IRQ_EN_CLEAR,		0x620) \
+	THIS_MACRO(GENI_S_CMD0,			0x630) \
+	THIS_MACRO(GENI_S_CMD_CTRL_REG,		0x634) \
+	THIS_MACRO(GENI_S_IRQ_STATUS,		0x640) \
+	THIS_MACRO(GENI_S_IRQ_EN,		0x644) \
+	THIS_MACRO(GENI_S_IRQ_CLEAR,		0x648) \
+	THIS_MACRO(GENI_S_IRQ_EN_SET,		0x64c) \
+	THIS_MACRO(GENI_S_IRQ_EN_CLEAR,		0x650) \
+	THIS_MACRO(GENI_TX_FIFOn,		0x700) \
+	THIS_MACRO(GENI_RX_FIFOn,		0x780) \
+	THIS_MACRO(GENI_TX_FIFO_STATUS,		0x800) \
+	THIS_MACRO(GENI_RX_FIFO_STATUS,		0x804) \
+	THIS_MACRO(GENI_TX_WATERMARK_REG,	0x80c) \
+	THIS_MACRO(GENI_RX_WATERMARK_REG,	0x810) \
+	THIS_MACRO(GENI_RX_RFR_WATERMARK_REG,	0x814) \
+	THIS_MACRO(GENI_IOS,			0x908) \
+	THIS_MACRO(GENI_M_GP_LENGTH,		0x910) \
+	THIS_MACRO(GENI_S_GP_LENGTH,		0x914) \
+	THIS_MACRO(GENI_DMA_TX_IRQ_STAT,	0xc40) \
+	THIS_MACRO(GENI_DMA_TX_IRQ_CLR,		0xc44) \
+	THIS_MACRO(GENI_DMA_TX_IRQ_EN,		0xc48) \
+	THIS_MACRO(GENI_DMA_TX_IRQ_EN_CLR,	0xc4c) \
+	THIS_MACRO(GENI_DMA_TX_IRQ_EN_SET,	0xc50) \
+	THIS_MACRO(GENI_DMA_TX_FSM_RST,		0xc58) \
+	THIS_MACRO(GENI_DMA_RX_IRQ_STAT,	0xd40) \
+	THIS_MACRO(GENI_DMA_RX_IRQ_CLR,		0xd44) \
+	THIS_MACRO(GENI_DMA_RX_IRQ_EN,		0xd48) \
+	THIS_MACRO(GENI_DMA_RX_IRQ_EN_CLR,	0xd4c) \
+	THIS_MACRO(GENI_DMA_RX_IRQ_EN_SET,	0xd50) \
+	THIS_MACRO(GENI_DMA_RX_LEN_IN,		0xd54) \
+	THIS_MACRO(GENI_DMA_RX_FSM_RST,		0xd58) \
+	THIS_MACRO(GENI_IRQ_EN,			0xe1c) \
+	THIS_MACRO(GENI_HW_PARAM_0,		0xe24) \
+	THIS_MACRO(GENI_HW_PARAM_1,		0xe28)
+
+enum geni_registers {
+#define ITER_MACRO(name, offset) name = offset,
+	GENI_ALL_REGISTERS(ITER_MACRO)
+#undef ITER_MACRO
+};
+
+#define RD(sc, reg) bus_read_4((sc)->regs_res, reg)
+#define WR(sc, reg, val) bus_write_4((sc)->regs_res, reg, val)
+
+static void
+geni_dump_regs(geniiic_softc_t *sc)
+{
+	device_printf(sc->dev, "Register Dump\n");
+#define DUMP_MACRO(name, offset) \
+	device_printf(sc->dev, \
+	    "    %08x %04x " #name "\n", \
+	    RD(sc, offset), offset);
+	GENI_ALL_REGISTERS(DUMP_MACRO)
+#undef DUMP_MACRO
+}
+
+static unsigned geniiic_debug_units = 0;
+
+static SYSCTL_NODE(_hw, OID_AUTO, geniiic, CTLFLAG_RW, 0, "GENI I2C");
+SYSCTL_INT(_hw_geniiic, OID_AUTO, debug_units, CTLFLAG_RWTUN,
+    &geniiic_debug_units, 1, "Bitmask of units to debug");
+
+
+static driver_filter_t geniiic_intr;
+
+static int
+geniiic_intr(void *cookie)
+{
+	uint32_t m_status, rx_fifo_status;
+	int retval = FILTER_STRAY;
+	geniiic_softc_t *sc = cookie;
+
+	mtx_lock_spin(&sc->intr_lock);
+	m_status = RD(sc, GENI_M_IRQ_STATUS);
+
+	rx_fifo_status = RD(sc, GENI_RX_FIFO_STATUS);
+	if (sc->rx_buf != NULL && rx_fifo_status & 0x3f) {
+
+		// Number of whole FIFO words, each 4 bytes
+		unsigned gotlen = (((rx_fifo_status & 0x3f) << 2)-1) * 4;
+
+		// Valid bytes in the last FIFO word
+		// (Field is 3 bits, we'll only ever see 0…3)
+		gotlen +=  (rx_fifo_status >> 28) & 0x7;
+
+		unsigned cnt;
+		for (cnt = 0; cnt < (rx_fifo_status & 0x3f); cnt++) {
+			uint32_t data = RD(sc, GENI_RX_FIFOn);
+			unsigned u;
+			for (u = 0; u < 4 && sc->rx_len && gotlen; u++) {
+				*sc->rx_buf++ = data & 0xff;
+				data >>= 8;
+				sc->rx_len--;
+				gotlen--;
+			}
+		}
+	}
+	if (m_status & (1<<26)) {
+		WR(sc, GENI_M_IRQ_CLEAR, (1<<26));
+		retval = FILTER_HANDLED;
+	}
+
+	if (m_status & (1<<0)) {
+		sc->rx_complete = true;
+		WR(sc, GENI_M_IRQ_EN_CLEAR, (1<<0));
+		WR(sc, GENI_M_IRQ_EN_CLEAR, (1<<26));
+		WR(sc, GENI_M_IRQ_CLEAR, (1<<0));
+		wakeup(sc);
+		retval = FILTER_HANDLED;
+	}
+	sc->cmd_status = m_status;
+
+	if (sc->rx_buf == NULL) {
+		device_printf(sc->dev,
+		    "Interrupt m_stat %x rx_fifo_status %x retval %d\n",
+		    m_status, rx_fifo_status, retval);
+		WR(sc, GENI_M_IRQ_EN, 0);
+		WR(sc, GENI_M_IRQ_CLEAR, m_status);
+		device_printf(sc->dev,
+		    "Interrupt M_IRQ_STATUS 0x%x M_IRQ_EN 0x%x\n",
+		    RD(sc, GENI_M_IRQ_STATUS), RD(sc, GENI_M_IRQ_EN));
+		device_printf(sc->dev,
+		    "Interrupt S_IRQ_STATUS 0x%x S_IRQ_EN 0x%x\n",
+		    RD(sc, GENI_S_IRQ_STATUS), RD(sc, GENI_S_IRQ_EN));
+		device_printf(sc->dev,
+		    "Interrupt DMA_TX_IRQ_STAT 0x%x DMA_RX_IRQ_STAT 0x%x\n",
+		    RD(sc, GENI_DMA_TX_IRQ_STAT), RD(sc, GENI_DMA_RX_IRQ_STAT));
+		device_printf(sc->dev,
+		    "Interrupt DMA_TX_IRQ_EN 0x%x DMA_RX_IRQ_EN 0x%x\n",
+		    RD(sc, GENI_DMA_TX_IRQ_EN), RD(sc, GENI_DMA_RX_IRQ_EN));
+		WR(sc, GENI_DMA_TX_IRQ_EN_CLR, RD(sc, GENI_DMA_TX_IRQ_STAT));
+		WR(sc, GENI_DMA_TX_IRQ_CLR, RD(sc, GENI_DMA_TX_IRQ_STAT));
+		WR(sc, GENI_DMA_RX_IRQ_EN_CLR, RD(sc, GENI_DMA_RX_IRQ_STAT));
+		WR(sc, GENI_DMA_RX_IRQ_CLR, RD(sc, GENI_DMA_RX_IRQ_STAT));
+	}
+	mtx_unlock_spin(&sc->intr_lock);
+	return(retval);
+}
+
+static int
+geniiic_wait_m_ireq(geniiic_softc_t *sc, uint32_t bits)
+{
+	uint32_t status;
+	int timeout;
+
+	for (timeout = 0; timeout < 10000; timeout++) {
+		status = RD(sc, GENI_M_IRQ_STATUS);
+		if (status & bits) {
+			return (0);
+		}
+		DELAY(10);
+	}
+	return (IIC_ETIMEOUT);
+}
+
+static int
+geniiic_read(geniiic_softc_t *sc,
+    uint8_t slave, uint8_t *buf, uint16_t len, bool nonfinal)
+{
+	uint32_t cmd, istatus;
+
+	istatus = RD(sc, GENI_M_IRQ_STATUS);
+	WR(sc, GENI_M_IRQ_CLEAR, istatus);
+
+	sc->rx_complete = false;
+	sc->rx_fifo = false;
+	sc->rx_buf = buf;
+	sc->rx_len = len;
+	WR(sc, GENI_I2C_RX_TRANS_LEN, len);
+
+	// GENI_M_CMD0_OPCODE_I2C_READ << M_OPCODE_SHFT
+	cmd = (0x2 << 27);
+
+	// GENI_M_CMD0_SLV_ADDR_SHIFT
+	cmd |= slave << 9;
+
+	if (nonfinal) {
+		// GENI_M_CMD0_STOP_STRETCH
+		cmd |= (1<<2);
+	}
+	WR(sc, GENI_RX_WATERMARK_REG, sc->rx_fifo_size - 4);
+
+	// CMD_DONE, RX_FIFO_WATERMARK
+	WR(sc, GENI_M_IRQ_EN, (1<<0) | (1<<26));
+
+	// M_IRQ
+	WR(sc, GENI_IRQ_EN, (1<<2));
+
+	WR(sc, GENI_M_CMD0, cmd);
+
+	mtx_lock_spin(&sc->intr_lock);
+	sc->rx_fifo = false;
+	unsigned msec;
+	for (msec = 0; msec < 100; msec++) {
+		msleep_spin_sbt(sc, &sc->intr_lock,
+		    "geniwait", SBT_1MS, SBT_1MS / 10, 0);
+		if (sc->rx_complete)
+			break;
+	}
+	if (msec > sc->worst) {
+		device_printf(sc->dev,
+		    "Tworst from %u to %u\n", sc->worst, msec);
+		if (msec != 100)
+		    sc->worst = msec;
+	}
+
+	if (!sc->rx_complete) {
+		// S_GENI_CMD_CANCEL
+		WR(sc, GENI_M_CMD_CTRL_REG, (1<<2));
+
+		WR(sc, GENI_IRQ_EN, 0);
+		device_printf(sc->dev,
+		    "Incomplete read (residual %x)\n", sc->rx_len);
+	}
+
+	sc->rx_buf = NULL;
+	len = sc->rx_len;
+	sc->rx_len = 0;
+
+	mtx_unlock_spin(&sc->intr_lock);
+
+#define COMPLAIN(about) \
+	device_printf(sc->dev, \
+	    "read " about " slave=0x%x len=0x%x, cmd=0x%x cmd_status=0x%x\n", \
+	    slave, len, cmd, sc->cmd_status \
+	)
+
+	if (geniiic_debug_units) {
+		unsigned unit = device_get_unit(sc->dev);
+		if (unit < 32 && geniiic_debug_units & (1<<unit) && len == 0) {
+			COMPLAIN("OK");
+			return(IIC_NOERR);
+		}
+	}
+	if (len == 0)
+		return(IIC_NOERR);
+
+	if (sc->cmd_status & (1<<10)) {
+		COMPLAIN("ESTATUS");
+		return(IIC_ESTATUS);
+	}
+	if (len) {
+		COMPLAIN("EUNDERFLOW");
+		return(IIC_EUNDERFLOW);
+	}
+	COMPLAIN("EBUSERR");
+	return (IIC_EBUSERR);
+#undef COMPLAIN
+}
+
+static int
+geniiic_write(geniiic_softc_t *sc,
+    uint8_t slave, uint8_t *buf, uint16_t len, bool nonfinal)
+{
+	uint32_t status, data, cmd;
+	int timeout, error;
+
+	status = RD(sc, GENI_M_IRQ_STATUS);
+	WR(sc, GENI_M_IRQ_CLEAR, status);
+
+	WR(sc, GENI_I2C_TX_TRANS_LEN, len);
+
+	// GENI_M_CMD0_OPCODE_I2C_WRITE << M_OPCODE_SHFT
+	cmd = (0x1 << 27);
+
+	// GENI_M_CMD0_SLV_ADDR_SHIFT
+	cmd |= slave << 9;
+
+	if (nonfinal) {
+		// GENI_M_CMD0_STOP_STRETCH
+		cmd |= (1<<2);
+	}
+	WR(sc, GENI_M_CMD0, cmd);
+	for(timeout = 0; len > 0 && timeout < 100; timeout++) {
+		status = RD(sc, GENI_TX_FIFO_STATUS);
+		if (status < 16) {
+			data = 0;
+			if (len) { data |= *buf <<  0; buf++; len--; }
+			if (len) { data |= *buf <<  8; buf++; len--; }
+			if (len) { data |= *buf << 16; buf++; len--; }
+			if (len) { data |= *buf << 24; buf++; len--; }
+			WR(sc, GENI_TX_FIFOn, data);
+		} else {
+			DELAY(10);
+		}
+	}
+
+	// GENI_M_IRQ_CMD_DONE
+	error = geniiic_wait_m_ireq(sc, 1);
+
+	if (len == 0 && error == 0)
+		return(IIC_NOERR);
+	device_printf(sc->dev,
+	    "write ERR len=%d, error=%d cmd=0x%x\n", len, error, cmd);
+	return (IIC_EBUSERR);
+}
+
+static void
+geniiic_dumpmsg(device_t dev, struct iic_msg *msgs, uint32_t nmsgs)
+{
+	unsigned u;
+
+	device_printf(dev, "transfer:\n");
+	for (u = 0; u < nmsgs; u++) {
+		device_printf(dev,
+		    "  [%d] slave=0x%x, flags=0x%x len=0x%x buf=%p\n",
+		    u, msgs[u].slave, msgs[u].flags, msgs[u].len, msgs[u].buf
+		);
+	}
+}
+
+int
+geniiic_transfer(device_t dev, struct iic_msg *msgs, uint32_t nmsgs)
+{
+	geniiic_softc_t *sc = device_get_softc(dev);
+	unsigned u;
+	int error;
+
+	if (sc->nfail > 4) {
+		pause_sbt("geniic_fail", SBT_1S * 5, SBT_1S, 0);
+		return (IIC_ERESOURCE);
+	}
+
+	sx_xlock(&sc->real_bus_lock);
+
+	if (geniiic_debug_units) {
+		unsigned unit = device_get_unit(dev);
+		if (unit < 32 && geniiic_debug_units & (1<<unit)) {
+			geniiic_dumpmsg(dev, msgs, nmsgs);
+		}
+	}
+
+	error = 0;
+	for (u = 0; u < nmsgs; u++) {
+		bool nonfinal =
+		    (u < nmsgs - 1) && (msgs[u].flags & IIC_M_NOSTOP);
+		unsigned slave = msgs[u].slave >> 1;
+		if (msgs[u].flags & IIC_M_RD) {
+			error = geniiic_read(sc,
+			    slave, msgs[u].buf, msgs[u].len, nonfinal);
+		} else {
+			error = geniiic_write(sc,
+			    slave, msgs[u].buf, msgs[u].len, nonfinal);
+		}
+	}
+	if (error) {
+		device_printf(dev, "transfer error %d\n", error);
+		geniiic_dumpmsg(dev, msgs, nmsgs);
+	}
+	if (error) {
+		geniiic_reset(dev, 0, 0, NULL);
+	}
+	if (error)
+		sc->nfail++;
+	else
+		sc->nfail = 0;
+	sx_xunlock(&sc->real_bus_lock);
+	return (error);
+}
+
+int
+geniiic_reset(device_t dev, u_char speed, u_char addr, u_char *oldaddr)
+{
+	geniiic_softc_t *sc = device_get_softc(dev);
+	unsigned u;
+
+	device_printf(dev, "reset\n");
+	WR(sc, GENI_M_IRQ_EN, 0);
+	WR(sc, GENI_M_IRQ_CLEAR, ~0);
+	WR(sc, GENI_DMA_TX_IRQ_EN_CLR, ~0);
+	WR(sc, GENI_DMA_TX_IRQ_CLR, ~0);
+	WR(sc, GENI_DMA_RX_IRQ_EN_CLR, ~0);
+	WR(sc, GENI_DMA_RX_IRQ_CLR, ~0);
+
+	// S_GENI_CMD_ABORT
+	WR(sc, GENI_M_CMD_CTRL_REG, (1<<1));
+
+	WR(sc, GENI_DMA_RX_FSM_RST, 1);
+	for (u = 0; u < 1000; u++) {
+		if (RD(sc, GENI_DMA_RX_IRQ_STAT) & 0x8)
+			break;
+		DELAY(10);
+	}
+	if (u > 0)
+		device_printf(dev, "RXRESET time %u\n", u);
+	WR(sc, GENI_DMA_TX_FSM_RST, 1);
+	for (u = 0; u < 1000; u++) {
+		if (RD(sc, GENI_DMA_TX_IRQ_STAT) & 0x8)
+			break;
+		DELAY(10);
+	}
+	if (u > 0)
+		device_printf(dev, "TXRESET time %u\n", u);
+	return (0);
+}
+
+int
+geniiic_callback(device_t dev, int index, caddr_t data)
+{
+	geniiic_softc_t *sc = device_get_softc(dev);
+	int error = 0;
+
+	return(0);
+	switch (index) {
+	case IIC_REQUEST_BUS:
+		if (sx_try_xlock(&sc->bus_lock) == 0)
+			error = IIC_EBUSBSY;
+		else
+			sc->bus_locked = true;
+		break;
+
+	case IIC_RELEASE_BUS:
+		if (!sc->bus_locked) {
+			device_printf(dev, "Unlocking unlocked bus\n");
+		}
+		sc->bus_locked = false;
+		sx_xunlock(&sc->bus_lock);
+		break;
+
+	default:
+		device_printf(dev, "callback unknown %d\n", index);
+		error = errno2iic(EINVAL);
+	}
+
+	return (error);
+}
+
+int
+geniiic_attach(geniiic_softc_t *sc)
+{
+	int error = 0;
+
+	if (bootverbose)
+		geni_dump_regs(sc);
+	mtx_init(&sc->intr_lock, "geniiic intr lock", NULL, MTX_SPIN);
+	sx_init(&sc->real_bus_lock, "geniiic real bus lock");
+	sx_init(&sc->bus_lock, "geniiic bus lock");
+
+	sc->rx_fifo_size = (RD(sc, GENI_HW_PARAM_1) >> 16) & 0x3f;
+	device_printf(sc->dev, "  RX fifo size= 0x%x\n", sc->rx_fifo_size);
+
+	// We might want to set/check the following registers:
+	//	GENI_BYTE_GRANULARITY	(0x00000000)
+	//	GENI_TX_PACKING_CFG0	(0x0007f8fe)
+	//	GENI_TX_PACKING_CFG1	(000ffefe)
+	//	GENI_RX_PACKING_CFG0	(0x0007f8fe)
+	//	GENI_RX_PACKING_CFG1	(000ffefe)
+
+	sc->iicbus = device_add_child(sc->dev, "iicbus", DEVICE_UNIT_ANY);
+	if (sc->iicbus == NULL) {
+		device_printf(sc->dev, "iicbus driver not found\n");
+		return(ENXIO);
+	}
+
+	error = bus_setup_intr(sc->dev,
+	    sc->intr_res, INTR_TYPE_MISC | INTR_MPSAFE,
+	    geniiic_intr, NULL, sc, &sc->intr_handle);
+	if (error) {
+		device_printf(sc->dev,
+		    "Unable to setup irq: error %d\n", error);
+	}
+
+	bus_attach_children(sc->dev);
+	return (error);
+}
+
+int
+geniiic_detach(geniiic_softc_t *sc)
+{
+	int error = 0;
+
+	error = bus_generic_detach(sc->dev);
+	if (error)
+		return (error);
+
+	WR(sc, GENI_M_IRQ_EN, 0);
+
+	if (sc->intr_handle) {
+		bus_teardown_intr(sc->dev, sc->intr_res, sc->intr_handle);
+	}
+
+	sx_xlock(&sc->bus_lock);
+	sx_xlock(&sc->real_bus_lock);
+
+	geniiic_reset(sc->dev, 0, 0, NULL);
+	sc->iicbus = NULL;
+	sc->intr_handle = NULL;
+
+	sx_xunlock(&sc->real_bus_lock);
+	sx_xunlock(&sc->bus_lock);
+
+	sx_destroy(&sc->real_bus_lock);
+	sx_destroy(&sc->bus_lock);
+
+	mtx_destroy(&sc->intr_lock);
+	return (error);
+}
+
+int
+geniiic_suspend(geniiic_softc_t *sc)
+{
+	int error;
+
+	device_printf(sc->dev, "suspend method is NO-OP (good luck!)\n");
+
+	error = bus_generic_suspend(sc->dev);
+
+	return (error);
+}
+
+int geniiic_resume(geniiic_softc_t *sc)
+{
+	int error;
+
+	device_printf(sc->dev, "resume method is NO-OP (good luck!)\n");
+
+	error = bus_generic_resume(sc->dev);
+
+	return (error);
+}
+
+DRIVER_MODULE(iicbus, geniiic, iicbus_driver, NULL, NULL);
+DRIVER_MODULE(acpi_iicbus, geniiic, acpi_iicbus_driver, NULL, NULL);
+MODULE_DEPEND(geniiic, iicbus, IICBUS_MINVER, IICBUS_PREFVER, IICBUS_MAXVER);
+MODULE_VERSION(geniiic, 1);
diff --git a/sys/dev/iicbus/controller/qcom/geni_iic_acpi.c b/sys/dev/iicbus/controller/qcom/geni_iic_acpi.c
new file mode 100644
index 000000000000..2105071f5609
--- /dev/null
+++ b/sys/dev/iicbus/controller/qcom/geni_iic_acpi.c
@@ -0,0 +1,189 @@
+/*
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2025 Poul-Henning Kamp <phk@FreeBSD.org>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ *
+ */
+
+#include <sys/cdefs.h>
+#include "opt_acpi.h"
+
+#include <sys/param.h>
+#include <sys/bus.h>
+#include <sys/kernel.h>
+#include <sys/module.h>
+#include <sys/proc.h>
+#include <sys/rman.h>
+
+#include <machine/bus.h>
+#include <machine/resource.h>
+
+#include <contrib/dev/acpica/include/acpi.h>
+#include <contrib/dev/acpica/include/accommon.h>
+
+#include <dev/acpica/acpivar.h>
+#include <dev/iicbus/iiconf.h>
+
+#include <dev/iicbus/controller/qcom/geni_iic_var.h>
+
+static int	geniiic_acpi_probe(device_t dev);
+static int	geniiic_acpi_attach(device_t dev);
+static int	geniiic_acpi_detach(device_t dev);
+
+static char *geniiic_ids[] = {
+	"QCOM0C10",
+	NULL
+};
+
+static int
+geniiic_acpi_probe(device_t dev)
+{
+	int rv;
+
+	if (acpi_disabled("geniiic"))
+		return (ENXIO);
+	rv = ACPI_ID_PROBE(device_get_parent(dev), dev, geniiic_ids, NULL);
+	if (rv > 0)
+		return (rv);
+
+	device_set_desc(dev, "Qualcomm GENI I2C Controller");
+	return (rv);
+}
+
+static int
+geniiic_acpi_attach(device_t dev)
+{
+	geniiic_softc_t	*sc;
+	char *str;
+	int error;
+
+	sc = device_get_softc(dev);
+
+	sc->dev = dev;
+	error = ACPI_ID_PROBE(device_get_parent(dev), dev, geniiic_ids, &str);
+	if (error > 0)
+		return (error);
+
+	sc->regs_rid = 0;
+	sc->regs_res = bus_alloc_resource_any(dev,
+	    SYS_RES_MEMORY, &sc->regs_rid, RF_ACTIVE);
+	if (sc->regs_res == NULL) {
+		device_printf(dev, "unable to map registers\n");
+		geniiic_acpi_detach(dev);
+		return (ENXIO);
+	}
+	sc->intr_rid = 0;
+	sc->intr_res = bus_alloc_resource_any(dev,
+	    SYS_RES_IRQ, &sc->intr_rid, RF_SHAREABLE | RF_ACTIVE);
+	if (sc->intr_res == NULL) {
+		device_printf(dev, "unable to map interrupt\n");
+		geniiic_acpi_detach(dev);
+		return (ENXIO);
+	}
+	sc->platform_attached = true;
+
+	error = geniiic_attach(sc);
+	if (error)
+		geniiic_acpi_detach(dev);
+
+	return (error);
+}
+
+static int
+geniiic_acpi_detach(device_t dev)
+{
+	geniiic_softc_t *sc = device_get_softc(dev);
+	int error;
+
+	if (sc->platform_attached) {
+		error = geniiic_detach(sc);
+		if (error)
+			return (error);
+		sc->platform_attached = false;
+	}
+
+	if (sc->intr_res) {
+		bus_release_resource(dev, SYS_RES_IRQ,
+				     sc->intr_rid, sc->intr_res);
+		sc->intr_res = NULL;
+	}
+	if (sc->regs_res) {
+		bus_release_resource(dev, SYS_RES_MEMORY,
+				     sc->regs_rid, sc->regs_res);
+		sc->regs_res = NULL;
+	}
+
+	return (0);
+}
+
+static int
+geniiic_acpi_suspend(device_t dev)
+{
+	geniiic_softc_t *sc = device_get_softc(dev);
+
+	return (geniiic_suspend(sc));
+}
+
+static int
+geniiic_acpi_resume(device_t dev)
+{
+	geniiic_softc_t *sc  = device_get_softc(dev);
+
+	return (geniiic_resume(sc));
+}
+
+static device_method_t geniiic_acpi_methods[] = {
+	/* Device interface */
+	DEVMETHOD(device_probe, geniiic_acpi_probe),
+	DEVMETHOD(device_attach, geniiic_acpi_attach),
+	DEVMETHOD(device_detach, geniiic_acpi_detach),
+	DEVMETHOD(device_suspend, geniiic_acpi_suspend),
+	DEVMETHOD(device_resume, geniiic_acpi_resume),
+
+	/* Bus interface */
+	DEVMETHOD(bus_setup_intr, bus_generic_setup_intr),
+	DEVMETHOD(bus_teardown_intr, bus_generic_teardown_intr),
+	DEVMETHOD(bus_alloc_resource, bus_generic_alloc_resource),
+	DEVMETHOD(bus_release_resource, bus_generic_release_resource),
+	DEVMETHOD(bus_activate_resource, bus_generic_activate_resource),
+	DEVMETHOD(bus_deactivate_resource, bus_generic_deactivate_resource),
+	DEVMETHOD(bus_adjust_resource, bus_generic_adjust_resource),
+
+	/* iicbus interface */
+	DEVMETHOD(iicbus_transfer, geniiic_transfer),
+	DEVMETHOD(iicbus_reset, geniiic_reset),
+	DEVMETHOD(iicbus_callback, geniiic_callback),
+
+	DEVMETHOD_END
+};
+
+static driver_t geniiic_acpi_driver = {
+	"geniiic",
+	geniiic_acpi_methods,
+	sizeof(struct geniiic_softc),
+};
+
+DRIVER_MODULE_ORDERED(geniiic, acpi, geniiic_acpi_driver, 0, 0, SI_ORDER_ANY);
+MODULE_DEPEND(geniiic, acpi, 1, 1, 1);
+ACPI_PNP_INFO(geniiic_ids);
diff --git a/sys/dev/iicbus/controller/qcom/geni_iic_var.h b/sys/dev/iicbus/controller/qcom/geni_iic_var.h
new file mode 100644
index 000000000000..9ce8200c6fe5
--- /dev/null
+++ b/sys/dev/iicbus/controller/qcom/geni_iic_var.h
@@ -0,0 +1,80 @@
+/*
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2025 Poul-Henning Kamp <phk@FreeBSD.org>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ *
+ */
+
+#ifndef _GENIIIC_GENI_VAR_H_
+#define _GENIIIC_GENI_VAR_H_
+
+#include "bus_if.h"
+#include "device_if.h"
+#include "iicbus_if.h"
+
+struct geniiic_softc {
+	device_t	dev;
+	device_t	iicbus;
+	struct resource	*regs_res;
+	int		regs_rid;
+	struct resource	*intr_res;
+	int		intr_rid;
+	void		*intr_handle;
+	int		intr_type;
+	uint32_t	intr_mask;
+
+	bool		bus_locked;
+
+	bool		platform_attached;
+
+	int		nfail;
+	unsigned	worst;
+
+	unsigned	rx_fifo_size;
+	bool		rx_complete;
+	bool		rx_fifo;
+	uint8_t		*rx_buf;
+	unsigned	rx_len;
+	uint32_t	cmd_status;
+
+	// Protect access to the bus
+	struct sx	bus_lock;
+	struct sx	real_bus_lock;
+
+	// Coordinate with interrupt routine
+	struct mtx	intr_lock;
+};
+
+typedef struct geniiic_softc geniiic_softc_t;
+
+int geniiic_attach(geniiic_softc_t *sc);
+int geniiic_detach(geniiic_softc_t *sc);
+int geniiic_suspend(geniiic_softc_t *sc);
+int geniiic_resume(geniiic_softc_t *sc);
+
+extern iicbus_transfer_t geniiic_transfer;
+extern iicbus_reset_t   geniiic_reset;
+extern iicbus_callback_t geniiic_callback;
+
+#endif /* _GENIIIC_GENI_VAR_H_ */