[alsa-devel] [PATCH 0/2] ASoC: atmel-classd: add the Audio Class D Amplifier
The Audio Class D Amplifier driver includes two parts. 1) Driver code to implement the Audio Class D Amplifier function. 2) Device tree binding document, it describes how to add the Audio Class D Amplifier in device tree.
Songjun Wu (2): ASoC: atmel-classd: add the Audio Class D Amplifier code ASoC: atmel-classd: DT binding for Class D audio amplifier driver
.../devicetree/bindings/sound/atmel-classd.txt | 73 ++ sound/soc/atmel/Kconfig | 9 + sound/soc/atmel/Makefile | 2 + sound/soc/atmel/atmel-classd.c | 801 ++++++++++++++++++++ sound/soc/atmel/atmel-classd.h | 120 +++ 5 files changed, 1005 insertions(+) create mode 100644 Documentation/devicetree/bindings/sound/atmel-classd.txt create mode 100644 sound/soc/atmel/atmel-classd.c create mode 100644 sound/soc/atmel/atmel-classd.h
Add driver for the digital imput to PWM output stereo class D amplifier. It comes with filter, digitally controlled gain, an equalizer and a dmphase filter.
Signed-off-by: Songjun Wu songjun.wu@atmel.com --- sound/soc/atmel/Kconfig | 9 + sound/soc/atmel/Makefile | 2 + sound/soc/atmel/atmel-classd.c | 801 ++++++++++++++++++++++++++++++++++++++++ sound/soc/atmel/atmel-classd.h | 120 ++++++ 4 files changed, 932 insertions(+) create mode 100644 sound/soc/atmel/atmel-classd.c create mode 100644 sound/soc/atmel/atmel-classd.h
diff --git a/sound/soc/atmel/Kconfig b/sound/soc/atmel/Kconfig index 1489cd4..2d30464 100644 --- a/sound/soc/atmel/Kconfig +++ b/sound/soc/atmel/Kconfig @@ -59,4 +59,13 @@ config SND_AT91_SOC_SAM9X5_WM8731 help Say Y if you want to add support for audio SoC on an at91sam9x5 based board that is using WM8731 codec. + +config SND_ATMEL_SOC_CLASSD + tristate "Atmel ASoC driver for boards using CLASSD" + depends on ARCH_AT91 || COMPILE_TEST + select SND_ATMEL_SOC_DMA + select REGMAP_MMIO + help + Say Y if you want to add support for Atmel ASoC driver for boards using + CLASSD. endif diff --git a/sound/soc/atmel/Makefile b/sound/soc/atmel/Makefile index b327e5c..f6f7db4 100644 --- a/sound/soc/atmel/Makefile +++ b/sound/soc/atmel/Makefile @@ -11,7 +11,9 @@ obj-$(CONFIG_SND_ATMEL_SOC_SSC) += snd-soc-atmel_ssc_dai.o snd-soc-sam9g20-wm8731-objs := sam9g20_wm8731.o snd-atmel-soc-wm8904-objs := atmel_wm8904.o snd-soc-sam9x5-wm8731-objs := sam9x5_wm8731.o +snd-atmel-soc-classd-objs := atmel-classd.o
obj-$(CONFIG_SND_AT91_SOC_SAM9G20_WM8731) += snd-soc-sam9g20-wm8731.o obj-$(CONFIG_SND_ATMEL_SOC_WM8904) += snd-atmel-soc-wm8904.o obj-$(CONFIG_SND_AT91_SOC_SAM9X5_WM8731) += snd-soc-sam9x5-wm8731.o +obj-$(CONFIG_SND_ATMEL_SOC_CLASSD) += snd-atmel-soc-classd.o diff --git a/sound/soc/atmel/atmel-classd.c b/sound/soc/atmel/atmel-classd.c new file mode 100644 index 0000000..5ceefa9 --- /dev/null +++ b/sound/soc/atmel/atmel-classd.c @@ -0,0 +1,801 @@ +/* Atmel ALSA SoC Audio Class D Amplifier (CLASSD) driver + * + * Copyright (C) 2015 Atmel + * + * Author: Songjun Wu songjun.wu@atmel.com + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 or later + * as published by the Free Software Foundation. + */ + +#include <linux/of.h> +#include <linux/clk.h> +#include <linux/module.h> +#include <linux/platform_device.h> +#include <linux/regmap.h> +#include <sound/core.h> +#include <sound/dmaengine_pcm.h> +#include <sound/pcm_params.h> +#include <sound/tlv.h> +#include "atmel-classd.h" + +struct atmel_classd_pdata { + bool non_overlap_enable; + int non_overlap_time; + int pwm_type; +}; + +struct atmel_classd { + dma_addr_t phy_base; + struct regmap *regmap; + struct clk *pclk; + struct clk *gclk; + struct clk *aclk; + int irq; + const struct atmel_classd_pdata *pdata; +}; + +#ifdef CONFIG_OF +static const struct of_device_id atmel_classd_of_match[] = { + { + .compatible = "atmel,sama5d2-classd", + }, { + /* sentinel */ + } +}; +MODULE_DEVICE_TABLE(of, atmel_classd_of_match); + +static struct atmel_classd_pdata *atmel_classd_dt_init(struct device *dev) +{ + struct device_node *np = dev->of_node; + struct atmel_classd_pdata *pdata; + const char *pwm_type; + int ret; + + if (!np) { + dev_err(dev, "device node not found\n"); + return ERR_PTR(-EINVAL); + } + + pdata = devm_kzalloc(dev, sizeof(*pdata), GFP_KERNEL); + if (!pdata) + return ERR_PTR(-ENOMEM); + + ret = of_property_read_string(np, "atmel,pwm-type", &pwm_type); + if ((ret == 0) && (strcmp(pwm_type, "diff") == 0)) + pdata->pwm_type = CLASSD_MR_PWMTYP_DIFF; + else + pdata->pwm_type = CLASSD_MR_PWMTYP_SINGLE; + + ret = of_property_read_u32(np, + "atmel,non-overlap-time", &pdata->non_overlap_time); + if (ret != 0) + pdata->non_overlap_enable = false; + else + pdata->non_overlap_enable = true; + + return pdata; +} +#else +static inline struct atmel_classd_pdata * +atmel_classd_dt_init(struct device *dev) +{ + return ERR_PTR(-EINVAL); +} +#endif + +#define ATMEL_CLASSD_RATES (SNDRV_PCM_RATE_8000 \ + | SNDRV_PCM_RATE_16000 | SNDRV_PCM_RATE_22050 \ + | SNDRV_PCM_RATE_32000 | SNDRV_PCM_RATE_44100 \ + | SNDRV_PCM_RATE_48000 | SNDRV_PCM_RATE_88200 \ + | SNDRV_PCM_RATE_96000) + +static const struct snd_pcm_hardware atmel_classd_hw = { + .info = SNDRV_PCM_INFO_MMAP + | SNDRV_PCM_INFO_MMAP_VALID + | SNDRV_PCM_INFO_INTERLEAVED + | SNDRV_PCM_INFO_RESUME + | SNDRV_PCM_INFO_PAUSE, + .formats = (SNDRV_PCM_FMTBIT_S16_LE), + .rates = ATMEL_CLASSD_RATES, + .rate_min = 8000, + .rate_max = 96000, + .channels_min = 2, + .channels_max = 2, + .buffer_bytes_max = 64 * 1024, + .period_bytes_min = 256, + .period_bytes_max = 32 * 1024, + .periods_min = 2, + .periods_max = 256, +}; + +#define ATMEL_CLASSD_PREALLOC_BUF_SIZE (64 * 1024) + +/* cpu dai component */ +static int atmel_classd_cpu_dai_startup(struct snd_pcm_substream *substream, + struct snd_soc_dai *cpu_dai) +{ + struct atmel_classd *dd = snd_soc_dai_get_drvdata(cpu_dai); + + regmap_write(dd->regmap, CLASSD_THR, 0x0); + + return clk_prepare_enable(dd->pclk); +} + +static void atmel_classd_cpu_dai_shutdown(struct snd_pcm_substream *substream, + struct snd_soc_dai *cpu_dai) +{ + struct atmel_classd *dd = snd_soc_dai_get_drvdata(cpu_dai); + + clk_disable_unprepare(dd->pclk); +} + +static const struct snd_soc_dai_ops atmel_classd_cpu_dai_ops = { + .startup = atmel_classd_cpu_dai_startup, + .shutdown = atmel_classd_cpu_dai_shutdown, +}; + +static struct snd_soc_dai_driver atmel_classd_cpu_dai = { + .playback = { + .channels_min = 2, + .channels_max = 2, + .rates = ATMEL_CLASSD_RATES, + .formats = SNDRV_PCM_FMTBIT_S16_LE,}, + .ops = &atmel_classd_cpu_dai_ops, +}; + +static const struct snd_soc_component_driver atmel_classd_cpu_dai_component = { + .name = "atmel-classd", +}; + +/* platform */ +static int +atmel_classd_platform_configure_dma(struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params, + struct dma_slave_config *slave_config) +{ + struct snd_soc_pcm_runtime *rtd = substream->private_data; + struct atmel_classd *dd = snd_soc_dai_get_drvdata(rtd->cpu_dai); + int bits; + + bits = params_physical_width(params); + if (bits != 16) { + dev_err(rtd->platform->dev, + "only supports 16-bit audio data\n"); + return -EINVAL; + } + + slave_config->direction = DMA_MEM_TO_DEV; + slave_config->dst_addr = dd->phy_base + CLASSD_THR; + slave_config->dst_addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES; + slave_config->dst_maxburst = 1; + slave_config->src_maxburst = 1; + slave_config->device_fc = false; + + return 0; +} + +static const struct snd_dmaengine_pcm_config +atmel_classd_dmaengine_pcm_config = { + .prepare_slave_config = atmel_classd_platform_configure_dma, + .pcm_hardware = &atmel_classd_hw, + .prealloc_buffer_size = ATMEL_CLASSD_PREALLOC_BUF_SIZE, +}; + +/* codec component */ +static const char * const swap_text[] = { + "No Swap", "Swap" +}; + +static SOC_ENUM_SINGLE_DECL(classd_swap_enum, + CLASSD_INTPMR, CLASSD_INTPMR_SWAP_SHIFT, + swap_text); + +static const char * const mono_text[] = { + "stereo", "mono" +}; + +static SOC_ENUM_SINGLE_DECL(classd_mono_enum, + CLASSD_INTPMR, CLASSD_INTPMR_MONO_SHIFT, + mono_text); + +static const char * const mono_mode_text[] = { + "mix", "sat", "left", "right" +}; + +static SOC_ENUM_SINGLE_DECL(classd_mono_mode_enum, + CLASSD_INTPMR, CLASSD_INTPMR_MONO_MODE_SHIFT, + mono_mode_text); + +static const char * const deemp_text[] = { + "disabled", "enabled" +}; + +static SOC_ENUM_SINGLE_DECL(classd_deemp_enum, + CLASSD_INTPMR, CLASSD_INTPMR_DEEMP_SHIFT, + deemp_text); + +static const char * const eqcfg_bass_text[] = { + "-12 dB", "-6 dB", "0 dB", "+6 dB", "+12 dB" +}; + +static const unsigned int eqcfg_bass_value[] = { + CLASSD_INTPMR_EQCFG_B_CUT_12, + CLASSD_INTPMR_EQCFG_B_CUT_6, CLASSD_INTPMR_EQCFG_FLAT, + CLASSD_INTPMR_EQCFG_B_BOOST_6, CLASSD_INTPMR_EQCFG_B_BOOST_12 +}; + +static SOC_VALUE_ENUM_SINGLE_DECL(classd_eqcfg_bass_enum, + CLASSD_INTPMR, CLASSD_INTPMR_EQCFG_SHIFT, 0xf, + eqcfg_bass_text, eqcfg_bass_value); + +static const char * const eqcfg_medium_text[] = { + "-8 dB", "-3 dB", "0 dB", "+3 dB", "+8 dB" +}; + +static const unsigned int eqcfg_medium_value[] = { + CLASSD_INTPMR_EQCFG_M_CUT_8, + CLASSD_INTPMR_EQCFG_M_CUT_3, CLASSD_INTPMR_EQCFG_FLAT, + CLASSD_INTPMR_EQCFG_M_BOOST_3, CLASSD_INTPMR_EQCFG_M_BOOST_8 +}; + +static SOC_VALUE_ENUM_SINGLE_DECL(classd_eqcfg_medium_enum, + CLASSD_INTPMR, CLASSD_INTPMR_EQCFG_SHIFT, 0xf, + eqcfg_medium_text, eqcfg_medium_value); + +static const char * const eqcfg_treble_text[] = { + "-12 dB", "-6 dB", "0 dB", "+6 dB", "+12 dB", +}; + +static const unsigned int eqcfg_treble_value[] = { + CLASSD_INTPMR_EQCFG_T_CUT_12, + CLASSD_INTPMR_EQCFG_T_CUT_6, CLASSD_INTPMR_EQCFG_FLAT, + CLASSD_INTPMR_EQCFG_T_BOOST_6, CLASSD_INTPMR_EQCFG_T_BOOST_12 +}; + +static SOC_VALUE_ENUM_SINGLE_DECL(classd_eqcfg_treble_enum, + CLASSD_INTPMR, CLASSD_INTPMR_EQCFG_SHIFT, 0xf, + eqcfg_treble_text, eqcfg_treble_value); + +static int classd_get_eq_enum(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_soc_component *component = snd_kcontrol_chip(kcontrol); + struct soc_enum *e = (struct soc_enum *)kcontrol->private_value; + unsigned int val, item; + unsigned int reg_val; + int ret; + + ret = snd_soc_component_read(component, e->reg, ®_val); + if (ret) + return ret; + val = (reg_val >> e->shift_l) & e->mask; + + if (strcmp(kcontrol->id.name, "EQ Bass") == 0) { + if (val > 4) + val = 0; + } else if (strcmp(kcontrol->id.name, "EQ Medium") == 0) { + if ((val > 8) || (val < 5)) + val = 0; + } else if (strcmp(kcontrol->id.name, "EQ Treble") == 0) { + if ((val > 12) || (val < 9)) + val = 0; + } else + return -EINVAL; + + item = snd_soc_enum_val_to_item(e, val); + ucontrol->value.enumerated.item[0] = item; + + return 0; +} + +static const DECLARE_TLV_DB_SCALE(classd_digital_tlv, -7800, 100, 1); + +static const struct snd_kcontrol_new atmel_classd_snd_controls[] = { +SOC_SINGLE_TLV("Left Volume", CLASSD_INTPMR, + CLASSD_INTPMR_ATTL_SHIFT, 78, 1, classd_digital_tlv), + +SOC_SINGLE_TLV("Right Volume", CLASSD_INTPMR, + CLASSD_INTPMR_ATTR_SHIFT, 78, 1, classd_digital_tlv), + +SOC_ENUM("De-emphasis", classd_deemp_enum), + +SOC_ENUM("Stereo", classd_mono_enum), + +SOC_ENUM("Swap", classd_swap_enum), + +SOC_ENUM("Mono Mode", classd_mono_mode_enum), + +SOC_ENUM_EXT("EQ Bass", classd_eqcfg_bass_enum, + classd_get_eq_enum, snd_soc_put_enum_double), + +SOC_ENUM_EXT("EQ Medium", classd_eqcfg_medium_enum, + classd_get_eq_enum, snd_soc_put_enum_double), + +SOC_ENUM_EXT("EQ Treble", classd_eqcfg_treble_enum, + classd_get_eq_enum, snd_soc_put_enum_double), +}; + +static const char * const pwm_type[] = { + "single-ended", "differential" +}; + +static int atmel_classd_codec_probe(struct snd_soc_codec *codec) +{ + struct atmel_classd *dd = snd_soc_codec_get_drvdata(codec); + const struct atmel_classd_pdata *pdata = dd->pdata; + u32 mask, val; + + mask = CLASSD_MR_PWMTYP_MASK; + val = pdata->pwm_type << CLASSD_MR_PWMTYP_SHIFT; + + mask |= CLASSD_MR_NON_OVERLAP_MASK; + if (pdata->non_overlap_enable) { + val |= (CLASSD_MR_NON_OVERLAP_EN + << CLASSD_MR_NON_OVERLAP_SHIFT); + + mask |= CLASSD_MR_NOVR_VAL_MASK; + switch (pdata->non_overlap_time) { + case 5: + val |= (CLASSD_MR_NOVR_VAL_5NS + << CLASSD_MR_NOVR_VAL_SHIFT); + break; + case 10: + val |= (CLASSD_MR_NOVR_VAL_10NS + << CLASSD_MR_NOVR_VAL_SHIFT); + break; + case 15: + val |= (CLASSD_MR_NOVR_VAL_15NS + << CLASSD_MR_NOVR_VAL_SHIFT); + break; + case 20: + val |= (CLASSD_MR_NOVR_VAL_20NS + << CLASSD_MR_NOVR_VAL_SHIFT); + break; + default: + val |= (CLASSD_MR_NOVR_VAL_10NS + << CLASSD_MR_NOVR_VAL_SHIFT); + break; + } + } + + snd_soc_update_bits(codec, CLASSD_MR, mask, val); + + dev_info(codec->dev, + "PWM modulation type is %s, non-overlapping is %s\n", + pwm_type[pdata->pwm_type], + pdata->non_overlap_enable?"enabled":"disabled"); + + return 0; +} + +static struct regmap *atmel_classd_codec_get_remap(struct device *dev) +{ + struct atmel_classd *dd = dev_get_drvdata(dev); + + return dd->regmap; +} + +static struct snd_soc_codec_driver soc_codec_dev_classd = { + .probe = atmel_classd_codec_probe, + .controls = atmel_classd_snd_controls, + .num_controls = ARRAY_SIZE(atmel_classd_snd_controls), + .get_regmap = atmel_classd_codec_get_remap, +}; + +static int atmel_classd_codec_dai_startup(struct snd_pcm_substream *substream, + struct snd_soc_dai *codec_dai) +{ + struct atmel_classd *dd = snd_soc_dai_get_drvdata(codec_dai); + + clk_prepare_enable(dd->aclk); + clk_prepare_enable(dd->gclk); + + return 0; +} + +static int atmel_classd_codec_dai_digital_mute(struct snd_soc_dai *codec_dai, + int mute) +{ + struct snd_soc_codec *codec = codec_dai->codec; + u32 mask, val; + + mask = CLASSD_MR_LMUTE_MASK | CLASSD_MR_RMUTE_MASK; + + if (mute) + val = mask; + else + val = 0; + + snd_soc_update_bits(codec, CLASSD_MR, mask, val); + + return 0; +} + +#define CLASSD_ACLK_RATE_11M2896_MPY_8 (112896 * 100 * 8) +#define CLASSD_ACLK_RATE_12M288_MPY_8 (12228 * 1000 * 8) + +static struct { + int rate; + int sample_rate; + int dsp_clk; + unsigned long aclk_rate; +} const sample_rates[] = { + { 8000, CLASSD_INTPMR_FRAME_8K, + CLASSD_INTPMR_DSP_CLK_FREQ_12M288, CLASSD_ACLK_RATE_12M288_MPY_8 }, + { 16000, CLASSD_INTPMR_FRAME_16K, + CLASSD_INTPMR_DSP_CLK_FREQ_12M288, CLASSD_ACLK_RATE_12M288_MPY_8 }, + { 32000, CLASSD_INTPMR_FRAME_32K, + CLASSD_INTPMR_DSP_CLK_FREQ_12M288, CLASSD_ACLK_RATE_12M288_MPY_8 }, + { 48000, CLASSD_INTPMR_FRAME_48K, + CLASSD_INTPMR_DSP_CLK_FREQ_12M288, CLASSD_ACLK_RATE_12M288_MPY_8 }, + { 96000, CLASSD_INTPMR_FRAME_96K, + CLASSD_INTPMR_DSP_CLK_FREQ_12M288, CLASSD_ACLK_RATE_12M288_MPY_8 }, + { 22050, CLASSD_INTPMR_FRAME_22K, + CLASSD_INTPMR_DSP_CLK_FREQ_11M2896, CLASSD_ACLK_RATE_11M2896_MPY_8 }, + { 44100, CLASSD_INTPMR_FRAME_44K, + CLASSD_INTPMR_DSP_CLK_FREQ_11M2896, CLASSD_ACLK_RATE_11M2896_MPY_8 }, + { 88200, CLASSD_INTPMR_FRAME_88K, + CLASSD_INTPMR_DSP_CLK_FREQ_11M2896, CLASSD_ACLK_RATE_11M2896_MPY_8 }, +}; + +static int +atmel_classd_codec_dai_hw_params(struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params, + struct snd_soc_dai *codec_dai) +{ + struct snd_soc_codec *codec = codec_dai->codec; + struct atmel_classd *dd = snd_soc_dai_get_drvdata(codec_dai); + int fs; + int i, best, best_val, cur_val, ret; + u32 mask, val; + + fs = params_rate(params); + + best = 0; + best_val = abs(fs - sample_rates[0].rate); + for (i = 1; i < ARRAY_SIZE(sample_rates); i++) { + /* Closest match */ + cur_val = abs(fs - sample_rates[i].rate); + if (cur_val < best_val) { + best = i; + best_val = cur_val; + } + } + + dev_dbg(codec->dev, + "Selected SAMPLE_RATE of %dHz, ACLK_RATE of %ldHz\n", + sample_rates[best].rate, sample_rates[best].aclk_rate); + + clk_disable_unprepare(dd->gclk); + clk_disable_unprepare(dd->aclk); + + ret = clk_set_rate(dd->aclk, sample_rates[best].aclk_rate); + if (ret) + return ret; + + mask = CLASSD_INTPMR_DSP_CLK_FREQ_MASK | CLASSD_INTPMR_FRAME_MASK; + val = (sample_rates[best].dsp_clk << CLASSD_INTPMR_DSP_CLK_FREQ_SHIFT) + | (sample_rates[best].sample_rate << CLASSD_INTPMR_FRAME_SHIFT); + + snd_soc_update_bits(codec, CLASSD_INTPMR, mask, val); + + clk_prepare_enable(dd->aclk); + clk_prepare_enable(dd->gclk); + + return 0; +} + +static void +atmel_classd_codec_dai_shutdown(struct snd_pcm_substream *substream, + struct snd_soc_dai *codec_dai) +{ + struct atmel_classd *dd = snd_soc_dai_get_drvdata(codec_dai); + + clk_disable_unprepare(dd->gclk); + clk_disable_unprepare(dd->aclk); +} + +static int atmel_classd_codec_dai_prepare(struct snd_pcm_substream *substream, + struct snd_soc_dai *codec_dai) +{ + struct snd_soc_codec *codec = codec_dai->codec; + + snd_soc_update_bits(codec, CLASSD_MR, + CLASSD_MR_LEN_MASK | CLASSD_MR_REN_MASK, + (CLASSD_MR_LEN_DIS << CLASSD_MR_LEN_SHIFT) + |(CLASSD_MR_REN_DIS << CLASSD_MR_REN_SHIFT)); + + return 0; +} + +static int atmel_classd_codec_dai_trigger(struct snd_pcm_substream *substream, + int cmd, struct snd_soc_dai *codec_dai) +{ + struct snd_soc_codec *codec = codec_dai->codec; + u32 mask, val; + + mask = CLASSD_MR_LEN_MASK | CLASSD_MR_REN_MASK; + + switch (cmd) { + case SNDRV_PCM_TRIGGER_START: + case SNDRV_PCM_TRIGGER_RESUME: + case SNDRV_PCM_TRIGGER_PAUSE_RELEASE: + val = mask; + break; + case SNDRV_PCM_TRIGGER_STOP: + case SNDRV_PCM_TRIGGER_SUSPEND: + case SNDRV_PCM_TRIGGER_PAUSE_PUSH: + val = (CLASSD_MR_LEN_DIS << CLASSD_MR_LEN_SHIFT) + | (CLASSD_MR_REN_DIS << CLASSD_MR_REN_SHIFT); + break; + default: + return -EINVAL; + } + + snd_soc_update_bits(codec, CLASSD_MR, mask, val); + + return 0; +} + +static const struct snd_soc_dai_ops atmel_classd_codec_dai_ops = { + .digital_mute = atmel_classd_codec_dai_digital_mute, + .startup = atmel_classd_codec_dai_startup, + .shutdown = atmel_classd_codec_dai_shutdown, + .hw_params = atmel_classd_codec_dai_hw_params, + .prepare = atmel_classd_codec_dai_prepare, + .trigger = atmel_classd_codec_dai_trigger, +}; + +#define ATMEL_CLASSD_CODEC_DAI_NAME "atmel-classd-hifi" + +static struct snd_soc_dai_driver atmel_classd_codec_dai = { + .name = ATMEL_CLASSD_CODEC_DAI_NAME, + .playback = { + .stream_name = "Playback", + .channels_min = 2, + .channels_max = 2, + .rates = ATMEL_CLASSD_RATES, + .formats = SNDRV_PCM_FMTBIT_S16_LE, + }, + .ops = &atmel_classd_codec_dai_ops, +}; + +/* regmap configuration */ +static const struct reg_default atmel_classd_reg_defaults[] = { + { CLASSD_INTPMR, 0x00302727 }, +}; + +#define ATMEL_CLASSD_REG_MAX 0xE4 +static const struct regmap_config atmel_classd_regmap_config = { + .reg_bits = 32, + .reg_stride = 4, + .val_bits = 32, + .max_register = ATMEL_CLASSD_REG_MAX, + + .cache_type = REGCACHE_FLAT, + .reg_defaults = atmel_classd_reg_defaults, + .num_reg_defaults = ARRAY_SIZE(atmel_classd_reg_defaults), +}; + +static int atmel_classd_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct atmel_classd *dd; + struct resource *res; + void __iomem *io_base; + const struct atmel_classd_pdata *pdata; + int ret; + + pdata = dev_get_platdata(dev); + if (!pdata) { + pdata = atmel_classd_dt_init(dev); + if (IS_ERR(pdata)) + return PTR_ERR(pdata); + } + + dd = devm_kzalloc(&pdev->dev, sizeof(*dd), GFP_KERNEL); + if (!dd) + return -ENOMEM; + + dd->pdata = pdata; + + platform_set_drvdata(pdev, dd); + + dd->irq = platform_get_irq(pdev, 0); + if (dd->irq < 0) { + ret = dd->irq; + dev_err(&pdev->dev, "failed to could not get irq: %d\n", ret); + return ret; + } + + dd->pclk = devm_clk_get(dev, "pclk"); + if (IS_ERR(dd->pclk)) { + ret = PTR_ERR(dd->pclk); + dev_err(dev, "failed to get peripheral clock: %d\n", ret); + return ret; + } + + dd->gclk = devm_clk_get(dev, "gclk"); + if (IS_ERR(dd->aclk)) { + ret = PTR_ERR(dd->gclk); + dev_err(dev, "failed to get GCK clock: %d\n", ret); + return ret; + } + + dd->aclk = devm_clk_get(dev, "aclk"); + if (IS_ERR(dd->aclk)) { + ret = PTR_ERR(dd->aclk); + dev_err(dev, "failed to get audio clock: %d\n", ret); + return ret; + } + + res = platform_get_resource(pdev, IORESOURCE_MEM, 0); + if (!res) { + dev_err(&pdev->dev, "no memory resource\n"); + return -ENXIO; + } + + io_base = devm_ioremap_resource(dev, res); + if (IS_ERR(io_base)) { + ret = PTR_ERR(io_base); + dev_err(dev, "failed to remap register memory: %d\n", ret); + return ret; + } + + dd->phy_base = res->start; + + dd->regmap = devm_regmap_init_mmio(dev, io_base, + &atmel_classd_regmap_config); + if (IS_ERR(dd->regmap)) { + ret = PTR_ERR(dd->regmap); + dev_err(dev, "failed to init register map: %d\n", ret); + return ret; + } + + ret = devm_snd_soc_register_component(&pdev->dev, + &atmel_classd_cpu_dai_component, + &atmel_classd_cpu_dai, 1); + if (ret) { + dev_err(dev, "Could not register CPU DAI: %d\n", ret); + return ret; + } + + ret = devm_snd_dmaengine_pcm_register(&pdev->dev, + &atmel_classd_dmaengine_pcm_config, + 0); + if (ret) { + dev_err(dev, "Could not register platform: %d\n", ret); + return ret; + } + + ret = snd_soc_register_codec(dev, &soc_codec_dev_classd, + &atmel_classd_codec_dai, 1); + if (ret) { + dev_err(dev, "Could not register codec: %d\n", ret); + return ret; + } + + dev_info(dev, + "Atmel Class D Amplifier (CLASSD) device at 0x%p (irq %d)\n", + io_base, dd->irq); + + return 0; +} + +static int atmel_classd_remove(struct platform_device *pdev) +{ + snd_soc_unregister_codec(&pdev->dev); + return 0; +} + +static struct platform_driver classd_driver = { + .driver = { + .name = "atmel-classd", + .owner = THIS_MODULE, + .of_match_table = of_match_ptr(atmel_classd_of_match), + }, + .probe = atmel_classd_probe, + .remove = atmel_classd_remove, +}; +module_platform_driver(classd_driver); + +static struct snd_soc_dai_link atmel_asoc_classd_dailink = { + .name = "CLASSD", + .stream_name = "CLASSD PCM", + .codec_dai_name = ATMEL_CLASSD_CODEC_DAI_NAME, +}; + +static struct snd_soc_card atmel_asoc_classd_card = { + .owner = THIS_MODULE, + .dai_link = &atmel_asoc_classd_dailink, + .num_links = 1, +}; + +#ifdef CONFIG_OF +static const struct of_device_id atmel_asoc_dt_ids[] = { + { .compatible = "atmel,asoc-classd", }, + { } +}; +#endif + +static int atmel_asoc_classd_dt_init(struct platform_device *pdev, + struct snd_soc_card *card) +{ + struct device_node *np = pdev->dev.of_node; + struct device_node *codec_np, *platform_np; + const char *cpu_dai_name; + struct snd_soc_dai_link *dailink = card->dai_link; + int ret; + + if (!np) { + dev_err(&pdev->dev, "only device tree supported\n"); + return -EINVAL; + } + + ret = snd_soc_of_parse_card_name(card, "atmel,model"); + if (ret) { + dev_err(&pdev->dev, "failed to parse card name\n"); + return ret; + } + + of_property_read_string(np, "atmel,audio-cpu-dai-name", &cpu_dai_name); + dailink->cpu_dai_name = cpu_dai_name; + + platform_np = of_parse_phandle(np, "atmel,audio-platform", 0); + if (!platform_np) { + dev_err(&pdev->dev, "failed to get platform info\n"); + ret = -EINVAL; + return ret; + } + dailink->platform_of_node = platform_np; + of_node_put(platform_np); + + codec_np = of_parse_phandle(np, "atmel,audio-codec", 0); + if (!codec_np) { + dev_err(&pdev->dev, "failed to get codec info\n"); + ret = -EINVAL; + return ret; + } + dailink->codec_of_node = codec_np; + of_node_put(codec_np); + + return 0; +} + +static int atmel_asoc_classd_probe(struct platform_device *pdev) +{ + struct snd_soc_card *card = &atmel_asoc_classd_card; + int ret; + + card->dev = &pdev->dev; + ret = atmel_asoc_classd_dt_init(pdev, card); + if (ret) { + dev_err(&pdev->dev, "failed to init dt info\n"); + return ret; + } + + ret = devm_snd_soc_register_card(&pdev->dev, card); + if (ret) { + dev_err(&pdev->dev, "failed to register asoc card\n"); + return ret; + } + + return 0; +} + +static struct platform_driver atmel_asoc_classd_driver = { + .driver = { + .name = "atmel-asoc-audio", + .of_match_table = of_match_ptr(atmel_asoc_dt_ids), + .pm = &snd_soc_pm_ops, + }, + .probe = atmel_asoc_classd_probe, +}; + +module_platform_driver(atmel_asoc_classd_driver); + +MODULE_DESCRIPTION("Atmel ClassD driver under ALSA SoC architecture"); +MODULE_AUTHOR("Songjun Wu songjun.wu@atmel.com"); +MODULE_LICENSE("GPL"); diff --git a/sound/soc/atmel/atmel-classd.h b/sound/soc/atmel/atmel-classd.h new file mode 100644 index 0000000..8b4e018 --- /dev/null +++ b/sound/soc/atmel/atmel-classd.h @@ -0,0 +1,120 @@ +#ifndef __ATMEL_CLASSD_H_ +#define __ATMEL_CLASSD_H_ + +#define CLASSD_CR 0x00000000 +#define CLASSD_CR_RESET 0x1 + +#define CLASSD_MR 0x00000004 + +#define CLASSD_MR_LEN_DIS 0x0 +#define CLASSD_MR_LEN_EN 0x1 +#define CLASSD_MR_LEN_MASK (0x1 << 0) +#define CLASSD_MR_LEN_SHIFT (0) + +#define CLASSD_MR_LMUTE_DIS 0x0 +#define CLASSD_MR_LMUTE_EN 0x1 +#define CLASSD_MR_LMUTE_SHIFT (0x1) +#define CLASSD_MR_LMUTE_MASK (0x1 << 1) + +#define CLASSD_MR_REN_DIS 0x0 +#define CLASSD_MR_REN_EN 0x1 +#define CLASSD_MR_REN_MASK (0x1 << 4) +#define CLASSD_MR_REN_SHIFT (4) + +#define CLASSD_MR_RMUTE_DIS 0x0 +#define CLASSD_MR_RMUTE_EN 0x1 +#define CLASSD_MR_RMUTE_SHIFT (0x5) +#define CLASSD_MR_RMUTE_MASK (0x1 << 5) + +#define CLASSD_MR_PWMTYP_SINGLE 0x0 +#define CLASSD_MR_PWMTYP_DIFF 0x1 +#define CLASSD_MR_PWMTYP_MASK (0x1 << 8) +#define CLASSD_MR_PWMTYP_SHIFT (8) + +#define CLASSD_MR_NON_OVERLAP_DIS 0x0 +#define CLASSD_MR_NON_OVERLAP_EN 0x1 +#define CLASSD_MR_NON_OVERLAP_MASK (0x1 << 16) +#define CLASSD_MR_NON_OVERLAP_SHIFT (16) + +#define CLASSD_MR_NOVR_VAL_5NS 0x0 +#define CLASSD_MR_NOVR_VAL_10NS 0x1 +#define CLASSD_MR_NOVR_VAL_15NS 0x2 +#define CLASSD_MR_NOVR_VAL_20NS 0x3 +#define CLASSD_MR_NOVR_VAL_MASK (0x3 << 20) +#define CLASSD_MR_NOVR_VAL_SHIFT (20) + +#define CLASSD_INTPMR 0x00000008 + +#define CLASSD_INTPMR_ATTL_MASK (0x3f << 0) +#define CLASSD_INTPMR_ATTL_SHIFT 0 +#define CLASSD_INTPMR_ATTR_MASK (0x3f << 8) +#define CLASSD_INTPMR_ATTR_SHIFT 8 + +#define CLASSD_INTPMR_DSP_CLK_FREQ_12M288 0x0 +#define CLASSD_INTPMR_DSP_CLK_FREQ_11M2896 0x1 +#define CLASSD_INTPMR_DSP_CLK_FREQ_MASK (0x1 << 16) +#define CLASSD_INTPMR_DSP_CLK_FREQ_SHIFT (16) + +#define CLASSD_INTPMR_DEEMP_DIS 0x0 +#define CLASSD_INTPMR_DEEMP_EN 0x1 +#define CLASSD_INTPMR_DEEMP_MASK (0x1 << 18) +#define CLASSD_INTPMR_DEEMP_SHIFT (18) + +#define CLASSD_INTPMR_SWAP_LEFT_ON_LSB 0x0 +#define CLASSD_INTPMR_SWAP_RIGHT_ON_LSB 0x1 +#define CLASSD_INTPMR_SWAP_MASK (0x1 << 19) +#define CLASSD_INTPMR_SWAP_SHIFT (19) + +#define CLASSD_INTPMR_FRAME_8K 0x0 +#define CLASSD_INTPMR_FRAME_16K 0x1 +#define CLASSD_INTPMR_FRAME_32K 0x2 +#define CLASSD_INTPMR_FRAME_48K 0x3 +#define CLASSD_INTPMR_FRAME_96K 0x4 +#define CLASSD_INTPMR_FRAME_22K 0x5 +#define CLASSD_INTPMR_FRAME_44K 0x6 +#define CLASSD_INTPMR_FRAME_88K 0x7 +#define CLASSD_INTPMR_FRAME_MASK (0x7 << 20) +#define CLASSD_INTPMR_FRAME_SHIFT (20) + +#define CLASSD_INTPMR_EQCFG_FLAT 0x0 +#define CLASSD_INTPMR_EQCFG_B_BOOST_12 0x1 +#define CLASSD_INTPMR_EQCFG_B_BOOST_6 0x2 +#define CLASSD_INTPMR_EQCFG_B_CUT_12 0x3 +#define CLASSD_INTPMR_EQCFG_B_CUT_6 0x4 +#define CLASSD_INTPMR_EQCFG_M_BOOST_3 0x5 +#define CLASSD_INTPMR_EQCFG_M_BOOST_8 0x6 +#define CLASSD_INTPMR_EQCFG_M_CUT_3 0x7 +#define CLASSD_INTPMR_EQCFG_M_CUT_8 0x8 +#define CLASSD_INTPMR_EQCFG_T_BOOST_12 0x9 +#define CLASSD_INTPMR_EQCFG_T_BOOST_6 0xa +#define CLASSD_INTPMR_EQCFG_T_CUT_12 0xb +#define CLASSD_INTPMR_EQCFG_T_CUT_6 0xc +#define CLASSD_INTPMR_EQCFG_SHIFT (24) + +#define CLASSD_INTPMR_MONO_DIS 0x0 +#define CLASSD_INTPMR_MONO_EN 0x1 +#define CLASSD_INTPMR_MONO_MASK (0x1 << 28) +#define CLASSD_INTPMR_MONO_SHIFT (28) + +#define CLASSD_INTPMR_MONO_MODE_MIX 0x0 +#define CLASSD_INTPMR_MONO_MODE_SAT 0x1 +#define CLASSD_INTPMR_MONO_MODE_LEFT 0x2 +#define CLASSD_INTPMR_MONO_MODE_RIGHT 0x3 +#define CLASSD_INTPMR_MONO_MODE_MASK (0x3 << 29) +#define CLASSD_INTPMR_MONO_MODE_SHIFT (29) + +#define CLASSD_INTSR 0x0000000c + +#define CLASSD_THR 0x00000010 + +#define CLASSD_IER 0x00000014 + +#define CLASSD_IDR 0x00000018 + +#define CLASSD_IMR 0x0000001c + +#define CLASSD_ISR 0x00000020 + +#define CLASSD_WPMR 0x000000e4 + +#endif
On Tue, Sep 01, 2015 at 01:41:40PM +0800, Songjun Wu wrote:
+static const char * const mono_text[] = {
- "stereo", "mono"
+};
+static SOC_ENUM_SINGLE_DECL(classd_mono_enum,
CLASSD_INTPMR, CLASSD_INTPMR_MONO_SHIFT,
mono_text);
This looks like it should be a simple Switch control called something like "Stereo Switch" or "Mono Switch".
+static const char * const deemp_text[] = {
- "disabled", "enabled"
+};
+static SOC_ENUM_SINGLE_DECL(classd_deemp_enum,
CLASSD_INTPMR, CLASSD_INTPMR_DEEMP_SHIFT,
deemp_text);
Similarly this looks like it should be "Deemph Switch".
+static const char * const eqcfg_bass_text[] = {
- "-12 dB", "-6 dB", "0 dB", "+6 dB", "+12 dB"
+};
+static const unsigned int eqcfg_bass_value[] = {
- CLASSD_INTPMR_EQCFG_B_CUT_12,
- CLASSD_INTPMR_EQCFG_B_CUT_6, CLASSD_INTPMR_EQCFG_FLAT,
- CLASSD_INTPMR_EQCFG_B_BOOST_6, CLASSD_INTPMR_EQCFG_B_BOOST_12
+};
This should be a Volume control with TLV information, as should the following few controls.
+static const struct snd_kcontrol_new atmel_classd_snd_controls[] = { +SOC_SINGLE_TLV("Left Volume", CLASSD_INTPMR,
CLASSD_INTPMR_ATTL_SHIFT, 78, 1, classd_digital_tlv),
+SOC_SINGLE_TLV("Right Volume", CLASSD_INTPMR,
CLASSD_INTPMR_ATTR_SHIFT, 78, 1, classd_digital_tlv),
This should be a single stereo control rather than separate left and right controls.
+static const char * const pwm_type[] = {
- "single-ended", "differential"
+};
The normal style for ALSA controls is to capitalise strings so "Single ended" and "Differential".
- if (pdata->non_overlap_enable) {
val |= (CLASSD_MR_NON_OVERLAP_EN
<< CLASSD_MR_NON_OVERLAP_SHIFT);
mask |= CLASSD_MR_NOVR_VAL_MASK;
switch (pdata->non_overlap_time) {
case 5:
val |= (CLASSD_MR_NOVR_VAL_5NS
<< CLASSD_MR_NOVR_VAL_SHIFT);
break;
case 10:
val |= (CLASSD_MR_NOVR_VAL_10NS
<< CLASSD_MR_NOVR_VAL_SHIFT);
break;
case 15:
val |= (CLASSD_MR_NOVR_VAL_15NS
<< CLASSD_MR_NOVR_VAL_SHIFT);
break;
case 20:
val |= (CLASSD_MR_NOVR_VAL_20NS
<< CLASSD_MR_NOVR_VAL_SHIFT);
break;
default:
val |= (CLASSD_MR_NOVR_VAL_10NS
<< CLASSD_MR_NOVR_VAL_SHIFT);
break;
}
I'd expect at least a warning if the user trys to specify an invalid value (if they didn't specify any value then I'd expect the DT parsing function to assign the default value).
+static struct regmap *atmel_classd_codec_get_remap(struct device *dev) +{
- struct atmel_classd *dd = dev_get_drvdata(dev);
- return dd->regmap;
+}
Can you just use dev_get_regmap()?
+static int atmel_classd_codec_dai_startup(struct snd_pcm_substream *substream,
struct snd_soc_dai *codec_dai)
+{
- struct atmel_classd *dd = snd_soc_dai_get_drvdata(codec_dai);
- clk_prepare_enable(dd->aclk);
- clk_prepare_enable(dd->gclk);
Should check for errors here.
- dev_info(dev,
"Atmel Class D Amplifier (CLASSD) device at 0x%p (irq %d)\n",
io_base, dd->irq);
This is a bit noisy and not really based on interaction with the hardware... dev_dbg() seems better.
On 9/3/2015 19:37, Mark Brown wrote:
On Tue, Sep 01, 2015 at 01:41:40PM +0800, Songjun Wu wrote:
+static const char * const mono_text[] = {
- "stereo", "mono"
+};
+static SOC_ENUM_SINGLE_DECL(classd_mono_enum,
CLASSD_INTPMR, CLASSD_INTPMR_MONO_SHIFT,
mono_text);
This looks like it should be a simple Switch control called something like "Stereo Switch" or "Mono Switch".
Thank you, the code will be modified as you suggest.
+static const char * const deemp_text[] = {
- "disabled", "enabled"
+};
+static SOC_ENUM_SINGLE_DECL(classd_deemp_enum,
CLASSD_INTPMR, CLASSD_INTPMR_DEEMP_SHIFT,
deemp_text);
Similarly this looks like it should be "Deemph Switch".
Yes, it also will be modified.
+static const char * const eqcfg_bass_text[] = {
- "-12 dB", "-6 dB", "0 dB", "+6 dB", "+12 dB"
+};
+static const unsigned int eqcfg_bass_value[] = {
- CLASSD_INTPMR_EQCFG_B_CUT_12,
- CLASSD_INTPMR_EQCFG_B_CUT_6, CLASSD_INTPMR_EQCFG_FLAT,
- CLASSD_INTPMR_EQCFG_B_BOOST_6, CLASSD_INTPMR_EQCFG_B_BOOST_12
+};
This should be a Volume control with TLV information, as should the following few controls.
The Volume control with TLV information is not suitable for this case. Bass, Medium, and treble are mutually exclusive. So I think the SOC_ENUM control is suitable for this case. The register layout is not very good, The register is defined as below. • EQCFG: Equalization Selection Value Name Description 0 FLAT Flat Response 1 BBOOST12 Bass boost +12 dB 2 BBOOST6 Bass boost +6 dB 3 BCUT12 Bass cut -12 dB 4 BCUT6 Bass cut -6 dB 5 MBOOST3 Medium boost +3 dB 6 MBOOST8 Medium boost +8 dB 7 MCUT3 Medium cut -3 dB 8 MCUT8 Medium cut -8 dB 9 TBOOST12 Treble boost +12 dB 10 TBOOST6 Treble boost +6 dB 11 TCUT12 Treble cut -12 dB 12 TCUT6 Treble cut -6 dB
+static const struct snd_kcontrol_new atmel_classd_snd_controls[] = { +SOC_SINGLE_TLV("Left Volume", CLASSD_INTPMR,
CLASSD_INTPMR_ATTL_SHIFT, 78, 1, classd_digital_tlv),
+SOC_SINGLE_TLV("Right Volume", CLASSD_INTPMR,
CLASSD_INTPMR_ATTR_SHIFT, 78, 1, classd_digital_tlv),
This should be a single stereo control rather than separate left and right controls.
Since the classD IP defines two register fields to control left volume and right volume respectively, I think it's better to provide two controls to user.
+static const char * const pwm_type[] = {
- "single-ended", "differential"
+};
The normal style for ALSA controls is to capitalise strings so "Single ended" and "Differential".
Accept. It will be modified.
- if (pdata->non_overlap_enable) {
val |= (CLASSD_MR_NON_OVERLAP_EN
<< CLASSD_MR_NON_OVERLAP_SHIFT);
mask |= CLASSD_MR_NOVR_VAL_MASK;
switch (pdata->non_overlap_time) {
case 5:
val |= (CLASSD_MR_NOVR_VAL_5NS
<< CLASSD_MR_NOVR_VAL_SHIFT);
break;
case 10:
val |= (CLASSD_MR_NOVR_VAL_10NS
<< CLASSD_MR_NOVR_VAL_SHIFT);
break;
case 15:
val |= (CLASSD_MR_NOVR_VAL_15NS
<< CLASSD_MR_NOVR_VAL_SHIFT);
break;
case 20:
val |= (CLASSD_MR_NOVR_VAL_20NS
<< CLASSD_MR_NOVR_VAL_SHIFT);
break;
default:
val |= (CLASSD_MR_NOVR_VAL_10NS
<< CLASSD_MR_NOVR_VAL_SHIFT);
break;
}
I'd expect at least a warning if the user trys to specify an invalid value (if they didn't specify any value then I'd expect the DT parsing function to assign the default value).
Accept. A warning will be added if the user trys to sepcify an invalid value. This function is optional, So if the user did not specify any value, this function will be disabled.
+static struct regmap *atmel_classd_codec_get_remap(struct device *dev) +{
- struct atmel_classd *dd = dev_get_drvdata(dev);
- return dd->regmap;
+}
Can you just use dev_get_regmap()?
Thank you for your reminder, it's better to use dev_get_regmap(). The code will be modified.
+static int atmel_classd_codec_dai_startup(struct snd_pcm_substream *substream,
struct snd_soc_dai *codec_dai)
+{
- struct atmel_classd *dd = snd_soc_dai_get_drvdata(codec_dai);
- clk_prepare_enable(dd->aclk);
- clk_prepare_enable(dd->gclk);
Should check for errors here.
Accept.
- dev_info(dev,
"Atmel Class D Amplifier (CLASSD) device at 0x%p (irq %d)\n",
io_base, dd->irq);
This is a bit noisy and not really based on interaction with the hardware... dev_dbg() seems better.
This information will occur only once when linux kernel starts. It shows the classD is loaded to linux kernel. I think it's better to provide more information to user.
On Sun, Sep 06, 2015 at 05:44:21PM +0800, Wu, Songjun wrote:
On 9/3/2015 19:37, Mark Brown wrote:
On Tue, Sep 01, 2015 at 01:41:40PM +0800, Songjun Wu wrote:
+static const char * const eqcfg_bass_text[] = {
- "-12 dB", "-6 dB", "0 dB", "+6 dB", "+12 dB"
+};
+static const unsigned int eqcfg_bass_value[] = {
- CLASSD_INTPMR_EQCFG_B_CUT_12,
- CLASSD_INTPMR_EQCFG_B_CUT_6, CLASSD_INTPMR_EQCFG_FLAT,
- CLASSD_INTPMR_EQCFG_B_BOOST_6, CLASSD_INTPMR_EQCFG_B_BOOST_12
+};
This should be a Volume control with TLV information, as should the following few controls.
The Volume control with TLV information is not suitable for this case. Bass, Medium, and treble are mutually exclusive. So I think the SOC_ENUM control is suitable for this case. The register layout is not very good, The register is defined as below. • EQCFG: Equalization Selection Value Name Description 0 FLAT Flat Response 1 BBOOST12 Bass boost +12 dB 2 BBOOST6 Bass boost +6 dB 3 BCUT12 Bass cut -12 dB 4 BCUT6 Bass cut -6 dB 5 MBOOST3 Medium boost +3 dB 6 MBOOST8 Medium boost +8 dB 7 MCUT3 Medium cut -3 dB 8 MCUT8 Medium cut -8 dB 9 TBOOST12 Treble boost +12 dB 10 TBOOST6 Treble boost +6 dB 11 TCUT12 Treble cut -12 dB 12 TCUT6 Treble cut -6 dB
OK, so that's not actually what the code was doing - it had separate enums for bass, mid and treble. If you make this a single enum with all the above options in it that seems like the best way of handling things.
+static const struct snd_kcontrol_new atmel_classd_snd_controls[] = { +SOC_SINGLE_TLV("Left Volume", CLASSD_INTPMR,
CLASSD_INTPMR_ATTL_SHIFT, 78, 1, classd_digital_tlv),
+SOC_SINGLE_TLV("Right Volume", CLASSD_INTPMR,
CLASSD_INTPMR_ATTR_SHIFT, 78, 1, classd_digital_tlv),
This should be a single stereo control rather than separate left and right controls.
Since the classD IP defines two register fields to control left volume and right volume respectively, I think it's better to provide two controls to user.
No, this is really common, we combine them in Linux to present a consistent interface to userspace.
- dev_info(dev,
"Atmel Class D Amplifier (CLASSD) device at 0x%p (irq %d)\n",
io_base, dd->irq);
This is a bit noisy and not really based on interaction with the hardware... dev_dbg() seems better.
This information will occur only once when linux kernel starts. It shows the classD is loaded to linux kernel. I think it's better to provide more information to user.
This stuff all adds up and since it'll go out on the console by default it both makes things more noisy and slows down boot - printing on the serial port isn't free. If we want to have this sort of information we printed we should really do it in the driver core so it appears consistently for all devices rather than having individual code in each driver.
On 9/8/2015 00:23, Mark Brown wrote:
On Sun, Sep 06, 2015 at 05:44:21PM +0800, Wu, Songjun wrote:
On 9/3/2015 19:37, Mark Brown wrote:
On Tue, Sep 01, 2015 at 01:41:40PM +0800, Songjun Wu wrote:
+static const char * const eqcfg_bass_text[] = {
- "-12 dB", "-6 dB", "0 dB", "+6 dB", "+12 dB"
+};
+static const unsigned int eqcfg_bass_value[] = {
- CLASSD_INTPMR_EQCFG_B_CUT_12,
- CLASSD_INTPMR_EQCFG_B_CUT_6, CLASSD_INTPMR_EQCFG_FLAT,
- CLASSD_INTPMR_EQCFG_B_BOOST_6, CLASSD_INTPMR_EQCFG_B_BOOST_12
+};
This should be a Volume control with TLV information, as should the following few controls.
The Volume control with TLV information is not suitable for this case. Bass, Medium, and treble are mutually exclusive. So I think the SOC_ENUM control is suitable for this case. The register layout is not very good, The register is defined as below. • EQCFG: Equalization Selection Value Name Description 0 FLAT Flat Response 1 BBOOST12 Bass boost +12 dB 2 BBOOST6 Bass boost +6 dB 3 BCUT12 Bass cut -12 dB 4 BCUT6 Bass cut -6 dB 5 MBOOST3 Medium boost +3 dB 6 MBOOST8 Medium boost +8 dB 7 MCUT3 Medium cut -3 dB 8 MCUT8 Medium cut -8 dB 9 TBOOST12 Treble boost +12 dB 10 TBOOST6 Treble boost +6 dB 11 TCUT12 Treble cut -12 dB 12 TCUT6 Treble cut -6 dB
OK, so that's not actually what the code was doing - it had separate enums for bass, mid and treble. If you make this a single enum with all the above options in it that seems like the best way of handling things.
A single enum seems not very friendly to user, there are tree EQs, bass, medium and treble. So I create tree enum controls to control three EQs. The 'get' function is replaced by 'classd_get_eq_enum', if user operates one of the tree EQ controls, the other two EQs will show 0 dB.
+static const struct snd_kcontrol_new atmel_classd_snd_controls[] = { +SOC_SINGLE_TLV("Left Volume", CLASSD_INTPMR,
CLASSD_INTPMR_ATTL_SHIFT, 78, 1, classd_digital_tlv),
+SOC_SINGLE_TLV("Right Volume", CLASSD_INTPMR,
CLASSD_INTPMR_ATTR_SHIFT, 78, 1, classd_digital_tlv),
This should be a single stereo control rather than separate left and right controls.
Since the classD IP defines two register fields to control left volume and right volume respectively, I think it's better to provide two controls to user.
No, this is really common, we combine them in Linux to present a consistent interface to userspace.
I think carefully, your suggestion is reasonable. The code will be modified, combine the left and right to a single stereo control. Thank you.
- dev_info(dev,
"Atmel Class D Amplifier (CLASSD) device at 0x%p (irq %d)\n",
io_base, dd->irq);
This is a bit noisy and not really based on interaction with the hardware... dev_dbg() seems better.
This information will occur only once when linux kernel starts. It shows the classD is loaded to linux kernel. I think it's better to provide more information to user.
This stuff all adds up and since it'll go out on the console by default it both makes things more noisy and slows down boot - printing on the serial port isn't free. If we want to have this sort of information we printed we should really do it in the driver core so it appears consistently for all devices rather than having individual code in each driver.
Accept, the code will be modified to dev_dbg().
On Tue, Sep 08, 2015 at 05:36:01PM +0800, Wu, Songjun wrote:
On 9/8/2015 00:23, Mark Brown wrote:
OK, so that's not actually what the code was doing - it had separate enums for bass, mid and treble. If you make this a single enum with all the above options in it that seems like the best way of handling things.
A single enum seems not very friendly to user, there are tree EQs, bass, medium and treble. So I create tree enum controls to control three EQs. The 'get' function is replaced by 'classd_get_eq_enum', if user operates one of the tree EQ controls, the other two EQs will show 0 dB.
If you want to have three controls you need to write code so that the user can only change one of them from 0dB at once, returning an error otherwise. That was why it looked like they were three separate controls.
On 9/8/2015 20:23, Mark Brown wrote:
On Tue, Sep 08, 2015 at 05:36:01PM +0800, Wu, Songjun wrote:
On 9/8/2015 00:23, Mark Brown wrote:
OK, so that's not actually what the code was doing - it had separate enums for bass, mid and treble. If you make this a single enum with all the above options in it that seems like the best way of handling things.
A single enum seems not very friendly to user, there are tree EQs, bass, medium and treble. So I create tree enum controls to control three EQs. The 'get' function is replaced by 'classd_get_eq_enum', if user operates one of the tree EQ controls, the other two EQs will show 0 dB.
If you want to have three controls you need to write code so that the user can only change one of them from 0dB at once, returning an error otherwise. That was why it looked like they were three separate controls.
If user operates two or tree controls at the same time, for my understanding, these operations are serial actually in kernel, not parallel, and the last operation will be effective. I only write the function 'classd_get_eq_enum' to get the enumeration value, if user changes one of controls, the other controls will get 0dB. Is my understanding correct?
On Wed, Sep 09, 2015 at 11:16:08AM +0800, Wu, Songjun wrote:
On 9/8/2015 20:23, Mark Brown wrote:
If you want to have three controls you need to write code so that the user can only change one of them from 0dB at once, returning an error otherwise. That was why it looked like they were three separate controls.
If user operates two or tree controls at the same time, for my understanding, these operations are serial actually in kernel, not parallel, and the last operation will be effective. I only write the function 'classd_get_eq_enum' to get the enumeration value, if user changes one of controls, the other controls will get 0dB. Is my understanding correct?
Yes, that's what's going to end up happening but it's not how controls are expected to behave - applications will expect changing one control to leave others unaffected so it's better to return an error rather than change the other control.
On 9/9/2015 17:52, Mark Brown wrote:
On Wed, Sep 09, 2015 at 11:16:08AM +0800, Wu, Songjun wrote:
On 9/8/2015 20:23, Mark Brown wrote:
If you want to have three controls you need to write code so that the user can only change one of them from 0dB at once, returning an error otherwise. That was why it looked like they were three separate controls.
If user operates two or tree controls at the same time, for my understanding, these operations are serial actually in kernel, not parallel, and the last operation will be effective. I only write the function 'classd_get_eq_enum' to get the enumeration value, if user changes one of controls, the other controls will get 0dB. Is my understanding correct?
Yes, that's what's going to end up happening but it's not how controls are expected to behave - applications will expect changing one control to leave others unaffected so it's better to return an error rather than change the other control.
If application change non EQ controls, the others will be unaffected. But the classD IP can only supports one EQ control at once, these three EQ controls point to the same register field, if application set a different EQ control, the error occurs, there will be many errors, it's not very reasonable to application. The best way I think is if application set one EQ control, the other EQ controls will change to 0dB, it's also consistent with fact.
On Thu, Sep 10, 2015 at 10:31:04AM +0800, Wu, Songjun wrote:
On 9/9/2015 17:52, Mark Brown wrote:
Yes, that's what's going to end up happening but it's not how controls are expected to behave - applications will expect changing one control to leave others unaffected so it's better to return an error rather than change the other control.
If application change non EQ controls, the others will be unaffected. But the classD IP can only supports one EQ control at once, these three EQ controls point to the same register field, if application set a different EQ control, the error occurs, there will be many errors, it's not very reasonable to application. The best way I think is if application set one EQ control, the other EQ controls will change to 0dB, it's also consistent with fact.
There's no really good solutions here - this is why my initial suggestion was to have a single enumerated control.
On 9/11/2015 18:34, Mark Brown wrote:
On Thu, Sep 10, 2015 at 10:31:04AM +0800, Wu, Songjun wrote:
On 9/9/2015 17:52, Mark Brown wrote:
Yes, that's what's going to end up happening but it's not how controls are expected to behave - applications will expect changing one control to leave others unaffected so it's better to return an error rather than change the other control.
If application change non EQ controls, the others will be unaffected. But the classD IP can only supports one EQ control at once, these three EQ controls point to the same register field, if application set a different EQ control, the error occurs, there will be many errors, it's not very reasonable to application. The best way I think is if application set one EQ control, the other EQ controls will change to 0dB, it's also consistent with fact.
There's no really good solutions here - this is why my initial suggestion was to have a single enumerated control.
You are right, your suggestion is reasonable, to have a single enumerated control. The second version will be made and sent soon.
DT binding documentation for this new ASoC driver.
Signed-off-by: Songjun Wu songjun.wu@atmel.com --- .../devicetree/bindings/sound/atmel-classd.txt | 73 ++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 Documentation/devicetree/bindings/sound/atmel-classd.txt
diff --git a/Documentation/devicetree/bindings/sound/atmel-classd.txt b/Documentation/devicetree/bindings/sound/atmel-classd.txt new file mode 100644 index 0000000..29d181a --- /dev/null +++ b/Documentation/devicetree/bindings/sound/atmel-classd.txt @@ -0,0 +1,73 @@ +* Atmel ClassD driver under ALSA SoC architecture + +* Atmel ClassD driver +Required properties: +- compatible + Should be "atmel,sama5d2-classd". +- reg + Should contain ClassD registers location and length. +- interrupts + Should contain the IRQ line for the ClassD. +- dmas + One DMA specifiers as described in atmel-dma.txt and dma.txt files. +- dma-names + Must be "tx". +- clock-names + tuple listing input clock names. + Required elements: "pclk", "gclk" and "aclk". +- clocks + phandles to input clocks. + +Optional properties: +- pinctrl-names, pinctrl-0 + Please refer to pinctrl-bindings.txt. +- atmel,pwm-type + PWM modulation type, "single" or "diff". +- atmel,non-overlap-time + Set non-overlapping time, the unit is nanosecond(ns). + There are four values, + <5>, <10>, <15>, <20>, the default value is <10>. + Non-overlapping will be disabled if not specified. + +Example: +classd: classd@fc048000 { + compatible = "atmel,sama5d2-classd"; + reg = <0xfc048000 0x100>; + interrupts = <59 IRQ_TYPE_LEVEL_HIGH 7>; + dmas = <&dma0 + (AT91_XDMAC_DT_MEM_IF(0) | AT91_XDMAC_DT_PER_IF(1) + | AT91_XDMAC_DT_PERID(47))>; + dma-names = "tx"; + clocks = <&classd_clk>, <&classd_gclk>, <&audio_pll_pmc>; + clock-names = "pclk", "gclk", "aclk"; + + pinctrl-names = "default"; + pinctrl-0 = <&pinctrl_classd_default>; + atmel,pwm-type = "diff"; + atmel,non-overlap-time = <10>; +}; + + +* Atmel ASoC driver with ClassD +Required properties: +- compatible + Should be "atmel,asoc-classd". +- atmel,model + The user-visible name of this sound complex. +- atmel,audio-platform + Should be <&classd>. +- atmel,audio-cpu-dai-name + The name of the CPU DAI in ALSA SoC architecture. + The format is <classd registers location>.classd. +- atmel,audio-codec + Should be <&classd>. + +Example: +sound { + compatible = "atmel,asoc-classd"; + + atmel,model = "classd @ SAMA5D2-Xplained"; + atmel,audio-platform = <&classd>; + atmel,audio-cpu-dai-name = "fc048000.classd"; + atmel,audio-codec = <&classd>; +};
On Tue, Sep 01, 2015 at 01:41:41PM +0800, Songjun Wu wrote:
+classd: classd@fc048000 {
compatible = "atmel,sama5d2-classd";
reg = <0xfc048000 0x100>;
interrupts = <59 IRQ_TYPE_LEVEL_HIGH 7>;
dmas = <&dma0
(AT91_XDMAC_DT_MEM_IF(0) | AT91_XDMAC_DT_PER_IF(1)
| AT91_XDMAC_DT_PERID(47))>;
dma-names = "tx";
clocks = <&classd_clk>, <&classd_gclk>, <&audio_pll_pmc>;
clock-names = "pclk", "gclk", "aclk";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_classd_default>;
atmel,pwm-type = "diff";
atmel,non-overlap-time = <10>;
+};
+Example: +sound {
compatible = "atmel,asoc-classd";
atmel,model = "classd @ SAMA5D2-Xplained";
atmel,audio-platform = <&classd>;
atmel,audio-cpu-dai-name = "fc048000.classd";
atmel,audio-codec = <&classd>;
+};
Why is this a separate DT node? It seems that this IP is entirely self contained so I'm not clear why we need a separate node for the card, the card is usually a separate node because it ties together multiple different devices in the system but that's not the case here.
On 9/3/2015 19:43, Mark Brown wrote:
On Tue, Sep 01, 2015 at 01:41:41PM +0800, Songjun Wu wrote:
+classd: classd@fc048000 {
compatible = "atmel,sama5d2-classd";
reg = <0xfc048000 0x100>;
interrupts = <59 IRQ_TYPE_LEVEL_HIGH 7>;
dmas = <&dma0
(AT91_XDMAC_DT_MEM_IF(0) | AT91_XDMAC_DT_PER_IF(1)
| AT91_XDMAC_DT_PERID(47))>;
dma-names = "tx";
clocks = <&classd_clk>, <&classd_gclk>, <&audio_pll_pmc>;
clock-names = "pclk", "gclk", "aclk";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_classd_default>;
atmel,pwm-type = "diff";
atmel,non-overlap-time = <10>;
+};
+Example: +sound {
compatible = "atmel,asoc-classd";
atmel,model = "classd @ SAMA5D2-Xplained";
atmel,audio-platform = <&classd>;
atmel,audio-cpu-dai-name = "fc048000.classd";
atmel,audio-codec = <&classd>;
+};
Why is this a separate DT node? It seems that this IP is entirely self contained so I'm not clear why we need a separate node for the card, the card is usually a separate node because it ties together multiple different devices in the system but that's not the case here.
The classD can finish the audio function without other devices. But I want to reuse the code in ASoC, leave many things(like creating PCM, DMA operations) to ASoC, then the driver can only focus on how to configure classD. The classD IP is divided to tree parts logically, platform, CPU dai, and codec, and these parts are registered to ASoC.
This separate DT node is needed in ASoC, ties these tree parts in ClassD.
On Sun, Sep 06, 2015 at 05:44:30PM +0800, Wu, Songjun wrote:
On 9/3/2015 19:43, Mark Brown wrote:
Why is this a separate DT node? It seems that this IP is entirely self contained so I'm not clear why we need a separate node for the card, the card is usually a separate node because it ties together multiple different devices in the system but that's not the case here.
The classD can finish the audio function without other devices. But I want to reuse the code in ASoC, leave many things(like creating PCM, DMA operations) to ASoC, then the driver can only focus on how to configure classD. The classD IP is divided to tree parts logically, platform, CPU dai, and codec, and these parts are registered to ASoC.
This separate DT node is needed in ASoC, ties these tree parts in ClassD.
Sure, there's no problem at all having that structure in software but it should be possible to do this without having to represent this structure in DT. It should be possible to register the card at the same time as the rest of the components rather than needing the separate device in the DT.
On 9/8/2015 00:25, Mark Brown wrote:
On Sun, Sep 06, 2015 at 05:44:30PM +0800, Wu, Songjun wrote:
On 9/3/2015 19:43, Mark Brown wrote:
Why is this a separate DT node? It seems that this IP is entirely self contained so I'm not clear why we need a separate node for the card, the card is usually a separate node because it ties together multiple different devices in the system but that's not the case here.
The classD can finish the audio function without other devices. But I want to reuse the code in ASoC, leave many things(like creating PCM, DMA operations) to ASoC, then the driver can only focus on how to configure classD. The classD IP is divided to tree parts logically, platform, CPU dai, and codec, and these parts are registered to ASoC.
This separate DT node is needed in ASoC, ties these tree parts in ClassD.
Sure, there's no problem at all having that structure in software but it should be possible to do this without having to represent this structure in DT. It should be possible to register the card at the same time as the rest of the components rather than needing the separate device in the DT.
Do you mean using a single entry in the DT for the whole classD system and instantiate ASoC components from it. For now, there are two entry, they could be combined to one entry.
On Tue, Sep 08, 2015 at 05:36:13PM +0800, Wu, Songjun wrote:
On 9/8/2015 00:25, Mark Brown wrote:
Sure, there's no problem at all having that structure in software but it should be possible to do this without having to represent this structure in DT. It should be possible to register the card at the same time as the rest of the components rather than needing the separate device in the DT.
Do you mean using a single entry in the DT for the whole classD system and instantiate ASoC components from it. For now, there are two entry, they could be combined to one entry.
Yes, exactly.
On 9/8/2015 20:23, Mark Brown wrote:
On Tue, Sep 08, 2015 at 05:36:13PM +0800, Wu, Songjun wrote:
On 9/8/2015 00:25, Mark Brown wrote:
Sure, there's no problem at all having that structure in software but it should be possible to do this without having to represent this structure in DT. It should be possible to register the card at the same time as the rest of the components rather than needing the separate device in the DT.
Do you mean using a single entry in the DT for the whole classD system and instantiate ASoC components from it. For now, there are two entry, they could be combined to one entry.
Yes, exactly.
Accept. the two entries will be combined to one entry.
On 9/8/2015 20:23, Mark Brown wrote:
On Tue, Sep 08, 2015 at 05:36:13PM +0800, Wu, Songjun wrote:
On 9/8/2015 00:25, Mark Brown wrote:
Sure, there's no problem at all having that structure in software but it should be possible to do this without having to represent this structure in DT. It should be possible to register the card at the same time as the rest of the components rather than needing the separate device in the DT.
Do you mean using a single entry in the DT for the whole classD system and instantiate ASoC components from it. For now, there are two entry, they could be combined to one entry.
Yes, exactly.
I try to use one entry, but there is a problem. It's about 'driver_data' in struct device. In function snd_soc_register_card, the parameter 'card' will be set to 'driver_data' by the code 'dev_set_drvdata(card->dev, card)'. Then some resources(eg. regmap, clock) also need be recorded by 'driver_data'. One entry could only has one 'driver_data'. I think the best way is to create two entries, like the current dts. What's your opinion?
On Tue, Sep 15, 2015 at 11:11:53AM +0800, Wu, Songjun wrote:
I try to use one entry, but there is a problem. It's about 'driver_data' in struct device. In function snd_soc_register_card, the parameter 'card' will be set to 'driver_data' by the code 'dev_set_drvdata(card->dev, card)'. Then some resources(eg. regmap, clock) also need be recorded by 'driver_data'. One entry could only has one 'driver_data'. I think the best way is to create two entries, like the current dts. What's your opinion?
Look at the recently applied sunxi driver for an example of how to do this - it's a similar piece of hardware (entirely in the SoC and so on).
On 9/17/2015 03:42, Mark Brown wrote:
On Tue, Sep 15, 2015 at 11:11:53AM +0800, Wu, Songjun wrote:
I try to use one entry, but there is a problem. It's about 'driver_data' in struct device. In function snd_soc_register_card, the parameter 'card' will be set to 'driver_data' by the code 'dev_set_drvdata(card->dev, card)'. Then some resources(eg. regmap, clock) also need be recorded by 'driver_data'. One entry could only has one 'driver_data'. I think the best way is to create two entries, like the current dts. What's your opinion?
Look at the recently applied sunxi driver for an example of how to do this - it's a similar piece of hardware (entirely in the SoC and so on).
Thank you, It really helps me. I will make a second version soon.
participants (3)
-
Mark Brown
-
Songjun Wu
-
Wu, Songjun