root/drivers/infiniband/hw/hfi1/eprom.c
// SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
/*
 * Copyright(c) 2015, 2016 Intel Corporation.
 */

#include <linux/delay.h>
#include "hfi.h"
#include "common.h"
#include "eprom.h"

/*
 * The EPROM is logically divided into three partitions:
 *      partition 0: the first 128K, visible from PCI ROM BAR
 *      partition 1: 4K config file (sector size)
 *      partition 2: the rest
 */
#define P0_SIZE (128 * 1024)
#define P1_SIZE   (4 * 1024)
#define P1_START P0_SIZE
#define P2_START (P0_SIZE + P1_SIZE)

/* controller page size, in bytes */
#define EP_PAGE_SIZE 256
#define EP_PAGE_MASK (EP_PAGE_SIZE - 1)
#define EP_PAGE_DWORDS (EP_PAGE_SIZE / sizeof(u32))

/* controller commands */
#define CMD_SHIFT 24
#define CMD_NOP                     (0)
#define CMD_READ_DATA(addr)         ((0x03 << CMD_SHIFT) | addr)
#define CMD_RELEASE_POWERDOWN_NOID  ((0xab << CMD_SHIFT))

/* controller interface speeds */
#define EP_SPEED_FULL 0x2       /* full speed */

/*
 * How long to wait for the EPROM to become available, in ms.
 * The spec 32 Mb EPROM takes around 40s to erase then write.
 * Double it for safety.
 */
#define EPROM_TIMEOUT 80000 /* ms */

/*
 * Read a 256 byte (64 dword) EPROM page.
 * All callers have verified the offset is at a page boundary.
 */
static void read_page(struct hfi1_devdata *dd, u32 offset, u32 *result)
{
        int i;

        write_csr(dd, ASIC_EEP_ADDR_CMD, CMD_READ_DATA(offset));
        for (i = 0; i < EP_PAGE_DWORDS; i++)
                result[i] = (u32)read_csr(dd, ASIC_EEP_DATA);
        write_csr(dd, ASIC_EEP_ADDR_CMD, CMD_NOP); /* close open page */
}

/*
 * Read length bytes starting at offset from the start of the EPROM.
 */
static int read_length(struct hfi1_devdata *dd, u32 start, u32 len, void *dest)
{
        u32 buffer[EP_PAGE_DWORDS];
        u32 end;
        u32 start_offset;
        u32 read_start;
        u32 bytes;

        if (len == 0)
                return 0;

        end = start + len;

        /*
         * Make sure the read range is not outside of the controller read
         * command address range.  Note that '>' is correct below - the end
         * of the range is OK if it stops at the limit, but no higher.
         */
        if (end > (1 << CMD_SHIFT))
                return -EINVAL;

        /* read the first partial page */
        start_offset = start & EP_PAGE_MASK;
        if (start_offset) {
                /* partial starting page */

                /* align and read the page that contains the start */
                read_start = start & ~EP_PAGE_MASK;
                read_page(dd, read_start, buffer);

                /* the rest of the page is available data */
                bytes = EP_PAGE_SIZE - start_offset;

                if (len <= bytes) {
                        /* end is within this page */
                        memcpy(dest, (u8 *)buffer + start_offset, len);
                        return 0;
                }

                memcpy(dest, (u8 *)buffer + start_offset, bytes);

                start += bytes;
                len -= bytes;
                dest += bytes;
        }
        /* start is now page aligned */

        /* read whole pages */
        while (len >= EP_PAGE_SIZE) {
                read_page(dd, start, buffer);
                memcpy(dest, buffer, EP_PAGE_SIZE);

                start += EP_PAGE_SIZE;
                len -= EP_PAGE_SIZE;
                dest += EP_PAGE_SIZE;
        }

        /* read the last partial page */
        if (len) {
                read_page(dd, start, buffer);
                memcpy(dest, buffer, len);
        }

        return 0;
}

/*
 * Initialize the EPROM handler.
 */
int eprom_init(struct hfi1_devdata *dd)
{
        int ret = 0;

        /* only the discrete chip has an EPROM */
        if (dd->pcidev->device != PCI_DEVICE_ID_INTEL0)
                return 0;

        /*
         * It is OK if both HFIs reset the EPROM as long as they don't
         * do it at the same time.
         */
        ret = acquire_chip_resource(dd, CR_EPROM, EPROM_TIMEOUT);
        if (ret) {
                dd_dev_err(dd,
                           "%s: unable to acquire EPROM resource, no EPROM support\n",
                           __func__);
                goto done_asic;
        }

        /* reset EPROM to be sure it is in a good state */

        /* set reset */
        write_csr(dd, ASIC_EEP_CTL_STAT, ASIC_EEP_CTL_STAT_EP_RESET_SMASK);
        /* clear reset, set speed */
        write_csr(dd, ASIC_EEP_CTL_STAT,
                  EP_SPEED_FULL << ASIC_EEP_CTL_STAT_RATE_SPI_SHIFT);

        /* wake the device with command "release powerdown NoID" */
        write_csr(dd, ASIC_EEP_ADDR_CMD, CMD_RELEASE_POWERDOWN_NOID);

        dd->eprom_available = true;
        release_chip_resource(dd, CR_EPROM);
done_asic:
        return ret;
}

