openwrt/target/linux/bcm27xx/patches-6.6/950-1404-Adding-Pimidi-kernel-module.patch
Álvaro Fernández Rojas 3a5584e0df bcm27xx: pull 6.6 patches from RPi repo
Adds latest 6.6 patches from the Raspberry Pi repository.

These patches were generated from:
https://github.com/raspberrypi/linux/commits/rpi-6.6.y/
With the following command:
git format-patch -N v6.6.67..HEAD
(HEAD -> 811ff707533bcd67cdcd368bbd46223082009b12)

Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com>
(cherry picked from commit 692205305db14deeff1a2dc4a6d7f87e19fc418b)
2024-12-28 14:11:52 +01:00

1168 lines
34 KiB
Diff

From 8a6f640708627ac8ebf79f88793038933f169198 Mon Sep 17 00:00:00 2001
From: Giedrius <giedrius@blokas.io>
Date: Thu, 21 Nov 2024 08:04:02 +0000
Subject: [PATCH] Adding Pimidi 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>
---
sound/drivers/Kconfig | 10 +
sound/drivers/Makefile | 2 +
sound/drivers/pimidi.c | 1113 ++++++++++++++++++++++++++++++++++++++++
3 files changed, 1125 insertions(+)
create mode 100644 sound/drivers/pimidi.c
--- a/sound/drivers/Kconfig
+++ b/sound/drivers/Kconfig
@@ -263,4 +263,14 @@ config SND_AC97_POWER_SAVE_DEFAULT
See SND_AC97_POWER_SAVE for more details.
+config SND_PIMIDI
+ tristate "Pimidi driver"
+ depends on SND_SEQUENCER && CRC8
+ select SND_RAWMIDI
+ help
+ Say Y here to include support for Blokas Pimidi.
+
+ To compile this driver as a module, choose M here: the module
+ will be called snd-pimidi.
+
endif # SND_DRIVERS
--- a/sound/drivers/Makefile
+++ b/sound/drivers/Makefile
@@ -9,6 +9,7 @@ snd-aloop-objs := aloop.o
snd-mtpav-objs := mtpav.o
snd-mts64-objs := mts64.o
snd-pcmtest-objs := pcmtest.o
+snd-pimidi-objs := pimidi.o
snd-portman2x4-objs := portman2x4.o
snd-serial-u16550-objs := serial-u16550.o
snd-serial-generic-objs := serial-generic.o
@@ -23,6 +24,7 @@ obj-$(CONFIG_SND_SERIAL_U16550) += snd-s
obj-$(CONFIG_SND_SERIAL_GENERIC) += snd-serial-generic.o
obj-$(CONFIG_SND_MTPAV) += snd-mtpav.o
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/
--- /dev/null
+++ b/sound/drivers/pimidi.c
@@ -0,0 +1,1113 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Pimidi Linux kernel module.
+ * Copyright (C) 2017-2024 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/completion.h>
+#include <linux/module.h>
+#include <linux/i2c.h>
+#include <linux/irq.h>
+#include <linux/irqdesc.h>
+#include <linux/bitops.h>
+#include <linux/of_irq.h>
+#include <linux/kfifo.h>
+#include <linux/list.h>
+#include <linux/workqueue.h>
+#include <linux/gpio.h>
+#include <linux/gpio/consumer.h>
+#include <linux/interrupt.h>
+#include <linux/mutex.h>
+#include <linux/refcount.h>
+#include <linux/crc8.h>
+#include <linux/delay.h>
+
+#include <sound/core.h>
+#include <sound/initval.h>
+#include <sound/rawmidi.h>
+#include <sound/asequencer.h>
+#include <sound/info.h>
+
+#define PIMIDI_LOG_IMPL(instance, log_func, msg, ...) log_func("pimidi(%s)[%c]: " msg "\n", \
+ __func__, (instance) ? (instance)->d + '0' : 'G', ## __VA_ARGS__)
+
+#ifdef PIMIDI_DEBUG
+# define printd(instance, ...) PIMIDI_LOG_IMPL(instance, pr_alert, __VA_ARGS__)
+# define printd_rl(instance, ...) PIMIDI_LOG_IMPL(instance, pr_alert_ratelimited, __VA_ARGS__)
+# define printd_g(...) printd((struct pimidi_instance *)NULL, __VA_ARGS__)
+#else
+# define printd(instance, ...) do {} while (0)
+# define printd_rl(instance, ...) do {} while (0)
+# define printd_g(...) do {} while (0)
+#endif
+
+#define printe(instance, ...) PIMIDI_LOG_IMPL(instance, pr_err, __VA_ARGS__)
+#define printe_rl(instance, ...) PIMIDI_LOG_IMPL(instance, pr_err_ratelimited, __VA_ARGS__)
+#define printi(instance, ...) PIMIDI_LOG_IMPL(instance, pr_info, __VA_ARGS__)
+#define printw(instance, ...) PIMIDI_LOG_IMPL(instance, pr_warn, __VA_ARGS__)
+#define printw_rl(instance, ...) PIMIDI_LOG_IMPL(instance, pr_warn_ratelimited, __VA_ARGS__)
+
+#define printe_g(...) printe((struct pimidi_instance *)NULL, __VA_ARGS__)
+#define printi_g(...) printi((struct pimidi_instance *)NULL, __VA_ARGS__)
+
+DECLARE_CRC8_TABLE(pimidi_crc8_table);
+enum { PIMIDI_CRC8_POLYNOMIAL = 0x83 };
+enum { PIMIDI_MAX_DEVICES = 4 };
+enum { PIMIDI_MAX_PACKET_SIZE = 17 };
+enum { PIMIDI_PORTS = 2 };
+
+struct pimidi_shared {
+ // lock protects the shared reset_gpio and devices list.
+ struct mutex lock;
+ struct gpio_desc *reset_gpio;
+ struct workqueue_struct *work_queue;
+ struct list_head devices;
+};
+
+static struct pimidi_shared pimidi_global = {
+ .devices = LIST_HEAD_INIT(pimidi_global.devices),
+};
+
+struct pimidi_version_t {
+ u8 hwrev;
+ u8 major;
+ u8 minor;
+ u8 build;
+};
+
+enum { PIMIDI_IN_FIFO_SIZE = 4096 };
+
+struct pimidi_midi_port {
+ // in_lock protects the input substream.
+ struct mutex in_lock;
+ // out_lock protects the output substream.
+ struct mutex out_lock;
+ DECLARE_KFIFO(in_fifo, uint8_t, PIMIDI_IN_FIFO_SIZE);
+ unsigned int last_output_at;
+ unsigned int output_buffer_used_in_millibytes;
+ struct work_struct in_handler;
+ struct delayed_work out_handler;
+ unsigned long enabled_streams;
+ unsigned int tx_cnt;
+ unsigned int rx_cnt;
+};
+
+struct pimidi_instance {
+ struct list_head list;
+ struct i2c_client *i2c_client;
+ struct pimidi_version_t version;
+ char serial[11];
+ char d;
+ struct gpio_desc *data_ready_gpio;
+
+ struct work_struct drdy_handler;
+
+ // comm_lock serializes I2C communication.
+ struct mutex comm_lock;
+ char *rx_buf;
+ size_t rx_len;
+ int rx_status;
+ struct completion *rx_completion;
+
+ struct snd_rawmidi *rawmidi;
+ struct pimidi_midi_port midi_port[PIMIDI_PORTS];
+ bool stopping;
+};
+
+static struct snd_rawmidi_substream *pimidi_find_substream(struct snd_rawmidi *rawmidi,
+ int stream,
+ int number
+ )
+{
+ struct snd_rawmidi_substream *substream;
+
+ list_for_each_entry(substream, &rawmidi->streams[stream].substreams, list) {
+ if (substream->number == number)
+ return substream;
+ }
+ return NULL;
+}
+
+static void pimidi_midi_in_handler(struct pimidi_instance *instance, int port)
+{
+ int i, n, err;
+
+ printd(instance, "(%d)", port);
+
+ struct pimidi_midi_port *midi_port = &instance->midi_port[port];
+
+ if (!test_bit(SNDRV_RAWMIDI_STREAM_INPUT, &midi_port->enabled_streams)) {
+ printd(instance, "Input not enabled for %d", port);
+ return;
+ }
+
+ u8 data[512];
+
+ n = kfifo_out_peek(&midi_port->in_fifo, data, sizeof(data));
+ printd(instance, "Peeked %d MIDI bytes", n);
+
+ mutex_lock(&midi_port->in_lock);
+ struct snd_rawmidi_substream *substream =
+ pimidi_find_substream(instance->rawmidi,
+ SNDRV_RAWMIDI_STREAM_INPUT,
+ port);
+
+ err = snd_rawmidi_receive(substream, data, n);
+ if (err > 0)
+ midi_port->rx_cnt += err;
+ mutex_unlock(&midi_port->in_lock);
+
+ for (i = 0; i < err; ++i)
+ kfifo_skip(&midi_port->in_fifo);
+
+ if (n != err)
+ printw_rl(instance,
+ "Not all MIDI data consumed for port %d: %d / %d", port, err, n);
+
+ if (!kfifo_is_empty(&midi_port->in_fifo) && !instance->stopping)
+ queue_work(pimidi_global.work_queue, &midi_port->in_handler);
+
+ printd(instance, "Done");
+}
+
+static void pimidi_midi_in_handler_0(struct work_struct *work)
+{
+ pimidi_midi_in_handler(container_of(work, struct pimidi_instance, midi_port[0].in_handler),
+ 0);
+}
+
+static void pimidi_midi_in_handler_1(struct work_struct *work)
+{
+ pimidi_midi_in_handler(container_of(work, struct pimidi_instance, midi_port[1].in_handler),
+ 1);
+}
+
+static void pimidi_midi_out_handler(struct pimidi_instance *instance, int port)
+{
+ printd(instance, "(%d)", port);
+ if (!test_bit(SNDRV_RAWMIDI_STREAM_OUTPUT, &instance->midi_port[port].enabled_streams)) {
+ printd(instance, "Output not enabled for %d", port);
+ return;
+ }
+
+ struct pimidi_midi_port *midi_port = &instance->midi_port[port];
+
+ struct snd_rawmidi_substream *substream =
+ pimidi_find_substream(instance->rawmidi, SNDRV_RAWMIDI_STREAM_OUTPUT, port);
+
+ mutex_lock(&midi_port->out_lock);
+
+ enum { MIDI_MILLI_BYTES_PER_JIFFY = 3125000 / HZ };
+ enum { MIDI_MAX_OUTPUT_BUFFER_SIZE_IN_MILLIBYTES =
+ (512 - PIMIDI_MAX_PACKET_SIZE - 1) * 1000 };
+
+ unsigned int now = jiffies;
+ unsigned int millibytes_became_available =
+ (MIDI_MILLI_BYTES_PER_JIFFY) * (now - midi_port->last_output_at);
+
+ midi_port->output_buffer_used_in_millibytes =
+ midi_port->output_buffer_used_in_millibytes <=
+ millibytes_became_available ? 0 : midi_port->output_buffer_used_in_millibytes -
+ millibytes_became_available;
+
+ unsigned int output_buffer_available =
+ (MIDI_MAX_OUTPUT_BUFFER_SIZE_IN_MILLIBYTES
+ - midi_port->output_buffer_used_in_millibytes)
+ / 1000;
+
+ u8 buffer[PIMIDI_MAX_PACKET_SIZE];
+ int n, batch, err;
+
+ for (batch = 0; batch < 3; ++batch) {
+ if (output_buffer_available == 0)
+ printd(instance, "Buffer full");
+
+ printd(instance, "Buffer available: %u (%u +%u, %u -> %u, dt %u) (%u) @ %u",
+ output_buffer_available, midi_port->output_buffer_used_in_millibytes,
+ millibytes_became_available, midi_port->last_output_at, now,
+ now - midi_port->last_output_at, midi_port->tx_cnt, HZ);
+ midi_port->last_output_at = now;
+
+ n = output_buffer_available
+ ? snd_rawmidi_transmit_peek(substream, buffer + 1,
+ min(output_buffer_available,
+ sizeof(buffer) - 2))
+ : 0;
+ if (n > 0) {
+ printd(instance, "Peeked: %d", n);
+ snd_rawmidi_transmit_ack(substream, n);
+
+ buffer[0] = (port << 4) | n;
+ buffer[n + 1] = ~crc8(pimidi_crc8_table, buffer, n + 1, CRC8_INIT_VALUE);
+
+#ifdef PIMIDI_DEBUG
+ pr_debug("%s[%d]: Sending %d bytes:", __func__, instance->d, n + 2);
+ int i;
+
+ for (i = 0; i < n + 2; ++i)
+ pr_cont(" %02x", buffer[i]);
+
+ pr_cont("\n");
+#endif
+ mutex_lock(&instance->comm_lock);
+ err = i2c_master_send(instance->i2c_client, buffer, n + 2);
+ mutex_unlock(&instance->comm_lock);
+
+ if (err < 0) {
+ printe(instance,
+ "Error occurred when sending MIDI data over I2C! (%d)",
+ err);
+ goto cleanup;
+ }
+
+ midi_port->tx_cnt += n;
+ midi_port->output_buffer_used_in_millibytes += n * 1000;
+ output_buffer_available -= n;
+ } else if (n < 0) {
+ err = n;
+ printe(instance, "snd_rawmidi_transmit_peek returned error %d!", err);
+ goto cleanup;
+ } else {
+ break;
+ }
+ }
+
+ printd(instance, "Checking if empty %p", substream);
+ if (!snd_rawmidi_transmit_empty(substream) && !instance->stopping) {
+ unsigned int delay = 1;
+
+ if (output_buffer_available == 0)
+ delay = 125000 / MIDI_MILLI_BYTES_PER_JIFFY;
+ printd(instance, "Queue more work after %u jiffies", delay);
+ mod_delayed_work(pimidi_global.work_queue, &midi_port->out_handler, delay);
+ }
+
+cleanup:
+ mutex_unlock(&midi_port->out_lock);
+ printd(instance, "Done");
+}
+
+static void pimidi_midi_out_handler_0(struct work_struct *work)
+{
+ pimidi_midi_out_handler(container_of(work, struct pimidi_instance,
+ midi_port[0].out_handler.work), 0);
+}
+
+static void pimidi_midi_out_handler_1(struct work_struct *work)
+{
+ pimidi_midi_out_handler(container_of(work, struct pimidi_instance,
+ midi_port[1].out_handler.work), 1);
+}
+
+static void pimidi_midi_output_trigger(struct snd_rawmidi_substream *substream, int up)
+{
+ struct pimidi_instance *instance = substream->rmidi->private_data;
+
+ printd(instance, "(%d, %d, %d)", substream->stream, substream->number, up);
+
+ if (up == 0) {
+ clear_bit(substream->stream,
+ &instance->midi_port[substream->number].enabled_streams);
+ } else {
+ set_bit(substream->stream,
+ &instance->midi_port[substream->number].enabled_streams);
+ if (!delayed_work_pending(&instance->midi_port[substream->number].out_handler)) {
+ printd(instance, "Queueing work");
+ queue_delayed_work(pimidi_global.work_queue,
+ &instance->midi_port[substream->number].out_handler, 0);
+ }
+ }
+}
+
+static void pimidi_midi_output_drain(struct snd_rawmidi_substream *substream)
+{
+ struct pimidi_instance *instance = substream->rmidi->private_data;
+
+ printd(instance, "(%d, %d)", substream->stream, substream->number);
+
+ printd(instance, "Begin draining!");
+
+ queue_delayed_work(pimidi_global.work_queue,
+ &instance->midi_port[substream->number].out_handler, 0);
+
+ unsigned long deadline = jiffies + 5 * HZ;
+
+ do {
+ printd(instance, "Before flush");
+ while (delayed_work_pending(&instance->midi_port[substream->number].out_handler))
+ flush_delayed_work(&instance->midi_port[substream->number].out_handler);
+ printd(instance, "Flushed");
+ } while (!snd_rawmidi_transmit_empty(substream) && time_before(jiffies, deadline));
+
+ printd(instance, "Done!");
+}
+
+static int pimidi_midi_output_close(struct snd_rawmidi_substream *substream)
+{
+ struct pimidi_instance *instance = substream->rmidi->private_data;
+ struct pimidi_midi_port *midi_port = &instance->midi_port[substream->number];
+
+ mutex_lock(&midi_port->out_lock);
+ clear_bit(substream->stream, &midi_port->enabled_streams);
+ mutex_unlock(&midi_port->out_lock);
+ return 0;
+}
+
+static int pimidi_midi_input_close(struct snd_rawmidi_substream *substream)
+{
+ struct pimidi_instance *instance = substream->rmidi->private_data;
+ struct pimidi_midi_port *midi_port = &instance->midi_port[substream->number];
+
+ mutex_lock(&midi_port->in_lock);
+ clear_bit(substream->stream, &midi_port->enabled_streams);
+ mutex_unlock(&midi_port->in_lock);
+ return 0;
+}
+
+static void pimidi_midi_input_trigger(struct snd_rawmidi_substream *substream, int up)
+{
+ struct pimidi_instance *instance = substream->rmidi->private_data;
+
+ printd(instance, "(%d, %d, %d)", substream->stream, substream->number, up);
+
+ if (up == 0) {
+ clear_bit(substream->stream,
+ &instance->midi_port[substream->number].enabled_streams);
+ cancel_work_sync(&instance->midi_port[substream->number].in_handler);
+ } else {
+ set_bit(substream->stream,
+ &instance->midi_port[substream->number].enabled_streams);
+ if (!instance->stopping)
+ queue_work(pimidi_global.work_queue,
+ &instance->midi_port[substream->number].in_handler);
+ }
+}
+
+static void pimidi_get_port_info(struct snd_rawmidi *rmidi, int number,
+ struct snd_seq_port_info *seq_port_info)
+{
+ printd_g("%p, %d, %p", rmidi, number, seq_port_info);
+ seq_port_info->type =
+ SNDRV_SEQ_PORT_TYPE_MIDI_GENERIC |
+ SNDRV_SEQ_PORT_TYPE_HARDWARE |
+ SNDRV_SEQ_PORT_TYPE_PORT;
+ strscpy(seq_port_info->name, number == 0 ? "a" : "b",
+ sizeof(seq_port_info->name));
+ seq_port_info->midi_voices = 0;
+}
+
+static const struct snd_rawmidi_global_ops pimidi_midi_ops = {
+ .get_port_info = pimidi_get_port_info,
+};
+
+static int pimidi_midi_open(struct snd_rawmidi_substream *substream)
+{
+ printd_g("(%p) stream=%d number=%d", substream, substream->stream, substream->number);
+ return 0;
+}
+
+static const struct snd_rawmidi_ops pimidi_midi_output_ops = {
+ .open = pimidi_midi_open,
+ .close = pimidi_midi_output_close,
+ .trigger = pimidi_midi_output_trigger,
+ .drain = pimidi_midi_output_drain,
+};
+
+static const struct snd_rawmidi_ops pimidi_midi_input_ops = {
+ .open = pimidi_midi_open,
+ .close = pimidi_midi_input_close,
+ .trigger = pimidi_midi_input_trigger,
+};
+
+static int pimidi_register(struct pimidi_instance *instance)
+{
+ int err = 0;
+
+ mutex_lock(&pimidi_global.lock);
+ printd(instance, "Registering...");
+ if (!pimidi_global.reset_gpio) {
+ printd_g("Getting reset pin.");
+ pimidi_global.reset_gpio = gpiod_get(&instance->i2c_client->dev, "reset",
+ GPIOD_OUT_LOW);
+ if (IS_ERR(pimidi_global.reset_gpio)) {
+ err = PTR_ERR(pimidi_global.reset_gpio);
+ printe_g("gpiod_get failed: %d", err);
+ pimidi_global.reset_gpio = NULL;
+ mutex_unlock(&pimidi_global.lock);
+ return err;
+ }
+ }
+ list_add_tail(&instance->list, &pimidi_global.devices);
+ mutex_unlock(&pimidi_global.lock);
+ return err;
+}
+
+static void pimidi_unregister(struct pimidi_instance *instance)
+{
+ mutex_lock(&pimidi_global.lock);
+ printd(instance, "Unregistering...");
+ list_del(&instance->list);
+ if (list_empty(&pimidi_global.devices)) {
+ printd_g("Releasing reset pin");
+ gpiod_put(pimidi_global.reset_gpio);
+ pimidi_global.reset_gpio = NULL;
+ }
+ mutex_unlock(&pimidi_global.lock);
+}
+
+static void pimidi_perform_reset(void)
+{
+ mutex_lock(&pimidi_global.lock);
+
+ printd_g("Performing reset.");
+
+ struct list_head *p;
+
+ list_for_each(p, &pimidi_global.devices) {
+ struct pimidi_instance *instance = list_entry(p, struct pimidi_instance, list);
+
+ printd(instance, "Pausing...");
+ instance->stopping = true;
+ disable_irq(instance->i2c_client->irq);
+ cancel_work(&instance->drdy_handler);
+
+ int i;
+
+ for (i = 0; i < PIMIDI_PORTS; ++i) {
+ cancel_work(&instance->midi_port[i].in_handler);
+ cancel_delayed_work(&instance->midi_port[i].out_handler);
+ }
+
+ drain_workqueue(pimidi_global.work_queue);
+ }
+
+ printd_g("Reset = low");
+ gpiod_set_value(pimidi_global.reset_gpio, 1);
+
+ list_for_each(p, &pimidi_global.devices) {
+ struct pimidi_instance *instance = list_entry(p, struct pimidi_instance, list);
+
+ if (gpiod_is_active_low(instance->data_ready_gpio))
+ gpiod_toggle_active_low(instance->data_ready_gpio);
+ gpiod_direction_output(instance->data_ready_gpio, 1);
+ printd(instance, "DRDY high");
+ }
+
+ usleep_range(1000, 5000);
+ printd_g("Reset = high");
+ gpiod_set_value(pimidi_global.reset_gpio, 0);
+ msleep(30);
+
+ int i;
+
+ for (i = 0; i < PIMIDI_MAX_DEVICES; ++i) {
+ usleep_range(1000, 3000);
+ list_for_each(p, &pimidi_global.devices) {
+ struct pimidi_instance *instance = list_entry(p, struct pimidi_instance,
+ list);
+
+ if (instance->d < i)
+ continue;
+ printd(instance, "DRDY -> %d", !gpiod_get_value(instance->data_ready_gpio));
+ gpiod_set_value(instance->data_ready_gpio,
+ !gpiod_get_value(instance->data_ready_gpio));
+ }
+ }
+ usleep_range(16000, 20000);
+
+ list_for_each(p, &pimidi_global.devices) {
+ struct pimidi_instance *instance = list_entry(p, struct pimidi_instance, list);
+
+ if (!gpiod_is_active_low(instance->data_ready_gpio))
+ gpiod_toggle_active_low(instance->data_ready_gpio);
+
+ printd(instance, "DRDY input");
+ gpiod_direction_input(instance->data_ready_gpio);
+
+ printd(instance, "Resume...");
+ instance->stopping = false;
+ enable_irq(instance->i2c_client->irq);
+ }
+
+ printd_g("Reset done.");
+ usleep_range(16000, 20000);
+
+ mutex_unlock(&pimidi_global.lock);
+}
+
+static int pimidi_read_version(struct pimidi_version_t *version, struct pimidi_instance *instance)
+{
+ memset(version, 0, sizeof(*version));
+
+ const char cmd[4] = { 0xb2, 0x01, 0x01, 0x95 };
+
+ char result[9];
+
+ memset(result, 0, sizeof(result));
+
+ DECLARE_COMPLETION_ONSTACK(done);
+
+ mutex_lock(&instance->comm_lock);
+ int err = i2c_master_send(instance->i2c_client, cmd, sizeof(cmd));
+
+ if (err < 0) {
+ mutex_unlock(&instance->comm_lock);
+ return err;
+ }
+ instance->rx_buf = result;
+ instance->rx_len = sizeof(result);
+ instance->rx_completion = &done;
+ mutex_unlock(&instance->comm_lock);
+
+ printd(instance, "Waiting for drdy");
+ wait_for_completion_io_timeout(&done, msecs_to_jiffies(1000u));
+ printd(instance, "Done waiting");
+
+ if (!completion_done(&done)) {
+ mutex_lock(&instance->comm_lock);
+ instance->rx_buf = NULL;
+ instance->rx_len = 0;
+ instance->rx_status = -ETIMEDOUT;
+ instance->rx_completion = NULL;
+ mutex_unlock(&instance->comm_lock);
+ return -ETIMEDOUT;
+ }
+
+ if (CRC8_GOOD_VALUE(pimidi_crc8_table) != crc8(pimidi_crc8_table, result, sizeof(result),
+ CRC8_INIT_VALUE))
+ return -EIO;
+
+ const char expected[4] = { 0xb7, 0x81, 0x01, 0x00 };
+
+ if (memcmp(result, expected, sizeof(expected)) != 0)
+ return -EPROTO;
+
+ u32 v = ntohl(*(uint32_t *)(result + 4));
+
+ version->hwrev = v >> 24;
+ version->major = (v & 0x00ff0000) >> 16;
+ version->minor = (v & 0x0000ff00) >> 8;
+ version->build = v & 0x000000ff;
+
+ return 0;
+}
+
+static int pimidi_read_serial(char serial[11], struct pimidi_instance *instance)
+{
+ memset(serial, 0, sizeof(char[11]));
+
+ const char cmd[4] = { 0xb2, 0x03, 0x04, 0x97 };
+
+ char result[PIMIDI_MAX_PACKET_SIZE];
+
+ memset(result, 0, sizeof(result));
+
+ DECLARE_COMPLETION_ONSTACK(done);
+
+ mutex_lock(&instance->comm_lock);
+ int err = i2c_master_send(instance->i2c_client, cmd, sizeof(cmd));
+
+ if (err < 0) {
+ mutex_unlock(&instance->comm_lock);
+ return err;
+ }
+ instance->rx_buf = result;
+ instance->rx_len = sizeof(result);
+ instance->rx_completion = &done;
+ mutex_unlock(&instance->comm_lock);
+
+ printd(instance, "Waiting for drdy");
+ wait_for_completion_io_timeout(&done, msecs_to_jiffies(1000u));
+ printd(instance, "Done waiting");
+
+ if (!completion_done(&done)) {
+ mutex_lock(&instance->comm_lock);
+ instance->rx_buf = NULL;
+ instance->rx_len = 0;
+ instance->rx_status = -ETIMEDOUT;
+ instance->rx_completion = NULL;
+ mutex_unlock(&instance->comm_lock);
+ printe(instance, "Timed out");
+ return -ETIMEDOUT;
+ }
+
+ if (CRC8_GOOD_VALUE(pimidi_crc8_table) != crc8(pimidi_crc8_table, result,
+ (result[0] & 0x0f) + 2, CRC8_INIT_VALUE))
+ return -EIO;
+
+ const char expected[4] = { 0xbd, 0x83, 0x04, 0x0a };
+
+ if (memcmp(result, expected, sizeof(expected)) != 0) {
+ printe(instance, "Unexpected response: %02x %02x %02x %02x", result[0], result[1],
+ result[2], result[3]);
+ return -EPROTO;
+ }
+
+ memcpy(serial, result + 4, 10);
+
+ if (strspn(serial, "\xff") == 10)
+ strscpy(serial, "(unset)", 8);
+
+ return 0;
+}
+
+static void pimidi_handle_midi_data(struct pimidi_instance *instance, int port, const uint8_t *data,
+ unsigned int n)
+{
+ printd(instance, "Handling MIDI data for port %d (%u bytes)", port, n);
+ if (n == 0)
+ return;
+
+ struct pimidi_midi_port *midi_port = &instance->midi_port[port];
+
+ kfifo_in(&midi_port->in_fifo, data, n);
+
+ if (!instance->stopping)
+ queue_work(pimidi_global.work_queue, &midi_port->in_handler);
+
+ printd(instance, "Done");
+}
+
+static void pimidi_drdy_continue(struct pimidi_instance *instance)
+{
+ if (instance->stopping) {
+ printd(instance, "Refusing to queue work / enable IRQ due to stopping.");
+ return;
+ }
+
+ if (gpiod_get_value(instance->data_ready_gpio)) {
+ printd_rl(instance, "Queue work due to DRDY line still low");
+ queue_work(pimidi_global.work_queue, &instance->drdy_handler);
+ } else {
+ printd_rl(instance, "Enabling irq for more data");
+ enable_irq(gpiod_to_irq(instance->data_ready_gpio));
+ }
+}
+
+static void pimidi_drdy_handler(struct work_struct *work)
+{
+ struct pimidi_instance *instance = container_of(work, struct pimidi_instance, drdy_handler);
+
+ printd(instance, "(%p)", work);
+
+ mutex_lock(&instance->comm_lock);
+ if (!instance->rx_completion) {
+ u8 data[PIMIDI_MAX_PACKET_SIZE];
+ int n = i2c_master_recv(instance->i2c_client, data, 3);
+
+ if (n < 0) {
+ printe(instance, "Error reading from device: %d", n);
+ mutex_unlock(&instance->comm_lock);
+ pimidi_drdy_continue(instance);
+ return;
+ }
+
+ if (data[0] == 0xfe) {
+ printe_rl(instance, "Invalid packet 0x%02x 0x%02x 0x%02x", data[0], data[1],
+ data[2]);
+ mutex_unlock(&instance->comm_lock);
+ pimidi_drdy_continue(instance);
+ return;
+ }
+
+ int len = (data[0] & 0x0f) + 2;
+
+ if (len > n) {
+ printd(instance, "Need %d more bytes", len - n);
+ int err = i2c_master_recv(instance->i2c_client, data + n, len - n);
+
+ if (err < 0) {
+ printe(instance, "Error reading remainder from device: %d", err);
+ mutex_unlock(&instance->comm_lock);
+ pimidi_drdy_continue(instance);
+ return;
+#ifdef PIMIDI_DEBUG
+ } else {
+ pr_debug("Recv_2:");
+ int i;
+
+ for (i = n; i < len; ++i)
+ pr_cont(" %02x", data[i]);
+ pr_cont("\n");
+#endif
+ }
+ }
+
+ if (CRC8_GOOD_VALUE(pimidi_crc8_table) == crc8(pimidi_crc8_table, data, len,
+ CRC8_INIT_VALUE)) {
+ switch (data[0] & 0xf0) {
+ case 0x00:
+ pimidi_handle_midi_data(instance, 0, data + 1, len - 2);
+ break;
+ case 0x10:
+ pimidi_handle_midi_data(instance, 1, data + 1, len - 2);
+ break;
+ default:
+ printd(instance, "Unhandled command %02x", data[0]);
+ break;
+ }
+ } else {
+ printe(instance, "I2C rx corruption detected.");
+ pr_info("Packet [%d]:", len);
+ int i;
+
+ for (i = 0; i < len; ++i)
+ pr_cont(" %02x", data[i]);
+ pr_cont("\n");
+ }
+
+ mutex_unlock(&instance->comm_lock);
+ } else {
+ printd(instance, "Completing drdy");
+ instance->rx_status = i2c_master_recv(instance->i2c_client, instance->rx_buf, 3);
+ printd(instance, "Recv_1 %02x %02x %02x", instance->rx_buf[0], instance->rx_buf[1],
+ instance->rx_buf[2]);
+ if (instance->rx_len > 3 && instance->rx_status == 3) {
+ instance->rx_status = i2c_master_recv(instance->i2c_client,
+ instance->rx_buf + 3,
+ instance->rx_len - 3);
+ if (instance->rx_status >= 0)
+ instance->rx_status += 3;
+#ifdef PIMIDI_DEBUG
+ pr_debug("Recv_2:");
+ int i;
+
+ for (i = 3; i < instance->rx_len; ++i)
+ pr_cont(" %02x", instance->rx_buf[i]);
+ pr_cont("\n");
+#endif
+ }
+ struct completion *done = instance->rx_completion;
+
+ instance->rx_buf = NULL;
+ instance->rx_len = 0;
+ instance->rx_completion = NULL;
+ complete_all(done);
+ mutex_unlock(&instance->comm_lock);
+ }
+
+ pimidi_drdy_continue(instance);
+}
+
+static irqreturn_t pimidi_drdy_interrupt_handler(int irq, void *dev_id)
+{
+ struct pimidi_instance *instance = (struct pimidi_instance *)dev_id;
+
+ if (instance->stopping) {
+ printd(instance, "DRDY interrupt, but stopping, ignoring...");
+ return IRQ_HANDLED;
+ }
+
+ printd(instance, "DRDY interrupt, masking");
+ disable_irq_nosync(irq);
+
+ printd(instance, "Queue work due to DRDY interrupt");
+ queue_work(pimidi_global.work_queue, &instance->drdy_handler);
+
+ return IRQ_HANDLED;
+}
+
+static void pimidi_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);
+}
+
+static void pimidi_proc_serial_show(struct snd_info_entry *entry, struct snd_info_buffer *buffer)
+{
+ struct pimidi_instance *instance = entry->private_data;
+
+ snd_iprintf(buffer, "%s\n", instance->serial);
+}
+
+static void pimidi_proc_version_show(struct snd_info_entry *entry, struct snd_info_buffer *buffer)
+{
+ struct pimidi_instance *instance = entry->private_data;
+
+ snd_iprintf(buffer, "%u.%u.%u\n", instance->version.major, instance->version.minor,
+ instance->version.build);
+}
+
+static void pimidi_proc_hwrev_show(struct snd_info_entry *entry, struct snd_info_buffer *buffer)
+{
+ struct pimidi_instance *instance = entry->private_data;
+
+ snd_iprintf(buffer, "%u\n", instance->version.hwrev);
+}
+
+static int pimidi_i2c_probe(struct i2c_client *client)
+{
+ struct snd_card *card = NULL;
+ int err, d, i;
+
+ d = client->addr - 0x20;
+
+ if (d < 0 || d >= 8) {
+ printe_g("Unexpected device address: %d", client->addr);
+ err = -EINVAL;
+ goto finalize;
+ }
+
+ err = snd_card_new(&client->dev, SNDRV_DEFAULT_IDX1, SNDRV_DEFAULT_STR1, THIS_MODULE,
+ sizeof(struct pimidi_instance), &card);
+
+ if (err) {
+ printe_g("snd_card_new failed: %d", err);
+ return err;
+ }
+
+ struct pimidi_instance *instance = (struct pimidi_instance *)card->private_data;
+
+ instance->i2c_client = client;
+ instance->d = d;
+
+ struct snd_rawmidi *rawmidi;
+
+ err = snd_rawmidi_new(card, card->shortname, 0, 2, 2, &rawmidi);
+ if (err < 0) {
+ printe(instance, "snd_rawmidi_new failed: %d", err);
+ goto finalize;
+ }
+
+ instance->rawmidi = rawmidi;
+ strscpy(rawmidi->name, "pimidi", sizeof(rawmidi->name));
+
+ rawmidi->info_flags =
+ SNDRV_RAWMIDI_INFO_OUTPUT | SNDRV_RAWMIDI_INFO_INPUT | SNDRV_RAWMIDI_INFO_DUPLEX;
+ rawmidi->private_data = instance;
+ rawmidi->ops = &pimidi_midi_ops;
+
+ snd_rawmidi_set_ops(rawmidi, SNDRV_RAWMIDI_STREAM_OUTPUT, &pimidi_midi_output_ops);
+ snd_rawmidi_set_ops(rawmidi, SNDRV_RAWMIDI_STREAM_INPUT, &pimidi_midi_input_ops);
+
+ instance->data_ready_gpio = devm_gpiod_get(&client->dev, "data-ready", GPIOD_OUT_HIGH);
+ if (IS_ERR(instance->data_ready_gpio)) {
+ err = PTR_ERR(instance->data_ready_gpio);
+ printe(instance, "devm_gpiod_get failed: %d", err);
+ goto finalize;
+ }
+
+ err = pimidi_register(instance);
+ if (err < 0) {
+ printe(instance, "pimidi_register failed: %d", err);
+ goto finalize;
+ }
+
+ pimidi_perform_reset();
+
+ INIT_WORK(&instance->drdy_handler, pimidi_drdy_handler);
+ mutex_init(&instance->comm_lock);
+
+ err = devm_request_irq(&client->dev, client->irq, pimidi_drdy_interrupt_handler,
+ IRQF_SHARED | IRQF_TRIGGER_LOW, "data_ready_int", instance);
+
+ if (err != 0) {
+ printe(instance, "data_available IRQ request failed! %d", err);
+ goto finalize;
+ }
+
+ err = pimidi_read_version(&instance->version, instance);
+ if (err < 0) {
+ printe(instance, "pimidi_read_version failed: %d", err);
+ goto finalize;
+ }
+
+ err = pimidi_read_serial(instance->serial, instance);
+ if (err < 0) {
+ printe(instance, "pimidi_read_serial failed: %d", err);
+ goto finalize;
+ } else if (instance->serial[0] != 'P' || instance->serial[1] != 'M' ||
+ strlen(instance->serial) != 10) {
+ printe(instance, "Unexpected serial number: %s", instance->serial);
+ err = -EIO;
+ goto finalize;
+ }
+
+ printi(instance, "pimidi%d hw:%d version %u.%u.%u-%u, serial %s",
+ d,
+ card->number,
+ instance->version.major,
+ instance->version.minor,
+ instance->version.build,
+ instance->version.hwrev,
+ instance->serial
+ );
+
+ strscpy(card->driver, "snd-pimidi", sizeof(card->driver));
+ snprintf(card->shortname, sizeof(card->shortname), "pimidi%d", d);
+ snprintf(card->longname, sizeof(card->longname), "pimidi%d %s", d, instance->serial);
+ snprintf(card->id, sizeof(card->id), "pimidi%d", d);
+
+ snprintf(pimidi_find_substream(rawmidi, SNDRV_RAWMIDI_STREAM_OUTPUT, 0)->name,
+ 10u, "pimidi%d-a", d);
+ snprintf(pimidi_find_substream(rawmidi, SNDRV_RAWMIDI_STREAM_INPUT, 0)->name,
+ 10u, "pimidi%d-a", d);
+ snprintf(pimidi_find_substream(rawmidi, SNDRV_RAWMIDI_STREAM_OUTPUT, 1)->name,
+ 10u, "pimidi%d-b", d);
+ snprintf(pimidi_find_substream(rawmidi, SNDRV_RAWMIDI_STREAM_INPUT, 1)->name,
+ 10u, "pimidi%d-b", d);
+
+ err = snd_card_ro_proc_new(card, "a-tx", &instance->midi_port[0].tx_cnt,
+ pimidi_proc_stat_show);
+ err = snd_card_ro_proc_new(card, "a-rx", &instance->midi_port[0].rx_cnt,
+ pimidi_proc_stat_show);
+ err = snd_card_ro_proc_new(card, "b-tx", &instance->midi_port[1].tx_cnt,
+ pimidi_proc_stat_show);
+ err = snd_card_ro_proc_new(card, "b-rx", &instance->midi_port[1].rx_cnt,
+ pimidi_proc_stat_show);
+ err = snd_card_ro_proc_new(card, "serial", instance, pimidi_proc_serial_show);
+ err = snd_card_ro_proc_new(card, "version", instance, pimidi_proc_version_show);
+ err = snd_card_ro_proc_new(card, "hwrev", instance, pimidi_proc_hwrev_show);
+ if (err < 0) {
+ printe(instance, "snd_card_ro_proc_new failed: %d", err);
+ goto finalize;
+ }
+
+ err = snd_card_register(card);
+ if (err < 0) {
+ printe(instance, "snd_card_register failed: %d", err);
+ goto finalize;
+ }
+
+finalize:
+ if (err) {
+ instance->stopping = true;
+ cancel_work_sync(&instance->drdy_handler);
+ mutex_destroy(&instance->comm_lock);
+ pimidi_unregister(instance);
+ snd_card_free(card);
+ return err;
+ }
+
+ for (i = 0; i < PIMIDI_PORTS; ++i) {
+ struct pimidi_midi_port *port = &instance->midi_port[i];
+
+ mutex_init(&port->in_lock);
+ mutex_init(&port->out_lock);
+ INIT_WORK(&port->in_handler,
+ i == 0 ? pimidi_midi_in_handler_0 : pimidi_midi_in_handler_1);
+ INIT_DELAYED_WORK(&port->out_handler,
+ i == 0 ? pimidi_midi_out_handler_0 : pimidi_midi_out_handler_1);
+ INIT_KFIFO(port->in_fifo);
+ port->last_output_at = jiffies;
+ }
+
+ i2c_set_clientdata(client, card);
+ return 0;
+}
+
+static void pimidi_i2c_remove(struct i2c_client *client)
+{
+ printd_g("(%p)", client);
+
+ int i;
+ struct snd_card *card = i2c_get_clientdata(client);
+
+ if (card) {
+ printi_g("Unloading hw:%d %s", card->number, card->longname);
+ struct pimidi_instance *instance = (struct pimidi_instance *)card->private_data;
+
+ instance->stopping = true;
+ i2c_set_clientdata(client, NULL);
+ devm_free_irq(&client->dev, client->irq, instance);
+ cancel_work_sync(&instance->drdy_handler);
+
+ for (i = 0; i < PIMIDI_PORTS; ++i) {
+ cancel_work_sync(&instance->midi_port[i].in_handler);
+ cancel_delayed_work_sync(&instance->midi_port[i].out_handler);
+ mutex_destroy(&instance->midi_port[i].out_lock);
+ mutex_destroy(&instance->midi_port[i].in_lock);
+ kfifo_free(&instance->midi_port[i].in_fifo);
+ }
+
+ mutex_destroy(&instance->comm_lock);
+ pimidi_unregister(instance);
+ snd_card_free(card);
+ }
+}
+
+static const struct i2c_device_id pimidi_i2c_ids[] = {
+ { "pimidi", 0 },
+ {}
+};
+MODULE_DEVICE_TABLE(i2c, pimidi_i2c_ids);
+
+static const struct of_device_id pimidi_i2c_dt_ids[] = {
+ { .compatible = "blokaslabs,pimidi", },
+ {}
+};
+MODULE_DEVICE_TABLE(of, pimidi_i2c_dt_ids);
+
+static struct i2c_driver pimidi_i2c_driver = {
+ .driver = {
+ .name = "pimidi",
+ .owner = THIS_MODULE,
+ .of_match_table = of_match_ptr(pimidi_i2c_dt_ids),
+ },
+ .probe = pimidi_i2c_probe,
+ .remove = pimidi_i2c_remove,
+ .id_table = pimidi_i2c_ids,
+};
+
+int pimidi_module_init(void)
+{
+ int err = 0;
+
+ mutex_init(&pimidi_global.lock);
+
+ INIT_LIST_HEAD(&pimidi_global.devices);
+
+ pimidi_global.work_queue = create_singlethread_workqueue("pimidi");
+ if (!pimidi_global.work_queue) {
+ err = -ENOMEM;
+ goto cleanup;
+ }
+
+ err = i2c_add_driver(&pimidi_i2c_driver);
+ if (err < 0)
+ goto cleanup;
+
+ crc8_populate_msb(pimidi_crc8_table, PIMIDI_CRC8_POLYNOMIAL);
+
+ return 0;
+
+cleanup:
+ mutex_destroy(&pimidi_global.lock);
+ return err;
+}
+
+void pimidi_module_exit(void)
+{
+ i2c_del_driver(&pimidi_i2c_driver);
+ mutex_lock(&pimidi_global.lock);
+ if (pimidi_global.reset_gpio) {
+ gpiod_put(pimidi_global.reset_gpio);
+ pimidi_global.reset_gpio = NULL;
+ }
+ mutex_unlock(&pimidi_global.lock);
+
+ destroy_workqueue(pimidi_global.work_queue);
+ pimidi_global.work_queue = NULL;
+
+ mutex_destroy(&pimidi_global.lock);
+}
+
+module_init(pimidi_module_init);
+module_exit(pimidi_module_exit);
+
+MODULE_AUTHOR("Giedrius Trainavi\xc4\x8dius <giedrius@blokas.io>");
+MODULE_DESCRIPTION("MIDI driver for Blokas Pimidi, https://blokas.io/");
+MODULE_LICENSE("GPL");
+
+/* vim: set ts=8 sw=8 noexpandtab: */