root/sound/firewire/dice/dice-transaction.c
// SPDX-License-Identifier: GPL-2.0-only
/*
 * dice_transaction.c - a part of driver for Dice based devices
 *
 * Copyright (c) Clemens Ladisch
 * Copyright (c) 2014 Takashi Sakamoto
 */

#include "dice.h"

static u64 get_subaddr(struct snd_dice *dice, enum snd_dice_addr_type type,
                       u64 offset)
{
        switch (type) {
        case SND_DICE_ADDR_TYPE_TX:
                offset += dice->tx_offset;
                break;
        case SND_DICE_ADDR_TYPE_RX:
                offset += dice->rx_offset;
                break;
        case SND_DICE_ADDR_TYPE_SYNC:
                offset += dice->sync_offset;
                break;
        case SND_DICE_ADDR_TYPE_RSRV:
                offset += dice->rsrv_offset;
                break;
        case SND_DICE_ADDR_TYPE_GLOBAL:
        default:
                offset += dice->global_offset;
                break;
        }
        offset += DICE_PRIVATE_SPACE;
        return offset;
}

int snd_dice_transaction_write(struct snd_dice *dice,
                               enum snd_dice_addr_type type,
                               unsigned int offset, void *buf, unsigned int len)
{
        return snd_fw_transaction(dice->unit,
                                  (len == 4) ? TCODE_WRITE_QUADLET_REQUEST :
                                               TCODE_WRITE_BLOCK_REQUEST,
                                  get_subaddr(dice, type, offset), buf, len, 0);
}

int snd_dice_transaction_read(struct snd_dice *dice,
                              enum snd_dice_addr_type type, unsigned int offset,
                              void *buf, unsigned int len)
{
        return snd_fw_transaction(dice->unit,
                                  (len == 4) ? TCODE_READ_QUADLET_REQUEST :
                                               TCODE_READ_BLOCK_REQUEST,
                                  get_subaddr(dice, type, offset), buf, len, 0);
}

static unsigned int get_clock_info(struct snd_dice *dice, __be32 *info)
{
        return snd_dice_transaction_read_global(dice, GLOBAL_CLOCK_SELECT,
                                                info, 4);
}

int snd_dice_transaction_get_clock_source(struct snd_dice *dice,
                                          unsigned int *source)
{
        __be32 info;
        int err;

        err = get_clock_info(dice, &info);
        if (err >= 0)
                *source = be32_to_cpu(info) & CLOCK_SOURCE_MASK;

        return err;
}

int snd_dice_transaction_get_rate(struct snd_dice *dice, unsigned int *rate)
{
        __be32 info;
        unsigned int index;
        int err;

        err = get_clock_info(dice, &info);
        if (err < 0)
                goto end;

        index = (be32_to_cpu(info) & CLOCK_RATE_MASK) >> CLOCK_RATE_SHIFT;
        if (index >= SND_DICE_RATES_COUNT) {
                err = -ENOSYS;
                goto end;
        }

        *rate = snd_dice_rates[index];
end:
        return err;
}

int snd_dice_transaction_set_enable(struct snd_dice *dice)
{
        __be32 value;
        int err = 0;

        if (dice->global_enabled)
                goto end;

        value = cpu_to_be32(1);
        err = snd_fw_transaction(dice->unit, TCODE_WRITE_QUADLET_REQUEST,
                                 get_subaddr(dice, SND_DICE_ADDR_TYPE_GLOBAL,
                                             GLOBAL_ENABLE),
                                 &value, 4,
                                 FW_FIXED_GENERATION | dice->owner_generation);
        if (err < 0)
                goto end;

        dice->global_enabled = true;
end:
        return err;
}

void snd_dice_transaction_clear_enable(struct snd_dice *dice)
{
        __be32 value;

        value = 0;
        snd_fw_transaction(dice->unit, TCODE_WRITE_QUADLET_REQUEST,
                           get_subaddr(dice, SND_DICE_ADDR_TYPE_GLOBAL,
                                       GLOBAL_ENABLE),
                           &value, 4, FW_QUIET |
                           FW_FIXED_GENERATION | dice->owner_generation);

        dice->global_enabled = false;
}

static void dice_notification(struct fw_card *card, struct fw_request *request,
                              int tcode, int destination, int source,
                              int generation, unsigned long long offset,
                              void *data, size_t length, void *callback_data)
{
        struct snd_dice *dice = callback_data;
        u32 bits;

        if (tcode != TCODE_WRITE_QUADLET_REQUEST) {
                fw_send_response(card, request, RCODE_TYPE_ERROR);
                return;
        }
        if ((offset & 3) != 0) {
                fw_send_response(card, request, RCODE_ADDRESS_ERROR);
                return;
        }

        bits = be32_to_cpup(data);

        scoped_guard(spinlock_irqsave, &dice->lock) {
                dice->notification_bits |= bits;
        }

        fw_send_response(card, request, RCODE_COMPLETE);

        if (bits & NOTIFY_CLOCK_ACCEPTED)
                complete(&dice->clock_accepted);
        wake_up(&dice->hwdep_wait);
}

