evidence · 30.6 KiB

mainline-audio-port.md

Committed at docs/research/mainline-audio-port.md. This is what grounds the requirements that cite it.

Open raw source

Forward-porting the K230 audio driver to mainline (task 5.1)

openspec/changes/the-mainline-shell-reaches-parity, task 5.1: “Forward-port sound/soc/canaan with the external-I2S-switch patch and owned audio/codec clocks; DT nodes from the vendor tree. Host proof: kernel builds; aplay -l lists the card in a board boot log.”

Update (task 5.x, board-proven): the generic dwc-i2s driver freezes the SoC

This doc’s original conclusion below — “no Canaan-specific I2S controller driver needs porting at all,” reusing mainline’s own sound/soc/dwc/dwc-i2s.c (compatible snps,designware-i2s) unmodified — is WRONG, found by an actual board boot: starting ALSA playback (speaker-test -D plughw:0,0) against that configuration freezes the whole SoC, even with clk_ignore_unused/pd_ignore_unused on the kernel command line (ruling out an unused-clock-gating hang, this project’s other recurring freeze cause). On the vendor 6.6 kernel, the identical command completes normally — the vendor kernel does not use the generic driver; it has its own fork, sound/soc/dwc_canaan/{canaan-dwc-i2s.c, canaan-dwc-pcm.c,canaan-local.h} (compatible canaan,snps,designware-i2s), which this doc’s original diff undersold as “only API-currency differences.”

Re-diffing both files directly (this project’s pinned mainline sound/soc/dwc/dwc-i2s.c vs. the vendor’s sound/soc/dwc_canaan/ canaan-dwc-i2s.c) finds real register-programming divergence, not just API currency:

Fix applied: forward-ported the vendor’s own sound/soc/dwc_canaan/ driver as nix/patches/mainline/dwc_canaan/{canaan-dwc-i2s.c, canaan-dwc-pcm.c,canaan-local.h,Kconfig,Makefile}, wired into nix/kernel-mainline.nix under its own CANAAN_SND_DESIGNWARE_I2S/ CANAAN_SND_DESIGNWARE_PCM Kconfig symbols (distinct from mainline’s own SND_DESIGNWARE_I2S/_PCM, left enabled since nothing on this board’s DT uses the generic compatible string anymore, so there is no bind conflict), and changed the i2s DT node’s compatible string in nix/dts/k230-tdisplay-mainline.dts from snps,designware-i2s to canaan,snps,designware-i2s. Every other property on that DT node (reg, dmas/dma-names, clocks/clock-names) is unchanged — checked directly against the vendor’s own k230.dtsi i2s node, which carries no other canaan,-prefixed property this driver reads.

