ad858508fd
This patchset tries to resolve the diversity in the audio LED control among the ALSA drivers. A new control layer registration is introduced which allows to run additional operations on top of the elementary ALSA sound controls. A new control access group (three bits in the access flags) was introduced to carry the LED group information for the sound controls. The low-level sound drivers can just mark those controls using this access group. This information is not exported to the user space, but user space can manage the LED sound control associations through sysfs (last patch) per Mark's request. It makes things fully configurable in the kernel and user space (UCM). The actual state ('route') evaluation is really easy (the minimal value check for all channels / controls / cards). If there's more complicated logic for a given hardware, the card driver may eventually export a new read-only sound control for the LED group and do the logic itself. The new LED trigger control code is completely separated and possibly optional (there's no symbol dependency). The full code separation allows eventually to move this LED trigger control to the user space in future. Actually it replaces the already present functionality in the kernel space (HDA drivers) and allows a quick adoption for the recent hardware (ASoC codecs including SoundWire). snd_ctl_led 24576 0 The sound driver implementation is really easy: 1) call snd_ctl_led_request() when control LED layer should be automatically activated / it calls module_request("snd-ctl-led") on demand / 2) mark all related kcontrols with SNDRV_CTL_ELEM_ACCESS_SPK_LED or SNDRV_CTL_ELEM_ACCESS_MIC_LED Link: https://lore.kernel.org/r/20210317172945.842280-1-perex@perex.cz Signed-off-by: Takashi Iwai <tiwai@suse.de> -----BEGIN PGP SIGNATURE----- iQJCBAABCAAsFiEEIXTw5fNLNI7mMiVaLtJE4w1nLE8FAmBjRuQOHHRpd2FpQHN1 c2UuZGUACgkQLtJE4w1nLE9ZQw/6Ao2X1io4TVnyO/gO8HtwmnZ6TWcrLUlySaep H6Suf0RHsOQO9VOaMcUarA3Wnz1vZ44qJ/fkdLTslnIPGSRJDUx15bbb+n2N6pQJ gS7Umxy6n73rQyEoDjd3ZorvDGjFSVFpjM+RYjk/Ohq+yziz7nQ/SZuHPPeqm1GU C5d9SxyXGdqlJJ6yFCHzbtjSmIey+l1TZ+j3rSSww/CzDKxB2l5J6JZAMUjVdL9b J80Mcw0XLdG9iTtEnkUt3TwvLMcMl95UPeQHIkVQlwsRRb3BtHNIwJLPQ/n+Cou7 Hre3y2miUYHrNICEzMdMlpDzQBqu5wvpXgbgIV0CwAwCXPZBVWE1hVJ9gG0l+r1G fy1a75OmEin4V9g8w+wNTWDEgjwAOkWhA67WVjpbvHtd6kEbISzt4JHFksG1rjU2 vXOIj2VBmQN6zHtxfcZqY8Ge4A7XGo7tAM/3NsUxin+2y7ZXI6sDvv+0esYmqrYr 9as/tD84L5LTrbUYewaUgEdauQXluQI1egRi7pSeg7hZyLeYYkmszk54Ra3zdlmM m7Hr6u+Y/G7yeFdn/WdeG3VzdmxhC2ZFfk3gq0vneBS3WrATbf6nAORp2bwzGSz4 pUsVLSv+vhpZdSF+IxpUuMnsLkkbUCvFivXLjQ6irSWvp7uts1HWdRowdg7Pe2lC FVbFRuA= =1uM7 -----END PGP SIGNATURE----- gpgsig -----BEGIN PGP SIGNATURE----- iQEzBAABCgAdFiEEreZoqmdXGLWf4p/qJNaLcl1Uh9AFAmBkoFEACgkQJNaLcl1U h9BqTgf5AUfR5NbQgJIUMtdMsuz+bthKGuPnpYhYzNUH0io9i3Sjpt0Mbw21dLp2 nXW+70BH0tJZBGR4DNGpmcpBLH5WPdOR9WIv4vCYy9Pr0uCGGKF/JkGeZAg7/llF C3+yDfTnF0AvR+74xbf/LTjBPGja/08PED1ZC00NAkLhE3C0Pa/VQ0NaEtPUULWE ZOWboF7hfQUeGzGPTqNp4Cy0/Pzokk6Fvl4kgfrxjNWO7ojIv9mCAvmYbusLW7NU 3Ph5VGsIuyMeorEoAVOFaBY2nrYG/mFZyzTNWJMgvF1MdeigMPNfbbffrUK0sn6k e81zgWbwxboEujQ91/EZU7glIqtb6A== =rr4V -----END PGP SIGNATURE----- Merge tag 'mute-led-rework' of https://git.kernel.org/pub/scm/linux/kernel/git/tiwai/sound into asoc-5.13 ALSA: control - add generic LED API This patchset tries to resolve the diversity in the audio LED control among the ALSA drivers. A new control layer registration is introduced which allows to run additional operations on top of the elementary ALSA sound controls. A new control access group (three bits in the access flags) was introduced to carry the LED group information for the sound controls. The low-level sound drivers can just mark those controls using this access group. This information is not exported to the user space, but user space can manage the LED sound control associations through sysfs (last patch) per Mark's request. It makes things fully configurable in the kernel and user space (UCM). The actual state ('route') evaluation is really easy (the minimal value check for all channels / controls / cards). If there's more complicated logic for a given hardware, the card driver may eventually export a new read-only sound control for the LED group and do the logic itself. The new LED trigger control code is completely separated and possibly optional (there's no symbol dependency). The full code separation allows eventually to move this LED trigger control to the user space in future. Actually it replaces the already present functionality in the kernel space (HDA drivers) and allows a quick adoption for the recent hardware (ASoC codecs including SoundWire). snd_ctl_led 24576 0 The sound driver implementation is really easy: 1) call snd_ctl_led_request() when control LED layer should be automatically activated / it calls module_request("snd-ctl-led") on demand / 2) mark all related kcontrols with SNDRV_CTL_ELEM_ACCESS_SPK_LED or SNDRV_CTL_ELEM_ACCESS_MIC_LED Link: https://lore.kernel.org/r/20210317172945.842280-1-perex@perex.cz Signed-off-by: Takashi Iwai <tiwai@suse.de>
924 lines
25 KiB
C
924 lines
25 KiB
C
// SPDX-License-Identifier: GPL-2.0-only
|
|
/*
|
|
* Copyright (c) 2010-2011,2013-2015 The Linux Foundation. All rights reserved.
|
|
*
|
|
* lpass-cpu.c -- ALSA SoC CPU DAI driver for QTi LPASS
|
|
*/
|
|
|
|
#include <linux/clk.h>
|
|
#include <linux/kernel.h>
|
|
#include <linux/module.h>
|
|
#include <linux/of.h>
|
|
#include <linux/of_device.h>
|
|
#include <linux/platform_device.h>
|
|
#include <sound/pcm.h>
|
|
#include <sound/pcm_params.h>
|
|
#include <linux/regmap.h>
|
|
#include <sound/soc.h>
|
|
#include <sound/soc-dai.h>
|
|
#include "lpass-lpaif-reg.h"
|
|
#include "lpass.h"
|
|
|
|
#define LPASS_CPU_MAX_MI2S_LINES 4
|
|
#define LPASS_CPU_I2S_SD0_MASK BIT(0)
|
|
#define LPASS_CPU_I2S_SD1_MASK BIT(1)
|
|
#define LPASS_CPU_I2S_SD2_MASK BIT(2)
|
|
#define LPASS_CPU_I2S_SD3_MASK BIT(3)
|
|
#define LPASS_CPU_I2S_SD0_1_MASK GENMASK(1, 0)
|
|
#define LPASS_CPU_I2S_SD2_3_MASK GENMASK(3, 2)
|
|
#define LPASS_CPU_I2S_SD0_1_2_MASK GENMASK(2, 0)
|
|
#define LPASS_CPU_I2S_SD0_1_2_3_MASK GENMASK(3, 0)
|
|
|
|
static int lpass_cpu_init_i2sctl_bitfields(struct device *dev,
|
|
struct lpaif_i2sctl *i2sctl, struct regmap *map)
|
|
{
|
|
struct lpass_data *drvdata = dev_get_drvdata(dev);
|
|
struct lpass_variant *v = drvdata->variant;
|
|
|
|
i2sctl->loopback = devm_regmap_field_alloc(dev, map, v->loopback);
|
|
i2sctl->spken = devm_regmap_field_alloc(dev, map, v->spken);
|
|
i2sctl->spkmode = devm_regmap_field_alloc(dev, map, v->spkmode);
|
|
i2sctl->spkmono = devm_regmap_field_alloc(dev, map, v->spkmono);
|
|
i2sctl->micen = devm_regmap_field_alloc(dev, map, v->micen);
|
|
i2sctl->micmode = devm_regmap_field_alloc(dev, map, v->micmode);
|
|
i2sctl->micmono = devm_regmap_field_alloc(dev, map, v->micmono);
|
|
i2sctl->wssrc = devm_regmap_field_alloc(dev, map, v->wssrc);
|
|
i2sctl->bitwidth = devm_regmap_field_alloc(dev, map, v->bitwidth);
|
|
|
|
if (IS_ERR(i2sctl->loopback) || IS_ERR(i2sctl->spken) ||
|
|
IS_ERR(i2sctl->spkmode) || IS_ERR(i2sctl->spkmono) ||
|
|
IS_ERR(i2sctl->micen) || IS_ERR(i2sctl->micmode) ||
|
|
IS_ERR(i2sctl->micmono) || IS_ERR(i2sctl->wssrc) ||
|
|
IS_ERR(i2sctl->bitwidth))
|
|
return -EINVAL;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int lpass_cpu_daiops_set_sysclk(struct snd_soc_dai *dai, int clk_id,
|
|
unsigned int freq, int dir)
|
|
{
|
|
struct lpass_data *drvdata = snd_soc_dai_get_drvdata(dai);
|
|
int ret;
|
|
|
|
ret = clk_set_rate(drvdata->mi2s_osr_clk[dai->driver->id], freq);
|
|
if (ret)
|
|
dev_err(dai->dev, "error setting mi2s osrclk to %u: %d\n",
|
|
freq, ret);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int lpass_cpu_daiops_startup(struct snd_pcm_substream *substream,
|
|
struct snd_soc_dai *dai)
|
|
{
|
|
struct lpass_data *drvdata = snd_soc_dai_get_drvdata(dai);
|
|
int ret;
|
|
|
|
ret = clk_prepare_enable(drvdata->mi2s_osr_clk[dai->driver->id]);
|
|
if (ret) {
|
|
dev_err(dai->dev, "error in enabling mi2s osr clk: %d\n", ret);
|
|
return ret;
|
|
}
|
|
ret = clk_prepare(drvdata->mi2s_bit_clk[dai->driver->id]);
|
|
if (ret) {
|
|
dev_err(dai->dev, "error in enabling mi2s bit clk: %d\n", ret);
|
|
clk_disable_unprepare(drvdata->mi2s_osr_clk[dai->driver->id]);
|
|
return ret;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static void lpass_cpu_daiops_shutdown(struct snd_pcm_substream *substream,
|
|
struct snd_soc_dai *dai)
|
|
{
|
|
struct lpass_data *drvdata = snd_soc_dai_get_drvdata(dai);
|
|
|
|
clk_disable_unprepare(drvdata->mi2s_osr_clk[dai->driver->id]);
|
|
clk_unprepare(drvdata->mi2s_bit_clk[dai->driver->id]);
|
|
}
|
|
|
|
static int lpass_cpu_daiops_hw_params(struct snd_pcm_substream *substream,
|
|
struct snd_pcm_hw_params *params, struct snd_soc_dai *dai)
|
|
{
|
|
struct lpass_data *drvdata = snd_soc_dai_get_drvdata(dai);
|
|
struct lpaif_i2sctl *i2sctl = drvdata->i2sctl;
|
|
unsigned int id = dai->driver->id;
|
|
snd_pcm_format_t format = params_format(params);
|
|
unsigned int channels = params_channels(params);
|
|
unsigned int rate = params_rate(params);
|
|
unsigned int mode;
|
|
unsigned int regval;
|
|
int bitwidth, ret;
|
|
|
|
bitwidth = snd_pcm_format_width(format);
|
|
if (bitwidth < 0) {
|
|
dev_err(dai->dev, "invalid bit width given: %d\n", bitwidth);
|
|
return bitwidth;
|
|
}
|
|
|
|
ret = regmap_fields_write(i2sctl->loopback, id,
|
|
LPAIF_I2SCTL_LOOPBACK_DISABLE);
|
|
if (ret) {
|
|
dev_err(dai->dev, "error updating loopback field: %d\n", ret);
|
|
return ret;
|
|
}
|
|
|
|
ret = regmap_fields_write(i2sctl->wssrc, id,
|
|
LPAIF_I2SCTL_WSSRC_INTERNAL);
|
|
if (ret) {
|
|
dev_err(dai->dev, "error updating wssrc field: %d\n", ret);
|
|
return ret;
|
|
}
|
|
|
|
switch (bitwidth) {
|
|
case 16:
|
|
regval = LPAIF_I2SCTL_BITWIDTH_16;
|
|
break;
|
|
case 24:
|
|
regval = LPAIF_I2SCTL_BITWIDTH_24;
|
|
break;
|
|
case 32:
|
|
regval = LPAIF_I2SCTL_BITWIDTH_32;
|
|
break;
|
|
default:
|
|
dev_err(dai->dev, "invalid bitwidth given: %d\n", bitwidth);
|
|
return -EINVAL;
|
|
}
|
|
|
|
ret = regmap_fields_write(i2sctl->bitwidth, id, regval);
|
|
if (ret) {
|
|
dev_err(dai->dev, "error updating bitwidth field: %d\n", ret);
|
|
return ret;
|
|
}
|
|
|
|
if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK)
|
|
mode = drvdata->mi2s_playback_sd_mode[id];
|
|
else
|
|
mode = drvdata->mi2s_capture_sd_mode[id];
|
|
|
|
if (!mode) {
|
|
dev_err(dai->dev, "no line is assigned\n");
|
|
return -EINVAL;
|
|
}
|
|
|
|
switch (channels) {
|
|
case 1:
|
|
case 2:
|
|
switch (mode) {
|
|
case LPAIF_I2SCTL_MODE_QUAD01:
|
|
case LPAIF_I2SCTL_MODE_6CH:
|
|
case LPAIF_I2SCTL_MODE_8CH:
|
|
mode = LPAIF_I2SCTL_MODE_SD0;
|
|
break;
|
|
case LPAIF_I2SCTL_MODE_QUAD23:
|
|
mode = LPAIF_I2SCTL_MODE_SD2;
|
|
break;
|
|
}
|
|
|
|
break;
|
|
case 4:
|
|
if (mode < LPAIF_I2SCTL_MODE_QUAD01) {
|
|
dev_err(dai->dev, "cannot configure 4 channels with mode %d\n",
|
|
mode);
|
|
return -EINVAL;
|
|
}
|
|
|
|
switch (mode) {
|
|
case LPAIF_I2SCTL_MODE_6CH:
|
|
case LPAIF_I2SCTL_MODE_8CH:
|
|
mode = LPAIF_I2SCTL_MODE_QUAD01;
|
|
break;
|
|
}
|
|
break;
|
|
case 6:
|
|
if (mode < LPAIF_I2SCTL_MODE_6CH) {
|
|
dev_err(dai->dev, "cannot configure 6 channels with mode %d\n",
|
|
mode);
|
|
return -EINVAL;
|
|
}
|
|
|
|
switch (mode) {
|
|
case LPAIF_I2SCTL_MODE_8CH:
|
|
mode = LPAIF_I2SCTL_MODE_6CH;
|
|
break;
|
|
}
|
|
break;
|
|
case 8:
|
|
if (mode < LPAIF_I2SCTL_MODE_8CH) {
|
|
dev_err(dai->dev, "cannot configure 8 channels with mode %d\n",
|
|
mode);
|
|
return -EINVAL;
|
|
}
|
|
break;
|
|
default:
|
|
dev_err(dai->dev, "invalid channels given: %u\n", channels);
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) {
|
|
ret = regmap_fields_write(i2sctl->spkmode, id,
|
|
LPAIF_I2SCTL_SPKMODE(mode));
|
|
if (ret) {
|
|
dev_err(dai->dev, "error writing to i2sctl spkr mode: %d\n",
|
|
ret);
|
|
return ret;
|
|
}
|
|
if (channels >= 2)
|
|
ret = regmap_fields_write(i2sctl->spkmono, id,
|
|
LPAIF_I2SCTL_SPKMONO_STEREO);
|
|
else
|
|
ret = regmap_fields_write(i2sctl->spkmono, id,
|
|
LPAIF_I2SCTL_SPKMONO_MONO);
|
|
} else {
|
|
ret = regmap_fields_write(i2sctl->micmode, id,
|
|
LPAIF_I2SCTL_MICMODE(mode));
|
|
if (ret) {
|
|
dev_err(dai->dev, "error writing to i2sctl mic mode: %d\n",
|
|
ret);
|
|
return ret;
|
|
}
|
|
if (channels >= 2)
|
|
ret = regmap_fields_write(i2sctl->micmono, id,
|
|
LPAIF_I2SCTL_MICMONO_STEREO);
|
|
else
|
|
ret = regmap_fields_write(i2sctl->micmono, id,
|
|
LPAIF_I2SCTL_MICMONO_MONO);
|
|
}
|
|
|
|
if (ret) {
|
|
dev_err(dai->dev, "error writing to i2sctl channels mode: %d\n",
|
|
ret);
|
|
return ret;
|
|
}
|
|
|
|
ret = clk_set_rate(drvdata->mi2s_bit_clk[id],
|
|
rate * bitwidth * 2);
|
|
if (ret) {
|
|
dev_err(dai->dev, "error setting mi2s bitclk to %u: %d\n",
|
|
rate * bitwidth * 2, ret);
|
|
return ret;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int lpass_cpu_daiops_trigger(struct snd_pcm_substream *substream,
|
|
int cmd, struct snd_soc_dai *dai)
|
|
{
|
|
struct lpass_data *drvdata = snd_soc_dai_get_drvdata(dai);
|
|
struct lpaif_i2sctl *i2sctl = drvdata->i2sctl;
|
|
unsigned int id = dai->driver->id;
|
|
int ret = -EINVAL;
|
|
|
|
switch (cmd) {
|
|
case SNDRV_PCM_TRIGGER_START:
|
|
case SNDRV_PCM_TRIGGER_RESUME:
|
|
case SNDRV_PCM_TRIGGER_PAUSE_RELEASE:
|
|
if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) {
|
|
ret = regmap_fields_write(i2sctl->spken, id,
|
|
LPAIF_I2SCTL_SPKEN_ENABLE);
|
|
} else {
|
|
ret = regmap_fields_write(i2sctl->micen, id,
|
|
LPAIF_I2SCTL_MICEN_ENABLE);
|
|
}
|
|
if (ret)
|
|
dev_err(dai->dev, "error writing to i2sctl reg: %d\n",
|
|
ret);
|
|
|
|
ret = clk_enable(drvdata->mi2s_bit_clk[id]);
|
|
if (ret) {
|
|
dev_err(dai->dev, "error in enabling mi2s bit clk: %d\n", ret);
|
|
clk_disable(drvdata->mi2s_osr_clk[id]);
|
|
return ret;
|
|
}
|
|
break;
|
|
case SNDRV_PCM_TRIGGER_STOP:
|
|
case SNDRV_PCM_TRIGGER_SUSPEND:
|
|
case SNDRV_PCM_TRIGGER_PAUSE_PUSH:
|
|
if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) {
|
|
ret = regmap_fields_write(i2sctl->spken, id,
|
|
LPAIF_I2SCTL_SPKEN_DISABLE);
|
|
} else {
|
|
ret = regmap_fields_write(i2sctl->micen, id,
|
|
LPAIF_I2SCTL_MICEN_DISABLE);
|
|
}
|
|
if (ret)
|
|
dev_err(dai->dev, "error writing to i2sctl reg: %d\n",
|
|
ret);
|
|
|
|
clk_disable(drvdata->mi2s_bit_clk[dai->driver->id]);
|
|
|
|
break;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
const struct snd_soc_dai_ops asoc_qcom_lpass_cpu_dai_ops = {
|
|
.set_sysclk = lpass_cpu_daiops_set_sysclk,
|
|
.startup = lpass_cpu_daiops_startup,
|
|
.shutdown = lpass_cpu_daiops_shutdown,
|
|
.hw_params = lpass_cpu_daiops_hw_params,
|
|
.trigger = lpass_cpu_daiops_trigger,
|
|
};
|
|
EXPORT_SYMBOL_GPL(asoc_qcom_lpass_cpu_dai_ops);
|
|
|
|
int asoc_qcom_lpass_cpu_dai_probe(struct snd_soc_dai *dai)
|
|
{
|
|
struct lpass_data *drvdata = snd_soc_dai_get_drvdata(dai);
|
|
int ret;
|
|
|
|
/* ensure audio hardware is disabled */
|
|
ret = regmap_write(drvdata->lpaif_map,
|
|
LPAIF_I2SCTL_REG(drvdata->variant, dai->driver->id), 0);
|
|
if (ret)
|
|
dev_err(dai->dev, "error writing to i2sctl reg: %d\n", ret);
|
|
|
|
return ret;
|
|
}
|
|
EXPORT_SYMBOL_GPL(asoc_qcom_lpass_cpu_dai_probe);
|
|
|
|
static int asoc_qcom_of_xlate_dai_name(struct snd_soc_component *component,
|
|
const struct of_phandle_args *args,
|
|
const char **dai_name)
|
|
{
|
|
struct lpass_data *drvdata = snd_soc_component_get_drvdata(component);
|
|
struct lpass_variant *variant = drvdata->variant;
|
|
int id = args->args[0];
|
|
int ret = -EINVAL;
|
|
int i;
|
|
|
|
for (i = 0; i < variant->num_dai; i++) {
|
|
if (variant->dai_driver[i].id == id) {
|
|
*dai_name = variant->dai_driver[i].name;
|
|
ret = 0;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
static const struct snd_soc_component_driver lpass_cpu_comp_driver = {
|
|
.name = "lpass-cpu",
|
|
.of_xlate_dai_name = asoc_qcom_of_xlate_dai_name,
|
|
};
|
|
|
|
static bool lpass_cpu_regmap_writeable(struct device *dev, unsigned int reg)
|
|
{
|
|
struct lpass_data *drvdata = dev_get_drvdata(dev);
|
|
struct lpass_variant *v = drvdata->variant;
|
|
int i;
|
|
|
|
for (i = 0; i < v->i2s_ports; ++i)
|
|
if (reg == LPAIF_I2SCTL_REG(v, i))
|
|
return true;
|
|
|
|
for (i = 0; i < v->irq_ports; ++i) {
|
|
if (reg == LPAIF_IRQEN_REG(v, i))
|
|
return true;
|
|
if (reg == LPAIF_IRQCLEAR_REG(v, i))
|
|
return true;
|
|
}
|
|
|
|
for (i = 0; i < v->rdma_channels; ++i) {
|
|
if (reg == LPAIF_RDMACTL_REG(v, i))
|
|
return true;
|
|
if (reg == LPAIF_RDMABASE_REG(v, i))
|
|
return true;
|
|
if (reg == LPAIF_RDMABUFF_REG(v, i))
|
|
return true;
|
|
if (reg == LPAIF_RDMAPER_REG(v, i))
|
|
return true;
|
|
}
|
|
|
|
for (i = 0; i < v->wrdma_channels; ++i) {
|
|
if (reg == LPAIF_WRDMACTL_REG(v, i + v->wrdma_channel_start))
|
|
return true;
|
|
if (reg == LPAIF_WRDMABASE_REG(v, i + v->wrdma_channel_start))
|
|
return true;
|
|
if (reg == LPAIF_WRDMABUFF_REG(v, i + v->wrdma_channel_start))
|
|
return true;
|
|
if (reg == LPAIF_WRDMAPER_REG(v, i + v->wrdma_channel_start))
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
static bool lpass_cpu_regmap_readable(struct device *dev, unsigned int reg)
|
|
{
|
|
struct lpass_data *drvdata = dev_get_drvdata(dev);
|
|
struct lpass_variant *v = drvdata->variant;
|
|
int i;
|
|
|
|
for (i = 0; i < v->i2s_ports; ++i)
|
|
if (reg == LPAIF_I2SCTL_REG(v, i))
|
|
return true;
|
|
|
|
for (i = 0; i < v->irq_ports; ++i) {
|
|
if (reg == LPAIF_IRQEN_REG(v, i))
|
|
return true;
|
|
if (reg == LPAIF_IRQSTAT_REG(v, i))
|
|
return true;
|
|
}
|
|
|
|
for (i = 0; i < v->rdma_channels; ++i) {
|
|
if (reg == LPAIF_RDMACTL_REG(v, i))
|
|
return true;
|
|
if (reg == LPAIF_RDMABASE_REG(v, i))
|
|
return true;
|
|
if (reg == LPAIF_RDMABUFF_REG(v, i))
|
|
return true;
|
|
if (reg == LPAIF_RDMACURR_REG(v, i))
|
|
return true;
|
|
if (reg == LPAIF_RDMAPER_REG(v, i))
|
|
return true;
|
|
}
|
|
|
|
for (i = 0; i < v->wrdma_channels; ++i) {
|
|
if (reg == LPAIF_WRDMACTL_REG(v, i + v->wrdma_channel_start))
|
|
return true;
|
|
if (reg == LPAIF_WRDMABASE_REG(v, i + v->wrdma_channel_start))
|
|
return true;
|
|
if (reg == LPAIF_WRDMABUFF_REG(v, i + v->wrdma_channel_start))
|
|
return true;
|
|
if (reg == LPAIF_WRDMACURR_REG(v, i + v->wrdma_channel_start))
|
|
return true;
|
|
if (reg == LPAIF_WRDMAPER_REG(v, i + v->wrdma_channel_start))
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
static bool lpass_cpu_regmap_volatile(struct device *dev, unsigned int reg)
|
|
{
|
|
struct lpass_data *drvdata = dev_get_drvdata(dev);
|
|
struct lpass_variant *v = drvdata->variant;
|
|
int i;
|
|
|
|
for (i = 0; i < v->irq_ports; ++i)
|
|
if (reg == LPAIF_IRQSTAT_REG(v, i))
|
|
return true;
|
|
|
|
for (i = 0; i < v->rdma_channels; ++i)
|
|
if (reg == LPAIF_RDMACURR_REG(v, i))
|
|
return true;
|
|
|
|
for (i = 0; i < v->wrdma_channels; ++i)
|
|
if (reg == LPAIF_WRDMACURR_REG(v, i + v->wrdma_channel_start))
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
static struct regmap_config lpass_cpu_regmap_config = {
|
|
.name = "lpass_cpu",
|
|
.reg_bits = 32,
|
|
.reg_stride = 4,
|
|
.val_bits = 32,
|
|
.writeable_reg = lpass_cpu_regmap_writeable,
|
|
.readable_reg = lpass_cpu_regmap_readable,
|
|
.volatile_reg = lpass_cpu_regmap_volatile,
|
|
.cache_type = REGCACHE_FLAT,
|
|
};
|
|
|
|
static int lpass_hdmi_init_bitfields(struct device *dev, struct regmap *map)
|
|
{
|
|
struct lpass_data *drvdata = dev_get_drvdata(dev);
|
|
struct lpass_variant *v = drvdata->variant;
|
|
unsigned int i;
|
|
struct lpass_hdmi_tx_ctl *tx_ctl;
|
|
struct regmap_field *legacy_en;
|
|
struct lpass_vbit_ctrl *vbit_ctl;
|
|
struct regmap_field *tx_parity;
|
|
struct lpass_dp_metadata_ctl *meta_ctl;
|
|
struct lpass_sstream_ctl *sstream_ctl;
|
|
struct regmap_field *ch_msb;
|
|
struct regmap_field *ch_lsb;
|
|
struct lpass_hdmitx_dmactl *tx_dmactl;
|
|
int rval;
|
|
|
|
tx_ctl = devm_kzalloc(dev, sizeof(*tx_ctl), GFP_KERNEL);
|
|
if (!tx_ctl)
|
|
return -ENOMEM;
|
|
|
|
QCOM_REGMAP_FIELD_ALLOC(dev, map, v->soft_reset, tx_ctl->soft_reset);
|
|
QCOM_REGMAP_FIELD_ALLOC(dev, map, v->force_reset, tx_ctl->force_reset);
|
|
drvdata->tx_ctl = tx_ctl;
|
|
|
|
QCOM_REGMAP_FIELD_ALLOC(dev, map, v->legacy_en, legacy_en);
|
|
drvdata->hdmitx_legacy_en = legacy_en;
|
|
|
|
vbit_ctl = devm_kzalloc(dev, sizeof(*vbit_ctl), GFP_KERNEL);
|
|
if (!vbit_ctl)
|
|
return -ENOMEM;
|
|
|
|
QCOM_REGMAP_FIELD_ALLOC(dev, map, v->replace_vbit, vbit_ctl->replace_vbit);
|
|
QCOM_REGMAP_FIELD_ALLOC(dev, map, v->vbit_stream, vbit_ctl->vbit_stream);
|
|
drvdata->vbit_ctl = vbit_ctl;
|
|
|
|
|
|
QCOM_REGMAP_FIELD_ALLOC(dev, map, v->calc_en, tx_parity);
|
|
drvdata->hdmitx_parity_calc_en = tx_parity;
|
|
|
|
meta_ctl = devm_kzalloc(dev, sizeof(*meta_ctl), GFP_KERNEL);
|
|
if (!meta_ctl)
|
|
return -ENOMEM;
|
|
|
|
rval = devm_regmap_field_bulk_alloc(dev, map, &meta_ctl->mute, &v->mute, 7);
|
|
if (rval)
|
|
return rval;
|
|
drvdata->meta_ctl = meta_ctl;
|
|
|
|
sstream_ctl = devm_kzalloc(dev, sizeof(*sstream_ctl), GFP_KERNEL);
|
|
if (!sstream_ctl)
|
|
return -ENOMEM;
|
|
|
|
rval = devm_regmap_field_bulk_alloc(dev, map, &sstream_ctl->sstream_en, &v->sstream_en, 9);
|
|
if (rval)
|
|
return rval;
|
|
|
|
drvdata->sstream_ctl = sstream_ctl;
|
|
|
|
for (i = 0; i < LPASS_MAX_HDMI_DMA_CHANNELS; i++) {
|
|
QCOM_REGMAP_FIELD_ALLOC(dev, map, v->msb_bits, ch_msb);
|
|
drvdata->hdmitx_ch_msb[i] = ch_msb;
|
|
|
|
QCOM_REGMAP_FIELD_ALLOC(dev, map, v->lsb_bits, ch_lsb);
|
|
drvdata->hdmitx_ch_lsb[i] = ch_lsb;
|
|
|
|
tx_dmactl = devm_kzalloc(dev, sizeof(*tx_dmactl), GFP_KERNEL);
|
|
if (!tx_dmactl)
|
|
return -ENOMEM;
|
|
|
|
QCOM_REGMAP_FIELD_ALLOC(dev, map, v->use_hw_chs, tx_dmactl->use_hw_chs);
|
|
QCOM_REGMAP_FIELD_ALLOC(dev, map, v->use_hw_usr, tx_dmactl->use_hw_usr);
|
|
QCOM_REGMAP_FIELD_ALLOC(dev, map, v->hw_chs_sel, tx_dmactl->hw_chs_sel);
|
|
QCOM_REGMAP_FIELD_ALLOC(dev, map, v->hw_usr_sel, tx_dmactl->hw_usr_sel);
|
|
drvdata->hdmi_tx_dmactl[i] = tx_dmactl;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static bool lpass_hdmi_regmap_writeable(struct device *dev, unsigned int reg)
|
|
{
|
|
struct lpass_data *drvdata = dev_get_drvdata(dev);
|
|
struct lpass_variant *v = drvdata->variant;
|
|
int i;
|
|
|
|
if (reg == LPASS_HDMI_TX_CTL_ADDR(v))
|
|
return true;
|
|
if (reg == LPASS_HDMI_TX_LEGACY_ADDR(v))
|
|
return true;
|
|
if (reg == LPASS_HDMI_TX_VBIT_CTL_ADDR(v))
|
|
return true;
|
|
if (reg == LPASS_HDMI_TX_PARITY_ADDR(v))
|
|
return true;
|
|
if (reg == LPASS_HDMI_TX_DP_ADDR(v))
|
|
return true;
|
|
if (reg == LPASS_HDMI_TX_SSTREAM_ADDR(v))
|
|
return true;
|
|
if (reg == LPASS_HDMITX_APP_IRQEN_REG(v))
|
|
return true;
|
|
if (reg == LPASS_HDMITX_APP_IRQCLEAR_REG(v))
|
|
return true;
|
|
|
|
for (i = 0; i < v->hdmi_rdma_channels; i++) {
|
|
if (reg == LPASS_HDMI_TX_CH_LSB_ADDR(v, i))
|
|
return true;
|
|
if (reg == LPASS_HDMI_TX_CH_MSB_ADDR(v, i))
|
|
return true;
|
|
if (reg == LPASS_HDMI_TX_DMA_ADDR(v, i))
|
|
return true;
|
|
}
|
|
|
|
for (i = 0; i < v->hdmi_rdma_channels; ++i) {
|
|
if (reg == LPAIF_HDMI_RDMACTL_REG(v, i))
|
|
return true;
|
|
if (reg == LPAIF_HDMI_RDMABASE_REG(v, i))
|
|
return true;
|
|
if (reg == LPAIF_HDMI_RDMABUFF_REG(v, i))
|
|
return true;
|
|
if (reg == LPAIF_HDMI_RDMAPER_REG(v, i))
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static bool lpass_hdmi_regmap_readable(struct device *dev, unsigned int reg)
|
|
{
|
|
struct lpass_data *drvdata = dev_get_drvdata(dev);
|
|
struct lpass_variant *v = drvdata->variant;
|
|
int i;
|
|
|
|
if (reg == LPASS_HDMI_TX_CTL_ADDR(v))
|
|
return true;
|
|
if (reg == LPASS_HDMI_TX_LEGACY_ADDR(v))
|
|
return true;
|
|
if (reg == LPASS_HDMI_TX_VBIT_CTL_ADDR(v))
|
|
return true;
|
|
|
|
for (i = 0; i < v->hdmi_rdma_channels; i++) {
|
|
if (reg == LPASS_HDMI_TX_CH_LSB_ADDR(v, i))
|
|
return true;
|
|
if (reg == LPASS_HDMI_TX_CH_MSB_ADDR(v, i))
|
|
return true;
|
|
if (reg == LPASS_HDMI_TX_DMA_ADDR(v, i))
|
|
return true;
|
|
}
|
|
|
|
if (reg == LPASS_HDMI_TX_PARITY_ADDR(v))
|
|
return true;
|
|
if (reg == LPASS_HDMI_TX_DP_ADDR(v))
|
|
return true;
|
|
if (reg == LPASS_HDMI_TX_SSTREAM_ADDR(v))
|
|
return true;
|
|
if (reg == LPASS_HDMITX_APP_IRQEN_REG(v))
|
|
return true;
|
|
if (reg == LPASS_HDMITX_APP_IRQSTAT_REG(v))
|
|
return true;
|
|
|
|
for (i = 0; i < v->hdmi_rdma_channels; ++i) {
|
|
if (reg == LPAIF_HDMI_RDMACTL_REG(v, i))
|
|
return true;
|
|
if (reg == LPAIF_HDMI_RDMABASE_REG(v, i))
|
|
return true;
|
|
if (reg == LPAIF_HDMI_RDMABUFF_REG(v, i))
|
|
return true;
|
|
if (reg == LPAIF_HDMI_RDMAPER_REG(v, i))
|
|
return true;
|
|
if (reg == LPAIF_HDMI_RDMACURR_REG(v, i))
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
static bool lpass_hdmi_regmap_volatile(struct device *dev, unsigned int reg)
|
|
{
|
|
struct lpass_data *drvdata = dev_get_drvdata(dev);
|
|
struct lpass_variant *v = drvdata->variant;
|
|
int i;
|
|
|
|
if (reg == LPASS_HDMITX_APP_IRQSTAT_REG(v))
|
|
return true;
|
|
if (reg == LPASS_HDMI_TX_LEGACY_ADDR(v))
|
|
return true;
|
|
|
|
for (i = 0; i < v->hdmi_rdma_channels; ++i) {
|
|
if (reg == LPAIF_HDMI_RDMACURR_REG(v, i))
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static struct regmap_config lpass_hdmi_regmap_config = {
|
|
.name = "lpass_hdmi",
|
|
.reg_bits = 32,
|
|
.reg_stride = 4,
|
|
.val_bits = 32,
|
|
.writeable_reg = lpass_hdmi_regmap_writeable,
|
|
.readable_reg = lpass_hdmi_regmap_readable,
|
|
.volatile_reg = lpass_hdmi_regmap_volatile,
|
|
.cache_type = REGCACHE_FLAT,
|
|
};
|
|
|
|
static unsigned int of_lpass_cpu_parse_sd_lines(struct device *dev,
|
|
struct device_node *node,
|
|
const char *name)
|
|
{
|
|
unsigned int lines[LPASS_CPU_MAX_MI2S_LINES];
|
|
unsigned int sd_line_mask = 0;
|
|
int num_lines, i;
|
|
|
|
num_lines = of_property_read_variable_u32_array(node, name, lines, 0,
|
|
LPASS_CPU_MAX_MI2S_LINES);
|
|
if (num_lines < 0)
|
|
return LPAIF_I2SCTL_MODE_NONE;
|
|
|
|
for (i = 0; i < num_lines; i++)
|
|
sd_line_mask |= BIT(lines[i]);
|
|
|
|
switch (sd_line_mask) {
|
|
case LPASS_CPU_I2S_SD0_MASK:
|
|
return LPAIF_I2SCTL_MODE_SD0;
|
|
case LPASS_CPU_I2S_SD1_MASK:
|
|
return LPAIF_I2SCTL_MODE_SD1;
|
|
case LPASS_CPU_I2S_SD2_MASK:
|
|
return LPAIF_I2SCTL_MODE_SD2;
|
|
case LPASS_CPU_I2S_SD3_MASK:
|
|
return LPAIF_I2SCTL_MODE_SD3;
|
|
case LPASS_CPU_I2S_SD0_1_MASK:
|
|
return LPAIF_I2SCTL_MODE_QUAD01;
|
|
case LPASS_CPU_I2S_SD2_3_MASK:
|
|
return LPAIF_I2SCTL_MODE_QUAD23;
|
|
case LPASS_CPU_I2S_SD0_1_2_MASK:
|
|
return LPAIF_I2SCTL_MODE_6CH;
|
|
case LPASS_CPU_I2S_SD0_1_2_3_MASK:
|
|
return LPAIF_I2SCTL_MODE_8CH;
|
|
default:
|
|
dev_err(dev, "Unsupported SD line mask: %#x\n", sd_line_mask);
|
|
return LPAIF_I2SCTL_MODE_NONE;
|
|
}
|
|
}
|
|
|
|
static void of_lpass_cpu_parse_dai_data(struct device *dev,
|
|
struct lpass_data *data)
|
|
{
|
|
struct device_node *node;
|
|
int ret, id;
|
|
|
|
/* Allow all channels by default for backwards compatibility */
|
|
for (id = 0; id < data->variant->num_dai; id++) {
|
|
data->mi2s_playback_sd_mode[id] = LPAIF_I2SCTL_MODE_8CH;
|
|
data->mi2s_capture_sd_mode[id] = LPAIF_I2SCTL_MODE_8CH;
|
|
}
|
|
|
|
for_each_child_of_node(dev->of_node, node) {
|
|
ret = of_property_read_u32(node, "reg", &id);
|
|
if (ret || id < 0) {
|
|
dev_err(dev, "valid dai id not found: %d\n", ret);
|
|
continue;
|
|
}
|
|
if (id == LPASS_DP_RX) {
|
|
data->hdmi_port_enable = 1;
|
|
} else {
|
|
data->mi2s_playback_sd_mode[id] =
|
|
of_lpass_cpu_parse_sd_lines(dev, node,
|
|
"qcom,playback-sd-lines");
|
|
data->mi2s_capture_sd_mode[id] =
|
|
of_lpass_cpu_parse_sd_lines(dev, node,
|
|
"qcom,capture-sd-lines");
|
|
}
|
|
}
|
|
}
|
|
|
|
int asoc_qcom_lpass_cpu_platform_probe(struct platform_device *pdev)
|
|
{
|
|
struct lpass_data *drvdata;
|
|
struct device_node *dsp_of_node;
|
|
struct resource *res;
|
|
struct lpass_variant *variant;
|
|
struct device *dev = &pdev->dev;
|
|
const struct of_device_id *match;
|
|
int ret, i, dai_id;
|
|
|
|
dsp_of_node = of_parse_phandle(pdev->dev.of_node, "qcom,adsp", 0);
|
|
if (dsp_of_node) {
|
|
dev_err(dev, "DSP exists and holds audio resources\n");
|
|
return -EBUSY;
|
|
}
|
|
|
|
drvdata = devm_kzalloc(dev, sizeof(struct lpass_data), GFP_KERNEL);
|
|
if (!drvdata)
|
|
return -ENOMEM;
|
|
platform_set_drvdata(pdev, drvdata);
|
|
|
|
match = of_match_device(dev->driver->of_match_table, dev);
|
|
if (!match || !match->data)
|
|
return -EINVAL;
|
|
|
|
drvdata->variant = (struct lpass_variant *)match->data;
|
|
variant = drvdata->variant;
|
|
|
|
of_lpass_cpu_parse_dai_data(dev, drvdata);
|
|
|
|
res = platform_get_resource_byname(pdev, IORESOURCE_MEM, "lpass-lpaif");
|
|
|
|
drvdata->lpaif = devm_ioremap_resource(dev, res);
|
|
if (IS_ERR(drvdata->lpaif))
|
|
return PTR_ERR(drvdata->lpaif);
|
|
|
|
lpass_cpu_regmap_config.max_register = LPAIF_WRDMAPER_REG(variant,
|
|
variant->wrdma_channels +
|
|
variant->wrdma_channel_start);
|
|
|
|
drvdata->lpaif_map = devm_regmap_init_mmio(dev, drvdata->lpaif,
|
|
&lpass_cpu_regmap_config);
|
|
if (IS_ERR(drvdata->lpaif_map)) {
|
|
dev_err(dev, "error initializing regmap: %ld\n",
|
|
PTR_ERR(drvdata->lpaif_map));
|
|
return PTR_ERR(drvdata->lpaif_map);
|
|
}
|
|
|
|
if (drvdata->hdmi_port_enable) {
|
|
res = platform_get_resource_byname(pdev, IORESOURCE_MEM, "lpass-hdmiif");
|
|
|
|
drvdata->hdmiif = devm_ioremap_resource(dev, res);
|
|
if (IS_ERR(drvdata->hdmiif))
|
|
return PTR_ERR(drvdata->hdmiif);
|
|
|
|
lpass_hdmi_regmap_config.max_register = LPAIF_HDMI_RDMAPER_REG(variant,
|
|
variant->hdmi_rdma_channels - 1);
|
|
drvdata->hdmiif_map = devm_regmap_init_mmio(dev, drvdata->hdmiif,
|
|
&lpass_hdmi_regmap_config);
|
|
if (IS_ERR(drvdata->hdmiif_map)) {
|
|
dev_err(dev, "error initializing regmap: %ld\n",
|
|
PTR_ERR(drvdata->hdmiif_map));
|
|
return PTR_ERR(drvdata->hdmiif_map);
|
|
}
|
|
}
|
|
|
|
if (variant->init) {
|
|
ret = variant->init(pdev);
|
|
if (ret) {
|
|
dev_err(dev, "error initializing variant: %d\n", ret);
|
|
return ret;
|
|
}
|
|
}
|
|
|
|
for (i = 0; i < variant->num_dai; i++) {
|
|
dai_id = variant->dai_driver[i].id;
|
|
if (dai_id == LPASS_DP_RX)
|
|
continue;
|
|
|
|
drvdata->mi2s_osr_clk[dai_id] = devm_clk_get(dev,
|
|
variant->dai_osr_clk_names[i]);
|
|
if (IS_ERR(drvdata->mi2s_osr_clk[dai_id])) {
|
|
dev_warn(dev,
|
|
"%s() error getting optional %s: %ld\n",
|
|
__func__,
|
|
variant->dai_osr_clk_names[i],
|
|
PTR_ERR(drvdata->mi2s_osr_clk[dai_id]));
|
|
|
|
drvdata->mi2s_osr_clk[dai_id] = NULL;
|
|
}
|
|
|
|
drvdata->mi2s_bit_clk[dai_id] = devm_clk_get(dev,
|
|
variant->dai_bit_clk_names[i]);
|
|
if (IS_ERR(drvdata->mi2s_bit_clk[dai_id])) {
|
|
dev_err(dev,
|
|
"error getting %s: %ld\n",
|
|
variant->dai_bit_clk_names[i],
|
|
PTR_ERR(drvdata->mi2s_bit_clk[dai_id]));
|
|
return PTR_ERR(drvdata->mi2s_bit_clk[dai_id]);
|
|
}
|
|
}
|
|
|
|
/* Allocation for i2sctl regmap fields */
|
|
drvdata->i2sctl = devm_kzalloc(&pdev->dev, sizeof(struct lpaif_i2sctl),
|
|
GFP_KERNEL);
|
|
|
|
/* Initialize bitfields for dai I2SCTL register */
|
|
ret = lpass_cpu_init_i2sctl_bitfields(dev, drvdata->i2sctl,
|
|
drvdata->lpaif_map);
|
|
if (ret) {
|
|
dev_err(dev, "error init i2sctl field: %d\n", ret);
|
|
return ret;
|
|
}
|
|
|
|
if (drvdata->hdmi_port_enable) {
|
|
ret = lpass_hdmi_init_bitfields(dev, drvdata->hdmiif_map);
|
|
if (ret) {
|
|
dev_err(dev, "%s error hdmi init failed\n", __func__);
|
|
return ret;
|
|
}
|
|
}
|
|
ret = devm_snd_soc_register_component(dev,
|
|
&lpass_cpu_comp_driver,
|
|
variant->dai_driver,
|
|
variant->num_dai);
|
|
if (ret) {
|
|
dev_err(dev, "error registering cpu driver: %d\n", ret);
|
|
goto err;
|
|
}
|
|
|
|
ret = asoc_qcom_lpass_platform_register(pdev);
|
|
if (ret) {
|
|
dev_err(dev, "error registering platform driver: %d\n", ret);
|
|
goto err;
|
|
}
|
|
|
|
err:
|
|
return ret;
|
|
}
|
|
EXPORT_SYMBOL_GPL(asoc_qcom_lpass_cpu_platform_probe);
|
|
|
|
int asoc_qcom_lpass_cpu_platform_remove(struct platform_device *pdev)
|
|
{
|
|
struct lpass_data *drvdata = platform_get_drvdata(pdev);
|
|
|
|
if (drvdata->variant->exit)
|
|
drvdata->variant->exit(pdev);
|
|
|
|
|
|
return 0;
|
|
}
|
|
EXPORT_SYMBOL_GPL(asoc_qcom_lpass_cpu_platform_remove);
|
|
|
|
void asoc_qcom_lpass_cpu_platform_shutdown(struct platform_device *pdev)
|
|
{
|
|
struct lpass_data *drvdata = platform_get_drvdata(pdev);
|
|
|
|
if (drvdata->variant->exit)
|
|
drvdata->variant->exit(pdev);
|
|
|
|
}
|
|
EXPORT_SYMBOL_GPL(asoc_qcom_lpass_cpu_platform_shutdown);
|
|
|
|
MODULE_DESCRIPTION("QTi LPASS CPU Driver");
|
|
MODULE_LICENSE("GPL v2");
|