static int register_notification_address(struct snd_dice *dice, bool retry)
{
        struct fw_device *device = fw_parent_device(dice->unit);
        __be64 *buffer;
        unsigned int retries;
        int err;

        retries = (retry) ? 3 : 0;

        buffer = kmalloc(2 * 8, GFP_KERNEL);
        if (!buffer)
                return -ENOMEM;

        for (;;) {
                buffer[0] = cpu_to_be64(OWNER_NO_OWNER);
                buffer[1] = cpu_to_be64(
                        ((u64)device->card->node_id << OWNER_NODE_SHIFT) |
                        dice->notification_handler.offset);

                dice->owner_generation = device->generation;
                smp_rmb(); /* node_id vs. generation */
                err = snd_fw_transaction(dice->unit, TCODE_LOCK_COMPARE_SWAP,
                                         get_subaddr(dice,
                                                     SND_DICE_ADDR_TYPE_GLOBAL,
                                                     GLOBAL_OWNER),
                                         buffer, 2 * 8,
                                         FW_FIXED_GENERATION |
                                                        dice->owner_generation);
                if (err == 0) {
                        /* success */
                        if (buffer[0] == cpu_to_be64(OWNER_NO_OWNER))
                                break;
                        /* The address seems to be already registered. */
                        if (buffer[0] == buffer[1])
                                break;

                        dev_err(&dice->unit->device,
                                "device is already in use\n");
                        err = -EBUSY;
                }
                if (err != -EAGAIN || retries-- > 0)
                        break;

                msleep(20);
        }

        kfree(buffer);

        if (err < 0)
                dice->owner_generation = -1;

        return err;
}

static void unregister_notification_address(struct snd_dice *dice)
{
        struct fw_device *device = fw_parent_device(dice->unit);
        __be64 *buffer;

        buffer = kmalloc(2 * 8, GFP_KERNEL);
        if (buffer == NULL)
                return;

        buffer[0] = cpu_to_be64(
                ((u64)device->card->node_id << OWNER_NODE_SHIFT) |
                dice->notification_handler.offset);
        buffer[1] = cpu_to_be64(OWNER_NO_OWNER);
        snd_fw_transaction(dice->unit, TCODE_LOCK_COMPARE_SWAP,
                           get_subaddr(dice, SND_DICE_ADDR_TYPE_GLOBAL,
                                       GLOBAL_OWNER),
                           buffer, 2 * 8, FW_QUIET |
                           FW_FIXED_GENERATION | dice->owner_generation);

        kfree(buffer);

        dice->owner_generation = -1;
}

void snd_dice_transaction_destroy(struct snd_dice *dice)
{
        struct fw_address_handler *handler = &dice->notification_handler;

        if (handler->callback_data == NULL)
                return;

        unregister_notification_address(dice);

        fw_core_remove_address_handler(handler);
        handler->callback_data = NULL;
}

int snd_dice_transaction_reinit(struct snd_dice *dice)
{
        struct fw_address_handler *handler = &dice->notification_handler;

        if (handler->callback_data == NULL)
                return -EINVAL;

        return register_notification_address(dice, false);
}

static int get_subaddrs(struct snd_dice *dice)
{
        static const int min_values[10] = {
                10, 0x60 / 4,
                10, 0x18 / 4,
                10, 0x18 / 4,
                0, 0,
                0, 0,
        };
        __be32 *pointers;
        __be32 version;
        u32 data;
        unsigned int i;
        int err;

        pointers = kmalloc_array(ARRAY_SIZE(min_values), sizeof(__be32),
                                 GFP_KERNEL);
        if (pointers == NULL)
                return -ENOMEM;

        /*
         * Check that the sub address spaces exist and are located inside the
         * private address space.  The minimum values are chosen so that all
         * minimally required registers are included.
         */
        err = snd_fw_transaction(dice->unit, TCODE_READ_BLOCK_REQUEST,
                                 DICE_PRIVATE_SPACE, pointers,
                                 sizeof(__be32) * ARRAY_SIZE(min_values), 0);
        if (err < 0)
                goto end;

        for (i = 0; i < ARRAY_SIZE(min_values); ++i) {
                data = be32_to_cpu(pointers[i]);
                if (data < min_values[i] || data >= 0x40000) {
                        err = -ENODEV;
                        goto end;
                }
        }

        if (be32_to_cpu(pointers[1]) > 0x18) {
                /*
                 * Check that the implemented DICE driver specification major
                 * version number matches.
                 */
                err = snd_fw_transaction(dice->unit, TCODE_READ_QUADLET_REQUEST,
                                DICE_PRIVATE_SPACE +
                                be32_to_cpu(pointers[0]) * 4 + GLOBAL_VERSION,
                                &version, sizeof(version), 0);
                if (err < 0)
                        goto end;

                if ((version & cpu_to_be32(0xff000000)) !=
                                                cpu_to_be32(0x01000000)) {
                        dev_err(&dice->unit->device,
                                "unknown DICE version: 0x%08x\n",
                                be32_to_cpu(version));
                        err = -ENODEV;
                        goto end;
                }

                /* Set up later. */
                dice->clock_caps = 1;
        }

        dice->global_offset = be32_to_cpu(pointers[0]) * 4;
        dice->tx_offset = be32_to_cpu(pointers[2]) * 4;
        dice->rx_offset = be32_to_cpu(pointers[4]) * 4;

        /* Old firmware doesn't support these fields. */
        if (pointers[7])
                dice->sync_offset = be32_to_cpu(pointers[6]) * 4;
        if (pointers[9])
                dice->rsrv_offset = be32_to_cpu(pointers[8]) * 4;
end:
        kfree(pointers);
        return err;
}

int snd_dice_transaction_init(struct snd_dice *dice)
{
        struct fw_address_handler *handler = &dice->notification_handler;
        int err;

        err = get_subaddrs(dice);
        if (err < 0)
                return err;

        /* Allocation callback in address space over host controller */
        handler->length = 4;
        handler->address_callback = dice_notification;
        handler->callback_data = dice;
        err = fw_core_add_address_handler(handler, &fw_high_memory_region);
        if (err < 0) {
                handler->callback_data = NULL;
                return err;
        }

        /* Register the address space */
        err = register_notification_address(dice, true);
        if (err < 0) {
                fw_core_remove_address_handler(handler);
                handler->callback_data = NULL;
        }

        return err;
}