difos/target/linux/bcm27xx/patches-6.12/950-0909-Adding-Pisound-Micro-kernel-module.patch
Álvaro Fernández Rojas 8f9e91ad03 bcm27xx: add 6.12 patches from RPi repo
These patches were generated from:
https://github.com/raspberrypi/linux/commits/rpi-6.12.y
With the following command:
git format-patch -N v6.12.27..HEAD
(HEAD -> 8d3206ee456a5ecdf9ddbfd8e5e231e4f0cd716e)

Exceptions:
- (def)configs patches
- github workflows patches
- applied & reverted patches
- readme patches
- wireless patches

Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com>
2025-05-21 11:32:18 +02:00

7329 lines
217 KiB
Diff

From 8ab68f8c2cf6828cac9674eec4150eff777bf39d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Giedrius=20Trainavi=C4=8Dius?= <giedrius@blokas.io>
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 <giedrius@blokas.io>
---
.../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 <linux/i2c.h>
+#include <linux/mod_devicetable.h>
+#include <linux/module.h>
+#include <linux/regmap.h>
+#include <linux/clk.h>
+#include <linux/device.h>
+#include <linux/regmap.h>
+#include <linux/list.h>
+#include <linux/gcd.h>
+
+#include <sound/core.h>
+#include <sound/pcm.h>
+#include <sound/pcm_params.h>
+#include <sound/soc.h>
+#include <sound/tlv.h>
+
+#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 <giedrius@blokas.io>");
+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 <linux/init.h>
+#include <linux/mutex.h>
+#include <linux/rwsem.h>
+#include <linux/module.h>
+#include <linux/input.h>
+#include <linux/i2c.h>
+#include <linux/slab.h>
+#include <linux/interrupt.h>
+#include <linux/gpio.h>
+#include <linux/workqueue.h>
+#include <linux/platform_device.h>
+#include <linux/regmap.h>
+#include <linux/kfifo.h>
+#include <linux/kobject.h>
+#include <linux/completion.h>
+#include <linux/list.h>
+#include <linux/crc8.h>
+#include <linux/rbtree.h>
+#include <linux/kref.h>
+#include <linux/irq.h>
+#include <linux/irqdesc.h>
+#include <linux/of_irq.h>
+#include <linux/gpio/driver.h>
+#include <linux/gpio/machine.h>
+#include <linux/gpio/consumer.h>
+
+#include <linux/pinctrl/pinconf-generic.h>
+
+#include <sound/asound.h>
+#include <sound/asequencer.h>
+#include <sound/soc.h>
+#include <sound/rawmidi.h>
+#include <sound/pcm_params.h>
+
+#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 <giedrius@blokas.io>");
+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 <stdint.h>
+#include <stdbool.h>
+
+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: */