]> git.proxmox.com Git - mirror_ubuntu-artful-kernel.git/commitdiff
Support for Blokas Labs pisound board
authorgtrainavicius <gtrainavicius@users.noreply.github.com>
Sun, 23 Oct 2016 09:06:53 +0000 (12:06 +0300)
committerSeth Forshee <seth.forshee@canonical.com>
Wed, 21 Jun 2017 16:34:54 +0000 (11:34 -0500)
Pisound dynamic overlay (#1760)

Restructuring pisound-overlay.dts, so it can be loaded and unloaded dynamically using dtoverlay.

Print a logline when the kernel module is removed.

pisound improvements:

* Added a writable sysfs object to enable scripts / user space software
to blink MIDI activity LEDs for variable duration.
* Improved hw_param constraints setting.
* Added compatibility with S16_LE sample format.
* Exposed some simple placeholder volume controls, so the card appears
in volumealsa widget.

Signed-off-by: Giedrius Trainavicius <giedrius@blokas.io>
Documentation/devicetree/bindings/vendor-prefixes.txt
sound/soc/bcm/Kconfig
sound/soc/bcm/Makefile
sound/soc/bcm/pisound.c [new file with mode: 0644]

index ec0bfb9bbebd42c828a3b4978db070924275f609..6b6b0b13c361b9cd0df0a9aea86366bd92c520cf 100644 (file)
@@ -44,6 +44,7 @@ avia  avia semiconductor
 avic   Shanghai AVIC Optoelectronics Co., Ltd.
 axentia        Axentia Technologies AB
 axis   Axis Communications AB
+blokaslabs     Vilniaus Blokas UAB
 boe    BOE Technology Group Co., Ltd.
 bosch  Bosch Sensortec GmbH
 boundary       Boundary Devices Inc.
index b600cfb98ac9baa21b3c6f09eff05149f14fda0a..ff4284a0000fd8dc58deb9e50be1422fd7ad6e65 100644 (file)
@@ -131,3 +131,9 @@ config SND_BCM2708_SOC_ALLO_PIANO_DAC
        select SND_SOC_PCM512x_I2C
        help
          Say Y or M if you want to add support for Allo Piano DAC.
+
+config SND_PISOUND
+       tristate "Support for Blokas Labs pisound"
+       depends on SND_BCM2708_SOC_I2S || SND_BCM2835_SOC_I2S
+       help
+         Say Y or M if you want to add support for Blokas Labs pisound.
index 64f007f8ba38276a42e0bd8db92544db9412544b..bb1df438540193652ec5464e8bc51f636a1b844e 100644 (file)
@@ -25,6 +25,7 @@ snd-soc-audioinjector-pi-soundcard-objs := audioinjector-pi-soundcard.o
 snd-soc-digidac1-soundcard-objs := digidac1-soundcard.o
 snd-soc-dionaudio-loco-objs := dionaudio_loco.o
 snd-soc-allo-piano-dac-objs := allo-piano-dac.o
+snd-soc-pisound-objs := pisound.o
 
 obj-$(CONFIG_SND_BCM2708_SOC_ADAU1977_ADC) += snd-soc-adau1977-adc.o
 obj-$(CONFIG_SND_BCM2708_SOC_HIFIBERRY_AMP) += snd-soc-hifiberry-amp.o
@@ -42,3 +43,4 @@ obj-$(CONFIG_SND_AUDIOINJECTOR_PI_SOUNDCARD) += snd-soc-audioinjector-pi-soundca
 obj-$(CONFIG_SND_DIGIDAC1_SOUNDCARD) += snd-soc-digidac1-soundcard.o
 obj-$(CONFIG_SND_BCM2708_SOC_DIONAUDIO_LOCO) += snd-soc-dionaudio-loco.o
 obj-$(CONFIG_SND_BCM2708_SOC_ALLO_PIANO_DAC) += snd-soc-allo-piano-dac.o
+obj-$(CONFIG_SND_PISOUND) += snd-soc-pisound.o
diff --git a/sound/soc/bcm/pisound.c b/sound/soc/bcm/pisound.c
new file mode 100644 (file)
index 0000000..06ff1e5
--- /dev/null
@@ -0,0 +1,1123 @@
+/*
+ * pisound Linux kernel module.
+ * Copyright (C) 2016  Vilniaus Blokas UAB, http://blokas.io/pisound
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA  02110-1301, USA.
+ */
+
+#include <linux/init.h>
+#include <linux/module.h>
+#include <linux/platform_device.h>
+#include <linux/gpio.h>
+#include <linux/kobject.h>
+#include <linux/sysfs.h>
+#include <linux/delay.h>
+#include <linux/spi/spi.h>
+#include <linux/interrupt.h>
+#include <linux/kfifo.h>
+
+#include <sound/core.h>
+#include <sound/pcm.h>
+#include <sound/pcm_params.h>
+#include <sound/soc.h>
+#include <sound/jack.h>
+#include <sound/rawmidi.h>
+#include <sound/asequencer.h>
+#include <sound/control.h>
+
+static int pisnd_spi_init(struct device *dev);
+static void pisnd_spi_uninit(void);
+
+static void pisnd_spi_send(uint8_t val);
+static uint8_t pisnd_spi_recv(uint8_t *buffer, uint8_t length);
+
+typedef void (*pisnd_spi_recv_cb)(void *data);
+static void pisnd_spi_set_callback(pisnd_spi_recv_cb cb, void *data);
+
+static const char *pisnd_spi_get_serial(void);
+static const char *pisnd_spi_get_id(void);
+static const char *pisnd_spi_get_version(void);
+
+static int pisnd_midi_init(struct snd_card *card);
+static void pisnd_midi_uninit(void);
+
+#define PISOUND_LOG_PREFIX "pisound: "
+
+#ifdef DEBUG
+#      define printd(...) pr_alert(PISOUND_LOG_PREFIX __VA_ARGS__)
+#else
+#      define printd(...) do {} while (0)
+#endif
+
+#define printe(...) pr_err(PISOUND_LOG_PREFIX __VA_ARGS__)
+#define printi(...) pr_info(PISOUND_LOG_PREFIX __VA_ARGS__)
+
+static int pisnd_output_open(struct snd_rawmidi_substream *substream)
+{
+       return 0;
+}
+
+static int pisnd_output_close(struct snd_rawmidi_substream *substream)
+{
+       return 0;
+}
+
+static void pisnd_output_trigger(
+       struct snd_rawmidi_substream *substream,
+       int up
+       )
+{
+       uint8_t data;
+
+       if (!up)
+               return;
+
+       while (snd_rawmidi_transmit_peek(substream, &data, 1)) {
+               pisnd_spi_send(data);
+               snd_rawmidi_transmit_ack(substream, 1);
+       }
+}
+
+static void pisnd_output_drain(struct snd_rawmidi_substream *substream)
+{
+       uint8_t data;
+
+       while (snd_rawmidi_transmit_peek(substream, &data, 1)) {
+               pisnd_spi_send(data);
+
+               snd_rawmidi_transmit_ack(substream, 1);
+       }
+}
+
+static int pisnd_input_open(struct snd_rawmidi_substream *substream)
+{
+       return 0;
+}
+
+static int pisnd_input_close(struct snd_rawmidi_substream *substream)
+{
+       return 0;
+}
+
+static void pisnd_midi_recv_callback(void *substream)
+{
+       uint8_t data[128];
+       uint8_t n = 0;
+
+       while ((n = pisnd_spi_recv(data, sizeof(data)))) {
+               int res = snd_rawmidi_receive(substream, data, n);
+               (void)res;
+               printd("midi recv 0x%02x, res = %d\n", data, res);
+       }
+}
+
+static void pisnd_input_trigger(struct snd_rawmidi_substream *substream, int up)
+{
+       if (up) {
+               pisnd_spi_set_callback(pisnd_midi_recv_callback, substream);
+               pisnd_midi_recv_callback(substream);
+       } else {
+               pisnd_spi_set_callback(NULL, NULL);
+       }
+}
+
+static struct snd_rawmidi *g_rmidi;
+
+static struct snd_rawmidi_ops pisnd_output_ops = {
+       .open = pisnd_output_open,
+       .close = pisnd_output_close,
+       .trigger = pisnd_output_trigger,
+       .drain = pisnd_output_drain,
+};
+
+static struct snd_rawmidi_ops pisnd_input_ops = {
+       .open = pisnd_input_open,
+       .close = pisnd_input_close,
+       .trigger = pisnd_input_trigger,
+};
+
+static void pisnd_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 struct snd_rawmidi_global_ops pisnd_global_ops = {
+       .get_port_info = pisnd_get_port_info,
+};
+
+static int pisnd_midi_init(struct snd_card *card)
+{
+       int err = snd_rawmidi_new(card, "pisound MIDI", 0, 1, 1, &g_rmidi);
+
+       if (err < 0) {
+               printe("snd_rawmidi_new failed: %d\n", err);
+               return err;
+       }
+
+       strcpy(g_rmidi->name, "pisound MIDI ");
+       strcat(g_rmidi->name, pisnd_spi_get_serial());
+
+       g_rmidi->info_flags =
+               SNDRV_RAWMIDI_INFO_OUTPUT |
+               SNDRV_RAWMIDI_INFO_INPUT |
+               SNDRV_RAWMIDI_INFO_DUPLEX;
+
+       g_rmidi->ops = &pisnd_global_ops;
+
+       g_rmidi->private_data = (void *)0;
+
+       snd_rawmidi_set_ops(
+               g_rmidi,
+               SNDRV_RAWMIDI_STREAM_OUTPUT,
+               &pisnd_output_ops
+               );
+
+       snd_rawmidi_set_ops(
+               g_rmidi,
+               SNDRV_RAWMIDI_STREAM_INPUT,
+               &pisnd_input_ops
+               );
+
+       return 0;
+}
+
+static void pisnd_midi_uninit(void)
+{
+}
+
+static void *g_recvData;
+static pisnd_spi_recv_cb g_recvCallback;
+
+#define FIFO_SIZE 512
+
+static char g_serial_num[11];
+static char g_id[25];
+static char g_version[5];
+
+static uint8_t g_ledFlashDuration;
+static bool    g_ledFlashDurationChanged;
+
+DEFINE_KFIFO(spi_fifo_in,  uint8_t, FIFO_SIZE);
+DEFINE_KFIFO(spi_fifo_out, uint8_t, FIFO_SIZE);
+
+static struct gpio_desc *data_available;
+static struct gpio_desc *spi_reset;
+
+static struct spi_device *pisnd_spi_device;
+
+static struct workqueue_struct *pisnd_workqueue;
+static struct work_struct pisnd_work_process;
+
+static void pisnd_work_handler(struct work_struct *work);
+
+static uint16_t spi_transfer16(uint16_t val);
+
+static int pisnd_init_workqueues(void)
+{
+       pisnd_workqueue = create_singlethread_workqueue("pisnd_workqueue");
+       INIT_WORK(&pisnd_work_process, pisnd_work_handler);
+
+       return 0;
+}
+
+static void pisnd_uninit_workqueues(void)
+{
+       flush_workqueue(pisnd_workqueue);
+       destroy_workqueue(pisnd_workqueue);
+
+       pisnd_workqueue = NULL;
+}
+
+static bool pisnd_spi_has_more(void)
+{
+       return gpiod_get_value(data_available);
+}
+
+enum task_e {
+       TASK_PROCESS = 0,
+};
+
+static void pisnd_schedule_process(enum task_e task)
+{
+       if (pisnd_spi_device != NULL &&
+               pisnd_workqueue != NULL &&
+               !work_pending(&pisnd_work_process)
+               ) {
+               printd("schedule: has more = %d\n", pisnd_spi_has_more());
+               if (task == TASK_PROCESS)
+                       queue_work(pisnd_workqueue, &pisnd_work_process);
+       }
+}
+
+static irqreturn_t data_available_interrupt_handler(int irq, void *dev_id)
+{
+       if (irq == gpiod_to_irq(data_available) && pisnd_spi_has_more()) {
+               printd("schedule from irq\n");
+               pisnd_schedule_process(TASK_PROCESS);
+       }
+
+       return IRQ_HANDLED;
+}
+
+static DEFINE_SPINLOCK(spilock);
+static unsigned long spilockflags;
+
+static uint16_t spi_transfer16(uint16_t val)
+{
+       int err;
+       struct spi_transfer transfer;
+       struct spi_message msg;
+       uint8_t txbuf[2];
+       uint8_t rxbuf[2];
+
+       if (!pisnd_spi_device) {
+               printe("pisnd_spi_device null, returning\n");
+               return 0;
+       }
+
+       spi_message_init(&msg);
+
+       memset(&transfer, 0, sizeof(transfer));
+       memset(&rxbuf, 0, sizeof(rxbuf));
+
+       txbuf[0] = val >> 8;
+       txbuf[1] = val & 0xff;
+
+       transfer.tx_buf = &txbuf;
+       transfer.rx_buf = &rxbuf;
+       transfer.len = sizeof(txbuf);
+       transfer.speed_hz = 125000;
+       transfer.delay_usecs = 100;
+       spi_message_add_tail(&transfer, &msg);
+
+       spin_lock_irqsave(&spilock, spilockflags);
+       err = spi_sync(pisnd_spi_device, &msg);
+       spin_unlock_irqrestore(&spilock, spilockflags);
+
+       if (err < 0) {
+               printe("spi_sync error %d\n", err);
+               return 0;
+       }
+
+       printd("received: %02x%02x\n", rxbuf[0], rxbuf[1]);
+       printd("hasMore %d\n", pisnd_spi_has_more());
+
+       return (rxbuf[0] << 8) | rxbuf[1];
+}
+
+static int spi_read_bytes(char *dst, size_t length, uint8_t *bytesRead)
+{
+       uint16_t rx;
+       uint8_t size;
+       uint8_t i;
+
+       memset(dst, 0, length);
+       *bytesRead = 0;
+
+        rx = spi_transfer16(0);
+       if (!(rx >> 8))
+               return -EINVAL;
+
+       size = rx & 0xff;
+
+       if (size > length)
+               return -EINVAL;
+
+       for (i = 0; i < size; ++i) {
+               rx = spi_transfer16(0);
+               if (!(rx >> 8))
+                       return -EINVAL;
+
+               dst[i] = rx & 0xff;
+       }
+
+       *bytesRead = i;
+
+       return 0;
+}
+
+static int spi_device_match(struct device *dev, void *data)
+{
+       struct spi_device *spi = container_of(dev, struct spi_device, dev);
+
+       printd("      %s %s %dkHz %d bits mode=0x%02X\n",
+               spi->modalias, dev_name(dev), spi->max_speed_hz/1000,
+               spi->bits_per_word, spi->mode);
+
+       if (strcmp("pisound-spi", spi->modalias) == 0) {
+               printi("\tFound!\n");
+               return 1;
+       }
+
+       printe("\tNot found!\n");
+       return 0;
+}
+
+static struct spi_device *pisnd_spi_find_device(void)
+{
+       struct device *dev;
+
+       printi("Searching for spi device...\n");
+       dev = bus_find_device(&spi_bus_type, NULL, NULL, spi_device_match);
+       if (dev != NULL)
+               return container_of(dev, struct spi_device, dev);
+       else
+               return NULL;
+}
+
+static void pisnd_work_handler(struct work_struct *work)
+{
+       uint16_t rx;
+       uint16_t tx;
+       uint8_t val;
+
+       if (work == &pisnd_work_process) {
+               if (pisnd_spi_device == NULL)
+                       return;
+
+               do {
+                       val = 0;
+                       tx = 0;
+
+                       if (g_ledFlashDurationChanged) {
+                               tx = 0xf000 | g_ledFlashDuration;
+                               g_ledFlashDuration = 0;
+                               g_ledFlashDurationChanged = false;
+                       } else if (kfifo_get(&spi_fifo_out, &val)) {
+                               tx = 0x0f00 | val;
+                       }
+
+                       rx = spi_transfer16(tx);
+
+                       if (rx & 0xff00) {
+                               kfifo_put(&spi_fifo_in, rx & 0xff);
+                               if (kfifo_len(&spi_fifo_in) > 16
+                                       && g_recvCallback)
+                                       g_recvCallback(g_recvData);
+                       }
+               } while (rx != 0
+                       || !kfifo_is_empty(&spi_fifo_out)
+                       || pisnd_spi_has_more()
+                       || g_ledFlashDurationChanged
+                       );
+
+               if (!kfifo_is_empty(&spi_fifo_in) && g_recvCallback)
+                       g_recvCallback(g_recvData);
+       }
+}
+
+static int pisnd_spi_gpio_init(struct device *dev)
+{
+       spi_reset = gpiod_get_index(dev, "reset", 1, GPIOD_ASIS);
+       data_available = gpiod_get_index(dev, "data_available", 0, GPIOD_ASIS);
+
+       gpiod_direction_output(spi_reset, 1);
+       gpiod_direction_input(data_available);
+
+       /* Reset the slave. */
+       gpiod_set_value(spi_reset, false);
+       mdelay(1);
+       gpiod_set_value(spi_reset, true);
+
+       /* Give time for spi slave to start. */
+       mdelay(64);
+
+       return 0;
+}
+
+static void pisnd_spi_gpio_uninit(void)
+{
+       gpiod_set_value(spi_reset, false);
+       gpiod_put(spi_reset);
+       spi_reset = NULL;
+
+       gpiod_put(data_available);
+       data_available = NULL;
+}
+
+static int pisnd_spi_gpio_irq_init(struct device *dev)
+{
+       return request_irq(
+               gpiod_to_irq(data_available),
+               data_available_interrupt_handler,
+               IRQF_TIMER | IRQF_TRIGGER_RISING,
+               "data_available_int",
+               NULL
+               );
+}
+
+static void pisnd_spi_gpio_irq_uninit(void)
+{
+       free_irq(gpiod_to_irq(data_available), NULL);
+}
+
+static int spi_read_info(void)
+{
+       uint16_t tmp;
+       uint8_t count;
+       uint8_t n;
+       uint8_t i;
+       uint8_t j;
+       char buffer[257];
+       int ret;
+       char *p;
+
+       memset(g_serial_num, 0, sizeof(g_serial_num));
+       memset(g_version, 0, sizeof(g_version));
+       memset(g_id, 0, sizeof(g_id));
+
+       tmp = spi_transfer16(0);
+
+       if (!(tmp >> 8))
+               return -EINVAL;
+
+        count = tmp & 0xff;
+
+       for (i = 0; i < count; ++i) {
+               memset(buffer, 0, sizeof(buffer));
+               ret = spi_read_bytes(buffer, sizeof(buffer)-1, &n);
+
+               if (ret < 0)
+                       return ret;
+
+               switch (i) {
+               case 0:
+                       if (n != 2)
+                               return -EINVAL;
+
+                       snprintf(
+                               g_version,
+                               sizeof(g_version),
+                               "%x.%02x",
+                               buffer[0],
+                               buffer[1]
+                               );
+                       break;
+               case 1:
+                       if (n >= sizeof(g_serial_num))
+                               return -EINVAL;
+
+                       memcpy(g_serial_num, buffer, sizeof(g_serial_num));
+                       break;
+               case 2:
+                       {
+                               if (n >= sizeof(g_id))
+                                       return -EINVAL;
+
+                               p = g_id;
+                               for (j = 0; j < n; ++j)
+                                       p += sprintf(p, "%02x", buffer[j]);
+                       }
+                       break;
+               default:
+                       break;
+               }
+       }
+
+       return 0;
+}
+
+static int pisnd_spi_init(struct device *dev)
+{
+       int ret;
+       struct spi_device *spi;
+
+       memset(g_serial_num, 0, sizeof(g_serial_num));
+       memset(g_id, 0, sizeof(g_id));
+       memset(g_version, 0, sizeof(g_version));
+
+       spi = pisnd_spi_find_device();
+
+       if (spi != NULL) {
+               printd("initializing spi!\n");
+               pisnd_spi_device = spi;
+               ret = spi_setup(pisnd_spi_device);
+       } else {
+               printe("SPI device not found, deferring!\n");
+               return -EPROBE_DEFER;
+       }
+
+       ret = pisnd_spi_gpio_init(dev);
+
+       if (ret < 0) {
+               printe("SPI GPIO init failed: %d\n", ret);
+               spi_dev_put(pisnd_spi_device);
+               pisnd_spi_device = NULL;
+               pisnd_spi_gpio_uninit();
+               return ret;
+       }
+
+       ret = spi_read_info();
+
+       if (ret < 0) {
+               printe("Reading card info failed: %d\n", ret);
+               spi_dev_put(pisnd_spi_device);
+               pisnd_spi_device = NULL;
+               pisnd_spi_gpio_uninit();
+               return ret;
+       }
+
+       /* Flash the LEDs. */
+       spi_transfer16(0xf008);
+
+       ret = pisnd_spi_gpio_irq_init(dev);
+       if (ret < 0) {
+               printe("SPI irq request failed: %d\n", ret);
+               spi_dev_put(pisnd_spi_device);
+               pisnd_spi_device = NULL;
+               pisnd_spi_gpio_irq_uninit();
+               pisnd_spi_gpio_uninit();
+       }
+
+       ret = pisnd_init_workqueues();
+       if (ret != 0) {
+               printe("Workqueue initialization failed: %d\n", ret);
+               spi_dev_put(pisnd_spi_device);
+               pisnd_spi_device = NULL;
+               pisnd_spi_gpio_irq_uninit();
+               pisnd_spi_gpio_uninit();
+               pisnd_uninit_workqueues();
+               return ret;
+       }
+
+       if (pisnd_spi_has_more()) {
+               printd("data is available, scheduling from init\n");
+               pisnd_schedule_process(TASK_PROCESS);
+       }
+
+       return 0;
+}
+
+static void pisnd_spi_uninit(void)
+{
+       pisnd_uninit_workqueues();
+
+       spi_dev_put(pisnd_spi_device);
+       pisnd_spi_device = NULL;
+
+       pisnd_spi_gpio_irq_uninit();
+       pisnd_spi_gpio_uninit();
+}
+
+static void pisnd_spi_flash_leds(uint8_t duration)
+{
+       g_ledFlashDuration = duration;
+       g_ledFlashDurationChanged = true;
+       printd("schedule from spi_flash_leds\n");
+       pisnd_schedule_process(TASK_PROCESS);
+}
+
+static void pisnd_spi_send(uint8_t val)
+{
+       kfifo_put(&spi_fifo_out, val);
+       printd("schedule from spi_send\n");
+       pisnd_schedule_process(TASK_PROCESS);
+}
+
+static uint8_t pisnd_spi_recv(uint8_t *buffer, uint8_t length)
+{
+       return kfifo_out(&spi_fifo_in, buffer, length);
+}
+
+static void pisnd_spi_set_callback(pisnd_spi_recv_cb cb, void *data)
+{
+       g_recvData = data;
+       g_recvCallback = cb;
+}
+
+static const char *pisnd_spi_get_serial(void)
+{
+       if (strlen(g_serial_num))
+               return g_serial_num;
+
+       return "";
+}
+
+static const char *pisnd_spi_get_id(void)
+{
+       if (strlen(g_id))
+               return g_id;
+
+       return "";
+}
+
+static const char *pisnd_spi_get_version(void)
+{
+       if (strlen(g_version))
+               return g_version;
+
+       return "";
+}
+
+static const struct of_device_id pisound_of_match[] = {
+       { .compatible = "blokaslabs,pisound", },
+       { .compatible = "blokaslabs,pisound-spi", },
+       {},
+};
+
+enum {
+       SWITCH = 0,
+       VOLUME = 1,
+};
+
+static int pisnd_ctl_info(struct snd_kcontrol *kcontrol,
+       struct snd_ctl_elem_info *uinfo)
+{
+       if (kcontrol->private_value == SWITCH) {
+               uinfo->type = SNDRV_CTL_ELEM_TYPE_BOOLEAN;
+               uinfo->count = 1;
+               uinfo->value.integer.min = 0;
+               uinfo->value.integer.max = 1;
+               return 0;
+       } else if (kcontrol->private_value == VOLUME) {
+               uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
+               uinfo->count = 1;
+               uinfo->value.integer.min = 0;
+               uinfo->value.integer.max = 100;
+               return 0;
+       }
+       return -EINVAL;
+}
+
+static int pisnd_ctl_get(struct snd_kcontrol *kcontrol,
+       struct snd_ctl_elem_value *ucontrol)
+{
+       if (kcontrol->private_value == SWITCH) {
+               ucontrol->value.integer.value[0] = 1;
+               return 0;
+       } else if (kcontrol->private_value == VOLUME) {
+               ucontrol->value.integer.value[0] = 100;
+               return 0;
+       }
+
+       return -EINVAL;
+}
+
+static struct snd_kcontrol_new pisnd_ctl[] = {
+       {
+               .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+               .name = "PCM Playback Switch",
+               .index = 0,
+               .private_value = SWITCH,
+               .access = SNDRV_CTL_ELEM_ACCESS_READ,
+               .info = pisnd_ctl_info,
+               .get = pisnd_ctl_get,
+       },
+       {
+               .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+               .name = "PCM Playback Volume",
+               .index = 0,
+               .private_value = VOLUME,
+               .access = SNDRV_CTL_ELEM_ACCESS_READ,
+               .info = pisnd_ctl_info,
+               .get = pisnd_ctl_get,
+       },
+};
+
+static int pisnd_ctl_init(struct snd_card *card)
+{
+       int err, i;
+
+       for (i = 0; i < ARRAY_SIZE(pisnd_ctl); ++i) {
+               err = snd_ctl_add(card, snd_ctl_new1(&pisnd_ctl[i], NULL));
+               if (err < 0)
+                       return err;
+       }
+
+       return 0;
+}
+
+static int pisnd_ctl_uninit(void)
+{
+       return 0;
+}
+
+static struct gpio_desc *osr0, *osr1, *osr2;
+static struct gpio_desc *reset;
+static struct gpio_desc *button;
+
+static int pisnd_hw_params(
+       struct snd_pcm_substream *substream,
+       struct snd_pcm_hw_params *params
+       )
+{
+       struct snd_soc_pcm_runtime *rtd = substream->private_data;
+       struct snd_soc_dai *cpu_dai = rtd->cpu_dai;
+
+       /* pisound runs on fixed 32 clock counts per channel,
+        * as generated by the master ADC.
+        */
+       snd_soc_dai_set_bclk_ratio(cpu_dai, 32*2);
+
+       printd("rate   = %d\n", params_rate(params));
+       printd("ch     = %d\n", params_channels(params));
+       printd("bits   = %u\n",
+               snd_pcm_format_physical_width(params_format(params)));
+       printd("format = %d\n", params_format(params));
+
+       gpiod_set_value(reset, false);
+
+       switch (params_rate(params)) {
+       case 48000:
+               gpiod_set_value(osr0, true);
+               gpiod_set_value(osr1, false);
+               gpiod_set_value(osr2, false);
+               break;
+       case 96000:
+               gpiod_set_value(osr0, true);
+               gpiod_set_value(osr1, true);
+               gpiod_set_value(osr2, false);
+               break;
+       case 192000:
+               gpiod_set_value(osr0, true);
+               gpiod_set_value(osr1, true);
+               gpiod_set_value(osr2, true);
+               break;
+       default:
+               printe("Unsupported rate %u!\n", params_rate(params));
+               return -EINVAL;
+       }
+
+       gpiod_set_value(reset, true);
+
+       return 0;
+}
+
+static unsigned int rates[3] = {
+       48000, 96000, 192000
+};
+
+static struct snd_pcm_hw_constraint_list constraints_rates = {
+       .count = ARRAY_SIZE(rates),
+       .list = rates,
+       .mask = 0,
+};
+
+static int pisnd_startup(struct snd_pcm_substream *substream)
+{
+       int err = snd_pcm_hw_constraint_list(
+               substream->runtime,
+               0,
+               SNDRV_PCM_HW_PARAM_RATE,
+               &constraints_rates
+               );
+
+       if (err < 0)
+               return err;
+
+       err = snd_pcm_hw_constraint_single(
+               substream->runtime,
+               SNDRV_PCM_HW_PARAM_CHANNELS,
+               2
+               );
+
+       if (err < 0)
+               return err;
+
+       err = snd_pcm_hw_constraint_mask64(
+               substream->runtime,
+               SNDRV_PCM_HW_PARAM_FORMAT,
+               SNDRV_PCM_FMTBIT_S16_LE |
+               SNDRV_PCM_FMTBIT_S24_LE |
+               SNDRV_PCM_FMTBIT_S32_LE
+               );
+
+       if (err < 0)
+               return err;
+
+       return 0;
+}
+
+static struct snd_soc_ops pisnd_ops = {
+       .startup = pisnd_startup,
+       .hw_params = pisnd_hw_params,
+};
+
+static struct snd_soc_dai_link pisnd_dai[] = {
+       {
+               .name           = "pisound",
+               .stream_name    = "pisound",
+               .cpu_dai_name   = "bcm2708-i2s.0",
+               .codec_dai_name = "snd-soc-dummy-dai",
+               .platform_name  = "bcm2708-i2s.0",
+               .codec_name     = "snd-soc-dummy",
+               .dai_fmt        =
+                       SND_SOC_DAIFMT_I2S |
+                       SND_SOC_DAIFMT_NB_NF |
+                       SND_SOC_DAIFMT_CBM_CFM,
+               .ops            = &pisnd_ops,
+       },
+};
+
+static int pisnd_card_probe(struct snd_soc_card *card)
+{
+       int err = pisnd_midi_init(card->snd_card);
+
+       if (err < 0) {
+               printe("pisnd_midi_init failed: %d\n", err);
+               return err;
+       }
+
+       err = pisnd_ctl_init(card->snd_card);
+       if (err < 0) {
+               printe("pisnd_ctl_init failed: %d\n", err);
+               return err;
+       }
+
+       return 0;
+}
+
+static int pisnd_card_remove(struct snd_soc_card *card)
+{
+       pisnd_ctl_uninit();
+       pisnd_midi_uninit();
+       return 0;
+}
+
+static struct snd_soc_card pisnd_card = {
+       .name         = "pisound",
+       .owner        = THIS_MODULE,
+       .dai_link     = pisnd_dai,
+       .num_links    = ARRAY_SIZE(pisnd_dai),
+       .probe        = pisnd_card_probe,
+       .remove       = pisnd_card_remove,
+};
+
+static int pisnd_init_gpio(struct device *dev)
+{
+       osr0 = gpiod_get_index(dev, "osr", 0, GPIOD_ASIS);
+       osr1 = gpiod_get_index(dev, "osr", 1, GPIOD_ASIS);
+       osr2 = gpiod_get_index(dev, "osr", 2, GPIOD_ASIS);
+
+       reset = gpiod_get_index(dev, "reset", 0, GPIOD_ASIS);
+
+       button = gpiod_get_index(dev, "button", 0, GPIOD_ASIS);
+
+       gpiod_direction_output(osr0,  1);
+       gpiod_direction_output(osr1,  1);
+       gpiod_direction_output(osr2,  1);
+       gpiod_direction_output(reset, 1);
+
+       gpiod_set_value(reset, false);
+       gpiod_set_value(osr0,   true);
+       gpiod_set_value(osr1,  false);
+       gpiod_set_value(osr2,  false);
+       gpiod_set_value(reset,  true);
+
+       gpiod_export(button, false);
+
+       return 0;
+}
+
+static int pisnd_uninit_gpio(void)
+{
+       int i;
+
+       struct gpio_desc **gpios[] = {
+               &osr0, &osr1, &osr2, &reset, &button,
+       };
+
+       gpiod_unexport(button);
+
+       for (i = 0; i < ARRAY_SIZE(gpios); ++i) {
+               if (*gpios[i] == NULL) {
+                       printd("weird, GPIO[%d] is NULL already\n", i);
+                       continue;
+               }
+
+               gpiod_put(*gpios[i]);
+               *gpios[i] = NULL;
+       }
+
+       return 0;
+}
+
+static struct kobject *pisnd_kobj;
+
+static ssize_t pisnd_serial_show(
+       struct kobject *kobj,
+       struct kobj_attribute *attr,
+       char *buf
+       )
+{
+       return sprintf(buf, "%s\n", pisnd_spi_get_serial());
+}
+
+static ssize_t pisnd_id_show(
+       struct kobject *kobj,
+       struct kobj_attribute *attr,
+       char *buf
+       )
+{
+       return sprintf(buf, "%s\n", pisnd_spi_get_id());
+}
+
+static ssize_t pisnd_version_show(
+       struct kobject *kobj,
+       struct kobj_attribute *attr,
+       char *buf
+       )
+{
+       return sprintf(buf, "%s\n", pisnd_spi_get_version());
+}
+
+static ssize_t pisnd_led_store(
+       struct kobject *kobj,
+       struct kobj_attribute *attr,
+       const char *buf,
+       size_t length
+       )
+{
+       uint32_t timeout;
+       int err;
+
+       err = kstrtou32(buf, 10, &timeout);
+
+       if (err == 0 && timeout <= 255)
+               pisnd_spi_flash_leds(timeout);
+
+       return length;
+}
+
+static struct kobj_attribute pisnd_serial_attribute =
+       __ATTR(serial, 0444, pisnd_serial_show, NULL);
+static struct kobj_attribute pisnd_id_attribute =
+       __ATTR(id, 0444, pisnd_id_show, NULL);
+static struct kobj_attribute pisnd_version_attribute =
+       __ATTR(version, 0444, pisnd_version_show, NULL);
+static struct kobj_attribute pisnd_led_attribute =
+       __ATTR(led, 0644, NULL, pisnd_led_store);
+
+static struct attribute *attrs[] = {
+       &pisnd_serial_attribute.attr,
+       &pisnd_id_attribute.attr,
+       &pisnd_version_attribute.attr,
+       &pisnd_led_attribute.attr,
+       NULL
+};
+
+static struct attribute_group attr_group = { .attrs = attrs };
+
+static int pisnd_probe(struct platform_device *pdev)
+{
+       int ret = 0;
+       int i;
+
+       ret = pisnd_spi_init(&pdev->dev);
+       if (ret < 0) {
+               printe("pisnd_spi_init failed: %d\n", ret);
+               return ret;
+       }
+
+       printi("Detected pisound card:\n");
+       printi("\tSerial:  %s\n", pisnd_spi_get_serial());
+       printi("\tVersion: %s\n", pisnd_spi_get_version());
+       printi("\tId:      %s\n", pisnd_spi_get_id());
+
+       pisnd_kobj = kobject_create_and_add("pisound", kernel_kobj);
+       if (!pisnd_kobj) {
+               pisnd_spi_uninit();
+               return -ENOMEM;
+       }
+
+       ret = sysfs_create_group(pisnd_kobj, &attr_group);
+       if (ret < 0) {
+               pisnd_spi_uninit();
+               kobject_put(pisnd_kobj);
+               return -ENOMEM;
+       }
+
+       pisnd_init_gpio(&pdev->dev);
+       pisnd_card.dev = &pdev->dev;
+
+       if (pdev->dev.of_node) {
+               struct device_node *i2s_node;
+
+               i2s_node = of_parse_phandle(
+                       pdev->dev.of_node,
+                       "i2s-controller",
+                       0
+                       );
+
+               for (i = 0; i < pisnd_card.num_links; ++i) {
+                       struct snd_soc_dai_link *dai = &pisnd_dai[i];
+
+                       if (i2s_node) {
+                               dai->cpu_dai_name = NULL;
+                               dai->cpu_of_node = i2s_node;
+                               dai->platform_name = NULL;
+                               dai->platform_of_node = i2s_node;
+                               dai->stream_name = pisnd_spi_get_serial();
+                       }
+               }
+       }
+
+       ret = snd_soc_register_card(&pisnd_card);
+
+       if (ret < 0) {
+               if (ret != -EPROBE_DEFER)
+                       printe("snd_soc_register_card() failed: %d\n", ret);
+               pisnd_uninit_gpio();
+               kobject_put(pisnd_kobj);
+               pisnd_spi_uninit();
+       }
+
+       return ret;
+}
+
+static int pisnd_remove(struct platform_device *pdev)
+{
+       printi("Unloading.\n");
+
+       if (pisnd_kobj) {
+               kobject_put(pisnd_kobj);
+               pisnd_kobj = NULL;
+       }
+
+       pisnd_spi_uninit();
+
+       /* Turn off */
+       gpiod_set_value(reset, false);
+       pisnd_uninit_gpio();
+
+       return snd_soc_unregister_card(&pisnd_card);
+}
+
+MODULE_DEVICE_TABLE(of, pisound_of_match);
+
+static struct platform_driver pisnd_driver = {
+       .driver = {
+               .name           = "snd-rpi-pisound",
+               .owner          = THIS_MODULE,
+               .of_match_table = pisound_of_match,
+       },
+       .probe              = pisnd_probe,
+       .remove             = pisnd_remove,
+};
+
+module_platform_driver(pisnd_driver);
+
+MODULE_AUTHOR("Giedrius Trainavicius <giedrius@blokas.io>");
+MODULE_DESCRIPTION("ASoC Driver for pisound, http://blokas.io/pisound");
+MODULE_LICENSE("GPL v2");