[RFC PATCH v2 0/5] Apple Macs machine/platform ASoC driver
Hi all,
This is again RFC with a machine-level ASoC driver for recent Apple Macs with the M1 line of chips. This time I attached the platform driver too for good measure. What I am interested in the most is checking the overall approach, especially on two points (both in some ways already discussed in previous RFC [0]):
- The way the platform/machine driver handles the fact that multiple I2S ports (now backend DAIs) can be driven by/connected to the same SERDES unit (now in effect a frontend DAI). After previous discussion I have transitioned to DPCM to model this. I took the opportunity of dynamic backend/frontend routing to support speakers/headphones runtime switching. More on this in comments at top of the machine and platform driver.
- The way the machine driver deactivates some of the controls where suitable, and limits volume on others. I added a new ASoC card method to that end.
Kind regards, Martin
[0] https://lore.kernel.org/linux-devicetree/20220331000449.41062-1-povik+lin@cu...
Martin Povišer (5): dt-bindings: sound: Add Apple MCA I2S transceiver dt-bindings: sound: Add Apple Macs sound peripherals ASoC: apple: Add MCA platform driver for Apple SoCs ASoC: Introduce 'fixup_controls' card method ASoC: apple: Add macaudio machine driver
.../bindings/sound/apple,macaudio.yaml | 157 +++ .../devicetree/bindings/sound/apple,mca.yaml | 102 ++ include/sound/soc-card.h | 1 + include/sound/soc.h | 1 + sound/soc/Kconfig | 1 + sound/soc/Makefile | 1 + sound/soc/apple/Kconfig | 25 + sound/soc/apple/Makefile | 5 + sound/soc/apple/macaudio.c | 1004 +++++++++++++++ sound/soc/apple/mca.c | 1122 +++++++++++++++++ sound/soc/soc-card.c | 6 + sound/soc/soc-core.c | 1 + 12 files changed, 2426 insertions(+) create mode 100644 Documentation/devicetree/bindings/sound/apple,macaudio.yaml create mode 100644 Documentation/devicetree/bindings/sound/apple,mca.yaml create mode 100644 sound/soc/apple/Kconfig create mode 100644 sound/soc/apple/Makefile create mode 100644 sound/soc/apple/macaudio.c create mode 100644 sound/soc/apple/mca.c
Add binding schema for MCA I2S transceiver found on Apple M1 and other chips.
Signed-off-by: Martin Povišer povik+lin@cutebit.org --- .../devicetree/bindings/sound/apple,mca.yaml | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 Documentation/devicetree/bindings/sound/apple,mca.yaml
diff --git a/Documentation/devicetree/bindings/sound/apple,mca.yaml b/Documentation/devicetree/bindings/sound/apple,mca.yaml new file mode 100644 index 000000000000..c8a36d8c38ad --- /dev/null +++ b/Documentation/devicetree/bindings/sound/apple,mca.yaml @@ -0,0 +1,102 @@ +# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/sound/apple,mca.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: Apple MCA I2S transceiver + +description: | + MCA is an I2S transceiver peripheral found on M1 and other Apple chips. It is + composed of a number of identical clusters which can operate independently + or in an interlinked fashion. Up to 6 clusters have been seen on an MCA. + +maintainers: + - Martin Povišer povik+lin@cutebit.org + +properties: + compatible: + items: + - enum: + - apple,t8103-mca + - apple,t6000-mca + - const: apple,mca + + reg: + minItems: 2 + maxItems: 2 + + interrupts: + maxItems: 6 + description: | + One interrupt per each cluster + + "#address-cells": + const: 1 + + "#size-cells": + const: 0 + + dmas: + minItems: 16 + maxItems: 24 + description: | + DMA channels associated to the SERDES units within the peripheral. They + are listed in groups of four per cluster, and within the cluster they are + given in order TXA, RXA, TXB, RXB of the respective SERDES units. + + dma-names: + minItems: 16 + maxItems: 24 + description: | + Names for the DMA channels: 'tx'/'rx', then cluster number, then 'a'/'b' + based on the associated SERDES unit. + + clocks: + minItems: 4 + maxItems: 6 + description: | + Clusters' input reference clock. + + power-domains: + minItems: 5 + maxItems: 7 + description: | + First the overall power domain for register access, then the power + domains of individual clusters for their operation. + + "#sound-dai-cells": + const: 1 + +required: + - compatible + - reg + - dmas + - dma-names + - clocks + - power-domains + - '#sound-dai-cells' + +additionalProperties: false + +examples: + - | + mca: mca@9b600000 { + compatible = "apple,t6000-mca", "apple,mca"; + reg = <0x9b600000 0x10000>, + <0x9b500000 0x20000>; + + clocks = <&nco 0>, <&nco 1>, <&nco 2>, <&nco 3>; + power-domains = <&ps_audio_p>, <&ps_mca0>, <&ps_mca1>, + <&ps_mca2>, <&ps_mca3>; + dmas = <&admac 0>, <&admac 1>, <&admac 2>, <&admac 3>, + <&admac 4>, <&admac 5>, <&admac 6>, <&admac 7>, + <&admac 8>, <&admac 9>, <&admac 10>, <&admac 11>, + <&admac 12>, <&admac 13>, <&admac 14>, <&admac 15>; + dma-names = "tx0a", "rx0a", "tx0b", "rx0b", + "tx1a", "rx1a", "tx1b", "rx1b", + "tx2a", "rx2a", "tx2b", "rx2b", + "tx3a", "rx3a", "tx3b", "rx3b"; + + #sound-dai-cells = <1>; + };
On Mon, Jun 06, 2022 at 09:19:06PM +0200, Martin Povišer wrote:
- interrupts:
- maxItems: 6
- description: |
One interrupt per each cluster
Other properties are specified in a manner which implies that there's a minimum of 4 clusters, eg:
- clocks:
- minItems: 4
- maxItems: 6
- description: |
Clusters' input reference clock.
On Mon, Jun 06, 2022 at 09:19:06PM +0200, Martin Povišer wrote:
Add binding schema for MCA I2S transceiver found on Apple M1 and other chips.
Signed-off-by: Martin Povišer povik+lin@cutebit.org
.../devicetree/bindings/sound/apple,mca.yaml | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 Documentation/devicetree/bindings/sound/apple,mca.yaml
diff --git a/Documentation/devicetree/bindings/sound/apple,mca.yaml b/Documentation/devicetree/bindings/sound/apple,mca.yaml new file mode 100644 index 000000000000..c8a36d8c38ad --- /dev/null +++ b/Documentation/devicetree/bindings/sound/apple,mca.yaml @@ -0,0 +1,102 @@ +# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/sound/apple,mca.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml#
+title: Apple MCA I2S transceiver
+description: |
- MCA is an I2S transceiver peripheral found on M1 and other Apple chips. It is
- composed of a number of identical clusters which can operate independently
- or in an interlinked fashion. Up to 6 clusters have been seen on an MCA.
+maintainers:
- Martin Povišer povik+lin@cutebit.org
+properties:
- compatible:
- items:
- enum:
- apple,t8103-mca
- apple,t6000-mca
- const: apple,mca
- reg:
- minItems: 2
- maxItems: 2
Need to define what each entry is.
- interrupts:
- maxItems: 6
- description: |
One interrupt per each cluster
- "#address-cells":
- const: 1
- "#size-cells":
- const: 0
- dmas:
- minItems: 16
- maxItems: 24
- description: |
DMA channels associated to the SERDES units within the peripheral. They
are listed in groups of four per cluster, and within the cluster they are
given in order TXA, RXA, TXB, RXB of the respective SERDES units.
- dma-names:
- minItems: 16
- maxItems: 24
- description: |
Names for the DMA channels: 'tx'/'rx', then cluster number, then 'a'/'b'
based on the associated SERDES unit.
Express as a schema:
items: pattern: '^(tx|rx)[0-5][ab]$'
- clocks:
- minItems: 4
- maxItems: 6
- description: |
Clusters' input reference clock.
- power-domains:
- minItems: 5
- maxItems: 7
- description: |
First the overall power domain for register access, then the power
domains of individual clusters for their operation.
- "#sound-dai-cells":
- const: 1
+required:
- compatible
- reg
- dmas
- dma-names
- clocks
- power-domains
- '#sound-dai-cells'
+additionalProperties: false
+examples:
- |
- mca: mca@9b600000 {
compatible = "apple,t6000-mca", "apple,mca";
reg = <0x9b600000 0x10000>,
<0x9b500000 0x20000>;
clocks = <&nco 0>, <&nco 1>, <&nco 2>, <&nco 3>;
power-domains = <&ps_audio_p>, <&ps_mca0>, <&ps_mca1>,
<&ps_mca2>, <&ps_mca3>;
dmas = <&admac 0>, <&admac 1>, <&admac 2>, <&admac 3>,
<&admac 4>, <&admac 5>, <&admac 6>, <&admac 7>,
<&admac 8>, <&admac 9>, <&admac 10>, <&admac 11>,
<&admac 12>, <&admac 13>, <&admac 14>, <&admac 15>;
dma-names = "tx0a", "rx0a", "tx0b", "rx0b",
"tx1a", "rx1a", "tx1b", "rx1b",
"tx2a", "rx2a", "tx2b", "rx2b",
"tx3a", "rx3a", "tx3b", "rx3b";
#sound-dai-cells = <1>;
- };
-- 2.33.0
Add binding for Apple Silicon Macs' machine-level integration of sound peripherals.
Signed-off-by: Martin Povišer povik+lin@cutebit.org --- .../bindings/sound/apple,macaudio.yaml | 157 ++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 Documentation/devicetree/bindings/sound/apple,macaudio.yaml
diff --git a/Documentation/devicetree/bindings/sound/apple,macaudio.yaml b/Documentation/devicetree/bindings/sound/apple,macaudio.yaml new file mode 100644 index 000000000000..f7c12697beab --- /dev/null +++ b/Documentation/devicetree/bindings/sound/apple,macaudio.yaml @@ -0,0 +1,157 @@ +# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/sound/apple,macaudio.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: Apple Silicon Macs integrated sound peripherals + +description: | + This binding represents the overall machine-level integration of sound + peripherals on 'Apple Silicon' machines by Apple. + +maintainers: + - Martin Povišer povik+lin@cutebit.org + +properties: + compatible: + items: + - enum: + - apple,j274-macaudio + - apple,j293-macaudio + - apple,j314-macaudio + - const: apple,macaudio + + "#address-cells": + const: 1 + + "#size-cells": + const: 0 + + model: + description: | + Model name for presentation to users + $ref: /schemas/types.yaml#/definitions/string + +patternProperties: + "^dai-link(@[0-9a-f]+)?$": + description: | + Node for each sound peripheral such as the speaker array, headphones jack, + or microphone. + type: object + properties: + reg: + maxItems: 1 + + link-name: + description: | + Name for the peripheral, expecting 'Speaker' or 'Speakers' if this is + the speaker array. + $ref: /schemas/types.yaml#/definitions/string + + cpu: + type: object + properties: + sound-dai: + description: | + DAI list with CPU-side I2S ports involved in this peripheral. + minItems: 1 + maxItems: 2 + required: + - sound-dai + + codec: + type: object + properties: + sound-dai: + description: | + DAI list with the CODEC-side DAIs connected to the above CPU-side + DAIs and involved in this sound peripheral. + + The list is in left/right order if applicable. If there are more + than one CPU-side DAIs (there can be two), the CODECs must be + listed first those connected to the first CPU, then those + connected to the second. + + In addition, on some machines with many speaker codecs, the CODECs + are listed in this fixed order: + + J293: Left Front, Left Rear, Right Front, Right Rear + J314: Left Woofer 1, Left Tweeter, Left Woofer 2, + Right Woofer 1, Right Tweeter, Right Woofer 2 + minItems: 1 + maxItems: 8 + required: + - sound-dai + + required: + - reg + - cpu + - codec + + additionalProperties: false + +required: + - compatible + - model + +additionalProperties: false + +examples: + - | + mca: mca@9b600000 { + compatible = "apple,t6000-mca", "apple,mca"; + reg = <0x9b600000 0x10000>, + <0x9b500000 0x20000>; + + clocks = <&nco 0>, <&nco 1>, <&nco 2>, <&nco 3>; + power-domains = <&ps_audio_p>, <&ps_mca0>, <&ps_mca1>, + <&ps_mca2>, <&ps_mca3>; + dmas = <&admac 0>, <&admac 1>, <&admac 2>, <&admac 3>, + <&admac 4>, <&admac 5>, <&admac 6>, <&admac 7>, + <&admac 8>, <&admac 9>, <&admac 10>, <&admac 11>, + <&admac 12>, <&admac 13>, <&admac 14>, <&admac 15>; + dma-names = "tx0a", "rx0a", "tx0b", "rx0b", + "tx1a", "rx1a", "tx1b", "rx1b", + "tx2a", "rx2a", "tx2b", "rx2b", + "tx3a", "rx3a", "tx3b", "rx3b"; + + #sound-dai-cells = <1>; + }; + + sound { + compatible = "apple,j314-macaudio", "apple,macaudio"; + model = "MacBook Pro J314 integrated audio"; + + #address-cells = <1>; + #size-cells = <0>; + + dai-link@0 { + reg = <0>; + link-name = "Speakers"; + + cpu { + sound-dai = <&mca 0>, <&mca 1>; + }; + codec { + sound-dai = <&speaker_left_woof1>, + <&speaker_left_tweet>, + <&speaker_left_woof2>, + <&speaker_right_woof1>, + <&speaker_right_tweet>, + <&speaker_right_woof2>; + }; + }; + + dai-link@1 { + reg = <1>; + link-name = "Headphones Jack"; + + cpu { + sound-dai = <&mca 2>; + }; + codec { + sound-dai = <&jack_codec>; + }; + }; + };
On Mon, Jun 06, 2022 at 09:19:07PM +0200, Martin Povišer wrote:
dai-link@1 {
reg = <1>;
link-name = "Headphones Jack";
Incredibly trivial bikeshed but normally that'd be "Headphone Jack" (singular).
On Mon, Jun 06, 2022 at 09:19:07PM +0200, Martin Povišer wrote:
Add binding for Apple Silicon Macs' machine-level integration of sound peripherals.
Signed-off-by: Martin Povišer povik+lin@cutebit.org
.../bindings/sound/apple,macaudio.yaml | 157 ++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 Documentation/devicetree/bindings/sound/apple,macaudio.yaml
diff --git a/Documentation/devicetree/bindings/sound/apple,macaudio.yaml b/Documentation/devicetree/bindings/sound/apple,macaudio.yaml new file mode 100644 index 000000000000..f7c12697beab --- /dev/null +++ b/Documentation/devicetree/bindings/sound/apple,macaudio.yaml @@ -0,0 +1,157 @@ +# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/sound/apple,macaudio.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml#
+title: Apple Silicon Macs integrated sound peripherals
+description: |
- This binding represents the overall machine-level integration of sound
- peripherals on 'Apple Silicon' machines by Apple.
+maintainers:
- Martin Povišer povik+lin@cutebit.org
+properties:
- compatible:
- items:
- enum:
- apple,j274-macaudio
- apple,j293-macaudio
- apple,j314-macaudio
- const: apple,macaudio
- "#address-cells":
- const: 1
- "#size-cells":
- const: 0
- model:
- description: |
Don't need '|' if there's no formatting to preserve.
Model name for presentation to users
- $ref: /schemas/types.yaml#/definitions/string
+patternProperties:
- "^dai-link(@[0-9a-f]+)?$":
- description: |
Node for each sound peripheral such as the speaker array, headphones jack,
or microphone.
- type: object
blank line
- properties:
reg:
maxItems: 1
link-name:
description: |
Name for the peripheral, expecting 'Speaker' or 'Speakers' if this is
the speaker array.
$ref: /schemas/types.yaml#/definitions/string
cpu:
type: object
additionalProperties: false
blank line before properties.
properties:
sound-dai:
description: |
DAI list with CPU-side I2S ports involved in this peripheral.
minItems: 1
maxItems: 2
blank line
required:
- sound-dai
codec:
type: object
blank line
properties:
sound-dai:
description: |
DAI list with the CODEC-side DAIs connected to the above CPU-side
DAIs and involved in this sound peripheral.
The list is in left/right order if applicable. If there are more
than one CPU-side DAIs (there can be two), the CODECs must be
listed first those connected to the first CPU, then those
connected to the second.
In addition, on some machines with many speaker codecs, the CODECs
are listed in this fixed order:
J293: Left Front, Left Rear, Right Front, Right Rear
J314: Left Woofer 1, Left Tweeter, Left Woofer 2,
Right Woofer 1, Right Tweeter, Right Woofer 2
minItems: 1
maxItems: 8
blank line
required:
- sound-dai
- required:
- reg
- cpu
- codec
- additionalProperties: false
I prefer this to be above 'properties' in the indented cases.
+required:
- compatible
- model
+additionalProperties: false
+examples:
- |
- mca: mca@9b600000 {
compatible = "apple,t6000-mca", "apple,mca";
reg = <0x9b600000 0x10000>,
<0x9b500000 0x20000>;
clocks = <&nco 0>, <&nco 1>, <&nco 2>, <&nco 3>;
power-domains = <&ps_audio_p>, <&ps_mca0>, <&ps_mca1>,
<&ps_mca2>, <&ps_mca3>;
dmas = <&admac 0>, <&admac 1>, <&admac 2>, <&admac 3>,
<&admac 4>, <&admac 5>, <&admac 6>, <&admac 7>,
<&admac 8>, <&admac 9>, <&admac 10>, <&admac 11>,
<&admac 12>, <&admac 13>, <&admac 14>, <&admac 15>;
dma-names = "tx0a", "rx0a", "tx0b", "rx0b",
"tx1a", "rx1a", "tx1b", "rx1b",
"tx2a", "rx2a", "tx2b", "rx2b",
"tx3a", "rx3a", "tx3b", "rx3b";
#sound-dai-cells = <1>;
- };
- sound {
compatible = "apple,j314-macaudio", "apple,macaudio";
model = "MacBook Pro J314 integrated audio";
#address-cells = <1>;
#size-cells = <0>;
dai-link@0 {
reg = <0>;
link-name = "Speakers";
cpu {
sound-dai = <&mca 0>, <&mca 1>;
};
codec {
sound-dai = <&speaker_left_woof1>,
<&speaker_left_tweet>,
<&speaker_left_woof2>,
<&speaker_right_woof1>,
<&speaker_right_tweet>,
<&speaker_right_woof2>;
};
};
dai-link@1 {
reg = <1>;
link-name = "Headphones Jack";
cpu {
sound-dai = <&mca 2>;
};
codec {
sound-dai = <&jack_codec>;
};
};
- };
-- 2.33.0
Add ASoC platform driver for the MCA peripheral found on Apple M1 and other chips.
Signed-off-by: Martin Povišer povik+lin@cutebit.org --- sound/soc/Kconfig | 1 + sound/soc/Makefile | 1 + sound/soc/apple/Kconfig | 9 + sound/soc/apple/Makefile | 3 + sound/soc/apple/mca.c | 1122 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 1136 insertions(+) create mode 100644 sound/soc/apple/Kconfig create mode 100644 sound/soc/apple/Makefile create mode 100644 sound/soc/apple/mca.c
diff --git a/sound/soc/Kconfig b/sound/soc/Kconfig index 5dcf77af07af..aec82db3114b 100644 --- a/sound/soc/Kconfig +++ b/sound/soc/Kconfig @@ -61,6 +61,7 @@ config SND_SOC_ACPI # All the supported SoCs source "sound/soc/adi/Kconfig" source "sound/soc/amd/Kconfig" +source "sound/soc/apple/Kconfig" source "sound/soc/atmel/Kconfig" source "sound/soc/au1x/Kconfig" source "sound/soc/bcm/Kconfig" diff --git a/sound/soc/Makefile b/sound/soc/Makefile index a7b37c06dc43..706dfbc28a6d 100644 --- a/sound/soc/Makefile +++ b/sound/soc/Makefile @@ -29,6 +29,7 @@ obj-$(CONFIG_SND_SOC_ACPI) += snd-soc-acpi.o obj-$(CONFIG_SND_SOC) += snd-soc-core.o obj-$(CONFIG_SND_SOC) += codecs/ obj-$(CONFIG_SND_SOC) += generic/ +obj-$(CONFIG_SND_SOC) += apple/ obj-$(CONFIG_SND_SOC) += adi/ obj-$(CONFIG_SND_SOC) += amd/ obj-$(CONFIG_SND_SOC) += atmel/ diff --git a/sound/soc/apple/Kconfig b/sound/soc/apple/Kconfig new file mode 100644 index 000000000000..0ba955657e98 --- /dev/null +++ b/sound/soc/apple/Kconfig @@ -0,0 +1,9 @@ +config SND_SOC_APPLE_MCA + tristate "Apple Silicon MCA driver" + depends on ARCH_APPLE || COMPILE_TEST + select SND_DMAENGINE_PCM + select COMMON_CLK + default ARCH_APPLE + help + This option enables an ASoC platform driver for MCA peripherals found + on Apple Silicon SoCs. diff --git a/sound/soc/apple/Makefile b/sound/soc/apple/Makefile new file mode 100644 index 000000000000..7a30bf452817 --- /dev/null +++ b/sound/soc/apple/Makefile @@ -0,0 +1,3 @@ +snd-soc-apple-mca-objs := mca.o + +obj-$(CONFIG_SND_SOC_APPLE_MCA) += snd-soc-apple-mca.o diff --git a/sound/soc/apple/mca.c b/sound/soc/apple/mca.c new file mode 100644 index 000000000000..c53c9367efa5 --- /dev/null +++ b/sound/soc/apple/mca.c @@ -0,0 +1,1122 @@ +/* + * Apple SoCs MCA driver + * + * Copyright (C) The Asahi Linux Contributors + * + * The MCA peripheral is made up of a number of identical units called clusters. + * Each cluster has its separate clock parent, SYNC signal generator, carries + * four SERDES units and has a dedicated I2S port on the SoC's periphery. + * + * The clusters can operate independently, or can be combined together in a + * configurable manner. We mostly treat them as self-contained independent + * units and don't configure any cross-cluster connections except for the I2S + * ports. The I2S ports can be routed to any of the clusters (irrespective + * of their native cluster). We map this onto ASoC's (DPCM) notion of backend + * and frontend DAIs. The 'cluster guts' are frontends which are dynamically + * routed to backend I2S ports. + * + * DAI references in devicetree are resolved as backends. The machine driver + * makes up some routing to the frontends in the DAPM paths it supplies, which + * can mostly be arbitrary. + */ + +#include <linux/clk.h> +#include <linux/dma-mapping.h> +#include <linux/init.h> +#include <linux/bitfield.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/of.h> +#include <linux/of_clk.h> +#include <linux/of_dma.h> +#include <linux/platform_device.h> +#include <linux/pm_domain.h> +#include <linux/regmap.h> +#include <linux/reset.h> +#include <linux/slab.h> + +#include <sound/core.h> +#include <sound/pcm.h> +#include <sound/pcm_params.h> +#include <sound/soc.h> +#include <sound/dmaengine_pcm.h> + +#define USE_RXB_FOR_CAPTURE + +/* relative to cluster base */ +#define REG_STATUS 0x0 +#define STATUS_MCLK_EN BIT(0) +#define REG_MCLK_CONF 0x4 +#define MCLK_CONF_DIV GENMASK(11, 8) + +#define REG_SYNCGEN_STATUS 0x100 +#define SYNCGEN_STATUS_EN BIT(0) +#define REG_SYNCGEN_MCLK_SEL 0x104 +#define SYNCGEN_MCLK_SEL GENMASK(3, 0) +#define REG_SYNCGEN_HI_PERIOD 0x108 +#define REG_SYNCGEN_LO_PERIOD 0x10c + +#define REG_PORT_ENABLES 0x600 +#define PORT_ENABLES_CLOCKS GENMASK(2, 1) +#define PORT_ENABLES_TX_DATA BIT(3) +#define REG_PORT_CLOCK_SEL 0x604 +#define PORT_CLOCK_SEL GENMASK(11, 8) +#define REG_PORT_DATA_SEL 0x608 +#define PORT_DATA_SEL_TXA(cl) (1 << ((cl)*2)) +#define PORT_DATA_SEL_TXB(cl) (2 << ((cl)*2)) + +#define REG_INTSTATE 0x700 +#define REG_INTMASK 0x704 + +/* bases of serdes units (relative to cluster) */ +#define CLUSTER_RXA_OFF 0x200 +#define CLUSTER_TXA_OFF 0x300 +#define CLUSTER_RXB_OFF 0x400 +#define CLUSTER_TXB_OFF 0x500 + +#define CLUSTER_TX_OFF CLUSTER_TXA_OFF + +#ifndef USE_RXB_FOR_CAPTURE +#define CLUSTER_RX_OFF CLUSTER_RXA_OFF +#else +#define CLUSTER_RX_OFF CLUSTER_RXB_OFF +#endif + +/* relative to serdes unit base */ +#define REG_SERDES_STATUS 0x00 +#define SERDES_STATUS_EN BIT(0) +#define SERDES_STATUS_RST BIT(1) +#define REG_TX_SERDES_CONF 0x04 +#define REG_RX_SERDES_CONF 0x08 +#define SERDES_CONF_NCHANS GENMASK(3, 0) +#define SERDES_CONF_WIDTH_MASK GENMASK(8, 4) +#define SERDES_CONF_WIDTH_16BIT 0x40 +#define SERDES_CONF_WIDTH_20BIT 0x80 +#define SERDES_CONF_WIDTH_24BIT 0xc0 +#define SERDES_CONF_WIDTH_32BIT 0x100 +#define SERDES_CONF_BCLK_POL 0x400 +#define SERDES_CONF_LSB_FIRST 0x800 +#define SERDES_CONF_UNK1 BIT(12) +#define SERDES_CONF_UNK2 BIT(13) +#define SERDES_CONF_UNK3 BIT(14) +#define SERDES_CONF_NO_DATA_FEEDBACK BIT(14) +#define SERDES_CONF_SYNC_SEL GENMASK(18, 16) +#define SERDES_CONF_SOME_RST BIT(19) +#define REG_TX_SERDES_BITSTART 0x08 +#define REG_RX_SERDES_BITSTART 0x0c +#define REG_TX_SERDES_SLOTMASK 0x0c +#define REG_RX_SERDES_SLOTMASK 0x10 +#define REG_RX_SERDES_PORT 0x04 + +/* relative to switch base */ +#define REG_DMA_ADAPTER_A(cl) (0x8000 * (cl)) +#define REG_DMA_ADAPTER_B(cl) (0x8000 * (cl) + 0x4000) +#define DMA_ADAPTER_TX_LSB_PAD GENMASK(4, 0) +#define DMA_ADAPTER_TX_NCHANS GENMASK(6, 5) +#define DMA_ADAPTER_RX_MSB_PAD GENMASK(12, 8) +#define DMA_ADAPTER_RX_NCHANS GENMASK(14, 13) +#define DMA_ADAPTER_NCHANS GENMASK(22, 20) + +#define SWITCH_STRIDE 0x8000 +#define CLUSTER_STRIDE 0x4000 + +#define MAX_NCLUSTERS 6 + +#define APPLE_MCA_FMTBITS (SNDRV_PCM_FMTBIT_S16_LE | \ + SNDRV_PCM_FMTBIT_S24_LE | \ + SNDRV_PCM_FMTBIT_S32_LE) + +struct mca_cluster { + int no; + __iomem void *base; + struct mca_data *host; + struct device *pd_dev; + struct clk *clk_parent; + struct dma_chan *dma_chans[SNDRV_PCM_STREAM_LAST + 1]; + + bool port_started[SNDRV_PCM_STREAM_LAST + 1]; + int port_driver; + bool clocks_in_use[SNDRV_PCM_STREAM_LAST + 1]; + struct device_link *pd_link; + + unsigned int set_sysclk; + + int tdm_slots; + int tdm_slot_width; + unsigned int tdm_tx_mask; + unsigned int tdm_rx_mask; +}; + +struct mca_data { + struct device *dev; + + __iomem void *switch_base; + + struct device *pd_dev; + struct device_link *pd_link; + + int nclusters; + struct mca_cluster clusters[]; +}; + +static void mca_modify(struct mca_cluster *cl, int regoffset, + u32 mask, u32 val) +{ + __iomem void *ptr = cl->base + regoffset; + u32 newval; + + newval = (val & mask) | (readl_relaxed(ptr) & ~mask); + writel_relaxed(newval, ptr); +} + +/* + * Get the cluster of FE or BE DAI + */ +static struct mca_cluster *mca_dai_to_cluster(struct snd_soc_dai *dai) +{ + struct mca_data *mca = snd_soc_dai_get_drvdata(dai); + /* + * FE DAIs are 0 ... nclusters - 1 + * BE DAIs are nclusters ... 2*nclusters - 1 + */ + int cluster_no = dai->id % mca->nclusters; + + return &mca->clusters[cluster_no]; +} + +/* called before PCM trigger */ +static void mca_fe_early_trigger(struct snd_pcm_substream *substream, + int cmd, struct snd_soc_dai *dai) +{ + struct mca_cluster *cl = mca_dai_to_cluster(dai); + bool is_tx = substream->stream == SNDRV_PCM_STREAM_PLAYBACK; + int serdes_unit = is_tx ? CLUSTER_TX_OFF : CLUSTER_RX_OFF; + int serdes_conf = serdes_unit + (is_tx ? REG_TX_SERDES_CONF : REG_RX_SERDES_CONF); + + switch (cmd) { + case SNDRV_PCM_TRIGGER_START: + case SNDRV_PCM_TRIGGER_RESUME: + case SNDRV_PCM_TRIGGER_PAUSE_RELEASE: + mca_modify(cl, serdes_unit + REG_SERDES_STATUS, + SERDES_STATUS_EN | SERDES_STATUS_RST, + SERDES_STATUS_RST); + mca_modify(cl, serdes_conf, + SERDES_CONF_SOME_RST, SERDES_CONF_SOME_RST); + (void) readl_relaxed(cl->base + serdes_conf); + mca_modify(cl, serdes_conf, + SERDES_STATUS_RST, 0); + WARN_ON(readl_relaxed(cl->base + REG_SERDES_STATUS) + & SERDES_STATUS_RST); + break; + default: + break; + } +} + +static int mca_fe_trigger(struct snd_pcm_substream *substream, + int cmd, struct snd_soc_dai *dai) +{ + struct mca_cluster *cl = mca_dai_to_cluster(dai); + bool is_tx = substream->stream == SNDRV_PCM_STREAM_PLAYBACK; + int serdes_unit = is_tx ? CLUSTER_TX_OFF : CLUSTER_RX_OFF; + + switch (cmd) { + case SNDRV_PCM_TRIGGER_START: + case SNDRV_PCM_TRIGGER_RESUME: + case SNDRV_PCM_TRIGGER_PAUSE_RELEASE: + mca_modify(cl, serdes_unit + REG_SERDES_STATUS, + SERDES_STATUS_EN | SERDES_STATUS_RST, + SERDES_STATUS_EN); + break; + + case SNDRV_PCM_TRIGGER_STOP: + case SNDRV_PCM_TRIGGER_SUSPEND: + case SNDRV_PCM_TRIGGER_PAUSE_PUSH: + mca_modify(cl, serdes_unit + REG_SERDES_STATUS, + SERDES_STATUS_EN, 0); + break; + + default: + return -EINVAL; + } + + return 0; +} + +static int mca_fe_enable_clocks(struct mca_cluster *cl) +{ + struct mca_data *mca = cl->host; + int ret; + + ret = clk_prepare_enable(cl->clk_parent); + if (ret) { + dev_err(mca->dev, "cluster %d: unable to enable clock parent: %d\n", + cl->no, ret); + return ret; + } + + cl->pd_link = device_link_add(mca->dev, cl->pd_dev, + DL_FLAG_STATELESS | DL_FLAG_PM_RUNTIME | + DL_FLAG_RPM_ACTIVE); + if (!cl->pd_link) { + dev_err(mca->dev, "cluster %d: unable to prop-up power domain\n", + cl->no); + clk_disable_unprepare(cl->clk_parent); + return -EINVAL; + } + + writel_relaxed(cl->no + 1, + cl->base + REG_SYNCGEN_MCLK_SEL); + mca_modify(cl, REG_SYNCGEN_STATUS, + SYNCGEN_STATUS_EN, SYNCGEN_STATUS_EN); + mca_modify(cl, REG_STATUS, + STATUS_MCLK_EN, STATUS_MCLK_EN); + + return 0; +} + +static void mca_fe_disable_clocks(struct mca_cluster *cl) +{ + mca_modify(cl, REG_SYNCGEN_STATUS, + SYNCGEN_STATUS_EN, 0); + mca_modify(cl, REG_STATUS, + STATUS_MCLK_EN, 0); + + device_link_del(cl->pd_link); + clk_disable_unprepare(cl->clk_parent); +} + +static bool mca_fe_clocks_in_use(struct mca_cluster *cl) +{ + struct mca_data *mca = cl->host; + struct mca_cluster *be_cl; + int stream, i; + + for (i = 0; i < mca->nclusters; i++) { + be_cl = &mca->clusters[i]; + + if (be_cl->port_driver != cl->no) + continue; + + for_each_pcm_streams(stream) + if (be_cl->clocks_in_use[stream]) + return true; + } + return false; +} + +static int mca_be_prepare(struct snd_pcm_substream *substream, + struct snd_soc_dai *dai) +{ + struct mca_cluster *cl = mca_dai_to_cluster(dai); + struct mca_data *mca = cl->host; + struct mca_cluster *fe_cl; + int ret; + + if (cl->port_driver < 0) + return -EINVAL; + + fe_cl = &mca->clusters[cl->port_driver]; + + /* + * Codecs require clocks at time of umute with the 'mute_stream' op. + * We need to enable them here at the latest (frontend prepare would + * be too late). + */ + if (!mca_fe_clocks_in_use(fe_cl)) { + ret = mca_fe_enable_clocks(fe_cl); + if (ret < 0) + return ret; + } + + cl->clocks_in_use[substream->stream] = true; + + return 0; +} + +static int mca_be_hw_free(struct snd_pcm_substream *substream, + struct snd_soc_dai *dai) +{ + struct mca_cluster *cl = mca_dai_to_cluster(dai); + struct mca_data *mca = cl->host; + struct mca_cluster *fe_cl; + + if (cl->port_driver < 0) + return -EINVAL; + + fe_cl = &mca->clusters[cl->port_driver]; + if (!mca_fe_clocks_in_use(fe_cl)) + return 0; /* Nothing to do */ + + cl->clocks_in_use[substream->stream] = false; + + if (!mca_fe_clocks_in_use(fe_cl)) + mca_fe_disable_clocks(fe_cl); + + return 0; +} + +static unsigned int mca_crop_mask(unsigned int mask, int nchans) +{ + while (hweight32(mask) > nchans) + mask &= ~(1 << __fls(mask)); + + return mask; +} + +static int mca_configure_serdes(struct mca_cluster *cl, int serdes_unit, + unsigned int mask, int slots, int nchans, int slot_width, + bool is_tx, int port) +{ + u32 serdes_conf, serdes_conf_mask; + + serdes_conf_mask = SERDES_CONF_WIDTH_MASK | SERDES_CONF_NCHANS; + serdes_conf = FIELD_PREP(SERDES_CONF_NCHANS, max(slots, 1) - 1); + switch (slot_width) { + case 16: + serdes_conf |= SERDES_CONF_WIDTH_16BIT; + break; + case 20: + serdes_conf |= SERDES_CONF_WIDTH_20BIT; + break; + case 24: + serdes_conf |= SERDES_CONF_WIDTH_24BIT; + break; + case 32: + serdes_conf |= SERDES_CONF_WIDTH_32BIT; + break; + default: + goto err; + } + + serdes_conf_mask |= SERDES_CONF_SYNC_SEL; + serdes_conf |= FIELD_PREP(SERDES_CONF_SYNC_SEL, cl->no + 1); + + if (is_tx) { + serdes_conf_mask |= SERDES_CONF_UNK1 | SERDES_CONF_UNK2 | SERDES_CONF_UNK3; + serdes_conf |= SERDES_CONF_UNK1 | SERDES_CONF_UNK2 | SERDES_CONF_UNK3; + } else { + + serdes_conf_mask |= SERDES_CONF_UNK1 | SERDES_CONF_UNK2 | SERDES_CONF_UNK3 + | SERDES_CONF_NO_DATA_FEEDBACK; + serdes_conf |= SERDES_CONF_UNK1 | SERDES_CONF_UNK2 + | SERDES_CONF_NO_DATA_FEEDBACK; + } + + mca_modify(cl, serdes_unit + (is_tx ? REG_TX_SERDES_CONF : REG_RX_SERDES_CONF), + serdes_conf_mask, serdes_conf); + + if (is_tx) { + writel_relaxed(0xffffffff, + cl->base + serdes_unit + REG_TX_SERDES_SLOTMASK); + writel_relaxed(~((u32) mca_crop_mask(mask, nchans)), + cl->base + serdes_unit + REG_TX_SERDES_SLOTMASK + 0x4); + writel_relaxed(0xffffffff, + cl->base + serdes_unit + REG_TX_SERDES_SLOTMASK + 0x8); + writel_relaxed(~((u32) mask), + cl->base + serdes_unit + REG_TX_SERDES_SLOTMASK + 0xc); + } else { + writel_relaxed(0xffffffff, + cl->base + serdes_unit + REG_RX_SERDES_SLOTMASK); + writel_relaxed(~((u32) mask), + cl->base + serdes_unit + REG_RX_SERDES_SLOTMASK + 0x4); + writel_relaxed(1 << port, + cl->base + serdes_unit + REG_RX_SERDES_PORT); + } + + return 0; + +err: + dev_err(cl->host->dev, "unsupported SERDES configuration requested (mask=0x%x slots=%d slot_width=%d)\n", + mask, slots, slot_width); + return -EINVAL; +} + +static int mca_fe_set_tdm_slot(struct snd_soc_dai *dai, unsigned int tx_mask, + unsigned int rx_mask, int slots, int slot_width) +{ + struct mca_cluster *cl = mca_dai_to_cluster(dai); + + cl->tdm_slots = slots; + cl->tdm_slot_width = slot_width; + cl->tdm_tx_mask = tx_mask; + cl->tdm_rx_mask = rx_mask; + + return 0; +} + +static int mca_fe_set_fmt(struct snd_soc_dai *dai, + unsigned int fmt) +{ + struct mca_cluster *cl = mca_dai_to_cluster(dai); + struct mca_data *mca = cl->host; + bool fpol_inv = false; + u32 serdes_conf = 0; + u32 bitstart; + + if ((fmt & SND_SOC_DAIFMT_CLOCK_PROVIDER_MASK) != + SND_SOC_DAIFMT_CBC_CFC) + goto err; + + switch (fmt & SND_SOC_DAIFMT_FORMAT_MASK) { + case SND_SOC_DAIFMT_I2S: + fpol_inv = 0; + bitstart = 1; + break; + case SND_SOC_DAIFMT_LEFT_J: + fpol_inv = 1; + bitstart = 0; + break; + default: + goto err; + } + + switch (fmt & SND_SOC_DAIFMT_INV_MASK) { + case SND_SOC_DAIFMT_NB_IF: + case SND_SOC_DAIFMT_IB_IF: + fpol_inv ^= 1; + break; + } + + switch (fmt & SND_SOC_DAIFMT_INV_MASK) { + case SND_SOC_DAIFMT_NB_NF: + case SND_SOC_DAIFMT_NB_IF: + serdes_conf |= SERDES_CONF_BCLK_POL; + break; + } + + if (!fpol_inv) + goto err; + + mca_modify(cl, CLUSTER_TX_OFF + REG_TX_SERDES_CONF, + SERDES_CONF_BCLK_POL, + serdes_conf); + writel_relaxed(bitstart, + cl->base + CLUSTER_TX_OFF + REG_TX_SERDES_BITSTART); + mca_modify(cl, CLUSTER_RX_OFF + REG_RX_SERDES_CONF, + SERDES_CONF_BCLK_POL, + serdes_conf); + writel_relaxed(bitstart, + cl->base + CLUSTER_RX_OFF + REG_RX_SERDES_BITSTART); + + return 0; + +err: + dev_err(mca->dev, "unsupported DAI format (0x%x) requested\n", fmt); + return -EINVAL; +} + +static int mca_fe_set_sysclk(struct snd_soc_dai *dai, int clk_id, + unsigned int freq, int dir) +{ + struct mca_cluster *cl = mca_dai_to_cluster(dai); + int ret; + + if (freq == cl->set_sysclk) + return 0; + + if (mca_fe_clocks_in_use(cl)) + return -EBUSY; + + ret = clk_set_rate(cl->clk_parent, freq); + if (!ret) + cl->set_sysclk = freq; + return ret; +} + +static int mca_fe_hw_params(struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params, + struct snd_soc_dai *dai) +{ + struct mca_cluster *cl = mca_dai_to_cluster(dai); + struct mca_data *mca = cl->host; + struct device *dev = mca->dev; + unsigned int samp_rate = params_rate(params); + bool is_tx = substream->stream == SNDRV_PCM_STREAM_PLAYBACK; + bool refine_tdm = false; + unsigned long bclk_ratio; + unsigned int tdm_slots, tdm_slot_width, tdm_mask; + u32 regval, pad; + int ret, nchans_ceiled; + + if (!cl->tdm_slot_width) { + /* + * We were not given TDM settings from above, set initial + * guesses which will later be refined. + */ + tdm_slot_width = params_width(params); + tdm_slots = params_channels(params); + refine_tdm = true; + } else { + tdm_slot_width = cl->tdm_slot_width; + tdm_slots = cl->tdm_slots; + tdm_mask = is_tx ? cl->tdm_tx_mask : cl->tdm_rx_mask; + } + + if (cl->set_sysclk) + bclk_ratio = cl->set_sysclk / samp_rate; + else + bclk_ratio = tdm_slot_width * tdm_slots; + + if (refine_tdm) { + int nchannels = params_channels(params); + + if (nchannels > 2) { + dev_err(dev, "missing TDM for stream with over two channels\n"); + return -EINVAL; + } + + if ((bclk_ratio % nchannels) != 0) { + dev_err(dev, "BCLK ratio (%ld) not divisible by no of channels (%d)\n", + bclk_ratio, nchannels); + return -EINVAL; + } + + tdm_slot_width = bclk_ratio / nchannels; + + if (tdm_slot_width > 32 && nchannels == 1) + tdm_slot_width = 32; + + if (tdm_slot_width < params_width(params)) { + dev_err(dev, "TDM slots too narrow (tdm=%d params=%d)\n", + tdm_slot_width, params_width(params)); + return -EINVAL; + } + + tdm_mask = (1 << tdm_slots) - 1; + } + + ret = mca_configure_serdes(cl, is_tx ? CLUSTER_TX_OFF : CLUSTER_RX_OFF, + tdm_mask, tdm_slots, params_channels(params), + tdm_slot_width, is_tx, cl->no); + if (ret) + return ret; + + pad = 32 - params_width(params); + + /* + * TODO: Here the register semantics aren't clear. + */ + nchans_ceiled = min_t(int, params_channels(params), 4); + regval = FIELD_PREP(DMA_ADAPTER_NCHANS, nchans_ceiled) + | FIELD_PREP(DMA_ADAPTER_TX_NCHANS, 0x2) + | FIELD_PREP(DMA_ADAPTER_RX_NCHANS, 0x2) + | FIELD_PREP(DMA_ADAPTER_TX_LSB_PAD, pad) + | FIELD_PREP(DMA_ADAPTER_RX_MSB_PAD, pad); + +#ifndef USE_RXB_FOR_CAPTURE + writel_relaxed(regval, mca->switch_base + REG_DMA_ADAPTER_A(cl->no)); +#else + if (is_tx) + writel_relaxed(regval, mca->switch_base + REG_DMA_ADAPTER_A(cl->no)); + else + writel_relaxed(regval, mca->switch_base + REG_DMA_ADAPTER_B(cl->no)); +#endif + + if (!mca_fe_clocks_in_use(cl)) { + /* + * Set up FSYNC duty cycle as even as possible. + */ + writel_relaxed((bclk_ratio / 2) - 1, cl->base + REG_SYNCGEN_HI_PERIOD); + writel_relaxed(((bclk_ratio + 1) / 2) - 1, cl->base + REG_SYNCGEN_LO_PERIOD); + writel_relaxed(FIELD_PREP(MCLK_CONF_DIV, 0x1), cl->base + REG_MCLK_CONF); + + ret = clk_set_rate(cl->clk_parent, bclk_ratio * samp_rate); + if (ret) { + dev_err(mca->dev, "cluster %d: unable to set clock parent: %d\n", + cl->no, ret); + return ret; + } + } + + return 0; +} + + +static const struct snd_soc_dai_ops mca_fe_ops = { + .set_fmt = mca_fe_set_fmt, + .set_sysclk = mca_fe_set_sysclk, + .set_tdm_slot = mca_fe_set_tdm_slot, + .hw_params = mca_fe_hw_params, + .trigger = mca_fe_trigger, +}; + +static bool mca_be_started(struct mca_cluster *cl) +{ + int stream; + + for_each_pcm_streams(stream) + if (cl->port_started[stream]) + return true; + return false; +} + +static int mca_be_startup(struct snd_pcm_substream *substream, + struct snd_soc_dai *dai) +{ + struct snd_soc_pcm_runtime *be = asoc_substream_to_rtd(substream); + struct snd_soc_pcm_runtime *fe; + struct mca_cluster *cl = mca_dai_to_cluster(dai); + struct mca_cluster *fe_cl; + struct mca_data *mca = cl->host; + struct snd_soc_dpcm *dpcm; + + fe = NULL; + + for_each_dpcm_fe(be, substream->stream, dpcm) { + if (fe && dpcm->fe != fe) { + dev_err(mca->dev, "many FE per one BE unsupported\n"); + return -EINVAL; + } + + fe = dpcm->fe; + } + + if (!fe) + return -EINVAL; + + fe_cl = mca_dai_to_cluster(asoc_rtd_to_cpu(fe, 0)); + + if (mca_be_started(cl)) { + /* + * Port is already started in the other direction. + * Check it isn't driven from different cluster. + */ + if (fe_cl->no != cl->port_driver) + return -EINVAL; + + cl->port_started[substream->stream] = true; + return 0; + } + + writel_relaxed(PORT_ENABLES_CLOCKS | PORT_ENABLES_TX_DATA, + cl->base + REG_PORT_ENABLES); + writel_relaxed(FIELD_PREP(PORT_CLOCK_SEL, fe_cl->no + 1), + cl->base + REG_PORT_CLOCK_SEL); + writel_relaxed(PORT_DATA_SEL_TXA(fe_cl->no), + cl->base + REG_PORT_DATA_SEL); + cl->port_driver = fe_cl->no; + cl->port_started[substream->stream] = true; + + return 0; +} + +static void mca_be_shutdown(struct snd_pcm_substream *substream, + struct snd_soc_dai *dai) +{ + struct mca_cluster *cl = mca_dai_to_cluster(dai); + + cl->port_started[substream->stream] = false; + + if (!mca_be_started(cl)) { + /* + * Were we the last direction to shutdown? + * Turn off the lights. + */ + writel_relaxed(0, cl->base + REG_PORT_ENABLES); + writel_relaxed(0, cl->base + REG_PORT_DATA_SEL); + cl->port_driver = -1; + } +} + +static const struct snd_soc_dai_ops mca_be_ops = { + .prepare = mca_be_prepare, + .hw_free = mca_be_hw_free, + .startup = mca_be_startup, + .shutdown = mca_be_shutdown, +}; + +static int mca_set_runtime_hwparams(struct snd_soc_component *component, + struct snd_pcm_substream *substream, struct dma_chan *chan) +{ + struct device *dma_dev = chan->device->dev; + struct snd_dmaengine_dai_dma_data dma_data = {}; + int ret; + + struct snd_pcm_hardware hw; + + memset(&hw, 0, sizeof(hw)); + + hw.info = SNDRV_PCM_INFO_MMAP | SNDRV_PCM_INFO_MMAP_VALID | + SNDRV_PCM_INFO_INTERLEAVED; + hw.periods_min = 2; + hw.periods_max = UINT_MAX; + hw.period_bytes_min = 256; + hw.period_bytes_max = dma_get_max_seg_size(dma_dev); + hw.buffer_bytes_max = SIZE_MAX; + hw.fifo_size = 16; + + ret = snd_dmaengine_pcm_refine_runtime_hwparams(substream, + &dma_data, &hw, chan); + + if (ret) + return ret; + + return snd_soc_set_runtime_hwparams(substream, &hw); +} + +static int mca_pcm_open(struct snd_soc_component *component, + struct snd_pcm_substream *substream) +{ + struct snd_soc_pcm_runtime *rtd = asoc_substream_to_rtd(substream); + struct mca_cluster *cl = mca_dai_to_cluster(asoc_rtd_to_cpu(rtd, 0)); + struct dma_chan *chan = cl->dma_chans[substream->stream]; + int ret; + + if (rtd->dai_link->no_pcm) + return 0; + + ret = mca_set_runtime_hwparams(component, substream, chan); + if (ret) + return ret; + + return snd_dmaengine_pcm_open(substream, chan); +} + +static int mca_hw_params(struct snd_soc_component *component, + struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params) +{ + struct snd_soc_pcm_runtime *rtd = asoc_substream_to_rtd(substream); + struct dma_chan *chan = snd_dmaengine_pcm_get_chan(substream); + struct dma_slave_config slave_config; + int ret; + + if (rtd->dai_link->no_pcm) + return 0; + + memset(&slave_config, 0, sizeof(slave_config)); + ret = snd_hwparams_to_dma_slave_config(substream, params, &slave_config); + if (ret < 0) + return ret; + + if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) + slave_config.dst_port_window_size = min_t(u32, params_channels(params), 4); + else + slave_config.src_port_window_size = min_t(u32, params_channels(params), 4); + + return dmaengine_slave_config(chan, &slave_config); +} + +static int mca_close(struct snd_soc_component *component, + struct snd_pcm_substream *substream) +{ + struct snd_soc_pcm_runtime *rtd = asoc_substream_to_rtd(substream); + + if (rtd->dai_link->no_pcm) + return 0; + + return snd_dmaengine_pcm_close(substream); +} + +static int mca_trigger(struct snd_soc_component *component, + struct snd_pcm_substream *substream, int cmd) +{ + struct snd_soc_pcm_runtime *rtd = asoc_substream_to_rtd(substream); + + if (rtd->dai_link->no_pcm) + return 0; + + /* + * Before we do the PCM trigger proper, insert an opportunity + * to reset the frontend's SERDES. + */ + mca_fe_early_trigger(substream, cmd, asoc_rtd_to_cpu(rtd, 0)); + + return snd_dmaengine_pcm_trigger(substream, cmd); +} + +static snd_pcm_uframes_t mca_pointer(struct snd_soc_component *component, + struct snd_pcm_substream *substream) +{ + struct snd_soc_pcm_runtime *rtd = asoc_substream_to_rtd(substream); + + if (rtd->dai_link->no_pcm) + return -ENOTSUPP; + + return snd_dmaengine_pcm_pointer(substream); +} + +static int mca_pcm_new(struct snd_soc_component *component, + struct snd_soc_pcm_runtime *rtd) +{ + struct mca_cluster *cl = mca_dai_to_cluster(asoc_rtd_to_cpu(rtd, 0)); + unsigned int i; + + if (rtd->dai_link->no_pcm) + return 0; + + for_each_pcm_streams(i) { + struct snd_pcm_substream *substream = rtd->pcm->streams[i].substream; + struct dma_chan *chan = cl->dma_chans[i]; + + if (!substream) + continue; + + if (!chan) { + dev_err(component->dev, "missing DMA channel for stream %d on SERDES %d\n", + i, cl->no); + return -EINVAL; + } + + snd_pcm_set_managed_buffer(substream, SNDRV_DMA_TYPE_DEV_IRAM, + chan->device->dev, 512*1024*6, + SIZE_MAX); + } + + return 0; +} + +static const struct snd_soc_component_driver mca_component = { + .name = "apple-mca", + .open = mca_pcm_open, + .close = mca_close, + .hw_params = mca_hw_params, + .trigger = mca_trigger, + .pointer = mca_pointer, + .pcm_construct = mca_pcm_new, +}; + +static void apple_mca_release(struct mca_data *mca) +{ + int i, stream; + + for (i = 0; i < mca->nclusters; i++) { + struct mca_cluster *cl = &mca->clusters[i]; + + for_each_pcm_streams(stream) { + if (IS_ERR_OR_NULL(cl->dma_chans[stream])) + continue; + + dma_release_channel(cl->dma_chans[stream]); + } + + if (!IS_ERR_OR_NULL(cl->clk_parent)) + clk_put(cl->clk_parent); + + if (!IS_ERR_OR_NULL(cl->pd_dev)) + dev_pm_domain_detach(cl->pd_dev, true); + } + + if (mca->pd_link) + device_link_del(mca->pd_link); + + if (!IS_ERR_OR_NULL(mca->pd_dev)) + dev_pm_domain_detach(mca->pd_dev, true); +} + +static int apple_mca_probe(struct platform_device *pdev) +{ + struct mca_data *mca; + struct mca_cluster *clusters; + struct snd_soc_dai_driver *dai_drivers; + struct resource *res; + void __iomem *base; + int nclusters; + int ret, i; + + base = devm_platform_get_and_ioremap_resource(pdev, 0, &res); + if (IS_ERR(base)) + return PTR_ERR(base); + + if (resource_size(res) < CLUSTER_STRIDE) + return -EINVAL; + nclusters = (resource_size(res) - CLUSTER_STRIDE) / CLUSTER_STRIDE + 1; + + mca = devm_kzalloc(&pdev->dev, struct_size(mca, clusters, nclusters), + GFP_KERNEL); + if (!mca) + return -ENOMEM; + mca->dev = &pdev->dev; + mca->nclusters = nclusters; + platform_set_drvdata(pdev, mca); + clusters = mca->clusters; + + mca->switch_base = devm_platform_ioremap_resource_byname(pdev, "switch"); + if (IS_ERR(mca->switch_base)) + return PTR_ERR(mca->switch_base); + + { + /* + * TODO: The only reset domain which seems to have an effect is + * AUDIO_P, which is shared with ADMAC (at minimum) and so can only + * be triggered non-exclusively. + */ + struct reset_control *rst; + + rst = of_reset_control_array_get(pdev->dev.of_node, true, true, false); + if (IS_ERR(rst)) { + dev_err(&pdev->dev, "unable to obtain reset control: %ld\n", + PTR_ERR(rst)); + } else if (rst) { + reset_control_reset(rst); + reset_control_put(rst); + } + } + + dai_drivers = devm_kzalloc(&pdev->dev, sizeof(*dai_drivers) * 2 * nclusters, + GFP_KERNEL); + if (!dai_drivers) + return -ENOMEM; + + mca->pd_dev = dev_pm_domain_attach_by_id(&pdev->dev, 0); + if (IS_ERR(mca->pd_dev)) + return -EINVAL; + + /* + * TODO: need cleanup? + */ + mca->pd_link = device_link_add(&pdev->dev, mca->pd_dev, + DL_FLAG_STATELESS | DL_FLAG_PM_RUNTIME | + DL_FLAG_RPM_ACTIVE); + if (!mca->pd_link) { + ret = -EINVAL; + goto err_release; + } + + for (i = 0; i < nclusters; i++) { + struct mca_cluster *cl = &clusters[i]; + struct snd_soc_dai_driver *fe = &dai_drivers[mca->nclusters + i]; + struct snd_soc_dai_driver *be = &dai_drivers[i]; + int stream; + + cl->host = mca; + cl->no = i; + cl->base = base + CLUSTER_STRIDE * i; + cl->port_driver = -1; + cl->clk_parent = of_clk_get(pdev->dev.of_node, i); + if (IS_ERR(cl->clk_parent)) { + dev_err(&pdev->dev, "unable to obtain clock %d: %ld\n", + i, PTR_ERR(cl->clk_parent)); + ret = PTR_ERR(cl->clk_parent); + goto err_release; + } + cl->pd_dev = dev_pm_domain_attach_by_id(&pdev->dev, i + 1); + if (IS_ERR(cl->pd_dev)) { + dev_err(&pdev->dev, "unable to obtain cluster %d PD: %ld\n", + i, PTR_ERR(cl->pd_dev)); + ret = PTR_ERR(cl->pd_dev); + goto err_release; + } + + for_each_pcm_streams(stream) { + struct dma_chan *chan; + bool is_tx = (stream == SNDRV_PCM_STREAM_PLAYBACK); +#ifndef USE_RXB_FOR_CAPTURE + char *name = devm_kasprintf(&pdev->dev, GFP_KERNEL, + is_tx ? "tx%da" : "rx%da", i); +#else + char *name = devm_kasprintf(&pdev->dev, GFP_KERNEL, + is_tx ? "tx%da" : "rx%db", i); +#endif + + chan = of_dma_request_slave_channel(pdev->dev.of_node, name); + if (IS_ERR(chan)) { + if (PTR_ERR(chan) != -EPROBE_DEFER) + dev_err(&pdev->dev, "no %s DMA channel: %ld\n", + name, PTR_ERR(chan)); + + ret = PTR_ERR(chan); + goto err_release; + } + + cl->dma_chans[stream] = chan; + } + + fe->id = i; + fe->name = devm_kasprintf(&pdev->dev, GFP_KERNEL, + "mca-pcm-%d", i); + if (!fe->name) { + ret = -ENOMEM; + goto err_release; + } + fe->ops = &mca_fe_ops; + fe->playback.channels_min = 1; + fe->playback.channels_max = 32; + fe->playback.rates = SNDRV_PCM_RATE_8000_192000; + fe->playback.formats = APPLE_MCA_FMTBITS; + fe->capture.channels_min = 1; + fe->capture.channels_max = 32; + fe->capture.rates = SNDRV_PCM_RATE_8000_192000; + fe->capture.formats = APPLE_MCA_FMTBITS; + fe->symmetric_rate = 1; + + fe->playback.stream_name = devm_kasprintf(&pdev->dev, GFP_KERNEL, + "PCM%d TX", i); + fe->capture.stream_name = devm_kasprintf(&pdev->dev, GFP_KERNEL, + "PCM%d RX", i); + + if (!fe->playback.stream_name || !fe->capture.stream_name) { + ret = -ENOMEM; + goto err_release; + } + + be->id = i + nclusters; + be->name = devm_kasprintf(&pdev->dev, GFP_KERNEL, + "mca-i2s-%d", i); + if (!be->name) { + ret = -ENOMEM; + goto err_release; + } + be->ops = &mca_be_ops; + be->playback.channels_min = 1; + be->playback.channels_max = 32; + be->playback.rates = SNDRV_PCM_RATE_8000_192000; + be->playback.formats = APPLE_MCA_FMTBITS; + be->capture.channels_min = 1; + be->capture.channels_max = 32; + be->capture.rates = SNDRV_PCM_RATE_8000_192000; + be->capture.formats = APPLE_MCA_FMTBITS; + + be->playback.stream_name = devm_kasprintf(&pdev->dev, GFP_KERNEL, + "I2S%d TX", i); + be->capture.stream_name = devm_kasprintf(&pdev->dev, GFP_KERNEL, + "I2S%d RX", i); + if (!be->playback.stream_name || !be->capture.stream_name) { + ret = -ENOMEM; + goto err_release; + } + } + + ret = devm_snd_soc_register_component(&pdev->dev, &mca_component, + dai_drivers, nclusters * 2); + if (ret) { + dev_err(&pdev->dev, "unable to register ASoC component: %d\n", ret); + goto err_release; + } + + return 0; + +err_release: + apple_mca_release(mca); + return ret; +} + +static int apple_mca_remove(struct platform_device *pdev) +{ + struct mca_data *mca = platform_get_drvdata(pdev); + + apple_mca_release(mca); + return 0; +} + +static const struct of_device_id apple_mca_of_match[] = { + { .compatible = "apple,mca", }, + {} +}; +MODULE_DEVICE_TABLE(of, apple_mca_of_match); + +static struct platform_driver apple_mca_driver = { + .driver = { + .name = "apple-mca", + .owner = THIS_MODULE, + .of_match_table = apple_mca_of_match, + }, + .probe = apple_mca_probe, + .remove = apple_mca_remove, +}; +module_platform_driver(apple_mca_driver); + +MODULE_AUTHOR("Martin Povišer povik+lin@cutebit.org"); +MODULE_DESCRIPTION("ASoC platform driver for Apple Silicon SoCs"); +MODULE_LICENSE("GPL");
On Mon, Jun 06, 2022 at 09:19:08PM +0200, Martin Povišer wrote:
+++ b/sound/soc/apple/mca.c @@ -0,0 +1,1122 @@ +/*
- Apple SoCs MCA driver
Please add SPDX headers to all your files.
mca_modify(cl, serdes_conf,
SERDES_CONF_SOME_RST, SERDES_CONF_SOME_RST);
(void) readl_relaxed(cl->base + serdes_conf);
Please drop the cast, casts to/from void are generally a warning sign as they're unneeded in C. If you want to document the barrier use a comment or wrapper function.
- /*
* Codecs require clocks at time of umute with the 'mute_stream' op.
* We need to enable them here at the latest (frontend prepare would
* be too late).
*/
- if (!mca_fe_clocks_in_use(fe_cl)) {
ret = mca_fe_enable_clocks(fe_cl);
if (ret < 0)
return ret;
- }
This requirement is CODEC specific. It's fine to bodge around to satisfy it though, especially given the restricted set of platforms this can be used with.
- fe_cl = &mca->clusters[cl->port_driver];
- if (!mca_fe_clocks_in_use(fe_cl))
return 0; /* Nothing to do */
- cl->clocks_in_use[substream->stream] = false;
- if (!mca_fe_clocks_in_use(fe_cl))
mca_fe_disable_clocks(fe_cl);
Are you sure this doesn't need locking?
On 6. 6. 2022, at 22:17, Mark Brown broonie@kernel.org wrote:
On Mon, Jun 06, 2022 at 09:19:08PM +0200, Martin Povišer wrote:
+++ b/sound/soc/apple/mca.c @@ -0,0 +1,1122 @@ +/*
- Apple SoCs MCA driver
Please add SPDX headers to all your files.
mca_modify(cl, serdes_conf,
SERDES_CONF_SOME_RST, SERDES_CONF_SOME_RST);
(void) readl_relaxed(cl->base + serdes_conf);
Please drop the cast, casts to/from void are generally a warning sign as they're unneeded in C. If you want to document the barrier use a comment or wrapper function.
- /*
* Codecs require clocks at time of umute with the 'mute_stream' op.
* We need to enable them here at the latest (frontend prepare would
* be too late).
*/
- if (!mca_fe_clocks_in_use(fe_cl)) {
ret = mca_fe_enable_clocks(fe_cl);
if (ret < 0)
return ret;
- }
This requirement is CODEC specific. It's fine to bodge around to satisfy it though, especially given the restricted set of platforms this can be used with.
- fe_cl = &mca->clusters[cl->port_driver];
- if (!mca_fe_clocks_in_use(fe_cl))
return 0; /* Nothing to do */
- cl->clocks_in_use[substream->stream] = false;
- if (!mca_fe_clocks_in_use(fe_cl))
mca_fe_disable_clocks(fe_cl);
Are you sure this doesn't need locking?
I am not sure. I need to study what locking is already done by ALSA/ASoC. I assume the two stream directions here don’t share a lock already...
The new method is called just before the card is registered, providing an opportune time for machine-level drivers to do some final controls amending: deactivating individual controls or obtaining control references for later use.
Some controls can be created by DAPM after 'late_probe' has been called, hence the need for this new method.
Signed-off-by: Martin Povišer povik+lin@cutebit.org --- include/sound/soc-card.h | 1 + include/sound/soc.h | 1 + sound/soc/soc-card.c | 6 ++++++ sound/soc/soc-core.c | 1 + 4 files changed, 9 insertions(+)
diff --git a/include/sound/soc-card.h b/include/sound/soc-card.h index 4f2cc4fb56b7..fd8a6bd2bb4c 100644 --- a/include/sound/soc-card.h +++ b/include/sound/soc-card.h @@ -26,6 +26,7 @@ int snd_soc_card_resume_post(struct snd_soc_card *card);
int snd_soc_card_probe(struct snd_soc_card *card); int snd_soc_card_late_probe(struct snd_soc_card *card); +void snd_soc_card_fixup_controls(struct snd_soc_card *card); int snd_soc_card_remove(struct snd_soc_card *card);
int snd_soc_card_set_bias_level(struct snd_soc_card *card, diff --git a/include/sound/soc.h b/include/sound/soc.h index d3d3a26e8867..8be0258c74e9 100644 --- a/include/sound/soc.h +++ b/include/sound/soc.h @@ -895,6 +895,7 @@ struct snd_soc_card {
int (*probe)(struct snd_soc_card *card); int (*late_probe)(struct snd_soc_card *card); + void (*fixup_controls)(struct snd_soc_card *card); int (*remove)(struct snd_soc_card *card);
/* the pre and post PM functions are used to do any PM work before and diff --git a/sound/soc/soc-card.c b/sound/soc/soc-card.c index 41c586b86dc3..377e4f9eda91 100644 --- a/sound/soc/soc-card.c +++ b/sound/soc/soc-card.c @@ -167,6 +167,12 @@ int snd_soc_card_late_probe(struct snd_soc_card *card) return 0; }
+void snd_soc_card_fixup_controls(struct snd_soc_card *card) +{ + if (card->fixup_controls) + card->fixup_controls(card); +} + int snd_soc_card_remove(struct snd_soc_card *card) { int ret = 0; diff --git a/sound/soc/soc-core.c b/sound/soc/soc-core.c index aa687fd126db..ef4d9cb67bb2 100644 --- a/sound/soc/soc-core.c +++ b/sound/soc/soc-core.c @@ -2074,6 +2074,7 @@ static int snd_soc_bind_card(struct snd_soc_card *card) goto probe_end;
snd_soc_dapm_new_widgets(card); + snd_soc_card_fixup_controls(card);
ret = snd_card_register(card->snd_card); if (ret < 0) {
Signed-off-by: Martin Povišer povik+lin@cutebit.org --- sound/soc/apple/Kconfig | 16 + sound/soc/apple/Makefile | 2 + sound/soc/apple/macaudio.c | 1004 ++++++++++++++++++++++++++++++++++++ 3 files changed, 1022 insertions(+) create mode 100644 sound/soc/apple/macaudio.c
diff --git a/sound/soc/apple/Kconfig b/sound/soc/apple/Kconfig index 0ba955657e98..8db30569af9c 100644 --- a/sound/soc/apple/Kconfig +++ b/sound/soc/apple/Kconfig @@ -1,3 +1,19 @@ +config SND_SOC_APPLE_MACAUDIO + tristate "Audio support for Apple Silicon Macs" + depends on ARCH_APPLE || COMPILE_TEST + select SND_SOC_APPLE_MCA + select SND_SIMPLE_CARD_UTILS + select APPLE_ADMAC + select COMMON_CLK_APPLE_NCO + select SND_SOC_TAS2764 + select SND_SOC_TAS2770 + select SND_SOC_CS42L42 + default ARCH_APPLE + help + This option enables an ASoC machine-level driver for Apple Silicon Macs + and it also enables the required SoC and codec drivers for overall + sound support on these machines. + config SND_SOC_APPLE_MCA tristate "Apple Silicon MCA driver" depends on ARCH_APPLE || COMPILE_TEST diff --git a/sound/soc/apple/Makefile b/sound/soc/apple/Makefile index 7a30bf452817..3ffb19ed1d0a 100644 --- a/sound/soc/apple/Makefile +++ b/sound/soc/apple/Makefile @@ -1,3 +1,5 @@ snd-soc-apple-mca-objs := mca.o +snd-soc-macaudio-objs := macaudio.o
+obj-$(CONFIG_SND_SOC_APPLE_MACAUDIO) += snd-soc-macaudio.o obj-$(CONFIG_SND_SOC_APPLE_MCA) += snd-soc-apple-mca.o diff --git a/sound/soc/apple/macaudio.c b/sound/soc/apple/macaudio.c new file mode 100644 index 000000000000..24a7200e06f6 --- /dev/null +++ b/sound/soc/apple/macaudio.c @@ -0,0 +1,1004 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * ASoC machine driver for Apple Silicon Macs + * + * Copyright (C) The Asahi Linux Contributors + * + * Based on sound/soc/qcom/{sc7180.c|common.c} + * + * Copyright (c) 2018, Linaro Limited. + * Copyright (c) 2020, The Linux Foundation. All rights reserved. + * + * + * Virtual FE/BE Playback Topology + * ------------------------------- + * + * The platform driver has independent frontend and backend DAIs with the + * option of routing backends to any of the frontends. The platform + * driver configures the routing based on DPCM couplings in ASoC runtime + * structures, which in turn is determined from DAPM paths by ASoC. But the + * platform driver doesn't supply relevant DAPM paths and leaves that up for + * the machine driver to fill in. The filled-in virtual topology can be + * anything as long as a particular backend isn't connected to more than one + * frontend at any given time. (The limitation is due to the unsupported case + * of reparenting of live BEs.) + * + * The DAPM routing that this machine-level driver makes up has two use-cases + * in mind: + * + * - Using a single PCM for playback such that it conditionally sinks to either + * speakers or headphones based on the plug-in state of the headphones jack. + * All the while making the switch transparent to userspace. This has the + * drawback of requiring a sample stream suited for both speakers and + * headphones, which is hard to come by on machines where tailored DSP for + * speakers in userspace is desirable or required. + * + * - Driving the headphones and speakers from distinct PCMs, having userspace + * bridge the difference and apply different signal processing to the two. + * + * In the end the topology supplied by this driver looks like this: + * + * PCMs (frontends) I2S Port Groups (backends) + * ──────────────── ────────────────────────── + * + * ┌──────────┐ ┌───────────────► ┌─────┐ ┌──────────┐ + * │ Primary ├───────┤ │ Mux │ ──► │ Speakers │ + * └──────────┘ │ ┌──────────► └─────┘ └──────────┘ + * ┌─── │ ───┘ ▲ + * ┌──────────┐ │ │ │ + * │Secondary ├──┘ │ ┌────────────┴┐ + * └──────────┘ ├────►│Plug-in Demux│ + * │ └────────────┬┘ + * │ │ + * │ ▼ + * │ ┌─────┐ ┌──────────┐ + * └───────────────► │ Mux │ ──► │Headphones│ + * └─────┘ └──────────┘ + */ + +#include <linux/module.h> +#include <linux/of_device.h> +#include <linux/platform_device.h> +#include <sound/core.h> +#include <sound/jack.h> +#include <sound/pcm.h> +#include <sound/simple_card_utils.h> +#include <sound/soc.h> +#include <uapi/linux/input-event-codes.h> + +#define DRIVER_NAME "snd-soc-macaudio" + +/* + * CPU side is bit and frame clock provider + * I2S has both clocks inverted + */ +#define MACAUDIO_DAI_FMT (SND_SOC_DAIFMT_I2S | \ + SND_SOC_DAIFMT_CBC_CFC | \ + SND_SOC_DAIFMT_GATED | \ + SND_SOC_DAIFMT_IB_IF) +#define MACAUDIO_JACK_MASK (SND_JACK_HEADSET | SND_JACK_HEADPHONE) +#define MACAUDIO_SLOTWIDTH 32 + +struct macaudio_model_data { + bool deactive_asi1_sel; + int spk_amp_gain_max; +}; + +struct macaudio_snd_data { + struct snd_soc_card card; + struct snd_soc_jack_pin pin; + struct snd_soc_jack jack; + int jack_plugin_state; + struct snd_kcontrol *plugin_demux_kcontrol; + + struct macaudio_link_props { + /* frontend props */ + unsigned int mclk_fs; + + /* backend props */ + bool is_speakers; + bool is_headphones; + unsigned int tdm_mask; + } *link_props; + + unsigned int speaker_nchans_array[2]; + struct snd_pcm_hw_constraint_list speaker_nchans_list; + + struct macaudio_model_data *mdata; +}; + +static bool void_warranty; +module_param(void_warranty, bool, 0644); +MODULE_PARM_DESC(void_warranty, "Keep going even without speaker volume safety caps"); + +SND_SOC_DAILINK_DEFS(primary, + DAILINK_COMP_ARRAY(COMP_CPU("mca-pcm-0")), // CPU + DAILINK_COMP_ARRAY(COMP_DUMMY()), // CODEC + DAILINK_COMP_ARRAY(COMP_EMPTY())); // platform (filled at runtime) + +SND_SOC_DAILINK_DEFS(secondary, + DAILINK_COMP_ARRAY(COMP_CPU("mca-pcm-1")), // CPU + DAILINK_COMP_ARRAY(COMP_DUMMY()), // CODEC + DAILINK_COMP_ARRAY(COMP_EMPTY())); + +static struct snd_soc_dai_link macaudio_fe_links[] = { + { + .name = "Primary", + .stream_name = "Primary", + .dynamic = 1, + .dpcm_playback = 1, + .dpcm_capture = 1, + .dpcm_merged_rate = 1, + .dpcm_merged_chan = 1, + .dpcm_merged_format = 1, + .dai_fmt = MACAUDIO_DAI_FMT, + SND_SOC_DAILINK_REG(primary), + }, + { + .name = "Secondary", + .stream_name = "Secondary", + .dynamic = 1, + .dpcm_playback = 1, + .dpcm_merged_rate = 1, + .dpcm_merged_chan = 1, + .dpcm_merged_format = 1, + .dai_fmt = MACAUDIO_DAI_FMT, + SND_SOC_DAILINK_REG(secondary), + }, +}; + +static struct macaudio_link_props macaudio_fe_link_props[] = { + { + /* + * Primary FE + * + * The mclk/fs ratio at 64 for the primary frontend is important + * to ensure that the headphones codec's idea of left and right + * in a stereo stream over I2S fits in nicely with everyone else's. + * (This is until the headphones codec's driver supports + * set_tdm_slot.) + * + * The low mclk/fs ratio precludes transmitting more than two + * channels over I2S, but that's okay since there is the secondary + * FE for speaker arrays anyway. + */ + .mclk_fs = 64, + }, + { + /* + * Secondary FE + * + * Here we want frames plenty long to be able to drive all + * those fancy speaker arrays. + */ + .mclk_fs = 256, + } +}; + +static int macaudio_copy_link(struct device *dev, struct snd_soc_dai_link *target, + struct snd_soc_dai_link *source) +{ + memcpy(target, source, sizeof(struct snd_soc_dai_link)); + + target->cpus = devm_kcalloc(dev, target->num_cpus, + sizeof(*target->cpus), GFP_KERNEL); + target->codecs = devm_kcalloc(dev, target->num_codecs, + sizeof(*target->codecs), GFP_KERNEL); + target->platforms = devm_kcalloc(dev, target->num_platforms, + sizeof(*target->platforms), GFP_KERNEL); + + if (!target->cpus || !target->codecs || !target->platforms) + return -ENOMEM; + + memcpy(target->cpus, source->cpus, sizeof(*target->cpus) * target->num_cpus); + memcpy(target->codecs, source->codecs, sizeof(*target->codecs) * target->num_codecs); + memcpy(target->platforms, source->platforms, sizeof(*target->platforms) * target->num_platforms); + + return 0; +} + +static int macaudio_parse_of_component(struct device_node *node, int index, + struct snd_soc_dai_link_component *comp) +{ + struct of_phandle_args args; + int ret; + + ret = of_parse_phandle_with_args(node, "sound-dai", "#sound-dai-cells", + index, &args); + if (ret) + return ret; + comp->of_node = args.np; + return snd_soc_get_dai_name(&args, &comp->dai_name); +} + +/* + * Parse one DPCM backend from the devicetree. This means taking one + * of the CPU DAIs and combining it with one or more CODEC DAIs. + */ +static int macaudio_parse_of_be_dai_link(struct macaudio_snd_data *ma, + struct snd_soc_dai_link *link, + int be_index, int ncodecs_per_be, + struct device_node *cpu, + struct device_node *codec) +{ + struct snd_soc_dai_link_component *comp; + struct device *dev = ma->card.dev; + int codec_base = be_index * ncodecs_per_be; + int ret, i; + + link->no_pcm = 1; + link->dpcm_playback = 1; + link->dpcm_capture = 1; + + link->dai_fmt = MACAUDIO_DAI_FMT; + + link->num_codecs = ncodecs_per_be; + link->codecs = devm_kcalloc(dev, ncodecs_per_be, + sizeof(*comp), GFP_KERNEL); + link->num_cpus = 1; + link->cpus = devm_kzalloc(dev, sizeof(*comp), GFP_KERNEL); + + if (!link->codecs || !link->cpus) + return -ENOMEM; + + link->num_platforms = 0; + + for_each_link_codecs(link, i, comp) { + ret = macaudio_parse_of_component(codec, codec_base + i, comp); + if (ret) + return ret; + } + + ret = macaudio_parse_of_component(cpu, be_index, link->cpus); + if (ret) + return ret; + + link->name = link->cpus[0].dai_name; + + return 0; +} + +static int macaudio_parse_of(struct macaudio_snd_data *ma) +{ + struct device_node *codec = NULL; + struct device_node *cpu = NULL; + struct device_node *np = NULL; + struct device_node *platform = NULL; + struct snd_soc_dai_link *link = NULL; + struct snd_soc_card *card = &ma->card; + struct device *dev = card->dev; + struct macaudio_link_props *link_props; + int ret, num_links, i; + + ret = snd_soc_of_parse_card_name(card, "model"); + if (ret) { + dev_err(dev, "Error parsing card name: %d\n", ret); + return ret; + } + + /* Populate links, start with the fixed number of FE links */ + num_links = ARRAY_SIZE(macaudio_fe_links); + + /* Now add together the (dynamic) number of BE links */ + for_each_available_child_of_node(dev->of_node, np) { + int num_cpus; + + cpu = of_get_child_by_name(np, "cpu"); + if (!cpu) { + dev_err(dev, "missing CPU DAI node at %pOF\n", np); + ret = -EINVAL; + goto err_free; + } + + num_cpus = of_count_phandle_with_args(cpu, "sound-dai", + "#sound-dai-cells"); + + if (num_cpus <= 0) { + dev_err(card->dev, "missing sound-dai property at %pOF\n", cpu); + ret = -EINVAL; + goto err_free; + } + of_node_put(cpu); + cpu = NULL; + + /* Each CPU specified counts as one BE link */ + num_links += num_cpus; + } + + /* Allocate the DAI link array */ + card->dai_link = devm_kcalloc(dev, num_links, sizeof(*link), GFP_KERNEL); + ma->link_props = devm_kcalloc(dev, num_links, sizeof(*ma->link_props), GFP_KERNEL); + if (!card->dai_link || !ma->link_props) + return -ENOMEM; + + card->num_links = num_links; + link = card->dai_link; + link_props = ma->link_props; + + for (i = 0; i < ARRAY_SIZE(macaudio_fe_links); i++) { + ret = macaudio_copy_link(dev, link, &macaudio_fe_links[i]); + if (ret) + goto err_free; + + memcpy(link_props, &macaudio_fe_link_props[i], sizeof(struct macaudio_link_props)); + link++; link_props++; + } + + for (i = 0; i < num_links; i++) + card->dai_link[i].id = i; + + /* Fill in the BEs */ + for_each_available_child_of_node(dev->of_node, np) { + const char *link_name; + bool speakers; + int be_index, num_codecs, num_bes, ncodecs_per_cpu, nchannels; + unsigned int left_mask, right_mask; + + ret = of_property_read_string(np, "link-name", &link_name); + if (ret) { + dev_err(card->dev, "missing link name\n"); + goto err_free; + } + + speakers = !strcmp(link_name, "Speaker") + || !strcmp(link_name, "Speakers"); + + cpu = of_get_child_by_name(np, "cpu"); + codec = of_get_child_by_name(np, "codec"); + + if (!codec || !cpu) { + dev_err(dev, "missing DAI specifications for '%s'\n", link_name); + ret = -EINVAL; + goto err_free; + } + + num_bes = of_count_phandle_with_args(cpu, "sound-dai", + "#sound-dai-cells"); + if (num_bes <= 0) { + dev_err(card->dev, "missing sound-dai property at %pOF\n", cpu); + ret = -EINVAL; + goto err_free; + } + + num_codecs = of_count_phandle_with_args(codec, "sound-dai", + "#sound-dai-cells"); + if (num_codecs <= 0) { + dev_err(card->dev, "missing sound-dai property at %pOF\n", codec); + ret = -EINVAL; + goto err_free; + } + + if (num_codecs % num_bes != 0) { + dev_err(card->dev, "bad combination of CODEC (%d) and CPU (%d) number at %pOF\n", + num_codecs, num_bes, np); + ret = -EINVAL; + goto err_free; + } + + /* + * Now parse the cpu/codec lists into a number of DPCM backend links. + * In each link there will be one DAI from the cpu list paired with + * an evenly distributed number of DAIs from the codec list. (As is + * the binding semantics.) + */ + ncodecs_per_cpu = num_codecs / num_bes; + nchannels = num_codecs * (speakers ? 1 : 2); + + /* + * If there is a single speaker, assign two channels to it, because + * it can do downmix. + */ + if (nchannels < 2) + nchannels = 2; + + left_mask = 0; + for (i = 0; i < nchannels; i += 2) + left_mask = left_mask << 2 | 1; + right_mask = left_mask << 1; + + for (be_index = 0; be_index < num_bes; be_index++) { + ret = macaudio_parse_of_be_dai_link(ma, link, be_index, + ncodecs_per_cpu, cpu, codec); + if (ret) + goto err_free; + + link_props->is_speakers = speakers; + link_props->is_headphones = !speakers; + + if (num_bes == 2) + /* This sound peripheral is split between left and right BE */ + link_props->tdm_mask = be_index ? right_mask : left_mask; + else + /* One BE covers all of the peripheral */ + link_props->tdm_mask = left_mask | right_mask; + + /* Steal platform OF reference for use in FE links later */ + platform = link->cpus->of_node; + + link++; link_props++; + } + + of_node_put(codec); + of_node_put(cpu); + cpu = codec = NULL; + } + + for (i = 0; i < ARRAY_SIZE(macaudio_fe_links); i++) + card->dai_link[i].platforms->of_node = platform; + + return 0; + +err_free: + of_node_put(codec); + of_node_put(cpu); + of_node_put(np); + + if (!card->dai_link) + return ret; + + for (i = 0; i < num_links; i++) { + /* + * TODO: If we don't go through this path are the references + * freed inside ASoC? + */ + snd_soc_of_put_dai_link_codecs(&card->dai_link[i]); + snd_soc_of_put_dai_link_cpus(&card->dai_link[i]); + } + + return ret; +} + +static int macaudio_get_runtime_mclk_fs(struct snd_pcm_substream *substream) +{ + struct snd_soc_pcm_runtime *rtd = asoc_substream_to_rtd(substream); + struct macaudio_snd_data *ma = snd_soc_card_get_drvdata(rtd->card); + struct snd_soc_dpcm *dpcm; + + /* + * If this is a FE, look it up in link_props directly. + * If this is a BE, look it up in the respective FE. + */ + if (!rtd->dai_link->no_pcm) + return ma->link_props[rtd->dai_link->id].mclk_fs; + + for_each_dpcm_fe(rtd, substream->stream, dpcm) { + int fe_id = dpcm->fe->dai_link->id; + + return ma->link_props[fe_id].mclk_fs; + } + + return 0; +} + +static int macaudio_dpcm_hw_params(struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params) +{ + struct snd_soc_pcm_runtime *rtd = asoc_substream_to_rtd(substream); + struct snd_soc_dai *cpu_dai = asoc_rtd_to_cpu(rtd, 0); + int mclk_fs = macaudio_get_runtime_mclk_fs(substream); + int i; + + if (mclk_fs) { + struct snd_soc_dai *dai; + int mclk = params_rate(params) * mclk_fs; + + for_each_rtd_codec_dais(rtd, i, dai) + snd_soc_dai_set_sysclk(dai, 0, mclk, SND_SOC_CLOCK_IN); + + snd_soc_dai_set_sysclk(cpu_dai, 0, mclk, SND_SOC_CLOCK_OUT); + } + + return 0; +} + +static void macaudio_dpcm_shutdown(struct snd_pcm_substream *substream) +{ + struct snd_soc_pcm_runtime *rtd = asoc_substream_to_rtd(substream); + struct snd_soc_dai *cpu_dai = asoc_rtd_to_cpu(rtd, 0); + struct snd_soc_dai *dai; + int mclk_fs = macaudio_get_runtime_mclk_fs(substream); + int i; + + if (mclk_fs) { + for_each_rtd_codec_dais(rtd, i, dai) + snd_soc_dai_set_sysclk(dai, 0, 0, SND_SOC_CLOCK_IN); + + snd_soc_dai_set_sysclk(cpu_dai, 0, 0, SND_SOC_CLOCK_OUT); + } +} + +static const struct snd_soc_ops macaudio_fe_ops = { + .shutdown = macaudio_dpcm_shutdown, + .hw_params = macaudio_dpcm_hw_params, +}; + +static const struct snd_soc_ops macaudio_be_ops = { + .shutdown = macaudio_dpcm_shutdown, + .hw_params = macaudio_dpcm_hw_params, +}; + +static int macaudio_be_assign_tdm(struct snd_soc_pcm_runtime *rtd) +{ + struct snd_soc_card *card = rtd->card; + struct macaudio_snd_data *ma = snd_soc_card_get_drvdata(card); + struct macaudio_link_props *props = &ma->link_props[rtd->dai_link->id]; + struct snd_soc_dai *dai; + unsigned int mask; + int nslots, ret, i; + + if (!props->tdm_mask) + return 0; + + mask = props->tdm_mask; + nslots = __fls(mask) + 1; + + if (rtd->num_codecs == 1) { + ret = snd_soc_dai_set_tdm_slot(asoc_rtd_to_codec(rtd, 0), mask, + 0, nslots, MACAUDIO_SLOTWIDTH); + + /* + * Headphones get a pass on -EOPNOTSUPP (see the comment + * around mclk_fs value for primary FE). + */ + if (ret == -EOPNOTSUPP && props->is_headphones) + return 0; + + return ret; + } + + for_each_rtd_codec_dais(rtd, i, dai) { + int slot = __ffs(mask); + + mask &= ~(1 << slot); + ret = snd_soc_dai_set_tdm_slot(dai, 1 << slot, 0, nslots, + MACAUDIO_SLOTWIDTH); + if (ret) + return ret; + } + + return 0; +} + +static int macaudio_be_init(struct snd_soc_pcm_runtime *rtd) +{ + struct snd_soc_card *card = rtd->card; + struct macaudio_snd_data *ma = snd_soc_card_get_drvdata(card); + struct macaudio_link_props *props = &ma->link_props[rtd->dai_link->id]; + struct snd_soc_dai *dai; + int i, ret; + + ret = macaudio_be_assign_tdm(rtd); + if (ret < 0) + return ret; + + if (props->is_headphones) { + for_each_rtd_codec_dais(rtd, i, dai) + snd_soc_component_set_jack(dai->component, &ma->jack, NULL); + } + + return 0; +} + +static void macaudio_be_exit(struct snd_soc_pcm_runtime *rtd) +{ + struct snd_soc_card *card = rtd->card; + struct macaudio_snd_data *ma = snd_soc_card_get_drvdata(card); + struct macaudio_link_props *props = &ma->link_props[rtd->dai_link->id]; + struct snd_soc_dai *dai; + int i; + + if (props->is_headphones) { + for_each_rtd_codec_dais(rtd, i, dai) + snd_soc_component_set_jack(dai->component, NULL, NULL); + } +} + +static int macaudio_fe_init(struct snd_soc_pcm_runtime *rtd) +{ + struct snd_soc_card *card = rtd->card; + struct macaudio_snd_data *ma = snd_soc_card_get_drvdata(card); + struct macaudio_link_props *props = &ma->link_props[rtd->dai_link->id]; + int nslots = props->mclk_fs / MACAUDIO_SLOTWIDTH; + + return snd_soc_dai_set_tdm_slot(asoc_rtd_to_cpu(rtd, 0), (1 << nslots) - 1, + (1 << nslots) - 1, nslots, MACAUDIO_SLOTWIDTH); +} + + +static int macaudio_jack_event(struct notifier_block *nb, unsigned long event, + void *data); + +static struct notifier_block macaudio_jack_nb = { + .notifier_call = macaudio_jack_event, +}; + +static int macaudio_probe(struct snd_soc_card *card) +{ + struct macaudio_snd_data *ma = snd_soc_card_get_drvdata(card); + int ret; + + ma->pin.pin = "Headphones"; + ma->pin.mask = SND_JACK_HEADSET | SND_JACK_HEADPHONE; + ret = snd_soc_card_jack_new(card, ma->pin.pin, + SND_JACK_HEADSET | + SND_JACK_HEADPHONE | + SND_JACK_BTN_0 | SND_JACK_BTN_1 | + SND_JACK_BTN_2 | SND_JACK_BTN_3, + &ma->jack, &ma->pin, 1); + + if (ret < 0) { + dev_err(card->dev, "jack creation failed: %d\n", ret); + return ret; + } + + snd_soc_jack_notifier_register(&ma->jack, &macaudio_jack_nb); + + return ret; +} + +static int macaudio_add_backend_dai_route(struct snd_soc_card *card, struct snd_soc_dai *dai, + bool is_speakers) +{ + struct snd_soc_dapm_route routes[2]; + int nroutes; + int ret; + memset(routes, 0, sizeof(routes)); + + dev_dbg(card->dev, "adding routes for '%s'\n", dai->name); + + if (is_speakers) + routes[0].source = "Speakers Playback"; + else + routes[0].source = "Headphones Playback"; + routes[0].sink = dai->playback_widget->name; + nroutes = 1; + + if (!is_speakers) { + routes[1].source = dai->capture_widget->name; + routes[1].sink = "Headphones Capture"; + nroutes = 2; + } + + ret = snd_soc_dapm_add_routes(&card->dapm, routes, nroutes); + if (ret) + dev_err(card->dev, "failed adding dynamic DAPM routes for %s\n", + dai->name); + return ret; +} + +static bool macaudio_match_kctl_name(const char *pattern, const char *name) +{ + if (pattern[0] == '*') { + int namelen, patternlen; + + pattern++; + if (pattern[0] == ' ') + pattern++; + + namelen = strlen(name); + patternlen = strlen(pattern); + + if (namelen > patternlen) + name += (namelen - patternlen); + } + + return !strcmp(name, pattern); +} + +static int macaudio_limit_volume(struct snd_soc_card *card, + const char *pattern, int max) +{ + struct snd_kcontrol *kctl; + struct soc_mixer_control *mc; + int found = 0; + + list_for_each_entry(kctl, &card->snd_card->controls, list) { + if (!macaudio_match_kctl_name(pattern, kctl->id.name)) + continue; + + found++; + dev_dbg(card->dev, "limiting volume on '%s'\n", kctl->id.name); + + /* + * TODO: This doesn't decrease the volume if it's already + * above the limit! + */ + mc = (struct soc_mixer_control *)kctl->private_value; + if (max <= mc->max) + mc->platform_max = max; + + } + + return found; +} + +static int macaudio_late_probe(struct snd_soc_card *card) +{ + struct macaudio_snd_data *ma = snd_soc_card_get_drvdata(card); + struct snd_soc_pcm_runtime *rtd; + struct snd_soc_dai *dai; + int ret, i; + + /* Add the dynamic DAPM routes */ + for_each_card_rtds(card, rtd) { + struct macaudio_link_props *props = &ma->link_props[rtd->dai_link->id]; + + if (!rtd->dai_link->no_pcm) + continue; + + for_each_rtd_cpu_dais(rtd, i, dai) { + ret = macaudio_add_backend_dai_route(card, dai, props->is_speakers); + + if (ret) + return ret; + } + } + + if (!ma->mdata) { + dev_err(card->dev, "driver doesn't know speaker limits for this model\n"); + return void_warranty ? 0 : -EINVAL; + } + + macaudio_limit_volume(card, "* Amp Gain", ma->mdata->spk_amp_gain_max); + return 0; +} + +static const char * const macaudio_plugin_demux_texts[] = { + "Speakers", + "Headphones" +}; + +SOC_ENUM_SINGLE_VIRT_DECL(macaudio_plugin_demux_enum, macaudio_plugin_demux_texts); + +static int macaudio_plugin_demux_get(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_soc_dapm_context *dapm = snd_soc_dapm_kcontrol_dapm(kcontrol); + struct macaudio_snd_data *ma = snd_soc_card_get_drvdata(dapm->card); + + /* + * TODO: Determine what locking is in order here... + */ + ucontrol->value.enumerated.item[0] = ma->jack_plugin_state; + + return 0; +} + +static int macaudio_jack_event(struct notifier_block *nb, unsigned long event, + void *data) +{ + struct snd_soc_jack *jack = data; + struct macaudio_snd_data *ma = snd_soc_card_get_drvdata(jack->card); + + ma->jack_plugin_state = !!event; + + if (!ma->plugin_demux_kcontrol) + return 0; + + snd_soc_dapm_mux_update_power(&ma->card.dapm, ma->plugin_demux_kcontrol, + ma->jack_plugin_state, + (struct soc_enum *) &macaudio_plugin_demux_enum, NULL); + + return 0; +} + +static const struct snd_kcontrol_new macaudio_plugin_demux = { + .access = (SNDRV_CTL_ELEM_ACCESS_READ | + SNDRV_CTL_ELEM_ACCESS_VOLATILE), + .iface = SNDRV_CTL_ELEM_IFACE_MIXER, + .name = "Plug-in Playback Demux", + .info = snd_soc_info_enum_double, + .get = macaudio_plugin_demux_get, + .private_value = (unsigned long) &macaudio_plugin_demux_enum +}; + +static int macaudio_kctl_set_enum(struct snd_kcontrol *kctl, + const char *strvalue) +{ + struct snd_ctl_elem_value value; + struct snd_ctl_elem_info info; + int sel, i, ret; + + ret = kctl->info(kctl, &info); + if (ret < 0) + return ret; + + if (info.type != SNDRV_CTL_ELEM_TYPE_ENUMERATED) + return -EINVAL; + + for (sel = 0; sel < info.value.enumerated.items; sel++) { + info.value.enumerated.item = sel; + ret = kctl->info(kctl, &info); + if (ret < 0) + return ret; + + if (!strcmp(strvalue, info.value.enumerated.name)) + break; + } + + if (sel == info.value.enumerated.items) + return -EINVAL; + + for (i = 0; i < info.count; i++) + value.value.enumerated.item[i] = sel; + + return kctl->put(kctl, &value); +} + +static void macaudio_deactivate_asi1_sel(struct snd_soc_card *card) +{ + struct snd_kcontrol *kctl; + int ret; + + list_for_each_entry(kctl, &card->snd_card->controls, list) { + if (!macaudio_match_kctl_name("* ASI1 Sel", kctl->id.name)) + continue; + + ret = macaudio_kctl_set_enum(kctl, "Left"); + if (ret < 0) + dev_err(card->dev, "can't pin '%s': %d\n", kctl->id.name, ret); + + ret = snd_ctl_activate_id(card->snd_card, &kctl->id, 0); + if (ret < 0) + dev_err(card->dev, "can't deactivate '%s': %d\n", kctl->id.name, ret); + else + dev_dbg(card->dev, "deactivated '%s'\n", kctl->id.name); + } +} + +static void macaudio_fixup_controls(struct snd_soc_card *card) +{ + struct macaudio_snd_data *ma = snd_soc_card_get_drvdata(card); + const char *name = macaudio_plugin_demux.name; + + ma->plugin_demux_kcontrol = snd_soc_card_get_kcontrol(card, name); + + if (!ma->plugin_demux_kcontrol) + dev_err(card->dev, "can't find control '%s'\n", name); + + if (ma->mdata && ma->mdata->deactive_asi1_sel) + macaudio_deactivate_asi1_sel(card); + + macaudio_limit_volume(card, "* Amp Gain Volume", ma->mdata->spk_amp_gain_max); +} + +static const char * const macaudio_spk_mux_texts[] = { + "Primary (Conditional)", + "Primary", + "Secondary" +}; + +SOC_ENUM_SINGLE_VIRT_DECL(macaudio_spk_mux_enum, macaudio_spk_mux_texts); + +static const struct snd_kcontrol_new macaudio_spk_mux = + SOC_DAPM_ENUM("Speakers Playback Mux", macaudio_spk_mux_enum); + +static const char * const macaudio_hp_mux_texts[] = { + "Primary (Conditional)", + "Primary", +}; + +SOC_ENUM_SINGLE_VIRT_DECL(macaudio_hp_mux_enum, macaudio_hp_mux_texts); + +static const struct snd_kcontrol_new macaudio_hp_mux = + SOC_DAPM_ENUM("Headphones Playback Mux", macaudio_hp_mux_enum); + +static const struct snd_soc_dapm_widget macaudio_snd_widgets[] = { + SND_SOC_DAPM_HP("Headphones", NULL), + SND_SOC_DAPM_SPK("Speakers", NULL), + + SND_SOC_DAPM_MUX("Speakers Playback Mux", SND_SOC_NOPM, 0, 0, &macaudio_spk_mux), + SND_SOC_DAPM_MUX("Headphones Playback Mux", SND_SOC_NOPM, 0, 0, &macaudio_hp_mux), + SND_SOC_DAPM_DEMUX("Plug-in Playback Demux", SND_SOC_NOPM, 0, 0, &macaudio_plugin_demux), + + SND_SOC_DAPM_AIF_OUT("Plug-in Headphones Playback", NULL, 0, SND_SOC_NOPM, 0, 0), + SND_SOC_DAPM_AIF_OUT("Plug-in Speakers Playback", NULL, 0, SND_SOC_NOPM, 0, 0), + SND_SOC_DAPM_AIF_OUT("Speakers Playback", NULL, 0, SND_SOC_NOPM, 0, 0), + SND_SOC_DAPM_AIF_OUT("Headphones Playback", NULL, 0, SND_SOC_NOPM, 0, 0), + + SND_SOC_DAPM_AIF_IN("Headphones Capture", NULL, 0, SND_SOC_NOPM, 0, 0), +}; + +static const struct snd_soc_dapm_route macaudio_dapm_routes[] = { + /* Playback paths */ + { "Plug-in Playback Demux", NULL, "PCM0 TX" }, + { "Plug-in Speakers Playback", "Speakers", "Plug-in Playback Demux" }, + { "Plug-in Headphones Playback", "Headphones", "Plug-in Playback Demux" }, + + { "Speakers Playback Mux", "Primary (Conditional)", "Plug-in Speakers Playback" }, + { "Speakers Playback Mux", "Primary", "PCM0 TX" }, + { "Speakers Playback Mux", "Secondary", "PCM1 TX" }, + { "Speakers Playback", NULL, "Speakers Playback Mux"}, + + { "Headphones Playback Mux", "Primary (Conditional)", "Plug-in Headphones Playback" }, + { "Headphones Playback Mux", "Primary", "PCM0 TX" }, + { "Headphones Playback", NULL, "Headphones Playback Mux"}, + /* + * Additional paths (to specific I2S ports) are added dynamically. + */ + + /* Capture paths */ + { "PCM0 RX", NULL, "Headphones Capture" }, +}; + +struct macaudio_model_data macaudio_j274_mdata = { + .spk_amp_gain_max = 20, +}; + +struct macaudio_model_data macaudio_j314_mdata = { + .deactive_asi1_sel = true, + .spk_amp_gain_max = 15, +}; + +static const struct of_device_id macaudio_snd_device_id[] = { + { .compatible = "apple,j274-macaudio", .data = &macaudio_j274_mdata }, + { .compatible = "apple,j314-macaudio", .data = &macaudio_j314_mdata }, + { .compatible = "apple,macaudio"}, + { } +}; +MODULE_DEVICE_TABLE(of, macaudio_snd_device_id); + +static int macaudio_snd_platform_probe(struct platform_device *pdev) +{ + struct snd_soc_card *card; + struct macaudio_snd_data *data; + struct device *dev = &pdev->dev; + struct snd_soc_dai_link *link; + const struct of_device_id *of_id; + int ret; + int i; + + of_id = of_match_device(macaudio_snd_device_id, dev); + if (!of_id) + return -EINVAL; + + data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL); + if (!data) + return -ENOMEM; + card = &data->card; + snd_soc_card_set_drvdata(card, data); + + data->mdata = (struct macaudio_model_data *) of_id->data; + + card->owner = THIS_MODULE; + card->driver_name = DRIVER_NAME; + card->dev = dev; + card->dapm_widgets = macaudio_snd_widgets; + card->num_dapm_widgets = ARRAY_SIZE(macaudio_snd_widgets); + card->dapm_routes = macaudio_dapm_routes; + card->num_dapm_routes = ARRAY_SIZE(macaudio_dapm_routes); + card->probe = macaudio_probe; + card->late_probe = macaudio_late_probe; + card->fixup_controls = macaudio_fixup_controls; + + ret = macaudio_parse_of(data); + if (ret) + return ret; + + for_each_card_prelinks(card, i, link) { + if (link->no_pcm) { + link->ops = &macaudio_be_ops; + link->init = macaudio_be_init; + link->exit = macaudio_be_exit; + } else { + link->ops = &macaudio_fe_ops; + link->init = macaudio_fe_init; + } + } + + return devm_snd_soc_register_card(dev, card); +} + +static struct platform_driver macaudio_snd_driver = { + .probe = macaudio_snd_platform_probe, + .driver = { + .name = DRIVER_NAME, + .of_match_table = macaudio_snd_device_id, + .pm = &snd_soc_pm_ops, + }, +}; +module_platform_driver(macaudio_snd_driver); + +MODULE_AUTHOR("Martin Povišer povik+lin@cutebit.org"); +MODULE_DESCRIPTION("Apple Silicon Macs machine-level sound driver"); +MODULE_LICENSE("GPL");
- Virtual FE/BE Playback Topology
- The platform driver has independent frontend and backend DAIs with the
- option of routing backends to any of the frontends. The platform
- driver configures the routing based on DPCM couplings in ASoC runtime
- structures, which in turn is determined from DAPM paths by ASoC. But the
- platform driver doesn't supply relevant DAPM paths and leaves that up for
- the machine driver to fill in. The filled-in virtual topology can be
- anything as long as a particular backend isn't connected to more than one
- frontend at any given time. (The limitation is due to the unsupported case
- of reparenting of live BEs.)
- The DAPM routing that this machine-level driver makes up has two use-cases
- in mind:
- Using a single PCM for playback such that it conditionally sinks to either
- speakers or headphones based on the plug-in state of the headphones jack.
- All the while making the switch transparent to userspace. This has the
- drawback of requiring a sample stream suited for both speakers and
- headphones, which is hard to come by on machines where tailored DSP for
- speakers in userspace is desirable or required.
- Driving the headphones and speakers from distinct PCMs, having userspace
- bridge the difference and apply different signal processing to the two.
- In the end the topology supplied by this driver looks like this:
- PCMs (frontends) I2S Port Groups (backends)
- ──────────────── ──────────────────────────
- ┌──────────┐ ┌───────────────► ┌─────┐ ┌──────────┐
- │ Primary ├───────┤ │ Mux │ ──► │ Speakers │
- └──────────┘ │ ┌──────────► └─────┘ └──────────┘
┌─── │ ───┘ ▲
- ┌──────────┐ │ │ │
- │Secondary ├──┘ │ ┌────────────┴┐
- └──────────┘ ├────►│Plug-in Demux│
│ └────────────┬┘
│ │
│ ▼
│ ┌─────┐ ┌──────────┐
└───────────────► │ Mux │ ──► │Headphones│
└─────┘ └──────────┘
- */
In Patch2, the 'clusters' are described as front-ends, with I2S as back-ends. Here the PCMs are described as front-ends, but there's no mention of clusters. Either one of the two descriptions is outdated, or there's something missing to help reconcile the two pieces of information?
+static int macaudio_copy_link(struct device *dev, struct snd_soc_dai_link *target,
struct snd_soc_dai_link *source)
+{
- memcpy(target, source, sizeof(struct snd_soc_dai_link));
- target->cpus = devm_kcalloc(dev, target->num_cpus,
sizeof(*target->cpus), GFP_KERNEL);
- target->codecs = devm_kcalloc(dev, target->num_codecs,
sizeof(*target->codecs), GFP_KERNEL);
- target->platforms = devm_kcalloc(dev, target->num_platforms,
sizeof(*target->platforms), GFP_KERNEL);
- if (!target->cpus || !target->codecs || !target->platforms)
return -ENOMEM;
- memcpy(target->cpus, source->cpus, sizeof(*target->cpus) * target->num_cpus);
- memcpy(target->codecs, source->codecs, sizeof(*target->codecs) * target->num_codecs);
- memcpy(target->platforms, source->platforms, sizeof(*target->platforms) * target->num_platforms);
use devm_kmemdup?
- return 0;
+}
+static int macaudio_get_runtime_mclk_fs(struct snd_pcm_substream *substream) +{
- struct snd_soc_pcm_runtime *rtd = asoc_substream_to_rtd(substream);
- struct macaudio_snd_data *ma = snd_soc_card_get_drvdata(rtd->card);
- struct snd_soc_dpcm *dpcm;
- /*
* If this is a FE, look it up in link_props directly.
* If this is a BE, look it up in the respective FE.
*/
- if (!rtd->dai_link->no_pcm)
return ma->link_props[rtd->dai_link->id].mclk_fs;
- for_each_dpcm_fe(rtd, substream->stream, dpcm) {
int fe_id = dpcm->fe->dai_link->id;
return ma->link_props[fe_id].mclk_fs;
- }
I am not sure what the concept of mclk would mean for a front-end? This is typically very I2S-specific, i.e. a back-end property, no?
- return 0;
+}
+static int macaudio_dpcm_hw_params(struct snd_pcm_substream *substream,
struct snd_pcm_hw_params *params)
+{
- struct snd_soc_pcm_runtime *rtd = asoc_substream_to_rtd(substream);
- struct snd_soc_dai *cpu_dai = asoc_rtd_to_cpu(rtd, 0);
- int mclk_fs = macaudio_get_runtime_mclk_fs(substream);
- int i;
- if (mclk_fs) {
struct snd_soc_dai *dai;
int mclk = params_rate(params) * mclk_fs;
for_each_rtd_codec_dais(rtd, i, dai)
snd_soc_dai_set_sysclk(dai, 0, mclk, SND_SOC_CLOCK_IN);
snd_soc_dai_set_sysclk(cpu_dai, 0, mclk, SND_SOC_CLOCK_OUT);
- }
- return 0;
+}
+static void macaudio_dpcm_shutdown(struct snd_pcm_substream *substream) +{
- struct snd_soc_pcm_runtime *rtd = asoc_substream_to_rtd(substream);
- struct snd_soc_dai *cpu_dai = asoc_rtd_to_cpu(rtd, 0);
- struct snd_soc_dai *dai;
- int mclk_fs = macaudio_get_runtime_mclk_fs(substream);
- int i;
- if (mclk_fs) {
for_each_rtd_codec_dais(rtd, i, dai)
snd_soc_dai_set_sysclk(dai, 0, 0, SND_SOC_CLOCK_IN);
snd_soc_dai_set_sysclk(cpu_dai, 0, 0, SND_SOC_CLOCK_OUT);
- }
+}
+static const struct snd_soc_ops macaudio_fe_ops = {
- .shutdown = macaudio_dpcm_shutdown,
- .hw_params = macaudio_dpcm_hw_params,
+};
+static const struct snd_soc_ops macaudio_be_ops = {
- .shutdown = macaudio_dpcm_shutdown,
- .hw_params = macaudio_dpcm_hw_params,
+};
+static int macaudio_be_assign_tdm(struct snd_soc_pcm_runtime *rtd) +{
- struct snd_soc_card *card = rtd->card;
- struct macaudio_snd_data *ma = snd_soc_card_get_drvdata(card);
- struct macaudio_link_props *props = &ma->link_props[rtd->dai_link->id];
- struct snd_soc_dai *dai;
- unsigned int mask;
- int nslots, ret, i;
- if (!props->tdm_mask)
return 0;
- mask = props->tdm_mask;
- nslots = __fls(mask) + 1;
- if (rtd->num_codecs == 1) {
ret = snd_soc_dai_set_tdm_slot(asoc_rtd_to_codec(rtd, 0), mask,
0, nslots, MACAUDIO_SLOTWIDTH);
/*
* Headphones get a pass on -EOPNOTSUPP (see the comment
* around mclk_fs value for primary FE).
*/
if (ret == -EOPNOTSUPP && props->is_headphones)
return 0;
return ret;
- }
- for_each_rtd_codec_dais(rtd, i, dai) {
int slot = __ffs(mask);
mask &= ~(1 << slot);
ret = snd_soc_dai_set_tdm_slot(dai, 1 << slot, 0, nslots,
MACAUDIO_SLOTWIDTH);
if (ret)
return ret;
- }
- return 0;
+}
+static int macaudio_be_init(struct snd_soc_pcm_runtime *rtd) +{
- struct snd_soc_card *card = rtd->card;
- struct macaudio_snd_data *ma = snd_soc_card_get_drvdata(card);
- struct macaudio_link_props *props = &ma->link_props[rtd->dai_link->id];
- struct snd_soc_dai *dai;
- int i, ret;
- ret = macaudio_be_assign_tdm(rtd);
- if (ret < 0)
return ret;
- if (props->is_headphones) {
for_each_rtd_codec_dais(rtd, i, dai)
snd_soc_component_set_jack(dai->component, &ma->jack, NULL);
- }
this is weird, set_jack() is invoked by the ASoC core. You shouldn't need to do this?
- return 0;
+}
+static void macaudio_be_exit(struct snd_soc_pcm_runtime *rtd) +{
- struct snd_soc_card *card = rtd->card;
- struct macaudio_snd_data *ma = snd_soc_card_get_drvdata(card);
- struct macaudio_link_props *props = &ma->link_props[rtd->dai_link->id];
- struct snd_soc_dai *dai;
- int i;
- if (props->is_headphones) {
for_each_rtd_codec_dais(rtd, i, dai)
snd_soc_component_set_jack(dai->component, NULL, NULL);
- }
same, why is this needed?
+}
+static int macaudio_fe_init(struct snd_soc_pcm_runtime *rtd) +{
- struct snd_soc_card *card = rtd->card;
- struct macaudio_snd_data *ma = snd_soc_card_get_drvdata(card);
- struct macaudio_link_props *props = &ma->link_props[rtd->dai_link->id];
- int nslots = props->mclk_fs / MACAUDIO_SLOTWIDTH;
- return snd_soc_dai_set_tdm_slot(asoc_rtd_to_cpu(rtd, 0), (1 << nslots) - 1,
(1 << nslots) - 1, nslots, MACAUDIO_SLOTWIDTH);
+}
+static int macaudio_jack_event(struct notifier_block *nb, unsigned long event,
void *data);
+static struct notifier_block macaudio_jack_nb = {
- .notifier_call = macaudio_jack_event,
+};
why is this needed? we have already many ways of dealing with the jack events (dare I say too many ways?).
+static int macaudio_probe(struct snd_soc_card *card) +{
- struct macaudio_snd_data *ma = snd_soc_card_get_drvdata(card);
- int ret;
- ma->pin.pin = "Headphones";
- ma->pin.mask = SND_JACK_HEADSET | SND_JACK_HEADPHONE;
- ret = snd_soc_card_jack_new(card, ma->pin.pin,
SND_JACK_HEADSET |
SND_JACK_HEADPHONE |
SND_JACK_BTN_0 | SND_JACK_BTN_1 |
SND_JACK_BTN_2 | SND_JACK_BTN_3,
&ma->jack, &ma->pin, 1);
- if (ret < 0) {
dev_err(card->dev, "jack creation failed: %d\n", ret);
return ret;
- }
- snd_soc_jack_notifier_register(&ma->jack, &macaudio_jack_nb);
- return ret;
+}
+static int macaudio_add_backend_dai_route(struct snd_soc_card *card, struct snd_soc_dai *dai,
bool is_speakers)
+{
- struct snd_soc_dapm_route routes[2];
- int nroutes;
- int ret;
newline?
- memset(routes, 0, sizeof(routes));
- dev_dbg(card->dev, "adding routes for '%s'\n", dai->name);
- if (is_speakers)
routes[0].source = "Speakers Playback";
- else
routes[0].source = "Headphones Playback";
- routes[0].sink = dai->playback_widget->name;
- nroutes = 1;
- if (!is_speakers) {
routes[1].source = dai->capture_widget->name;
routes[1].sink = "Headphones Capture";
nroutes = 2;
- }
- ret = snd_soc_dapm_add_routes(&card->dapm, routes, nroutes);
- if (ret)
dev_err(card->dev, "failed adding dynamic DAPM routes for %s\n",
dai->name);
- return ret;
+}
+static bool macaudio_match_kctl_name(const char *pattern, const char *name) +{
- if (pattern[0] == '*') {
int namelen, patternlen;
pattern++;
if (pattern[0] == ' ')
pattern++;
namelen = strlen(name);
patternlen = strlen(pattern);
if (namelen > patternlen)
name += (namelen - patternlen);
- }
- return !strcmp(name, pattern);
+}
+static int macaudio_limit_volume(struct snd_soc_card *card,
const char *pattern, int max)
+{
- struct snd_kcontrol *kctl;
- struct soc_mixer_control *mc;
- int found = 0;
- list_for_each_entry(kctl, &card->snd_card->controls, list) {
if (!macaudio_match_kctl_name(pattern, kctl->id.name))
continue;
found++;
dev_dbg(card->dev, "limiting volume on '%s'\n", kctl->id.name);
/*
* TODO: This doesn't decrease the volume if it's already
* above the limit!
*/
mc = (struct soc_mixer_control *)kctl->private_value;
if (max <= mc->max)
mc->platform_max = max;
- }
- return found;
+}
+static int macaudio_late_probe(struct snd_soc_card *card) +{
- struct macaudio_snd_data *ma = snd_soc_card_get_drvdata(card);
- struct snd_soc_pcm_runtime *rtd;
- struct snd_soc_dai *dai;
- int ret, i;
- /* Add the dynamic DAPM routes */
- for_each_card_rtds(card, rtd) {
struct macaudio_link_props *props = &ma->link_props[rtd->dai_link->id];
if (!rtd->dai_link->no_pcm)
continue;
for_each_rtd_cpu_dais(rtd, i, dai) {
ret = macaudio_add_backend_dai_route(card, dai, props->is_speakers);
if (ret)
return ret;
}
- }
- if (!ma->mdata) {
dev_err(card->dev, "driver doesn't know speaker limits for this model\n");
return void_warranty ? 0 : -EINVAL;
- }
- macaudio_limit_volume(card, "* Amp Gain", ma->mdata->spk_amp_gain_max);
- return 0;
+}
+static const char * const macaudio_plugin_demux_texts[] = {
- "Speakers",
- "Headphones"
+};
+SOC_ENUM_SINGLE_VIRT_DECL(macaudio_plugin_demux_enum, macaudio_plugin_demux_texts);
+static int macaudio_plugin_demux_get(struct snd_kcontrol *kcontrol,
struct snd_ctl_elem_value *ucontrol)
+{
- struct snd_soc_dapm_context *dapm = snd_soc_dapm_kcontrol_dapm(kcontrol);
- struct macaudio_snd_data *ma = snd_soc_card_get_drvdata(dapm->card);
- /*
* TODO: Determine what locking is in order here...
*/
- ucontrol->value.enumerated.item[0] = ma->jack_plugin_state;
- return 0;
+}
+static int macaudio_jack_event(struct notifier_block *nb, unsigned long event,
void *data)
+{
- struct snd_soc_jack *jack = data;
- struct macaudio_snd_data *ma = snd_soc_card_get_drvdata(jack->card);
- ma->jack_plugin_state = !!event;
- if (!ma->plugin_demux_kcontrol)
return 0;
- snd_soc_dapm_mux_update_power(&ma->card.dapm, ma->plugin_demux_kcontrol,
ma->jack_plugin_state,
(struct soc_enum *) &macaudio_plugin_demux_enum, NULL);
the term 'plugin' can be understood in many ways by different audio folks. 'plugin' is usually the term used for processing libraries (VST, LADSPA, etc). I think here you meant 'jack control'?
- return 0;
+}
(I am having trouble delivering mail to linux.intel.com, so I reply to the list and CC at least...)
On 6. 6. 2022, at 22:02, Pierre-Louis Bossart pierre-louis.bossart@linux.intel.com wrote:
- Virtual FE/BE Playback Topology
- The platform driver has independent frontend and backend DAIs with the
- option of routing backends to any of the frontends. The platform
- driver configures the routing based on DPCM couplings in ASoC runtime
- structures, which in turn is determined from DAPM paths by ASoC. But the
- platform driver doesn't supply relevant DAPM paths and leaves that up for
- the machine driver to fill in. The filled-in virtual topology can be
- anything as long as a particular backend isn't connected to more than one
- frontend at any given time. (The limitation is due to the unsupported case
- of reparenting of live BEs.)
- The DAPM routing that this machine-level driver makes up has two use-cases
- in mind:
- Using a single PCM for playback such that it conditionally sinks to either
- speakers or headphones based on the plug-in state of the headphones jack.
- All the while making the switch transparent to userspace. This has the
- drawback of requiring a sample stream suited for both speakers and
- headphones, which is hard to come by on machines where tailored DSP for
- speakers in userspace is desirable or required.
- Driving the headphones and speakers from distinct PCMs, having userspace
- bridge the difference and apply different signal processing to the two.
- In the end the topology supplied by this driver looks like this:
- PCMs (frontends) I2S Port Groups (backends)
- ──────────────── ──────────────────────────
- ┌──────────┐ ┌───────────────► ┌─────┐ ┌──────────┐
- │ Primary ├───────┤ │ Mux │ ──► │ Speakers │
- └──────────┘ │ ┌──────────► └─────┘ └──────────┘
┌─── │ ───┘ ▲
- ┌──────────┐ │ │ │
- │Secondary ├──┘ │ ┌────────────┴┐
- └──────────┘ ├────►│Plug-in Demux│
│ └────────────┬┘
│ │
│ ▼
│ ┌─────┐ ┌──────────┐
└───────────────► │ Mux │ ──► │Headphones│
└─────┘ └──────────┘
- */
In Patch2, the 'clusters' are described as front-ends, with I2S as back-ends. Here the PCMs are described as front-ends, but there's no mention of clusters. Either one of the two descriptions is outdated, or there's something missing to help reconcile the two pieces of information?
Both descriptions should be in sync. Maybe I don’t know the proper terminology. In both cases the frontend is meant to be the actual I2S transceiver unit, and backend the I2S port on the SoC’s periphery, which can be routed to any of transceiver units. (Multiple ports can be routed to the same unit, which means the ports will have the same clocks and data line -- that's a configuration we need to support to drive some of the speaker arrays, hence the backend/frontend distinction).
Maybe I am using 'PCM' in a confusing way here? What I meant is a subdevice that’s visible from userspace, because I have seen it used that way in ALSA codebase.
+static int macaudio_copy_link(struct device *dev, struct snd_soc_dai_link *target,
struct snd_soc_dai_link *source)
+{
- memcpy(target, source, sizeof(struct snd_soc_dai_link));
- target->cpus = devm_kcalloc(dev, target->num_cpus,
sizeof(*target->cpus), GFP_KERNEL);
- target->codecs = devm_kcalloc(dev, target->num_codecs,
sizeof(*target->codecs), GFP_KERNEL);
- target->platforms = devm_kcalloc(dev, target->num_platforms,
sizeof(*target->platforms), GFP_KERNEL);
- if (!target->cpus || !target->codecs || !target->platforms)
return -ENOMEM;
- memcpy(target->cpus, source->cpus, sizeof(*target->cpus) * target->num_cpus);
- memcpy(target->codecs, source->codecs, sizeof(*target->codecs) * target->num_codecs);
- memcpy(target->platforms, source->platforms, sizeof(*target->platforms) * target->num_platforms);
use devm_kmemdup?
Looks like what I am looking for.
- return 0;
+}
+static int macaudio_get_runtime_mclk_fs(struct snd_pcm_substream *substream) +{
- struct snd_soc_pcm_runtime *rtd = asoc_substream_to_rtd(substream);
- struct macaudio_snd_data *ma = snd_soc_card_get_drvdata(rtd->card);
- struct snd_soc_dpcm *dpcm;
- /*
* If this is a FE, look it up in link_props directly.
* If this is a BE, look it up in the respective FE.
*/
- if (!rtd->dai_link->no_pcm)
return ma->link_props[rtd->dai_link->id].mclk_fs;
- for_each_dpcm_fe(rtd, substream->stream, dpcm) {
int fe_id = dpcm->fe->dai_link->id;
return ma->link_props[fe_id].mclk_fs;
- }
I am not sure what the concept of mclk would mean for a front-end? This is typically very I2S-specific, i.e. a back-end property, no?
Right, that’s a result of the confusion from above. Hope I cleared it up somehow. The frontend already decides the clocks and data serialization, hence mclk/fs is a frontend-prop here.
- return 0;
+}
+static int macaudio_dpcm_hw_params(struct snd_pcm_substream *substream,
struct snd_pcm_hw_params *params)
+{
- struct snd_soc_pcm_runtime *rtd = asoc_substream_to_rtd(substream);
- struct snd_soc_dai *cpu_dai = asoc_rtd_to_cpu(rtd, 0);
- int mclk_fs = macaudio_get_runtime_mclk_fs(substream);
- int i;
- if (mclk_fs) {
struct snd_soc_dai *dai;
int mclk = params_rate(params) * mclk_fs;
for_each_rtd_codec_dais(rtd, i, dai)
snd_soc_dai_set_sysclk(dai, 0, mclk, SND_SOC_CLOCK_IN);
snd_soc_dai_set_sysclk(cpu_dai, 0, mclk, SND_SOC_CLOCK_OUT);
- }
- return 0;
+}
+static void macaudio_dpcm_shutdown(struct snd_pcm_substream *substream) +{
- struct snd_soc_pcm_runtime *rtd = asoc_substream_to_rtd(substream);
- struct snd_soc_dai *cpu_dai = asoc_rtd_to_cpu(rtd, 0);
- struct snd_soc_dai *dai;
- int mclk_fs = macaudio_get_runtime_mclk_fs(substream);
- int i;
- if (mclk_fs) {
for_each_rtd_codec_dais(rtd, i, dai)
snd_soc_dai_set_sysclk(dai, 0, 0, SND_SOC_CLOCK_IN);
snd_soc_dai_set_sysclk(cpu_dai, 0, 0, SND_SOC_CLOCK_OUT);
- }
+}
+static const struct snd_soc_ops macaudio_fe_ops = {
- .shutdown = macaudio_dpcm_shutdown,
- .hw_params = macaudio_dpcm_hw_params,
+};
+static const struct snd_soc_ops macaudio_be_ops = {
- .shutdown = macaudio_dpcm_shutdown,
- .hw_params = macaudio_dpcm_hw_params,
+};
+static int macaudio_be_assign_tdm(struct snd_soc_pcm_runtime *rtd) +{
- struct snd_soc_card *card = rtd->card;
- struct macaudio_snd_data *ma = snd_soc_card_get_drvdata(card);
- struct macaudio_link_props *props = &ma->link_props[rtd->dai_link->id];
- struct snd_soc_dai *dai;
- unsigned int mask;
- int nslots, ret, i;
- if (!props->tdm_mask)
return 0;
- mask = props->tdm_mask;
- nslots = __fls(mask) + 1;
- if (rtd->num_codecs == 1) {
ret = snd_soc_dai_set_tdm_slot(asoc_rtd_to_codec(rtd, 0), mask,
0, nslots, MACAUDIO_SLOTWIDTH);
/*
* Headphones get a pass on -EOPNOTSUPP (see the comment
* around mclk_fs value for primary FE).
*/
if (ret == -EOPNOTSUPP && props->is_headphones)
return 0;
return ret;
- }
- for_each_rtd_codec_dais(rtd, i, dai) {
int slot = __ffs(mask);
mask &= ~(1 << slot);
ret = snd_soc_dai_set_tdm_slot(dai, 1 << slot, 0, nslots,
MACAUDIO_SLOTWIDTH);
if (ret)
return ret;
- }
- return 0;
+}
+static int macaudio_be_init(struct snd_soc_pcm_runtime *rtd) +{
- struct snd_soc_card *card = rtd->card;
- struct macaudio_snd_data *ma = snd_soc_card_get_drvdata(card);
- struct macaudio_link_props *props = &ma->link_props[rtd->dai_link->id];
- struct snd_soc_dai *dai;
- int i, ret;
- ret = macaudio_be_assign_tdm(rtd);
- if (ret < 0)
return ret;
- if (props->is_headphones) {
for_each_rtd_codec_dais(rtd, i, dai)
snd_soc_component_set_jack(dai->component, &ma->jack, NULL);
- }
this is weird, set_jack() is invoked by the ASoC core. You shouldn't need to do this?
That’s interesting. Where would it be invoked? How does ASoC know which codec it attaches to?
- return 0;
+}
+static void macaudio_be_exit(struct snd_soc_pcm_runtime *rtd) +{
- struct snd_soc_card *card = rtd->card;
- struct macaudio_snd_data *ma = snd_soc_card_get_drvdata(card);
- struct macaudio_link_props *props = &ma->link_props[rtd->dai_link->id];
- struct snd_soc_dai *dai;
- int i;
- if (props->is_headphones) {
for_each_rtd_codec_dais(rtd, i, dai)
snd_soc_component_set_jack(dai->component, NULL, NULL);
- }
same, why is this needed?
+}
+static int macaudio_fe_init(struct snd_soc_pcm_runtime *rtd) +{
- struct snd_soc_card *card = rtd->card;
- struct macaudio_snd_data *ma = snd_soc_card_get_drvdata(card);
- struct macaudio_link_props *props = &ma->link_props[rtd->dai_link->id];
- int nslots = props->mclk_fs / MACAUDIO_SLOTWIDTH;
- return snd_soc_dai_set_tdm_slot(asoc_rtd_to_cpu(rtd, 0), (1 << nslots) - 1,
(1 << nslots) - 1, nslots, MACAUDIO_SLOTWIDTH);
+}
+static int macaudio_jack_event(struct notifier_block *nb, unsigned long event,
void *data);
+static struct notifier_block macaudio_jack_nb = {
- .notifier_call = macaudio_jack_event,
+};
why is this needed? we have already many ways of dealing with the jack events (dare I say too many ways?).
Because I want to update the DAPM paths based on the jack status, specifically I want to set macaudio_plugin_demux. I don’t know how else it could be done.
+static int macaudio_probe(struct snd_soc_card *card) +{
- struct macaudio_snd_data *ma = snd_soc_card_get_drvdata(card);
- int ret;
- ma->pin.pin = "Headphones";
- ma->pin.mask = SND_JACK_HEADSET | SND_JACK_HEADPHONE;
- ret = snd_soc_card_jack_new(card, ma->pin.pin,
SND_JACK_HEADSET |
SND_JACK_HEADPHONE |
SND_JACK_BTN_0 | SND_JACK_BTN_1 |
SND_JACK_BTN_2 | SND_JACK_BTN_3,
&ma->jack, &ma->pin, 1);
- if (ret < 0) {
dev_err(card->dev, "jack creation failed: %d\n", ret);
return ret;
- }
- snd_soc_jack_notifier_register(&ma->jack, &macaudio_jack_nb);
- return ret;
+}
+static int macaudio_add_backend_dai_route(struct snd_soc_card *card, struct snd_soc_dai *dai,
bool is_speakers)
+{
- struct snd_soc_dapm_route routes[2];
- int nroutes;
- int ret;
newline?
- memset(routes, 0, sizeof(routes));
- dev_dbg(card->dev, "adding routes for '%s'\n", dai->name);
- if (is_speakers)
routes[0].source = "Speakers Playback";
- else
routes[0].source = "Headphones Playback";
- routes[0].sink = dai->playback_widget->name;
- nroutes = 1;
- if (!is_speakers) {
routes[1].source = dai->capture_widget->name;
routes[1].sink = "Headphones Capture";
nroutes = 2;
- }
- ret = snd_soc_dapm_add_routes(&card->dapm, routes, nroutes);
- if (ret)
dev_err(card->dev, "failed adding dynamic DAPM routes for %s\n",
dai->name);
- return ret;
+}
+static bool macaudio_match_kctl_name(const char *pattern, const char *name) +{
- if (pattern[0] == '*') {
int namelen, patternlen;
pattern++;
if (pattern[0] == ' ')
pattern++;
namelen = strlen(name);
patternlen = strlen(pattern);
if (namelen > patternlen)
name += (namelen - patternlen);
- }
- return !strcmp(name, pattern);
+}
+static int macaudio_limit_volume(struct snd_soc_card *card,
const char *pattern, int max)
+{
- struct snd_kcontrol *kctl;
- struct soc_mixer_control *mc;
- int found = 0;
- list_for_each_entry(kctl, &card->snd_card->controls, list) {
if (!macaudio_match_kctl_name(pattern, kctl->id.name))
continue;
found++;
dev_dbg(card->dev, "limiting volume on '%s'\n", kctl->id.name);
/*
* TODO: This doesn't decrease the volume if it's already
* above the limit!
*/
mc = (struct soc_mixer_control *)kctl->private_value;
if (max <= mc->max)
mc->platform_max = max;
- }
- return found;
+}
+static int macaudio_late_probe(struct snd_soc_card *card) +{
- struct macaudio_snd_data *ma = snd_soc_card_get_drvdata(card);
- struct snd_soc_pcm_runtime *rtd;
- struct snd_soc_dai *dai;
- int ret, i;
- /* Add the dynamic DAPM routes */
- for_each_card_rtds(card, rtd) {
struct macaudio_link_props *props = &ma->link_props[rtd->dai_link->id];
if (!rtd->dai_link->no_pcm)
continue;
for_each_rtd_cpu_dais(rtd, i, dai) {
ret = macaudio_add_backend_dai_route(card, dai, props->is_speakers);
if (ret)
return ret;
}
- }
- if (!ma->mdata) {
dev_err(card->dev, "driver doesn't know speaker limits for this model\n");
return void_warranty ? 0 : -EINVAL;
- }
- macaudio_limit_volume(card, "* Amp Gain", ma->mdata->spk_amp_gain_max);
- return 0;
+}
+static const char * const macaudio_plugin_demux_texts[] = {
- "Speakers",
- "Headphones"
+};
+SOC_ENUM_SINGLE_VIRT_DECL(macaudio_plugin_demux_enum, macaudio_plugin_demux_texts);
+static int macaudio_plugin_demux_get(struct snd_kcontrol *kcontrol,
struct snd_ctl_elem_value *ucontrol)
+{
- struct snd_soc_dapm_context *dapm = snd_soc_dapm_kcontrol_dapm(kcontrol);
- struct macaudio_snd_data *ma = snd_soc_card_get_drvdata(dapm->card);
- /*
* TODO: Determine what locking is in order here...
*/
- ucontrol->value.enumerated.item[0] = ma->jack_plugin_state;
- return 0;
+}
+static int macaudio_jack_event(struct notifier_block *nb, unsigned long event,
void *data)
+{
- struct snd_soc_jack *jack = data;
- struct macaudio_snd_data *ma = snd_soc_card_get_drvdata(jack->card);
- ma->jack_plugin_state = !!event;
- if (!ma->plugin_demux_kcontrol)
return 0;
- snd_soc_dapm_mux_update_power(&ma->card.dapm, ma->plugin_demux_kcontrol,
ma->jack_plugin_state,
(struct soc_enum *) &macaudio_plugin_demux_enum, NULL);
the term 'plugin' can be understood in many ways by different audio folks. 'plugin' is usually the term used for processing libraries (VST, LADSPA, etc). I think here you meant 'jack control'?
So ‘jack control’ would be understood as the jack plugged/unplugged status?
- return 0;
+}
Martin
On 6/6/22 15:46, Martin Povišer wrote:
(I am having trouble delivering mail to linux.intel.com, so I reply to the list and CC at least...)
On 6. 6. 2022, at 22:02, Pierre-Louis Bossart pierre-louis.bossart@linux.intel.com wrote:
- Virtual FE/BE Playback Topology
- The platform driver has independent frontend and backend DAIs with the
- option of routing backends to any of the frontends. The platform
- driver configures the routing based on DPCM couplings in ASoC runtime
- structures, which in turn is determined from DAPM paths by ASoC. But the
- platform driver doesn't supply relevant DAPM paths and leaves that up for
- the machine driver to fill in. The filled-in virtual topology can be
- anything as long as a particular backend isn't connected to more than one
- frontend at any given time. (The limitation is due to the unsupported case
- of reparenting of live BEs.)
- The DAPM routing that this machine-level driver makes up has two use-cases
- in mind:
- Using a single PCM for playback such that it conditionally sinks to either
- speakers or headphones based on the plug-in state of the headphones jack.
- All the while making the switch transparent to userspace. This has the
- drawback of requiring a sample stream suited for both speakers and
- headphones, which is hard to come by on machines where tailored DSP for
- speakers in userspace is desirable or required.
- Driving the headphones and speakers from distinct PCMs, having userspace
- bridge the difference and apply different signal processing to the two.
- In the end the topology supplied by this driver looks like this:
- PCMs (frontends) I2S Port Groups (backends)
- ──────────────── ──────────────────────────
- ┌──────────┐ ┌───────────────► ┌─────┐ ┌──────────┐
- │ Primary ├───────┤ │ Mux │ ──► │ Speakers │
- └──────────┘ │ ┌──────────► └─────┘ └──────────┘
┌─── │ ───┘ ▲
- ┌──────────┐ │ │ │
- │Secondary ├──┘ │ ┌────────────┴┐
- └──────────┘ ├────►│Plug-in Demux│
│ └────────────┬┘
│ │
│ ▼
│ ┌─────┐ ┌──────────┐
└───────────────► │ Mux │ ──► │Headphones│
└─────┘ └──────────┘
- */
In Patch2, the 'clusters' are described as front-ends, with I2S as back-ends. Here the PCMs are described as front-ends, but there's no mention of clusters. Either one of the two descriptions is outdated, or there's something missing to help reconcile the two pieces of information?
Both descriptions should be in sync. Maybe I don’t know the proper terminology. In both cases the frontend is meant to be the actual I2S transceiver unit, and backend the I2S port on the SoC’s periphery, which can be routed to any of transceiver units. (Multiple ports can be routed to the same unit, which means the ports will have the same clocks and data line -- that's a configuration we need to support to drive some of the speaker arrays, hence the backend/frontend distinction).
Maybe I am using 'PCM' in a confusing way here? What I meant is a subdevice that’s visible from userspace, because I have seen it used that way in ALSA codebase.
Yes, I think most people familiar with DPCM would take the 'PCM frontend' as some sort of generic DMA transfer from system memory, while the 'back end' is more the actual serial link. In your case, the front-end is already very low-level and tied to I2S already. I think that's fine, it's just that using different terms for 'cluster' and 'PCM' in different patches could lead to confusions.
+static int macaudio_get_runtime_mclk_fs(struct snd_pcm_substream *substream) +{
- struct snd_soc_pcm_runtime *rtd = asoc_substream_to_rtd(substream);
- struct macaudio_snd_data *ma = snd_soc_card_get_drvdata(rtd->card);
- struct snd_soc_dpcm *dpcm;
- /*
* If this is a FE, look it up in link_props directly.
* If this is a BE, look it up in the respective FE.
*/
- if (!rtd->dai_link->no_pcm)
return ma->link_props[rtd->dai_link->id].mclk_fs;
- for_each_dpcm_fe(rtd, substream->stream, dpcm) {
int fe_id = dpcm->fe->dai_link->id;
return ma->link_props[fe_id].mclk_fs;
- }
I am not sure what the concept of mclk would mean for a front-end? This is typically very I2S-specific, i.e. a back-end property, no?
Right, that’s a result of the confusion from above. Hope I cleared it up somehow. The frontend already decides the clocks and data serialization, hence mclk/fs is a frontend-prop here.
What confuses me in this code is that it looks possible that the front- and back-end could have different mclk values? I think a comment is missing that the values are identical, it's just that there's a different way to access it depending on the link type?
+static int macaudio_be_init(struct snd_soc_pcm_runtime *rtd) +{
- struct snd_soc_card *card = rtd->card;
- struct macaudio_snd_data *ma = snd_soc_card_get_drvdata(card);
- struct macaudio_link_props *props = &ma->link_props[rtd->dai_link->id];
- struct snd_soc_dai *dai;
- int i, ret;
- ret = macaudio_be_assign_tdm(rtd);
- if (ret < 0)
return ret;
- if (props->is_headphones) {
for_each_rtd_codec_dais(rtd, i, dai)
snd_soc_component_set_jack(dai->component, &ma->jack, NULL);
- }
this is weird, set_jack() is invoked by the ASoC core. You shouldn't need to do this?
That’s interesting. Where would it be invoked? How does ASoC know which codec it attaches to?
sorry, my comment was partly invalid.
set_jack() is invoked in the machine driver indeed, what I found strange is that you may have different codecs handling the jack? What is the purpose of that loop?
+static int macaudio_jack_event(struct notifier_block *nb, unsigned long event,
void *data);
+static struct notifier_block macaudio_jack_nb = {
- .notifier_call = macaudio_jack_event,
+};
why is this needed? we have already many ways of dealing with the jack events (dare I say too many ways?).
Because I want to update the DAPM paths based on the jack status, specifically I want to set macaudio_plugin_demux. I don’t know how else it could be done.
I don't know either but I have never seen notifier blocks being used. I would think there are already ways to do this with DAPM events.
+static int macaudio_jack_event(struct notifier_block *nb, unsigned long event,
void *data)
+{
- struct snd_soc_jack *jack = data;
- struct macaudio_snd_data *ma = snd_soc_card_get_drvdata(jack->card);
- ma->jack_plugin_state = !!event;
- if (!ma->plugin_demux_kcontrol)
return 0;
- snd_soc_dapm_mux_update_power(&ma->card.dapm, ma->plugin_demux_kcontrol,
ma->jack_plugin_state,
(struct soc_enum *) &macaudio_plugin_demux_enum, NULL);
the term 'plugin' can be understood in many ways by different audio folks. 'plugin' is usually the term used for processing libraries (VST, LADSPA, etc). I think here you meant 'jack control'?
So ‘jack control’ would be understood as the jack plugged/unplugged status?
The 'Headphone Jack' or 'Headset Mic Jack' kcontrols typically track the status. Other terms are 'jack detection'. "plugin" is not a very common term here.
On 6. 6. 2022, at 23:22, Pierre-Louis Bossart pierre-louis.bossart@linux.intel.com wrote:
On 6/6/22 15:46, Martin Povišer wrote:
(I am having trouble delivering mail to linux.intel.com, so I reply to the list and CC at least...)
On 6. 6. 2022, at 22:02, Pierre-Louis Bossart pierre-louis.bossart@linux.intel.com wrote:
- Virtual FE/BE Playback Topology
- The platform driver has independent frontend and backend DAIs with the
- option of routing backends to any of the frontends. The platform
- driver configures the routing based on DPCM couplings in ASoC runtime
- structures, which in turn is determined from DAPM paths by ASoC. But the
- platform driver doesn't supply relevant DAPM paths and leaves that up for
- the machine driver to fill in. The filled-in virtual topology can be
- anything as long as a particular backend isn't connected to more than one
- frontend at any given time. (The limitation is due to the unsupported case
- of reparenting of live BEs.)
- The DAPM routing that this machine-level driver makes up has two use-cases
- in mind:
- Using a single PCM for playback such that it conditionally sinks to either
- speakers or headphones based on the plug-in state of the headphones jack.
- All the while making the switch transparent to userspace. This has the
- drawback of requiring a sample stream suited for both speakers and
- headphones, which is hard to come by on machines where tailored DSP for
- speakers in userspace is desirable or required.
- Driving the headphones and speakers from distinct PCMs, having userspace
- bridge the difference and apply different signal processing to the two.
- In the end the topology supplied by this driver looks like this:
- PCMs (frontends) I2S Port Groups (backends)
- ──────────────── ──────────────────────────
- ┌──────────┐ ┌───────────────► ┌─────┐ ┌──────────┐
- │ Primary ├───────┤ │ Mux │ ──► │ Speakers │
- └──────────┘ │ ┌──────────► └─────┘ └──────────┘
┌─── │ ───┘ ▲
- ┌──────────┐ │ │ │
- │Secondary ├──┘ │ ┌────────────┴┐
- └──────────┘ ├────►│Plug-in Demux│
│ └────────────┬┘
│ │
│ ▼
│ ┌─────┐ ┌──────────┐
└───────────────► │ Mux │ ──► │Headphones│
└─────┘ └──────────┘
- */
In Patch2, the 'clusters' are described as front-ends, with I2S as back-ends. Here the PCMs are described as front-ends, but there's no mention of clusters. Either one of the two descriptions is outdated, or there's something missing to help reconcile the two pieces of information?
Both descriptions should be in sync. Maybe I don’t know the proper terminology. In both cases the frontend is meant to be the actual I2S transceiver unit, and backend the I2S port on the SoC’s periphery, which can be routed to any of transceiver units. (Multiple ports can be routed to the same unit, which means the ports will have the same clocks and data line -- that's a configuration we need to support to drive some of the speaker arrays, hence the backend/frontend distinction).
Maybe I am using 'PCM' in a confusing way here? What I meant is a subdevice that’s visible from userspace, because I have seen it used that way in ALSA codebase.
Yes, I think most people familiar with DPCM would take the 'PCM frontend' as some sort of generic DMA transfer from system memory, while the 'back end' is more the actual serial link. In your case, the front-end is already very low-level and tied to I2S already. I think that's fine, it's just that using different terms for 'cluster' and 'PCM' in different patches could lead to confusions.
OK, will take this into account then.
+static int macaudio_get_runtime_mclk_fs(struct snd_pcm_substream *substream) +{
- struct snd_soc_pcm_runtime *rtd = asoc_substream_to_rtd(substream);
- struct macaudio_snd_data *ma = snd_soc_card_get_drvdata(rtd->card);
- struct snd_soc_dpcm *dpcm;
- /*
* If this is a FE, look it up in link_props directly.
* If this is a BE, look it up in the respective FE.
*/
- if (!rtd->dai_link->no_pcm)
return ma->link_props[rtd->dai_link->id].mclk_fs;
- for_each_dpcm_fe(rtd, substream->stream, dpcm) {
int fe_id = dpcm->fe->dai_link->id;
return ma->link_props[fe_id].mclk_fs;
- }
I am not sure what the concept of mclk would mean for a front-end? This is typically very I2S-specific, i.e. a back-end property, no?
Right, that’s a result of the confusion from above. Hope I cleared it up somehow. The frontend already decides the clocks and data serialization, hence mclk/fs is a frontend-prop here.
What confuses me in this code is that it looks possible that the front- and back-end could have different mclk values? I think a comment is missing that the values are identical, it's just that there's a different way to access it depending on the link type?
Well, that’s exactly what I am trying to convey by the comment above in macaudio_get_runtime_mclk_fs. Maybe I should do something to make it more obvious.
+static int macaudio_be_init(struct snd_soc_pcm_runtime *rtd) +{
- struct snd_soc_card *card = rtd->card;
- struct macaudio_snd_data *ma = snd_soc_card_get_drvdata(card);
- struct macaudio_link_props *props = &ma->link_props[rtd->dai_link->id];
- struct snd_soc_dai *dai;
- int i, ret;
- ret = macaudio_be_assign_tdm(rtd);
- if (ret < 0)
return ret;
- if (props->is_headphones) {
for_each_rtd_codec_dais(rtd, i, dai)
snd_soc_component_set_jack(dai->component, &ma->jack, NULL);
- }
this is weird, set_jack() is invoked by the ASoC core. You shouldn't need to do this?
That’s interesting. Where would it be invoked? How does ASoC know which codec it attaches to?
sorry, my comment was partly invalid.
set_jack() is invoked in the machine driver indeed, what I found strange is that you may have different codecs handling the jack? What is the purpose of that loop?
There’s no need for the loop, there’s a single codec. OK, will remove the loop to make it less confusing.
+static int macaudio_jack_event(struct notifier_block *nb, unsigned long event,
void *data);
+static struct notifier_block macaudio_jack_nb = {
- .notifier_call = macaudio_jack_event,
+};
why is this needed? we have already many ways of dealing with the jack events (dare I say too many ways?).
Because I want to update the DAPM paths based on the jack status, specifically I want to set macaudio_plugin_demux. I don’t know how else it could be done.
I don't know either but I have never seen notifier blocks being used. I would think there are already ways to do this with DAPM events.
+static int macaudio_jack_event(struct notifier_block *nb, unsigned long event,
void *data)
+{
- struct snd_soc_jack *jack = data;
- struct macaudio_snd_data *ma = snd_soc_card_get_drvdata(jack->card);
- ma->jack_plugin_state = !!event;
- if (!ma->plugin_demux_kcontrol)
return 0;
- snd_soc_dapm_mux_update_power(&ma->card.dapm, ma->plugin_demux_kcontrol,
ma->jack_plugin_state,
(struct soc_enum *) &macaudio_plugin_demux_enum, NULL);
the term 'plugin' can be understood in many ways by different audio folks. 'plugin' is usually the term used for processing libraries (VST, LADSPA, etc). I think here you meant 'jack control'?
So ‘jack control’ would be understood as the jack plugged/unplugged status?
The 'Headphone Jack' or 'Headset Mic Jack' kcontrols typically track the status. Other terms are 'jack detection'. "plugin" is not a very common term here.
OK
On Mon, Jun 06, 2022 at 09:19:10PM +0200, Martin Povišer wrote:
- ┌──────────┐ ┌───────────────► ┌─────┐ ┌──────────┐
- │ Primary ├───────┤ │ Mux │ ──► │ Speakers │
- └──────────┘ │ ┌──────────► └─────┘ └──────────┘
┌─── │ ───┘ ▲
- ┌──────────┐ │ │ │
- │Secondary ├──┘ │ ┌────────────┴┐
- └──────────┘ ├────►│Plug-in Demux│
│ └────────────┬┘
│ │
│ ▼
│ ┌─────┐ ┌──────────┐
└───────────────► │ Mux │ ──► │Headphones│
└─────┘ └──────────┘
As far as I can tell this demux is entirely software based - why not just expose the routing control to userspace and let it handle switching (which I suspect may be more featureful than what's implemented here)?
+static int macaudio_jack_event(struct notifier_block *nb, unsigned long event,
void *data)
+{
struct snd_soc_jack *jack = data;
struct macaudio_snd_data *ma = snd_soc_card_get_drvdata(jack->card);
ma->jack_plugin_state = !!event;
if (!ma->plugin_demux_kcontrol)
return 0;
snd_soc_dapm_mux_update_power(&ma->card.dapm, ma->plugin_demux_kcontrol,
ma->jack_plugin_state,
(struct soc_enum *) &macaudio_plugin_demux_enum, NULL);
return 0;
+}
This should be integrated with the core jack detection stuff in soc-jack.c and/or the core stuff that's wrapping - that way you'll ensure that events are generated and status readable via all the interfaces userspace might be looking for. The ASoC stuff also has some DAPM integration for turning on/off outputs which might DTRT for you if you do need it in kernel.
On 9. 6. 2022, at 15:16, Mark Brown broonie@kernel.org wrote:
On Mon, Jun 06, 2022 at 09:19:10PM +0200, Martin Povišer wrote:
- ┌──────────┐ ┌───────────────► ┌─────┐ ┌──────────┐
- │ Primary ├───────┤ │ Mux │ ──► │ Speakers │
- └──────────┘ │ ┌──────────► └─────┘ └──────────┘
┌─── │ ───┘ ▲
- ┌──────────┐ │ │ │
- │Secondary ├──┘ │ ┌────────────┴┐
- └──────────┘ ├────►│Plug-in Demux│
│ └────────────┬┘
│ │
│ ▼
│ ┌─────┐ ┌──────────┐
└───────────────► │ Mux │ ──► │Headphones│
└─────┘ └──────────┘
As far as I can tell this demux is entirely software based - why not just expose the routing control to userspace and let it handle switching (which I suspect may be more featureful than what's implemented here)?
Well, userspace should have the other two muxes at its disposal to implement any routing/switching it wishes -- but in addition we are also offering letting kernel take care of the switching, by pointing the muxes to the demux.
I assume (but I don’t know the extent of what’s possible with UCM files), that this will be of some value to users running plain ALSA with no sound server.
+static int macaudio_jack_event(struct notifier_block *nb, unsigned long event,
void *data)
+{
struct snd_soc_jack *jack = data;
struct macaudio_snd_data *ma = snd_soc_card_get_drvdata(jack->card);
ma->jack_plugin_state = !!event;
if (!ma->plugin_demux_kcontrol)
return 0;
snd_soc_dapm_mux_update_power(&ma->card.dapm, ma->plugin_demux_kcontrol,
ma->jack_plugin_state,
(struct soc_enum *) &macaudio_plugin_demux_enum, NULL);
return 0;
+}
This should be integrated with the core jack detection stuff in soc-jack.c and/or the core stuff that's wrapping - that way you'll ensure that events are generated and status readable via all the interfaces userspace might be looking for. The ASoC stuff also has some DAPM integration for turning on/off outputs which might DTRT for you if you do need it in kernel.
Aren’t all the right events to userspace generated already by the codec calling snd_soc_jack_report?
I looked at the existing DAPM integration but I couldn’t figure out how to switch the demux with it.
On Thu, Jun 09, 2022 at 03:42:09PM +0200, Martin Povišer wrote:
On 9. 6. 2022, at 15:16, Mark Brown broonie@kernel.org wrote:
As far as I can tell this demux is entirely software based - why not just expose the routing control to userspace and let it handle switching (which I suspect may be more featureful than what's implemented here)?
Well, userspace should have the other two muxes at its disposal to implement any routing/switching it wishes -- but in addition we are also offering letting kernel take care of the switching, by pointing the muxes to the demux.
I assume (but I don’t know the extent of what’s possible with UCM files), that this will be of some value to users running plain ALSA with no sound server.
That's basically no userspaces at this point TBH. I'm not convinced it's a good idea to be adding custom code for that use case.
This should be integrated with the core jack detection stuff in soc-jack.c and/or the core stuff that's wrapping - that way you'll ensure that events are generated and status readable via all the interfaces userspace might be looking for. The ASoC stuff also has some DAPM integration for turning on/off outputs which might DTRT for you if you do need it in kernel.
Aren’t all the right events to userspace generated already by the codec calling snd_soc_jack_report?
I wasn't able to find any references to snd_soc_jack_report() in your series?
I looked at the existing DAPM integration but I couldn’t figure out how to switch the demux with it.
Yes, it won't do that. If you can't stream the same audio to both then you'd need something else.
On Mon, Jun 06, 2022 at 09:19:10PM +0200, Martin Povišer wrote:
/*
* Primary FE
*
* The mclk/fs ratio at 64 for the primary frontend is important
* to ensure that the headphones codec's idea of left and right
* in a stereo stream over I2S fits in nicely with everyone else's.
* (This is until the headphones codec's driver supports
* set_tdm_slot.)
*
* The low mclk/fs ratio precludes transmitting more than two
* channels over I2S, but that's okay since there is the secondary
* FE for speaker arrays anyway.
*/
.mclk_fs = 64,
- },
This seems weird - it looks like it's confusing MCLK and the bit clock for the audio bus. These are two different clocks. Note that it's very common for devices to require a higher MCLK/fs ratio to deliver the best audio performance, 256fs is standard.
- {
/*
* Secondary FE
*
* Here we want frames plenty long to be able to drive all
* those fancy speaker arrays.
*/
.mclk_fs = 256,
- }
Same thing here - this is at least confusing MCLK and the bit clock.
+static bool macaudio_match_kctl_name(const char *pattern, const char *name) +{
- if (pattern[0] == '*') {
int namelen, patternlen;
pattern++;
if (pattern[0] == ' ')
pattern++;
namelen = strlen(name);
patternlen = strlen(pattern);
if (namelen > patternlen)
name += (namelen - patternlen);
- }
- return !strcmp(name, pattern);
+}
+static int macaudio_limit_volume(struct snd_soc_card *card,
const char *pattern, int max)
+{
- struct snd_kcontrol *kctl;
- struct soc_mixer_control *mc;
- int found = 0;
- list_for_each_entry(kctl, &card->snd_card->controls, list) {
if (!macaudio_match_kctl_name(pattern, kctl->id.name))
continue;
found++;
dev_dbg(card->dev, "limiting volume on '%s'\n", kctl->id.name);
/*
* TODO: This doesn't decrease the volume if it's already
* above the limit!
*/
mc = (struct soc_mixer_control *)kctl->private_value;
if (max <= mc->max)
mc->platform_max = max;
- }
- return found;
+}
This shouldn't be open coded in a driver, please factor it out into the core so we've got an API for "set limit X on control Y" then call that.
On 9. 6. 2022, at 15:33, Mark Brown broonie@kernel.org wrote:
On Mon, Jun 06, 2022 at 09:19:10PM +0200, Martin Povišer wrote:
/*
* Primary FE
*
* The mclk/fs ratio at 64 for the primary frontend is important
* to ensure that the headphones codec's idea of left and right
* in a stereo stream over I2S fits in nicely with everyone else's.
* (This is until the headphones codec's driver supports
* set_tdm_slot.)
*
* The low mclk/fs ratio precludes transmitting more than two
* channels over I2S, but that's okay since there is the secondary
* FE for speaker arrays anyway.
*/
.mclk_fs = 64,
- },
This seems weird - it looks like it's confusing MCLK and the bit clock for the audio bus. These are two different clocks. Note that it's very common for devices to require a higher MCLK/fs ratio to deliver the best audio performance, 256fs is standard.
On these machines we are not producing any other clock for the codecs besides the bit clock, so I am using MCLK interchangeably for it. (It is what the sample rate is derived from after all.)
One of the codec drivers this is to be used with (cs42l42) expects to be given the I2S bit clock with
snd_soc_dai_set_sysclk(dai, 0, mclk, SND_SOC_CLOCK_IN);
I can rename mclk to bclk in all of the code to make it clearer maybe. Also the platform driver can take the bit clock value from set_bclk_ratio, instead of set_sysclk from where it takes it now. The cs42l42 driver I can patch too to accept set_bclk_ratio.
- {
/*
* Secondary FE
*
* Here we want frames plenty long to be able to drive all
* those fancy speaker arrays.
*/
.mclk_fs = 256,
- }
Same thing here - this is at least confusing MCLK and the bit clock.
+static bool macaudio_match_kctl_name(const char *pattern, const char *name) +{
- if (pattern[0] == '*') {
int namelen, patternlen;
pattern++;
if (pattern[0] == ' ')
pattern++;
namelen = strlen(name);
patternlen = strlen(pattern);
if (namelen > patternlen)
name += (namelen - patternlen);
- }
- return !strcmp(name, pattern);
+}
+static int macaudio_limit_volume(struct snd_soc_card *card,
const char *pattern, int max)
+{
- struct snd_kcontrol *kctl;
- struct soc_mixer_control *mc;
- int found = 0;
- list_for_each_entry(kctl, &card->snd_card->controls, list) {
if (!macaudio_match_kctl_name(pattern, kctl->id.name))
continue;
found++;
dev_dbg(card->dev, "limiting volume on '%s'\n", kctl->id.name);
/*
* TODO: This doesn't decrease the volume if it's already
* above the limit!
*/
mc = (struct soc_mixer_control *)kctl->private_value;
if (max <= mc->max)
mc->platform_max = max;
- }
- return found;
+}
This shouldn't be open coded in a driver, please factor it out into the core so we've got an API for "set limit X on control Y" then call that.
There’s already snd_soc_limit_volume, but it takes a fixed control name. Can I extend it to understand patterns beginning with a wildcard, like '* Amp Gain Volume’?
On Thu, Jun 09, 2022 at 04:09:57PM +0200, Martin Povišer wrote:
On 9. 6. 2022, at 15:33, Mark Brown broonie@kernel.org wrote:
/*
* Primary FE
*
* The mclk/fs ratio at 64 for the primary frontend is important
* to ensure that the headphones codec's idea of left and right
* in a stereo stream over I2S fits in nicely with everyone else's.
* (This is until the headphones codec's driver supports
* set_tdm_slot.)
*
* The low mclk/fs ratio precludes transmitting more than two
* channels over I2S, but that's okay since there is the secondary
* FE for speaker arrays anyway.
*/
.mclk_fs = 64,
- },
This seems weird - it looks like it's confusing MCLK and the bit clock for the audio bus. These are two different clocks. Note that it's very common for devices to require a higher MCLK/fs ratio to deliver the best audio performance, 256fs is standard.
On these machines we are not producing any other clock for the codecs besides the bit clock, so I am using MCLK interchangeably for it. (It is what the sample rate is derived from after all.)
Please don't do this, you're just making everything needlessly hard to understand by using standard terminology inappropriately and there's a risk of breakage further down the line with drivers implementing the standard ops.
One of the codec drivers this is to be used with (cs42l42) expects to be given the I2S bit clock with
snd_soc_dai_set_sysclk(dai, 0, mclk, SND_SOC_CLOCK_IN);
That's very, very non-standard...
I can rename mclk to bclk in all of the code to make it clearer maybe. Also the platform driver can take the bit clock value from set_bclk_ratio, instead of set_sysclk from where it takes it now. The cs42l42 driver I can patch too to accept set_bclk_ratio.
...indeed, set_bclk_ratio() is a better interface for setting the bclk ratio - the CODEC driver should really be doing that as well.
This shouldn't be open coded in a driver, please factor it out into the core so we've got an API for "set limit X on control Y" then call that.
There’s already snd_soc_limit_volume, but it takes a fixed control name. Can I extend it to understand patterns beginning with a wildcard, like '* Amp Gain Volume’?
Or add a new call that does that.
On 9. 6. 2022, at 17:16, Mark Brown broonie@kernel.org wrote:
On Thu, Jun 09, 2022 at 04:09:57PM +0200, Martin Povišer wrote:
On 9. 6. 2022, at 15:33, Mark Brown broonie@kernel.org wrote:
/*
* Primary FE
*
* The mclk/fs ratio at 64 for the primary frontend is important
* to ensure that the headphones codec's idea of left and right
* in a stereo stream over I2S fits in nicely with everyone else's.
* (This is until the headphones codec's driver supports
* set_tdm_slot.)
*
* The low mclk/fs ratio precludes transmitting more than two
* channels over I2S, but that's okay since there is the secondary
* FE for speaker arrays anyway.
*/
.mclk_fs = 64,
- },
This seems weird - it looks like it's confusing MCLK and the bit clock for the audio bus. These are two different clocks. Note that it's very common for devices to require a higher MCLK/fs ratio to deliver the best audio performance, 256fs is standard.
On these machines we are not producing any other clock for the codecs besides the bit clock, so I am using MCLK interchangeably for it. (It is what the sample rate is derived from after all.)
Please don't do this, you're just making everything needlessly hard to understand by using standard terminology inappropriately and there's a risk of breakage further down the line with drivers implementing the standard ops.
OK
One of the codec drivers this is to be used with (cs42l42) expects to be given the I2S bit clock with
snd_soc_dai_set_sysclk(dai, 0, mclk, SND_SOC_CLOCK_IN);
That's very, very non-standard...
I can rename mclk to bclk in all of the code to make it clearer maybe. Also the platform driver can take the bit clock value from set_bclk_ratio, instead of set_sysclk from where it takes it now. The cs42l42 driver I can patch too to accept set_bclk_ratio.
...indeed, set_bclk_ratio() is a better interface for setting the bclk ratio - the CODEC driver should really be doing that as well.
OK, adding that to my TODOs.
This shouldn't be open coded in a driver, please factor it out into the core so we've got an API for "set limit X on control Y" then call that.
There’s already snd_soc_limit_volume, but it takes a fixed control name. Can I extend it to understand patterns beginning with a wildcard, like '* Amp Gain Volume’?
Or add a new call that does that.
OK
On Mon, Jun 06, 2022 at 09:19:05PM +0200, Martin Povišer wrote:
- The way the platform/machine driver handles the fact that multiple I2S ports (now backend DAIs) can be driven by/connected to the same SERDES unit (now in effect a frontend DAI). After previous discussion I have transitioned to DPCM to model this. I took the opportunity of dynamic backend/frontend routing to support speakers/headphones runtime switching. More on this in comments at top of the machine and platform driver.
This looks roughly like I'd expect now, there's some issues from myself and Pierre but it's more around the edges than anything big picture.
On Mon, 6 Jun 2022 21:19:05 +0200, Martin Povišer wrote:
This is again RFC with a machine-level ASoC driver for recent Apple Macs with the M1 line of chips. This time I attached the platform driver too for good measure. What I am interested in the most is checking the overall approach, especially on two points (both in some ways already discussed in previous RFC [0]):
- The way the platform/machine driver handles the fact that multiple I2S ports (now backend DAIs) can be driven by/connected to the same SERDES unit (now in effect a frontend DAI). After previous discussion I have transitioned to DPCM to model this. I took the opportunity of dynamic backend/frontend routing to support speakers/headphones runtime switching. More on this in comments at top of the machine and platform driver.
[...]
Applied to
https://git.kernel.org/pub/scm/linux/kernel/git/broonie/sound.git for-next
Thanks!
[4/5] ASoC: Introduce 'fixup_controls' card method commit: df4d27b19b892f464685ea45fa6132dd1a2b6864
All being well this means that it will be integrated into the linux-next tree (usually sometime in the next 24 hours) and sent to Linus during the next merge window (or sooner if it is a bug fix), however if problems are discovered then the patch may be dropped or reverted.
You may get further e-mails resulting from automated or manual testing and review of the tree, please engage with people reporting problems and send followup patches addressing any issues that are reported if needed.
If any updates are required or you are submitting further changes they should be sent as incremental updates against current git, existing patches will not be replaced.
Please add any relevant lists and maintainers to the CCs when replying to this mail.
Thanks, Mark
participants (4)
-
Mark Brown
-
Martin Povišer
-
Pierre-Louis Bossart
-
Rob Herring