// SPDX-License-Identifier: GPL-2.0 /* * SmartSens SC235HAI CMOS Image Sensor driver * * Copyright (C) 2026 * * Based on IMX219 and other V4L2 sensor drivers */ #include #include #include #include #include #include #include #include #include #include #include #include #define SC235HAI_REG_CHIP_ID_H 0x3107 #define SC235HAI_REG_CHIP_ID_L 0x3108 #define SC235HAI_CHIP_ID 0xcb6a #define SC235HAI_REG_MODE_SELECT 0x0100 #define SC235HAI_MODE_STANDBY 0x00 #define SC235HAI_MODE_STREAMING 0x01 /* Chip ID */ #define SC235HAI_REG_LOW_POWER 0x302c /* Default initial values */ #define SC235HAI_DEFAULT_WIDTH 1920 #define SC235HAI_DEFAULT_HEIGHT 1080 #define SC235HAI_DEFAULT_FPS 30 struct sc235hai_mode { u32 width; u32 height; u32 hts; u32 vts; u32 fps; const struct sc235hai_reg *reg_list; u32 num_regs; }; struct sc235hai_reg { u16 addr; u8 val; }; struct sc235hai { struct v4l2_subdev sd; struct media_pad pad; struct v4l2_ctrl_handler ctrl_handler; struct clk *xclk; struct gpio_desc *reset_gpio; struct regulator_bulk_data supplies[3]; const struct sc235hai_mode *cur_mode; struct v4l2_ctrl *exposure; struct v4l2_ctrl *gain; struct mutex lock; bool streaming; }; static inline struct sc235hai *to_sc235hai(struct v4l2_subdev *sd) { return container_of(sd, struct sc235hai, sd); } /* SC231AI 1080p30 initialization sequence from vendor */ static const struct sc235hai_reg sc235hai_1080p_regs[] = { {0x0103, 0x01}, {0x36e9, 0x80}, {0x37f9, 0x80}, {0x301f, 0x05}, {0x3058, 0x21}, {0x3059, 0x53}, {0x305a, 0x40}, {0x3250, 0x00}, {0x3301, 0x0a}, {0x3302, 0x20}, {0x3304, 0x90}, {0x3305, 0x00}, {0x3306, 0x68}, {0x3309, 0xd0}, {0x330b, 0xd8}, {0x330d, 0x08}, {0x331c, 0x04}, {0x331e, 0x81}, {0x331f, 0xc1}, {0x3323, 0x06}, {0x3333, 0x10}, {0x3334, 0x40}, {0x3364, 0x5e}, {0x336c, 0x8e}, {0x337f, 0x13}, {0x338f, 0x80}, {0x3390, 0x08}, {0x3391, 0x18}, {0x3392, 0xb8}, {0x3393, 0x0e}, {0x3394, 0x14}, {0x3395, 0x10}, {0x3396, 0x88}, {0x3397, 0x98}, {0x3398, 0xf8}, {0x3399, 0x0a}, {0x339a, 0x0e}, {0x339b, 0x10}, {0x339c, 0x16}, {0x33ae, 0x80}, {0x33af, 0xc0}, {0x33b1, 0x80}, {0x33b2, 0x50}, {0x33b3, 0x14}, {0x33f8, 0x00}, {0x33f9, 0x68}, {0x33fa, 0x00}, {0x33fb, 0x68}, {0x33fc, 0x48}, {0x33fd, 0x78}, {0x349f, 0x03}, {0x34a6, 0x40}, {0x34a7, 0x58}, {0x34a8, 0x10}, {0x34a9, 0x10}, {0x34f8, 0x78}, {0x34f9, 0x10}, {0x3619, 0x20}, {0x361a, 0x90}, {0x3633, 0x44}, {0x3637, 0x5c}, {0x363c, 0xc0}, {0x363d, 0x02}, {0x3660, 0x80}, {0x3661, 0x81}, {0x3662, 0x8f}, {0x3663, 0x81}, {0x3664, 0x81}, {0x3665, 0x82}, {0x3666, 0x8f}, {0x3667, 0x08}, {0x3668, 0x80}, {0x3669, 0x88}, {0x366a, 0x98}, {0x366b, 0xb8}, {0x366c, 0xf8}, {0x3670, 0xb2}, {0x3671, 0xa2}, {0x3672, 0x88}, {0x3680, 0x33}, {0x3681, 0x33}, {0x3682, 0x43}, {0x36c0, 0x80}, {0x36c1, 0x88}, {0x36c8, 0x88}, {0x36c9, 0xb8}, {0x36ea, 0x0b}, {0x36eb, 0x0c}, {0x36ec, 0x5c}, {0x36ed, 0x24}, {0x3718, 0x04}, {0x3722, 0x8b}, {0x3724, 0xd1}, {0x3741, 0x08}, {0x3770, 0x17}, {0x3771, 0x9b}, {0x3772, 0x9b}, {0x37c0, 0x88}, {0x37c1, 0xb8}, {0x37fa, 0x0b}, {0x37fc, 0x10}, {0x37fd, 0x24}, {0x3902, 0xc0}, {0x3903, 0x40}, {0x3909, 0x00}, {0x391f, 0x41}, {0x3926, 0xe0}, {0x3933, 0x80}, {0x3934, 0x02}, {0x3937, 0x6f}, {0x3e00, 0x00}, {0x3e01, 0x8b}, {0x3e02, 0xf0}, {0x3e08, 0x00}, {0x4509, 0x20}, {0x450d, 0x07}, {0x4837, 0x36}, {0x5780, 0x76}, {0x5784, 0x10}, {0x5787, 0x0a}, {0x5788, 0x0a}, {0x5789, 0x08}, {0x578a, 0x0a}, {0x578b, 0x0a}, {0x578c, 0x08}, {0x578d, 0x40}, {0x5792, 0x04}, {0x5795, 0x04}, {0x57ac, 0x00}, {0x57ad, 0x00}, {0x36e9, 0x20}, {0x37f9, 0x20}, {0x0100, 0x01}, /* Stream on */ }; static const struct sc235hai_mode sc235hai_modes[] = { { .width = 1920, .height = 1080, .hts = 2200, .vts = 1125, .fps = 30, .reg_list = sc235hai_1080p_regs, .num_regs = ARRAY_SIZE(sc235hai_1080p_regs), }, }; static int sc235hai_write_reg(struct sc235hai *sc235hai, u16 reg, u8 val) { struct i2c_client *client = v4l2_get_subdevdata(&sc235hai->sd); u8 buf[3] = {reg >> 8, reg & 0xff, val}; int ret; ret = i2c_master_send(client, buf, 3); if (ret < 0) { dev_err(&client->dev, "Write reg 0x%04x failed: %d\n", reg, ret); return ret; } return 0; } static int sc235hai_read_reg(struct sc235hai *sc235hai, u16 reg, u8 *val) { struct i2c_client *client = v4l2_get_subdevdata(&sc235hai->sd); u8 buf[2] = {reg >> 8, reg & 0xff}; struct i2c_msg msgs[] = { { .addr = client->addr, .flags = 0, .len = 2, .buf = buf, }, { .addr = client->addr, .flags = I2C_M_RD, .len = 1, .buf = val, }, }; int ret; ret = i2c_transfer(client->adapter, msgs, ARRAY_SIZE(msgs)); if (ret < 0) { dev_err(&client->dev, "Read reg 0x%04x failed: %d\n", reg, ret); return ret; } return 0; } static int sc235hai_check_hwcfg(struct device *dev) { struct fwnode_handle *ep; struct v4l2_fwnode_endpoint bus_cfg = { .bus_type = V4L2_MBUS_CSI2_DPHY }; u32 lanes; int ret; ep = fwnode_graph_get_next_endpoint(dev_fwnode(dev), NULL); if (!ep) return -ENXIO; ret = v4l2_fwnode_endpoint_alloc_parse(ep, &bus_cfg); fwnode_handle_put(ep); if (ret) return ret; if (bus_cfg.bus_type != V4L2_MBUS_CSI2_DPHY) { dev_err(dev, "Unsupported bus type %d\n", bus_cfg.bus_type); ret = -EINVAL; goto out; } lanes = bus_cfg.bus.mipi_csi2.num_data_lanes; if (lanes != 2) { dev_err(dev, "Expected 2 data lanes, got %d\n", lanes); ret = -EINVAL; } out: v4l2_fwnode_endpoint_free(&bus_cfg); return ret; } static int sc235hai_start_streaming(struct sc235hai *sc235hai) { int i, ret; u8 val; struct i2c_client *client = v4l2_get_subdevdata(&sc235hai->sd); /* Apply mode settings */ for (i = 0; i < sc235hai->cur_mode->num_regs; i++) { ret = sc235hai_write_reg(sc235hai, sc235hai->cur_mode->reg_list[i].addr, sc235hai->cur_mode->reg_list[i].val); if (ret) return ret; } /* Verify critical exposure/gain registers */ dev_info(&client->dev, "=== Verifying registers ===\n"); sc235hai_read_reg(sc235hai, 0x3e00, &val); dev_info(&client->dev, "0x3e00 = 0x%02x (expected 0x00)\n", val); sc235hai_read_reg(sc235hai, 0x3e01, &val); dev_info(&client->dev, "0x3e01 = 0x%02x (expected 0x8b)\n", val); sc235hai_read_reg(sc235hai, 0x3e02, &val); dev_info(&client->dev, "0x3e02 = 0x%02x (expected 0xf0)\n", val); sc235hai_read_reg(sc235hai, 0x3e08, &val); dev_info(&client->dev, "0x3e08 = 0x%02x (expected 0x00)\n", val); /* Enable streaming */ ret = sc235hai_write_reg(sc235hai, SC235HAI_REG_MODE_SELECT, SC235HAI_MODE_STREAMING); if (ret) return ret; msleep(10); return 0; } static int sc235hai_stop_streaming(struct sc235hai *sc235hai) { return sc235hai_write_reg(sc235hai, SC235HAI_REG_MODE_SELECT, SC235HAI_MODE_STANDBY); } /* Supported media bus formats — SC235HAI outputs RAW10 Bayer BGGR */ static const u32 sc235hai_mbus_fmts[] = { MEDIA_BUS_FMT_SBGGR10_1X10, }; static int sc235hai_enum_mbus_code(struct v4l2_subdev *sd, struct v4l2_subdev_state *sd_state, struct v4l2_subdev_mbus_code_enum *code) { if (code->index >= ARRAY_SIZE(sc235hai_mbus_fmts)) return -EINVAL; code->code = sc235hai_mbus_fmts[code->index]; return 0; } static int sc235hai_enum_frame_size(struct v4l2_subdev *sd, struct v4l2_subdev_state *sd_state, struct v4l2_subdev_frame_size_enum *fse) { if (fse->index >= ARRAY_SIZE(sc235hai_modes) || fse->code != MEDIA_BUS_FMT_SBGGR10_1X10) return -EINVAL; fse->min_width = sc235hai_modes[fse->index].width; fse->max_width = sc235hai_modes[fse->index].width; fse->min_height = sc235hai_modes[fse->index].height; fse->max_height = sc235hai_modes[fse->index].height; return 0; } static void sc235hai_fill_fmt(const struct sc235hai_mode *mode, struct v4l2_mbus_framefmt *fmt) { fmt->width = mode->width; fmt->height = mode->height; fmt->code = MEDIA_BUS_FMT_SBGGR10_1X10; fmt->field = V4L2_FIELD_NONE; fmt->colorspace = V4L2_COLORSPACE_RAW; fmt->ycbcr_enc = V4L2_YCBCR_ENC_DEFAULT; fmt->quantization = V4L2_QUANTIZATION_DEFAULT; fmt->xfer_func = V4L2_XFER_FUNC_NONE; } static int sc235hai_get_fmt(struct v4l2_subdev *sd, struct v4l2_subdev_state *sd_state, struct v4l2_subdev_format *format) { struct sc235hai *sc235hai = to_sc235hai(sd); mutex_lock(&sc235hai->lock); sc235hai_fill_fmt(sc235hai->cur_mode, &format->format); mutex_unlock(&sc235hai->lock); return 0; } static int sc235hai_set_fmt(struct v4l2_subdev *sd, struct v4l2_subdev_state *sd_state, struct v4l2_subdev_format *format) { struct sc235hai *sc235hai = to_sc235hai(sd); /* Single mode only — return the fixed format */ mutex_lock(&sc235hai->lock); sc235hai_fill_fmt(sc235hai->cur_mode, &format->format); mutex_unlock(&sc235hai->lock); return 0; } static const struct v4l2_subdev_pad_ops sc235hai_pad_ops = { .enum_mbus_code = sc235hai_enum_mbus_code, .enum_frame_size = sc235hai_enum_frame_size, .get_fmt = sc235hai_get_fmt, .set_fmt = sc235hai_set_fmt, }; static int sc235hai_s_stream(struct v4l2_subdev *sd, int enable) { struct sc235hai *sc235hai = to_sc235hai(sd); int ret = 0; mutex_lock(&sc235hai->lock); if (sc235hai->streaming == enable) { mutex_unlock(&sc235hai->lock); return 0; } if (enable) { ret = pm_runtime_get_sync(sd->dev); if (ret < 0) { pm_runtime_put_noidle(sd->dev); goto unlock; } ret = sc235hai_start_streaming(sc235hai); if (ret) { pm_runtime_put(sd->dev); goto unlock; } } else { sc235hai_stop_streaming(sc235hai); pm_runtime_put(sd->dev); } sc235hai->streaming = enable; unlock: mutex_unlock(&sc235hai->lock); return ret; } static const struct v4l2_subdev_video_ops sc235hai_video_ops = { .s_stream = sc235hai_s_stream, }; static const struct v4l2_subdev_ops sc235hai_subdev_ops = { .video = &sc235hai_video_ops, .pad = &sc235hai_pad_ops, }; static int sc235hai_power_on(struct device *dev) { struct v4l2_subdev *sd = dev_get_drvdata(dev); struct sc235hai *sc235hai = to_sc235hai(sd); int ret; ret = regulator_bulk_enable(ARRAY_SIZE(sc235hai->supplies), sc235hai->supplies); if (ret) { dev_err(dev, "Failed to enable regulators: %d\n", ret); return ret; } ret = clk_prepare_enable(sc235hai->xclk); if (ret) { dev_err(dev, "Failed to enable clock: %d\n", ret); regulator_bulk_disable(ARRAY_SIZE(sc235hai->supplies), sc235hai->supplies); return ret; } gpiod_set_value_cansleep(sc235hai->reset_gpio, 0); msleep(20); return 0; } static int sc235hai_power_off(struct device *dev) { struct v4l2_subdev *sd = dev_get_drvdata(dev); struct sc235hai *sc235hai = to_sc235hai(sd); gpiod_set_value_cansleep(sc235hai->reset_gpio, 1); clk_disable_unprepare(sc235hai->xclk); regulator_bulk_disable(ARRAY_SIZE(sc235hai->supplies), sc235hai->supplies); return 0; } static int sc235hai_probe(struct i2c_client *client) { struct device *dev = &client->dev; struct sc235hai *sc235hai; u8 chip_id_h, chip_id_l; u16 chip_id; int ret; sc235hai = devm_kzalloc(dev, sizeof(*sc235hai), GFP_KERNEL); if (!sc235hai) return -ENOMEM; v4l2_i2c_subdev_init(&sc235hai->sd, client, &sc235hai_subdev_ops); ret = sc235hai_check_hwcfg(dev); if (ret) { dev_err(dev, "Hardware config check failed: %d\n", ret); return ret; } /* Get regulators */ sc235hai->supplies[0].supply = "VANA"; /* 2.8V analog */ sc235hai->supplies[1].supply = "VDIG"; /* 1.2V digital core */ sc235hai->supplies[2].supply = "VDDL"; /* 1.8V I/O */ ret = devm_regulator_bulk_get(dev, ARRAY_SIZE(sc235hai->supplies), sc235hai->supplies); if (ret) { dev_err(dev, "Failed to get regulators: %d\n", ret); return ret; } /* Get clock */ sc235hai->xclk = devm_clk_get(dev, "xclk"); if (IS_ERR(sc235hai->xclk)) { dev_err(dev, "Failed to get xclk: %ld\n", PTR_ERR(sc235hai->xclk)); return PTR_ERR(sc235hai->xclk); } ret = clk_set_rate(sc235hai->xclk, 27000000); if (ret) { dev_err(dev, "Failed to set xclk rate: %d\n", ret); return ret; } /* Get reset GPIO */ sc235hai->reset_gpio = devm_gpiod_get_optional(dev, "reset", GPIOD_OUT_HIGH); if (IS_ERR(sc235hai->reset_gpio)) return PTR_ERR(sc235hai->reset_gpio); mutex_init(&sc235hai->lock); /* Power on and verify chip ID */ ret = sc235hai_power_on(dev); if (ret) { dev_err(dev, "Failed to power on: %d\n", ret); goto error_mutex; } ret = sc235hai_read_reg(sc235hai, SC235HAI_REG_CHIP_ID_H, &chip_id_h); if (ret) goto error_power_off; ret = sc235hai_read_reg(sc235hai, SC235HAI_REG_CHIP_ID_L, &chip_id_l); if (ret) goto error_power_off; chip_id = (chip_id_h << 8) | chip_id_l; if (chip_id != SC235HAI_CHIP_ID) { dev_err(dev, "Unexpected chip ID: 0x%04x (expected 0x%04x)\n", chip_id, SC235HAI_CHIP_ID); ret = -ENODEV; goto error_power_off; } dev_info(dev, "SC235HAI chip ID: 0x%04x detected\n", chip_id); /* Set default mode */ sc235hai->cur_mode = &sc235hai_modes[0]; /* Initialize media entity */ sc235hai->pad.flags = MEDIA_PAD_FL_SOURCE; sc235hai->sd.entity.function = MEDIA_ENT_F_CAM_SENSOR; ret = media_entity_pads_init(&sc235hai->sd.entity, 1, &sc235hai->pad); if (ret) { dev_err(dev, "Failed to init entity pads: %d\n", ret); goto error_power_off; } ret = v4l2_async_register_subdev(&sc235hai->sd); if (ret) { dev_err(dev, "Failed to register subdev: %d\n", ret); goto error_media; } pm_runtime_set_active(dev); pm_runtime_enable(dev); pm_runtime_idle(dev); return 0; error_media: media_entity_cleanup(&sc235hai->sd.entity); error_power_off: sc235hai_power_off(dev); error_mutex: mutex_destroy(&sc235hai->lock); return ret; } static void sc235hai_remove(struct i2c_client *client) { struct v4l2_subdev *sd = i2c_get_clientdata(client); struct sc235hai *sc235hai = to_sc235hai(sd); v4l2_async_unregister_subdev(sd); media_entity_cleanup(&sd->entity); v4l2_ctrl_handler_free(&sc235hai->ctrl_handler); pm_runtime_disable(&client->dev); if (!pm_runtime_status_suspended(&client->dev)) sc235hai_power_off(&client->dev); pm_runtime_set_suspended(&client->dev); mutex_destroy(&sc235hai->lock); } static const struct of_device_id sc235hai_of_match[] = { { .compatible = "smartsens,sc235hai" }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, sc235hai_of_match); static const struct dev_pm_ops sc235hai_pm_ops = { SET_RUNTIME_PM_OPS(sc235hai_power_off, sc235hai_power_on, NULL) }; static struct i2c_driver sc235hai_i2c_driver = { .driver = { .name = "sc235hai", .of_match_table = sc235hai_of_match, .pm = &sc235hai_pm_ops, }, .probe = sc235hai_probe, .remove = sc235hai_remove, }; module_i2c_driver(sc235hai_i2c_driver); MODULE_AUTHOR("Hardware Team"); MODULE_DESCRIPTION("SmartSens SC235HAI sensor driver"); MODULE_LICENSE("GPL v2");