[PATCH 6/8] media: ov2740: add manual white balance controls

From: Maurizio Casciano

Date: Wed Aug 26 2026 - 09:28:53 EST


The sensor has separate red, green and blue manual white-balance gain
registers, but the driver currently writes the same digital-gain value
to all three channels. This prevents userspace from correcting the
strong color cast of raw Bayer capture.

Expose red- and blue-balance controls relative to the digital gain,
update all three channels under group hold, and always release and
launch the group even when a channel write fails.

Tested on the Yoga Book OV2740 with live gain changes and continuous raw
capture.

Signed-off-by: Maurizio Casciano <mauriziocasciano7@xxxxxxxxx>
Assisted-by: Codex:gpt-5.6-sol sparse
---
drivers/media/i2c/ov2740.c | 71 +++++++++++++++++++++++++++-----------
1 file changed, 50 insertions(+), 21 deletions(-)

diff --git a/drivers/media/i2c/ov2740.c b/drivers/media/i2c/ov2740.c
index b760d4dc0e68..7651443b28c1 100644
--- a/drivers/media/i2c/ov2740.c
+++ b/drivers/media/i2c/ov2740.c
@@ -607,6 +607,9 @@ struct ov2740 {
struct v4l2_ctrl *vblank;
struct v4l2_ctrl *hblank;
struct v4l2_ctrl *exposure;
+ struct v4l2_ctrl *digital_gain;
+ struct v4l2_ctrl *red_balance;
+ struct v4l2_ctrl *blue_balance;

/* GPIOs, clocks, regulators */
struct gpio_desc *reset_gpio;
@@ -736,35 +739,46 @@ static int ov2740_identify_module(struct ov2740 *ov2740)
return 0;
}

-static int ov2740_update_digital_gain(struct ov2740 *ov2740, u32 d_gain)
+static int ov2740_update_mwb_gains(struct ov2740 *ov2740)
{
- int ret;
+ u32 green_gain = ov2740->digital_gain->val;
+ u32 red_gain, blue_gain;
+ int end_ret, launch_ret, ret;
+
+ /* Balance controls use 1024 as unity relative to the digital gain. */
+ red_gain = min_t(u64,
+ DIV_ROUND_CLOSEST_ULL((u64)green_gain *
+ ov2740->red_balance->val,
+ OV2740_DGTL_GAIN_DEFAULT),
+ OV2740_DGTL_GAIN_MAX);
+ blue_gain = min_t(u64,
+ DIV_ROUND_CLOSEST_ULL((u64)green_gain *
+ ov2740->blue_balance->val,
+ OV2740_DGTL_GAIN_DEFAULT),
+ OV2740_DGTL_GAIN_MAX);

ret = ov2740_write_reg(ov2740, OV2740_REG_GROUP_ACCESS, 1,
OV2740_GROUP_HOLD_START);
if (ret)
return ret;

- ret = ov2740_write_reg(ov2740, OV2740_REG_MWB_R_GAIN, 2, d_gain);
+ ret = ov2740_write_reg(ov2740, OV2740_REG_MWB_R_GAIN, 2, red_gain);
if (ret)
- return ret;
+ goto release_group;

- ret = ov2740_write_reg(ov2740, OV2740_REG_MWB_G_GAIN, 2, d_gain);
+ ret = ov2740_write_reg(ov2740, OV2740_REG_MWB_G_GAIN, 2, green_gain);
if (ret)
- return ret;
+ goto release_group;

- ret = ov2740_write_reg(ov2740, OV2740_REG_MWB_B_GAIN, 2, d_gain);
- if (ret)
- return ret;
+ ret = ov2740_write_reg(ov2740, OV2740_REG_MWB_B_GAIN, 2, blue_gain);

- ret = ov2740_write_reg(ov2740, OV2740_REG_GROUP_ACCESS, 1,
- OV2740_GROUP_HOLD_END);
- if (ret)
- return ret;
+release_group:
+ end_ret = ov2740_write_reg(ov2740, OV2740_REG_GROUP_ACCESS, 1,
+ OV2740_GROUP_HOLD_END);
+ launch_ret = ov2740_write_reg(ov2740, OV2740_REG_GROUP_ACCESS, 1,
+ OV2740_GROUP_HOLD_LAUNCH);

- ret = ov2740_write_reg(ov2740, OV2740_REG_GROUP_ACCESS, 1,
- OV2740_GROUP_HOLD_LAUNCH);
- return ret;
+ return ret ?: end_ret ?: launch_ret;
}

static int ov2740_test_pattern(struct ov2740 *ov2740, u32 pattern)
@@ -805,7 +819,9 @@ static int ov2740_set_ctrl(struct v4l2_ctrl *ctrl)
break;

case V4L2_CID_DIGITAL_GAIN:
- ret = ov2740_update_digital_gain(ov2740, ctrl->val);
+ case V4L2_CID_RED_BALANCE:
+ case V4L2_CID_BLUE_BALANCE:
+ ret = ov2740_update_mwb_gains(ov2740);
break;

case V4L2_CID_EXPOSURE:
@@ -846,7 +862,7 @@ static int ov2740_init_controls(struct ov2740 *ov2740)
int ret;

ctrl_hdlr = &ov2740->ctrl_handler;
- ret = v4l2_ctrl_handler_init(ctrl_hdlr, 10);
+ ret = v4l2_ctrl_handler_init(ctrl_hdlr, 12);
if (ret)
return ret;

@@ -881,9 +897,22 @@ static int ov2740_init_controls(struct ov2740 *ov2740)
v4l2_ctrl_new_std(ctrl_hdlr, &ov2740_ctrl_ops, V4L2_CID_ANALOGUE_GAIN,
OV2740_ANAL_GAIN_MIN, OV2740_ANAL_GAIN_MAX,
OV2740_ANAL_GAIN_STEP, OV2740_ANAL_GAIN_MIN);
- v4l2_ctrl_new_std(ctrl_hdlr, &ov2740_ctrl_ops, V4L2_CID_DIGITAL_GAIN,
- OV2740_DGTL_GAIN_MIN, OV2740_DGTL_GAIN_MAX,
- OV2740_DGTL_GAIN_STEP, OV2740_DGTL_GAIN_DEFAULT);
+ ov2740->digital_gain =
+ v4l2_ctrl_new_std(ctrl_hdlr, &ov2740_ctrl_ops,
+ V4L2_CID_DIGITAL_GAIN,
+ OV2740_DGTL_GAIN_MIN, OV2740_DGTL_GAIN_MAX,
+ OV2740_DGTL_GAIN_STEP,
+ OV2740_DGTL_GAIN_DEFAULT);
+ ov2740->red_balance =
+ v4l2_ctrl_new_std(ctrl_hdlr, &ov2740_ctrl_ops,
+ V4L2_CID_RED_BALANCE,
+ 1, OV2740_DGTL_GAIN_MAX, 1,
+ OV2740_DGTL_GAIN_DEFAULT);
+ ov2740->blue_balance =
+ v4l2_ctrl_new_std(ctrl_hdlr, &ov2740_ctrl_ops,
+ V4L2_CID_BLUE_BALANCE,
+ 1, OV2740_DGTL_GAIN_MAX, 1,
+ OV2740_DGTL_GAIN_DEFAULT);
exposure_max = ov2740->cur_mode->vts_def - OV2740_EXPOSURE_MAX_MARGIN;
ov2740->exposure = v4l2_ctrl_new_std(ctrl_hdlr, &ov2740_ctrl_ops,
V4L2_CID_EXPOSURE,
--
2.53.0