imx: mach: imx8: fdt: set correct frequencies for the industrial SoC

Set correct CPU and GPU frequencies for the industrial i.MX8 SoC
variant.

Ensure that the CPU and GPU frequencies are properly configured for the
industrial variant of the SoC. According to the "i.MX 8QuadMax
Industrial Applications Processors" datasheet, the frequency limits for
this variant are as follows:
- Cortex-A72: 1.296 GHz
- Cortex-A53: 1.104 GHz
- GPU core: 625 MHz
- GPU shader: 625 MHz

The CPU clock is enforced by the System Controller Firmware (SCFW), but
the cpufreq driver is unaware of this enforcement. By removing
unsupported frequencies from the operating points, we ensure that the
cpufreq driver aligns correctly with the SCFW's settings.

The GPU frequency, on the other hand, is not enforced by the SCFW. As a
result, the GPU could potentially be overclocked. To prevent this, we
set the correct clock frequency and update the operating points
accordingly, ensuring compliance with the datasheet specifications.

Signed-off-by: Stefan Eichenberger <stefan.eichenberger@toradex.com>
diff --git a/arch/arm/mach-imx/imx8/fdt.c b/arch/arm/mach-imx/imx8/fdt.c
index 6d0585f..ce78c8c 100644
--- a/arch/arm/mach-imx/imx8/fdt.c
+++ b/arch/arm/mach-imx/imx8/fdt.c
@@ -11,6 +11,8 @@
 #include <fdt_support.h>
 #include <linux/libfdt.h>
 #include <linux/printk.h>
+#include <cpu.h>
+#include <dm.h>
 
 DECLARE_GLOBAL_DATA_PTR;
 
@@ -279,6 +281,134 @@
 	return 0;
 }
 
+static int delete_node(void *blob, const char *node)
+{
+	int nodeoffset;
+	int err;
+
+	nodeoffset = fdt_path_offset(blob, node);
+	if (nodeoffset < 0)
+		return -ENXIO;
+
+	err = fdt_del_node(blob, nodeoffset);
+	if (err)
+		return -EINVAL;
+
+	return 0;
+}
+
+static int change_property(void *blob, const char *node, const char *property,
+			   const void *value, int len)
+{
+	int nodeoffset;
+	int err;
+
+	nodeoffset = fdt_path_offset(blob, node);
+	if (nodeoffset < 0)
+		return -ENXIO;
+
+	err = fdt_setprop(blob, nodeoffset, property, value, len);
+	if (err)
+		return -EINVAL;
+
+	return 0;
+}
+
+static void update_fdt_gpu_industrial_frequencies(void *blob)
+{
+	u32 gpu_opp_table[6];
+	u32 gpu_assigned_clocks[2];
+	int err;
+
+	gpu_opp_table[0] = cpu_to_fdt32(625000); /* Normal Core Clock */
+	gpu_opp_table[1] = cpu_to_fdt32(0);
+	gpu_opp_table[2] = cpu_to_fdt32(625000); /* Normal Shader Clock */
+	gpu_opp_table[3] = cpu_to_fdt32(0);
+	gpu_opp_table[4] = cpu_to_fdt32(400000); /* Low Shader and Core Clock */
+	gpu_opp_table[5] = cpu_to_fdt32(0);
+
+	gpu_assigned_clocks[0] = cpu_to_fdt32(625000000); /* Core Clock */
+	gpu_assigned_clocks[1] = cpu_to_fdt32(625000000); /* Shader Clock */
+
+	err = change_property(blob, "/bus@53100000/gpu@53100000",
+			      "assigned-clock-rates", gpu_assigned_clocks,
+			      sizeof(gpu_assigned_clocks));
+	if (err && err != ENXIO)
+		printf("Failed to set assigned-clock-rates for GPU0: %s\n",
+		       fdt_strerror(err));
+
+	err = change_property(blob, "/bus@54100000/gpu@54100000",
+			      "assigned-clock-rates", gpu_assigned_clocks,
+			      sizeof(gpu_assigned_clocks));
+	if (err && err != ENXIO)
+		printf("Failed to set assigned-clock-rates for GPU1: %s\n",
+		       fdt_strerror(err));
+
+	err = change_property(blob, "/bus@54100000/imx8_gpu1_ss@80000000",
+			      "operating-points", &gpu_opp_table,
+			      sizeof(gpu_opp_table));
+	if (err && err != ENXIO)
+		printf("Failed to set operating-points for GPU: %s\n",
+		       fdt_strerror(err));
+}
+
+static void update_fdt_cpu_industrial_frequencies(void *blob)
+{
+	int err;
+
+	err = delete_node(blob, "/opp-table-0/opp-1200000000");
+	if (err && err != -ENXIO)
+		printf("Failed to delete 1.2 GHz node on A53: %s\n",
+		       fdt_strerror(err));
+
+	err = delete_node(blob, "/opp-table-1/opp-1596000000");
+	if (err && err != -ENXIO)
+		printf("Failed to delete 1.596 GHz node on A72: %s\n",
+		       fdt_strerror(err));
+}
+
+static void update_fdt_frequencies(void *blob)
+{
+	struct cpu_info cpu;
+	struct udevice *dev;
+	int err;
+
+	uclass_first_device(UCLASS_CPU, &dev);
+
+	err = cpu_get_info(dev, &cpu);
+	if (err) {
+		printf("Failed to get CPU info\n");
+		return;
+	}
+
+	/*
+	 * Differentiate between the automotive and industrial variants of the
+	 * i.MX8. The difference of these two CPUs is the maximum frequencies
+	 * for the CPU and GPU.
+	 * Core			Automotive [max. MHz]	Industrial [max. MHz]
+	 * A53			1200			1104
+	 * A72			1596			1296
+	 * GPU Core		800			625
+	 * GPU Shader		1000			625
+	 *
+	 * While the SCFW enforces these limits for the CPU, the OS cpufreq
+	 * driver remains unaware, causing a mismatch between reported and
+	 * actual frequencies. This is resolved by removing the unsupprted
+	 * frequencies from the device tree.
+	 *
+	 * The GPU frequencies are not enforced by the SCFW, therefore without
+	 * updating the device tree we overclock the GPU.
+	 *
+	 * Using the cpu_freq variable is the only know way to differentiate
+	 * between the automotive and industrial variants of the i.MX8.
+	 */
+	if (cpu.cpu_freq != 1104000000)
+		return;
+
+	update_fdt_cpu_industrial_frequencies(blob);
+	update_fdt_gpu_industrial_frequencies(blob);
+}
+
 int ft_system_setup(void *blob, struct bd_info *bd)
 {
 	int ret;
@@ -294,6 +424,8 @@
 
 	update_fdt_with_owned_resources(blob);
 
+	update_fdt_frequencies(blob);
+
 	if (is_imx8qm()) {
 		ret = config_smmu_fdt(blob);
 		if (ret)