[PATCH v3 09/12] media: ov2740: add manual white balance controls
From: Maurizio Casciano
Date: Thu Aug 27 2026 - 19:21:04 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 | 65 ++++++++++++++++++++++++++------------
1 file changed, 45 insertions(+), 20 deletions(-)
diff --git a/drivers/media/i2c/ov2740.c b/drivers/media/i2c/ov2740.c
index 6083dd28e4ad..ae8074cb7443 100644
--- a/drivers/media/i2c/ov2740.c
+++ b/drivers/media/i2c/ov2740.c
@@ -608,6 +608,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;
@@ -737,35 +740,41 @@ 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 d_gain = ov2740->digital_gain->val;
+ u32 blue_gain, red_gain;
+ int end_ret, launch_ret, ret;
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);
+ /* Balance controls use 1024 as unity relative to the digital gain. */
+ red_gain = DIV_ROUND_CLOSEST(d_gain * ov2740->red_balance->val,
+ OV2740_DGTL_GAIN_DEFAULT);
+ red_gain = min(red_gain, OV2740_DGTL_GAIN_MAX);
+ 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);
if (ret)
- return ret;
+ goto release_group;
- ret = ov2740_write_reg(ov2740, OV2740_REG_MWB_B_GAIN, 2, d_gain);
- if (ret)
- return ret;
+ blue_gain = DIV_ROUND_CLOSEST(d_gain * ov2740->blue_balance->val,
+ OV2740_DGTL_GAIN_DEFAULT);
+ blue_gain = min(blue_gain, OV2740_DGTL_GAIN_MAX);
+ 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)
@@ -806,7 +815,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:
@@ -847,7 +858,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;
@@ -882,9 +893,23 @@ 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);
+ v4l2_ctrl_cluster(3, &ov2740->digital_gain);
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