From: Takashi Iwai Date: Wed, 9 Jul 2025 16:04:05 +0000 (+0200) Subject: ALSA: hda: Move HD-audio core stuff into sound/hda/core X-Git-Tag: ceph-for-6.17-rc6~318^2~36 X-Git-Url: http://git.apps.os.sepia.ceph.com/?a=commitdiff_plain;h=b2660d1ebde1ba8f3edf963f3aac2bea884457c3;p=ceph-client.git ALSA: hda: Move HD-audio core stuff into sound/hda/core This is a part of HD-audio code restructuring. Simply move the current code of sound/hda/* into the subdirectory sound/hda/core, so that more stuff can be moved into sound/hda cleanly later. Most of file names with hdac_ and hdac_ext_ prefix are renamed without the prefix, since they can be identified well in the directory name and superfluous. Signed-off-by: Takashi Iwai Link: https://patch.msgid.link/20250709160434.1859-3-tiwai@suse.de --- diff --git a/sound/hda/Kconfig b/sound/hda/Kconfig index eb488a5225724..e380146560772 100644 --- a/sound/hda/Kconfig +++ b/sound/hda/Kconfig @@ -1,67 +1,2 @@ # SPDX-License-Identifier: GPL-2.0-only -config SND_HDA_CORE - tristate - select REGMAP - -config SND_HDA_DSP_LOADER - bool - -config SND_HDA_ALIGNED_MMIO - bool - -config SND_HDA_COMPONENT - bool - -config SND_HDA_I915 - bool - select SND_HDA_COMPONENT - -config SND_HDA_EXT_CORE - tristate - select SND_HDA_CORE - -config SND_HDA_PREALLOC_SIZE - int "Pre-allocated buffer size for HD-audio driver" - range 0 32768 - default 0 if SND_DMA_SGBUF - default 64 if !SND_DMA_SGBUF - help - Specifies the default pre-allocated buffer-size in kB for the - HD-audio driver. A larger buffer (e.g. 2048) is preferred - for systems using PulseAudio. The default 64 is chosen just - for compatibility reasons. - On x86 systems, the default is zero as S/G allocation works - and no preallocation is needed in most cases. - - Note that the pre-allocation size can be changed dynamically - via a proc file (/proc/asound/card*/pcm*/sub*/prealloc), too. - -config SND_INTEL_NHLT - bool - # this config should be selected only for Intel ACPI platforms. - # A fallback is provided so that the code compiles in all cases. - -config SND_INTEL_DSP_CONFIG - tristate - select ACPI_NHLT if ACPI - select SND_INTEL_NHLT if ACPI - select SND_INTEL_SOUNDWIRE_ACPI if ACPI - # this config should be selected only for Intel DSP platforms. - # A fallback is provided so that the code compiles in all cases. - -config SND_INTEL_SOUNDWIRE_ACPI - tristate - -config SND_INTEL_BYT_PREFER_SOF - bool "Prefer SOF driver over SST on BY/CHT platforms" - depends on SND_SST_ATOM_HIFI2_PLATFORM_ACPI && SND_SOC_SOF_BAYTRAIL - default n - help - The kernel has 2 drivers for the Low Power Engine audio-block on - Bay- and Cherry-Trail SoCs. The old SST driver and the new SOF - driver. If both drivers are enabled then the kernel will default - to using the old SST driver, unless told otherwise through the - snd_intel_dspcfg.dsp_driver module-parameter. - - Set this option to Y to make the kernel default to the new SOF - driver instead. +source "sound/hda/core/Kconfig" diff --git a/sound/hda/Makefile b/sound/hda/Makefile index 83cceafe0d4c3..3fdbc22b15304 100644 --- a/sound/hda/Makefile +++ b/sound/hda/Makefile @@ -1,22 +1,2 @@ # SPDX-License-Identifier: GPL-2.0 -snd-hda-core-y := hda_bus_type.o hdac_bus.o hdac_device.o hdac_sysfs.o \ - hdac_regmap.o hdac_controller.o hdac_stream.o array.o hdmi_chmap.o - -snd-hda-core-y += trace.o -CFLAGS_trace.o := -I$(src) - -# for sync with i915 gfx driver -snd-hda-core-$(CONFIG_SND_HDA_COMPONENT) += hdac_component.o -snd-hda-core-$(CONFIG_SND_HDA_I915) += hdac_i915.o - -obj-$(CONFIG_SND_HDA_CORE) += snd-hda-core.o - -#extended hda -obj-$(CONFIG_SND_HDA_EXT_CORE) += ext/ - -snd-intel-dspcfg-y := intel-dsp-config.o -snd-intel-dspcfg-$(CONFIG_SND_INTEL_NHLT) += intel-nhlt.o -obj-$(CONFIG_SND_INTEL_DSP_CONFIG) += snd-intel-dspcfg.o - -snd-intel-sdw-acpi-y := intel-sdw-acpi.o -obj-$(CONFIG_SND_INTEL_SOUNDWIRE_ACPI) += snd-intel-sdw-acpi.o +obj-y += core/ diff --git a/sound/hda/array.c b/sound/hda/array.c deleted file mode 100644 index a204dcee0034f..0000000000000 --- a/sound/hda/array.c +++ /dev/null @@ -1,52 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-only -/* - * generic arrays - */ - -#include -#include -#include - -/** - * snd_array_new - get a new element from the given array - * @array: the array object - * - * Get a new element from the given array. If it exceeds the - * pre-allocated array size, re-allocate the array. - * - * Returns NULL if allocation failed. - */ -void *snd_array_new(struct snd_array *array) -{ - if (snd_BUG_ON(!array->elem_size)) - return NULL; - if (array->used >= array->alloced) { - int num = array->alloced + array->alloc_align; - int oldsize = array->alloced * array->elem_size; - int size = (num + 1) * array->elem_size; - void *nlist; - if (snd_BUG_ON(num >= 4096)) - return NULL; - nlist = krealloc(array->list, size, GFP_KERNEL); - if (!nlist) - return NULL; - memset(nlist + oldsize, 0, size - oldsize); - array->list = nlist; - array->alloced = num; - } - return snd_array_elem(array, array->used++); -} -EXPORT_SYMBOL_GPL(snd_array_new); - -/** - * snd_array_free - free the given array elements - * @array: the array object - */ -void snd_array_free(struct snd_array *array) -{ - kfree(array->list); - array->used = 0; - array->alloced = 0; - array->list = NULL; -} -EXPORT_SYMBOL_GPL(snd_array_free); diff --git a/sound/hda/core/Kconfig b/sound/hda/core/Kconfig new file mode 100644 index 0000000000000..eb488a5225724 --- /dev/null +++ b/sound/hda/core/Kconfig @@ -0,0 +1,67 @@ +# SPDX-License-Identifier: GPL-2.0-only +config SND_HDA_CORE + tristate + select REGMAP + +config SND_HDA_DSP_LOADER + bool + +config SND_HDA_ALIGNED_MMIO + bool + +config SND_HDA_COMPONENT + bool + +config SND_HDA_I915 + bool + select SND_HDA_COMPONENT + +config SND_HDA_EXT_CORE + tristate + select SND_HDA_CORE + +config SND_HDA_PREALLOC_SIZE + int "Pre-allocated buffer size for HD-audio driver" + range 0 32768 + default 0 if SND_DMA_SGBUF + default 64 if !SND_DMA_SGBUF + help + Specifies the default pre-allocated buffer-size in kB for the + HD-audio driver. A larger buffer (e.g. 2048) is preferred + for systems using PulseAudio. The default 64 is chosen just + for compatibility reasons. + On x86 systems, the default is zero as S/G allocation works + and no preallocation is needed in most cases. + + Note that the pre-allocation size can be changed dynamically + via a proc file (/proc/asound/card*/pcm*/sub*/prealloc), too. + +config SND_INTEL_NHLT + bool + # this config should be selected only for Intel ACPI platforms. + # A fallback is provided so that the code compiles in all cases. + +config SND_INTEL_DSP_CONFIG + tristate + select ACPI_NHLT if ACPI + select SND_INTEL_NHLT if ACPI + select SND_INTEL_SOUNDWIRE_ACPI if ACPI + # this config should be selected only for Intel DSP platforms. + # A fallback is provided so that the code compiles in all cases. + +config SND_INTEL_SOUNDWIRE_ACPI + tristate + +config SND_INTEL_BYT_PREFER_SOF + bool "Prefer SOF driver over SST on BY/CHT platforms" + depends on SND_SST_ATOM_HIFI2_PLATFORM_ACPI && SND_SOC_SOF_BAYTRAIL + default n + help + The kernel has 2 drivers for the Low Power Engine audio-block on + Bay- and Cherry-Trail SoCs. The old SST driver and the new SOF + driver. If both drivers are enabled then the kernel will default + to using the old SST driver, unless told otherwise through the + snd_intel_dspcfg.dsp_driver module-parameter. + + Set this option to Y to make the kernel default to the new SOF + driver instead. diff --git a/sound/hda/core/Makefile b/sound/hda/core/Makefile new file mode 100644 index 0000000000000..89cb461430509 --- /dev/null +++ b/sound/hda/core/Makefile @@ -0,0 +1,22 @@ +# SPDX-License-Identifier: GPL-2.0 +snd-hda-core-y := hda_bus_type.o bus.o device.o sysfs.o \ + regmap.o controller.o stream.o array.o hdmi_chmap.o + +snd-hda-core-y += trace.o +CFLAGS_trace.o := -I$(src) + +# for sync with i915 gfx driver +snd-hda-core-$(CONFIG_SND_HDA_COMPONENT) += component.o +snd-hda-core-$(CONFIG_SND_HDA_I915) += i915.o + +obj-$(CONFIG_SND_HDA_CORE) += snd-hda-core.o + +#extended hda +obj-$(CONFIG_SND_HDA_EXT_CORE) += ext/ + +snd-intel-dspcfg-y := intel-dsp-config.o +snd-intel-dspcfg-$(CONFIG_SND_INTEL_NHLT) += intel-nhlt.o +obj-$(CONFIG_SND_INTEL_DSP_CONFIG) += snd-intel-dspcfg.o + +snd-intel-sdw-acpi-y := intel-sdw-acpi.o +obj-$(CONFIG_SND_INTEL_SOUNDWIRE_ACPI) += snd-intel-sdw-acpi.o diff --git a/sound/hda/core/array.c b/sound/hda/core/array.c new file mode 100644 index 0000000000000..a204dcee0034f --- /dev/null +++ b/sound/hda/core/array.c @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * generic arrays + */ + +#include +#include +#include + +/** + * snd_array_new - get a new element from the given array + * @array: the array object + * + * Get a new element from the given array. If it exceeds the + * pre-allocated array size, re-allocate the array. + * + * Returns NULL if allocation failed. + */ +void *snd_array_new(struct snd_array *array) +{ + if (snd_BUG_ON(!array->elem_size)) + return NULL; + if (array->used >= array->alloced) { + int num = array->alloced + array->alloc_align; + int oldsize = array->alloced * array->elem_size; + int size = (num + 1) * array->elem_size; + void *nlist; + if (snd_BUG_ON(num >= 4096)) + return NULL; + nlist = krealloc(array->list, size, GFP_KERNEL); + if (!nlist) + return NULL; + memset(nlist + oldsize, 0, size - oldsize); + array->list = nlist; + array->alloced = num; + } + return snd_array_elem(array, array->used++); +} +EXPORT_SYMBOL_GPL(snd_array_new); + +/** + * snd_array_free - free the given array elements + * @array: the array object + */ +void snd_array_free(struct snd_array *array) +{ + kfree(array->list); + array->used = 0; + array->alloced = 0; + array->list = NULL; +} +EXPORT_SYMBOL_GPL(snd_array_free); diff --git a/sound/hda/core/bus.c b/sound/hda/core/bus.c new file mode 100644 index 0000000000000..d497414a5538f --- /dev/null +++ b/sound/hda/core/bus.c @@ -0,0 +1,289 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * HD-audio core bus driver + */ + +#include +#include +#include +#include +#include +#include +#include "local.h" +#include "trace.h" + +static void snd_hdac_bus_process_unsol_events(struct work_struct *work); + +static const struct hdac_bus_ops default_ops = { + .command = snd_hdac_bus_send_cmd, + .get_response = snd_hdac_bus_get_response, + .link_power = snd_hdac_bus_link_power, +}; + +/** + * snd_hdac_bus_init - initialize a HD-audio bas bus + * @bus: the pointer to bus object + * @dev: device pointer + * @ops: bus verb operators + * + * Returns 0 if successful, or a negative error code. + */ +int snd_hdac_bus_init(struct hdac_bus *bus, struct device *dev, + const struct hdac_bus_ops *ops) +{ + memset(bus, 0, sizeof(*bus)); + bus->dev = dev; + if (ops) + bus->ops = ops; + else + bus->ops = &default_ops; + bus->dma_type = SNDRV_DMA_TYPE_DEV; + INIT_LIST_HEAD(&bus->stream_list); + INIT_LIST_HEAD(&bus->codec_list); + INIT_WORK(&bus->unsol_work, snd_hdac_bus_process_unsol_events); + spin_lock_init(&bus->reg_lock); + mutex_init(&bus->cmd_mutex); + mutex_init(&bus->lock); + INIT_LIST_HEAD(&bus->hlink_list); + init_waitqueue_head(&bus->rirb_wq); + bus->irq = -1; + + /* + * Default value of '8' is as per the HD audio specification (Rev 1.0a). + * Following relation is used to derive STRIPE control value. + * For sample rate <= 48K: + * { ((num_channels * bits_per_sample) / number of SDOs) >= 8 } + * For sample rate > 48K: + * { ((num_channels * bits_per_sample * rate/48000) / + * number of SDOs) >= 8 } + */ + bus->sdo_limit = 8; + + return 0; +} +EXPORT_SYMBOL_GPL(snd_hdac_bus_init); + +/** + * snd_hdac_bus_exit - clean up a HD-audio bas bus + * @bus: the pointer to bus object + */ +void snd_hdac_bus_exit(struct hdac_bus *bus) +{ + WARN_ON(!list_empty(&bus->stream_list)); + WARN_ON(!list_empty(&bus->codec_list)); + cancel_work_sync(&bus->unsol_work); +} +EXPORT_SYMBOL_GPL(snd_hdac_bus_exit); + +/** + * snd_hdac_bus_exec_verb - execute a HD-audio verb on the given bus + * @bus: bus object + * @addr: the HDAC device address + * @cmd: HD-audio encoded verb + * @res: pointer to store the response, NULL if performing asynchronously + * + * Returns 0 if successful, or a negative error code. + */ +int snd_hdac_bus_exec_verb(struct hdac_bus *bus, unsigned int addr, + unsigned int cmd, unsigned int *res) +{ + int err; + + mutex_lock(&bus->cmd_mutex); + err = snd_hdac_bus_exec_verb_unlocked(bus, addr, cmd, res); + mutex_unlock(&bus->cmd_mutex); + return err; +} + +/** + * snd_hdac_bus_exec_verb_unlocked - unlocked version + * @bus: bus object + * @addr: the HDAC device address + * @cmd: HD-audio encoded verb + * @res: pointer to store the response, NULL if performing asynchronously + * + * Returns 0 if successful, or a negative error code. + */ +int snd_hdac_bus_exec_verb_unlocked(struct hdac_bus *bus, unsigned int addr, + unsigned int cmd, unsigned int *res) +{ + unsigned int tmp; + int err; + + if (cmd == ~0) + return -EINVAL; + + if (res) + *res = -1; + else if (bus->sync_write) + res = &tmp; + for (;;) { + trace_hda_send_cmd(bus, cmd); + err = bus->ops->command(bus, cmd); + if (err != -EAGAIN) + break; + /* process pending verbs */ + err = bus->ops->get_response(bus, addr, &tmp); + if (err) + break; + } + if (!err && res) { + err = bus->ops->get_response(bus, addr, res); + trace_hda_get_response(bus, addr, *res); + } + return err; +} +EXPORT_SYMBOL_GPL(snd_hdac_bus_exec_verb_unlocked); + +/** + * snd_hdac_bus_queue_event - add an unsolicited event to queue + * @bus: the BUS + * @res: unsolicited event (lower 32bit of RIRB entry) + * @res_ex: codec addr and flags (upper 32bit or RIRB entry) + * + * Adds the given event to the queue. The events are processed in + * the workqueue asynchronously. Call this function in the interrupt + * hanlder when RIRB receives an unsolicited event. + */ +void snd_hdac_bus_queue_event(struct hdac_bus *bus, u32 res, u32 res_ex) +{ + unsigned int wp; + + if (!bus) + return; + + trace_hda_unsol_event(bus, res, res_ex); + wp = (bus->unsol_wp + 1) % HDA_UNSOL_QUEUE_SIZE; + bus->unsol_wp = wp; + + wp <<= 1; + bus->unsol_queue[wp] = res; + bus->unsol_queue[wp + 1] = res_ex; + + schedule_work(&bus->unsol_work); +} + +/* + * process queued unsolicited events + */ +static void snd_hdac_bus_process_unsol_events(struct work_struct *work) +{ + struct hdac_bus *bus = container_of(work, struct hdac_bus, unsol_work); + struct hdac_device *codec; + struct hdac_driver *drv; + unsigned int rp, caddr, res; + + spin_lock_irq(&bus->reg_lock); + while (bus->unsol_rp != bus->unsol_wp) { + rp = (bus->unsol_rp + 1) % HDA_UNSOL_QUEUE_SIZE; + bus->unsol_rp = rp; + rp <<= 1; + res = bus->unsol_queue[rp]; + caddr = bus->unsol_queue[rp + 1]; + if (!(caddr & (1 << 4))) /* no unsolicited event? */ + continue; + codec = bus->caddr_tbl[caddr & 0x0f]; + if (!codec || !codec->registered) + continue; + spin_unlock_irq(&bus->reg_lock); + drv = drv_to_hdac_driver(codec->dev.driver); + if (drv->unsol_event) + drv->unsol_event(codec, res); + spin_lock_irq(&bus->reg_lock); + } + spin_unlock_irq(&bus->reg_lock); +} + +/** + * snd_hdac_bus_add_device - Add a codec to bus + * @bus: HDA core bus + * @codec: HDA core device to add + * + * Adds the given codec to the list in the bus. The caddr_tbl array + * and codec_powered bits are updated, as well. + * Returns zero if success, or a negative error code. + */ +int snd_hdac_bus_add_device(struct hdac_bus *bus, struct hdac_device *codec) +{ + if (bus->caddr_tbl[codec->addr]) { + dev_err(bus->dev, "address 0x%x is already occupied\n", + codec->addr); + return -EBUSY; + } + + list_add_tail(&codec->list, &bus->codec_list); + bus->caddr_tbl[codec->addr] = codec; + set_bit(codec->addr, &bus->codec_powered); + bus->num_codecs++; + return 0; +} + +/** + * snd_hdac_bus_remove_device - Remove a codec from bus + * @bus: HDA core bus + * @codec: HDA core device to remove + */ +void snd_hdac_bus_remove_device(struct hdac_bus *bus, + struct hdac_device *codec) +{ + WARN_ON(bus != codec->bus); + if (list_empty(&codec->list)) + return; + list_del_init(&codec->list); + bus->caddr_tbl[codec->addr] = NULL; + clear_bit(codec->addr, &bus->codec_powered); + bus->num_codecs--; + flush_work(&bus->unsol_work); +} + +#ifdef CONFIG_SND_HDA_ALIGNED_MMIO +/* Helpers for aligned read/write of mmio space, for Tegra */ +unsigned int snd_hdac_aligned_read(void __iomem *addr, unsigned int mask) +{ + void __iomem *aligned_addr = + (void __iomem *)((unsigned long)(addr) & ~0x3); + unsigned int shift = ((unsigned long)(addr) & 0x3) << 3; + unsigned int v; + + v = readl(aligned_addr); + return (v >> shift) & mask; +} +EXPORT_SYMBOL_GPL(snd_hdac_aligned_read); + +void snd_hdac_aligned_write(unsigned int val, void __iomem *addr, + unsigned int mask) +{ + void __iomem *aligned_addr = + (void __iomem *)((unsigned long)(addr) & ~0x3); + unsigned int shift = ((unsigned long)(addr) & 0x3) << 3; + unsigned int v; + + v = readl(aligned_addr); + v &= ~(mask << shift); + v |= val << shift; + writel(v, aligned_addr); +} +EXPORT_SYMBOL_GPL(snd_hdac_aligned_write); +#endif /* CONFIG_SND_HDA_ALIGNED_MMIO */ + +void snd_hdac_codec_link_up(struct hdac_device *codec) +{ + struct hdac_bus *bus = codec->bus; + + if (bus->ops->link_power) + bus->ops->link_power(codec, true); + else + snd_hdac_bus_link_power(codec, true); +} +EXPORT_SYMBOL_GPL(snd_hdac_codec_link_up); + +void snd_hdac_codec_link_down(struct hdac_device *codec) +{ + struct hdac_bus *bus = codec->bus; + + if (bus->ops->link_power) + bus->ops->link_power(codec, false); + else + snd_hdac_bus_link_power(codec, false); +} +EXPORT_SYMBOL_GPL(snd_hdac_codec_link_down); diff --git a/sound/hda/core/component.c b/sound/hda/core/component.c new file mode 100644 index 0000000000000..9c82a2864a2fb --- /dev/null +++ b/sound/hda/core/component.c @@ -0,0 +1,353 @@ +// SPDX-License-Identifier: GPL-2.0 +// hdac_component.c - routines for sync between HD-A core and DRM driver + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static void hdac_acomp_release(struct device *dev, void *res) +{ +} + +static struct drm_audio_component *hdac_get_acomp(struct device *dev) +{ + return devres_find(dev, hdac_acomp_release, NULL, NULL); +} + +/** + * snd_hdac_set_codec_wakeup - Enable / disable HDMI/DP codec wakeup + * @bus: HDA core bus + * @enable: enable or disable the wakeup + * + * This function is supposed to be used only by a HD-audio controller + * driver that needs the interaction with graphics driver. + * + * This function should be called during the chip reset, also called at + * resume for updating STATESTS register read. + * + * Returns zero for success or a negative error code. + */ +int snd_hdac_set_codec_wakeup(struct hdac_bus *bus, bool enable) +{ + struct drm_audio_component *acomp = bus->audio_component; + + if (!acomp || !acomp->ops) + return -ENODEV; + + if (!acomp->ops->codec_wake_override) + return 0; + + dev_dbg(bus->dev, "%s codec wakeup\n", str_enable_disable(enable)); + + acomp->ops->codec_wake_override(acomp->dev, enable); + + return 0; +} +EXPORT_SYMBOL_GPL(snd_hdac_set_codec_wakeup); + +/** + * snd_hdac_display_power - Power up / down the power refcount + * @bus: HDA core bus + * @idx: HDA codec address, pass HDA_CODEC_IDX_CONTROLLER for controller + * @enable: power up or down + * + * This function is used by either HD-audio controller or codec driver that + * needs the interaction with graphics driver. + * + * This function updates the power status, and calls the get_power() and + * put_power() ops accordingly, toggling the codec wakeup, too. + */ +void snd_hdac_display_power(struct hdac_bus *bus, unsigned int idx, bool enable) +{ + struct drm_audio_component *acomp = bus->audio_component; + + dev_dbg(bus->dev, "display power %s\n", str_enable_disable(enable)); + + mutex_lock(&bus->lock); + if (enable) + set_bit(idx, &bus->display_power_status); + else + clear_bit(idx, &bus->display_power_status); + + if (!acomp || !acomp->ops) + goto unlock; + + if (bus->display_power_status) { + if (!bus->display_power_active) { + unsigned long cookie = -1; + + if (acomp->ops->get_power) + cookie = acomp->ops->get_power(acomp->dev); + + snd_hdac_set_codec_wakeup(bus, true); + snd_hdac_set_codec_wakeup(bus, false); + bus->display_power_active = cookie; + } + } else { + if (bus->display_power_active) { + unsigned long cookie = bus->display_power_active; + + if (acomp->ops->put_power) + acomp->ops->put_power(acomp->dev, cookie); + + bus->display_power_active = 0; + } + } + unlock: + mutex_unlock(&bus->lock); +} +EXPORT_SYMBOL_GPL(snd_hdac_display_power); + +/** + * snd_hdac_sync_audio_rate - Set N/CTS based on the sample rate + * @codec: HDA codec + * @nid: the pin widget NID + * @dev_id: device identifier + * @rate: the sample rate to set + * + * This function is supposed to be used only by a HD-audio controller + * driver that needs the interaction with graphics driver. + * + * This function sets N/CTS value based on the given sample rate. + * Returns zero for success, or a negative error code. + */ +int snd_hdac_sync_audio_rate(struct hdac_device *codec, hda_nid_t nid, + int dev_id, int rate) +{ + struct hdac_bus *bus = codec->bus; + struct drm_audio_component *acomp = bus->audio_component; + int port, pipe; + + if (!acomp || !acomp->ops || !acomp->ops->sync_audio_rate) + return -ENODEV; + port = nid; + if (acomp->audio_ops && acomp->audio_ops->pin2port) { + port = acomp->audio_ops->pin2port(codec, nid); + if (port < 0) + return -EINVAL; + } + pipe = dev_id; + return acomp->ops->sync_audio_rate(acomp->dev, port, pipe, rate); +} +EXPORT_SYMBOL_GPL(snd_hdac_sync_audio_rate); + +/** + * snd_hdac_acomp_get_eld - Get the audio state and ELD via component + * @codec: HDA codec + * @nid: the pin widget NID + * @dev_id: device identifier + * @audio_enabled: the pointer to store the current audio state + * @buffer: the buffer pointer to store ELD bytes + * @max_bytes: the max bytes to be stored on @buffer + * + * This function is supposed to be used only by a HD-audio controller + * driver that needs the interaction with graphics driver. + * + * This function queries the current state of the audio on the given + * digital port and fetches the ELD bytes onto the given buffer. + * It returns the number of bytes for the total ELD data, zero for + * invalid ELD, or a negative error code. + * + * The return size is the total bytes required for the whole ELD bytes, + * thus it may be over @max_bytes. If it's over @max_bytes, it implies + * that only a part of ELD bytes have been fetched. + */ +int snd_hdac_acomp_get_eld(struct hdac_device *codec, hda_nid_t nid, int dev_id, + bool *audio_enabled, char *buffer, int max_bytes) +{ + struct hdac_bus *bus = codec->bus; + struct drm_audio_component *acomp = bus->audio_component; + int port, pipe; + + if (!acomp || !acomp->ops || !acomp->ops->get_eld) + return -ENODEV; + + port = nid; + if (acomp->audio_ops && acomp->audio_ops->pin2port) { + port = acomp->audio_ops->pin2port(codec, nid); + if (port < 0) + return -EINVAL; + } + pipe = dev_id; + return acomp->ops->get_eld(acomp->dev, port, pipe, audio_enabled, + buffer, max_bytes); +} +EXPORT_SYMBOL_GPL(snd_hdac_acomp_get_eld); + +static int hdac_component_master_bind(struct device *dev) +{ + struct drm_audio_component *acomp = hdac_get_acomp(dev); + int ret; + + if (WARN_ON(!acomp)) + return -EINVAL; + + ret = component_bind_all(dev, acomp); + if (ret < 0) + return ret; + + if (WARN_ON(!(acomp->dev && acomp->ops))) { + ret = -EINVAL; + goto out_unbind; + } + + /* pin the module to avoid dynamic unbinding, but only if given */ + if (!try_module_get(acomp->ops->owner)) { + ret = -ENODEV; + goto out_unbind; + } + + if (acomp->audio_ops && acomp->audio_ops->master_bind) { + ret = acomp->audio_ops->master_bind(dev, acomp); + if (ret < 0) + goto module_put; + } + + complete_all(&acomp->master_bind_complete); + return 0; + + module_put: + module_put(acomp->ops->owner); +out_unbind: + component_unbind_all(dev, acomp); + complete_all(&acomp->master_bind_complete); + + return ret; +} + +static void hdac_component_master_unbind(struct device *dev) +{ + struct drm_audio_component *acomp = hdac_get_acomp(dev); + + if (acomp->audio_ops && acomp->audio_ops->master_unbind) + acomp->audio_ops->master_unbind(dev, acomp); + module_put(acomp->ops->owner); + component_unbind_all(dev, acomp); + WARN_ON(acomp->ops || acomp->dev); +} + +static const struct component_master_ops hdac_component_master_ops = { + .bind = hdac_component_master_bind, + .unbind = hdac_component_master_unbind, +}; + +/** + * snd_hdac_acomp_register_notifier - Register audio component ops + * @bus: HDA core bus + * @aops: audio component ops + * + * This function is supposed to be used only by a HD-audio controller + * driver that needs the interaction with graphics driver. + * + * This function sets the given ops to be called by the graphics driver. + * + * Returns zero for success or a negative error code. + */ +int snd_hdac_acomp_register_notifier(struct hdac_bus *bus, + const struct drm_audio_component_audio_ops *aops) +{ + if (!bus->audio_component) + return -ENODEV; + + bus->audio_component->audio_ops = aops; + return 0; +} +EXPORT_SYMBOL_GPL(snd_hdac_acomp_register_notifier); + +/** + * snd_hdac_acomp_init - Initialize audio component + * @bus: HDA core bus + * @aops: audio component ops + * @match_master: match function for finding components + * @extra_size: Extra bytes to allocate + * + * This function is supposed to be used only by a HD-audio controller + * driver that needs the interaction with graphics driver. + * + * This function initializes and sets up the audio component to communicate + * with graphics driver. + * + * Unlike snd_hdac_i915_init(), this function doesn't synchronize with the + * binding with the DRM component. Each caller needs to sync via master_bind + * audio_ops. + * + * Returns zero for success or a negative error code. + */ +int snd_hdac_acomp_init(struct hdac_bus *bus, + const struct drm_audio_component_audio_ops *aops, + int (*match_master)(struct device *, int, void *), + size_t extra_size) +{ + struct component_match *match = NULL; + struct device *dev = bus->dev; + struct drm_audio_component *acomp; + int ret; + + if (WARN_ON(hdac_get_acomp(dev))) + return -EBUSY; + + acomp = devres_alloc(hdac_acomp_release, sizeof(*acomp) + extra_size, + GFP_KERNEL); + if (!acomp) + return -ENOMEM; + acomp->audio_ops = aops; + init_completion(&acomp->master_bind_complete); + bus->audio_component = acomp; + devres_add(dev, acomp); + + component_match_add_typed(dev, &match, match_master, bus); + ret = component_master_add_with_match(dev, &hdac_component_master_ops, + match); + if (ret < 0) + goto out_err; + + return 0; + +out_err: + bus->audio_component = NULL; + devres_destroy(dev, hdac_acomp_release, NULL, NULL); + dev_info(dev, "failed to add audio component master (%d)\n", ret); + + return ret; +} +EXPORT_SYMBOL_GPL(snd_hdac_acomp_init); + +/** + * snd_hdac_acomp_exit - Finalize audio component + * @bus: HDA core bus + * + * This function is supposed to be used only by a HD-audio controller + * driver that needs the interaction with graphics driver. + * + * This function releases the audio component that has been used. + * + * Returns zero for success or a negative error code. + */ +int snd_hdac_acomp_exit(struct hdac_bus *bus) +{ + struct device *dev = bus->dev; + struct drm_audio_component *acomp = bus->audio_component; + + if (!acomp) + return 0; + + if (WARN_ON(bus->display_power_active) && acomp->ops) + acomp->ops->put_power(acomp->dev, bus->display_power_active); + + bus->display_power_active = 0; + bus->display_power_status = 0; + + component_master_del(dev, &hdac_component_master_ops); + + bus->audio_component = NULL; + devres_destroy(dev, hdac_acomp_release, NULL, NULL); + + return 0; +} +EXPORT_SYMBOL_GPL(snd_hdac_acomp_exit); diff --git a/sound/hda/core/controller.c b/sound/hda/core/controller.c new file mode 100644 index 0000000000000..b5c833b9f8b9c --- /dev/null +++ b/sound/hda/core/controller.c @@ -0,0 +1,778 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * HD-audio controller helpers + */ + +#include +#include +#include +#include +#include +#include +#include "local.h" + +/* clear CORB read pointer properly */ +static void azx_clear_corbrp(struct hdac_bus *bus) +{ + int timeout; + + for (timeout = 1000; timeout > 0; timeout--) { + if (snd_hdac_chip_readw(bus, CORBRP) & AZX_CORBRP_RST) + break; + udelay(1); + } + if (timeout <= 0) + dev_err(bus->dev, "CORB reset timeout#1, CORBRP = %d\n", + snd_hdac_chip_readw(bus, CORBRP)); + + snd_hdac_chip_writew(bus, CORBRP, 0); + for (timeout = 1000; timeout > 0; timeout--) { + if (snd_hdac_chip_readw(bus, CORBRP) == 0) + break; + udelay(1); + } + if (timeout <= 0) + dev_err(bus->dev, "CORB reset timeout#2, CORBRP = %d\n", + snd_hdac_chip_readw(bus, CORBRP)); +} + +/** + * snd_hdac_bus_init_cmd_io - set up CORB/RIRB buffers + * @bus: HD-audio core bus + */ +void snd_hdac_bus_init_cmd_io(struct hdac_bus *bus) +{ + WARN_ON_ONCE(!bus->rb.area); + + spin_lock_irq(&bus->reg_lock); + /* CORB set up */ + bus->corb.addr = bus->rb.addr; + bus->corb.buf = (__le32 *)bus->rb.area; + snd_hdac_chip_writel(bus, CORBLBASE, (u32)bus->corb.addr); + snd_hdac_chip_writel(bus, CORBUBASE, upper_32_bits(bus->corb.addr)); + + /* set the corb size to 256 entries (ULI requires explicitly) */ + snd_hdac_chip_writeb(bus, CORBSIZE, 0x02); + /* set the corb write pointer to 0 */ + snd_hdac_chip_writew(bus, CORBWP, 0); + + /* reset the corb hw read pointer */ + snd_hdac_chip_writew(bus, CORBRP, AZX_CORBRP_RST); + if (!bus->corbrp_self_clear) + azx_clear_corbrp(bus); + + /* enable corb dma */ + if (!bus->use_pio_for_commands) + snd_hdac_chip_writeb(bus, CORBCTL, AZX_CORBCTL_RUN); + + /* RIRB set up */ + bus->rirb.addr = bus->rb.addr + 2048; + bus->rirb.buf = (__le32 *)(bus->rb.area + 2048); + bus->rirb.wp = bus->rirb.rp = 0; + memset(bus->rirb.cmds, 0, sizeof(bus->rirb.cmds)); + snd_hdac_chip_writel(bus, RIRBLBASE, (u32)bus->rirb.addr); + snd_hdac_chip_writel(bus, RIRBUBASE, upper_32_bits(bus->rirb.addr)); + + /* set the rirb size to 256 entries (ULI requires explicitly) */ + snd_hdac_chip_writeb(bus, RIRBSIZE, 0x02); + /* reset the rirb hw write pointer */ + snd_hdac_chip_writew(bus, RIRBWP, AZX_RIRBWP_RST); + /* set N=1, get RIRB response interrupt for new entry */ + snd_hdac_chip_writew(bus, RINTCNT, 1); + /* enable rirb dma and response irq */ + if (bus->not_use_interrupts) + snd_hdac_chip_writeb(bus, RIRBCTL, AZX_RBCTL_DMA_EN); + else + snd_hdac_chip_writeb(bus, RIRBCTL, AZX_RBCTL_DMA_EN | AZX_RBCTL_IRQ_EN); + /* Accept unsolicited responses */ + snd_hdac_chip_updatel(bus, GCTL, AZX_GCTL_UNSOL, AZX_GCTL_UNSOL); + spin_unlock_irq(&bus->reg_lock); +} +EXPORT_SYMBOL_GPL(snd_hdac_bus_init_cmd_io); + +/* wait for cmd dmas till they are stopped */ +static void hdac_wait_for_cmd_dmas(struct hdac_bus *bus) +{ + unsigned long timeout; + + timeout = jiffies + msecs_to_jiffies(100); + while ((snd_hdac_chip_readb(bus, RIRBCTL) & AZX_RBCTL_DMA_EN) + && time_before(jiffies, timeout)) + udelay(10); + + timeout = jiffies + msecs_to_jiffies(100); + while ((snd_hdac_chip_readb(bus, CORBCTL) & AZX_CORBCTL_RUN) + && time_before(jiffies, timeout)) + udelay(10); +} + +/** + * snd_hdac_bus_stop_cmd_io - clean up CORB/RIRB buffers + * @bus: HD-audio core bus + */ +void snd_hdac_bus_stop_cmd_io(struct hdac_bus *bus) +{ + spin_lock_irq(&bus->reg_lock); + /* disable ringbuffer DMAs */ + snd_hdac_chip_writeb(bus, RIRBCTL, 0); + snd_hdac_chip_writeb(bus, CORBCTL, 0); + spin_unlock_irq(&bus->reg_lock); + + hdac_wait_for_cmd_dmas(bus); + + spin_lock_irq(&bus->reg_lock); + /* disable unsolicited responses */ + snd_hdac_chip_updatel(bus, GCTL, AZX_GCTL_UNSOL, 0); + spin_unlock_irq(&bus->reg_lock); +} +EXPORT_SYMBOL_GPL(snd_hdac_bus_stop_cmd_io); + +static unsigned int azx_command_addr(u32 cmd) +{ + unsigned int addr = cmd >> 28; + + if (snd_BUG_ON(addr >= HDA_MAX_CODECS)) + addr = 0; + return addr; +} + +/* receive an Immediate Response with PIO */ +static int snd_hdac_bus_wait_for_pio_response(struct hdac_bus *bus, + unsigned int addr) +{ + int timeout = 50; + + while (timeout--) { + /* check IRV bit */ + if (snd_hdac_chip_readw(bus, IRS) & AZX_IRS_VALID) { + /* reuse rirb.res as the response return value */ + bus->rirb.res[addr] = snd_hdac_chip_readl(bus, IR); + return 0; + } + udelay(1); + } + + dev_dbg_ratelimited(bus->dev, "get_response_pio timeout: IRS=%#x\n", + snd_hdac_chip_readw(bus, IRS)); + + bus->rirb.res[addr] = -1; + + return -EIO; +} + +/** + * snd_hdac_bus_send_cmd_pio - send a command verb via Immediate Command + * @bus: HD-audio core bus + * @val: encoded verb value to send + * + * Returns zero for success or a negative error code. + */ +static int snd_hdac_bus_send_cmd_pio(struct hdac_bus *bus, unsigned int val) +{ + unsigned int addr = azx_command_addr(val); + int timeout = 50; + int ret = -EIO; + + spin_lock_irq(&bus->reg_lock); + + while (timeout--) { + /* check ICB bit */ + if (!((snd_hdac_chip_readw(bus, IRS) & AZX_IRS_BUSY))) { + /* Clear IRV bit */ + snd_hdac_chip_updatew(bus, IRS, AZX_IRS_VALID, AZX_IRS_VALID); + snd_hdac_chip_writel(bus, IC, val); + /* Set ICB bit */ + snd_hdac_chip_updatew(bus, IRS, AZX_IRS_BUSY, AZX_IRS_BUSY); + + ret = snd_hdac_bus_wait_for_pio_response(bus, addr); + goto out; + } + udelay(1); + } + + dev_dbg_ratelimited(bus->dev, "send_cmd_pio timeout: IRS=%#x, val=%#x\n", + snd_hdac_chip_readw(bus, IRS), val); + +out: + spin_unlock_irq(&bus->reg_lock); + + return ret; +} + +/** + * snd_hdac_bus_get_response_pio - receive a response via Immediate Response + * @bus: HD-audio core bus + * @addr: codec address + * @res: pointer to store the value, NULL when not needed + * + * Returns zero if a value is read, or a negative error code. + */ +static int snd_hdac_bus_get_response_pio(struct hdac_bus *bus, + unsigned int addr, unsigned int *res) +{ + if (res) + *res = bus->rirb.res[addr]; + + return 0; +} + +/** + * snd_hdac_bus_send_cmd_corb - send a command verb via CORB + * @bus: HD-audio core bus + * @val: encoded verb value to send + * + * Returns zero for success or a negative error code. + */ +static int snd_hdac_bus_send_cmd_corb(struct hdac_bus *bus, unsigned int val) +{ + unsigned int addr = azx_command_addr(val); + unsigned int wp, rp; + + spin_lock_irq(&bus->reg_lock); + + bus->last_cmd[azx_command_addr(val)] = val; + + /* add command to corb */ + wp = snd_hdac_chip_readw(bus, CORBWP); + if (wp == 0xffff) { + /* something wrong, controller likely turned to D3 */ + spin_unlock_irq(&bus->reg_lock); + return -EIO; + } + wp++; + wp %= AZX_MAX_CORB_ENTRIES; + + rp = snd_hdac_chip_readw(bus, CORBRP); + if (wp == rp) { + /* oops, it's full */ + spin_unlock_irq(&bus->reg_lock); + return -EAGAIN; + } + + bus->rirb.cmds[addr]++; + bus->corb.buf[wp] = cpu_to_le32(val); + snd_hdac_chip_writew(bus, CORBWP, wp); + + spin_unlock_irq(&bus->reg_lock); + + return 0; +} + +#define AZX_RIRB_EX_UNSOL_EV (1<<4) + +/** + * snd_hdac_bus_update_rirb - retrieve RIRB entries + * @bus: HD-audio core bus + * + * Usually called from interrupt handler. + * The caller needs bus->reg_lock spinlock before calling this. + */ +void snd_hdac_bus_update_rirb(struct hdac_bus *bus) +{ + unsigned int rp, wp; + unsigned int addr; + u32 res, res_ex; + + wp = snd_hdac_chip_readw(bus, RIRBWP); + if (wp == 0xffff) { + /* something wrong, controller likely turned to D3 */ + return; + } + + if (wp == bus->rirb.wp) + return; + bus->rirb.wp = wp; + + while (bus->rirb.rp != wp) { + bus->rirb.rp++; + bus->rirb.rp %= AZX_MAX_RIRB_ENTRIES; + + rp = bus->rirb.rp << 1; /* an RIRB entry is 8-bytes */ + res_ex = le32_to_cpu(bus->rirb.buf[rp + 1]); + res = le32_to_cpu(bus->rirb.buf[rp]); + addr = res_ex & 0xf; + if (addr >= HDA_MAX_CODECS) { + dev_err(bus->dev, + "spurious response %#x:%#x, rp = %d, wp = %d", + res, res_ex, bus->rirb.rp, wp); + snd_BUG(); + } else if (res_ex & AZX_RIRB_EX_UNSOL_EV) + snd_hdac_bus_queue_event(bus, res, res_ex); + else if (bus->rirb.cmds[addr]) { + bus->rirb.res[addr] = res; + bus->rirb.cmds[addr]--; + if (!bus->rirb.cmds[addr] && + waitqueue_active(&bus->rirb_wq)) + wake_up(&bus->rirb_wq); + } else { + dev_err_ratelimited(bus->dev, + "spurious response %#x:%#x, last cmd=%#08x\n", + res, res_ex, bus->last_cmd[addr]); + } + } +} +EXPORT_SYMBOL_GPL(snd_hdac_bus_update_rirb); + +/** + * snd_hdac_bus_get_response_rirb - receive a response via RIRB + * @bus: HD-audio core bus + * @addr: codec address + * @res: pointer to store the value, NULL when not needed + * + * Returns zero if a value is read, or a negative error code. + */ +static int snd_hdac_bus_get_response_rirb(struct hdac_bus *bus, + unsigned int addr, unsigned int *res) +{ + unsigned long timeout; + unsigned long loopcounter; + wait_queue_entry_t wait; + bool warned = false; + + init_wait_entry(&wait, 0); + timeout = jiffies + msecs_to_jiffies(1000); + + for (loopcounter = 0;; loopcounter++) { + spin_lock_irq(&bus->reg_lock); + if (!bus->polling_mode) + prepare_to_wait(&bus->rirb_wq, &wait, + TASK_UNINTERRUPTIBLE); + if (bus->polling_mode) + snd_hdac_bus_update_rirb(bus); + if (!bus->rirb.cmds[addr]) { + if (res) + *res = bus->rirb.res[addr]; /* the last value */ + if (!bus->polling_mode) + finish_wait(&bus->rirb_wq, &wait); + spin_unlock_irq(&bus->reg_lock); + return 0; + } + spin_unlock_irq(&bus->reg_lock); + if (time_after(jiffies, timeout)) + break; +#define LOOP_COUNT_MAX 3000 + if (!bus->polling_mode) { + schedule_timeout(msecs_to_jiffies(2)); + } else if (bus->needs_damn_long_delay || + loopcounter > LOOP_COUNT_MAX) { + if (loopcounter > LOOP_COUNT_MAX && !warned) { + dev_dbg_ratelimited(bus->dev, + "too slow response, last cmd=%#08x\n", + bus->last_cmd[addr]); + warned = true; + } + msleep(2); /* temporary workaround */ + } else { + udelay(10); + cond_resched(); + } + } + + if (!bus->polling_mode) + finish_wait(&bus->rirb_wq, &wait); + + return -EIO; +} + +/** + * snd_hdac_bus_send_cmd - send a command verb via CORB or PIO + * @bus: HD-audio core bus + * @val: encoded verb value to send + * + * Returns zero for success or a negative error code. + */ +int snd_hdac_bus_send_cmd(struct hdac_bus *bus, unsigned int val) +{ + if (bus->use_pio_for_commands) + return snd_hdac_bus_send_cmd_pio(bus, val); + + return snd_hdac_bus_send_cmd_corb(bus, val); +} +EXPORT_SYMBOL_GPL(snd_hdac_bus_send_cmd); + +/** + * snd_hdac_bus_get_response - receive a response via RIRB or PIO + * @bus: HD-audio core bus + * @addr: codec address + * @res: pointer to store the value, NULL when not needed + * + * Returns zero if a value is read, or a negative error code. + */ +int snd_hdac_bus_get_response(struct hdac_bus *bus, unsigned int addr, + unsigned int *res) +{ + if (bus->use_pio_for_commands) + return snd_hdac_bus_get_response_pio(bus, addr, res); + + return snd_hdac_bus_get_response_rirb(bus, addr, res); +} +EXPORT_SYMBOL_GPL(snd_hdac_bus_get_response); + +#define HDAC_MAX_CAPS 10 +/** + * snd_hdac_bus_parse_capabilities - parse capability structure + * @bus: the pointer to bus object + * + * Returns 0 if successful, or a negative error code. + */ +int snd_hdac_bus_parse_capabilities(struct hdac_bus *bus) +{ + unsigned int cur_cap; + unsigned int offset; + unsigned int counter = 0; + + offset = snd_hdac_chip_readw(bus, LLCH); + + /* Lets walk the linked capabilities list */ + do { + cur_cap = _snd_hdac_chip_readl(bus, offset); + + dev_dbg(bus->dev, "Capability version: 0x%x\n", + (cur_cap & AZX_CAP_HDR_VER_MASK) >> AZX_CAP_HDR_VER_OFF); + + dev_dbg(bus->dev, "HDA capability ID: 0x%x\n", + (cur_cap & AZX_CAP_HDR_ID_MASK) >> AZX_CAP_HDR_ID_OFF); + + if (cur_cap == -1) { + dev_dbg(bus->dev, "Invalid capability reg read\n"); + break; + } + + switch ((cur_cap & AZX_CAP_HDR_ID_MASK) >> AZX_CAP_HDR_ID_OFF) { + case AZX_ML_CAP_ID: + dev_dbg(bus->dev, "Found ML capability\n"); + bus->mlcap = bus->remap_addr + offset; + break; + + case AZX_GTS_CAP_ID: + dev_dbg(bus->dev, "Found GTS capability offset=%x\n", offset); + bus->gtscap = bus->remap_addr + offset; + break; + + case AZX_PP_CAP_ID: + /* PP capability found, the Audio DSP is present */ + dev_dbg(bus->dev, "Found PP capability offset=%x\n", offset); + bus->ppcap = bus->remap_addr + offset; + break; + + case AZX_SPB_CAP_ID: + /* SPIB capability found, handler function */ + dev_dbg(bus->dev, "Found SPB capability\n"); + bus->spbcap = bus->remap_addr + offset; + break; + + case AZX_DRSM_CAP_ID: + /* DMA resume capability found, handler function */ + dev_dbg(bus->dev, "Found DRSM capability\n"); + bus->drsmcap = bus->remap_addr + offset; + break; + + default: + dev_err(bus->dev, "Unknown capability %d\n", cur_cap); + cur_cap = 0; + break; + } + + counter++; + + if (counter > HDAC_MAX_CAPS) { + dev_err(bus->dev, "We exceeded HDAC capabilities!!!\n"); + break; + } + + /* read the offset of next capability */ + offset = cur_cap & AZX_CAP_HDR_NXT_PTR_MASK; + + } while (offset); + + return 0; +} +EXPORT_SYMBOL_GPL(snd_hdac_bus_parse_capabilities); + +/* + * Lowlevel interface + */ + +/** + * snd_hdac_bus_enter_link_reset - enter link reset + * @bus: HD-audio core bus + * + * Enter to the link reset state. + */ +void snd_hdac_bus_enter_link_reset(struct hdac_bus *bus) +{ + unsigned long timeout; + + /* reset controller */ + snd_hdac_chip_updatel(bus, GCTL, AZX_GCTL_RESET, 0); + + timeout = jiffies + msecs_to_jiffies(100); + while ((snd_hdac_chip_readb(bus, GCTL) & AZX_GCTL_RESET) && + time_before(jiffies, timeout)) + usleep_range(500, 1000); +} +EXPORT_SYMBOL_GPL(snd_hdac_bus_enter_link_reset); + +/** + * snd_hdac_bus_exit_link_reset - exit link reset + * @bus: HD-audio core bus + * + * Exit from the link reset state. + */ +void snd_hdac_bus_exit_link_reset(struct hdac_bus *bus) +{ + unsigned long timeout; + + snd_hdac_chip_updateb(bus, GCTL, AZX_GCTL_RESET, AZX_GCTL_RESET); + + timeout = jiffies + msecs_to_jiffies(100); + while (!snd_hdac_chip_readb(bus, GCTL) && time_before(jiffies, timeout)) + usleep_range(500, 1000); +} +EXPORT_SYMBOL_GPL(snd_hdac_bus_exit_link_reset); + +/* reset codec link */ +int snd_hdac_bus_reset_link(struct hdac_bus *bus, bool full_reset) +{ + if (!full_reset) + goto skip_reset; + + /* clear STATESTS if not in reset */ + if (snd_hdac_chip_readb(bus, GCTL) & AZX_GCTL_RESET) + snd_hdac_chip_writew(bus, STATESTS, STATESTS_INT_MASK); + + /* reset controller */ + snd_hdac_bus_enter_link_reset(bus); + + /* delay for >= 100us for codec PLL to settle per spec + * Rev 0.9 section 5.5.1 + */ + usleep_range(500, 1000); + + /* Bring controller out of reset */ + snd_hdac_bus_exit_link_reset(bus); + + /* Brent Chartrand said to wait >= 540us for codecs to initialize */ + usleep_range(1000, 1200); + + skip_reset: + /* check to see if controller is ready */ + if (!snd_hdac_chip_readb(bus, GCTL)) { + dev_dbg(bus->dev, "controller not ready!\n"); + return -EBUSY; + } + + /* detect codecs */ + if (!bus->codec_mask) { + bus->codec_mask = snd_hdac_chip_readw(bus, STATESTS); + dev_dbg(bus->dev, "codec_mask = 0x%lx\n", bus->codec_mask); + } + + return 0; +} +EXPORT_SYMBOL_GPL(snd_hdac_bus_reset_link); + +/* enable interrupts */ +static void azx_int_enable(struct hdac_bus *bus) +{ + /* enable controller CIE and GIE */ + snd_hdac_chip_updatel(bus, INTCTL, + AZX_INT_CTRL_EN | AZX_INT_GLOBAL_EN, + AZX_INT_CTRL_EN | AZX_INT_GLOBAL_EN); +} + +/* disable interrupts */ +static void azx_int_disable(struct hdac_bus *bus) +{ + struct hdac_stream *azx_dev; + + /* disable interrupts in stream descriptor */ + list_for_each_entry(azx_dev, &bus->stream_list, list) + snd_hdac_stream_updateb(azx_dev, SD_CTL, SD_INT_MASK, 0); + + /* disable SIE for all streams & disable controller CIE and GIE */ + snd_hdac_chip_writel(bus, INTCTL, 0); +} + +/* clear interrupts */ +static void azx_int_clear(struct hdac_bus *bus) +{ + struct hdac_stream *azx_dev; + + /* clear stream status */ + list_for_each_entry(azx_dev, &bus->stream_list, list) + snd_hdac_stream_writeb(azx_dev, SD_STS, SD_INT_MASK); + + /* clear STATESTS */ + snd_hdac_chip_writew(bus, STATESTS, STATESTS_INT_MASK); + + /* clear rirb status */ + snd_hdac_chip_writeb(bus, RIRBSTS, RIRB_INT_MASK); + + /* clear int status */ + snd_hdac_chip_writel(bus, INTSTS, AZX_INT_CTRL_EN | AZX_INT_ALL_STREAM); +} + +/** + * snd_hdac_bus_init_chip - reset and start the controller registers + * @bus: HD-audio core bus + * @full_reset: Do full reset + */ +bool snd_hdac_bus_init_chip(struct hdac_bus *bus, bool full_reset) +{ + if (bus->chip_init) + return false; + + /* reset controller */ + snd_hdac_bus_reset_link(bus, full_reset); + + /* clear interrupts */ + azx_int_clear(bus); + + /* initialize the codec command I/O */ + snd_hdac_bus_init_cmd_io(bus); + + /* enable interrupts after CORB/RIRB buffers are initialized above */ + azx_int_enable(bus); + + /* program the position buffer */ + if (bus->use_posbuf && bus->posbuf.addr) { + snd_hdac_chip_writel(bus, DPLBASE, (u32)bus->posbuf.addr); + snd_hdac_chip_writel(bus, DPUBASE, upper_32_bits(bus->posbuf.addr)); + } + + bus->chip_init = true; + + return true; +} +EXPORT_SYMBOL_GPL(snd_hdac_bus_init_chip); + +/** + * snd_hdac_bus_stop_chip - disable the whole IRQ and I/Os + * @bus: HD-audio core bus + */ +void snd_hdac_bus_stop_chip(struct hdac_bus *bus) +{ + if (!bus->chip_init) + return; + + /* disable interrupts */ + azx_int_disable(bus); + azx_int_clear(bus); + + /* disable CORB/RIRB */ + snd_hdac_bus_stop_cmd_io(bus); + + /* disable position buffer */ + if (bus->posbuf.addr) { + snd_hdac_chip_writel(bus, DPLBASE, 0); + snd_hdac_chip_writel(bus, DPUBASE, 0); + } + + bus->chip_init = false; +} +EXPORT_SYMBOL_GPL(snd_hdac_bus_stop_chip); + +/** + * snd_hdac_bus_handle_stream_irq - interrupt handler for streams + * @bus: HD-audio core bus + * @status: INTSTS register value + * @ack: callback to be called for woken streams + * + * Returns the bits of handled streams, or zero if no stream is handled. + */ +int snd_hdac_bus_handle_stream_irq(struct hdac_bus *bus, unsigned int status, + void (*ack)(struct hdac_bus *, + struct hdac_stream *)) +{ + struct hdac_stream *azx_dev; + u8 sd_status; + int handled = 0; + + list_for_each_entry(azx_dev, &bus->stream_list, list) { + if (status & azx_dev->sd_int_sta_mask) { + sd_status = snd_hdac_stream_readb(azx_dev, SD_STS); + snd_hdac_stream_writeb(azx_dev, SD_STS, SD_INT_MASK); + handled |= 1 << azx_dev->index; + if ((!azx_dev->substream && !azx_dev->cstream) || + !azx_dev->running || !(sd_status & SD_INT_COMPLETE)) + continue; + if (ack) + ack(bus, azx_dev); + } + } + return handled; +} +EXPORT_SYMBOL_GPL(snd_hdac_bus_handle_stream_irq); + +/** + * snd_hdac_bus_alloc_stream_pages - allocate BDL and other buffers + * @bus: HD-audio core bus + * + * Call this after assigning the all streams. + * Returns zero for success, or a negative error code. + */ +int snd_hdac_bus_alloc_stream_pages(struct hdac_bus *bus) +{ + struct hdac_stream *s; + int num_streams = 0; + int dma_type = bus->dma_type ? bus->dma_type : SNDRV_DMA_TYPE_DEV; + int err; + + list_for_each_entry(s, &bus->stream_list, list) { + /* allocate memory for the BDL for each stream */ + err = snd_dma_alloc_pages(dma_type, bus->dev, + BDL_SIZE, &s->bdl); + num_streams++; + if (err < 0) + return -ENOMEM; + } + + if (WARN_ON(!num_streams)) + return -EINVAL; + /* allocate memory for the position buffer */ + err = snd_dma_alloc_pages(dma_type, bus->dev, + num_streams * 8, &bus->posbuf); + if (err < 0) + return -ENOMEM; + list_for_each_entry(s, &bus->stream_list, list) + s->posbuf = (__le32 *)(bus->posbuf.area + s->index * 8); + + /* single page (at least 4096 bytes) must suffice for both ringbuffes */ + return snd_dma_alloc_pages(dma_type, bus->dev, PAGE_SIZE, &bus->rb); +} +EXPORT_SYMBOL_GPL(snd_hdac_bus_alloc_stream_pages); + +/** + * snd_hdac_bus_free_stream_pages - release BDL and other buffers + * @bus: HD-audio core bus + */ +void snd_hdac_bus_free_stream_pages(struct hdac_bus *bus) +{ + struct hdac_stream *s; + + list_for_each_entry(s, &bus->stream_list, list) { + if (s->bdl.area) + snd_dma_free_pages(&s->bdl); + } + + if (bus->rb.area) + snd_dma_free_pages(&bus->rb); + if (bus->posbuf.area) + snd_dma_free_pages(&bus->posbuf); +} +EXPORT_SYMBOL_GPL(snd_hdac_bus_free_stream_pages); + +/** + * snd_hdac_bus_link_power - power up/down codec link + * @codec: HD-audio device + * @enable: whether to power-up the link + */ +void snd_hdac_bus_link_power(struct hdac_device *codec, bool enable) +{ + if (enable) + set_bit(codec->addr, &codec->bus->codec_powered); + else + clear_bit(codec->addr, &codec->bus->codec_powered); +} +EXPORT_SYMBOL_GPL(snd_hdac_bus_link_power); diff --git a/sound/hda/core/device.c b/sound/hda/core/device.c new file mode 100644 index 0000000000000..018f9e176b1b8 --- /dev/null +++ b/sound/hda/core/device.c @@ -0,0 +1,1170 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * HD-audio codec core device + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "local.h" + +static void setup_fg_nodes(struct hdac_device *codec); +static int get_codec_vendor_name(struct hdac_device *codec); + +static void default_release(struct device *dev) +{ + snd_hdac_device_exit(dev_to_hdac_dev(dev)); +} + +/** + * snd_hdac_device_init - initialize the HD-audio codec base device + * @codec: device to initialize + * @bus: but to attach + * @name: device name string + * @addr: codec address + * + * Returns zero for success or a negative error code. + * + * This function increments the runtime PM counter and marks it active. + * The caller needs to turn it off appropriately later. + * + * The caller needs to set the device's release op properly by itself. + */ +int snd_hdac_device_init(struct hdac_device *codec, struct hdac_bus *bus, + const char *name, unsigned int addr) +{ + struct device *dev; + hda_nid_t fg; + int err; + + dev = &codec->dev; + device_initialize(dev); + dev->parent = bus->dev; + dev->bus = &snd_hda_bus_type; + dev->release = default_release; + dev->groups = hdac_dev_attr_groups; + dev_set_name(dev, "%s", name); + device_enable_async_suspend(dev); + + codec->bus = bus; + codec->addr = addr; + codec->type = HDA_DEV_CORE; + mutex_init(&codec->widget_lock); + mutex_init(&codec->regmap_lock); + pm_runtime_set_active(&codec->dev); + pm_runtime_get_noresume(&codec->dev); + atomic_set(&codec->in_pm, 0); + + err = snd_hdac_bus_add_device(bus, codec); + if (err < 0) + goto error; + + /* fill parameters */ + codec->vendor_id = snd_hdac_read_parm(codec, AC_NODE_ROOT, + AC_PAR_VENDOR_ID); + if (codec->vendor_id == -1) { + /* read again, hopefully the access method was corrected + * in the last read... + */ + codec->vendor_id = snd_hdac_read_parm(codec, AC_NODE_ROOT, + AC_PAR_VENDOR_ID); + } + + codec->subsystem_id = snd_hdac_read_parm(codec, AC_NODE_ROOT, + AC_PAR_SUBSYSTEM_ID); + codec->revision_id = snd_hdac_read_parm(codec, AC_NODE_ROOT, + AC_PAR_REV_ID); + + setup_fg_nodes(codec); + if (!codec->afg && !codec->mfg) { + dev_err(dev, "no AFG or MFG node found\n"); + err = -ENODEV; + goto error; + } + + fg = codec->afg ? codec->afg : codec->mfg; + + err = snd_hdac_refresh_widgets(codec); + if (err < 0) + goto error; + + codec->power_caps = snd_hdac_read_parm(codec, fg, AC_PAR_POWER_STATE); + /* reread ssid if not set by parameter */ + if (codec->subsystem_id == -1 || codec->subsystem_id == 0) + snd_hdac_read(codec, fg, AC_VERB_GET_SUBSYSTEM_ID, 0, + &codec->subsystem_id); + + err = get_codec_vendor_name(codec); + if (err < 0) + goto error; + + codec->chip_name = kasprintf(GFP_KERNEL, "ID %x", + codec->vendor_id & 0xffff); + if (!codec->chip_name) { + err = -ENOMEM; + goto error; + } + + return 0; + + error: + put_device(&codec->dev); + return err; +} +EXPORT_SYMBOL_GPL(snd_hdac_device_init); + +/** + * snd_hdac_device_exit - clean up the HD-audio codec base device + * @codec: device to clean up + */ +void snd_hdac_device_exit(struct hdac_device *codec) +{ + pm_runtime_put_noidle(&codec->dev); + /* keep balance of runtime PM child_count in parent device */ + pm_runtime_set_suspended(&codec->dev); + snd_hdac_bus_remove_device(codec->bus, codec); + kfree(codec->vendor_name); + kfree(codec->chip_name); +} +EXPORT_SYMBOL_GPL(snd_hdac_device_exit); + +/** + * snd_hdac_device_register - register the hd-audio codec base device + * @codec: the device to register + */ +int snd_hdac_device_register(struct hdac_device *codec) +{ + int err; + + err = device_add(&codec->dev); + if (err < 0) + return err; + mutex_lock(&codec->widget_lock); + err = hda_widget_sysfs_init(codec); + mutex_unlock(&codec->widget_lock); + if (err < 0) { + device_del(&codec->dev); + return err; + } + + return 0; +} +EXPORT_SYMBOL_GPL(snd_hdac_device_register); + +/** + * snd_hdac_device_unregister - unregister the hd-audio codec base device + * @codec: the device to unregister + */ +void snd_hdac_device_unregister(struct hdac_device *codec) +{ + if (device_is_registered(&codec->dev)) { + mutex_lock(&codec->widget_lock); + hda_widget_sysfs_exit(codec); + mutex_unlock(&codec->widget_lock); + device_del(&codec->dev); + snd_hdac_bus_remove_device(codec->bus, codec); + } +} +EXPORT_SYMBOL_GPL(snd_hdac_device_unregister); + +/** + * snd_hdac_device_set_chip_name - set/update the codec name + * @codec: the HDAC device + * @name: name string to set + * + * Returns 0 if the name is set or updated, or a negative error code. + */ +int snd_hdac_device_set_chip_name(struct hdac_device *codec, const char *name) +{ + char *newname; + + if (!name) + return 0; + newname = kstrdup(name, GFP_KERNEL); + if (!newname) + return -ENOMEM; + kfree(codec->chip_name); + codec->chip_name = newname; + return 0; +} +EXPORT_SYMBOL_GPL(snd_hdac_device_set_chip_name); + +/** + * snd_hdac_codec_modalias - give the module alias name + * @codec: HDAC device + * @buf: string buffer to store + * @size: string buffer size + * + * Returns the size of string, like snprintf(), or a negative error code. + */ +int snd_hdac_codec_modalias(const struct hdac_device *codec, char *buf, size_t size) +{ + return scnprintf(buf, size, "hdaudio:v%08Xr%08Xa%02X\n", + codec->vendor_id, codec->revision_id, codec->type); +} +EXPORT_SYMBOL_GPL(snd_hdac_codec_modalias); + +/** + * snd_hdac_make_cmd - compose a 32bit command word to be sent to the + * HD-audio controller + * @codec: the codec object + * @nid: NID to encode + * @verb: verb to encode + * @parm: parameter to encode + * + * Return an encoded command verb or -1 for error. + */ +static unsigned int snd_hdac_make_cmd(struct hdac_device *codec, hda_nid_t nid, + unsigned int verb, unsigned int parm) +{ + u32 val, addr; + + addr = codec->addr; + if ((addr & ~0xf) || (nid & ~0x7f) || + (verb & ~0xfff) || (parm & ~0xffff)) { + dev_err(&codec->dev, "out of range cmd %x:%x:%x:%x\n", + addr, nid, verb, parm); + return -1; + } + + val = addr << 28; + val |= (u32)nid << 20; + val |= verb << 8; + val |= parm; + return val; +} + +/** + * snd_hdac_exec_verb - execute an encoded verb + * @codec: the codec object + * @cmd: encoded verb to execute + * @flags: optional flags, pass zero for default + * @res: the pointer to store the result, NULL if running async + * + * Returns zero if successful, or a negative error code. + * + * This calls the exec_verb op when set in hdac_codec. If not, + * call the default snd_hdac_bus_exec_verb(). + */ +int snd_hdac_exec_verb(struct hdac_device *codec, unsigned int cmd, + unsigned int flags, unsigned int *res) +{ + if (codec->exec_verb) + return codec->exec_verb(codec, cmd, flags, res); + return snd_hdac_bus_exec_verb(codec->bus, codec->addr, cmd, res); +} + + +/** + * snd_hdac_read - execute a verb + * @codec: the codec object + * @nid: NID to execute a verb + * @verb: verb to execute + * @parm: parameter for a verb + * @res: the pointer to store the result, NULL if running async + * + * Returns zero if successful, or a negative error code. + */ +int snd_hdac_read(struct hdac_device *codec, hda_nid_t nid, + unsigned int verb, unsigned int parm, unsigned int *res) +{ + unsigned int cmd = snd_hdac_make_cmd(codec, nid, verb, parm); + + return snd_hdac_exec_verb(codec, cmd, 0, res); +} +EXPORT_SYMBOL_GPL(snd_hdac_read); + +/** + * _snd_hdac_read_parm - read a parmeter + * @codec: the codec object + * @nid: NID to read a parameter + * @parm: parameter to read + * @res: pointer to store the read value + * + * This function returns zero or an error unlike snd_hdac_read_parm(). + */ +int _snd_hdac_read_parm(struct hdac_device *codec, hda_nid_t nid, int parm, + unsigned int *res) +{ + unsigned int cmd; + + cmd = snd_hdac_regmap_encode_verb(nid, AC_VERB_PARAMETERS) | parm; + return snd_hdac_regmap_read_raw(codec, cmd, res); +} +EXPORT_SYMBOL_GPL(_snd_hdac_read_parm); + +/** + * snd_hdac_read_parm_uncached - read a codec parameter without caching + * @codec: the codec object + * @nid: NID to read a parameter + * @parm: parameter to read + * + * Returns -1 for error. If you need to distinguish the error more + * strictly, use snd_hdac_read() directly. + */ +int snd_hdac_read_parm_uncached(struct hdac_device *codec, hda_nid_t nid, + int parm) +{ + unsigned int cmd, val; + + cmd = snd_hdac_regmap_encode_verb(nid, AC_VERB_PARAMETERS) | parm; + if (snd_hdac_regmap_read_raw_uncached(codec, cmd, &val) < 0) + return -1; + return val; +} +EXPORT_SYMBOL_GPL(snd_hdac_read_parm_uncached); + +/** + * snd_hdac_override_parm - override read-only parameters + * @codec: the codec object + * @nid: NID for the parameter + * @parm: the parameter to change + * @val: the parameter value to overwrite + */ +int snd_hdac_override_parm(struct hdac_device *codec, hda_nid_t nid, + unsigned int parm, unsigned int val) +{ + unsigned int verb = (AC_VERB_PARAMETERS << 8) | (nid << 20) | parm; + int err; + + if (!codec->regmap) + return -EINVAL; + + codec->caps_overwriting = true; + err = snd_hdac_regmap_write_raw(codec, verb, val); + codec->caps_overwriting = false; + return err; +} +EXPORT_SYMBOL_GPL(snd_hdac_override_parm); + +/** + * snd_hdac_get_sub_nodes - get start NID and number of subtree nodes + * @codec: the codec object + * @nid: NID to inspect + * @start_id: the pointer to store the starting NID + * + * Returns the number of subtree nodes or zero if not found. + * This function reads parameters always without caching. + */ +int snd_hdac_get_sub_nodes(struct hdac_device *codec, hda_nid_t nid, + hda_nid_t *start_id) +{ + unsigned int parm; + + parm = snd_hdac_read_parm_uncached(codec, nid, AC_PAR_NODE_COUNT); + if (parm == -1) { + *start_id = 0; + return 0; + } + *start_id = (parm >> 16) & 0x7fff; + return (int)(parm & 0x7fff); +} +EXPORT_SYMBOL_GPL(snd_hdac_get_sub_nodes); + +/* + * look for an AFG and MFG nodes + */ +static void setup_fg_nodes(struct hdac_device *codec) +{ + int i, total_nodes, function_id; + hda_nid_t nid; + + total_nodes = snd_hdac_get_sub_nodes(codec, AC_NODE_ROOT, &nid); + for (i = 0; i < total_nodes; i++, nid++) { + function_id = snd_hdac_read_parm(codec, nid, + AC_PAR_FUNCTION_TYPE); + switch (function_id & 0xff) { + case AC_GRP_AUDIO_FUNCTION: + codec->afg = nid; + codec->afg_function_id = function_id & 0xff; + codec->afg_unsol = (function_id >> 8) & 1; + break; + case AC_GRP_MODEM_FUNCTION: + codec->mfg = nid; + codec->mfg_function_id = function_id & 0xff; + codec->mfg_unsol = (function_id >> 8) & 1; + break; + default: + break; + } + } +} + +/** + * snd_hdac_refresh_widgets - Reset the widget start/end nodes + * @codec: the codec object + */ +int snd_hdac_refresh_widgets(struct hdac_device *codec) +{ + hda_nid_t start_nid; + int nums, err = 0; + + /* + * Serialize against multiple threads trying to update the sysfs + * widgets array. + */ + mutex_lock(&codec->widget_lock); + nums = snd_hdac_get_sub_nodes(codec, codec->afg, &start_nid); + if (!start_nid || nums <= 0 || nums >= 0xff) { + dev_err(&codec->dev, "cannot read sub nodes for FG 0x%02x\n", + codec->afg); + err = -EINVAL; + goto unlock; + } + + err = hda_widget_sysfs_reinit(codec, start_nid, nums); + if (err < 0) + goto unlock; + + codec->num_nodes = nums; + codec->start_nid = start_nid; + codec->end_nid = start_nid + nums; +unlock: + mutex_unlock(&codec->widget_lock); + return err; +} +EXPORT_SYMBOL_GPL(snd_hdac_refresh_widgets); + +/* return CONNLIST_LEN parameter of the given widget */ +static unsigned int get_num_conns(struct hdac_device *codec, hda_nid_t nid) +{ + unsigned int wcaps = snd_hdac_get_wcaps(codec, nid); + unsigned int parm; + + if (!(wcaps & AC_WCAP_CONN_LIST) && + snd_hdac_get_wcaps_type(wcaps) != AC_WID_VOL_KNB) + return 0; + + parm = snd_hdac_read_parm(codec, nid, AC_PAR_CONNLIST_LEN); + if (parm == -1) + parm = 0; + return parm; +} + +/** + * snd_hdac_get_connections - get a widget connection list + * @codec: the codec object + * @nid: NID + * @conn_list: the array to store the results, can be NULL + * @max_conns: the max size of the given array + * + * Returns the number of connected widgets, zero for no connection, or a + * negative error code. When the number of elements don't fit with the + * given array size, it returns -ENOSPC. + * + * When @conn_list is NULL, it just checks the number of connections. + */ +int snd_hdac_get_connections(struct hdac_device *codec, hda_nid_t nid, + hda_nid_t *conn_list, int max_conns) +{ + unsigned int parm; + int i, conn_len, conns, err; + unsigned int shift, num_elems, mask; + hda_nid_t prev_nid; + int null_count = 0; + + parm = get_num_conns(codec, nid); + if (!parm) + return 0; + + if (parm & AC_CLIST_LONG) { + /* long form */ + shift = 16; + num_elems = 2; + } else { + /* short form */ + shift = 8; + num_elems = 4; + } + conn_len = parm & AC_CLIST_LENGTH; + mask = (1 << (shift-1)) - 1; + + if (!conn_len) + return 0; /* no connection */ + + if (conn_len == 1) { + /* single connection */ + err = snd_hdac_read(codec, nid, AC_VERB_GET_CONNECT_LIST, 0, + &parm); + if (err < 0) + return err; + if (conn_list) + conn_list[0] = parm & mask; + return 1; + } + + /* multi connection */ + conns = 0; + prev_nid = 0; + for (i = 0; i < conn_len; i++) { + int range_val; + hda_nid_t val, n; + + if (i % num_elems == 0) { + err = snd_hdac_read(codec, nid, + AC_VERB_GET_CONNECT_LIST, i, + &parm); + if (err < 0) + return -EIO; + } + range_val = !!(parm & (1 << (shift-1))); /* ranges */ + val = parm & mask; + if (val == 0 && null_count++) { /* no second chance */ + dev_dbg(&codec->dev, + "invalid CONNECT_LIST verb %x[%i]:%x\n", + nid, i, parm); + return 0; + } + parm >>= shift; + if (range_val) { + /* ranges between the previous and this one */ + if (!prev_nid || prev_nid >= val) { + dev_warn(&codec->dev, + "invalid dep_range_val %x:%x\n", + prev_nid, val); + continue; + } + for (n = prev_nid + 1; n <= val; n++) { + if (conn_list) { + if (conns >= max_conns) + return -ENOSPC; + conn_list[conns] = n; + } + conns++; + } + } else { + if (conn_list) { + if (conns >= max_conns) + return -ENOSPC; + conn_list[conns] = val; + } + conns++; + } + prev_nid = val; + } + return conns; +} +EXPORT_SYMBOL_GPL(snd_hdac_get_connections); + +#ifdef CONFIG_PM +/** + * snd_hdac_power_up - power up the codec + * @codec: the codec object + * + * This function calls the runtime PM helper to power up the given codec. + * Unlike snd_hdac_power_up_pm(), you should call this only for the code + * path that isn't included in PM path. Otherwise it gets stuck. + * + * Returns zero if successful, or a negative error code. + */ +int snd_hdac_power_up(struct hdac_device *codec) +{ + return pm_runtime_get_sync(&codec->dev); +} +EXPORT_SYMBOL_GPL(snd_hdac_power_up); + +/** + * snd_hdac_power_down - power down the codec + * @codec: the codec object + * + * Returns zero if successful, or a negative error code. + */ +int snd_hdac_power_down(struct hdac_device *codec) +{ + struct device *dev = &codec->dev; + + return pm_runtime_put_autosuspend(dev); +} +EXPORT_SYMBOL_GPL(snd_hdac_power_down); + +/** + * snd_hdac_power_up_pm - power up the codec + * @codec: the codec object + * + * This function can be called in a recursive code path like init code + * which may be called by PM suspend/resume again. OTOH, if a power-up + * call must wake up the sleeper (e.g. in a kctl callback), use + * snd_hdac_power_up() instead. + * + * Returns zero if successful, or a negative error code. + */ +int snd_hdac_power_up_pm(struct hdac_device *codec) +{ + if (!atomic_inc_not_zero(&codec->in_pm)) + return snd_hdac_power_up(codec); + return 0; +} +EXPORT_SYMBOL_GPL(snd_hdac_power_up_pm); + +/* like snd_hdac_power_up_pm(), but only increment the pm count when + * already powered up. Returns -1 if not powered up, 1 if incremented + * or 0 if unchanged. Only used in hdac_regmap.c + */ +int snd_hdac_keep_power_up(struct hdac_device *codec) +{ + if (!atomic_inc_not_zero(&codec->in_pm)) { + int ret = pm_runtime_get_if_active(&codec->dev); + if (!ret) + return -1; + if (ret < 0) + return 0; + } + return 1; +} + +/** + * snd_hdac_power_down_pm - power down the codec + * @codec: the codec object + * + * Like snd_hdac_power_up_pm(), this function is used in a recursive + * code path like init code which may be called by PM suspend/resume again. + * + * Returns zero if successful, or a negative error code. + */ +int snd_hdac_power_down_pm(struct hdac_device *codec) +{ + if (atomic_dec_if_positive(&codec->in_pm) < 0) + return snd_hdac_power_down(codec); + return 0; +} +EXPORT_SYMBOL_GPL(snd_hdac_power_down_pm); +#endif + +/* codec vendor labels */ +struct hda_vendor_id { + unsigned int id; + const char *name; +}; + +static const struct hda_vendor_id hda_vendor_ids[] = { + { 0x0014, "Loongson" }, + { 0x1002, "ATI" }, + { 0x1013, "Cirrus Logic" }, + { 0x1057, "Motorola" }, + { 0x1095, "Silicon Image" }, + { 0x10de, "Nvidia" }, + { 0x10ec, "Realtek" }, + { 0x1102, "Creative" }, + { 0x1106, "VIA" }, + { 0x111d, "IDT" }, + { 0x11c1, "LSI" }, + { 0x11d4, "Analog Devices" }, + { 0x13f6, "C-Media" }, + { 0x14f1, "Conexant" }, + { 0x17e8, "Chrontel" }, + { 0x1854, "LG" }, + { 0x19e5, "Huawei" }, + { 0x1aec, "Wolfson Microelectronics" }, + { 0x1af4, "QEMU" }, + { 0x1fa8, "Senarytech" }, + { 0x434d, "C-Media" }, + { 0x8086, "Intel" }, + { 0x8384, "SigmaTel" }, + {} /* terminator */ +}; + +/* store the codec vendor name */ +static int get_codec_vendor_name(struct hdac_device *codec) +{ + const struct hda_vendor_id *c; + u16 vendor_id = codec->vendor_id >> 16; + + for (c = hda_vendor_ids; c->id; c++) { + if (c->id == vendor_id) { + codec->vendor_name = kstrdup(c->name, GFP_KERNEL); + return codec->vendor_name ? 0 : -ENOMEM; + } + } + + codec->vendor_name = kasprintf(GFP_KERNEL, "Generic %04x", vendor_id); + return codec->vendor_name ? 0 : -ENOMEM; +} + +/* + * stream formats + */ +struct hda_rate_tbl { + unsigned int hz; + unsigned int alsa_bits; + unsigned int hda_fmt; +}; + +/* rate = base * mult / div */ +#define HDA_RATE(base, mult, div) \ + (AC_FMT_BASE_##base##K | (((mult) - 1) << AC_FMT_MULT_SHIFT) | \ + (((div) - 1) << AC_FMT_DIV_SHIFT)) + +static const struct hda_rate_tbl rate_bits[] = { + /* rate in Hz, ALSA rate bitmask, HDA format value */ + + /* autodetected value used in snd_hda_query_supported_pcm */ + { 8000, SNDRV_PCM_RATE_8000, HDA_RATE(48, 1, 6) }, + { 11025, SNDRV_PCM_RATE_11025, HDA_RATE(44, 1, 4) }, + { 16000, SNDRV_PCM_RATE_16000, HDA_RATE(48, 1, 3) }, + { 22050, SNDRV_PCM_RATE_22050, HDA_RATE(44, 1, 2) }, + { 32000, SNDRV_PCM_RATE_32000, HDA_RATE(48, 2, 3) }, + { 44100, SNDRV_PCM_RATE_44100, HDA_RATE(44, 1, 1) }, + { 48000, SNDRV_PCM_RATE_48000, HDA_RATE(48, 1, 1) }, + { 88200, SNDRV_PCM_RATE_88200, HDA_RATE(44, 2, 1) }, + { 96000, SNDRV_PCM_RATE_96000, HDA_RATE(48, 2, 1) }, + { 176400, SNDRV_PCM_RATE_176400, HDA_RATE(44, 4, 1) }, + { 192000, SNDRV_PCM_RATE_192000, HDA_RATE(48, 4, 1) }, +#define AC_PAR_PCM_RATE_BITS 11 + /* up to bits 10, 384kHZ isn't supported properly */ + + /* not autodetected value */ + { 9600, SNDRV_PCM_RATE_KNOT, HDA_RATE(48, 1, 5) }, + + { 0 } /* terminator */ +}; + +static snd_pcm_format_t snd_hdac_format_normalize(snd_pcm_format_t format) +{ + switch (format) { + case SNDRV_PCM_FORMAT_S20_LE: + case SNDRV_PCM_FORMAT_S24_LE: + return SNDRV_PCM_FORMAT_S32_LE; + + case SNDRV_PCM_FORMAT_U20_LE: + case SNDRV_PCM_FORMAT_U24_LE: + return SNDRV_PCM_FORMAT_U32_LE; + + case SNDRV_PCM_FORMAT_S20_BE: + case SNDRV_PCM_FORMAT_S24_BE: + return SNDRV_PCM_FORMAT_S32_BE; + + case SNDRV_PCM_FORMAT_U20_BE: + case SNDRV_PCM_FORMAT_U24_BE: + return SNDRV_PCM_FORMAT_U32_BE; + + default: + return format; + } +} + +/** + * snd_hdac_stream_format_bits - obtain bits per sample value. + * @format: the PCM format. + * @subformat: the PCM subformat. + * @maxbits: the maximum bits per sample. + * + * Return: The number of bits per sample. + */ +unsigned int snd_hdac_stream_format_bits(snd_pcm_format_t format, snd_pcm_subformat_t subformat, + unsigned int maxbits) +{ + struct snd_pcm_hw_params params; + unsigned int bits; + + memset(¶ms, 0, sizeof(params)); + + params_set_format(¶ms, snd_hdac_format_normalize(format)); + snd_mask_set(hw_param_mask(¶ms, SNDRV_PCM_HW_PARAM_SUBFORMAT), + (__force unsigned int)subformat); + + bits = snd_pcm_hw_params_bits(¶ms); + if (maxbits) + return min(bits, maxbits); + return bits; +} +EXPORT_SYMBOL_GPL(snd_hdac_stream_format_bits); + +/** + * snd_hdac_stream_format - convert format parameters to SDxFMT value. + * @channels: the number of channels. + * @bits: bits per sample. + * @rate: the sample rate. + * + * Return: The format bitset or zero if invalid. + */ +unsigned int snd_hdac_stream_format(unsigned int channels, unsigned int bits, unsigned int rate) +{ + unsigned int val = 0; + int i; + + for (i = 0; rate_bits[i].hz; i++) { + if (rate_bits[i].hz == rate) { + val = rate_bits[i].hda_fmt; + break; + } + } + + if (!rate_bits[i].hz) + return 0; + + if (channels == 0 || channels > 16) + return 0; + val |= channels - 1; + + switch (bits) { + case 8: + val |= AC_FMT_BITS_8; + break; + case 16: + val |= AC_FMT_BITS_16; + break; + case 20: + val |= AC_FMT_BITS_20; + break; + case 24: + val |= AC_FMT_BITS_24; + break; + case 32: + val |= AC_FMT_BITS_32; + break; + default: + return 0; + } + + return val; +} +EXPORT_SYMBOL_GPL(snd_hdac_stream_format); + +/** + * snd_hdac_spdif_stream_format - convert format parameters to SDxFMT value. + * @channels: the number of channels. + * @bits: bits per sample. + * @rate: the sample rate. + * @spdif_ctls: HD-audio SPDIF status bits (0 if irrelevant). + * + * Return: The format bitset or zero if invalid. + */ +unsigned int snd_hdac_spdif_stream_format(unsigned int channels, unsigned int bits, + unsigned int rate, unsigned short spdif_ctls) +{ + unsigned int val = snd_hdac_stream_format(channels, bits, rate); + + if (val && spdif_ctls & AC_DIG1_NONAUDIO) + val |= AC_FMT_TYPE_NON_PCM; + + return val; +} +EXPORT_SYMBOL_GPL(snd_hdac_spdif_stream_format); + +static unsigned int query_pcm_param(struct hdac_device *codec, hda_nid_t nid) +{ + unsigned int val = 0; + + if (nid != codec->afg && + (snd_hdac_get_wcaps(codec, nid) & AC_WCAP_FORMAT_OVRD)) + val = snd_hdac_read_parm(codec, nid, AC_PAR_PCM); + if (!val || val == -1) + val = snd_hdac_read_parm(codec, codec->afg, AC_PAR_PCM); + if (!val || val == -1) + return 0; + return val; +} + +static unsigned int query_stream_param(struct hdac_device *codec, hda_nid_t nid) +{ + unsigned int streams = snd_hdac_read_parm(codec, nid, AC_PAR_STREAM); + + if (!streams || streams == -1) + streams = snd_hdac_read_parm(codec, codec->afg, AC_PAR_STREAM); + if (!streams || streams == -1) + return 0; + return streams; +} + +/** + * snd_hdac_query_supported_pcm - query the supported PCM rates and formats + * @codec: the codec object + * @nid: NID to query + * @ratesp: the pointer to store the detected rate bitflags + * @formatsp: the pointer to store the detected formats + * @subformatsp: the pointer to store the detected subformats for S32_LE format + * @bpsp: the pointer to store the detected format widths + * + * Queries the supported PCM rates and formats. The NULL @ratesp, @formatsp, + * @subformatsp or @bpsp argument is ignored. + * + * Returns 0 if successful, otherwise a negative error code. + */ +int snd_hdac_query_supported_pcm(struct hdac_device *codec, hda_nid_t nid, + u32 *ratesp, u64 *formatsp, u32 *subformatsp, + unsigned int *bpsp) +{ + unsigned int i, val, wcaps; + + wcaps = snd_hdac_get_wcaps(codec, nid); + val = query_pcm_param(codec, nid); + + if (ratesp) { + u32 rates = 0; + for (i = 0; i < AC_PAR_PCM_RATE_BITS; i++) { + if (val & (1 << i)) + rates |= rate_bits[i].alsa_bits; + } + if (rates == 0) { + dev_err(&codec->dev, + "rates == 0 (nid=0x%x, val=0x%x, ovrd=%i)\n", + nid, val, + (wcaps & AC_WCAP_FORMAT_OVRD) ? 1 : 0); + return -EIO; + } + *ratesp = rates; + } + + if (formatsp || subformatsp || bpsp) { + unsigned int streams, bps; + u32 subformats = 0; + u64 formats = 0; + + streams = query_stream_param(codec, nid); + if (!streams) + return -EIO; + + bps = 0; + if (streams & AC_SUPFMT_PCM) { + if (val & AC_SUPPCM_BITS_8) { + formats |= SNDRV_PCM_FMTBIT_U8; + bps = 8; + } + if (val & AC_SUPPCM_BITS_16) { + formats |= SNDRV_PCM_FMTBIT_S16_LE; + bps = 16; + } + if (val & AC_SUPPCM_BITS_20) { + formats |= SNDRV_PCM_FMTBIT_S32_LE; + subformats |= SNDRV_PCM_SUBFMTBIT_MSBITS_20; + bps = 20; + } + if (val & AC_SUPPCM_BITS_24) { + formats |= SNDRV_PCM_FMTBIT_S32_LE; + subformats |= SNDRV_PCM_SUBFMTBIT_MSBITS_24; + bps = 24; + } + if (val & AC_SUPPCM_BITS_32) { + if (wcaps & AC_WCAP_DIGITAL) { + formats |= SNDRV_PCM_FMTBIT_IEC958_SUBFRAME_LE; + } else { + formats |= SNDRV_PCM_FMTBIT_S32_LE; + subformats |= SNDRV_PCM_SUBFMTBIT_MSBITS_MAX; + bps = 32; + } + } + } +#if 0 /* FIXME: CS4206 doesn't work, which is the only codec supporting float */ + if (streams & AC_SUPFMT_FLOAT32) { + formats |= SNDRV_PCM_FMTBIT_FLOAT_LE; + if (!bps) + bps = 32; + } +#endif + if (streams == AC_SUPFMT_AC3) { + /* should be exclusive */ + /* temporary hack: we have still no proper support + * for the direct AC3 stream... + */ + formats |= SNDRV_PCM_FMTBIT_U8; + bps = 8; + } + if (formats == 0) { + dev_err(&codec->dev, + "formats == 0 (nid=0x%x, val=0x%x, ovrd=%i, streams=0x%x)\n", + nid, val, + (wcaps & AC_WCAP_FORMAT_OVRD) ? 1 : 0, + streams); + return -EIO; + } + if (formatsp) + *formatsp = formats; + if (subformatsp) + *subformatsp = subformats; + if (bpsp) + *bpsp = bps; + } + + return 0; +} +EXPORT_SYMBOL_GPL(snd_hdac_query_supported_pcm); + +/** + * snd_hdac_is_supported_format - Check the validity of the format + * @codec: the codec object + * @nid: NID to check + * @format: the HD-audio format value to check + * + * Check whether the given node supports the format value. + * + * Returns true if supported, false if not. + */ +bool snd_hdac_is_supported_format(struct hdac_device *codec, hda_nid_t nid, + unsigned int format) +{ + int i; + unsigned int val = 0, rate, stream; + + val = query_pcm_param(codec, nid); + if (!val) + return false; + + rate = format & 0xff00; + for (i = 0; i < AC_PAR_PCM_RATE_BITS; i++) + if (rate_bits[i].hda_fmt == rate) { + if (val & (1 << i)) + break; + return false; + } + if (i >= AC_PAR_PCM_RATE_BITS) + return false; + + stream = query_stream_param(codec, nid); + if (!stream) + return false; + + if (stream & AC_SUPFMT_PCM) { + switch (format & 0xf0) { + case 0x00: + if (!(val & AC_SUPPCM_BITS_8)) + return false; + break; + case 0x10: + if (!(val & AC_SUPPCM_BITS_16)) + return false; + break; + case 0x20: + if (!(val & AC_SUPPCM_BITS_20)) + return false; + break; + case 0x30: + if (!(val & AC_SUPPCM_BITS_24)) + return false; + break; + case 0x40: + if (!(val & AC_SUPPCM_BITS_32)) + return false; + break; + default: + return false; + } + } else { + /* FIXME: check for float32 and AC3? */ + } + + return true; +} +EXPORT_SYMBOL_GPL(snd_hdac_is_supported_format); + +static unsigned int codec_read(struct hdac_device *hdac, hda_nid_t nid, + int flags, unsigned int verb, unsigned int parm) +{ + unsigned int cmd = snd_hdac_make_cmd(hdac, nid, verb, parm); + unsigned int res; + + if (snd_hdac_exec_verb(hdac, cmd, flags, &res)) + return -1; + + return res; +} + +static int codec_write(struct hdac_device *hdac, hda_nid_t nid, + int flags, unsigned int verb, unsigned int parm) +{ + unsigned int cmd = snd_hdac_make_cmd(hdac, nid, verb, parm); + + return snd_hdac_exec_verb(hdac, cmd, flags, NULL); +} + +/** + * snd_hdac_codec_read - send a command and get the response + * @hdac: the HDAC device + * @nid: NID to send the command + * @flags: optional bit flags + * @verb: the verb to send + * @parm: the parameter for the verb + * + * Send a single command and read the corresponding response. + * + * Returns the obtained response value, or -1 for an error. + */ +int snd_hdac_codec_read(struct hdac_device *hdac, hda_nid_t nid, + int flags, unsigned int verb, unsigned int parm) +{ + return codec_read(hdac, nid, flags, verb, parm); +} +EXPORT_SYMBOL_GPL(snd_hdac_codec_read); + +/** + * snd_hdac_codec_write - send a single command without waiting for response + * @hdac: the HDAC device + * @nid: NID to send the command + * @flags: optional bit flags + * @verb: the verb to send + * @parm: the parameter for the verb + * + * Send a single command without waiting for response. + * + * Returns 0 if successful, or a negative error code. + */ +int snd_hdac_codec_write(struct hdac_device *hdac, hda_nid_t nid, + int flags, unsigned int verb, unsigned int parm) +{ + return codec_write(hdac, nid, flags, verb, parm); +} +EXPORT_SYMBOL_GPL(snd_hdac_codec_write); + +/** + * snd_hdac_check_power_state - check whether the actual power state matches + * with the target state + * + * @hdac: the HDAC device + * @nid: NID to send the command + * @target_state: target state to check for + * + * Return true if state matches, false if not + */ +bool snd_hdac_check_power_state(struct hdac_device *hdac, + hda_nid_t nid, unsigned int target_state) +{ + unsigned int state = codec_read(hdac, nid, 0, + AC_VERB_GET_POWER_STATE, 0); + + if (state & AC_PWRST_ERROR) + return true; + state = (state >> 4) & 0x0f; + return (state == target_state); +} +EXPORT_SYMBOL_GPL(snd_hdac_check_power_state); +/** + * snd_hdac_sync_power_state - wait until actual power state matches + * with the target state + * + * @codec: the HDAC device + * @nid: NID to send the command + * @power_state: target power state to wait for + * + * Return power state or PS_ERROR if codec rejects GET verb. + */ +unsigned int snd_hdac_sync_power_state(struct hdac_device *codec, + hda_nid_t nid, unsigned int power_state) +{ + unsigned long end_time = jiffies + msecs_to_jiffies(500); + unsigned int state, actual_state, count; + + for (count = 0; count < 500; count++) { + state = snd_hdac_codec_read(codec, nid, 0, + AC_VERB_GET_POWER_STATE, 0); + if (state & AC_PWRST_ERROR) { + msleep(20); + break; + } + actual_state = (state >> 4) & 0x0f; + if (actual_state == power_state) + break; + if (time_after_eq(jiffies, end_time)) + break; + /* wait until the codec reachs to the target state */ + msleep(1); + } + return state; +} +EXPORT_SYMBOL_GPL(snd_hdac_sync_power_state); diff --git a/sound/hda/core/ext/Makefile b/sound/hda/core/ext/Makefile new file mode 100644 index 0000000000000..85190a7eb5de7 --- /dev/null +++ b/sound/hda/core/ext/Makefile @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: GPL-2.0-only +snd-hda-ext-core-y := bus.o controller.o stream.o + +obj-$(CONFIG_SND_HDA_EXT_CORE) += snd-hda-ext-core.o diff --git a/sound/hda/core/ext/bus.c b/sound/hda/core/ext/bus.c new file mode 100644 index 0000000000000..6004ea1c373e1 --- /dev/null +++ b/sound/hda/core/ext/bus.c @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * hdac-ext-bus.c - HD-audio extended core bus functions. + * + * Copyright (C) 2014-2015 Intel Corp + * Author: Jeeja KP + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + */ + +#include +#include +#include +#include + +MODULE_DESCRIPTION("HDA extended core"); +MODULE_LICENSE("GPL v2"); + +/** + * snd_hdac_ext_bus_init - initialize a HD-audio extended bus + * @bus: the pointer to HDAC bus object + * @dev: device pointer + * @ops: bus verb operators + * @ext_ops: operators used for ASoC HDA codec drivers + * + * Returns 0 if successful, or a negative error code. + */ +int snd_hdac_ext_bus_init(struct hdac_bus *bus, struct device *dev, + const struct hdac_bus_ops *ops, + const struct hdac_ext_bus_ops *ext_ops) +{ + int ret; + + ret = snd_hdac_bus_init(bus, dev, ops); + if (ret < 0) + return ret; + + bus->ext_ops = ext_ops; + /* FIXME: + * Currently only one bus is supported, if there is device with more + * buses, bus->idx should be greater than 0, but there needs to be a + * reliable way to always assign same number. + */ + bus->idx = 0; + bus->cmd_dma_state = true; + + return 0; +} +EXPORT_SYMBOL_GPL(snd_hdac_ext_bus_init); + +/** + * snd_hdac_ext_bus_exit - clean up a HD-audio extended bus + * @bus: the pointer to HDAC bus object + */ +void snd_hdac_ext_bus_exit(struct hdac_bus *bus) +{ + snd_hdac_bus_exit(bus); + WARN_ON(!list_empty(&bus->hlink_list)); +} +EXPORT_SYMBOL_GPL(snd_hdac_ext_bus_exit); + +/** + * snd_hdac_ext_bus_device_remove - remove HD-audio extended codec base devices + * + * @bus: the pointer to HDAC bus object + */ +void snd_hdac_ext_bus_device_remove(struct hdac_bus *bus) +{ + struct hdac_device *codec, *__codec; + /* + * we need to remove all the codec devices objects created in the + * snd_hdac_ext_bus_device_init + */ + list_for_each_entry_safe(codec, __codec, &bus->codec_list, list) { + snd_hdac_device_unregister(codec); + put_device(&codec->dev); + } +} +EXPORT_SYMBOL_GPL(snd_hdac_ext_bus_device_remove); +#define dev_to_hdac(dev) (container_of((dev), \ + struct hdac_device, dev)) + +static inline struct hdac_driver *get_hdrv(struct device *dev) +{ + struct hdac_driver *hdrv = drv_to_hdac_driver(dev->driver); + return hdrv; +} + +static inline struct hdac_device *get_hdev(struct device *dev) +{ + struct hdac_device *hdev = dev_to_hdac_dev(dev); + return hdev; +} + +static int hda_ext_drv_probe(struct device *dev) +{ + return (get_hdrv(dev))->probe(get_hdev(dev)); +} + +static int hdac_ext_drv_remove(struct device *dev) +{ + return (get_hdrv(dev))->remove(get_hdev(dev)); +} + +static void hdac_ext_drv_shutdown(struct device *dev) +{ + return (get_hdrv(dev))->shutdown(get_hdev(dev)); +} + +/** + * snd_hda_ext_driver_register - register a driver for ext hda devices + * + * @drv: ext hda driver structure + */ +int snd_hda_ext_driver_register(struct hdac_driver *drv) +{ + drv->type = HDA_DEV_ASOC; + drv->driver.bus = &snd_hda_bus_type; + /* we use default match */ + + if (drv->probe) + drv->driver.probe = hda_ext_drv_probe; + if (drv->remove) + drv->driver.remove = hdac_ext_drv_remove; + if (drv->shutdown) + drv->driver.shutdown = hdac_ext_drv_shutdown; + + return driver_register(&drv->driver); +} +EXPORT_SYMBOL_GPL(snd_hda_ext_driver_register); + +/** + * snd_hda_ext_driver_unregister - unregister a driver for ext hda devices + * + * @drv: ext hda driver structure + */ +void snd_hda_ext_driver_unregister(struct hdac_driver *drv) +{ + driver_unregister(&drv->driver); +} +EXPORT_SYMBOL_GPL(snd_hda_ext_driver_unregister); diff --git a/sound/hda/core/ext/controller.c b/sound/hda/core/ext/controller.c new file mode 100644 index 0000000000000..c84754434d162 --- /dev/null +++ b/sound/hda/core/ext/controller.c @@ -0,0 +1,412 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * hdac-ext-controller.c - HD-audio extended controller functions. + * + * Copyright (C) 2014-2015 Intel Corp + * Author: Jeeja KP + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + */ + +#include +#include +#include +#include +#include + +/* + * processing pipe helpers - these helpers are useful for dealing with HDA + * new capability of processing pipelines + */ + +/** + * snd_hdac_ext_bus_ppcap_enable - enable/disable processing pipe capability + * @bus: the pointer to HDAC bus object + * @enable: flag to turn on/off the capability + */ +void snd_hdac_ext_bus_ppcap_enable(struct hdac_bus *bus, bool enable) +{ + + if (!bus->ppcap) { + dev_err(bus->dev, "Address of PP capability is NULL"); + return; + } + + if (enable) + snd_hdac_updatel(bus->ppcap, AZX_REG_PP_PPCTL, + AZX_PPCTL_GPROCEN, AZX_PPCTL_GPROCEN); + else + snd_hdac_updatel(bus->ppcap, AZX_REG_PP_PPCTL, + AZX_PPCTL_GPROCEN, 0); +} +EXPORT_SYMBOL_GPL(snd_hdac_ext_bus_ppcap_enable); + +/** + * snd_hdac_ext_bus_ppcap_int_enable - ppcap interrupt enable/disable + * @bus: the pointer to HDAC bus object + * @enable: flag to enable/disable interrupt + */ +void snd_hdac_ext_bus_ppcap_int_enable(struct hdac_bus *bus, bool enable) +{ + + if (!bus->ppcap) { + dev_err(bus->dev, "Address of PP capability is NULL\n"); + return; + } + + if (enable) + snd_hdac_updatel(bus->ppcap, AZX_REG_PP_PPCTL, + AZX_PPCTL_PIE, AZX_PPCTL_PIE); + else + snd_hdac_updatel(bus->ppcap, AZX_REG_PP_PPCTL, + AZX_PPCTL_PIE, 0); +} +EXPORT_SYMBOL_GPL(snd_hdac_ext_bus_ppcap_int_enable); + +/* + * Multilink helpers - these helpers are useful for dealing with HDA + * new multilink capability + */ + +/** + * snd_hdac_ext_bus_get_ml_capabilities - get multilink capability + * @bus: the pointer to HDAC bus object + * + * This will parse all links and read the mlink capabilities and add them + * in hlink_list of extended hdac bus + * Note: this will be freed on bus exit by driver + */ +int snd_hdac_ext_bus_get_ml_capabilities(struct hdac_bus *bus) +{ + int idx; + u32 link_count; + struct hdac_ext_link *hlink; + u32 leptr; + + link_count = readl(bus->mlcap + AZX_REG_ML_MLCD) + 1; + + dev_dbg(bus->dev, "In %s Link count: %d\n", __func__, link_count); + + for (idx = 0; idx < link_count; idx++) { + hlink = kzalloc(sizeof(*hlink), GFP_KERNEL); + if (!hlink) + return -ENOMEM; + hlink->index = idx; + hlink->bus = bus; + hlink->ml_addr = bus->mlcap + AZX_ML_BASE + + (AZX_ML_INTERVAL * idx); + hlink->lcaps = readl(hlink->ml_addr + AZX_REG_ML_LCAP); + hlink->lsdiid = readw(hlink->ml_addr + AZX_REG_ML_LSDIID); + hlink->slcount = FIELD_GET(AZX_ML_HDA_LCAP_SLCOUNT, hlink->lcaps) + 1; + + if (hdac_ext_link_alt(hlink)) { + leptr = readl(hlink->ml_addr + AZX_REG_ML_LEPTR); + hlink->id = FIELD_GET(AZX_REG_ML_LEPTR_ID, leptr); + } + + /* since link in On, update the ref */ + hlink->ref_count = 1; + + list_add_tail(&hlink->list, &bus->hlink_list); + } + + return 0; +} +EXPORT_SYMBOL_GPL(snd_hdac_ext_bus_get_ml_capabilities); + +/** + * snd_hdac_ext_link_free_all- free hdac extended link objects + * + * @bus: the pointer to HDAC bus object + */ + +void snd_hdac_ext_link_free_all(struct hdac_bus *bus) +{ + struct hdac_ext_link *hlink; + + while (!list_empty(&bus->hlink_list)) { + hlink = list_first_entry(&bus->hlink_list, struct hdac_ext_link, list); + list_del(&hlink->list); + kfree(hlink); + } +} +EXPORT_SYMBOL_GPL(snd_hdac_ext_link_free_all); + +struct hdac_ext_link *snd_hdac_ext_bus_get_hlink_by_id(struct hdac_bus *bus, u32 id) +{ + struct hdac_ext_link *hlink; + + list_for_each_entry(hlink, &bus->hlink_list, list) + if (hdac_ext_link_alt(hlink) && hlink->id == id) + return hlink; + return NULL; +} +EXPORT_SYMBOL_GPL(snd_hdac_ext_bus_get_hlink_by_id); + +/** + * snd_hdac_ext_bus_get_hlink_by_addr - get hlink at specified address + * @bus: hlink's parent bus device + * @addr: codec device address + * + * Returns hlink object or NULL if matching hlink is not found. + */ +struct hdac_ext_link *snd_hdac_ext_bus_get_hlink_by_addr(struct hdac_bus *bus, int addr) +{ + struct hdac_ext_link *hlink; + + list_for_each_entry(hlink, &bus->hlink_list, list) + if (hlink->lsdiid & (0x1 << addr)) + return hlink; + return NULL; +} +EXPORT_SYMBOL_GPL(snd_hdac_ext_bus_get_hlink_by_addr); + +/** + * snd_hdac_ext_bus_get_hlink_by_name - get hlink based on codec name + * @bus: the pointer to HDAC bus object + * @codec_name: codec name + */ +struct hdac_ext_link *snd_hdac_ext_bus_get_hlink_by_name(struct hdac_bus *bus, + const char *codec_name) +{ + int bus_idx, addr; + + if (sscanf(codec_name, "ehdaudio%dD%d", &bus_idx, &addr) != 2) + return NULL; + if (bus->idx != bus_idx) + return NULL; + if (addr < 0 || addr > 31) + return NULL; + + return snd_hdac_ext_bus_get_hlink_by_addr(bus, addr); +} +EXPORT_SYMBOL_GPL(snd_hdac_ext_bus_get_hlink_by_name); + +static int check_hdac_link_power_active(struct hdac_ext_link *hlink, bool enable) +{ + int timeout; + u32 val; + int mask = (1 << AZX_ML_LCTL_CPA_SHIFT); + + udelay(3); + timeout = 150; + + do { + val = readl(hlink->ml_addr + AZX_REG_ML_LCTL); + if (enable) { + if (((val & mask) >> AZX_ML_LCTL_CPA_SHIFT)) + return 0; + } else { + if (!((val & mask) >> AZX_ML_LCTL_CPA_SHIFT)) + return 0; + } + udelay(3); + } while (--timeout); + + return -EIO; +} + +/** + * snd_hdac_ext_bus_link_power_up -power up hda link + * @hlink: HD-audio extended link + */ +int snd_hdac_ext_bus_link_power_up(struct hdac_ext_link *hlink) +{ + snd_hdac_updatel(hlink->ml_addr, AZX_REG_ML_LCTL, + AZX_ML_LCTL_SPA, AZX_ML_LCTL_SPA); + + return check_hdac_link_power_active(hlink, true); +} +EXPORT_SYMBOL_GPL(snd_hdac_ext_bus_link_power_up); + +/** + * snd_hdac_ext_bus_link_power_down -power down hda link + * @hlink: HD-audio extended link + */ +int snd_hdac_ext_bus_link_power_down(struct hdac_ext_link *hlink) +{ + snd_hdac_updatel(hlink->ml_addr, AZX_REG_ML_LCTL, AZX_ML_LCTL_SPA, 0); + + return check_hdac_link_power_active(hlink, false); +} +EXPORT_SYMBOL_GPL(snd_hdac_ext_bus_link_power_down); + +/** + * snd_hdac_ext_bus_link_power_up_all -power up all hda link + * @bus: the pointer to HDAC bus object + */ +int snd_hdac_ext_bus_link_power_up_all(struct hdac_bus *bus) +{ + struct hdac_ext_link *hlink = NULL; + int ret; + + list_for_each_entry(hlink, &bus->hlink_list, list) { + ret = snd_hdac_ext_bus_link_power_up(hlink); + if (ret < 0) + return ret; + } + + return 0; +} +EXPORT_SYMBOL_GPL(snd_hdac_ext_bus_link_power_up_all); + +/** + * snd_hdac_ext_bus_link_power_down_all -power down all hda link + * @bus: the pointer to HDAC bus object + */ +int snd_hdac_ext_bus_link_power_down_all(struct hdac_bus *bus) +{ + struct hdac_ext_link *hlink = NULL; + int ret; + + list_for_each_entry(hlink, &bus->hlink_list, list) { + ret = snd_hdac_ext_bus_link_power_down(hlink); + if (ret < 0) + return ret; + } + + return 0; +} +EXPORT_SYMBOL_GPL(snd_hdac_ext_bus_link_power_down_all); + +/** + * snd_hdac_ext_bus_link_set_stream_id - maps stream id to link output + * @link: HD-audio ext link to set up + * @stream: stream id + */ +void snd_hdac_ext_bus_link_set_stream_id(struct hdac_ext_link *link, + int stream) +{ + snd_hdac_updatew(link->ml_addr, AZX_REG_ML_LOSIDV, (1 << stream), 1 << stream); +} +EXPORT_SYMBOL_GPL(snd_hdac_ext_bus_link_set_stream_id); + +/** + * snd_hdac_ext_bus_link_clear_stream_id - maps stream id to link output + * @link: HD-audio ext link to set up + * @stream: stream id + */ +void snd_hdac_ext_bus_link_clear_stream_id(struct hdac_ext_link *link, + int stream) +{ + snd_hdac_updatew(link->ml_addr, AZX_REG_ML_LOSIDV, (1 << stream), 0); +} +EXPORT_SYMBOL_GPL(snd_hdac_ext_bus_link_clear_stream_id); + +int snd_hdac_ext_bus_link_get(struct hdac_bus *bus, + struct hdac_ext_link *hlink) +{ + unsigned long codec_mask; + int ret = 0; + + mutex_lock(&bus->lock); + + /* + * if we move from 0 to 1, count will be 1 so power up this link + * as well, also check the dma status and trigger that + */ + if (++hlink->ref_count == 1) { + if (!bus->cmd_dma_state) { + snd_hdac_bus_init_cmd_io(bus); + bus->cmd_dma_state = true; + } + + ret = snd_hdac_ext_bus_link_power_up(hlink); + + /* + * clear the register to invalidate all the output streams + */ + snd_hdac_updatew(hlink->ml_addr, AZX_REG_ML_LOSIDV, + AZX_ML_LOSIDV_STREAM_MASK, 0); + /* + * wait for 521usec for codec to report status + * HDA spec section 4.3 - Codec Discovery + */ + udelay(521); + codec_mask = snd_hdac_chip_readw(bus, STATESTS); + dev_dbg(bus->dev, "codec_mask = 0x%lx\n", codec_mask); + snd_hdac_chip_writew(bus, STATESTS, codec_mask); + if (!bus->codec_mask) + bus->codec_mask = codec_mask; + } + + mutex_unlock(&bus->lock); + return ret; +} +EXPORT_SYMBOL_GPL(snd_hdac_ext_bus_link_get); + +int snd_hdac_ext_bus_link_put(struct hdac_bus *bus, + struct hdac_ext_link *hlink) +{ + int ret = 0; + struct hdac_ext_link *hlink_tmp; + bool link_up = false; + + mutex_lock(&bus->lock); + + /* + * if we move from 1 to 0, count will be 0 + * so power down this link as well + */ + if (--hlink->ref_count == 0) { + ret = snd_hdac_ext_bus_link_power_down(hlink); + + /* + * now check if all links are off, if so turn off + * cmd dma as well + */ + list_for_each_entry(hlink_tmp, &bus->hlink_list, list) { + if (hlink_tmp->ref_count) { + link_up = true; + break; + } + } + + if (!link_up) { + snd_hdac_bus_stop_cmd_io(bus); + bus->cmd_dma_state = false; + } + } + + mutex_unlock(&bus->lock); + return ret; +} +EXPORT_SYMBOL_GPL(snd_hdac_ext_bus_link_put); + +static void hdac_ext_codec_link_up(struct hdac_device *codec) +{ + const char *devname = dev_name(&codec->dev); + struct hdac_ext_link *hlink = + snd_hdac_ext_bus_get_hlink_by_name(codec->bus, devname); + + if (hlink) + snd_hdac_ext_bus_link_get(codec->bus, hlink); +} + +static void hdac_ext_codec_link_down(struct hdac_device *codec) +{ + const char *devname = dev_name(&codec->dev); + struct hdac_ext_link *hlink = + snd_hdac_ext_bus_get_hlink_by_name(codec->bus, devname); + + if (hlink) + snd_hdac_ext_bus_link_put(codec->bus, hlink); +} + +void snd_hdac_ext_bus_link_power(struct hdac_device *codec, bool enable) +{ + struct hdac_bus *bus = codec->bus; + bool oldstate = test_bit(codec->addr, &bus->codec_powered); + + if (enable == oldstate) + return; + + snd_hdac_bus_link_power(codec, enable); + + if (enable) + hdac_ext_codec_link_up(codec); + else + hdac_ext_codec_link_down(codec); +} +EXPORT_SYMBOL_GPL(snd_hdac_ext_bus_link_power); diff --git a/sound/hda/core/ext/stream.c b/sound/hda/core/ext/stream.c new file mode 100644 index 0000000000000..a3ac738f1130b --- /dev/null +++ b/sound/hda/core/ext/stream.c @@ -0,0 +1,453 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * hdac-ext-stream.c - HD-audio extended stream operations. + * + * Copyright (C) 2015 Intel Corp + * Author: Jeeja KP + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +/** + * snd_hdac_ext_host_stream_setup - Setup a HOST stream. + * @hext_stream: HDAudio stream to set up. + * @code_loading: Whether the stream is for PCM or code-loading. + * + * Return: Zero on success or negative error code. + */ +int snd_hdac_ext_host_stream_setup(struct hdac_ext_stream *hext_stream, bool code_loading) +{ + return hext_stream->host_setup(hdac_stream(hext_stream), code_loading); +} +EXPORT_SYMBOL_GPL(snd_hdac_ext_host_stream_setup); + +/** + * snd_hdac_apl_host_stream_setup - Setup a HOST stream following procedure + * recommended for ApolloLake devices. + * @hstream: HDAudio stream to set up. + * @code_loading: Whether the stream is for PCM or code-loading. + * + * Return: Zero on success or negative error code. + */ +static int snd_hdac_apl_host_stream_setup(struct hdac_stream *hstream, bool code_loading) +{ + struct hdac_ext_stream *hext_stream = stream_to_hdac_ext_stream(hstream); + int ret; + + snd_hdac_ext_stream_decouple(hstream->bus, hext_stream, false); + ret = snd_hdac_stream_setup(hstream, code_loading); + snd_hdac_ext_stream_decouple(hstream->bus, hext_stream, true); + + return ret; +} + +/** + * snd_hdac_ext_stream_init - initialize each stream (aka device) + * @bus: HD-audio core bus + * @hext_stream: HD-audio ext core stream object to initialize + * @idx: stream index number + * @direction: stream direction (SNDRV_PCM_STREAM_PLAYBACK or SNDRV_PCM_STREAM_CAPTURE) + * @tag: the tag id to assign + * + * initialize the stream, if ppcap is enabled then init those and then + * invoke hdac stream initialization routine + */ +static void snd_hdac_ext_stream_init(struct hdac_bus *bus, + struct hdac_ext_stream *hext_stream, + int idx, int direction, int tag) +{ + if (bus->ppcap) { + hext_stream->pphc_addr = bus->ppcap + AZX_PPHC_BASE + + AZX_PPHC_INTERVAL * idx; + + hext_stream->pplc_addr = bus->ppcap + AZX_PPLC_BASE + + AZX_PPLC_MULTI * bus->num_streams + + AZX_PPLC_INTERVAL * idx; + } + + hext_stream->decoupled = false; + snd_hdac_stream_init(bus, &hext_stream->hstream, idx, direction, tag); +} + +/** + * snd_hdac_ext_stream_init_all - create and initialize the stream objects + * for an extended hda bus + * @bus: HD-audio core bus + * @start_idx: start index for streams + * @num_stream: number of streams to initialize + * @dir: direction of streams + */ +int snd_hdac_ext_stream_init_all(struct hdac_bus *bus, int start_idx, + int num_stream, int dir) +{ + struct pci_dev *pci = to_pci_dev(bus->dev); + int (*setup_op)(struct hdac_stream *, bool); + int stream_tag = 0; + int i, tag, idx = start_idx; + + if (pci->device == PCI_DEVICE_ID_INTEL_HDA_APL) + setup_op = snd_hdac_apl_host_stream_setup; + else + setup_op = snd_hdac_stream_setup; + + for (i = 0; i < num_stream; i++) { + struct hdac_ext_stream *hext_stream = + kzalloc(sizeof(*hext_stream), GFP_KERNEL); + if (!hext_stream) + return -ENOMEM; + tag = ++stream_tag; + snd_hdac_ext_stream_init(bus, hext_stream, idx, dir, tag); + idx++; + hext_stream->host_setup = setup_op; + } + + return 0; + +} +EXPORT_SYMBOL_GPL(snd_hdac_ext_stream_init_all); + +/** + * snd_hdac_ext_stream_free_all - free hdac extended stream objects + * + * @bus: HD-audio core bus + */ +void snd_hdac_ext_stream_free_all(struct hdac_bus *bus) +{ + struct hdac_stream *s, *_s; + struct hdac_ext_stream *hext_stream; + + list_for_each_entry_safe(s, _s, &bus->stream_list, list) { + hext_stream = stream_to_hdac_ext_stream(s); + snd_hdac_ext_stream_decouple(bus, hext_stream, false); + list_del(&s->list); + kfree(hext_stream); + } +} +EXPORT_SYMBOL_GPL(snd_hdac_ext_stream_free_all); + +void snd_hdac_ext_stream_decouple_locked(struct hdac_bus *bus, + struct hdac_ext_stream *hext_stream, + bool decouple) +{ + struct hdac_stream *hstream = &hext_stream->hstream; + u32 val; + int mask = AZX_PPCTL_PROCEN(hstream->index); + + val = readw(bus->ppcap + AZX_REG_PP_PPCTL) & mask; + + if (decouple && !val) + snd_hdac_updatel(bus->ppcap, AZX_REG_PP_PPCTL, mask, mask); + else if (!decouple && val) + snd_hdac_updatel(bus->ppcap, AZX_REG_PP_PPCTL, mask, 0); + + hext_stream->decoupled = decouple; +} +EXPORT_SYMBOL_GPL(snd_hdac_ext_stream_decouple_locked); + +/** + * snd_hdac_ext_stream_decouple - decouple the hdac stream + * @bus: HD-audio core bus + * @hext_stream: HD-audio ext core stream object to initialize + * @decouple: flag to decouple + */ +void snd_hdac_ext_stream_decouple(struct hdac_bus *bus, + struct hdac_ext_stream *hext_stream, bool decouple) +{ + spin_lock_irq(&bus->reg_lock); + snd_hdac_ext_stream_decouple_locked(bus, hext_stream, decouple); + spin_unlock_irq(&bus->reg_lock); +} +EXPORT_SYMBOL_GPL(snd_hdac_ext_stream_decouple); + +/** + * snd_hdac_ext_stream_start - start a stream + * @hext_stream: HD-audio ext core stream to start + */ +void snd_hdac_ext_stream_start(struct hdac_ext_stream *hext_stream) +{ + snd_hdac_updatel(hext_stream->pplc_addr, AZX_REG_PPLCCTL, + AZX_PPLCCTL_RUN, AZX_PPLCCTL_RUN); +} +EXPORT_SYMBOL_GPL(snd_hdac_ext_stream_start); + +/** + * snd_hdac_ext_stream_clear - stop a stream DMA + * @hext_stream: HD-audio ext core stream to stop + */ +void snd_hdac_ext_stream_clear(struct hdac_ext_stream *hext_stream) +{ + snd_hdac_updatel(hext_stream->pplc_addr, AZX_REG_PPLCCTL, AZX_PPLCCTL_RUN, 0); +} +EXPORT_SYMBOL_GPL(snd_hdac_ext_stream_clear); + +/** + * snd_hdac_ext_stream_reset - reset a stream + * @hext_stream: HD-audio ext core stream to reset + */ +void snd_hdac_ext_stream_reset(struct hdac_ext_stream *hext_stream) +{ + unsigned char val; + int timeout; + + snd_hdac_ext_stream_clear(hext_stream); + + snd_hdac_updatel(hext_stream->pplc_addr, AZX_REG_PPLCCTL, + AZX_PPLCCTL_STRST, AZX_PPLCCTL_STRST); + udelay(3); + timeout = 50; + do { + val = readl(hext_stream->pplc_addr + AZX_REG_PPLCCTL) & + AZX_PPLCCTL_STRST; + if (val) + break; + udelay(3); + } while (--timeout); + val &= ~AZX_PPLCCTL_STRST; + writel(val, hext_stream->pplc_addr + AZX_REG_PPLCCTL); + udelay(3); + + timeout = 50; + /* waiting for hardware to report that the stream is out of reset */ + do { + val = readl(hext_stream->pplc_addr + AZX_REG_PPLCCTL) & AZX_PPLCCTL_STRST; + if (!val) + break; + udelay(3); + } while (--timeout); + +} +EXPORT_SYMBOL_GPL(snd_hdac_ext_stream_reset); + +/** + * snd_hdac_ext_stream_setup - set up the SD for streaming + * @hext_stream: HD-audio ext core stream to set up + * @fmt: stream format + */ +int snd_hdac_ext_stream_setup(struct hdac_ext_stream *hext_stream, int fmt) +{ + struct hdac_stream *hstream = &hext_stream->hstream; + unsigned int val; + + /* make sure the run bit is zero for SD */ + snd_hdac_ext_stream_clear(hext_stream); + /* program the stream_tag */ + val = readl(hext_stream->pplc_addr + AZX_REG_PPLCCTL); + val = (val & ~AZX_PPLCCTL_STRM_MASK) | + (hstream->stream_tag << AZX_PPLCCTL_STRM_SHIFT); + writel(val, hext_stream->pplc_addr + AZX_REG_PPLCCTL); + + /* program the stream format */ + writew(fmt, hext_stream->pplc_addr + AZX_REG_PPLCFMT); + + return 0; +} +EXPORT_SYMBOL_GPL(snd_hdac_ext_stream_setup); + +static struct hdac_ext_stream * +hdac_ext_link_dma_stream_assign(struct hdac_bus *bus, + struct snd_pcm_substream *substream) +{ + struct hdac_ext_stream *res = NULL; + struct hdac_stream *hstream = NULL; + + if (!bus->ppcap) { + dev_err(bus->dev, "stream type not supported\n"); + return NULL; + } + + spin_lock_irq(&bus->reg_lock); + list_for_each_entry(hstream, &bus->stream_list, list) { + struct hdac_ext_stream *hext_stream = container_of(hstream, + struct hdac_ext_stream, + hstream); + if (hstream->direction != substream->stream) + continue; + + /* check if link stream is available */ + if (!hext_stream->link_locked) { + res = hext_stream; + break; + } + + } + if (res) { + snd_hdac_ext_stream_decouple_locked(bus, res, true); + res->link_locked = 1; + res->link_substream = substream; + } + spin_unlock_irq(&bus->reg_lock); + return res; +} + +static struct hdac_ext_stream * +hdac_ext_host_dma_stream_assign(struct hdac_bus *bus, + struct snd_pcm_substream *substream) +{ + struct hdac_ext_stream *res = NULL; + struct hdac_stream *hstream = NULL; + + if (!bus->ppcap) { + dev_err(bus->dev, "stream type not supported\n"); + return NULL; + } + + spin_lock_irq(&bus->reg_lock); + list_for_each_entry(hstream, &bus->stream_list, list) { + struct hdac_ext_stream *hext_stream = container_of(hstream, + struct hdac_ext_stream, + hstream); + if (hstream->direction != substream->stream) + continue; + + if (!hstream->opened) { + res = hext_stream; + break; + } + } + if (res) { + snd_hdac_ext_stream_decouple_locked(bus, res, true); + res->hstream.opened = 1; + res->hstream.running = 0; + res->hstream.substream = substream; + } + spin_unlock_irq(&bus->reg_lock); + + return res; +} + +/** + * snd_hdac_ext_stream_assign - assign a stream for the PCM + * @bus: HD-audio core bus + * @substream: PCM substream to assign + * @type: type of stream (coupled, host or link stream) + * + * This assigns the stream based on the type (coupled/host/link), for the + * given PCM substream, assigns it and returns the stream object + * + * coupled: Looks for an unused stream + * host: Looks for an unused decoupled host stream + * link: Looks for an unused decoupled link stream + * + * If no stream is free, returns NULL. The function tries to keep using + * the same stream object when it's used beforehand. when a stream is + * decoupled, it becomes a host stream and link stream. + */ +struct hdac_ext_stream *snd_hdac_ext_stream_assign(struct hdac_bus *bus, + struct snd_pcm_substream *substream, + int type) +{ + struct hdac_ext_stream *hext_stream = NULL; + struct hdac_stream *hstream = NULL; + + switch (type) { + case HDAC_EXT_STREAM_TYPE_COUPLED: + hstream = snd_hdac_stream_assign(bus, substream); + if (hstream) + hext_stream = container_of(hstream, + struct hdac_ext_stream, + hstream); + return hext_stream; + + case HDAC_EXT_STREAM_TYPE_HOST: + return hdac_ext_host_dma_stream_assign(bus, substream); + + case HDAC_EXT_STREAM_TYPE_LINK: + return hdac_ext_link_dma_stream_assign(bus, substream); + + default: + return NULL; + } +} +EXPORT_SYMBOL_GPL(snd_hdac_ext_stream_assign); + +/** + * snd_hdac_ext_stream_release - release the assigned stream + * @hext_stream: HD-audio ext core stream to release + * @type: type of stream (coupled, host or link stream) + * + * Release the stream that has been assigned by snd_hdac_ext_stream_assign(). + */ +void snd_hdac_ext_stream_release(struct hdac_ext_stream *hext_stream, int type) +{ + struct hdac_bus *bus = hext_stream->hstream.bus; + + switch (type) { + case HDAC_EXT_STREAM_TYPE_COUPLED: + snd_hdac_stream_release(&hext_stream->hstream); + break; + + case HDAC_EXT_STREAM_TYPE_HOST: + spin_lock_irq(&bus->reg_lock); + /* couple link only if not in use */ + if (!hext_stream->link_locked) + snd_hdac_ext_stream_decouple_locked(bus, hext_stream, false); + snd_hdac_stream_release_locked(&hext_stream->hstream); + spin_unlock_irq(&bus->reg_lock); + break; + + case HDAC_EXT_STREAM_TYPE_LINK: + spin_lock_irq(&bus->reg_lock); + /* couple host only if not in use */ + if (!hext_stream->hstream.opened) + snd_hdac_ext_stream_decouple_locked(bus, hext_stream, false); + hext_stream->link_locked = 0; + hext_stream->link_substream = NULL; + spin_unlock_irq(&bus->reg_lock); + break; + + default: + dev_dbg(bus->dev, "Invalid type %d\n", type); + } + +} +EXPORT_SYMBOL_GPL(snd_hdac_ext_stream_release); + +/** + * snd_hdac_ext_cstream_assign - assign a host stream for compress + * @bus: HD-audio core bus + * @cstream: Compress stream to assign + * + * Assign an unused host stream for the given compress stream. + * If no stream is free, NULL is returned. Stream is decoupled + * before assignment. + */ +struct hdac_ext_stream *snd_hdac_ext_cstream_assign(struct hdac_bus *bus, + struct snd_compr_stream *cstream) +{ + struct hdac_ext_stream *res = NULL; + struct hdac_stream *hstream; + + spin_lock_irq(&bus->reg_lock); + list_for_each_entry(hstream, &bus->stream_list, list) { + struct hdac_ext_stream *hext_stream = stream_to_hdac_ext_stream(hstream); + + if (hstream->direction != cstream->direction) + continue; + + if (!hstream->opened) { + res = hext_stream; + break; + } + } + + if (res) { + snd_hdac_ext_stream_decouple_locked(bus, res, true); + res->hstream.opened = 1; + res->hstream.running = 0; + res->hstream.cstream = cstream; + } + spin_unlock_irq(&bus->reg_lock); + + return res; +} +EXPORT_SYMBOL_GPL(snd_hdac_ext_cstream_assign); diff --git a/sound/hda/core/hda_bus_type.c b/sound/hda/core/hda_bus_type.c new file mode 100644 index 0000000000000..eb72a7af2e56e --- /dev/null +++ b/sound/hda/core/hda_bus_type.c @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * HD-audio bus + */ +#include +#include +#include +#include +#include +#include + +MODULE_DESCRIPTION("HD-audio bus"); +MODULE_LICENSE("GPL"); + +/** + * hdac_get_device_id - gets the hdac device id entry + * @hdev: HD-audio core device + * @drv: HD-audio codec driver + * + * Compares the hdac device vendor_id and revision_id to the hdac_device + * driver id_table and returns the matching device id entry. + */ +const struct hda_device_id * +hdac_get_device_id(struct hdac_device *hdev, const struct hdac_driver *drv) +{ + if (drv->id_table) { + const struct hda_device_id *id = drv->id_table; + + while (id->vendor_id) { + if (hdev->vendor_id == id->vendor_id && + (!id->rev_id || id->rev_id == hdev->revision_id)) + return id; + id++; + } + } + + return NULL; +} +EXPORT_SYMBOL_GPL(hdac_get_device_id); + +static int hdac_codec_match(struct hdac_device *dev, const struct hdac_driver *drv) +{ + if (hdac_get_device_id(dev, drv)) + return 1; + else + return 0; +} + +static int hda_bus_match(struct device *dev, const struct device_driver *drv) +{ + struct hdac_device *hdev = dev_to_hdac_dev(dev); + const struct hdac_driver *hdrv = drv_to_hdac_driver(drv); + + if (hdev->type != hdrv->type) + return 0; + + /* + * if driver provided a match function use that otherwise we will + * use hdac_codec_match function + */ + if (hdrv->match) + return hdrv->match(hdev, hdrv); + else + return hdac_codec_match(hdev, hdrv); + return 1; +} + +static int hda_uevent(const struct device *dev, struct kobj_uevent_env *env) +{ + char modalias[32]; + + snd_hdac_codec_modalias(dev_to_hdac_dev(dev), modalias, + sizeof(modalias)); + if (add_uevent_var(env, "MODALIAS=%s", modalias)) + return -ENOMEM; + return 0; +} + +const struct bus_type snd_hda_bus_type = { + .name = "hdaudio", + .match = hda_bus_match, + .uevent = hda_uevent, +}; +EXPORT_SYMBOL_GPL(snd_hda_bus_type); + +static int __init hda_bus_init(void) +{ + return bus_register(&snd_hda_bus_type); +} + +static void __exit hda_bus_exit(void) +{ + bus_unregister(&snd_hda_bus_type); +} + +subsys_initcall(hda_bus_init); +module_exit(hda_bus_exit); diff --git a/sound/hda/core/hdmi_chmap.c b/sound/hda/core/hdmi_chmap.c new file mode 100644 index 0000000000000..7b276047f85a7 --- /dev/null +++ b/sound/hda/core/hdmi_chmap.c @@ -0,0 +1,868 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * HDMI Channel map support helpers + */ + +#include +#include +#include +#include + +/* + * CEA speaker placement: + * + * FLH FCH FRH + * FLW FL FLC FC FRC FR FRW + * + * LFE + * TC + * + * RL RLC RC RRC RR + * + * The Left/Right Surround channel _notions_ LS/RS in SMPTE 320M corresponds to + * CEA RL/RR; The SMPTE channel _assignment_ C/LFE is swapped to CEA LFE/FC. + */ +enum cea_speaker_placement { + FL = (1 << 0), /* Front Left */ + FC = (1 << 1), /* Front Center */ + FR = (1 << 2), /* Front Right */ + FLC = (1 << 3), /* Front Left Center */ + FRC = (1 << 4), /* Front Right Center */ + RL = (1 << 5), /* Rear Left */ + RC = (1 << 6), /* Rear Center */ + RR = (1 << 7), /* Rear Right */ + RLC = (1 << 8), /* Rear Left Center */ + RRC = (1 << 9), /* Rear Right Center */ + LFE = (1 << 10), /* Low Frequency Effect */ + FLW = (1 << 11), /* Front Left Wide */ + FRW = (1 << 12), /* Front Right Wide */ + FLH = (1 << 13), /* Front Left High */ + FCH = (1 << 14), /* Front Center High */ + FRH = (1 << 15), /* Front Right High */ + TC = (1 << 16), /* Top Center */ +}; + +static const char * const cea_speaker_allocation_names[] = { + /* 0 */ "FL/FR", + /* 1 */ "LFE", + /* 2 */ "FC", + /* 3 */ "RL/RR", + /* 4 */ "RC", + /* 5 */ "FLC/FRC", + /* 6 */ "RLC/RRC", + /* 7 */ "FLW/FRW", + /* 8 */ "FLH/FRH", + /* 9 */ "TC", + /* 10 */ "FCH", +}; + +/* + * ELD SA bits in the CEA Speaker Allocation data block + */ +static const int eld_speaker_allocation_bits[] = { + [0] = FL | FR, + [1] = LFE, + [2] = FC, + [3] = RL | RR, + [4] = RC, + [5] = FLC | FRC, + [6] = RLC | RRC, + /* the following are not defined in ELD yet */ + [7] = FLW | FRW, + [8] = FLH | FRH, + [9] = TC, + [10] = FCH, +}; + +/* + * ALSA sequence is: + * + * surround40 surround41 surround50 surround51 surround71 + * ch0 front left = = = = + * ch1 front right = = = = + * ch2 rear left = = = = + * ch3 rear right = = = = + * ch4 LFE center center center + * ch5 LFE LFE + * ch6 side left + * ch7 side right + * + * surround71 = {FL, FR, RLC, RRC, FC, LFE, RL, RR} + */ +static int hdmi_channel_mapping[0x32][8] = { + /* stereo */ + [0x00] = { 0x00, 0x11, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7 }, + /* 2.1 */ + [0x01] = { 0x00, 0x11, 0x22, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7 }, + /* Dolby Surround */ + [0x02] = { 0x00, 0x11, 0x23, 0xf2, 0xf4, 0xf5, 0xf6, 0xf7 }, + /* surround40 */ + [0x08] = { 0x00, 0x11, 0x24, 0x35, 0xf3, 0xf2, 0xf6, 0xf7 }, + /* 4ch */ + [0x03] = { 0x00, 0x11, 0x23, 0x32, 0x44, 0xf5, 0xf6, 0xf7 }, + /* surround41 */ + [0x09] = { 0x00, 0x11, 0x24, 0x35, 0x42, 0xf3, 0xf6, 0xf7 }, + /* surround50 */ + [0x0a] = { 0x00, 0x11, 0x24, 0x35, 0x43, 0xf2, 0xf6, 0xf7 }, + /* surround51 */ + [0x0b] = { 0x00, 0x11, 0x24, 0x35, 0x43, 0x52, 0xf6, 0xf7 }, + /* 7.1 */ + [0x13] = { 0x00, 0x11, 0x26, 0x37, 0x43, 0x52, 0x64, 0x75 }, +}; + +/* + * This is an ordered list! + * + * The preceding ones have better chances to be selected by + * hdmi_channel_allocation(). + */ +static struct hdac_cea_channel_speaker_allocation channel_allocations[] = { +/* channel: 7 6 5 4 3 2 1 0 */ +{ .ca_index = 0x00, .speakers = { 0, 0, 0, 0, 0, 0, FR, FL } }, + /* 2.1 */ +{ .ca_index = 0x01, .speakers = { 0, 0, 0, 0, 0, LFE, FR, FL } }, + /* Dolby Surround */ +{ .ca_index = 0x02, .speakers = { 0, 0, 0, 0, FC, 0, FR, FL } }, + /* surround40 */ +{ .ca_index = 0x08, .speakers = { 0, 0, RR, RL, 0, 0, FR, FL } }, + /* surround41 */ +{ .ca_index = 0x09, .speakers = { 0, 0, RR, RL, 0, LFE, FR, FL } }, + /* surround50 */ +{ .ca_index = 0x0a, .speakers = { 0, 0, RR, RL, FC, 0, FR, FL } }, + /* surround51 */ +{ .ca_index = 0x0b, .speakers = { 0, 0, RR, RL, FC, LFE, FR, FL } }, + /* 6.1 */ +{ .ca_index = 0x0f, .speakers = { 0, RC, RR, RL, FC, LFE, FR, FL } }, + /* surround71 */ +{ .ca_index = 0x13, .speakers = { RRC, RLC, RR, RL, FC, LFE, FR, FL } }, + +{ .ca_index = 0x03, .speakers = { 0, 0, 0, 0, FC, LFE, FR, FL } }, +{ .ca_index = 0x04, .speakers = { 0, 0, 0, RC, 0, 0, FR, FL } }, +{ .ca_index = 0x05, .speakers = { 0, 0, 0, RC, 0, LFE, FR, FL } }, +{ .ca_index = 0x06, .speakers = { 0, 0, 0, RC, FC, 0, FR, FL } }, +{ .ca_index = 0x07, .speakers = { 0, 0, 0, RC, FC, LFE, FR, FL } }, +{ .ca_index = 0x0c, .speakers = { 0, RC, RR, RL, 0, 0, FR, FL } }, +{ .ca_index = 0x0d, .speakers = { 0, RC, RR, RL, 0, LFE, FR, FL } }, +{ .ca_index = 0x0e, .speakers = { 0, RC, RR, RL, FC, 0, FR, FL } }, +{ .ca_index = 0x10, .speakers = { RRC, RLC, RR, RL, 0, 0, FR, FL } }, +{ .ca_index = 0x11, .speakers = { RRC, RLC, RR, RL, 0, LFE, FR, FL } }, +{ .ca_index = 0x12, .speakers = { RRC, RLC, RR, RL, FC, 0, FR, FL } }, +{ .ca_index = 0x14, .speakers = { FRC, FLC, 0, 0, 0, 0, FR, FL } }, +{ .ca_index = 0x15, .speakers = { FRC, FLC, 0, 0, 0, LFE, FR, FL } }, +{ .ca_index = 0x16, .speakers = { FRC, FLC, 0, 0, FC, 0, FR, FL } }, +{ .ca_index = 0x17, .speakers = { FRC, FLC, 0, 0, FC, LFE, FR, FL } }, +{ .ca_index = 0x18, .speakers = { FRC, FLC, 0, RC, 0, 0, FR, FL } }, +{ .ca_index = 0x19, .speakers = { FRC, FLC, 0, RC, 0, LFE, FR, FL } }, +{ .ca_index = 0x1a, .speakers = { FRC, FLC, 0, RC, FC, 0, FR, FL } }, +{ .ca_index = 0x1b, .speakers = { FRC, FLC, 0, RC, FC, LFE, FR, FL } }, +{ .ca_index = 0x1c, .speakers = { FRC, FLC, RR, RL, 0, 0, FR, FL } }, +{ .ca_index = 0x1d, .speakers = { FRC, FLC, RR, RL, 0, LFE, FR, FL } }, +{ .ca_index = 0x1e, .speakers = { FRC, FLC, RR, RL, FC, 0, FR, FL } }, +{ .ca_index = 0x1f, .speakers = { FRC, FLC, RR, RL, FC, LFE, FR, FL } }, +{ .ca_index = 0x20, .speakers = { 0, FCH, RR, RL, FC, 0, FR, FL } }, +{ .ca_index = 0x21, .speakers = { 0, FCH, RR, RL, FC, LFE, FR, FL } }, +{ .ca_index = 0x22, .speakers = { TC, 0, RR, RL, FC, 0, FR, FL } }, +{ .ca_index = 0x23, .speakers = { TC, 0, RR, RL, FC, LFE, FR, FL } }, +{ .ca_index = 0x24, .speakers = { FRH, FLH, RR, RL, 0, 0, FR, FL } }, +{ .ca_index = 0x25, .speakers = { FRH, FLH, RR, RL, 0, LFE, FR, FL } }, +{ .ca_index = 0x26, .speakers = { FRW, FLW, RR, RL, 0, 0, FR, FL } }, +{ .ca_index = 0x27, .speakers = { FRW, FLW, RR, RL, 0, LFE, FR, FL } }, +{ .ca_index = 0x28, .speakers = { TC, RC, RR, RL, FC, 0, FR, FL } }, +{ .ca_index = 0x29, .speakers = { TC, RC, RR, RL, FC, LFE, FR, FL } }, +{ .ca_index = 0x2a, .speakers = { FCH, RC, RR, RL, FC, 0, FR, FL } }, +{ .ca_index = 0x2b, .speakers = { FCH, RC, RR, RL, FC, LFE, FR, FL } }, +{ .ca_index = 0x2c, .speakers = { TC, FCH, RR, RL, FC, 0, FR, FL } }, +{ .ca_index = 0x2d, .speakers = { TC, FCH, RR, RL, FC, LFE, FR, FL } }, +{ .ca_index = 0x2e, .speakers = { FRH, FLH, RR, RL, FC, 0, FR, FL } }, +{ .ca_index = 0x2f, .speakers = { FRH, FLH, RR, RL, FC, LFE, FR, FL } }, +{ .ca_index = 0x30, .speakers = { FRW, FLW, RR, RL, FC, 0, FR, FL } }, +{ .ca_index = 0x31, .speakers = { FRW, FLW, RR, RL, FC, LFE, FR, FL } }, +}; + +static int hdmi_pin_set_slot_channel(struct hdac_device *codec, + hda_nid_t pin_nid, int asp_slot, int channel) +{ + return snd_hdac_codec_write(codec, pin_nid, 0, + AC_VERB_SET_HDMI_CHAN_SLOT, + (channel << 4) | asp_slot); +} + +static int hdmi_pin_get_slot_channel(struct hdac_device *codec, + hda_nid_t pin_nid, int asp_slot) +{ + return (snd_hdac_codec_read(codec, pin_nid, 0, + AC_VERB_GET_HDMI_CHAN_SLOT, + asp_slot) & 0xf0) >> 4; +} + +static int hdmi_get_channel_count(struct hdac_device *codec, hda_nid_t cvt_nid) +{ + return 1 + snd_hdac_codec_read(codec, cvt_nid, 0, + AC_VERB_GET_CVT_CHAN_COUNT, 0); +} + +static void hdmi_set_channel_count(struct hdac_device *codec, + hda_nid_t cvt_nid, int chs) +{ + if (chs != hdmi_get_channel_count(codec, cvt_nid)) + snd_hdac_codec_write(codec, cvt_nid, 0, + AC_VERB_SET_CVT_CHAN_COUNT, chs - 1); +} + +/* + * Channel mapping routines + */ + +/* + * Compute derived values in channel_allocations[]. + */ +static void init_channel_allocations(void) +{ + int i, j; + struct hdac_cea_channel_speaker_allocation *p; + + for (i = 0; i < ARRAY_SIZE(channel_allocations); i++) { + p = channel_allocations + i; + p->channels = 0; + p->spk_mask = 0; + for (j = 0; j < ARRAY_SIZE(p->speakers); j++) + if (p->speakers[j]) { + p->channels++; + p->spk_mask |= p->speakers[j]; + } + } +} + +static int get_channel_allocation_order(int ca) +{ + int i; + + for (i = 0; i < ARRAY_SIZE(channel_allocations); i++) { + if (channel_allocations[i].ca_index == ca) + break; + } + return i; +} + +void snd_hdac_print_channel_allocation(int spk_alloc, char *buf, int buflen) +{ + int i, j; + + for (i = 0, j = 0; i < ARRAY_SIZE(cea_speaker_allocation_names); i++) { + if (spk_alloc & (1 << i)) + j += scnprintf(buf + j, buflen - j, " %s", + cea_speaker_allocation_names[i]); + } + buf[j] = '\0'; /* necessary when j == 0 */ +} +EXPORT_SYMBOL_GPL(snd_hdac_print_channel_allocation); + +/* + * The transformation takes two steps: + * + * eld->spk_alloc => (eld_speaker_allocation_bits[]) => spk_mask + * spk_mask => (channel_allocations[]) => ai->CA + * + * TODO: it could select the wrong CA from multiple candidates. +*/ +static int hdmi_channel_allocation_spk_alloc_blk(struct hdac_device *codec, + int spk_alloc, int channels) +{ + int i; + int ca = 0; + int spk_mask = 0; + char buf[SND_PRINT_CHANNEL_ALLOCATION_ADVISED_BUFSIZE]; + + /* + * CA defaults to 0 for basic stereo audio + */ + if (channels <= 2) + return 0; + + /* + * expand ELD's speaker allocation mask + * + * ELD tells the speaker mask in a compact(paired) form, + * expand ELD's notions to match the ones used by Audio InfoFrame. + */ + for (i = 0; i < ARRAY_SIZE(eld_speaker_allocation_bits); i++) { + if (spk_alloc & (1 << i)) + spk_mask |= eld_speaker_allocation_bits[i]; + } + + /* search for the first working match in the CA table */ + for (i = 0; i < ARRAY_SIZE(channel_allocations); i++) { + if (channels == channel_allocations[i].channels && + (spk_mask & channel_allocations[i].spk_mask) == + channel_allocations[i].spk_mask) { + ca = channel_allocations[i].ca_index; + break; + } + } + + if (!ca) { + /* + * if there was no match, select the regular ALSA channel + * allocation with the matching number of channels + */ + for (i = 0; i < ARRAY_SIZE(channel_allocations); i++) { + if (channels == channel_allocations[i].channels) { + ca = channel_allocations[i].ca_index; + break; + } + } + } + + snd_hdac_print_channel_allocation(spk_alloc, buf, sizeof(buf)); + dev_dbg(&codec->dev, "HDMI: select CA 0x%x for %d-channel allocation: %s\n", + ca, channels, buf); + + return ca; +} + +static void hdmi_debug_channel_mapping(struct hdac_chmap *chmap, + hda_nid_t pin_nid) +{ +#ifdef CONFIG_SND_DEBUG_VERBOSE + int i; + int channel; + + for (i = 0; i < 8; i++) { + channel = chmap->ops.pin_get_slot_channel( + chmap->hdac, pin_nid, i); + dev_dbg(&chmap->hdac->dev, "HDMI: ASP channel %d => slot %d\n", + channel, i); + } +#endif +} + +static void hdmi_std_setup_channel_mapping(struct hdac_chmap *chmap, + hda_nid_t pin_nid, + bool non_pcm, + int ca) +{ + struct hdac_cea_channel_speaker_allocation *ch_alloc; + int i; + int err; + int order; + int non_pcm_mapping[8]; + + order = get_channel_allocation_order(ca); + ch_alloc = &channel_allocations[order]; + + if (hdmi_channel_mapping[ca][1] == 0) { + int hdmi_slot = 0; + /* fill actual channel mappings in ALSA channel (i) order */ + for (i = 0; i < ch_alloc->channels; i++) { + while (!WARN_ON(hdmi_slot >= 8) && + !ch_alloc->speakers[7 - hdmi_slot]) + hdmi_slot++; /* skip zero slots */ + + hdmi_channel_mapping[ca][i] = (i << 4) | hdmi_slot++; + } + /* fill the rest of the slots with ALSA channel 0xf */ + for (hdmi_slot = 0; hdmi_slot < 8; hdmi_slot++) + if (!ch_alloc->speakers[7 - hdmi_slot]) + hdmi_channel_mapping[ca][i++] = (0xf << 4) | hdmi_slot; + } + + if (non_pcm) { + for (i = 0; i < ch_alloc->channels; i++) + non_pcm_mapping[i] = (i << 4) | i; + for (; i < 8; i++) + non_pcm_mapping[i] = (0xf << 4) | i; + } + + for (i = 0; i < 8; i++) { + int slotsetup = non_pcm ? non_pcm_mapping[i] : hdmi_channel_mapping[ca][i]; + int hdmi_slot = slotsetup & 0x0f; + int channel = (slotsetup & 0xf0) >> 4; + + err = chmap->ops.pin_set_slot_channel(chmap->hdac, + pin_nid, hdmi_slot, channel); + if (err) { + dev_dbg(&chmap->hdac->dev, "HDMI: channel mapping failed\n"); + break; + } + } +} + +struct channel_map_table { + unsigned char map; /* ALSA API channel map position */ + int spk_mask; /* speaker position bit mask */ +}; + +static struct channel_map_table map_tables[] = { + { SNDRV_CHMAP_FL, FL }, + { SNDRV_CHMAP_FR, FR }, + { SNDRV_CHMAP_RL, RL }, + { SNDRV_CHMAP_RR, RR }, + { SNDRV_CHMAP_LFE, LFE }, + { SNDRV_CHMAP_FC, FC }, + { SNDRV_CHMAP_RLC, RLC }, + { SNDRV_CHMAP_RRC, RRC }, + { SNDRV_CHMAP_RC, RC }, + { SNDRV_CHMAP_FLC, FLC }, + { SNDRV_CHMAP_FRC, FRC }, + { SNDRV_CHMAP_TFL, FLH }, + { SNDRV_CHMAP_TFR, FRH }, + { SNDRV_CHMAP_FLW, FLW }, + { SNDRV_CHMAP_FRW, FRW }, + { SNDRV_CHMAP_TC, TC }, + { SNDRV_CHMAP_TFC, FCH }, + {} /* terminator */ +}; + +/* from ALSA API channel position to speaker bit mask */ +int snd_hdac_chmap_to_spk_mask(unsigned char c) +{ + struct channel_map_table *t = map_tables; + + for (; t->map; t++) { + if (t->map == c) + return t->spk_mask; + } + return 0; +} +EXPORT_SYMBOL_GPL(snd_hdac_chmap_to_spk_mask); + +/* from ALSA API channel position to CEA slot */ +static int to_cea_slot(int ordered_ca, unsigned char pos) +{ + int mask = snd_hdac_chmap_to_spk_mask(pos); + int i; + + /* Add sanity check to pass klockwork check. + * This should never happen. + */ + if (ordered_ca >= ARRAY_SIZE(channel_allocations)) + return -1; + + if (mask) { + for (i = 0; i < 8; i++) { + if (channel_allocations[ordered_ca].speakers[7 - i] == mask) + return i; + } + } + + return -1; +} + +/* from speaker bit mask to ALSA API channel position */ +int snd_hdac_spk_to_chmap(int spk) +{ + struct channel_map_table *t = map_tables; + + for (; t->map; t++) { + if (t->spk_mask == spk) + return t->map; + } + return 0; +} +EXPORT_SYMBOL_GPL(snd_hdac_spk_to_chmap); + +/* from CEA slot to ALSA API channel position */ +static int from_cea_slot(int ordered_ca, unsigned char slot) +{ + int mask; + + /* Add sanity check to pass klockwork check. + * This should never happen. + */ + if (slot >= 8) + return 0; + + mask = channel_allocations[ordered_ca].speakers[7 - slot]; + + return snd_hdac_spk_to_chmap(mask); +} + +/* get the CA index corresponding to the given ALSA API channel map */ +static int hdmi_manual_channel_allocation(int chs, unsigned char *map) +{ + int i, spks = 0, spk_mask = 0; + + for (i = 0; i < chs; i++) { + int mask = snd_hdac_chmap_to_spk_mask(map[i]); + + if (mask) { + spk_mask |= mask; + spks++; + } + } + + for (i = 0; i < ARRAY_SIZE(channel_allocations); i++) { + if ((chs == channel_allocations[i].channels || + spks == channel_allocations[i].channels) && + (spk_mask & channel_allocations[i].spk_mask) == + channel_allocations[i].spk_mask) + return channel_allocations[i].ca_index; + } + return -1; +} + +/* set up the channel slots for the given ALSA API channel map */ +static int hdmi_manual_setup_channel_mapping(struct hdac_chmap *chmap, + hda_nid_t pin_nid, + int chs, unsigned char *map, + int ca) +{ + int ordered_ca = get_channel_allocation_order(ca); + int alsa_pos, hdmi_slot; + int assignments[8] = {[0 ... 7] = 0xf}; + + for (alsa_pos = 0; alsa_pos < chs; alsa_pos++) { + + hdmi_slot = to_cea_slot(ordered_ca, map[alsa_pos]); + + if (hdmi_slot < 0) + continue; /* unassigned channel */ + + assignments[hdmi_slot] = alsa_pos; + } + + for (hdmi_slot = 0; hdmi_slot < 8; hdmi_slot++) { + int err; + + err = chmap->ops.pin_set_slot_channel(chmap->hdac, + pin_nid, hdmi_slot, assignments[hdmi_slot]); + if (err) + return -EINVAL; + } + return 0; +} + +/* store ALSA API channel map from the current default map */ +static void hdmi_setup_fake_chmap(unsigned char *map, int ca) +{ + int i; + int ordered_ca = get_channel_allocation_order(ca); + + for (i = 0; i < 8; i++) { + if (ordered_ca < ARRAY_SIZE(channel_allocations) && + i < channel_allocations[ordered_ca].channels) + map[i] = from_cea_slot(ordered_ca, hdmi_channel_mapping[ca][i] & 0x0f); + else + map[i] = 0; + } +} + +void snd_hdac_setup_channel_mapping(struct hdac_chmap *chmap, + hda_nid_t pin_nid, bool non_pcm, int ca, + int channels, unsigned char *map, + bool chmap_set) +{ + if (!non_pcm && chmap_set) { + hdmi_manual_setup_channel_mapping(chmap, pin_nid, + channels, map, ca); + } else { + hdmi_std_setup_channel_mapping(chmap, pin_nid, non_pcm, ca); + hdmi_setup_fake_chmap(map, ca); + } + + hdmi_debug_channel_mapping(chmap, pin_nid); +} +EXPORT_SYMBOL_GPL(snd_hdac_setup_channel_mapping); + +int snd_hdac_get_active_channels(int ca) +{ + int ordered_ca = get_channel_allocation_order(ca); + + /* Add sanity check to pass klockwork check. + * This should never happen. + */ + if (ordered_ca >= ARRAY_SIZE(channel_allocations)) + ordered_ca = 0; + + return channel_allocations[ordered_ca].channels; +} +EXPORT_SYMBOL_GPL(snd_hdac_get_active_channels); + +struct hdac_cea_channel_speaker_allocation *snd_hdac_get_ch_alloc_from_ca(int ca) +{ + return &channel_allocations[get_channel_allocation_order(ca)]; +} +EXPORT_SYMBOL_GPL(snd_hdac_get_ch_alloc_from_ca); + +int snd_hdac_channel_allocation(struct hdac_device *hdac, int spk_alloc, + int channels, bool chmap_set, bool non_pcm, unsigned char *map) +{ + int ca; + + if (!non_pcm && chmap_set) + ca = hdmi_manual_channel_allocation(channels, map); + else + ca = hdmi_channel_allocation_spk_alloc_blk(hdac, + spk_alloc, channels); + + if (ca < 0) + ca = 0; + + return ca; +} +EXPORT_SYMBOL_GPL(snd_hdac_channel_allocation); + +/* + * ALSA API channel-map control callbacks + */ +static int hdmi_chmap_ctl_info(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_info *uinfo) +{ + struct snd_pcm_chmap *info = snd_kcontrol_chip(kcontrol); + struct hdac_chmap *chmap = info->private_data; + + uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER; + uinfo->count = chmap->channels_max; + uinfo->value.integer.min = 0; + uinfo->value.integer.max = SNDRV_CHMAP_LAST; + return 0; +} + +static int hdmi_chmap_cea_alloc_validate_get_type(struct hdac_chmap *chmap, + struct hdac_cea_channel_speaker_allocation *cap, int channels) +{ + /* If the speaker allocation matches the channel count, it is OK.*/ + if (cap->channels != channels) + return -1; + + /* all channels are remappable freely */ + return SNDRV_CTL_TLVT_CHMAP_VAR; +} + +static void hdmi_cea_alloc_to_tlv_chmap(struct hdac_chmap *hchmap, + struct hdac_cea_channel_speaker_allocation *cap, + unsigned int *chmap, int channels) +{ + int count = 0; + int c; + + for (c = 7; c >= 0; c--) { + int spk = cap->speakers[c]; + + if (!spk) + continue; + + chmap[count++] = snd_hdac_spk_to_chmap(spk); + } + + WARN_ON(count != channels); +} + +static int spk_mask_from_spk_alloc(int spk_alloc) +{ + int i; + int spk_mask = eld_speaker_allocation_bits[0]; + + for (i = 0; i < ARRAY_SIZE(eld_speaker_allocation_bits); i++) { + if (spk_alloc & (1 << i)) + spk_mask |= eld_speaker_allocation_bits[i]; + } + + return spk_mask; +} + +static int hdmi_chmap_ctl_tlv(struct snd_kcontrol *kcontrol, int op_flag, + unsigned int size, unsigned int __user *tlv) +{ + struct snd_pcm_chmap *info = snd_kcontrol_chip(kcontrol); + struct hdac_chmap *chmap = info->private_data; + int pcm_idx = kcontrol->private_value; + unsigned int __user *dst; + int chs, count = 0; + unsigned long max_chs; + int type; + int spk_alloc, spk_mask; + + if (size < 8) + return -ENOMEM; + if (put_user(SNDRV_CTL_TLVT_CONTAINER, tlv)) + return -EFAULT; + size -= 8; + dst = tlv + 2; + + spk_alloc = chmap->ops.get_spk_alloc(chmap->hdac, pcm_idx); + spk_mask = spk_mask_from_spk_alloc(spk_alloc); + + max_chs = hweight_long(spk_mask); + + for (chs = 2; chs <= max_chs; chs++) { + int i; + struct hdac_cea_channel_speaker_allocation *cap; + + cap = channel_allocations; + for (i = 0; i < ARRAY_SIZE(channel_allocations); i++, cap++) { + int chs_bytes = chs * 4; + unsigned int tlv_chmap[8]; + + if (cap->channels != chs) + continue; + + if (!(cap->spk_mask == (spk_mask & cap->spk_mask))) + continue; + + type = chmap->ops.chmap_cea_alloc_validate_get_type( + chmap, cap, chs); + if (type < 0) + return -ENODEV; + if (size < 8) + return -ENOMEM; + + if (put_user(type, dst) || + put_user(chs_bytes, dst + 1)) + return -EFAULT; + + dst += 2; + size -= 8; + count += 8; + + if (size < chs_bytes) + return -ENOMEM; + + size -= chs_bytes; + count += chs_bytes; + chmap->ops.cea_alloc_to_tlv_chmap(chmap, cap, + tlv_chmap, chs); + + if (copy_to_user(dst, tlv_chmap, chs_bytes)) + return -EFAULT; + dst += chs; + } + } + + if (put_user(count, tlv + 1)) + return -EFAULT; + + return 0; +} + +static int hdmi_chmap_ctl_get(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_pcm_chmap *info = snd_kcontrol_chip(kcontrol); + struct hdac_chmap *chmap = info->private_data; + int pcm_idx = kcontrol->private_value; + unsigned char pcm_chmap[8]; + int i; + + memset(pcm_chmap, 0, sizeof(pcm_chmap)); + chmap->ops.get_chmap(chmap->hdac, pcm_idx, pcm_chmap); + + for (i = 0; i < ARRAY_SIZE(pcm_chmap); i++) + ucontrol->value.integer.value[i] = pcm_chmap[i]; + + return 0; +} + +/* a simple sanity check for input values to chmap kcontrol */ +static int chmap_value_check(struct hdac_chmap *hchmap, + const struct snd_ctl_elem_value *ucontrol) +{ + int i; + + for (i = 0; i < hchmap->channels_max; i++) { + if (ucontrol->value.integer.value[i] < 0 || + ucontrol->value.integer.value[i] > SNDRV_CHMAP_LAST) + return -EINVAL; + } + return 0; +} + +static int hdmi_chmap_ctl_put(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_pcm_chmap *info = snd_kcontrol_chip(kcontrol); + struct hdac_chmap *hchmap = info->private_data; + int pcm_idx = kcontrol->private_value; + unsigned int ctl_idx; + struct snd_pcm_substream *substream; + unsigned char chmap[8], per_pin_chmap[8]; + int i, err, ca, prepared = 0; + + err = chmap_value_check(hchmap, ucontrol); + if (err < 0) + return err; + + /* No monitor is connected in dyn_pcm_assign. + * It's invalid to setup the chmap + */ + if (!hchmap->ops.is_pcm_attached(hchmap->hdac, pcm_idx)) + return 0; + + ctl_idx = snd_ctl_get_ioffidx(kcontrol, &ucontrol->id); + substream = snd_pcm_chmap_substream(info, ctl_idx); + if (!substream || !substream->runtime) + return 0; /* just for avoiding error from alsactl restore */ + switch (substream->runtime->state) { + case SNDRV_PCM_STATE_OPEN: + case SNDRV_PCM_STATE_SETUP: + break; + case SNDRV_PCM_STATE_PREPARED: + prepared = 1; + break; + default: + return -EBUSY; + } + memset(chmap, 0, sizeof(chmap)); + for (i = 0; i < ARRAY_SIZE(chmap); i++) + chmap[i] = ucontrol->value.integer.value[i]; + + hchmap->ops.get_chmap(hchmap->hdac, pcm_idx, per_pin_chmap); + if (!memcmp(chmap, per_pin_chmap, sizeof(chmap))) + return 0; + ca = hdmi_manual_channel_allocation(ARRAY_SIZE(chmap), chmap); + if (ca < 0) + return -EINVAL; + if (hchmap->ops.chmap_validate) { + err = hchmap->ops.chmap_validate(hchmap, ca, + ARRAY_SIZE(chmap), chmap); + if (err) + return err; + } + + hchmap->ops.set_chmap(hchmap->hdac, pcm_idx, chmap, prepared); + + return 0; +} + +static const struct hdac_chmap_ops chmap_ops = { + .chmap_cea_alloc_validate_get_type = hdmi_chmap_cea_alloc_validate_get_type, + .cea_alloc_to_tlv_chmap = hdmi_cea_alloc_to_tlv_chmap, + .pin_get_slot_channel = hdmi_pin_get_slot_channel, + .pin_set_slot_channel = hdmi_pin_set_slot_channel, + .set_channel_count = hdmi_set_channel_count, +}; + +void snd_hdac_register_chmap_ops(struct hdac_device *hdac, + struct hdac_chmap *chmap) +{ + chmap->ops = chmap_ops; + chmap->hdac = hdac; + init_channel_allocations(); +} +EXPORT_SYMBOL_GPL(snd_hdac_register_chmap_ops); + +int snd_hdac_add_chmap_ctls(struct snd_pcm *pcm, int pcm_idx, + struct hdac_chmap *hchmap) +{ + struct snd_pcm_chmap *chmap; + struct snd_kcontrol *kctl; + int err, i; + + err = snd_pcm_add_chmap_ctls(pcm, + SNDRV_PCM_STREAM_PLAYBACK, + NULL, 0, pcm_idx, &chmap); + if (err < 0) + return err; + /* override handlers */ + chmap->private_data = hchmap; + kctl = chmap->kctl; + for (i = 0; i < kctl->count; i++) + kctl->vd[i].access |= SNDRV_CTL_ELEM_ACCESS_WRITE; + kctl->info = hdmi_chmap_ctl_info; + kctl->get = hdmi_chmap_ctl_get; + kctl->put = hdmi_chmap_ctl_put; + kctl->tlv.c = hdmi_chmap_ctl_tlv; + + return 0; +} +EXPORT_SYMBOL_GPL(snd_hdac_add_chmap_ctls); diff --git a/sound/hda/core/i915.c b/sound/hda/core/i915.c new file mode 100644 index 0000000000000..e9425213320ea --- /dev/null +++ b/sound/hda/core/i915.c @@ -0,0 +1,208 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * hdac_i915.c - routines for sync between HD-A core and i915 display driver + */ + +#include +#include +#include +#include +#include +#include +#include +#include