mirror of
https://github.com/torvalds/linux.git
synced 2024-12-02 00:51:44 +00:00
a9dd8a61b6
This commit takes ALSA firewire-tascam driver to perform sequence replay for media clock recovery. The protocol specific to Tascam FireWire series is not compliant to IEC 61883-1/6 in terms of syt field of CIP. The protocol doesn't use presentation time in received CIP for playback timing. The sequence of the number of data blocks per packet is important for media clock recovery. Although the devices in Tascam FireWire series transfer packets regardless of receiving packets, the tx packets includes no events in the beginning of streaming. It takes so long to multiplex any event into the packet after receiving the sequence of packets. As long as I experienced, it takes several thousands of isochronous cycle. Furthermore, just after changing sampling transmission frequency, it stops multiplexing event at once, then starts multiplexing again. The sequence replay is tested with below models: * FW-1884 * FW-1804 * FW-1082 Signed-off-by: Takashi Sakamoto <o-takashi@sakamocchi.jp> Link: https://lore.kernel.org/r/20210531025103.17880-6-o-takashi@sakamocchi.jp Signed-off-by: Takashi Iwai <tiwai@suse.de>
560 lines
13 KiB
C
560 lines
13 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 READY_TIMEOUT_MS 4000
|
|
|
|
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;
|
|
}
|
|
|
|
tscm->need_long_tx_init_skip = (rate != curr_rate);
|
|
}
|
|
|
|
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;
|
|
unsigned int tx_init_skip_cycles;
|
|
|
|
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;
|
|
|
|
if (tscm->need_long_tx_init_skip)
|
|
tx_init_skip_cycles = 16000;
|
|
else
|
|
tx_init_skip_cycles = 0;
|
|
|
|
// MEMO: Just after starting packet streaming, it transfers packets without any
|
|
// event. Enough after receiving the sequence of packets, it multiplexes events into
|
|
// the packet. However, just after changing sampling transfer frequency, it stops
|
|
// multiplexing during packet transmission. Enough after, it restarts multiplexing
|
|
// again. The device ignores presentation time expressed by the value of syt field
|
|
// of CIP header in received packets. The sequence of the number of data blocks per
|
|
// packet is important for media clock recovery.
|
|
err = amdtp_domain_start(&tscm->domain, tx_init_skip_cycles, true, true);
|
|
if (err < 0)
|
|
return err;
|
|
|
|
if (!amdtp_domain_wait_ready(&tscm->domain, READY_TIMEOUT_MS)) {
|
|
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);
|
|
|
|
tscm->need_long_tx_init_skip = false;
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|