forked from Qortal/Brooklyn
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
392 lines
11 KiB
392 lines
11 KiB
// SPDX-License-Identifier: GPL-2.0 |
|
// |
|
// Copyright(c) 2021-2022 Intel Corporation. All rights reserved. |
|
// |
|
// Author: Cezary Rojewski <[email protected]> |
|
// |
|
|
|
#include <linux/module.h> |
|
#include <linux/pm_runtime.h> |
|
#include <sound/soc.h> |
|
#include <sound/hdaudio_ext.h> |
|
#include <sound/hda_i915.h> |
|
#include <sound/hda_codec.h> |
|
#include "hda.h" |
|
|
|
static int hda_codec_create_dais(struct hda_codec *codec, int pcm_count, |
|
struct snd_soc_dai_driver **drivers) |
|
{ |
|
struct device *dev = &codec->core.dev; |
|
struct snd_soc_dai_driver *drvs; |
|
struct hda_pcm *pcm; |
|
int i; |
|
|
|
drvs = devm_kcalloc(dev, pcm_count, sizeof(*drvs), GFP_KERNEL); |
|
if (!drvs) |
|
return -ENOMEM; |
|
|
|
pcm = list_first_entry(&codec->pcm_list_head, struct hda_pcm, list); |
|
|
|
for (i = 0; i < pcm_count; i++, pcm = list_next_entry(pcm, list)) { |
|
struct snd_soc_pcm_stream *stream; |
|
int dir; |
|
|
|
dev_info(dev, "creating for %s %d\n", pcm->name, i); |
|
drvs[i].id = i; |
|
drvs[i].name = pcm->name; |
|
drvs[i].ops = &snd_soc_hda_codec_dai_ops; |
|
|
|
dir = SNDRV_PCM_STREAM_PLAYBACK; |
|
stream = &drvs[i].playback; |
|
if (!pcm->stream[dir].substreams) { |
|
dev_info(dev, "skipping playback dai for %s\n", pcm->name); |
|
goto capture_dais; |
|
} |
|
|
|
stream->stream_name = |
|
devm_kasprintf(dev, GFP_KERNEL, "%s %s", pcm->name, |
|
snd_pcm_direction_name(dir)); |
|
if (!stream->stream_name) |
|
return -ENOMEM; |
|
stream->channels_min = pcm->stream[dir].channels_min; |
|
stream->channels_max = pcm->stream[dir].channels_max; |
|
stream->rates = pcm->stream[dir].rates; |
|
stream->formats = pcm->stream[dir].formats; |
|
stream->sig_bits = pcm->stream[dir].maxbps; |
|
|
|
capture_dais: |
|
dir = SNDRV_PCM_STREAM_CAPTURE; |
|
stream = &drvs[i].capture; |
|
if (!pcm->stream[dir].substreams) { |
|
dev_info(dev, "skipping capture dai for %s\n", pcm->name); |
|
continue; |
|
} |
|
|
|
stream->stream_name = |
|
devm_kasprintf(dev, GFP_KERNEL, "%s %s", pcm->name, |
|
snd_pcm_direction_name(dir)); |
|
if (!stream->stream_name) |
|
return -ENOMEM; |
|
stream->channels_min = pcm->stream[dir].channels_min; |
|
stream->channels_max = pcm->stream[dir].channels_max; |
|
stream->rates = pcm->stream[dir].rates; |
|
stream->formats = pcm->stream[dir].formats; |
|
stream->sig_bits = pcm->stream[dir].maxbps; |
|
} |
|
|
|
*drivers = drvs; |
|
return 0; |
|
} |
|
|
|
static int hda_codec_register_dais(struct hda_codec *codec, struct snd_soc_component *component) |
|
{ |
|
struct snd_soc_dai_driver *drvs = NULL; |
|
struct snd_soc_dapm_context *dapm; |
|
struct hda_pcm *pcm; |
|
int ret, pcm_count = 0; |
|
|
|
if (list_empty(&codec->pcm_list_head)) |
|
return -EINVAL; |
|
list_for_each_entry(pcm, &codec->pcm_list_head, list) |
|
pcm_count++; |
|
|
|
ret = hda_codec_create_dais(codec, pcm_count, &drvs); |
|
if (ret < 0) |
|
return ret; |
|
|
|
dapm = snd_soc_component_get_dapm(component); |
|
|
|
list_for_each_entry(pcm, &codec->pcm_list_head, list) { |
|
struct snd_soc_dai *dai; |
|
|
|
dai = snd_soc_register_dai(component, drvs, false); |
|
if (!dai) { |
|
dev_err(component->dev, "register dai for %s failed\n", pcm->name); |
|
return -EINVAL; |
|
} |
|
|
|
ret = snd_soc_dapm_new_dai_widgets(dapm, dai); |
|
if (ret < 0) { |
|
dev_err(component->dev, "create widgets failed: %d\n", ret); |
|
snd_soc_unregister_dai(dai); |
|
return ret; |
|
} |
|
|
|
snd_soc_dai_init_dma_data(dai, &pcm->stream[0], &pcm->stream[1]); |
|
drvs++; |
|
} |
|
|
|
return 0; |
|
} |
|
|
|
static void hda_codec_unregister_dais(struct hda_codec *codec, |
|
struct snd_soc_component *component) |
|
{ |
|
struct snd_soc_dai *dai, *save; |
|
struct hda_pcm *pcm; |
|
|
|
for_each_component_dais_safe(component, dai, save) { |
|
list_for_each_entry(pcm, &codec->pcm_list_head, list) { |
|
if (strcmp(dai->driver->name, pcm->name)) |
|
continue; |
|
|
|
if (dai->playback_widget) |
|
snd_soc_dapm_free_widget(dai->playback_widget); |
|
if (dai->capture_widget) |
|
snd_soc_dapm_free_widget(dai->capture_widget); |
|
snd_soc_unregister_dai(dai); |
|
break; |
|
} |
|
} |
|
} |
|
|
|
int hda_codec_probe_complete(struct hda_codec *codec) |
|
{ |
|
struct hdac_device *hdev = &codec->core; |
|
struct hdac_bus *bus = hdev->bus; |
|
int ret; |
|
|
|
ret = snd_hda_codec_build_controls(codec); |
|
if (ret < 0) { |
|
dev_err(&hdev->dev, "unable to create controls %d\n", ret); |
|
goto out; |
|
} |
|
|
|
/* Bus suspended codecs as it does not manage their pm */ |
|
pm_runtime_set_active(&hdev->dev); |
|
/* rpm was forbidden in snd_hda_codec_device_new() */ |
|
snd_hda_codec_set_power_save(codec, 2000); |
|
snd_hda_codec_register(codec); |
|
out: |
|
/* Complement pm_runtime_get_sync(bus) in probe */ |
|
pm_runtime_mark_last_busy(bus->dev); |
|
pm_runtime_put_autosuspend(bus->dev); |
|
|
|
return ret; |
|
} |
|
EXPORT_SYMBOL_GPL(hda_codec_probe_complete); |
|
|
|
/* Expects codec with usage_count=1 and status=suspended */ |
|
static int hda_codec_probe(struct snd_soc_component *component) |
|
{ |
|
struct hda_codec *codec = dev_to_hda_codec(component->dev); |
|
struct hdac_device *hdev = &codec->core; |
|
struct hdac_bus *bus = hdev->bus; |
|
struct hdac_ext_link *hlink; |
|
hda_codec_patch_t patch; |
|
int ret; |
|
|
|
#ifdef CONFIG_PM |
|
WARN_ON(atomic_read(&hdev->dev.power.usage_count) != 1 || |
|
!pm_runtime_status_suspended(&hdev->dev)); |
|
#endif |
|
|
|
hlink = snd_hdac_ext_bus_link_at(bus, hdev->addr); |
|
if (!hlink) { |
|
dev_err(&hdev->dev, "hdac link not found\n"); |
|
return -EIO; |
|
} |
|
|
|
pm_runtime_get_sync(bus->dev); |
|
if (hda_codec_is_display(codec)) |
|
snd_hdac_display_power(bus, hdev->addr, true); |
|
snd_hdac_ext_bus_link_get(bus, hlink); |
|
|
|
ret = snd_hda_codec_device_new(codec->bus, component->card->snd_card, hdev->addr, codec, |
|
false); |
|
if (ret < 0) { |
|
dev_err(&hdev->dev, "create hda codec failed: %d\n", ret); |
|
goto device_new_err; |
|
} |
|
|
|
ret = snd_hda_codec_set_name(codec, codec->preset->name); |
|
if (ret < 0) { |
|
dev_err(&hdev->dev, "name failed %s\n", codec->preset->name); |
|
goto err; |
|
} |
|
|
|
ret = snd_hdac_regmap_init(&codec->core); |
|
if (ret < 0) { |
|
dev_err(&hdev->dev, "regmap init failed\n"); |
|
goto err; |
|
} |
|
|
|
patch = (hda_codec_patch_t)codec->preset->driver_data; |
|
if (!patch) { |
|
dev_err(&hdev->dev, "no patch specified?\n"); |
|
ret = -EINVAL; |
|
goto err; |
|
} |
|
|
|
ret = patch(codec); |
|
if (ret < 0) { |
|
dev_err(&hdev->dev, "patch failed %d\n", ret); |
|
goto err; |
|
} |
|
|
|
ret = snd_hda_codec_parse_pcms(codec); |
|
if (ret < 0) { |
|
dev_err(&hdev->dev, "unable to map pcms to dai %d\n", ret); |
|
goto parse_pcms_err; |
|
} |
|
|
|
ret = hda_codec_register_dais(codec, component); |
|
if (ret < 0) { |
|
dev_err(&hdev->dev, "update dais failed: %d\n", ret); |
|
goto parse_pcms_err; |
|
} |
|
|
|
if (!hda_codec_is_display(codec)) { |
|
ret = hda_codec_probe_complete(codec); |
|
if (ret < 0) |
|
goto complete_err; |
|
} |
|
|
|
codec->core.lazy_cache = true; |
|
|
|
return 0; |
|
|
|
complete_err: |
|
hda_codec_unregister_dais(codec, component); |
|
parse_pcms_err: |
|
if (codec->patch_ops.free) |
|
codec->patch_ops.free(codec); |
|
err: |
|
snd_hda_codec_cleanup_for_unbind(codec); |
|
device_new_err: |
|
if (hda_codec_is_display(codec)) |
|
snd_hdac_display_power(bus, hdev->addr, false); |
|
|
|
snd_hdac_ext_bus_link_put(bus, hlink); |
|
|
|
pm_runtime_mark_last_busy(bus->dev); |
|
pm_runtime_put_autosuspend(bus->dev); |
|
return ret; |
|
} |
|
|
|
/* Leaves codec with usage_count=1 and status=suspended */ |
|
static void hda_codec_remove(struct snd_soc_component *component) |
|
{ |
|
struct hda_codec *codec = dev_to_hda_codec(component->dev); |
|
struct hdac_device *hdev = &codec->core; |
|
struct hdac_bus *bus = hdev->bus; |
|
struct hdac_ext_link *hlink; |
|
bool was_registered = codec->core.registered; |
|
|
|
/* Don't allow any more runtime suspends */ |
|
pm_runtime_forbid(&hdev->dev); |
|
|
|
hda_codec_unregister_dais(codec, component); |
|
|
|
if (codec->patch_ops.free) |
|
codec->patch_ops.free(codec); |
|
|
|
snd_hda_codec_cleanup_for_unbind(codec); |
|
pm_runtime_put_noidle(&hdev->dev); |
|
/* snd_hdac_device_exit() is only called on bus remove */ |
|
pm_runtime_set_suspended(&hdev->dev); |
|
|
|
if (hda_codec_is_display(codec)) |
|
snd_hdac_display_power(bus, hdev->addr, false); |
|
|
|
hlink = snd_hdac_ext_bus_link_at(bus, hdev->addr); |
|
if (hlink) |
|
snd_hdac_ext_bus_link_put(bus, hlink); |
|
/* |
|
* HDMI card's hda_codec_probe_complete() (see late_probe()) may |
|
* not be called due to early error, leaving bus uc unbalanced |
|
*/ |
|
if (!was_registered) { |
|
pm_runtime_mark_last_busy(bus->dev); |
|
pm_runtime_put_autosuspend(bus->dev); |
|
} |
|
|
|
#ifdef CONFIG_PM |
|
WARN_ON(atomic_read(&hdev->dev.power.usage_count) != 1 || |
|
!pm_runtime_status_suspended(&hdev->dev)); |
|
#endif |
|
} |
|
|
|
static const struct snd_soc_dapm_route hda_dapm_routes[] = { |
|
{"AIF1TX", NULL, "Codec Input Pin1"}, |
|
{"AIF2TX", NULL, "Codec Input Pin2"}, |
|
{"AIF3TX", NULL, "Codec Input Pin3"}, |
|
|
|
{"Codec Output Pin1", NULL, "AIF1RX"}, |
|
{"Codec Output Pin2", NULL, "AIF2RX"}, |
|
{"Codec Output Pin3", NULL, "AIF3RX"}, |
|
}; |
|
|
|
static const struct snd_soc_dapm_widget hda_dapm_widgets[] = { |
|
/* Audio Interface */ |
|
SND_SOC_DAPM_AIF_IN("AIF1RX", "Analog Codec Playback", 0, SND_SOC_NOPM, 0, 0), |
|
SND_SOC_DAPM_AIF_IN("AIF2RX", "Digital Codec Playback", 0, SND_SOC_NOPM, 0, 0), |
|
SND_SOC_DAPM_AIF_IN("AIF3RX", "Alt Analog Codec Playback", 0, SND_SOC_NOPM, 0, 0), |
|
SND_SOC_DAPM_AIF_OUT("AIF1TX", "Analog Codec Capture", 0, SND_SOC_NOPM, 0, 0), |
|
SND_SOC_DAPM_AIF_OUT("AIF2TX", "Digital Codec Capture", 0, SND_SOC_NOPM, 0, 0), |
|
SND_SOC_DAPM_AIF_OUT("AIF3TX", "Alt Analog Codec Capture", 0, SND_SOC_NOPM, 0, 0), |
|
|
|
/* Input Pins */ |
|
SND_SOC_DAPM_INPUT("Codec Input Pin1"), |
|
SND_SOC_DAPM_INPUT("Codec Input Pin2"), |
|
SND_SOC_DAPM_INPUT("Codec Input Pin3"), |
|
|
|
/* Output Pins */ |
|
SND_SOC_DAPM_OUTPUT("Codec Output Pin1"), |
|
SND_SOC_DAPM_OUTPUT("Codec Output Pin2"), |
|
SND_SOC_DAPM_OUTPUT("Codec Output Pin3"), |
|
}; |
|
|
|
static struct snd_soc_dai_driver card_binder_dai = { |
|
.id = -1, |
|
.name = "codec-probing-DAI", |
|
}; |
|
|
|
static int hda_hdev_attach(struct hdac_device *hdev) |
|
{ |
|
struct hda_codec *codec = dev_to_hda_codec(&hdev->dev); |
|
struct snd_soc_component_driver *comp_drv; |
|
|
|
comp_drv = devm_kzalloc(&hdev->dev, sizeof(*comp_drv), GFP_KERNEL); |
|
if (!comp_drv) |
|
return -ENOMEM; |
|
|
|
/* |
|
* It's save to rely on dev_name() rather than a copy as component |
|
* driver's lifetime is directly tied to hda codec one |
|
*/ |
|
comp_drv->name = dev_name(&hdev->dev); |
|
comp_drv->probe = hda_codec_probe; |
|
comp_drv->remove = hda_codec_remove; |
|
comp_drv->idle_bias_on = false; |
|
if (!hda_codec_is_display(codec)) { |
|
comp_drv->dapm_widgets = hda_dapm_widgets; |
|
comp_drv->num_dapm_widgets = ARRAY_SIZE(hda_dapm_widgets); |
|
comp_drv->dapm_routes = hda_dapm_routes; |
|
comp_drv->num_dapm_routes = ARRAY_SIZE(hda_dapm_routes); |
|
} |
|
|
|
return snd_soc_register_component(&hdev->dev, comp_drv, &card_binder_dai, 1); |
|
} |
|
|
|
static int hda_hdev_detach(struct hdac_device *hdev) |
|
{ |
|
struct hda_codec *codec = dev_to_hda_codec(&hdev->dev); |
|
|
|
if (codec->core.registered) |
|
cancel_delayed_work_sync(&codec->jackpoll_work); |
|
|
|
snd_soc_unregister_component(&hdev->dev); |
|
|
|
return 0; |
|
} |
|
|
|
const struct hdac_ext_bus_ops soc_hda_ext_bus_ops = { |
|
.hdev_attach = hda_hdev_attach, |
|
.hdev_detach = hda_hdev_detach, |
|
}; |
|
EXPORT_SYMBOL_GPL(soc_hda_ext_bus_ops); |
|
|
|
MODULE_DESCRIPTION("HD-Audio codec driver"); |
|
MODULE_AUTHOR("Cezary Rojewski <[email protected]>"); |
|
MODULE_LICENSE("GPL");
|
|
|