diff options
Diffstat (limited to 'sound/firewire/cmp.c')
-rw-r--r-- | sound/firewire/cmp.c | 305 |
1 files changed, 305 insertions, 0 deletions
diff --git a/sound/firewire/cmp.c b/sound/firewire/cmp.c new file mode 100644 index 00000000000..c992dab4bb9 --- /dev/null +++ b/sound/firewire/cmp.c @@ -0,0 +1,305 @@ +/* + * Connection Management Procedures (IEC 61883-1) helper functions + * + * Copyright (c) Clemens Ladisch <clemens@ladisch.de> + * Licensed under the terms of the GNU General Public License, version 2. + */ + +#include <linux/device.h> +#include <linux/firewire.h> +#include <linux/firewire-constants.h> +#include <linux/module.h> +#include <linux/sched.h> +#include "lib.h" +#include "iso-resources.h" +#include "cmp.h" + +#define IMPR_SPEED_MASK 0xc0000000 +#define IMPR_SPEED_SHIFT 30 +#define IMPR_XSPEED_MASK 0x00000060 +#define IMPR_XSPEED_SHIFT 5 +#define IMPR_PLUGS_MASK 0x0000001f + +#define IPCR_ONLINE 0x80000000 +#define IPCR_BCAST_CONN 0x40000000 +#define IPCR_P2P_CONN_MASK 0x3f000000 +#define IPCR_P2P_CONN_SHIFT 24 +#define IPCR_CHANNEL_MASK 0x003f0000 +#define IPCR_CHANNEL_SHIFT 16 + +enum bus_reset_handling { + ABORT_ON_BUS_RESET, + SUCCEED_ON_BUS_RESET, +}; + +static __attribute__((format(printf, 2, 3))) +void cmp_error(struct cmp_connection *c, const char *fmt, ...) +{ + va_list va; + + va_start(va, fmt); + dev_err(&c->resources.unit->device, "%cPCR%u: %pV", + 'i', c->pcr_index, &(struct va_format){ fmt, &va }); + va_end(va); +} + +static int pcr_modify(struct cmp_connection *c, + __be32 (*modify)(struct cmp_connection *c, __be32 old), + int (*check)(struct cmp_connection *c, __be32 pcr), + enum bus_reset_handling bus_reset_handling) +{ + struct fw_device *device = fw_parent_device(c->resources.unit); + __be32 *buffer = c->resources.buffer; + int generation = c->resources.generation; + int rcode, errors = 0; + __be32 old_arg; + int err; + + buffer[0] = c->last_pcr_value; + for (;;) { + old_arg = buffer[0]; + buffer[1] = modify(c, buffer[0]); + + rcode = fw_run_transaction( + device->card, TCODE_LOCK_COMPARE_SWAP, + device->node_id, generation, device->max_speed, + CSR_REGISTER_BASE + CSR_IPCR(c->pcr_index), + buffer, 8); + + if (rcode == RCODE_COMPLETE) { + if (buffer[0] == old_arg) /* success? */ + break; + + if (check) { + err = check(c, buffer[0]); + if (err < 0) + return err; + } + } else if (rcode == RCODE_GENERATION) + goto bus_reset; + else if (rcode_is_permanent_error(rcode) || ++errors >= 3) + goto io_error; + } + c->last_pcr_value = buffer[1]; + + return 0; + +io_error: + cmp_error(c, "transaction failed: %s\n", rcode_string(rcode)); + return -EIO; + +bus_reset: + return bus_reset_handling == ABORT_ON_BUS_RESET ? -EAGAIN : 0; +} + + +/** + * cmp_connection_init - initializes a connection manager + * @c: the connection manager to initialize + * @unit: a unit of the target device + * @ipcr_index: the index of the iPCR on the target device + */ +int cmp_connection_init(struct cmp_connection *c, + struct fw_unit *unit, + unsigned int ipcr_index) +{ + __be32 impr_be; + u32 impr; + int err; + + err = snd_fw_transaction(unit, TCODE_READ_QUADLET_REQUEST, + CSR_REGISTER_BASE + CSR_IMPR, + &impr_be, 4); + if (err < 0) + return err; + impr = be32_to_cpu(impr_be); + + if (ipcr_index >= (impr & IMPR_PLUGS_MASK)) + return -EINVAL; + + c->connected = false; + mutex_init(&c->mutex); + fw_iso_resources_init(&c->resources, unit); + c->last_pcr_value = cpu_to_be32(0x80000000); + c->pcr_index = ipcr_index; + c->max_speed = (impr & IMPR_SPEED_MASK) >> IMPR_SPEED_SHIFT; + if (c->max_speed == SCODE_BETA) + c->max_speed += (impr & IMPR_XSPEED_MASK) >> IMPR_XSPEED_SHIFT; + + return 0; +} +EXPORT_SYMBOL(cmp_connection_init); + +/** + * cmp_connection_destroy - free connection manager resources + * @c: the connection manager + */ +void cmp_connection_destroy(struct cmp_connection *c) +{ + WARN_ON(c->connected); + mutex_destroy(&c->mutex); + fw_iso_resources_destroy(&c->resources); +} +EXPORT_SYMBOL(cmp_connection_destroy); + + +static __be32 ipcr_set_modify(struct cmp_connection *c, __be32 ipcr) +{ + ipcr &= ~cpu_to_be32(IPCR_BCAST_CONN | + IPCR_P2P_CONN_MASK | + IPCR_CHANNEL_MASK); + ipcr |= cpu_to_be32(1 << IPCR_P2P_CONN_SHIFT); + ipcr |= cpu_to_be32(c->resources.channel << IPCR_CHANNEL_SHIFT); + + return ipcr; +} + +static int ipcr_set_check(struct cmp_connection *c, __be32 ipcr) +{ + if (ipcr & cpu_to_be32(IPCR_BCAST_CONN | + IPCR_P2P_CONN_MASK)) { + cmp_error(c, "plug is already in use\n"); + return -EBUSY; + } + if (!(ipcr & cpu_to_be32(IPCR_ONLINE))) { + cmp_error(c, "plug is not on-line\n"); + return -ECONNREFUSED; + } + + return 0; +} + +/** + * cmp_connection_establish - establish a connection to the target + * @c: the connection manager + * @max_payload_bytes: the amount of data (including CIP headers) per packet + * + * This function establishes a point-to-point connection from the local + * computer to the target by allocating isochronous resources (channel and + * bandwidth) and setting the target's input plug control register. When this + * function succeeds, the caller is responsible for starting transmitting + * packets. + */ +int cmp_connection_establish(struct cmp_connection *c, + unsigned int max_payload_bytes) +{ + int err; + + if (WARN_ON(c->connected)) + return -EISCONN; + + c->speed = min(c->max_speed, + fw_parent_device(c->resources.unit)->max_speed); + + mutex_lock(&c->mutex); + +retry_after_bus_reset: + err = fw_iso_resources_allocate(&c->resources, + max_payload_bytes, c->speed); + if (err < 0) + goto err_mutex; + + err = pcr_modify(c, ipcr_set_modify, ipcr_set_check, + ABORT_ON_BUS_RESET); + if (err == -EAGAIN) { + fw_iso_resources_free(&c->resources); + goto retry_after_bus_reset; + } + if (err < 0) + goto err_resources; + + c->connected = true; + + mutex_unlock(&c->mutex); + + return 0; + +err_resources: + fw_iso_resources_free(&c->resources); +err_mutex: + mutex_unlock(&c->mutex); + + return err; +} +EXPORT_SYMBOL(cmp_connection_establish); + +/** + * cmp_connection_update - update the connection after a bus reset + * @c: the connection manager + * + * This function must be called from the driver's .update handler to reestablish + * any connection that might have been active. + * + * Returns zero on success, or a negative error code. On an error, the + * connection is broken and the caller must stop transmitting iso packets. + */ +int cmp_connection_update(struct cmp_connection *c) +{ + int err; + + mutex_lock(&c->mutex); + + if (!c->connected) { + mutex_unlock(&c->mutex); + return 0; + } + + err = fw_iso_resources_update(&c->resources); + if (err < 0) + goto err_unconnect; + + err = pcr_modify(c, ipcr_set_modify, ipcr_set_check, + SUCCEED_ON_BUS_RESET); + if (err < 0) + goto err_resources; + + mutex_unlock(&c->mutex); + + return 0; + +err_resources: + fw_iso_resources_free(&c->resources); +err_unconnect: + c->connected = false; + mutex_unlock(&c->mutex); + + return err; +} +EXPORT_SYMBOL(cmp_connection_update); + + +static __be32 ipcr_break_modify(struct cmp_connection *c, __be32 ipcr) +{ + return ipcr & ~cpu_to_be32(IPCR_BCAST_CONN | IPCR_P2P_CONN_MASK); +} + +/** + * cmp_connection_break - break the connection to the target + * @c: the connection manager + * + * This function deactives the connection in the target's input plug control + * register, and frees the isochronous resources of the connection. Before + * calling this function, the caller should cease transmitting packets. + */ +void cmp_connection_break(struct cmp_connection *c) +{ + int err; + + mutex_lock(&c->mutex); + + if (!c->connected) { + mutex_unlock(&c->mutex); + return; + } + + err = pcr_modify(c, ipcr_break_modify, NULL, SUCCEED_ON_BUS_RESET); + if (err < 0) + cmp_error(c, "plug is still connected\n"); + + fw_iso_resources_free(&c->resources); + + c->connected = false; + + mutex_unlock(&c->mutex); +} +EXPORT_SYMBOL(cmp_connection_break); |