diff --git a/openrtx/include/protocols/M17/ClockRecovery.hpp b/openrtx/include/protocols/M17/ClockRecovery.hpp
new file mode 100644
index 00000000..f83d8810
--- /dev/null
+++ b/openrtx/include/protocols/M17/ClockRecovery.hpp
@@ -0,0 +1,136 @@
+/***************************************************************************
+ * Copyright (C) 2025 by Federico Amedeo Izzo IU2NUO, *
+ * Niccolò Izzo IU2KIN *
+ * Frederik Saraci IU2NRO *
+ * Silvano Seva IU2KWO *
+ * *
+ * This program is free software; you can redistribute it and/or modify *
+ * it under the terms of the GNU General Public License as published by *
+ * the Free Software Foundation; either version 3 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * This program is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the GNU General Public License *
+ * along with this program; if not, see *
+ ***************************************************************************/
+
+#ifndef CLOCK_RECOVERY_H
+#define CLOCK_RECOVERY_H
+
+#include
+#include
+#include
+
+/**
+ * Class to construct clock recovery objects.
+ * The clock recovery algorithm estimates the best sampling point by finding,
+ * within a symbol, the point with maximum energy. The algorithm will work
+ * properly only if correctly synchronized with the baseband stream.
+ */
+template
+class ClockRecovery
+{
+public:
+ /**
+ * Constructor
+ */
+ ClockRecovery()
+ {
+ reset();
+ }
+
+ /**
+ * Destructor
+ */
+ ~ClockRecovery()
+ {
+ }
+
+ /**
+ * Reset the internal state.
+ */
+ void reset()
+ {
+ curIdx = 0;
+ prevSample = 0;
+ numSamples = 0;
+ updateReq = false;
+ energy.fill(0);
+ }
+
+ /**
+ * Process a new sample.
+ *
+ * @param sample: baseband sample.
+ */
+ void sample(int16_t &sample)
+ {
+ int32_t delta = static_cast(sample)
+ - static_cast(prevSample);
+
+ if ((sample + prevSample) < 0)
+ delta = -delta;
+
+ energy[curIdx] += delta;
+ prevSample = sample;
+ curIdx = (curIdx + 1) % SAMPLES_PER_SYMBOL;
+ numSamples += 1;
+ }
+
+ /**
+ * Update the best sampling point estimate.
+ */
+ void update()
+ {
+ if (numSamples == 0)
+ return;
+
+ uint8_t index = 0;
+ bool is_positive = false;
+
+ for (size_t i = 0; i < SAMPLES_PER_SYMBOL; i++) {
+ int32_t phase = energy[i];
+
+ if (!is_positive && phase > 0) {
+ is_positive = true;
+ } else if (is_positive && phase < 0) {
+ index = i;
+ break;
+ }
+ }
+
+ if (index == 0)
+ sp = SAMPLES_PER_SYMBOL - 1;
+ else
+ sp = index - 1;
+
+ energy.fill(0);
+ numSamples = 0;
+ }
+
+ /**
+ * Get the best sampling point estimate.
+ * The returned value is within the space of a simbol, that is in the
+ * range [0 SAMPLES_PER_SYMBOL - 1].
+ *
+ * @return sampling point.
+ */
+ uint8_t samplingPoint()
+ {
+ return sp;
+ }
+
+private:
+ std::array energy;
+ size_t curIdx;
+ size_t numSamples;
+ int16_t prevSample;
+ uint8_t sp;
+ bool updateReq;
+};
+
+#endif