diff --git a/arch/arm/boot/dts/overlays/Makefile b/arch/arm/boot/dts/overlays/Makefile index cf7c0154ad6d20..86c05d3ae22e45 100644 --- a/arch/arm/boot/dts/overlays/Makefile +++ b/arch/arm/boot/dts/overlays/Makefile @@ -105,6 +105,8 @@ dtbo-$(CONFIG_ARCH_BCM2835) += \ hifiberry-dacplushd.dtbo \ hifiberry-digi.dtbo \ hifiberry-digi-pro.dtbo \ + hifiberry-studio-dac8x.dtbo \ + hifiberry-studio-dac8x-pro.dtbo \ highperi.dtbo \ hy28a.dtbo \ hy28b.dtbo \ diff --git a/arch/arm/boot/dts/overlays/README b/arch/arm/boot/dts/overlays/README index d49fa17cbbe4e1..b0b25fcf6856e3 100644 --- a/arch/arm/boot/dts/overlays/README +++ b/arch/arm/boot/dts/overlays/README @@ -2062,6 +2062,18 @@ Load: dtoverlay=hifiberry-digi-pro Params: +Name: hifiberry-studio-dac8x +Info: Configures the HifiBerry Studio DAC8x audio card +Load: dtoverlay=hifiberry-studio-dac8x +Params: + + +Name: hifiberry-studio-dac8x-pro +Info: Configures the HifiBerry Studio DAC8x PRO audio card +Load: dtoverlay=hifiberry-studio-dac8x-pro +Params: + + Name: highperi Info: Enables "High Peripheral" mode Load: dtoverlay=highperi diff --git a/arch/arm/boot/dts/overlays/hifiberry-studio-dac8x-overlay.dts b/arch/arm/boot/dts/overlays/hifiberry-studio-dac8x-overlay.dts new file mode 100644 index 00000000000000..b19b7d359ffd71 --- /dev/null +++ b/arch/arm/boot/dts/overlays/hifiberry-studio-dac8x-overlay.dts @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: GPL-2.0 +// Definitions for HiFiBerry Studio DAC8x soundcard +/dts-v1/; +/plugin/; + +/ { + compatible = "brcm,bcm2712"; + + fragment@0 { + target = <&i2c1>; + __overlay__ { + status = "okay"; + }; + }; + + fragment@1 { + target = <&gpio>; + __overlay__ { + rp1_i2s0_dac8x: rp1_i2s0_dac8x { + function = "i2s0"; + pins = "gpio18", "gpio19", "gpio20", + "gpio21", "gpio22", "gpio23", + "gpio24", "gpio25", "gpio26", + "gpio27"; + bias-disable; + }; + }; + }; + + fragment@2 { + target = <&i2s_clk_producer>; + __overlay__ { + pinctrl-names = "default"; + pinctrl-0 = <&rp1_i2s0_dac8x>; + status = "okay"; + }; + }; + + fragment@3 { + target-path = "/"; + __overlay__ { + dummy-codec { + #sound-dai-cells = <0>; + compatible = "snd-soc-dummy"; + status = "okay"; + }; + }; + }; + + fragment@4 { + target = <&sound>; + __overlay__ { + compatible = "hifiberry,hifiberry-studio-dac8x"; + i2s-controller = <&i2s_clk_producer>; + status = "okay"; + }; + }; + +}; diff --git a/arch/arm/boot/dts/overlays/hifiberry-studio-dac8x-pro-overlay.dts b/arch/arm/boot/dts/overlays/hifiberry-studio-dac8x-pro-overlay.dts new file mode 100644 index 00000000000000..fd4f0a3485baeb --- /dev/null +++ b/arch/arm/boot/dts/overlays/hifiberry-studio-dac8x-pro-overlay.dts @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: GPL-2.0 +// Definitions for HiFiBerry Studio DAC8x PRO soundcard +/dts-v1/; +/plugin/; + +/ { + compatible = "brcm,bcm2712"; + + fragment@0 { + target = <&i2c1>; + __overlay__ { + status = "okay"; + }; + }; + + fragment@1 { + target = <&gpio>; + __overlay__ { + rp1_i2s1_dac8x: rp1_i2s1_dac8x { + function = "i2s1"; + pins = "gpio18", "gpio19", "gpio20", + "gpio21", "gpio22", "gpio23", + "gpio24", "gpio25", "gpio26", + "gpio27"; + bias-disable; + }; + }; + }; + + fragment@2 { + target = <&i2s_clk_consumer>; + __overlay__ { + pinctrl-names = "default"; + pinctrl-0 = <&rp1_i2s1_dac8x>; + status = "okay"; + }; + }; + + fragment@3 { + target-path = "/"; + __overlay__ { + dummy-codec { + #sound-dai-cells = <0>; + compatible = "snd-soc-dummy"; + status = "okay"; + }; + }; + }; + + fragment@4 { + target = <&sound>; + __overlay__ { + compatible = "hifiberry,hifiberry-studio-dac8x"; + i2s-controller = <&i2s_clk_consumer>; + clk-provider; + status = "okay"; + }; + }; + +}; diff --git a/arch/arm64/configs/bcm2711_defconfig b/arch/arm64/configs/bcm2711_defconfig index 45102076c43f1f..4142fb80981b8a 100644 --- a/arch/arm64/configs/bcm2711_defconfig +++ b/arch/arm64/configs/bcm2711_defconfig @@ -1155,6 +1155,7 @@ CONFIG_SND_BCM2708_SOC_HIFIBERRY_DACPLUSHD=m CONFIG_SND_BCM2708_SOC_HIFIBERRY_DACPLUSADC=m CONFIG_SND_BCM2708_SOC_HIFIBERRY_DACPLUSADCPRO=m CONFIG_SND_BCM2708_SOC_HIFIBERRY_DACPLUSDSP=m +CONFIG_SND_BCM2708_SOC_HIFIBERRY_STUDIO_DAC8X=m CONFIG_SND_BCM2708_SOC_HIFIBERRY_DIGI=m CONFIG_SND_BCM2708_SOC_HIFIBERRY_AMP=m CONFIG_SND_BCM2708_SOC_PIFI_40=m diff --git a/arch/arm64/configs/bcm2712_defconfig b/arch/arm64/configs/bcm2712_defconfig index bf16ca18b13255..6194b630ed8a65 100644 --- a/arch/arm64/configs/bcm2712_defconfig +++ b/arch/arm64/configs/bcm2712_defconfig @@ -1157,6 +1157,7 @@ CONFIG_SND_BCM2708_SOC_HIFIBERRY_DACPLUSHD=m CONFIG_SND_BCM2708_SOC_HIFIBERRY_DACPLUSADC=m CONFIG_SND_BCM2708_SOC_HIFIBERRY_DACPLUSADCPRO=m CONFIG_SND_BCM2708_SOC_HIFIBERRY_DACPLUSDSP=m +CONFIG_SND_BCM2708_SOC_HIFIBERRY_STUDIO_DAC8X=m CONFIG_SND_BCM2708_SOC_HIFIBERRY_DIGI=m CONFIG_SND_BCM2708_SOC_HIFIBERRY_AMP=m CONFIG_SND_BCM2708_SOC_PIFI_40=m diff --git a/sound/soc/bcm/Kconfig b/sound/soc/bcm/Kconfig index fa50cab51478c4..0429dbeac3a7f0 100644 --- a/sound/soc/bcm/Kconfig +++ b/sound/soc/bcm/Kconfig @@ -100,6 +100,14 @@ config SND_BCM2708_SOC_HIFIBERRY_DACPLUSDSP help Say Y or M if you want to add support for HifiBerry DSP-DAC. +config SND_BCM2708_SOC_HIFIBERRY_STUDIO_DAC8X + tristate "Support for HifiBerry Studio DAC8x soundcards" + help + Say Y or M if you want to add support for + HifiBerry Studio DAC8x soundcards. + Supports mulit channel DAC and ADC combnations. + Only available for Pi 5 + config SND_BCM2708_SOC_HIFIBERRY_DIGI tristate "Support for HifiBerry Digi" select SND_SOC_WM8804 diff --git a/sound/soc/bcm/Makefile b/sound/soc/bcm/Makefile index 15c3c4a742c603..0ce245bf8a0a97 100644 --- a/sound/soc/bcm/Makefile +++ b/sound/soc/bcm/Makefile @@ -24,6 +24,7 @@ snd-soc-hifiberry-dacplushd-objs := hifiberry_dacplushd.o snd-soc-hifiberry-dacplusadc-objs := hifiberry_dacplusadc.o snd-soc-hifiberry-dacplusadcpro-objs := hifiberry_dacplusadcpro.o snd-soc-hifiberry-dacplusdsp-objs := hifiberry_dacplusdsp.o +snd-soc-hifiberry-studio-dac8x-objs := hifiberry_studio_dac8x.o snd-soc-justboom-both-objs := justboom-both.o snd-soc-justboom-dac-objs := justboom-dac.o snd-soc-rpi-cirrus-objs := rpi-cirrus.o @@ -58,6 +59,7 @@ obj-$(CONFIG_SND_BCM2708_SOC_HIFIBERRY_DACPLUSHD) += snd-soc-hifiberry-dacplushd obj-$(CONFIG_SND_BCM2708_SOC_HIFIBERRY_DACPLUSADC) += snd-soc-hifiberry-dacplusadc.o obj-$(CONFIG_SND_BCM2708_SOC_HIFIBERRY_DACPLUSADCPRO) += snd-soc-hifiberry-dacplusadcpro.o obj-$(CONFIG_SND_BCM2708_SOC_HIFIBERRY_DACPLUSDSP) += snd-soc-hifiberry-dacplusdsp.o +obj-$(CONFIG_SND_BCM2708_SOC_HIFIBERRY_STUDIO_DAC8X) += snd-soc-hifiberry-studio-dac8x.o obj-$(CONFIG_SND_BCM2708_SOC_JUSTBOOM_BOTH) += snd-soc-justboom-both.o obj-$(CONFIG_SND_BCM2708_SOC_JUSTBOOM_DAC) += snd-soc-justboom-dac.o obj-$(CONFIG_SND_BCM2708_SOC_RPI_CIRRUS) += snd-soc-rpi-cirrus.o diff --git a/sound/soc/bcm/hifiberry_studio_dac8x.c b/sound/soc/bcm/hifiberry_studio_dac8x.c new file mode 100644 index 00000000000000..1e841f83f60019 --- /dev/null +++ b/sound/soc/bcm/hifiberry_studio_dac8x.c @@ -0,0 +1,937 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * hifiberry_studio_dac8x.c -- driver for more complex + * multichannel soundcards with own onboard firmware. + * + * Copyright (C) 2026 HiFiBerry + * + * Author: Joerg Schambacher + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * version 2 as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* register definitions and firmware settings */ +#define FIRMWARE_MAJOR 0x00 +#define FIRMWARE_MINOR 0x01 +#define FIRMWARE_SUBVERSION 0x02 +#define HARDWARE_MAJOR 0x03 +#define HARDWARE_MINOR 0x04 +#define HARDWARE_SUBVERSION 0x05 +#define UUID 0x10 +#define SUPPORTED_RATES_0 0x20 +#define SUPPORTED_RATES_1 0x21 +#define SUPPORTED_RATES_2 0x22 +#define SUPPORTED_RATES_3 0x23 +#define SUPPORTED_FORMATS_0 0x24 +#define SUPPORTED_FORMATS_1 0x25 +#define SUPPORTED_FORMATS_2 0x26 +#define SUPPORTED_FORMATS_3 0x27 +#define NUM_OF_INPUT_CH 0x28 +#define NUM_OF_OUTPUT_CH 0x29 +#define CARD_RESET 0x2A +#define CURRENT_RATE 0x2B +#define CURRENT_FORMAT 0x2C +#define MAX_VOLUME 0x2D +#define MIN_VOLUME 0x2E +#define VOLUME_STP 0x2F +#define MAX_GAIN 0x30 +#define MIN_GAIN 0x31 +#define GAIN_STP 0x32 +#define CARD_BUSY 0x33 +#define CARD_NOERR 0x34 +#define CARD_CLK_OPTIONS 0x35 +#define CARD_CLOCK_MODE 0x36 +#define DAC_STATE 0x40 +#define DAC_CLOCK_SOURCE 0x41 +#define DAC_SYS_CLK 0x42 +#define DAC_SAMPLE_FORMAT 0x43 +#define DAC_FILTER_SETTING_0 0x44 +#define DAC_FILTER_SETTING_1 0x45 +#define DAC_FILTER_SETTING_2 0x46 +#define DAC_FILTER_SETTING_3 0x47 +#define DAC_OUTPUT_MODE 0x48 +#define MASTER_VOL 0x50 +#define VOL_CH0 0x51 +#define VOL_CH1 0x52 +#define VOL_CH2 0x53 +#define VOL_CH3 0x54 +#define VOL_CH4 0x55 +#define VOL_CH5 0x56 +#define VOL_CH6 0x57 +#define VOL_CH7 0x58 +#define MUTE_OUTPUTS 0x59 +#define ADC_INPUT_MODE 0x70 +#define ADC_STATE 0x70 +#define ADC_CLOCK_SOURCE 0x71 +#define ADC_SYS_CLK 0x72 +#define ADC_SAMPLE_FORMAT 0x73 +#define ADC_FILTER_SETTING_0 0x74 +#define ADC_FILTER_SETTING_1 0x75 +#define ADC_FILTER_SETTING_2 0x76 +#define ADC_FILTER_SETTING_3 0x77 +#define ADC_CLIPPING_ATT 0x78 +#define GAIN_CH0 0x81 +#define GAIN_CH1 0x82 +#define GAIN_CH2 0x83 +#define GAIN_CH3 0x84 +#define GAIN_CH4 0x85 +#define GAIN_CH5 0x86 +#define GAIN_CH6 0x87 +#define GAIN_CH7 0x88 +#define MUTE_INPUTS 0x89 +#define CLOCK_CONSUMER_MODE 0x00 +#define CLOCK_PROVIDER_MODE 0x01 + +/* Mask encoding for provider frequency settings */ +#define MASK_5512 0x00 +#define MASK_8000 0x01 +#define MASK_11025 0x02 +#define MASK_16000 0x03 +#define MASK_22050 0x04 +#define MASK_32000 0x05 +#define MASK_44100 0x06 +#define MASK_48000 0x07 +#define MASK_64000 0x08 +#define MASK_88200 0x09 +#define MASK_96000 0x0A +#define MASK_176400 0x0B +#define MASK_192000 0x0C +#define MASK_352800 0x0D +#define MASK_384000 0x0E + +/* Mask encoding for sample formats */ +#define MASK_16_BIT_SF 0x01 +#define MASK_24_BIT_SF 0x02 +#define MASK_32_BIT_SF 0x03 + +/* struct definition for easier access to firmware registers */ +struct hb_studio_dac8x_regs_t { + unsigned char firmware_major; + unsigned char firmware_minor; + unsigned char firmware_subversion; + unsigned char hardware_major; + unsigned char hardware_minor; + unsigned char hardware_subversion; + unsigned char res1[10]; + uuid_t uuid; + unsigned int supported_rates; // 0x20 + unsigned int supported_formats; // 0x24 + unsigned char num_of_input_ch; // 0x28 + unsigned char num_of_output_ch; // 0x29 + unsigned char card_reset; // 0x2a + unsigned char current_rate; // 0x2b + unsigned char current_format; // 0x2c + unsigned char max_volume; // 0x2d + unsigned char min_volume; // 0x2e + unsigned char volume_stp; // 0x2f + unsigned char max_gain; // 0x30 + unsigned char min_gain; // 0x31 + unsigned char gain_stp; // 0x32 + unsigned char card_busy; // 0x33 + unsigned char card_noerr; // 0x34 + unsigned char card_clk_options; // 0x35 + unsigned char card_clk_mode; // 0x36 + unsigned char res3[9]; // 0x37 + unsigned char dac_state; // 0x40 + unsigned char dac_clock_source; // 0x41 + unsigned char dac_sys_clk; // 0x42 + unsigned char dac_sample_format; + unsigned char dac_filter_setting_0; + unsigned char dac_filter_setting_1; + unsigned char dac_filter_setting_2; + unsigned char dac_filter_setting_3; + unsigned char dac_output_mode; // 0x48 + unsigned char res4[7]; // 0x49 - 0x4f + unsigned char master_vol; // 0x50 + unsigned char vol_ch0; // 0x51 + unsigned char vol_ch1; // 0x52 + unsigned char vol_ch2; // 0x53 + unsigned char vol_ch3; // 0x54 + unsigned char vol_ch4; // 0x55 + unsigned char vol_ch5; // 0x56 + unsigned char vol_ch6; // 0x57 + unsigned char vol_ch7; // 0x58 + unsigned char mute_outputs; // 0x59 + unsigned char res5[22]; // 0x5a- 0x6f + unsigned char adc_input_mode; // 0x70 + unsigned char adc_state; // 0x70 + unsigned char adc_clock_source; // 0x71 + unsigned char adc_sys_clk; // 0x72 + unsigned char adc_sample_format; // 0x73 + unsigned char adc_filter_setting_0; // 0x74 + unsigned char adc_filter_setting_1; // 0x75 + unsigned char adc_filter_setting_2; // 0x76 + unsigned char adc_filter_setting_3; // 0x77 + unsigned char adc_clipping_att; // 0x78 + unsigned char res6[7]; // 0x79- 0x7f + unsigned char res[1]; // 0x80 + unsigned char gain_ch0; // 0x81 + unsigned char gain_ch1; // 0x82 + unsigned char gain_ch2; // 0x83 + unsigned char gain_ch3; // 0x84 + unsigned char gain_ch4; // 0x85 + unsigned char gain_ch5; // 0x86 + unsigned char gain_ch6; // 0x87 + unsigned char gain_ch7; // 0x88 + unsigned char mute_inputs; // 0x89 + }; + + +static struct snd_soc_card snd_rpi_hifiberry_studio_dac8x; +static struct i2c_client *hb_uni_i2c_client; +struct hb_uni_private { + struct regmap *regmap; + uuid_t uuid; + unsigned int sample_bits; + unsigned int current_rate; + struct hb_studio_dac8x_regs_t card_info; +}; + +static struct hb_uni_private *priv; +static bool card_is_clk_provider; + +static bool hb_uni_volatile_reg(struct device *dev, unsigned int reg) +{ + switch (reg) { + case CARD_BUSY: + case CARD_RESET: + case DAC_STATE: + case DAC_CLOCK_SOURCE: + case DAC_SYS_CLK: + case MASTER_VOL: + case VOL_CH0: + case VOL_CH1: + case VOL_CH2: + case VOL_CH3: + case VOL_CH4: + case VOL_CH5: + case VOL_CH6: + case VOL_CH7: + case GAIN_CH0: + case GAIN_CH1: + case GAIN_CH2: + case GAIN_CH3: + case GAIN_CH4: + case GAIN_CH5: + case GAIN_CH6: + case GAIN_CH7: + return true; + default: + return false; + } +} + +static bool hb_uni_readable_reg(struct device *dev, unsigned int reg) +{ + switch (reg) { + case FIRMWARE_MAJOR: + case FIRMWARE_MINOR: + case FIRMWARE_SUBVERSION: + case HARDWARE_MAJOR: + case HARDWARE_MINOR: + case HARDWARE_SUBVERSION: + case NUM_OF_INPUT_CH: + case NUM_OF_OUTPUT_CH: + case SUPPORTED_RATES_0: + case SUPPORTED_RATES_1: + case SUPPORTED_RATES_2: + case SUPPORTED_RATES_3: + case SUPPORTED_FORMATS_0: + case SUPPORTED_FORMATS_1: + case SUPPORTED_FORMATS_2: + case SUPPORTED_FORMATS_3: + case UUID: + case DAC_STATE: + case CARD_BUSY: + case CARD_RESET: + case CARD_CLOCK_MODE: + case DAC_CLOCK_SOURCE: + case DAC_SYS_CLK: + case DAC_SAMPLE_FORMAT: + case DAC_FILTER_SETTING_0: + case DAC_FILTER_SETTING_1: + case DAC_FILTER_SETTING_2: + case DAC_FILTER_SETTING_3: + case DAC_OUTPUT_MODE: + case GAIN_CH0: + case GAIN_CH1: + case GAIN_CH2: + case GAIN_CH3: + case GAIN_CH4: + case GAIN_CH5: + case GAIN_CH6: + case GAIN_CH7: + case ADC_CLIPPING_ATT: + return true; + default: + return reg < 0xff; + } +} + +static const struct regmap_config hb_uni_regmap = { + .reg_bits = 8, + .val_bits = 8, + .max_register = 0xff, + .cache_type = REGCACHE_RBTREE, + .volatile_reg = hb_uni_volatile_reg, + .readable_reg = hb_uni_readable_reg, +}; + +static const DECLARE_TLV_DB_MINMAX(adc_att_tlv, -600, -300); +static const DECLARE_TLV_DB_MINMAX(gain_tlv, -1200, 4000); +static const DECLARE_TLV_DB_MINMAX(volume_tlv, -10300, 2400); +static const DECLARE_TLV_DB_MINMAX(spkr_tlv, -10300, 0); +static const char * const pll_lock_texts[] = {"unlocked", "locked"}; +static const char * const mute_texts[] = {"unmuted", "muted"}; +static const char * const dac_filter_texts[] = { + "FIR w/ De-Emph.", + "Low Latency IIR w/ De-Emph.", + "High Att. w/ De-Emph.", + "Ringingless Low Latency FIR w/o Deemph.", + }; +static const char * const adc_att_texts[] = { + "Clip att. off", "-3dB", "-4dB", "-5dB", "-6dB", + }; + +struct hb_uni_vol_control_single { + unsigned int reg; + unsigned int shift; + int min; + int max; + bool invert; + const unsigned int *tlv; +}; + +static int hb_uni_vol_info_single(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_info *uinfo) +{ + struct hb_uni_vol_control_single *ctl = (void *)kcontrol->private_value; + + uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER; + uinfo->count = 1; // mono + uinfo->value.integer.min = ctl->min; + uinfo->value.integer.max = ctl->max; + uinfo->value.integer.step = 1; + return 0; +} + +static int hb_uni_vol_get_single(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + struct hb_uni_vol_control_single *ctl = (void *)kcontrol->private_value; + unsigned int val; + + regmap_read(priv->regmap, ctl->reg, &val); + val = (val >> ctl->shift) & 0xff; + if (ctl->invert) + val = ctl->max - val; + ucontrol->value.integer.value[0] = val; + return 0; +} + +static int hb_uni_vol_put_single(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + struct hb_uni_vol_control_single *ctl = (void *)kcontrol->private_value; + unsigned int val = ucontrol->value.integer.value[0]; + unsigned int new; + + if (ctl->invert) + val = ctl->max - val; + new = (val & 0xff) << ctl->shift; + regmap_write(priv->regmap, ctl->reg, new); + return 0; +} + +struct hb_uni_enum_control { + unsigned int reg; + unsigned int shift; + unsigned int mask; + const char * const *texts; + unsigned int items; +}; + +static int hb_uni_enum_info(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_info *uinfo) +{ + struct hb_uni_enum_control *ctl = (void *)kcontrol->private_value; + + uinfo->type = SNDRV_CTL_ELEM_TYPE_ENUMERATED; + uinfo->count = 1; + uinfo->value.enumerated.items = ctl->items; + + if (uinfo->value.enumerated.item >= ctl->items) + uinfo->value.enumerated.item = ctl->items - 1; + + strscpy(uinfo->value.enumerated.name, + ctl->texts[uinfo->value.enumerated.item], + sizeof(uinfo->value.enumerated.name) - 1); + + return 0; +} + +static int hb_uni_enum_get(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + struct hb_uni_enum_control *ctl = (void *)kcontrol->private_value; + unsigned int val; + + regmap_read(priv->regmap, ctl->reg, &val); + + val = (val >> ctl->shift) & ctl->mask; + if (val >= ctl->items) + val = 0; + + ucontrol->value.enumerated.item[0] = val; + + return 0; +} + +static int hb_uni_enum_put(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + struct hb_uni_enum_control *ctl = (void *)kcontrol->private_value; + unsigned int val = ucontrol->value.enumerated.item[0]; + + if (val >= ctl->items) + return -EINVAL; + + regmap_update_bits(priv->regmap, ctl->reg, + ctl->mask << ctl->shift, + (val & ctl->mask) << ctl->shift); + + return 0; +} + +#define VOL_CTL_SINGLE(kname, controls, ktlv) {\ + .iface = SNDRV_CTL_ELEM_IFACE_MIXER, \ + .name = kname, \ + .access = SNDRV_CTL_ELEM_ACCESS_TLV_READ | \ + SNDRV_CTL_ELEM_ACCESS_READWRITE, \ + .tlv.p = ktlv, \ + .info = hb_uni_vol_info_single, \ + .get = hb_uni_vol_get_single, \ + .put = hb_uni_vol_put_single, \ + .private_value = (unsigned long)&controls, } + +#define ENUM_CTL_SINGLE(kname, controls) {\ + .iface = SNDRV_CTL_ELEM_IFACE_MIXER, \ + .name = kname, \ + .info = hb_uni_enum_info, \ + .get = hb_uni_enum_get, \ + .put = hb_uni_enum_put, \ + .private_value = (unsigned long)&controls, } + +#define ENUM_CTL_SINGLE_RO(kname, controls) {\ + .iface = SNDRV_CTL_ELEM_IFACE_MIXER, \ + .name = kname, \ + .info = hb_uni_enum_info, \ + .get = hb_uni_enum_get, \ + .put = NULL, \ + .private_value = (unsigned long)&controls, } + +static const struct hb_uni_vol_control_single hb_uni_vol_ctls_single[] = { + { MASTER_VOL, 0, 0, 254, true, volume_tlv }, + { VOL_CH0, 0, 0, 206, true, spkr_tlv }, + { VOL_CH1, 0, 0, 206, true, spkr_tlv }, + { VOL_CH2, 0, 0, 206, true, spkr_tlv }, + { VOL_CH3, 0, 0, 206, true, spkr_tlv }, + { VOL_CH4, 0, 0, 206, true, spkr_tlv }, + { VOL_CH5, 0, 0, 206, true, spkr_tlv }, + { VOL_CH6, 0, 0, 206, true, spkr_tlv }, + { VOL_CH7, 0, 0, 206, true, spkr_tlv }, +}; + +static const struct hb_uni_vol_control_single hb_uni_gain_ctls_single[] = { + { GAIN_CH0, 0, 0, 104, false, gain_tlv }, + { GAIN_CH1, 0, 0, 104, false, gain_tlv }, + { GAIN_CH2, 0, 0, 104, false, gain_tlv }, + { GAIN_CH3, 0, 0, 104, false, gain_tlv }, + { GAIN_CH4, 0, 0, 104, false, gain_tlv }, + { GAIN_CH5, 0, 0, 104, false, gain_tlv }, + { GAIN_CH6, 0, 0, 104, false, gain_tlv }, + { GAIN_CH7, 0, 0, 104, false, gain_tlv }, +}; + +static const struct snd_kcontrol_new hb_uni_play_controls_single[] = { + VOL_CTL_SINGLE("Master Playback Volume", hb_uni_vol_ctls_single[0], volume_tlv), + VOL_CTL_SINGLE("Output Ch0 Playback Volume", hb_uni_vol_ctls_single[1], spkr_tlv), + VOL_CTL_SINGLE("Output Ch1 Playback Volume", hb_uni_vol_ctls_single[2], spkr_tlv), + VOL_CTL_SINGLE("Output Ch2 Playback Volume", hb_uni_vol_ctls_single[3], spkr_tlv), + VOL_CTL_SINGLE("Output Ch3 Playback Volume", hb_uni_vol_ctls_single[4], spkr_tlv), + VOL_CTL_SINGLE("Output Ch4 Playback Volume", hb_uni_vol_ctls_single[5], spkr_tlv), + VOL_CTL_SINGLE("Output Ch5 Playback Volume", hb_uni_vol_ctls_single[6], spkr_tlv), + VOL_CTL_SINGLE("Output Ch6 Playback Volume", hb_uni_vol_ctls_single[7], spkr_tlv), + VOL_CTL_SINGLE("Output Ch7 Playback Volume", hb_uni_vol_ctls_single[8], spkr_tlv), +}; + +static const struct snd_kcontrol_new hb_uni_rec_controls_single[] = { + VOL_CTL_SINGLE("Input Ch0 Capture Volume", hb_uni_gain_ctls_single[0], gain_tlv), + VOL_CTL_SINGLE("Input Ch1 Capture Volume", hb_uni_gain_ctls_single[1], gain_tlv), + VOL_CTL_SINGLE("Input Ch2 Capture Volume", hb_uni_gain_ctls_single[2], gain_tlv), + VOL_CTL_SINGLE("Input Ch3 Capture Volume", hb_uni_gain_ctls_single[3], gain_tlv), + VOL_CTL_SINGLE("Input Ch4 Capture Volume", hb_uni_gain_ctls_single[4], gain_tlv), + VOL_CTL_SINGLE("Input Ch5 Capture Volume", hb_uni_gain_ctls_single[5], gain_tlv), + VOL_CTL_SINGLE("Input Ch6 Capture Volume", hb_uni_gain_ctls_single[6], gain_tlv), + VOL_CTL_SINGLE("Input Ch7 Capture Volume", hb_uni_gain_ctls_single[7], gain_tlv), +}; + +static const struct hb_uni_enum_control hb_uni_play_enum_ctls[] = { + { DAC_STATE, 0, 0x1, pll_lock_texts, ARRAY_SIZE(pll_lock_texts) }, + { DAC_FILTER_SETTING_0, 0, 0x03, dac_filter_texts, ARRAY_SIZE(dac_filter_texts) }, + { MUTE_OUTPUTS, 0, 0x01, mute_texts, ARRAY_SIZE(mute_texts) }, +}; + +static const struct hb_uni_enum_control hb_uni_rec_enum_ctls[] = { + { ADC_CLIPPING_ATT, 0, 0x7, adc_att_texts, ARRAY_SIZE(adc_att_texts) }, +}; + +static const struct snd_kcontrol_new hb_uni_gen_controls_single[] = { + ENUM_CTL_SINGLE("DAC Filter", hb_uni_play_enum_ctls[1]), + ENUM_CTL_SINGLE("DAC Mute", hb_uni_play_enum_ctls[2]), +}; + +static const struct snd_kcontrol_new adc_controls_single[] = { + ENUM_CTL_SINGLE("Clipping Attenuation Capture Volume", hb_uni_rec_enum_ctls[0]), +}; + +static int snd_rpi_hifiberry_studio_dac8x_hw_params( + struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params) +{ + struct snd_soc_pcm_runtime *rtd = substream->private_data; + struct snd_soc_dai *cpu_dai = snd_soc_rtd_to_cpu(rtd, 0); + struct device *dev = rtd->dev; + unsigned char tmp; + int trials; + int busy; + int err; + + priv->sample_bits = snd_pcm_format_width(params_format(params)); + priv->sample_bits = priv->sample_bits <= 16 ? 16 : 32; + + priv->current_rate = params_rate(params); + dev_info(dev, "using %ibits @ %isps\n", + priv->sample_bits, priv->current_rate); + + /* write requested samplerate and word length back to card */ + switch (priv->current_rate) { + case 5512: + tmp = MASK_5512; + break; + case 8000: + tmp = MASK_8000; + break; + case 11025: + tmp = MASK_11025; + break; + case 16000: + tmp = MASK_16000; + break; + case 22050: + tmp = MASK_22050; + break; + case 32000: + tmp = MASK_32000; + break; + case 44100: + tmp = MASK_44100; + break; + case 88200: + tmp = MASK_88200; + break; + case 176400: + tmp = MASK_176400; + break; + case 352800: + tmp = MASK_352800; + break; + case 48000: + tmp = MASK_48000; + break; + case 96000: + tmp = MASK_96000; + break; + case 192000: + tmp = MASK_192000; + break; + case 384000: + tmp = MASK_384000; + break; + default: + dev_info(dev, "rate not supported (%u)\n", priv->current_rate); + return -EINVAL; + } + + err = regmap_write(priv->regmap, CURRENT_RATE, tmp); + if (err < 0) + return err; + + switch (priv->sample_bits) { + case 16: + tmp = MASK_16_BIT_SF; + break; + case 24: + tmp = MASK_24_BIT_SF; + break; + case 32: + tmp = MASK_32_BIT_SF; + break; + default: + dev_info(dev, "word length not supported (%u)\n", + priv->sample_bits); + return -EINVAL; + } + err = regmap_write(priv->regmap, CURRENT_FORMAT, tmp); + if (err < 0) + return err; + + /* If card provides clocks wait max. ~40ms for PLL */ + if (card_is_clk_provider) { + /* trigger card to set new rate and format */ + err = regmap_write(priv->regmap, CARD_CLOCK_MODE, 0x02); + if (err < 0) + return err; + trials = 10; + do { + usleep_range(3000, 4000); + regmap_read(priv->regmap, CARD_BUSY, &busy); + } while (busy && --trials); + if (!trials) { + dev_err(dev, "Card is unable to set clocks\n"); + return -EINVAL; + } + } + /* always run with 64bit frames */ + return snd_soc_dai_set_bclk_ratio(cpu_dai, 64); +} + +static const struct snd_soc_ops snd_rpi_hifiberry_studio_dac8x_ops = { + .hw_params = snd_rpi_hifiberry_studio_dac8x_hw_params, +}; + +SND_SOC_DAILINK_DEFS(hifiberry_studio_dac8x, + DAILINK_COMP_ARRAY(COMP_EMPTY()), + DAILINK_COMP_ARRAY(COMP_CODEC("snd-soc-dummy", "snd-soc-dummy-dai")), + DAILINK_COMP_ARRAY(COMP_EMPTY())); + +static int hifiberry_studio_dac8x_init(struct snd_soc_pcm_runtime *rtd) +{ + struct snd_soc_dai *codec_dai = snd_soc_rtd_to_codec(rtd, 0); + struct snd_soc_card *card = rtd->card; + + /* Configure playback */ + codec_dai->driver->playback.channels_max = + priv->card_info.num_of_output_ch; + codec_dai->driver->playback.rates = priv->card_info.supported_rates; + codec_dai->driver->playback.formats = priv->card_info.supported_formats; + + if (priv->card_info.num_of_input_ch) { + struct snd_soc_dai_link *dai = rtd->dai_link; + + dev_info(card->dev, "inputs detected: capture enabled\n"); + codec_dai->driver->symmetric_rate = 1; + codec_dai->driver->symmetric_sample_bits = 1; + codec_dai->driver->capture.formats = + priv->card_info.supported_formats; + codec_dai->driver->capture.rates = + priv->card_info.supported_rates; + codec_dai->driver->capture.channels_max = + priv->card_info.num_of_input_ch; + dai->name = "HiFiBerry Studio DAC8x-ADC8x"; + dai->stream_name = "HiFiBerry Studio HiFi"; + } else { + rtd->dai_link->playback_only = 1; // Disable capture + } + + if ((priv->card_info.card_clk_options & 0x02) && card_is_clk_provider) { + struct snd_soc_dai_link *dai = rtd->dai_link; + + dai->stream_name = "HiFiBerry Studio Pro HiFi"; + dai->dai_fmt = SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_NB_NF + | SND_SOC_DAIFMT_CBM_CFM; + } + dev_info(card->dev, + "HiFiBerry Studio DAC8x successfully initialized\n"); + + return 0; +} + +static struct snd_soc_dai_link snd_rpi_hifiberry_studio_dac8x_dai[] = { + { + .name = "HiFiBerry Studio DAC8x", + .stream_name = "HifiBerry Studio HiFi", + .dai_fmt = SND_SOC_DAIFMT_I2S | + SND_SOC_DAIFMT_NB_NF | + SND_SOC_DAIFMT_CBS_CFS, + .init = hifiberry_studio_dac8x_init, + .ops = &snd_rpi_hifiberry_studio_dac8x_ops, + SND_SOC_DAILINK_REG(hifiberry_studio_dac8x), + }, +}; + +/* audio machine driver */ +static struct snd_soc_card snd_rpi_hifiberry_studio_dac8x = { + .name = "Hifiberry Studio DAC8x", + .driver_name = "HifiberryStudio", + .owner = THIS_MODULE, + .dai_link = snd_rpi_hifiberry_studio_dac8x_dai, + .num_links = ARRAY_SIZE(snd_rpi_hifiberry_studio_dac8x_dai), +}; + +static int hb_uni_read_card_info(struct platform_device *pdev) +{ + int ret; + + /* read basic card info */ + ret = regmap_bulk_read(priv->regmap, 0x00, &priv->card_info, 0x06); + if (ret) { + dev_err(&pdev->dev, "Failed to read card info: %d\n", ret); + return ret; + } + + dev_info(&pdev->dev, "hardware V%d.%d.%d\n", + priv->card_info.hardware_major, + priv->card_info.hardware_minor, + priv->card_info.hardware_subversion + ); + + dev_info(&pdev->dev, "firmware V%d.%d.%d\n", + priv->card_info.firmware_major, + priv->card_info.firmware_minor, + priv->card_info.firmware_subversion + ); + + /* read card capabilities */ + ret = regmap_bulk_read(priv->regmap, UUID, &priv->card_info.uuid, 0x20); + if (ret) { + dev_err(&pdev->dev, "Failed to read card info: %d\n", ret); + return ret; + } + + dev_info(&pdev->dev, "UUID: %*phN\n", + (int)sizeof(priv->card_info.uuid.b), priv->card_info.uuid.b); + dev_info(&pdev->dev, "%i output channels reported\n", + priv->card_info.num_of_output_ch); + dev_dbg(&pdev->dev, "supported rates %08x\n", + priv->card_info.supported_rates); + dev_dbg(&pdev->dev, "supported formats %08x\n", + priv->card_info.supported_formats); + + if (priv->card_info.num_of_output_ch > 8 || + priv->card_info.num_of_input_ch > 8) { + dev_err(&pdev->dev, "Maximum of 8 channels exceeded!\n"); + return -EINVAL; + } + + if (priv->card_info.num_of_input_ch > 0) { + dev_info(&pdev->dev, + "Inputs detected: %u channels\n", + priv->card_info.num_of_input_ch); + } else { + dev_info(&pdev->dev, "No inputs present, playback only\n"); + } + + ret = regmap_bulk_read(priv->regmap, CARD_BUSY, + &priv->card_info.card_busy, 20); + if (ret) { + dev_err(&pdev->dev, "Failed to read card info: %d\n", ret); + return ret; + } + + if (card_is_clk_provider) { + if (priv->card_info.card_clk_options & 0x02) { + dev_info(&pdev->dev, "Card provides i2s clocks\n"); + } else { + dev_err(&pdev->dev, + "Card cannot provide i2s clocks\n"); + return -EINVAL; + } + } else { + if ((priv->card_info.card_clk_options == 0x02)) { + dev_err(&pdev->dev, + "Card cannot run as i2s clock consumer\n"); + return -EINVAL; + } + } + + switch (cpu_to_be32(*(unsigned int *)&priv->card_info.uuid)) { + case 0x74e7ae95: + if (card_is_clk_provider) + snd_rpi_hifiberry_studio_dac8x.name = + "HiFiBerry Studio DAC8x Pro"; + else + snd_rpi_hifiberry_studio_dac8x.name = + "HiFiBerry Studio DAC8x"; + break; + default: + } + + + regcache_cache_only(priv->regmap, true); + ret = regmap_bulk_read(priv->regmap, MASTER_VOL, + &priv->card_info.master_vol, MUTE_OUTPUTS - MASTER_VOL); + regcache_cache_only(priv->regmap, false); + + return 0; +} + +static int hb_uni_add_card_controls(struct platform_device *pdev) +{ + int ret; + + ret = snd_soc_add_card_controls(&snd_rpi_hifiberry_studio_dac8x, + hb_uni_gen_controls_single, + ARRAY_SIZE(hb_uni_gen_controls_single)); + if (ret < 0) { + dev_err(&pdev->dev, + "snd_soc_add_card_controls() failed: %d\n", ret); + return ret; + } + ret = snd_soc_add_card_controls(&snd_rpi_hifiberry_studio_dac8x, + hb_uni_play_controls_single, + ARRAY_SIZE(hb_uni_play_controls_single) / 9 * + (priv->card_info.num_of_output_ch + 1)); + if (ret < 0) { + dev_err(&pdev->dev, + "snd_soc_add_card_controls() failed: %d\n", ret); + return ret; + } + + /* add optional ADC controls if inputs detected */ + if (priv->card_info.num_of_input_ch > 0) { + ret = snd_soc_add_card_controls(&snd_rpi_hifiberry_studio_dac8x, + hb_uni_rec_controls_single, + ARRAY_SIZE(hb_uni_rec_controls_single) / 8 * + priv->card_info.num_of_input_ch); + if (ret < 0) { + dev_err(&pdev->dev, + "snd_soc_add_card_controls() failed: %d\n", ret); + } + ret = snd_soc_add_card_controls(&snd_rpi_hifiberry_studio_dac8x, + adc_controls_single, + ARRAY_SIZE(adc_controls_single)); + if (ret < 0) { + dev_err(&pdev->dev, + "snd_soc_add_card_controls() failed: %d\n", ret); + } + } + return ret; +} + +static int hb_controller_probe(struct platform_device *pdev) +{ + struct i2c_adapter *adap = i2c_get_adapter(1); + struct device_node *np = pdev->dev.of_node; + int ret; + + if (!adap) + return -EPROBE_DEFER; /* I2C module not yet available */ + + struct i2c_board_info info = { + I2C_BOARD_INFO("hb_controller", 0x10), + }; + + hb_uni_i2c_client = i2c_new_client_device(adap, &info); + if (IS_ERR(hb_uni_i2c_client)) + return PTR_ERR(hb_uni_i2c_client); + + priv = devm_kzalloc(&hb_uni_i2c_client->dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + priv->regmap = devm_regmap_init_i2c(hb_uni_i2c_client, &hb_uni_regmap); + if (IS_ERR(priv->regmap)) + return dev_err_probe(&hb_uni_i2c_client->dev, + PTR_ERR(priv->regmap), "Failed to init regmap\n"); + + if (np && of_property_read_bool(np, "clk-provider")) + card_is_clk_provider = true; + + ret = hb_uni_read_card_info(pdev); + if (ret < 0) { + dev_err(&hb_uni_i2c_client->dev, + "Failed to read card info or wrong configuration!\n"); + } + + return ret; +}; + +static int snd_rpi_hifiberry_studio_dac8x_probe(struct platform_device *pdev) +{ + int ret = 0; + + /* probe for controller */ + ret = hb_controller_probe(pdev); + if (ret < 0) + return ret; + + snd_rpi_hifiberry_studio_dac8x.dev = &pdev->dev; + + if (pdev->dev.of_node) { + struct device_node *i2s_node; + struct snd_soc_dai_link *dai; + + dai = &snd_rpi_hifiberry_studio_dac8x_dai[0]; + i2s_node = of_parse_phandle(pdev->dev.of_node, + "i2s-controller", 0); + + if (i2s_node) { + dai->cpus->dai_name = NULL; + dai->cpus->of_node = i2s_node; + dai->platforms->name = NULL; + dai->platforms->of_node = i2s_node; + } + } + + ret = devm_snd_soc_register_card(&pdev->dev, + &snd_rpi_hifiberry_studio_dac8x); + if (ret && ret != -EPROBE_DEFER) + dev_err(&pdev->dev, + "devm_snd_soc_register_card() failed: %d\n", ret); + + /* as we do not have components use card-controls */ + ret = hb_uni_add_card_controls(pdev); + + return ret; +} + +static const struct of_device_id snd_rpi_hifiberry_studio_dac8x_of_match[] = { + { .compatible = "hifiberry,hifiberry-studio-dac8x", }, + {}, +}; +MODULE_DEVICE_TABLE(of, snd_rpi_hifiberry_studio_dac8x_of_match); + +static struct platform_driver snd_rpi_hifiberry_studio_dac8x_driver = { + .driver = { + .name = "snd-rpi-hifiberry-studio-dac8x", + .owner = THIS_MODULE, + .of_match_table = snd_rpi_hifiberry_studio_dac8x_of_match, + }, + .probe = snd_rpi_hifiberry_studio_dac8x_probe, +}; + +module_platform_driver(snd_rpi_hifiberry_studio_dac8x_driver); + +MODULE_AUTHOR("Joerg Schambacher "); +MODULE_DESCRIPTION("HiFiBerry Studio DAC8x Soundcard Driver"); +MODULE_LICENSE("GPL");