// SPDX-License-Identifier: GPL-2.0-only /* * Copyright (C) 2026 Intel Corporation */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "icvs.h" /* Command timeouts determined experimentally */ #define CMD_TIMEOUT (5 * HZ) #define FW_READY_DELAY_MS 100 #define PCI_DEVICE_ID_INTEL_IPU7 0x645d /* MTL / LNL */ #define PCI_DEVICE_ID_INTEL_IPU7P5 0xb05d /* ARL / PTL */ /* * IPU7 PCI device IDs not covered by ipu6_pci_tbl in ipu6-pci-table.h. * Once the IPU6 driver gains support for IPU7, this table can be dropped. */ static const struct pci_device_id icvs_ipu7_tbl[] = { { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_IPU7) }, { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_IPU7P5) }, { } }; static const struct acpi_gpio_params gpio_wake = { 0, 0, false }; static const struct acpi_gpio_params gpio_rst = { 1, 0, false }; static const struct acpi_gpio_params gpio_req = { 2, 0, false }; static const struct acpi_gpio_params gpio_resp = { 3, 0, false }; static const struct acpi_gpio_mapping icvs_acpi_gpios[] = { { "wake-gpio", &gpio_wake, 1 }, { "rst-gpio", &gpio_rst, 1 }, { "req-gpio", &gpio_req, 1 }, { "resp-gpio", &gpio_resp, 1 }, { } }; static const struct acpi_gpio_params lgpio_req = { 0, 0, false }; static const struct acpi_gpio_params lgpio_resp = { 1, 0, false }; static const struct acpi_gpio_mapping icvs_acpi_lgpios[] = { { "req-gpio", &lgpio_req, 1 }, { "resp-gpio", &lgpio_resp, 1 }, { } }; /* Device quirk table */ static const struct icvs_device_quirk cvs_quirk_table[] = { { 0x2ac1, 0x20d0, ICVS_NO_MIPI_CONFIG | ICVS_NO_CAPS | ICVS_NO_FW_UPDATE }, /* Lattice NX33 */ { 0x06CB, 0x0701, ICVS_SKIP_FW_RESET | ICVS_HOST_SENSOR_PWR_CTRL | ICVS_HOST_PRIV_CTRL | ICVS_FW_BUF_SIZE_256 | ICVS_FW_HEADER_SIZE_256 }, /* Synaptics SVP7xxx */ { } }; /** * cvs_set_quirks - Match device VID/PID and set quirks * @ctx: CVS device context * @vid: Vendor ID * @pid: Product ID * * Searches the quirk table for a matching VID/PID and populates ctx->quirks * with the corresponding quirk flags. * If no match is found, quirks is set to 0. */ static void cvs_set_quirks(struct icvs *ctx, u16 vid, u16 pid) { ctx->quirks = 0; for (unsigned int i = 0; i < ARRAY_SIZE(cvs_quirk_table); i++) { if (cvs_quirk_table[i].vid == vid && cvs_quirk_table[i].pid == pid) { ctx->quirks = cvs_quirk_table[i].quirks; dev_info(cvs_dev(ctx), "Quirks: 0x%lx (VID:0x%04x PID:0x%04x)\n", ctx->quirks, vid, pid); return; } } dev_info(cvs_dev(ctx), "No quirks for device (VID:0x%04x PID:0x%04x)\n", vid, pid); } /* I2C transport helpers */ /** * cvs_read_i2c - Issue a read-type command and fetch device response * @ctx: CVS device context * @cmd_id: Command identifier (big endian) * @resp: Destination buffer for response payload * @size: Size of payload to read into @resp (without prefix) * * Sends @cmd_id and reads back the response in a single I2C transaction. * When the device prepends a 4-byte protocol prefix, the combined * prefix+payload is read into a temporary buffer and only the payload is * copied to @resp, avoiding any dependency on the layout of the caller's * buffer. * * Return: 0 on success or negative errno. */ static int cvs_read_i2c(struct icvs *ctx, __be16 cmd_id, void *resp, size_t size) { size_t prefix_size = ctx->prefix ? sizeof(u32) : 0; size_t read_size = size + prefix_size; struct i2c_client *i2c = ctx->i2c_client; u8 *buf __free(kfree) = NULL; int cnt; if (!resp || !size) return -EINVAL; cnt = i2c_master_send(i2c, (const char *)&cmd_id, sizeof(cmd_id)); if (cnt != sizeof(cmd_id)) return cnt < 0 ? cnt : -EIO; buf = kmalloc(read_size, GFP_KERNEL); if (!buf) return -ENOMEM; cnt = i2c_master_recv(i2c, buf, read_size); if (cnt != read_size) { dev_dbg(cvs_dev(ctx), "recv cmd 0x%04x short read (%d/%zu)\n", be16_to_cpu(cmd_id), cnt, read_size); return cnt < 0 ? cnt : -EIO; } memcpy(resp, buf + prefix_size, size); return 0; } /** * cvs_write_i2c - Write a raw command buffer to the device over I2C * @ctx: CVS device context * @data: Buffer containing command + payload * @size: Total bytes to write * * Return: 0 on success or negative errno. */ static int cvs_write_i2c(struct icvs *ctx, const void *data, int size) { struct i2c_client *i2c = ctx->i2c_client; int cnt; if (size < 0 || !data) return -EINVAL; cnt = i2c_master_send(i2c, data, size); if (cnt != size) { dev_dbg(cvs_dev(ctx), "send short (%d/%d)\n", cnt, size); return cnt < 0 ? cnt : -EIO; } return 0; } /** * cvs_checksum - Simple additive checksum helper * @data: 32-bit aligned data buffer * @len: Length in bytes (multiple of 4) * * Return: 32-bit additive checksum of the dwords in @data. */ static u32 cvs_checksum(const void *data, size_t len) { const u32 *words = data; u32 csum = 0; if (WARN_ON_ONCE(len % sizeof(u32))) return 0; for (unsigned int i = 0; i < len / sizeof(u32); i++) csum += words[i]; return csum; } /** * cvs_schedule_and_wait - Schedule polling work then wait for completion * @ctx: CVS device context * @work_delay_ms: Delay in milliseconds before polling work executes * @wait_jiffies: Timeout (jiffies) to wait for cmd completion * * Queues ctx->work to run after @work_delay_ms and then waits up to * @wait_jiffies for ctx->cmd_completion. * * Return: 0 on success, -ETIMEDOUT on timeout, negative errno on error. */ static int cvs_schedule_and_wait(struct icvs *ctx, unsigned int work_delay_ms, unsigned long wait_jiffies) { int ret; schedule_delayed_work(&ctx->work, msecs_to_jiffies(work_delay_ms)); ret = wait_for_completion_killable_timeout(&ctx->cmd_completion, wait_jiffies); if (ret < 0) return ret; if (!ret) return -ETIMEDOUT; return 0; } /** * cvs_wait_wake_or_sleep - Wait for wake IRQ or sleep fallback * @ctx: CVS device context * @timeout_jiffies: Timeout (jiffies) for wake event on full-cap devices * @sleep_ms: Milliseconds to sleep on light-cap devices * * For full capability devices (ICVS_FULLCAP) waits interruptibly on * ctx->hostwake_event until ctx->hostwake_event_arg becomes true or * @timeout_jiffies elapses. The flag is cleared after the wait. * For light capability devices performs a blocking msleep(@sleep_ms). * * Return codes normalized for caller switch handling: * <0 : error / interrupted * -ETIMEDOUT : timeout (wake event not observed) * 0 : success (wake observed OR light-cap sleep elapsed) * * Return: negative errno, -ETIMEDOUT on timeout, 0 on success. */ static int cvs_wait_wake_or_sleep(struct icvs *ctx, unsigned long timeout_jiffies, unsigned int sleep_ms) { int ret; if (ctx->res == ICVS_FULLCAP) { ret = wait_event_interruptible_timeout(ctx->hostwake_event, ctx->hostwake_event_arg, timeout_jiffies); ctx->hostwake_event_arg = false; if (ret < 0) return ret; if (!ret) return -ETIMEDOUT; return 0; } msleep(sleep_ms); return 0; /* treat sleep path as success */ } /** * cvs_config_mipi - Send a HOST_SET_MIPI_CONFIG command * @ctx: CVS device context * @c: Command container with conf field populated * @len: Length of original command structure (unused except for symmetry) * * Packages @c->param.conf into a cvs_mipi_data_packet including size and * checksum, then writes it to the device. * * Firmware note: the CVS firmware expects the MIPI configuration to be * sent as a single transaction, including all relevant parameters and checksum. * * Return: 0 on success, NO_MIPI_CONFIG or negative errno from I2C write. */ static int cvs_config_mipi(struct icvs *ctx, struct icvs_cmd *c, size_t len) { struct icvs_mipi_data_packet pkt = { .cmd_id = c->cmd_id, .size = sizeof(c->param.conf), .crc = cvs_checksum(&c->param.conf, sizeof(c->param.conf)), .conf = c->param.conf, }; if (ctx->quirks & ICVS_NO_MIPI_CONFIG) return 0; return cvs_write_i2c(ctx, &pkt, sizeof(pkt)); } /** * cvs_get_device_state - Query current device state bitfield * @ctx: CVS device context * @state: Returned state value * * Issues GET_DEV_STATE and fills @state. * * Return: 0 on success or negative errno. */ static int cvs_get_device_state(struct icvs *ctx, u8 *state) { struct icvs_resp n = { .cmd_id = cpu_to_be16(ICVS_GET_DEV_STATE), }; int ret; ret = cvs_read_i2c(ctx, n.cmd_id, &n.resp.state, sizeof(n.resp.state)); if (ret) return ret; *state = n.resp.state; return 0; } /** * cvs_get_device_caps - Read protocol capabilities * @ctx: CVS device context * @caps: Capability structure to populate * * Return: 0 on success or negative errno. */ static int cvs_get_device_caps(struct icvs *ctx, struct icvs_dev_capabilities *caps) { struct icvs_resp n = { .cmd_id = cpu_to_be16(ICVS_GET_DEV_CAPABILITY), }; int ret; if (ctx->quirks & ICVS_NO_CAPS) return 0; ret = cvs_read_i2c(ctx, n.cmd_id, &n.resp.cap, sizeof(n.resp.cap)); if (ret) return ret; *caps = n.resp.cap; return 0; } /** * cvs_hw_init - Probe device for prefix support and apply quirks * @ctx: CVS device context * * Sends GET_DEV_VID_PID and probes for a 32-bit prefix. * If it matches ICVS_PREFIX_VAL, sets ctx->prefix for subsequent reads. * Then reads VID/PID and applies matching quirks. * GET_DEV_VID_PID is supported by all protocol versions. * * Return: 0 on success or negative errno. */ static int cvs_hw_init(struct icvs *ctx) { struct icvs_resp n = { }; __be16 cmd = cpu_to_be16(ICVS_GET_DEV_VID_PID); u32 resp; int ret; /* * Clear prefix so cvs_read_i2c always reads exactly sizeof(u32) bytes * here, regardless of any value left over from a previous call (e.g. * on resume). */ ctx->prefix = false; ret = cvs_read_i2c(ctx, cmd, &resp, sizeof(resp)); if (ret) return ret; ctx->prefix = resp == ICVS_PREFIX_VAL; /* Now read VID/PID to apply quirks */ ret = cvs_read_i2c(ctx, cmd, &n.resp.vid_pid, sizeof(n.resp.vid_pid)); if (ret) return ret; cvs_set_quirks(ctx, n.resp.vid_pid.v_id, n.resp.vid_pid.p_id); return 0; } /** * cvs_irq_handler - Wake IRQ handler (full capability devices) * @irq: IRQ number * @dev_id: Device context pointer * * Sets a waitqueue flag and wakes up sleeping waiters. * * Return: IRQ_HANDLED always. */ static irqreturn_t cvs_irq_handler(int irq, void *dev_id) { struct icvs *ctx = dev_id; ctx->hostwake_event_arg = true; wake_up_interruptible(&ctx->hostwake_event); return IRQ_HANDLED; } /** * cvs_reset - Toggle reset GPIO for full capability devices * @ctx: CVS device context * * Drives reset low briefly then high if device resources indicate full * capability. Light devices have no reset line. */ static void cvs_reset(struct icvs *ctx) { if (ctx->quirks & ICVS_SKIP_FW_RESET) return; if (ctx->res == ICVS_FULLCAP) { gpiod_set_value(ctx->rst, 0); fsleep(2000); gpiod_set_value(ctx->rst, 1); } } /** * cvs_recv - Delayed work handler polling for command completion * @work: Embedded delayed_work member * * Re-reads device state; if device_busy remains set, re-schedules itself. * Otherwise stores state into wq_resp and completes the command. */ static void cvs_recv(struct work_struct *work) { struct icvs *ctx = container_of(work, struct icvs, work.work); u8 state = 0; int ret; ret = cvs_get_device_state(ctx, &state); if (ret < 0) { dev_dbg(cvs_dev(ctx), "state read failed: %d\n", ret); return; } if (state & ICVS_DEV_STATE_BUSY) { dev_dbg(cvs_dev(ctx), "device busy, reschedule\n"); schedule_delayed_work(&ctx->work, msecs_to_jiffies(FW_READY_DELAY_MS)); return; } ctx->wq_resp.resp.state = state; complete(&ctx->cmd_completion); } /** * cvs_send - Common command submission path * @ctx: CVS device context * @cmd: Command buffer (icvs_cmd) with cmd_id and param populated * @len: Buffer length * * Dispatches a set of supported commands: * - ICVS_SET_DEV_HOST_ID, * - ICVS_HOST_SENSOR_OWNER, * - ICVS_HOST_SET_MIPI_CONFIG * - ICVS_FW_LOADER_* * * For I2C based commands it sets big-endian cmd ids, writes to the device * and waits (via delayed work) for completion or timeout. * GPIO based ownership toggles are handled locally. * * Caller must hold ctx->lock when invoking this function and check for i2c * bus availability. * * Return: 0 on success, negative errno, -EINVAL for unsupported command * or status from device in ctx->wq_resp. */ int cvs_send(struct icvs *ctx, struct icvs_cmd *cmd, size_t len) { int ret, status = 0; lockdep_assert_held(&ctx->lock); dev_dbg(cvs_dev(ctx), "send cmd = 0x%04x", be16_to_cpu(cmd->cmd_id)); reinit_completion(&ctx->cmd_completion); switch (be16_to_cpu(cmd->cmd_id)) { case ICVS_SET_DEV_HOST_ID: cmd->cmd_id = cpu_to_be16(ICVS_SET_DEV_HOST_ID); ret = cvs_write_i2c(ctx, cmd, len); if (ret < 0) break; ret = cvs_schedule_and_wait(ctx, FW_READY_DELAY_MS, CMD_TIMEOUT); if (ret < 0) break; status = ctx->wq_resp.resp.state & ICVS_DEV_STATE_ERROR ? -EINVAL : 0; break; case ICVS_HOST_SENSOR_OWNER: gpiod_set_value_cansleep(ctx->req, cmd->param.param); fsleep(FW_READY_DELAY_MS * USEC_PER_MSEC); ret = gpiod_get_value_cansleep(ctx->resp); status = cmd->param.param == ret ? 0 : -EINVAL; ret = 0; /* success */ break; case ICVS_HOST_SET_MIPI_CONFIG: cmd->cmd_id = cpu_to_be16(ICVS_HOST_SET_MIPI_CONFIG); ret = cvs_config_mipi(ctx, cmd, len); if (ret < 0) break; ret = cvs_schedule_and_wait(ctx, FW_READY_DELAY_MS, CMD_TIMEOUT); status = (ctx->wq_resp.resp.state & ICVS_DEV_STATE_ERROR) ? -EINVAL : 0; break; case ICVS_FW_LOADER_START: cmd->cmd_id = cpu_to_be16(ICVS_FW_LOADER_START); ret = cvs_write_i2c(ctx, cmd, len); if (ret < 0) break; ret = cvs_wait_wake_or_sleep(ctx, CMD_TIMEOUT, FW_READY_DELAY_MS); if (ret) break; ret = cvs_schedule_and_wait(ctx, FW_READY_DELAY_MS, CMD_TIMEOUT); status = (ctx->wq_resp.resp.state & ICVS_DEV_STATE_DOWNLOAD) ? 0 : -EINVAL; break; case ICVS_FW_LOADER_DATA: /* Quirk for older protocols */ if (ctx->caps.protocol_version_major >= 2 && ctx->caps.protocol_version_minor >= 2) { cmd->cmd_id = cpu_to_be16(ICVS_FW_LOADER_DATA); ret = cvs_write_i2c(ctx, cmd, len); } else { ret = cvs_write_i2c(ctx, &cmd->param, len - sizeof(cmd->cmd_id)); } if (ret < 0) return ret; ret = cvs_wait_wake_or_sleep(ctx, FW_READY_DELAY_MS, FW_READY_DELAY_MS); if (ret) break; ret = cvs_schedule_and_wait(ctx, FW_READY_DELAY_MS, CMD_TIMEOUT); status = ctx->wq_resp.resp.state & ICVS_DEV_STATE_ERROR ? -EINVAL : 0; break; case ICVS_FW_LOADER_END: cmd->cmd_id = cpu_to_be16(ICVS_FW_LOADER_END); ret = cvs_write_i2c(ctx, cmd, len); if (ret < 0) break; ret = cvs_wait_wake_or_sleep(ctx, CMD_TIMEOUT, FW_READY_DELAY_MS); if (ret) break; ret = cvs_schedule_and_wait(ctx, FW_READY_DELAY_MS, CMD_TIMEOUT); status = !(ctx->wq_resp.resp.state & ICVS_DEV_STATE_DOWNLOAD) ? 0 : -EINVAL; break; default: ret = -EINVAL; break; } if (ret < 0) return ret; return ctx->wq_resp.status = status; } /** * cvs_set_link_owner - Switch CSI-2 link ownership between host and device * @ctx: CVS device context * @owner: Desired owner (ICVS_CSI_LINK_HOST or ICVS_CSI_LINK_CVS) * * Called from runtime PM callbacks to claim or release the CSI-2 link. * Also callable directly for error recovery paths. * * Return: 0 on success or negative errno. */ int cvs_set_link_owner(struct icvs *ctx, enum icvs_csi_link_owner owner) { struct icvs_cmd cmd = { .cmd_id = cpu_to_be16(ICVS_HOST_SENSOR_OWNER), .param.param = owner, }; size_t cmd_size = sizeof(cmd.cmd_id) + sizeof(cmd.param.param); guard(mutex)(&ctx->lock); return cvs_send(ctx, &cmd, cmd_size); } /** * cvs_configure_dev_caps - Configure device capability ownership bits * @ctx: CVS device context * * Tells the CVS device which of its features (privacy LED, RGB camera * power-up, vision sensing) are controlled by the host, then sends * SET_DEV_HOST_ID. * * Return: 0 on success or negative errno. */ static int cvs_configure_dev_caps(struct icvs *ctx) { struct icvs_cmd cmd = { .cmd_id = cpu_to_be16(ICVS_SET_DEV_HOST_ID) }; size_t sz = sizeof(cmd.cmd_id) + sizeof(cmd.param.host_id); if (ctx->quirks & ICVS_NO_CAPS) return 0; if (ctx->quirks & ICVS_HOST_VISION_SENSING) cmd.param.host_id |= ICVS_HOST_ID_VISION_SENSING; if (ctx->quirks & ICVS_HOST_PRIV_CTRL) cmd.param.host_id |= ICVS_HOST_ID_PRIVACY_LED; if (ctx->quirks & ICVS_HOST_SENSOR_PWR_CTRL) cmd.param.host_id |= ICVS_HOST_ID_RGBCAMERA_PWRUP; guard(mutex)(&ctx->lock); return cvs_send(ctx, &cmd, sz); } /** * cvs_core_probe - Shared probe path for I2C & platform instantiation * @dev: Parent device * @i2c: I2C client (NULL for platform devices) * * Discovers IPU, parses ACPI resources, sets up GPIOs/IRQs, initializes * sub-device (CSI) and host identifier, and exposes sysfs firmware interface. * * Return: 0 on success or negative errno. */ static int cvs_core_probe(struct device *dev, struct i2c_client *i2c) { struct pci_dev *ipu = NULL; struct icvs *ctx; int ret; /* Locate IPU device */ for (unsigned int i = 0; !ipu && ipu6_pci_tbl[i].vendor; i++) ipu = pci_get_device(ipu6_pci_tbl[i].vendor, ipu6_pci_tbl[i].device, NULL); for (unsigned int i = 0; !ipu && icvs_ipu7_tbl[i].vendor; i++) ipu = pci_get_device(icvs_ipu7_tbl[i].vendor, icvs_ipu7_tbl[i].device, NULL); if (!ipu) return -ENODEV; ret = ipu_bridge_init(&ipu->dev, ipu_bridge_parse_ssdb); if (ret < 0) goto err_put_ipu; if (!dev_fwnode(dev)) { ret = -ENXIO; goto err_put_ipu; } ctx = devm_kzalloc(dev, sizeof(*ctx), GFP_KERNEL); if (!ctx) { ret = -ENOMEM; goto err_put_ipu; } ctx->i2c_client = i2c; ret = gpiod_count(dev, NULL); switch (ret) { case ICVS_GPIO_SYNC: ctx->res = ICVS_LIGHTCAP; break; case ICVS_GPIO_ASYNC: ctx->res = ICVS_FULLCAP; break; default: dev_err(dev, "unexpected GPIO count %d\n", ret); ret = -EINVAL; goto err_put_ipu; } ret = devm_acpi_dev_add_driver_gpios(dev, ctx->res == ICVS_FULLCAP ? icvs_acpi_gpios : icvs_acpi_lgpios); if (ret) { dev_err_probe(dev, ret, "failed to add ACPI GPIOs\n"); goto err_put_ipu; } ctx->req = devm_gpiod_get(dev, "req", GPIOD_OUT_HIGH); if (IS_ERR(ctx->req)) { ret = dev_err_probe(dev, PTR_ERR(ctx->req), "failed to get req GPIO\n"); goto err_put_ipu; } ctx->resp = devm_gpiod_get(dev, "resp", GPIOD_IN); if (IS_ERR(ctx->resp)) { ret = dev_err_probe(dev, PTR_ERR(ctx->resp), "failed to get resp GPIO\n"); goto err_put_ipu; } if (ctx->res == ICVS_FULLCAP) { struct gpio_desc *wake; ctx->rst = devm_gpiod_get(dev, "rst", GPIOD_OUT_HIGH); if (IS_ERR(ctx->rst)) { ret = dev_err_probe(dev, PTR_ERR(ctx->rst), "failed to get rst GPIO\n"); goto err_put_ipu; } wake = devm_gpiod_get(dev, "wake", GPIOD_IN); if (IS_ERR(wake)) { ret = dev_err_probe(dev, PTR_ERR(wake), "failed to get wake GPIO\n"); goto err_put_ipu; } ctx->irq = gpiod_to_irq(wake); if (ctx->irq < 0) { ret = dev_err_probe(dev, ctx->irq, "failed to get wake IRQ\n"); goto err_put_ipu; } ret = devm_request_threaded_irq(dev, ctx->irq, NULL, cvs_irq_handler, IRQF_ONESHOT | IRQF_NO_SUSPEND, "cvs_wake", ctx); if (ret) { dev_err_probe(dev, ret, "failed to request IRQ\n"); goto err_put_ipu; } } ret = devm_mutex_init(dev, &ctx->lock); if (ret) goto err_put_ipu; init_completion(&ctx->cmd_completion); init_waitqueue_head(&ctx->hostwake_event); INIT_DELAYED_WORK(&ctx->work, cvs_recv); if (i2c) { ret = cvs_hw_init(ctx); if (ret) { dev_err(dev, "HW init failed (%d)\n", ret); /* * Fallback to GPIO-only mode. * Some BIOS show the device on the I2C bus, however, * the device is not accessible via I2C. */ ctx->i2c_client = NULL; goto fail_i2c; } ret = cvs_get_device_caps(ctx, &ctx->caps); if (ret) { dev_err_probe(dev, ret, "get caps failed\n"); goto err_put_ipu; } ret = cvs_configure_dev_caps(ctx); if (ret) { dev_err_probe(dev, ret, "configure dev caps failed\n"); goto err_put_ipu; } } fail_i2c: ret = cvs_csi_init(ctx, dev, i2c); if (ret) { dev_err_probe(dev, ret, "CSI init failed\n"); goto err_put_ipu; } dev_set_drvdata(dev, ctx); pm_runtime_set_autosuspend_delay(dev, 1000); pm_runtime_use_autosuspend(dev); pm_runtime_enable(dev); pm_runtime_idle(dev); /* * Create a PM runtime device link with IPU as consumer and CVS as * supplier. When the IPU runtime-resumes to start streaming, the PM * framework automatically resumes CVS first, triggering * cvs_runtime_resume() which hands CSI-2 link ownership to the host. */ ctx->ipu_link = device_link_add(&ipu->dev, dev, DL_FLAG_PM_RUNTIME | DL_FLAG_RPM_ACTIVE | DL_FLAG_STATELESS); if (!ctx->ipu_link) { dev_err(dev, "IPU device link failed\n"); ret = -ENODEV; goto err_csi_remove; } if (has_acpi_companion(dev)) acpi_dev_clear_dependencies(ACPI_COMPANION(dev)); put_device(&ipu->dev); return 0; err_csi_remove: if (ctx->ipu_link) device_link_del(ctx->ipu_link); cvs_csi_remove(ctx); pm_runtime_dont_use_autosuspend(dev); pm_runtime_disable(dev); pm_runtime_set_suspended(dev); err_put_ipu: put_device(&ipu->dev); return ret; } /** * cvs_probe - I2C driver probe entry * @i2c: I2C client * * Return: 0 on success or negative errno. */ static int cvs_probe(struct i2c_client *i2c) { return cvs_core_probe(&i2c->dev, i2c); } /** * cvs_core_remove - Shared remove logic * @dev: Device */ static void cvs_core_remove(struct device *dev) { struct icvs *ctx = dev_get_drvdata(dev); cancel_delayed_work_sync(&ctx->work); cvs_csi_remove(ctx); if (ctx->ipu_link) device_link_del(ctx->ipu_link); pm_runtime_put_noidle(dev); pm_runtime_disable(dev); pm_runtime_set_suspended(dev); cvs_reset(ctx); } /** * cvs_remove - I2C driver remove * @client: I2C client */ static void cvs_remove(struct i2c_client *client) { cvs_core_remove(&client->dev); } /** * cvs_suspend - System suspend callback * @dev: Device * * Return: 0. */ static int __maybe_unused cvs_suspend(struct device *dev) { struct icvs *ctx = dev_get_drvdata(dev); cancel_delayed_work_sync(&ctx->work); return 0; } /** * cvs_resume - System resume callback * @dev: Device * * Re-validates I2C link prefix and re-sends host id if transport available. * * Return: 0 on success or negative errno if I2C check fails. */ static int __maybe_unused cvs_resume(struct device *dev) { struct icvs *ctx = dev_get_drvdata(dev); int ret; if (ctx->i2c_client) { ret = cvs_hw_init(ctx); if (ret) return ret; return cvs_configure_dev_caps(ctx); } return 0; } /** * cvs_runtime_resume - Runtime PM resume: claim CSI-2 link ownership * @dev: Device * * Triggered automatically when the IPU (consumer) runtime-resumes, because * a DL_FLAG_PM_RUNTIME device link makes CVS the supplier. Transfers CSI-2 * link ownership to the host so the IPU can start receiving sensor frames. * * Return: 0 on success or negative errno. */ static int __maybe_unused cvs_runtime_resume(struct device *dev) { struct icvs *ctx = dev_get_drvdata(dev); return cvs_set_link_owner(ctx, ICVS_CSI_LINK_HOST); } /** * cvs_runtime_suspend - Runtime PM suspend: release CSI-2 link ownership * @dev: Device * * Called when the streaming reference is dropped by cvs_csi_disable_streams * via pm_runtime_put_autosuspend. Returns CSI-2 link ownership to CVS firmware. * * Return: 0 on success or negative errno. */ static int __maybe_unused cvs_runtime_suspend(struct device *dev) { struct icvs *ctx = dev_get_drvdata(dev); return cvs_set_link_owner(ctx, ICVS_CSI_LINK_CVS); } static const struct dev_pm_ops __maybe_unused cvs_pm_ops = { SET_SYSTEM_SLEEP_PM_OPS(cvs_suspend, cvs_resume) SET_RUNTIME_PM_OPS(cvs_runtime_suspend, cvs_runtime_resume, NULL) }; static const struct acpi_device_id intel_cvs_acpi_match[] = { { "INTC10DE" }, /* LNL */ { "INTC10E0" }, /* ARL */ { "INTC10E1" }, /* PTL */ { } }; MODULE_DEVICE_TABLE(acpi, intel_cvs_acpi_match); static struct i2c_driver cvs_driver = { .driver = { .name = "intel_cvs", .acpi_match_table = intel_cvs_acpi_match, .pm = pm_ptr(&cvs_pm_ops), }, .probe = cvs_probe, .remove = cvs_remove, }; static int cvs_platform_probe(struct platform_device *pdev) { return cvs_core_probe(&pdev->dev, NULL); } static void cvs_platform_remove(struct platform_device *pdev) { cvs_core_remove(&pdev->dev); } /* * Platform driver structure. * * Some platforms may instantiate the CVS device as a platform device * without I2C support. This driver binding allows such platforms to use the * CVS core functionality (GPIOs, CSI sub-device) without I2C. */ static struct platform_driver cvs_platform_driver = { .driver = { .name = "cvs_platform", .acpi_match_table = intel_cvs_acpi_match, .pm = pm_ptr(&cvs_pm_ops), }, .probe = cvs_platform_probe, .remove = cvs_platform_remove, }; /** * cvs_init - Module init registering I2C and platform drivers * * Return: 0 on success or negative errno. */ static int __init cvs_init(void) { int ret; ret = i2c_add_driver(&cvs_driver); if (ret) return ret; ret = platform_driver_register(&cvs_platform_driver); if (ret) { i2c_del_driver(&cvs_driver); return ret; } return 0; } /** * cvs_exit - Module exit unregistering drivers */ static void __exit cvs_exit(void) { platform_driver_unregister(&cvs_platform_driver); i2c_del_driver(&cvs_driver); } module_init(cvs_init); module_exit(cvs_exit); MODULE_IMPORT_NS("INTEL_IPU_BRIDGE"); MODULE_AUTHOR("Miguel Vadillo "); MODULE_DESCRIPTION("Intel Vision Sensing Controller driver"); MODULE_LICENSE("GPL");