Skip to content

Commit

Permalink
media: i2c: Add driver for DW9719 VCM
Browse files Browse the repository at this point in the history
Add a driver for the DW9719 VCM. The driver creates a v4l2 subdevice
and registers a control to set the desired focus.

Reviewed-by: Andy Shevchenko <andy.shevchenko@gmail.com>
Signed-off-by: Daniel Scally <djrscally@gmail.com>
Signed-off-by: Hans de Goede <hdegoede@redhat.com>
Signed-off-by: Sakari Ailus <sakari.ailus@linux.intel.com>
Signed-off-by: Mauro Carvalho Chehab <mchehab@kernel.org>
  • Loading branch information
Daniel Scally authored and Mauro Carvalho Chehab committed Aug 10, 2023
1 parent f663fb4 commit fed60fc
Show file tree
Hide file tree
Showing 4 changed files with 370 additions and 0 deletions.
7 changes: 7 additions & 0 deletions MAINTAINERS
Original file line number Diff line number Diff line change
Expand Up @@ -6243,6 +6243,13 @@ T: git git://linuxtv.org/media_tree.git
F: Documentation/devicetree/bindings/media/i2c/dongwoon,dw9714.yaml
F: drivers/media/i2c/dw9714.c

DONGWOON DW9719 LENS VOICE COIL DRIVER
M: Daniel Scally <djrscally@gmail.com>
L: linux-media@vger.kernel.org
S: Maintained
T: git git://linuxtv.org/media_tree.git
F: drivers/media/i2c/dw9719.c

DONGWOON DW9768 LENS VOICE COIL DRIVER
L: linux-media@vger.kernel.org
S: Orphan
Expand Down
12 changes: 12 additions & 0 deletions drivers/media/i2c/Kconfig
Original file line number Diff line number Diff line change
Expand Up @@ -652,6 +652,18 @@ config VIDEO_DW9714
capability. This is designed for linear control of
voice coil motors, controlled via I2C serial interface.

config VIDEO_DW9719
tristate "DW9719 lens voice coil support"
depends on I2C && VIDEO_DEV
select MEDIA_CONTROLLER
select VIDEO_V4L2_SUBDEV_API
select V4L2_ASYNC
select V4L2_CCI_I2C
help
This is a driver for the DW9719 camera lens voice coil.
This is designed for linear control of voice coil motors,
controlled via I2C serial interface.

config VIDEO_DW9768
tristate "DW9768 lens voice coil support"
depends on I2C && VIDEO_DEV
Expand Down
1 change: 1 addition & 0 deletions drivers/media/i2c/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ obj-$(CONFIG_VIDEO_DS90UB913) += ds90ub913.o
obj-$(CONFIG_VIDEO_DS90UB953) += ds90ub953.o
obj-$(CONFIG_VIDEO_DS90UB960) += ds90ub960.o
obj-$(CONFIG_VIDEO_DW9714) += dw9714.o
obj-$(CONFIG_VIDEO_DW9719) += dw9719.o
obj-$(CONFIG_VIDEO_DW9768) += dw9768.o
obj-$(CONFIG_VIDEO_DW9807_VCM) += dw9807-vcm.o
obj-$(CONFIG_VIDEO_ET8EK8) += et8ek8/
Expand Down
350 changes: 350 additions & 0 deletions drivers/media/i2c/dw9719.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,350 @@
// SPDX-License-Identifier: GPL-2.0
// Copyright (c) 2012 Intel Corporation

/*
* Based on linux/modules/camera/drivers/media/i2c/imx/dw9719.c in this repo:
* https://github.com/ZenfoneArea/android_kernel_asus_zenfone5
*/

#include <linux/delay.h>
#include <linux/i2c.h>
#include <linux/pm_runtime.h>
#include <linux/regulator/consumer.h>
#include <linux/types.h>

#include <media/v4l2-cci.h>
#include <media/v4l2-common.h>
#include <media/v4l2-ctrls.h>
#include <media/v4l2-subdev.h>

#define DW9719_MAX_FOCUS_POS 1023
#define DW9719_CTRL_STEPS 16
#define DW9719_CTRL_DELAY_US 1000

#define DW9719_INFO CCI_REG8(0)
#define DW9719_ID 0xF1

#define DW9719_CONTROL CCI_REG8(2)
#define DW9719_ENABLE_RINGING 0x02

#define DW9719_VCM_CURRENT CCI_REG16(3)

#define DW9719_MODE CCI_REG8(6)
#define DW9719_MODE_SAC_SHIFT 4
#define DW9719_MODE_SAC3 4

#define DW9719_VCM_FREQ CCI_REG8(7)
#define DW9719_DEFAULT_VCM_FREQ 0x60

#define to_dw9719_device(x) container_of(x, struct dw9719_device, sd)

