Audio clocks: pll-audio runs 9.5% slow for the 44.1 kHz family #52

Closed
opened 2026-08-28 16:33:58 +00:00 by tiagoagueda · 3 comments
Owner

pll-audio on sun9i produces the wrong rate for the 44.1 kHz family. The 48 kHz
family is correct as of the M divider offset fix; this is a separate, still-open
fault in the same PLL.

Measured

Playback of a buffer of known length, timed on a Tronsmart Draco AW80 Telos, on the
HDMI card:

60.000 s of 48000 Hz S16_LE stereo   ->  60.238 s     correct
60.000 s of 44100 Hz S16_LE stereo   ->  65.719 s     9.5% slow

Independently, the HDMI transmitter's automatic CTS counter, which is clocked by the
TMDS clock and counts incoming audio frames:

fs        N       CTS measured   CTS expected at 148.5 MHz
48000     6144    148500         148500     correct
44100     6272    180510         165000     9.4% high

Both say the same thing, by different mechanisms: the audio clock in the 44.1 kHz
family runs about 9.5% slow.

Effect

HDMI audio is silent at 44.1 kHz. The sink is sent an ACR describing a ~40.3 kHz
stream while the audio infoframe claims 44.1 kHz, and a Sony HT-G700 soundbar declines
to lock it rather than playing it flat. Resampling the same track to 48 kHz plays
correctly, which is how this was isolated.

S/PDIF shares this PLL and is presumably affected identically. Not yet tested at
44.1 kHz.

What is known about the cause

For 44.1 kHz the framework selects N=143, M=37 and computes
24 MHz * 143 / 38 = 90 315 789 Hz, which is what clk_summary reports.

pll-audio reg[008] = 80008f25   n=143 m=37     -> framework says 90 315 789
i2s1      reg[444] = 80000003   M=3, div 4     -> framework says 22 578 948

The downstream dividers are the same integers the 48 kHz case uses and they are in
sun4i-i2s's divider tables, so the error is upstream of them.

Unlike the M-versus-M+1 fault that this PLL already had, the residual here is not
an integer divider change.
Correcting for the measured startup overhead the ratio is
close to 12/11, which no change of M explains.

The most likely candidate is the pair of dividers the driver openly does not model:

/*
 * The Audio PLL has d1, d2 dividers in addition to the usual N, M
 * factors. Since we only need 2 frequencies from this PLL: 22.5792 MHz
 * and 24.576 MHz, ignore them for now. Enforce d1 = 0 and d2 = 0.
 */

Those would only bite for N/M combinations the 48 kHz family never selects, which
fits the shape of this exactly. This is a hypothesis, not a measurement.

Next steps

  1. Read d1/d2 out of reg[008] for both families and see whether they differ, and
    whether the "enforce d1 = 0, d2 = 0" claim actually holds.
  2. Check the A80 manual's PLL3 chapter for the real transfer function.
  3. Until it is understood, constrain pll-audio to rates the model provably gets right,
    so ALSA resamples instead of playing at the wrong pitch. Silently wrong is worse than
    unsupported.
  4. Re-test S/PDIF at 44.1 kHz.

The M divider offset fix that corrected the 48 kHz family is a separate commit and is
already in patches/linux-clk-sun9i/. Before it, every audio rate on this SoC was
1.04x slow while reading back as exactly correct - see 47-hdmi-audio-cec.md.

Worth noting for whoever picks this up: every symptom of both bugs reads back as
perfectly configured hardware. clk_summary reports the rate that was asked for, the
I2S registers hold the right dividers, and only a clock the SoC does not control - the
TMDS counter - or a stopwatch, disagrees.

