forked from Minki/linux
acfedcbe1c
Some devices have a quirk to postpone transmission of isoc packet for several dozen or hundred isoc cycles since configured to transmit. Furthermore, some devices have a quirk to transmit isoc packet with discontinued data of its header. In 1394 OHCI specification, software allows to start isoc context with certain isoc cycle. Linux firewire subsystem has kernel API to use it as well. This commit uses the functionality of 1394 OHCI controller to handle the quirks. At present, this feature is convenient to ALSA bebob and fireface driver. As a result, some devices can be safely handled, as long as I know: - MAudio FireWire solo - MAudio ProFire Lightbridge - MAudio FireWire 410 - Roland FA-66 Signed-off-by: Takashi Sakamoto <o-takashi@sakamocchi.jp> Link: https://lore.kernel.org/r/20191018061911.24909-7-o-takashi@sakamocchi.jp Signed-off-by: Takashi Iwai <tiwai@suse.de>
546 lines
12 KiB
C
546 lines
12 KiB
C
// SPDX-License-Identifier: GPL-2.0-only
|
|
/*
|
|
* tascam-stream.c - a part of driver for TASCAM FireWire series
|
|
*
|
|
* Copyright (c) 2015 Takashi Sakamoto
|
|
*/
|
|
|
|
#include <linux/delay.h>
|
|
#include "tascam.h"
|
|
|
|
#define CLOCK_STATUS_MASK 0xffff0000
|
|
#define CLOCK_CONFIG_MASK 0x0000ffff
|
|
|
|
#define CALLBACK_TIMEOUT 500
|
|
|
|
static int get_clock(struct snd_tscm *tscm, u32 *data)
|
|
{
|
|
int trial = 0;
|
|
__be32 reg;
|
|
int err;
|
|
|
|
while (trial++ < 5) {
|
|
err = snd_fw_transaction(tscm->unit, TCODE_READ_QUADLET_REQUEST,
|
|
TSCM_ADDR_BASE + TSCM_OFFSET_CLOCK_STATUS,
|
|
®, sizeof(reg), 0);
|
|
if (err < 0)
|
|
return err;
|
|
|
|
*data = be32_to_cpu(reg);
|
|
if (*data & CLOCK_STATUS_MASK)
|
|
break;
|
|
|
|
// In intermediate state after changing clock status.
|
|
msleep(50);
|
|
}
|
|
|
|
// Still in the intermediate state.
|
|
if (trial >= 5)
|
|
return -EAGAIN;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int set_clock(struct snd_tscm *tscm, unsigned int rate,
|
|
enum snd_tscm_clock clock)
|
|
{
|
|
u32 data;
|
|
__be32 reg;
|
|
int err;
|
|
|
|
err = get_clock(tscm, &data);
|
|
if (err < 0)
|
|
return err;
|
|
data &= CLOCK_CONFIG_MASK;
|
|
|
|
if (rate > 0) {
|
|
data &= 0x000000ff;
|
|
/* Base rate. */
|
|
if ((rate % 44100) == 0) {
|
|
data |= 0x00000100;
|
|
/* Multiplier. */
|
|
if (rate / 44100 == 2)
|
|
data |= 0x00008000;
|
|
} else if ((rate % 48000) == 0) {
|
|
data |= 0x00000200;
|
|
/* Multiplier. */
|
|
if (rate / 48000 == 2)
|
|
data |= 0x00008000;
|
|
} else {
|
|
return -EAGAIN;
|
|
}
|
|
}
|
|
|
|
if (clock != INT_MAX) {
|
|
data &= 0x0000ff00;
|
|
data |= clock + 1;
|
|
}
|
|
|
|
reg = cpu_to_be32(data);
|
|
|
|
err = snd_fw_transaction(tscm->unit, TCODE_WRITE_QUADLET_REQUEST,
|
|
TSCM_ADDR_BASE + TSCM_OFFSET_CLOCK_STATUS,
|
|
®, sizeof(reg), 0);
|
|
if (err < 0)
|
|
return err;
|
|
|
|
if (data & 0x00008000)
|
|
reg = cpu_to_be32(0x0000001a);
|
|
else
|
|
reg = cpu_to_be32(0x0000000d);
|
|
|
|
return snd_fw_transaction(tscm->unit, TCODE_WRITE_QUADLET_REQUEST,
|
|
TSCM_ADDR_BASE + TSCM_OFFSET_MULTIPLEX_MODE,
|
|
®, sizeof(reg), 0);
|
|
}
|
|
|
|
int snd_tscm_stream_get_rate(struct snd_tscm *tscm, unsigned int *rate)
|
|
{
|
|
u32 data;
|
|
int err;
|
|
|
|
err = get_clock(tscm, &data);
|
|
if (err < 0)
|
|
return err;
|
|
|
|
data = (data & 0xff000000) >> 24;
|
|
|
|
/* Check base rate. */
|
|
if ((data & 0x0f) == 0x01)
|
|
*rate = 44100;
|
|
else if ((data & 0x0f) == 0x02)
|
|
*rate = 48000;
|
|
else
|
|
return -EAGAIN;
|
|
|
|
/* Check multiplier. */
|
|
if ((data & 0xf0) == 0x80)
|
|
*rate *= 2;
|
|
else if ((data & 0xf0) != 0x00)
|
|
return -EAGAIN;
|
|
|
|
return err;
|
|
}
|
|
|
|
int snd_tscm_stream_get_clock(struct snd_tscm *tscm, enum snd_tscm_clock *clock)
|
|
{
|
|
u32 data;
|
|
int err;
|
|
|
|
err = get_clock(tscm, &data);
|
|
if (err < 0)
|
|
return err;
|
|
|
|
*clock = ((data & 0x00ff0000) >> 16) - 1;
|
|
if (*clock < 0 || *clock > SND_TSCM_CLOCK_ADAT)
|
|
return -EIO;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int enable_data_channels(struct snd_tscm *tscm)
|
|
{
|
|
__be32 reg;
|
|
u32 data;
|
|
unsigned int i;
|
|
int err;
|
|
|
|
data = 0;
|
|
for (i = 0; i < tscm->spec->pcm_capture_analog_channels; ++i)
|
|
data |= BIT(i);
|
|
if (tscm->spec->has_adat)
|
|
data |= 0x0000ff00;
|
|
if (tscm->spec->has_spdif)
|
|
data |= 0x00030000;
|
|
|
|
reg = cpu_to_be32(data);
|
|
err = snd_fw_transaction(tscm->unit, TCODE_WRITE_QUADLET_REQUEST,
|
|
TSCM_ADDR_BASE + TSCM_OFFSET_TX_PCM_CHANNELS,
|
|
®, sizeof(reg), 0);
|
|
if (err < 0)
|
|
return err;
|
|
|
|
data = 0;
|
|
for (i = 0; i < tscm->spec->pcm_playback_analog_channels; ++i)
|
|
data |= BIT(i);
|
|
if (tscm->spec->has_adat)
|
|
data |= 0x0000ff00;
|
|
if (tscm->spec->has_spdif)
|
|
data |= 0x00030000;
|
|
|
|
reg = cpu_to_be32(data);
|
|
return snd_fw_transaction(tscm->unit, TCODE_WRITE_QUADLET_REQUEST,
|
|
TSCM_ADDR_BASE + TSCM_OFFSET_RX_PCM_CHANNELS,
|
|
®, sizeof(reg), 0);
|
|
}
|
|
|
|
static int set_stream_formats(struct snd_tscm *tscm, unsigned int rate)
|
|
{
|
|
__be32 reg;
|
|
int err;
|
|
|
|
// Set an option for unknown purpose.
|
|
reg = cpu_to_be32(0x00200000);
|
|
err = snd_fw_transaction(tscm->unit, TCODE_WRITE_QUADLET_REQUEST,
|
|
TSCM_ADDR_BASE + TSCM_OFFSET_SET_OPTION,
|
|
®, sizeof(reg), 0);
|
|
if (err < 0)
|
|
return err;
|
|
|
|
return enable_data_channels(tscm);
|
|
}
|
|
|
|
static void finish_session(struct snd_tscm *tscm)
|
|
{
|
|
__be32 reg;
|
|
|
|
reg = 0;
|
|
snd_fw_transaction(tscm->unit, TCODE_WRITE_QUADLET_REQUEST,
|
|
TSCM_ADDR_BASE + TSCM_OFFSET_START_STREAMING,
|
|
®, sizeof(reg), 0);
|
|
|
|
reg = 0;
|
|
snd_fw_transaction(tscm->unit, TCODE_WRITE_QUADLET_REQUEST,
|
|
TSCM_ADDR_BASE + TSCM_OFFSET_ISOC_RX_ON,
|
|
®, sizeof(reg), 0);
|
|
|
|
// Unregister channels.
|
|
reg = cpu_to_be32(0x00000000);
|
|
snd_fw_transaction(tscm->unit, TCODE_WRITE_QUADLET_REQUEST,
|
|
TSCM_ADDR_BASE + TSCM_OFFSET_ISOC_TX_CH,
|
|
®, sizeof(reg), 0);
|
|
reg = cpu_to_be32(0x00000000);
|
|
snd_fw_transaction(tscm->unit, TCODE_WRITE_QUADLET_REQUEST,
|
|
TSCM_ADDR_BASE + TSCM_OFFSET_UNKNOWN,
|
|
®, sizeof(reg), 0);
|
|
reg = cpu_to_be32(0x00000000);
|
|
snd_fw_transaction(tscm->unit, TCODE_WRITE_QUADLET_REQUEST,
|
|
TSCM_ADDR_BASE + TSCM_OFFSET_ISOC_RX_CH,
|
|
®, sizeof(reg), 0);
|
|
}
|
|
|
|
static int begin_session(struct snd_tscm *tscm)
|
|
{
|
|
__be32 reg;
|
|
int err;
|
|
|
|
// Register the isochronous channel for transmitting stream.
|
|
reg = cpu_to_be32(tscm->tx_resources.channel);
|
|
err = snd_fw_transaction(tscm->unit, TCODE_WRITE_QUADLET_REQUEST,
|
|
TSCM_ADDR_BASE + TSCM_OFFSET_ISOC_TX_CH,
|
|
®, sizeof(reg), 0);
|
|
if (err < 0)
|
|
return err;
|
|
|
|
// Unknown.
|
|
reg = cpu_to_be32(0x00000002);
|
|
err = snd_fw_transaction(tscm->unit, TCODE_WRITE_QUADLET_REQUEST,
|
|
TSCM_ADDR_BASE + TSCM_OFFSET_UNKNOWN,
|
|
®, sizeof(reg), 0);
|
|
if (err < 0)
|
|
return err;
|
|
|
|
// Register the isochronous channel for receiving stream.
|
|
reg = cpu_to_be32(tscm->rx_resources.channel);
|
|
err = snd_fw_transaction(tscm->unit, TCODE_WRITE_QUADLET_REQUEST,
|
|
TSCM_ADDR_BASE + TSCM_OFFSET_ISOC_RX_CH,
|
|
®, sizeof(reg), 0);
|
|
if (err < 0)
|
|
return err;
|
|
|
|
reg = cpu_to_be32(0x00000001);
|
|
err = snd_fw_transaction(tscm->unit, TCODE_WRITE_QUADLET_REQUEST,
|
|
TSCM_ADDR_BASE + TSCM_OFFSET_START_STREAMING,
|
|
®, sizeof(reg), 0);
|
|
if (err < 0)
|
|
return err;
|
|
|
|
reg = cpu_to_be32(0x00000001);
|
|
err = snd_fw_transaction(tscm->unit, TCODE_WRITE_QUADLET_REQUEST,
|
|
TSCM_ADDR_BASE + TSCM_OFFSET_ISOC_RX_ON,
|
|
®, sizeof(reg), 0);
|
|
if (err < 0)
|
|
return err;
|
|
|
|
// Set an option for unknown purpose.
|
|
reg = cpu_to_be32(0x00002000);
|
|
err = snd_fw_transaction(tscm->unit, TCODE_WRITE_QUADLET_REQUEST,
|
|
TSCM_ADDR_BASE + TSCM_OFFSET_SET_OPTION,
|
|
®, sizeof(reg), 0);
|
|
if (err < 0)
|
|
return err;
|
|
|
|
// Start multiplexing PCM samples on packets.
|
|
reg = cpu_to_be32(0x00000001);
|
|
return snd_fw_transaction(tscm->unit,
|
|
TCODE_WRITE_QUADLET_REQUEST,
|
|
TSCM_ADDR_BASE + TSCM_OFFSET_ISOC_TX_ON,
|
|
®, sizeof(reg), 0);
|
|
}
|
|
|
|
static int keep_resources(struct snd_tscm *tscm, unsigned int rate,
|
|
struct amdtp_stream *stream)
|
|
{
|
|
struct fw_iso_resources *resources;
|
|
int err;
|
|
|
|
if (stream == &tscm->tx_stream)
|
|
resources = &tscm->tx_resources;
|
|
else
|
|
resources = &tscm->rx_resources;
|
|
|
|
err = amdtp_tscm_set_parameters(stream, rate);
|
|
if (err < 0)
|
|
return err;
|
|
|
|
return fw_iso_resources_allocate(resources,
|
|
amdtp_stream_get_max_payload(stream),
|
|
fw_parent_device(tscm->unit)->max_speed);
|
|
}
|
|
|
|
static int init_stream(struct snd_tscm *tscm, struct amdtp_stream *s)
|
|
{
|
|
struct fw_iso_resources *resources;
|
|
enum amdtp_stream_direction dir;
|
|
unsigned int pcm_channels;
|
|
int err;
|
|
|
|
if (s == &tscm->tx_stream) {
|
|
resources = &tscm->tx_resources;
|
|
dir = AMDTP_IN_STREAM;
|
|
pcm_channels = tscm->spec->pcm_capture_analog_channels;
|
|
} else {
|
|
resources = &tscm->rx_resources;
|
|
dir = AMDTP_OUT_STREAM;
|
|
pcm_channels = tscm->spec->pcm_playback_analog_channels;
|
|
}
|
|
|
|
if (tscm->spec->has_adat)
|
|
pcm_channels += 8;
|
|
if (tscm->spec->has_spdif)
|
|
pcm_channels += 2;
|
|
|
|
err = fw_iso_resources_init(resources, tscm->unit);
|
|
if (err < 0)
|
|
return err;
|
|
|
|
err = amdtp_tscm_init(s, tscm->unit, dir, pcm_channels);
|
|
if (err < 0)
|
|
fw_iso_resources_free(resources);
|
|
|
|
return err;
|
|
}
|
|
|
|
static void destroy_stream(struct snd_tscm *tscm, struct amdtp_stream *s)
|
|
{
|
|
amdtp_stream_destroy(s);
|
|
|
|
if (s == &tscm->tx_stream)
|
|
fw_iso_resources_destroy(&tscm->tx_resources);
|
|
else
|
|
fw_iso_resources_destroy(&tscm->rx_resources);
|
|
}
|
|
|
|
int snd_tscm_stream_init_duplex(struct snd_tscm *tscm)
|
|
{
|
|
int err;
|
|
|
|
err = init_stream(tscm, &tscm->tx_stream);
|
|
if (err < 0)
|
|
return err;
|
|
|
|
err = init_stream(tscm, &tscm->rx_stream);
|
|
if (err < 0) {
|
|
destroy_stream(tscm, &tscm->tx_stream);
|
|
return err;
|
|
}
|
|
|
|
err = amdtp_domain_init(&tscm->domain);
|
|
if (err < 0) {
|
|
destroy_stream(tscm, &tscm->tx_stream);
|
|
destroy_stream(tscm, &tscm->rx_stream);
|
|
}
|
|
|
|
return err;
|
|
}
|
|
|
|
// At bus reset, streaming is stopped and some registers are clear.
|
|
void snd_tscm_stream_update_duplex(struct snd_tscm *tscm)
|
|
{
|
|
amdtp_domain_stop(&tscm->domain);
|
|
|
|
amdtp_stream_pcm_abort(&tscm->tx_stream);
|
|
amdtp_stream_pcm_abort(&tscm->rx_stream);
|
|
}
|
|
|
|
// This function should be called before starting streams or after stopping
|
|
// streams.
|
|
void snd_tscm_stream_destroy_duplex(struct snd_tscm *tscm)
|
|
{
|
|
amdtp_domain_destroy(&tscm->domain);
|
|
|
|
destroy_stream(tscm, &tscm->rx_stream);
|
|
destroy_stream(tscm, &tscm->tx_stream);
|
|
}
|
|
|
|
int snd_tscm_stream_reserve_duplex(struct snd_tscm *tscm, unsigned int rate,
|
|
unsigned int frames_per_period,
|
|
unsigned int frames_per_buffer)
|
|
{
|
|
unsigned int curr_rate;
|
|
int err;
|
|
|
|
err = snd_tscm_stream_get_rate(tscm, &curr_rate);
|
|
if (err < 0)
|
|
return err;
|
|
|
|
if (tscm->substreams_counter == 0 || rate != curr_rate) {
|
|
amdtp_domain_stop(&tscm->domain);
|
|
|
|
finish_session(tscm);
|
|
|
|
fw_iso_resources_free(&tscm->tx_resources);
|
|
fw_iso_resources_free(&tscm->rx_resources);
|
|
|
|
err = set_clock(tscm, rate, INT_MAX);
|
|
if (err < 0)
|
|
return err;
|
|
|
|
err = keep_resources(tscm, rate, &tscm->tx_stream);
|
|
if (err < 0)
|
|
return err;
|
|
|
|
err = keep_resources(tscm, rate, &tscm->rx_stream);
|
|
if (err < 0) {
|
|
fw_iso_resources_free(&tscm->tx_resources);
|
|
return err;
|
|
}
|
|
|
|
err = amdtp_domain_set_events_per_period(&tscm->domain,
|
|
frames_per_period, frames_per_buffer);
|
|
if (err < 0) {
|
|
fw_iso_resources_free(&tscm->tx_resources);
|
|
fw_iso_resources_free(&tscm->rx_resources);
|
|
return err;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int snd_tscm_stream_start_duplex(struct snd_tscm *tscm, unsigned int rate)
|
|
{
|
|
unsigned int generation = tscm->rx_resources.generation;
|
|
int err;
|
|
|
|
if (tscm->substreams_counter == 0)
|
|
return 0;
|
|
|
|
if (amdtp_streaming_error(&tscm->rx_stream) ||
|
|
amdtp_streaming_error(&tscm->tx_stream)) {
|
|
amdtp_domain_stop(&tscm->domain);
|
|
finish_session(tscm);
|
|
}
|
|
|
|
if (generation != fw_parent_device(tscm->unit)->card->generation) {
|
|
err = fw_iso_resources_update(&tscm->tx_resources);
|
|
if (err < 0)
|
|
goto error;
|
|
|
|
err = fw_iso_resources_update(&tscm->rx_resources);
|
|
if (err < 0)
|
|
goto error;
|
|
}
|
|
|
|
if (!amdtp_stream_running(&tscm->rx_stream)) {
|
|
int spd = fw_parent_device(tscm->unit)->max_speed;
|
|
|
|
err = set_stream_formats(tscm, rate);
|
|
if (err < 0)
|
|
goto error;
|
|
|
|
err = begin_session(tscm);
|
|
if (err < 0)
|
|
goto error;
|
|
|
|
err = amdtp_domain_add_stream(&tscm->domain, &tscm->rx_stream,
|
|
tscm->rx_resources.channel, spd);
|
|
if (err < 0)
|
|
goto error;
|
|
|
|
err = amdtp_domain_add_stream(&tscm->domain, &tscm->tx_stream,
|
|
tscm->tx_resources.channel, spd);
|
|
if (err < 0)
|
|
goto error;
|
|
|
|
err = amdtp_domain_start(&tscm->domain, 0);
|
|
if (err < 0)
|
|
return err;
|
|
|
|
if (!amdtp_stream_wait_callback(&tscm->rx_stream,
|
|
CALLBACK_TIMEOUT) ||
|
|
!amdtp_stream_wait_callback(&tscm->tx_stream,
|
|
CALLBACK_TIMEOUT)) {
|
|
err = -ETIMEDOUT;
|
|
goto error;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
error:
|
|
amdtp_domain_stop(&tscm->domain);
|
|
finish_session(tscm);
|
|
|
|
return err;
|
|
}
|
|
|
|
void snd_tscm_stream_stop_duplex(struct snd_tscm *tscm)
|
|
{
|
|
if (tscm->substreams_counter == 0) {
|
|
amdtp_domain_stop(&tscm->domain);
|
|
finish_session(tscm);
|
|
|
|
fw_iso_resources_free(&tscm->tx_resources);
|
|
fw_iso_resources_free(&tscm->rx_resources);
|
|
}
|
|
}
|
|
|
|
void snd_tscm_stream_lock_changed(struct snd_tscm *tscm)
|
|
{
|
|
tscm->dev_lock_changed = true;
|
|
wake_up(&tscm->hwdep_wait);
|
|
}
|
|
|
|
int snd_tscm_stream_lock_try(struct snd_tscm *tscm)
|
|
{
|
|
int err;
|
|
|
|
spin_lock_irq(&tscm->lock);
|
|
|
|
/* user land lock this */
|
|
if (tscm->dev_lock_count < 0) {
|
|
err = -EBUSY;
|
|
goto end;
|
|
}
|
|
|
|
/* this is the first time */
|
|
if (tscm->dev_lock_count++ == 0)
|
|
snd_tscm_stream_lock_changed(tscm);
|
|
err = 0;
|
|
end:
|
|
spin_unlock_irq(&tscm->lock);
|
|
return err;
|
|
}
|
|
|
|
void snd_tscm_stream_lock_release(struct snd_tscm *tscm)
|
|
{
|
|
spin_lock_irq(&tscm->lock);
|
|
|
|
if (WARN_ON(tscm->dev_lock_count <= 0))
|
|
goto end;
|
|
if (--tscm->dev_lock_count == 0)
|
|
snd_tscm_stream_lock_changed(tscm);
|
|
end:
|
|
spin_unlock_irq(&tscm->lock);
|
|
}
|