mirror of https://github.com/Qortal/Brooklyn
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1241 lines
26 KiB
1241 lines
26 KiB
/* |
|
* Pisound Linux kernel module. |
|
* Copyright (C) 2016-2020 Vilniaus Blokas UAB, https://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 <linux/jiffies.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_flush(void); |
|
static void pisnd_spi_start(void); |
|
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_fw_version(void); |
|
static const char *pisnd_spi_get_hw_version(void); |
|
|
|
static int pisnd_midi_init(struct snd_card *card); |
|
static void pisnd_midi_uninit(void); |
|
|
|
enum task_e { |
|
TASK_PROCESS = 0, |
|
}; |
|
|
|
static void pisnd_schedule_process(enum task_e task); |
|
|
|
#define PISOUND_LOG_PREFIX "pisound: " |
|
|
|
#ifdef PISOUND_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 struct snd_rawmidi *g_rmidi; |
|
static struct snd_rawmidi_substream *g_midi_output_substream; |
|
|
|
static int pisnd_output_open(struct snd_rawmidi_substream *substream) |
|
{ |
|
g_midi_output_substream = substream; |
|
return 0; |
|
} |
|
|
|
static int pisnd_output_close(struct snd_rawmidi_substream *substream) |
|
{ |
|
g_midi_output_substream = NULL; |
|
return 0; |
|
} |
|
|
|
static void pisnd_output_trigger( |
|
struct snd_rawmidi_substream *substream, |
|
int up |
|
) |
|
{ |
|
if (substream != g_midi_output_substream) { |
|
printe("MIDI output trigger called for an unexpected stream!"); |
|
return; |
|
} |
|
|
|
if (!up) |
|
return; |
|
|
|
pisnd_spi_start(); |
|
} |
|
|
|
static void pisnd_output_drain(struct snd_rawmidi_substream *substream) |
|
{ |
|
pisnd_spi_flush(); |
|
} |
|
|
|
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 %u bytes, res = %d\n", n, 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_schedule_process(TASK_PROCESS); |
|
} else { |
|
pisnd_spi_set_callback(NULL, NULL); |
|
} |
|
} |
|
|
|
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; |
|
|
|
g_midi_output_substream = NULL; |
|
|
|
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 4096 |
|
|
|
static char g_serial_num[11]; |
|
static char g_id[25]; |
|
enum { MAX_VERSION_STR_LEN = 6 }; |
|
static char g_fw_version[MAX_VERSION_STR_LEN]; |
|
static char g_hw_version[MAX_VERSION_STR_LEN]; |
|
|
|
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 void spi_transfer(const uint8_t *txbuf, uint8_t *rxbuf, int len); |
|
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); |
|
} |
|
|
|
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 uint16_t spi_transfer16(uint16_t val) |
|
{ |
|
uint8_t txbuf[2]; |
|
uint8_t rxbuf[2]; |
|
|
|
if (!pisnd_spi_device) { |
|
printe("pisnd_spi_device null, returning\n"); |
|
return 0; |
|
} |
|
|
|
txbuf[0] = val >> 8; |
|
txbuf[1] = val & 0xff; |
|
|
|
spi_transfer(txbuf, rxbuf, sizeof(txbuf)); |
|
|
|
printd("received: %02x%02x\n", rxbuf[0], rxbuf[1]); |
|
|
|
return (rxbuf[0] << 8) | rxbuf[1]; |
|
} |
|
|
|
static void spi_transfer(const uint8_t *txbuf, uint8_t *rxbuf, int len) |
|
{ |
|
int err; |
|
struct spi_transfer transfer; |
|
struct spi_message msg; |
|
|
|
memset(rxbuf, 0, len); |
|
|
|
if (!pisnd_spi_device) { |
|
printe("pisnd_spi_device null, returning\n"); |
|
return; |
|
} |
|
|
|
spi_message_init(&msg); |
|
|
|
memset(&transfer, 0, sizeof(transfer)); |
|
|
|
transfer.tx_buf = txbuf; |
|
transfer.rx_buf = rxbuf; |
|
transfer.len = len; |
|
transfer.speed_hz = 150000; |
|
transfer.delay.value = 10; |
|
transfer.delay.unit = SPI_DELAY_UNIT_USECS; |
|
|
|
spi_message_add_tail(&transfer, &msg); |
|
|
|
err = spi_sync(pisnd_spi_device, &msg); |
|
|
|
if (err < 0) { |
|
printe("spi_sync error %d\n", err); |
|
return; |
|
} |
|
|
|
printd("hasMore %d\n", pisnd_spi_has_more()); |
|
} |
|
|
|
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, const 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) |
|
{ |
|
enum { TRANSFER_SIZE = 4 }; |
|
enum { PISOUND_OUTPUT_BUFFER_SIZE_MILLIBYTES = 127 * 1000 }; |
|
enum { MIDI_MILLIBYTES_PER_JIFFIE = (3125 * 1000) / HZ }; |
|
int out_buffer_used_millibytes = 0; |
|
unsigned long now; |
|
uint8_t val; |
|
uint8_t txbuf[TRANSFER_SIZE]; |
|
uint8_t rxbuf[TRANSFER_SIZE]; |
|
uint8_t midibuf[TRANSFER_SIZE]; |
|
int i, n; |
|
bool had_data; |
|
|
|
unsigned long last_transfer_at = jiffies; |
|
|
|
if (work == &pisnd_work_process) { |
|
if (pisnd_spi_device == NULL) |
|
return; |
|
|
|
do { |
|
if (g_midi_output_substream && |
|
kfifo_avail(&spi_fifo_out) >= sizeof(midibuf)) { |
|
|
|
n = snd_rawmidi_transmit_peek( |
|
g_midi_output_substream, |
|
midibuf, sizeof(midibuf) |
|
); |
|
|
|
if (n > 0) { |
|
for (i = 0; i < n; ++i) |
|
kfifo_put( |
|
&spi_fifo_out, |
|
midibuf[i] |
|
); |
|
snd_rawmidi_transmit_ack( |
|
g_midi_output_substream, |
|
i |
|
); |
|
} |
|
} |
|
|
|
had_data = false; |
|
memset(txbuf, 0, sizeof(txbuf)); |
|
for (i = 0; i < sizeof(txbuf) && |
|
((out_buffer_used_millibytes+1000 < |
|
PISOUND_OUTPUT_BUFFER_SIZE_MILLIBYTES) || |
|
g_ledFlashDurationChanged); |
|
i += 2) { |
|
|
|
val = 0; |
|
|
|
if (g_ledFlashDurationChanged) { |
|
txbuf[i+0] = 0xf0; |
|
txbuf[i+1] = g_ledFlashDuration; |
|
g_ledFlashDuration = 0; |
|
g_ledFlashDurationChanged = false; |
|
} else if (kfifo_get(&spi_fifo_out, &val)) { |
|
txbuf[i+0] = 0x0f; |
|
txbuf[i+1] = val; |
|
out_buffer_used_millibytes += 1000; |
|
} |
|
} |
|
|
|
spi_transfer(txbuf, rxbuf, sizeof(txbuf)); |
|
/* Estimate the Pisound's MIDI output buffer usage, so |
|
* that we don't overflow it. Space in the buffer should |
|
* be becoming available at the UART MIDI byte transfer |
|
* rate. |
|
*/ |
|
now = jiffies; |
|
if (now != last_transfer_at) { |
|
out_buffer_used_millibytes -= |
|
(now - last_transfer_at) * |
|
MIDI_MILLIBYTES_PER_JIFFIE; |
|
if (out_buffer_used_millibytes < 0) |
|
out_buffer_used_millibytes = 0; |
|
last_transfer_at = now; |
|
} |
|
|
|
for (i = 0; i < sizeof(rxbuf); i += 2) { |
|
if (rxbuf[i]) { |
|
kfifo_put(&spi_fifo_in, rxbuf[i+1]); |
|
if (kfifo_len(&spi_fifo_in) > 16 && |
|
g_recvCallback) |
|
g_recvCallback(g_recvData); |
|
had_data = true; |
|
} |
|
} |
|
} while (had_data |
|
|| !kfifo_is_empty(&spi_fifo_out) |
|
|| pisnd_spi_has_more() |
|
|| g_ledFlashDurationChanged |
|
|| out_buffer_used_millibytes != 0 |
|
); |
|
|
|
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_threaded_irq( |
|
gpiod_to_irq(data_available), NULL, |
|
data_available_interrupt_handler, |
|
IRQF_TIMER | IRQF_TRIGGER_RISING | IRQF_ONESHOT, |
|
"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_fw_version, 0, sizeof(g_fw_version)); |
|
strcpy(g_hw_version, "1.0"); // Assume 1.0 hw 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_fw_version, |
|
MAX_VERSION_STR_LEN, |
|
"%x.%02x", |
|
buffer[0], |
|
buffer[1] |
|
); |
|
|
|
g_fw_version[MAX_VERSION_STR_LEN-1] = '\0'; |
|
break; |
|
case 3: |
|
if (n != 2) |
|
return -EINVAL; |
|
|
|
snprintf( |
|
g_hw_version, |
|
MAX_VERSION_STR_LEN, |
|
"%x.%x", |
|
buffer[0], |
|
buffer[1] |
|
); |
|
|
|
g_hw_version[MAX_VERSION_STR_LEN-1] = '\0'; |
|
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*2 >= sizeof(g_id)) |
|
return -EINVAL; |
|
|
|
p = g_id; |
|
for (j = 0; j < n; ++j) |
|
p += sprintf(p, "%02x", buffer[j]); |
|
|
|
*p = '\0'; |
|
} |
|
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_fw_version, 0, sizeof(g_fw_version)); |
|
memset(g_hw_version, 0, sizeof(g_hw_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_flush(void) |
|
{ |
|
while (!kfifo_is_empty(&spi_fifo_out)) { |
|
pisnd_spi_start(); |
|
flush_workqueue(pisnd_workqueue); |
|
} |
|
} |
|
|
|
static void pisnd_spi_start(void) |
|
{ |
|
printd("schedule from spi_start\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) |
|
{ |
|
return g_serial_num; |
|
} |
|
|
|
static const char *pisnd_spi_get_id(void) |
|
{ |
|
return g_id; |
|
} |
|
|
|
static const char *pisnd_spi_get_fw_version(void) |
|
{ |
|
return g_fw_version; |
|
} |
|
|
|
static const char *pisnd_spi_get_hw_version(void) |
|
{ |
|
return g_hw_version; |
|
} |
|
|
|
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 = asoc_rtd_to_cpu(rtd, 0); |
|
|
|
/* 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, false); |
|
gpiod_set_value(osr2, true); |
|
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, |
|
}; |
|
|
|
SND_SOC_DAILINK_DEFS(pisnd, |
|
DAILINK_COMP_ARRAY(COMP_CPU("bcm2708-i2s.0")), |
|
DAILINK_COMP_ARRAY(COMP_DUMMY()), |
|
DAILINK_COMP_ARRAY(COMP_PLATFORM("bcm2708-i2s.0"))); |
|
|
|
static struct snd_soc_dai_link pisnd_dai[] = { |
|
{ |
|
.name = "pisound", |
|
.stream_name = "pisound", |
|
.dai_fmt = |
|
SND_SOC_DAIFMT_I2S | |
|
SND_SOC_DAIFMT_NB_NF | |
|
SND_SOC_DAIFMT_CBM_CFM, |
|
.ops = &pisnd_ops, |
|
SND_SOC_DAILINK_REG(pisnd), |
|
}, |
|
}; |
|
|
|
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_fw_version_show( |
|
struct kobject *kobj, |
|
struct kobj_attribute *attr, |
|
char *buf |
|
) |
|
{ |
|
return sprintf(buf, "%s\n", pisnd_spi_get_fw_version()); |
|
} |
|
|
|
static ssize_t pisnd_hw_version_show( |
|
struct kobject *kobj, |
|
struct kobj_attribute *attr, |
|
char *buf |
|
) |
|
{ |
|
return sprintf(buf, "%s\n", pisnd_spi_get_hw_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_fw_version_attribute = |
|
__ATTR(version, 0444, pisnd_fw_version_show, NULL); |
|
static struct kobj_attribute pisnd_hw_version_attribute = |
|
__ATTR(hw_version, 0444, pisnd_hw_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_fw_version_attribute.attr, |
|
&pisnd_hw_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("\tFirmware Version: %s\n", pisnd_spi_get_fw_version()); |
|
printi("\tHardware Version: %s\n", pisnd_spi_get_hw_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->cpus->dai_name = NULL; |
|
dai->cpus->of_node = i2s_node; |
|
dai->platforms->name = NULL; |
|
dai->platforms->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(); |
|
|
|
snd_soc_unregister_card(&pisnd_card); |
|
return 0; |
|
} |
|
|
|
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 <[email protected]>"); |
|
MODULE_DESCRIPTION("ASoC Driver for Pisound, https://blokas.io/pisound"); |
|
MODULE_LICENSE("GPL v2");
|
|
|