API-currency changes made porting the vendor file to v7.3-rc5 (see nix/patches/mainline/dwc_canaan/canaan-dwc-i2s.c’s own header comment for the full list, kept in sync): .remove_new -> .remove (already void-returning in the vendor source, only the struct field name moved), asoc_substream_to_rtd() -> snd_soc_substream_to_rtd(), asoc_rtd_to_cpu() -> snd_soc_rtd_to_cpu() (in canaan-dwc-pcm.c), SET_RUNTIME_PM_OPS -> RUNTIME_PM_OPS wrapped in pm_ptr(&dwc_pm_ops) on .driver.pm (and dw_i2s_runtime_suspend()/_resume() moved out from under #ifdef CONFIG_PM to stay unconditionally compiled, since the dev_pm_ops struct referencing them by name is itself unconditionally compiled now), devm_clk_get() + clk_prepare_enable() -> devm_clk_get_enabled() with the err_clk_disable probe-error-path label and the remove() function’s own clk_disable_unprepare() call both dropped (devm’s reverse-order cleanup replaces them), irq >= 0 -> explicit if (irq == -EPROBE_DEFER) return irq; then if (irq > 0) (mainline’s own currency fix, ported verbatim), and .pcm_construct -> .pcm_new in canaan-dwc-pcm.c’s component driver struct. One pre-existing vendor bug fixed in canaan-local.h (not an API-currency change): its CONFIG_CANAAN_SND_DESIGNWARE_PCM=n fallback declared unprefixed inline stubs (dw_pcm_push_tx/_pop_rx/_register) that don’t match what canaan-dwc-i2s.c actually calls (canaan_dw_pcm_push_tx/etc.) — a mismatch the vendor tree never hit because its own defconfig always turns the PCM symbol on, but one that fails this board’s build (which leaves it off, same reasoning as mainline’s own SND_DESIGNWARE_PCM below). Renamed the fallback stubs to match.

Verification performed (host-only; no board):

What landed (original task 5.1 port; still current for canaan/codecs)

Forward-ported, compiled clean (zero warnings) against this project’s pinned mainline v7.3-rc5 dev tree, and wired into nix/kernel-mainline.nix (Kconfig/Makefile postPatch + structuredExtraConfig):

SUPERSEDED by the “Update” section above: this section originally said mainline’s own sound/soc/dwc/{dwc-i2s.c,dwc-pcm.c} (Synopsys DesignWare I2S) was reused unmodified, with only API-currency differences from the vendor’s sound/soc/dwc_canaan/canaan-dwc-i2s.c, and that “no Canaan-specific I2S controller driver needs porting at all.” A board boot proved that wrong (playback freezes the SoC against the generic driver); see the “Update” section above for the real register-programming divergence and the forward-ported sound/soc/dwc_canaan/ fix. The DT nodes below now use canaan,snps,designware-i2s for the i2s node, not the generic snps,designware-i2s this paragraph originally described.

API changes made (6.6 vendor -> 7.3-rc5 mainline), confirmed against headers

Clocks claimed, and why (the AGENTS.md “claim every clock” lesson)

Checked directly against this project’s pinned mainline drivers/clk/clk-k230.c and include/dt-bindings/clock/canaan,k230-clk.h:

Clock IDValueParent in clk-k230.cClaimed by
K230_LS_AUDIO_DEV_RATE158ls_audio_dev_gatei2s node, clock-names i2sclk
K230_LS_AUDIO_DEV_GATE95pll0_div4(cascade-enabled by DEV_RATE above — not claimed directly)
K230_LS_AUDIO_APB_GATE85ls_apb_src_rateaudio node, clock-names apb (new)
K230_LS_CODEC_ADC_RATE156ls_codec_adc_gateinno_codec node, clock-names adc
K230_LS_CODEC_ADC_GATE93pll0_div4(cascade-enabled by ADC_RATE above)
K230_LS_CODEC_DAC_RATE157ls_codec_dac_gateinno_codec node, clock-names dac
K230_LS_CODEC_DAC_GATE94pll0_div4(cascade-enabled by DAC_RATE above)
K230_LS_CODEC_APB_GATE87ls_apb_src_rateinno_codec node, clock-names apb (new)
K230_LS_PDM_GATE/_RATE96/159—not claimed — PDM mic input path is not part of this board’s k230-audio-inno (I2S) configuration; left ungated-but-unclaimed deliberately, same as upstream leaves any unused peripheral. A future PDM-mic change must claim it.

The common clock framework enables a clock’s parent whenever the clock itself is prepared/enabled (clk_core_prepare/clk_core_enable walk the parent chain). Because K230_CLK_RATE_FORMAT(ls_audio_dev_rate, ..., &ls_audio_dev_gate.clk.hw) (and the matching ADC/DAC pairs) wire the RATE clock’s hardware parent directly to its GATE, a single devm_clk_get_enabled() call on the RATE clock (what the vendor code already did, unchanged, for i2sclk/adc/dac) already cascades to the GATE — confirmed by reading the macro invocations themselves, not assumed. The two APB gates (ls_audio_apb_gate, ls_codec_apb_gate) are parented from ls_apb_src_rate instead, a branch nothing else here claims, so each needed its own explicit, additional claim — added as devm_clk_get_optional_enabled() so a device tree written before this change (with no apb clock listed) still probes.

Resolution (2026-10-06): the PDMA controller is now forward-ported

The blocker below (no mainline DMA provider for compatible = "canaan,k230-pdma") is resolved by porting drivers/dma/k230_peridma.c from the pinned vendor tree as nix/patches/mainline/k230-peridma.c, wired into nix/kernel-mainline.nix (new drivers/dma/k230-peridma.c via postPatch, a K230_PERIDMA Kconfig entry inserted before DW_AXI_DMAC in drivers/dma/Kconfig, a Makefile line, and structuredExtraConfig.K230_PERIDMA = yes), with a &pdma DT node and dmas/dma-names on the i2s node added to nix/dts/k230-tdisplay-mainline.dts.

API changes from the vendor’s 6.6-era source, confirmed against this project’s pinned v7.3-rc5 headers (see the ported file’s own header comment for the same list, kept in sync):

Clock claimed: K230_SHRM_PDMA_AXI_GATE = 130 (include/dt-bindings/clock/canaan,k230-clk.h), claimed by the driver itself via devm_clk_get_enabled(&pdev->dev, NULL) (unnamed, single clock, matching the vendor’s own single-clock probe call) and separately listed on the DT node (clocks = <&sysclk K230_SHRM_PDMA_AXI_GATE>;, no clock-names needed for an unnamed devm_clk_get()/_enabled() call). Confirmed register-identical to the vendor’s own gate, not inferred:

SourceRegister offsetBitParent
Vendor k230_clock_provider.dtsi, pdma_aclk_gate nodeclk-gate-reg-offset = <0x5C>clk-gate-reg-bit-enable = <3>shrm_axim_clk_gate
Mainline drivers/clk/clk-k230.c, K230_CLK_GATE_FORMAT(shrm_pdma_axi_gate, K230_SHRM_PDMA_AXI_GATE, 0x5C, 3, 0, 0, ...)0x5C3shrm_axim_clk_gate (mainline’s own clock of that exact name, drivers/clk/clk-k230.c:1772: &shrm_pdma_axi_gate)

Same offset, same bit, same parent name — the mainline clock ID is the correct one for this gate, confirmed by reading both sources directly.

DT changes: a pdma: pdma@80804000 node added to nix/dts/k230-tdisplay-mainline.dts with reg, interrupts = <203 IRQ_TYPE_LEVEL_HIGH>;, #dma-cells = <4>;, dma-channels = <8>;, dma-requests = <35>;, and status = "okay"; — every field copied verbatim from the vendor k230.dtsi’s own pdma@0x80804000 node. The i2s node’s dmas = <&pdma 1 0xfff 0 0x14>, <&pdma 1 0xfff 0 0x15>; and dma-names = "tx", "rx"; are likewise copied verbatim from the vendor’s i2s@0x9140f000 node (args are <priority, dev_tout, dat_endian, dev_sel>; 0x14/0x15 are AUDIO_TX/AUDIO_RX in the driver’s own enum ch_peri_dev_sel_t). cpp+dtc validation (docs/evidence/mainline-audio-port/pdma-dtc-validate.json) decompiles the resulting .dtb and confirms both the clock-ID and dmas phandle args resolve to these exact numeric values, not merely that the syntax parses.

Verification performed (host-only; no board, no full kernel build), same evidence classes as the rest of this task:

What remains: this task’s own host-proof criterion — aplay -l listing the card in a board boot log — requires a full kernel build and a board boot, neither performed by this worktree (board access, /dev/ttyACM0 and the nix build lock were explicitly out of scope). The K230_PERIDMA/audio Kconfig symbols have not been run through a real make oldconfig/defconfig pass against the full tree, only checked against the already-built pinned dev tree’s .config for the dependencies this entry needs (DMADEVICES/DMA_ENGINE/ DMA_VIRTUAL_CHANNELS/DMA_OF/OF/HAS_IOMEM, all already y). UNVERIFIED, same as the rest of this task’s clock claims: whether K230_SHRM_PDMA_AXI_GATE’s register-level programming actually gates the PDMA block correctly at runtime — read directly from both sources and found identical, not board-tested. UNVERIFIED: the PDMA controller’s actual descriptor-chain DMA behavior on real silicon (the pdma_llt_t/pdma_ch_cfg_t register programming itself, unchanged from the vendor source) — this port changed only API-currency surface, not the register-level logic, so a correct compile does not by itself prove correct hardware behavior.

The blocker (historical; now resolved above): no mainline DMA provider

The vendor i2s DT node (k230.dtsi, shared by every Canaan board in that tree, not just this one) is:

i2s: i2s@0x9140f000 {
	compatible = "canaan,snps,designware-i2s";
	reg = <0x0 0x9140f000 0x0 0x400>;
	dmas = <&pdma 1 0xfff 0 0x14>, <&pdma 1 0xfff 0 0x15>;
	dma-names = "tx", "rx";
	clocks = <&audio_dev_clk>;
	clock-names = "i2sclk";
};

It carries no interrupts property. In sound/soc/dwc/dwc-i2s.c’s dw_i2s_probe():

if (!pdata || dev->is_jh7110) {
	if (irq >= 0) {
		ret = dw_pcm_register(pdev);   /* PIO FIFO push/pop, no DMA */
		dev->use_pio = true;
		...
	} else {
		ret = devm_snd_dmaengine_pcm_register(&pdev->dev, NULL, 0);
		dev->use_pio = false;
	}

pdata is always NULL for a device-tree-probed platform device (it is only ever set by x86 board files), so this reduces to: with an IRQ, use PIO; without one, require a dmaengine-registered DMA channel. Since the only vendor DT reference for this IP has no interrupts property, the vendor’s own reference configuration takes the DMA branch, which calls (sound/soc/soc-generic-dmaengine-pcm.c, dmaengine_pcm_request_chan_of(), invoked synchronously from dw_i2s_probe() itself, i.e. at platform-driver probe time, not deferred to stream open):

chan = dma_request_chan(dev, name);   /* name = "tx" / "rx" */

This resolves the dmas = <&pdma ...> phandle via the kernel’s of_dma/dmaengine framework, which requires a registered DMA controller driver bound to the pdma node’s compatible = "canaan,k230-pdma". No such driver exists in mainline. Checked directly:

With no provider ever registered for that compatible string, dma_request_chan() cannot resolve the phandle to a channel. Checked against dmaengine_pcm_request_chan_of()’s own error handling: a probe deferral (-EPROBE_DEFER) is returned and retried forever only while there is some hope a provider will eventually register; here none ever will, so the i2s platform device never completes probe, the canaan,k230-audio-inno card’s cpu/platform DAI component is never registered, and devm_snd_soc_register_card() never completes either. aplay -l would not list the card — this task’s own host-proof criterion is not reachable without resolving this.

Why this DT does not instead add an interrupts property to force PIO

The PIO branch above is attractive — it needs no DMA engine at all, and mainline’s dw_pcm_register() is already compiled in by this change’s own structuredExtraConfig groundwork (SND_DESIGNWARE_I2S; SND_DESIGNWARE_PCM was left unset only because nothing can use it yet — see the Kconfig comment). But it requires knowing the real PLIC interrupt line this SoC wires to this specific DesignWare I2S instance, and:

What would unblock task 5.1’s remaining host proof (historical)

This section recorded two options at the time the DMA gap was first found; option 1 is now done (see “Resolution” above). Recorded here unedited for history, plus what’s now left:

  1. Port drivers/dma/k230_peridma.c as a mainline dmaengine driver. Done — see “Resolution” above, nix/patches/mainline/k230-peridma.c.
  2. Confirm the real PLIC IRQ number for this DesignWare I2S instance remains a live alternative/supplement nobody has pursued: it would let dw_i2s_probe() take the PIO branch instead of (or in addition to testing) the now-available dmaengine_pcm branch, but still requires a source this project has not located (SoC TRM, or reading PLIC registers on real hardware). Not needed now that the DMA provider exists, but would be a useful independent cross-check if a board boot shows the dmaengine_pcm path failing for an unrelated reason.

What’s left now is purely the board-side proof: a full kernel build (this worktree did not build one, as directed) and a board boot capturing aplay -l listing the canaan,k230-audio-inno card — this task’s own named host-proof criterion, still open.

Verification performed (host-only; no board, no full kernel build)

Remaining risks (not independently confirmed, flagged UNVERIFIED)