struct dw9719_device {
struct v4l2_subdev sd;
struct device *dev;
struct regmap *regmap;
struct regulator *regulator;
u32 sac_mode;
u32 vcm_freq;

struct dw9719_v4l2_ctrls {
struct v4l2_ctrl_handler handler;
struct v4l2_ctrl *focus;
} ctrls;
};

static int dw9719_detect(struct dw9719_device *dw9719)
{
int ret;
u64 val;

ret = cci_read(dw9719->regmap, DW9719_INFO, &val, NULL);
if (ret < 0)
return ret;

if (val != DW9719_ID) {
dev_err(dw9719->dev, "Failed to detect correct id\n");
return -ENXIO;
}

return 0;
}

static int dw9719_power_down(struct dw9719_device *dw9719)
{
return regulator_disable(dw9719->regulator);
}

static int dw9719_power_up(struct dw9719_device *dw9719)
{
int ret;

ret = regulator_enable(dw9719->regulator);
if (ret)
return ret;

/* Jiggle SCL pin to wake up device */
cci_write(dw9719->regmap, DW9719_CONTROL, 1, &ret);

/* Need 100us to transit from SHUTDOWN to STANDBY */
fsleep(100);

cci_write(dw9719->regmap, DW9719_CONTROL, DW9719_ENABLE_RINGING, &ret);
cci_write(dw9719->regmap, DW9719_MODE,
dw9719->sac_mode << DW9719_MODE_SAC_SHIFT, &ret);
cci_write(dw9719->regmap, DW9719_VCM_FREQ, dw9719->vcm_freq, &ret);

if (ret)
dw9719_power_down(dw9719);

return ret;
}

static int dw9719_t_focus_abs(struct dw9719_device *dw9719, s32 value)
{
return cci_write(dw9719->regmap, DW9719_VCM_CURRENT, value, NULL);
}

static int dw9719_set_ctrl(struct v4l2_ctrl *ctrl)
{
struct dw9719_device *dw9719 = container_of(ctrl->handler,
struct dw9719_device,
ctrls.handler);
int ret;

/* Only apply changes to the controls if the device is powered up */
if (!pm_runtime_get_if_in_use(dw9719->dev))
return 0;

switch (ctrl->id) {
case V4L2_CID_FOCUS_ABSOLUTE:
ret = dw9719_t_focus_abs(dw9719, ctrl->val);
break;
default:
ret = -EINVAL;
}

pm_runtime_put(dw9719->dev);

return ret;
}

static const struct v4l2_ctrl_ops dw9719_ctrl_ops = {
.s_ctrl = dw9719_set_ctrl,
};

static int dw9719_suspend(struct device *dev)
{
struct v4l2_subdev *sd = dev_get_drvdata(dev);
struct dw9719_device *dw9719 = to_dw9719_device(sd);
int ret;
int val;

for (val = dw9719->ctrls.focus->val; val >= 0;
val -= DW9719_CTRL_STEPS) {
ret = dw9719_t_focus_abs(dw9719, val);
if (ret)
return ret;

usleep_range(DW9719_CTRL_DELAY_US, DW9719_CTRL_DELAY_US + 10);
}

return dw9719_power_down(dw9719);
}

static int dw9719_resume(struct device *dev)
{
struct v4l2_subdev *sd = dev_get_drvdata(dev);
struct dw9719_device *dw9719 = to_dw9719_device(sd);
int current_focus = dw9719->ctrls.focus->val;
int ret;
int val;

ret = dw9719_power_up(dw9719);
if (ret)
return ret;

for (val = current_focus % DW9719_CTRL_STEPS; val < current_focus;
val += DW9719_CTRL_STEPS) {
ret = dw9719_t_focus_abs(dw9719, val);
if (ret)
goto err_power_down;

usleep_range(DW9719_CTRL_DELAY_US, DW9719_CTRL_DELAY_US + 10);
}

return 0;

err_power_down:
dw9719_power_down(dw9719);
return ret;
}

static int dw9719_open(struct v4l2_subdev *sd, struct v4l2_subdev_fh *fh)
{
return pm_runtime_resume_and_get(sd->dev);
}

static int dw9719_close(struct v4l2_subdev *sd, struct v4l2_subdev_fh *fh)
{
pm_runtime_put(sd->dev);

return 0;
}

static const struct v4l2_subdev_internal_ops dw9719_internal_ops = {
.open = dw9719_open,
.close = dw9719_close,
};

static int dw9719_init_controls(struct dw9719_device *dw9719)
{
const struct v4l2_ctrl_ops *ops = &dw9719_ctrl_ops;
int ret;

v4l2_ctrl_handler_init(&dw9719->ctrls.handler, 1);

dw9719->ctrls.focus = v4l2_ctrl_new_std(&dw9719->ctrls.handler, ops,
V4L2_CID_FOCUS_ABSOLUTE, 0,
DW9719_MAX_FOCUS_POS, 1, 0);

if (dw9719->ctrls.handler.error) {
dev_err(dw9719->dev, "Error initialising v4l2 ctrls\n");
ret = dw9719->ctrls.handler.error;
goto err_free_handler;
}

dw9719->sd.ctrl_handler = &dw9719->ctrls.handler;
return 0;

err_free_handler:
v4l2_ctrl_handler_free(&dw9719->ctrls.handler);
return ret;
}

