diff --git a/src/libcamera/pipeline/ipu3/ipu3.cpp b/src/libcamera/pipeline/ipu3/ipu3.cpp
index 4f1ab72debf8..9fa59c1bc97e 100644
--- a/src/libcamera/pipeline/ipu3/ipu3.cpp
+++ b/src/libcamera/pipeline/ipu3/ipu3.cpp
@@ -24,6 +24,28 @@ namespace libcamera {
 
 LOG_DEFINE_CATEGORY(IPU3)
 
+struct ImguDevice {
+	ImguDevice()
+		: imgu(nullptr), input(nullptr), output(nullptr),
+		  viewfinder(nullptr), stat(nullptr) {}
+
+	~ImguDevice()
+	{
+		delete imgu;
+		delete input;
+		delete output;
+		delete viewfinder;
+		delete stat;
+	}
+
+	V4L2Subdevice *imgu;
+	V4L2Device *input;
+	V4L2Device *output;
+	V4L2Device *viewfinder;
+	V4L2Device *stat;
+	/* TODO: add param video device for 3A tuning */
+};
+
 struct Cio2Device {
 	Cio2Device()
 		: output(nullptr), csi2(nullptr), sensor(nullptr) {}
@@ -44,6 +66,7 @@ class IPU3CameraData : public CameraData
 {
 public:
 	Cio2Device cio2;
+	ImguDevice *imgu;
 
 	Stream stream_;
 };
@@ -71,6 +94,10 @@ public:
 	bool match(DeviceEnumerator *enumerator);
 
 private:
+	static constexpr unsigned int IMGU_PAD_INPUT = 0;
+	static constexpr unsigned int IMGU_PAD_OUTPUT = 2;
+	static constexpr unsigned int IMGU_PAD_VF = 3;
+	static constexpr unsigned int IMGU_PAD_STAT = 4;
 	static constexpr unsigned int IPU3_BUF_NUM = 4;
 
 	IPU3CameraData *cameraData(const Camera *camera)
@@ -79,9 +106,17 @@ private:
 			PipelineHandler::cameraData(camera));
 	}
 
+	int linkImgu(ImguDevice *imgu);
+
+	V4L2Device *openDevice(MediaDevice *media, std::string &name);
+	V4L2Subdevice *openSubdevice(MediaDevice *media, std::string &name);
+	int initImgu(ImguDevice *imgu);
 	int initCio2(unsigned int index, Cio2Device *cio2);
 	void registerCameras();
 
+	ImguDevice imgu0_;
+	ImguDevice imgu1_;
+
 	std::shared_ptr<MediaDevice> cio2MediaDev_;
 	std::shared_ptr<MediaDevice> imguMediaDev_;
 };
@@ -159,6 +194,15 @@ int PipelineHandlerIPU3::configureStreams(Camera *camera,
 	V4L2DeviceFormat devFormat = {};
 	int ret;
 
+	/*
+	 * TODO: dynamically assign ImgU devices; as of now, with a single
+	 * stream supported, always use 'imgu0'.
+	 */
+	data->imgu = &imgu0_;
+	ret = linkImgu(data->imgu);
+	if (ret)
+		return ret;
+
 	/*
 	 * FIXME: as of now, the format gets applied to the sensor and is
 	 * propagated along the pipeline. It should instead be applied on the
@@ -334,17 +378,29 @@ bool PipelineHandlerIPU3::match(DeviceEnumerator *enumerator)
 	if (cio2MediaDev_->open())
 		goto error_release_mdev;
 
+	if (imguMediaDev_->open())
+		goto error_close_mdev;
+
 	if (cio2MediaDev_->disableLinks())
-		goto error_close_cio2;
+		goto error_close_mdev;
+
+	if (initImgu(&imgu0_))
+		goto error_close_mdev;
+
+	if (initImgu(&imgu1_))
+		goto error_close_mdev;
+
 
 	registerCameras();
 
 	cio2MediaDev_->close();
+	imguMediaDev_->close();
 
 	return true;
 
-error_close_cio2:
+error_close_mdev:
 	cio2MediaDev_->close();
+	imguMediaDev_->close();
 
 error_release_mdev:
 	cio2MediaDev_->release();
@@ -353,6 +409,153 @@ error_release_mdev:
 	return false;
 }
 
+/* Link entities in the ImgU unit to prepare for capture operations. */
+int PipelineHandlerIPU3::linkImgu(ImguDevice *imguDevice)
+{
+	MediaLink *link;
+	int ret;
+
+	unsigned int index = imguDevice == &imgu0_ ? 0 : 1;
+	std::string imguName = "ipu3-imgu " + std::to_string(index);
+	std::string inputName = imguName + " input";
+	std::string outputName = imguName + " output";
+	std::string viewfinderName = imguName + " viewfinder";
+	std::string statName = imguName + " 3a stat";
+
+	ret = imguMediaDev_->open();
+	if (ret)
+		return ret;
+
+	ret = imguMediaDev_->disableLinks();
+	if (ret) {
+		imguMediaDev_->close();
+		return ret;
+	}
+
+	/* Link entities to configure the IMGU unit for capture. */
+	link = imguMediaDev_->link(inputName, 0, imguName, IMGU_PAD_INPUT);
+	if (!link) {
+		LOG(IPU3, Error)
+			<< "Failed to get link '" << inputName << "':0 -> '"
+			<< imguName << "':0";
+		ret = -ENODEV;
+		goto error_close_mediadev;
+	}
+	link->setEnabled(true);
+
+	link = imguMediaDev_->link(imguName, IMGU_PAD_OUTPUT, outputName, 0);
+	if (!link) {
+		LOG(IPU3, Error)
+			<< "Failed to get link '" << imguName << "':2 -> '"
+			<< outputName << "':0";
+		ret = -ENODEV;
+		goto error_close_mediadev;
+	}
+	link->setEnabled(true);
+
+	link = imguMediaDev_->link(imguName, IMGU_PAD_VF, viewfinderName, 0);
+	if (!link) {
+		LOG(IPU3, Error)
+			<< "Failed to get link '" << imguName << "':3 -> '"
+			<< viewfinderName << "':0";
+		ret = -ENODEV;
+		goto error_close_mediadev;
+	}
+	link->setEnabled(true);
+
+	link = imguMediaDev_->link(imguName, IMGU_PAD_STAT, statName, 0);
+	if (!link) {
+		LOG(IPU3, Error)
+			<< "Failed to get link '" << imguName << "':4 -> '"
+			<< statName << "':0";
+		ret = -ENODEV;
+		goto error_close_mediadev;
+	}
+	link->setEnabled(true);
+
+	imguMediaDev_->close();
+
+	return 0;
+
+error_close_mediadev:
+	imguMediaDev_->close();
+
+	return ret;
+
+}
+
+V4L2Device *PipelineHandlerIPU3::openDevice(MediaDevice *media,
+					    std::string &name)
+{
+	V4L2Device *dev;
+
+	MediaEntity *entity = media->getEntityByName(name);
+	if (!entity) {
+		LOG(IPU3, Error)
+			<< "Failed to get entity '" << name << "'";
+		return nullptr;
+	}
+
+	dev = new V4L2Device(entity);
+	if (dev->open())
+		return nullptr;
+
+	return dev;
+}
+
+V4L2Subdevice *PipelineHandlerIPU3::openSubdevice(MediaDevice *media,
+						  std::string &name)
+{
+	V4L2Subdevice *dev;
+
+	MediaEntity *entity = media->getEntityByName(name);
+	if (!entity) {
+		LOG(IPU3, Error)
+			<< "Failed to get entity '" << name << "'";
+		return nullptr;
+	}
+
+	dev = new V4L2Subdevice(entity);
+	if (dev->open())
+		return nullptr;
+
+	return dev;
+}
+
+/* Create video devices and subdevices for the ImgU instance. */
+int PipelineHandlerIPU3::initImgu(ImguDevice *imgu)
+{
+	unsigned int index = imgu == &imgu0_ ? 0 : 1;
+	std::string imguName = "ipu3-imgu " + std::to_string(index);
+	std::string devName;
+
+	imgu->imgu = openSubdevice(imguMediaDev_.get(), imguName);
+	if (!imgu->imgu)
+		return -ENODEV;
+
+	devName = imguName + " input";
+	imgu->input = openDevice(imguMediaDev_.get(), devName);
+	if (!imgu->input)
+		return -ENODEV;
+
+	devName = imguName + " output";
+	imgu->output = openDevice(imguMediaDev_.get(), devName);
+	if (!imgu->output)
+		return -ENODEV;
+
+	devName = imguName + " viewfinder";
+	imgu->viewfinder = openDevice(imguMediaDev_.get(), devName);
+	if (!imgu->viewfinder)
+		return -ENODEV;
+
+	devName = imguName + " 3a stat";
+	imgu->stat = openDevice(imguMediaDev_.get(), devName);
+	if (!imgu->stat)
+		return -ENODEV;
+
+	return 0;
+}
+
 int PipelineHandlerIPU3::initCio2(unsigned int index, Cio2Device *cio2)
 {
 	int ret;
@@ -400,16 +603,8 @@ int PipelineHandlerIPU3::initCio2(unsigned int index, Cio2Device *cio2)
 		return ret;
 
 	std::string cio2Name = "ipu3-cio2 " + std::to_string(index);
-	entity = cio2MediaDev_->getEntityByName(cio2Name);
-	if (!entity) {
-		LOG(IPU3, Error)
-			<< "Failed to get entity '" << cio2Name << "'";
-		return -EINVAL;
-	}
-
-	cio2->output = new V4L2Device(entity);
-	ret = cio2->output->open();
-	if (ret)
+	cio2->output = openDevice(cio2MediaDev_.get(), cio2Name);
+	if (!cio2->output)
 		return ret;
 
 	return 0;
