First steps of mainlining a random ARM64 phone

Android devices (the older, the worse) typically ship with outdated Linux kernels (usually up to 4 years old). As these are meant to be used with an Android environment, there are lots of HALs and blobs that cannot work outside it, rendering a lot of the deivce peripherals useless - unless you use Hybris, but that’s not ideal. This leaves us with only one rational choice - getting the mainline Linux kernel to work.

Now, this is a process that is very specific to the SoC, used in the device. The majority of Qualcomm SoCs are already supported in the mainline kernel well, which means that you only need to do the device-specific work, consisting of:

  • write a device tree for your phone/tablet
  • write drivers for:
    • panel
    • touchscreen and sensors, if not already supported
    • cameras, LEDs
    • speakers/mic
    • USB MUIC or type-c controllers, if non-standard

However, there are a few Qualcomm SoCs that have not received this blessing, but adding support for them is not that hard. Qualcomm doesn’t change IP blocks that much across platforms, so typically you need to change only a few things. I managed to write almost complete support for Snapdragon 680 and the Lenovo Tab M10+ 3rd gen in the span of a week. The only things that did not work were interconnects, type-c controller (USB peripheral works), audio and cameras.

But… what if it’s, let’s say, a phone with an Exynos SoC?

The Samsung Galaxy S7 #

Exynos SoCs have always been behind in support due to Samsung stopping the all upstreaming in 2015. Needless to say, in the past few years, the Samsung SoC subsystem has seen a lot more activity from Linaro and the community. Needless to say, it’s still missing a lot of cruical drivers for everything past 2016 - display engine, frequency scaling, PCIe and everything more complex.

This should not be demotivational though, because we will only see these things working if we.. make them work.

So, booting minimally - up to initramfs shell - requires:

  • a second stage bootloader to easen removing android-specific filth that may confuse our new kernel
  • an SoC and phone device tree with timer, interrupt controller, memory nodes and some sort of output

Barebox #

For the first part, even though uniLoader is the most widely used solution for avoiding the hardcoded android parameters from the previous bootloader, I went with another bootloader called Barebox. I recently implemented generic board support for Exynos devices in it and upstreamed it. The way it functions is that it takes the model string from the previous stage bootloader and matches its own device tree based on it, then boots a FIT image, packaged as our ramdisk in the Android boot image.

Adding support for our new Exynos SoC was as simple as:

  • enabling framebuffer refreshing by turning on a bit in the display engine
--- a/arch/arm/boards/samsung-exynos/board.c
+++ b/arch/arm/boards/samsung-exynos/board.c
@@ -12,12 +12,19 @@
 #include <asm/system.h>
 #include <of_address.h>
 
+#define EXYNOS8890_DECON0      0x13960000
 #define EXYNOS8895_DECON0      0x12860000
 #define EXYNOS990_DECON0       0x19050000
 #define HW_SW_TRIG_CONTROL     0x70
+/* bits for exynos8895 onwards */
 #define TRIG_AUTO_MASK_EN      BIT(12)
 #define SW_TRIG_EN             BIT(8)
 #define HW_TRIG_EN             BIT(0)
