From 8ab68f8c2cf6828cac9674eec4150eff777bf39d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Giedrius=20Trainavi=C4=8Dius?= Date: Wed, 19 Mar 2025 19:55:46 +0200 Subject: [PATCH] Adding Pisound Micro kernel module MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Giedrius Trainavičius --- .../devicetree/bindings/vendor-prefixes.txt | 1 + .../devicetree/bindings/vendor-prefixes.yaml | 2 +- sound/drivers/Kconfig | 9 + sound/drivers/Makefile | 2 +- sound/drivers/upisnd/Makefile | 23 + sound/drivers/upisnd/upisnd_codec.c | 1739 +++++++++++++++++ sound/drivers/upisnd/upisnd_codec.h | 23 + sound/drivers/upisnd/upisnd_comm.c | 638 ++++++ sound/drivers/upisnd/upisnd_comm.h | 104 + sound/drivers/upisnd/upisnd_common.h | 97 + sound/drivers/upisnd/upisnd_ctrl.c | 246 +++ sound/drivers/upisnd/upisnd_ctrl.h | 36 + sound/drivers/upisnd/upisnd_debug.h | 34 + sound/drivers/upisnd/upisnd_gpio.c | 867 ++++++++ sound/drivers/upisnd/upisnd_gpio.h | 75 + sound/drivers/upisnd/upisnd_midi.c | 326 +++ sound/drivers/upisnd/upisnd_midi.h | 47 + sound/drivers/upisnd/upisnd_module.c | 273 +++ sound/drivers/upisnd/upisnd_pins.c | 113 ++ sound/drivers/upisnd/upisnd_pins.h | 67 + sound/drivers/upisnd/upisnd_protocol.h | 322 +++ sound/drivers/upisnd/upisnd_sound.c | 277 +++ sound/drivers/upisnd/upisnd_sound.h | 28 + sound/drivers/upisnd/upisnd_sysfs.c | 1728 ++++++++++++++++ sound/drivers/upisnd/upisnd_sysfs.h | 32 + sound/drivers/upisnd/upisnd_utils.c | 33 + sound/drivers/upisnd/upisnd_utils.h | 25 + 27 files changed, 7165 insertions(+), 2 deletions(-) create mode 100644 sound/drivers/upisnd/Makefile create mode 100644 sound/drivers/upisnd/upisnd_codec.c create mode 100644 sound/drivers/upisnd/upisnd_codec.h create mode 100644 sound/drivers/upisnd/upisnd_comm.c create mode 100644 sound/drivers/upisnd/upisnd_comm.h create mode 100644 sound/drivers/upisnd/upisnd_common.h create mode 100644 sound/drivers/upisnd/upisnd_ctrl.c create mode 100644 sound/drivers/upisnd/upisnd_ctrl.h create mode 100644 sound/drivers/upisnd/upisnd_debug.h create mode 100644 sound/drivers/upisnd/upisnd_gpio.c create mode 100644 sound/drivers/upisnd/upisnd_gpio.h create mode 100644 sound/drivers/upisnd/upisnd_midi.c create mode 100644 sound/drivers/upisnd/upisnd_midi.h create mode 100644 sound/drivers/upisnd/upisnd_module.c create mode 100644 sound/drivers/upisnd/upisnd_pins.c create mode 100644 sound/drivers/upisnd/upisnd_pins.h create mode 100644 sound/drivers/upisnd/upisnd_protocol.h create mode 100644 sound/drivers/upisnd/upisnd_sound.c create mode 100644 sound/drivers/upisnd/upisnd_sound.h create mode 100644 sound/drivers/upisnd/upisnd_sysfs.c create mode 100644 sound/drivers/upisnd/upisnd_sysfs.h create mode 100644 sound/drivers/upisnd/upisnd_utils.c create mode 100644 sound/drivers/upisnd/upisnd_utils.h --- a/Documentation/devicetree/bindings/vendor-prefixes.txt +++ b/Documentation/devicetree/bindings/vendor-prefixes.txt @@ -57,6 +57,7 @@ axis Axis Communications AB bananapi BIPAI KEJI LIMITED bhf Beckhoff Automation GmbH & Co. KG bitmain Bitmain Technologies +blokas Vilniaus Blokas UAB blokaslabs Vilniaus Blokas UAB boe BOE Technology Group Co., Ltd. bosch Bosch Sensortec GmbH --- a/Documentation/devicetree/bindings/vendor-prefixes.yaml +++ b/Documentation/devicetree/bindings/vendor-prefixes.yaml @@ -218,7 +218,7 @@ patternProperties: description: Shenzhen BigTree Tech Co., LTD "^bitmain,.*": description: Bitmain Technologies - "^blokaslabs,.*": + "^blokas(labs)?,.*": description: Vilniaus Blokas UAB "^blutek,.*": description: BluTek Power --- a/sound/drivers/Kconfig +++ b/sound/drivers/Kconfig @@ -273,4 +273,13 @@ config SND_PIMIDI To compile this driver as a module, choose M here: the module will be called snd-pimidi. +config SND_PISOUND_MICRO + tristate "Pisound Micro driver" + depends on SND_SEQUENCER && CRC8 + help + Say Y here to include support for Blokas Pisound Micro. + + To compile this driver as modules, choose M here: the modules + will be called snd-soc-upisnd-ctrl and snd-soc-upisnd-codec. + endif # SND_DRIVERS --- a/sound/drivers/Makefile +++ b/sound/drivers/Makefile @@ -27,4 +27,4 @@ obj-$(CONFIG_SND_MTS64) += snd-mts64.o obj-$(CONFIG_SND_PIMIDI) += snd-pimidi.o obj-$(CONFIG_SND_PORTMAN2X4) += snd-portman2x4.o -obj-$(CONFIG_SND) += opl3/ opl4/ mpu401/ vx/ pcsp/ +obj-$(CONFIG_SND) += opl3/ opl4/ mpu401/ vx/ pcsp/ upisnd/ --- /dev/null +++ b/sound/drivers/upisnd/Makefile @@ -0,0 +1,23 @@ +# Pisound Micro Linux kernel module. +# Copyright (C) 2017-2025 Vilniaus Blokas UAB, https://blokas.io/ +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; version 2 of the +# License. +# +# 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 $(shell pwd)/Makefile.dev + +ifdef M +-include $(M)/Makefile.dev +endif + +obj-$(CONFIG_SND_PISOUND_MICRO) := snd-soc-upisnd-ctrl.o snd-soc-upisnd-codec.o +snd-soc-upisnd-ctrl-objs := upisnd_module.o upisnd_ctrl.o upisnd_comm.o upisnd_sound.o upisnd_midi.o upisnd_pins.o upisnd_gpio.o upisnd_sysfs.o upisnd_utils.o +snd-soc-upisnd-codec-objs := upisnd_codec.o --- /dev/null +++ b/sound/drivers/upisnd/upisnd_codec.c @@ -0,0 +1,1739 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Pisound Micro Linux kernel module. + * Copyright (C) 2017-2025 Vilniaus Blokas UAB, https://blokas.io/ + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; version 2 of the + * License. + * + * 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 +#include +#include +#include + +#include "upisnd_codec.h" +#include "upisnd_debug.h" + +// Based on ADAU1361/ADAU1461/ADAU1761/ADAU1961 driver by Lars-Peter Clausen. +// Customized specifically for ADAU1961 on Pisound Micro. + +static int adau_calc_pll_cfg(unsigned int freq_in, unsigned int freq_out, + u8 regs[5]) +{ + unsigned int r, n, m, i, j; + unsigned int div; + + if (!freq_out) { + r = 0; + n = 0; + m = 0; + div = 0; + } else { + if (freq_out % freq_in != 0) { + div = DIV_ROUND_UP(freq_in, 13500000); + freq_in /= div; + r = freq_out / freq_in; + i = freq_out % freq_in; + j = gcd(i, freq_in); + n = i / j; + m = freq_in / j; + div--; + } else { + r = freq_out / freq_in; + n = 0; + m = 0; + div = 0; + } + if (n > 0xffff || m > 0xffff || div > 3 || r > 8 || r < 2) + return -EINVAL; + } + + regs[0] = m >> 8; + regs[1] = m & 0xff; + regs[2] = n >> 8; + regs[3] = n & 0xff; + regs[4] = (r << 3) | (div << 1); + if (m != 0) + regs[4] |= 1; /* Fractional mode */ + + return 0; +} + +/** + * enum adau1961_micbias_voltage - Microphone bias voltage + * @ADAU1961_MICBIAS_0_90_AVDD: 0.9 * AVDD + * @ADAU1961_MICBIAS_0_65_AVDD: 0.65 * AVDD + */ +enum adau1961_micbias_voltage { + ADAU1961_MICBIAS_0_90_AVDD = 0, + ADAU1961_MICBIAS_0_65_AVDD = 1, +}; + +/** + * enum adau1961_output_mode - Output mode configuration + * @ADAU1961_OUTPUT_MODE_HEADPHONE: Headphone output + * @ADAU1961_OUTPUT_MODE_HEADPHONE_CAPLESS: Capless headphone output + * @ADAU1961_OUTPUT_MODE_LINE: Line output + */ +enum adau1961_output_mode { + ADAU1961_OUTPUT_MODE_HEADPHONE, + ADAU1961_OUTPUT_MODE_HEADPHONE_CAPLESS, + ADAU1961_OUTPUT_MODE_LINE, +}; + +/** + * struct adau1961_platform_data - ADAU1961 Codec driver platform data + * @input_differential: If true the input pins will be configured in + * differential mode. + * @lineout_mode: Output mode for the LOUT/ROUT pins + * @headphone_mode: Output mode for the LHP/RHP pins + * @monoout_mode: Output mode for the MONOOUT pin. Ignored if headphone_mode + * is set to ADAU1961_OUTPUT_MODE_HEADPHONE_CAPLESS. + */ +struct adau1961_platform_data { + bool input_differential; + enum adau1961_output_mode lineout_mode; + enum adau1961_output_mode headphone_mode; + enum adau1961_output_mode monoout_mode; +}; + +struct snd_soc_component; +struct snd_pcm_substream; + +struct i2c_client; + +enum adau1961_pll { + ADAU1961_PLL, +}; + +enum adau1961_pll_src { + ADAU1961_PLL_SRC_MCLK, +}; + +enum adau1961_clk_src { + /* Automatically configure PLL based on the sample rate */ + ADAU1961_CLK_SRC_PLL_AUTO, + ADAU1961_CLK_SRC_MCLK, + ADAU1961_CLK_SRC_PLL, +}; + +struct clk; + +struct adau { + unsigned int sysclk; + unsigned int pll_freq; + struct clk *mclk; + + enum adau1961_clk_src clk_src; + void (*switch_mode)(struct device *dev); + + unsigned int dai_fmt; + + u8 pll_regs[6]; + + bool master; + bool vgnd_shorted; + bool hpl_unmuted; + bool hpr_unmuted; + + unsigned int tdm_slot[2]; + + struct regmap *regmap; +}; + +#define ADAU1961_CLOCK_CONTROL 0x4000 +#define ADAU1961_PLL_CONTROL 0x4002 +#define ADAU1961_DIGMIC_JACKDETECT 0x4008 +#define ADAU1961_REC_MIXER_LEFT0 0x400a +#define ADAU1961_REC_MIXER_LEFT1 0x400b +#define ADAU1961_REC_MIXER_RIGHT0 0x400c +#define ADAU1961_REC_MIXER_RIGHT1 0x400d +#define ADAU1961_LEFT_DIFF_INPUT_VOL 0x400e +#define ADAU1961_RIGHT_DIFF_INPUT_VOL 0x400f +#define ADAU1961_MICBIAS 0x4010 +#define ADAU1961_ALC_CTRL0 0x4011 +#define ADAU1961_ALC_CTRL1 0x4012 +#define ADAU1961_ALC_CTRL2 0x4013 +#define ADAU1961_ALC_CTRL3 0x4014 +#define ADAU1961_SERIAL_PORT0 0x4015 +#define ADAU1961_SERIAL_PORT1 0x4016 +#define ADAU1961_CONVERTER0 0x4017 +#define ADAU1961_CONVERTER1 0x4018 +#define ADAU1961_ADC_CONTROL 0x4019 +#define ADAU1961_LEFT_INPUT_DIGITAL_VOL 0x401a +#define ADAU1961_RIGHT_INPUT_DIGITAL_VOL 0x401b +#define ADAU1961_PLAY_MIXER_LEFT0 0x401c +#define ADAU1961_PLAY_MIXER_LEFT1 0x401d +#define ADAU1961_PLAY_MIXER_RIGHT0 0x401e +#define ADAU1961_PLAY_MIXER_RIGHT1 0x401f +#define ADAU1961_PLAY_LR_MIXER_LEFT 0x4020 +#define ADAU1961_PLAY_LR_MIXER_RIGHT 0x4021 +#define ADAU1961_PLAY_MIXER_MONO 0x4022 +#define ADAU1961_PLAY_HP_LEFT_VOL 0x4023 +#define ADAU1961_PLAY_HP_RIGHT_VOL 0x4024 +#define ADAU1961_PLAY_LINE_LEFT_VOL 0x4025 +#define ADAU1961_PLAY_LINE_RIGHT_VOL 0x4026 +#define ADAU1961_PLAY_MONO_OUTPUT_VOL 0x4027 +#define ADAU1961_POP_CLICK_SUPPRESS 0x4028 +#define ADAU1961_PLAY_POWER_MGMT 0x4029 +#define ADAU1961_DAC_CONTROL0 0x402a +#define ADAU1961_DAC_CONTROL1 0x402b +#define ADAU1961_DAC_CONTROL2 0x402c +#define ADAU1961_SERIAL_PORT_PAD 0x402d +#define ADAU1961_CONTROL_PORT_PAD0 0x402f +#define ADAU1961_CONTROL_PORT_PAD1 0x4030 +#define ADAU1961_JACK_DETECT_PIN 0x4031 +#define ADAU1961_DEJITTER 0x4036 + +#define ADAU1961_CLOCK_CONTROL_INFREQ_MASK 0x6 +#define ADAU1961_CLOCK_CONTROL_CORECLK_SRC_PLL BIT(3) +#define ADAU1961_CLOCK_CONTROL_SYSCLK_EN BIT(0) + +#define ADAU1961_SERIAL_PORT0_BCLK_POL BIT(4) +#define ADAU1961_SERIAL_PORT0_LRCLK_POL BIT(3) +#define ADAU1961_SERIAL_PORT0_MASTER BIT(0) +#define ADAU1961_SERIAL_PORT0_STEREO (0x0 << 1) +#define ADAU1961_SERIAL_PORT0_TDM4 (0x1 << 1) +#define ADAU1961_SERIAL_PORT0_TDM_MASK (0x3 << 1) +#define ADAU1961_SERIAL_PORT0_PULSE_MODE BIT(5) + +#define ADAU1961_SERIAL_PORT1_DELAY1 0x00 +#define ADAU1961_SERIAL_PORT1_DELAY0 0x01 +#define ADAU1961_SERIAL_PORT1_DELAY8 0x02 +#define ADAU1961_SERIAL_PORT1_DELAY16 0x03 +#define ADAU1961_SERIAL_PORT1_DELAY_MASK 0x03 +#define ADAU1961_SERIAL_PORT1_BCLK64 (0x0 << 5) +#define ADAU1961_SERIAL_PORT1_BCLK32 (0x1 << 5) +#define ADAU1961_SERIAL_PORT1_BCLK48 (0x2 << 5) +#define ADAU1961_SERIAL_PORT1_BCLK128 (0x3 << 5) +#define ADAU1961_SERIAL_PORT1_BCLK256 (0x4 << 5) +#define ADAU1961_SERIAL_PORT1_BCLK_MASK (0x7 << 5) + +#define ADAU1961_CONVERTER0_DAC_PAIR(x) (((x) - 1) << 5) +#define ADAU1961_CONVERTER0_DAC_PAIR_MASK (0x3 << 5) +#define ADAU1961_CONVERTER0_CONVSR_MASK 0x7 +#define ADAU1961_CONVERTER0_ADOSR BIT(3) + +#define ADAU1961_CONVERTER1_ADC_PAIR(x) ((x) - 1) +#define ADAU1961_CONVERTER1_ADC_PAIR_MASK 0x3 + +#define ADAU1961_DIFF_INPUT_VOL_LDEN BIT(0) + +#define ADAU1961_PLAY_MIXER_MONO_EN BIT(0) + +#define ADAU1961_PLAY_MONO_OUTPUT_VOL_MODE_HP BIT(0) +#define ADAU1961_PLAY_MONO_OUTPUT_VOL_UNMUTE BIT(1) + +#define ADAU1961_PLAY_HP_RIGHT_VOL_MODE_HP BIT(0) + +#define ADAU1961_PLAY_LINE_LEFT_VOL_MODE_HP BIT(0) + +#define ADAU1961_PLAY_LINE_RIGHT_VOL_MODE_HP BIT(0) + +static const DECLARE_TLV_DB_MINMAX(adau1961_digital_tlv, -9563, 0); + +static int adau1961_pll_event(struct snd_soc_dapm_widget *w, + struct snd_kcontrol *kcontrol, int event) +{ + struct snd_soc_component *component = snd_soc_dapm_to_component(w->dapm); + struct adau *adau = snd_soc_component_get_drvdata(component); + + if (SND_SOC_DAPM_EVENT_ON(event)) { + adau->pll_regs[5] = 1; + } else { + adau->pll_regs[5] = 0; + // Bypass the PLL when disabled, otherwise registers will become + // inaccessible. + regmap_update_bits(adau->regmap, ADAU1961_CLOCK_CONTROL, + ADAU1961_CLOCK_CONTROL_CORECLK_SRC_PLL, 0); + } + + // The PLL register is 6 bytes long and can only be written at once. + regmap_raw_write(adau->regmap, ADAU1961_PLL_CONTROL, + adau->pll_regs, ARRAY_SIZE(adau->pll_regs)); + + if (SND_SOC_DAPM_EVENT_ON(event)) { + mdelay(5); + regmap_update_bits(adau->regmap, ADAU1961_CLOCK_CONTROL, + ADAU1961_CLOCK_CONTROL_CORECLK_SRC_PLL, + ADAU1961_CLOCK_CONTROL_CORECLK_SRC_PLL); + } + + return 0; +} + +static int adau1961_adc_fixup(struct snd_soc_dapm_widget *w, + struct snd_kcontrol *kcontrol, int event) +{ + struct snd_soc_component *component = snd_soc_dapm_to_component(w->dapm); + struct adau *adau = snd_soc_component_get_drvdata(component); + + /* + * If we are capturing, toggle the ADOSR bit in Converter Control 0 to + * avoid losing SNR (workaround from ADI). This must be done after + * the ADC(s) have been enabled. According to the data sheet, it is + * normally illegal to set this bit when the sampling rate is 96 kHz, + * but according to ADI it is acceptable for this workaround. + */ + regmap_update_bits(adau->regmap, ADAU1961_CONVERTER0, + ADAU1961_CONVERTER0_ADOSR, ADAU1961_CONVERTER0_ADOSR); + regmap_update_bits(adau->regmap, ADAU1961_CONVERTER0, + ADAU1961_CONVERTER0_ADOSR, 0); + + return 0; +} + +static const char * const adau1961_mono_stereo_text[] = { + "Stereo", + "Mono Left Channel (L+R)", + "Mono Right Channel (L+R)", + "Mono (L+R)", +}; + +static SOC_ENUM_SINGLE_DECL(adau1961_dac_mode_enum, + ADAU1961_DAC_CONTROL0, 6, adau1961_mono_stereo_text); + +static const struct snd_kcontrol_new adau1961_dac_mode_mux = + SOC_DAPM_ENUM("DAC Mono-Stereo-Mode", adau1961_dac_mode_enum); + +static const struct snd_soc_dapm_route adau1961_dapm_pll_route = { + "SYSCLK", NULL, "PLL", +}; + +static int adau1961_set_dai_pll(struct snd_soc_dai *dai, int pll_id, + int source, unsigned int freq_in, unsigned int freq_out) +{ + struct snd_soc_component *component = dai->component; + struct adau *adau = snd_soc_component_get_drvdata(component); + int ret; + + if (freq_in < 8000000 || freq_in > 27000000) + return -EINVAL; + + ret = adau_calc_pll_cfg(freq_in, freq_out, adau->pll_regs); + if (ret < 0) + return ret; + + /* The PLL register is 6 bytes long and can only be written at once. */ + ret = regmap_raw_write(adau->regmap, ADAU1961_PLL_CONTROL, + adau->pll_regs, ARRAY_SIZE(adau->pll_regs)); + if (ret) + return ret; + + adau->pll_freq = freq_out; + + return 0; +} + +static int adau1961_set_dai_sysclk(struct snd_soc_dai *dai, + int clk_id, unsigned int freq, int dir) +{ + struct snd_soc_dapm_context *dapm = snd_soc_component_get_dapm(dai->component); + struct adau *adau = snd_soc_component_get_drvdata(dai->component); + bool is_pll; + bool was_pll; + + switch (clk_id) { + case ADAU1961_CLK_SRC_MCLK: + is_pll = false; + break; + case ADAU1961_CLK_SRC_PLL_AUTO: + if (!adau->mclk) + return -EINVAL; + fallthrough; + case ADAU1961_CLK_SRC_PLL: + is_pll = true; + break; + default: + return -EINVAL; + } + + switch (adau->clk_src) { + case ADAU1961_CLK_SRC_MCLK: + was_pll = false; + break; + case ADAU1961_CLK_SRC_PLL: + case ADAU1961_CLK_SRC_PLL_AUTO: + was_pll = true; + break; + default: + return -EINVAL; + } + + adau->sysclk = freq; + + if (is_pll != was_pll) { + if (is_pll) { + snd_soc_dapm_add_routes(dapm, + &adau1961_dapm_pll_route, 1); + } else { + snd_soc_dapm_del_routes(dapm, + &adau1961_dapm_pll_route, 1); + } + } + + adau->clk_src = clk_id; + + return 0; +} + +static int adau1961_auto_pll(struct snd_soc_dai *dai, + struct snd_pcm_hw_params *params) +{ + struct adau *adau = snd_soc_dai_get_drvdata(dai); + unsigned int pll_rate; + + switch (params_rate(params)) { + case 48000: + case 8000: + case 12000: + case 16000: + case 24000: + case 32000: + case 96000: + pll_rate = 48000 * 1024; + break; + case 44100: + case 7350: + case 11025: + case 14700: + case 22050: + case 29400: + case 88200: + pll_rate = 44100 * 1024; + break; + default: + return -EINVAL; + } + + return adau1961_set_dai_pll(dai, ADAU1961_PLL, ADAU1961_PLL_SRC_MCLK, + clk_get_rate(adau->mclk), pll_rate); +} + +static int adau1961_hw_params(struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params, struct snd_soc_dai *dai) +{ + struct snd_soc_component *component = dai->component; + struct adau *adau = snd_soc_component_get_drvdata(component); + unsigned int val, div, dsp_div; + unsigned int freq; + int ret; + + switch (adau->clk_src) { + case ADAU1961_CLK_SRC_PLL_AUTO: + ret = adau1961_auto_pll(dai, params); + if (ret) + return ret; + fallthrough; + case ADAU1961_CLK_SRC_PLL: + freq = adau->pll_freq; + break; + default: + freq = adau->sysclk; + break; + } + + if (freq % params_rate(params) != 0) + return -EINVAL; + + switch (freq / params_rate(params)) { + case 1024: /* fs */ + div = 0; + dsp_div = 1; + break; + case 6144: /* fs / 6 */ + div = 1; + dsp_div = 6; + break; + case 4096: /* fs / 4 */ + div = 2; + dsp_div = 5; + break; + case 3072: /* fs / 3 */ + div = 3; + dsp_div = 4; + break; + case 2048: /* fs / 2 */ + div = 4; + dsp_div = 3; + break; + case 1536: /* fs / 1.5 */ + div = 5; + dsp_div = 2; + break; + case 512: /* fs / 0.5 */ + div = 6; + dsp_div = 0; + break; + default: + return -EINVAL; + } + + regmap_update_bits(adau->regmap, ADAU1961_CONVERTER0, + ADAU1961_CONVERTER0_CONVSR_MASK, div); + + if (adau->dai_fmt != SND_SOC_DAIFMT_RIGHT_J) + return 0; + + switch (params_width(params)) { + case 16: + val = ADAU1961_SERIAL_PORT1_DELAY16; + break; + case 24: + val = ADAU1961_SERIAL_PORT1_DELAY8; + break; + case 32: + val = ADAU1961_SERIAL_PORT1_DELAY0; + break; + default: + return -EINVAL; + } + + return regmap_update_bits(adau->regmap, ADAU1961_SERIAL_PORT1, + ADAU1961_SERIAL_PORT1_DELAY_MASK, val); +} + +static int adau1961_set_dai_fmt(struct snd_soc_dai *dai, + unsigned int fmt) +{ + struct adau *adau = snd_soc_component_get_drvdata(dai->component); + unsigned int ctrl0, ctrl1; + unsigned int ctrl0_mask; + int lrclk_pol; + + switch (fmt & SND_SOC_DAIFMT_MASTER_MASK) { + case SND_SOC_DAIFMT_CBM_CFM: + ctrl0 = ADAU1961_SERIAL_PORT0_MASTER; + adau->master = true; + break; + case SND_SOC_DAIFMT_CBS_CFS: + ctrl0 = 0; + adau->master = false; + break; + default: + return -EINVAL; + } + + switch (fmt & SND_SOC_DAIFMT_FORMAT_MASK) { + case SND_SOC_DAIFMT_I2S: + lrclk_pol = 0; + ctrl1 = ADAU1961_SERIAL_PORT1_DELAY1; + break; + case SND_SOC_DAIFMT_LEFT_J: + case SND_SOC_DAIFMT_RIGHT_J: + lrclk_pol = 1; + ctrl1 = ADAU1961_SERIAL_PORT1_DELAY0; + break; + default: + return -EINVAL; + } + + switch (fmt & SND_SOC_DAIFMT_INV_MASK) { + case SND_SOC_DAIFMT_NB_NF: + break; + case SND_SOC_DAIFMT_IB_NF: + ctrl0 |= ADAU1961_SERIAL_PORT0_BCLK_POL; + break; + case SND_SOC_DAIFMT_NB_IF: + lrclk_pol = !lrclk_pol; + break; + case SND_SOC_DAIFMT_IB_IF: + ctrl0 |= ADAU1961_SERIAL_PORT0_BCLK_POL; + lrclk_pol = !lrclk_pol; + break; + default: + return -EINVAL; + } + + if (lrclk_pol) + ctrl0 |= ADAU1961_SERIAL_PORT0_LRCLK_POL; + + /* Set the mask to update all relevant bits in ADAU1961_SERIAL_PORT0 */ + ctrl0_mask = ADAU1961_SERIAL_PORT0_MASTER | + ADAU1961_SERIAL_PORT0_LRCLK_POL | + ADAU1961_SERIAL_PORT0_BCLK_POL | + ADAU1961_SERIAL_PORT0_PULSE_MODE; + + regmap_update_bits(adau->regmap, ADAU1961_SERIAL_PORT0, ctrl0_mask, + ctrl0); + regmap_update_bits(adau->regmap, ADAU1961_SERIAL_PORT1, + ADAU1961_SERIAL_PORT1_DELAY_MASK, ctrl1); + + adau->dai_fmt = fmt & SND_SOC_DAIFMT_FORMAT_MASK; + + return 0; +} + +static int adau1961_set_dai_tdm_slot(struct snd_soc_dai *dai, + unsigned int tx_mask, + unsigned int rx_mask, + int slots, + int slot_width) +{ + struct adau *adau = snd_soc_component_get_drvdata(dai->component); + unsigned int ser_ctrl0, ser_ctrl1; + unsigned int conv_ctrl0, conv_ctrl1; + + /* I2S mode */ + if (slots == 0) { + slots = 2; + rx_mask = 3; + tx_mask = 3; + slot_width = 32; + } + + switch (slots) { + case 2: + ser_ctrl0 = ADAU1961_SERIAL_PORT0_STEREO; + break; + case 4: + ser_ctrl0 = ADAU1961_SERIAL_PORT0_TDM4; + break; + default: + return -EINVAL; + } + + switch (slot_width * slots) { + case 32: + ser_ctrl1 = ADAU1961_SERIAL_PORT1_BCLK32; + break; + case 64: + ser_ctrl1 = ADAU1961_SERIAL_PORT1_BCLK64; + break; + case 48: + ser_ctrl1 = ADAU1961_SERIAL_PORT1_BCLK48; + break; + case 128: + ser_ctrl1 = ADAU1961_SERIAL_PORT1_BCLK128; + break; + default: + return -EINVAL; + } + + switch (rx_mask) { + case 0x03: + conv_ctrl1 = ADAU1961_CONVERTER1_ADC_PAIR(1); + adau->tdm_slot[SNDRV_PCM_STREAM_CAPTURE] = 0; + break; + case 0x0c: + conv_ctrl1 = ADAU1961_CONVERTER1_ADC_PAIR(2); + adau->tdm_slot[SNDRV_PCM_STREAM_CAPTURE] = 1; + break; + case 0x30: + conv_ctrl1 = ADAU1961_CONVERTER1_ADC_PAIR(3); + adau->tdm_slot[SNDRV_PCM_STREAM_CAPTURE] = 2; + break; + case 0xc0: + conv_ctrl1 = ADAU1961_CONVERTER1_ADC_PAIR(4); + adau->tdm_slot[SNDRV_PCM_STREAM_CAPTURE] = 3; + break; + default: + return -EINVAL; + } + + switch (tx_mask) { + case 0x03: + conv_ctrl0 = ADAU1961_CONVERTER0_DAC_PAIR(1); + adau->tdm_slot[SNDRV_PCM_STREAM_PLAYBACK] = 0; + break; + case 0x0c: + conv_ctrl0 = ADAU1961_CONVERTER0_DAC_PAIR(2); + adau->tdm_slot[SNDRV_PCM_STREAM_PLAYBACK] = 1; + break; + case 0x30: + conv_ctrl0 = ADAU1961_CONVERTER0_DAC_PAIR(3); + adau->tdm_slot[SNDRV_PCM_STREAM_PLAYBACK] = 2; + break; + case 0xc0: + conv_ctrl0 = ADAU1961_CONVERTER0_DAC_PAIR(4); + adau->tdm_slot[SNDRV_PCM_STREAM_PLAYBACK] = 3; + break; + default: + return -EINVAL; + } + + regmap_update_bits(adau->regmap, ADAU1961_CONVERTER0, + ADAU1961_CONVERTER0_DAC_PAIR_MASK, conv_ctrl0); + regmap_update_bits(adau->regmap, ADAU1961_CONVERTER1, + ADAU1961_CONVERTER1_ADC_PAIR_MASK, conv_ctrl1); + regmap_update_bits(adau->regmap, ADAU1961_SERIAL_PORT0, + ADAU1961_SERIAL_PORT0_TDM_MASK, ser_ctrl0); + regmap_update_bits(adau->regmap, ADAU1961_SERIAL_PORT1, + ADAU1961_SERIAL_PORT1_BCLK_MASK, ser_ctrl1); + + return 0; +} + +static int adau1961_startup(struct snd_pcm_substream *substream, + struct snd_soc_dai *dai) +{ + struct adau *adau = snd_soc_component_get_drvdata(dai->component); + (void)adau; + return 0; +} + +static const struct snd_soc_dai_ops adau1961_dai_ops = { + .hw_params = adau1961_hw_params, + .set_sysclk = adau1961_set_dai_sysclk, + .set_fmt = adau1961_set_dai_fmt, + .set_pll = adau1961_set_dai_pll, + .set_tdm_slot = adau1961_set_dai_tdm_slot, + .startup = adau1961_startup, +}; + +static bool adau1961_precious_register(struct device *dev, unsigned int reg) +{ + return false; +} + +static bool adau1961_volatile_register(struct device *dev, unsigned int reg) +{ + switch (reg) { + /* The PLL register is 6 bytes long */ + case ADAU1961_PLL_CONTROL: + case ADAU1961_PLL_CONTROL + 1: + case ADAU1961_PLL_CONTROL + 2: + case ADAU1961_PLL_CONTROL + 3: + case ADAU1961_PLL_CONTROL + 4: + case ADAU1961_PLL_CONTROL + 5: + return true; + default: + break; + } + + return false; +} + +static int adau1961_add_routes(struct snd_soc_component *component) +{ + struct snd_soc_dapm_context *dapm = snd_soc_component_get_dapm(component); + struct adau *adau = snd_soc_component_get_drvdata(component); + int ret; + + if (adau->clk_src != ADAU1961_CLK_SRC_MCLK) + ret = snd_soc_dapm_add_routes(dapm, &adau1961_dapm_pll_route, 1); + + return ret; +} + +static int adau1961_resume(struct snd_soc_component *component) +{ + struct adau *adau = snd_soc_component_get_drvdata(component); + + if (adau->switch_mode) + adau->switch_mode(component->dev); + + regcache_sync(adau->regmap); + + return 0; +} + +static void adau1961_remove(struct device *dev) +{ + struct adau *adau = dev_get_drvdata(dev); + + clk_disable_unprepare(adau->mclk); +} + +void adau1961_set_vgnd_shorted(struct snd_soc_component *component, bool shorted) +{ + struct adau *p = snd_soc_component_get_drvdata(component); + + struct snd_ctl_elem_value value; + struct snd_kcontrol *k; + + long prev_left, prev_right; + + if (!p) { + printe("Failed to get adau driver data!"); + return; + } + + p->vgnd_shorted = shorted; + + k = snd_soc_card_get_kcontrol(component->card, "Headphone Playback Switch"); + if (!k) { + printe("Failed to get Headphone Playback Switch control!"); + return; + } + + snd_soc_get_volsw(k, &value); + + prev_left = value.value.integer.value[0]; + prev_right = value.value.integer.value[1]; + + if (shorted) { + p->hpl_unmuted = value.value.integer.value[0]; + p->hpr_unmuted = value.value.integer.value[1]; + + value.value.integer.value[0] = 0; + value.value.integer.value[1] = 0; + } else { + value.value.integer.value[0] = p->hpl_unmuted ? 1 : 0; + value.value.integer.value[1] = p->hpr_unmuted ? 1 : 0; + } + + if (value.value.integer.value[0] != prev_left || + value.value.integer.value[1] != prev_right) { + snd_soc_put_volsw(k, &value); + snd_ctl_notify(component->card->snd_card, SNDRV_CTL_EVENT_MASK_VALUE, &k->id); + } + + regmap_update_bits(p->regmap, ADAU1961_PLAY_MIXER_MONO, + ADAU1961_PLAY_MIXER_MONO_EN, shorted ? 0 : ADAU1961_PLAY_MIXER_MONO_EN); +} +EXPORT_SYMBOL(adau1961_set_vgnd_shorted); + +bool adau1961_is_hp_capless(struct snd_soc_component *component) +{ + struct adau1961_platform_data *pdata = component->dev->platform_data; + + return pdata->headphone_mode == ADAU1961_OUTPUT_MODE_HEADPHONE_CAPLESS; +} +EXPORT_SYMBOL(adau1961_is_hp_capless); + +static int upisnd_put_hp_mute_volsw(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_soc_component *component = snd_kcontrol_chip(kcontrol); + struct adau *adau = snd_soc_component_get_drvdata(component); + + if (adau->vgnd_shorted) { + adau->hpl_unmuted = ucontrol->value.integer.value[0]; + adau->hpr_unmuted = ucontrol->value.integer.value[1]; + + if (ucontrol->value.integer.value[0] != 0 || + ucontrol->value.integer.value[1] != 0) { + printe("Ignoring unmute request while VGND is shorted!"); + + // Ensure alsamixer shows the correct state. + snd_ctl_notify(component->card->snd_card, + SNDRV_CTL_EVENT_MASK_VALUE, + &kcontrol->id + ); + return 0; + } + } + + return snd_soc_put_volsw(kcontrol, ucontrol); +} + +static const struct reg_default adau1961_reg_defaults[] = { + { ADAU1961_DEJITTER, 0x03 }, + { ADAU1961_REC_MIXER_LEFT0, 0x01 }, + { ADAU1961_REC_MIXER_LEFT1, 0x00 }, + { ADAU1961_REC_MIXER_RIGHT0, 0x01 }, + { ADAU1961_REC_MIXER_RIGHT1, 0x00 }, + { ADAU1961_LEFT_DIFF_INPUT_VOL, 0x00 }, + { ADAU1961_ALC_CTRL0, 0x00 }, + { ADAU1961_ALC_CTRL1, 0x00 }, + { ADAU1961_ALC_CTRL2, 0x00 }, + { ADAU1961_ALC_CTRL3, 0x00 }, + { ADAU1961_RIGHT_DIFF_INPUT_VOL, 0x00 }, + { ADAU1961_PLAY_LR_MIXER_LEFT, 0x01 }, + { ADAU1961_PLAY_MIXER_LEFT0, 0x00 }, + { ADAU1961_PLAY_MIXER_LEFT1, 0x00 }, + { ADAU1961_PLAY_MIXER_RIGHT0, 0x00 }, + { ADAU1961_PLAY_MIXER_RIGHT1, 0x00 }, + { ADAU1961_PLAY_LR_MIXER_RIGHT, 0x01 }, + { ADAU1961_PLAY_MIXER_MONO, 0x00 }, + { ADAU1961_PLAY_HP_LEFT_VOL, 0x00 }, + { ADAU1961_PLAY_HP_RIGHT_VOL, 0x00 }, + { ADAU1961_PLAY_LINE_LEFT_VOL, 0x00 }, + { ADAU1961_PLAY_LINE_RIGHT_VOL, 0x00 }, + { ADAU1961_PLAY_MONO_OUTPUT_VOL, 0x02 }, + { ADAU1961_POP_CLICK_SUPPRESS, 0x00 }, + { ADAU1961_JACK_DETECT_PIN, 0x00 }, + { ADAU1961_CLOCK_CONTROL, 0x00 }, + { ADAU1961_PLL_CONTROL, 0x00 }, + { ADAU1961_MICBIAS, 0x00 }, + { ADAU1961_SERIAL_PORT0, 0x00 }, + { ADAU1961_SERIAL_PORT1, 0x00 }, + { ADAU1961_CONVERTER0, 0x00 }, + { ADAU1961_CONVERTER1, 0x00 }, + { ADAU1961_LEFT_INPUT_DIGITAL_VOL, 0x00 }, + { ADAU1961_RIGHT_INPUT_DIGITAL_VOL, 0x00 }, + { ADAU1961_ADC_CONTROL, 0x00 }, + { ADAU1961_PLAY_POWER_MGMT, 0x03 }, + { ADAU1961_DAC_CONTROL0, 0x00 }, + { ADAU1961_DAC_CONTROL1, 0x00 }, + { ADAU1961_DAC_CONTROL2, 0x00 }, + { ADAU1961_SERIAL_PORT_PAD, 0xaa }, + { ADAU1961_CONTROL_PORT_PAD0, 0xaa }, + { ADAU1961_CONTROL_PORT_PAD1, 0x00 }, +}; + +static const DECLARE_TLV_DB_RANGE(adau1961_mono_output_tlv, + 1, 1, TLV_DB_SCALE_ITEM(0, 0, 0), // 0dB MX7[2:1]=01 + 2, 2, TLV_DB_SCALE_ITEM(600, 0, 0) // +6dB MX7[2:1]=10 +); + +static const DECLARE_TLV_DB_SCALE(adau1961_sing_in_tlv, -1500, 300, 1); +static const DECLARE_TLV_DB_SCALE(adau1961_diff_in_tlv, -1200, 75, 0); +static const DECLARE_TLV_DB_SCALE(adau1961_out_tlv, -5700, 100, 0); +static const DECLARE_TLV_DB_SCALE(adau1961_sidetone_tlv, -1800, 300, 1); +static const DECLARE_TLV_DB_SCALE(adau1961_boost_tlv, -600, 600, 1); +static const DECLARE_TLV_DB_SCALE(adau1961_pga_boost_tlv, -2000, 2000, 1); + +static const DECLARE_TLV_DB_SCALE(adau1961_alc_max_gain_tlv, -1200, 600, 0); +static const DECLARE_TLV_DB_SCALE(adau1961_alc_target_tlv, -2850, 150, 0); +static const DECLARE_TLV_DB_SCALE(adau1961_alc_ng_threshold_tlv, -7650, 150, 0); + +static const unsigned int adau1961_pga_slew_time_values[] = { + 3, 0, 1, 2, +}; + +static const char * const adau1961_pga_slew_time_text[] = { + "Off", + "24 ms", + "48 ms", + "96 ms", +}; + +static const char * const adau1961_alc_function_text[] = { + "Off", + "Right", + "Left", + "Stereo", +}; + +static const char * const adau1961_alc_hold_time_text[] = { + "2.67 ms", + "5.34 ms", + "10.68 ms", + "21.36 ms", + "42.72 ms", + "85.44 ms", + "170.88 ms", + "341.76 ms", + "683.52 ms", + "1367 ms", + "2734.1 ms", + "5468.2 ms", + "10936 ms", + "21873 ms", + "43745 ms", + "87491 ms", +}; + +static const char * const adau1961_alc_attack_time_text[] = { + "6 ms", + "12 ms", + "24 ms", + "48 ms", + "96 ms", + "192 ms", + "384 ms", + "768 ms", + "1540 ms", + "3070 ms", + "6140 ms", + "12290 ms", + "24580 ms", + "49150 ms", + "98300 ms", + "196610 ms", +}; + +static const char * const adau1961_alc_decay_time_text[] = { + "24 ms", + "48 ms", + "96 ms", + "192 ms", + "384 ms", + "768 ms", + "15400 ms", + "30700 ms", + "61400 ms", + "12290 ms", + "24580 ms", + "49150 ms", + "98300 ms", + "196610 ms", + "393220 ms", + "786430 ms", +}; + +static const char * const adau1961_alc_ng_type_text[] = { + "Hold", + "Mute", + "Fade", + "Fade + Mute", +}; + +static SOC_VALUE_ENUM_SINGLE_DECL(adau1961_pga_slew_time_enum, + ADAU1961_ALC_CTRL0, 6, 0x3, adau1961_pga_slew_time_text, + adau1961_pga_slew_time_values); +static SOC_ENUM_SINGLE_DECL(adau1961_alc_function_enum, + ADAU1961_ALC_CTRL0, 0, adau1961_alc_function_text); +static SOC_ENUM_SINGLE_DECL(adau1961_alc_hold_time_enum, + ADAU1961_ALC_CTRL1, 4, adau1961_alc_hold_time_text); +static SOC_ENUM_SINGLE_DECL(adau1961_alc_attack_time_enum, + ADAU1961_ALC_CTRL2, 4, adau1961_alc_attack_time_text); +static SOC_ENUM_SINGLE_DECL(adau1961_alc_decay_time_enum, + ADAU1961_ALC_CTRL2, 0, adau1961_alc_decay_time_text); +static SOC_ENUM_SINGLE_DECL(adau1961_alc_ng_type_enum, + ADAU1961_ALC_CTRL3, 6, adau1961_alc_ng_type_text); + +static const struct snd_kcontrol_new adau1961_differential_mode_controls[] = { + SOC_DOUBLE_R_TLV("Capture Volume", ADAU1961_LEFT_DIFF_INPUT_VOL, + ADAU1961_RIGHT_DIFF_INPUT_VOL, 2, 0x3f, 0, + adau1961_diff_in_tlv), + SOC_DOUBLE_R("Capture Switch", ADAU1961_LEFT_DIFF_INPUT_VOL, + ADAU1961_RIGHT_DIFF_INPUT_VOL, 1, 1, 0), + + SOC_DOUBLE_R_TLV("PGA Boost Capture Volume", ADAU1961_REC_MIXER_LEFT1, + ADAU1961_REC_MIXER_RIGHT1, 3, 2, 0, adau1961_pga_boost_tlv), + + SOC_ENUM("PGA Capture Slew Time", adau1961_pga_slew_time_enum), + + SOC_SINGLE_TLV("ALC Capture Max Gain Volume", ADAU1961_ALC_CTRL0, + 3, 7, 0, adau1961_alc_max_gain_tlv), + SOC_ENUM("ALC Capture Function", adau1961_alc_function_enum), + SOC_ENUM("ALC Capture Hold Time", adau1961_alc_hold_time_enum), + SOC_SINGLE_TLV("ALC Capture Target Volume", ADAU1961_ALC_CTRL1, + 0, 15, 0, adau1961_alc_target_tlv), + SOC_ENUM("ALC Capture Attack Time", adau1961_alc_decay_time_enum), + SOC_ENUM("ALC Capture Decay Time", adau1961_alc_attack_time_enum), + SOC_ENUM("ALC Capture Noise Gate Type", adau1961_alc_ng_type_enum), + SOC_SINGLE("ALC Capture Noise Gate Switch", + ADAU1961_ALC_CTRL3, 5, 1, 0), + SOC_SINGLE_TLV("ALC Capture Noise Gate Threshold Volume", + ADAU1961_ALC_CTRL3, 0, 31, 0, adau1961_alc_ng_threshold_tlv), +}; + +static const struct snd_kcontrol_new adau1961_single_mode_controls[] = { + SOC_SINGLE_TLV("Input 1 Capture Volume", ADAU1961_REC_MIXER_LEFT0, + 4, 7, 0, adau1961_sing_in_tlv), + SOC_SINGLE_TLV("Input 2 Capture Volume", ADAU1961_REC_MIXER_LEFT0, + 1, 7, 0, adau1961_sing_in_tlv), + SOC_SINGLE_TLV("Input 3 Capture Volume", ADAU1961_REC_MIXER_RIGHT0, + 4, 7, 0, adau1961_sing_in_tlv), + SOC_SINGLE_TLV("Input 4 Capture Volume", ADAU1961_REC_MIXER_RIGHT0, + 1, 7, 0, adau1961_sing_in_tlv), +}; + +static const struct snd_kcontrol_new adau1961_mono_controls[] = { + SOC_SINGLE_TLV("Mono Playback Volume", ADAU1961_PLAY_MONO_OUTPUT_VOL, + 2, 0x3f, 0, adau1961_out_tlv), + SOC_SINGLE("Mono Playback Switch", ADAU1961_PLAY_MONO_OUTPUT_VOL, + 1, 1, 0), + SOC_SINGLE_TLV("Mono Playback Boost Volume", ADAU1961_PLAY_MIXER_MONO, + 1, 2, 0, adau1961_mono_output_tlv), +}; + +static const struct snd_kcontrol_new adau1961_left_mixer_controls[] = { + SOC_DAPM_SINGLE_AUTODISABLE("Left DAC Switch", + ADAU1961_PLAY_MIXER_LEFT0, 5, 1, 0), + SOC_DAPM_SINGLE_AUTODISABLE("Right DAC Switch", + ADAU1961_PLAY_MIXER_LEFT0, 6, 1, 0), + SOC_DAPM_SINGLE_TLV("Aux Bypass Volume", + ADAU1961_PLAY_MIXER_LEFT0, 1, 8, 0, adau1961_sidetone_tlv), + SOC_DAPM_SINGLE_TLV("Right Bypass Volume", + ADAU1961_PLAY_MIXER_LEFT1, 4, 8, 0, adau1961_sidetone_tlv), + SOC_DAPM_SINGLE_TLV("Left Bypass Volume", + ADAU1961_PLAY_MIXER_LEFT1, 0, 8, 0, adau1961_sidetone_tlv), +}; + +static const struct snd_kcontrol_new adau1961_right_mixer_controls[] = { + SOC_DAPM_SINGLE_AUTODISABLE("Left DAC Switch", + ADAU1961_PLAY_MIXER_RIGHT0, 5, 1, 0), + SOC_DAPM_SINGLE_AUTODISABLE("Right DAC Switch", + ADAU1961_PLAY_MIXER_RIGHT0, 6, 1, 0), + SOC_DAPM_SINGLE_TLV("Aux Bypass Volume", + ADAU1961_PLAY_MIXER_RIGHT0, 1, 8, 0, adau1961_sidetone_tlv), + SOC_DAPM_SINGLE_TLV("Right Bypass Volume", + ADAU1961_PLAY_MIXER_RIGHT1, 4, 8, 0, adau1961_sidetone_tlv), + SOC_DAPM_SINGLE_TLV("Left Bypass Volume", + ADAU1961_PLAY_MIXER_RIGHT1, 0, 8, 0, adau1961_sidetone_tlv), +}; + +static const struct snd_kcontrol_new adau1961_left_lr_mixer_controls[] = { + SOC_DAPM_SINGLE_TLV("Left Volume", + ADAU1961_PLAY_LR_MIXER_LEFT, 1, 2, 0, adau1961_boost_tlv), + SOC_DAPM_SINGLE_TLV("Right Volume", + ADAU1961_PLAY_LR_MIXER_LEFT, 3, 2, 0, adau1961_boost_tlv), +}; + +static const struct snd_kcontrol_new adau1961_right_lr_mixer_controls[] = { + SOC_DAPM_SINGLE_TLV("Left Volume", + ADAU1961_PLAY_LR_MIXER_RIGHT, 1, 2, 0, adau1961_boost_tlv), + SOC_DAPM_SINGLE_TLV("Right Volume", + ADAU1961_PLAY_LR_MIXER_RIGHT, 3, 2, 0, adau1961_boost_tlv), +}; + +static const struct snd_kcontrol_new adau1961_controls[] = { + SOC_DOUBLE_R_TLV("Digital Capture Volume", + ADAU1961_LEFT_INPUT_DIGITAL_VOL, + ADAU1961_RIGHT_INPUT_DIGITAL_VOL, + 0, 0xff, 1, adau1961_digital_tlv), + SOC_DOUBLE_R_TLV("Digital Playback Volume", ADAU1961_DAC_CONTROL1, + ADAU1961_DAC_CONTROL2, 0, 0xff, 1, adau1961_digital_tlv), + + SOC_SINGLE("ADC High Pass Filter Switch", ADAU1961_ADC_CONTROL, + 5, 1, 0), + SOC_SINGLE("Playback De-emphasis Switch", ADAU1961_DAC_CONTROL0, + 2, 1, 0), + + SOC_DOUBLE_R_TLV("Aux Capture Volume", ADAU1961_REC_MIXER_LEFT1, + ADAU1961_REC_MIXER_RIGHT1, 0, 7, 0, adau1961_sing_in_tlv), + + SOC_DOUBLE_R_TLV("Headphone Playback Volume", ADAU1961_PLAY_HP_LEFT_VOL, + ADAU1961_PLAY_HP_RIGHT_VOL, 2, 0x3f, 0, adau1961_out_tlv), + { + .iface = SNDRV_CTL_ELEM_IFACE_MIXER, + .name = "Headphone Playback Switch", + .info = snd_soc_info_volsw, + .get = snd_soc_get_volsw, + .put = upisnd_put_hp_mute_volsw, + .private_value = SOC_DOUBLE_R_VALUE(ADAU1961_PLAY_HP_LEFT_VOL, + ADAU1961_PLAY_HP_RIGHT_VOL, 1, 1, 0) + }, + SOC_DOUBLE_R_TLV("Lineout Playback Volume", ADAU1961_PLAY_LINE_LEFT_VOL, + ADAU1961_PLAY_LINE_RIGHT_VOL, 2, 0x3f, 0, adau1961_out_tlv), + SOC_DOUBLE_R("Lineout Playback Switch", ADAU1961_PLAY_LINE_LEFT_VOL, + ADAU1961_PLAY_LINE_RIGHT_VOL, 1, 1, 0), + SOC_SINGLE("MicBias Switch", ADAU1961_MICBIAS, 0, 1, 0), +}; + +static int adau1961_dejitter_fixup(struct snd_soc_dapm_widget *w, + struct snd_kcontrol *kcontrol, int event) +{ + struct snd_soc_component *component = snd_soc_dapm_to_component(w->dapm); + struct adau *adau = snd_soc_component_get_drvdata(component); + + // After any power changes have been made the dejitter circuit + // has to be reinitialized. + regmap_write(adau->regmap, ADAU1961_DEJITTER, 0); + if (!adau->master) + regmap_write(adau->regmap, ADAU1961_DEJITTER, 3); + + return 0; +} + +static const struct snd_soc_dapm_widget adau1961_dapm_widgets[] = { + SND_SOC_DAPM_MIXER("Left Input Mixer", SND_SOC_NOPM, 0, 0, + NULL, 0), + SND_SOC_DAPM_MIXER("Right Input Mixer", SND_SOC_NOPM, 0, 0, + NULL, 0), + + SND_SOC_DAPM_SUPPLY("MICBIAS", ADAU1961_MICBIAS, 0, 0, NULL, 0), + + SOC_MIXER_ARRAY("Left Playback Mixer", ADAU1961_PLAY_MIXER_LEFT0, + 0, 0, adau1961_left_mixer_controls), + SOC_MIXER_ARRAY("Right Playback Mixer", ADAU1961_PLAY_MIXER_RIGHT0, + 0, 0, adau1961_right_mixer_controls), + + SOC_MIXER_ARRAY("Left LR Playback Mixer", SND_SOC_NOPM, + 0, 0, adau1961_left_lr_mixer_controls), + SOC_MIXER_ARRAY("Right LR Playback Mixer", SND_SOC_NOPM, + 0, 0, adau1961_right_lr_mixer_controls), + + SND_SOC_DAPM_SUPPLY("Headphone", ADAU1961_PLAY_HP_LEFT_VOL, + 0, 0, NULL, 0), + + SND_SOC_DAPM_SUPPLY_S("SYSCLK", 2, SND_SOC_NOPM, 0, 0, NULL, 0), + + SND_SOC_DAPM_POST("Dejitter fixup", adau1961_dejitter_fixup), + + SND_SOC_DAPM_INPUT("LAUX"), + SND_SOC_DAPM_INPUT("RAUX"), + SND_SOC_DAPM_INPUT("LINP"), + SND_SOC_DAPM_INPUT("LINN"), + SND_SOC_DAPM_INPUT("RINP"), + SND_SOC_DAPM_INPUT("RINN"), + + SND_SOC_DAPM_OUTPUT("LOUT"), + SND_SOC_DAPM_OUTPUT("ROUT"), + SND_SOC_DAPM_OUTPUT("LHP"), + SND_SOC_DAPM_OUTPUT("RHP"), + + SND_SOC_DAPM_SUPPLY_S("PLL", 3, SND_SOC_NOPM, 0, 0, adau1961_pll_event, + SND_SOC_DAPM_PRE_PMU | SND_SOC_DAPM_POST_PMD), + + SND_SOC_DAPM_SUPPLY("AIFCLK", SND_SOC_NOPM, 0, 0, NULL, 0), + + SND_SOC_DAPM_SUPPLY("Left Playback Enable", SND_SOC_NOPM, + 0, 0, NULL, 0), + SND_SOC_DAPM_SUPPLY("Right Playback Enable", SND_SOC_NOPM, + 1, 0, NULL, 0), + + SND_SOC_DAPM_MUX("Left DAC Mode Mux", SND_SOC_NOPM, 0, 0, + &adau1961_dac_mode_mux), + SND_SOC_DAPM_MUX("Right DAC Mode Mux", SND_SOC_NOPM, 0, 0, + &adau1961_dac_mode_mux), + + SND_SOC_DAPM_ADC_E("Left Decimator", NULL, ADAU1961_ADC_CONTROL, 0, 0, + adau1961_adc_fixup, SND_SOC_DAPM_POST_PMU), + SND_SOC_DAPM_ADC("Right Decimator", NULL, ADAU1961_ADC_CONTROL, 1, 0), + SND_SOC_DAPM_DAC("Left DAC", NULL, ADAU1961_DAC_CONTROL0, 0, 0), + SND_SOC_DAPM_DAC("Right DAC", NULL, ADAU1961_DAC_CONTROL0, 1, 0), +}; + +static const struct snd_kcontrol_new adau1961_mono_mixer_controls[] = { + SOC_DAPM_SINGLE_TLV("Mono Mixer Gain", ADAU1961_PLAY_MIXER_MONO, + 1, 2, 0, adau1961_mono_output_tlv), +}; + +static const struct snd_soc_dapm_widget adau1961_mono_dapm_widgets[] = { + SND_SOC_DAPM_MIXER("Mono Playback Mixer", ADAU1961_PLAY_MIXER_MONO, + 0, 0, adau1961_mono_mixer_controls, + ARRAY_SIZE(adau1961_mono_mixer_controls)), + + SND_SOC_DAPM_OUTPUT("MONOOUT"), +}; + +static const struct snd_soc_dapm_widget adau1961_capless_dapm_widgets[] = { + SND_SOC_DAPM_SUPPLY_S("Headphone VGND", 1, ADAU1961_PLAY_MIXER_MONO, + 0, 0, NULL, 0), +}; + +static const struct snd_soc_dapm_route adau1961_dapm_routes[] = { + { "Left Input Mixer", NULL, "LINP" }, + { "Left Input Mixer", NULL, "LINN" }, + { "Left Input Mixer", NULL, "LAUX" }, + + { "Right Input Mixer", NULL, "RINP" }, + { "Right Input Mixer", NULL, "RINN" }, + { "Right Input Mixer", NULL, "RAUX" }, + + { "Left Input Mixer", NULL, "MICBIAS" }, + { "Right Input Mixer", NULL, "MICBIAS" }, + + { "Left Playback Mixer", NULL, "Left Playback Enable"}, + { "Right Playback Mixer", NULL, "Right Playback Enable"}, + { "Left LR Playback Mixer", NULL, "Left Playback Enable"}, + { "Right LR Playback Mixer", NULL, "Right Playback Enable"}, + + { "Left Playback Mixer", "Left DAC Switch", "Left DAC" }, + { "Left Playback Mixer", "Right DAC Switch", "Right DAC" }, + + { "Right Playback Mixer", "Left DAC Switch", "Left DAC" }, + { "Right Playback Mixer", "Right DAC Switch", "Right DAC" }, + + { "Left LR Playback Mixer", "Left Volume", "Left Playback Mixer" }, + { "Left LR Playback Mixer", "Right Volume", "Right Playback Mixer" }, + + { "Right LR Playback Mixer", "Left Volume", "Left Playback Mixer" }, + { "Right LR Playback Mixer", "Right Volume", "Right Playback Mixer" }, + + { "LHP", NULL, "Left Playback Mixer" }, + { "RHP", NULL, "Right Playback Mixer" }, + + { "LHP", NULL, "Headphone" }, + { "RHP", NULL, "Headphone" }, + + { "LOUT", NULL, "Left LR Playback Mixer" }, + { "ROUT", NULL, "Right LR Playback Mixer" }, + + { "Left Playback Mixer", "Aux Bypass Volume", "LAUX" }, + { "Left Playback Mixer", "Left Bypass Volume", "Left Input Mixer" }, + { "Left Playback Mixer", "Right Bypass Volume", "Right Input Mixer" }, + { "Right Playback Mixer", "Aux Bypass Volume", "RAUX" }, + { "Right Playback Mixer", "Left Bypass Volume", "Left Input Mixer" }, + { "Right Playback Mixer", "Right Bypass Volume", "Right Input Mixer" }, + + { "Left Decimator", NULL, "SYSCLK" }, + { "Right Decimator", NULL, "SYSCLK" }, + { "Left DAC", NULL, "SYSCLK" }, + { "Right DAC", NULL, "SYSCLK" }, + { "Capture", NULL, "SYSCLK" }, + { "Playback", NULL, "SYSCLK" }, + + { "Left DAC", NULL, "Left DAC Mode Mux" }, + { "Right DAC", NULL, "Right DAC Mode Mux" }, + + { "Capture", NULL, "AIFCLK" }, + { "Playback", NULL, "AIFCLK" }, + + { "Left DAC Mode Mux", "Stereo", "Playback" }, + { "Left DAC Mode Mux", "Mono (L+R)", "Playback" }, + { "Left DAC Mode Mux", "Mono Left Channel (L+R)", "Playback" }, + { "Right DAC Mode Mux", "Stereo", "Playback" }, + { "Right DAC Mode Mux", "Mono (L+R)", "Playback" }, + { "Right DAC Mode Mux", "Mono Right Channel (L+R)", "Playback" }, + { "Capture", NULL, "Left Decimator" }, + { "Capture", NULL, "Right Decimator" }, + + { "Left Decimator", NULL, "Left Input Mixer" }, + { "Right Decimator", NULL, "Right Input Mixer" }, +}; + +static const struct snd_soc_dapm_route adau1961_mono_dapm_routes[] = { + { "Mono Playback Mixer", NULL, "Left Playback Mixer" }, + { "Mono Playback Mixer", NULL, "Right Playback Mixer" }, + + { "MONOOUT", NULL, "Mono Playback Mixer" }, +}; + +static const struct snd_soc_dapm_route adau1961_capless_dapm_routes[] = { + { "Headphone", NULL, "Headphone VGND" }, +}; + +static int adau1961_set_bias_level(struct snd_soc_component *component, + enum snd_soc_bias_level level) +{ + struct adau *adau = snd_soc_component_get_drvdata(component); + + switch (level) { + case SND_SOC_BIAS_ON: + break; + case SND_SOC_BIAS_PREPARE: + break; + case SND_SOC_BIAS_STANDBY: + regcache_cache_only(adau->regmap, false); + regmap_update_bits(adau->regmap, ADAU1961_CLOCK_CONTROL, + ADAU1961_CLOCK_CONTROL_SYSCLK_EN, + ADAU1961_CLOCK_CONTROL_SYSCLK_EN); + if (snd_soc_component_get_bias_level(component) == SND_SOC_BIAS_OFF) + regcache_sync(adau->regmap); + break; + case SND_SOC_BIAS_OFF: + regmap_update_bits(adau->regmap, ADAU1961_CLOCK_CONTROL, + ADAU1961_CLOCK_CONTROL_SYSCLK_EN, 0); + regcache_cache_only(adau->regmap, true); + break; + } + return 0; +} + +static enum adau1961_output_mode adau1961_get_lineout_mode(struct snd_soc_component *component) +{ + struct adau1961_platform_data *pdata = component->dev->platform_data; + + if (pdata) + return pdata->lineout_mode; + + return ADAU1961_OUTPUT_MODE_LINE; +} + +static enum adau1961_output_mode adau1961_get_monoout_mode(struct snd_soc_component *component) +{ + struct adau1961_platform_data *pdata = component->dev->platform_data; + + if (pdata) + return pdata->monoout_mode; + + return ADAU1961_OUTPUT_MODE_LINE; +} + +static int adau1961_setup_headphone_mode(struct snd_soc_component *component) +{ + struct snd_soc_dapm_context *dapm = snd_soc_component_get_dapm(component); + struct adau *adau = snd_soc_component_get_drvdata(component); + struct adau1961_platform_data *pdata = component->dev->platform_data; + enum adau1961_output_mode mode; + int ret; + + if (pdata) + mode = pdata->headphone_mode; + else + mode = ADAU1961_OUTPUT_MODE_HEADPHONE; + + switch (mode) { + case ADAU1961_OUTPUT_MODE_LINE: + break; + case ADAU1961_OUTPUT_MODE_HEADPHONE_CAPLESS: + regmap_update_bits(adau->regmap, ADAU1961_PLAY_MONO_OUTPUT_VOL, + ADAU1961_PLAY_MONO_OUTPUT_VOL_MODE_HP | + ADAU1961_PLAY_MONO_OUTPUT_VOL_UNMUTE, + ADAU1961_PLAY_MONO_OUTPUT_VOL_MODE_HP | + ADAU1961_PLAY_MONO_OUTPUT_VOL_UNMUTE); + fallthrough; + case ADAU1961_OUTPUT_MODE_HEADPHONE: + regmap_update_bits(adau->regmap, ADAU1961_PLAY_HP_RIGHT_VOL, + ADAU1961_PLAY_HP_RIGHT_VOL_MODE_HP, + ADAU1961_PLAY_HP_RIGHT_VOL_MODE_HP); + break; + default: + return -EINVAL; + } + + if (mode == ADAU1961_OUTPUT_MODE_HEADPHONE_CAPLESS) { + ret = snd_soc_dapm_new_controls(dapm, + adau1961_capless_dapm_widgets, + ARRAY_SIZE(adau1961_capless_dapm_widgets)); + if (ret) + return ret; + ret = snd_soc_dapm_add_routes(dapm, + adau1961_capless_dapm_routes, + ARRAY_SIZE(adau1961_capless_dapm_routes)); + } else { + ret = snd_soc_add_component_controls(component, adau1961_mono_controls, + ARRAY_SIZE(adau1961_mono_controls)); + if (ret) + return ret; + ret = snd_soc_dapm_new_controls(dapm, + adau1961_mono_dapm_widgets, + ARRAY_SIZE(adau1961_mono_dapm_widgets)); + if (ret) + return ret; + ret = snd_soc_dapm_add_routes(dapm, + adau1961_mono_dapm_routes, + ARRAY_SIZE(adau1961_mono_dapm_routes)); + } + + return ret; +} + +static bool adau1961_readable_register(struct device *dev, unsigned int reg) +{ + switch (reg) { + case ADAU1961_REC_MIXER_LEFT0: + case ADAU1961_REC_MIXER_LEFT1: + case ADAU1961_REC_MIXER_RIGHT0: + case ADAU1961_REC_MIXER_RIGHT1: + case ADAU1961_LEFT_DIFF_INPUT_VOL: + case ADAU1961_RIGHT_DIFF_INPUT_VOL: + case ADAU1961_PLAY_LR_MIXER_LEFT: + case ADAU1961_PLAY_MIXER_LEFT0: + case ADAU1961_PLAY_MIXER_LEFT1: + case ADAU1961_PLAY_MIXER_RIGHT0: + case ADAU1961_PLAY_MIXER_RIGHT1: + case ADAU1961_PLAY_LR_MIXER_RIGHT: + case ADAU1961_PLAY_MIXER_MONO: + case ADAU1961_PLAY_HP_LEFT_VOL: + case ADAU1961_PLAY_HP_RIGHT_VOL: + case ADAU1961_PLAY_LINE_LEFT_VOL: + case ADAU1961_PLAY_LINE_RIGHT_VOL: + case ADAU1961_PLAY_MONO_OUTPUT_VOL: + case ADAU1961_POP_CLICK_SUPPRESS: + case ADAU1961_JACK_DETECT_PIN: + case ADAU1961_DEJITTER: + case ADAU1961_ALC_CTRL0: + case ADAU1961_ALC_CTRL1: + case ADAU1961_ALC_CTRL2: + case ADAU1961_ALC_CTRL3: + case ADAU1961_CLOCK_CONTROL: + case ADAU1961_PLL_CONTROL: + case ADAU1961_MICBIAS: + case ADAU1961_SERIAL_PORT0: + case ADAU1961_SERIAL_PORT1: + case ADAU1961_CONVERTER0: + case ADAU1961_CONVERTER1: + case ADAU1961_LEFT_INPUT_DIGITAL_VOL: + case ADAU1961_RIGHT_INPUT_DIGITAL_VOL: + case ADAU1961_ADC_CONTROL: + case ADAU1961_PLAY_POWER_MGMT: + case ADAU1961_DAC_CONTROL0: + case ADAU1961_DAC_CONTROL1: + case ADAU1961_DAC_CONTROL2: + case ADAU1961_SERIAL_PORT_PAD: + case ADAU1961_CONTROL_PORT_PAD0: + case ADAU1961_CONTROL_PORT_PAD1: + return true; + default: + break; + } + + return false; +} + +static int adau1961_component_probe(struct snd_soc_component *component) +{ + struct adau1961_platform_data *pdata = component->dev->platform_data; + struct adau *adau = snd_soc_component_get_drvdata(component); + int ret; + + if (pdata && pdata->input_differential) { + regmap_update_bits(adau->regmap, ADAU1961_LEFT_DIFF_INPUT_VOL, + ADAU1961_DIFF_INPUT_VOL_LDEN, + ADAU1961_DIFF_INPUT_VOL_LDEN); + regmap_update_bits(adau->regmap, ADAU1961_RIGHT_DIFF_INPUT_VOL, + ADAU1961_DIFF_INPUT_VOL_LDEN, + ADAU1961_DIFF_INPUT_VOL_LDEN); + ret = snd_soc_add_component_controls(component, + adau1961_differential_mode_controls, + ARRAY_SIZE(adau1961_differential_mode_controls)); + if (ret) + return ret; + } else { + ret = snd_soc_add_component_controls(component, + adau1961_single_mode_controls, + ARRAY_SIZE(adau1961_single_mode_controls)); + if (ret) + return ret; + } + + switch (adau1961_get_lineout_mode(component)) { + case ADAU1961_OUTPUT_MODE_LINE: + break; + case ADAU1961_OUTPUT_MODE_HEADPHONE: + regmap_update_bits(adau->regmap, ADAU1961_PLAY_LINE_LEFT_VOL, + ADAU1961_PLAY_LINE_LEFT_VOL_MODE_HP, + ADAU1961_PLAY_LINE_LEFT_VOL_MODE_HP); + regmap_update_bits(adau->regmap, ADAU1961_PLAY_LINE_RIGHT_VOL, + ADAU1961_PLAY_LINE_RIGHT_VOL_MODE_HP, + ADAU1961_PLAY_LINE_RIGHT_VOL_MODE_HP); + break; + default: + return -EINVAL; + } + + switch (adau1961_get_monoout_mode(component)) { + case ADAU1961_OUTPUT_MODE_LINE: + break; + case ADAU1961_OUTPUT_MODE_HEADPHONE: + regmap_update_bits(adau->regmap, ADAU1961_PLAY_MONO_OUTPUT_VOL, + ADAU1961_PLAY_MONO_OUTPUT_VOL_MODE_HP, + ADAU1961_PLAY_MONO_OUTPUT_VOL_MODE_HP); + break; + default: + return -EINVAL; + } + + ret = adau1961_setup_headphone_mode(component); + if (ret) + return ret; + + ret = adau1961_add_routes(component); + if (ret < 0) + return ret; + + return 0; +} + +static const struct snd_soc_component_driver adau1961_component_driver = { + .probe = adau1961_component_probe, + .resume = adau1961_resume, + .set_bias_level = adau1961_set_bias_level, + .controls = adau1961_controls, + .num_controls = ARRAY_SIZE(adau1961_controls), + .dapm_widgets = adau1961_dapm_widgets, + .num_dapm_widgets = ARRAY_SIZE(adau1961_dapm_widgets), + .dapm_routes = adau1961_dapm_routes, + .num_dapm_routes = ARRAY_SIZE(adau1961_dapm_routes), + .suspend_bias_off = 1, + .idle_bias_on = 1, + .use_pmdown_time = 1, + .endianness = 1, + .legacy_dai_naming = 0, +}; + +#define ADAU1961_FORMATS (SNDRV_PCM_FMTBIT_S16_LE | SNDRV_PCM_FMTBIT_S24_LE | \ + SNDRV_PCM_FMTBIT_S32_LE) + +static struct snd_soc_dai_driver adau1961_dai_driver = { + .name = "adau-hifi", + .playback = { + .stream_name = "Playback", + .channels_min = 2, + .channels_max = 2, + .rates = SNDRV_PCM_RATE_8000_96000, + .formats = ADAU1961_FORMATS, + }, + .capture = { + .stream_name = "Capture", + .channels_min = 2, + .channels_max = 2, + .rates = SNDRV_PCM_RATE_8000_96000, + .formats = ADAU1961_FORMATS, + }, + .ops = &adau1961_dai_ops, +}; + +static int adau1961_probe(struct device *dev, struct regmap *regmap, + void (*switch_mode)(struct device *dev)) +{ + struct snd_soc_dai_driver *dai_drv; + int ret; + + dai_drv = &adau1961_dai_driver; + + struct adau *adau; + + if (IS_ERR(regmap)) + return PTR_ERR(regmap); + + adau = devm_kzalloc(dev, sizeof(*adau), GFP_KERNEL); + if (!adau) + return -ENOMEM; + + adau->mclk = devm_clk_get(dev, "mclk"); + if (IS_ERR(adau->mclk)) { + if (PTR_ERR(adau->mclk) != -ENOENT) + return PTR_ERR(adau->mclk); + /* Clock is optional (for the driver) */ + adau->mclk = NULL; + } else if (adau->mclk) { + adau->clk_src = ADAU1961_CLK_SRC_PLL_AUTO; + + /* + * Any valid PLL output rate will work at this point, use one + * that is likely to be chosen later as well. The register will + * be written when the PLL is powered up for the first time. + */ + ret = adau_calc_pll_cfg(clk_get_rate(adau->mclk), 48000 * 1024, + adau->pll_regs); + if (ret < 0) + return ret; + + ret = clk_prepare_enable(adau->mclk); + if (ret) + return ret; + } + + adau->regmap = regmap; + adau->switch_mode = switch_mode; + + dev_set_drvdata(dev, adau); + + if (switch_mode) + switch_mode(dev); + + // Enable cache only mode as we could miss writes before bias level + // reaches standby and the core clock is enabled + regcache_cache_only(regmap, true); + + return devm_snd_soc_register_component(dev, &adau1961_component_driver, dai_drv, 1); +} + +static const struct regmap_config adau1961_regmap_config = { + .val_bits = 8, + .reg_bits = 16, + .max_register = 0x4036, + .reg_defaults = adau1961_reg_defaults, + .num_reg_defaults = ARRAY_SIZE(adau1961_reg_defaults), + .readable_reg = adau1961_readable_register, + .volatile_reg = adau1961_volatile_register, + .precious_reg = adau1961_precious_register, + .cache_type = REGCACHE_RBTREE, +}; + +static int upisnd_codec_fill_pdata(struct device_node *of_node, + struct adau1961_platform_data *pdata) +{ + const char *input_mode = "differential"; + const char *hp_out_mode = "capless-headphone"; + const char *line_out_mode = "line-out"; + const char *mono_out_mode = "line-out"; + + of_property_read_string(of_node, "input-mode", &input_mode); + of_property_read_string(of_node, "hp-out-mode", &hp_out_mode); + of_property_read_string(of_node, "line-out-mode", &line_out_mode); + of_property_read_string(of_node, "mono-out-mode", &mono_out_mode); + + if (strncmp(input_mode, "differential", 13) == 0) { + pdata->input_differential = true; + } else if (strncmp(input_mode, "single-ended", 13) == 0) { + pdata->input_differential = false; + } else { + printe("Invalid input mode: %s", input_mode); + return -EINVAL; + } + + if (strncmp(hp_out_mode, "headphone", 10) == 0) { + pdata->headphone_mode = ADAU1961_OUTPUT_MODE_HEADPHONE; + } else if (strncmp(hp_out_mode, "capless-headphone", 18) == 0) { + pdata->headphone_mode = ADAU1961_OUTPUT_MODE_HEADPHONE_CAPLESS; + } else if (strncmp(hp_out_mode, "line-out", 9) == 0) { + pdata->headphone_mode = ADAU1961_OUTPUT_MODE_LINE; + } else { + printe("Invalid headphone output mode: %s", hp_out_mode); + return -EINVAL; + } + + if (strncmp(line_out_mode, "line-out", 9) == 0) { + pdata->lineout_mode = ADAU1961_OUTPUT_MODE_LINE; + } else if (strncmp(line_out_mode, "headphone", 10) == 0) { + pdata->lineout_mode = ADAU1961_OUTPUT_MODE_HEADPHONE; + } else { + printe("Invalid line-out mode: %s", line_out_mode); + return -EINVAL; + } + + if (strncmp(mono_out_mode, "line-out", 9) == 0) { + pdata->monoout_mode = ADAU1961_OUTPUT_MODE_LINE; + } else if (strncmp(line_out_mode, "headphone", 10) == 0) { + pdata->monoout_mode = ADAU1961_OUTPUT_MODE_HEADPHONE; + } else { + printe("Invalid mono-out mode: %s", mono_out_mode); + return -EINVAL; + } + + return 0; +} + +static int upisnd_codec_i2c_probe(struct i2c_client *client) +{ + struct adau1961_platform_data *pdata = NULL; + + if (client->dev.of_node) { + pdata = devm_kzalloc(&client->dev, + sizeof(struct adau1961_platform_data), GFP_KERNEL); + + if (!pdata) + return -ENOMEM; + + int err = upisnd_codec_fill_pdata(client->dev.of_node, pdata); + + if (err < 0) { + printe("Failed to fill platform data from device tree! (%d)", err); + return err; + } + } + + client->dev.platform_data = pdata; + + struct regmap_config config; + + config = adau1961_regmap_config; + config.val_bits = 8; + config.reg_bits = 16; + + return adau1961_probe(&client->dev, devm_regmap_init_i2c(client, &config), NULL); +} + +static void upisnd_codec_i2c_remove(struct i2c_client *client) +{ + adau1961_remove(&client->dev); +} + +static const struct i2c_device_id upisnd_codec_i2c_ids[] = { + { "upisnd-codec", 0 }, + { } +}; +MODULE_DEVICE_TABLE(i2c, upisnd_codec_i2c_ids); + +static const struct of_device_id upisnd_codec_i2c_dt_ids[] = { + { .compatible = "blokas,upisnd-codec", }, + { }, +}; +MODULE_DEVICE_TABLE(of, upisnd_codec_i2c_dt_ids); + +struct i2c_driver upisnd_codec_i2c_driver = { + .driver = { + .name = "upisnd_codec", + .owner = THIS_MODULE, + .of_match_table = of_match_ptr(upisnd_codec_i2c_dt_ids), + }, + .probe = upisnd_codec_i2c_probe, + .remove = upisnd_codec_i2c_remove, + .id_table = upisnd_codec_i2c_ids, +}; +module_i2c_driver(upisnd_codec_i2c_driver); + +MODULE_DESCRIPTION("Codec Driver for Pisound Micro"); +MODULE_AUTHOR("Giedrius Trainavi\xc4\x8dius "); +MODULE_LICENSE("GPL v2"); + +/* vim: set ts=8 sw=8 noexpandtab: */ --- /dev/null +++ b/sound/drivers/upisnd/upisnd_codec.h @@ -0,0 +1,23 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Pisound Micro Linux kernel module. + * Copyright (C) 2017-2025 Vilniaus Blokas UAB, https://blokas.io/ + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; version 2 of the + * License. + * + * 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. + */ + + #ifndef UPISOUND_CODEC_H + #define UPISOUND_CODEC_H + +extern void adau1961_set_vgnd_shorted(struct snd_soc_component *component, bool shorted); +extern bool adau1961_is_hp_capless(struct snd_soc_component *component); + +#endif // UPISOUND_CODEC_H --- /dev/null +++ b/sound/drivers/upisnd/upisnd_comm.c @@ -0,0 +1,638 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Pisound Micro Linux kernel module. + * Copyright (C) 2017-2025 Vilniaus Blokas UAB, https://blokas.io/ + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; version 2 of the + * License. + * + * 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 "upisnd_common.h" + +DECLARE_CRC8_TABLE(upisnd_crc8_table); +enum { UPISND_CRC8_POLYNOMIAL = 0x83 }; + +static upisnd_msg_id_t upisnd_next_msg_id(struct upisnd_instance *instance) +{ + instance->comm.msg_id_counter = instance->comm.msg_id_counter != 127 ? + instance->comm.msg_id_counter + 1 : 1; + + return instance->comm.msg_id_counter; +} + +struct upisnd_command_task_t { + struct list_head list; + upisnd_msg_id_t msg_id; + struct completion done; + int result; + u8 cmd[UPISND_MAX_PACKET_LENGTH]; + u8 response[UPISND_MAX_PACKET_LENGTH]; +}; + +static void upisnd_command_response(struct upisnd_command_task_t *task, + const struct upisnd_cmd_t *response, + unsigned int n) +{ + memcpy(task->response, response, min_t(unsigned int, n, UPISND_MAX_PACKET_LENGTH)); + if (upisnd_cmd_matches(response, UPISND_CMD_RESULT, sizeof(struct upisnd_cmd_result_t))) { + int response_code = ((const struct upisnd_cmd_result_t *)response)->result; + + printd("hi %p %d %d", task, response_code == -ETIME, response_code); + task->result = response_code; + } else { + task->result = 0; + } +} + +static void upisnd_command_timeout(struct upisnd_command_task_t *task) +{ + struct upisnd_cmd_result_t result; + + result.cmd.cmd_and_size = upisnd_cmd_encode(UPISND_CMD_RESULT, sizeof(result)); + result.cmd.flags_and_msg_id = upisnd_msg_id_encode(task->msg_id, true); + result.result = -ETIME; + upisnd_command_response(task, &result.cmd, sizeof(result)); +} + +static int upisnd_i2c_send(struct i2c_client *client, const void *data, int n) +{ + u8 buffer[UPISND_MAX_PACKET_LENGTH + 1]; + + memcpy(buffer, data, n); + buffer[n] = ~crc8(upisnd_crc8_table, buffer, n, CRC8_INIT_VALUE); + ++n; + +#ifdef UPISOUND_DEBUG + printk(KERN_DEBUG "%s: Sending %d bytes:", __func__, n); + int i; + + for (i = 0; i < n; ++i) + printk(KERN_CONT " %02x", buffer[i]); + printk(KERN_CONT "\n"); + + u8 check = crc8(upisnd_crc8_table, buffer, n, CRC8_INIT_VALUE); + + printd("CRC CHECK: %u, good: %u", check, CRC8_GOOD_VALUE(upisnd_crc8_table)); +#endif + + return i2c_master_send(client, buffer, n); +} + +static void upisnd_command_send(struct upisnd_instance *instance, + struct upisnd_command_task_t *task, + struct upisnd_cmd_t *cmd) +{ + mutex_lock(&instance->comm.comm_lock); + upisnd_i2c_send(instance->comm.client, cmd, upisnd_cmd_decode_length(cmd->cmd_and_size)); + mutex_unlock(&instance->comm.comm_lock); +} + +static void upisnd_init_command_task(struct upisnd_command_task_t *task) +{ + memset(task, 0, sizeof(struct upisnd_command_task_t)); + init_completion(&task->done); + task->result = -EINPROGRESS; + task->cmd[0] = UPISND_CMD_INVALID; +} + +static int upisnd_execute_void_command(struct upisnd_instance *instance, const void *cmd) +{ + mutex_lock(&instance->comm.comm_lock); + int result = upisnd_i2c_send(instance->comm.client, cmd, + upisnd_cmd_decode_length(*(const uint8_t *)cmd)); + + mutex_unlock(&instance->comm.comm_lock); + return result; +} + +static int upisnd_execute_command(struct upisnd_instance *instance, + struct upisnd_command_task_t *task) +{ + struct upisnd_cmd_t *cmd = (struct upisnd_cmd_t *)task->cmd; + + mutex_lock(&instance->comm.msg_lock); + upisnd_msg_id_t msg_id = upisnd_next_msg_id(instance); + + cmd->flags_and_msg_id = upisnd_msg_id_encode(msg_id, false); + task->msg_id = msg_id; + list_add_tail(&task->list, &instance->comm.msg_handlers); + mutex_unlock(&instance->comm.msg_lock); + + upisnd_command_send(instance, task, cmd); + + printd("Before wait"); + unsigned long t = wait_for_completion_io_timeout(&task->done, msecs_to_jiffies(5000u)); + (void)t; + + mutex_lock(&instance->comm.msg_lock); + if (!completion_done(&task->done)) { + printe("Message %d timed out!", msg_id); + upisnd_command_timeout(task); + } + list_del(&task->list); + mutex_unlock(&instance->comm.msg_lock); + printd("Wait complete! %lu %d", t, task->result); + + return task->result; +} + +static void upisnd_handle_response(struct upisnd_instance *instance, + const struct upisnd_cmd_t *response, + unsigned int n) +{ + upisnd_msg_id_t msg_id = upisnd_msg_id_decode_id(response->flags_and_msg_id); + + printd("Response to %d", msg_id); + + struct list_head *p; + struct upisnd_command_task_t *t; + + mutex_lock(&instance->comm.msg_lock); + list_for_each(p, &instance->comm.msg_handlers) { + t = list_entry(p, struct upisnd_command_task_t, list); + if (t->msg_id == msg_id) { + upisnd_command_response(t, response, n); + t->msg_id = UPISND_MSG_ID_INVALID; + complete_all(&t->done); + break; + } + } + mutex_unlock(&instance->comm.msg_lock); +} + +void upisnd_handle_comm_interrupt(struct upisnd_instance *instance) +{ + printd("Comm handler"); + + u8 data[UPISND_MAX_PACKET_LENGTH + 1]; + + mutex_lock(&instance->comm.comm_lock); + int i, n = 0, err; + + err = i2c_master_recv(instance->comm.client, data, 3); + + if (err == 3 && data[0] != UPISND_CMD_INVALID) { + n = upisnd_cmd_decode_length(data[0]) + 1; // Includes CRC8 byte at the end. + if (n > 3) { + err = i2c_master_recv(instance->comm.client, data + 3, n - 3); + if (err != n - 3) { + printe("Error occurred when receiving data over I2C! (%d)", err); + mutex_unlock(&instance->comm.comm_lock); + return; + } + } + } else { + printe("Error occurred when receiving data over I2C! (%d)", err); + } + +#ifdef UPISOUND_DEBUG + printk(KERN_DEBUG "upisnd_handle_comm_interrupt: Read %d bytes:", n); + for (i = 0; i < n; ++i) + printk(KERN_CONT " %02x", data[i]); + printk(KERN_CONT "\n"); +#endif + + mutex_unlock(&instance->comm.comm_lock); + + if (n <= 0 || data[0] == UPISND_CMD_INVALID) { + printe("Error occurred when receiving data over I2C! (%d)", n); + return; + } + + u8 crc = crc8(upisnd_crc8_table, data, n, CRC8_INIT_VALUE); + + if (crc != CRC8_GOOD_VALUE(upisnd_crc8_table)) { + printe("CRC check failed, calculated value: %02x (expected value: %02x)", + crc, CRC8_GOOD_VALUE(upisnd_crc8_table)); + return; + } + + enum upisnd_cmd_type_e type = upisnd_cmd_decode_type(data[0]); + + if (upisnd_cmd_type_has_msg_id(type)) { + upisnd_handle_response(instance, (const struct upisnd_cmd_t *)data, n); + } else { + switch (type) { + case UPISND_CMD_MIDI: + instance->comm.handler_ops->handle_midi_data(instance, + &data[1], + upisnd_cmd_decode_length(data[0]) - 1 + ); + break; + case UPISND_CMD_IRQ_EVENT: + { + int j = 0; + int k = 0; + struct irq_event_t gpio_events[15]; + struct irq_event_t sound_events[15]; + unsigned int n = upisnd_cmd_decode_length(data[0]) - 1; + + for (i = 0; i < n; ++i) { + struct irq_event_t e; + + e.num = data[i + 1] & UPISND_IRQ_NUM_MASK; + e.high = (data[i + 1] & UPISND_ON_BIT_MASK) != 0; + + switch (e.num) { + case UPISND_IRQ_VGND_SHORT_ALERT: + sound_events[j++] = e; + break; + case UPISND_IRQ_GPIO_START...UPISND_IRQ_GPIO_END: + gpio_events[k++] = e; + break; + default: + printe("Unknown IRQ event %d", e.num); + continue; + } + } + + if (j > 0) { + instance->comm.handler_ops->handle_sound_irq_events + (instance, + sound_events, + j); + } + if (k > 0) { + instance->comm.handler_ops->handle_gpio_irq_events + (instance, + gpio_events, + k); + } + } + break; + case UPISND_CMD_CONTROL_EVENT: + { + const struct upisnd_cmd_control_event_t *cmd = + (const struct upisnd_cmd_control_event_t *)data; + int i; + unsigned int n = upisnd_cmd_decode_length(data[0]) + / sizeof(uint16_t); + struct control_event_t events[7]; + + printd("Received %u control events", n); + for (i = 0; i < n; ++i) { + u16 e = ntohs(cmd->values[i]); + + events[i].pin = e >> 10; + events[i].raw_value = e & 0x3ff; + printd("%d: %d 0x%04x u=%u d=%d", i, events[i].pin, + events[i].raw_value, events[i].raw_value, + events[i].raw_value); + } + + printd("Start handling control events"); + instance->comm.handler_ops->handle_control_events(instance, + events, + n); + printd("Done handling control events"); + } + break; + default: + printe("Unknown command received (%02x)", data[0]); + break; + } + } +} + +int upisnd_comm_module_init(void) +{ + crc8_populate_msb(upisnd_crc8_table, UPISND_CRC8_POLYNOMIAL); + return 0; +} + +int upisnd_comm_init(struct upisnd_instance *instance, + struct i2c_client *client, + const struct upisnd_comm_handler_ops *ops) +{ + struct upisnd_comm *comm = &instance->comm; + + mutex_init(&comm->comm_lock); + mutex_init(&comm->msg_lock); + + comm->client = client; + comm->handler_ops = ops; + + INIT_LIST_HEAD(&comm->msg_handlers); + comm->msg_id_counter = 0; + + return 0; +} + +int upisnd_comm_send_midi(struct upisnd_instance *instance, const void *data, unsigned int n) +{ + if (n >= UPISND_MAX_PACKET_LENGTH) + return -EINVAL; + + u8 buffer[UPISND_MAX_PACKET_LENGTH]; + + buffer[0] = upisnd_cmd_encode(UPISND_CMD_MIDI, n + 1); + memcpy(&buffer[1], data, n); + + mutex_lock(&instance->comm.comm_lock); + int err = upisnd_i2c_send(instance->comm.client, buffer, n + 1); + + mutex_unlock(&instance->comm.comm_lock); + + if (err < 0) + printe("Error occurred when sending MIDI data over I2C! (%d)", err); + + return err; +} + +int upisnd_comm_commit_setup(struct upisnd_instance *instance, upisnd_setup_t setup) +{ + struct upisnd_command_task_t task; + + upisnd_init_command_task(&task); + + struct upisnd_cmd_setup_t *cmd = (struct upisnd_cmd_setup_t *)task.cmd; + + cmd->cmd.cmd_and_size = upisnd_cmd_encode(UPISND_CMD_SETUP, sizeof(*cmd)); + cmd->setup = htonl(setup); + + int result = upisnd_execute_command(instance, &task); + + return result <= 0 ? result : 0; +} + +int upisnd_comm_gpio_set(struct upisnd_instance *instance, uint8_t pin, bool high) +{ + struct upisnd_cmd_set_gpio_t cmd; + + cmd.cmd_and_size = upisnd_cmd_encode(UPISND_CMD_SET_GPIO, sizeof(cmd)); + cmd.on_and_pin = pin | (high ? UPISND_ON_BIT_MASK : 0x00); + + return upisnd_execute_void_command(instance, &cmd); +} + +int upisnd_comm_gpio_get(struct upisnd_instance *instance, uint8_t pin) +{ + struct upisnd_command_task_t task; + + upisnd_init_command_task(&task); + + struct upisnd_cmd_get_gpio_t *cmd = (struct upisnd_cmd_get_gpio_t *)task.cmd; + + cmd->cmd.cmd_and_size = upisnd_cmd_encode(UPISND_CMD_GET_GPIO, sizeof(*cmd)); + cmd->pin = pin; + + return upisnd_execute_command(instance, &task); +} + +int upisnd_comm_gpio_set_all(struct upisnd_instance *instance, + const struct upisnd_all_gpio_state_t *state) +{ + struct upisnd_cmd_set_all_gpios_t cmd; + + cmd.cmd_and_size = upisnd_cmd_encode(UPISND_CMD_SET_ALL_GPIOS, sizeof(cmd)); + + memcpy(cmd.state.state, state->state, sizeof(cmd.state)); + + return upisnd_execute_void_command(instance, &cmd); +} + +int upisnd_comm_gpio_get_all(struct upisnd_instance *instance, + struct upisnd_all_gpio_state_t *result) +{ + int err; + struct upisnd_command_task_t task; + + upisnd_init_command_task(&task); + + struct upisnd_cmd_get_all_gpios_t *cmd = (struct upisnd_cmd_get_all_gpios_t *)task.cmd; + + cmd->cmd.cmd_and_size = upisnd_cmd_encode(UPISND_CMD_GET_ALL_GPIOS, + sizeof(struct upisnd_cmd_get_all_gpios_t)); + + err = upisnd_execute_command(instance, &task); + if (err >= 0) { + const struct upisnd_cmd_get_all_gpios_response_t *resp = + (const struct upisnd_cmd_get_all_gpios_response_t *)task.response; + + if (upisnd_cmd_is_response(&resp->cmd) && + upisnd_cmd_decode_type(resp->cmd.cmd_and_size) == UPISND_CMD_GET_ALL_GPIOS && + upisnd_cmd_decode_length(resp->cmd.cmd_and_size) == + sizeof(struct upisnd_cmd_get_all_gpios_response_t)) { + memcpy(result->state, resp->state.state, sizeof(result->state)); + err = 0; + } else { + err = -EINVAL; + } + } + + return err; +} + +int upisnd_comm_set_irq_types(struct upisnd_instance *instance, + const struct upisnd_irq_type_config_t *irq_type_config) +{ + struct upisnd_command_task_t task; + + upisnd_init_command_task(&task); + + struct upisnd_cmd_set_irq_types_t *cmd = (struct upisnd_cmd_set_irq_types_t *)task.cmd; + + cmd->cmd.cmd_and_size = upisnd_cmd_encode(UPISND_CMD_SET_IRQ_TYPES, sizeof(*cmd)); + memcpy(&cmd->config, irq_type_config, sizeof(struct upisnd_irq_type_config_t)); + + return upisnd_execute_command(instance, &task); +} + +int upisnd_comm_set_irq_masks(struct upisnd_instance *instance, + const struct upisnd_irq_mask_config_t *irq_mask_config) +{ + struct upisnd_command_task_t task; + + upisnd_init_command_task(&task); + + struct upisnd_cmd_set_irq_masks_t *cmd = (struct upisnd_cmd_set_irq_masks_t *)task.cmd; + + cmd->cmd.cmd_and_size = upisnd_cmd_encode(UPISND_CMD_SET_IRQ_MASKS, sizeof(*cmd)); + memcpy(&cmd->config, irq_mask_config, sizeof(struct upisnd_irq_mask_config_t)); + + return upisnd_execute_command(instance, &task); +} + +int upisnd_comm_set_subscription(struct upisnd_instance *instance, upisnd_irq_num_t irq, bool on) +{ + struct upisnd_command_task_t task; + + upisnd_init_command_task(&task); + + struct upisnd_cmd_set_subscription_t *cmd = + (struct upisnd_cmd_set_subscription_t *)task.cmd; + + cmd->cmd.cmd_and_size = upisnd_cmd_encode(UPISND_CMD_SET_SUBSCRIPTION, sizeof(*cmd)); + cmd->on_and_irq_num = (irq & UPISND_IRQ_NUM_MASK) | (on ? UPISND_ON_BIT_MASK : 0); + + return upisnd_execute_command(instance, &task); +} + +int upisnd_comm_get_version(struct upisnd_instance *instance, struct upisnd_version_t *result) +{ + memset(result, 0, sizeof(*result)); + + struct upisnd_command_task_t task; + + upisnd_init_command_task(&task); + + struct upisnd_cmd_get_t *cmd = (struct upisnd_cmd_get_t *)task.cmd; + + cmd->cmd.cmd_and_size = upisnd_cmd_encode(UPISND_CMD_GET, sizeof(*cmd)); + cmd->value_id = UPISND_VALUE_VERSION_INFO; + + int err = upisnd_execute_command(instance, &task); + + if (err >= 0) { + const struct upisnd_cmd_get_response_t *cmd = + (const struct upisnd_cmd_get_response_t *)task.response; + + if (upisnd_cmd_decode_length(cmd->cmd_get.cmd.cmd_and_size) == + UPISND_CMD_GET_RESPONSE_INT32_SIZE && + upisnd_cmd_is_response(&cmd->cmd_get.cmd) && + cmd->cmd_get.value_id == UPISND_VALUE_VERSION_INFO) { + u32 ver = (uint32_t)ntohl(cmd->value); + + result->bootloader_mode = (ver & 0x80000000) >> 31; + result->hwrev = (ver & 0x7f000000) >> 24; + result->major = (ver & 0x00ff0000) >> 16; + result->minor = (ver & 0x0000ff00) >> 8; + result->build = ver & 0x000000ff; + err = cmd->result; + } else { + err = -EINVAL; + } + } + + return err; +} + +int upisnd_comm_get_serial_number(struct upisnd_instance *instance, char result[12]) +{ + memset(result, 0, 12); + + struct upisnd_command_task_t task; + + upisnd_init_command_task(&task); + + struct upisnd_cmd_get_t *cmd = (struct upisnd_cmd_get_t *)task.cmd; + + cmd->cmd.cmd_and_size = upisnd_cmd_encode(UPISND_CMD_GET, sizeof(*cmd)); + cmd->value_id = UPISND_VALUE_SERIAL_NUMBER; + + int err = upisnd_execute_command(instance, &task); + + if (err >= 0) { + const struct upisnd_cmd_get_response_t *cmd = + (const struct upisnd_cmd_get_response_t *)task.response; + + if (upisnd_cmd_decode_length(cmd->cmd_get.cmd.cmd_and_size) == + (UPISND_CMD_GET_RESPONSE_DATA_MIN_SIZE + 11) && + upisnd_cmd_is_response(&cmd->cmd_get.cmd) && + cmd->cmd_get.value_id == UPISND_VALUE_SERIAL_NUMBER) { + memcpy(result, cmd->data, 11); + err = cmd->result; + } else { + err = -EINVAL; + } + } + + return err; +} + +int upisnd_comm_get_element_value(struct upisnd_instance *instance, uint8_t pin, int32_t *result) +{ + if (pin >= UPISND_NUM_GPIOS) + return -EINVAL; + + *result = 0; + + struct upisnd_command_task_t task; + + upisnd_init_command_task(&task); + + struct upisnd_cmd_get_t *cmd = (struct upisnd_cmd_get_t *)task.cmd; + + cmd->cmd.cmd_and_size = upisnd_cmd_encode(UPISND_CMD_GET, sizeof(*cmd)); + cmd->value_id = UPISND_VALUE_ELEMENT_VAL_BASE + pin; + + int err = upisnd_execute_command(instance, &task); + + if (err >= 0) { + const struct upisnd_cmd_get_response_t *cmd = + (const struct upisnd_cmd_get_response_t *)task.response; + + if (upisnd_cmd_decode_length(cmd->cmd_get.cmd.cmd_and_size) == + UPISND_CMD_GET_RESPONSE_INT32_SIZE && + upisnd_cmd_is_response(&cmd->cmd_get.cmd) && + cmd->cmd_get.value_id == UPISND_VALUE_ELEMENT_VAL_BASE + pin) { + *result = (int32_t)ntohl(cmd->value); + err = cmd->result; + } else { + err = -EINVAL; + } + } + + return err; +} + +int upisnd_comm_get_value(struct upisnd_instance *instance, + enum upisnd_value_id_t value_id, + int32_t *result) +{ + memset(result, 0, sizeof(*result)); + + struct upisnd_command_task_t task; + + upisnd_init_command_task(&task); + + struct upisnd_cmd_get_t *cmd = (struct upisnd_cmd_get_t *)task.cmd; + + cmd->cmd.cmd_and_size = upisnd_cmd_encode(UPISND_CMD_GET, sizeof(*cmd)); + cmd->value_id = value_id; + + int err = upisnd_execute_command(instance, &task); + + if (err >= 0) { + const struct upisnd_cmd_get_response_t *cmd = + (const struct upisnd_cmd_get_response_t *)task.response; + + if (upisnd_cmd_decode_length(cmd->cmd_get.cmd.cmd_and_size) == + UPISND_CMD_GET_RESPONSE_INT32_SIZE && + upisnd_cmd_is_response(&cmd->cmd_get.cmd) && + cmd->cmd_get.value_id == value_id) { + *result = (int32_t)ntohl(cmd->value); + err = cmd->result; + } else { + err = -EINVAL; + } + } + + return err; +} + +int upisnd_comm_set_value(struct upisnd_instance *instance, + enum upisnd_value_id_t value_id, + int32_t value) +{ + struct upisnd_command_task_t task; + + upisnd_init_command_task(&task); + + struct upisnd_cmd_set_t *cmd = (struct upisnd_cmd_set_t *)task.cmd; + + cmd->cmd.cmd_and_size = upisnd_cmd_encode(UPISND_CMD_SET, sizeof(*cmd)); + cmd->value_id = value_id; + cmd->value = htonl(value); + + return upisnd_execute_command(instance, &task); +} + +/* vim: set ts=8 sw=8 noexpandtab: */ --- /dev/null +++ b/sound/drivers/upisnd/upisnd_comm.h @@ -0,0 +1,104 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Pisound Micro Linux kernel module. + * Copyright (C) 2017-2025 Vilniaus Blokas UAB, https://blokas.io/ + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; version 2 of the + * License. + * + * 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. + */ + +#ifndef UPISOUND_COMM_H +#define UPISOUND_COMM_H + +struct upisnd_instance; + +struct upisnd_version_t { + uint8_t bootloader_mode:1; + uint8_t hwrev:7; + u8 major; + u8 minor; + u8 build; +}; + +struct irq_event_t { + upisnd_irq_num_t num; + bool high; +}; + +struct control_event_t { + upisnd_pin_t pin:6; + int16_t raw_value:10; +}; + +struct upisnd_comm_handler_ops { + void (*handle_midi_data)(struct upisnd_instance *instance, + const u8 *data, + unsigned int n); + void (*handle_gpio_irq_events)(struct upisnd_instance *instance, + const struct irq_event_t *events, + unsigned int n); + void (*handle_sound_irq_events)(struct upisnd_instance *instance, + const struct irq_event_t *events, + unsigned int n); + void (*handle_control_events)(struct upisnd_instance *instance, + const struct control_event_t *events, + unsigned int n); +}; + +struct upisnd_comm { + struct mutex comm_lock; + // comm_lock serializes I2C communication. + struct i2c_client *client; + + const struct upisnd_comm_handler_ops *handler_ops; + + // msg_lock protects the message handlers list and id. + struct mutex msg_lock; + struct list_head msg_handlers; + upisnd_msg_id_t msg_id_counter; +}; + +int upisnd_comm_module_init(void); +int upisnd_comm_init(struct upisnd_instance *instance, + struct i2c_client *client, + const struct upisnd_comm_handler_ops *ops); +int upisnd_comm_send_midi(struct upisnd_instance *instance, const void *data, unsigned int n); +int upisnd_comm_commit_setup(struct upisnd_instance *instance, upisnd_setup_t setup); +int upisnd_comm_gpio_set(struct upisnd_instance *instance, u8 pin, bool high); +int upisnd_comm_gpio_get(struct upisnd_instance *instance, u8 pin); +int upisnd_comm_gpio_set_all(struct upisnd_instance *instance, + const struct upisnd_all_gpio_state_t *state); +int upisnd_comm_gpio_get_all(struct upisnd_instance *instance, + struct upisnd_all_gpio_state_t *result); +int upisnd_comm_set_irq_types(struct upisnd_instance *instance, + const struct upisnd_irq_type_config_t *irq_type_config); +int upisnd_comm_set_irq_masks(struct upisnd_instance *instance, + const struct upisnd_irq_mask_config_t *irq_mask_config); +int upisnd_comm_set_subscription(struct upisnd_instance *instance, + upisnd_irq_num_t irq, bool on); +int upisnd_comm_get_version(struct upisnd_instance *instance, + struct upisnd_version_t *result); +int upisnd_comm_get_element_value(struct upisnd_instance *instance, + u8 pin, + int32_t *result); +int upisnd_comm_get_serial_number(struct upisnd_instance *instance, + char result[12]); +int upisnd_comm_get_value(struct upisnd_instance *instance, + enum upisnd_value_id_t value_id, + int32_t *result); +int upisnd_comm_set_value(struct upisnd_instance *instance, + enum upisnd_value_id_t value_id, + int32_t value); + +void upisnd_handle_comm_interrupt(struct upisnd_instance *instance); + +#endif // UPISOUND_COMM_H + +/* vim: set ts=8 sw=8 noexpandtab: */ --- /dev/null +++ b/sound/drivers/upisnd/upisnd_common.h @@ -0,0 +1,97 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Pisound Micro Linux kernel module. + * Copyright (C) 2017-2025 Vilniaus Blokas UAB, https://blokas.io/ + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; version 2 of the + * License. + * + * 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. + */ + +#ifndef UPISOUND_COMMON_H +#define UPISOUND_COMMON_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +#include "upisnd_protocol.h" +#include "upisnd_comm.h" +#include "upisnd_debug.h" +#include "upisnd_midi.h" +#include "upisnd_sound.h" +#include "upisnd_sysfs.h" +#include "upisnd_ctrl.h" +#include "upisnd_pins.h" +#include "upisnd_gpio.h" +#include "upisnd_utils.h" + +enum upisnd_flags_e { + UPISND_FLAG_DUMMY = 1 << 0, + UPISND_FLAG_ADC_CALIBRATION = 1 << 1, +}; + +struct upisnd_instance { + struct kref refcount; + struct platform_device *pdev; + struct device *ctrl_dev; + struct device *codec_dev; + struct workqueue_struct *work_queue; + + struct upisnd_ctrl ctrl; + struct upisnd_comm comm; + struct upisnd_midi midi; + struct upisnd_gpio gpio; + + struct rw_semaphore rw_gpio_config_sem; + struct upisnd_config *config; + DECLARE_KFIFO(ctrl_event_fifo, struct control_event_t, 128); + struct work_struct ctrl_event_handler; + + struct snd_soc_card sound_card; + struct snd_soc_dai_link dai_link; + + u32 flags; +}; + +void upisnd_instance_release(struct kref *kref); + +#endif // UPISOUND_COMMON_H + +/* vim: set ts=8 sw=8 noexpandtab: */ --- /dev/null +++ b/sound/drivers/upisnd/upisnd_ctrl.c @@ -0,0 +1,246 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Pisound Micro Linux kernel module. + * Copyright (C) 2017-2025 Vilniaus Blokas UAB, https://blokas.io/ + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; version 2 of the + * License. + * + * 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 "upisnd_common.h" + +static void upisnd_ctrl_handle_irq_events(struct upisnd_instance *instance, + const struct irq_event_t *events, + unsigned int n); +static void upisnd_ctrl_handle_control_events(struct upisnd_instance *instance, + const struct control_event_t *events, + unsigned int n); + +static const struct upisnd_comm_handler_ops upisnd_comm_handlers = { + .handle_midi_data = &upisnd_handle_midi_data, + .handle_gpio_irq_events = &upisnd_ctrl_handle_irq_events, + .handle_sound_irq_events = &upisnd_sound_handle_irq_events, + .handle_control_events = &upisnd_ctrl_handle_control_events, +}; + +static void upisnd_ctrl_cleanup(struct upisnd_instance *instance); + +static inline bool upisnd_has_data(struct upisnd_instance *instance) +{ + bool x = gpiod_get_value(instance->ctrl.data_available); + + printd("data_available = %d", x); + return x; +} + +static irqreturn_t upisnd_data_available_interrupt_handler(int irq, void *dev_id) +{ + printd("irq_handler called"); + + struct upisnd_instance *instance = dev_id; + + if (irq == instance->ctrl.client->irq) { + printd("handling comm interrupt"); + upisnd_handle_comm_interrupt(instance); + printd("handling done"); + return IRQ_HANDLED; + } + + return IRQ_NONE; +} + +static void upisnd_reset(struct upisnd_instance *instance) +{ + printd("Resetting..."); + + gpiod_direction_output(instance->ctrl.reset, 1); + usleep_range(1000, 5000); +#ifdef UPISOUND_DEV + gpiod_direction_input(ctrl->reset); +#else + gpiod_set_value(instance->ctrl.reset, 0); +#endif + msleep(30); + + upisnd_gpio_reset(instance); + + printd("Done!"); +} + +int upisnd_ctrl_probe(struct i2c_client *client) +{ + int err = 0; + + printd("Ctrl %p irq %d", client, client->irq); + printd("Dev %p", &client->dev); + printd("Data %p", dev_get_platdata(&client->dev)); + + struct upisnd_instance *instance = dev_get_platdata(&client->dev); + + if (!instance) + return -EPROBE_DEFER; + + struct upisnd_ctrl *ctrl = &instance->ctrl; + + ctrl->client = client; + + err = upisnd_gpio_init(instance); + if (err < 0) { + printe("GPIO init failed! %d", err); + goto cleanup; + } + + ctrl->reset = devm_gpiod_get_index(&client->dev, "reset", 0, GPIOD_ASIS); + if (IS_ERR(ctrl->reset)) { + printe("Failed getting reset gpio!"); + err = PTR_ERR(ctrl->reset); + ctrl->reset = NULL; + goto cleanup; + } + + upisnd_reset(instance); + + err = upisnd_comm_init(instance, client, &upisnd_comm_handlers); + if (err < 0) { + printe("Communication init failed! (%d)", err); + goto cleanup; + } + + ctrl->data_available = devm_gpiod_get_index(&client->dev, "data_available", 0, GPIOD_IN); + if (IS_ERR(ctrl->data_available)) { + printe("Failed getting data_available gpio!"); + err = PTR_ERR(ctrl->data_available); + ctrl->data_available = NULL; + goto cleanup; + } + + err = devm_request_threaded_irq(&client->dev, + ctrl->client->irq, + NULL, + upisnd_data_available_interrupt_handler, + IRQF_SHARED | IRQF_ONESHOT | IRQF_TRIGGER_LOW, + "data_available_int", + instance); + if (err != 0) { + printe("data_available IRQ request failed! %d", err); + goto cleanup; + } + + err = upisnd_comm_get_version(instance, &instance->ctrl.version); + if (err < 0) { + printe("Failed getting version! %d", err); + goto cleanup; + } + err = upisnd_comm_get_serial_number(instance, instance->ctrl.serial); + if (err < 0) { + printe("Failed getting serial number! %d", err); + goto cleanup; + } + + if (instance->ctrl.serial[0] != 'P' || + instance->ctrl.serial[1] != 'S' || + instance->ctrl.serial[2] != 'M' || + strlen(instance->ctrl.serial) != 11) { + printe("Unexpected serial number %s!", instance->ctrl.serial); + err = -EINVAL; + goto cleanup; + } + + printi("Pisound Micro %s version %d.%d.%d, hw rev: %d", + instance->ctrl.serial, + instance->ctrl.version.major, + instance->ctrl.version.minor, + instance->ctrl.version.build, + instance->ctrl.version.hwrev); + + if (instance->ctrl.version.bootloader_mode) { + printe("Pisound Micro is in bootloader mode! Please reflash the firmware, refer to documentation on https://blokas.io/"); + err = -EINVAL; + goto cleanup; + } + + const char *name = NULL; + + if (client->dev.of_node) { + err = of_property_read_string(client->dev.of_node, "kobj-name", &name); + + if (of_property_read_bool(client->dev.of_node, "adc-calibration")) + instance->flags |= UPISND_FLAG_ADC_CALIBRATION; + } + err = upisnd_sysfs_init(instance, name); + if (err != 0) { + printe("sysfs init failed! %d", err); + goto cleanup; + } + +cleanup: + if (err != 0) { + printe("Error %d!", err); + upisnd_ctrl_cleanup(instance); + return err; + } + + return 0; +} + +void upisnd_ctrl_remove(struct i2c_client *client) +{ + printd("Ctrl %p", client); + + struct upisnd_instance *instance = dev_get_platdata(&client->dev); + + if (!instance) + return; + + upisnd_ctrl_cleanup(instance); + + kref_put(&instance->refcount, &upisnd_instance_release); + client->dev.platform_data = NULL; +} + +static void upisnd_off(struct upisnd_instance *instance) +{ + printd("Turning off..."); + +#ifndef UPISOUND_DEV + gpiod_direction_output(instance->ctrl.reset, 1); +#else + upisnd_reset(&instance->ctrl); +#endif + printd("Done!"); +} + +static void upisnd_ctrl_cleanup(struct upisnd_instance *instance) +{ + printd("cleanup"); + upisnd_sysfs_uninit(instance); + + if (instance->ctrl.reset) { + upisnd_off(instance); + instance->ctrl.reset = NULL; + } +} + +static void upisnd_ctrl_handle_irq_events(struct upisnd_instance *instance, + const struct irq_event_t *events, + unsigned int n) +{ + upisnd_gpio_handle_irq_event(instance, events, n); + upisnd_sysfs_handle_irq_event(instance, events, n); +} + +static void upisnd_ctrl_handle_control_events(struct upisnd_instance *instance, + const struct control_event_t *events, + unsigned int n) +{ + upisnd_sysfs_handle_control_event(instance, events, n); +} + +/* vim: set ts=8 sw=8 noexpandtab: */ --- /dev/null +++ b/sound/drivers/upisnd/upisnd_ctrl.h @@ -0,0 +1,36 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Pisound Micro Linux kernel module. + * Copyright (C) 2017-2025 Vilniaus Blokas UAB, https://blokas.io/ + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; version 2 of the + * License. + * + * 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. + */ + +#ifndef UPISOUND_CTRL_H +#define UPISOUND_CTRL_H + +struct upisnd_ctrl { + struct i2c_client *client; + struct upisnd_gpio *gpio; + + struct upisnd_version_t version; + char serial[12]; + + struct gpio_desc *data_available; + struct gpio_desc *reset; +}; + +int upisnd_ctrl_probe(struct i2c_client *client); +void upisnd_ctrl_remove(struct i2c_client *client); + +#endif // UPISOUND_CTRL_H + +/* vim: set ts=8 sw=8 noexpandtab: */ --- /dev/null +++ b/sound/drivers/upisnd/upisnd_debug.h @@ -0,0 +1,34 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Pisound Micro Linux kernel module. + * Copyright (C) 2017-2025 Vilniaus Blokas UAB, https://blokas.io/ + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; version 2 of the + * License. + * + * 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. + */ + +#ifndef UPISOUND_DEBUG_H +#define UPISOUND_DEBUG_H + +#define UPISOUND_LOG_IMPL(log_func, msg, ...) \ + log_func("pisound-micro(%s): " msg "\n", __func__, ## __VA_ARGS__) + +#ifdef UPISOUND_DEBUG +# define printd(...) UPISOUND_LOG_IMPL(pr_alert, __VA_ARGS__) +#else +# define printd(...) do {} while (0) +#endif + +#define printe(...) UPISOUND_LOG_IMPL(pr_err, __VA_ARGS__) +#define printi(...) UPISOUND_LOG_IMPL(pr_info, __VA_ARGS__) + +#endif // UPISOUND_DEBUG_H + +/* vim: set ts=8 sw=8 noexpandtab: */ --- /dev/null +++ b/sound/drivers/upisnd/upisnd_gpio.c @@ -0,0 +1,867 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Pisound Micro Linux kernel module. + * Copyright (C) 2017-2025 Vilniaus Blokas UAB, https://blokas.io/ + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; version 2 of the + * License. + * + * 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 "upisnd_common.h" + +enum upisnd_pinconf_dir_e { + UPISND_PINCONF_DIR_INPUT_PULL_AS_IS_OR_NONE = 0, + UPISND_PINCONF_DIR_INPUT_PULL_UP = 1, + UPISND_PINCONF_DIR_INPUT_PULL_DOWN = 2, + UPISND_PINCONF_DIR_OUTPUT_LOW = 4, + UPISND_PINCONF_DIR_OUTPUT_HIGH = 5, + + // If 3rd bit is set, direction is output, otherwise it's input. + UPISND_PINCONF_DIR_MASK = 0x04, +}; + +enum { UPISND_PINCONF_DIRECTION = (PIN_CONFIG_END + 1) }; // enum upisnd_pinconf_dir_e + +static int upisnd_gpio_chip_get_direction(struct gpio_chip *gc, unsigned int offset); +static int upisnd_gpio_chip_get(struct gpio_chip *gc, unsigned int offset); +static void upisnd_gpio_chip_set(struct gpio_chip *gc, unsigned int offset, int val); +static int upisnd_gpio_chip_get_multiple(struct gpio_chip *gc, + unsigned long *mask, + unsigned long *bits); +static void upisnd_gpio_chip_set_multiple(struct gpio_chip *gc, + unsigned long *mask, + unsigned long *bits); +static int upisnd_gpio_chip_set_config(struct gpio_chip *gc, + unsigned int offset, + unsigned long config); +static int upisnd_gpio_chip_dir_in(struct gpio_chip *gc, unsigned int offset); +static int upisnd_gpio_chip_dir_out(struct gpio_chip *gc, unsigned int offset, int val); +static int upisnd_gpio_chip_request(struct gpio_chip *gc, unsigned int offset); +static void upisnd_gpio_chip_free(struct gpio_chip *gc, unsigned int offset); + +static const char * const upisnd_gpio_names[UPISND_NUM_GPIOS] = { + "A27", "A28", "A29", "A30", "A31", "A32", "B03", "B04", + "B05", "B06", "B07", "B08", "B09", "B10", "B11", "B12", + "B13", "B14", "B15", "B16", "B17", "B18", "B23", "B24", + "B25", "B26", "B27", "B28", "B29", "B30", "B31", "B32", + "B33", "B34", "B37", "B38", "B39" +}; + +static const struct gpio_chip upisnd_gpio_chip = { + .label = "pisound-micro-gpio", + .owner = THIS_MODULE, + .request = &upisnd_gpio_chip_request, + .free = &upisnd_gpio_chip_free, + .direction_input = &upisnd_gpio_chip_dir_in, + .direction_output = &upisnd_gpio_chip_dir_out, + .get_direction = &upisnd_gpio_chip_get_direction, + .get = &upisnd_gpio_chip_get, + .set = &upisnd_gpio_chip_set, + .get_multiple = &upisnd_gpio_chip_get_multiple, + .set_multiple = &upisnd_gpio_chip_set_multiple, + .set_config = &upisnd_gpio_chip_set_config, + .base = -1, + .ngpio = UPISND_NUM_GPIOS, + .can_sleep = true, + .names = upisnd_gpio_names, +}; + +enum { + IRQ_FLAG_TYPES_CHANGED_BIT = 0, + IRQ_FLAG_MASKS_CHANGED_BIT = 1, + IRQ_FLAG_HANDLING_IRQ_BIT = 2, +}; + +static int upisnd_gpio_chip_get_direction(struct gpio_chip *gc, unsigned int offset) +{ + struct upisnd_instance *instance = gpiochip_get_data(gc); + struct upisnd_gpio *gpio = &instance->gpio; + + down_read(&instance->rw_gpio_config_sem); + upisnd_setup_t setup = gpio->pin_configs[offset].setup; + + up_read(&instance->rw_gpio_config_sem); + + int dir = -EINVAL; + + if (upisnd_setup_get_element_type(setup) == UPISND_ELEMENT_TYPE_GPIO) + dir = upisnd_setup_get_gpio_dir(setup) == UPISND_PIN_DIR_INPUT ? 1 : 0; + + printd("%p %u %d %08x", gc, offset, dir, setup); + return dir; +} + +static const char *upisnd_gpio_pull_to_str(enum upisnd_pin_pull_e pull) +{ + switch (pull) { + default: + case UPISND_PIN_PULL_NONE: return "pull_none"; + case UPISND_PIN_PULL_UP: return "pull_up"; + case UPISND_PIN_PULL_DOWN: return "pull_down"; + } +} + +static void upisnd_print_setup(const upisnd_setup_t setup) +{ + switch (upisnd_setup_get_element_type(setup)) { + case UPISND_ELEMENT_TYPE_NONE: + printi("Setup None %d", upisnd_setup_get_pin_id(setup)); + break; + case UPISND_ELEMENT_TYPE_ENCODER: + printi("Setup Encoder %d %s %d %s", + upisnd_setup_get_pin_id(setup), + upisnd_gpio_pull_to_str(upisnd_setup_get_gpio_pull(setup)), + upisnd_setup_get_encoder_pin_b_id(setup), + upisnd_gpio_pull_to_str(upisnd_setup_get_encoder_pin_b_pull(setup))); + break; + case UPISND_ELEMENT_TYPE_ANALOG_IN: + printi("Setup Analog In %d (%d)", + upisnd_setup_get_pin_id(setup), + upisnd_setup_get_pin_id(setup)); + break; + case UPISND_ELEMENT_TYPE_GPIO: + if (upisnd_setup_get_gpio_dir(setup) == UPISND_PIN_DIR_INPUT) + printi("Setup GPIO Input %d %s", + upisnd_setup_get_pin_id(setup), + upisnd_gpio_pull_to_str(upisnd_setup_get_gpio_pull(setup))); + else + printi("Setup GPIO Output %d %s", + upisnd_setup_get_pin_id(setup), + upisnd_setup_get_gpio_output(setup) ? "high" : "low"); + break; + case UPISND_ELEMENT_TYPE_ACTIVITY: + printi("Setup Activity %d %d", + upisnd_setup_get_pin_id(setup), + upisnd_setup_get_activity_type(setup)); + break; + default: + printe("Unknown Setup type %d", upisnd_setup_get_element_type(setup)); + break; + } +} + +int upisnd_gpio_setup(struct upisnd_instance *instance, upisnd_setup_t setup) +{ + int err; + +#ifdef UPISND_DEBUG + printd("Committing setup:"); + upisnd_print_setup(setup); + err = upisnd_comm_commit_setup(instance, setup); + printd("Result: %d", err) +#else + err = upisnd_comm_commit_setup(instance, setup); + + if (err < 0) { + printe("Failed to commit setup (%d), failed request:", err); + upisnd_print_setup(setup); + } +#endif + + return err; +} + +static int upisnd_gpio_chip_get(struct gpio_chip *gc, unsigned int offset) +{ + printd("%p %u", gc, offset); + + int value = -EINVAL; + + struct upisnd_instance *instance = gpiochip_get_data(gc); + + unsigned long state[2]; + + down_read(&instance->rw_gpio_config_sem); + upisnd_setup_t setup = instance->gpio.pin_configs[offset].setup; + + state[0] = instance->gpio.gpio_state[0]; + state[1] = instance->gpio.gpio_state[1]; + up_read(&instance->rw_gpio_config_sem); + + if (upisnd_setup_get_element_type(setup) == UPISND_ELEMENT_TYPE_GPIO) { + switch (upisnd_setup_get_gpio_dir(setup)) { + case UPISND_PIN_DIR_OUTPUT: + value = test_bit(offset, state) ? 1 : 0; + break; + case UPISND_PIN_DIR_INPUT: + if (test_bit(IRQ_FLAG_HANDLING_IRQ_BIT, &instance->gpio.irq_flags)) { + value = test_bit(offset, state) ? 1 : 0; + } else { + down_write(&instance->rw_gpio_config_sem); + value = upisnd_comm_gpio_get(instance, offset); + assign_bit(offset, instance->gpio.gpio_state, value); + up_write(&instance->rw_gpio_config_sem); + } + break; + default: + break; + } + } + + return value; +} + +static void upisnd_gpio_chip_set(struct gpio_chip *gc, unsigned int offset, int val) +{ + printd("%p %u %d", gc, offset, val); + + struct upisnd_instance *instance = gpiochip_get_data(gc); + + down_write(&instance->rw_gpio_config_sem); + upisnd_setup_t setup = instance->gpio.pin_configs[offset].setup; + + if (upisnd_setup_get_element_type(setup) == UPISND_ELEMENT_TYPE_GPIO) { + if (upisnd_setup_get_gpio_dir(setup) == UPISND_PIN_DIR_OUTPUT) { + bool on = val != 0; + + assign_bit(offset, instance->gpio.gpio_state, on); + upisnd_comm_gpio_set(instance, offset, on); + } + } + up_write(&instance->rw_gpio_config_sem); +} + +static void encode_gpio_state(struct upisnd_all_gpio_state_t *s, const unsigned long *bits) +{ + u8 *p = (uint8_t *)s->state; + + p[0] = (bits[0]) & 0xff; + p[1] = (bits[0] >> 8) & 0xff; + p[2] = (bits[0] >> 16) & 0xff; + p[3] = (bits[0] >> 24) & 0xff; + p[4] = (bits[1]) & 0xff; +} + +static void decode_gpio_state(unsigned long *bits, const struct upisnd_all_gpio_state_t *s) +{ + const u8 *p = (const uint8_t *)s->state; + + bits[0] = + (p[0]) | + (p[1] << 8) | + (p[2] << 16) | + (p[3] << 24); + bits[1] = + (p[4]); +} + +static int upisnd_gpio_chip_get_multiple(struct gpio_chip *gc, + unsigned long *mask, + unsigned long *bits) +{ + unsigned int i; + int err = 0; + + memset(bits, 0, sizeof(bits[0]) * 2); + + printd("%p %02lx%08lx", gc, mask[1], mask[0]); + + struct upisnd_instance *instance = gpiochip_get_data(gc); + + unsigned long state[2]; + + down_read(&instance->rw_gpio_config_sem); + for_each_set_bit(i, mask, UPISND_NUM_GPIOS) { + upisnd_setup_t setup = instance->gpio.pin_configs[i].setup; + + if (upisnd_setup_get_element_type(setup) != UPISND_ELEMENT_TYPE_GPIO) { + printe("Pin %u is not configured as GPIO!", i); + err = -EINVAL; + } + } + up_read(&instance->rw_gpio_config_sem); + + if (err < 0) + return err; + + if (test_bit(IRQ_FLAG_HANDLING_IRQ_BIT, &instance->gpio.irq_flags)) { + state[0] = instance->gpio.gpio_state[0]; + state[1] = instance->gpio.gpio_state[1]; + } else { + struct upisnd_all_gpio_state_t s; + + down_write(&instance->rw_gpio_config_sem); + + err = upisnd_comm_gpio_get_all(instance, &s); + + if (err < 0) { + up_write(&instance->rw_gpio_config_sem); + return err; + } + + decode_gpio_state(state, &s); + + instance->gpio.gpio_state[0] = state[0]; + instance->gpio.gpio_state[1] = state[1]; + up_write(&instance->rw_gpio_config_sem); + } + + bits[0] = state[0] & mask[0]; + bits[1] = state[1] & mask[1]; + + return err; +} + +static void upisnd_gpio_chip_set_multiple(struct gpio_chip *gc, + unsigned long *mask, + unsigned long *bits) +{ + unsigned long state[2]; + + printd("%p %02lx%08lx %02lx%08lx", gc, mask[1], mask[0], bits[1], bits[0]); + + struct upisnd_instance *instance = gpiochip_get_data(gc); + + down_write(&instance->rw_gpio_config_sem); + + state[0] = (instance->gpio.gpio_state[0] & ~mask[0]) | (bits[0] & mask[0]); + state[1] = (instance->gpio.gpio_state[1] & ~mask[1]) | (bits[1] & mask[1]); + + struct upisnd_all_gpio_state_t s; + + encode_gpio_state(&s, state); + + upisnd_comm_gpio_set_all(instance, &s); + + instance->gpio.gpio_state[0] = state[0]; + instance->gpio.gpio_state[1] = state[1]; + + up_write(&instance->rw_gpio_config_sem); +} + +static int upisnd_gpio_chip_dir_in(struct gpio_chip *gc, unsigned int offset) +{ + printd("%p %u", gc, offset); + return upisnd_gpio_chip_set_config(gc, offset, pinconf_to_config_packed + ((enum pin_config_param)UPISND_PINCONF_DIRECTION, + UPISND_PINCONF_DIR_INPUT_PULL_AS_IS_OR_NONE)); +} + +static int upisnd_gpio_chip_dir_out(struct gpio_chip *gc, unsigned int offset, int val) +{ + printd("%p %u %d", gc, offset, val); + return upisnd_gpio_chip_set_config(gc, offset, pinconf_to_config_packed + ((enum pin_config_param)UPISND_PINCONF_DIRECTION, + val ? UPISND_PINCONF_DIR_OUTPUT_HIGH : UPISND_PINCONF_DIR_OUTPUT_LOW)); +} + +static int upisnd_gpio_chip_request(struct gpio_chip *gc, unsigned int offset) +{ + printd("(%p, %u)", gc, offset); + + struct upisnd_instance *instance = gpiochip_get_data(gc); + + int err; + upisnd_setup_t setup = 0; + + down_write(&instance->rw_gpio_config_sem); + + if (upisnd_setup_get_element_type(instance->gpio.pin_configs[offset].setup) == + UPISND_ELEMENT_TYPE_GPIO) { + setup = instance->gpio.pin_configs[offset].setup; + } else { + upisnd_setup_set_element_type(&setup, UPISND_ELEMENT_TYPE_GPIO); + upisnd_setup_set_gpio_dir(&setup, UPISND_PIN_DIR_INPUT); + upisnd_setup_set_gpio_pull(&setup, UPISND_PIN_PULL_NONE); + upisnd_setup_set_pin_id(&setup, offset); + } + + // If the pin is already in use via setup interface. + if (instance->gpio.pin_configs[offset].element) { + // If the pin is not being requested via setup interface, deny the request. + if (!(instance->gpio.pin_configs[offset].flags & + UPISND_PIN_FLAG_CONFIGURING_THROUGH_SYSFS)) { + printe("Pin %u already exported through pisound-micro sysfs!", offset); + err = -EBUSY; + } else { + err = 0; + } + + up_write(&instance->rw_gpio_config_sem); + return err; + } + + err = upisnd_gpio_setup(instance, setup); + + if (err >= 0) { + instance->gpio.pin_configs[offset].element = NULL; + instance->gpio.pin_configs[offset].flags = 0; + instance->gpio.pin_configs[offset].setup = setup; + instance->gpio.pin_configs[offset].gpio_desc = NULL; + } + + up_write(&instance->rw_gpio_config_sem); + + return err; +} + +static void upisnd_gpio_chip_free(struct gpio_chip *gc, unsigned int offset) +{ + printd("(%p, %u)", gc, offset); + + struct upisnd_instance *instance = gpiochip_get_data(gc); + + down_write(&instance->rw_gpio_config_sem); + if (instance->gpio.pin_configs[offset].element || + (instance->gpio.pin_configs[offset].flags & UPISND_PIN_FLAG_CONFIGURING_THROUGH_SYSFS) + ) { + // Unsetup will be taken care of in upisnd_element_cleanup, + // ignore requests that come through gpiochip_free_own_desc. + up_write(&instance->rw_gpio_config_sem); + return; + } + + upisnd_setup_t setup = instance->gpio.pin_configs[offset].setup; + + upisnd_setup_set_element_type(&setup, UPISND_ELEMENT_TYPE_NONE); + upisnd_gpio_setup(instance, setup); + + memset(&instance->gpio.pin_configs[offset], 0, sizeof(struct upisnd_pin_config_t)); + + up_write(&instance->rw_gpio_config_sem); + + printd("Done"); +} + +static int upisnd_gpio_chip_set_config(struct gpio_chip *gc, + unsigned int offset, + unsigned long config) +{ + int p = pinconf_to_config_param(config); + u32 arg = pinconf_to_config_argument(config); + + printd("%p %u %08lx p=%d arg=%u", gc, offset, config, p, arg); + + struct upisnd_instance *instance = gpiochip_get_data(gc); + + down_write(&instance->rw_gpio_config_sem); + if (instance->gpio.pin_configs[offset].flags & UPISND_PIN_FLAG_CONFIGURING_THROUGH_SYSFS) { + // Setup has taken care of configuring the pin already, + // ignore requests that come through gpiochip_request_own_desc. + up_write(&instance->rw_gpio_config_sem); + return 0; + } + + int err = 0; + + upisnd_setup_t setup = instance->gpio.pin_configs[offset].setup; + + if (upisnd_setup_get_element_type(setup) != UPISND_ELEMENT_TYPE_GPIO) { + err = -EINVAL; + goto cleanup; + } + + switch (p) { + case PIN_CONFIG_BIAS_PULL_DOWN: + case PIN_CONFIG_BIAS_PULL_UP: + if (upisnd_setup_get_gpio_dir(setup) != UPISND_PIN_DIR_INPUT) { + err = -EINVAL; + goto cleanup; + } + if (arg != 0) + upisnd_setup_set_gpio_pull(&setup, p == PIN_CONFIG_BIAS_PULL_UP ? + UPISND_PIN_PULL_UP : UPISND_PIN_PULL_DOWN); + else + upisnd_setup_set_gpio_pull(&setup, UPISND_PIN_PULL_NONE); + break; + case PIN_CONFIG_BIAS_DISABLE: + case PIN_CONFIG_BIAS_HIGH_IMPEDANCE: + if (upisnd_setup_get_gpio_dir(setup) != UPISND_PIN_DIR_INPUT) { + err = -EINVAL; + goto cleanup; + } + upisnd_setup_set_gpio_pull(&setup, UPISND_PIN_PULL_NONE); + break; + case UPISND_PINCONF_DIRECTION: + if (arg & UPISND_PINCONF_DIR_MASK) { + // Output. + upisnd_setup_set_gpio_dir(&setup, UPISND_PIN_DIR_OUTPUT); + upisnd_setup_set_gpio_output(&setup, arg == UPISND_PINCONF_DIR_OUTPUT_HIGH); + } else { + // Input. + if (arg == UPISND_PINCONF_DIR_INPUT_PULL_AS_IS_OR_NONE) { + if (upisnd_setup_get_gpio_dir(setup) == UPISND_PIN_DIR_OUTPUT) + upisnd_setup_set_gpio_pull(&setup, UPISND_PIN_PULL_NONE); + } else { + upisnd_setup_set_gpio_pull(&setup, (enum upisnd_pin_pull_e)arg); + } + upisnd_setup_set_gpio_dir(&setup, UPISND_PIN_DIR_INPUT); + } + break; + default: + printe("Not supported param %d", p); + err = -ENOTSUPP; + goto cleanup; + } + + err = upisnd_gpio_setup(instance, setup); + + if (err >= 0) + instance->gpio.pin_configs[offset].setup = setup; + + printd("Done"); + +cleanup: + up_write(&instance->rw_gpio_config_sem); + + return err; +} + +static int upisnd_gpio_irq_set_type(struct irq_data *data, unsigned int type) +{ + printd("%p %u", data, type); + + if (!(type & IRQ_TYPE_EDGE_BOTH)) { + printe("IRQ %d: unsupported type %u\n", data->irq, type); + return -ENOTSUPP; + } + + struct gpio_chip *gc = irq_data_get_irq_chip_data(data); + struct upisnd_instance *instance = gpiochip_get_data(gc); + + unsigned long pin = data->hwirq; + + down_read(&instance->rw_gpio_config_sem); + if (upisnd_setup_get_element_type(instance->gpio.pin_configs[pin].setup) != + UPISND_ELEMENT_TYPE_GPIO || + upisnd_setup_get_gpio_dir(instance->gpio.pin_configs[pin].setup) != + UPISND_PIN_DIR_INPUT) { + printe("Pin %s is not set up as an input!", upisnd_pin_name(pin)); + up_read(&instance->rw_gpio_config_sem); + return -EACCES; + } + up_read(&instance->rw_gpio_config_sem); + + upisnd_irq_config_set_irq_type(&instance->gpio.irq_type_config, pin, type); + set_bit(IRQ_FLAG_TYPES_CHANGED_BIT, &instance->gpio.irq_flags); + + return 0; +} + +static void upisnd_gpio_irq_ack(struct irq_data *data) +{ + printd("%p mask=%08x irq=%u hwirq=%lu", data, data->mask, data->irq, data->hwirq); +} + +static void upisnd_gpio_irq_mask(struct irq_data *data) +{ + printd("%p mask=%08x irq=%u hwirq=%lu", data, data->mask, data->irq, data->hwirq); + + struct gpio_chip *gc = irq_data_get_irq_chip_data(data); + struct upisnd_instance *instance = gpiochip_get_data(gc); + + unsigned long pin = data->hwirq; + + down_read(&instance->rw_gpio_config_sem); + if (upisnd_setup_get_element_type(instance->gpio.pin_configs[pin].setup) != + UPISND_ELEMENT_TYPE_GPIO || + upisnd_setup_get_gpio_dir(instance->gpio.pin_configs[pin].setup) != + UPISND_PIN_DIR_INPUT) { + printe("Pin %s is not set up as an input!", upisnd_pin_name(pin)); + up_read(&instance->rw_gpio_config_sem); + return; + } + up_read(&instance->rw_gpio_config_sem); + + upisnd_irq_mask_config_set(&instance->gpio.irq_mask_config, pin, true); + set_bit(IRQ_FLAG_MASKS_CHANGED_BIT, &instance->gpio.irq_flags); +} + +static void upisnd_gpio_irq_unmask(struct irq_data *data) +{ + printd("%p mask=%08x irq=%u hwirq=%lu", data, data->mask, data->irq, data->hwirq); + + struct gpio_chip *gc = irq_data_get_irq_chip_data(data); + struct upisnd_instance *instance = gpiochip_get_data(gc); + + unsigned long pin = data->hwirq; + + down_read(&instance->rw_gpio_config_sem); + if (upisnd_setup_get_element_type(instance->gpio.pin_configs[pin].setup) != + UPISND_ELEMENT_TYPE_GPIO || + upisnd_setup_get_gpio_dir(instance->gpio.pin_configs[pin].setup) != + UPISND_PIN_DIR_INPUT) { + printe("Pin %s is not set up as an input!", upisnd_pin_name(pin)); + up_read(&instance->rw_gpio_config_sem); + return; + } + up_read(&instance->rw_gpio_config_sem); + + upisnd_irq_mask_config_set(&instance->gpio.irq_mask_config, pin, false); + set_bit(IRQ_FLAG_MASKS_CHANGED_BIT, &instance->gpio.irq_flags); +} + +static void upisnd_gpio_irq_bus_lock(struct irq_data *data) +{ + printd("%p mask=%08x irq=%u hwirq=%lu", data, data->mask, data->irq, data->hwirq); + + struct gpio_chip *gc = irq_data_get_irq_chip_data(data); + struct upisnd_instance *instance = gpiochip_get_data(gc); + + mutex_lock(&instance->gpio.gpio_irq_lock); +} + +static void upisnd_gpio_irq_bus_sync_unlock(struct irq_data *data) +{ + printd("%p mask=%08x irq=%u hwirq=%lu", data, data->mask, data->irq, data->hwirq); + + struct gpio_chip *gc = irq_data_get_irq_chip_data(data); + struct upisnd_instance *instance = gpiochip_get_data(gc); + + int result = 0; + + if (test_bit(IRQ_FLAG_TYPES_CHANGED_BIT, &instance->gpio.irq_flags)) { + printd("Types changed"); + result = upisnd_comm_set_irq_types(instance, &instance->gpio.irq_type_config); + printd("result: %d", result); + + clear_bit(IRQ_FLAG_TYPES_CHANGED_BIT, &instance->gpio.irq_flags); + } + + if (test_bit(IRQ_FLAG_MASKS_CHANGED_BIT, &instance->gpio.irq_flags)) { + printd("Masks changed"); + result = upisnd_comm_set_irq_masks(instance, &instance->gpio.irq_mask_config); + printd("result: %d", result); + + clear_bit(IRQ_FLAG_MASKS_CHANGED_BIT, &instance->gpio.irq_flags); + } + + printd("done"); + mutex_unlock(&instance->gpio.gpio_irq_lock); +} + +static const struct irq_chip upisnd_gpio_irq_chip = { + .name = "pisound-micro-gpio", + .irq_set_type = upisnd_gpio_irq_set_type, + .irq_ack = upisnd_gpio_irq_ack, + .irq_mask = upisnd_gpio_irq_mask, + .irq_unmask = upisnd_gpio_irq_unmask, + .irq_bus_lock = upisnd_gpio_irq_bus_lock, + .irq_bus_sync_unlock = upisnd_gpio_irq_bus_sync_unlock, + .flags = IRQCHIP_MASK_ON_SUSPEND | IRQCHIP_IMMUTABLE, +}; + +static void upisnd_gpio_irq_event_handler(struct work_struct *work) +{ + struct upisnd_instance *instance = container_of(work, + struct upisnd_instance, + gpio.irq_event_handler); + + int n; + struct irq_event_t events[128]; + + set_bit(IRQ_FLAG_HANDLING_IRQ_BIT, &instance->gpio.irq_flags); + + unsigned int offset; + unsigned long masked[2]; + + memset(masked, 0, sizeof(masked)); + u8 i; + + down_write(&instance->rw_gpio_config_sem); + n = kfifo_out(&instance->gpio.irq_event_fifo, events, ARRAY_SIZE(events)); + if (n > 0) { + printd("Handling %u IRQ events:", n); + for (i = 0; i < n; ++i) { + offset = events[i].num; + + if (offset == 0x7f) { + printi("\tAlert event %s", events[i].high ? "on" : "off"); + continue; + } + + printd("\t%d %s %s", offset, upisnd_pin_name(offset), + events[i].high ? "up" : "down"); + + assign_bit(offset, instance->gpio.gpio_state, events[i].high); + + if (upisnd_irq_mask_config_get(&instance->gpio.irq_mask_config, offset)) + set_bit(offset, masked); + } + } + up_write(&instance->rw_gpio_config_sem); + + for (i = 0; i < n; ++i) { + if (!test_bit(i, masked)) { + offset = events[i].num; + int nested_irq = irq_find_mapping(instance->gpio.gpio_chip.irq.domain, + offset); + + if (unlikely(nested_irq <= 0)) { + dev_warn_ratelimited(instance->gpio.gpio_chip.parent, + "unmapped interrupt %d\n", + offset); + continue; + } else { + printd("Before handle_nested_irq"); + handle_nested_irq(nested_irq); + printd("After handle_nested_irq"); + } + } + } + + clear_bit(IRQ_FLAG_HANDLING_IRQ_BIT, &instance->gpio.irq_flags); + + if (!kfifo_is_empty(&instance->gpio.irq_event_fifo)) + queue_work(instance->work_queue, &instance->gpio.irq_event_handler); +} + +void upisnd_gpio_handle_irq_event(struct upisnd_instance *instance, + const struct irq_event_t *events, + unsigned int n) +{ + printd("Pushing %u events", n); + kfifo_in(&instance->gpio.irq_event_fifo, events, n); + if (!work_pending(&instance->gpio.irq_event_handler)) + queue_work(instance->work_queue, &instance->gpio.irq_event_handler); +} + +void upisnd_gpio_reset(struct upisnd_instance *instance) +{ + down_write(&instance->rw_gpio_config_sem); + + memset(&instance->gpio.gpio_state, 0, sizeof(instance->gpio.gpio_state)); + memset(instance->gpio.pin_configs, 0, sizeof(instance->gpio.pin_configs)); + memset(&instance->gpio.irq_mask_config, 0xff, sizeof(instance->gpio.irq_mask_config)); + + up_write(&instance->rw_gpio_config_sem); +} + +int upisnd_gpio_init(struct upisnd_instance *instance) +{ + struct upisnd_gpio *gpio = &instance->gpio; + + mutex_init(&gpio->gpio_irq_lock); + + // Set all IRQs as masked initially. + memset(&gpio->irq_mask_config, 0xff, sizeof(gpio->irq_mask_config)); + + INIT_KFIFO(gpio->irq_event_fifo); + INIT_WORK(&gpio->irq_event_handler, upisnd_gpio_irq_event_handler); + + gpio->gpio_chip = upisnd_gpio_chip; + gpio->gpio_chip.parent = instance->ctrl_dev; + + gpio->gpio_irq = &upisnd_gpio_irq_chip; + + struct gpio_irq_chip *girq = &gpio->gpio_chip.irq; + + gpio_irq_chip_set_chip(girq, gpio->gpio_irq); + girq->threaded = true; + girq->num_parents = 1; + girq->parents = devm_kzalloc(instance->ctrl_dev, sizeof(*girq->parents), GFP_KERNEL); + + if (!girq->parents) + return -ENOMEM; + + *girq->parents = irq_of_parse_and_map(instance->ctrl_dev->of_node, 0); + + printd("Doing devm_gpiochip_add_data"); + int err = devm_gpiochip_add_data(instance->ctrl_dev, &gpio->gpio_chip, instance); + + printd("result: %d, base = %d", err, gpio->gpio_chip.base); + return err; +} + +void upisnd_gpio_uninit(struct upisnd_instance *instance) +{ + kfifo_free(&instance->gpio.irq_event_fifo); +} + +void upisnd_gpio_set(struct upisnd_instance *instance, unsigned int offset, bool value) +{ + upisnd_gpio_chip_set(&instance->gpio.gpio_chip, offset, value); +} + +int upisnd_gpio_get(struct upisnd_instance *instance, unsigned int offset) +{ + return upisnd_gpio_chip_get(&instance->gpio.gpio_chip, offset); +} + +int upisnd_gpio_get_direction(struct upisnd_instance *instance, unsigned int offset) +{ + return upisnd_gpio_chip_get_direction(&instance->gpio.gpio_chip, offset); +} + +int upisnd_gpio_set_irq_type(struct upisnd_instance *instance, + unsigned int offset, + unsigned int irq_type) +{ + printd("(%d, %d)", offset, irq_type); + if (offset >= UPISND_NUM_GPIOS) + return -EINVAL; + + if (!(irq_type & IRQ_TYPE_EDGE_BOTH)) { + printe("IRQ %d: unsupported type %u\n", offset, irq_type); + return -ENOTSUPP; + } + + mutex_lock(&instance->gpio.gpio_irq_lock); + + if (upisnd_setup_get_element_type(instance->gpio.pin_configs[offset].setup) != + UPISND_ELEMENT_TYPE_GPIO || + upisnd_setup_get_gpio_dir(instance->gpio.pin_configs[offset].setup) != + UPISND_PIN_DIR_INPUT) { + printe("Pin %s is not set up as an input!", upisnd_pin_name(offset)); + return -EACCES; + } + upisnd_irq_config_set_irq_type(&instance->gpio.irq_type_config, offset, irq_type); + int result = upisnd_comm_set_irq_types(instance, &instance->gpio.irq_type_config); + (void)result; + + mutex_unlock(&instance->gpio.gpio_irq_lock); + + printd("result: %d", result); + return 0; +} + +int upisnd_gpio_set_subscription(struct upisnd_instance *instance, unsigned int offset, bool on) +{ + printd("(%d, %d)", offset, on); + + if (offset >= UPISND_NUM_GPIOS) + return -EINVAL; + + int result = 0; + int *rc = &instance->gpio.pin_subscription_refcounts[offset]; + int c; + + mutex_lock(&instance->gpio.gpio_irq_lock); + + if (on) + c = (*rc)++; + else + c = --(*rc); + + if (c == 0) { + printd("Count was 0, setting %d sub to %d", offset, on); + result = upisnd_comm_set_subscription(instance, offset, on); + } + + mutex_unlock(&instance->gpio.gpio_irq_lock); + + return result; +} + +enum upisnd_element_type_e upisnd_gpio_get_type(struct upisnd_instance *instance, + unsigned int offset) +{ + if (offset >= UPISND_NUM_GPIOS) + return UPISND_ELEMENT_TYPE_NONE; + + down_read(&instance->rw_gpio_config_sem); + upisnd_setup_t setup = instance->gpio.pin_configs[offset].setup; + + up_read(&instance->rw_gpio_config_sem); + + return upisnd_setup_get_element_type(setup); +} + +/* vim: set ts=8 sw=8 noexpandtab: */ --- /dev/null +++ b/sound/drivers/upisnd/upisnd_gpio.h @@ -0,0 +1,75 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Pisound Micro Linux kernel module. + * Copyright (C) 2017-2025 Vilniaus Blokas UAB, https://blokas.io/ + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; version 2 of the + * License. + * + * 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. + */ + +#ifndef UPISOUND_GPIO_H +#define UPISOUND_GPIO_H + +enum { + UPISND_PIN_FLAG_IS_ENCODER_B = 1 << 0, + UPISND_PIN_FLAG_CONFIGURING_THROUGH_SYSFS = 1 << 1, + UPISND_PIN_FLAG_IRQ_RISING = 1 << 2, + UPISND_PIN_FLAG_IRQ_FALLING = 1 << 3, +}; + +struct upisnd_pin_config_t { + upisnd_setup_t setup; + u32 flags; + struct gpio_desc *gpio_desc; + struct upisnd_element *element; +}; + +struct upisnd_gpio { + struct gpio_chip gpio_chip; + const struct irq_chip *gpio_irq; + // Protects IRQ state and bus. + struct mutex gpio_irq_lock; + unsigned long gpio_state[2]; + unsigned long irq_flags; + struct upisnd_irq_type_config_t irq_type_config; + struct upisnd_irq_mask_config_t irq_mask_config; + struct upisnd_pin_config_t pin_configs[UPISND_NUM_GPIOS]; + int pin_subscription_refcounts[UPISND_NUM_GPIOS]; + DECLARE_KFIFO(irq_event_fifo, struct irq_event_t, 128); + struct work_struct irq_event_handler; +}; + +int upisnd_gpio_init(struct upisnd_instance *instance); +void upisnd_gpio_uninit(struct upisnd_instance *instance); + +void upisnd_gpio_reset(struct upisnd_instance *instance); + +int upisnd_gpio_setup(struct upisnd_instance *instance, upisnd_setup_t setup); + +void upisnd_gpio_handle_irq_event(struct upisnd_instance *instance, + const struct irq_event_t *events, + unsigned int n); + +void upisnd_gpio_set(struct upisnd_instance *instance, unsigned int offset, bool value); +int upisnd_gpio_get(struct upisnd_instance *instance, unsigned int offset); +int upisnd_gpio_get_direction(struct upisnd_instance *instance, unsigned int offset); + +int upisnd_gpio_set_irq_type(struct upisnd_instance *instance, + unsigned int offset, + unsigned int irq_type); + +int upisnd_gpio_set_subscription(struct upisnd_instance *instance, unsigned int offset, bool on); + +enum upisnd_element_type_e upisnd_gpio_get_type(struct upisnd_instance *instance, + unsigned int offset); + +#endif // UPISOUND_GPIO_H + +/* vim: set ts=8 sw=8 noexpandtab: */ --- /dev/null +++ b/sound/drivers/upisnd/upisnd_midi.c @@ -0,0 +1,326 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Pisound Micro Linux kernel module. + * Copyright (C) 2017-2025 Vilniaus Blokas UAB, https://blokas.io/ + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; version 2 of the + * License. + * + * 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 "upisnd_common.h" + +static int upisnd_midi_output_open(struct snd_rawmidi_substream *substream) +{ + struct upisnd_instance *instance = substream->rmidi->private_data; + + mutex_lock(&instance->midi.out_lock); + instance->midi.midi_output = substream; + mutex_unlock(&instance->midi.out_lock); + return 0; +} + +static void upisnd_midi_output_drain(struct snd_rawmidi_substream *substream); + +static int upisnd_midi_output_close(struct snd_rawmidi_substream *substream) +{ + struct upisnd_instance *instance = substream->rmidi->private_data; + + printd("Close: draining output"); + upisnd_midi_output_drain(substream); + printd("Close: setting to null"); + mutex_lock(&instance->midi.out_lock); + instance->midi.midi_output = NULL; + mutex_unlock(&instance->midi.out_lock); + return 0; +} + +static void upisnd_midi_output_trigger(struct snd_rawmidi_substream *substream, int up) +{ + if (up == 0) + return; + + struct upisnd_instance *instance = substream->rmidi->private_data; + + if (!delayed_work_pending(&instance->midi.midi_out_handler)) + queue_delayed_work(instance->work_queue, &instance->midi.midi_out_handler, 0); +} + +static void upisnd_midi_output_drain(struct snd_rawmidi_substream *substream) +{ + struct upisnd_instance *instance = substream->rmidi->private_data; + + printd("Begin draining!"); + + do { + printd("Before flush"); + while (delayed_work_pending(&instance->midi.midi_out_handler)) + flush_delayed_work(&instance->midi.midi_out_handler); + printd("Flushed"); + } while (!snd_rawmidi_transmit_empty(substream)); + + printd("Done!"); +} + +static int upisnd_midi_input_open(struct snd_rawmidi_substream *substream) +{ + struct upisnd_instance *instance = substream->rmidi->private_data; + + mutex_lock(&instance->midi.in_lock); + instance->midi.midi_input = substream; + mutex_unlock(&instance->midi.in_lock); + return 0; +} + +static int upisnd_midi_input_close(struct snd_rawmidi_substream *substream) +{ + struct upisnd_instance *instance = substream->rmidi->private_data; + + mutex_lock(&instance->midi.in_lock); + instance->midi.midi_input = NULL; + mutex_unlock(&instance->midi.in_lock); + return 0; +} + +static void upisnd_midi_input_trigger(struct snd_rawmidi_substream *substream, int up) +{ + if (up == 0) + return; + + struct upisnd_instance *instance = substream->rmidi->private_data; + + if (!work_pending(&instance->midi.midi_in_handler)) + queue_work(instance->work_queue, &instance->midi.midi_in_handler); +} + +static const struct snd_rawmidi_ops upisnd_midi_output_ops = { + .open = upisnd_midi_output_open, + .close = upisnd_midi_output_close, + .trigger = upisnd_midi_output_trigger, + .drain = upisnd_midi_output_drain, +}; + +static const struct snd_rawmidi_ops upisnd_midi_input_ops = { + .open = upisnd_midi_input_open, + .close = upisnd_midi_input_close, + .trigger = upisnd_midi_input_trigger, +}; + +static void upisnd_get_port_info(struct snd_rawmidi *rmidi, + int number, + struct snd_seq_port_info *seq_port_info) +{ + seq_port_info->type = + SNDRV_SEQ_PORT_TYPE_MIDI_GENERIC | + SNDRV_SEQ_PORT_TYPE_HARDWARE | + SNDRV_SEQ_PORT_TYPE_PORT; + seq_port_info->midi_voices = 0; +} + +static const struct snd_rawmidi_global_ops upisnd_midi_ops = { + .get_port_info = upisnd_get_port_info, +}; + +static void upisnd_midi_in_handler(struct work_struct *work) +{ + int i, n, err; + + printd("In handler"); + struct upisnd_instance *instance = container_of(work, + struct upisnd_instance, + midi.midi_in_handler); + + mutex_lock(&instance->midi.in_lock); + if (!instance->midi.midi_input) + goto cleanup; + + u8 data[512]; + + n = kfifo_out_peek(&instance->midi.midi_in_fifo, data, sizeof(data)); + err = snd_rawmidi_receive(instance->midi.midi_input, data, n); + + if (err < 0) + printe("snd_rawmidi_receive failed! (%d)", err); + + if (err > 0) { + printd("Received %d MIDI bytes", err); + instance->midi.rx_cnt += err; + } + + for (i = 0; i < err; ++i) + kfifo_skip(&instance->midi.midi_in_fifo); + + if (!kfifo_is_empty(&instance->midi.midi_in_fifo) && + !work_pending(&instance->midi.midi_in_handler)) { + queue_work(instance->work_queue, &instance->midi.midi_in_handler); + } +cleanup: + mutex_unlock(&instance->midi.in_lock); + printd("Done"); +} + +static void upisnd_midi_out_handler(struct work_struct *work) +{ + printd("Out handler"); + struct upisnd_instance *instance = container_of(work, + struct upisnd_instance, + midi.midi_out_handler.work); + + mutex_lock(&instance->midi.out_lock); + printd("midi_output = %p", instance->midi.midi_output); + if (!instance->midi.midi_output) + goto cleanup; + + enum { MIDI_MILLI_BYTES_PER_JIFFY = 3125000 / HZ }; + enum { MIDI_MAX_OUTPUT_BUFFER_SIZE_IN_MILLIBYTES = 4096000 }; + + unsigned int now = jiffies; + unsigned int millibytes_became_available = (MIDI_MILLI_BYTES_PER_JIFFY) * + (now - instance->midi.last_midi_output_at); + + instance->midi.output_buffer_used_in_millibytes = + instance->midi.output_buffer_used_in_millibytes <= millibytes_became_available ? + 0 : instance->midi.output_buffer_used_in_millibytes - millibytes_became_available; + instance->midi.last_midi_output_at = now; + + unsigned int output_buffer_available = (MIDI_MAX_OUTPUT_BUFFER_SIZE_IN_MILLIBYTES - + instance->midi.output_buffer_used_in_millibytes) / 1000; + + u8 buffer[UPISND_MAX_PACKET_LENGTH - 1]; + + printd("Available: %u", output_buffer_available); + int n = snd_rawmidi_transmit_peek(instance->midi.midi_output, + buffer, + min(output_buffer_available, sizeof(buffer))); + + if (n > 0) { + printd("Peeked: %d", n); + snd_rawmidi_transmit_ack(instance->midi.midi_output, n); + n = upisnd_comm_send_midi(instance, buffer, (unsigned int)n); + if (n < 0) + printe("Error occurred when sending MIDI data over I2C! (%d)", n); + } else { + printe("snd_rawmidi_transmit_peek returned error %d!", n); + goto cleanup; + } + + instance->midi.tx_cnt += n; + instance->midi.output_buffer_used_in_millibytes += n * 1000; + + printd("Checking if empty %p", instance->midi.midi_output); + if (!snd_rawmidi_transmit_empty(instance->midi.midi_output)) { + unsigned int delay = 0; + + if (instance->midi.output_buffer_used_in_millibytes > + MIDI_MAX_OUTPUT_BUFFER_SIZE_IN_MILLIBYTES - 127000) + delay = 127000 / MIDI_MILLI_BYTES_PER_JIFFY; + printd("Queue more work after %u jiffies", delay); + queue_delayed_work(instance->work_queue, &instance->midi.midi_out_handler, delay); + } + +cleanup: + mutex_unlock(&instance->midi.out_lock); + printd("Done"); +} + +static void upisnd_proc_stat_show(struct snd_info_entry *entry, struct snd_info_buffer *buffer) +{ + const unsigned int *d = entry->private_data; + + snd_iprintf(buffer, "%u\n", *d); +} + +int upisnd_midi_init(struct upisnd_instance *instance) +{ + int err; + + mutex_init(&instance->midi.in_lock); + mutex_init(&instance->midi.out_lock); + + err = snd_card_ro_proc_new(instance->sound_card.snd_card, "tx", &instance->midi.tx_cnt, + upisnd_proc_stat_show); + err = snd_card_ro_proc_new(instance->sound_card.snd_card, "rx", &instance->midi.rx_cnt, + upisnd_proc_stat_show); + + err = snd_rawmidi_new(instance->sound_card.snd_card, + "pisoundmicro", 0, 1, 1, + &instance->midi.rawmidi); + + struct snd_rawmidi *rawmidi = instance->midi.rawmidi; + + if (err < 0) { + printe("snd_rawmidi_new failed: %d\n", err); + return err; + } + + strscpy(rawmidi->name, "pisound-micro ", sizeof(rawmidi->name)); + strcat(rawmidi->name, instance->ctrl.serial); + + rawmidi->info_flags = + SNDRV_RAWMIDI_INFO_OUTPUT | + SNDRV_RAWMIDI_INFO_INPUT | + SNDRV_RAWMIDI_INFO_DUPLEX; + + rawmidi->ops = &upisnd_midi_ops; + + rawmidi->private_data = instance; + + snd_rawmidi_set_ops(rawmidi, SNDRV_RAWMIDI_STREAM_OUTPUT, &upisnd_midi_output_ops); + + snd_rawmidi_set_ops(rawmidi, SNDRV_RAWMIDI_STREAM_INPUT, &upisnd_midi_input_ops); + + INIT_KFIFO(instance->midi.midi_in_fifo); + instance->midi.last_midi_output_at = jiffies; + + INIT_WORK(&instance->midi.midi_in_handler, upisnd_midi_in_handler); + INIT_DELAYED_WORK(&instance->midi.midi_out_handler, upisnd_midi_out_handler); + + return 0; +} + +void upisnd_midi_uninit(struct upisnd_instance *instance) +{ + if (!instance->midi.rawmidi) + return; + + cancel_work_sync(&instance->midi.midi_in_handler); + cancel_delayed_work_sync(&instance->midi.midi_out_handler); + + instance->midi.rawmidi->private_data = NULL; + + instance->midi.rawmidi = NULL; + mutex_lock(&instance->midi.in_lock); + instance->midi.midi_input = NULL; + mutex_unlock(&instance->midi.in_lock); + mutex_lock(&instance->midi.out_lock); + instance->midi.midi_output = NULL; + mutex_unlock(&instance->midi.out_lock); + + kfifo_free(&instance->midi.midi_in_fifo); +} + +void upisnd_handle_midi_data(struct upisnd_instance *instance, const uint8_t *data, unsigned int n) +{ + printd("%p, %u", instance, n); + if (n == 0) + return; + + int i; + + for (i = 0; i < n; ++i) { + kfifo_put(&instance->midi.midi_in_fifo, data[i]); + printd("Received MIDI %02x", data[i]); + } + + if (!work_pending(&instance->midi.midi_in_handler)) + queue_work(instance->work_queue, &instance->midi.midi_in_handler); + printd("Done"); +} + +/* vim: set ts=8 sw=8 noexpandtab: */ --- /dev/null +++ b/sound/drivers/upisnd/upisnd_midi.h @@ -0,0 +1,47 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Pisound Micro Linux kernel module. + * Copyright (C) 2017-2025 Vilniaus Blokas UAB, https://blokas.io/ + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; version 2 of the + * License. + * + * 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. + */ + +#ifndef UPISOUND_MIDI_H +#define UPISOUND_MIDI_H + +struct upisnd_instance; + +struct upisnd_midi { + // in_lock protects the input substream. + struct mutex in_lock; + // out_lock protects the output substream. + struct mutex out_lock; + struct work_struct midi_in_handler; + struct delayed_work midi_out_handler; + + struct snd_rawmidi *rawmidi; + struct snd_rawmidi_substream *midi_input; + struct snd_rawmidi_substream *midi_output; + DECLARE_KFIFO(midi_in_fifo, uint8_t, 4096); + unsigned int last_midi_output_at; + unsigned int output_buffer_used_in_millibytes; + unsigned int tx_cnt; + unsigned int rx_cnt; +}; + +int upisnd_midi_init(struct upisnd_instance *instance); +void upisnd_midi_uninit(struct upisnd_instance *instance); + +void upisnd_handle_midi_data(struct upisnd_instance *instance, const uint8_t *data, unsigned int n); + +#endif // UPISOUND_MIDI_H + +/* vim: set ts=8 sw=8 noexpandtab: */ --- /dev/null +++ b/sound/drivers/upisnd/upisnd_module.c @@ -0,0 +1,273 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Pisound Micro Linux kernel module. + * Copyright (C) 2017-2025 Vilniaus Blokas UAB, https://blokas.io/ + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; version 2 of the + * License. + * + * 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 "upisnd_common.h" + +void upisnd_instance_release(struct kref *kref) +{ + struct upisnd_instance *instance = container_of(kref, struct upisnd_instance, refcount); + + if (instance->codec_dev) { + put_device(instance->codec_dev); + instance->codec_dev = NULL; + } + + if (instance->ctrl_dev) { + instance->ctrl_dev->platform_data = NULL; + put_device(instance->ctrl_dev); + instance->ctrl_dev = NULL; + } + + if (instance->sound_card.dev) { + snd_soc_unregister_card(&instance->sound_card); + memset(&instance->sound_card, 0, sizeof(instance->sound_card)); + } + + if (instance->work_queue) { + flush_workqueue(instance->work_queue); + destroy_workqueue(instance->work_queue); + instance->work_queue = NULL; + } + + printd("Releasing instance %p", instance); + kfree(instance); +} + +static int of_dev_node_match(struct device *dev, const void *data) +{ + return dev->of_node == data; +} + +static int upisnd_probe(struct platform_device *pdev) +{ + printd("Load %p", pdev); + + int err = 0; + struct device_node *node; + struct device_node *i2c_node; + struct device *dev; + + struct upisnd_instance *instance = pdev->dev.platform_data; + + if (!instance) { + instance = kzalloc(sizeof(*instance), GFP_KERNEL); + + if (!instance) { + printe("Failed to allocate instance!"); + err = -ENOMEM; + goto cleanup; + } + kref_init(&instance->refcount); + + pdev->dev.platform_data = instance; + instance->pdev = pdev; + + init_rwsem(&instance->rw_gpio_config_sem); + + instance->work_queue = create_singlethread_workqueue("upisnd_workqueue"); + if (!instance->work_queue) { + printe("Failed creating single thread work queue!"); + err = -ENOMEM; + goto cleanup; + } + } + + node = pdev->dev.of_node; + if (!node) { + printe("Device node not found!"); + err = -ENODEV; + goto cleanup; + } + + i2c_node = of_parse_phandle(node, "ctrl", 0); + if (!i2c_node) { + printe("Failed to read 'ctrl' node!"); + err = -ENODEV; + goto cleanup; + } + + dev = bus_find_device(&i2c_bus_type, NULL, i2c_node, of_dev_node_match); + of_node_put(i2c_node); + + if (!dev) { + printe("Failed to find 'ctrl' device (%pOF)!", i2c_node); + err = -ENODEV; + goto cleanup; + } + + bool got_ref = false; + + if (!dev->platform_data) { + kref_get(&instance->refcount); + dev->platform_data = instance; + got_ref = true; + } + instance->ctrl_dev = dev; + + if (instance->ctrl.serial[0] == '\0') { + printd("Deferring probe until serial is retrieved!"); + return -EPROBE_DEFER; + } + + i2c_node = of_parse_phandle(node, "codec", 0); + + if (!i2c_node) { + printe("Failed to read 'codec' node!"); + err = -ENODEV; + goto cleanup; + } + + dev = bus_find_device(&i2c_bus_type, NULL, i2c_node, of_dev_node_match); + of_node_put(i2c_node); + + if (!dev) { + printe("Failed to find 'codec' device!"); + err = -ENODEV; + goto cleanup; + } + + instance->codec_dev = dev; + + err = upisnd_sound_init(pdev, instance); + if (err != 0) { + if (err != -EPROBE_DEFER) + printe("Failed initializing sound card! (%d)", err); + goto cleanup; + } + +cleanup: + if (err != 0) { + if (err != -EPROBE_DEFER) + printe("Error %d!", err); + + if (instance) { + if (got_ref) { + instance->ctrl_dev->platform_data = NULL; + kref_put(&instance->refcount, &upisnd_instance_release); + } + + if (instance->codec_dev) { + put_device(instance->codec_dev); + instance->codec_dev = NULL; + } + } + } + + return err; +} + +static void upisnd_remove(struct platform_device *pdev) +{ + printd("Unload %p", pdev); + struct upisnd_instance *instance = dev_get_platdata(&pdev->dev); + + kref_put(&instance->refcount, &upisnd_instance_release); + pdev->dev.platform_data = NULL; +} + +static const struct of_device_id upisnd_of_match[] = { + {.compatible = "blokas,pisound-micro" }, + {} +}; + +static const struct of_device_id upisnd_ctrl_of_match[] = { + {.compatible = "blokas,upisnd-ctrl" }, + {} +}; + +static struct platform_driver upisnd_driver = { + .driver = { + .name = "snd-pisound-micro", + .owner = THIS_MODULE, + .of_match_table = of_match_ptr(upisnd_of_match), + }, + .probe = upisnd_probe, + .remove = upisnd_remove, +}; + +static const struct i2c_device_id upisnd_ctrl_idtable[] = { + { "blokas,upisnd-ctrl", 0 }, + {} +}; + +static struct i2c_driver upisnd_ctrl_driver = { + .driver = { + .name = "snd-pisound-micro-ctrl", + .owner = THIS_MODULE, + .of_match_table = of_match_ptr(upisnd_ctrl_of_match), + }, + .id_table = upisnd_ctrl_idtable, + .probe = upisnd_ctrl_probe, + .remove = upisnd_ctrl_remove, +}; + +static int upisnd_module_init(void) +{ + int err, progress = 0; + + err = upisnd_comm_module_init(); + if (err != 0) + goto cleanup; + + ++progress; + err = platform_driver_register(&upisnd_driver); + if (err != 0) + goto cleanup; + + ++progress; + err = i2c_add_driver(&upisnd_ctrl_driver); + if (err != 0) + goto cleanup; + +cleanup: + if (err) { + printe("Error %d occurred, progress: %d", err, progress); + switch (progress) { + case 2: + i2c_del_driver(&upisnd_ctrl_driver); + fallthrough; + case 1: + platform_driver_unregister(&upisnd_driver); + fallthrough; + case 0: + // No comm uninit. + fallthrough; + default: + break; + } + } + + return err; +} + +static void upisnd_module_exit(void) +{ + i2c_del_driver(&upisnd_ctrl_driver); + platform_driver_unregister(&upisnd_driver); +} + +module_init(upisnd_module_init); +module_exit(upisnd_module_exit); + +MODULE_DEVICE_TABLE(of, upisnd_of_match); +MODULE_DEVICE_TABLE(of, upisnd_ctrl_of_match); +MODULE_DEVICE_TABLE(i2c, upisnd_ctrl_idtable); + +MODULE_AUTHOR("Giedrius Trainavi\xc4\x8dius "); +MODULE_DESCRIPTION("Audio, MIDI & I/O Driver for Pisound Micro, https://blokas.io/"); +MODULE_LICENSE("GPL v2"); + +/* vim: set ts=8 sw=8 noexpandtab: */ --- /dev/null +++ b/sound/drivers/upisnd/upisnd_pins.c @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Pisound Micro Linux kernel module. + * Copyright (C) 2017-2025 Vilniaus Blokas UAB, https://blokas.io/ + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; version 2 of the + * License. + * + * 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 "upisnd_common.h" + +struct upisnd_pin_def_t { + const char *name; + upisnd_pin_capability_mask_t capabilities; +}; + +static const struct upisnd_pin_def_t upisnd_pins[UPISND_PIN_COUNT] = { + { "A27", UPISND_PIN_CAP_GPIO | UPISND_PIN_CAP_MIDI_ACTIVITY }, + { "A28", UPISND_PIN_CAP_GPIO | UPISND_PIN_CAP_MIDI_ACTIVITY }, + { "A29", UPISND_PIN_CAP_GPIO | UPISND_PIN_CAP_MIDI_ACTIVITY }, + { "A30", UPISND_PIN_CAP_GPIO | UPISND_PIN_CAP_MIDI_ACTIVITY }, + { "A31", UPISND_PIN_CAP_GPIO | UPISND_PIN_CAP_MIDI_ACTIVITY }, + { "A32", UPISND_PIN_CAP_GPIO | UPISND_PIN_CAP_MIDI_ACTIVITY }, + { "B03", UPISND_PIN_CAP_GPIO | UPISND_PIN_CAP_MIDI_ACTIVITY | UPISND_PIN_CAP_ENCODER }, + { "B04", UPISND_PIN_CAP_GPIO | UPISND_PIN_CAP_MIDI_ACTIVITY | UPISND_PIN_CAP_ENCODER }, + { "B05", UPISND_PIN_CAP_GPIO | UPISND_PIN_CAP_MIDI_ACTIVITY | UPISND_PIN_CAP_ENCODER }, + { "B06", UPISND_PIN_CAP_GPIO | UPISND_PIN_CAP_MIDI_ACTIVITY | UPISND_PIN_CAP_ENCODER }, + { "B07", UPISND_PIN_CAP_GPIO | UPISND_PIN_CAP_MIDI_ACTIVITY | UPISND_PIN_CAP_ENCODER }, + { "B08", UPISND_PIN_CAP_GPIO | UPISND_PIN_CAP_MIDI_ACTIVITY | UPISND_PIN_CAP_ENCODER }, + { "B09", UPISND_PIN_CAP_GPIO | UPISND_PIN_CAP_MIDI_ACTIVITY | UPISND_PIN_CAP_ENCODER }, + { "B10", UPISND_PIN_CAP_GPIO | UPISND_PIN_CAP_MIDI_ACTIVITY | UPISND_PIN_CAP_ENCODER }, + // Pull down is not available in MCU revisions A, B, C, D, E. F and G are not affected. + { "B11", UPISND_PIN_CAP_GPIO | UPISND_PIN_CAP_MIDI_ACTIVITY | UPISND_PIN_CAP_ENCODER }, + // Pull down is not available in MCU revisions A, B, C, D, E. F and G are not affected. + { "B12", UPISND_PIN_CAP_GPIO | UPISND_PIN_CAP_MIDI_ACTIVITY | UPISND_PIN_CAP_ENCODER }, + { "B13", UPISND_PIN_CAP_GPIO | UPISND_PIN_CAP_MIDI_ACTIVITY | UPISND_PIN_CAP_ENCODER }, + { "B14", UPISND_PIN_CAP_GPIO | UPISND_PIN_CAP_MIDI_ACTIVITY | UPISND_PIN_CAP_ENCODER }, + { "B15", UPISND_PIN_CAP_GPIO | UPISND_PIN_CAP_MIDI_ACTIVITY | UPISND_PIN_CAP_ENCODER }, + { "B16", UPISND_PIN_CAP_GPIO | UPISND_PIN_CAP_MIDI_ACTIVITY | UPISND_PIN_CAP_ENCODER }, + { "B17", UPISND_PIN_CAP_GPIO | UPISND_PIN_CAP_MIDI_ACTIVITY | UPISND_PIN_CAP_ENCODER }, + { "B18", UPISND_PIN_CAP_GPIO | UPISND_PIN_CAP_MIDI_ACTIVITY | UPISND_PIN_CAP_ENCODER }, + { "B23", UPISND_PIN_CAP_GPIO | UPISND_PIN_CAP_MIDI_ACTIVITY | UPISND_PIN_CAP_ANALOG_IN }, + { "B24", UPISND_PIN_CAP_GPIO | UPISND_PIN_CAP_MIDI_ACTIVITY | UPISND_PIN_CAP_ANALOG_IN }, + { "B25", UPISND_PIN_CAP_GPIO | UPISND_PIN_CAP_MIDI_ACTIVITY | UPISND_PIN_CAP_ANALOG_IN }, + { "B26", UPISND_PIN_CAP_GPIO | UPISND_PIN_CAP_MIDI_ACTIVITY | UPISND_PIN_CAP_ANALOG_IN }, + { "B27", UPISND_PIN_CAP_GPIO | UPISND_PIN_CAP_MIDI_ACTIVITY | UPISND_PIN_CAP_ANALOG_IN }, + { "B28", UPISND_PIN_CAP_GPIO | UPISND_PIN_CAP_MIDI_ACTIVITY | UPISND_PIN_CAP_ANALOG_IN }, + { "B29", UPISND_PIN_CAP_GPIO | UPISND_PIN_CAP_MIDI_ACTIVITY | UPISND_PIN_CAP_ANALOG_IN }, + { "B30", UPISND_PIN_CAP_GPIO | UPISND_PIN_CAP_MIDI_ACTIVITY | UPISND_PIN_CAP_ANALOG_IN }, + { "B31", UPISND_PIN_CAP_GPIO | UPISND_PIN_CAP_MIDI_ACTIVITY | UPISND_PIN_CAP_ANALOG_IN }, + { "B32", UPISND_PIN_CAP_GPIO | UPISND_PIN_CAP_MIDI_ACTIVITY | UPISND_PIN_CAP_ANALOG_IN }, + { "B33", UPISND_PIN_CAP_GPIO | UPISND_PIN_CAP_MIDI_ACTIVITY | UPISND_PIN_CAP_ANALOG_IN }, + { "B34", UPISND_PIN_CAP_GPIO | UPISND_PIN_CAP_MIDI_ACTIVITY | UPISND_PIN_CAP_ANALOG_IN }, + { "B37", UPISND_PIN_CAP_GPIO | UPISND_PIN_CAP_MIDI_ACTIVITY }, + { "B38", UPISND_PIN_CAP_GPIO | UPISND_PIN_CAP_MIDI_ACTIVITY }, + { "B39", UPISND_PIN_CAP_GPIO | UPISND_PIN_CAP_MIDI_ACTIVITY }, +}; + +upisnd_pin_t upisnd_name_to_pin(const char *name) +{ + if (!name || strlen(name) != 3 || !isdigit(name[1]) || !isdigit(name[2])) + return UPISND_PIN_INVALID; + + char sanitized[4]; + + switch (*name) { + case 'a': case 'A': + sanitized[0] = 'A'; + break; + case 'b': case 'B': + sanitized[0] = 'B'; + break; + default: + return UPISND_PIN_INVALID; + } + + memcpy(&sanitized[1], &name[1], 2); + sanitized[3] = '\0'; + + int i; + + for (i = 0; i < ARRAY_SIZE(upisnd_pins); ++i) { + if (strcmp(sanitized, upisnd_pins[i].name) == 0) + return i; + } + + return UPISND_PIN_INVALID; +} + +const char *upisnd_pin_name(upisnd_pin_t pin) +{ + if (!upisnd_is_pin_valid(pin)) + return ""; + + return upisnd_pins[pin].name; +} + +int upisnd_check_caps(upisnd_pin_t pin, upisnd_pin_capability_mask_t mask) +{ + if (!upisnd_is_pin_valid(pin)) + return -ENXIO; + + return (upisnd_pins[pin].capabilities & mask) == mask ? 0 : -EINVAL; +} + +/* vim: set ts=8 sw=8 noexpandtab: */ --- /dev/null +++ b/sound/drivers/upisnd/upisnd_pins.h @@ -0,0 +1,67 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Pisound Micro Linux kernel module. + * Copyright (C) 2017-2025 Vilniaus Blokas UAB, https://blokas.io/ + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; version 2 of the + * License. + * + * 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. + */ + +#ifndef UPISOUND_PINS_H +#define UPISOUND_PINS_H + +enum { UPISND_NUM_GPIOS = 37 }; + +enum upisnd_pin_e { + UPISND_PIN_A27, UPISND_PIN_A28, UPISND_PIN_A29, UPISND_PIN_A30, + UPISND_PIN_A31, UPISND_PIN_A32, UPISND_PIN_B03, UPISND_PIN_B04, + UPISND_PIN_B05, UPISND_PIN_B06, UPISND_PIN_B07, UPISND_PIN_B08, + UPISND_PIN_B09, UPISND_PIN_B10, UPISND_PIN_B11, UPISND_PIN_B12, + UPISND_PIN_B13, UPISND_PIN_B14, UPISND_PIN_B15, UPISND_PIN_B16, + UPISND_PIN_B17, UPISND_PIN_B18, UPISND_PIN_B23, UPISND_PIN_B24, + UPISND_PIN_B25, UPISND_PIN_B26, UPISND_PIN_B27, UPISND_PIN_B28, + UPISND_PIN_B29, UPISND_PIN_B30, UPISND_PIN_B31, UPISND_PIN_B32, + UPISND_PIN_B33, UPISND_PIN_B34, UPISND_PIN_B37, UPISND_PIN_B38, + UPISND_PIN_B39, + + UPISND_PIN_COUNT, + UPISND_PIN_INVALID = UPISND_PIN_COUNT +}; + +typedef u8 upisnd_pin_t; + +static inline bool upisnd_is_pin_valid(upisnd_pin_t pin) +{ + return pin < UPISND_PIN_COUNT; +} + +enum upisnd_pin_capability_flags_e { + UPISND_PIN_CAP_GPIO_DIR_INPUT = 1 << 0, + UPISND_PIN_CAP_GPIO_DIR_OUTPUT = 1 << 1, + UPISND_PIN_CAP_GPIO_PULL_UP = 1 << 2, + UPISND_PIN_CAP_GPIO_PULL_DOWN = 1 << 3, + UPISND_PIN_CAP_GPIO = UPISND_PIN_CAP_GPIO_DIR_INPUT | + UPISND_PIN_CAP_GPIO_DIR_OUTPUT | + UPISND_PIN_CAP_GPIO_PULL_UP | + UPISND_PIN_CAP_GPIO_PULL_DOWN, + UPISND_PIN_CAP_ENCODER = 1 << 4, + UPISND_PIN_CAP_ANALOG_IN = 1 << 5, + UPISND_PIN_CAP_MIDI_ACTIVITY = 1 << 6, +}; + +typedef u8 upisnd_pin_capability_mask_t; + +upisnd_pin_t upisnd_name_to_pin(const char *name); +const char *upisnd_pin_name(upisnd_pin_t pin); +int upisnd_check_caps(upisnd_pin_t pin, upisnd_pin_capability_mask_t mask); + +#endif // UPISOUND_PINS_H + +/* vim: set ts=8 sw=8 noexpandtab: */ --- /dev/null +++ b/sound/drivers/upisnd/upisnd_protocol.h @@ -0,0 +1,322 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Pisound Micro Linux kernel module. + * Copyright (C) 2017-2025 Vilniaus Blokas UAB, https://blokas.io/ + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; version 2 of the + * License. + * + * 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. + */ + +#ifndef UPISND_PROTOCOL_H +#define UPISND_PROTOCOL_H + +#ifndef __KERNEL__ +#include +#include + +enum upisnd_irq_type_e { + IRQ_TYPE_NONE = 0, + IRQ_TYPE_EDGE_RISING = 1, + IRQ_TYPE_EDGE_FALLING = 2, + IRQ_TYPE_EDGES_BOTH = 3, +}; +#endif + +enum { UPISND_ON_BIT_MASK = 0x80 }; +enum { UPISND_IRQ_NUM_MASK = 0x7f }; +enum { UPISND_PIN_MASK = 0x3f }; + +enum upisnd_irq_num_e { + UPISND_IRQ_GPIO_START = 0, + // IRQ numbers match GPIO numbers. + UPISND_IRQ_GPIO_END = UPISND_PIN_MASK, + + UPISND_IRQ_VGND_SHORT_ALERT = 0x7f, +}; + +typedef u8 upisnd_pin_t; +typedef u8 upisnd_irq_num_t; +typedef u8 upisnd_msg_id_t; + +typedef u32 upisnd_setup_t; + +enum upisnd_activity_type_e { + UPISND_ACTIVITY_TYPE_MIDI_IN = 0, + UPISND_ACTIVITY_TYPE_MIDI_OUT = 1, +}; + +enum upisnd_pin_pull_e { + UPISND_PIN_PULL_NONE = 0, + UPISND_PIN_PULL_UP = 1, + UPISND_PIN_PULL_DOWN = 2 +}; + +enum upisnd_pin_direction_e { + UPISND_PIN_DIR_INPUT = 0, + UPISND_PIN_DIR_OUTPUT = 1 +}; + +enum upisnd_element_type_e { + UPISND_ELEMENT_TYPE_NONE = 0, + UPISND_ELEMENT_TYPE_ENCODER = 1, + UPISND_ELEMENT_TYPE_ANALOG_IN = 2, + UPISND_ELEMENT_TYPE_GPIO = 3, + UPISND_ELEMENT_TYPE_ACTIVITY = 4, +}; + +#define UPISND_DEFINE_SETUP_FIELD(shift, bits, type, name) \ + static inline type upisnd_setup_get_ ## name(upisnd_setup_t setup) \ + { \ + return (type)(((setup) & (((1 << (bits)) - 1) << (shift))) >> (shift)); \ + } \ + static inline void upisnd_setup_set_ ## name(upisnd_setup_t *setup, type value) \ + { \ + *(setup) = ((*(setup)) & ~(((1 << (bits)) - 1) << (shift))) | \ + (((value) & ((1 << (bits)) - 1)) << (shift)); \ + } + +UPISND_DEFINE_SETUP_FIELD(0, 3, enum upisnd_element_type_e, element_type); +UPISND_DEFINE_SETUP_FIELD(3, 8, upisnd_pin_t, pin_id); +UPISND_DEFINE_SETUP_FIELD(11, 2, enum upisnd_pin_pull_e, gpio_pull); +UPISND_DEFINE_SETUP_FIELD(13, 1, enum upisnd_pin_direction_e, gpio_dir); +UPISND_DEFINE_SETUP_FIELD(12, 1, bool, gpio_output); +UPISND_DEFINE_SETUP_FIELD(13, 8, upisnd_pin_t, encoder_pin_b_id); +UPISND_DEFINE_SETUP_FIELD(21, 2, enum upisnd_pin_pull_e, encoder_pin_b_pull); +UPISND_DEFINE_SETUP_FIELD(11, 2, enum upisnd_activity_type_e, activity_type); + +#undef UPISND_DEFINE_SETUP_FIELD + +struct upisnd_irq_type_config_t { + u8 irq_types[10]; +} __packed; + +static inline void upisnd_irq_config_set_irq_type(struct upisnd_irq_type_config_t *cfg, + upisnd_pin_t pin, + unsigned int type) +{ + u8 *d = &cfg->irq_types[pin >> 2]; + *d = (*d & ~(0x3 << (pin & 0x3))) | ((type & 0x3) << (pin & 0x3)); +} + +static inline unsigned int upisnd_irq_config_get_irq_type(const struct upisnd_irq_type_config_t + *cfg, upisnd_pin_t pin) +{ + return (cfg->irq_types[pin >> 2] >> (pin & 0x3)) & 0x3; +} + +struct upisnd_irq_mask_config_t { + u8 irq_mask[5]; +} __packed; + +static inline void upisnd_irq_mask_config_set(struct upisnd_irq_mask_config_t *cfg, + upisnd_pin_t pin, + bool mask) +{ + u8 *d = &cfg->irq_mask[pin >> 3]; + + if (mask) + *d |= 1 << (pin & 0x7); + else + *d &= ~(1 << (pin & 0x7)); +} + +static inline bool upisnd_irq_mask_config_get(const struct upisnd_irq_mask_config_t *cfg, + upisnd_pin_t pin) +{ + return (cfg->irq_mask[pin >> 3] & (1 << (pin & 0x7))) != 0; +} + +struct upisnd_all_gpio_state_t { + u8 state[5]; +} __packed; + +enum { UPISND_MAX_PACKET_LENGTH = 16 }; +enum { UPISND_MSG_RESPONSE_FLAG = 0x80 }; +enum { UPISND_MSG_ID_MASK = 0x7f }; +enum { UPISND_MSG_ID_INVALID = 0x00 }; + +enum upisnd_cmd_type_e { + UPISND_CMD_MIDI = 0x00, + UPISND_CMD_SETUP = 0x10, + UPISND_CMD_SET_IRQ_TYPES = 0x20, + UPISND_CMD_SET_IRQ_MASKS = 0x30, + UPISND_CMD_SET_GPIO = 0x40, + UPISND_CMD_GET_GPIO = 0x50, + UPISND_CMD_SET_ALL_GPIOS = 0x60, + UPISND_CMD_GET_ALL_GPIOS = 0x70, + UPISND_CMD_RESULT = 0x80, + UPISND_CMD_SET_SUBSCRIPTION = 0x90, + UPISND_CMD_IRQ_EVENT = 0xa0, + UPISND_CMD_GET = 0xb0, + UPISND_CMD_SET = 0xc0, + UPISND_CMD_CONTROL_EVENT = 0xd0, + + UPISND_CMD_INVALID = 0xff +}; + +enum { UPISND_CMD_TYPE_MASK = 0xf0 }; +enum { UPISND_CMD_LENGTH_MASK = 0x0f }; + +enum upisnd_value_id_t { + UPISND_VALUE_INVALID = 0x00, + UPISND_VALUE_VERSION_INFO = 0x01, + UPISND_VALUE_ADC_OFFSET = 0x02, + UPISND_VALUE_ADC_GAIN = 0x03, + UPISND_VALUE_SERIAL_NUMBER = 0x04, + UPISND_VALUE_ELEMENT_VAL_BASE = 0x80, +}; + +struct upisnd_cmd_t { + u8 cmd_and_size; + u8 flags_and_msg_id; +} __packed; + +static inline bool upisnd_cmd_is_response(const struct upisnd_cmd_t *cmd) +{ + return (cmd->flags_and_msg_id & UPISND_MSG_RESPONSE_FLAG) != 0; +} + +struct upisnd_cmd_setup_t { + struct upisnd_cmd_t cmd; + upisnd_setup_t setup; +} __packed; + +struct upisnd_cmd_set_gpio_t { + u8 cmd_and_size; + u8 on_and_pin; +} __packed; + +struct upisnd_cmd_get_gpio_t { + struct upisnd_cmd_t cmd; + u8 pin; +} __packed; + +struct upisnd_cmd_set_all_gpios_t { + u8 cmd_and_size; + struct upisnd_all_gpio_state_t state; +} __packed; + +struct upisnd_cmd_get_all_gpios_t { + struct upisnd_cmd_t cmd; +} __packed; + +struct upisnd_cmd_get_all_gpios_response_t { + struct upisnd_cmd_t cmd; + struct upisnd_all_gpio_state_t state; +} __packed; + +struct upisnd_cmd_set_irq_types_t { + struct upisnd_cmd_t cmd; + struct upisnd_irq_type_config_t config; +} __packed; + +struct upisnd_cmd_set_irq_masks_t { + struct upisnd_cmd_t cmd; + struct upisnd_irq_mask_config_t config; +} __packed; + +struct upisnd_cmd_set_subscription_t { + struct upisnd_cmd_t cmd; + u8 on_and_irq_num; +} __packed; + +struct upisnd_cmd_result_t { + struct upisnd_cmd_t cmd; + s8 result; +} __packed; + +struct upisnd_cmd_get_t { + struct upisnd_cmd_t cmd; + u8 value_id; +} __packed; + +enum { UPISND_CMD_GET_RESPONSE_INT32_SIZE = sizeof(struct upisnd_cmd_get_t) + sizeof(int8_t) + + sizeof(int32_t) }; +enum { UPISND_CMD_GET_RESPONSE_DATA_MIN_SIZE = sizeof(struct upisnd_cmd_get_t) + sizeof(int8_t) }; + +struct upisnd_cmd_get_response_t { + struct upisnd_cmd_get_t cmd_get; + s8 result; + union { + s32 value; + u8 data[12]; + }; +} __packed; + +struct upisnd_cmd_set_t { + struct upisnd_cmd_t cmd; + u8 value_id; + s32 value; +} __packed; + +struct upisnd_cmd_control_event_t { + u8 cmd_and_size; + u16 values[7]; +} __packed; + +static inline enum upisnd_cmd_type_e upisnd_cmd_decode_type(u8 b) +{ + return (enum upisnd_cmd_type_e)(b & UPISND_CMD_TYPE_MASK); +} + +static inline u8 upisnd_cmd_decode_length(u8 b) +{ + return (b & UPISND_CMD_LENGTH_MASK) + 1u; +} + +static inline u8 upisnd_cmd_encode(enum upisnd_cmd_type_e type, u8 size) +{ + return (size < UPISND_MAX_PACKET_LENGTH && size > 0) ? (type | (size - 1)) : 0xff; +} + +static inline u8 upisnd_msg_id_encode(upisnd_msg_id_t msg_id, bool response) +{ + return (response ? 0x80 : 0x00) | (msg_id & UPISND_MSG_ID_MASK); +} + +static inline upisnd_msg_id_t upisnd_msg_id_decode_id(u8 b) +{ + return b & UPISND_MSG_ID_MASK; +} + +static inline void upisnd_cmd_prepare(struct upisnd_cmd_t *cmd, + enum upisnd_cmd_type_e type, + u8 size, + upisnd_msg_id_t msg_id, + bool response) +{ + cmd->cmd_and_size = upisnd_cmd_encode(type, size); + cmd->flags_and_msg_id = upisnd_msg_id_encode(msg_id, response); +} + +#define upisnd_cmd_matches(_cmd, type, size) \ + (upisnd_cmd_decode_type((_cmd)->cmd_and_size) == (type) && \ + upisnd_cmd_decode_length((_cmd)->cmd_and_size) >= (size)) + +static inline bool upisnd_cmd_type_has_msg_id(enum upisnd_cmd_type_e type) +{ + switch (type) { + default: + return false; + case UPISND_CMD_SETUP: + case UPISND_CMD_SET_IRQ_TYPES: + case UPISND_CMD_SET_IRQ_MASKS: + case UPISND_CMD_GET_GPIO: + case UPISND_CMD_GET_ALL_GPIOS: + case UPISND_CMD_RESULT: + case UPISND_CMD_GET: + case UPISND_CMD_SET: + return true; + } +} + +#endif // UPISND_PROTOCOL_H + +/* vim: set ts=8 sw=8 noexpandtab: */ --- /dev/null +++ b/sound/drivers/upisnd/upisnd_sound.c @@ -0,0 +1,277 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Pisound Micro Linux kernel module. + * Copyright (C) 2017-2025 Vilniaus Blokas UAB, https://blokas.io/ + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; version 2 of the + * License. + * + * 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 "upisnd_common.h" +#include "upisnd_codec.h" + +static void upisnd_proc_serial_show(struct snd_info_entry *entry, struct snd_info_buffer *buffer) +{ + struct upisnd_instance *instance = entry->private_data; + + snd_iprintf(buffer, "%s\n", instance->ctrl.serial); +} + +static void upisnd_proc_version_show(struct snd_info_entry *entry, struct snd_info_buffer *buffer) +{ + struct upisnd_instance *instance = entry->private_data; + + snd_iprintf(buffer, + "%u.%u.%u\n", + instance->ctrl.version.major, + instance->ctrl.version.minor, + instance->ctrl.version.build); +} + +static void upisnd_proc_hwrev_show(struct snd_info_entry *entry, struct snd_info_buffer *buffer) +{ + struct upisnd_instance *instance = entry->private_data; + + snd_iprintf(buffer, "%u\n", instance->ctrl.version.hwrev); +} + +static int upisnd_card_probe(struct snd_soc_card *card) +{ + struct upisnd_instance *instance = container_of(card, struct upisnd_instance, sound_card); + int err; + + err = upisnd_midi_init(instance); + + if (err < 0) { + printe("Failed to initialize MIDI subsystem! (%d)", err); + return err; + } + + err = snd_card_ro_proc_new(card->snd_card, "serial", instance, upisnd_proc_serial_show); + + if (err < 0) { + printe("Failed to create serial proc entry! (%d)", err); + return err; + } + + err = snd_card_ro_proc_new(card->snd_card, "version", instance, upisnd_proc_version_show); + + if (err < 0) { + printe("Failed to create version proc entry! (%d)", err); + return err; + } + + err = snd_card_ro_proc_new(card->snd_card, "hwrev", instance, upisnd_proc_hwrev_show); + + if (err < 0) { + printe("Failed to create hwrev proc entry! (%d)", err); + return err; + } + + return 0; +} + +static int upisnd_card_remove(struct snd_soc_card *card) +{ + struct upisnd_instance *instance = container_of(card, struct upisnd_instance, sound_card); + + upisnd_midi_uninit(instance); + return 0; +} + +static int upisnd_startup(struct snd_pcm_substream *substream) +{ + printd("startup"); + return 0; +} + +static int upisnd_hw_params(struct snd_pcm_substream *substream, struct snd_pcm_hw_params *params) +{ + printd("hw_params"); + + struct snd_soc_pcm_runtime *rtd = substream->private_data; + + struct snd_soc_dai *cpu_dai = snd_soc_rtd_to_cpu(rtd, 0); + int ret = 0; + + struct upisnd_instance *instance = container_of(rtd->card, + struct upisnd_instance, + sound_card); + + if (!(instance->flags & UPISND_FLAG_DUMMY)) + ret = snd_soc_dai_set_bclk_ratio(cpu_dai, 64); + + if (ret < 0) { + printe("Failed setting dai bclk ratio!\n"); + return ret; + } + + return 0; +} + +static int upisnd_dai_init(struct snd_soc_pcm_runtime *rtd) +{ + struct snd_soc_dai *codec_dai = snd_soc_rtd_to_codec(rtd, 0); + + return snd_soc_dai_set_tdm_slot(codec_dai, 0, 0, 0, 0); // Enable I2S mode. +} + +static const struct snd_soc_ops upisnd_ops = { + .startup = upisnd_startup, + .hw_params = upisnd_hw_params, +}; + +SND_SOC_DAILINK_DEFS(adau1961, + DAILINK_COMP_ARRAY(COMP_EMPTY()), + DAILINK_COMP_ARRAY(COMP_CODEC("adau1961", "adau-hifi")), + DAILINK_COMP_ARRAY(COMP_EMPTY())); + +static const struct snd_soc_dai_link upisnd_dai_link = { + .name = "pisound-micro", + .stream_name = "pisound-micro PCM", + .dai_fmt = + SND_SOC_DAIFMT_I2S | + SND_SOC_DAIFMT_NB_NF | + SND_SOC_DAIFMT_CBM_CFM, + .ops = &upisnd_ops, + .init = upisnd_dai_init, + SND_SOC_DAILINK_REG(adau1961), +}; + +static const struct snd_soc_card upisnd_sound_card = { + .name = "pisoundmicro", + .owner = THIS_MODULE, + .probe = upisnd_card_probe, + .remove = upisnd_card_remove, +}; + +int upisnd_sound_init(struct platform_device *pdev, struct upisnd_instance *instance) +{ + struct snd_soc_dai_link_component *comp = NULL; + struct device_node *i2s_node = NULL; + + memcpy(&instance->sound_card, &upisnd_sound_card, sizeof(upisnd_sound_card)); + memcpy(&instance->dai_link, &upisnd_dai_link, sizeof(upisnd_dai_link)); + + if (pdev->dev.of_node) { + of_property_read_string(pdev->dev.of_node, "card-name", &instance->sound_card.name); + i2s_node = of_parse_phandle(pdev->dev.of_node, "i2s-controller", 0); + if (!i2s_node) + printi("'i2s-controller' node not specified, will use dummy one instead!"); + } + + comp = devm_kzalloc(&pdev->dev, sizeof(*comp) * 3, GFP_KERNEL); + if (!comp) + return -ENOMEM; + + char *long_name = devm_kzalloc(&pdev->dev, 26, GFP_KERNEL); + + if (!long_name) + return -ENOMEM; + + instance->dai_link.cpus = &comp[0]; + instance->dai_link.codecs = &comp[1]; + instance->dai_link.platforms = &comp[2]; + + instance->dai_link.num_cpus = 1; + instance->dai_link.num_codecs = 1; + instance->dai_link.num_platforms = 1; + + if (i2s_node) { + instance->dai_link.cpus->of_node = i2s_node; + instance->dai_link.platforms->of_node = i2s_node; + } else { + printi("Setting up dummy interface."); + instance->dai_link.cpus->name = "snd-soc-dummy"; + instance->dai_link.platforms->name = "snd-soc-dummy"; + instance->dai_link.cpus->dai_name = "snd-soc-dummy-dai"; + instance->dai_link.platforms->dai_name = "snd-soc-dummy-dai"; + instance->dai_link.dai_fmt = (instance->dai_link.dai_fmt & ~SND_SOC_DAIFMT_CBM_CFM) + | SND_SOC_DAIFMT_CBS_CFS; + instance->flags |= UPISND_FLAG_DUMMY; + } + instance->dai_link.codecs->of_node = instance->codec_dev->of_node; + instance->dai_link.codecs->dai_name = "adau-hifi"; + instance->dai_link.stream_name = instance->ctrl.serial; + + instance->sound_card.dev = &pdev->dev; + + instance->sound_card.dai_link = &instance->dai_link; + instance->sound_card.num_links = 1; + + snprintf(long_name, 26, "Pisound Micro %s", instance->ctrl.serial); + instance->sound_card.long_name = long_name; + printd("About to register card %s", instance->sound_card.long_name); + + int err = snd_soc_register_card(&instance->sound_card); + + if (i2s_node) + of_node_put(i2s_node); + + if (err < 0) { + instance->sound_card.dev = NULL; + if (err != -EPROBE_DEFER) + printe("snd_soc_register_card failed with %d!", err); + return err; + } + + struct snd_soc_dai *dai = snd_soc_card_get_codec_dai(&instance->sound_card, "adau-hifi"); + + if (!dai) { + printe("Failed to get codec dai!"); + instance->sound_card.dev = NULL; + return -ENODEV; + } + + if (adau1961_is_hp_capless(dai->component)) { + err = upisnd_comm_set_subscription(instance, UPISND_IRQ_VGND_SHORT_ALERT, true); + if (err < 0) { + instance->sound_card.dev = NULL; + printe("Failed to subscribe to VGND short alert IRQ! (%d)", err); + return err; + } + } + + return 0; +} + +static void upisnd_sound_handle_irq_event(struct upisnd_instance *instance, + const struct irq_event_t *event) +{ + struct snd_soc_dai *dai = NULL; + + switch (event->num) { + case UPISND_IRQ_VGND_SHORT_ALERT: + dai = snd_soc_card_get_codec_dai(&instance->sound_card, "adau-hifi"); + printe("VGND short alert %s Headphone output!", + event->high ? "ON, muting" : "OFF, restoring last state"); + if (dai) + adau1961_set_vgnd_shorted(dai->component, event->high); + else + printe("Failed to get codec dai!"); + break; + default: + break; + } +} + +void upisnd_sound_handle_irq_events(struct upisnd_instance *instance, + const struct irq_event_t *events, + unsigned int n) +{ + unsigned int i; + + printd("Handling %u sound IRQ events", n); + + for (i = 0; i < n; ++i) + upisnd_sound_handle_irq_event(instance, &events[i]); +} + +/* vim: set ts=8 sw=8 noexpandtab: */ --- /dev/null +++ b/sound/drivers/upisnd/upisnd_sound.h @@ -0,0 +1,28 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Pisound Micro Linux kernel module. + * Copyright (C) 2017-2025 Vilniaus Blokas UAB, https://blokas.io/ + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; version 2 of the + * License. + * + * 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. + */ + +#ifndef UPISOUND_SOUND_H +#define UPISOUND_SOUND_H + +int upisnd_sound_init(struct platform_device *pdev, struct upisnd_instance *instance); + +void upisnd_sound_handle_irq_events(struct upisnd_instance *instance, + const struct irq_event_t *events, + unsigned int n); + +#endif // UPISOUND_SOUND_H + +/* vim: set ts=8 sw=8 noexpandtab: */ --- /dev/null +++ b/sound/drivers/upisnd/upisnd_sysfs.c @@ -0,0 +1,1728 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Pisound Micro Linux kernel module. + * Copyright (C) 2017-2025 Vilniaus Blokas UAB, https://blokas.io/ + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; version 2 of the + * License. + * + * 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 "upisnd_common.h" + +enum { + // Max lengths are with '\0' included. + MAX_ELEMENT_NAME_LENGTH = 64, + MAX_SETUP_REQUEST_LENGTH = 63 + MAX_ELEMENT_NAME_LENGTH, +}; + +static void upisnd_config_release(struct kobject *kobj); +static void upisnd_element_release(struct kobject *kobj); + +static ssize_t upisnd_attr_show(struct kobject *kobj, struct attribute *attr, char *buf) +{ + printd("hi %s %s", kobject_name(kobj), attr->name); + struct kobj_attribute *attribute = container_of(attr, struct kobj_attribute, attr); + + if (!attribute->show) + return -EIO; + + int err = attribute->show(kobj, attribute, buf); + + return err; +} + +static ssize_t upisnd_attr_store(struct kobject *kobj, + struct attribute *attr, + const char *buf, + size_t len) +{ + struct kobj_attribute *attribute = container_of(attr, struct kobj_attribute, attr); + + if (!attribute->store) + return -EIO; + + int err = attribute->store(kobj, attribute, buf, len); + + return err; +} + +static const struct sysfs_ops upisnd_attr_ops = { + .show = &upisnd_attr_show, + .store = &upisnd_attr_store +}; + +#define to_config(kobj_) container_of(kobj_, struct upisnd_config, kset.kobj) +struct upisnd_config { + struct kset kset; + struct upisnd_instance *instance; + struct kset *elements; +}; + +static int upisnd_element_cleanup(struct kobject *kobj, struct upisnd_instance *instance); + +static void upisnd_config_release(struct kobject *kobj) +{ + printd("hi"); + struct upisnd_config *cfg = to_config(kobj); + + kfree(cfg); +} + +static const struct kobj_type upisnd_config_type = { + .release = &upisnd_config_release, + .sysfs_ops = &upisnd_attr_ops +}; + +static const struct kobj_type upisnd_element_type = { + .release = &upisnd_element_release, + .sysfs_ops = &upisnd_attr_ops +}; + +// Used for handling encoder value overflow. +enum upisnd_value_mode { + UPISND_VALUE_MODE_CLAMP = 0, + UPISND_VALUE_MODE_WRAP, +}; + +#define to_element(kobj_) container_of(kobj_, struct upisnd_element, kobj) +struct upisnd_element { + struct kobject kobj; + // Mapped from upisnd pins to gpio pin numbering. + // gpio_pins[1] is used only for encoder B pin. + // Negative number is used to indicate invalid/unused pin. + int gpio_pins[2]; + int value; + int raw_value; + int input_min; + int input_max; + int value_low; + int value_high; + enum upisnd_value_mode value_mode; +}; + +// Returns true if the value changed. +static bool upisnd_element_update_value(struct upisnd_element *el) +{ + int old_value = el->value; + + int value = el->raw_value; + + switch (el->value_mode) { + case UPISND_VALUE_MODE_CLAMP: + value = min(max(value, el->input_min), el->input_max); + break; + case UPISND_VALUE_MODE_WRAP: + if (el->input_max != el->input_min) { + while (value < el->input_min) + value += el->input_max - el->input_min; + value = (value - el->input_min) % (el->input_max - el->input_min) + + el->input_min; + } else { + value = el->input_min; + } + break; + default: + return false; + } + + el->raw_value = value; + el->value = upisnd_map_value_range(value, el->input_min, el->input_max, el->value_low, + el->value_high); + + return el->value != old_value; +} + +static int upisnd_parse_gpio_dir(enum upisnd_pin_direction_e *dir, char **s, const char *sep) +{ + if (!dir || !s || !sep) + return -EINVAL; + + char *t = strsep(s, sep); + + if (!t || *t == '\0') + return -EINVAL; + + if (strncasecmp(t, "input", 6u) == 0) { + *dir = UPISND_PIN_DIR_INPUT; + return 0; + } else if (strncasecmp(t, "output", 7u) == 0) { + *dir = UPISND_PIN_DIR_OUTPUT; + return 0; + } + + return -EINVAL; +} + +static int upisnd_parse_gpio_input_pull(enum upisnd_pin_pull_e *pull, char **s, const char *sep) +{ + if (!pull || !s || !sep) + return -EINVAL; + + char *t = strsep(s, sep); + + if (!t || *t == '\0') + return -EINVAL; + + if (strncasecmp(t, "pull_up", 8u) == 0) { + *pull = UPISND_PIN_PULL_UP; + return 0; + } else if (strncasecmp(t, "pull_down", 10u) == 0) { + *pull = UPISND_PIN_PULL_DOWN; + return 0; + } else if (strncasecmp(t, "pull_none", 10u) == 0) { + *pull = UPISND_PIN_PULL_NONE; + return 0; + } + + return -EINVAL; +} + +static int upisnd_parse_gpio_output_level(bool *level, char **s, const char *sep) +{ + if (!level || !s || !sep) + return -EINVAL; + + char *t = strsep(s, sep); + + if (!t || *t == '\0') + return -EINVAL; + + return kstrtobool(t, level); +} + +static int upisnd_parse_pin(upisnd_pin_t *pin, char **s, const char *sep) +{ + if (!pin || !s || !sep) + return -EINVAL; + + char *t = strsep(s, sep); + + *pin = upisnd_name_to_pin(t); + + return upisnd_is_pin_valid(*pin) ? 0 : -EINVAL; +} + +static int upisnd_validate_setup(upisnd_setup_t setup) +{ + switch (upisnd_setup_get_element_type(setup)) { + case UPISND_ELEMENT_TYPE_ENCODER: + { + int a = upisnd_check_caps(upisnd_setup_get_pin_id(setup), UPISND_PIN_CAP_ENCODER); + int b = upisnd_check_caps(upisnd_setup_get_encoder_pin_b_id(setup), + UPISND_PIN_CAP_ENCODER); + + if (a < 0 || b < 0) { + printe("Invalid or incapable pins for Encoder (%d, %d)", a, b); + return -EINVAL; + } + return 0; + } + case UPISND_ELEMENT_TYPE_ANALOG_IN: + { + int err = upisnd_check_caps(upisnd_setup_get_pin_id(setup), + UPISND_PIN_CAP_ANALOG_IN); + + if (err < 0) { + printe("Invalid or incapable pin for Analog In (%d)", err); + return err; + } + return 0; + } + case UPISND_ELEMENT_TYPE_GPIO: + { + int err = upisnd_check_caps(upisnd_setup_get_pin_id(setup), UPISND_PIN_CAP_GPIO); + + if (err < 0) { + printe("Invalid or incapable pin for GPIO (%d)", err); + return err; + } + return 0; + } + case UPISND_ELEMENT_TYPE_ACTIVITY: + { + int err; + + switch (upisnd_setup_get_activity_type(setup)) { + case UPISND_ACTIVITY_TYPE_MIDI_IN: + case UPISND_ACTIVITY_TYPE_MIDI_OUT: + err = upisnd_check_caps(upisnd_setup_get_pin_id(setup), + UPISND_PIN_CAP_MIDI_ACTIVITY); + if (err < 0) { + printe("Invalid or incapable pin for MIDI Activity (%d)", err); + return err; + } + break; + default: + printe("Invalid activity type!"); + err = -EINVAL; + break; + } + + return err; + } + break; + case UPISND_ELEMENT_TYPE_NONE: + default: + return -EINVAL; + } +} + +static int upisnd_parse_setup(upisnd_setup_t *setup, const char *buf, size_t len) +{ + if (!setup || !buf || len + 1 >= MAX_SETUP_REQUEST_LENGTH) + return -EINVAL; + + *setup = 0; + upisnd_setup_set_element_type(setup, UPISND_ELEMENT_TYPE_NONE); + + char b[MAX_SETUP_REQUEST_LENGTH]; + + strscpy(b, buf, len + 1); + + static const char *const SEP = "\n\t "; + + char *s = b; + char *token = strsep(&s, SEP); + + if (!token || *token == '\0') + return -EINVAL; + + if (strncasecmp(token, "encoder", 8u) == 0) { + upisnd_setup_set_element_type(setup, UPISND_ELEMENT_TYPE_ENCODER); + } else if (strncasecmp(token, "analog_in", 10u) == 0) { + upisnd_setup_set_element_type(setup, UPISND_ELEMENT_TYPE_ANALOG_IN); + } else if (strncasecmp(token, "activity_", 9u) == 0) { + if (strncasecmp(&token[9], "midi_in", 8u) == 0) + upisnd_setup_set_activity_type(setup, UPISND_ACTIVITY_TYPE_MIDI_IN); + else if (strncasecmp(&token[9], "midi_out", 9u) == 0) + upisnd_setup_set_activity_type(setup, UPISND_ACTIVITY_TYPE_MIDI_OUT); + else + return -EINVAL; + upisnd_setup_set_element_type(setup, UPISND_ELEMENT_TYPE_ACTIVITY); + } else if (strncasecmp(token, "gpio", 5u) == 0) { + upisnd_setup_set_element_type(setup, UPISND_ELEMENT_TYPE_GPIO); + } else { + return -EINVAL; + } + + int err = 0; + + upisnd_pin_t pin; + enum upisnd_pin_direction_e dir; + enum upisnd_pin_pull_e pull; + bool output; + + switch (upisnd_setup_get_element_type(*setup)) { + case UPISND_ELEMENT_TYPE_ANALOG_IN: + case UPISND_ELEMENT_TYPE_ACTIVITY: + case UPISND_ELEMENT_TYPE_GPIO: + case UPISND_ELEMENT_TYPE_ENCODER: + err = upisnd_parse_pin(&pin, &s, SEP); + + if (err != 0) + break; + + upisnd_setup_set_pin_id(setup, pin); + + switch (upisnd_setup_get_element_type(*setup)) { + case UPISND_ELEMENT_TYPE_ENCODER: + err = upisnd_parse_gpio_input_pull(&pull, &s, SEP); + if (err != 0) + break; + upisnd_setup_set_gpio_pull(setup, pull); + + err = upisnd_parse_pin(&pin, &s, SEP); + if (err != 0) + break; + upisnd_setup_set_encoder_pin_b_id(setup, pin); + + err = upisnd_parse_gpio_input_pull(&pull, &s, SEP); + if (err == 0) + upisnd_setup_set_encoder_pin_b_pull(setup, pull); + break; + case UPISND_ELEMENT_TYPE_GPIO: + err = upisnd_parse_gpio_dir(&dir, &s, SEP); + if (err != 0) + break; + + upisnd_setup_set_gpio_dir(setup, dir); + + switch (dir) { + case UPISND_PIN_DIR_INPUT: + err = upisnd_parse_gpio_input_pull(&pull, &s, SEP); + if (err == 0) + upisnd_setup_set_gpio_pull(setup, pull); + break; + case UPISND_PIN_DIR_OUTPUT: + err = upisnd_parse_gpio_output_level(&output, &s, SEP); + if (err == 0) + upisnd_setup_set_gpio_output(setup, output); + break; + default: + return -EINVAL; + } + break; + default: + break; + } + break; + default: + return -EINVAL; + } + + if (err >= 0) + err = upisnd_validate_setup(*setup); + + return err; +} + +static ssize_t upisnd_element_type_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf); +static ssize_t upisnd_element_value_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf); +static ssize_t upisnd_element_value_store(struct kobject *kobj, struct kobj_attribute *attr, + const char *buf, size_t len); +static ssize_t upisnd_element_input_min_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf); +static ssize_t upisnd_element_input_min_store(struct kobject *kobj, struct kobj_attribute *attr, + const char *buf, size_t len); +static ssize_t upisnd_element_input_max_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf); +static ssize_t upisnd_element_input_max_store(struct kobject *kobj, struct kobj_attribute *attr, + const char *buf, size_t len); +static ssize_t upisnd_element_value_low_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf); +static ssize_t upisnd_element_value_low_store(struct kobject *kobj, struct kobj_attribute *attr, + const char *buf, size_t len); +static ssize_t upisnd_element_value_high_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf); +static ssize_t upisnd_element_value_high_store(struct kobject *kobj, struct kobj_attribute *attr, + const char *buf, size_t len); +static ssize_t upisnd_element_value_mode_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf); +static ssize_t upisnd_element_value_mode_store(struct kobject *kobj, struct kobj_attribute *attr, + const char *buf, size_t len); +static ssize_t upisnd_element_direction_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf); +static ssize_t upisnd_element_pin_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf); +static ssize_t upisnd_element_pin_name_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf); +static ssize_t upisnd_element_pin_b_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf); +static ssize_t upisnd_element_pin_b_name_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf); +static ssize_t upisnd_element_activity_type_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf); +static ssize_t upisnd_element_gpio_export(struct kobject *kobj, struct kobj_attribute *attr, + const char *buf, size_t len); +static ssize_t upisnd_element_gpio_unexport(struct kobject *kobj, struct kobj_attribute *attr, + const char *buf, size_t len); + +static struct kobj_attribute upisnd_element_type_attr = __ATTR(type, 0444, upisnd_element_type_show, + NULL); +static struct kobj_attribute upisnd_element_value_attr = __ATTR(value, 0664, + upisnd_element_value_show, upisnd_element_value_store); +static struct kobj_attribute upisnd_element_input_min_attr = __ATTR(input_min, 0664, + upisnd_element_input_min_show, upisnd_element_input_min_store); +static struct kobj_attribute upisnd_element_input_max_attr = __ATTR(input_max, 0664, + upisnd_element_input_max_show, upisnd_element_input_max_store); +static struct kobj_attribute upisnd_element_value_low_attr = __ATTR(value_low, 0664, + upisnd_element_value_low_show, upisnd_element_value_low_store); +static struct kobj_attribute upisnd_element_value_high_attr = __ATTR(value_high, 0664, + upisnd_element_value_high_show, upisnd_element_value_high_store); +static struct kobj_attribute upisnd_element_value_mode_attr = __ATTR(value_mode, 0664, + upisnd_element_value_mode_show, upisnd_element_value_mode_store); +static struct kobj_attribute upisnd_element_direction_attr = __ATTR(direction, 0444, + upisnd_element_direction_show, NULL); +static struct kobj_attribute upisnd_element_pin_attr = __ATTR(pin, 0444, upisnd_element_pin_show, + NULL); +static struct kobj_attribute upisnd_element_pin_name_attr = __ATTR(pin_name, 0444, + upisnd_element_pin_name_show, NULL); +static struct kobj_attribute upisnd_element_pin_b_attr = __ATTR(pin_b, 0444, + upisnd_element_pin_b_show, NULL); +static struct kobj_attribute upisnd_element_pin_b_name_attr = __ATTR(pin_b_name, 0444, + upisnd_element_pin_b_name_show, NULL); +static struct kobj_attribute upisnd_element_activity_type_attr = __ATTR(activity_type, 0444, + upisnd_element_activity_type_show, NULL); +static struct kobj_attribute upisnd_element_gpio_export_attr = __ATTR(gpio_export, 0220, NULL, + upisnd_element_gpio_export); +static struct kobj_attribute upisnd_element_gpio_unexport_attr = __ATTR(gpio_unexport, 0220, NULL, + upisnd_element_gpio_unexport); + +static struct attribute *upisnd_element_attrs[] = { + &upisnd_element_type_attr.attr, + &upisnd_element_value_attr.attr, + &upisnd_element_input_min_attr.attr, + &upisnd_element_input_max_attr.attr, + &upisnd_element_value_low_attr.attr, + &upisnd_element_value_high_attr.attr, + &upisnd_element_value_mode_attr.attr, + &upisnd_element_direction_attr.attr, + &upisnd_element_pin_attr.attr, + &upisnd_element_pin_name_attr.attr, + &upisnd_element_pin_b_attr.attr, + &upisnd_element_pin_b_name_attr.attr, + &upisnd_element_activity_type_attr.attr, + &upisnd_element_gpio_export_attr.attr, + &upisnd_element_gpio_unexport_attr.attr, + NULL +}; + +static umode_t upisnd_element_attr_is_visible(struct kobject *kobj, struct attribute *attr, int idx) +{ + printd("Hi!"); + + struct upisnd_element *element = to_element(kobj); + struct upisnd_config *cfg = to_config(kobj->parent->parent); + struct upisnd_instance *instance = cfg->instance; + + enum upisnd_element_type_e t = upisnd_setup_get_element_type(instance->gpio.pin_configs + [element->gpio_pins[0]].setup); + + switch (t) { + case UPISND_ELEMENT_TYPE_ENCODER: + if (attr == &upisnd_element_value_mode_attr.attr || + attr == &upisnd_element_pin_b_attr.attr || + attr == &upisnd_element_pin_b_name_attr.attr) + return attr->mode; + fallthrough; + case UPISND_ELEMENT_TYPE_ANALOG_IN: + if (attr == &upisnd_element_input_min_attr.attr || + attr == &upisnd_element_input_max_attr.attr || + attr == &upisnd_element_value_low_attr.attr || + attr == &upisnd_element_value_high_attr.attr) + return attr->mode; + break; + case UPISND_ELEMENT_TYPE_GPIO: + if (attr == &upisnd_element_gpio_export_attr.attr || + attr == &upisnd_element_gpio_unexport_attr.attr || + attr == &upisnd_element_direction_attr.attr) + return attr->mode; + break; + default: + break; + } + + switch (t) { + case UPISND_ELEMENT_TYPE_ANALOG_IN: + case UPISND_ELEMENT_TYPE_ENCODER: + case UPISND_ELEMENT_TYPE_GPIO: + if (attr == &upisnd_element_value_attr.attr) { + if (t == UPISND_ELEMENT_TYPE_GPIO) { + if (upisnd_setup_get_gpio_dir(instance->gpio.pin_configs + [element->gpio_pins[0]].setup) == UPISND_PIN_DIR_INPUT) + return 0444; + } + return attr->mode; + } + break; + case UPISND_ELEMENT_TYPE_ACTIVITY: + if (attr == &upisnd_element_activity_type_attr.attr) + return attr->mode; + break; + case UPISND_ELEMENT_TYPE_NONE: + default: + break; + } + + if (attr == &upisnd_element_type_attr.attr || + attr == &upisnd_element_pin_attr.attr || + attr == &upisnd_element_pin_name_attr.attr) + return attr->mode; + + return 0; +} + +static struct attribute_group upisnd_element_group = { + .attrs = upisnd_element_attrs, + .is_visible = &upisnd_element_attr_is_visible +}; + +static struct upisnd_element *upisnd_create_element(struct upisnd_config *config, const char *name) +{ + struct upisnd_element *el = kzalloc(sizeof(*el), GFP_KERNEL); + + if (!el) { + printe("Failed allocating upisnd_element!"); + return NULL; + } + + el->kobj.kset = config->elements; + el->gpio_pins[0] = UPISND_PIN_INVALID; + el->gpio_pins[1] = UPISND_PIN_INVALID; + + int err = kobject_init_and_add(&el->kobj, &upisnd_element_type, NULL, "%s", name); + + if (err < 0) { + printe("Failed initializing upisnd_element kobject! (err=%d, name='%s')", + err, name); + goto cleanup; + } + + return el; + +cleanup: + kobject_put(&el->kobj); + kfree(el); + return NULL; +} + +static void upisnd_element_release(struct kobject *kobj) +{ + printd("hi"); + struct upisnd_element *element = to_element(kobj); + + kfree(element); +} + +static int upisnd_setup_get_pins(int pins[2], upisnd_setup_t setup) +{ + pins[0] = UPISND_PIN_INVALID; + pins[1] = UPISND_PIN_INVALID; + + switch (upisnd_setup_get_element_type(setup)) { + case UPISND_ELEMENT_TYPE_ENCODER: + pins[1] = upisnd_setup_get_encoder_pin_b_id(setup); + if (!upisnd_is_pin_valid(pins[1])) + pins[1] = UPISND_PIN_INVALID; + fallthrough; + case UPISND_ELEMENT_TYPE_ANALOG_IN: + case UPISND_ELEMENT_TYPE_GPIO: + case UPISND_ELEMENT_TYPE_NONE: + case UPISND_ELEMENT_TYPE_ACTIVITY: + pins[0] = upisnd_setup_get_pin_id(setup); + if (!upisnd_is_pin_valid(pins[0])) + pins[0] = UPISND_PIN_INVALID; + break; + default: + return -EINVAL; + } + + return 0; +} + +static int upisnd_setup_do(struct upisnd_instance *instance, struct upisnd_element *element, + upisnd_setup_t setup, int pins[2]) +{ + int err; + + element->raw_value = 0; + element->value = 0; + element->input_min = 0; + element->value_low = 0; + element->input_max = upisnd_setup_get_element_type(setup) != + UPISND_ELEMENT_TYPE_ENCODER ? 1023 : 23; + element->value_high = element->input_max; + element->value_mode = UPISND_VALUE_MODE_CLAMP; + + int i; + + for (i = 0; i < 2; ++i) { + if (pins[i] == UPISND_PIN_INVALID) + break; + + int pin = pins[i]; + + printd("Check %d %08x %d %p %p", pin, instance->gpio.pin_configs[pin].setup, + upisnd_setup_get_element_type(instance->gpio.pin_configs[pin].setup), + instance->gpio.pin_configs[pin].element, element); + + if (upisnd_setup_get_element_type(instance->gpio.pin_configs[pin].setup) != + UPISND_ELEMENT_TYPE_NONE && instance->gpio.pin_configs[pin].element != + element) { + printe("Pin %s already used via %s%s!", + upisnd_pin_name(pin), + instance->gpio.pin_configs[pin].element ? "sysfs " : "gpio", + instance->gpio.pin_configs[pin].element ? + instance->gpio.pin_configs[pin].element->kobj.name : ""); + return -EBUSY; + } + } + + // Unsetup previous config if necessary. + for (i = 0; i < 2; ++i) { + if (element->gpio_pins[i] != UPISND_PIN_INVALID && + element->gpio_pins[i] != pins[i]) { + upisnd_setup_t s = instance->gpio.pin_configs[element->gpio_pins[i]].setup; + + upisnd_setup_set_element_type(&s, UPISND_ELEMENT_TYPE_NONE); + upisnd_gpio_setup(instance, s); + instance->gpio.pin_configs[element->gpio_pins[i]].setup = s; + instance->gpio.pin_subscription_refcounts[element->gpio_pins[i]] = 0; + } + } + + err = upisnd_gpio_setup(instance, setup); + + if (err < 0) + return err; + + instance->gpio.pin_configs[pins[0]].element = element; + instance->gpio.pin_configs[pins[0]].flags = 0; + instance->gpio.pin_configs[pins[0]].setup = setup; + instance->gpio.pin_configs[pins[0]].gpio_desc = NULL; + + element->gpio_pins[0] = pins[0]; + element->gpio_pins[1] = pins[1]; + + switch (upisnd_setup_get_element_type(setup)) { + case UPISND_ELEMENT_TYPE_ENCODER: + upisnd_gpio_set_subscription(instance, pins[0], true); + upisnd_gpio_set_subscription(instance, pins[1], true); + instance->gpio.pin_configs[pins[1]].element = element; + instance->gpio.pin_configs[pins[1]].flags = UPISND_PIN_FLAG_IS_ENCODER_B; + instance->gpio.pin_configs[pins[1]].setup = setup; + instance->gpio.pin_configs[pins[1]].gpio_desc = NULL; + break; + case UPISND_ELEMENT_TYPE_GPIO: + if (upisnd_setup_get_gpio_dir(setup) != UPISND_PIN_DIR_INPUT) + break; + fallthrough; + default: + upisnd_gpio_set_subscription(instance, pins[0], true); + break; + } + + return 0; +} + +static ssize_t upisnd_setup(struct kobject *kobj, struct kobj_attribute *attr, const char *buf, + size_t length) +{ + if (length + 1 >= MAX_SETUP_REQUEST_LENGTH) { + printe("Request exceeds maximum length!"); + return -EINVAL; + } + + int err; + + char b[MAX_SETUP_REQUEST_LENGTH]; + + strscpy(b, buf, length + 1); + + char *s = b; + char *name = strsep(&s, " /\n\t"); + + if (!name || *name == '\0' || !s || *s == '\0') + return -EINVAL; + + upisnd_setup_t setup; + + err = upisnd_parse_setup(&setup, s, length - (s - name)); + if (err < 0) { + printe("Failed parsing setup request! (%d)", err); + return err; + } + + int pins[2]; + + upisnd_setup_get_pins(pins, setup); + + struct upisnd_config *cfg = to_config(kobj); + + struct upisnd_instance *instance = cfg->instance; + + down_write(&instance->rw_gpio_config_sem); + + struct kobject *existing = kset_find_obj(cfg->elements, name); + + if (existing) { + bool setup_matches = false; + struct upisnd_element *el = to_element(existing); + + if (upisnd_is_pin_valid(el->gpio_pins[0])) + setup_matches = instance->gpio.pin_configs[el->gpio_pins[0]].setup == setup; + + kobject_put(existing); + up_write(&instance->rw_gpio_config_sem); + + if (setup_matches) { + printd("%s already existed and requested setup matched, returning success.", + name); + return length; + } + + printe("%s is already setup! (%d)", name, -EEXIST); + return -EEXIST; + } + + struct upisnd_element *element = upisnd_create_element(cfg, name); + + if (!element) { + up_write(&instance->rw_gpio_config_sem); + return -ENOMEM; + } + + err = upisnd_setup_do(instance, element, setup, pins); + + if (err >= 0) { + err = sysfs_create_group(&element->kobj, &upisnd_element_group); + if (err < 0) + printe + ("Failed creating pisound-micro element attributes! (err=%d, name ='%s')", + err, name); + } + + up_write(&instance->rw_gpio_config_sem); + + if (err < 0) { + kobject_put(&element->kobj); + return err; + } + + kobject_uevent(&element->kobj, KOBJ_ADD); + return length; +} + +static ssize_t upisnd_unsetup(struct kobject *kobj, struct kobj_attribute *attr, const char *buf, + size_t length) +{ + if (length + 1 >= MAX_ELEMENT_NAME_LENGTH) { + printe("Element name exceeds maximum length!"); + return -EINVAL; + } + + char b[MAX_ELEMENT_NAME_LENGTH]; + + strscpy(b, buf, length + 1); + + char *s = b; + char *name = strsep(&s, " /\n\t"); + + if (!name || *name == '\0') + return -EINVAL; + + struct upisnd_config *cfg = to_config(kobj); + + down_write(&cfg->instance->rw_gpio_config_sem); + + struct kobject *existing = kset_find_obj(cfg->elements, name); + + if (!existing) { + printe("%s not found!", name); + up_write(&cfg->instance->rw_gpio_config_sem); + return -ENOENT; + } + + upisnd_element_cleanup(existing, cfg->instance); + + printd("Found %p", existing); + kobject_put(existing); // kset_find_obj increments the + kobject_put(existing); // refcount, so we have to put twice. + + up_write(&cfg->instance->rw_gpio_config_sem); + + return length; +} + +static ssize_t upisnd_adc_gain_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf) +{ + struct upisnd_config *cfg = to_config(kobj); + + s32 adc_gain; + int err = upisnd_comm_get_value(cfg->instance, UPISND_VALUE_ADC_GAIN, &adc_gain); + + if (err < 0) { + printe("Failed getting ADC gain value! (%d)", err); + return err; + } + + return sprintf(buf, "%d\n", adc_gain); +} + +static ssize_t upisnd_adc_gain_store(struct kobject *kobj, struct kobj_attribute *attr, + const char *buf, size_t length) +{ + struct upisnd_config *cfg = to_config(kobj); + + int adc_gain; + int err = kstrtoint(buf, 10, &adc_gain); + + if (err < 0) { + printe("Failed parsing ADC gain! (%d)", err); + return err; + } + + err = upisnd_comm_set_value(cfg->instance, UPISND_VALUE_ADC_GAIN, adc_gain); + if (err < 0) { + printe("Failed setting ADC gain value! (%d)", err); + return err; + } + + return length; +} + +static ssize_t upisnd_adc_offset_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf) +{ + struct upisnd_config *cfg = to_config(kobj); + + s32 adc_offset; + int err = upisnd_comm_get_value(cfg->instance, UPISND_VALUE_ADC_OFFSET, &adc_offset); + + if (err < 0) { + printe("Failed getting ADC offset value! (%d)", err); + return err; + } + + return sprintf(buf, "%d\n", adc_offset); +} + +static ssize_t upisnd_adc_offset_store(struct kobject *kobj, struct kobj_attribute *attr, + const char *buf, size_t length) +{ + struct upisnd_config *cfg = to_config(kobj); + + int adc_offset; + int err = kstrtoint(buf, 10, &adc_offset); + + if (err < 0) { + printe("Failed parsing ADC offset! (%d)", err); + return err; + } + + err = upisnd_comm_set_value(cfg->instance, UPISND_VALUE_ADC_OFFSET, adc_offset); + if (err < 0) { + printe("Failed setting ADC offset value! (%d)", err); + return err; + } + + return length; +} + +static struct kobj_attribute upisnd_setup_attr = __ATTR(setup, 0220, NULL, upisnd_setup); +static struct kobj_attribute upisnd_unsetup_attr = __ATTR(unsetup, 0220, NULL, upisnd_unsetup); +static struct kobj_attribute upisnd_adc_gain_attr = __ATTR(adc_gain, 0664, upisnd_adc_gain_show, + upisnd_adc_gain_store); +static struct kobj_attribute upisnd_adc_offset_attr = __ATTR(adc_offset, 0664, + upisnd_adc_offset_show, + upisnd_adc_offset_store); + +static struct attribute *upisnd_root_attrs[] = { + &upisnd_setup_attr.attr, + &upisnd_unsetup_attr.attr, + &upisnd_adc_gain_attr.attr, + &upisnd_adc_offset_attr.attr, + NULL +}; + +static umode_t upisnd_root_is_visible(struct kobject *kobj, struct attribute *attr, int idx) +{ + struct upisnd_config *cfg = to_config(kobj); + struct upisnd_instance *instance = cfg->instance; + + if (!(instance->flags & UPISND_FLAG_ADC_CALIBRATION)) { + if (attr == &upisnd_adc_gain_attr.attr || attr == &upisnd_adc_offset_attr.attr) + return 0; + } + + return attr->mode; +} + +static const struct attribute_group upisnd_root_group = { + .attrs = upisnd_root_attrs, + .is_visible = &upisnd_root_is_visible +}; + +static struct upisnd_config *upisnd_create_config(struct upisnd_instance *instance, + const char *name) +{ + struct upisnd_config *cfg = kzalloc(sizeof(*cfg), GFP_KERNEL); + + if (!cfg) { + printe("Failed allocating upisnd_config!"); + return NULL; + } + + printd("cfg=%p", cfg); + + cfg->instance = instance; + + int err = kobject_set_name(&cfg->kset.kobj, name); + + if (err < 0) { + printe("Failed setting config kset name! (%d)", err); + goto cleanup; + } + + cfg->kset.kobj.ktype = &upisnd_config_type; + err = kset_register(&cfg->kset); + + if (err < 0) { + printe("Failed registering config kset! (%d)", err); + goto cleanup; + } + + cfg->kset.kobj.kset = &cfg->kset; + + cfg->elements = kset_create_and_add("elements", NULL, &cfg->kset.kobj); + if (!cfg->elements) { + printe("Failed creating elements kset!"); + goto cleanup; + } + + err = sysfs_create_group(&cfg->kset.kobj, &upisnd_root_group); + if (err < 0) { + printe("Failed creating pisound-micro attributes! (%d)", err); + goto cleanup; + } + + err = kobject_uevent(&cfg->kset.kobj, KOBJ_ADD); + printd("kobject_uevent(&cfg->kobj, KOBJ_ADD) = %d", err); + + return cfg; + +cleanup: + if (cfg->elements) { + kset_unregister(cfg->elements); + cfg->elements = NULL; + } + + kset_unregister(&cfg->kset); + return NULL; +} + +static ssize_t upisnd_element_gpio_export(struct kobject *kobj, + struct kobj_attribute *attr, + const char *buf, + size_t len) +{ + if (!kobj || !kobj->parent || !kobj->parent->parent) + return -EINVAL; + + struct upisnd_element *element = to_element(kobj); + struct upisnd_config *cfg = to_config(kobj->parent->parent); + struct upisnd_instance *instance = cfg->instance; + + struct gpio_desc *desc = NULL; + + int pin = element->gpio_pins[0]; + + if (pin == UPISND_PIN_INVALID) { + printe("Element %s is not set up yet!", kobj->name); + return -EINVAL; + } + + down_write(&instance->rw_gpio_config_sem); + if (upisnd_setup_get_element_type(instance->gpio.pin_configs[pin].setup) != + UPISND_ELEMENT_TYPE_GPIO) { + printe("Element %s is not set up as a GPIO!", kobj->name); + up_write(&instance->rw_gpio_config_sem); + return -EINVAL; + } + + instance->gpio.pin_configs[pin].flags |= UPISND_PIN_FLAG_CONFIGURING_THROUGH_SYSFS; + up_write(&instance->rw_gpio_config_sem); + + int err = 0; + + printd("Requesting own %d", pin); + desc = gpiochip_request_own_desc(&instance->gpio.gpio_chip, pin, kobj->name, 0, 0); + if (IS_ERR(desc)) { + printe("Failed requesting own GPIO desc for pin %d! (%ld)", + upisnd_setup_get_pin_id(instance->gpio.pin_configs[pin].setup), + PTR_ERR(desc)); + err = PTR_ERR(desc); + desc = NULL; + } + + if (err >= 0) { + err = gpiod_export(desc, true); + + if (err < 0) { + printe("Failed exporting GPIO via sysfs for pin %d! (%d)", + upisnd_setup_get_pin_id(instance->gpio.pin_configs[pin].setup), err); + gpiochip_free_own_desc(desc); + desc = NULL; + } + } + + printd("Result: %p %d", desc, err); + + down_write(&instance->rw_gpio_config_sem); + instance->gpio.pin_configs[pin].flags = 0; + instance->gpio.pin_configs[pin].gpio_desc = desc; + up_write(&instance->rw_gpio_config_sem); + + return err >= 0 ? len : err; +} + +static ssize_t upisnd_element_gpio_unexport(struct kobject *kobj, struct kobj_attribute *attr, + const char *buf, size_t len) +{ + if (!kobj || !kobj->parent || !kobj->parent->parent) + return -EINVAL; + + struct upisnd_element *element = to_element(kobj); + struct upisnd_config *cfg = to_config(kobj->parent->parent); + struct upisnd_instance *instance = cfg->instance; + + int pin = element->gpio_pins[0]; + + if (pin == UPISND_PIN_INVALID) { + printe("Element %s is not set up yet!", kobj->name); + return -EINVAL; + } + + down_write(&instance->rw_gpio_config_sem); + struct gpio_desc *desc = instance->gpio.pin_configs[pin].gpio_desc; + + if (!desc) { + up_write(&instance->rw_gpio_config_sem); + printe("Element %s is not exported via gpio sysfs!", kobj->name); + return -EINVAL; + } + + instance->gpio.pin_configs[pin].flags |= UPISND_PIN_FLAG_CONFIGURING_THROUGH_SYSFS; + up_write(&instance->rw_gpio_config_sem); + + printd("Freeing own %d", pin); + gpiochip_free_own_desc(desc); + + down_write(&instance->rw_gpio_config_sem); + instance->gpio.pin_configs[pin].flags &= ~UPISND_PIN_FLAG_CONFIGURING_THROUGH_SYSFS; + instance->gpio.pin_configs[pin].gpio_desc = NULL; + up_write(&instance->rw_gpio_config_sem); + + return len; +} + +static int upisnd_element_cleanup(struct kobject *kobj, struct upisnd_instance *instance) +{ + printd("(%p)", kobj); + + struct upisnd_element *element = to_element(kobj); + + if (element->gpio_pins[0] == UPISND_PIN_INVALID) + return 0; + + upisnd_setup_t setup = instance->gpio.pin_configs[element->gpio_pins[0]].setup; + + if (upisnd_setup_get_element_type(setup) == UPISND_ELEMENT_TYPE_NONE) { + printe("Pin %d is assigned, but the '%s' element is not set up", + element->gpio_pins[0], kobj->name); + return 0; + } + + if (instance->gpio.pin_configs[element->gpio_pins[0]].gpio_desc) { + printd("Freeing own desc"); + struct gpio_desc *desc = instance->gpio.pin_configs[element->gpio_pins[0]] + .gpio_desc; + + instance->gpio.pin_configs[element->gpio_pins[0]].flags |= + UPISND_PIN_FLAG_CONFIGURING_THROUGH_SYSFS; + up_write(&instance->rw_gpio_config_sem); + gpiochip_free_own_desc(desc); + down_write(&instance->rw_gpio_config_sem); + instance->gpio.pin_configs[element->gpio_pins[0]].flags &= + ~UPISND_PIN_FLAG_CONFIGURING_THROUGH_SYSFS; + printd("Freed"); + } + + upisnd_setup_set_element_type(&setup, UPISND_ELEMENT_TYPE_NONE); + int err = upisnd_gpio_setup(instance, setup); + + int i; + + for (i = 0; i < ARRAY_SIZE(element->gpio_pins); ++i) { + if (element->gpio_pins[i] == UPISND_PIN_INVALID) + break; + + memset(&instance->gpio.pin_configs[element->gpio_pins[i]], 0, + sizeof(struct upisnd_pin_config_t)); + instance->gpio.pin_subscription_refcounts[element->gpio_pins[i]] = 0; + + element->gpio_pins[i] = UPISND_PIN_INVALID; + } + + return err; +} + +static ssize_t upisnd_element_type_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + const char *type = NULL; + struct upisnd_element *element = to_element(kobj); + struct upisnd_config *cfg = to_config(kobj->parent->parent); + struct upisnd_instance *instance = cfg->instance; + + enum upisnd_element_type_e t = UPISND_ELEMENT_TYPE_NONE; + + if (element->gpio_pins[0] != UPISND_PIN_INVALID) { + t = upisnd_setup_get_element_type(instance->gpio.pin_configs[element->gpio_pins[0]] + .setup); + } + + switch (t) { + case UPISND_ELEMENT_TYPE_ENCODER: + type = "encoder"; + break; + case UPISND_ELEMENT_TYPE_ANALOG_IN: + type = "analog_in"; + break; + case UPISND_ELEMENT_TYPE_GPIO: + type = "gpio"; + break; + case UPISND_ELEMENT_TYPE_ACTIVITY: + type = "activity"; + break; + case UPISND_ELEMENT_TYPE_NONE: + type = "none"; + break; + default: + type = "unknown"; + break; + } + + return sprintf(buf, "%s\n", type); +} + +static ssize_t upisnd_element_value_show(struct kobject *kobj, + struct kobj_attribute *attr, + char *buf) +{ + struct upisnd_element *element = to_element(kobj); + struct upisnd_config *cfg = to_config(kobj->parent->parent); + struct upisnd_instance *instance = cfg->instance; + + enum upisnd_element_type_e t = UPISND_ELEMENT_TYPE_NONE; + int pin = UPISND_PIN_INVALID; + + if (element->gpio_pins[0] != UPISND_PIN_INVALID) { + t = upisnd_setup_get_element_type(instance->gpio.pin_configs[element->gpio_pins[0]] + .setup); + pin = element->gpio_pins[0]; + } + + switch (t) { + case UPISND_ELEMENT_TYPE_ANALOG_IN: + case UPISND_ELEMENT_TYPE_ENCODER: + return sprintf(buf, "%d\n", element->value); + case UPISND_ELEMENT_TYPE_GPIO: + return sprintf(buf, "%d\n", upisnd_gpio_get(instance, pin)); + case UPISND_ELEMENT_TYPE_ACTIVITY: + case UPISND_ELEMENT_TYPE_NONE: + default: + return 0; + } +} + +static ssize_t upisnd_element_value_store(struct kobject *kobj, struct kobj_attribute *attr, + const char *buf, size_t len) +{ + if (!kobj || !kobj->parent || !kobj->parent->parent) + return -EINVAL; + + struct upisnd_element *element = to_element(kobj); + struct upisnd_config *cfg = to_config(kobj->parent->parent); + struct upisnd_instance *instance = cfg->instance; + + printd("%p %p", kobj, cfg); + + enum upisnd_element_type_e t = UPISND_ELEMENT_TYPE_NONE; + int pin = UPISND_PIN_INVALID; + + if (element->gpio_pins[0] != UPISND_PIN_INVALID) { + t = upisnd_setup_get_element_type(instance->gpio.pin_configs[element->gpio_pins[0]] + .setup); + pin = element->gpio_pins[0]; + } + + int value, err; + bool b; + + bool changed = false; + + switch (t) { + case UPISND_ELEMENT_TYPE_NONE: + return -EINVAL; + case UPISND_ELEMENT_TYPE_GPIO: + err = kstrtobool(buf, &b); + if (err < 0) { + printe("Failed parsing the provided boolean! (%d)", err); + return err; + } + changed = b != upisnd_gpio_get(instance, pin); + upisnd_gpio_set(instance, pin, b); + break; + case UPISND_ELEMENT_TYPE_ENCODER: + case UPISND_ELEMENT_TYPE_ANALOG_IN: + err = kstrtoint(buf, 0, &value); + if (err < 0) { + printe("Failed parsing the provided integer! (%d)", err); + return err; + } + down_write(&instance->rw_gpio_config_sem); + element->raw_value = upisnd_unmap_value_range(value, element->input_min, + element->input_max, + element->value_low, + element->value_high); + changed = upisnd_element_update_value(element); + up_write(&instance->rw_gpio_config_sem); + break; + case UPISND_ELEMENT_TYPE_ACTIVITY: + return -EINVAL; + } + + if (changed) + sysfs_notify(kobj, NULL, "value"); + + return len; +} + +static ssize_t upisnd_element_input_min_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + struct upisnd_element *element = to_element(kobj); + + return sprintf(buf, "%d\n", element->input_min); +} + +static ssize_t upisnd_element_store_value_int(struct upisnd_element *element, int *value, + const char *buf, size_t len) +{ + int v; + int err = kstrtoint(buf, 0, &v); + + if (err < 0) { + printe("Failed parsing the provided integer! (%d)", err); + return err; + } + + struct upisnd_config *cfg = to_config(element->kobj.parent->parent); + struct upisnd_instance *instance = cfg->instance; + + down_write(&instance->rw_gpio_config_sem); + *value = v; + bool changed = upisnd_element_update_value(element); + + up_write(&instance->rw_gpio_config_sem); + + if (changed) + sysfs_notify(&element->kobj, NULL, "value"); + + return len; +} + +static ssize_t upisnd_element_input_min_store(struct kobject *kobj, struct kobj_attribute *attr, + const char *buf, size_t len) +{ + if (!kobj || !kobj->parent || !kobj->parent->parent) + return -EINVAL; + + struct upisnd_element *element = to_element(kobj); + int result = upisnd_element_store_value_int(element, &element->input_min, buf, len); + + if (result >= 0) { + if (element->input_min > element->input_max) { + int t = element->input_min; + + element->input_min = element->input_max; + element->input_max = t; + } + } + return result; +} + +static ssize_t upisnd_element_input_max_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + if (!kobj || !kobj->parent || !kobj->parent->parent) + return -EINVAL; + + struct upisnd_element *element = to_element(kobj); + + return sprintf(buf, "%d\n", element->input_max); +} + +static ssize_t upisnd_element_input_max_store(struct kobject *kobj, struct kobj_attribute *attr, + const char *buf, size_t len) +{ + if (!kobj || !kobj->parent || !kobj->parent->parent) + return -EINVAL; + + struct upisnd_element *element = to_element(kobj); + int result = upisnd_element_store_value_int(element, &element->input_max, buf, len); + + if (result >= 0) { + if (element->input_min > element->input_max) { + int t = element->input_min; + + element->input_min = element->input_max; + element->input_max = t; + } + } + return result; +} + +static ssize_t upisnd_element_value_low_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + if (!kobj || !kobj->parent || !kobj->parent->parent) + return -EINVAL; + + struct upisnd_element *element = to_element(kobj); + + return sprintf(buf, "%d\n", element->value_low); +} + +static ssize_t upisnd_element_value_low_store(struct kobject *kobj, struct kobj_attribute *attr, + const char *buf, size_t len) +{ + if (!kobj || !kobj->parent || !kobj->parent->parent) + return -EINVAL; + + struct upisnd_element *element = to_element(kobj); + + return upisnd_element_store_value_int(element, &element->value_low, buf, len); +} + +static ssize_t upisnd_element_value_high_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + if (!kobj || !kobj->parent || !kobj->parent->parent) + return -EINVAL; + + struct upisnd_element *element = to_element(kobj); + + return sprintf(buf, "%d\n", element->value_high); +} + +static ssize_t upisnd_element_value_high_store(struct kobject *kobj, struct kobj_attribute *attr, + const char *buf, size_t len) +{ + if (!kobj || !kobj->parent || !kobj->parent->parent) + return -EINVAL; + + struct upisnd_element *element = to_element(kobj); + + return upisnd_element_store_value_int(element, &element->value_high, buf, len); +} + +static ssize_t upisnd_element_value_mode_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + if (!kobj || !kobj->parent || !kobj->parent->parent) + return -EINVAL; + + struct upisnd_element *element = to_element(kobj); + + const char *s; + + switch (element->value_mode) { + case UPISND_VALUE_MODE_CLAMP: + s = "clamp"; + break; + case UPISND_VALUE_MODE_WRAP: + s = "wrap"; + break; + default: + return -EINVAL; + } + + return sprintf(buf, "%s\n", s); +} + +static ssize_t upisnd_element_value_mode_store(struct kobject *kobj, struct kobj_attribute *attr, + const char *buf, size_t len) +{ + if (!kobj || !kobj->parent || !kobj->parent->parent) + return -EINVAL; + + static const char *const SEP = "\n\t "; + + char b[8]; + + strscpy(b, buf, min(len + 1, sizeof(b))); + + char *s = b; + char *token = strsep(&s, SEP); + + if (!token || *token == '\0') + return -EINVAL; + + enum upisnd_value_mode mode; + + if (strncasecmp(token, "clamp", 6u) == 0) + mode = UPISND_VALUE_MODE_CLAMP; + else if (strncasecmp(token, "wrap", 5u) == 0) + mode = UPISND_VALUE_MODE_WRAP; + else + return -EINVAL; + + struct upisnd_element *element = to_element(kobj); + struct upisnd_config *cfg = to_config(element->kobj.parent->parent); + struct upisnd_instance *instance = cfg->instance; + + down_write(&instance->rw_gpio_config_sem); + element->value_mode = mode; + bool changed = upisnd_element_update_value(element); + + up_write(&instance->rw_gpio_config_sem); + + if (changed) + sysfs_notify(&element->kobj, NULL, "value"); + + return len; +} + +static ssize_t upisnd_element_direction_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + struct upisnd_element *element = to_element(kobj); + struct upisnd_config *cfg = to_config(kobj->parent->parent); + struct upisnd_instance *instance = cfg->instance; + + enum upisnd_element_type_e t = UPISND_ELEMENT_TYPE_NONE; + int pin = UPISND_PIN_INVALID; + + if (element->gpio_pins[0] != UPISND_PIN_INVALID) { + t = upisnd_setup_get_element_type(instance->gpio.pin_configs[element->gpio_pins[0]] + .setup); + pin = element->gpio_pins[0]; + } + + switch (t) { + case UPISND_ELEMENT_TYPE_ENCODER: + return sprintf(buf, "in\n"); + case UPISND_ELEMENT_TYPE_GPIO: + return sprintf(buf, upisnd_gpio_get_direction(instance, pin) == 0 ? + "out\n" : "in\n"); + case UPISND_ELEMENT_TYPE_ACTIVITY: + return sprintf(buf, "out\n"); + case UPISND_ELEMENT_TYPE_NONE: + default: + return 0; + } +} + +static ssize_t upisnd_element_pin_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf) +{ + struct upisnd_element *element = to_element(kobj); + + if (element->gpio_pins[0] != UPISND_PIN_INVALID) + return sprintf(buf, "%d\n", element->gpio_pins[0]); + + return -EINVAL; +} + +static ssize_t upisnd_element_pin_name_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + struct upisnd_element *element = to_element(kobj); + + if (element->gpio_pins[0] != UPISND_PIN_INVALID) + return sprintf(buf, "%s\n", upisnd_pin_name(element->gpio_pins[0])); + + return -EINVAL; +} + +static ssize_t upisnd_element_pin_b_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + struct upisnd_element *element = to_element(kobj); + + if (element->gpio_pins[1] != UPISND_PIN_INVALID) + return sprintf(buf, "%d\n", element->gpio_pins[1]); + + return -EINVAL; +} + +static ssize_t upisnd_element_pin_b_name_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + struct upisnd_element *element = to_element(kobj); + + if (element->gpio_pins[1] != UPISND_PIN_INVALID) + return sprintf(buf, "%s\n", upisnd_pin_name(element->gpio_pins[1])); + + return -EINVAL; +} + +static ssize_t upisnd_element_activity_type_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + struct upisnd_element *element = to_element(kobj); + + struct upisnd_config *cfg = to_config(kobj->parent->parent); + struct upisnd_instance *instance = cfg->instance; + + enum upisnd_element_type_e t = UPISND_ELEMENT_TYPE_NONE; + + if (element->gpio_pins[0] != UPISND_PIN_INVALID) + t = upisnd_setup_get_element_type(instance->gpio.pin_configs[element->gpio_pins[0]] + .setup); + + if (t != UPISND_ELEMENT_TYPE_ACTIVITY) + return -EINVAL; + + switch (upisnd_setup_get_activity_type(instance->gpio.pin_configs[element->gpio_pins[0]] + .setup)) { + case UPISND_ACTIVITY_TYPE_MIDI_IN: + return sprintf(buf, "midi_in\n"); + case UPISND_ACTIVITY_TYPE_MIDI_OUT: + return sprintf(buf, "midi_out\n"); + default: + return 0; + } +} + +static void upisnd_sysfs_ctrl_event_handler(struct work_struct *work); + +int upisnd_sysfs_init(struct upisnd_instance *instance, const char *name) +{ + instance->config = upisnd_create_config(instance, name ? name : "pisound-micro"); + if (!instance->config) + return -ENOMEM; + + INIT_KFIFO(instance->ctrl_event_fifo); + INIT_WORK(&instance->ctrl_event_handler, upisnd_sysfs_ctrl_event_handler); + + return 0; +} + +void upisnd_sysfs_uninit(struct upisnd_instance *instance) +{ + unsigned int i; + + for (i = 0; i < ARRAY_SIZE(instance->gpio.pin_configs); ++i) { + if (instance->gpio.pin_configs[i].gpio_desc) { + gpiochip_free_own_desc(instance->gpio.pin_configs[i].gpio_desc); + instance->gpio.pin_configs[i].gpio_desc = NULL; + } + } + + kfifo_free(&instance->ctrl_event_fifo); + + if (instance->config) { + struct kobject *k, *t; + + if (instance->config->elements) { + list_for_each_entry_safe(k, t, &instance->config->elements->list, entry) { + printd("Putting element %p", k); + kobject_put(k); + } + + printd("Unregistering elements set"); + kset_unregister(instance->config->elements); + instance->config->elements = NULL; + } + + instance->config->kset.kobj.kset = NULL; + kset_unregister(&instance->config->kset); + instance->config = NULL; + } +} + +static void upisnd_sysfs_ctrl_event_handler(struct work_struct *work) +{ + printd("ctrl event handler"); + struct upisnd_instance *instance = container_of(work, struct upisnd_instance, + ctrl_event_handler); + struct kobject *objs[UPISND_NUM_GPIOS]; + + memset(objs, 0, sizeof(objs)); + + unsigned int i, j = 0; + int n; + struct control_event_t events[32]; + + down_write(&instance->rw_gpio_config_sem); + while ((n = kfifo_out(&instance->ctrl_event_fifo, events, 32)) > 0) { + printd("Got %d events", n); + for (i = 0; i < n; ++i) { + upisnd_pin_t pin = events[i].pin; + + if (!upisnd_is_pin_valid(pin)) { + printe("Received invalid pin (%d), ignoring", pin); + continue; + } + + struct upisnd_element *element = instance->gpio.pin_configs[pin].element; + + if (element) { + switch (upisnd_setup_get_element_type(instance->gpio.pin_configs + [pin].setup)) { + case UPISND_ELEMENT_TYPE_ENCODER: + element->raw_value += (int16_t)events[i].raw_value; + printd("Encoder %s, old raw: %d, new raw: %d", + element->kobj.name, + element->raw_value - (int16_t)events[i].raw_value, + element->raw_value); + break; + case UPISND_ELEMENT_TYPE_ANALOG_IN: + element->raw_value = (uint16_t)(events[i].raw_value + & 0x03ff); + printd("Analog in %s, raw: %d", element->kobj.name, + element->raw_value); + break; + case UPISND_ELEMENT_TYPE_GPIO: + if (upisnd_setup_get_gpio_dir(instance->gpio + .pin_configs[pin].setup) == UPISND_PIN_DIR_INPUT) { + element->raw_value = (uint16_t)events[i].raw_value; + printd("Button %s, raw: %d", element->kobj.name, + element->raw_value); + break; + } + break; + default: + printe + ("Got control event for non gpio, enc or analog element %s!" + , element->kobj.name); + continue; + } + + objs[pin] = &element->kobj; + } + } + } + for (i = 0; i < ARRAY_SIZE(objs); ++i) { + if (objs[i] && upisnd_element_update_value(to_element(objs[i]))) + objs[j++] = kobject_get(objs[i]); + } + up_write(&instance->rw_gpio_config_sem); + + for (i = 0; i < j; ++i) { + printd("Notify %d %p", i, objs[i]); + sysfs_notify(objs[i], NULL, "value"); + kobject_put(objs[i]); + } + + if (!kfifo_is_empty(&instance->ctrl_event_fifo) && + !work_pending(&instance->ctrl_event_handler)) + queue_work(instance->work_queue, &instance->ctrl_event_handler); +} + +void upisnd_sysfs_handle_irq_event(struct upisnd_instance *instance, + const struct irq_event_t *events, unsigned int n) +{ + printd("Converting and pushing %u events", n); + struct control_event_t cev; + unsigned int i; + + for (i = 0; i < n; ++i) { + cev.pin = events[i].num; + cev.raw_value = events[i].high ? 1 : 0; + kfifo_put(&instance->ctrl_event_fifo, cev); + } + if (!work_pending(&instance->ctrl_event_handler)) + queue_work(instance->work_queue, &instance->ctrl_event_handler); +} + +void upisnd_sysfs_handle_control_event(struct upisnd_instance *instance, + const struct control_event_t *events, unsigned int n) +{ + printd("Pushing %u events", n); + kfifo_in(&instance->ctrl_event_fifo, events, n); + if (!work_pending(&instance->ctrl_event_handler)) + queue_work(instance->work_queue, &instance->ctrl_event_handler); +} + +/* vim: set ts=8 sw=8 noexpandtab: */ --- /dev/null +++ b/sound/drivers/upisnd/upisnd_sysfs.h @@ -0,0 +1,32 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Pisound Micro Linux kernel module. + * Copyright (C) 2017-2025 Vilniaus Blokas UAB, https://blokas.io/ + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; version 2 of the + * License. + * + * 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. + */ + +#ifndef UPISOUND_SYSFS_H +#define UPISOUND_SYSFS_H + +int upisnd_sysfs_init(struct upisnd_instance *instance, const char *name); +void upisnd_sysfs_uninit(struct upisnd_instance *instance); + +void upisnd_sysfs_handle_irq_event(struct upisnd_instance *instance, + const struct irq_event_t *events, + unsigned int n); +void upisnd_sysfs_handle_control_event(struct upisnd_instance *instance, + const struct control_event_t *events, + unsigned int n); + +#endif // UPISOUND_SYSFS_H + +/* vim: set ts=8 sw=8 noexpandtab: */ --- /dev/null +++ b/sound/drivers/upisnd/upisnd_utils.c @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Pisound Micro Linux kernel module. + * Copyright (C) 2017-2025 Vilniaus Blokas UAB, https://blokas.io/ + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; version 2 of the + * License. + * + * 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 "upisnd_common.h" + +int upisnd_map_value_range(int v, int input_min, int input_max, int output_min, int output_max) +{ + int x = (v - input_min) * (output_max - output_min); + + return x / (input_max - input_min) + output_min; +} + +int upisnd_unmap_value_range(int v, int input_min, int input_max, int output_min, int output_max) +{ + int x = (v - output_min) * (input_max - input_min); + + return x / (output_max - output_min) + input_min; +} + +/* vim: set ts=8 sw=8 noexpandtab: */ --- /dev/null +++ b/sound/drivers/upisnd/upisnd_utils.h @@ -0,0 +1,25 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Pisound Micro Linux kernel module. + * Copyright (C) 2017-2025 Vilniaus Blokas UAB, https://blokas.io/ + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; version 2 of the + * License. + * + * 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. + */ + +#ifndef UPISOUND_UTILS_H +#define UPISOUND_UTILS_H + +int upisnd_map_value_range(int v, int input_min, int input_max, int output_min, int output_max); +int upisnd_unmap_value_range(int v, int input_min, int input_max, int output_min, int output_max); + +#endif // UPISOUND_UTILS_H + +/* vim: set ts=8 sw=8 noexpandtab: */