diff --git a/src/ipa/mali-c55/algorithms/ccm.cpp b/src/ipa/mali-c55/algorithms/ccm.cpp
new file mode 100644
index 000000000000..008ab2d1e31e
--- /dev/null
+++ b/src/ipa/mali-c55/algorithms/ccm.cpp
@@ -0,0 +1,175 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2026, Ideas On Board
+ *
+ * Mali C55 Color Correction Matrix control algorithm
+ */
+
+#include "ccm.h"
+
+#include <libcamera/base/log.h>
+
+/**
+ * \file ccm.h
+ * \brief Mali-C55 CCM implementation
+ */
+
+namespace libcamera {
+
+namespace ipa::mali_c55::algorithms {
+
+LOG_DEFINE_CATEGORY(MaliC55Ccm)
+
+/**
+ * \class Ccm
+ * \brief Mali-C55 color correction matrix algorithm
+ */
+
+/**
+ * \copydoc libcamera::ipa::Algorithm::init
+ */
+int Ccm::init(IPAContext &context, const ValueNode &tuningData)
+{
+	auto &cmap = context.ctrlMap;
+	int ret = ccmAlgo_.init(tuningData, cmap);
+	if (ret)
+		return ret;
+
+	/*
+	 * Mali-C55 allows to perform WB in the RGB color space as part of the
+	 * CCM. Fix the gains at 1.0 as we perform White Balance in the Bayer
+	 * domain.
+	 */
+	gain_ = 1.0;
+
+	return 0;
+}
+
+/**
+ * \copydoc libcamera::ipa::Algorithm::configure
+ */
+int Ccm::configure(IPAContext &context,
+		   [[maybe_unused]] const IPACameraSensorInfo &configInfo)
+{
+	lastCt_ = context.activeState.awb.automatic.temperatureK;
+	return ccmAlgo_.configure(context.activeState.ccm, lastCt_);
+}
+
+void Ccm::queueRequest(IPAContext &context,
+		       [[maybe_unused]] const uint32_t frame,
+		       IPAFrameContext &frameContext,
+		       const ControlList &controls)
+{
+	/* Nothing to do here, the ccm will be calculated in prepare() */
+	if (frameContext.awb.autoEnabled)
+		return;
+
+	ccmAlgo_.queueRequest(context.activeState.ccm, frameContext.ccm, controls);
+}
+
+void Ccm::setParameters(MaliC55Params *params, const IPAFrameContext &frameContext)
+{
+	auto config = params->block<MaliC55Blocks::Ccm>();
+
+	for (unsigned int i = 0; i < 3; i++)
+		config->gains[i] = UQ<4, 8>(gain_).quantized();
+
+	for (unsigned int i = 0; i < 3; i++)
+		config->offs[i] = frameContext.ccm.offsets[i][0];
+
+	const Matrix<float, 3, 3> &ccm = frameContext.ccm.ccm;
+	for (unsigned int i = 0; i < 3; i++) {
+		for (unsigned int j = 0; j < 3; j++) {
+			uint16_t val = Q<5, 8>(ccm[i][j]).quantized();
+
+			if (val && ccm[i][j] < 0) {
+				/*
+				 * CCM coefficients are expected to be in
+				 * sign/magnitude format.
+				 *
+				 * As we're here handling the case where
+				 * ccm[i][j] is negative, its quantized
+				 * representation is stored in 'val' as
+				 * 2's complement.
+				 *
+				 * We need to invert the 2's complement by
+				 * re-complementing it to 1 and adding 1 back.
+				 *
+				 * Let's make a practical example on how to
+				 * reverse the 2's complement of -7 with 4 bits
+				 * and BIT(5) for sign.
+				 *
+				 * 2's complement (ignore sign bit)
+				 *  - 1's complement of abs(-7)
+				 *    2^4 - 1 - 7 = 8 -> 	1000
+				 *  - Add one: 8 + 1 = 9 -> 	1001
+				 *
+				 * -7 is then [1 1001] in memory in 2's complement
+				 *
+				 * Reverse 2's complement (ignore sign bit)
+				 *  - 1's complement of the 2's complement representation
+				 *    2^4 - 1 - 9 = 6 ->	0110
+				 *  - add one: 6 + 1 = 7 ->	0111
+				 *
+				 *  -7 is then [1 0111] in memory in Sign/Magnitude
+				 */
+				val = ((~(val & ~(1 << 12)) & 0x1ff) + 1) | (1 << 12);
+			}
+			config->coeffs[i][j] = val;
+		}
+	}
+
+	config.setEnabled(true);
+
+	LOG(MaliC55Ccm, Debug) << "Setting ccm: " << ccm << " offsets: "
+			       << frameContext.ccm.offsets
+			       << " with fixed gain " << gain_;
+}
+
+/**
+ * \copydoc libcamera::ipa::Algorithm::prepare
+ */
+void Ccm::prepare(IPAContext &context, const uint32_t frame,
+		  IPAFrameContext &frameContext, MaliC55Params *params)
+{
+	if (!frameContext.awb.autoEnabled) {
+		setParameters(params, frameContext);
+		return;
+	}
+
+	/*
+	 * The Mali-C55 Ccm implementation is slightly stricter than the
+	 * CcmAlgorithm class and only re-interpolates if the colour temperature
+	 * changes of a certain amount.
+	 */
+	float ct = frameContext.awb.temperatureK * 1.0f;
+	if (frame > 0 && (ct < lastCt_ * 1.2 && ct > lastCt_ * 0.8)) {
+		frameContext.ccm.ccm = context.activeState.ccm.automatic.ccm;
+		frameContext.ccm.offsets = context.activeState.ccm.automatic.offsets;
+		lastCt_ = ct;
+
+		return;
+	}
+
+	ccmAlgo_.prepare(context.activeState.ccm, frameContext.ccm, frame, ct);
+	setParameters(params, frameContext);
+	lastCt_ = ct;
+}
+
+/**
+ * \copydoc libcamera::ipa::Algorithm::process
+ */
+void Ccm::process([[maybe_unused]] IPAContext &context,
+		  [[maybe_unused]] const uint32_t frame,
+		  IPAFrameContext &frameContext,
+		  [[maybe_unused]] const mali_c55_stats_buffer *stats,
+		  ControlList &metadata)
+{
+	ccmAlgo_.process(frameContext.ccm, metadata);
+}
+
+REGISTER_IPA_ALGORITHM(Ccm, "Ccm")
+
+} /* namespace ipa::mali_c55::algorithms */
+
+} /* namespace libcamera */
diff --git a/src/ipa/mali-c55/algorithms/ccm.h b/src/ipa/mali-c55/algorithms/ccm.h
new file mode 100644
index 000000000000..73649204a7ee
--- /dev/null
+++ b/src/ipa/mali-c55/algorithms/ccm.h
@@ -0,0 +1,66 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2026, Ideas On Board
+ *
+ * Mali C55 Color Correction Matrix control algorithm
+ */
+
+#pragma once
+
+#include <linux/media/arm/mali-c55-config.h>
+
+#include <libcamera/controls.h>
+
+#include "libcamera/internal/value_node.h"
+
+#include "libipa/ccm.h"
+#include "libipa/fixedpoint.h"
+
+#include "algorithm.h"
+#include "ipa_context.h"
+#include "params.h"
+
+namespace libcamera {
+
+namespace ipa::mali_c55::algorithms {
+
+class Ccm : public Algorithm
+{
+public:
+	Ccm() {}
+	~Ccm() = default;
+
+	int init(IPAContext &context, const ValueNode &tuningData) override;
+	int configure(IPAContext &context,
+		      const IPACameraSensorInfo &configInfo) override;
+	void queueRequest(IPAContext &context,
+			  const uint32_t frame,
+			  IPAFrameContext &frameContext,
+			  const ControlList &controls) override;
+	void prepare(IPAContext &context, const uint32_t frame,
+		     IPAFrameContext &frameContext,
+		     MaliC55Params *params) override;
+	void process(IPAContext &context, const uint32_t frame,
+		     IPAFrameContext &frameContext,
+		     const mali_c55_stats_buffer *stats,
+		     ControlList &metadata) override;
+
+private:
+	void setParameters(MaliC55Params *params, const IPAFrameContext &context);
+
+	/*
+	 * The CCM coefficient registers are said to be in Q<4,8> but this
+	 * doesn't include the sign bit as the register is 13 bits wide
+	 * (Q-format TI variant).
+	 *
+	 * As the Quantized class uses the ARM variant of the Q-format notation,
+	 * make it <5, 8> to include the sign bit.
+	 */
+	CcmAlgorithm<Q<5, 8>> ccmAlgo_;
+	float gain_;
+	float lastCt_;
+};
+
+} /* namespace ipa::mali_c55::algorithms */
+
+} /* namespace libcamera */
diff --git a/src/ipa/mali-c55/algorithms/meson.build b/src/ipa/mali-c55/algorithms/meson.build
index 1665da071634..24a27ce66562 100644
--- a/src/ipa/mali-c55/algorithms/meson.build
+++ b/src/ipa/mali-c55/algorithms/meson.build
@@ -4,5 +4,6 @@ mali_c55_ipa_algorithms = files([
     'agc.cpp',
     'awb.cpp',
     'blc.cpp',
+    'ccm.cpp',
     'lsc.cpp',
 ])
diff --git a/src/ipa/mali-c55/ipa_context.h b/src/ipa/mali-c55/ipa_context.h
index 3d884ea17eb8..2d3e91d56baa 100644
--- a/src/ipa/mali-c55/ipa_context.h
+++ b/src/ipa/mali-c55/ipa_context.h
@@ -16,6 +16,7 @@
 #include <libipa/fc_queue.h>
 
 #include "libipa/awb.h"
+#include "libipa/ccm.h"
 #include "libipa/fixedpoint.h"
 
 namespace libcamera {
@@ -59,6 +60,8 @@ struct IPAActiveState {
 	} agc;
 
 	ipa::awb::ActiveState awb;
+
+	ipa::ccm::ActiveState ccm;
 };
 
 struct IPAFrameContext : public FrameContext {
@@ -69,6 +72,8 @@ struct IPAFrameContext : public FrameContext {
 	} agc;
 
 	ipa::awb::FrameContext awb;
+
+	ipa::ccm::FrameContext ccm;
 };
 
 struct IPAContext {