+/* bits for exynos8890 */
+#define EXYNOS8890_HW_TRIG_CONTROL_TRIG_AUTO_MASK      BIT(12)
+#define EXYNOS8890_HW_TRIG_CONTROL_HW_TRIG_EN          BIT(4)
+#define EXYNOS8890_HW_TRIG_EN  (EXYNOS8890_HW_TRIG_CONTROL_TRIG_AUTO_MASK | \
+                                EXYNOS8890_HW_TRIG_CONTROL_HW_TRIG_EN)
 
 static int exynos_postcore_init(void)
 {
@@ -33,7 +40,11 @@ static int exynos_postcore_init(void)
                trig_ctrl = IOMEM(EXYNOS8895_DECON0 + HW_SW_TRIG_CONTROL);
        else if (of_machine_is_compatible("samsung,exynos990"))
                trig_ctrl = IOMEM(EXYNOS990_DECON0 + HW_SW_TRIG_CONTROL);
-       else
+       else if (of_machine_is_compatible("samsung,exynos8890")) {
+               trig_ctrl = IOMEM(EXYNOS8890_DECON0 + HW_SW_TRIG_CONTROL);
+               writel(EXYNOS8890_HW_TRIG_CONTROL_HW_TRIG_EN, trig_ctrl);
+               return 0;
+       } else
                return 0;
  • telling Barebox what our vendor DT reports as model string:
--- a/arch/arm/boards/samsung-exynos/lowlevel.c
+++ b/arch/arm/boards/samsung-exynos/lowlevel.c
@@ -11,6 +11,7 @@
 #include <asm/cache.h>
 #include <asm/mmu.h>
 
+extern char __dtb_exynos8890_herolte_start[];
 extern char __dtb_exynos8895_dreamlte_start[];
 extern char __dtb_exynos990_x1s_start[];
 
@@ -58,6 +59,8 @@ static noinline void exynos_continue(void *downstream_fdt)
                __dtb_start = __dtb_exynos8895_dreamlte_start;
        } else if (is_model(downstream_fdt, "Samsung X1S")) {
                __dtb_start = __dtb_exynos990_x1s_start;
+       } else if (is_model(downstream_fdt, "Samsung UNIVERSAL8890")) {
+               __dtb_start = __dtb_exynos8890_herolte_start;

And we’re in!

alt

Mainline Linux #

To boot mainline Linux, you’re required to have a defined memory range, interrupt controller and timer. This SoC uses the good-ol’ GIC and armv8-timer, so booting was as simple as building torvalds/linux from source for arm64, packaging as a FIT-image the compiled Image and such minimal device tree:

/ {
	...
	chosen {
		stdout-path = "earlycon=exynos4210,0x14c50000";
	};

	memory@mem_base {
		device_type = "memory";
		reg = <mem_base_addr mem_size_addr>;
	};
	...
	cpus {
		#address-cells = <1>;
		#size-cells = <0>;

		cpu0: cpu@0 {
			device_type = "cpu";
			compatible = "arm,cortex-a53";
			reg = <0x0>;
			enable-method = "psci";
		};
		...
	};

	soc {
		...
		gic: interrupt-controller@c000000 {
			...

			ppi-partitions {
				...
			};
		};
	};

	timer {
		compatible = "arm,armv8-timer";
		...
	};

From where on, using a cable with a 619k resistor between ID and GND, we get UART logs from PostmarketOS initramfs. Buuuut… something is odd…

alt

As it turns out, the Exynos 8890, being the first Samsung SoC with custom Mongoose cores, has a very specific errata with them, where memory accesses to certain virtual addresses can trigger a TLB conflict abort. Disabling them and only using the Cortex-A53 cores works fine temporarily.

Pinctrl and clocks #

Adding support for pin control, unlike on other SoC vendors, is easy, due to the common-ish register layout of pin controller blocks across Exynoses. To do that, I followed the vendor kernel driver with adjustments for the new driver style:

--- a/drivers/pinctrl/samsung/pinctrl-exynos-arm64.c
+++ b/drivers/pinctrl/samsung/pinctrl-exynos-arm64.c
@@ -1476,6 +1476,163 @@  const struct samsung_pinctrl_of_match_data exynosautov920_of_data __initconst =
 	.num_ctrl	= ARRAY_SIZE(exynosautov920_pin_ctrl),
 };
 
+/* pin banks of exynos8890 pin-controller 0 (ALIVE) */
+static const struct samsung_pin_bank_data exynos8890_pin_banks0[] __initconst = {
+	/* Must start with EINTG banks, ordered by EINT group number. */
+	EXYNOS7870_PIN_BANK_EINTW(8, 0x000, "gpa0", 0x00),
+	EXYNOS7870_PIN_BANK_EINTW(8, 0x020, "gpa1", 0x04),
+	EXYNOS7870_PIN_BANK_EINTW(8, 0x040, "gpa2", 0x08),
+	EXYNOS7870_PIN_BANK_EINTW(8, 0x060, "gpa3", 0x0c),
+};
+
+/* pin banks of exynos8890 pin-controller 1 (AUD) */
+static const struct samsung_pin_bank_data exynos8890_pin_banks1[] __initconst = {
+	/* Must start with EINTG banks, ordered by EINT group number. */
+	EXYNOS8895_PIN_BANK_EINTG(7, 0x000, "gph0", 0x00),
+};
+
...
+/* pin banks of exynos8890 pin-controller 10 (TOUCH) */
+static const struct samsung_pin_bank_data exynos8890_pin_banks10[] __initconst = {
+	/* Must start with EINTG banks, ordered by EINT group number. */
+	EXYNOS8895_PIN_BANK_EINTG(3, 0x000, "gpf1", 0x00),
+};
+
+static const struct samsung_pin_ctrl exynos8890_pin_ctrl[] __initconst = {
+	{
+		/* pin-controller instance 0 Alive data */
+		.pin_banks	= exynos8890_pin_banks0,
+		.nr_banks	= ARRAY_SIZE(exynos8890_pin_banks0),
+		.eint_wkup_init = exynos_eint_wkup_init,
+	}, {
...
+	}, {
+		/* pin-controller instance 10 TOUCH data */
+		.pin_banks	= exynos8890_pin_banks10,
+		.nr_banks	= ARRAY_SIZE(exynos8890_pin_banks10),
+		.eint_gpio_init = exynos_eint_gpio_init,
+	},
+};
+
+const struct samsung_pinctrl_of_match_data exynos8890_of_data __initconst = {
+	.ctrl		= exynos8890_pin_ctrl,
+	.num_ctrl	= ARRAY_SIZE(exynos8890_pin_ctrl),
+};
+
 /* pin banks of exynos8895 pin-controller 0 (ALIVE) */
 static const struct samsung_pin_bank_data exynos8895_pin_banks0[] __initconst = {
 	EXYNOS_PIN_BANK_EINTW(8, 0x020, "gpa0", 0x00),
diff --git a/drivers/pinctrl/samsung/pinctrl-samsung.c b/drivers/pinctrl/samsung/pinctrl-samsung.c
index 24745e1d7..f58b7b10f 100644
--- a/drivers/pinctrl/samsung/pinctrl-samsung.c
+++ b/drivers/pinctrl/samsung/pinctrl-samsung.c
@@ -1496,6 +1496,8 @@  static const struct of_device_id samsung_pinctrl_dt_match[] = {
 		.data = &exynos7885_of_data },
 	{ .compatible = "samsung,exynos850-pinctrl",
 		.data = &exynos850_of_data },
+	{ .compatible = "samsung,exynos8890-pinctrl",
+		.data = &exynos8890_of_data },
 	{ .compatible = "samsung,exynos8895-pinctrl",
 		.data = &exynos8895_of_data },
 	{ .compatible = "samsung,exynos9810-pinctrl",
diff --git a/drivers/pinctrl/samsung/pinctrl-samsung.h b/drivers/pinctrl/samsung/pinctrl-samsung.h
index 1cabcbe14..4236d7ad8 100644
--- a/drivers/pinctrl/samsung/pinctrl-samsung.h
+++ b/drivers/pinctrl/samsung/pinctrl-samsung.h
@@ -394,6 +394,7 @@  extern const struct samsung_pinctrl_of_match_data exynos7_of_data;
 extern const struct samsung_pinctrl_of_match_data exynos7870_of_data;
 extern const struct samsung_pinctrl_of_match_data exynos7885_of_data;
 extern const struct samsung_pinctrl_of_match_data exynos850_of_data;
+extern const struct samsung_pinctrl_of_match_data exynos8890_of_data;
 extern const struct samsung_pinctrl_of_match_data exynos8895_of_data;
 extern const struct samsung_pinctrl_of_match_data exynos9810_of_data;
 extern const struct samsung_pinctrl_of_match_data exynos990_of_data;

Clocks are a lot more complex, as all of them are mapped under MMIO and Linux treats them as separate muxes/divs/gates and not composites. Samsung uses their own VCLK and HWACG frameworks (CMUCAL), so I had to rewrite everything to match the upstream format of manually controlling clocks: https://lore.kernel.org/all/20250914122116.2616801-6-ivo.ivanov.ivanov1@gmail.com

And then add it as nodes, so the driver can match CMU blocks with their base addresses:

...
		cmu_top: clock-controller@10570000 {
			compatible = "samsung,exynos8890-cmu-top";
			reg = <0x10570000 0x8000>;
			#clock-cells = <1>;
			clocks = <&oscclk>;
			clock-names = "oscclk";
		};
...

The biggest issue was figuring out which gate clocks I need to always keep enabled and which not to. Otherwise the device may hang. Also, some gate registers can’t even be read, causing an abort:

[    3.814764] udevd[100]: starting eudev-3.2.14
[    3.897351] udevd[114]: failed to execute '/usr/lib/udev/libinput-device-group' 'libinput-device-group /sys/devices/platform/gpio-keys/input/input0/event0': No such file or directory
[    4.014158] Internal error: synchronous external abort: 0000000096000210 [#1]  SMP
[    4.014365] Modules linked in:
[    4.014453] CPU: 1 UID: 0 PID: 125 Comm: cat Tainted: G   M                6.17.0-rc3-00018-g2e6290161af4-dirty #33 PREEMPT 
[    4.014693] Tainted: [M]=MACHINE_CHECK
[    4.017508] Hardware name: Samsung Galaxy S7 (DT)
[    4.022242] pstate: 60000005 (nZCv daif -PAN -UAO -TCO -DIT -SSBS BTYPE=--)
[    4.029253] pc : clk_gate_readl+0x28/0x38
[    4.033285] lr : clk_gate_is_enabled+0x14/0x48
[    4.037755] sp : ffff800081d0ba70
[    4.041086] x29: ffff800081d0ba70 x28: 0000000000000000 x27: 0000000000000000
[    4.048273] x26: 0000000000000000 x25: ffff800080eba743 x24: 0000000000000003
[    4.055460] x23: ffff800080e6eb7a x22: 0000000000000001 x21: ffff800080ea6a80
[    4.062648] x20: ffff000803633000 x19: ffff0008000a0c00 x18: 000000000000000a
[    4.069835] x17: 0000000000000000 x16: 0000000000000000 x15: ffff800081d0b8f0
[    4.077023] x14: 000000000000c350 x13: 0000000000000006 x12: 00000000ffffffff
[    4.084210] x11: 0000000000000003 x10: ffff000801395000 x9 : ffff00080139469a
[    4.091397] x8 : 000000000000c350 x7 : 000000000000c350 x6 : 0000000000000030
[    4.098585] x5 : 0000000000000004 x4 : ffff800080ea6a7f x3 : 0000000000000000
[    4.105773] x2 : ffff00080008fdc0 x1 : 0000000000000000 x0 : ffff800081558844
[    4.112961] Call trace:
[    4.115415]  clk_gate_readl+0x28/0x38 (P)
[    4.119447]  clk_core_is_enabled+0x60/0xe4
[    4.123566]  clk_summary_show_subtree+0xf0/0x20c
[    4.128211]  clk_summary_show_subtree+0x1fc/0x20c
[    4.132944]  clk_summary_show+0xb4/0xbc
[    4.136801]  seq_read_iter+0x1d4/0x384
[    4.140570]  seq_read+0xe4/0x12c
[    4.143813]  full_proxy_read+0x68/0x9c
[    4.147582]  vfs_read+0xa0/0x1a8
[    4.150826]  ksys_read+0x78/0xe4
[    4.154068]  __arm64_sys_read+0x18/0x24
[    4.157925]  invoke_syscall+0x64/0xec
[    4.161606]  el0_svc_common.constprop.0+0xb8/0xd4
[    4.166340]  do_el0_svc+0x1c/0x28
[    4.169670]  el0_svc+0x84/0xd0
[    4.172738]  el0t_64_sync_handler+0x5c/0x13c
[    4.177033]  el0t_64_sync+0x198/0x19c
[    4.180722] Code: d50331bf ca010021 b5000001 d65f03c0 (b9400000) 
[    4.186855] ---[ end trace 0000000000000000 ]---
[    4.192536] [pmOS-rd]: Segmentation fault

Serial busses, storage, other peripherals #

The next step was escaping the initramfs shell. As the S7 has an SD card slot, wired to the only MMC controller of the SoC, I needed to:

  1. Add I2C busses, reusing the existing support for exynos8895 as it’s functionally identical
...
		hsi2c_15: i2c@10550000 {
			compatible = "samsung,exynos8890-hsi2c",
				     "samsung,exynos8895-hsi2c";
			reg = <0x10550000 0x1000>;

			clocks = <&cmu_ccore CLK_GOUT_CCORE_PCLK_HSI2C>;
			clock-names = "hsi2c";

			interrupts = <GIC_SPI 340 IRQ_TYPE_LEVEL_HIGH>;

			pinctrl-0 = <&hsi2c15_bus>;
			pinctrl-names = "default";

			#address-cells = <1>;
			#size-cells = <0>;

			status = "disabled";
		};
...
  1. Implement support for the main PMIC and define the regulators in DT:
&hsi2c_15 {
	#address-cells = <1>;
	#size-cells = <0>;
	status = "okay";

	pmic@66 {
		compatible = "samsung,s2mps16-pmic";
		reg = <0x66>;
		interrupts = <2 IRQ_TYPE_LEVEL_LOW>;
		interrupt-parent = <&gpa0>;
		pinctrl-names = "default";
		pinctrl-0 = <&pmic_irq>;
		wakeup-source;

		s2mps16_osc: clocks {
			compatible = "samsung,s2mps16-clk";
			#clock-cells = <1>;
			clock-output-names = "s2mps16_ap", "s2mps16_cp",
					     "s2mps16_bt";
		};

		regulators {
			s2mps16_buck1: buck1 {
				regulator-always-on;
				regulator-min-microvolt = <500000>;
				regulator-max-microvolt = <1000000>;
				regulator-name = "vdd_buck1";
				regulator-ramp-delay = <12000>;
			};
...
  1. Add the MMC controller in DT, reusing the SMU exynos7 support and setting phandles to the correct regulators:
...
	soc@0 {
		...
		mmc: mmc@15740000 {
			compatible = "samsung,exynos8890-dw-mshc-smu",
				     "samsung,exynos7-dw-mshc-smu";
			reg = <0x15740000 0x2000>;

			assigned-clocks = <&cmu_top CLK_DOUT_TOP_SCLK_FSYS1_MMC2>;
			assigned-clock-rates = <800000000>;

			clocks = <&cmu_fsys1 CLK_GOUT_FSYS1_ACLK_MMC2>,
				 <&cmu_fsys1 CLK_GOUT_FSYS1_SCLK_MMC2>;
			clock-names = "biu", "ciu";

			fifo-depth = <64>;

			interrupts = <GIC_SPI 201 IRQ_TYPE_LEVEL_HIGH>;

			#address-cells = <1>;
			#size-cells = <0>;

			status = "disabled";
		};
		...
	};
...
	vdd_fixed_mmc: regulator-fixed-mmc {
		compatible = "regulator-fixed";

		enable-active-high;
		gpio = <&gpa3 7 GPIO_ACTIVE_HIGH>;

		regulator-max-microvolt = <2800000>;
		regulator-min-microvolt = <2800000>;
		regulator-name = "vdd_fixed_mmc";
	};
};
...
&mmc {
	bus-width = <4>;

	card-detect-delay = <200>;
	cd-gpios = <&gpa1 5 GPIO_ACTIVE_LOW>;

	clock-frequency = <800000000>;

	disable-wp;

	pinctrl-names = "default";
	pinctrl-0 = <&sd2_clk &sd2_cmd &sd2_bus1 &sd2_bus4 &sd2_cd>;

	sd-uhs-sdr50;
	sd-uhs-sdr104;

	vmmc-supply = <&vdd_fixed_mmc>;
	vqmmc-supply = <&s2mps16_ldo2>;

	samsung,dw-mshc-ciu-div = <3>;
	samsung,dw-mshc-sdr-timing = <0 2>;
	samsung,dw-mshc-ddr-timing = <1 2>;

	status = "okay";
};

After which the rootfs from SD card boots and GNOME loads up:

alt

The touchscreen IC is similar to S6SY761, but different in terms of I2C messages and function registers/bits, for which a new driver was needed: https://lists.infradead.org/pipermail/linux-arm-kernel/2025-September/1062429.html

And its corresponding device tree node:

&hsi2c_7 {
	status = "okay";

	touchscreen@48 {
		compatible = "samsung,s6sa552";
		reg = <0x48>;

		avdd-supply = <&s2mps16_ldo33>;
		vdd-supply = <&s2mps16_ldo32>;

		interrupt-parent = <&gpa1>;
		interrupts = <0 IRQ_TYPE_LEVEL_LOW>;

		pinctrl-0 = <&ts_int>;
		pinctrl-names = "default";
	};
};

GPU was straightforward, as the Mali T880 is supported by panfrost, so only this was needed..

		gpu: gpu@14ac0000 {
			compatible = "samsung,exynos8890-mali", "arm,mali-t880";
			reg = <0x14ac0000 0x5000>;

			clocks = <&cmu_g3d CLK_GOUT_G3D_ACLK_G3D>;
			clock-names = "core";

			interrupts = <GIC_SPI 225 IRQ_TYPE_LEVEL_HIGH>,
				     <GIC_SPI 226 IRQ_TYPE_LEVEL_HIGH>,
				     <GIC_SPI 224 IRQ_TYPE_LEVEL_HIGH>;
			interrupt-names = "job", "mmu", "gpu";

			status = "disabled";
		};
...
&gpu {
	/* we can only afford a slightly higher rate till there's dvfs */
	assigned-clocks = <&cmu_top CLK_FOUT_G3D_PLL>;
	assigned-clock-rates = <455000000>;

	mali-supply = <&s2mps16_buck6>;

	status = "okay";
};

..for a graphics accelerated gnome on S7, with Linux 6.17

alt
alt

(Note: I am in no way responsible for any damage you do to your device by trying to replicate what I’ve done. If you want to experiment with flashing directly to your device, proceed at your own risk.)