| // SPDX-License-Identifier: GPL-2.0 |
| /* |
| * Maxim MAX9286 Quad GMSL2 Deserializer Driver |
| * |
| * Copyright (C) 2021 Renesas Electronics Corporation |
| * Copyright (C) 2021 Niklas Söderlund |
| */ |
| |
| #include <linux/delay.h> |
| #include <linux/i2c.h> |
| #include <linux/module.h> |
| #include <linux/of_graph.h> |
| #include <linux/regmap.h> |
| |
| #include <media/v4l2-ctrls.h> |
| #include <media/v4l2-fwnode.h> |
| #include <media/v4l2-subdev.h> |
| |
| #define MAX96712_ID 0x20 |
| |
| #define MAX96712_DPLL_FREQ 1000 |
| |
| enum max96712_pattern { |
| MAX96712_PATTERN_CHECKERBOARD = 0, |
| MAX96712_PATTERN_GRADIENT, |
| }; |
| |
| struct max96712_priv { |
| struct i2c_client *client; |
| struct regmap *regmap; |
| struct gpio_desc *gpiod_pwdn; |
| |
| struct v4l2_mbus_config_mipi_csi2 mipi; |
| |
| struct v4l2_subdev sd; |
| struct v4l2_ctrl_handler ctrl_handler; |
| struct media_pad pads[1]; |
| |
| enum max96712_pattern pattern; |
| }; |
| |
| static int max96712_read(struct max96712_priv *priv, int reg) |
| { |
| int ret, val; |
| |
| ret = regmap_read(priv->regmap, reg, &val); |
| if (ret) { |
| dev_err(&priv->client->dev, "read 0x%04x failed\n", reg); |
| return ret; |
| } |
| |
| return val; |
| } |
| |
| static int max96712_write(struct max96712_priv *priv, unsigned int reg, u8 val) |
| { |
| int ret; |
| |
| ret = regmap_write(priv->regmap, reg, val); |
| if (ret) |
| dev_err(&priv->client->dev, "write 0x%04x failed\n", reg); |
| |
| return ret; |
| } |
| |
| static int max96712_update_bits(struct max96712_priv *priv, unsigned int reg, |
| u8 mask, u8 val) |
| { |
| int ret; |
| |
| ret = regmap_update_bits(priv->regmap, reg, mask, val); |
| if (ret) |
| dev_err(&priv->client->dev, "update 0x%04x failed\n", reg); |
| |
| return ret; |
| } |
| |
| static int max96712_write_bulk(struct max96712_priv *priv, unsigned int reg, |
| const void *val, size_t val_count) |
| { |
| int ret; |
| |
| ret = regmap_bulk_write(priv->regmap, reg, val, val_count); |
| if (ret) |
| dev_err(&priv->client->dev, "bulk write 0x%04x failed\n", reg); |
| |
| return ret; |
| } |
| |
| static int max96712_write_bulk_value(struct max96712_priv *priv, |
| unsigned int reg, unsigned int val, |
| size_t val_count) |
| { |
| unsigned int i; |
| u8 values[4]; |
| |
| for (i = 1; i <= val_count; i++) |
| values[i - 1] = (val >> ((val_count - i) * 8)) & 0xff; |
| |
| return max96712_write_bulk(priv, reg, &values, val_count); |
| } |
| |
| static void max96712_reset(struct max96712_priv *priv) |
| { |
| max96712_update_bits(priv, 0x13, 0x40, 0x40); |
| msleep(20); |
| } |
| |
| static void max96712_mipi_enable(struct max96712_priv *priv, bool enable) |
| { |
| if (enable) { |
| max96712_update_bits(priv, 0x40b, 0x02, 0x02); |
| max96712_update_bits(priv, 0x8a0, 0x80, 0x80); |
| } else { |
| max96712_update_bits(priv, 0x8a0, 0x80, 0x00); |
| max96712_update_bits(priv, 0x40b, 0x02, 0x00); |
| } |
| } |
| |
| static void max96712_mipi_configure(struct max96712_priv *priv) |
| { |
| unsigned int i; |
| u8 phy5 = 0; |
| |
| max96712_mipi_enable(priv, false); |
| |
| /* Select 2x4 mode. */ |
| max96712_write(priv, 0x8a0, 0x04); |
| |
| /* Configure a 4-lane DPHY using PHY0 and PHY1. */ |
| /* TODO: Add support for 2-lane and 1-lane configurations. */ |
| /* TODO: Add support CPHY mode. */ |
| max96712_write(priv, 0x94a, 0xc0); |
| |
| /* Configure lane mapping for PHY0 and PHY1. */ |
| /* TODO: Add support for lane swapping. */ |
| max96712_write(priv, 0x8a3, 0xe4); |
| |
| /* Configure lane polarity for PHY0 and PHY1. */ |
| for (i = 0; i < priv->mipi.num_data_lanes + 1; i++) |
| if (priv->mipi.lane_polarities[i]) |
| phy5 |= BIT(i == 0 ? 5 : i < 3 ? i - 1 : i); |
| max96712_write(priv, 0x8a5, phy5); |
| |
| /* Set link frequency for PHY0 and PHY1. */ |
| max96712_update_bits(priv, 0x415, 0x3f, |
| ((MAX96712_DPLL_FREQ / 100) & 0x1f) | BIT(5)); |
| max96712_update_bits(priv, 0x418, 0x3f, |
| ((MAX96712_DPLL_FREQ / 100) & 0x1f) | BIT(5)); |
| |
| /* Enable PHY0 and PHY1 */ |
| max96712_update_bits(priv, 0x8a2, 0xf0, 0x30); |
| } |
| |
| static void max96712_pattern_enable(struct max96712_priv *priv, bool enable) |
| { |
| const u32 h_active = 1920; |
| const u32 h_fp = 88; |
| const u32 h_sw = 44; |
| const u32 h_bp = 148; |
| const u32 h_tot = h_active + h_fp + h_sw + h_bp; |
| |
| const u32 v_active = 1080; |
| const u32 v_fp = 4; |
| const u32 v_sw = 5; |
| const u32 v_bp = 36; |
| const u32 v_tot = v_active + v_fp + v_sw + v_bp; |
| |
| if (!enable) { |
| max96712_write(priv, 0x1051, 0x00); |
| return; |
| } |
| |
| /* PCLK 75MHz. */ |
| max96712_write(priv, 0x0009, 0x01); |
| |
| /* Configure Video Timing Generator for 1920x1080 @ 30 fps. */ |
| max96712_write_bulk_value(priv, 0x1052, 0, 3); |
| max96712_write_bulk_value(priv, 0x1055, v_sw * h_tot, 3); |
| max96712_write_bulk_value(priv, 0x1058, |
| (v_active + v_fp + + v_bp) * h_tot, 3); |
| max96712_write_bulk_value(priv, 0x105b, 0, 3); |
| max96712_write_bulk_value(priv, 0x105e, h_sw, 2); |
| max96712_write_bulk_value(priv, 0x1060, h_active + h_fp + h_bp, 2); |
| max96712_write_bulk_value(priv, 0x1062, v_tot, 2); |
| max96712_write_bulk_value(priv, 0x1064, |
| h_tot * (v_sw + v_bp) + (h_sw + h_bp), 3); |
| max96712_write_bulk_value(priv, 0x1067, h_active, 2); |
| max96712_write_bulk_value(priv, 0x1069, h_fp + h_sw + h_bp, 2); |
| max96712_write_bulk_value(priv, 0x106b, v_active, 2); |
| |
| /* Generate VS, HS and DE in free-running mode. */ |
| max96712_write(priv, 0x1050, 0xfb); |
| |
| /* Configure Video Pattern Generator. */ |
| if (priv->pattern == MAX96712_PATTERN_CHECKERBOARD) { |
| /* Set checkerboard pattern size. */ |
| max96712_write(priv, 0x1074, 0x3c); |
| max96712_write(priv, 0x1075, 0x3c); |
| max96712_write(priv, 0x1076, 0x3c); |
| |
| /* Set checkerboard pattern colors. */ |
| max96712_write_bulk_value(priv, 0x106e, 0xfecc00, 3); |
| max96712_write_bulk_value(priv, 0x1071, 0x006aa7, 3); |
| |
| /* Generate checkerboard pattern. */ |
| max96712_write(priv, 0x1051, 0x10); |
| } else { |
| /* Set gradient increment. */ |
| max96712_write(priv, 0x106d, 0x10); |
| |
| /* Generate gradient pattern. */ |
| max96712_write(priv, 0x1051, 0x20); |
| } |
| } |
| |
| static int max96712_s_stream(struct v4l2_subdev *sd, int enable) |
| { |
| struct max96712_priv *priv = v4l2_get_subdevdata(sd); |
| |
| if (enable) { |
| max96712_pattern_enable(priv, true); |
| max96712_mipi_enable(priv, true); |
| } else { |
| max96712_mipi_enable(priv, false); |
| max96712_pattern_enable(priv, false); |
| } |
| |
| return 0; |
| } |
| |
| static const struct v4l2_subdev_video_ops max96712_video_ops = { |
| .s_stream = max96712_s_stream, |
| }; |
| |
| static int max96712_get_pad_format(struct v4l2_subdev *sd, |
| struct v4l2_subdev_state *sd_state, |
| struct v4l2_subdev_format *format) |
| { |
| format->format.width = 1920; |
| format->format.height = 1080; |
| format->format.code = MEDIA_BUS_FMT_RGB888_1X24; |
| format->format.field = V4L2_FIELD_NONE; |
| |
| return 0; |
| } |
| |
| static const struct v4l2_subdev_pad_ops max96712_pad_ops = { |
| .get_fmt = max96712_get_pad_format, |
| .set_fmt = max96712_get_pad_format, |
| }; |
| |
| static const struct v4l2_subdev_ops max96712_subdev_ops = { |
| .video = &max96712_video_ops, |
| .pad = &max96712_pad_ops, |
| }; |
| |
| static const char * const max96712_test_pattern[] = { |
| "Checkerboard", |
| "Gradient", |
| }; |
| |
| static int max96712_s_ctrl(struct v4l2_ctrl *ctrl) |
| { |
| struct max96712_priv *priv = |
| container_of(ctrl->handler, struct max96712_priv, ctrl_handler); |
| |
| switch (ctrl->id) { |
| case V4L2_CID_TEST_PATTERN: |
| priv->pattern = ctrl->val ? |
| MAX96712_PATTERN_GRADIENT : |
| MAX96712_PATTERN_CHECKERBOARD; |
| break; |
| } |
| return 0; |
| } |
| |
| static const struct v4l2_ctrl_ops max96712_ctrl_ops = { |
| .s_ctrl = max96712_s_ctrl, |
| }; |
| |
| static int max96712_v4l2_register(struct max96712_priv *priv) |
| { |
| long pixel_rate; |
| int ret; |
| |
| v4l2_i2c_subdev_init(&priv->sd, priv->client, &max96712_subdev_ops); |
| priv->sd.flags |= V4L2_SUBDEV_FL_HAS_DEVNODE; |
| priv->sd.entity.function = MEDIA_ENT_F_VID_IF_BRIDGE; |
| |
| v4l2_ctrl_handler_init(&priv->ctrl_handler, 2); |
| |
| /* |
| * TODO: Once V4L2_CID_LINK_FREQ is changed from a menu control to an |
| * INT64 control it should be used here instead of V4L2_CID_PIXEL_RATE. |
| */ |
| pixel_rate = MAX96712_DPLL_FREQ / priv->mipi.num_data_lanes * 1000000; |
| v4l2_ctrl_new_std(&priv->ctrl_handler, NULL, V4L2_CID_PIXEL_RATE, |
| pixel_rate, pixel_rate, 1, pixel_rate); |
| |
| v4l2_ctrl_new_std_menu_items(&priv->ctrl_handler, &max96712_ctrl_ops, |
| V4L2_CID_TEST_PATTERN, |
| ARRAY_SIZE(max96712_test_pattern) - 1, |
| 0, 0, max96712_test_pattern); |
| |
| priv->sd.ctrl_handler = &priv->ctrl_handler; |
| ret = priv->ctrl_handler.error; |
| if (ret) |
| goto error; |
| |
| priv->pads[0].flags = MEDIA_PAD_FL_SOURCE; |
| ret = media_entity_pads_init(&priv->sd.entity, 1, priv->pads); |
| if (ret) |
| goto error; |
| |
| v4l2_set_subdevdata(&priv->sd, priv); |
| |
| ret = v4l2_async_register_subdev(&priv->sd); |
| if (ret < 0) { |
| dev_err(&priv->client->dev, "Unable to register subdevice\n"); |
| goto error; |
| } |
| |
| return 0; |
| error: |
| v4l2_ctrl_handler_free(&priv->ctrl_handler); |
| |
| return ret; |
| } |
| |
| static int max96712_parse_dt(struct max96712_priv *priv) |
| { |
| struct fwnode_handle *ep; |
| struct v4l2_fwnode_endpoint v4l2_ep = { |
| .bus_type = V4L2_MBUS_CSI2_DPHY |
| }; |
| int ret; |
| |
| ep = fwnode_graph_get_endpoint_by_id(dev_fwnode(&priv->client->dev), 4, |
| 0, 0); |
| if (!ep) { |
| dev_err(&priv->client->dev, "Not connected to subdevice\n"); |
| return -EINVAL; |
| } |
| |
| ret = v4l2_fwnode_endpoint_parse(ep, &v4l2_ep); |
| fwnode_handle_put(ep); |
| if (ret) { |
| dev_err(&priv->client->dev, "Could not parse v4l2 endpoint\n"); |
| return -EINVAL; |
| } |
| |
| if (v4l2_ep.bus.mipi_csi2.num_data_lanes != 4) { |
| dev_err(&priv->client->dev, "Only 4 data lanes supported\n"); |
| return -EINVAL; |
| } |
| |
| priv->mipi = v4l2_ep.bus.mipi_csi2; |
| |
| return 0; |
| } |
| |
| static const struct regmap_config max96712_i2c_regmap = { |
| .reg_bits = 16, |
| .val_bits = 8, |
| .max_register = 0x1f00, |
| }; |
| |
| static int max96712_probe(struct i2c_client *client) |
| { |
| struct max96712_priv *priv; |
| int ret; |
| |
| priv = devm_kzalloc(&client->dev, sizeof(*priv), GFP_KERNEL); |
| if (!priv) |
| return -ENOMEM; |
| |
| priv->client = client; |
| i2c_set_clientdata(client, priv); |
| |
| priv->regmap = devm_regmap_init_i2c(client, &max96712_i2c_regmap); |
| if (IS_ERR(priv->regmap)) |
| return PTR_ERR(priv->regmap); |
| |
| priv->gpiod_pwdn = devm_gpiod_get_optional(&client->dev, "enable", |
| GPIOD_OUT_HIGH); |
| if (IS_ERR(priv->gpiod_pwdn)) |
| return PTR_ERR(priv->gpiod_pwdn); |
| |
| gpiod_set_consumer_name(priv->gpiod_pwdn, "max96712-pwdn"); |
| gpiod_set_value_cansleep(priv->gpiod_pwdn, 1); |
| |
| if (priv->gpiod_pwdn) |
| usleep_range(4000, 5000); |
| |
| if (max96712_read(priv, 0x4a) != MAX96712_ID) |
| return -ENODEV; |
| |
| max96712_reset(priv); |
| |
| ret = max96712_parse_dt(priv); |
| if (ret) |
| return ret; |
| |
| max96712_mipi_configure(priv); |
| |
| return max96712_v4l2_register(priv); |
| } |
| |
| static void max96712_remove(struct i2c_client *client) |
| { |
| struct max96712_priv *priv = i2c_get_clientdata(client); |
| |
| v4l2_async_unregister_subdev(&priv->sd); |
| |
| gpiod_set_value_cansleep(priv->gpiod_pwdn, 0); |
| } |
| |
| static const struct of_device_id max96712_of_table[] = { |
| { .compatible = "maxim,max96712" }, |
| { /* sentinel */ }, |
| }; |
| MODULE_DEVICE_TABLE(of, max96712_of_table); |
| |
| static struct i2c_driver max96712_i2c_driver = { |
| .driver = { |
| .name = "max96712", |
| .of_match_table = of_match_ptr(max96712_of_table), |
| }, |
| .probe_new = max96712_probe, |
| .remove = max96712_remove, |
| }; |
| |
| module_i2c_driver(max96712_i2c_driver); |
| |
| MODULE_DESCRIPTION("Maxim MAX96712 Quad GMSL2 Deserializer Driver"); |
| MODULE_AUTHOR("Niklas Söderlund <niklas.soderlund@ragnatech.se>"); |
| MODULE_LICENSE("GPL"); |