`pll-audio` on sun9i produces the wrong rate for the 44.1 kHz family. The 48 kHz family is correct as of the `M` divider offset fix; this is a **separate, still-open** fault in the same PLL. ## Measured Playback of a buffer of known length, timed on a Tronsmart Draco AW80 Telos, on the HDMI card: ``` 60.000 s of 48000 Hz S16_LE stereo -> 60.238 s correct 60.000 s of 44100 Hz S16_LE stereo -> 65.719 s 9.5% slow ``` Independently, the HDMI transmitter's automatic CTS counter, which is clocked by the TMDS clock and counts incoming audio frames: ``` fs N CTS measured CTS expected at 148.5 MHz 48000 6144 148500 148500 correct 44100 6272 180510 165000 9.4% high ``` Both say the same thing, by different mechanisms: the audio clock in the 44.1 kHz family runs about 9.5% slow. ## Effect **HDMI audio is silent at 44.1 kHz.** The sink is sent an ACR describing a ~40.3 kHz stream while the audio infoframe claims 44.1 kHz, and a Sony HT-G700 soundbar declines to lock it rather than playing it flat. Resampling the same track to 48 kHz plays correctly, which is how this was isolated. S/PDIF shares this PLL and is presumably affected identically. **Not yet tested at 44.1 kHz.** ## What is known about the cause For 44.1 kHz the framework selects `N=143, M=37` and computes `24 MHz * 143 / 38 = 90 315 789 Hz`, which is what `clk_summary` reports. ``` pll-audio reg[008] = 80008f25 n=143 m=37 -> framework says 90 315 789 i2s1 reg[444] = 80000003 M=3, div 4 -> framework says 22 578 948 ``` The downstream dividers are the same integers the 48 kHz case uses and they are in `sun4i-i2s`'s divider tables, so the error is upstream of them. Unlike the `M`-versus-`M+1` fault that this PLL already had, **the residual here is not an integer divider change.** Correcting for the measured startup overhead the ratio is close to `12/11`, which no change of `M` explains. The most likely candidate is the pair of dividers the driver openly does not model: ```c /* * The Audio PLL has d1, d2 dividers in addition to the usual N, M * factors. Since we only need 2 frequencies from this PLL: 22.5792 MHz * and 24.576 MHz, ignore them for now. Enforce d1 = 0 and d2 = 0. */ ``` Those would only bite for `N`/`M` combinations the 48 kHz family never selects, which fits the shape of this exactly. **This is a hypothesis, not a measurement.** ## Next steps 1. Read `d1`/`d2` out of `reg[008]` for both families and see whether they differ, and whether the "enforce d1 = 0, d2 = 0" claim actually holds. 2. Check the A80 manual's PLL3 chapter for the real transfer function. 3. Until it is understood, constrain `pll-audio` to rates the model provably gets right, so ALSA resamples instead of playing at the wrong pitch. Silently wrong is worse than unsupported. 4. Re-test S/PDIF at 44.1 kHz. ## Related The `M` divider offset fix that corrected the 48 kHz family is a separate commit and is already in `patches/linux-clk-sun9i/`. Before it, *every* audio rate on this SoC was 1.04x slow while reading back as exactly correct - see `47-hdmi-audio-cec.md`. Worth noting for whoever picks this up: every symptom of both bugs reads back as perfectly configured hardware. `clk_summary` reports the rate that was asked for, the I2S registers hold the right dividers, and only a clock the SoC does not control - the TMDS counter - or a stopwatch, disagrees.
Author
Owner

Root-caused: the framework asks this PLL for a VCO the part cannot reach

The hypothesis in the issue is wrong. It is not d1/d2, not the sigma-delta
modulator, and not a divider offset. Read live, mid-playback, at 44 100 Hz:

pllaudiosdm: reg[008]=80008f25 en=1 sdm=0 d1=0 d2=0 N=143 P=37
pllaudiosdm: pat[108]=00000000 sdm_pat_en=0  lock[09c]=00000aef locked=1
pllaudiosdm: i2s1[444]=80000003 spdif[44c]=00000000

d1 = 0, d2 = 0 — the driver's "enforce d1 = 0 and d2 = 0" claim holds. The modulator is
off and its pattern word is zero. The PLL reports locked. Every field is exactly what the
model says, and the output is still 9.5% low.

The transfer function puts the fault upstream of every divider

A80 User Manual r1.1 §3.3.5.3, PLL_AUDIO_CTRL_REG at 0x008:

The PLL Output = 24MHz*N/(Input_div+1)/(Output_div+1)/(P+1).

P is a post-divider — it sits after the VCO and does nothing to keep it in range. With
d1 = 0 the VCO is simply 24 MHz x N. For the 44.1 kHz family the framework picks
N = 143, asking for a 3.432 GHz VCO. The manual specifies these PLLs to 3 GHz
("The PLL output ranges from 200MHz to 3GHz", §3.3.5.1).

