diff --git a/src/ipa/raspberrypi/cam_helper.hpp b/src/ipa/raspberrypi/cam_helper.hpp
index 0c8aa29a..37281c78 100644
--- a/src/ipa/raspberrypi/cam_helper.hpp
+++ b/src/ipa/raspberrypi/cam_helper.hpp
@@ -74,6 +74,7 @@ public:
 	MdParser &Parser() const { return *parser_; }
 	uint32_t ExposureLines(double exposure_us) const;
 	double Exposure(uint32_t exposure_lines) const; // in us
+	virtual uint32_t GetVBlanking(double exposure_us) const = 0;
 	virtual uint32_t GainCode(double gain) const = 0;
 	virtual double Gain(uint32_t gain_code) const = 0;
 	virtual void GetDelays(int &exposure_delay, int &gain_delay) const;
diff --git a/src/ipa/raspberrypi/cam_helper_imx219.cpp b/src/ipa/raspberrypi/cam_helper_imx219.cpp
index 35c6597c..d34a1f1f 100644
--- a/src/ipa/raspberrypi/cam_helper_imx219.cpp
+++ b/src/ipa/raspberrypi/cam_helper_imx219.cpp
@@ -45,11 +45,16 @@ class CamHelperImx219 : public CamHelper
 {
 public:
 	CamHelperImx219();
+	uint32_t GetVBlanking(double exposure) const override;
 	uint32_t GainCode(double gain) const override;
 	double Gain(uint32_t gain_code) const override;
 	unsigned int MistrustFramesModeSwitch() const override;
 	bool SensorEmbeddedDataPresent() const override;
 	CamTransform GetOrientation() const override;
+
+private:
+	/* Smallest difference between the frame length and integration time. */
+	static constexpr int frameIntegrationDiff = 4;
 };
 
 CamHelperImx219::CamHelperImx219()
@@ -61,6 +66,14 @@ CamHelperImx219::CamHelperImx219()
 {
 }
 
+uint32_t CamHelperImx219::GetVBlanking(double exposure) const
+{
+	uint32_t exposureLines = ExposureLines(exposure);
+
+	return std::max<uint32_t>(mode_.height, exposureLines) -
+	       mode_.height + frameIntegrationDiff;
+}
+
 uint32_t CamHelperImx219::GainCode(double gain) const
 {
 	return (uint32_t)(256 - 256 / gain);
diff --git a/src/ipa/raspberrypi/cam_helper_imx477.cpp b/src/ipa/raspberrypi/cam_helper_imx477.cpp
index 69544456..242d9689 100644
--- a/src/ipa/raspberrypi/cam_helper_imx477.cpp
+++ b/src/ipa/raspberrypi/cam_helper_imx477.cpp
@@ -35,10 +35,15 @@ class CamHelperImx477 : public CamHelper
 {
 public:
 	CamHelperImx477();
+	uint32_t GetVBlanking(double exposure) const override;
 	uint32_t GainCode(double gain) const override;
 	double Gain(uint32_t gain_code) const override;
 	bool SensorEmbeddedDataPresent() const override;
 	CamTransform GetOrientation() const override;
+
+private:
+	/* Smallest difference between the frame length and integration time. */
+	static constexpr int frameIntegrationDiff = 22;
 };
 
 CamHelperImx477::CamHelperImx477()
@@ -46,6 +51,14 @@ CamHelperImx477::CamHelperImx477()
 {
 }
 
+uint32_t CamHelperImx477::GetVBlanking(double exposure) const
+{
+	uint32_t exposureLines = ExposureLines(exposure);
+
+	return std::max<uint32_t>(mode_.height, exposureLines) -
+	       mode_.height + frameIntegrationDiff;
+}
+
 uint32_t CamHelperImx477::GainCode(double gain) const
 {
 	return static_cast<uint32_t>(1024 - 1024 / gain);
diff --git a/src/ipa/raspberrypi/cam_helper_ov5647.cpp b/src/ipa/raspberrypi/cam_helper_ov5647.cpp
index 3dbcb164..ff37779c 100644
--- a/src/ipa/raspberrypi/cam_helper_ov5647.cpp
+++ b/src/ipa/raspberrypi/cam_helper_ov5647.cpp
@@ -16,12 +16,17 @@ class CamHelperOv5647 : public CamHelper
 {
 public:
 	CamHelperOv5647();
+	uint32_t GetVBlanking(double exposure) const override;
 	uint32_t GainCode(double gain) const override;
 	double Gain(uint32_t gain_code) const override;
 	void GetDelays(int &exposure_delay, int &gain_delay) const override;
 	unsigned int HideFramesModeSwitch() const override;
 	unsigned int MistrustFramesStartup() const override;
 	unsigned int MistrustFramesModeSwitch() const override;
+
+private:
+	/* Smallest difference between the frame length and integration time. */
+	static constexpr int frameIntegrationDiff = 4;
 };
 
 /*
@@ -34,6 +39,14 @@ CamHelperOv5647::CamHelperOv5647()
 {
 }
 
+uint32_t CamHelperOv5647::GetVBlanking(double exposure) const
+{
+	uint32_t exposureLines = ExposureLines(exposure);
+
+	return std::max<uint32_t>(mode_.height, exposureLines) -
+	       mode_.height + frameIntegrationDiff;
+}
+
 uint32_t CamHelperOv5647::GainCode(double gain) const
 {
 	return static_cast<uint32_t>(gain * 16.0);
diff --git a/src/ipa/raspberrypi/raspberrypi.cpp b/src/ipa/raspberrypi/raspberrypi.cpp
index 80dc77a3..104727ea 100644
--- a/src/ipa/raspberrypi/raspberrypi.cpp
+++ b/src/ipa/raspberrypi/raspberrypi.cpp
@@ -795,6 +795,7 @@ void IPARPi::applyAGC(const struct AgcStatus *agcStatus)
 
 	int32_t gain_code = helper_->GainCode(agcStatus->analogue_gain);
 	int32_t exposure_lines = helper_->ExposureLines(agcStatus->shutter_time);
+	int32_t vblanking = helper_->GetVBlanking(agcStatus->shutter_time);
 
 	if (unicam_ctrls_.find(V4L2_CID_ANALOGUE_GAIN) == unicam_ctrls_.end()) {
 		LOG(IPARPI, Error) << "Can't find analogue gain control";
@@ -812,8 +813,13 @@ void IPARPi::applyAGC(const struct AgcStatus *agcStatus)
 			   << gain_code << ")";
 
 	ControlList ctrls(unicam_ctrls_);
-	ctrls.set(V4L2_CID_ANALOGUE_GAIN, gain_code);
+	/*
+	 * VBLANK must be set before EXPOSURE as the former will adjust the
+	 * limits of the latter control.
+	 */
+	ctrls.set(V4L2_CID_VBLANK, vblanking);
 	ctrls.set(V4L2_CID_EXPOSURE, exposure_lines);
+	ctrls.set(V4L2_CID_ANALOGUE_GAIN, gain_code);
 	op.controls.push_back(ctrls);
 	queueFrameAction.emit(0, op);
 }
diff --git a/src/libcamera/pipeline/raspberrypi/raspberrypi.cpp b/src/libcamera/pipeline/raspberrypi/raspberrypi.cpp
index 16850819..e4c5d8f0 100644
--- a/src/libcamera/pipeline/raspberrypi/raspberrypi.cpp
+++ b/src/libcamera/pipeline/raspberrypi/raspberrypi.cpp
@@ -1159,7 +1159,9 @@ void RPiCameraData::queueFrameAction(unsigned int frame, const IPAOperationData
 		if (!staggeredCtrl_) {
 			staggeredCtrl_.init(unicam_[Unicam::Image].dev(),
 					    { { V4L2_CID_ANALOGUE_GAIN, action.data[0] },
+					      { V4L2_CID_VBLANK, action.data[1] },
 					      { V4L2_CID_EXPOSURE, action.data[1] } });
+
 			sensorMetadata_ = action.data[2];
 		}
 