static const struct v4l2_subdev_ops dw9719_ops = { };

static int dw9719_probe(struct i2c_client *client)
{
struct dw9719_device *dw9719;
int ret;

dw9719 = devm_kzalloc(&client->dev, sizeof(*dw9719), GFP_KERNEL);
if (!dw9719)
return -ENOMEM;

dw9719->regmap = devm_cci_regmap_init_i2c(client, 8);
if (IS_ERR(dw9719->regmap))
return PTR_ERR(dw9719->regmap);

dw9719->dev = &client->dev;
dw9719->sac_mode = DW9719_MODE_SAC3;
dw9719->vcm_freq = DW9719_DEFAULT_VCM_FREQ;

/* Optional indication of SAC mode select */
device_property_read_u32(&client->dev, "dongwoon,sac-mode",
&dw9719->sac_mode);

/* Optional indication of VCM frequency */
device_property_read_u32(&client->dev, "dongwoon,vcm-freq",
&dw9719->vcm_freq);

dw9719->regulator = devm_regulator_get(&client->dev, "vdd");
if (IS_ERR(dw9719->regulator))
return dev_err_probe(&client->dev, PTR_ERR(dw9719->regulator),
"getting regulator\n");

v4l2_i2c_subdev_init(&dw9719->sd, client, &dw9719_ops);
dw9719->sd.flags |= V4L2_SUBDEV_FL_HAS_DEVNODE;
dw9719->sd.internal_ops = &dw9719_internal_ops;

ret = dw9719_init_controls(dw9719);
if (ret)
return ret;

ret = media_entity_pads_init(&dw9719->sd.entity, 0, NULL);
if (ret < 0)
goto err_free_ctrl_handler;

dw9719->sd.entity.function = MEDIA_ENT_F_LENS;

/*
* We need the driver to work in the event that pm runtime is disable in
* the kernel, so power up and verify the chip now. In the event that
* runtime pm is disabled this will leave the chip on, so that the lens
* will work.
*/

ret = dw9719_power_up(dw9719);
if (ret)
goto err_cleanup_media;

ret = dw9719_detect(dw9719);
if (ret)
goto err_powerdown;

pm_runtime_set_active(&client->dev);
pm_runtime_get_noresume(&client->dev);
pm_runtime_enable(&client->dev);

ret = v4l2_async_register_subdev(&dw9719->sd);
if (ret < 0)
goto err_pm_runtime;

pm_runtime_set_autosuspend_delay(&client->dev, 1000);
pm_runtime_use_autosuspend(&client->dev);
pm_runtime_put_autosuspend(&client->dev);

return ret;

err_pm_runtime:
pm_runtime_disable(&client->dev);
pm_runtime_put_noidle(&client->dev);
err_powerdown:
dw9719_power_down(dw9719);
err_cleanup_media:
media_entity_cleanup(&dw9719->sd.entity);
err_free_ctrl_handler:
v4l2_ctrl_handler_free(&dw9719->ctrls.handler);

return ret;
}

static void dw9719_remove(struct i2c_client *client)
{
struct v4l2_subdev *sd = i2c_get_clientdata(client);
struct dw9719_device *dw9719 =
container_of(sd, struct dw9719_device, sd);

v4l2_async_unregister_subdev(sd);
v4l2_ctrl_handler_free(&dw9719->ctrls.handler);
media_entity_cleanup(&dw9719->sd.entity);

pm_runtime_disable(&client->dev);
if (!pm_runtime_status_suspended(&client->dev))
dw9719_power_down(dw9719);
pm_runtime_set_suspended(&client->dev);
}

static const struct i2c_device_id dw9719_id_table[] = {
{ "dw9719" },
{ }
};
MODULE_DEVICE_TABLE(i2c, dw9719_id_table);

static DEFINE_RUNTIME_DEV_PM_OPS(dw9719_pm_ops, dw9719_suspend, dw9719_resume,
NULL);

static struct i2c_driver dw9719_i2c_driver = {
.driver = {
.name = "dw9719",
.pm = pm_sleep_ptr(&dw9719_pm_ops),
},
.probe = dw9719_probe,
.remove = dw9719_remove,
.id_table = dw9719_id_table,
};
module_i2c_driver(dw9719_i2c_driver);

MODULE_AUTHOR("Daniel Scally <djrscally@gmail.com>");
MODULE_DESCRIPTION("DW9719 VCM Driver");
MODULE_LICENSE("GPL");

0 comments on commit fed60fc

Please sign in to comment.