[PATCH 8/8] media: i2c: Add WV517S lens actuator driver

From: Maurizio Casciano

Date: Wed Aug 26 2026 - 09:27:02 EST


The Lenovo Yoga Book YB1-X91 rear camera contains a WV517S
voice-coil actuator. Add a V4L2 lens subdevice exposing the standard
10-bit FOCUS_ABSOLUTE control and the device ringing-control mode.

Tie register access to runtime PM so the IPU bridge sensor link keeps
shared power resources active. Propagate PM acquisition failures and
restore the drive mode and requested focus position after resume.

The register addresses and drive-mode value are derived from Intel's
GPL-2.0 WV517 driver. Retain its copyright notice.

Link: https://github.com/jekhor/yogabook-linux-android-kernel/blob/574bae692716f1b14093497bfab8a007fe8e460b/drivers/external_drivers/camera/drivers/media/i2c/wv517.c

Signed-off-by: Maurizio Casciano <mauriziocasciano7@xxxxxxxxx>
Assisted-by: Codex:gpt-5.6-sol sparse
---
MAINTAINERS | 1 +
drivers/media/i2c/Kconfig | 8 ++
drivers/media/i2c/Makefile | 1 +
drivers/media/i2c/wv517s.c | 199 +++++++++++++++++++++++++++++++++++++
4 files changed, 209 insertions(+)
create mode 100644 drivers/media/i2c/wv517s.c

diff --git a/MAINTAINERS b/MAINTAINERS
index 3785b8c1de0a..205a42646a7d 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -28268,6 +28268,7 @@ S: Maintained
F: drivers/media/i2c/ak*
F: drivers/media/i2c/dw*
F: drivers/media/i2c/lm*
+F: drivers/media/i2c/wv517s.c

V4L2 CAMERA SENSOR DRIVERS
M: Sakari Ailus <sakari.ailus@xxxxxxxxxxxxxxx>
diff --git a/drivers/media/i2c/Kconfig b/drivers/media/i2c/Kconfig
index 5c52007f9cbe..c488452c1b38 100644
--- a/drivers/media/i2c/Kconfig
+++ b/drivers/media/i2c/Kconfig
@@ -949,6 +949,14 @@ config VIDEO_DW9807_VCM
capability. This is designed for linear control of
voice coil motors, controlled via I2C serial interface.

+config VIDEO_WV517S
+ tristate "WV517S lens voice coil support"
+ help
+ This is a driver for the WV517S camera lens voice coil. It supports
+ the 10-bit focus control used by the Lenovo Yoga Book rear camera.
+ The driver exposes the actuator through the standard V4L2 lens
+ sub-device interface.
+
endif

