/*
 * QEMU model of the Xilinx BBRAM Battery Backed RAM
 *
 * Copyright (c) 2014-2021 Xilinx Inc.
 * Copyright (c) 2023 Advanced Micro Devices, Inc.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include "qemu/osdep.h"
#include "hw/nvram/xlnx-bbram.h"

#include "qemu/error-report.h"
#include "qemu/log.h"
#include "qapi/error.h"
#include "sysemu/blockdev.h"
#include "migration/vmstate.h"
#include "hw/qdev-properties.h"
#include "hw/qdev-properties-system.h"
#include "hw/nvram/xlnx-efuse.h"

#ifndef XLNX_BBRAM_ERR_DEBUG
#define XLNX_BBRAM_ERR_DEBUG 0
#endif

REG32(BBRAM_STATUS, 0x0)
    FIELD(BBRAM_STATUS, AES_CRC_PASS, 9, 1)
    FIELD(BBRAM_STATUS, AES_CRC_DONE, 8, 1)
    FIELD(BBRAM_STATUS, BBRAM_ZEROIZED, 4, 1)
    FIELD(BBRAM_STATUS, PGM_MODE, 0, 1)
REG32(BBRAM_CTRL, 0x4)
    FIELD(BBRAM_CTRL, ZEROIZE, 0, 1)
REG32(PGM_MODE, 0x8)
REG32(BBRAM_AES_CRC, 0xc)
REG32(BBRAM_0, 0x10)
REG32(BBRAM_1, 0x14)
REG32(BBRAM_2, 0x18)
REG32(BBRAM_3, 0x1c)
REG32(BBRAM_4, 0x20)
REG32(BBRAM_5, 0x24)
REG32(BBRAM_6, 0x28)
REG32(BBRAM_7, 0x2c)
REG32(BBRAM_8, 0x30)
REG32(BBRAM_SLVERR, 0x34)
    FIELD(BBRAM_SLVERR, ENABLE, 0, 1)
REG32(BBRAM_ISR, 0x38)
    FIELD(BBRAM_ISR, APB_SLVERR, 0, 1)
REG32(BBRAM_IMR, 0x3c)
    FIELD(BBRAM_IMR, APB_SLVERR, 0, 1)
REG32(BBRAM_IER, 0x40)
    FIELD(BBRAM_IER, APB_SLVERR, 0, 1)
REG32(BBRAM_IDR, 0x44)
    FIELD(BBRAM_IDR, APB_SLVERR, 0, 1)
REG32(BBRAM_MSW_LOCK, 0x4c)
    FIELD(BBRAM_MSW_LOCK, VAL, 0, 1)

#define R_MAX (R_BBRAM_MSW_LOCK + 1)

#define RAM_MAX (A_BBRAM_8 + 4 - A_BBRAM_0)

#define BBRAM_PGM_MAGIC 0x757bdf0d

QEMU_BUILD_BUG_ON(R_MAX != ARRAY_SIZE(((XlnxBBRam *)0)->regs));

static bool bbram_msw_locked(XlnxBBRam *s)
{
    return ARRAY_FIELD_EX32(s->regs, BBRAM_MSW_LOCK, VAL) != 0;
}

static bool bbram_pgm_enabled(XlnxBBRam *s)
{
    return ARRAY_FIELD_EX32(s->regs, BBRAM_STATUS, PGM_MODE) != 0;
}

static void bbram_aes_sync(XlnxBBRam *s)
{
    uint8_t end;
    uint8_t i;
    enum {
        U256_U8 = 256 / 8,
        U256_U32 = 256 / 32,
    };
    union {
        uint8_t u8[U256_U8];
        uint32_t u32[U256_U32];
    } key;

    if (!s->aes) {
        return;
    }

    end = U256_U32 - 1;
    for (i = 0; i <= end; ++i) {
        key.u32[end - i] = s->regs[R_BBRAM_0 + i];
    }

    zynqmp_aes_key_update(s->aes, key.u8, U256_U8);
}

static void bbram_bdrv_error(XlnxBBRam *s, int rc, gchar *detail)
{
    Error *errp = NULL;

    error_setg_errno(&errp, -rc, "%s: BBRAM backstore %s failed.",
                     blk_name(s->blk), detail);
    error_report("%s", error_get_pretty(errp));
    error_free(errp);

    g_free(detail);
}

static void bbram_bdrv_read(XlnxBBRam *s, Error **errp)
{
    uint32_t *ram = &s->regs[R_BBRAM_0];
    int nr = RAM_MAX;

    if (!s->blk) {
        return;
    }

    s->blk_ro = !blk_supports_write_perm(s->blk);
    if (!s->blk_ro) {
        int rc;

        rc = blk_set_perm(s->blk,
                          (BLK_PERM_CONSISTENT_READ | BLK_PERM_WRITE),
                          BLK_PERM_ALL, NULL);
        if (rc) {
            s->blk_ro = true;
        }
    }
    if (s->blk_ro) {
        warn_report("%s: Skip saving updates to read-only BBRAM backstore.",
                    blk_name(s->blk));
    }

    if (blk_pread(s->blk, 0, nr, ram, 0) < 0) {
        error_setg(errp,
                   "%s: Failed to read %u bytes from BBRAM backstore.",
                   blk_name(s->blk), nr);
        return;
    }

    /* Convert from little-endian backstore for each 32-bit word */
    nr /= 4;
    while (nr--) {
        ram[nr] = le32_to_cpu(ram[nr]);
    }
}

static void bbram_bdrv_sync(XlnxBBRam *s, uint64_t hwaddr)
{
    uint32_t le32;
    unsigned offset;
    int rc;

    assert(A_BBRAM_0 <= hwaddr && hwaddr <= A_BBRAM_8);

    /* Backstore is always in little-endian */
    le32 = cpu_to_le32(s->regs[hwaddr / 4]);

    /* Update zeroized flag */
    if (le32 && (hwaddr != A_BBRAM_8 || s->bbram8_wo)) {
        ARRAY_FIELD_DP32(s->regs, BBRAM_STATUS, BBRAM_ZEROIZED, 0);
    }

    if (!s->blk || s->blk_ro) {
        return;
    }

    offset = hwaddr - A_BBRAM_0;
    rc = blk_pwrite(s->blk, offset, 4, &le32, 0);
    if (rc < 0) {
        bbram_bdrv_error(s, rc, g_strdup_printf("write to offset %u", offset));
    }
}

static void bbram_bdrv_zero(XlnxBBRam *s)
{
    int rc;

    ARRAY_FIELD_DP32(s->regs, BBRAM_STATUS, BBRAM_ZEROIZED, 1);

    if (!s->blk || s->blk_ro) {
        return;
    }

    rc = blk_make_zero(s->blk, 0);
    if (rc < 0) {
        bbram_bdrv_error(s, rc, g_strdup("zeroizing"));
    }

    /* Restore bbram8 if it is non-zero */
    if (s->regs[R_BBRAM_8]) {
        bbram_bdrv_sync(s, A_BBRAM_8);
    }
}

static void bbram_zeroize(XlnxBBRam *s)
{
    int nr = RAM_MAX - (s->bbram8_wo ? 0 : 4); /* only wo bbram8 is cleared */

    memset(&s->regs[R_BBRAM_0], 0, nr);
    bbram_aes_sync(s);
    bbram_bdrv_zero(s);
}

static void bbram_update_irq(XlnxBBRam *s)
{
    bool pending = s->regs[R_BBRAM_ISR] & ~s->regs[R_BBRAM_IMR];

    qemu_set_irq(s->irq_bbram, pending);
}

static void bbram_ctrl_postw(RegisterInfo *reg, uint64_t val64)
{
    XlnxBBRam *s = XLNX_BBRAM(reg->opaque);
    uint32_t val = val64;

    if (val & R_BBRAM_CTRL_ZEROIZE_MASK) {
        bbram_zeroize(s);
        /* The bit is self clearing */
        s->regs[R_BBRAM_CTRL] &= ~R_BBRAM_CTRL_ZEROIZE_MASK;
    }
}

static void bbram_pgm_mode_postw(RegisterInfo *reg, uint64_t val64)
{
    XlnxBBRam *s = XLNX_BBRAM(reg->opaque);
    uint32_t val = val64;

    if (val == BBRAM_PGM_MAGIC) {
        bbram_zeroize(s);
        ARRAY_FIELD_DP32(s->regs, BBRAM_STATUS, PGM_MODE, 1);
    } else {
        ARRAY_FIELD_DP32(s->regs, BBRAM_STATUS, PGM_MODE, 0);
    }
}

static void bbram_aes_crc_postw(RegisterInfo *reg, uint64_t val64)
{
    XlnxBBRam *s = XLNX_BBRAM(reg->opaque);
    uint32_t calc_crc;

    if (!bbram_pgm_enabled(s)) {
        /* We are not in programming mode, don't do anything */
        return;
    }

    /* Perform the AES integrity check */
    s->regs[R_BBRAM_STATUS] |= R_BBRAM_STATUS_AES_CRC_DONE_MASK;

    /*
     * Set check status.
     *
     * ZynqMP BBRAM check has a zero-u32 prepended; see:
     *  https://github.com/Xilinx/embeddedsw/blob/release-2019.2/lib/sw_services/xilskey/src/xilskey_bbramps_zynqmp.c#L311
     */
    calc_crc = xlnx_efuse_calc_crc(&s->regs[R_BBRAM_0],
                                   (R_BBRAM_8 - R_BBRAM_0), s->crc_zpads);

    ARRAY_FIELD_DP32(s->regs, BBRAM_STATUS, AES_CRC_PASS,
                     (s->regs[R_BBRAM_AES_CRC] == calc_crc));
}

static uint64_t bbram_key_prew(RegisterInfo *reg, uint64_t val64)
{
    XlnxBBRam *s = XLNX_BBRAM(reg->opaque);
    uint32_t original_data = *(uint32_t *) reg->data;

    if (bbram_pgm_enabled(s)) {
        return val64;
    } else {
        /* We are not in programming mode, don't do anything */
        qemu_log_mask(LOG_GUEST_ERROR,
                      "Not in programming mode, dropping the write\n");
        return original_data;
    }
}

static void bbram_key_postw(RegisterInfo *reg, uint64_t val64)
{
    XlnxBBRam *s = XLNX_BBRAM(reg->opaque);

    bbram_bdrv_sync(s, reg->access->addr);
    bbram_aes_sync(s);
}

static uint64_t bbram_wo_postr(RegisterInfo *reg, uint64_t val)
{
    return 0;
}

static uint64_t bbram_r8_postr(RegisterInfo *reg, uint64_t val)
{
    XlnxBBRam *s = XLNX_BBRAM(reg->opaque);

    return s->bbram8_wo ? bbram_wo_postr(reg, val) : val;
}

static bool bbram_r8_readonly(XlnxBBRam *s)
{
    return s->bbram8_wo ? !bbram_pgm_enabled(s) : bbram_msw_locked(s);
}

static uint64_t bbram_r8_prew(RegisterInfo *reg, uint64_t val64)
{
    XlnxBBRam *s = XLNX_BBRAM(reg->opaque);

    if (bbram_r8_readonly(s)) {
        val64 = *(uint32_t *)reg->data;
    }

    return val64;
}

static void bbram_r8_postw(RegisterInfo *reg, uint64_t val64)
{
    XlnxBBRam *s = XLNX_BBRAM(reg->opaque);

    if (!bbram_r8_readonly(s)) {
        bbram_bdrv_sync(s, A_BBRAM_8);
    }
}

static uint64_t bbram_msw_lock_prew(RegisterInfo *reg, uint64_t val64)
{
    XlnxBBRam *s = XLNX_BBRAM(reg->opaque);

    /* Never lock if bbram8 is wo; and, only POR can clear the lock */
    if (s->bbram8_wo) {
        val64 = 0;
    } else {
        val64 |= s->regs[R_BBRAM_MSW_LOCK];
    }

    return val64;
}

static void bbram_isr_postw(RegisterInfo *reg, uint64_t val64)
{
    XlnxBBRam *s = XLNX_BBRAM(reg->opaque);

    bbram_update_irq(s);
}

static uint64_t bbram_ier_prew(RegisterInfo *reg, uint64_t val64)
{
    XlnxBBRam *s = XLNX_BBRAM(reg->opaque);
    uint32_t val = val64;

    s->regs[R_BBRAM_IMR] &= ~val;
    bbram_update_irq(s);
    return 0;
}

static uint64_t bbram_idr_prew(RegisterInfo *reg, uint64_t val64)
{
    XlnxBBRam *s = XLNX_BBRAM(reg->opaque);
    uint32_t val = val64;

    s->regs[R_BBRAM_IMR] |= val;
    bbram_update_irq(s);
    return 0;
}

static RegisterAccessInfo bbram_ctrl_regs_info[] = {
    {   .name = "BBRAM_STATUS",  .addr = A_BBRAM_STATUS,
        .rsvd = 0xee,
        .ro = 0x3ff,
    },{ .name = "BBRAM_CTRL",  .addr = A_BBRAM_CTRL,
        .post_write = bbram_ctrl_postw,
    },{ .name = "PGM_MODE",  .addr = A_PGM_MODE,
        .post_write = bbram_pgm_mode_postw,
    },{ .name = "BBRAM_AES_CRC",  .addr = A_BBRAM_AES_CRC,
        .post_write = bbram_aes_crc_postw,
        .post_read = bbram_wo_postr,
    },{ .name = "BBRAM_0",  .addr = A_BBRAM_0,
        .pre_write = bbram_key_prew,
        .post_write = bbram_key_postw,
        .post_read = bbram_wo_postr,
    },{ .name = "BBRAM_1",  .addr = A_BBRAM_1,
        .pre_write = bbram_key_prew,
        .post_write = bbram_key_postw,
        .post_read = bbram_wo_postr,
    },{ .name = "BBRAM_2",  .addr = A_BBRAM_2,
        .pre_write = bbram_key_prew,
        .post_write = bbram_key_postw,
        .post_read = bbram_wo_postr,
    },{ .name = "BBRAM_3",  .addr = A_BBRAM_3,
        .pre_write = bbram_key_prew,
        .post_write = bbram_key_postw,
        .post_read = bbram_wo_postr,
    },{ .name = "BBRAM_4",  .addr = A_BBRAM_4,
        .pre_write = bbram_key_prew,
        .post_write = bbram_key_postw,
        .post_read = bbram_wo_postr,
    },{ .name = "BBRAM_5",  .addr = A_BBRAM_5,
        .pre_write = bbram_key_prew,
        .post_write = bbram_key_postw,
        .post_read = bbram_wo_postr,
    },{ .name = "BBRAM_6",  .addr = A_BBRAM_6,
        .pre_write = bbram_key_prew,
        .post_write = bbram_key_postw,
        .post_read = bbram_wo_postr,
    },{ .name = "BBRAM_7",  .addr = A_BBRAM_7,
        .pre_write = bbram_key_prew,
        .post_write = bbram_key_postw,
        .post_read = bbram_wo_postr,
    },{ .name = "BBRAM_8",  .addr = A_BBRAM_8,
        .pre_write = bbram_r8_prew,
        .post_write = bbram_r8_postw,
        .post_read = bbram_r8_postr,
    },{ .name = "BBRAM_SLVERR",  .addr = A_BBRAM_SLVERR,
        .rsvd = ~1,
    },{ .name = "BBRAM_ISR",  .addr = A_BBRAM_ISR,
        .w1c = 0x1,
        .post_write = bbram_isr_postw,
    },{ .name = "BBRAM_IMR",  .addr = A_BBRAM_IMR,
        .ro = 0x1,
    },{ .name = "BBRAM_IER",  .addr = A_BBRAM_IER,
        .pre_write = bbram_ier_prew,
    },{ .name = "BBRAM_IDR",  .addr = A_BBRAM_IDR,
        .pre_write = bbram_idr_prew,
    },{ .name = "BBRAM_MSW_LOCK",  .addr = A_BBRAM_MSW_LOCK,
        .pre_write = bbram_msw_lock_prew,
        .ro = ~R_BBRAM_MSW_LOCK_VAL_MASK,
    }
};

static void bbram_ctrl_reset_hold(Object *obj)
{
    XlnxBBRam *s = XLNX_BBRAM(obj);
    unsigned int i;

    for (i = 0; i < ARRAY_SIZE(s->regs_info); ++i) {
        if (i < R_BBRAM_0 || i > R_BBRAM_8) {
            register_reset(&s->regs_info[i]);
        }
    }

    bbram_aes_sync(s);
    bbram_update_irq(s);
}

static const MemoryRegionOps bbram_ctrl_ops = {
    .read = register_read_memory,
    .write = register_write_memory,
    .endianness = DEVICE_LITTLE_ENDIAN,
    .valid = {
        .min_access_size = 4,
        .max_access_size = 4,
    },
};

static void bbram_ctrl_realize(DeviceState *dev, Error **errp)
{
    XlnxBBRam *s = XLNX_BBRAM(dev);

    if (s->crc_zpads) {
        s->bbram8_wo = true;
    }

    bbram_bdrv_read(s, errp);
}