/* magic character sequence that begins an image */
#define IMAGE_START_MAGIC "APO="

/* magic character sequence that might trail an image */
#define IMAGE_TRAIL_MAGIC "egamiAPO"

/* EPROM file types */
#define HFI1_EFT_PLATFORM_CONFIG 2

/* segment size - 128 KiB */
#define SEG_SIZE (128 * 1024)

struct hfi1_eprom_footer {
        u32 oprom_size;         /* size of the oprom, in bytes */
        u16 num_table_entries;
        u16 version;            /* version of this footer */
        u32 magic;              /* must be last */
};

struct hfi1_eprom_table_entry {
        u32 type;               /* file type */
        u32 offset;             /* file offset from start of EPROM */
        u32 size;               /* file size, in bytes */
};

/*
 * Calculate the max number of table entries that will fit within a directory
 * buffer of size 'dir_size'.
 */
#define MAX_TABLE_ENTRIES(dir_size) \
        (((dir_size) - sizeof(struct hfi1_eprom_footer)) / \
                sizeof(struct hfi1_eprom_table_entry))

#define DIRECTORY_SIZE(n) (sizeof(struct hfi1_eprom_footer) + \
        (sizeof(struct hfi1_eprom_table_entry) * (n)))

#define MAGIC4(a, b, c, d) ((d) << 24 | (c) << 16 | (b) << 8 | (a))
#define FOOTER_MAGIC MAGIC4('e', 'p', 'r', 'm')
#define FOOTER_VERSION 1

/*
 * Read all of partition 1.  The actual file is at the front.  Adjust
 * the returned size if a trailing image magic is found.
 */
static int read_partition_platform_config(struct hfi1_devdata *dd, void **data,
                                          u32 *size)
{
        void *buffer;
        void *p;
        u32 length;
        int ret;

        buffer = kmalloc(P1_SIZE, GFP_KERNEL);
        if (!buffer)
                return -ENOMEM;

        ret = read_length(dd, P1_START, P1_SIZE, buffer);
        if (ret) {
                kfree(buffer);
                return ret;
        }

        /* config partition is valid only if it starts with IMAGE_START_MAGIC */
        if (memcmp(buffer, IMAGE_START_MAGIC, strlen(IMAGE_START_MAGIC))) {
                kfree(buffer);
                return -ENOENT;
        }

        /* scan for image magic that may trail the actual data */
        p = strnstr(buffer, IMAGE_TRAIL_MAGIC, P1_SIZE);
        if (p)
                length = p - buffer;
        else
                length = P1_SIZE;

        *data = buffer;
        *size = length;
        return 0;
}

/*
 * The segment magic has been checked.  There is a footer and table of
 * contents present.
 *
 * directory is a u32 aligned buffer of size EP_PAGE_SIZE.
 */
