diff --git a/projects/ROCKNIX/config.xml b/projects/ROCKNIX/config.xml
index 187bb8e9855..3a76e6c37e9 100644
--- a/projects/ROCKNIX/config.xml
+++ b/projects/ROCKNIX/config.xml
@@ -60,6 +60,7 @@
rk3326-magicx-xu-mini-m
rk3326-gameconsole-eeclone
rk3326-batlexp-g350
+ rk3326s-gkd-pixel2
diff --git a/projects/ROCKNIX/devices/RK3326/linux/dts/rockchip/rk3326s-gkd-pixel2.dts b/projects/ROCKNIX/devices/RK3326/linux/dts/rockchip/rk3326s-gkd-pixel2.dts
new file mode 100644
index 00000000000..d7f535f4d51
--- /dev/null
+++ b/projects/ROCKNIX/devices/RK3326/linux/dts/rockchip/rk3326s-gkd-pixel2.dts
@@ -0,0 +1,1011 @@
+// SPDX-License-Identifier: (GPL-2.0+ OR MIT)
+/*
+ * Copyright (c) 2024 GameKiddy
+ *
+ * Reconstructed from the vendor-extracted rk3326s-gkd-pixel2.dtsi into a
+ * mainline/ROCKNIX-style board .dts on top of the shared rk3326.dtsi,
+ * mirroring the other standalone RK3326 boards in this tree (e.g.
+ * rk3326-gameforce-chi.dts).
+ *
+ * No analog sticks - every input is a plain GPIO switch - so the gamepad
+ * uses rocknix-singleadc-joypad with amux-count = <0>, same as the other
+ * all-digital RK3326 boards (e.g. rk3326-gameconsole-r33s.dts).
+ */
+
+/dts-v1/;
+#include
+#include
+#include
+#include
+#include "rk3326.dtsi"
+
+/ {
+ model = "GameKiddy GKD Pixel2";
+ compatible = "gamekiddy,gkd-pixel2", "rockchip,rk3326";
+ chassis-type = "handset";
+
+ aliases {
+ mmc0 = &sdmmc;
+ };
+
+ chosen {
+ stdout-path = "serial2:1500000n8";
+ };
+
+ backlight: backlight {
+ compatible = "pwm-backlight";
+ pwms = <&pwm1 0 25000 1>;
+ /*
+ * The first 17 entries (levels 0-16) are clamped to 17 instead
+ * of counting up from 0: below that, this panel's backlight
+ * circuit can't sustain visible output at all and the screen
+ * just goes dark rather than dimming further. Confirmed on
+ * hardware down to a floor of 15 on one unit and 12 on
+ * another - 17 adds a bit of margin above both for unit-to-
+ * unit variance in the backlight driver circuit we haven't
+ * sampled yet.
+ */
+ brightness-levels = <
+ 17 17 17 17 17 17 17 17
+ 17 17 17 17 17 17 17 17
+ 17 17 18 19 20 21 22 23
+ 24 25 26 27 28 29 30 31
+ 32 33 34 35 36 37 38 39
+ 40 41 42 43 44 45 46 47
+ 48 49 50 51 52 53 54 55
+ 56 57 58 59 60 61 62 63
+ 64 65 66 67 68 69 70 71
+ 72 73 74 75 76 77 78 79
+ 80 81 82 83 84 85 86 87
+ 88 89 90 91 92 93 94 95
+ 96 97 98 99 100 101 102 103
+ 104 105 106 107 108 109 110 111
+ 112 113 114 115 116 117 118 119
+ 120 121 122 123 124 125 126 127
+ 128 129 130 131 132 133 134 135
+ 136 137 138 139 140 141 142 143
+ 144 145 146 147 148 149 150 151
+ 152 153 154 155 156 157 158 159
+ 160 161 162 163 164 165 166 167
+ 168 169 170 171 172 173 174 175
+ 176 177 178 179 180 181 182 183
+ 184 185 186 187 188 189 190 191
+ 192 193 194 195 196 197 198 199
+ 200 201 202 203 204 205 206 207
+ 208 209 210 211 212 213 214 215
+ 216 217 218 219 220 221 222 223
+ 224 225 226 227 228 229 230 231
+ 232 233 234 235 236 237 238 239
+ 240 241 242 243 244 245 246 247
+ 248 249 250 251 252 253 254 255>;
+ default-brightness-level = <200>;
+ };
+
+ battery: battery {
+ compatible = "simple-battery";
+ charge-full-design-microamp-hours = <1730000>;
+ charge-term-current-microamp = <300000>;
+ constant-charge-current-max-microamp = <2000000>;
+ constant-charge-voltage-max-microvolt = <4350000>;
+ factory-internal-resistance-micro-ohms = <111000>;
+ voltage-max-design-microvolt = <4333000>;
+ voltage-min-design-microvolt = <3400000>;
+
+ ocv-capacity-celsius = <20>;
+ ocv-capacity-table-0 = <4333000 100>, <4276000 95>, <4217000 90>, <4163000 85>,
+ <4107000 80>, <4062000 75>, <4011000 70>, <3967000 65>,
+ <3921000 60>, <3878000 55>, <3848000 50>, <3826000 45>,
+ <3805000 40>, <3790000 35>, <3776000 30>, <3754000 25>,
+ <3738000 20>, <3708000 15>, <3694000 10>, <3643000 5>,
+ <3444000 0>;
+ };
+
+ gpio_keys: gpio-keys {
+ compatible = "gpio-keys";
+ pinctrl-names = "default";
+ pinctrl-0 = <&volume_gpio>;
+ autorepeat;
+
+ button-vol-down {
+ gpios = <&gpio3 RK_PB6 GPIO_ACTIVE_LOW>;
+ label = "VOLUMEDOWN";
+ linux,code = ;
+ };
+
+ button-vol-up {
+ gpios = <&gpio3 RK_PB4 GPIO_ACTIVE_LOW>;
+ label = "VOLUMEUP";
+ linux,code = ;
+ };
+ };
+
+ builtin_gamepad: gkd-pixel2-joypad {
+ compatible = "rocknix-singleadc-joypad";
+ pinctrl-names = "default";
+ pinctrl-0 = <&btn_pins>;
+ joypad-name = "gkd_pixel2_joypad";
+ joypad-vendor = <0x0001>;
+ joypad-product = <0x2102>;
+ joypad-revision = <0x0100>;
+ status = "okay";
+
+ /* purely digital switches - no ADC/analog stick present */
+ amux-count = <0>;
+ poll-interval = <10>;
+
+ /*
+ * No rumble properties here - handled by the standalone
+ * "vibrator" node below instead of this driver's own
+ * has_rumble path (see there for why).
+ */
+
+ button-up {
+ gpios = <&gpio3 RK_PB5 GPIO_ACTIVE_LOW>;
+ label = "DPAD-UP";
+ linux,code = ;
+ };
+
+ button-down {
+ gpios = <&gpio3 RK_PC4 GPIO_ACTIVE_LOW>;
+ label = "DPAD-DOWN";
+ linux,code = ;
+ };
+
+ button-left {
+ gpios = <&gpio3 RK_PB3 GPIO_ACTIVE_LOW>;
+ label = "DPAD-LEFT";
+ linux,code = ;
+ };
+
+ button-right {
+ gpios = <&gpio3 RK_PA4 GPIO_ACTIVE_LOW>;
+ label = "DPAD-RIGHT";
+ linux,code = ;
+ };
+
+ button-a {
+ gpios = <&gpio3 RK_PD1 GPIO_ACTIVE_LOW>;
+ label = "SOUTH";
+ linux,code = ;
+ };
+
+ button-b {
+ gpios = <&gpio3 RK_PD2 GPIO_ACTIVE_LOW>;
+ label = "EAST";
+ linux,code = ;
+ };
+
+ button-x {
+ gpios = <&gpio3 RK_PA5 GPIO_ACTIVE_LOW>;
+ label = "NORTH";
+ linux,code = ;
+ };
+
+ button-y {
+ gpios = <&gpio3 RK_PA6 GPIO_ACTIVE_LOW>;
+ label = "WEST";
+ linux,code = ;
+ };
+
+ button-l1 {
+ gpios = <&gpio3 RK_PC0 GPIO_ACTIVE_LOW>;
+ label = "TOP-LEFT";
+ linux,code = ;
+ };
+
+ button-r1 {
+ gpios = <&gpio3 RK_PB2 GPIO_ACTIVE_LOW>;
+ label = "TOP-RIGHT";
+ linux,code = ;
+ };
+
+ button-l2 {
+ gpios = <&gpio3 RK_PC1 GPIO_ACTIVE_LOW>;
+ label = "TOP-LEFT2";
+ linux,code = ;
+ };
+
+ button-r2 {
+ gpios = <&gpio3 RK_PC3 GPIO_ACTIVE_LOW>;
+ label = "TOP-RIGHT2";
+ linux,code = ;
+ };
+
+ button-select {
+ gpios = <&gpio3 RK_PC2 GPIO_ACTIVE_LOW>;
+ label = "SELECT";
+ linux,code = ;
+ };
+
+ button-start {
+ gpios = <&gpio3 RK_PD3 GPIO_ACTIVE_LOW>;
+ label = "START";
+ linux,code = ;
+ };
+
+ button-menu {
+ gpios = <&gpio3 RK_PC5 GPIO_ACTIVE_LOW>;
+ label = "FUNCTION";
+ linux,code = ;
+ };
+ };
+
+ /*
+ * Rumble motor - fixed and confirmed working on real hardware.
+ *
+ * Handled via the mainline pwm-vibrator driver (CONFIG_INPUT_PWM_VIBRA=y,
+ * built into the kernel image), not rocknix-singleadc-joypad's own
+ * has_rumble path - that driver is an out-of-tree module that doesn't
+ * probe until ~10s into boot, vs. ~0.65s for pwm-vibrator on stock
+ * firmware (reference_stock_dmesg.txt). Sibling boards
+ * (rg351m/rg351v/gameforce-chi) already use this same built-in driver.
+ *
+ * Polarity matches the vendor-extracted rk3326s-gkd-pixel2.dtsi
+ * exactly, but period does not: the vendor's 200000000ns (5Hz) is
+ * unusually slow for motor PWM and produces clearly perceptible
+ * individual on/off pulses at any partial duty (confirmed on real
+ * hardware), not just a boot-time artifact - it'd affect any
+ * variable-strength rumble effect. 1000000ns (1kHz) matches sibling
+ * RK3326 boards (rg351m/rg351v) and gives smooth-feeling vibration
+ * at any strength.
+ *
+ * Polarity alone isn't sufficient to fix the stuck-on-boot bug
+ * though: pwm-vibra.c's probe does the same naive disable
+ * rocknix-singleadc-joypad originally did, so
+ * patches/linux/030-pwm-vibra-kick-stuck-boot-state.patch ports the
+ * same probe-time "kick" fix into pwm-vibra.c itself. See git history
+ * on this file for the full debugging account.
+ */
+ vibrator: pwm-vibrator {
+ compatible = "pwm-vibrator";
+ pwms = <&pwm0 0 1000000 0>;
+ pwm-names = "enable";
+ };
+
+ rk817-sound {
+ compatible = "simple-audio-card";
+ simple-audio-card,name = "rk817_int";
+ simple-audio-card,format = "i2s";
+ simple-audio-card,hp-det-gpio = <&gpio2 RK_PC6 GPIO_ACTIVE_HIGH>;
+ simple-audio-card,mclk-fs = <256>;
+ simple-audio-card,widgets =
+ "Microphone", "Mic Jack",
+ "Headphone", "Headphones",
+ "Speaker", "Speaker";
+ simple-audio-card,routing =
+ "MICL", "Mic Jack",
+ "Headphones", "HPOL",
+ "Headphones", "HPOR",
+ "Speaker", "SPKO";
+
+ simple-audio-card,codec {
+ sound-dai = <&rk817>;
+ };
+
+ simple-audio-card,cpu {
+ sound-dai = <&i2s1_2ch>;
+ };
+ };
+
+ vccsys: regulator-vccsys {
+ compatible = "regulator-fixed";
+ regulator-name = "vcc3v8_sys";
+ regulator-always-on;
+ regulator-boot-on;
+ regulator-min-microvolt = <3800000>;
+ regulator-max-microvolt = <3800000>;
+ };
+
+ vcc_mipi_lcd0: regulator-vcc-mipi-lcd0 {
+ compatible = "regulator-fixed";
+ regulator-name = "vcc_mipi_lcd0";
+ gpio = <&gpio0 RK_PB5 GPIO_ACTIVE_LOW>;
+ pinctrl-0 = <&vcc_mipi_lcd_n>;
+ pinctrl-names = "default";
+ regulator-boot-on;
+
+ regulator-state-mem {
+ regulator-off-in-suspend;
+ };
+ };
+
+ leds {
+ compatible = "gpio-leds";
+ pinctrl-names = "default";
+ pinctrl-0 = <&led_gpio>;
+
+ led-0 {
+ gpios = <&gpio1 RK_PB2 GPIO_ACTIVE_HIGH>;
+ label = "battery0";
+ };
+ led-1 {
+ gpios = <&gpio1 RK_PB4 GPIO_ACTIVE_HIGH>;
+ label = "battery1";
+ };
+ led-2 {
+ gpios = <&gpio1 RK_PB5 GPIO_ACTIVE_HIGH>;
+ label = "battery2";
+ };
+ led-3 {
+ gpios = <&gpio1 RK_PB6 GPIO_ACTIVE_HIGH>;
+ label = "battery3";
+ };
+ led-4 {
+ gpios = <&gpio1 RK_PB7 GPIO_ACTIVE_HIGH>;
+ label = "battery4";
+ };
+ };
+
+ /*
+ * Forwards these values to BL31 firmware via SIP calls so it can arm
+ * wake sources for PX30S's firmware-controlled deep suspend states -
+ * see 031-px30s-suspend-wakeup-config.patch and the KNOWN ISSUE
+ * comment on the rk817 node below. Values match the vendor-extracted
+ * rk3326s-gkd-pixel2.dtsi exactly.
+ */
+ rockchip_suspend: rockchip-suspend {
+ compatible = "rockchip,pm-px30";
+ status = "okay";
+ rockchip,sleep-debug-en = <0>;
+ rockchip,sleep-mode-config = <132866>;
+ rockchip,wakeup-config = <133>;
+ };
+
+ /*
+ * This board's own decompiled vendor devicetree wires &cpu0 to its
+ * generic cpu0_opp_table (not a PX30S-specific one) with all nine of
+ * vendor's real frequency steps - confirmed from the real, compiled
+ * vendor devicetree blob, not just the shared BSP source tree, and no
+ * vendor C code was found that ever swaps this at runtime. Mainline's
+ * own shared cpu0_opp_table in px30.dtsi only carries three of those
+ * nine steps, and is used by several *other* RK3326 boards in this
+ * tree by default - extending or replacing it directly would have
+ * silently changed their CPU/GPU behavior too, so instead this is a
+ * board-local copy, used only here. All nine CPU steps and all five
+ * GPU steps use each entry's plain (unsuffixed) opp-microvolt value,
+ * which in vendor's table always equals its own -L0 row: the safest,
+ * highest-margin bin, since mainline has no driver to parse the
+ * per-chip rockchip,bin-scaling-sel/opp-microvolt-Lx AVS selection
+ * vendor's own driver uses instead.
+ *
+ * An earlier version of this board's CPU/GPU tables used a separate,
+ * statically lower-voltage px30s_cpu0_opp_table/px30s_gpu_opp_table
+ * pair instead (ported from the vendor BSP's PX30S-suffixed tables).
+ * Real hardware testing showed the CPU overclock toggle (1416/1512MHz)
+ * then froze the system, even though this exact silicon had already
+ * run stable at 1416MHz under the *generic* table's higher 1.35V.
+ * Lower voltage being less stable than higher voltage at the same
+ * frequency doesn't fit an overvoltage theory; it fits 1512MHz itself
+ * being under-provisioned by the px30s table's lower ceiling - so
+ * this now matches the real vendor board wiring instead.
+ */
+ gkd_pixel2_cpu0_opp_table: gkd-pixel2-cpu0-opp-table {
+ compatible = "operating-points-v2";
+ opp-shared;
+
+ opp-408000000 {
+ opp-hz = /bits/ 64 <408000000>;
+ opp-microvolt = <950000 950000 1350000>;
+ clock-latency-ns = <40000>;
+ opp-suspend;
+ };
+ opp-600000000 {
+ opp-hz = /bits/ 64 <600000000>;
+ opp-microvolt = <950000 950000 1350000>;
+ clock-latency-ns = <40000>;
+ };
+ opp-816000000 {
+ opp-hz = /bits/ 64 <816000000>;
+ opp-microvolt = <1050000 1050000 1350000>;
+ clock-latency-ns = <40000>;
+ };
+ opp-1008000000 {
+ opp-hz = /bits/ 64 <1008000000>;
+ opp-microvolt = <1175000 1175000 1350000>;
+ clock-latency-ns = <40000>;
+ };
+ opp-1200000000 {
+ opp-hz = /bits/ 64 <1200000000>;
+ opp-microvolt = <1300000 1300000 1350000>;
+ clock-latency-ns = <40000>;
+ };
+ opp-1248000000 {
+ opp-hz = /bits/ 64 <1248000000>;
+ opp-microvolt = <1350000 1350000 1350000>;
+ clock-latency-ns = <40000>;
+ };
+ opp-1296000000 {
+ opp-hz = /bits/ 64 <1296000000>;
+ opp-microvolt = <1350000 1350000 1350000>;
+ clock-latency-ns = <40000>;
+ };
+ opp-1416000000 {
+ opp-hz = /bits/ 64 <1416000000>;
+ opp-microvolt = <1350000 1350000 1350000>;
+ clock-latency-ns = <40000>;
+ turbo-mode;
+ };
+ /*
+ * 1512MHz disabled: this top step can trigger a full device
+ * reset on its own, even from a fresh boot with no suspend/
+ * resume involved (confirmed by direct reproduction - a single
+ * isolated overclock/boost toggle was enough). 1416MHz remains
+ * available as the highest turbo-mode step.
+ */
+ /*
+ opp-1512000000 {
+ opp-hz = /bits/ 64 <1512000000>;
+ opp-microvolt = <1350000 1350000 1350000>;
+ clock-latency-ns = <40000>;
+ turbo-mode;
+ };
+ */
+ };
+
+ gkd_pixel2_gpu_opp_table: gkd-pixel2-gpu-opp-table {
+ compatible = "operating-points-v2";
+
+ opp-200000000 {
+ opp-hz = /bits/ 64 <200000000>;
+ opp-microvolt = <950000>;
+ };
+ opp-300000000 {
+ opp-hz = /bits/ 64 <300000000>;
+ opp-microvolt = <975000>;
+ };
+ opp-400000000 {
+ opp-hz = /bits/ 64 <400000000>;
+ opp-microvolt = <1050000>;
+ };
+ opp-480000000 {
+ opp-hz = /bits/ 64 <480000000>;
+ opp-microvolt = <1125000>;
+ };
+ opp-520000000 {
+ opp-hz = /bits/ 64 <520000000>;
+ opp-microvolt = <1175000>;
+ };
+ };
+};
+
+&cpu0 {
+ operating-points-v2 = <&gkd_pixel2_cpu0_opp_table>;
+ cpu-supply = <&vdd_arm>;
+};
+
+&cpu1 {
+ operating-points-v2 = <&gkd_pixel2_cpu0_opp_table>;
+ cpu-supply = <&vdd_arm>;
+};
+
+&cpu2 {
+ operating-points-v2 = <&gkd_pixel2_cpu0_opp_table>;
+ cpu-supply = <&vdd_arm>;
+};
+
+&cpu3 {
+ operating-points-v2 = <&gkd_pixel2_cpu0_opp_table>;
+ cpu-supply = <&vdd_arm>;
+};
+
+&display_subsystem {
+ status = "okay";
+};
+
+&dsi {
+ #address-cells = <1>;
+ #size-cells = <0>;
+ status = "okay";
+
+ internal_display: panel@0 {
+ reg = <0>;
+ /*
+ * Uses the rocknix,generic-dsi driver (already used by
+ * several other RK3326 boards, e.g. rk3326-gameconsole-
+ * eeclone.dts) with an init sequence reconstructed for this
+ * panel, rather than the previous gkd,pixel2-panel/st7701
+ * driver approach (029-gkd-pixel2-st7701-panel.patch, since
+ * removed - confirmed working on real hardware with this
+ * driver instead). vdd-supply/iovcc-supply (not VCC-supply/
+ * IOVCC-supply) are this driver's actual regulator binding
+ * names - see devm_regulator_get(dev, "vdd"/"iovcc") in
+ * panel-generic-dsi.c. reset-gpios is GPIO_ACTIVE_LOW here,
+ * matching the vendor-extracted rk3326s-gkd-pixel2.dtsi -
+ * confirmed on real hardware (screen was blank at
+ * GPIO_ACTIVE_HIGH).
+ */
+ compatible = "rocknix,generic-dsi";
+ panel_description =
+ "G size=68,121 delays=2,1,20,120,20 format=rgb888 lanes=2 flags=0xe03",
+ "M clock=25000 horizontal=480,40,10,40 vertical=640,40,10,40 default=1",
+ "I seq=ff7701000013",
+ "I seq=ef08",
+ "I seq=ff7701000010",
+ "I seq=c04f00", "I seq=c1100c", "I seq=c20114", "I seq=cc10",
+ "I seq=b0000b130d10070208071f04110f282f1f",
+ "I seq=b1000c130c10050208081e05131127301f",
+ "I seq=ff7701000011",
+ "I seq=b04d", "I seq=b14d", "I seq=b287", "I seq=b380", "I seq=b545", "I seq=b785", "I seq=b820",
+ "I seq=c009", "I seq=c178", "I seq=c278",
+ "I seq=d088 wait=100",
+ "I seq=e0000002",
+ "I seq=e104b006b005b007b0004444",
+ "I seq=e22020444496a0000096a00000",
+ "I seq=e300002222",
+ "I seq=e44444",
+ "I seq=e50c90b0a00e92b0a0088cb0a00a8eb0a0",
+ "I seq=e600002222",
+ "I seq=e74444",
+ "I seq=e80d91b0a00f93b0a0098db0a00b8fb0a0",
+ "I seq=e93600",
+ "I seq=eb0000e4e4448840",
+ "I seq=edc1a2bf0f6745ffffffff5476f0fb2a1c",
+ "I seq=ef100d04083f1f",
+ "I seq=ff7701000013",
+ "I seq=e8000e",
+ "I seq=ff7701000000",
+ "I seq=11 wait=120",
+ "I seq=ff7701000013",
+ "I seq=e8000c wait=10",
+ "I seq=e80000",
+ "I seq=ff7701000000",
+ "I seq=29 wait=50";
+ backlight = <&backlight>;
+ vdd-supply = <&vcc_mipi_lcd0>;
+ iovcc-supply = <&vcc_mipi_lcd0>;
+ enable-gpios = <&gpio0 RK_PC1 GPIO_ACTIVE_HIGH>;
+ reset-gpios = <&gpio0 RK_PB4 GPIO_ACTIVE_LOW>;
+ pinctrl-names = "default";
+ pinctrl-0 = <&dsi0_lcd_gpio>;
+ /*
+ * 90 produced an upside-down image (180 degrees off from
+ * correct) once the panel actually started displaying
+ * anything - this panel is mounted 270 degrees relative to
+ * its native portrait orientation in this chassis, not 90.
+ */
+ rotation = <270>;
+
+ port {
+ mipi_in_panel: endpoint {
+ remote-endpoint = <&mipi_out_panel>;
+ };
+ };
+ };
+
+ ports {
+ mipi_out: port@1 {
+ reg = <1>;
+ mipi_out_panel: endpoint {
+ remote-endpoint = <&mipi_in_panel>;
+ };
+ };
+ };
+};
+
+&dsi_dphy {
+ compatible = "rockchip,px30s-dsi-dphy", "rockchip,px30-dsi-dphy";
+ status = "okay";
+};
+
+&gpu {
+ /*
+ * See gkd_pixel2_cpu0_opp_table's comment above - same reasoning
+ * applies here: the real vendor board wires &gpu to its generic
+ * gpu_opp_table, with all five of vendor's real frequency steps
+ * (mainline's shared gpu_opp_table in px30.dtsi has only a single,
+ * non-vendor-sourced 560MHz entry, and is used by default by several
+ * other RK3326 boards in this tree, so this is a board-local copy
+ * rather than a change to that shared table).
+ */
+ operating-points-v2 = <&gkd_pixel2_gpu_opp_table>;
+ mali-supply = <&vdd_logic>;
+ status = "okay";
+};
+
+&i2c0 {
+ clock-frequency = <400000>;
+ i2c-scl-falling-time-ns = <16>;
+ i2c-scl-rising-time-ns = <280>;
+ status = "okay";
+
+ /*
+ * KNOWN ISSUE: device suspends correctly but never wakes (confirmed
+ * on hardware). Two fixes were tried and reverted after testing
+ * showed no change: (1) dev_pm_set_wake_irq() for the rk805-pwrkey
+ * fall_irq, and (2) a one-shot port of rockchip_pm_config.c that only
+ * forwarded rockchip,sleep-mode-config/wakeup-config once, at probe
+ * time. On PX30S the deepest sleep states are governed by ATF/BL31
+ * firmware rather than Linux.
+ *
+ * Third attempt in progress: 031-px30s-suspend-wakeup-config.patch
+ * (see the rockchip-suspend node above) additionally sends
+ * LINUX_PM_STATE via SIP on every suspend attempt through a
+ * dev_pm_ops .prepare hook, matching the vendor driver exactly -
+ * the one-shot probe-time-only port never sent this at all. NOT yet
+ * confirmed on real hardware.
+ */
+ rk817: pmic@20 {
+ compatible = "rockchip,rk817";
+ reg = <0x20>;
+ interrupt-parent = <&gpio0>;
+ interrupts = ;
+ clock-output-names = "rk808-clkout1", "xin32k";
+ clock-names = "mclk";
+ clocks = <&cru SCLK_I2S1_OUT>;
+ pinctrl-names = "default";
+ pinctrl-0 = <&pmic_int>, <&i2s1_2ch_mclk>;
+ rockchip,system-power-controller;
+ wakeup-source;
+ #clock-cells = <1>;
+ #sound-dai-cells = <0>;
+
+ vcc1-supply = <&vccsys>;
+ vcc2-supply = <&vccsys>;
+ vcc3-supply = <&vccsys>;
+ vcc4-supply = <&vccsys>;
+ vcc5-supply = <&vccsys>;
+ vcc6-supply = <&vccsys>;
+ vcc7-supply = <&vccsys>;
+ vcc8-supply = <&vccsys>;
+ vcc9-supply = <&dcdc_boost>;
+
+ regulators {
+ vdd_logic: DCDC_REG1 {
+ regulator-name = "vdd_logic";
+ regulator-min-microvolt = <850000>;
+ regulator-max-microvolt = <1350000>;
+ regulator-ramp-delay = <6001>;
+ regulator-initial-mode = <2>;
+ regulator-always-on;
+ regulator-boot-on;
+
+ regulator-state-mem {
+ regulator-on-in-suspend;
+ regulator-suspend-microvolt = <950000>;
+ };
+ };
+
+ vdd_arm: DCDC_REG2 {
+ regulator-name = "vdd_arm";
+ regulator-min-microvolt = <850000>;
+ regulator-max-microvolt = <1350000>;
+ regulator-ramp-delay = <6001>;
+ regulator-initial-mode = <2>;
+ regulator-always-on;
+ regulator-boot-on;
+
+ regulator-state-mem {
+ regulator-off-in-suspend;
+ regulator-suspend-microvolt = <950000>;
+ };
+ };
+
+ vcc_ddr: DCDC_REG3 {
+ regulator-name = "vcc_ddr";
+ regulator-initial-mode = <2>;
+ regulator-always-on;
+ regulator-boot-on;
+
+ regulator-state-mem {
+ regulator-on-in-suspend;
+ };
+ };
+
+ vcc_3v0: DCDC_REG4 {
+ regulator-name = "vcc_3v0";
+ regulator-min-microvolt = <3000000>;
+ regulator-max-microvolt = <3000000>;
+ regulator-initial-mode = <2>;
+ regulator-always-on;
+ regulator-boot-on;
+
+ regulator-state-mem {
+ regulator-off-in-suspend;
+ regulator-suspend-microvolt = <3000000>;
+ };
+ };
+
+ vcc_1v0: LDO_REG1 {
+ regulator-name = "vcc_1v0";
+ regulator-min-microvolt = <1000000>;
+ regulator-max-microvolt = <1000000>;
+ regulator-always-on;
+ regulator-boot-on;
+
+ regulator-state-mem {
+ regulator-off-in-suspend;
+ regulator-suspend-microvolt = <1000000>;
+ };
+ };
+
+ vcc1v8_soc: LDO_REG2 {
+ regulator-name = "vcc1v8_soc";
+ regulator-min-microvolt = <1800000>;
+ regulator-max-microvolt = <1800000>;
+ regulator-always-on;
+ regulator-boot-on;
+
+ regulator-state-mem {
+ regulator-on-in-suspend;
+ regulator-suspend-microvolt = <1800000>;
+ };
+ };
+
+ vdd1v0_soc: LDO_REG3 {
+ regulator-name = "vcc1v0_soc";
+ regulator-min-microvolt = <1000000>;
+ regulator-max-microvolt = <1000000>;
+ regulator-always-on;
+ regulator-boot-on;
+
+ regulator-state-mem {
+ regulator-on-in-suspend;
+ regulator-suspend-microvolt = <1000000>;
+ };
+ };
+
+ vcc3v0_pmu: LDO_REG4 {
+ regulator-name = "vcc3v0_pmu";
+ regulator-min-microvolt = <3000000>;
+ regulator-max-microvolt = <3000000>;
+ regulator-always-on;
+ regulator-boot-on;
+
+ regulator-state-mem {
+ regulator-on-in-suspend;
+ regulator-suspend-microvolt = <3000000>;
+ };
+ };
+
+ vccio_sd: LDO_REG5 {
+ regulator-name = "vccio_sd";
+ regulator-min-microvolt = <1800000>;
+ regulator-max-microvolt = <3300000>;
+ regulator-always-on;
+ regulator-boot-on;
+
+ regulator-state-mem {
+ regulator-off-in-suspend;
+ regulator-suspend-microvolt = <3300000>;
+ };
+ };
+
+ vcc_sd: LDO_REG6 {
+ regulator-name = "vcc_sd";
+ regulator-min-microvolt = <3300000>;
+ regulator-max-microvolt = <3300000>;
+ regulator-boot-on;
+
+ regulator-state-mem {
+ regulator-off-in-suspend;
+ regulator-suspend-microvolt = <3300000>;
+ };
+ };
+
+ dcdc_boost: BOOST {
+ regulator-name = "boost";
+ regulator-min-microvolt = <5000000>;
+ regulator-max-microvolt = <5000000>;
+ regulator-always-on;
+ regulator-boot-on;
+
+ regulator-state-mem {
+ regulator-off-in-suspend;
+ };
+ };
+
+ otg_switch: OTG_SWITCH {
+ regulator-name = "otg_switch";
+
+ regulator-state-mem {
+ regulator-off-in-suspend;
+ };
+ };
+ };
+
+ rk817_charger: charger {
+ rockchip,resistor-sense-micro-ohms = <10000>;
+ rockchip,sleep-enter-current-microamp = <300000>;
+ rockchip,sleep-filter-current-microamp = <100000>;
+ monitored-battery = <&battery>;
+ };
+
+ rk817_codec: codec {
+ rockchip,mic-in-differential;
+ };
+ };
+};
+
+&i2s1_2ch {
+ status = "okay";
+};
+
+&io_domains {
+ vccio1-supply = <&vcc_3v0>;
+ vccio2-supply = <&vccio_sd>;
+ vccio3-supply = <&vcc_3v0>;
+ vccio4-supply = <&vcc_3v0>;
+ vccio5-supply = <&vcc_3v0>;
+ vccio6-supply = <&vcc_3v0>;
+ status = "okay";
+};
+
+&pmu_io_domains {
+ pmuio1-supply = <&vcc3v0_pmu>;
+ pmuio2-supply = <&vcc3v0_pmu>;
+ status = "okay";
+};
+
+/* pwm0 drives the rumble motor (see comment on the joypad node above). */
+&pwm0 {
+ status = "okay";
+};
+
+&pwm1 {
+ status = "okay";
+};
+
+&sdmmc {
+ bus-width = <4>;
+ cap-mmc-highspeed;
+ cap-sd-highspeed;
+ card-detect-delay = <800>;
+ cd-gpios = <&gpio0 RK_PA3 GPIO_ACTIVE_LOW>;
+ disable-wp;
+ sd-uhs-sdr12;
+ sd-uhs-sdr25;
+ sd-uhs-sdr50;
+ sd-uhs-sdr104;
+ vmmc-supply = <&vcc_sd>;
+ vqmmc-supply = <&vccio_sd>;
+ status = "okay";
+};
+
+/*
+ * This board's silicon is PX30S, a distinct chip variant from plain PX30
+ * (confirmed via DDR_GRF_CON1 bits[15:14] == 0b11), needing a different
+ * ADC init sequence, a mandatory 4MHz clock rate, a ~2.1ms post-enable
+ * settle delay, and a linear code<->temp conversion instead of a lookup
+ * table - see 023-px30s-tsadc-support.patch, which adds the
+ * "rockchip,px30s-tsadc" compatible used below.
+ *
+ * If a silent reset recurs, check the hardware rockchip,hw-tshut-temp
+ * auto-reset circuit first - it sits outside Linux's control entirely.
+ */
+&tsadc {
+ compatible = "rockchip,px30s-tsadc";
+ status = "okay";
+};
+
+&u2phy {
+ status = "okay";
+
+ u2phy_host: host-port {
+ status = "okay";
+ };
+
+ u2phy_otg: otg-port {
+ status = "okay";
+ };
+};
+
+&usb20_otg {
+ /*
+ * Without this, dwc2 never enables the PMIC's otg_switch 5V boost
+ * regulator in host mode (confirmed via stock-firmware dmesg: dwc2
+ * looks up "vbus-supply" and otg_switch stays disabled with nothing
+ * to enable it) - so a plugged-in peripheral gets data lines but no
+ * bus power and never enumerates.
+ */
+ vbus-supply = <&otg_switch>;
+ /*
+ * px30.dtsi's shared usb-role-switch (022-usb-role-switch.patch)
+ * defaults every RK3326 board to host mode. That's been harmless
+ * elsewhere - no other board wires vbus-supply, so "default host"
+ * never actually drives VBUS. Here it does: the port permanently
+ * self-sources 5V from boot, which starves dwc2's ID/VBUS-valid
+ * detection of any real transition to notice and switch to device
+ * mode - so plugging into a charger/PC never auto-negotiates
+ * charging, only forcing gadget mode manually does. The vendor
+ * kernel never sets a default mode at all here (plain dr_mode =
+ * "otg", nothing else) and lets dwc2's own hardware ID/VBUS
+ * detection decide from a neutral starting point - drop the
+ * inherited default to match that.
+ */
+ /delete-property/ role-switch-default-mode;
+ status = "okay";
+};
+
+&uart2 {
+ pinctrl-names = "default";
+ pinctrl-0 = <&uart2m1_xfer>;
+ status = "okay";
+};
+
+&vopb {
+ status = "okay";
+};
+
+&vopb_mmu {
+ status = "okay";
+};
+
+&pinctrl {
+ btns {
+ btn_pins: btn-pins {
+ rockchip,pins =
+ <3 RK_PB5 RK_FUNC_GPIO &pcfg_pull_up>,
+ <3 RK_PC4 RK_FUNC_GPIO &pcfg_pull_up>,
+ <3 RK_PB3 RK_FUNC_GPIO &pcfg_pull_up>,
+ <3 RK_PA4 RK_FUNC_GPIO &pcfg_pull_up>,
+ <3 RK_PD1 RK_FUNC_GPIO &pcfg_pull_up>,
+ <3 RK_PD2 RK_FUNC_GPIO &pcfg_pull_up>,
+ <3 RK_PA5 RK_FUNC_GPIO &pcfg_pull_up>,
+ <3 RK_PA6 RK_FUNC_GPIO &pcfg_pull_up>,
+ <3 RK_PC5 RK_FUNC_GPIO &pcfg_pull_up>,
+ <3 RK_PD3 RK_FUNC_GPIO &pcfg_pull_up>,
+ <3 RK_PC2 RK_FUNC_GPIO &pcfg_pull_up>,
+ <3 RK_PC0 RK_FUNC_GPIO &pcfg_pull_up>,
+ <3 RK_PB2 RK_FUNC_GPIO &pcfg_pull_up>,
+ <3 RK_PC1 RK_FUNC_GPIO &pcfg_pull_up>,
+ <3 RK_PC3 RK_FUNC_GPIO &pcfg_pull_up>;
+ };
+ };
+
+ keys {
+ volume_gpio: volume-gpio {
+ rockchip,pins =
+ <3 RK_PB4 RK_FUNC_GPIO &pcfg_pull_up>,
+ <3 RK_PB6 RK_FUNC_GPIO &pcfg_pull_up>;
+ };
+ };
+
+ headphone {
+ hp_det: hp-det {
+ rockchip,pins = <2 RK_PC6 RK_FUNC_GPIO &pcfg_pull_none>;
+ };
+ };
+
+ led {
+ led_gpio: led-gpio {
+ rockchip,pins =
+ <1 RK_PB2 RK_FUNC_GPIO &pcfg_pull_none>,
+ <1 RK_PB4 RK_FUNC_GPIO &pcfg_pull_none>,
+ <1 RK_PB5 RK_FUNC_GPIO &pcfg_pull_none>,
+ <1 RK_PB6 RK_FUNC_GPIO &pcfg_pull_none>,
+ <1 RK_PB7 RK_FUNC_GPIO &pcfg_pull_none>;
+ };
+ };
+
+ dsi0-lcd {
+ dsi0_lcd_gpio: dsi0-lcd-gpio {
+ rockchip,pins =
+ <0 RK_PC1 RK_FUNC_GPIO &pcfg_pull_none>,
+ <0 RK_PB4 RK_FUNC_GPIO &pcfg_pull_up>;
+ };
+ };
+
+ vcc-mipi-lcd {
+ vcc_mipi_lcd_n: vcc-mipi-lcd-n {
+ rockchip,pins = <0 RK_PB5 RK_FUNC_GPIO &pcfg_pull_none>;
+ };
+ };
+
+ pmic {
+ pmic_int: pmic-int {
+ rockchip,pins = <0 RK_PB2 RK_FUNC_GPIO &pcfg_pull_up>;
+ };
+
+ soc_slppin_gpio: soc_slppin_gpio {
+ rockchip,pins = <0 RK_PA4 RK_FUNC_GPIO &pcfg_output_low>;
+ };
+
+ soc_slppin_rst: soc_slppin_rst {
+ rockchip,pins = <0 RK_PA4 2 &pcfg_pull_none>;
+ };
+
+ soc_slppin_slp: soc_slppin_slp {
+ rockchip,pins = <0 RK_PA4 1 &pcfg_pull_none>;
+ };
+ };
+};
diff --git a/projects/ROCKNIX/devices/RK3326/packages/u-boot/config/b_boot.ini b/projects/ROCKNIX/devices/RK3326/packages/u-boot/config/b_boot.ini
index 0631face267..72b52de216e 100644
--- a/projects/ROCKNIX/devices/RK3326/packages/u-boot/config/b_boot.ini
+++ b/projects/ROCKNIX/devices/RK3326/packages/u-boot/config/b_boot.ini
@@ -39,8 +39,31 @@ if test -n "${hwid_adc}"; then setenv hwid_adc b,${hwid_adc};
else setenv hwid_adc b,${hwrev}
fi
-# Depending on ADC value select a proper DTB
-if itest.w ${adc} > 139 && itest.w ${adc} < 191; then
+# GKD Pixel2 (PX30S silicon) is folded into this subdevice rather than
+# split out on its own, since its u-boot/kernel/system payload is
+# identical to every other board here - only the DTB differs. It can't
+# be told apart via the ADC hwid mechanism below like the other boards:
+# this board has no analog stick wired to the SARADC channel the hwid
+# read samples, so that pin floats and can land anywhere in the ADC
+# range on any given boot. Detect it directly from the SoC's own
+# silicon instead: PX30S and plain PX30 share the same base ID but
+# differ in the DDR_GRF_CON1 register (bits 15:14 read 0b11 only on
+# PX30S) - see rockchip_cpuinfo's px30_init() in
+# 026-px30s-cpuinfo-soc-detection.patch, ported from the vendor
+# kernel's 84d26633c7a4 ("soc: rockchip: add cpuid for px30/px30s").
+# This is a hard silicon fact, so it's checked before, and takes
+# priority over, the ADC-based board selection below. (Using division
+# + modulo here rather than a bitwise AND - a bare "&" is a shell
+# control character even in u-boot's hush, and broke script parsing
+# when tried; division by 0x4000 shifts right 14 bits, then %4 isolates
+# just those two bits, with no ambiguous characters involved.)
+setexpr.l px30_ddr_grf_con1 *0xff630004
+setexpr px30_variant_shifted ${px30_ddr_grf_con1} / 0x4000
+setexpr px30_variant_bits ${px30_variant_shifted} % 4
+if test ${px30_variant_bits} = 3; then
+ echo "PX30S silicon detected - GKD Pixel2"
+ setenv fdtfile "rk3326s-gkd-pixel2.dtb"
+elif itest.w ${adc} > 139 && itest.w ${adc} < 191; then
setenv fdtfile "rk3326-powkiddy-rgb10x.dtb"
elif itest.w ${adc} > 999 && itest.w ${adc} < 1051; then
setenv fdtfile "rk3326-magicx-xu-mini-m.dtb"
diff --git a/projects/ROCKNIX/devices/RK3326/packages/u-boot/sources/configs/rk3326-handheld_defconfig b/projects/ROCKNIX/devices/RK3326/packages/u-boot/sources/configs/rk3326-handheld_defconfig
index 72f1fb7ca9d..fa4fb139821 100644
--- a/projects/ROCKNIX/devices/RK3326/packages/u-boot/sources/configs/rk3326-handheld_defconfig
+++ b/projects/ROCKNIX/devices/RK3326/packages/u-boot/sources/configs/rk3326-handheld_defconfig
@@ -80,9 +80,9 @@ CONFIG_SPL_CLK=y
CONFIG_FASTBOOT_BUF_ADDR=0x800800
CONFIG_FASTBOOT_BUF_SIZE=0x04000000
CONFIG_ROCKCHIP_GPIO=y
+CONFIG_ROCKCHIP_BOOT_MODE_REG=0x0
CONFIG_SYS_I2C_ROCKCHIP=y
CONFIG_MISC=y
-CONFIG_ROCKCHIP_OTP=y
CONFIG_MMC_DW=y
CONFIG_MMC_DW_ROCKCHIP=y
CONFIG_PHY_REALTEK=y
diff --git a/projects/ROCKNIX/devices/RK3326/patches/linux/023-px30s-tsadc-support.patch b/projects/ROCKNIX/devices/RK3326/patches/linux/023-px30s-tsadc-support.patch
new file mode 100644
index 00000000000..137b803378f
--- /dev/null
+++ b/projects/ROCKNIX/devices/RK3326/patches/linux/023-px30s-tsadc-support.patch
@@ -0,0 +1,239 @@
+From: Finley Xiao
+Date: Mon, 30 Aug 2021 09:55:37 +0000
+Subject: [PATCH] thermal: rockchip: Support the px30s SoC in thermal driver
+
+There are two Temperature Sensor on px30s, channel 0 is for CPU,
+channel 1 is for GPU.
+
+set trim for px30s.
+
+Change-Id: I25e16c8d398634d83a3611fa829ee2e9dd974538
+Signed-off-by: Finley Xiao
+
+Ported from rockchip-linux/kernel commit
+bc7c0c8e19f4caa34ac139d571153c90dd7437b8 to mainline 6.12, adding a
+soc_is_px30s() runtime-detection override (see
+026-px30s-cpuinfo-soc-detection.patch) alongside this driver's
+existing px30s-tsadc devicetree-compatible match, as a defensive
+belt-and-suspenders measure.
+
+---
+--- a/drivers/thermal/rockchip_thermal.c
++++ b/drivers/thermal/rockchip_thermal.c
+@@ -15,6 +15,7 @@
+ #include
+ #include
+ #include
++#include
+ #include
+ #include
+ #include
+@@ -57,12 +58,17 @@
+ * @length: size of conversion table
+ * @data_mask: mask to apply on data inputs
+ * @mode: sort mode of this adc variant (incrementing or decrementing)
++ * @kNum: linear conversion slope, used instead of the table/id lookup when
++ * non-zero (e.g. PX30S, which has no discrete calibration table)
++ * @bNum: linear conversion intercept, paired with kNum
+ */
+ struct chip_tsadc_table {
+ const struct tsadc_table *id;
+ unsigned int length;
+ u32 data_mask;
+ enum adc_sort_mode mode;
++ int kNum;
++ int bNum;
+ };
+
+ /**
+@@ -91,11 +97,20 @@
+ enum tshut_mode tshut_mode;
+ enum tshut_polarity tshut_polarity;
+
++ /* Time (us) to wait for a conversion to complete after enabling the
++ * ADC, before the first reading is valid (e.g. PX30S needs ~2100us;
++ * chips that don't set this need no extra delay) */
++ int conversion_time;
++
+ /* Chip-wide methods */
+ void (*initialize)(struct regmap *grf,
+ void __iomem *reg, enum tshut_polarity p);
+ void (*irq_ack)(void __iomem *reg);
+ void (*control)(void __iomem *reg, bool on);
++ /* Override the tsadc clock rate away from whatever the devicetree's
++ * assigned-clock-rates says, for chips that require a specific rate
++ * (e.g. PX30S needs 4MHz) */
++ int (*set_clk_rate)(struct platform_device *pdev);
+
+ /* Per-sensor methods */
+ int (*get_temp)(const struct chip_tsadc_table *table,
+@@ -228,7 +243,13 @@
+ #define GRF_TSADC_TESTBIT_L 0x0e648
+ #define GRF_TSADC_TESTBIT_H 0x0e64c
+
++#define PX30_GRF_SOC_CON0 0x0400
+ #define PX30_GRF_SOC_CON2 0x0408
++#define PX30S_TSADC_TDC_MODE (0x10001 << 4)
++#define PX30S_TSADC_TRIM (0xf0007 << 0)
++
++#define MIN_TEMP (-40000)
++#define MAX_TEMP (125000)
+
+ #define RK3568_GRF_TSADC_CON 0x0600
+ #define RK3568_GRF_TSADC_ANA_REG0 (0x10001 << 0)
+@@ -554,6 +575,9 @@
+ unsigned int denom;
+ u32 error = table->data_mask;
+
++ if (table->kNum)
++ return (((temp / 1000) * table->kNum) / 1000 + table->bNum);
++
+ low = 0;
+ high = (table->length - 1) - 1; /* ignore the last check for table */
+ mid = (high + low) / 2;
+@@ -607,6 +631,13 @@
+ unsigned int num;
+ unsigned long denom;
+
++ if (table->kNum) {
++ *temp = (((int)code - table->bNum) * 10000 / table->kNum) * 100;
++ if (*temp < MIN_TEMP || *temp > MAX_TEMP)
++ return -EAGAIN;
++ return 0;
++ }
++
+ WARN_ON(table->length < 2);
+
+ switch (table->mode) {
+@@ -773,6 +804,14 @@
+ regmap_write(grf, PX30_GRF_SOC_CON2, GRF_CON_TSADC_CH_INV);
+ }
+
++static void rk_tsadcv9_initialize(struct regmap *grf, void __iomem *regs,
++ enum tshut_polarity tshut_polarity)
++{
++ rk_tsadcv2_initialize(grf, regs, tshut_polarity);
++ regmap_write(grf, PX30_GRF_SOC_CON0, PX30S_TSADC_TDC_MODE);
++ regmap_write(grf, PX30_GRF_SOC_CON0, PX30S_TSADC_TRIM);
++}
++
+ static void rk_tsadcv7_initialize(struct regmap *grf, void __iomem *regs,
+ enum tshut_polarity tshut_polarity)
+ {
+@@ -1085,6 +1124,65 @@
+ },
+ };
+
++static int rk_tsadcv1_set_clk_rate(struct platform_device *pdev)
++{
++ struct clk *clk;
++ int error;
++
++ clk = devm_clk_get(&pdev->dev, "tsadc");
++ if (IS_ERR(clk)) {
++ error = PTR_ERR(clk);
++ dev_err(&pdev->dev, "failed to get tsadc clock\n");
++ return error;
++ }
++ error = clk_set_rate(clk, 4000000);
++ if (error < 0) {
++ devm_clk_put(&pdev->dev, clk);
++ dev_err(&pdev->dev,
++ "failed to set tsadc clk rate to 4000000Hz\n");
++ return error;
++ }
++ devm_clk_put(&pdev->dev, clk);
++
++ return 0;
++}
++
++/*
++ * PX30S is a distinct chip variant from plain PX30 (detected via a
++ * DDR_GRF_CON1 register bit, see rockchip,px30s-tsadc in the devicetree
++ * binding), with a different ADC init sequence (rk_tsadcv9_initialize vs
++ * rk_tsadcv4_initialize), a required 4MHz tsadc clock rate rather than
++ * whatever the devicetree assigns, a mandatory ~2.1ms settle time after
++ * enabling the ADC before the first reading is valid, and a linear
++ * code<->temperature conversion (kNum/bNum) rather than a discrete lookup
++ * table. Without all of these, raw ADC reads come back stuck at 0.
++ */
++static const struct rockchip_tsadc_chip px30s_tsadc_data = {
++ /* cpu, gpu */
++ .chn_offset = 0,
++ .chn_num = 2, /* 2 channels for tsadc */
++ .conversion_time = 2100, /* us */
++
++ .tshut_mode = TSHUT_MODE_CRU, /* default TSHUT via CRU */
++ .tshut_temp = 95000,
++
++ .initialize = rk_tsadcv9_initialize,
++ .irq_ack = rk_tsadcv3_irq_ack,
++ .control = rk_tsadcv2_control,
++ .get_temp = rk_tsadcv2_get_temp,
++ .set_alarm_temp = rk_tsadcv2_alarm_temp,
++ .set_tshut_temp = rk_tsadcv2_tshut_temp,
++ .set_tshut_mode = rk_tsadcv2_tshut_mode,
++ .set_clk_rate = rk_tsadcv1_set_clk_rate,
++
++ .table = {
++ .kNum = 2699,
++ .bNum = 2796,
++ .data_mask = TSADCV2_DATA_MASK,
++ .mode = ADC_INCREMENT,
++ },
++};
++
+ static const struct rockchip_tsadc_chip rv1108_tsadc_data = {
+ /* cpu */
+ .chn_offset = 0,
+@@ -1310,6 +1408,9 @@
+ { .compatible = "rockchip,px30-tsadc",
+ .data = (void *)&px30_tsadc_data,
+ },
++ { .compatible = "rockchip,px30s-tsadc",
++ .data = (void *)&px30s_tsadc_data,
++ },
+ {
+ .compatible = "rockchip,rv1108-tsadc",
+ .data = (void *)&rv1108_tsadc_data,
+@@ -1534,6 +1635,10 @@
+ thermal->chip = device_get_match_data(&pdev->dev);
+ if (!thermal->chip)
+ return -EINVAL;
++ /* Override to the PX30S table on real PX30S silicon regardless of
++ * compatible string, matching vendor commit bc7c0c8e19f4. */
++ if (soc_is_px30s())
++ thermal->chip = &px30s_tsadc_data;
+
+ thermal->sensors = devm_kcalloc(&pdev->dev, thermal->chip->chn_num,
+ sizeof(*thermal->sensors), GFP_KERNEL);
+@@ -1554,6 +1659,9 @@
+ return dev_err_probe(&pdev->dev, PTR_ERR(thermal->clk),
+ "failed to get tsadc clock.\n");
+
++ if (thermal->chip->set_clk_rate)
++ thermal->chip->set_clk_rate(pdev);
++
+ thermal->pclk = devm_clk_get_enabled(&pdev->dev, "apb_pclk");
+ if (IS_ERR(thermal->pclk))
+ return dev_err_probe(&pdev->dev, PTR_ERR(thermal->pclk),
+@@ -1587,6 +1695,9 @@
+ "failed to request tsadc irq.\n");
+
+ thermal->chip->control(thermal->regs, true);
++ if (thermal->chip->conversion_time)
++ usleep_range(thermal->chip->conversion_time,
++ thermal->chip->conversion_time + 50);
+
+ for (i = 0; i < thermal->chip->chn_num; i++) {
+ rockchip_thermal_toggle_sensor(&thermal->sensors[i], true);
+@@ -1671,6 +1782,9 @@
+ }
+
+ thermal->chip->control(thermal->regs, true);
++ if (thermal->chip->conversion_time)
++ usleep_range(thermal->chip->conversion_time,
++ thermal->chip->conversion_time + 50);
+
+ for (i = 0; i < thermal->chip->chn_num; i++)
+ rockchip_thermal_toggle_sensor(&thermal->sensors[i], true);
diff --git a/projects/ROCKNIX/devices/RK3326/patches/linux/024-px30s-gpu-power-domain-workaround.patch b/projects/ROCKNIX/devices/RK3326/patches/linux/024-px30s-gpu-power-domain-workaround.patch
new file mode 100644
index 00000000000..308d383b618
--- /dev/null
+++ b/projects/ROCKNIX/devices/RK3326/patches/linux/024-px30s-gpu-power-domain-workaround.patch
@@ -0,0 +1,106 @@
+Ported from two rockchip-linux/kernel commits, combined here since the
+second is a direct fix to the first:
+
+commit 41f9bdf3cf406a768dce672ab2c0c9a6d2d6c1a1
+Author: Liang Chen
+Date: Tue, 15 Mar 2022 02:07:59 +0000
+Subject: soc: rockchip: power-domain: keep pd_gpu always on for px30s
+
+ Change-Id: Ieb81bdd80aaabb24d490c13be147cb3e1a1417dd
+ Signed-off-by: Liang Chen
+
+commit 480163a87a56c277f3fb9153a2333499a7b918cf
+Author: Zhen Chen
+Date: Tue, 20 Sep 2022 08:21:43 +0000
+Subject: soc: rockchip: power-domain: correct name of pd_gpu for px30s
+
+ Fixes: 41f9bdf3cf40 ("soc: rockchip: power-domain: keep pd_gpu always on for px30s")
+ Change-Id: I9af58d9471204c4540cf491b206c6c6f5d318a7f
+ Signed-off-by: Zhen Chen
+
+Ported to mainline 6.12 using the shared soc_is_px30s() runtime-detection
+helper (see 026-px30s-cpuinfo-soc-detection.patch) instead of an inline
+per-file check.
+
+That workaround alone turned out to be insufficient - real hardware
+testing (a live dmesg capture through an actual suspend/resume cycle)
+caught the exact "Enabling unprepared clk_gpu" warning this comment
+already describes, firing from inside genpd_resume_noirq() ->
+pm_clk_resume() during the real resume path, followed immediately by
+"mali ff400000.gpu: __pm_clk_enable: failed to enable clk ..., error
+-108" (-ESHUTDOWN). The mechanism: rockchip_pd_attach_dev() (this same
+file) unconditionally scans each attaching device's own devicetree
+clocks and feeds them into genpd's generic GENPD_FLAG_PM_CLK tracking
+via pm_clk_add_clk() - for the GPU device this is the same "bus" clock
+(SCLK_GPU) that mali_kbase's own driver *also* directly manages via its
+own clk_prepare_enable()/clk_disable_unprepare() calls
+(power_control_init() in mali_kbase_core_linux.c). Two independent,
+uncoordinated consumers tracking prepare/enable state for the same
+underlying clk_core drift out of sync across suspend/resume, regardless
+of whether the domain's hardware power state ever actually toggles -
+so the power-off workaround below doesn't prevent it. Second part of
+the fix: skip GENPD_FLAG_PM_CLK for this domain entirely, so mali_kbase
+remains the sole manager of its own clock.
+
+---
+--- a/drivers/pmdomain/rockchip/pm-domains.c
++++ b/drivers/pmdomain/rockchip/pm-domains.c
+@@ -35,6 +35,7 @@
+ #include
+ #include
+ #include
++#include
+
+ struct rockchip_domain_info {
+ const char *name;
+@@ -574,6 +575,19 @@
+ struct rockchip_pmu *pmu = pd->pmu;
+ int ret;
+
++ /*
++ * Ported from vendor commit 41f9bdf3cf40 ("keep pd_gpu always on for
++ * px30s"), corrected per follow-up fix 480163a87a56: the vendor's
++ * check compared against genpd name "pd_gpu", but this driver's
++ * actual name is just "gpu", so it never matched. Powering the GPU
++ * domain off via genpd on PX30S is erratic and can corrupt the GPU
++ * clock's enable refcount ("Enabling unprepared clk_gpu" / -ESHUTDOWN
++ * on a later resume), so just never power it off.
++ */
++ if (!power_on && soc_is_px30s() &&
++ pd->genpd.name && !strcmp(pd->genpd.name, "gpu"))
++ return 0;
++
+ mutex_lock(&pmu->mutex);
+
+ if (rockchip_pmu_domain_is_on(pd) != power_on) {
+@@ -790,7 +804,29 @@
+ pd->genpd.power_on = rockchip_pd_power_on;
+ pd->genpd.attach_dev = rockchip_pd_attach_dev;
+ pd->genpd.detach_dev = rockchip_pd_detach_dev;
+- pd->genpd.flags = GENPD_FLAG_PM_CLK;
++ /*
++ * rockchip_pd_attach_dev() (below) unconditionally scans each
++ * attaching device's own devicetree clocks and feeds them into
++ * genpd's generic GENPD_FLAG_PM_CLK tracking (pm_clk_add_clk()) -
++ * separate from and uncoordinated with whatever clock management
++ * that device's own driver does directly. On PX30S, mali_kbase
++ * already does its own complete clk_prepare_enable()/
++ * disable_unprepare() on the GPU's "bus" clock - having genpd
++ * *also* independently track prepare/enable state for the same
++ * underlying clk_core is exactly what produces "Enabling
++ * unprepared clk_gpu" / -ESHUTDOWN (confirmed via a live dmesg
++ * capture showing the failure inside genpd_resume_noirq() ->
++ * pm_clk_resume(), the same failure this file's power-off
++ * workaround above was written to prevent, but doesn't - the
++ * refcount desync happens independently of whether the domain's
++ * hardware power state ever actually toggles). Skip
++ * GENPD_FLAG_PM_CLK for this domain so mali_kbase remains the
++ * sole manager of its own clock.
++ */
++ if (soc_is_px30s() && pd->genpd.name && !strcmp(pd->genpd.name, "gpu"))
++ pd->genpd.flags = 0;
++ else
++ pd->genpd.flags = GENPD_FLAG_PM_CLK;
+ if (pd_info->active_wakeup)
+ pd->genpd.flags |= GENPD_FLAG_ACTIVE_WAKEUP;
+ pm_genpd_init(&pd->genpd, NULL,
diff --git a/projects/ROCKNIX/devices/RK3326/patches/linux/025-px30s-dsi-dphy-timing-table.patch b/projects/ROCKNIX/devices/RK3326/patches/linux/025-px30s-dsi-dphy-timing-table.patch
new file mode 100644
index 00000000000..75d4585736f
--- /dev/null
+++ b/projects/ROCKNIX/devices/RK3326/patches/linux/025-px30s-dsi-dphy-timing-table.patch
@@ -0,0 +1,99 @@
+From: Guochun Huang
+Date: Wed, 8 Feb 2023 03:00:53 +0000
+Subject: [PATCH] phy/rockchip: inno-dsidphy: add support px30s
+
+Change-Id: I275d589f56e5963649aee9397eba3a9994e5901d
+Signed-off-by: Guochun Huang
+
+Ported from rockchip-linux/kernel commit
+b6de31a0225481b3eb3a8d0b232e5d46c8de83f6 to mainline 6.12. PX30S has a
+different, higher-capability DPHY IP revision than base PX30
+(2.5GHz-capable vs 1GHz-capable) with genuinely different analog
+bias/timing calibration values; this adds the "rockchip,px30s-dsi-dphy"
+compatible (mapped to the vendor's 2.5GHz timing table) plus a
+soc_is_px30s() runtime-detection override at probe time (see
+026-px30s-cpuinfo-soc-detection.patch).
+
+---
+--- a/drivers/phy/rockchip/phy-rockchip-inno-dsidphy.c
++++ b/drivers/phy/rockchip/phy-rockchip-inno-dsidphy.c
+@@ -18,6 +18,7 @@
+ #include
+ #include
+ #include
++#include
+ #include
+
+ #include
+@@ -200,6 +201,14 @@
+ const struct inno_mipi_dphy_timing *inno_mipi_dphy_timing_table;
+ const unsigned int num_timings;
+ enum phy_max_rate max_rate;
++ /*
++ * PX30S needs pin_txclkesc/pin_txbyteclk invert explicitly disabled,
++ * gated on soc_type == PX30S rather than max_rate (shared with
++ * RK3568/RV1126). Confirmed via register dump: stock firmware leaves
++ * INVERT_TXCLKESC at 0; without this write it stays at whatever
++ * U-Boot left it (1, inverted) on this board.
++ */
++ bool invert_txclkesc_disable;
+ };
+
+ struct inno_dsidphy {
+@@ -380,6 +389,12 @@
+ /* Select MIPI mode */
+ phy_update_bits(inno, REGISTER_PART_LVDS, 0x03,
+ MODE_ENABLE_MASK, MIPI_MODE_ENABLE);
++
++ /* set pin_txclkesc_0 pin_txbyteclk invert disable */
++ if (inno->pdata->invert_txclkesc_disable)
++ phy_update_bits(inno, REGISTER_PART_DIGITAL, 0x01,
++ INVERT_TXCLKESC_MASK, INVERT_TXCLKESC_DISABLE);
++
+ /* Configure PLL */
+ phy_update_bits(inno, REGISTER_PART_ANALOG, 0x03,
+ REG_PREDIV_MASK, REG_PREDIV(inno->pll.prediv));
+@@ -688,6 +703,13 @@
+ .max_rate = MAX_2_5GHZ,
+ };
+
++static const struct inno_video_phy_plat_data px30s_video_phy_plat_data = {
++ .inno_mipi_dphy_timing_table = inno_mipi_dphy_timing_table_max_2_5ghz,
++ .num_timings = ARRAY_SIZE(inno_mipi_dphy_timing_table_max_2_5ghz),
++ .max_rate = MAX_2_5GHZ,
++ .invert_txclkesc_disable = true,
++};
++
+ static int inno_dsidphy_probe(struct platform_device *pdev)
+ {
+ struct device *dev = &pdev->dev;
+@@ -702,6 +724,10 @@
+
+ inno->dev = dev;
+ inno->pdata = of_device_get_match_data(inno->dev);
++ /* Override to the PX30S plat_data on real PX30S silicon regardless of
++ * compatible string, matching vendor commit b6de31a02254. */
++ if (soc_is_px30s())
++ inno->pdata = &px30s_video_phy_plat_data;
+ platform_set_drvdata(pdev, inno);
+
+ inno->phy_base = devm_platform_ioremap_resource(pdev, 0);
+@@ -762,6 +788,18 @@
+ .compatible = "rockchip,px30-dsi-dphy",
+ .data = &max_1ghz_video_phy_plat_data,
+ }, {
++ /*
++ * PX30S (== RK3326S) has a different DPHY IP revision than
++ * base PX30 - 2.5GHz-capable rather than 1GHz, with genuinely
++ * different analog bias/timing calibration (vendor's
++ * soc_is_px30s() swaps in its own timing table for this
++ * reason). The plain "rockchip,px30-dsi-dphy" (max_1ghz) table
++ * skips the PLL post-divider enable, the clock-lane VOD range
++ * register, and high-order bits of several timing counters.
++ */
++ .compatible = "rockchip,px30s-dsi-dphy",
++ .data = &px30s_video_phy_plat_data,
++ }, {
+ .compatible = "rockchip,rk3128-dsi-dphy",
+ .data = &max_1ghz_video_phy_plat_data,
+ }, {
diff --git a/projects/ROCKNIX/devices/RK3326/patches/linux/026-px30s-cpuinfo-soc-detection.patch b/projects/ROCKNIX/devices/RK3326/patches/linux/026-px30s-cpuinfo-soc-detection.patch
new file mode 100644
index 00000000000..e81f88a2893
--- /dev/null
+++ b/projects/ROCKNIX/devices/RK3326/patches/linux/026-px30s-cpuinfo-soc-detection.patch
@@ -0,0 +1,530 @@
+From: Liang Chen
+Date: Sun, 15 Aug 2021 07:59:38 +0000
+Subject: [PATCH] soc: rockchip: add cpuid for px30/px30s
+
+The value of bit[15:14] in DDR_GRF_BASE1 define px30/px30s.
+px30: bit[15:14]=0x00
+px30s: bit[15:14]=0x03
+
+Change-Id: I07e31e8fd56ee2eea7883a5f5de012740ec0e98a
+Signed-off-by: Liang Chen
+
+Ported from rockchip-linux/kernel commit
+84d26633c7a4105fb8d3d8bba604326b64b30c9c to mainline 6.12, which
+entirely lacks this cpuinfo/soc_id infrastructure. Ports
+include/linux/rockchip/cpu.h and drivers/soc/rockchip/rockchip-cpuinfo.c
+wholesale, simplified only by dropping per-SoC CONFIG_CPU_* Kconfig
+gates (mainline uses ARCH_ROCKCHIP broadly, not vendor's fine-grained
+per-chip symbols) and the ARM32-only CONFIG_NO_GKI legacy serial-number
+code path. This is the shared soc_is_px30s() helper the other PX30S
+patches in this series (023-025, 027-028) build on.
+
+---
+--- /dev/null
++++ b/include/linux/rockchip/cpu.h
+@@ -0,0 +1,191 @@
++/* SPDX-License-Identifier: GPL-2.0 */
++/*
++ * Copyright (C) 2017 Rockchip Electronics Co. Ltd.
++ *
++ * Ported from the Rockchip vendor BSP kernel
++ * (https://github.com/rockchip-linux/kernel) for PX30S/RK3326S support -
++ * see 84d26633c7a4 ("soc: rockchip: add cpuid for px30/px30s"). Unlike the
++ * vendor source, this doesn't gate each SoC family behind its own
++ * CONFIG_CPU_* symbol - mainline has no such per-chip Kconfig options, so
++ * the functions are always declared; of_machine_is_compatible() and
++ * rockchip_soc_id remain the only run-time gates, exactly as upstream.
++ */
++
++#ifndef __LINUX_ROCKCHIP_CPU_H
++#define __LINUX_ROCKCHIP_CPU_H
++
++#include
++
++#define ROCKCHIP_CPU_MASK 0xffff0000
++#define ROCKCHIP_CPU_SHIFT 16
++#define ROCKCHIP_CPU_PX30 0x33260000
++#define ROCKCHIP_CPU_RV1109 0x11090000
++#define ROCKCHIP_CPU_RV1126 0x11260000
++#define ROCKCHIP_CPU_RK312X 0x31260000
++#define ROCKCHIP_CPU_RK3288 0x32880000
++#define ROCKCHIP_CPU_RK3308 0x33080000
++#define ROCKCHIP_CPU_RK3566 0x35660000
++#define ROCKCHIP_CPU_RK3568 0x35680000
++
++#if IS_REACHABLE(CONFIG_ROCKCHIP_CPUINFO)
++
++extern unsigned long rockchip_soc_id;
++
++#define ROCKCHIP_CPU_VERION_MASK 0x0000f000
++#define ROCKCHIP_CPU_VERION_SHIFT 12
++
++static inline unsigned long rockchip_get_cpu_version(void)
++{
++ return (rockchip_soc_id & ROCKCHIP_CPU_VERION_MASK)
++ >> ROCKCHIP_CPU_VERION_SHIFT;
++}
++
++static inline void rockchip_set_cpu_version(unsigned long ver)
++{
++ rockchip_soc_id &= ~ROCKCHIP_CPU_VERION_MASK;
++ rockchip_soc_id |=
++ (ver << ROCKCHIP_CPU_VERION_SHIFT) & ROCKCHIP_CPU_VERION_MASK;
++}
++
++static inline void rockchip_set_cpu(unsigned long code)
++{
++ if (!code)
++ return;
++
++ rockchip_soc_id &= ~ROCKCHIP_CPU_MASK;
++ rockchip_soc_id |= (code << ROCKCHIP_CPU_SHIFT) & ROCKCHIP_CPU_MASK;
++}
++
++int rockchip_soc_id_init(void);
++
++#else
++
++#define rockchip_soc_id 0
++
++static inline unsigned long rockchip_get_cpu_version(void)
++{
++ return 0;
++}
++
++static inline void rockchip_set_cpu_version(unsigned long ver)
++{
++}
++
++static inline void rockchip_set_cpu(unsigned long code)
++{
++}
++
++static inline int rockchip_soc_id_init(void)
++{
++ return 0;
++}
++
++#endif
++
++static inline bool cpu_is_px30(void)
++{
++ if (rockchip_soc_id)
++ return (rockchip_soc_id & ROCKCHIP_CPU_MASK) == ROCKCHIP_CPU_PX30;
++ return of_machine_is_compatible("rockchip,px30") ||
++ of_machine_is_compatible("rockchip,px30s") ||
++ of_machine_is_compatible("rockchip,rk3326") ||
++ of_machine_is_compatible("rockchip,rk3326s");
++}
++
++static inline bool cpu_is_rv1109(void)
++{
++ if (rockchip_soc_id)
++ return (rockchip_soc_id & ROCKCHIP_CPU_MASK) == ROCKCHIP_CPU_RV1109;
++ return of_machine_is_compatible("rockchip,rv1109");
++}
++
++static inline bool cpu_is_rv1126(void)
++{
++ if (rockchip_soc_id)
++ return (rockchip_soc_id & ROCKCHIP_CPU_MASK) == ROCKCHIP_CPU_RV1126;
++ return of_machine_is_compatible("rockchip,rv1126");
++}
++
++static inline bool cpu_is_rk312x(void)
++{
++ if (rockchip_soc_id)
++ return (rockchip_soc_id & ROCKCHIP_CPU_MASK) == ROCKCHIP_CPU_RK312X;
++ return of_machine_is_compatible("rockchip,rk3126") ||
++ of_machine_is_compatible("rockchip,rk3126b") ||
++ of_machine_is_compatible("rockchip,rk3126c") ||
++ of_machine_is_compatible("rockchip,rk3128");
++}
++
++static inline bool cpu_is_rk3288(void)
++{
++ if (rockchip_soc_id)
++ return (rockchip_soc_id & ROCKCHIP_CPU_MASK) == ROCKCHIP_CPU_RK3288;
++ return of_machine_is_compatible("rockchip,rk3288") ||
++ of_machine_is_compatible("rockchip,rk3288w");
++}
++
++static inline bool cpu_is_rk3308(void)
++{
++ if (rockchip_soc_id)
++ return (rockchip_soc_id & ROCKCHIP_CPU_MASK) == ROCKCHIP_CPU_RK3308;
++
++ return of_machine_is_compatible("rockchip,rk3308") ||
++ of_machine_is_compatible("rockchip,rk3308bs");
++}
++
++static inline bool cpu_is_rk3566(void)
++{
++ if (rockchip_soc_id)
++ return (rockchip_soc_id & ROCKCHIP_CPU_MASK) == ROCKCHIP_CPU_RK3566;
++ return of_machine_is_compatible("rockchip,rk3566");
++}
++
++static inline bool cpu_is_rk3568(void)
++{
++ if (rockchip_soc_id)
++ return (rockchip_soc_id & ROCKCHIP_CPU_MASK) == ROCKCHIP_CPU_RK3568;
++ return of_machine_is_compatible("rockchip,rk3568");
++}
++
++#define ROCKCHIP_SOC_MASK (ROCKCHIP_CPU_MASK | 0xff)
++#define ROCKCHIP_SOC_PX30 (ROCKCHIP_CPU_PX30 | 0x00)
++#define ROCKCHIP_SOC_PX30S (ROCKCHIP_CPU_PX30 | 0x01)
++#define ROCKCHIP_SOC_RV1109 (ROCKCHIP_CPU_RV1109 | 0x00)
++#define ROCKCHIP_SOC_RV1126 (ROCKCHIP_CPU_RV1126 | 0x00)
++#define ROCKCHIP_SOC_RK3126 (ROCKCHIP_CPU_RK312X | 0x00)
++#define ROCKCHIP_SOC_RK3126B (ROCKCHIP_CPU_RK312X | 0x10)
++#define ROCKCHIP_SOC_RK3126C (ROCKCHIP_CPU_RK312X | 0x20)
++#define ROCKCHIP_SOC_RK3128 (ROCKCHIP_CPU_RK312X | 0x01)
++#define ROCKCHIP_SOC_RK3288 (ROCKCHIP_CPU_RK3288 | 0x00)
++#define ROCKCHIP_SOC_RK3288W (ROCKCHIP_CPU_RK3288 | 0x01)
++#define ROCKCHIP_SOC_RK3308 (ROCKCHIP_CPU_RK3308 | 0x00)
++#define ROCKCHIP_SOC_RK3308B (ROCKCHIP_CPU_RK3308 | 0x01)
++#define ROCKCHIP_SOC_RK3308BS (ROCKCHIP_CPU_RK3308 | 0x02)
++#define ROCKCHIP_SOC_RK3566 (ROCKCHIP_CPU_RK3566 | 0x00)
++#define ROCKCHIP_SOC_RK3568 (ROCKCHIP_CPU_RK3568 | 0x00)
++
++#define ROCKCHIP_SOC(id, ID) \
++static inline bool soc_is_##id(void) \
++{ \
++ if (rockchip_soc_id) \
++ return ((rockchip_soc_id & ROCKCHIP_SOC_MASK) == ROCKCHIP_SOC_ ##ID); \
++ return of_machine_is_compatible("rockchip,"#id); \
++}
++
++ROCKCHIP_SOC(px30, PX30)
++ROCKCHIP_SOC(px30s, PX30S)
++ROCKCHIP_SOC(rv1109, RV1109)
++ROCKCHIP_SOC(rv1126, RV1126)
++ROCKCHIP_SOC(rk3126, RK3126)
++ROCKCHIP_SOC(rk3126b, RK3126B)
++ROCKCHIP_SOC(rk3126c, RK3126C)
++ROCKCHIP_SOC(rk3128, RK3128)
++ROCKCHIP_SOC(rk3288, RK3288)
++ROCKCHIP_SOC(rk3288w, RK3288W)
++ROCKCHIP_SOC(rk3308, RK3308)
++ROCKCHIP_SOC(rk3308b, RK3308B)
++ROCKCHIP_SOC(rk3308bs, RK3308BS)
++ROCKCHIP_SOC(rk3566, RK3566)
++ROCKCHIP_SOC(rk3568, RK3568)
++
++#endif
+--- /dev/null
++++ b/drivers/soc/rockchip/rockchip-cpuinfo.c
+@@ -0,0 +1,262 @@
++// SPDX-License-Identifier: GPL-2.0
++/*
++ * Copyright (C) 2017 Rockchip Electronics Co. Ltd.
++ *
++ * Ported from the Rockchip vendor BSP kernel
++ * (https://github.com/rockchip-linux/kernel, drivers/soc/rockchip/
++ * rockchip-cpuinfo.c as of 84d26633c7a4) for PX30S/RK3326S support. The
++ * only change from upstream is dropping the CONFIG_NO_GKI legacy ATAG
++ * serial-number block, which depends on asm/system_info.h - an arm32-only
++ * header that doesn't exist on arm64, and CONFIG_NO_GKI itself is an
++ * Android-GKI-specific config not applicable here either way.
++ */
++
++#include
++#include
++#include
++#include
++#include
++#include
++#include
++#include
++#include
++
++unsigned long rockchip_soc_id;
++EXPORT_SYMBOL(rockchip_soc_id);
++
++static int rockchip_cpuinfo_probe(struct platform_device *pdev)
++{
++ struct device *dev = &pdev->dev;
++ struct nvmem_cell *cell;
++ unsigned char *efuse_buf;
++ size_t len = 0;
++ u32 serial_low, serial_high;
++ unsigned char buf[16];
++ int i;
++
++ cell = nvmem_cell_get(dev, "cpu-code");
++ if (!IS_ERR(cell)) {
++ efuse_buf = nvmem_cell_read(cell, &len);
++ nvmem_cell_put(cell);
++ if (IS_ERR(efuse_buf))
++ return PTR_ERR(efuse_buf);
++
++ if (len == 2)
++ rockchip_set_cpu((efuse_buf[0] << 8 | efuse_buf[1]));
++ kfree(efuse_buf);
++ }
++
++ cell = nvmem_cell_get(dev, "cpu-version");
++ if (!IS_ERR(cell)) {
++ efuse_buf = nvmem_cell_read(cell, &len);
++ nvmem_cell_put(cell);
++ if (IS_ERR(efuse_buf))
++ return PTR_ERR(efuse_buf);
++
++ if ((len == 1) && (efuse_buf[0] > rockchip_get_cpu_version()))
++ rockchip_set_cpu_version(efuse_buf[0]);
++ kfree(efuse_buf);
++ }
++
++ cell = nvmem_cell_get(dev, "id");
++ if (IS_ERR(cell)) {
++ dev_err(dev, "failed to get id cell: %ld\n", PTR_ERR(cell));
++ return PTR_ERR(cell);
++ }
++ efuse_buf = nvmem_cell_read(cell, &len);
++ nvmem_cell_put(cell);
++ if (IS_ERR(efuse_buf))
++ return PTR_ERR(efuse_buf);
++
++ if (len != 16) {
++ kfree(efuse_buf);
++ dev_err(dev, "invalid id len: %zu\n", len);
++ return -EINVAL;
++ }
++
++ for (i = 0; i < 8; i++) {
++ buf[i] = efuse_buf[1 + (i << 1)];
++ buf[i + 8] = efuse_buf[i << 1];
++ }
++
++ kfree(efuse_buf);
++
++ dev_info(dev, "SoC\t\t: %lx\n", rockchip_soc_id);
++
++ serial_low = crc32(0, buf, 8);
++ serial_high = crc32(serial_low, buf + 8, 8);
++ dev_info(dev, "Serial\t\t: %08x%08x\n", serial_high, serial_low);
++
++ return 0;
++}
++
++static const struct of_device_id rockchip_cpuinfo_of_match[] = {
++ { .compatible = "rockchip,cpuinfo", },
++ { },
++};
++MODULE_DEVICE_TABLE(of, rockchip_cpuinfo_of_match);
++
++static struct platform_driver rockchip_cpuinfo_driver = {
++ .probe = rockchip_cpuinfo_probe,
++ .driver = {
++ .name = "rockchip-cpuinfo",
++ .of_match_table = rockchip_cpuinfo_of_match,
++ },
++};
++
++static void px30_init(void)
++{
++ void __iomem *base;
++
++ rockchip_soc_id = ROCKCHIP_SOC_PX30;
++#define PX30_DDR_GRF_BASE 0xFF630000
++#define PX30_DDR_GRF_CON1 0x04
++ base = ioremap(PX30_DDR_GRF_BASE, SZ_4K);
++ if (base) {
++ unsigned int val = readl_relaxed(base + PX30_DDR_GRF_CON1);
++
++ if (((val >> 14) & 0x03) == 0x03)
++ rockchip_soc_id = ROCKCHIP_SOC_PX30S;
++ iounmap(base);
++ }
++}
++
++static void rv1109_init(void)
++{
++ rockchip_soc_id = ROCKCHIP_SOC_RV1109;
++}
++
++static void rv1126_init(void)
++{
++ rockchip_soc_id = ROCKCHIP_SOC_RV1126;
++}
++
++static void rk3288_init(void)
++{
++ void __iomem *base;
++
++ rockchip_soc_id = ROCKCHIP_SOC_RK3288;
++#define RK3288_HDMI_PHYS 0xFF980000
++ base = ioremap(RK3288_HDMI_PHYS, SZ_4K);
++ if (base) {
++ /* RK3288W HDMI Revision ID is 0x1A */
++ if (readl_relaxed(base + 4) == 0x1A)
++ rockchip_soc_id = ROCKCHIP_SOC_RK3288W;
++ iounmap(base);
++ }
++}
++
++static void rk3126_init(void)
++{
++ void __iomem *base;
++
++ rockchip_soc_id = ROCKCHIP_SOC_RK3126;
++#define RK312X_GRF_PHYS 0x20008000
++#define RK312X_GRF_SOC_CON1 0x00000144
++#define RK312X_GRF_CHIP_TAG 0x00000300
++ base = ioremap(RK312X_GRF_PHYS, SZ_4K);
++ if (base) {
++ if (readl_relaxed(base + RK312X_GRF_CHIP_TAG) == 0x3136) {
++ if (readl_relaxed(base + RK312X_GRF_SOC_CON1) & 0x1)
++ rockchip_soc_id = ROCKCHIP_SOC_RK3126C;
++ else
++ rockchip_soc_id = ROCKCHIP_SOC_RK3126B;
++ }
++ iounmap(base);
++ }
++}
++
++static void rk3308_init(void)
++{
++ void __iomem *base;
++
++ rockchip_soc_id = ROCKCHIP_SOC_RK3308;
++#define RK3308_GRF_PHYS 0xFF000000
++#define RK3308_GRF_CHIP_ID 0x800
++ base = ioremap(RK3308_GRF_PHYS, SZ_4K);
++ if (base) {
++ u32 v = readl_relaxed(base + RK3308_GRF_CHIP_ID);
++
++ if (v == 0x3308)
++ rockchip_soc_id = ROCKCHIP_SOC_RK3308B;
++ if (v == 0x3308c)
++ rockchip_soc_id = ROCKCHIP_SOC_RK3308BS;
++ iounmap(base);
++ }
++}
++
++#define RK356X_PMU_GRF_PHYS 0xfdc20000
++#define RK356X_PMU_GRF_SOC_CON0 0x00000100
++#define RK356X_CHIP_VERSION_MASK 0x00008000
++static void rk356x_set_cpu_version(void)
++{
++ void __iomem *base;
++
++ base = ioremap(RK356X_PMU_GRF_PHYS, SZ_4K);
++ if (base) {
++ if (readl_relaxed(base + RK356X_PMU_GRF_SOC_CON0) & RK356X_CHIP_VERSION_MASK)
++ rockchip_set_cpu_version(1);
++ iounmap(base);
++ }
++}
++
++static void rk3566_init(void)
++{
++ rockchip_soc_id = ROCKCHIP_SOC_RK3566;
++ rk356x_set_cpu_version();
++}
++
++static void rk3568_init(void)
++{
++ rockchip_soc_id = ROCKCHIP_SOC_RK3568;
++ rk356x_set_cpu_version();
++}
++
++int __init rockchip_soc_id_init(void)
++{
++ if (rockchip_soc_id)
++ return 0;
++
++ if (cpu_is_rk3288()) {
++ rk3288_init();
++ } else if (cpu_is_rk312x()) {
++ if (of_machine_is_compatible("rockchip,rk3128"))
++ rockchip_soc_id = ROCKCHIP_SOC_RK3128;
++ else
++ rk3126_init();
++ } else if (cpu_is_rk3308()) {
++ rk3308_init();
++ } else if (cpu_is_rv1109()) {
++ rv1109_init();
++ } else if (cpu_is_rv1126()) {
++ rv1126_init();
++ } else if (cpu_is_rk3566()) {
++ rk3566_init();
++ } else if (cpu_is_rk3568()) {
++ rk3568_init();
++ } else if (cpu_is_px30()) {
++ px30_init();
++ }
++
++ return 0;
++}
++#ifndef MODULE
++pure_initcall(rockchip_soc_id_init);
++#endif
++
++static int __init rockchip_cpuinfo_init(void)
++{
++#ifdef MODULE
++ rockchip_soc_id_init();
++#endif
++ return platform_driver_register(&rockchip_cpuinfo_driver);
++}
++subsys_initcall_sync(rockchip_cpuinfo_init);
++
++static void __exit rockchip_cpuinfo_exit(void)
++{
++ platform_driver_unregister(&rockchip_cpuinfo_driver);
++}
++module_exit(rockchip_cpuinfo_exit);
++
++MODULE_LICENSE("GPL");
+--- a/drivers/soc/rockchip/Kconfig
++++ b/drivers/soc/rockchip/Kconfig
+@@ -14,6 +14,16 @@
+ In a lot of cases there also need to be default settings initialized
+ to make some of them conform to expectations of the kernel.
+
++config ROCKCHIP_CPUINFO
++ tristate "Rockchip cpuinfo support"
++ depends on OF
++ default y if ARCH_ROCKCHIP
++ help
++ Say y here to enable support for reading Rockchip cpu info, such as
++ distinguishing SoC variants (e.g. PX30 vs PX30S) that share the same
++ base compatible string but differ in silicon revision, detected via
++ either an OTP-provisioned ID or a direct hardware register read.
++
+ config ROCKCHIP_IODOMAIN
+ tristate "Rockchip IO domain support"
+ depends on OF
+--- a/drivers/soc/rockchip/Makefile
++++ b/drivers/soc/rockchip/Makefile
+@@ -3,5 +3,6 @@
+ # Rockchip Soc drivers
+ #
+ obj-$(CONFIG_ROCKCHIP_GRF) += grf.o
++obj-$(CONFIG_ROCKCHIP_CPUINFO) += rockchip-cpuinfo.o
+ obj-$(CONFIG_ROCKCHIP_IODOMAIN) += io-domain.o
+ obj-$(CONFIG_ROCKCHIP_DTPM) += dtpm.o
+--- a/arch/arm64/boot/dts/rockchip/px30.dtsi
++++ b/arch/arm64/boot/dts/rockchip/px30.dtsi
+@@ -843,6 +843,18 @@
+ };
+ };
+
++ /*
++ * Distinguishes PX30 from PX30S at runtime (same base compatible,
++ * different DPHY/pinctrl/thermal/OTP silicon revision) - see
++ * drivers/soc/rockchip/rockchip-cpuinfo.c, ported from the vendor
++ * kernel's 84d26633c7a4 ("soc: rockchip: add cpuid for px30/px30s").
++ */
++ cpuinfo {
++ compatible = "rockchip,cpuinfo";
++ nvmem-cells = <&cpu_id>;
++ nvmem-cell-names = "id";
++ };
++
+ cru: clock-controller@ff2b0000 {
+ compatible = "rockchip,px30-cru";
+ reg = <0x0 0xff2b0000 0x0 0x1000>;
diff --git a/projects/ROCKNIX/devices/RK3326/patches/linux/027-px30s-io-domain-pmuio1.patch b/projects/ROCKNIX/devices/RK3326/patches/linux/027-px30s-io-domain-pmuio1.patch
new file mode 100644
index 00000000000..4a8975b5d6f
--- /dev/null
+++ b/projects/ROCKNIX/devices/RK3326/patches/linux/027-px30s-io-domain-pmuio1.patch
@@ -0,0 +1,39 @@
+From: Jianqun Xu
+Date: Wed, 29 Dec 2021 04:24:01 +0000
+Subject: [PATCH] power/avs: rockchip-io-domain: px30s not support pmuio1 1v8
+ mode
+
+Change-Id: I6e9a4d189788d4c0f8f900adf14bbcd4af44fb8c
+Signed-off-by: Jianqun Xu
+
+Ported from rockchip-linux/kernel commit
+779017748410791afb5315d2748fff8280ef74f3 to mainline 6.12, using the
+shared soc_is_px30s() helper (see 026-px30s-cpuinfo-soc-detection.patch)
+instead of an inline check. Confirmed reachable on this board: its
+devicetree has pmuio1-supply set on &pmu_io_domains.
+
+---
+--- a/drivers/soc/rockchip/io-domain.c
++++ b/drivers/soc/rockchip/io-domain.c
+@@ -14,6 +14,7 @@
+ #include
+ #include
+ #include
++#include
+
+ #define MAX_SUPPLIES 16
+
+@@ -656,6 +657,13 @@
+ if (!supply_name)
+ continue;
+
++ /*
++ * Ported from the vendor kernel's 779017748410 ("power/avs:
++ * rockchip-io-domain: px30s not support pmuio1 1v8 mode").
++ */
++ if (soc_is_px30s() && !strcmp(supply_name, "pmuio1"))
++ continue;
++
+ reg = devm_regulator_get_optional(iod->dev, supply_name);
+ if (IS_ERR(reg)) {
+ ret = PTR_ERR(reg);
diff --git a/projects/ROCKNIX/devices/RK3326/patches/linux/028-px30s-usb2phy-tuning.patch b/projects/ROCKNIX/devices/RK3326/patches/linux/028-px30s-usb2phy-tuning.patch
new file mode 100644
index 00000000000..f29e33eac07
--- /dev/null
+++ b/projects/ROCKNIX/devices/RK3326/patches/linux/028-px30s-usb2phy-tuning.patch
@@ -0,0 +1,101 @@
+From: Jianwei Zheng
+Date: Tue, 7 Dec 2021 08:30:21 +0000
+Subject: [PATCH] phy: rockchip: inno-usb2: support rk3326s and px30s tuning
+
+Tuing pre-emphasis and turn off differential receiver in suspend mode
+for rk3326s and px30s SoCs.
+
+Fix some pc can not recognize the device when using 5m cable, so tuning
+usb phy squelch trigger point configure to 100mv for px30s.
+
+Signed-off-by: Jianwei Zheng
+Change-Id: Ida216e8951c1f1dad19fa3ff4c31ede6a53b3458
+
+Ported from rockchip-linux/kernel commit
+711167d7b0b2200e747309ac6edc2d52f3d12946 to mainline 6.12, scoped to
+only the soc_is_px30s() branch (see
+026-px30s-cpuinfo-soc-detection.patch) - deliberately not wiring in the
+vendor's parallel non-PX30S branch, to avoid changing behavior for
+other RK3328-family boards sharing the same rk3328_phy_cfgs[] table.
+
+---
+--- a/drivers/phy/rockchip/phy-rockchip-inno-usb2.c
++++ b/drivers/phy/rockchip/phy-rockchip-inno-usb2.c
+@@ -23,6 +23,7 @@
+ #include
+ #include
+ #include
++#include
+ #include
+ #include
+ #include
+@@ -1705,6 +1706,61 @@
+ { /* sentinel */ }
+ };
+
++/*
++ * Ported from vendor commit 711167d7b0b2 ("support rk3326s and px30s
++ * tuning") - PX30S needs different pre-emphasis/squelch tuning than base
++ * PX30/RK3328 to fix some hosts not recognizing the device over a 5m
++ * cable. Scoped to soc_is_px30s() only, since rk3328_phy_cfgs is shared
++ * with other boards this hasn't been tested on.
++ */
++static int rk3328_usb2phy_tuning(struct rockchip_usb2phy *rphy)
++{
++ int ret;
++
++ if (!soc_is_px30s())
++ return 0;
++
++ /* Enable otg port pre-emphasis during non-chirp phase */
++ ret = regmap_update_bits(rphy->grf, 0x8000, GENMASK(2, 0), BIT(2));
++ if (ret)
++ return ret;
++
++ /* Set otg port squelch trigger point configure to 100mv */
++ ret = regmap_update_bits(rphy->grf, 0x8004, GENMASK(7, 5), 0x40);
++ if (ret)
++ return ret;
++
++ ret = regmap_update_bits(rphy->grf, 0x8008, BIT(0), 0x1);
++ if (ret)
++ return ret;
++
++ /* Turn off otg port differential receiver in suspend mode */
++ ret = regmap_update_bits(rphy->grf, 0x8030, BIT(2), 0);
++ if (ret)
++ return ret;
++
++ /* Enable host port pre-emphasis during non-chirp phase */
++ ret = regmap_update_bits(rphy->grf, 0x8400, GENMASK(2, 0), BIT(2));
++ if (ret)
++ return ret;
++
++ /* Set host port squelch trigger point configure to 100mv */
++ ret = regmap_update_bits(rphy->grf, 0x8404, GENMASK(7, 5), 0x40);
++ if (ret)
++ return ret;
++
++ ret = regmap_update_bits(rphy->grf, 0x8408, BIT(0), 0x1);
++ if (ret)
++ return ret;
++
++ /* Turn off host port differential receiver in suspend mode */
++ ret = regmap_update_bits(rphy->grf, 0x8430, BIT(2), 0);
++ if (ret)
++ return ret;
++
++ return 0;
++}
++
+ static const struct rockchip_usb2phy_cfg rk3328_phy_cfgs[] = {
+ {
+ .reg = 0x100,
+@@ -1751,6 +1807,7 @@
+ .vdm_src_en = { 0x0108, 12, 12, 0, 1 },
+ .vdp_src_en = { 0x0108, 11, 11, 0, 1 },
+ },
++ .phy_tuning = rk3328_usb2phy_tuning,
+ },
+ { /* sentinel */ }
+ };
diff --git a/projects/ROCKNIX/devices/RK3326/patches/linux/030-pwm-vibra-kick-stuck-boot-state.patch b/projects/ROCKNIX/devices/RK3326/patches/linux/030-pwm-vibra-kick-stuck-boot-state.patch
new file mode 100644
index 00000000000..0eb16c89dba
--- /dev/null
+++ b/projects/ROCKNIX/devices/RK3326/patches/linux/030-pwm-vibra-kick-stuck-boot-state.patch
@@ -0,0 +1,106 @@
+Subject: [PATCH] input: pwm-vibra: kick PWM out of stuck boot-inherited state before disabling
+
+Not ported from upstream - written directly while diagnosing the GKD
+Pixel 2's rumble motor running constantly from boot (rk3326s-gkd-pixel2.dts,
+PX30S). This channel can power up already latched in a stuck-on state
+(enabled, 100% duty), and this driver's probe-time disable (state.enabled
+= false with whatever duty_cycle pwm_init_state() inherited from that
+boot state) never clears it - confirmed via direct register readback
+that the "disabled" write is genuinely applied in software, yet the
+physical output never changes.
+
+Extensive live-hardware testing (raw register writes, and a real
+FF_RUMBLE upload/play/stop cycle through the evdev ioctl path) found
+that once the channel is driven through a real, substantial duty
+change - not just toggling the enable bit - a subsequent disable takes
+hold correctly. The very first transition away from the exact
+boot-inherited register state is the only one that's broken; every
+transition after that behaves normally.
+
+Fix: at probe, ramp the duty cycle down from 90% (skipping the
+redundant 100% step, since that's already where the boot-inherited
+state sits) to 0% in ten steps over a fixed 250ms, then disable -
+forcing that first real transition automatically before userspace ever
+sees the device, instead of leaving it to whatever the boot state
+happened to be. A gradual ramp rather than a hard stop, and a fixed
+duration rather than one scaled to the configured period, were both
+chosen after real-hardware listening tests: long enough to hold
+regardless of period, gradual enough to feel like a deliberate fade
+rather than a jarring pulse.
+
+Scoped to soc_is_px30s() (the shared helper from
+026-px30s-cpuinfo-soc-detection.patch) since PX30S/GKD Pixel 2 is the
+only silicon confirmed to hit this symptom - every other board using
+this shared mainline driver keeps the original, unmodified plain
+disable.
+
+---
+--- a/drivers/input/misc/pwm-vibra.c
++++ b/drivers/input/misc/pwm-vibra.c
+@@ -11,6 +11,7 @@
+ * Copyright (C) 2010, Lars-Peter Clausen
+ */
+
++#include
+ #include
+ #include
+ #include
+@@ -20,6 +21,7 @@
+ #include
+ #include
+ #include
++#include
+ #include
+
+ struct pwm_vibrator {
+@@ -129,7 +131,7 @@
+ {
+ struct pwm_vibrator *vibrator;
+ struct pwm_state state;
+- int err;
++ int err, i;
+
+ vibrator = devm_kzalloc(&pdev->dev, sizeof(*vibrator), GFP_KERNEL);
+ if (!vibrator)
+@@ -157,9 +159,40 @@
+
+ INIT_WORK(&vibrator->play_work, pwm_vibrator_play_work);
+
+- /* Sync up PWM state and ensure it is off. */
+ pwm_init_state(vibrator->pwm, &state);
++
++ if (soc_is_px30s()) {
++ /*
++ * This channel can power up already latched in a stuck-on
++ * state (enabled, 100% duty) that a plain disable never
++ * clears - the very first transition away from that
++ * boot-inherited state needs a real, substantial duty change
++ * to actually take hold in hardware. Ramp down from 90% duty
++ * (skipping the redundant 100% step, since that's already
++ * where the boot-inherited state sits) to 0% over a fixed
++ * 250ms before disabling - long enough to hold on any
++ * board's period, gradual enough to feel like a deliberate
++ * fade rather than an abrupt jolt. Scoped to PX30S, the only
++ * silicon variant confirmed to hit this symptom (GKD
++ * Pixel 2) - other boards using this driver keep the plain
++ * disable below unchanged.
++ */
++ state.enabled = true;
++ for (i = 0; i < 10; i++) {
++ state.duty_cycle = state.period * (9 - i) / 10;
++ err = pwm_apply_might_sleep(vibrator->pwm, &state);
++ if (err) {
++ dev_err(&pdev->dev,
++ "failed to kick pwm state: %d\n",
++ err);
++ return err;
++ }
++ msleep(25);
++ }
++ }
++
+ state.enabled = false;
++ state.duty_cycle = 0;
+ err = pwm_apply_might_sleep(vibrator->pwm, &state);
+ if (err) {
+ dev_err(&pdev->dev, "failed to apply initial PWM state: %d\n",
diff --git a/projects/ROCKNIX/devices/RK3326/patches/linux/031-px30s-suspend-wakeup-config.patch b/projects/ROCKNIX/devices/RK3326/patches/linux/031-px30s-suspend-wakeup-config.patch
new file mode 100644
index 00000000000..2b397703cb0
--- /dev/null
+++ b/projects/ROCKNIX/devices/RK3326/patches/linux/031-px30s-suspend-wakeup-config.patch
@@ -0,0 +1,181 @@
+Subject: [PATCH] soc: rockchip: add PX30S suspend wakeup configuration
+
+Not ported from upstream - trimmed from the vendor BSP kernel
+(https://github.com/rockchip-linux/kernel, drivers/soc/rockchip/
+rockchip_pm_config.c), which mainline lacks entirely. Forwards this
+board's rockchip,sleep-mode-config and rockchip,wakeup-config values to
+BL31 firmware via the SIP_SUSPEND_MODE SMC call - on PX30S the deepest
+suspend states are governed by firmware, not Linux, so these values
+have to reach BL31 for it to know what to arm as a wake source.
+
+A previous attempt at this only sent mode/wakeup config once, at probe
+time. The vendor driver additionally sends LINUX_PM_STATE - which
+suspend state (mem/standby/freeze) is actually being entered - from a
+dev_pm_ops .prepare hook that runs on every single suspend attempt, and
+re-sends mode/wakeup config at the same time. That's the piece this
+adds: without ever telling firmware which state Linux is entering, BL31
+may not arm the deep power-down/wake path the same way it would if
+this call were present, regardless of whether the mode/wakeup config
+values themselves are correct.
+
+Trimmed relative to the vendor source: this board's own rockchip-
+suspend node only ever sets rockchip,sleep-debug-en/sleep-mode-config/
+wakeup-config, so the regulator on/off lists, virtual-poweroff,
+mcu-sleep-cfg, PM-domain linking, and multi-state (mem-lite/mem-ultra)
+handling are all dropped entirely rather than ported unused.
+
+NOT yet confirmed fixing wake-from-sleep on real hardware.
+
+---
+--- /dev/null
++++ b/drivers/soc/rockchip/rockchip-pm-config.c
+@@ -0,0 +1,120 @@
++// SPDX-License-Identifier: GPL-2.0
++/*
++ * Rockchip PX30S suspend-to-RAM wakeup configuration.
++ *
++ * Not ported from upstream - trimmed from the vendor BSP kernel
++ * (https://github.com/rockchip-linux/kernel, drivers/soc/rockchip/
++ * rockchip_pm_config.c), which mainline lacks entirely. Forwards this
++ * board's rockchip,sleep-mode-config and rockchip,wakeup-config values to
++ * BL31 firmware via the SIP_SUSPEND_MODE SMC call - on PX30S the deepest
++ * suspend states are governed by firmware, not Linux, so these values
++ * have to reach BL31 for it to know what to arm as a wake source.
++ *
++ * A previous attempt at this only sent mode/wakeup config once, at probe
++ * time. The vendor driver additionally sends LINUX_PM_STATE - which
++ * suspend state (mem/standby/freeze) is actually being entered - from a
++ * dev_pm_ops .prepare hook that runs on every single suspend attempt, and
++ * re-sends mode/wakeup config at the same time. That's the piece this
++ * adds: without ever telling firmware which state Linux is entering, BL31
++ * may not arm the deep power-down/wake path the same way it would if
++ * this call were present, regardless of whether the mode/wakeup config
++ * values themselves are correct.
++ *
++ * Trimmed relative to the vendor source: this board's own rockchip-
++ * suspend node only ever sets rockchip,sleep-debug-en/sleep-mode-config/
++ * wakeup-config, so the regulator on/off lists, virtual-poweroff,
++ * mcu-sleep-cfg, PM-domain linking, and multi-state (mem-lite/mem-ultra)
++ * handling are all dropped entirely rather than ported unused.
++ *
++ * NOT yet confirmed fixing wake-from-sleep on real hardware.
++ */
++
++#include
++#include
++#include
++#include
++#include
++
++#define SIP_SUSPEND_MODE 0x82000003
++
++#define SUSPEND_MODE_CONFIG 0x01
++#define WKUP_SOURCE_CONFIG 0x02
++#define SUSPEND_DEBUG_ENABLE 0x05
++#define LINUX_PM_STATE 0x09
++
++static u32 rk_pm_sleep_mode_config;
++static u32 rk_pm_wakeup_config;
++
++static int rockchip_pm_sip_config(u32 ctrl, u32 config1, u32 config2)
++{
++ struct arm_smccc_res res;
++
++ arm_smccc_smc(SIP_SUSPEND_MODE, ctrl, config1, config2, 0, 0, 0, 0,
++ &res);
++ return res.a0;
++}
++
++/*
++ * Runs on every suspend attempt (not just once at boot) - this is the
++ * call an earlier attempt at this fix was missing entirely.
++ */
++static int rockchip_pm_config_prepare(struct device *dev)
++{
++ rockchip_pm_sip_config(LINUX_PM_STATE, mem_sleep_current, 0);
++
++ if (rk_pm_sleep_mode_config)
++ rockchip_pm_sip_config(SUSPEND_MODE_CONFIG,
++ rk_pm_sleep_mode_config, 0);
++ if (rk_pm_wakeup_config)
++ rockchip_pm_sip_config(WKUP_SOURCE_CONFIG,
++ rk_pm_wakeup_config, 0);
++
++ return 0;
++}
++
++static const struct dev_pm_ops rockchip_pm_config_ops = {
++ .prepare = rockchip_pm_config_prepare,
++};
++
++static int rockchip_pm_config_probe(struct platform_device *pdev)
++{
++ struct device_node *node = pdev->dev.of_node;
++ u32 sleep_debug_en = 0;
++
++ of_property_read_u32(node, "rockchip,sleep-mode-config",
++ &rk_pm_sleep_mode_config);
++ of_property_read_u32(node, "rockchip,wakeup-config",
++ &rk_pm_wakeup_config);
++
++ if (rk_pm_sleep_mode_config)
++ rockchip_pm_sip_config(SUSPEND_MODE_CONFIG,
++ rk_pm_sleep_mode_config, 0);
++ if (rk_pm_wakeup_config)
++ rockchip_pm_sip_config(WKUP_SOURCE_CONFIG,
++ rk_pm_wakeup_config, 0);
++
++ if (!of_property_read_u32(node, "rockchip,sleep-debug-en",
++ &sleep_debug_en))
++ rockchip_pm_sip_config(SUSPEND_DEBUG_ENABLE, sleep_debug_en, 0);
++
++ return 0;
++}
++
++static const struct of_device_id rockchip_pm_config_of_match[] = {
++ { .compatible = "rockchip,pm-px30", },
++ { },
++};
++MODULE_DEVICE_TABLE(of, rockchip_pm_config_of_match);
++
++static struct platform_driver rockchip_pm_config_driver = {
++ .probe = rockchip_pm_config_probe,
++ .driver = {
++ .name = "rockchip-pm-config",
++ .of_match_table = rockchip_pm_config_of_match,
++ .pm = &rockchip_pm_config_ops,
++ },
++};
++module_platform_driver(rockchip_pm_config_driver);
++
++MODULE_DESCRIPTION("Rockchip PX30S suspend mode config");
++MODULE_LICENSE("GPL");
+--- a/drivers/soc/rockchip/Kconfig
++++ b/drivers/soc/rockchip/Kconfig
+@@ -24,6 +24,17 @@
+ base compatible string but differ in silicon revision, detected via
+ either an OTP-provisioned ID or a direct hardware register read.
+
++config ROCKCHIP_PM_CONFIG
++ bool "Rockchip PX30S suspend wakeup configuration support"
++ depends on OF && HAVE_ARM_SMCCC
++ default y if ARCH_ROCKCHIP
++ help
++ Say y here to forward this board's sleep-mode/wakeup-source
++ configuration to BL31 firmware via SIP calls on every suspend
++ attempt, required for PX30S's firmware-controlled deep suspend
++ states to correctly arm wake sources. Built-in only (not a
++ module) since it reads the kernel's own mem_sleep_current state.
++
+ config ROCKCHIP_IODOMAIN
+ tristate "Rockchip IO domain support"
+ depends on OF
+--- a/drivers/soc/rockchip/Makefile
++++ b/drivers/soc/rockchip/Makefile
+@@ -4,5 +4,6 @@
+ #
+ obj-$(CONFIG_ROCKCHIP_GRF) += grf.o
+ obj-$(CONFIG_ROCKCHIP_CPUINFO) += rockchip-cpuinfo.o
++obj-$(CONFIG_ROCKCHIP_PM_CONFIG) += rockchip-pm-config.o
+ obj-$(CONFIG_ROCKCHIP_IODOMAIN) += io-domain.o
+ obj-$(CONFIG_ROCKCHIP_DTPM) += dtpm.o
diff --git a/projects/ROCKNIX/devices/RK3326/patches/linux/032-px30s-otp-read-protocol.patch b/projects/ROCKNIX/devices/RK3326/patches/linux/032-px30s-otp-read-protocol.patch
new file mode 100644
index 00000000000..9ea385401a9
--- /dev/null
+++ b/projects/ROCKNIX/devices/RK3326/patches/linux/032-px30s-otp-read-protocol.patch
@@ -0,0 +1,235 @@
+Ported from the vendor BSP kernel (rockchip-linux/kernel,
+drivers/nvmem/rockchip-otp.c), which selects a distinct active/standby
+OTP read protocol for PX30S at runtime:
+
+ data = of_device_get_match_data(dev);
+ ...
+ if (soc_is_px30s() || soc_is_rk3308bs())
+ data = &px30s_data;
+
+Mainline's rockchip-otp.c has no such override and always uses the plain
+PX30 SBPI/user-mode read path, regardless of whether the chip is actually
+PX30S - even on the vendor's own PX30S boards, which still use the
+generic "rockchip,px30-otp" compatible and rely entirely on this runtime
+check rather than a distinct compatible string. Ported using the shared
+soc_is_px30s() helper (see 026-px30s-cpuinfo-soc-detection.patch) instead
+of an inline per-file check.
+
+Adapted to mainline's rockchip-otp.c, which has been refactored since the
+vendor tree branched: clk_bulk_prepare_enable/disable now happens once in
+the shared rockchip_otp_read() wrapper rather than per reg_read
+implementation, and rockchip_otp_wait_status() already takes a register
+argument, so the vendor's separate px30s_otp_wait_status() collapses into
+reuse of the existing helper.
+
+This affects nvmem cells consumed by cpu0_opp_table/px30s_cpu0_opp_table
+("cpu_leakage", "performance") for Rockchip's AVS voltage-bin selection -
+moot on its own since mainline doesn't implement AVS at all, but the OTP
+read path itself was still wrong for this silicon and worth fixing
+correctly rather than leaving in place.
+
+---
+--- a/drivers/nvmem/rockchip-otp.c
++++ b/drivers/nvmem/rockchip-otp.c
+@@ -14,6 +14,7 @@
+ #include
+ #include
+ #include
++#include
+ #include
+ #include
+ #include
+@@ -32,6 +33,25 @@
+ #define OTPC_SBPI_CMD0_OFFSET 0x1000
+ #define OTPC_SBPI_CMD1_OFFSET 0x1004
+
++#define OTPC_MODE_CTRL 0x2000
++#define OTPC_IRQ_ST 0x2008
++#define OTPC_ACCESS_ADDR 0x200c
++#define OTPC_RD_DATA 0x2010
++#define OTPC_REPR_RD_TRANS_NUM 0x2020
++
++#define OTPC_DEEP_STANDBY 0x0
++#define OTPC_STANDBY 0x1
++#define OTPC_ACTIVE 0x2
++#define OTPC_READ_ACCESS 0x3
++#define OTPC_TRANS_NUM 0x1
++#define OTPC_RDM_IRQ_ST BIT(0)
++#define OTPC_STB2ACT_IRQ_ST BIT(7)
++#define OTPC_DP2STB_IRQ_ST BIT(8)
++#define OTPC_ACT2STB_IRQ_ST BIT(9)
++#define OTPC_STB2DP_IRQ_ST BIT(10)
++#define PX30S_NBYTES 4
++#define PX30S_NO_SECURE_OFFSET 224
++
+ /* OTP Register bits and masks */
+ #define OTPC_USER_ADDR_MASK GENMASK(31, 16)
+ #define OTPC_USE_USER BIT(0)
+@@ -184,6 +204,137 @@
+ return ret;
+ }
+
++/*
++ * PX30S uses a different OTPC than plain PX30: reads have to walk the
++ * controller through an active/standby power-state machine (deep-standby ->
++ * standby -> active before reading, then back down afterwards) instead of
++ * the SBPI/user-mode register dance px30_otp_read() uses. Ported from the
++ * vendor BSP kernel (rockchip-linux/kernel, drivers/nvmem/rockchip-otp.c),
++ * adapted to this driver's clk handling (clk_bulk_prepare_enable/disable
++ * already happens in the shared rockchip_otp_read() wrapper here, unlike
++ * the vendor driver where each reg_read implementation does its own).
++ */
++static int px30s_otp_active(struct rockchip_otp *otp)
++{
++ int ret = 0;
++ u32 mode;
++
++ mode = readl(otp->base + OTPC_MODE_CTRL);
++
++ switch (mode) {
++ case OTPC_DEEP_STANDBY:
++ writel(OTPC_STANDBY, otp->base + OTPC_MODE_CTRL);
++ ret = rockchip_otp_wait_status(otp, OTPC_IRQ_ST, OTPC_DP2STB_IRQ_ST);
++ if (ret < 0) {
++ dev_err(otp->dev, "timeout during wait dp2stb\n");
++ return ret;
++ }
++ fallthrough;
++ case OTPC_STANDBY:
++ writel(OTPC_ACTIVE, otp->base + OTPC_MODE_CTRL);
++ ret = rockchip_otp_wait_status(otp, OTPC_IRQ_ST, OTPC_STB2ACT_IRQ_ST);
++ if (ret < 0) {
++ dev_err(otp->dev, "timeout during wait stb2act\n");
++ return ret;
++ }
++ break;
++ default:
++ break;
++ }
++
++ return ret;
++}
++
++static int px30s_otp_standby(struct rockchip_otp *otp)
++{
++ int ret = 0;
++ u32 mode;
++
++ mode = readl(otp->base + OTPC_MODE_CTRL);
++
++ switch (mode) {
++ case OTPC_ACTIVE:
++ writel(OTPC_STANDBY, otp->base + OTPC_MODE_CTRL);
++ ret = rockchip_otp_wait_status(otp, OTPC_IRQ_ST, OTPC_ACT2STB_IRQ_ST);
++ if (ret < 0) {
++ dev_err(otp->dev, "timeout during wait act2stb\n");
++ return ret;
++ }
++ fallthrough;
++ case OTPC_STANDBY:
++ writel(OTPC_DEEP_STANDBY, otp->base + OTPC_MODE_CTRL);
++ ret = rockchip_otp_wait_status(otp, OTPC_IRQ_ST, OTPC_STB2DP_IRQ_ST);
++ if (ret < 0) {
++ dev_err(otp->dev, "timeout during wait stb2dp\n");
++ return ret;
++ }
++ break;
++ default:
++ break;
++ }
++
++ return ret;
++}
++
++static int px30s_otp_read(void *context, unsigned int offset,
++ void *val, size_t bytes)
++{
++ struct rockchip_otp *otp = context;
++ unsigned int addr_start, addr_end, addr_offset, addr_len;
++ int ret, i = 0;
++ u32 out_value;
++ u8 *buf;
++
++ if (offset >= otp->data->size)
++ return -ENOMEM;
++ if (offset + bytes > otp->data->size)
++ bytes = otp->data->size - offset;
++
++ ret = rockchip_otp_reset(otp);
++ if (ret) {
++ dev_err(otp->dev, "failed to reset otp phy\n");
++ return ret;
++ }
++
++ ret = px30s_otp_active(otp);
++ if (ret)
++ return ret;
++
++ addr_start = round_down(offset, PX30S_NBYTES) / PX30S_NBYTES;
++ addr_end = round_up(offset + bytes, PX30S_NBYTES) / PX30S_NBYTES;
++ addr_offset = offset % PX30S_NBYTES;
++ addr_len = addr_end - addr_start;
++ addr_start += PX30S_NO_SECURE_OFFSET;
++
++ buf = kzalloc(array_size(addr_len, PX30S_NBYTES), GFP_KERNEL);
++ if (!buf) {
++ ret = -ENOMEM;
++ goto read_end;
++ }
++
++ while (addr_len--) {
++ writel(OTPC_TRANS_NUM, otp->base + OTPC_REPR_RD_TRANS_NUM);
++ writel(addr_start++, otp->base + OTPC_ACCESS_ADDR);
++ writel(OTPC_READ_ACCESS, otp->base + OTPC_MODE_CTRL);
++ ret = rockchip_otp_wait_status(otp, OTPC_IRQ_ST, OTPC_RDM_IRQ_ST);
++ if (ret < 0) {
++ dev_err(otp->dev, "timeout during wait rd\n");
++ goto free_buf;
++ }
++ out_value = readl(otp->base + OTPC_RD_DATA);
++ memcpy(&buf[i], &out_value, PX30S_NBYTES);
++ i += PX30S_NBYTES;
++ }
++ memcpy(val, buf + addr_offset, bytes);
++
++free_buf:
++ kfree(buf);
++read_end:
++ px30s_otp_standby(otp);
++
++ return ret;
++}
++
+ static int rk3588_otp_read(void *context, unsigned int offset,
+ void *val, size_t bytes)
+ {
+@@ -274,6 +425,13 @@
+ .reg_read = px30_otp_read,
+ };
+
++static const struct rockchip_data px30s_data = {
++ .size = 0x80,
++ .clks = px30_otp_clocks,
++ .num_clks = ARRAY_SIZE(px30_otp_clocks),
++ .reg_read = px30s_otp_read,
++};
++
+ static const struct rockchip_data rk3576_data = {
+ .size = 0x100,
+ .read_offset = 0x700,
+@@ -327,6 +485,15 @@
+ if (!data)
+ return dev_err_probe(dev, -EINVAL, "failed to get match data\n");
+
++ /*
++ * PX30S boards, including ours, still use the plain "rockchip,px30-otp"
++ * compatible in their devicetree - the vendor BSP always overrides the
++ * match-data at runtime instead of using a distinct compatible string,
++ * so mirror that here rather than requiring a devicetree change.
++ */
++ if (soc_is_px30s())
++ data = &px30s_data;
++
+ otp = devm_kzalloc(&pdev->dev, sizeof(struct rockchip_otp),
+ GFP_KERNEL);
+ if (!otp)
diff --git a/projects/ROCKNIX/devices/RK3326/patches/linux/033-px30s-ddrphy-clock-topology.patch b/projects/ROCKNIX/devices/RK3326/patches/linux/033-px30s-ddrphy-clock-topology.patch
new file mode 100644
index 00000000000..6cdbf55c6b4
--- /dev/null
+++ b/projects/ROCKNIX/devices/RK3326/patches/linux/033-px30s-ddrphy-clock-topology.patch
@@ -0,0 +1,141 @@
+From: Elaine Zhang
+Date: Tue, 10 Aug 2021 11:16:34 +0800
+Subject: [PATCH] clk: rockchip: px30: support px30s
+
+Change-Id: Id86199e066e254279d59a76aaed02f657b40e0c7
+Signed-off-by: Elaine Zhang
+
+Ported from rockchip-linux/kernel commit
+f89c8138cab33025cc4fbc8c40756f89f5ba0d09 to mainline 6.12. Only the
+clk_ddrphy1x/clk_stdby_2wrap/clk_otp_usr topology split is ported here
+- the unrelated SRST_I2S0_RX_S/SRST_DCF_P_S dt-bindings additions from
+the same upstream commit aren't referenced by this hunk and are
+dropped.
+
+Every other PX30S-specific subsystem difference from the vendor BSP
+already has a corresponding patch in this series using soc_is_px30s()
+(023-025, 027-028) - this was the one gap: clk-px30.c still
+unconditionally used vanilla PX30's DDR PHY clock topology for PX30S
+boards too.
+
+On vanilla PX30, clk_ddrphy1x and clk_stdby_2wrap are both fixed /4
+derivatives of a clk_ddrphy4x parent (so they run at the same rate as
+each other). On PX30S there is no clk_ddrphy4x at all: clk_ddrphy1x is
+its own independently muxed/divided composite clock, and
+clk_stdby_2wrap is a /4 of clk_ddrphy1x itself - a quarter of its rate,
+not the same rate. The OTP clock chain differs too: vanilla derives
+clk_otp_usr from an intermediate clk_otp fed by xin24m; PX30S drives
+clk_otp_usr directly from a mux between xin24m and gpll, with no
+intermediate clk_otp at all.
+
+soc_is_px30s() falls back to of_machine_is_compatible() when
+rockchip_soc_id hasn't been set yet, but that fallback checks for the
+literal "rockchip,px30s" compatible string, which no board in this
+tree uses (they use "rockchip,rk3326" instead) - so it would evaluate
+false this early. clk-px30.c's init runs via CLK_OF_DECLARE, called
+from of_clk_init() long before pure_initcall (when
+rockchip_soc_id_init() would otherwise run on its own). Matching the
+vendor source exactly, rockchip_soc_id_init() is called explicitly
+here first - it's idempotent (returns immediately if rockchip_soc_id
+is already set) and, this early, still correctly detects PX30 vs
+PX30S via a direct DDR_GRF register read gated on cpu_is_px30()'s
+of_machine_is_compatible() check, which does include "rockchip,rk3326"
+and works fine at this point in boot.
+
+---
+--- a/drivers/clk/rockchip/clk-px30.c
++++ b/drivers/clk/rockchip/clk-px30.c
+@@ -8,6 +8,7 @@
+ #include
+ #include
+ #include
++#include
+ #include
+ #include
+ #include "clk.h"
+@@ -145,6 +146,7 @@
+ PNAME(mux_gpll_cpll_p) = { "gpll", "dummy_cpll" };
+ PNAME(mux_gpll_npll_p) = { "gpll", "npll" };
+ PNAME(mux_gpll_xin24m_p) = { "gpll", "xin24m"};
++PNAME(mux_xin24m_gpll_p) = { "xin24m", "gpll"};
+ PNAME(mux_gpll_cpll_npll_p) = { "gpll", "dummy_cpll", "npll" };
+ PNAME(mux_gpll_cpll_npll_xin24m_p) = { "gpll", "dummy_cpll", "npll", "xin24m" };
+ PNAME(mux_gpll_xin24m_npll_p) = { "gpll", "xin24m", "npll"};
+@@ -335,12 +337,6 @@
+ PX30_CLKGATE_CON(0), 13, GFLAGS),
+ COMPOSITE_NOGATE(SCLK_DDRCLK, "sclk_ddrc", mux_ddrphy_p, CLK_IGNORE_UNUSED,
+ PX30_CLKSEL_CON(2), 7, 1, MFLAGS, 0, 3, DFLAGS | CLK_DIVIDER_POWER_OF_TWO),
+- COMPOSITE_NOGATE(0, "clk_ddrphy4x", mux_ddrphy_p, CLK_IGNORE_UNUSED,
+- PX30_CLKSEL_CON(2), 7, 1, MFLAGS, 0, 3, DFLAGS),
+- FACTOR_GATE(0, "clk_ddrphy1x", "clk_ddrphy4x", CLK_IGNORE_UNUSED, 1, 4,
+- PX30_CLKGATE_CON(0), 14, GFLAGS),
+- FACTOR_GATE(0, "clk_stdby_2wrap", "clk_ddrphy4x", CLK_IGNORE_UNUSED, 1, 4,
+- PX30_CLKGATE_CON(1), 0, GFLAGS),
+ COMPOSITE_NODIV(0, "clk_ddrstdby", mux_ddrstdby_p, CLK_IGNORE_UNUSED,
+ PX30_CLKSEL_CON(2), 4, 1, MFLAGS,
+ PX30_CLKGATE_CON(1), 13, GFLAGS),
+@@ -767,12 +763,6 @@
+ COMPOSITE_NOMUX(SCLK_SARADC, "clk_saradc", "xin24m", 0,
+ PX30_CLKSEL_CON(55), 0, 11, DFLAGS,
+ PX30_CLKGATE_CON(12), 10, GFLAGS),
+- COMPOSITE_NOMUX(SCLK_OTP, "clk_otp", "xin24m", 0,
+- PX30_CLKSEL_CON(56), 0, 3, DFLAGS,
+- PX30_CLKGATE_CON(12), 11, GFLAGS),
+- COMPOSITE_NOMUX(SCLK_OTP_USR, "clk_otp_usr", "clk_otp", 0,
+- PX30_CLKSEL_CON(56), 4, 2, DFLAGS,
+- PX30_CLKGATE_CON(13), 6, GFLAGS),
+
+ GATE(0, "clk_cpu_boost", "xin24m", CLK_IGNORE_UNUSED,
+ PX30_CLKGATE_CON(12), 12, GFLAGS),
+@@ -999,6 +989,36 @@
+ "pclk_usb_grf",
+ };
+
++static struct rockchip_clk_branch px30_clk_ddrphy_otp[] __initdata = {
++ COMPOSITE_NOGATE(0, "clk_ddrphy4x", mux_ddrphy_p, CLK_IGNORE_UNUSED,
++ PX30_CLKSEL_CON(2), 7, 1, MFLAGS, 0, 3, DFLAGS),
++ FACTOR_GATE(0, "clk_ddrphy1x", "clk_ddrphy4x", CLK_IGNORE_UNUSED, 1, 4,
++ PX30_CLKGATE_CON(0), 14, GFLAGS),
++ FACTOR_GATE(0, "clk_stdby_2wrap", "clk_ddrphy4x",
++ CLK_IGNORE_UNUSED, 1, 4,
++ PX30_CLKGATE_CON(1), 0, GFLAGS),
++
++ COMPOSITE_NOMUX(SCLK_OTP, "clk_otp", "xin24m", 0,
++ PX30_CLKSEL_CON(56), 0, 3, DFLAGS,
++ PX30_CLKGATE_CON(12), 11, GFLAGS),
++ COMPOSITE_NOMUX(SCLK_OTP_USR, "clk_otp_usr", "clk_otp", 0,
++ PX30_CLKSEL_CON(56), 4, 2, DFLAGS,
++ PX30_CLKGATE_CON(13), 6, GFLAGS),
++};
++
++static struct rockchip_clk_branch px30s_clk_ddrphy_otp[] __initdata = {
++ COMPOSITE(0, "clk_ddrphy1x", mux_ddrphy_p, CLK_IGNORE_UNUSED,
++ PX30_CLKSEL_CON(2), 7, 1, MFLAGS, 0, 3, DFLAGS,
++ PX30_CLKGATE_CON(0), 14, GFLAGS),
++ FACTOR_GATE(0, "clk_stdby_2wrap", "clk_ddrphy1x",
++ CLK_IGNORE_UNUSED, 1, 4,
++ PX30_CLKGATE_CON(1), 0, GFLAGS),
++
++ COMPOSITE(SCLK_OTP_USR, "clk_otp_usr", mux_xin24m_gpll_p, 0,
++ PX30_CLKSEL_CON(56), 8, 1, MFLAGS, 0, 8, DFLAGS,
++ PX30_CLKGATE_CON(12), 11, GFLAGS),
++};
++
+ static void __init px30_clk_init(struct device_node *np)
+ {
+ struct rockchip_clk_provider *ctx;
+@@ -1026,6 +1046,14 @@
+ rockchip_clk_register_branches(ctx, px30_clk_branches,
+ ARRAY_SIZE(px30_clk_branches));
+
++ rockchip_soc_id_init();
++ if (soc_is_px30s())
++ rockchip_clk_register_branches(ctx, px30s_clk_ddrphy_otp,
++ ARRAY_SIZE(px30s_clk_ddrphy_otp));
++ else
++ rockchip_clk_register_branches(ctx, px30_clk_ddrphy_otp,
++ ARRAY_SIZE(px30_clk_ddrphy_otp));
++
+ rockchip_clk_register_armclk(ctx, ARMCLK, "armclk",
+ mux_armclk_p, ARRAY_SIZE(mux_armclk_p),
+ &px30_cpuclk_data, px30_cpuclk_rates,
diff --git a/projects/ROCKNIX/packages/apps/gamecontrollerdb/config/gamecontrollerdb.txt b/projects/ROCKNIX/packages/apps/gamecontrollerdb/config/gamecontrollerdb.txt
index d5d75b585b4..416d5c0911d 100644
--- a/projects/ROCKNIX/packages/apps/gamecontrollerdb/config/gamecontrollerdb.txt
+++ b/projects/ROCKNIX/packages/apps/gamecontrollerdb/config/gamecontrollerdb.txt
@@ -7,6 +7,7 @@
1900adda4b4800001211000000010000,retrogame_joypad_s1_f2,platform:Linux,x:b2,a:b1,b:b0,y:b3,back:b8,guide:b10,start:b9,dpleft:b15,dpdown:b14,dpright:b16,dpup:b13,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,leftstick:b11,rightstick:b12,leftx:a0,lefty:a1,rightx:a2,righty:a3,
19009321b0c300000002000010000000,XU10 Gamepad,platform:Linux,x:b3,a:b2,b:b1,y:b0,back:b8,guide:b16,start:b9,dpleft:b14,dpdown:b13,dpright:b15,dpup:b12,leftshoulder:b4,lefttrigger:b7,rightshoulder:b5,righttrigger:b6,leftstick:b10,rightstick:b11,leftx:a0,lefty:a1,rightx:a2,righty:a3,
190014b301000000a20a000000010000,r33s_joypad,platform:Linux,x:b2,a:b1,b:b0,y:b3,back:b8,start:b9,dpleft:b12,dpdown:b11,dpright:b13,dpup:b10,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,
+190043d2010000000221000000010000,gkd_pixel2_joypad,platform:Linux,a:b0,b:b1,x:b2,y:b3,leftshoulder:b4,rightshoulder:b5,lefttrigger:b6,righttrigger:b7,back:b8,start:b9,dpup:b10,dpdown:b11,dpleft:b12,dpright:b13,guide:b14,
1900e5914b4800007711000077010000,RGB20S Gamepad,platform:Linux,x:b2,a:b1,b:b0,y:b3,back:b12,start:b13,dpleft:b10,dpdown:b9,dpright:b11,dpup:b8,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,leftstick:b14,rightstick:b15,leftx:a0,lefty:a1,rightx:a2,righty:a3,
1900e7444b4800000111000034020000,rg552_joypad,platform:Linux,x:b2,a:b1,b:b0,y:b3,back:b8,start:b9,dpleft:b15,dpdown:b14,dpright:b16,dpup:b13,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,leftstick:b11,rightstick:b12,leftx:a0,lefty:a1,rightx:a2,righty:a3,
03001a3447616d65466f726365204100,GameForce ACE Gamepad,a:b1,b:b0,dpdown:b13,dpleft:b14,lefty:a1,leftx:a0,leftshoulder:b5,leftstick:b10,lefttrigger:-a2,dpright:b15,+righty:+a4,-rightx:-a3,+rightx:+a3,-righty:-a4,rightshoulder:b6,rightstick:b11,righttrigger:-a5,back:b7,guide:b9,start:b8,dpup:b12,x:b3,y:b4,platform:Linux,
diff --git a/projects/ROCKNIX/packages/emulators/libretro/retroarch/retroarch-joypads/gamepads/gkd_pixel2_joypad.cfg b/projects/ROCKNIX/packages/emulators/libretro/retroarch/retroarch-joypads/gamepads/gkd_pixel2_joypad.cfg
new file mode 100644
index 00000000000..b9e775bff89
--- /dev/null
+++ b/projects/ROCKNIX/packages/emulators/libretro/retroarch/retroarch-joypads/gamepads/gkd_pixel2_joypad.cfg
@@ -0,0 +1,35 @@
+input_driver = "udev"
+input_device = "gkd_pixel2_joypad"
+input_device_display_name = "GKD Pixel 2"
+
+input_b_btn = "1"
+input_y_btn = "3"
+input_select_btn = "8"
+input_start_btn = "9"
+input_a_btn = "0"
+input_x_btn = "2"
+input_l_btn = "4"
+input_r_btn = "5"
+input_l2_btn = "6"
+input_r2_btn = "7"
+
+input_up_btn = "10"
+input_down_btn = "11"
+input_left_btn = "12"
+input_right_btn = "13"
+
+# Hotkeys - this board has a dedicated Function button (index 14, separate
+# from Select) unlike r33s/odroidgo3, which have to double up Select as the
+# hotkey-hold modifier. Using the dedicated button here instead means Select
+# stays purely Select in-game.
+input_enable_hotkey_btn = "14"
+input_exit_emulator_btn = "9"
+
+input_menu_toggle_btn = "2"
+input_fps_toggle_btn = "3"
+
+input_load_state_btn = "4"
+input_save_state_btn = "5"
+
+input_rewind_btn = "6"
+input_toggle_fast_forward_btn = "7"
diff --git a/projects/ROCKNIX/packages/emulators/standalone/flycast-sa/config/RK3326/mappings/SDL_gkd_pixel2_joypad.cfg b/projects/ROCKNIX/packages/emulators/standalone/flycast-sa/config/RK3326/mappings/SDL_gkd_pixel2_joypad.cfg
new file mode 100644
index 00000000000..edde7940003
--- /dev/null
+++ b/projects/ROCKNIX/packages/emulators/standalone/flycast-sa/config/RK3326/mappings/SDL_gkd_pixel2_joypad.cfg
@@ -0,0 +1,21 @@
+[digital]
+bind0 = 0:btn_a
+bind1 = 1:btn_b
+bind10 = 10:btn_analog_up
+bind11 = 11:btn_analog_down
+bind12 = 12:btn_analog_left
+bind13 = 13:btn_analog_right
+bind2 = 2:btn_y
+bind3 = 3:btn_x
+bind4 = 4:btn_trigger_left
+bind5 = 5:btn_trigger_right
+bind6 = 6:btn_jump_state
+bind7 = 7:btn_quick_save
+bind8 = 8:btn_menu
+bind9 = 9:btn_start
+
+[emulator]
+dead_zone = 10
+mapping_name = gkd_pixel2_joypad
+rumble_power = 100
+version = 3
diff --git a/projects/ROCKNIX/packages/emulators/standalone/mupen64plus-sa/mupen64plus-sa-core/config/RK3326/mupen64plus.cfg.gkd_pixel2 b/projects/ROCKNIX/packages/emulators/standalone/mupen64plus-sa/mupen64plus-sa-core/config/RK3326/mupen64plus.cfg.gkd_pixel2
new file mode 100644
index 00000000000..6ab3cd91618
--- /dev/null
+++ b/projects/ROCKNIX/packages/emulators/standalone/mupen64plus-sa/mupen64plus-sa-core/config/RK3326/mupen64plus.cfg.gkd_pixel2
@@ -0,0 +1,670 @@
+# Mupen64Plus Configuration File
+# This file is automatically read and written by the Mupen64Plus Core library
+
+[64DD]
+
+# Filename of the 64DD IPL ROM
+IPL-ROM = ""
+# Filename of the disk to load into Disk Drive
+Disk = ""
+
+
+[Audio-SDL]
+
+# Mupen64Plus SDL Audio Plugin config parameter version number
+Version = 1.000000
+# Frequency which is used if rom doesn't want to change it
+DEFAULT_FREQUENCY = 33600
+# Swaps left and right channels
+SWAP_CHANNELS = False
+# Size of primary buffer in output samples. This is where audio is loaded after it's extracted from n64's memory.
+PRIMARY_BUFFER_SIZE = 16384
+# Fullness level target for Primary audio buffer, in equivalent output samples. This value must be larger than the SECONDARY_BUFFER_SIZE. Decreasing this value will reduce audio latency but requires a faster PC to avoid choppiness. Increasing this will increase audio latency but reduce the chance of drop-outs.
+PRIMARY_BUFFER_TARGET = 2048
+# Size of secondary buffer in output samples. This is SDL's hardware buffer. The SDL documentation states that this should be a power of two between 512 and 8192.
+SECONDARY_BUFFER_SIZE = 1024
+# Audio resampling algorithm. src-sinc-best-quality, src-sinc-medium-quality, src-sinc-fastest, src-zero-order-hold, src-linear, speex-fixed-{10-0}, trivial
+RESAMPLE = "trivial"
+# Volume control type: 1 = SDL (only affects Mupen64Plus output) 2 = OSS mixer (adjusts master PC volume)
+VOLUME_CONTROL_TYPE = 1
+# Percentage change each time the volume is increased or decreased
+VOLUME_ADJUST = 5
+# Default volume when a game is started. Only used if VOLUME_CONTROL_TYPE is 1
+VOLUME_DEFAULT = 100
+# Synchronize Video/Audio
+AUDIO_SYNC = False
+
+
+[Core]
+
+# Mupen64Plus Core config parameter set version number. Please don't change this version number.
+Version = 1.010000
+# Draw on-screen display if True, otherwise don't draw OSD
+OnScreenDisplay = True
+# Use Pure Interpreter if 0, Cached Interpreter if 1, or Dynamic Recompiler if 2 or more
+R4300Emulator = 2
+# Disable compiled jump commands in dynamic recompiler (should be set to False)
+NoCompiledJump = False
+# Disable 4MB expansion RAM pack. May be necessary for some games
+DisableExtraMem = False
+# Increment the save state slot after each save operation
+AutoStateSlotIncrement = False
+# Activate the R4300 debugger when ROM execution begins, if core was built with Debugger support
+EnableDebugger = False
+# Save state slot (0-9) to use when saving/loading the emulator state
+CurrentStateSlot = 0
+# Path to directory where screenshots are saved. If this is blank, the default value of ${UserDataPath}/screenshot will be used
+ScreenshotPath = "/storage/roms/n64"
+# Path to directory where emulator save states (snapshots) are saved. If this is blank, the default value of ${UserDataPath}/save will be used
+SaveStatePath = "/storage/roms/n64"
+# Path to directory where SRAM/EEPROM data (in-game saves) are stored. If this is blank, the default value of ${UserDataPath}/save will be used
+SaveSRAMPath = "/storage/roms/n64"
+# Path to a directory to search when looking for shared data files
+SharedDataPath = "/tmp/mupen64plussa"
+# Force number of cycles per emulated instruction
+CountPerOp = 0
+# Randomize PI/SI Interrupt Timing
+RandomizeInterrupt = True
+# Duration of SI DMA (-1: use per game settings)
+SiDmaDuration = -1
+# Gameboy Camera Video Capture backend
+GbCameraVideoCaptureBackend1 = ""
+# Disk Save Format (0: Full Disk Copy (*.ndr/*.d6r), 1: RAM Area Only (*.ram))
+SaveDiskFormat = 1
+
+
+[CoreEvents]
+
+# Mupen64Plus CoreEvents config parameter set version number. Please don't change this version number.
+Version = 1.000000
+# SDL keysym for stopping the emulator
+Kbd Mapping Stop = 27
+# SDL keysym for switching between fullscreen/windowed modes
+Kbd Mapping Fullscreen = 0
+# SDL keysym for saving the emulator state
+Kbd Mapping Save State = 286
+# SDL keysym for loading the emulator state
+Kbd Mapping Load State = 288
+# SDL keysym for advancing the save state slot
+Kbd Mapping Increment Slot = 0
+# SDL keysym for resetting the emulator
+Kbd Mapping Reset = 290
+# SDL keysym for slowing down the emulator
+Kbd Mapping Speed Down = 291
+# SDL keysym for speeding up the emulator
+Kbd Mapping Speed Up = 292
+# SDL keysym for taking a screenshot
+Kbd Mapping Screenshot = 293
+# SDL keysym for pausing the emulator
+Kbd Mapping Pause = 112
+# SDL keysym for muting/unmuting the sound
+Kbd Mapping Mute = 109
+# SDL keysym for increasing the volume
+Kbd Mapping Increase Volume = 93
+# SDL keysym for decreasing the volume
+Kbd Mapping Decrease Volume = 91
+# SDL keysym for temporarily going really fast
+Kbd Mapping Fast Forward = 102
+# SDL keysym for advancing by one frame when paused
+Kbd Mapping Frame Advance = 47
+# SDL keysym for pressing the game shark button
+Kbd Mapping Gameshark = 103
+# Joystick event string for stopping the emulator
+Joy Mapping Stop = "J0B14/B9"
+# Joystick event string for switching between fullscreen/windowed modes
+Joy Mapping Fullscreen = ""
+# Joystick event string for saving the emulator state
+Joy Mapping Save State = "J0B14/B5"
+# Joystick event string for loading the emulator state
+Joy Mapping Load State = "J0B14/B4"
+# Joystick event string for advancing the save state slot
+Joy Mapping Increment Slot = ""
+# Joystick event string for resetting the emulator
+Joy Mapping Reset = ""
+# Joystick event string for slowing down the emulator
+Joy Mapping Speed Down = ""
+# Joystick event string for speeding up the emulator
+Joy Mapping Speed Up = ""
+# Joystick event string for taking a screenshot
+Joy Mapping Screenshot = ""
+# Joystick event string for pausing the emulator
+Joy Mapping Pause = "J0B14/B0"
+# Joystick event string for muting/unmuting the sound
+Joy Mapping Mute = ""
+# Joystick event string for increasing the volume
+Joy Mapping Increase Volume = ""
+# Joystick event string for decreasing the volume
+Joy Mapping Decrease Volume = ""
+# Joystick event string for fast-forward
+Joy Mapping Fast Forward = "J0B14/B7"
+# Joystick event string for advancing by one frame when paused
+Joy Mapping Frame Advance = ""
+# Joystick event string for pressing the game shark button
+Joy Mapping Gameshark = ""
+# SDL keysym for save slot 0
+Kbd Mapping Slot 0 = 48
+# SDL keysym for save slot 1
+Kbd Mapping Slot 1 = 49
+# SDL keysym for save slot 2
+Kbd Mapping Slot 2 = 50
+# SDL keysym for save slot 3
+Kbd Mapping Slot 3 = 51
+# SDL keysym for save slot 4
+Kbd Mapping Slot 4 = 52
+# SDL keysym for save slot 5
+Kbd Mapping Slot 5 = 53
+# SDL keysym for save slot 6
+Kbd Mapping Slot 6 = 54
+# SDL keysym for save slot 7
+Kbd Mapping Slot 7 = 55
+# SDL keysym for save slot 8
+Kbd Mapping Slot 8 = 56
+# SDL keysym for save slot 9
+Kbd Mapping Slot 9 = 57
+
+
+[Input-SDL-Control1]
+
+# Mupen64Plus SDL Input Plugin config parameter version number. Please don't change this version number.
+version = 2.000000
+# Controller configuration mode: 0=Fully Manual, 1=Auto with named SDL Device, 2=Fully automatic
+mode = 2
+# Specifies which joystick is bound to this controller: -1=No joystick, 0 or more= SDL Joystick number
+device = 0
+# SDL joystick name (or Keyboard)
+name = "gkd_pixel2_joypad"
+# Specifies whether this controller is 'plugged in' to the simulated N64
+plugged = True
+# Specifies which type of expansion pak is in the controller: 1=None, 2=Mem pak, 4=Transfer pak, 5=Rumble pak
+plugin = 2
+# If True, then mouse buttons may be used with this controller
+mouse = False
+# Scaling factor for mouse movements. For X, Y axes.
+MouseSensitivity = "2.00,2.00"
+# The minimum absolute value of the SDL analog joystick axis to move the N64 controller axis value from 0. For X, Y axes.
+AnalogDeadzone = "0,0"
+# An absolute value of the SDL joystick axis >= AnalogPeak will saturate the N64 controller axis value (at 80). For X, Y axes. For each axis, this must be greater than the corresponding AnalogDeadzone value
+AnalogPeak = "32768,32768"
+# Digital button configuration mappings
+plugged = True
+mouse = False
+AnalogDeadzone = 0,0
+AnalogPeak = 32768,32768
+DPad R =
+DPad L =
+DPad D =
+DPad U =
+Start = button(9)
+Z Trig = button(4)
+B Button = button(3)
+A Button = button(0)
+C Button R = button(7)
+C Button L = button(6)
+C Button D = button(1)
+C Button U = button(2)
+R Trig = button(5)
+L Trig = button(4)
+Mempak switch =
+Rumblepak switch =
+# Analog axis configuration mappings
+X Axis = button(12,13)axis(0-,0+)
+Y Axis = butoon(10,11)axis(1-,1+)
+
+[Input-SDL-Control2]
+
+# Mupen64Plus SDL Input Plugin config parameter version number. Please don't change this version number.
+version = 2.000000
+# Controller configuration mode: 0=Fully Manual, 1=Auto with named SDL Device, 2=Fully automatic
+mode = 2
+# Specifies which joystick is bound to this controller: -1=No joystick, 0 or more= SDL Joystick number
+device = -1
+# SDL joystick name (or Keyboard)
+name = ""
+# Specifies whether this controller is 'plugged in' to the simulated N64
+plugged = False
+# Specifies which type of expansion pak is in the controller: 1=None, 2=Mem pak, 4=Transfer pak, 5=Rumble pak
+plugin = 2
+# If True, then mouse buttons may be used with this controller
+mouse = False
+# Scaling factor for mouse movements. For X, Y axes.
+MouseSensitivity = "2.00,2.00"
+# The minimum absolute value of the SDL analog joystick axis to move the N64 controller axis value from 0. For X, Y axes.
+AnalogDeadzone = "4096,4096"
+# An absolute value of the SDL joystick axis >= AnalogPeak will saturate the N64 controller axis value (at 80). For X, Y axes. For each axis, this must be greater than the corresponding AnalogDeadzone value
+AnalogPeak = "32768,32768"
+# Digital button configuration mappings
+DPad R = ""
+DPad L = ""
+DPad D = ""
+DPad U = ""
+Start = ""
+Z Trig = ""
+B Button = ""
+A Button = ""
+C Button R = ""
+C Button L = ""
+C Button D = ""
+C Button U = ""
+R Trig = ""
+L Trig = ""
+Mempak switch = ""
+Rumblepak switch = ""
+# Analog axis configuration mappings
+X Axis = ""
+Y Axis = ""
+
+
+[Input-SDL-Control3]
+
+# Mupen64Plus SDL Input Plugin config parameter version number. Please don't change this version number.
+version = 2.000000
+# Controller configuration mode: 0=Fully Manual, 1=Auto with named SDL Device, 2=Fully automatic
+mode = 2
+# Specifies which joystick is bound to this controller: -1=No joystick, 0 or more= SDL Joystick number
+device = -1
+# SDL joystick name (or Keyboard)
+name = ""
+# Specifies whether this controller is 'plugged in' to the simulated N64
+plugged = False
+# Specifies which type of expansion pak is in the controller: 1=None, 2=Mem pak, 4=Transfer pak, 5=Rumble pak
+plugin = 2
+# If True, then mouse buttons may be used with this controller
+mouse = False
+# Scaling factor for mouse movements. For X, Y axes.
+MouseSensitivity = "2.00,2.00"
+# The minimum absolute value of the SDL analog joystick axis to move the N64 controller axis value from 0. For X, Y axes.
+AnalogDeadzone = "4096,4096"
+# An absolute value of the SDL joystick axis >= AnalogPeak will saturate the N64 controller axis value (at 80). For X, Y axes. For each axis, this must be greater than the corresponding AnalogDeadzone value
+AnalogPeak = "32768,32768"
+# Digital button configuration mappings
+DPad R = ""
+DPad L = ""
+DPad D = ""
+DPad U = ""
+Start = ""
+Z Trig = ""
+B Button = ""
+A Button = ""
+C Button R = ""
+C Button L = ""
+C Button D = ""
+C Button U = ""
+R Trig = ""
+L Trig = ""
+Mempak switch = ""
+Rumblepak switch = ""
+# Analog axis configuration mappings
+X Axis = ""
+Y Axis = ""
+
+
+[Input-SDL-Control4]
+
+# Mupen64Plus SDL Input Plugin config parameter version number. Please don't change this version number.
+version = 2.000000
+# Controller configuration mode: 0=Fully Manual, 1=Auto with named SDL Device, 2=Fully automatic
+mode = 2
+# Specifies which joystick is bound to this controller: -1=No joystick, 0 or more= SDL Joystick number
+device = -1
+# SDL joystick name (or Keyboard)
+name = ""
+# Specifies whether this controller is 'plugged in' to the simulated N64
+plugged = False
+# Specifies which type of expansion pak is in the controller: 1=None, 2=Mem pak, 4=Transfer pak, 5=Rumble pak
+plugin = 2
+# If True, then mouse buttons may be used with this controller
+mouse = False
+# Scaling factor for mouse movements. For X, Y axes.
+MouseSensitivity = "2.00,2.00"
+# The minimum absolute value of the SDL analog joystick axis to move the N64 controller axis value from 0. For X, Y axes.
+AnalogDeadzone = "4096,4096"
+# An absolute value of the SDL joystick axis >= AnalogPeak will saturate the N64 controller axis value (at 80). For X, Y axes. For each axis, this must be greater than the corresponding AnalogDeadzone value
+AnalogPeak = "32768,32768"
+# Digital button configuration mappings
+DPad R = ""
+DPad L = ""
+DPad D = ""
+DPad U = ""
+Start = ""
+Z Trig = ""
+B Button = ""
+A Button = ""
+C Button R = ""
+C Button L = ""
+C Button D = ""
+C Button U = ""
+R Trig = ""
+L Trig = ""
+Mempak switch = ""
+Rumblepak switch = ""
+# Analog axis configuration mappings
+X Axis = ""
+Y Axis = ""
+
+
+[Rsp-HLE]
+
+# Mupen64Plus RSP HLE Plugin config parameter version number
+Version = 1.000000
+# Path to a RSP plugin which will be used when encountering an unknown ucode.You can disable this by letting an empty string.
+RspFallback = ""
+# Send display lists to the graphics plugin
+DisplayListToGraphicsPlugin = True
+# Send audio lists to the audio plugin
+AudioListToAudioPlugin = False
+
+
+[Transferpak]
+
+# Filename of the GB ROM to load into transferpak 1
+GB-rom-1 = ""
+# Filename of the GB RAM to load into transferpak 1
+GB-ram-1 = ""
+# Filename of the GB ROM to load into transferpak 2
+GB-rom-2 = ""
+# Filename of the GB RAM to load into transferpak 2
+GB-ram-2 = ""
+# Filename of the GB ROM to load into transferpak 3
+GB-rom-3 = ""
+# Filename of the GB RAM to load into transferpak 3
+GB-ram-3 = ""
+# Filename of the GB ROM to load into transferpak 4
+GB-rom-4 = ""
+# Filename of the GB RAM to load into transferpak 4
+GB-ram-4 = ""
+
+
+[UI-Console]
+
+# Mupen64Plus UI-Console config parameter set version number. Please don't change this version number.
+Version = 1.000000
+# Directory in which to search for plugins
+PluginDir = "/usr/local/lib/mupen64plus"
+# Filename of video plugin
+VideoPlugin = "mupen64plus-video-rice"
+# Filename of audio plugin
+AudioPlugin = "mupen64plus-audio-sdl.so"
+# Filename of input plugin
+InputPlugin = "mupen64plus-input-sdl.so"
+# Filename of RSP plugin
+RspPlugin = "mupen64plus-rsp-hle.so"
+
+
+[Video-General]
+
+# Width of output window or fullscreen width
+ScreenWidth = 427
+# Height of output window or fullscreen height
+ScreenHeight = 320
+# Use fullscreen mode if True, or windowed mode if False
+Fullscreen = True
+# If true, activate the SDL_GL_SWAP_CONTROL attribute
+VerticalSync = True
+# Rotate screen contents: 0=0 degree, 1=90 degree, 2 = 180 degree, 3=270 degree
+Rotate = 0
+
+[Video-GLideN64]
+
+###### Hardware Compatibility ######
+# Leave enabled unless a mobile gpu doesn't support it.
+EnableFragmentDepthWrite = 0
+# Hardware per pixel lighting, supposedly good for GLES3.1 devices or higher.
+EnableHWLighting = 0
+
+###### Enhancements #######
+# Internal render resoluton rather than output. So 2 x 240p is 480p, etc.
+UseNativeResolutionFactor = 1
+# Enable/Disable MultiSampling (0=off, 2,4,8,16=quality)
+MultiSampling = 0
+# Enable/Disable Fast Approximate Anti-Aliasing FXAA
+FXAA = 0
+# Max level of Anisotropic Filtering, 0 for off
+anisotropy = 0
+# Bilinear filtering mode (0=N64 3point, 1=standard)
+# @rishooty: best to use this at 1x native res, any higher turn it off.
+bilinearMode = 1
+
+###### Compatibility ######
+# Make texrect coordinates continuous to avoid black lines between them. (0=Off/default, 1=Auto, 2=Force)
+CorrectTexrectCoords = 1
+# Copy auxiliary buffers to RDRAM (default: 0)
+EnableCopyAuxiliaryToRDRAM = 1
+# Render 2D texrects in native resolution to fix misalignment between parts of 2D image. (0=Off/default, 1=Optimized, 2=Unoptimized)")
+EnableNativeResTexrects = 1
+
+###### Other ######
+# Enable threaded video backend at the cost of input lag
+ThreadedVideo = 1
+# Screen aspect ratio (0=stretch, 1=force 4:3, 2=force 16:9, 3=adjust)
+AspectRatio = 1
+# Display On-screen FPS
+ShowFPS = False
+
+[Video-Glide64mk2]
+
+# Enable full-scene anti-aliasing by setting this to a value greater than 1
+wrpAntiAliasing = 0
+# Card ID
+card_id = 0
+# If true, use polygon offset values specified below
+force_polygon_offset = False
+# Specifies a scale factor that is used to create a variable depth offset for each polygon
+polygon_offset_factor = 0.000000
+# Is multiplied by an implementation-specific value to create a constant depth offset
+polygon_offset_units = 0.000000
+# Vertical sync
+vsync = True
+# TODO:ssformat
+ssformat = False
+# Display performance stats (add together desired flags): 1=FPS counter, 2=VI/s counter, 4=% speed, 8=FPS transparent
+show_fps = 0
+# Clock enabled
+clock = False
+# Clock is 24-hour
+clock_24_hr = True
+# Wrapper resolution
+wrpResolution = 0
+# Wrapper VRAM
+wrpVRAM = 0
+# Wrapper FBO
+wrpFBO = True
+# Wrapper Anisotropic Filtering
+wrpAnisotropic = True
+# Texture Enhancement: Smooth/Sharpen Filters
+ghq_fltr = 0
+# Texture Compression: 0 for S3TC, 1 for FXT1
+ghq_cmpr = 0
+# Texture Enhancement: More filters
+ghq_enht = 0
+# Hi-res texture pack format (0 for none, 1 for Rice)
+ghq_hirs = 1
+# Compress texture cache with S3TC or FXT1
+ghq_enht_cmpr = False
+# Tile textures (saves memory but could cause issues)
+ghq_enht_tile = 0
+# Force 16bpp textures (saves ram but lower quality)
+ghq_enht_f16bpp = False
+# Compress texture cache
+ghq_enht_gz = True
+# Don't enhance textures for backgrounds
+ghq_enht_nobg = False
+# Enable S3TC and FXT1 compression
+ghq_hirs_cmpr = False
+# Tile hi-res textures (saves memory but could cause issues)
+ghq_hirs_tile = False
+# Force 16bpp hi-res textures (saves ram but lower quality)
+ghq_hirs_f16bpp = False
+# Compress hi-res texture cache
+ghq_hirs_gz = True
+# Alternative CRC calculation -- emulates Rice bug
+ghq_hirs_altcrc = True
+# Save tex cache to disk
+ghq_cache_save = True
+# Texture Cache Size (MB)
+ghq_cache_size = 128
+# Use full alpha channel -- could cause issues for some tex packs
+ghq_hirs_let_texartists_fly = False
+# Dump textures
+ghq_hirs_dump = False
+# Alternate texture size method: -1=Game default, 0=disable. 1=enable
+alt_tex_size = -1
+# Use first SETTILESIZE only: -1=Game default, 0=disable. 1=enable
+use_sts1_only = -1
+# Use spheric mapping only: -1=Game default, 0=disable. 1=enable
+force_calc_sphere = -1
+# Force positive viewport: -1=Game default, 0=disable. 1=enable
+correct_viewport = -1
+# Force texrect size to integral value: -1=Game default, 0=disable. 1=enable
+increase_texrect_edge = -1
+# Reduce fillrect size by 1: -1=Game default, 0=disable. 1=enable
+decrease_fillrect_edge = -1
+# Enable perspective texture correction emulation: -1=Game default, 0=disable. 1=enable
+texture_correction = -1
+# Set special scale for PAL games: -1=Game default, 0=disable. 1=enable
+pal230 = -1
+# 3DFX Dithered alpha emulation mode: -1=Game default, >=0=dithered alpha emulation mode
+stipple_mode = -1
+# 3DFX Dithered alpha pattern: -1=Game default, >=0=pattern used for dithered alpha emulation
+stipple_pattern = -1
+# Check microcode each frame: -1=Game default, 0=disable. 1=enable
+force_microcheck = -1
+# Force 0xb5 command to be quad, not line 3D: -1=Game default, 0=disable. 1=enable
+force_quad3d = -1
+# Enable near z clipping: -1=Game default, 0=disable. 1=enable
+clip_zmin = -1
+# Enable far plane clipping: -1=Game default, 0=disable. 1=enable
+clip_zmax = -1
+# Use fast CRC algorithm: -1=Game default, 0=disable. 1=enable
+fast_crc = -1
+# Adjust screen aspect for wide screen mode: -1=Game default, 0=disable. 1=enable
+adjust_aspect = -1
+# Force strict check in Depth buffer test: -1=Game default, 0=disable. 1=enable
+zmode_compare_less = -1
+# Apply alpha dither regardless of alpha_dither_mode: -1=Game default, 0=disable. 1=enable
+old_style_adither = -1
+# Scale vertex z value before writing to depth buffer: -1=Game default, 0=disable. 1=enable
+n64_z_scale = -1
+# Fast texrect rendering with hwfbe: -1=Game default, 0=disable. 1=enable
+optimize_texrect = -1
+# Do not copy auxiliary frame buffers: -1=Game default, 0=disable. 1=enable
+ignore_aux_copy = -1
+# Clear auxiliary texture frame buffers: -1=Game default, 0=disable. 1=enable
+hires_buf_clear = -1
+# Read alpha from framebuffer: -1=Game default, 0=disable. 1=enable
+fb_read_alpha = -1
+# Handle unchanged fb: -1=Game default, 0=disable. 1=enable
+useless_is_useless = -1
+# Set frambuffer CRC mode: -1=Game default, 0=disable CRC, 1=fast CRC, 2=safe CRC
+fb_crc_mode = -1
+# Filtering mode: -1=Game default, 0=automatic, 1=force bilinear, 2=force point sampled
+filtering = -1
+# Fog: -1=Game default, 0=disable. 1=enable
+fog = -1
+# Buffer clear on every frame: -1=Game default, 0=disable. 1=enable
+buff_clear = -1
+# Buffer swapping method: -1=Game default, 0=swap buffers when vertical interrupt has occurred, 1=swap buffers when set of conditions is satisfied. Prevents flicker on some games, 2=mix of first two methods
+swapmode = -1
+# Aspect ratio: -1=Game default, 0=Force 4:3, 1=Force 16:9, 2=Stretch, 3=Original
+aspect = -1
+# LOD calculation: -1=Game default, 0=disable. 1=fast, 2=precise
+lodmode = -1
+# Smart framebuffer: -1=Game default, 0=disable. 1=enable
+fb_smart = -1
+# Hardware frame buffer emulation: -1=Game default, 0=disable. 1=enable
+fb_hires = -1
+# Read framebuffer every frame (may be slow use only for effects that need it e.g. Banjo Kazooie, DK64 transitions): -1=Game default, 0=disable. 1=enable
+fb_read_always = -1
+# Render N64 frame buffer as texture: -1=Game default, 0=disable, 1=mode1, 2=mode2
+read_back_to_screen = -1
+# Show images written directly by CPU: -1=Game default, 0=disable. 1=enable
+detect_cpu_write = -1
+# Get frame buffer info: -1=Game default, 0=disable. 1=enable
+fb_get_info = -1
+# Enable software depth render: -1=Game default, 0=disable. 1=enable
+fb_render = -1
+# If true, skip up to maxframeskip frames to maintain clock schedule; if false, skip exactly maxframeskip frames
+autoframeskip = False
+# If autoframeskip is true, skip up to this many frames to maintain clock schedule; if autoframeskip is false, skip exactly this many frames
+maxframeskip = 0
+
+
+[Video-Rice]
+
+# Hack to accomodate widescreen devices
+ResolutionWidth = 480
+# Mupen64Plus Rice Video Plugin config parameter version number
+Version = 1
+# Frame Buffer Emulation (0=ROM default, 1=disable)
+FrameBufferSetting = 0
+# Frequency to write back the frame buffer (0=every frame, 1=every other frame, etc)
+FrameBufferWriteBackControl = 0
+# Render-to-texture emulation (0=none, 1=ignore, 2=normal, 3=write back, 4=write back and reload)
+RenderToTexture = 0
+# Control when the screen will be updated (0=ROM default, 1=VI origin update, 2=VI origin change, 3=CI change, 4=first CI change, 5=first primitive draw, 6=before screen clear, 7=after screen drawn)
+ScreenUpdateSetting = 4
+# Force to use normal alpha blender
+NormalAlphaBlender = False
+# Use a faster algorithm to speed up texture loading and CRC computation
+FastTextureLoading = False
+# Use different texture coordinate clamping code
+AccurateTextureMapping = True
+# Force emulated frame buffers to be in N64 native resolution
+InN64Resolution = False
+# Try to reduce Video RAM usage (should never be used)
+SaveVRAM = False
+# Enable this option to have better render-to-texture quality
+DoubleSizeForSmallTxtrBuf = False
+# Force to use normal color combiner
+DefaultCombinerDisable = False
+# Enable game-specific settings from INI file
+EnableHacks = True
+# If enabled, graphics will be drawn in WinFrame mode instead of solid and texture mode
+WinFrameMode = False
+# N64 Texture Memory Full Emulation (may fix some games, may break others)
+FullTMEMEmulation = False
+# Enable vertex clipper for fog operations
+OpenGLVertexClipper = False
+# Enable/Disable SSE optimizations for capable CPUs
+EnableSSE = True
+# If this option is enabled, the plugin will skip every other frame
+SkipFrame = False
+# If enabled, texture enhancement will be done only for TxtRect ucode
+TexRectOnly = False
+# If enabled, texture enhancement will be done only for textures width+height<=128
+SmallTextureOnly = False
+# Select hi-resolution textures based only on the CRC and ignore format+size information (Glide64 compatibility)
+LoadHiResCRCOnly = True
+# Enable hi-resolution texture file loading
+LoadHiResTextures = False
+# Enable texture dumping
+DumpTexturesToFiles = False
+# Display On-screen FPS
+ShowFPS = False
+# Use Mipmapping? 0=no, 1=nearest, 2=bilinear, 3=trilinear
+Mipmapping = 2
+# Enable, Disable fog generation (0=Disable, 1=Enable)
+FogMethod = 1
+# Force to use texture filtering or not (0=auto: n64 choose, 1=force no filtering, 2=force filtering)
+ForceTextureFilter = 0
+# Primary texture enhancement filter (0=None, 1=2X, 2=2XSAI, 3=HQ2X, 4=LQ2X, 5=HQ4X, 6=Sharpen, 7=Sharpen More, 8=External, 9=Mirrored)
+TextureEnhancement = 0
+# Secondary texture enhancement filter (0 = none, 1-4 = filtered)
+TextureEnhancementControl = 0
+# Color bit depth to use for textures (0=default, 1=32 bits, 2=16 bits)
+TextureQuality = 0
+# Z-buffer depth (only 16 or 32)
+OpenGLDepthBufferSetting = 16
+# Enable/Disable MultiSampling (0=off, 2,4,8,16=quality)
+MultiSampling = 0
+# Color bit depth for rendering window (0=32 bits, 1=16 bits)
+ColorQuality = 1
+# OpenGL level to support (0=auto, 1=OGL_FRAGMENT_PROGRAM)
+OpenGLRenderSetting = 0
+# Enable/Disable Anisotropic Filtering for Mipmapping (0=no filtering, 2-16=quality). This is uneffective if Mipmapping is 0. If the given value is to high to be supported by your graphic card, the value will be the highest value your graphic card can support. Better result with Trilinear filtering
+AnisotropicFiltering = 0
+# If true, use polygon offset values specified below
+ForcePolygonOffset = False
+# Specifies a scale factor that is used to create a variable depth offset for each polygon
+PolygonOffsetFactor = 0.000000
+# Is multiplied by an implementation-specific value to create a constant depth offset
+PolygonOffsetUnits = 0.000000
diff --git a/projects/ROCKNIX/packages/emulators/standalone/mupen64plus-sa/mupen64plus-sa-input-sdl/config/RK3326/default.ini b/projects/ROCKNIX/packages/emulators/standalone/mupen64plus-sa/mupen64plus-sa-input-sdl/config/RK3326/default.ini
index b62b91ecef5..f36b1b278d7 100644
--- a/projects/ROCKNIX/packages/emulators/standalone/mupen64plus-sa/mupen64plus-sa-input-sdl/config/RK3326/default.ini
+++ b/projects/ROCKNIX/packages/emulators/standalone/mupen64plus-sa/mupen64plus-sa-input-sdl/config/RK3326/default.ini
@@ -149,6 +149,31 @@ Rumblepak switch =
X Axis = button(12,13)axis(0-,0+)
Y Axis = button(10,11)axis(1-,1+)
+[gkd_pixel2_joypad]
+plugged = True
+mouse = False
+AnalogDeadzone = 0,0
+AnalogPeak = 32768,32768
+DPad R =
+DPad L =
+DPad D =
+DPad U =
+Start = button(9)
+Z Trig = button(4)
+B Button = button(3)
+A Button = button(0)
+C Button R = button(7)
+C Button L = button(6)
+C Button D = button(1)
+C Button U = button(2)
+R Trig = button(5)
+L Trig = button(4)
+Mempak switch =
+Rumblepak switch =
+# Analog axis configuration mappings
+X Axis = button(12,13)axis(0-,0+)
+Y Axis = button(10,11)axis(1-,1+)
+
[r36s_Gamepad]
plugged = True
mouse = False
diff --git a/projects/ROCKNIX/packages/hardware/quirks/devices/GameKiddy GKD Pixel2/001-device_config b/projects/ROCKNIX/packages/hardware/quirks/devices/GameKiddy GKD Pixel2/001-device_config
new file mode 100755
index 00000000000..9fdb5af9cec
--- /dev/null
+++ b/projects/ROCKNIX/packages/hardware/quirks/devices/GameKiddy GKD Pixel2/001-device_config
@@ -0,0 +1,8 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (C) 2023 JELOS (https://github.com/JustEnoughLinuxOS)
+
+cat </storage/.config/profile.d/001-device_config
+# Device Features
+DEVICE_BATTERY_LED_STATUS="true"
+EOF
diff --git a/projects/ROCKNIX/packages/hardware/quirks/devices/GameKiddy GKD Pixel2/020-suspend_mode b/projects/ROCKNIX/packages/hardware/quirks/devices/GameKiddy GKD Pixel2/020-suspend_mode
new file mode 100755
index 00000000000..b3fde9ada3a
--- /dev/null
+++ b/projects/ROCKNIX/packages/hardware/quirks/devices/GameKiddy GKD Pixel2/020-suspend_mode
@@ -0,0 +1,34 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (C) 2026-present ROCKNIX (https://github.com/ROCKNIX)
+
+# Workaround: GKD Pixel2 enters and wakes from "mem" (deep) suspend
+# correctly, but if a dynamic CPU governor (schedutil/ondemand) is active
+# after resume, the system reliably crashes within seconds - confirmed on
+# real hardware to be a memory corruption bug (kernel oops in
+# unmap_page_range, a real page-table translation fault), needing both a
+# CPU frequency/voltage transition and concurrent memory-management
+# activity (fork/exec) to manifest. Static governors (performance/
+# powersave, either voltage extreme) never trigger it, only after resume -
+# a fresh boot survives arbitrarily many/fast transitions under identical
+# load. See PX30S_DVFS_INVESTIGATION.md for the full session: kernel-side
+# regulator/clk/OPP theories were ruled out one by one, and BL31 firmware
+# reverse engineering found real, PX30S-specific DDR PHY calibration code
+# (ROCKCHIP_SIP_CONFIG_DRAM_FSP_INIT) that's never triggered on this
+# board - triggering it deliberately, twice (once with sentinel
+# parameters, once with real vendor-sourced electrical values) both
+# caused a full device reset rather than fixing anything, ruling out data
+# quality as the explanation and pointing at the SIP call itself being
+# unsafe to invoke from a live Linux kernel. No root cause found despite
+# that investigation. Until one is, default to "freeze" (s2idle) instead:
+# CPU cores stay mostly on and idle rather than powering off and later
+# resuming DVFS, sidestepping the corruption path entirely rather than
+# fixing it. Same mechanism RK3576 uses for the same class of issue.
+
+. /etc/profile.d/001-functions
+
+MYSLEEPMODE=$(get_setting system.suspendmode)
+if [ -z "${MYSLEEPMODE}" ]
+then
+ /usr/bin/suspendmode freeze
+fi
diff --git a/projects/ROCKNIX/packages/hardware/quirks/devices/GameKiddy GKD Pixel2/050-game-configs b/projects/ROCKNIX/packages/hardware/quirks/devices/GameKiddy GKD Pixel2/050-game-configs
new file mode 100755
index 00000000000..10f0cb73d4d
--- /dev/null
+++ b/projects/ROCKNIX/packages/hardware/quirks/devices/GameKiddy GKD Pixel2/050-game-configs
@@ -0,0 +1,17 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (C) 2026-present ROCKNIX (https://github.com/ROCKNIX)
+
+# Set mupen64plus-sa config for GKD Pixel2. The plain mupen64plus.cfg
+# shipped by the package is calibrated for a different reference
+# controller entirely, so without this the N64 button mapping (and the
+# Joy Mapping hotkey combos below) would be wrong for this board's
+# gkd_pixel2_joypad button layout.
+if [ ! -d "/storage/.config/mupen64plus" ]; then
+ mkdir -p "/storage/.config/mupen64plus/"
+ cp -r /usr/local/share/mupen64plus/mupen64plus.cfg* /storage/.config/mupen64plus/
+fi
+if [ -f "/storage/.config/mupen64plus/mupen64plus.cfg.gkd_pixel2" ]; then
+ rm /storage/.config/mupen64plus/mupen64plus.cfg
+ mv /storage/.config/mupen64plus/mupen64plus.cfg.gkd_pixel2 /storage/.config/mupen64plus/mupen64plus.cfg
+fi
diff --git a/projects/ROCKNIX/packages/hardware/quirks/devices/GameKiddy GKD Pixel2/050-modifiers b/projects/ROCKNIX/packages/hardware/quirks/devices/GameKiddy GKD Pixel2/050-modifiers
new file mode 100755
index 00000000000..32d16058777
--- /dev/null
+++ b/projects/ROCKNIX/packages/hardware/quirks/devices/GameKiddy GKD Pixel2/050-modifiers
@@ -0,0 +1,7 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (C) 2026-present ROCKNIX (https://github.com/ROCKNIX)
+
+cat </storage/.config/profile.d/050-modifiers
+DEVICE_FUNC_KEYA_MODIFIER="BTN_TRIGGER_HAPPY1"
+EOF
diff --git a/projects/ROCKNIX/packages/hardware/quirks/devices/GameKiddy GKD Pixel2/bin/battery_led_status b/projects/ROCKNIX/packages/hardware/quirks/devices/GameKiddy GKD Pixel2/bin/battery_led_status
new file mode 100755
index 00000000000..8396e97233f
--- /dev/null
+++ b/projects/ROCKNIX/packages/hardware/quirks/devices/GameKiddy GKD Pixel2/bin/battery_led_status
@@ -0,0 +1,185 @@
+#!/bin/sh
+#
+# Battery / Charging LED indicator
+#
+# LED layout:
+# battery4 = Green (top)
+# battery3 = Green
+# battery2 = Green
+# battery1 = Green (bottom)
+# battery0 = Red (bottom)
+#
+# LEDs are active-low:
+# 0 = ON
+# 1 = OFF
+#
+# On battery, not charging, VBUS not present:
+# 0% : shut the device down
+# 1-10% : one solid red
+# 11-25% : one solid green
+# 26-50% : two solid green
+# 51-75% : three solid green
+# 76-100% : four solid green
+#
+# Charging:
+# 1-10% : one solid red, one flashing green
+# 11-25% : one solid green, one flashing red
+# 26-50% : one solid green, one flashing green
+# 51-75% : two solid green, one flashing green
+# 76-99% : three solid green, one flashing green
+# 100% : four solid green (fully charged)
+#
+# Reading the role-switch state directly (host/device/none) isn't reliable
+# here - e.g. the port is in host role by default at boot, so that alone
+# false-positives on every normal boot. Instead combine two signals we do
+# trust: not charging (from the PMIC driver) plus VBUS actually present
+# (rk817_charger hwmon's input-voltage-average channel). Not charging while
+# VBUS is high can really only mean the port itself is sourcing VBUS (host
+# role) rather than sensing an external supply - so flash all five LEDs,
+# making it visibly obvious the system can't accept external power until
+# it's reset out of host mode.
+#
+# Debug:
+# ./battery_led_status 50
+# Sets 50% display (not-charging branch) and exits
+#
+
+. /etc/profile.d/001-functions
+
+RED="/sys/class/leds/battery0/brightness"
+G1="/sys/class/leds/battery1/brightness"
+G2="/sys/class/leds/battery2/brightness"
+G3="/sys/class/leds/battery3/brightness"
+G4="/sys/class/leds/battery4/brightness"
+
+BATTERY_STATUS="/sys/class/power_supply/battery/status"
+
+find_charger_hwmon()
+{
+ for hwmon in /sys/class/hwmon/hwmon*; do
+ [ "$(cat "$hwmon/name" 2>/dev/null)" = "rk817_charger" ] || continue
+ echo "$hwmon"
+ return
+ done
+}
+
+CHARGER_HWMON=$(find_charger_hwmon)
+
+led_on()
+{
+ echo 0 > "$1"
+}
+
+led_off()
+{
+ echo 1 > "$1"
+}
+
+all_off()
+{
+ led_off "$RED"
+ led_off "$G1"
+ led_off "$G2"
+ led_off "$G3"
+ led_off "$G4"
+}
+
+# $1..$5 = red g1 g2 g3 g4, each "1" (on) or "0" (off)
+set_leds()
+{
+ [ "$1" = "1" ] && led_on "$RED" || led_off "$RED"
+ [ "$2" = "1" ] && led_on "$G1" || led_off "$G1"
+ [ "$3" = "1" ] && led_on "$G2" || led_off "$G2"
+ [ "$4" = "1" ] && led_on "$G3" || led_off "$G3"
+ [ "$5" = "1" ] && led_on "$G4" || led_off "$G4"
+}
+
+# Same source and same "is charging" definition EmulationStation's battery
+# icon (Utils::Platform::queryBatteryInformation()) and powerstate.sh's GPU
+# profile switching already use, so the LED bar can't disagree with either
+# of them about whether we're charging.
+is_charging()
+{
+ case "$(cat "$BATTERY_STATUS" 2>/dev/null)" in
+ "Not charging"|Discharging|"") return 1 ;;
+ *) return 0 ;;
+ esac
+}
+
+# True if VBUS is actually present, read via the rk817_charger hwmon
+# input-voltage-average channel (charger_input_volt_avg_uv in
+# rk817_charger.c). Goes true for our own self-driven VBUS (host role)
+# just as much as a real external charger - only meaningful combined with
+# is_charging() below.
+is_vbus_present()
+{
+ [ -n "$CHARGER_HWMON" ] || return 1
+ [ -f "$CHARGER_HWMON/in0_average" ] || return 1
+
+ VOLTAGE=$(cat "$CHARGER_HWMON/in0_average" 2>/dev/null)
+ [ -n "$VOLTAGE" ] && [ "$VOLTAGE" -ge 1000 ]
+}
+
+# $1 = battery percent, $2 = blink phase ("1" or "0"), alternates every call
+show_level()
+{
+ PCT="$1"
+ BLINK="$2"
+
+ if is_charging; then
+ FLASH=0
+ [ "$BLINK" = "1" ] && FLASH=1
+
+ if [ "$PCT" -ge 100 ]; then set_leds 0 1 1 1 1
+ elif [ "$PCT" -le 10 ]; then set_leds 1 "$FLASH" 0 0 0
+ elif [ "$PCT" -le 25 ]; then set_leds "$FLASH" 1 0 0 0
+ elif [ "$PCT" -le 50 ]; then set_leds 0 1 "$FLASH" 0 0
+ elif [ "$PCT" -le 75 ]; then set_leds 0 1 1 "$FLASH" 0
+ else set_leds 0 1 1 1 "$FLASH"
+ fi
+
+ elif is_vbus_present; then
+ set_leds "$BLINK" "$BLINK" "$BLINK" "$BLINK" "$BLINK"
+
+ else
+ if [ "$PCT" -le 0 ]; then
+ all_off
+ systemctl poweroff
+ # Avoid re-triggering in a tight loop while poweroff completes.
+ sleep 60
+ elif [ "$PCT" -le 10 ]; then set_leds 1 0 0 0 0
+ elif [ "$PCT" -le 25 ]; then set_leds 0 1 0 0 0
+ elif [ "$PCT" -le 50 ]; then set_leds 0 1 1 0 0
+ elif [ "$PCT" -le 75 ]; then set_leds 0 1 1 1 0
+ else set_leds 0 1 1 1 1
+ fi
+ fi
+}
+
+
+# Debug mode
+if [ -n "$1" ]; then
+ show_level "$1" 1
+ exit 0
+fi
+
+
+# Normal daemon mode - runs at half-second granularity so the flashing
+# states (charging, and the VBUS-present-but-not-charging case) animate
+# smoothly instead of needing a separate nested flash loop.
+BLINK=0
+while :; do
+ PCT=$(battery_percent | tr -d '\r\n')
+
+ case "$PCT" in
+ ''|*[!0-9]*)
+ sleep 0.5
+ continue
+ ;;
+ esac
+
+ show_level "$PCT" "$BLINK"
+
+ BLINK=$(( 1 - BLINK ))
+ sleep 0.5
+done
diff --git a/projects/ROCKNIX/packages/ui/emulationstation/config/common/es_input.cfg b/projects/ROCKNIX/packages/ui/emulationstation/config/common/es_input.cfg
index c9b449fa57a..c4212e7ffa5 100644
--- a/projects/ROCKNIX/packages/ui/emulationstation/config/common/es_input.cfg
+++ b/projects/ROCKNIX/packages/ui/emulationstation/config/common/es_input.cfg
@@ -1420,6 +1420,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+