Skip to content

clk: rockchip: rk3328: armclk parent/mux mismatch — CPU runs at 24 MHz while cpufreq reports 1296 MHz #521

Description

@martintops

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_summaryarmclk 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions