Summary
On RK3328 the CPU runs from the 24 MHz crystal instead of its rated 1296 MHz, while the clock framework reports 1296 MHz the whole time. armclk's framework parent and its hardware mux value disagree: the framework programs APLL, the hardware mux selects NPLL, and NPLL is never programmed for CPU rates so it passes xin24m straight through.
Net effect is a silent ~54x slowdown that no sysfs interface reveals.
One-line fix at the bottom, verified on hardware.
Affected
Present and identical in every branch I checked:
rk-6.1-rkr7.2, rk-6.1-rkr6.1, rk-6.1-rkr5.1, rk-6.1-rkr4.1, rk-6.1-rkr1
rk-5.10-rkr8
The same code is also in rockchip-linux/kernel develop-6.1, so this looks inherited from the Rockchip vendor tree rather than introduced here.
Mainline is not affected — see below.
Symptom
Radxa ROCK Pi E (RK3328), Yocto scarthgap userspace, kernel 6.1.118 built from rk-6.1-rkr6.1:
|
kirkstone / 4.4 vendor kernel |
this tree |
scaling_cur_freq |
1296000 kHz |
1296000 kHz |
| actual CPU clock (PMU) |
1296.4 MHz |
23.7 MHz |
| IPC |
0.893 |
0.788 |
dd if=/dev/zero of=/dev/null bs=1M |
2.3 GB/s |
73 MB/s |
| boot to multi-user |
~25 s |
> 20 min |
Same board, same firmware (identical idbloader / DDR blob / BL31 / U-Boot), swapped only the eMMC.
The actual clock was measured with perf_event_open(2) / PERF_COUNT_HW_CPU_CYCLES against the arch timer, since /dev/mem is blocked by CONFIG_STRICT_DEVMEM and the CRU has no regmap debugfs entry. IPC stays healthy, so the memory system is fine — it is purely the core clock.
Root cause
Upstream clk-cpu.c derives the framework parent and the hardware mux from the same field, so they cannot disagree:
init.parent_names = &parent_names[reg_data->mux_core_main];
clk = __clk_lookup(parent_names[reg_data->mux_core_main]);
ret = clk_notifier_register(clk, &cpuclk->clk_nb);
With mux_core_main = 3 and
PNAME(mux_armclk_p) = { "apll_core", "gpll_core", "dpll_core", "npll_core" };
upstream puts armclk on npll_core and programs NPLL. Self-consistent.
This tree's rockchip_clk_register_armclk() instead takes the parent as an explicit struct clk *:
struct clk *parent, struct clk *alt_parent,
...
init.parent_names = &parent_name;
and clk-rk3328.c passes clks[PLL_APLL]:
rockchip_clk_register_armclk(ctx, ARMCLK, "armclk",
4, clks[PLL_APLL], clks[PLL_GPLL],
&rk3328_cpuclk_data, rk3328_cpuclk_rates,
ARRAY_SIZE(rk3328_cpuclk_rates));
while rk3328_cpuclk_data.mux_core_main was left at the upstream value 3. So the framework believes armclk's parent is APLL and programs APLL on every rate change, but rockchip_cpuclk_post_rate_change() writes mux value 3 into CLKSEL_CON0[7:6], selecting npll_core in hardware.
clk-rk3308.c in this same tree already uses the consistent combination — mux_core_main = 0 with clks[PLL_APLL]. rk3328 appears to be the only SoC here with the mismatch.
Corroboration from the board
/sys/kernel/debug/clk/clk_summary — armclk nested under apll at 1296 MHz (framework view), and NPLL sitting in slow mode:
pll_npll 0 0 0 800000000
npll 0 0 0 24000000 <-- mux on xin24m
npll_core 0 0 0 24000000
pll_apll 1 1 0 1296000000
apll 1 1 0 1296000000
armclk 2 2 0 1296000000 cpu0
- Writing every available OPP through
scaling_setspeed changes the reported rate and nothing in the measured one — cpufreq reprograms APLL, which the core is not connected to.
initcall_debug puts the onset at of_clk_set_defaults() on the cru node, where the assigned-clocks entry for ARMCLK in rk3328.dtsi triggers the first post_rate_change. Everything before that runs at full speed (ftrace_init 2.53 µs/entry, vs 2.87 µs/entry on the working 4.4 kernel).
- The 4.4 vendor kernel on the same board shows
armclk under npll at 1296 MHz — self-consistent the other way, which is why it is unaffected.
Why this has probably gone unnoticed
armbian/build only uses this repo for rk35xx.conf (RK3566/3568) and rockchip-rk3588.conf. RK3328 lives in rockchip64.conf, whose patch directories are rockchip64-6.12 / 6.18 / 7.1 / 7.2, i.e. mainline. So no Armbian image appears to build RK3328 from this tree, and the rk3328 clock driver here is inherited code that nothing exercises.
It still matters for anyone building RK3328 from the vendor tree outside Armbian — Yocto and Buildroot in particular, which is how we hit it.
A board where NPLL happens to be programmed for something else (HDMI, VOP) would run at that PLL's rate instead of 24 MHz — slow but not catastrophic, and easy to mistake for something else. Ours is headless, so NPLL stayed in slow mode.
Patch
--- a/drivers/clk/rockchip/clk-rk3328.c
+++ b/drivers/clk/rockchip/clk-rk3328.c
@@ static const struct rockchip_cpuclk_reg_data rk3328_cpuclk_data = {
.div_core_mask[0] = 0x1f,
.num_cores = 1,
.mux_core_alt = 1,
- .mux_core_main = 3,
+ .mux_core_main = 0,
.mux_core_shift = 6,
.mux_core_mask = 0x3,
};
Point the hardware mux at apll_core, the PLL this tree actually programs. mux_core_alt = 1 (gpll_core) already agrees with the clks[PLL_GPLL] passed as the alternate parent.
The equally valid alternative is to keep mux_core_main = 3 and pass clks[PLL_NPLL] at registration, matching upstream and the 4.4 vendor driver. I went with the one-liner; happy to send whichever you prefer as a PR.
Verification
With the patch, same board and image:
- PMU-measured CPU clock: 23.7 MHz → 1296 MHz
dd if=/dev/zero of=/dev/null bs=1M: 73 MB/s → 2.5 GB/s
- boot to multi-user: > 20 min → 14.97 s (3.2 s kernel + 11.7 s userspace)
- a syscall-free bash arithmetic loop, 20000 iterations: 57.6 s → 0.76 s
Note on how this was found
The analysis was done with the help of Claude Opus 5 (Anthropic), which is also what spotted the parent/mux mismatch. Everything above was then verified on real hardware: the clock figures are PMU counter readings from the board, the before/after numbers are measured on two physical ROCK Pi E units, and the patch has been built and booted. Flagging it in case that affects how you want to review it — please do sanity-check the reasoning rather than taking it on trust.
Summary
On RK3328 the CPU runs from the 24 MHz crystal instead of its rated 1296 MHz, while the clock framework reports 1296 MHz the whole time.
armclk's framework parent and its hardware mux value disagree: the framework programs APLL, the hardware mux selects NPLL, and NPLL is never programmed for CPU rates so it passesxin24mstraight through.Net effect is a silent ~54x slowdown that no sysfs interface reveals.
One-line fix at the bottom, verified on hardware.
Affected
Present and identical in every branch I checked:
rk-6.1-rkr7.2,rk-6.1-rkr6.1,rk-6.1-rkr5.1,rk-6.1-rkr4.1,rk-6.1-rkr1rk-5.10-rkr8The same code is also in
rockchip-linux/kerneldevelop-6.1, so this looks inherited from the Rockchip vendor tree rather than introduced here.Mainline is not affected — see below.
Symptom
Radxa ROCK Pi E (RK3328), Yocto scarthgap userspace, kernel 6.1.118 built from
rk-6.1-rkr6.1:scaling_cur_freqdd if=/dev/zero of=/dev/null bs=1MSame board, same firmware (identical idbloader / DDR blob / BL31 / U-Boot), swapped only the eMMC.
The actual clock was measured with
perf_event_open(2)/PERF_COUNT_HW_CPU_CYCLESagainst the arch timer, since/dev/memis blocked byCONFIG_STRICT_DEVMEMand the CRU has no regmap debugfs entry. IPC stays healthy, so the memory system is fine — it is purely the core clock.Root cause
Upstream
clk-cpu.cderives the framework parent and the hardware mux from the same field, so they cannot disagree:With
mux_core_main = 3andupstream puts
armclkonnpll_coreand programs NPLL. Self-consistent.This tree's
rockchip_clk_register_armclk()instead takes the parent as an explicitstruct clk *:and
clk-rk3328.cpassesclks[PLL_APLL]:while
rk3328_cpuclk_data.mux_core_mainwas left at the upstream value3. So the framework believesarmclk's parent is APLL and programs APLL on every rate change, butrockchip_cpuclk_post_rate_change()writes mux value 3 intoCLKSEL_CON0[7:6], selectingnpll_corein hardware.clk-rk3308.cin this same tree already uses the consistent combination —mux_core_main = 0withclks[PLL_APLL]. rk3328 appears to be the only SoC here with the mismatch.Corroboration from the board
/sys/kernel/debug/clk/clk_summary—armclknested underapllat 1296 MHz (framework view), and NPLL sitting in slow mode:scaling_setspeedchanges the reported rate and nothing in the measured one — cpufreq reprograms APLL, which the core is not connected to.initcall_debugputs the onset atof_clk_set_defaults()on thecrunode, where theassigned-clocksentry forARMCLKinrk3328.dtsitriggers the firstpost_rate_change. Everything before that runs at full speed (ftrace_init2.53 µs/entry, vs 2.87 µs/entry on the working 4.4 kernel).armclkunder npll at 1296 MHz — self-consistent the other way, which is why it is unaffected.Why this has probably gone unnoticed
armbian/buildonly uses this repo forrk35xx.conf(RK3566/3568) androckchip-rk3588.conf. RK3328 lives inrockchip64.conf, whose patch directories arerockchip64-6.12 / 6.18 / 7.1 / 7.2, i.e. mainline. So no Armbian image appears to build RK3328 from this tree, and the rk3328 clock driver here is inherited code that nothing exercises.It still matters for anyone building RK3328 from the vendor tree outside Armbian — Yocto and Buildroot in particular, which is how we hit it.
A board where NPLL happens to be programmed for something else (HDMI, VOP) would run at that PLL's rate instead of 24 MHz — slow but not catastrophic, and easy to mistake for something else. Ours is headless, so NPLL stayed in slow mode.
Patch
Point the hardware mux at
apll_core, the PLL this tree actually programs.mux_core_alt = 1(gpll_core) already agrees with theclks[PLL_GPLL]passed as the alternate parent.The equally valid alternative is to keep
mux_core_main = 3and passclks[PLL_NPLL]at registration, matching upstream and the 4.4 vendor driver. I went with the one-liner; happy to send whichever you prefer as a PR.Verification
With the patch, same board and image:
dd if=/dev/zero of=/dev/null bs=1M: 73 MB/s → 2.5 GB/sNote on how this was found
The analysis was done with the help of Claude Opus 5 (Anthropic), which is also what spotted the parent/mux mismatch. Everything above was then verified on real hardware: the clock figures are PMU counter readings from the board, the before/after numbers are measured on two physical ROCK Pi E units, and the patch has been built and booted. Flagging it in case that affects how you want to review it — please do sanity-check the reasoning rather than taking it on trust.