static int read_segment_platform_config(struct hfi1_devdata *dd,
                                        void *directory, void **data, u32 *size)
{
        struct hfi1_eprom_footer *footer;
        struct hfi1_eprom_table_entry *table;
        struct hfi1_eprom_table_entry *entry;
        void *buffer = NULL;
        void *table_buffer = NULL;
        int ret, i;
        u32 directory_size;
        u32 seg_base, seg_offset;
        u32 bytes_available, ncopied, to_copy;

        /* the footer is at the end of the directory */
        footer = (struct hfi1_eprom_footer *)
                        (directory + EP_PAGE_SIZE - sizeof(*footer));

        /* make sure the structure version is supported */
        if (footer->version != FOOTER_VERSION)
                return -EINVAL;

        /* oprom size cannot be larger than a segment */
        if (footer->oprom_size >= SEG_SIZE)
                return -EINVAL;

        /* the file table must fit in a segment with the oprom */
        if (footer->num_table_entries >
                        MAX_TABLE_ENTRIES(SEG_SIZE - footer->oprom_size))
                return -EINVAL;

        /* find the file table start, which precedes the footer */
        directory_size = DIRECTORY_SIZE(footer->num_table_entries);
        if (directory_size <= EP_PAGE_SIZE) {
                /* the file table fits into the directory buffer handed in */
                table = (struct hfi1_eprom_table_entry *)
                                (directory + EP_PAGE_SIZE - directory_size);
        } else {
                /* need to allocate and read more */
                table_buffer = kmalloc(directory_size, GFP_KERNEL);
                if (!table_buffer)
                        return -ENOMEM;
                ret = read_length(dd, SEG_SIZE - directory_size,
                                  directory_size, table_buffer);
                if (ret)
                        goto done;
                table = table_buffer;
        }

        /* look for the platform configuration file in the table */
        for (entry = NULL, i = 0; i < footer->num_table_entries; i++) {
                if (table[i].type == HFI1_EFT_PLATFORM_CONFIG) {
                        entry = &table[i];
                        break;
                }
        }
        if (!entry) {
                ret = -ENOENT;
                goto done;
        }

        /*
         * Sanity check on the configuration file size - it should never
         * be larger than 4 KiB.
         */
        if (entry->size > (4 * 1024)) {
                dd_dev_err(dd, "Bad configuration file size 0x%x\n",
                           entry->size);
                ret = -EINVAL;
                goto done;
        }

        /* check for bogus offset and size that wrap when added together */
        if (entry->offset + entry->size < entry->offset) {
                dd_dev_err(dd,
                           "Bad configuration file start + size 0x%x+0x%x\n",
                           entry->offset, entry->size);
                ret = -EINVAL;
                goto done;
        }

        /* allocate the buffer to return */
        buffer = kmalloc(entry->size, GFP_KERNEL);
        if (!buffer) {
                ret = -ENOMEM;
                goto done;
        }

        /*
         * Extract the file by looping over segments until it is fully read.
         */
        seg_offset = entry->offset % SEG_SIZE;
        seg_base = entry->offset - seg_offset;
        ncopied = 0;
        while (ncopied < entry->size) {
                /* calculate data bytes available in this segment */

                /* start with the bytes from the current offset to the end */
                bytes_available = SEG_SIZE - seg_offset;
                /* subtract off footer and table from segment 0 */
                if (seg_base == 0) {
                        /*
                         * Sanity check: should not have a starting point
                         * at or within the directory.
                         */
                        if (bytes_available <= directory_size) {
                                dd_dev_err(dd,
                                           "Bad configuration file - offset 0x%x within footer+table\n",
                                           entry->offset);
                                ret = -EINVAL;
                                goto done;
                        }
                        bytes_available -= directory_size;
                }

                /* calculate bytes wanted */
                to_copy = entry->size - ncopied;

                /* max out at the available bytes in this segment */
                if (to_copy > bytes_available)
                        to_copy = bytes_available;

                /*
                 * Read from the EPROM.
                 *
                 * The sanity check for entry->offset is done in read_length().
                 * The EPROM offset is validated against what the hardware
                 * addressing supports.  In addition, if the offset is larger
                 * than the actual EPROM, it silently wraps.  It will work
                 * fine, though the reader may not get what they expected
                 * from the EPROM.
                 */
                ret = read_length(dd, seg_base + seg_offset, to_copy,
                                  buffer + ncopied);
                if (ret)
                        goto done;

                ncopied += to_copy;

                /* set up for next segment */
                seg_offset = footer->oprom_size;
                seg_base += SEG_SIZE;
        }

        /* success */
        ret = 0;
        *data = buffer;
        *size = entry->size;

done:
        kfree(table_buffer);
        if (ret)
                kfree(buffer);
        return ret;
}

/*
 * Read the platform configuration file from the EPROM.
 *
 * On success, an allocated buffer containing the data and its size are
 * returned.  It is up to the caller to free this buffer.
 *
 * Return value:
 *   0        - success
 *   -ENXIO   - no EPROM is available
 *   -EBUSY   - not able to acquire access to the EPROM
 *   -ENOENT  - no recognizable file written
 *   -ENOMEM  - buffer could not be allocated
 *   -EINVAL  - invalid EPROM contentents found
 */
int eprom_read_platform_config(struct hfi1_devdata *dd, void **data, u32 *size)
{
        u32 directory[EP_PAGE_DWORDS]; /* aligned buffer */
        int ret;

        if (!dd->eprom_available)
                return -ENXIO;

        ret = acquire_chip_resource(dd, CR_EPROM, EPROM_TIMEOUT);
        if (ret)
                return -EBUSY;

        /* read the last page of the segment for the EPROM format magic */
        ret = read_length(dd, SEG_SIZE - EP_PAGE_SIZE, EP_PAGE_SIZE, directory);
        if (ret)
                goto done;

        /* last dword of the segment contains a magic value */
        if (directory[EP_PAGE_DWORDS - 1] == FOOTER_MAGIC) {
                /* segment format */
                ret = read_segment_platform_config(dd, directory, data, size);
        } else {
                /* partition format */
                ret = read_partition_platform_config(dd, data, size);
        }

done:
        release_chip_resource(dd, CR_EPROM);
        return ret;
}