From nobody Tue Apr 28 03:57:09 2026 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by smtp.lore.kernel.org (Postfix) with ESMTP id E6FE4C433EF for ; Mon, 6 Jun 2022 19:20:06 +0000 (UTC) Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S232195AbiFFTUF (ORCPT ); Mon, 6 Jun 2022 15:20:05 -0400 Received: from lindbergh.monkeyblade.net ([23.128.96.19]:49432 "EHLO lindbergh.monkeyblade.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S229758AbiFFTT6 (ORCPT ); Mon, 6 Jun 2022 15:19:58 -0400 Received: from hutie.ust.cz (unknown [IPv6:2a03:3b40:fe:f0::1]) by lindbergh.monkeyblade.net (Postfix) with ESMTPS id DF835113B75; Mon, 6 Jun 2022 12:19:54 -0700 (PDT) From: =?UTF-8?q?Martin=20Povi=C5=A1er?= DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=cutebit.org; s=mail; t=1654543192; bh=Vla6Rhfl/gMBj6xgJ0VP7fQelpDgmC8OEg+qQWxrpmQ=; h=From:To:Cc:Subject:Date:In-Reply-To:References; b=Ny6DtajuT28g+Hn74q+dQy3n8P98yg8HxM2Q2EGHrASbKTqYagLJ4C99wA52Wy8aJ EfSFqLu6+/CcTDGF6vq6pnWADSLQltOYIeVoqG+8EnqMZaRLewBpRxvRPV/2A5x9W3 blGBSzzqkwiD2T1MsUTMV4vEQg9LMMN83v7byraY= To: Liam Girdwood , Mark Brown , Rob Herring , Krzysztof Kozlowski , Jaroslav Kysela , Takashi Iwai Cc: =?UTF-8?q?Martin=20Povi=C5=A1er?= , alsa-devel@alsa-project.org, devicetree@vger.kernel.org, linux-kernel@vger.kernel.org, Mark Kettenis , Hector Martin , Sven Peter , asahi@lists.linux.dev Subject: [RFC PATCH v2 1/5] dt-bindings: sound: Add Apple MCA I2S transceiver Date: Mon, 6 Jun 2022 21:19:06 +0200 Message-Id: <20220606191910.16580-2-povik+lin@cutebit.org> In-Reply-To: <20220606191910.16580-1-povik+lin@cutebit.org> References: <20220606191910.16580-1-povik+lin@cutebit.org> MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable Precedence: bulk List-ID: X-Mailing-List: linux-kernel@vger.kernel.org Add binding schema for MCA I2S transceiver found on Apple M1 and other chips. Signed-off-by: Martin Povi=C5=A1er --- .../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/Docum= entation/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 independent= ly + or in an interlinked fashion. Up to 6 clusters have been seen on an MCA. + +maintainers: + - Martin Povi=C5=A1er + +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. T= hey + are listed in groups of four per cluster, and within the cluster the= y 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 =3D "apple,t6000-mca", "apple,mca"; + reg =3D <0x9b600000 0x10000>, + <0x9b500000 0x20000>; + + clocks =3D <&nco 0>, <&nco 1>, <&nco 2>, <&nco 3>; + power-domains =3D <&ps_audio_p>, <&ps_mca0>, <&ps_mca1>, + <&ps_mca2>, <&ps_mca3>; + dmas =3D <&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 =3D "tx0a", "rx0a", "tx0b", "rx0b", + "tx1a", "rx1a", "tx1b", "rx1b", + "tx2a", "rx2a", "tx2b", "rx2b", + "tx3a", "rx3a", "tx3b", "rx3b"; + + #sound-dai-cells =3D <1>; + }; --=20 2.33.0 From nobody Tue Apr 28 03:57:09 2026 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by smtp.lore.kernel.org (Postfix) with ESMTP id ADF00C43334 for ; Mon, 6 Jun 2022 19:20:20 +0000 (UTC) Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S232180AbiFFTUS (ORCPT ); Mon, 6 Jun 2022 15:20:18 -0400 Received: from lindbergh.monkeyblade.net ([23.128.96.19]:49474 "EHLO lindbergh.monkeyblade.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S229819AbiFFTT6 (ORCPT ); Mon, 6 Jun 2022 15:19:58 -0400 Received: from hutie.ust.cz (unknown [IPv6:2a03:3b40:fe:f0::1]) by lindbergh.monkeyblade.net (Postfix) with ESMTPS id 13E83113B77; Mon, 6 Jun 2022 12:19:54 -0700 (PDT) From: =?UTF-8?q?Martin=20Povi=C5=A1er?= DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=cutebit.org; s=mail; t=1654543192; bh=QBs7UtuS1Zqfk21JmPJtrPH28UjdCT91NFOWXH9PRtA=; h=From:To:Cc:Subject:Date:In-Reply-To:References; b=mdDf+14SP4WSk9ObzLihhJ+lqyl6VWwKgHzVVo5GUwW8+ZDIM1sS2y/jjgmk2buER 5P+G59wXVOHyxvMsoh1kEr3p0jkusK+lPcewH/5ba2fshFuYWUlcUoQ0T1AIFbhAs5 NkruLCpMelDMckZFD4dJ4Sgh61XuBcoMGO3V1dEY= To: Liam Girdwood , Mark Brown , Rob Herring , Krzysztof Kozlowski , Jaroslav Kysela , Takashi Iwai Cc: =?UTF-8?q?Martin=20Povi=C5=A1er?= , alsa-devel@alsa-project.org, devicetree@vger.kernel.org, linux-kernel@vger.kernel.org, Mark Kettenis , Hector Martin , Sven Peter , asahi@lists.linux.dev Subject: [RFC PATCH v2 2/5] dt-bindings: sound: Add Apple Macs sound peripherals Date: Mon, 6 Jun 2022 21:19:07 +0200 Message-Id: <20220606191910.16580-3-povik+lin@cutebit.org> In-Reply-To: <20220606191910.16580-1-povik+lin@cutebit.org> References: <20220606191910.16580-1-povik+lin@cutebit.org> MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable Precedence: bulk List-ID: X-Mailing-List: linux-kernel@vger.kernel.org Add binding for Apple Silicon Macs' machine-level integration of sound peripherals. Signed-off-by: Martin Povi=C5=A1er --- .../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=C5=A1er + +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 th= is 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 =3D "apple,t6000-mca", "apple,mca"; + reg =3D <0x9b600000 0x10000>, + <0x9b500000 0x20000>; + + clocks =3D <&nco 0>, <&nco 1>, <&nco 2>, <&nco 3>; + power-domains =3D <&ps_audio_p>, <&ps_mca0>, <&ps_mca1>, + <&ps_mca2>, <&ps_mca3>; + dmas =3D <&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 =3D "tx0a", "rx0a", "tx0b", "rx0b", + "tx1a", "rx1a", "tx1b", "rx1b", + "tx2a", "rx2a", "tx2b", "rx2b", + "tx3a", "rx3a", "tx3b", "rx3b"; + + #sound-dai-cells =3D <1>; + }; + + sound { + compatible =3D "apple,j314-macaudio", "apple,macaudio"; + model =3D "MacBook Pro J314 integrated audio"; + + #address-cells =3D <1>; + #size-cells =3D <0>; + + dai-link@0 { + reg =3D <0>; + link-name =3D "Speakers"; + + cpu { + sound-dai =3D <&mca 0>, <&mca 1>; + }; + codec { + sound-dai =3D <&speaker_left_woof1>, + <&speaker_left_tweet>, + <&speaker_left_woof2>, + <&speaker_right_woof1>, + <&speaker_right_tweet>, + <&speaker_right_woof2>; + }; + }; + + dai-link@1 { + reg =3D <1>; + link-name =3D "Headphones Jack"; + + cpu { + sound-dai =3D <&mca 2>; + }; + codec { + sound-dai =3D <&jack_codec>; + }; + }; + }; --=20 2.33.0 From nobody Tue Apr 28 03:57:09 2026 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by smtp.lore.kernel.org (Postfix) with ESMTP id 097E0C43334 for ; Mon, 6 Jun 2022 19:20:25 +0000 (UTC) Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S229518AbiFFTUX (ORCPT ); Mon, 6 Jun 2022 15:20:23 -0400 Received: from lindbergh.monkeyblade.net ([23.128.96.19]:49586 "EHLO lindbergh.monkeyblade.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S231918AbiFFTT7 (ORCPT ); Mon, 6 Jun 2022 15:19:59 -0400 Received: from hutie.ust.cz (unknown [IPv6:2a03:3b40:fe:f0::1]) by lindbergh.monkeyblade.net (Postfix) with ESMTPS id D915A1157C2; Mon, 6 Jun 2022 12:19:55 -0700 (PDT) From: =?UTF-8?q?Martin=20Povi=C5=A1er?= DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=cutebit.org; s=mail; t=1654543193; bh=Whh40fwqWZcpJb1EeiKoDhm+wZgTLHWIxoTYHUfhcmI=; h=From:To:Cc:Subject:Date:In-Reply-To:References; b=YVdYlCF/6HnCGu27UM3jCiMcQ3jK8IwNAsO1N6nxTfCxdrkqZNwXqvYPcbmLLc43J 8j5G2TTHfZKtXY/PaQ2Qh0nAbrQpirKPPRs167zPkld+Q8DEwXUEsNvM/se6NVztIn SIiWc6W4/3FGbVy3BcjUKVfLZ4KJbYuIz2HzyzZg= To: Liam Girdwood , Mark Brown , Rob Herring , Krzysztof Kozlowski , Jaroslav Kysela , Takashi Iwai Cc: =?UTF-8?q?Martin=20Povi=C5=A1er?= , alsa-devel@alsa-project.org, devicetree@vger.kernel.org, linux-kernel@vger.kernel.org, Mark Kettenis , Hector Martin , Sven Peter , asahi@lists.linux.dev Subject: [RFC PATCH v2 3/5] ASoC: apple: Add MCA platform driver for Apple SoCs Date: Mon, 6 Jun 2022 21:19:08 +0200 Message-Id: <20220606191910.16580-4-povik+lin@cutebit.org> In-Reply-To: <20220606191910.16580-1-povik+lin@cutebit.org> References: <20220606191910.16580-1-povik+lin@cutebit.org> MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable Precedence: bulk List-ID: X-Mailing-List: linux-kernel@vger.kernel.org Add ASoC platform driver for the MCA peripheral found on Apple M1 and other chips. Signed-off-by: Martin Povi=C5=A1er --- 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) +=3D snd-soc-acpi.o obj-$(CONFIG_SND_SOC) +=3D snd-soc-core.o obj-$(CONFIG_SND_SOC) +=3D codecs/ obj-$(CONFIG_SND_SOC) +=3D generic/ +obj-$(CONFIG_SND_SOC) +=3D apple/ obj-$(CONFIG_SND_SOC) +=3D adi/ obj-$(CONFIG_SND_SOC) +=3D amd/ obj-$(CONFIG_SND_SOC) +=3D 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 :=3D mca.o + +obj-$(CONFIG_SND_SOC_APPLE_MCA) +=3D 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 clu= sters. + * Each cluster has its separate clock parent, SYNC signal generator, carr= ies + * 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 back= end + * and frontend DAIs. The 'cluster guts' are frontends which are dynamical= ly + * routed to backend I2S ports. + * + * DAI references in devicetree are resolved as backends. The machine driv= er + * makes up some routing to the frontends in the DAPM paths it supplies, w= hich + * can mostly be arbitrary. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#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 =3D cl->base + regoffset; + u32 newval; + + newval =3D (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 =3D snd_soc_dai_get_drvdata(dai); + /* + * FE DAIs are 0 ... nclusters - 1 + * BE DAIs are nclusters ... 2*nclusters - 1 + */ + int cluster_no =3D 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 =3D mca_dai_to_cluster(dai); + bool is_tx =3D substream->stream =3D=3D SNDRV_PCM_STREAM_PLAYBACK; + int serdes_unit =3D is_tx ? CLUSTER_TX_OFF : CLUSTER_RX_OFF; + int serdes_conf =3D serdes_unit + (is_tx ? REG_TX_SERDES_CONF : REG_RX_SE= RDES_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 =3D mca_dai_to_cluster(dai); + bool is_tx =3D substream->stream =3D=3D SNDRV_PCM_STREAM_PLAYBACK; + int serdes_unit =3D 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 =3D cl->host; + int ret; + + ret =3D 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 =3D 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 =3D cl->host; + struct mca_cluster *be_cl; + int stream, i; + + for (i =3D 0; i < mca->nclusters; i++) { + be_cl =3D &mca->clusters[i]; + + if (be_cl->port_driver !=3D 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 =3D mca_dai_to_cluster(dai); + struct mca_data *mca =3D cl->host; + struct mca_cluster *fe_cl; + int ret; + + if (cl->port_driver < 0) + return -EINVAL; + + fe_cl =3D &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 =3D mca_fe_enable_clocks(fe_cl); + if (ret < 0) + return ret; + } + + cl->clocks_in_use[substream->stream] =3D true; + + return 0; +} + +static int mca_be_hw_free(struct snd_pcm_substream *substream, + struct snd_soc_dai *dai) +{ + struct mca_cluster *cl =3D mca_dai_to_cluster(dai); + struct mca_data *mca =3D cl->host; + struct mca_cluster *fe_cl; + + if (cl->port_driver < 0) + return -EINVAL; + + fe_cl =3D &mca->clusters[cl->port_driver]; + if (!mca_fe_clocks_in_use(fe_cl)) + return 0; /* Nothing to do */ + + cl->clocks_in_use[substream->stream] =3D 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 &=3D ~(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 =3D SERDES_CONF_WIDTH_MASK | SERDES_CONF_NCHANS; + serdes_conf =3D FIELD_PREP(SERDES_CONF_NCHANS, max(slots, 1) - 1); + switch (slot_width) { + case 16: + serdes_conf |=3D SERDES_CONF_WIDTH_16BIT; + break; + case 20: + serdes_conf |=3D SERDES_CONF_WIDTH_20BIT; + break; + case 24: + serdes_conf |=3D SERDES_CONF_WIDTH_24BIT; + break; + case 32: + serdes_conf |=3D SERDES_CONF_WIDTH_32BIT; + break; + default: + goto err; + } + + serdes_conf_mask |=3D SERDES_CONF_SYNC_SEL; + serdes_conf |=3D FIELD_PREP(SERDES_CONF_SYNC_SEL, cl->no + 1); + + if (is_tx) { + serdes_conf_mask |=3D SERDES_CONF_UNK1 | SERDES_CONF_UNK2 | SERDES_CONF_= UNK3; + serdes_conf |=3D SERDES_CONF_UNK1 | SERDES_CONF_UNK2 | SERDES_CONF_UNK3; + } else { + + serdes_conf_mask |=3D SERDES_CONF_UNK1 | SERDES_CONF_UNK2 | SERDES_CONF_= UNK3 + | SERDES_CONF_NO_DATA_FEEDBACK; + serdes_conf |=3D 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= =3D0x%x slots=3D%d slot_width=3D%d)\n", + mask, slots, slot_width); + return -EINVAL; +} + +static int mca_fe_set_tdm_slot(struct snd_soc_dai *dai, unsigned int tx_ma= sk, + unsigned int rx_mask, int slots, int slot_width) +{ + struct mca_cluster *cl =3D mca_dai_to_cluster(dai); + + cl->tdm_slots =3D slots; + cl->tdm_slot_width =3D slot_width; + cl->tdm_tx_mask =3D tx_mask; + cl->tdm_rx_mask =3D rx_mask; + + return 0; +} + +static int mca_fe_set_fmt(struct snd_soc_dai *dai, + unsigned int fmt) +{ + struct mca_cluster *cl =3D mca_dai_to_cluster(dai); + struct mca_data *mca =3D cl->host; + bool fpol_inv =3D false; + u32 serdes_conf =3D 0; + u32 bitstart; + + if ((fmt & SND_SOC_DAIFMT_CLOCK_PROVIDER_MASK) !=3D + SND_SOC_DAIFMT_CBC_CFC) + goto err; + + switch (fmt & SND_SOC_DAIFMT_FORMAT_MASK) { + case SND_SOC_DAIFMT_I2S: + fpol_inv =3D 0; + bitstart =3D 1; + break; + case SND_SOC_DAIFMT_LEFT_J: + fpol_inv =3D 1; + bitstart =3D 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 ^=3D 1; + break; + } + + switch (fmt & SND_SOC_DAIFMT_INV_MASK) { + case SND_SOC_DAIFMT_NB_NF: + case SND_SOC_DAIFMT_NB_IF: + serdes_conf |=3D 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 =3D mca_dai_to_cluster(dai); + int ret; + + if (freq =3D=3D cl->set_sysclk) + return 0; + + if (mca_fe_clocks_in_use(cl)) + return -EBUSY; + + ret =3D clk_set_rate(cl->clk_parent, freq); + if (!ret) + cl->set_sysclk =3D 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 =3D mca_dai_to_cluster(dai); + struct mca_data *mca =3D cl->host; + struct device *dev =3D mca->dev; + unsigned int samp_rate =3D params_rate(params); + bool is_tx =3D substream->stream =3D=3D SNDRV_PCM_STREAM_PLAYBACK; + bool refine_tdm =3D 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 =3D params_width(params); + tdm_slots =3D params_channels(params); + refine_tdm =3D true; + } else { + tdm_slot_width =3D cl->tdm_slot_width; + tdm_slots =3D cl->tdm_slots; + tdm_mask =3D is_tx ? cl->tdm_tx_mask : cl->tdm_rx_mask; + } + + if (cl->set_sysclk) + bclk_ratio =3D cl->set_sysclk / samp_rate; + else + bclk_ratio =3D tdm_slot_width * tdm_slots; + + if (refine_tdm) { + int nchannels =3D params_channels(params); + + if (nchannels > 2) { + dev_err(dev, "missing TDM for stream with over two channels\n"); + return -EINVAL; + } + + if ((bclk_ratio % nchannels) !=3D 0) { + dev_err(dev, "BCLK ratio (%ld) not divisible by no of channels (%d)\n", + bclk_ratio, nchannels); + return -EINVAL; + } + + tdm_slot_width =3D bclk_ratio / nchannels; + + if (tdm_slot_width > 32 && nchannels =3D=3D 1) + tdm_slot_width =3D 32; + + if (tdm_slot_width < params_width(params)) { + dev_err(dev, "TDM slots too narrow (tdm=3D%d params=3D%d)\n", + tdm_slot_width, params_width(params)); + return -EINVAL; + } + + tdm_mask =3D (1 << tdm_slots) - 1; + } + + ret =3D 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 =3D 32 - params_width(params); + + /* + * TODO: Here the register semantics aren't clear. + */ + nchans_ceiled =3D min_t(int, params_channels(params), 4); + regval =3D 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_PER= IOD); + writel_relaxed(FIELD_PREP(MCLK_CONF_DIV, 0x1), cl->base + REG_MCLK_CONF); + + ret =3D 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 =3D { + .set_fmt =3D mca_fe_set_fmt, + .set_sysclk =3D mca_fe_set_sysclk, + .set_tdm_slot =3D mca_fe_set_tdm_slot, + .hw_params =3D mca_fe_hw_params, + .trigger =3D 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 =3D asoc_substream_to_rtd(substream); + struct snd_soc_pcm_runtime *fe; + struct mca_cluster *cl =3D mca_dai_to_cluster(dai); + struct mca_cluster *fe_cl; + struct mca_data *mca =3D cl->host; + struct snd_soc_dpcm *dpcm; + + fe =3D NULL; + + for_each_dpcm_fe(be, substream->stream, dpcm) { + if (fe && dpcm->fe !=3D fe) { + dev_err(mca->dev, "many FE per one BE unsupported\n"); + return -EINVAL; + } + + fe =3D dpcm->fe; + } + + if (!fe) + return -EINVAL; + + fe_cl =3D 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 !=3D cl->port_driver) + return -EINVAL; + + cl->port_started[substream->stream] =3D 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 =3D fe_cl->no; + cl->port_started[substream->stream] =3D true; + + return 0; +} + +static void mca_be_shutdown(struct snd_pcm_substream *substream, + struct snd_soc_dai *dai) +{ + struct mca_cluster *cl =3D mca_dai_to_cluster(dai); + + cl->port_started[substream->stream] =3D 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 =3D -1; + } +} + +static const struct snd_soc_dai_ops mca_be_ops =3D { + .prepare =3D mca_be_prepare, + .hw_free =3D mca_be_hw_free, + .startup =3D mca_be_startup, + .shutdown =3D 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 =3D chan->device->dev; + struct snd_dmaengine_dai_dma_data dma_data =3D {}; + int ret; + + struct snd_pcm_hardware hw; + + memset(&hw, 0, sizeof(hw)); + + hw.info =3D SNDRV_PCM_INFO_MMAP | SNDRV_PCM_INFO_MMAP_VALID | + SNDRV_PCM_INFO_INTERLEAVED; + hw.periods_min =3D 2; + hw.periods_max =3D UINT_MAX; + hw.period_bytes_min =3D 256; + hw.period_bytes_max =3D dma_get_max_seg_size(dma_dev); + hw.buffer_bytes_max =3D SIZE_MAX; + hw.fifo_size =3D 16; + + ret =3D 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 =3D asoc_substream_to_rtd(substream); + struct mca_cluster *cl =3D mca_dai_to_cluster(asoc_rtd_to_cpu(rtd, 0)); + struct dma_chan *chan =3D cl->dma_chans[substream->stream]; + int ret; + + if (rtd->dai_link->no_pcm) + return 0; + + ret =3D 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 =3D asoc_substream_to_rtd(substream); + struct dma_chan *chan =3D 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 =3D snd_hwparams_to_dma_slave_config(substream, params, &slave_config= ); + if (ret < 0) + return ret; + + if (substream->stream =3D=3D SNDRV_PCM_STREAM_PLAYBACK) + slave_config.dst_port_window_size =3D min_t(u32, params_channels(params)= , 4); + else + slave_config.src_port_window_size =3D 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 =3D 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 =3D 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 =3D 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 =3D 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 =3D rtd->pcm->streams[i].substream; + struct dma_chan *chan =3D 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 =3D { + .name =3D "apple-mca", + .open =3D mca_pcm_open, + .close =3D mca_close, + .hw_params =3D mca_hw_params, + .trigger =3D mca_trigger, + .pointer =3D mca_pointer, + .pcm_construct =3D mca_pcm_new, +}; + +static void apple_mca_release(struct mca_data *mca) +{ + int i, stream; + + for (i =3D 0; i < mca->nclusters; i++) { + struct mca_cluster *cl =3D &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 =3D 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 =3D (resource_size(res) - CLUSTER_STRIDE) / CLUSTER_STRIDE + 1; + + mca =3D devm_kzalloc(&pdev->dev, struct_size(mca, clusters, nclusters), + GFP_KERNEL); + if (!mca) + return -ENOMEM; + mca->dev =3D &pdev->dev; + mca->nclusters =3D nclusters; + platform_set_drvdata(pdev, mca); + clusters =3D mca->clusters; + + mca->switch_base =3D 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 =3D 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 =3D devm_kzalloc(&pdev->dev, sizeof(*dai_drivers) * 2 * nclus= ters, + GFP_KERNEL); + if (!dai_drivers) + return -ENOMEM; + + mca->pd_dev =3D dev_pm_domain_attach_by_id(&pdev->dev, 0); + if (IS_ERR(mca->pd_dev)) + return -EINVAL; + + /* + * TODO: need cleanup? + */ + mca->pd_link =3D device_link_add(&pdev->dev, mca->pd_dev, + DL_FLAG_STATELESS | DL_FLAG_PM_RUNTIME | + DL_FLAG_RPM_ACTIVE); + if (!mca->pd_link) { + ret =3D -EINVAL; + goto err_release; + } + + for (i =3D 0; i < nclusters; i++) { + struct mca_cluster *cl =3D &clusters[i]; + struct snd_soc_dai_driver *fe =3D &dai_drivers[mca->nclusters + i]; + struct snd_soc_dai_driver *be =3D &dai_drivers[i]; + int stream; + + cl->host =3D mca; + cl->no =3D i; + cl->base =3D base + CLUSTER_STRIDE * i; + cl->port_driver =3D -1; + cl->clk_parent =3D 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 =3D PTR_ERR(cl->clk_parent); + goto err_release; + } + cl->pd_dev =3D 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 =3D PTR_ERR(cl->pd_dev); + goto err_release; + } + + for_each_pcm_streams(stream) { + struct dma_chan *chan; + bool is_tx =3D (stream =3D=3D SNDRV_PCM_STREAM_PLAYBACK); +#ifndef USE_RXB_FOR_CAPTURE + char *name =3D devm_kasprintf(&pdev->dev, GFP_KERNEL, + is_tx ? "tx%da" : "rx%da", i); +#else + char *name =3D devm_kasprintf(&pdev->dev, GFP_KERNEL, + is_tx ? "tx%da" : "rx%db", i); +#endif + + chan =3D of_dma_request_slave_channel(pdev->dev.of_node, name); + if (IS_ERR(chan)) { + if (PTR_ERR(chan) !=3D -EPROBE_DEFER) + dev_err(&pdev->dev, "no %s DMA channel: %ld\n", + name, PTR_ERR(chan)); + + ret =3D PTR_ERR(chan); + goto err_release; + } + + cl->dma_chans[stream] =3D chan; + } + + fe->id =3D i; + fe->name =3D devm_kasprintf(&pdev->dev, GFP_KERNEL, + "mca-pcm-%d", i); + if (!fe->name) { + ret =3D -ENOMEM; + goto err_release; + } + fe->ops =3D &mca_fe_ops; + fe->playback.channels_min =3D 1; + fe->playback.channels_max =3D 32; + fe->playback.rates =3D SNDRV_PCM_RATE_8000_192000; + fe->playback.formats =3D APPLE_MCA_FMTBITS; + fe->capture.channels_min =3D 1; + fe->capture.channels_max =3D 32; + fe->capture.rates =3D SNDRV_PCM_RATE_8000_192000; + fe->capture.formats =3D APPLE_MCA_FMTBITS; + fe->symmetric_rate =3D 1; + + fe->playback.stream_name =3D devm_kasprintf(&pdev->dev, GFP_KERNEL, + "PCM%d TX", i); + fe->capture.stream_name =3D devm_kasprintf(&pdev->dev, GFP_KERNEL, + "PCM%d RX", i); + + if (!fe->playback.stream_name || !fe->capture.stream_name) { + ret =3D -ENOMEM; + goto err_release; + } + + be->id =3D i + nclusters; + be->name =3D devm_kasprintf(&pdev->dev, GFP_KERNEL, + "mca-i2s-%d", i); + if (!be->name) { + ret =3D -ENOMEM; + goto err_release; + } + be->ops =3D &mca_be_ops; + be->playback.channels_min =3D 1; + be->playback.channels_max =3D 32; + be->playback.rates =3D SNDRV_PCM_RATE_8000_192000; + be->playback.formats =3D APPLE_MCA_FMTBITS; + be->capture.channels_min =3D 1; + be->capture.channels_max =3D 32; + be->capture.rates =3D SNDRV_PCM_RATE_8000_192000; + be->capture.formats =3D APPLE_MCA_FMTBITS; + + be->playback.stream_name =3D devm_kasprintf(&pdev->dev, GFP_KERNEL, + "I2S%d TX", i); + be->capture.stream_name =3D devm_kasprintf(&pdev->dev, GFP_KERNEL, + "I2S%d RX", i); + if (!be->playback.stream_name || !be->capture.stream_name) { + ret =3D -ENOMEM; + goto err_release; + } + } + + ret =3D 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 =3D platform_get_drvdata(pdev); + + apple_mca_release(mca); + return 0; +} + +static const struct of_device_id apple_mca_of_match[] =3D { + { .compatible =3D "apple,mca", }, + {} +}; +MODULE_DEVICE_TABLE(of, apple_mca_of_match); + +static struct platform_driver apple_mca_driver =3D { + .driver =3D { + .name =3D "apple-mca", + .owner =3D THIS_MODULE, + .of_match_table =3D apple_mca_of_match, + }, + .probe =3D apple_mca_probe, + .remove =3D apple_mca_remove, +}; +module_platform_driver(apple_mca_driver); + +MODULE_AUTHOR("Martin Povi=C5=A1er "); +MODULE_DESCRIPTION("ASoC platform driver for Apple Silicon SoCs"); +MODULE_LICENSE("GPL"); --=20 2.33.0 From nobody Tue Apr 28 03:57:09 2026 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by smtp.lore.kernel.org (Postfix) with ESMTP id 67786C433EF for ; Mon, 6 Jun 2022 19:20:09 +0000 (UTC) Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S232202AbiFFTUH (ORCPT ); Mon, 6 Jun 2022 15:20:07 -0400 Received: from lindbergh.monkeyblade.net ([23.128.96.19]:49476 "EHLO lindbergh.monkeyblade.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S229809AbiFFTT6 (ORCPT ); Mon, 6 Jun 2022 15:19:58 -0400 Received: from hutie.ust.cz (hutie.ust.cz [185.8.165.127]) by lindbergh.monkeyblade.net (Postfix) with ESMTPS id 87BFA118027; Mon, 6 Jun 2022 12:19:56 -0700 (PDT) From: =?UTF-8?q?Martin=20Povi=C5=A1er?= DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=cutebit.org; s=mail; t=1654543194; bh=QRlMmTx37FIKTUzJOBJewRJ+ymrpkMI9OoQilWk1xqQ=; h=From:To:Cc:Subject:Date:In-Reply-To:References; b=pCqAUtR5QapOooJvOBPYyu6davbskVeHkuQPoBRndcKKCB1ayMa43zPdunPZ8BdEz hqwIFeXKXSAzIiaJloXlHVS4XBW2OiJcYOcvxEpGzXL3TILws63LPcNosFqrqNnF5h BDeut6J0fhsCyD1wCOIaeQ88CR2aUEVF2Yedmjs8= To: Liam Girdwood , Mark Brown , Rob Herring , Krzysztof Kozlowski , Jaroslav Kysela , Takashi Iwai Cc: =?UTF-8?q?Martin=20Povi=C5=A1er?= , alsa-devel@alsa-project.org, devicetree@vger.kernel.org, linux-kernel@vger.kernel.org, Mark Kettenis , Hector Martin , Sven Peter , asahi@lists.linux.dev Subject: [RFC PATCH v2 4/5] ASoC: Introduce 'fixup_controls' card method Date: Mon, 6 Jun 2022 21:19:09 +0200 Message-Id: <20220606191910.16580-5-povik+lin@cutebit.org> In-Reply-To: <20220606191910.16580-1-povik+lin@cutebit.org> References: <20220606191910.16580-1-povik+lin@cutebit.org> MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable Precedence: bulk List-ID: X-Mailing-List: linux-kernel@vger.kernel.org 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=C5=A1er --- 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); =20 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); =20 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 { =20 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); =20 /* 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; } =20 +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 =3D 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 *car= d) goto probe_end; =20 snd_soc_dapm_new_widgets(card); + snd_soc_card_fixup_controls(card); =20 ret =3D snd_card_register(card->snd_card); if (ret < 0) { --=20 2.33.0 From nobody Tue Apr 28 03:57:09 2026 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by smtp.lore.kernel.org (Postfix) with ESMTP id EAB92C433EF for ; Mon, 6 Jun 2022 19:20:28 +0000 (UTC) Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S231785AbiFFTU1 (ORCPT ); Mon, 6 Jun 2022 15:20:27 -0400 Received: from lindbergh.monkeyblade.net ([23.128.96.19]:49730 "EHLO lindbergh.monkeyblade.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S232148AbiFFTUB (ORCPT ); Mon, 6 Jun 2022 15:20:01 -0400 Received: from hutie.ust.cz (unknown [IPv6:2a03:3b40:fe:f0::1]) by lindbergh.monkeyblade.net (Postfix) with ESMTPS id 6B56D11E1C3; Mon, 6 Jun 2022 12:19:58 -0700 (PDT) From: =?UTF-8?q?Martin=20Povi=C5=A1er?= DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=cutebit.org; s=mail; t=1654543195; bh=w4igCYE6reEqHPqBrqUDUNsl8S3K+iBgYS3AHy12wHo=; h=From:To:Cc:Subject:Date:In-Reply-To:References; b=pr8efmIHvE7DeWzURvQnSiKi0oOOHkwNYlnTU/5mMaaKBSvGLiedGl2cR9o/DAhdg pwWwo2e7LotPciGt+S/AWHTVHw2ylX7Kh47AwG6JSufCNTYMHe4T3dN43BOyfQkpt5 /v1D6W2uByioBbziH9PDj+p5oSH+x06GRmYUGrYo= To: Liam Girdwood , Mark Brown , Rob Herring , Krzysztof Kozlowski , Jaroslav Kysela , Takashi Iwai Cc: =?UTF-8?q?Martin=20Povi=C5=A1er?= , alsa-devel@alsa-project.org, devicetree@vger.kernel.org, linux-kernel@vger.kernel.org, Mark Kettenis , Hector Martin , Sven Peter , asahi@lists.linux.dev Subject: [RFC PATCH v2 5/5] ASoC: apple: Add macaudio machine driver Date: Mon, 6 Jun 2022 21:19:10 +0200 Message-Id: <20220606191910.16580-6-povik+lin@cutebit.org> In-Reply-To: <20220606191910.16580-1-povik+lin@cutebit.org> References: <20220606191910.16580-1-povik+lin@cutebit.org> MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable Precedence: bulk List-ID: X-Mailing-List: linux-kernel@vger.kernel.org Signed-off-by: Martin Povi=C5=A1er --- 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 :=3D mca.o +snd-soc-macaudio-objs :=3D macaudio.o =20 +obj-$(CONFIG_SND_SOC_APPLE_MACAUDIO) +=3D snd-soc-macaudio.o obj-$(CONFIG_SND_SOC_APPLE_MCA) +=3D 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 f= or + * 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 o= ne + * frontend at any given time. (The limitation is due to the unsupported c= ase + * of reparenting of live BEs.) + * + * The DAPM routing that this machine-level driver makes up has two use-ca= ses + * in mind: + * + * - Using a single PCM for playback such that it conditionally sinks to e= ither + * speakers or headphones based on the plug-in state of the headphones j= ack. + * 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 f= or + * speakers in userspace is desirable or required. + * + * - Driving the headphones and speakers from distinct PCMs, having usersp= ace + * bridge the difference and apply different signal processing to the tw= o. + * + * In the end the topology supplied by this driver looks like this: + * + * PCMs (frontends) I2S Port Groups (backends) + * =E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94= =80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80= =E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80= =E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2= =94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94= =80=E2=94=80=E2=94=80=E2=94=80 + * + * =E2=94=8C=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94= =80=E2=94=80=E2=94=80=E2=94=80=E2=94=90 =E2=94=8C=E2=94=80=E2=94=80= =E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2= =94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=96=BA =E2=94=8C=E2=94=80=E2= =94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=90 =E2=94=8C=E2=94=80=E2=94=80= =E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2= =94=90 + * =E2=94=82 Primary =E2=94=9C=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94= =80=E2=94=80=E2=94=80=E2=94=A4 =E2=94=82 Mux =E2=94=82 =E2= =94=80=E2=94=80=E2=96=BA =E2=94=82 Speakers =E2=94=82 + * =E2=94=94=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94= =80=E2=94=80=E2=94=80=E2=94=80=E2=94=98 =E2=94=82 =E2=94=8C=E2=94= =80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80= =E2=94=80=E2=96=BA =E2=94=94=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80= =E2=94=98 =E2=94=94=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94= =80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=98 + * =E2=94=8C=E2=94=80=E2=94=80=E2=94=80 =E2=94=82 =E2=94=80= =E2=94=80=E2=94=80=E2=94=98 =E2=96=B2 + * =E2=94=8C=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94= =80=E2=94=80=E2=94=80=E2=94=80=E2=94=90 =E2=94=82 =E2=94=82 = =E2=94=82 + * =E2=94=82Secondary =E2=94=9C=E2=94=80=E2=94=80=E2=94=98 =E2=94=82 = =E2=94=8C=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80= =E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=B4=E2=94=90 + * =E2=94=94=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94= =80=E2=94=80=E2=94=80=E2=94=80=E2=94=98 =E2=94=9C=E2=94=80=E2=94=80= =E2=94=80=E2=94=80=E2=96=BA=E2=94=82Plug-in Demux=E2=94=82 + * =E2=94=82 =E2=94=94=E2=94=80=E2=94=80=E2=94=80= =E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2= =94=80=E2=94=AC=E2=94=98 + * =E2=94=82 =E2=94=82 + * =E2=94=82 =E2=96=BC + * =E2=94=82 =E2=94=8C=E2=94=80=E2=94= =80=E2=94=80=E2=94=80=E2=94=80=E2=94=90 =E2=94=8C=E2=94=80=E2=94=80=E2= =94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94= =90 + * =E2=94=94=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94= =80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80= =E2=94=80=E2=94=80=E2=96=BA =E2=94=82 Mux =E2=94=82 =E2=94=80=E2=94=80=E2= =96=BA =E2=94=82Headphones=E2=94=82 + * =E2=94=94=E2=94=80=E2=94=80=E2=94= =80=E2=94=80=E2=94=80=E2=94=98 =E2=94=94=E2=94=80=E2=94=80=E2=94=80=E2= =94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=98 + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 sa= fety 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[] =3D { + { + .name =3D "Primary", + .stream_name =3D "Primary", + .dynamic =3D 1, + .dpcm_playback =3D 1, + .dpcm_capture =3D 1, + .dpcm_merged_rate =3D 1, + .dpcm_merged_chan =3D 1, + .dpcm_merged_format =3D 1, + .dai_fmt =3D MACAUDIO_DAI_FMT, + SND_SOC_DAILINK_REG(primary), + }, + { + .name =3D "Secondary", + .stream_name =3D "Secondary", + .dynamic =3D 1, + .dpcm_playback =3D 1, + .dpcm_merged_rate =3D 1, + .dpcm_merged_chan =3D 1, + .dpcm_merged_format =3D 1, + .dai_fmt =3D MACAUDIO_DAI_FMT, + SND_SOC_DAILINK_REG(secondary), + }, +}; + +static struct macaudio_link_props macaudio_fe_link_props[] =3D { + { + /* + * 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 =3D 64, + }, + { + /* + * Secondary FE + * + * Here we want frames plenty long to be able to drive all + * those fancy speaker arrays. + */ + .mclk_fs =3D 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 =3D devm_kcalloc(dev, target->num_cpus, + sizeof(*target->cpus), GFP_KERNEL); + target->codecs =3D devm_kcalloc(dev, target->num_codecs, + sizeof(*target->codecs), GFP_KERNEL); + target->platforms =3D 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_cp= us); + 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 =3D of_parse_phandle_with_args(node, "sound-dai", "#sound-dai-cells", + index, &args); + if (ret) + return ret; + comp->of_node =3D 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 =3D ma->card.dev; + int codec_base =3D be_index * ncodecs_per_be; + int ret, i; + + link->no_pcm =3D 1; + link->dpcm_playback =3D 1; + link->dpcm_capture =3D 1; + + link->dai_fmt =3D MACAUDIO_DAI_FMT; + + link->num_codecs =3D ncodecs_per_be; + link->codecs =3D devm_kcalloc(dev, ncodecs_per_be, + sizeof(*comp), GFP_KERNEL); + link->num_cpus =3D 1; + link->cpus =3D devm_kzalloc(dev, sizeof(*comp), GFP_KERNEL); + + if (!link->codecs || !link->cpus) + return -ENOMEM; + + link->num_platforms =3D 0; + + for_each_link_codecs(link, i, comp) { + ret =3D macaudio_parse_of_component(codec, codec_base + i, comp); + if (ret) + return ret; + } + + ret =3D macaudio_parse_of_component(cpu, be_index, link->cpus); + if (ret) + return ret; + + link->name =3D link->cpus[0].dai_name; + + return 0; +} + +static int macaudio_parse_of(struct macaudio_snd_data *ma) +{ + struct device_node *codec =3D NULL; + struct device_node *cpu =3D NULL; + struct device_node *np =3D NULL; + struct device_node *platform =3D NULL; + struct snd_soc_dai_link *link =3D NULL; + struct snd_soc_card *card =3D &ma->card; + struct device *dev =3D card->dev; + struct macaudio_link_props *link_props; + int ret, num_links, i; + + ret =3D 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 =3D 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 =3D of_get_child_by_name(np, "cpu"); + if (!cpu) { + dev_err(dev, "missing CPU DAI node at %pOF\n", np); + ret =3D -EINVAL; + goto err_free; + } + + num_cpus =3D of_count_phandle_with_args(cpu, "sound-dai", + "#sound-dai-cells"); + + if (num_cpus <=3D 0) { + dev_err(card->dev, "missing sound-dai property at %pOF\n", cpu); + ret =3D -EINVAL; + goto err_free; + } + of_node_put(cpu); + cpu =3D NULL; + + /* Each CPU specified counts as one BE link */ + num_links +=3D num_cpus; + } + + /* Allocate the DAI link array */ + card->dai_link =3D devm_kcalloc(dev, num_links, sizeof(*link), GFP_KERNEL= ); + ma->link_props =3D devm_kcalloc(dev, num_links, sizeof(*ma->link_props), = GFP_KERNEL); + if (!card->dai_link || !ma->link_props) + return -ENOMEM; + + card->num_links =3D num_links; + link =3D card->dai_link; + link_props =3D ma->link_props; + + for (i =3D 0; i < ARRAY_SIZE(macaudio_fe_links); i++) { + ret =3D 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_li= nk_props)); + link++; link_props++; + } + + for (i =3D 0; i < num_links; i++) + card->dai_link[i].id =3D 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 =3D of_property_read_string(np, "link-name", &link_name); + if (ret) { + dev_err(card->dev, "missing link name\n"); + goto err_free; + } + + speakers =3D !strcmp(link_name, "Speaker") + || !strcmp(link_name, "Speakers"); + + cpu =3D of_get_child_by_name(np, "cpu"); + codec =3D of_get_child_by_name(np, "codec"); + + if (!codec || !cpu) { + dev_err(dev, "missing DAI specifications for '%s'\n", link_name); + ret =3D -EINVAL; + goto err_free; + } + + num_bes =3D of_count_phandle_with_args(cpu, "sound-dai", + "#sound-dai-cells"); + if (num_bes <=3D 0) { + dev_err(card->dev, "missing sound-dai property at %pOF\n", cpu); + ret =3D -EINVAL; + goto err_free; + } + + num_codecs =3D of_count_phandle_with_args(codec, "sound-dai", + "#sound-dai-cells"); + if (num_codecs <=3D 0) { + dev_err(card->dev, "missing sound-dai property at %pOF\n", codec); + ret =3D -EINVAL; + goto err_free; + } + + if (num_codecs % num_bes !=3D 0) { + dev_err(card->dev, "bad combination of CODEC (%d) and CPU (%d) number a= t %pOF\n", + num_codecs, num_bes, np); + ret =3D -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 =3D num_codecs / num_bes; + nchannels =3D 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 =3D 2; + + left_mask =3D 0; + for (i =3D 0; i < nchannels; i +=3D 2) + left_mask =3D left_mask << 2 | 1; + right_mask =3D left_mask << 1; + + for (be_index =3D 0; be_index < num_bes; be_index++) { + ret =3D macaudio_parse_of_be_dai_link(ma, link, be_index, + ncodecs_per_cpu, cpu, codec); + if (ret) + goto err_free; + + link_props->is_speakers =3D speakers; + link_props->is_headphones =3D !speakers; + + if (num_bes =3D=3D 2) + /* This sound peripheral is split between left and right BE */ + link_props->tdm_mask =3D be_index ? right_mask : left_mask; + else + /* One BE covers all of the peripheral */ + link_props->tdm_mask =3D left_mask | right_mask; + + /* Steal platform OF reference for use in FE links later */ + platform =3D link->cpus->of_node; + + link++; link_props++; + } + + of_node_put(codec); + of_node_put(cpu); + cpu =3D codec =3D NULL; + } + + for (i =3D 0; i < ARRAY_SIZE(macaudio_fe_links); i++) + card->dai_link[i].platforms->of_node =3D 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 =3D 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 *substrea= m) +{ + struct snd_soc_pcm_runtime *rtd =3D asoc_substream_to_rtd(substream); + struct macaudio_snd_data *ma =3D 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 =3D 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 =3D asoc_substream_to_rtd(substream); + struct snd_soc_dai *cpu_dai =3D asoc_rtd_to_cpu(rtd, 0); + int mclk_fs =3D macaudio_get_runtime_mclk_fs(substream); + int i; + + if (mclk_fs) { + struct snd_soc_dai *dai; + int mclk =3D 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 =3D asoc_substream_to_rtd(substream); + struct snd_soc_dai *cpu_dai =3D asoc_rtd_to_cpu(rtd, 0); + struct snd_soc_dai *dai; + int mclk_fs =3D 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 =3D { + .shutdown =3D macaudio_dpcm_shutdown, + .hw_params =3D macaudio_dpcm_hw_params, +}; + +static const struct snd_soc_ops macaudio_be_ops =3D { + .shutdown =3D macaudio_dpcm_shutdown, + .hw_params =3D macaudio_dpcm_hw_params, +}; + +static int macaudio_be_assign_tdm(struct snd_soc_pcm_runtime *rtd) +{ + struct snd_soc_card *card =3D rtd->card; + struct macaudio_snd_data *ma =3D snd_soc_card_get_drvdata(card); + struct macaudio_link_props *props =3D &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 =3D props->tdm_mask; + nslots =3D __fls(mask) + 1; + + if (rtd->num_codecs =3D=3D 1) { + ret =3D 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 =3D=3D -EOPNOTSUPP && props->is_headphones) + return 0; + + return ret; + } + + for_each_rtd_codec_dais(rtd, i, dai) { + int slot =3D __ffs(mask); + + mask &=3D ~(1 << slot); + ret =3D 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 =3D rtd->card; + struct macaudio_snd_data *ma =3D snd_soc_card_get_drvdata(card); + struct macaudio_link_props *props =3D &ma->link_props[rtd->dai_link->id]; + struct snd_soc_dai *dai; + int i, ret; + + ret =3D 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 =3D rtd->card; + struct macaudio_snd_data *ma =3D snd_soc_card_get_drvdata(card); + struct macaudio_link_props *props =3D &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 =3D rtd->card; + struct macaudio_snd_data *ma =3D snd_soc_card_get_drvdata(card); + struct macaudio_link_props *props =3D &ma->link_props[rtd->dai_link->id]; + int nslots =3D 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 ev= ent, + void *data); + +static struct notifier_block macaudio_jack_nb =3D { + .notifier_call =3D macaudio_jack_event, +}; + +static int macaudio_probe(struct snd_soc_card *card) +{ + struct macaudio_snd_data *ma =3D snd_soc_card_get_drvdata(card); + int ret; + + ma->pin.pin =3D "Headphones"; + ma->pin.mask =3D SND_JACK_HEADSET | SND_JACK_HEADPHONE; + ret =3D 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, struc= t 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 =3D "Speakers Playback"; + else + routes[0].source =3D "Headphones Playback"; + routes[0].sink =3D dai->playback_widget->name; + nroutes =3D 1; + + if (!is_speakers) { + routes[1].source =3D dai->capture_widget->name; + routes[1].sink =3D "Headphones Capture"; + nroutes =3D 2; + } + + ret =3D 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] =3D=3D '*') { + int namelen, patternlen; + + pattern++; + if (pattern[0] =3D=3D ' ') + pattern++; + + namelen =3D strlen(name); + patternlen =3D strlen(pattern); + + if (namelen > patternlen) + name +=3D (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 =3D 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 =3D (struct soc_mixer_control *)kctl->private_value; + if (max <=3D mc->max) + mc->platform_max =3D max; + + } + + return found; +} + +static int macaudio_late_probe(struct snd_soc_card *card) +{ + struct macaudio_snd_data *ma =3D 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 =3D &ma->link_props[rtd->dai_link->id]; + + if (!rtd->dai_link->no_pcm) + continue; + + for_each_rtd_cpu_dais(rtd, i, dai) { + ret =3D 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[] =3D { + "Speakers", + "Headphones" +}; + +SOC_ENUM_SINGLE_VIRT_DECL(macaudio_plugin_demux_enum, macaudio_plugin_demu= x_texts); + +static int macaudio_plugin_demux_get(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_soc_dapm_context *dapm =3D snd_soc_dapm_kcontrol_dapm(kcontrol= ); + struct macaudio_snd_data *ma =3D snd_soc_card_get_drvdata(dapm->card); + + /* + * TODO: Determine what locking is in order here... + */ + ucontrol->value.enumerated.item[0] =3D ma->jack_plugin_state; + + return 0; +} + +static int macaudio_jack_event(struct notifier_block *nb, unsigned long ev= ent, + void *data) +{ + struct snd_soc_jack *jack =3D data; + struct macaudio_snd_data *ma =3D snd_soc_card_get_drvdata(jack->card); + + ma->jack_plugin_state =3D !!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 =3D { + .access =3D (SNDRV_CTL_ELEM_ACCESS_READ | + SNDRV_CTL_ELEM_ACCESS_VOLATILE), + .iface =3D SNDRV_CTL_ELEM_IFACE_MIXER, + .name =3D "Plug-in Playback Demux", + .info =3D snd_soc_info_enum_double, + .get =3D macaudio_plugin_demux_get, + .private_value =3D (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 =3D kctl->info(kctl, &info); + if (ret < 0) + return ret; + + if (info.type !=3D SNDRV_CTL_ELEM_TYPE_ENUMERATED) + return -EINVAL; + + for (sel =3D 0; sel < info.value.enumerated.items; sel++) { + info.value.enumerated.item =3D sel; + ret =3D kctl->info(kctl, &info); + if (ret < 0) + return ret; + + if (!strcmp(strvalue, info.value.enumerated.name)) + break; + } + + if (sel =3D=3D info.value.enumerated.items) + return -EINVAL; + + for (i =3D 0; i < info.count; i++) + value.value.enumerated.item[i] =3D 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 =3D macaudio_kctl_set_enum(kctl, "Left"); + if (ret < 0) + dev_err(card->dev, "can't pin '%s': %d\n", kctl->id.name, ret); + + ret =3D 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 =3D snd_soc_card_get_drvdata(card); + const char *name =3D macaudio_plugin_demux.name; + + ma->plugin_demux_kcontrol =3D 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[] =3D { + "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 =3D + SOC_DAPM_ENUM("Speakers Playback Mux", macaudio_spk_mux_enum); + +static const char * const macaudio_hp_mux_texts[] =3D { + "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 =3D + SOC_DAPM_ENUM("Headphones Playback Mux", macaudio_hp_mux_enum); + +static const struct snd_soc_dapm_widget macaudio_snd_widgets[] =3D { + 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_s= pk_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, &macaudi= o_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[] =3D { + /* 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 Pla= yback" }, + { "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 =3D { + .spk_amp_gain_max =3D 20, +}; + +struct macaudio_model_data macaudio_j314_mdata =3D { + .deactive_asi1_sel =3D true, + .spk_amp_gain_max =3D 15, +}; + +static const struct of_device_id macaudio_snd_device_id[] =3D { + { .compatible =3D "apple,j274-macaudio", .data =3D &macaudio_j274_mdata }, + { .compatible =3D "apple,j314-macaudio", .data =3D &macaudio_j314_mdata }, + { .compatible =3D "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 =3D &pdev->dev; + struct snd_soc_dai_link *link; + const struct of_device_id *of_id; + int ret; + int i; + + of_id =3D of_match_device(macaudio_snd_device_id, dev); + if (!of_id) + return -EINVAL; + + data =3D devm_kzalloc(dev, sizeof(*data), GFP_KERNEL); + if (!data) + return -ENOMEM; + card =3D &data->card; + snd_soc_card_set_drvdata(card, data); + + data->mdata =3D (struct macaudio_model_data *) of_id->data; + + card->owner =3D THIS_MODULE; + card->driver_name =3D DRIVER_NAME; + card->dev =3D dev; + card->dapm_widgets =3D macaudio_snd_widgets; + card->num_dapm_widgets =3D ARRAY_SIZE(macaudio_snd_widgets); + card->dapm_routes =3D macaudio_dapm_routes; + card->num_dapm_routes =3D ARRAY_SIZE(macaudio_dapm_routes); + card->probe =3D macaudio_probe; + card->late_probe =3D macaudio_late_probe; + card->fixup_controls =3D macaudio_fixup_controls; + + ret =3D macaudio_parse_of(data); + if (ret) + return ret; + + for_each_card_prelinks(card, i, link) { + if (link->no_pcm) { + link->ops =3D &macaudio_be_ops; + link->init =3D macaudio_be_init; + link->exit =3D macaudio_be_exit; + } else { + link->ops =3D &macaudio_fe_ops; + link->init =3D macaudio_fe_init; + } + } + + return devm_snd_soc_register_card(dev, card); +} + +static struct platform_driver macaudio_snd_driver =3D { + .probe =3D macaudio_snd_platform_probe, + .driver =3D { + .name =3D DRIVER_NAME, + .of_match_table =3D macaudio_snd_device_id, + .pm =3D &snd_soc_pm_ops, + }, +}; +module_platform_driver(macaudio_snd_driver); + +MODULE_AUTHOR("Martin Povi=C5=A1er "); +MODULE_DESCRIPTION("Apple Silicon Macs machine-level sound driver"); +MODULE_LICENSE("GPL"); --=20 2.33.0