linux-stable/sound/firewire/tascam/tascam-stream.c

560 lines
13 KiB
C
Raw Normal View History

// 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,
&reg, 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,
&reg, 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,
&reg, 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,
&reg, 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,
&reg, 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,
&reg, 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,
&reg, sizeof(reg), 0);
reg = 0;
snd_fw_transaction(tscm->unit, TCODE_WRITE_QUADLET_REQUEST,
TSCM_ADDR_BASE + TSCM_OFFSET_ISOC_RX_ON,
&reg, 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,
&reg, sizeof(reg), 0);
reg = cpu_to_be32(0x00000000);
snd_fw_transaction(tscm->unit, TCODE_WRITE_QUADLET_REQUEST,
TSCM_ADDR_BASE + TSCM_OFFSET_UNKNOWN,
&reg, 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,
&reg, 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,
&reg, 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,
&reg, 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,
&reg, 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,
&reg, 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,
&reg, 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,
&reg, 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,
&reg, 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)
ALSA: firewire-tascam: add missing unwind goto in snd_tscm_stream_start_duplex() Smatch Warns: sound/firewire/tascam/tascam-stream.c:493 snd_tscm_stream_start_duplex() warn: missing unwind goto? The direct return will cause the stream list of "&tscm->domain" unemptied and the session in "tscm" unfinished if amdtp_domain_start() returns with an error. Fix this by changing the direct return to a goto which will empty the stream list of "&tscm->domain" and finish the session in "tscm". The snd_tscm_stream_start_duplex() function is called in the prepare callback of PCM. According to "ALSA Kernel API Documentation", the prepare callback of PCM will be called many times at each setup. So, if the "&d->streams" list is not emptied, when the prepare callback is called next time, snd_tscm_stream_start_duplex() will receive -EBUSY from amdtp_domain_add_stream() that tries to add an existing stream to the domain. The error handling code after the "error" label will be executed in this case, and the "&d->streams" list will be emptied. So not emptying the "&d->streams" list will not cause an issue. But it is more efficient and readable to empty it on the first error by changing the direct return to a goto statement. The session in "tscm" has been begun before amdtp_domain_start(), so it needs to be finished when amdtp_domain_start() fails. Fixes: c281d46a51e3 ("ALSA: firewire-tascam: support AMDTP domain") Signed-off-by: Xu Biang <xubiang@hust.edu.cn> Reviewed-by: Dan Carpenter <error27@gmail.com> Acked-by: Takashi Sakamoto <o-takashi@sakamocchi.jp> Cc: <stable@vger.kernel.org> Link: https://lore.kernel.org/r/20230406132801.105108-1-xubiang@hust.edu.cn Signed-off-by: Takashi Iwai <tiwai@suse.de>
2023-04-06 13:28:01 +00:00
goto error;
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);
}