static void bbram_ctrl_init(Object *obj)
{
    XlnxBBRam *s = XLNX_BBRAM(obj);
    SysBusDevice *sbd = SYS_BUS_DEVICE(obj);
    RegisterInfoArray *reg_array;

    reg_array =
        register_init_block32(DEVICE(obj), bbram_ctrl_regs_info,
                              ARRAY_SIZE(bbram_ctrl_regs_info),
                              s->regs_info, s->regs,
                              &bbram_ctrl_ops,
                              XLNX_BBRAM_ERR_DEBUG,
                              R_MAX * 4);

    sysbus_init_mmio(sbd, &reg_array->mem);
    sysbus_init_irq(sbd, &s->irq_bbram);
}

static void bbram_prop_set_drive(Object *obj, Visitor *v, const char *name,
                                 void *opaque, Error **errp)
{
    DeviceState *dev = DEVICE(obj);

    qdev_prop_drive.set(obj, v, name, opaque, errp);

    /* Fill initial data if backend is attached after realized */
    if (dev->realized) {
        bbram_bdrv_read(XLNX_BBRAM(obj), errp);
    }
}

static void bbram_prop_get_drive(Object *obj, Visitor *v, const char *name,
                                 void *opaque, Error **errp)
{
    qdev_prop_drive.get(obj, v, name, opaque, errp);
}

static void bbram_prop_release_drive(Object *obj, const char *name,
                                     void *opaque)
{
    qdev_prop_drive.release(obj, name, opaque);
}

static const PropertyInfo bbram_prop_drive = {
    .name  = "str",
    .description = "Node name or ID of a block device to use as BBRAM backend",
    .realized_set_allowed = true,
    .get = bbram_prop_get_drive,
    .set = bbram_prop_set_drive,
    .release = bbram_prop_release_drive,
};

static void bbram_prop_set_erase(Object *obj, Visitor *v,
                                 const char *name, void *opaque,
                                 Error **errp)
{
    bool do_erase = false;

    visit_type_bool(v, name, &do_erase, errp);
    if (*errp) {
        return;
    }

    if (do_erase) {
        bbram_zeroize(XLNX_BBRAM(obj));
    }
}

static const PropertyInfo bbram_prop_erase = {
    .name = "bool",
    .description = "Set true to erase entire bbram",
    .set = bbram_prop_set_erase,
    .realized_set_allowed = true,
};

static const VMStateDescription vmstate_bbram_ctrl = {
    .name = TYPE_XLNX_BBRAM,
    .version_id = 1,
    .minimum_version_id = 1,
    .fields = (VMStateField[]) {
        VMSTATE_UINT32_ARRAY(regs, XlnxBBRam, R_MAX),
        VMSTATE_END_OF_LIST(),
    }
};

static Property bbram_ctrl_props[] = {
    DEFINE_PROP("drive", XlnxBBRam, blk, bbram_prop_drive, BlockBackend *),
    DEFINE_PROP("erase", XlnxBBRam, ext_erase, bbram_prop_erase, bool),
    DEFINE_PROP_LINK("zynqmp-aes-key-sink-bbram", XlnxBBRam, aes,
                     TYPE_ZYNQMP_AES_KEY_SINK, ZynqMPAESKeySink *),
    DEFINE_PROP_UINT32("crc-zpads", XlnxBBRam, crc_zpads, 1),
    DEFINE_PROP_END_OF_LIST(),
};

static void bbram_ctrl_class_init(ObjectClass *klass, void *data)
{
    DeviceClass *dc = DEVICE_CLASS(klass);
    ResettableClass *rc = RESETTABLE_CLASS(klass);

    rc->phases.hold = bbram_ctrl_reset_hold;
    dc->realize = bbram_ctrl_realize;
    dc->vmsd = &vmstate_bbram_ctrl;
    device_class_set_props(dc, bbram_ctrl_props);
}

static const TypeInfo bbram_ctrl_info = {
    .name          = TYPE_XLNX_BBRAM,
    .parent        = TYPE_SYS_BUS_DEVICE,
    .instance_size = sizeof(XlnxBBRam),
    .class_init    = bbram_ctrl_class_init,
    .instance_init = bbram_ctrl_init,
};

static void bbram_ctrl_register_types(void)
{
    type_register_static(&bbram_ctrl_info);
}

type_init(bbram_ctrl_register_types)