ccu_nm_find_best() has no VCO bound. It minimises output error over n and m and will
happily pick any N up to 255 — a 6.1 GHz VCO — as long as the quotient comes out close.

Measured: hold the output constant, vary only the VCO

Every pair below has N/(P+1) ~ 3.7632: the same ~90.3 MHz modelled output, the same i2s1
divider of 4, the same everything downstream. Only N changes. If the part behaved as
modelled, all eight rows would be identical.

15.000 s buffer of 44 100 Hz S16_LE stereo, timed around aplay:

N P VCO asked for elapsed error
64 16 1 536 MHz 15.055 s -
79 20 1 896 MHz 15.066 s -
113 29 2 712 MHz 15.047 s -
128 33 3 072 MHz 15.054 s -
132 34 3 168 MHz 15.184 s +0.9%
136 35 3 264 MHz 15.615 s +3.7%
139 36 3 336 MHz 16.045 s +6.6%
143 37 3 432 MHz 16.470 s +9.8%

Flat to 3.07 GHz, then a clean monotonic fall-off. Backing out the VCO actually delivered
from the four slow rows: 3 141 / 3 147 / 3 130 / 3 137 MHz — four independent estimates
inside +-0.3% of each other.

The VCO saturates at about 3.14 GHz. That is a hard ceiling, not a modelling error.

48 kHz works by luck

122.88 MHz wants N/(P+1) = 5.12 = 128/25, so N = 128 and the VCO lands at 3.072 GHz —
35 MHz under the measured ceiling. It has been running a hair below the rail all along.

The vendor never goes near this

get_factors_pll3() in clk-sun9iw1.c supports exactly two rates and returns -1 for
anything else
. There is no factor_pll3_tbl; pll1, pll2 and pll4..pll12 all have frequency
tables, the audio PLL alone does not.

target N d1 d2 P SDM pattern VCO
22 579 200 54 0 1 28 0xc00121ff 1 296 MHz
24 576 000 61 0 1 29 0xc000e147 1 464 MHz

Both under 1.5 GHz — less than half the ceiling.

Why sigma-delta is the fix, not an optimisation

No integer N/P can produce the 44.1 kHz family at all: 22.5792 / 24 = 588/625 in lowest
terms, so an exact solution needs P+1 to be a multiple of 625, and P+1 <= 64. This is
exactly why upstream converted a83t, a64, h3, r40, v3s and h6 to sigma-delta for the
audio PLL. sun9i-a80 was never converted, and ccu-sun8i-a83t.c carries the same two
pattern words the A80 vendor code uses.

.max_rate is not a fix — it clamps the output rate, and the output rate was never the
problem.

Also worth flagging

The M-offset fix from 47-hdmi-audio-cec.md / 49-audio-clock-wrong.md is confirmed by the
datasheet (the part divides by P+1), and _SUNXI_CCU_DIV_OFFSET(0, 6, 0) is still in
torvalds/master — so mainline's 48 kHz is 4% slow on every A80. Two patches to send here,
not one.

