diff --git a/src/ipa/ipa_rkisp1.cpp b/src/ipa/ipa_rkisp1.cpp
new file mode 100644
index 0000000000000000..063b075d8358c30d
--- /dev/null
+++ b/src/ipa/ipa_rkisp1.cpp
@@ -0,0 +1,165 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2019, Google Inc.
+ *
+ * ipa_rkisp1.cpp - RkISP1 Image Processing Algorithms
+ */
+
+#include <algorithm>
+#include <string.h>
+
+#include <linux/rkisp1-config.h>
+
+#include <libcamera/buffer.h>
+#include <libcamera/ipa/ipa_interface.h>
+#include <libcamera/ipa/ipa_module_info.h>
+#include <libcamera/request.h>
+
+#include "log.h"
+#include "utils.h"
+
+namespace libcamera {
+
+LOG_DEFINE_CATEGORY(IPARkISP1)
+
+class IPARkISP1 : public IPAInterface
+{
+public:
+	int initSensor(const V4L2ControlInfoMap &controls) override;
+	void processRequest(const void *cookie, const ControlList &controls,
+			    Buffer &parameters) override;
+	void updateStatistics(const void *cookie, Buffer &statistics) override;
+
+private:
+	void setControls();
+
+	uint64_t statFrame_;
+
+	/* Camera sensor controls. */
+	bool autoExposure_;
+	uint64_t exposure_;
+	uint64_t minExposure_;
+	uint64_t maxExposure_;
+	uint64_t gain_;
+	uint64_t minGain_;
+	uint64_t maxGain_;
+};
+
+int IPARkISP1::initSensor(const V4L2ControlInfoMap &controls)
+{
+	statFrame_ = 0;
+
+	const auto itExp = controls.find(V4L2_CID_EXPOSURE);
+	if (itExp == controls.end())
+		return -ENODEV;
+
+	const auto itGain = controls.find(V4L2_CID_ANALOGUE_GAIN);
+	if (itGain == controls.end())
+		return -ENODEV;
+
+	autoExposure_ = true;
+
+	minExposure_ = std::max<uint64_t>(itExp->second.min(), 1);
+	maxExposure_ = itExp->second.max();
+	exposure_ = minExposure_;
+
+	minGain_ = std::max<uint64_t>(itGain->second.min(), 1);
+	maxGain_ = itGain->second.max();
+	gain_ = minGain_;
+
+	LOG(IPARkISP1, Info)
+		<< "Exposure: " << minExposure_ << "-" << maxExposure_
+		<< " Gain: " << minGain_ << "-" << maxGain_;
+
+	setControls();
+
+	return 0;
+}
+
+void IPARkISP1::setControls()
+{
+	V4L2ControlList ctrls;
+	ctrls.add(V4L2_CID_EXPOSURE);
+	ctrls.add(V4L2_CID_ANALOGUE_GAIN);
+	ctrls[V4L2_CID_EXPOSURE]->setValue(exposure_);
+	ctrls[V4L2_CID_ANALOGUE_GAIN]->setValue(gain_);
+
+	updateSensor.emit(ctrls);
+}
+
+void IPARkISP1::processRequest(const void *cookie, const ControlList &controls,
+			       Buffer &parameters)
+{
+	rkisp1_isp_params_cfg *params =
+		static_cast<rkisp1_isp_params_cfg *>(parameters.mem()->planes()[0].mem());
+
+	memset(params, 0, sizeof(*params));
+
+	/* Auto Exposure on/off. */
+	if (controls.contains(AeEnable)) {
+		autoExposure_ = controls[AeEnable].getBool();
+		if (autoExposure_)
+			params->module_ens = CIFISP_MODULE_AEC;
+
+		params->module_en_update = CIFISP_MODULE_AEC;
+	}
+
+	queueRequest.emit(cookie);
+}
+
+void IPARkISP1::updateStatistics(const void *cookie, Buffer &statistics)
+{
+	const rkisp1_stat_buffer *stats =
+		static_cast<rkisp1_stat_buffer *>(statistics.mem()->planes()[0].mem());
+	const cifisp_stat *params = &stats->params;
+
+	if ((stats->meas_type & CIFISP_STAT_AUTOEXP) && (statFrame_ % 2 == 0)) {
+		const cifisp_ae_stat *ae = &params->ae;
+
+		const unsigned int target = 60;
+
+		unsigned int value = 0;
+		unsigned int num = 0;
+		for (int i = 0; i < CIFISP_AE_MEAN_MAX; i++) {
+			if (ae->exp_mean[i] > 15) {
+				value += ae->exp_mean[i];
+				num++;
+			}
+		}
+		value /= num;
+
+		double factor = (double)target / value;
+		double tmp;
+
+		tmp = factor * exposure_ * gain_ / minGain_;
+		exposure_ = utils::clamp<uint64_t>((uint64_t)tmp, minExposure_, maxExposure_);
+
+		tmp = tmp / exposure_ * minGain_;
+		gain_ = utils::clamp<uint64_t>((uint64_t)tmp, minGain_, maxGain_);
+
+		setControls();
+	}
+
+	statFrame_++;
+}
+
+/*
+ * External IPA module interface
+ */
+
+extern "C" {
+const struct IPAModuleInfo ipaModuleInfo = {
+	IPA_MODULE_API_VERSION,
+	1,
+	"PipelineHandlerRkISP1",
+	"RkISP1 IPA",
+	"LGPL-2.1-or-later",
+};
+
+IPAInterface *ipaCreate()
+{
+	return new IPARkISP1();
+}
+};
+
+}; /* namespace libcamera */
diff --git a/src/ipa/meson.build b/src/ipa/meson.build
index dca7a9461385b68d..59311e6adae90ada 100644
--- a/src/ipa/meson.build
+++ b/src/ipa/meson.build
@@ -1,11 +1,12 @@
-ipa_dummy_sources = [
+ipa_sources = [
     ['ipa_dummy', 'ipa_dummy.cpp'],
     ['ipa_dummy_isolate', 'ipa_dummy_isolate.cpp'],
+    ['ipa_rkisp1', 'ipa_rkisp1.cpp'],
 ]
 
 ipa_install_dir = join_paths(get_option('libdir'), 'libcamera')
 
-foreach t : ipa_dummy_sources
+foreach t : ipa_sources
     ipa = shared_module(t[0],
                         t[1],
                         name_prefix : '',