menu "Flash devices"
diff --git a/drivers/media/i2c/Makefile b/drivers/media/i2c/Makefile
index d04bd5724552..e480932a9540 100644
--- a/drivers/media/i2c/Makefile
+++ b/drivers/media/i2c/Makefile
@@ -173,4 +173,5 @@ obj-$(CONFIG_VIDEO_VP27SMPX) += vp27smpx.o
obj-$(CONFIG_VIDEO_VPX3220) += vpx3220.o
obj-$(CONFIG_VIDEO_WM8739) += wm8739.o
obj-$(CONFIG_VIDEO_WM8775) += wm8775.o
+obj-$(CONFIG_VIDEO_WV517S) += wv517s.o
obj-$(CONFIG_VIDEO_INTEL_CVS) += cvs/
diff --git a/drivers/media/i2c/wv517s.c b/drivers/media/i2c/wv517s.c
new file mode 100644
index 000000000000..43d15aadea30
--- /dev/null
+++ b/drivers/media/i2c/wv517s.c
@@ -0,0 +1,199 @@
+// SPDX-License-Identifier: GPL-2.0-only
+
+/*
+ * WV517S voice-coil motor driver
+ *
+ * Copyright (c) 2014 Intel Corporation.
+ *
+ * On the Lenovo Yoga Book the IPU bridge instantiates this actuator as a
+ * secondary I2C client of the rear camera. The bridge holds the sensor's
+ * shared power resources on while probing the actuator and adds a runtime-PM
+ * device link for subsequent accesses.
+ */
+
+#include <linux/i2c.h>
+#include <linux/module.h>
+#include <linux/pm_runtime.h>
+
+#include <media/v4l2-ctrls.h>
+#include <media/v4l2-device.h>
+
+#define WV517S_MAX_FOCUS_POSITION 1023
+#define WV517S_DEFAULT_FOCUS_POSITION 300
+
+#define WV517S_REG_FOCUS 0x41
+#define WV517S_REG_DRIVE_MODE 0x43
+#define WV517S_DRIVE_MODE_12_6_MS 0x0211
+
+struct wv517s_device {
+ struct v4l2_ctrl_handler ctrl_handler;
+ struct v4l2_subdev sd;
+ struct v4l2_ctrl *focus;
+};
+
+static inline struct wv517s_device *to_wv517s(struct v4l2_subdev *sd)
+{
+ return container_of(sd, struct wv517s_device, sd);
+}
+
+static int wv517s_write(struct i2c_client *client, u8 reg, u16 value)
+{
+ u8 buf[] = { reg, value >> 8, value };
+ int ret;
+
+ ret = i2c_master_send(client, buf, sizeof(buf));
+ if (ret < 0)
+ return ret;
+
+ return ret == sizeof(buf) ? 0 : -EIO;
+}
+
+static int wv517s_set_ctrl(struct v4l2_ctrl *ctrl)
+{
+ struct wv517s_device *wv517s = container_of(ctrl->handler,
+ struct wv517s_device,
+ ctrl_handler);
+ struct i2c_client *client = v4l2_get_subdevdata(&wv517s->sd);
+ int ret;
+
+ ret = pm_runtime_get_if_in_use(&client->dev);
+ if (ret <= 0)
+ return ret;
+
+ if (ctrl->id == V4L2_CID_FOCUS_ABSOLUTE)
+ ret = wv517s_write(client, WV517S_REG_FOCUS, ctrl->val);
+ else
+ ret = -EINVAL;
+
+ pm_runtime_put(&client->dev);
+
+ return ret;
+}
+
+static const struct v4l2_ctrl_ops wv517s_ctrl_ops = {
+ .s_ctrl = wv517s_set_ctrl,
+};
+
+static int wv517s_open(struct v4l2_subdev *sd, struct v4l2_subdev_fh *fh)
+{
+ return pm_runtime_resume_and_get(sd->dev);
+}
+
+static int wv517s_close(struct v4l2_subdev *sd, struct v4l2_subdev_fh *fh)
+{
+ pm_runtime_put(sd->dev);
+
+ return 0;
+}
+
+static const struct v4l2_subdev_internal_ops wv517s_internal_ops = {
+ .open = wv517s_open,
+ .close = wv517s_close,
+};
+
+static const struct v4l2_subdev_ops wv517s_subdev_ops = { };
+
+static int wv517s_resume(struct device *dev)
+{
+ struct v4l2_subdev *sd = dev_get_drvdata(dev);
+ struct wv517s_device *wv517s = to_wv517s(sd);
+ struct i2c_client *client = to_i2c_client(dev);
+ int ret;
+
+ /* Restore the vendor-recommended 12.6 ms ringing-control mode. */
+ ret = wv517s_write(client, WV517S_REG_DRIVE_MODE,
+ WV517S_DRIVE_MODE_12_6_MS);
+ if (ret)
+ return ret;
+
+ return wv517s_write(client, WV517S_REG_FOCUS, wv517s->focus->val);
+}
+
+static int wv517s_probe(struct i2c_client *client)
+{
+ struct wv517s_device *wv517s;
+ int ret;
+
+ wv517s = devm_kzalloc(&client->dev, sizeof(*wv517s), GFP_KERNEL);
+ if (!wv517s)
+ return -ENOMEM;
+
+ v4l2_i2c_subdev_init(&wv517s->sd, client, &wv517s_subdev_ops);
+ wv517s->sd.flags |= V4L2_SUBDEV_FL_HAS_DEVNODE;
+ wv517s->sd.internal_ops = &wv517s_internal_ops;
+ wv517s->sd.entity.function = MEDIA_ENT_F_LENS;
+
+ v4l2_ctrl_handler_init(&wv517s->ctrl_handler, 1);
+ wv517s->focus = v4l2_ctrl_new_std(&wv517s->ctrl_handler,
+ &wv517s_ctrl_ops,
+ V4L2_CID_FOCUS_ABSOLUTE, 0,
+ WV517S_MAX_FOCUS_POSITION, 1,
+ WV517S_DEFAULT_FOCUS_POSITION);
+ if (wv517s->ctrl_handler.error) {
+ ret = wv517s->ctrl_handler.error;
+ goto err_free_ctrl_handler;
+ }
+ wv517s->sd.ctrl_handler = &wv517s->ctrl_handler;
+
+ ret = media_entity_pads_init(&wv517s->sd.entity, 0, NULL);
+ if (ret)
+ goto err_free_ctrl_handler;
+
+ ret = wv517s_resume(&client->dev);
+ if (ret)
+ goto err_cleanup_entity;
+
+ pm_runtime_set_active(&client->dev);
+ pm_runtime_enable(&client->dev);
+
+ ret = v4l2_async_register_subdev(&wv517s->sd);
+ if (ret)
+ goto err_disable_pm;
+
+ pm_runtime_idle(&client->dev);
+
+ return 0;
+
+err_disable_pm:
+ pm_runtime_disable(&client->dev);
+err_cleanup_entity:
+ media_entity_cleanup(&wv517s->sd.entity);
+err_free_ctrl_handler:
+ v4l2_ctrl_handler_free(&wv517s->ctrl_handler);
+
+ return ret;
+}
+
+static void wv517s_remove(struct i2c_client *client)
+{
+ struct v4l2_subdev *sd = i2c_get_clientdata(client);
+ struct wv517s_device *wv517s = to_wv517s(sd);
+
+ v4l2_async_unregister_subdev(sd);
+ pm_runtime_disable(&client->dev);
+ v4l2_ctrl_handler_free(&wv517s->ctrl_handler);
+ media_entity_cleanup(&sd->entity);
+}
+
+static const struct i2c_device_id wv517s_id_table[] = {
+ { "wv517s" },
+ { }
+};
+MODULE_DEVICE_TABLE(i2c, wv517s_id_table);
+
+static DEFINE_RUNTIME_DEV_PM_OPS(wv517s_pm_ops, NULL, wv517s_resume, NULL);
+
+static struct i2c_driver wv517s_i2c_driver = {
+ .driver = {
+ .name = "wv517s",
+ .pm = pm_ptr(&wv517s_pm_ops),
+ },
+ .probe = wv517s_probe,
+ .remove = wv517s_remove,
+ .id_table = wv517s_id_table,
+};
+module_i2c_driver(wv517s_i2c_driver);
+
+MODULE_AUTHOR("Maurizio Casciano");
+MODULE_DESCRIPTION("WV517S VCM driver");
+MODULE_LICENSE("GPL");
--
2.53.0