summaryrefslogtreecommitdiff
path: root/drivers/media/i2c/dw9719.c
diff options
context:
space:
mode:
Diffstat (limited to 'drivers/media/i2c/dw9719.c')
-rw-r--r--drivers/media/i2c/dw9719.c467
1 files changed, 467 insertions, 0 deletions
diff --git a/drivers/media/i2c/dw9719.c b/drivers/media/i2c/dw9719.c
new file mode 100644
index 000000000000..59558335989e
--- /dev/null
+++ b/drivers/media/i2c/dw9719.c
@@ -0,0 +1,467 @@
+// SPDX-License-Identifier: GPL-2.0
+// Copyright (c) 2012 Intel Corporation
+
+/*
+ * Based on linux/modules/camera/drivers/media/i2c/imx/dw9719.c from:
+ * https://github.com/ZenfoneArea/android_kernel_asus_zenfone5 and
+ * latte-l-oss/drivers/external_drivers/camera/drivers/media/i2c/micam/dw9761.c
+ * from: https://github.com/MiCode/Xiaomi_Kernel_OpenSource/
+ */
+
+#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 DW9718S_PD CCI_REG8(0)
+
+#define DW9718S_CONTROL CCI_REG8(1)
+#define DW9718S_CONTROL_SW_LINEAR BIT(0)
+#define DW9718S_CONTROL_SAC_SHIFT 1
+#define DW9718S_CONTROL_SAC_MASK 0x7
+#define DW9718S_CONTROL_OCP_DISABLE BIT(4)
+#define DW9718S_CONTROL_UVLO_DISABLE BIT(5)
+#define DW9718S_DEFAULT_SAC 4
+
+#define DW9718S_VCM_CURRENT CCI_REG16(2)
+
+#define DW9718S_SW CCI_REG8(4)
+#define DW9718S_SW_VCM_FREQ_MASK 0xF
+#define DW9718S_DEFAULT_VCM_FREQ 0
+
+#define DW9718S_SACT CCI_REG8(5)
+#define DW9718S_SACT_PERIOD_8_8MS 0x19
+
+#define DW9719_INFO CCI_REG8(0)
+#define DW9719_ID 0xF1
+#define DW9761_ID 0xF4
+
+#define DW9719_CONTROL CCI_REG8(2)
+#define DW9719_STANDBY 0x00
+#define DW9719_SHUTDOWN 0x01
+#define DW9719_ENABLE_RINGING 0x02
+
+#define DW9719_VCM_CURRENT CCI_REG16(3)
+
+#define DW9719_STATUS CCI_REG16(5)
+#define DW9719_STATUS_BUSY BIT(0)
+
+#define DW9719_MODE CCI_REG8(6)
+#define DW9719_MODE_SAC_SHIFT 4
+#define DW9719_DEFAULT_SAC 4
+#define DW9761_DEFAULT_SAC 6
+
+#define DW9719_VCM_FREQ CCI_REG8(7)
+#define DW9719_DEFAULT_VCM_FREQ 0x60
+#define DW9761_DEFAULT_VCM_FREQ 0x3E
+
+#define DW9761_VCM_PRELOAD CCI_REG8(8)
+#define DW9761_DEFAULT_VCM_PRELOAD 0x73
+
+#define DW9800K_DEFAULT_SAC 1
+#define DW9800K_MODE_SAC_SHIFT 6
+#define DW9800K_DEFAULT_VCM_FREQ 0x10
+
+#define to_dw9719_device(x) container_of(x, struct dw9719_device, sd)
+
+enum dw9719_model {
+ DW9718S,
+ DW9719,
+ DW9761,
+ DW9800K,
+};
+
+struct dw9719_device {
+ struct v4l2_subdev sd;
+ struct device *dev;
+ struct regmap *regmap;
+ struct regulator *regulator;
+ enum dw9719_model model;
+ u32 mode_low_bits;
+ u32 sac_mode;
+ u32 vcm_freq;
+
+ struct dw9719_v4l2_ctrls {
+ struct v4l2_ctrl_handler handler;
+ struct v4l2_ctrl *focus;
+ } ctrls;
+};
+
+static int dw9719_power_down(struct dw9719_device *dw9719)
+{
+ u32 reg_pwr = dw9719->model == DW9718S ? DW9718S_PD : DW9719_CONTROL;
+
+ /*
+ * Worth engaging the internal SHUTDOWN mode especially due to the
+ * regulator being potentially shared with other devices.
+ */
+ if (cci_write(dw9719->regmap, reg_pwr, DW9719_SHUTDOWN, NULL))
+ dev_err(dw9719->dev, "Error writing to power register\n");
+ return regulator_disable(dw9719->regulator);
+}
+
+static int dw9719_power_up(struct dw9719_device *dw9719, bool detect)
+{
+ u32 reg_pwr = dw9719->model == DW9718S ? DW9718S_PD : DW9719_CONTROL;
+ u64 val;
+ int ret;
+ int err;
+
+ ret = regulator_enable(dw9719->regulator);
+ if (ret)
+ return ret;
+
+ /*
+ * Need 100us to transition from SHUTDOWN to STANDBY.
+ * Jiggle the SCL pin to wake up the device (even when the regulator is
+ * shared) and wait double the time to be sure, as 100us is not enough
+ * at least on the DW9718S as found on the motorola-nora smartphone,
+ * then retry the write.
+ */
+ cci_write(dw9719->regmap, reg_pwr, DW9719_STANDBY, NULL);
+ /* the jiggle is expected to fail, don't even log that as error */
+ fsleep(200);
+ cci_write(dw9719->regmap, reg_pwr, DW9719_STANDBY, &ret);
+
+ if (detect) {
+ /* These models do not have an INFO register */
+ switch (dw9719->model) {
+ case DW9718S:
+ dw9719->sac_mode = DW9718S_DEFAULT_SAC;
+ dw9719->vcm_freq = DW9718S_DEFAULT_VCM_FREQ;
+ goto props;
+ case DW9800K:
+ dw9719->sac_mode = DW9800K_DEFAULT_SAC;
+ dw9719->vcm_freq = DW9800K_DEFAULT_VCM_FREQ;
+ goto props;
+ default:
+ break;
+ }
+
+ ret = cci_read(dw9719->regmap, DW9719_INFO, &val, NULL);
+ if (ret < 0)
+ return ret;
+
+ switch (val) {
+ case DW9719_ID:
+ dw9719->model = DW9719;
+ dw9719->mode_low_bits = 0x00;
+ dw9719->sac_mode = DW9719_DEFAULT_SAC;
+ dw9719->vcm_freq = DW9719_DEFAULT_VCM_FREQ;
+ break;
+ case DW9761_ID:
+ dw9719->model = DW9761;
+ dw9719->mode_low_bits = 0x01;
+ dw9719->sac_mode = DW9761_DEFAULT_SAC;
+ dw9719->vcm_freq = DW9761_DEFAULT_VCM_FREQ;
+ break;
+ default:
+ dev_err(dw9719->dev,
+ "Error unknown device id 0x%02llx\n", val);
+ return -ENXIO;
+ }
+
+props:
+ /* Optional indication of SAC mode select */
+ device_property_read_u32(dw9719->dev, "dongwoon,sac-mode",
+ &dw9719->sac_mode);
+
+ /* Optional indication of VCM frequency */
+ err = device_property_read_u32(dw9719->dev, "dongwoon,vcm-freq",
+ &dw9719->vcm_freq);
+ if (err == 0)
+ dev_warn(dw9719->dev, "dongwoon,vcm-freq property is deprecated, please use dongwoon,vcm-prescale\n");
+
+ /* Optional indication of VCM prescale */
+ device_property_read_u32(dw9719->dev, "dongwoon,vcm-prescale",
+ &dw9719->vcm_freq);
+ }
+
+ switch (dw9719->model) {
+ case DW9800K:
+ cci_write(dw9719->regmap, DW9719_CONTROL, DW9719_ENABLE_RINGING, &ret);
+ cci_write(dw9719->regmap, DW9719_MODE,
+ dw9719->sac_mode << DW9800K_MODE_SAC_SHIFT, &ret);
+ cci_write(dw9719->regmap, DW9719_VCM_FREQ, dw9719->vcm_freq, &ret);
+ break;
+ case DW9718S:
+ /* Datasheet says [OCP/UVLO] should be disabled below 2.5V */
+ dw9719->sac_mode &= DW9718S_CONTROL_SAC_MASK;
+ cci_write(dw9719->regmap, DW9718S_CONTROL,
+ DW9718S_CONTROL_SW_LINEAR |
+ (dw9719->sac_mode << DW9718S_CONTROL_SAC_SHIFT) |
+ DW9718S_CONTROL_OCP_DISABLE |
+ DW9718S_CONTROL_UVLO_DISABLE, &ret);
+ cci_write(dw9719->regmap, DW9718S_SACT,
+ DW9718S_SACT_PERIOD_8_8MS, &ret);
+ cci_write(dw9719->regmap, DW9718S_SW,
+ dw9719->vcm_freq & DW9718S_SW_VCM_FREQ_MASK, &ret);
+ break;
+ case DW9761:
+ cci_write(dw9719->regmap, DW9761_VCM_PRELOAD,
+ DW9761_DEFAULT_VCM_PRELOAD, &ret);
+ fallthrough;
+ case DW9719:
+ cci_write(dw9719->regmap, DW9719_CONTROL, DW9719_ENABLE_RINGING, &ret);
+ cci_write(dw9719->regmap, DW9719_MODE, dw9719->mode_low_bits |
+ (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)
+{
+ u32 reg = dw9719->model == DW9718S ? DW9718S_VCM_CURRENT
+ : DW9719_VCM_CURRENT;
+ return cci_write(dw9719->regmap, reg, 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, false);
+ 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_autosuspend(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->model = (enum dw9719_model)(uintptr_t)i2c_get_match_data(client);
+
+ dw9719->regmap = devm_cci_regmap_init_i2c(client, 8);
+ if (IS_ERR(dw9719->regmap))
+ return PTR_ERR(dw9719->regmap);
+
+ dw9719->dev = &client->dev;
+
+ 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, true);
+ if (ret)
+ goto err_cleanup_media;
+
+ 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);
+ 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 of_device_id dw9719_of_table[] = {
+ { .compatible = "dongwoon,dw9718s", .data = (const void *)DW9718S },
+ { .compatible = "dongwoon,dw9719", .data = (const void *)DW9719 },
+ { .compatible = "dongwoon,dw9761", .data = (const void *)DW9761 },
+ { .compatible = "dongwoon,dw9800k", .data = (const void *)DW9800K },
+ { }
+};
+MODULE_DEVICE_TABLE(of, dw9719_of_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),
+ .of_match_table = dw9719_of_table,
+ },
+ .probe = dw9719_probe,
+ .remove = dw9719_remove,
+};
+module_i2c_driver(dw9719_i2c_driver);
+
+MODULE_AUTHOR("Daniel Scally <djrscally@gmail.com>");
+MODULE_DESCRIPTION("DW9719 VCM Driver");
+MODULE_LICENSE("GPL");