S/PDIF picks the same N = 143 and has the same fault (#42), still untested at 44.1 kHz.

Full write-up with method and caveats: 51-audio-pll-vco-ceiling.md. New probe:
probe/pllaudiosdm.c.

## Root-caused: the framework asks this PLL for a VCO the part cannot reach **The hypothesis in the issue is wrong.** It is not `d1`/`d2`, not the sigma-delta modulator, and not a divider offset. Read live, mid-playback, at 44 100 Hz: ``` pllaudiosdm: reg[008]=80008f25 en=1 sdm=0 d1=0 d2=0 N=143 P=37 pllaudiosdm: pat[108]=00000000 sdm_pat_en=0 lock[09c]=00000aef locked=1 pllaudiosdm: i2s1[444]=80000003 spdif[44c]=00000000 ``` `d1 = 0`, `d2 = 0` — the driver's "enforce d1 = 0 and d2 = 0" claim holds. The modulator is off and its pattern word is zero. The PLL reports locked. Every field is exactly what the model says, and the output is still 9.5% low. ### The transfer function puts the fault upstream of every divider A80 User Manual r1.1 §3.3.5.3, `PLL_AUDIO_CTRL_REG` at `0x008`: > The PLL Output = 24MHz\*N/(Input_div+1)/(Output_div+1)/(P+1). `P` is a **post**-divider — it sits after the VCO and does nothing to keep it in range. With `d1 = 0` the VCO is simply `24 MHz x N`. For the 44.1 kHz family the framework picks **N = 143**, asking for a **3.432 GHz VCO**. The manual specifies these PLLs to 3 GHz ("The PLL output ranges from 200MHz to 3GHz", §3.3.5.1). `ccu_nm_find_best()` has no VCO bound. It minimises output error over `n` and `m` and will happily pick any N up to 255 — a 6.1 GHz VCO — as long as the quotient comes out close. ### Measured: hold the output constant, vary only the VCO Every pair below has `N/(P+1) ~ 3.7632`: the same ~90.3 MHz modelled output, the same `i2s1` divider of 4, the same everything downstream. **Only N changes.** If the part behaved as modelled, all eight rows would be identical. 15.000 s buffer of 44 100 Hz S16_LE stereo, timed around `aplay`: | N | P | VCO asked for | elapsed | error | |---:|---:|---:|---:|---:| | 64 | 16 | 1 536 MHz | 15.055 s | - | | 79 | 20 | 1 896 MHz | 15.066 s | - | | 113 | 29 | 2 712 MHz | 15.047 s | - | | **128** | 33 | **3 072 MHz** | 15.054 s | - | | 132 | 34 | 3 168 MHz | 15.184 s | +0.9% | | 136 | 35 | 3 264 MHz | 15.615 s | +3.7% | | 139 | 36 | 3 336 MHz | 16.045 s | +6.6% | | **143** | **37** | **3 432 MHz** | **16.470 s** | **+9.8%** | Flat to 3.07 GHz, then a clean monotonic fall-off. Backing out the VCO actually delivered from the four slow rows: **3 141 / 3 147 / 3 130 / 3 137 MHz** — four independent estimates inside +-0.3% of each other. **The VCO saturates at about 3.14 GHz.** That is a hard ceiling, not a modelling error. ### 48 kHz works by luck 122.88 MHz wants `N/(P+1) = 5.12 = 128/25`, so N = 128 and the VCO lands at 3.072 GHz — **35 MHz under the measured ceiling**. It has been running a hair below the rail all along. ### The vendor never goes near this `get_factors_pll3()` in `clk-sun9iw1.c` supports exactly two rates and **returns `-1` for anything else**. There is no `factor_pll3_tbl`; pll1, pll2 and pll4..pll12 all have frequency tables, the audio PLL alone does not. | target | N | d1 | d2 | P | SDM pattern | VCO | |---|---:|---:|---:|---:|---|---:| | 22 579 200 | 54 | 0 | 1 | 28 | `0xc00121ff` | 1 296 MHz | | 24 576 000 | 61 | 0 | 1 | 29 | `0xc000e147` | 1 464 MHz | Both under 1.5 GHz — less than half the ceiling. ### Why sigma-delta is the fix, not an optimisation No integer N/P can produce the 44.1 kHz family at all: `22.5792 / 24 = 588/625` in lowest terms, so an exact solution needs `P+1` to be a multiple of 625, and `P+1 <= 64`. This is exactly why upstream converted **a83t, a64, h3, r40, v3s and h6** to sigma-delta for the audio PLL. `sun9i-a80` was never converted, and `ccu-sun8i-a83t.c` carries the same two pattern words the A80 vendor code uses. `.max_rate` is **not** a fix — it clamps the output rate, and the output rate was never the problem. ### Also worth flagging The M-offset fix from `47-hdmi-audio-cec.md` / `49-audio-clock-wrong.md` is confirmed by the datasheet (the part divides by `P+1`), and `_SUNXI_CCU_DIV_OFFSET(0, 6, 0)` is still in `torvalds/master` — so mainline's 48 kHz is 4% slow on every A80. Two patches to send here, not one. S/PDIF picks the same N = 143 and has the same fault (#42), still untested at 44.1 kHz. Full write-up with method and caveats: `51-audio-pll-vco-ceiling.md`. New probe: `probe/pllaudiosdm.c`.
Author
Owner

Fixed: sigma-delta table for pll-audio

Patch written, built, deployed and measured on the board.
patches/linux-clk-sun9i/0001-clk-sunxi-ng-sun9i-use-sigma-delta-modulation-for-pl.patch.

static struct ccu_sdm_setting pll_audio_sdm_table[] = {
	{ .rate = 45158400, .pattern = 0xc00121ff, .m = 29, .n = 54 },
	{ .rate = 49152000, .pattern = 0xc000e147, .m = 30, .n = 61 },
};

plus .sdm = _SUNXI_CCU_SDM(pll_audio_sdm_table, BIT(24), 0x108, BIT(31)) and
CCU_FEATURE_SIGMA_DELTA_MOD. The table's m/n are calculation values and the m field
carries offset 1, so m = 29 writes P = 28 - the vendor's exact register values.

ccu_nm_set_rate() needed nothing: ccu_sdm_helper_has_rate() short-circuits
ccu_nm_find_best() and writes the table's factors and pattern directly, so the unbounded
integer search never runs for these two rates.

What the driver now writes

44.1 kHz  reg[008]=8100361c en=1 sdm=1 d1=0 d2=0 N=54 P=28   pat[108]=c00121ff  locked=1
48   kHz  reg[008]=81003d1d en=1 sdm=1 d1=0 d2=0 N=61 P=29   pat[108]=c000e147  locked=1

VCO 1.296 and 1.464 GHz, against the ~3.14 GHz ceiling. Both families are now exact,
where the integer search could only approximate:

before after
pll-audio at 44.1 kHz 90 315 789 45 158 400
i2s1 at 44.1 kHz 22 578 948 22 579 200
pll-audio at 48 kHz 122 880 000 49 152 000
i2s1 at 48 kHz 24 576 000 24 576 000

Measured

15.000 s buffer 60.000 s buffer residual
HDMI 44.1 kHz 15.112 s 60.105 s constant
HDMI 48 kHz 15.220 s 60.225 s constant
S/PDIF 44.1 kHz 15.097 s -
S/PDIF 48 kHz 15.220 s -

44.1 kHz was 65.719 s for a 60 s buffer before this.

Two buffer lengths on purpose: the residual does not grow with duration, so it is fixed
startup overhead and not a rate error - a rate error would have quadrupled between 15 s and
60 s. That is the argument from 49-audio-clock-wrong.md run in reverse, to show there is
nothing left rather than to pin what is there.

S/PDIF is fixed with it (#42). It shares this PLL and picked the same N = 143, and had
never been tested at 44.1 kHz until now.

Not closing this yet

The fault was reported as silent, and only the speed has been proven fixed. Every
result above is a stopwatch and a register - nobody has listened to 44.1 kHz, and the
soundbar has not been confirmed to lock it. Leaving this open until someone hears it.

Two smaller things left over:

  • Rates outside the two table entries still get the unbounded integer search, so a request
    that lands on a large N could still walk the VCO into the rail. Not reachable through the
    current DT; not fixed either.
  • The pattern words are still copied, not decoded - upstream says the same ("we don't know
    what the step and bottom register fields mean"). The fractional N values they imply,
    54.5664 and 61.44, are now confirmed at the two rates that matter and nowhere else.

Write-up: 51-audio-pll-vco-ceiling.md.

## Fixed: sigma-delta table for `pll-audio` Patch written, built, deployed and measured on the board. `patches/linux-clk-sun9i/0001-clk-sunxi-ng-sun9i-use-sigma-delta-modulation-for-pl.patch`. ```c static struct ccu_sdm_setting pll_audio_sdm_table[] = { { .rate = 45158400, .pattern = 0xc00121ff, .m = 29, .n = 54 }, { .rate = 49152000, .pattern = 0xc000e147, .m = 30, .n = 61 }, }; ``` plus `.sdm = _SUNXI_CCU_SDM(pll_audio_sdm_table, BIT(24), 0x108, BIT(31))` and `CCU_FEATURE_SIGMA_DELTA_MOD`. The table's `m`/`n` are calculation values and the `m` field carries offset 1, so `m = 29` writes `P = 28` - the vendor's exact register values. `ccu_nm_set_rate()` needed nothing: `ccu_sdm_helper_has_rate()` short-circuits `ccu_nm_find_best()` and writes the table's factors and pattern directly, so the unbounded integer search never runs for these two rates. ### What the driver now writes ``` 44.1 kHz reg[008]=8100361c en=1 sdm=1 d1=0 d2=0 N=54 P=28 pat[108]=c00121ff locked=1 48 kHz reg[008]=81003d1d en=1 sdm=1 d1=0 d2=0 N=61 P=29 pat[108]=c000e147 locked=1 ``` VCO 1.296 and 1.464 GHz, against the ~3.14 GHz ceiling. Both families are now **exact**, where the integer search could only approximate: | | before | after | |---|---|---| | `pll-audio` at 44.1 kHz | 90 315 789 | **45 158 400** | | `i2s1` at 44.1 kHz | 22 578 948 | **22 579 200** | | `pll-audio` at 48 kHz | 122 880 000 | **49 152 000** | | `i2s1` at 48 kHz | 24 576 000 | **24 576 000** | ### Measured | | 15.000 s buffer | 60.000 s buffer | residual | |---|---|---|---| | HDMI 44.1 kHz | 15.112 s | 60.105 s | constant | | HDMI 48 kHz | 15.220 s | 60.225 s | constant | | S/PDIF 44.1 kHz | 15.097 s | - | | | S/PDIF 48 kHz | 15.220 s | - | | 44.1 kHz was **65.719 s** for a 60 s buffer before this. Two buffer lengths on purpose: the residual does not grow with duration, so it is fixed startup overhead and not a rate error - a rate error would have quadrupled between 15 s and 60 s. That is the argument from `49-audio-clock-wrong.md` run in reverse, to show there is nothing left rather than to pin what is there. **S/PDIF is fixed with it** (#42). It shares this PLL and picked the same N = 143, and had never been tested at 44.1 kHz until now. ### Not closing this yet The fault was reported as **silent**, and only the **speed** has been proven fixed. Every result above is a stopwatch and a register - nobody has listened to 44.1 kHz, and the soundbar has not been confirmed to lock it. Leaving this open until someone hears it. Two smaller things left over: - Rates outside the two table entries still get the unbounded integer search, so a request that lands on a large N could still walk the VCO into the rail. Not reachable through the current DT; not fixed either. - The pattern words are still copied, not decoded - upstream says the same ("we don't know what the step and bottom register fields mean"). The fractional N values they imply, 54.5664 and 61.44, are now confirmed at the two rates that matter and nowhere else. Write-up: `51-audio-pll-vco-ceiling.md`.
Author
Owner

Fixed and verified 2026-08-28. Root cause was not the d1/d2 dividers this issue
suspected - those measure 0, as the driver claims. ccu_nm_find_best() picks N = 143 for the
44.1 kHz family, asking the PLL's VCO for 3.432 GHz, and it saturates at about 3.14 GHz. The
48 kHz family only worked by luck: N = 128 is 3.072 GHz, just under the ceiling.

The fix is the ccu_sdm_setting sigma-delta table that a83t, a64, h3, r40, v3s and h6 all
have and sun9i never got. See 51-audio-pll-vco-ceiling.md.

Verified by the same timed-playback method that found it:

60.000 s at 44100 Hz -> 60.114 s     (was 65.719 s)
60.000 s at 48000 Hz -> 60.226 s

Both within the ~0.16 s fixed startup overhead. S/PDIF shares this PLL and is fixed with it,
though a bitstream on the pin is still unverified - that stays in #42.

**Fixed and verified 2026-08-28.** Root cause was not the `d1`/`d2` dividers this issue suspected - those measure 0, as the driver claims. `ccu_nm_find_best()` picks N = 143 for the 44.1 kHz family, asking the PLL's VCO for 3.432 GHz, and it saturates at about 3.14 GHz. The 48 kHz family only worked by luck: N = 128 is 3.072 GHz, just under the ceiling. The fix is the `ccu_sdm_setting` sigma-delta table that a83t, a64, h3, r40, v3s and h6 all have and sun9i never got. See `51-audio-pll-vco-ceiling.md`. Verified by the same timed-playback method that found it: ``` 60.000 s at 44100 Hz -> 60.114 s (was 65.719 s) 60.000 s at 48000 Hz -> 60.226 s ``` Both within the ~0.16 s fixed startup overhead. S/PDIF shares this PLL and is fixed with it, though a bitstream on the pin is still unverified - that stays in #42.
Sign in to join this conversation.
No description provided.