root/usr/src/lib/libjedec/common/libjedec_spd_lp4.c
/*
 * This file and its contents are supplied under the terms of the
 * Common Development and Distribution License ("CDDL"), version 1.0.
 * You may only use this file in accordance with the terms of version
 * 1.0 of the CDDL.
 *
 * A full copy of the text of the CDDL should have accompanied this
 * source.  A copy of the CDDL is also available via the Internet at
 * http://www.illumos.org/license/CDDL.
 */

/*
 * Copyright 2024 Oxide Computer Company
 */

/*
 * LPDDR4, LPDDR4X, and LPDDR3 SPD processing logic. For an overview of the
 * processing design please see libjedec_spd.c. These have a similar design to
 * DDR4 and leverages the existing manufacturing logic there.
 */

#include <sys/sysmacros.h>
#include <sys/debug.h>
#include "libjedec_spd.h"

static const spd_value_map_t spd_lp4_nbytes_used_map[] = {
        { SPD_LP4_NBYTES_USED_UNDEF, 0, true },
        { SPD_LP4_NBYTES_USED_128, 128, false },
        { SPD_LP4_NBYTES_USED_256, 256, false },
        { SPD_LP4_NBYTES_USED_384, 384, false },
        { SPD_LP4_NBYTES_USED_512, 512, false }
};

static const spd_value_map_t spd_lp4_nbytes_total_map[] = {
        { SPD_LP4_NBYTES_TOTAL_UNDEF, 0, true },
        { SPD_LP4_NBYTES_TOTAL_256, 256, false },
        { SPD_LP4_NBYTES_TOTAL_512, 512, false }
};

static void
spd_parse_lp4_nbytes(spd_info_t *si, uint32_t off, uint32_t len,
    const char *key)
{
        const uint8_t data = si->si_data[off];
        const uint8_t used = SPD_LP4_NBYTES_USED(data);
        const uint8_t total = SPD_LP4_NBYTES_TOTAL(data);

        spd_insert_map(si, SPD_KEY_NBYTES_USED, used, spd_lp4_nbytes_used_map,
            ARRAY_SIZE(spd_lp4_nbytes_used_map));
        spd_insert_map(si, SPD_KEY_NBYTES_TOTAL, total,
            spd_lp4_nbytes_total_map, ARRAY_SIZE(spd_lp4_nbytes_total_map));

        /*
         * Like with DDR4 there is no specific way to determine the type. We
         * take our best guess based upon the size. A 256 byte EEPROM is most
         * likely the EE1002 spec and the 512 byte EEPROM is the EE1004 as those
         * are the defended sized.
         */
        spd_upsert_flag(si, SPD_KEY_DEVS, SPD_DEVICE_SPD);
        if (total == SPD_LP4_NBYTES_TOTAL_256) {
                spd_nvl_insert_u32(si, SPD_KEY_DEV_SPD_TYPE, SPD_SPD_T_EE1002);
        } else if (total == SPD_LP4_NBYTES_TOTAL_512) {
                spd_nvl_insert_u32(si, SPD_KEY_DEV_SPD_TYPE, SPD_SPD_T_EE1004);
        }
}

/*
 * Like DDR4 the value of zero is defined for extensions; however, there are no
 * defined type extensions. As such we don't check for it.
 */
static const spd_value_map_t spd_lp4_mod_type_map[] = {
        { SPD_LP4_MOD_TYPE_TYPE_LPDIMM, SPD_MOD_TYPE_LPDIMM, false },
        { SPD_LP4_MOD_TYPE_TYPE_SOLDER, SPD_MOD_TYPE_SOLDER, false }
};

static const spd_value_map_t spd_lp4_mod_is_hybrid_map[] = {
        { 0, SPD_MOD_NOT_HYBRID, false },
        { 1, SPD_MOD_HYBRID_NVDIMMM, false }
};

static void
spd_parse_lp4_mod_type(spd_info_t *si, uint32_t off, uint32_t len,
    const char *key)
{
        const uint8_t data = si->si_data[off];
        const uint8_t type = SPD_LP4_MOD_TYPE_TYPE(data);
        const uint8_t is_hyb = SPD_LP4_MOD_TYPE_ISHYBRID(data);

        spd_insert_map(si, SPD_KEY_MOD_HYBRID_TYPE, is_hyb,
            spd_lp4_mod_is_hybrid_map, ARRAY_SIZE(spd_lp4_mod_is_hybrid_map));

        spd_insert_map(si, SPD_KEY_MOD_TYPE, type, spd_lp4_mod_type_map,
            ARRAY_SIZE(spd_lp4_mod_type_map));
}

static const spd_value_map64_t spd_lp4_density_map[] = {
        { SPD_LP4_DENSITY_DENSITY_1Gb, 1ULL * 1024ULL * 1024ULL * 1024ULL,
            false },
        { SPD_LP4_DENSITY_DENSITY_2Gb, 2ULL * 1024ULL * 1024ULL * 1024ULL,
            false },
        { SPD_LP4_DENSITY_DENSITY_4Gb, 4ULL * 1024ULL * 1024ULL * 1024ULL,
            false },
        { SPD_LP4_DENSITY_DENSITY_8Gb, 8ULL * 1024ULL * 1024ULL * 1024ULL,
            false },
        { SPD_LP4_DENSITY_DENSITY_16Gb, 16ULL * 1024ULL * 1024ULL * 1024ULL,
            false },
        { SPD_LP4_DENSITY_DENSITY_32Gb, 32ULL * 1024ULL * 1024ULL * 1024ULL,
            false },
        { SPD_LP4_DENSITY_DENSITY_12Gb, 12ULL * 1024ULL * 1024ULL * 1024ULL,
            false },
        { SPD_LP4_DENSITY_DENSITY_24Gb, 24ULL * 1024ULL * 1024ULL * 1024ULL,
            false },
        { SPD_LP4_DENSITY_DENSITY_3Gb, 3ULL * 1024ULL * 1024ULL * 1024ULL,
            false },
        { SPD_LP4_DENSITY_DENSITY_6Gb, 64ULL * 1024ULL * 1024ULL * 1024ULL,
            false },
        { SPD_LP4_DENSITY_DENSITY_18Gb, 18ULL * 1024ULL * 1024ULL * 1024ULL,
            false },
};

static const spd_value_range_t spd_lp4_nbgrp_range = {
        .svr_max = SPD_LP4_DENSITY_NBG_BITS_MAX
};

static const spd_value_range_t spd_lp4_nba_range = {
        .svr_max = SPD_LP4_DENSITY_NBA_BITS_MAX,
        .svr_base = SPD_LP4_DENSITY_NBA_BITS_BASE
};

static void
spd_parse_lp4_density(spd_info_t *si, uint32_t off, uint32_t len,
    const char *key)
{
        const uint8_t data = si->si_data[off];
        const uint8_t nbg = SPD_LP4_DENSITY_NBG_BITS(data);
        const uint8_t nbank = SPD_LP4_DENSITY_NBA_BITS(data);
        const uint8_t dens = SPD_LP4_DENSITY_DENSITY(data);

        spd_insert_range(si, SPD_KEY_NBGRP_BITS, nbg, &spd_lp4_nbgrp_range);
        spd_insert_range(si, SPD_KEY_NBANK_BITS, nbank, &spd_lp4_nba_range);
        spd_insert_map64(si, SPD_KEY_DIE_SIZE, dens, spd_lp4_density_map,
            ARRAY_SIZE(spd_lp4_density_map));
}

static const spd_value_range_t spd_lp4_nrow_range = {
        .svr_max = SPD_LP4_ADDR_NROWS_MAX,
        .svr_base = SPD_LP4_ADDR_NROWS_BASE
};

static const spd_value_range_t spd_lp4_ncol_range = {
        .svr_max = SPD_LP4_ADDR_NCOLS_MAX,
        .svr_base = SPD_LP4_ADDR_NCOLS_BASE
};

static void
spd_parse_lp4_addr(spd_info_t *si, uint32_t off, uint32_t len,
    const char *key)
{
        const uint8_t data = si->si_data[off];
        const uint8_t nrows = SPD_LP4_ADDR_NROWS(data);
        const uint8_t ncols = SPD_LP4_ADDR_NCOLS(data);

        spd_insert_range(si, SPD_KEY_NROW_BITS, nrows, &spd_lp4_nrow_range);
        spd_insert_range(si, SPD_KEY_NCOL_BITS, ncols, &spd_lp4_ncol_range);
}

static const spd_value_range_t spd_lp4_ndie_range = {
        .svr_base = SPD_LP4_PKG_DIE_CNT_BASE
};

static const spd_value_range_t spd_lp4_nchan_range = {
        .svr_max = SPD_LP4_PKG_NCHAN_MAX,
        .svr_exp = true
};

static void
spd_parse_lp4_pkg(spd_info_t *si, uint32_t off, uint32_t len,
    const char *key)
{
        const uint8_t data = si->si_data[off];
        const uint8_t ndie = SPD_LP4_PKG_DIE_CNT(data);
        const uint8_t nchan = SPD_LP4_PKG_NCHAN(data);


        if (SPD_LP4_PKG_TYPE(data) == SPD_LP4_PKG_TYPE_NOT) {
                spd_nvl_insert_key(si, SPD_KEY_PKG_NOT_MONO);
        }

        spd_insert_range(si, SPD_KEY_PKG_NDIE, ndie, &spd_lp4_ndie_range);
        spd_insert_range(si, SPD_KEY_DRAM_NCHAN, nchan, &spd_lp4_nchan_range);
}

static const spd_value_map_t spd_lp4_maw_map[] = {
        { SPD_LP4_OPT_FEAT_MAW_8192X, 8192, false },
        { SPD_LP4_OPT_FEAT_MAW_4096X, 4096, false },
        { SPD_LP4_OPT_FEAT_MAW_2048X, 2048, false }
};

static const spd_value_map_t spd_lp4_mac_map[] = {
        { SPD_LP4_OPT_FEAT_MAC_UNTESTED, 0, true},
        { SPD_LP4_OPT_FEAT_MAC_700K, 700000, false },
        { SPD_LP4_OPT_FEAT_MAC_600K, 600000, false },
        { SPD_LP4_OPT_FEAT_MAC_500K, 500000, false },
        { SPD_LP4_OPT_FEAT_MAC_400K, 400000, false },
        { SPD_LP4_OPT_FEAT_MAC_300K, 300000, false },
        { SPD_LP4_OPT_FEAT_MAC_200K, 200000, false },
        { SPD_LP4_OPT_FEAT_MAC_UNLIMITED, SPD_KEY_MAC_UNLIMITED, false }
};

static void
spd_parse_lp4_feat(spd_info_t *si, uint32_t off, uint32_t len,
    const char *key)
{
        const uint8_t data = si->si_data[off];
        const uint8_t maw = SPD_DDR4_OPT_FEAT_MAW(data);
        const uint8_t mac = SPD_DDR4_OPT_FEAT_MAC(data);

        spd_insert_map(si, SPD_KEY_MAW, maw, spd_lp4_maw_map,
            ARRAY_SIZE(spd_lp4_maw_map));
        spd_insert_map(si, SPD_KEY_MAC, mac, spd_lp4_mac_map,
            ARRAY_SIZE(spd_lp4_mac_map));
}

static void
spd_parse_lp4_feat2(spd_info_t *si, uint32_t off, uint32_t len,
    const char *key)
{
        const uint8_t data = si->si_data[off];
        const uint8_t ppr_sup = SPD_DDR4_OPT_FEAT2_PPR(data);
        spd_ppr_flags_t flags = 0;

        switch (ppr_sup) {
        case SPD_LP4_OPT_FEAT2_PPR_1RPBG:
                spd_nvl_insert_u32(si, SPD_KEY_PPR_GRAN,
                    SPD_PPR_GRAN_BANK_GROUP);
                flags |= SPD_PPR_F_HARD_PPR;
                break;
        case SPD_LP4_OPT_FEAT2_PPR_NOTSUP:
                /*
                 * No PPR, nothing to do.
                 */
                return;
        default:
                /*
                 * Unknown PPR value.
                 */
                spd_nvl_err(si, SPD_KEY_PPR, SPD_ERROR_NO_XLATE,
                    "encountered unknown value: 0x%x", ppr_sup);
                return;
        }

        if (SPD_LP4_OPT_FEAT2_SOFT_PPR(data))
                flags |= SPD_PPR_F_SOFT_PPR;
        spd_nvl_insert_u32(si, SPD_KEY_PPR, flags);
}

static const spd_value_range_t spd_lp4_nrank_range = {
        .svr_max = SPD_LP4_MOD_ORG_NPKG_RANK_MAX,
        .svr_base = SPD_LP4_MOD_ORG_NPKG_RANK_BASE
};

static const spd_value_range_t spd_lp4_width_range = {
        .svr_max = SPD_LP4_MOD_ORG_WIDTH_MAX,
        .svr_base = SPD_LP4_MOD_ORG_WIDTH_BASE,
        .svr_exp = true
};

static void
spd_parse_lp4_mod_org(spd_info_t *si, uint32_t off, uint32_t len,
    const char *key)
{
        const uint8_t data = si->si_data[off];
        const uint8_t byte = SPD_LP4_MOD_ORG_IDENT(data);
        const uint8_t nrank = SPD_LP4_MOD_ORG_NPKG_RANK(data);
        const uint8_t width = SPD_LP4_MOD_ORG_WIDTH(data);

        if (byte == SPD_LP4_MOD_ORG_IDENT_BYTE) {
                spd_nvl_insert_key(si, SPD_KEY_LP_BYTE_MODE);
        }

        spd_insert_range(si, SPD_KEY_NRANKS, nrank, &spd_lp4_nrank_range);
        spd_insert_range(si, SPD_KEY_DRAM_WIDTH, width, &spd_lp4_width_range);

}

static const spd_value_map_t spd_lp4_chan_map[] = {
        { SPD_LP4_BUS_WIDTH_NCHAN_1ch, 1, false },
        { SPD_LP4_BUS_WIDTH_NCHAN_2ch, 2, false },
        { SPD_LP4_BUS_WIDTH_NCHAN_3ch, 3, false },
        { SPD_LP4_BUS_WIDTH_NCHAN_4ch, 4, false },
        { SPD_LP4_BUS_WIDTH_NCHAN_8ch, 8, false }
};

static const spd_value_range_t spd_lp4_chan_width_range = {
        .svr_base = SPD_LP4_BUS_WIDTH_PRI_BASE,
        .svr_max = SPD_LP4_BUS_WIDTH_PRI_MAX,
        .svr_exp = true
};

static void
spd_parse_lp4_bus_width(spd_info_t *si, uint32_t off, uint32_t len,
    const char *key)
{
        const uint8_t data = si->si_data[off];
        const uint8_t nchan = SPD_LP4_BUS_WIDTH_NCHAN(data);
        const uint8_t ext = SPD_LP4_BUS_WIDTH_EXT(data);
        const uint8_t width = SPD_LP4_BUS_WIDTH_PRI(data);

        spd_insert_map(si, SPD_KEY_NSUBCHAN, nchan, spd_lp4_chan_map,
            ARRAY_SIZE(spd_lp4_chan_map));

        if (ext != SPD_LP4_BUS_WIDTH_EXT_NONE) {
                spd_nvl_err(si, SPD_KEY_ECC_WIDTH, SPD_ERROR_NO_XLATE,
                    "encountered invalid bus width extension: 0x%x", ext);
        } else {
                spd_nvl_insert_u32(si, SPD_KEY_ECC_WIDTH, 0);
        }

        spd_insert_range(si, SPD_KEY_DATA_WIDTH, width,
            &spd_lp4_chan_width_range);
}

static void
spd_parse_lp4_therm(spd_info_t *si, uint32_t off, uint32_t len,
    const char *key)
{
        const uint8_t data = si->si_data[off];

        if (SPD_LP4_MOD_THERM_PRES(data) != 0)
                spd_upsert_flag(si, key, SPD_DEVICE_TEMP_1);
        /*
         * Like DDR4, LPDDR3/4 only define that this must be TSE2004av
         * compliant.
         */
        spd_nvl_insert_u32(si, SPD_KEY_DEV_TEMP_TYPE, SPD_TEMP_T_TSE2004av);
}

static const spd_value_range_t spd_lp4_dsm_range = {
        .svr_max = SPD_LP4_SIGLOAD1_DSM_LOAD_MAX,
        .svr_exp = true
};

static const spd_value_range_t spd_lp4_cac_range = {
        .svr_max = SPD_LP4_SIGLOAD1_CAC_LOAD_MAX,
        .svr_exp = true
};

static const spd_value_range_t spd_lp4_cs_range = {
        .svr_max = SPD_LP4_SIGLOAD1_CS_LOAD_MAX,
        .svr_exp = true
};

static void
spd_parse_lp4_sigload(spd_info_t *si, uint32_t off, uint32_t len,
    const char *key)
{
        const uint8_t data = si->si_data[off];
        const uint8_t dsm = SPD_LP5_SIGLOAD1_DSM_LOAD(data);
        const uint8_t cac = SPD_LP5_SIGLOAD1_CAC_LOAD(data);
        const uint8_t cs = SPD_LP5_SIGLOAD1_CS_LOAD(data);

        spd_insert_range(si, SPD_KEY_LP_LOAD_DSM, dsm, &spd_lp4_dsm_range);
        spd_insert_range(si, SPD_KEY_LP_LOAD_CAC, cac, &spd_lp4_cac_range);
        spd_insert_range(si, SPD_KEY_LP_LOAD_CS, cs, &spd_lp4_cs_range);
}

static const spd_value_map_t spd_lp4_ts_mtb[] = {
        { SPD_LP4_TIMEBASE_MTB_125ps, SPD_LP4_MTB_PS, false }
};

static const spd_value_map_t spd_lp4_ts_ftb[] = {
        { SPD_LP4_TIMEBASE_FTB_1ps, SPD_LP4_FTB_PS, false }
};

static void
spd_parse_lp4_timebase(spd_info_t *si, uint32_t off, uint32_t len,
    const char *key)
{
        const uint8_t data = si->si_data[off];
        const uint8_t mtb = SPD_LP4_TIMEBASE_MTB(data);
        const uint8_t ftb = SPD_LP4_TIMEBASE_FTB(data);

        spd_insert_map(si, SPD_KEY_MTB, mtb, spd_lp4_ts_mtb,
            ARRAY_SIZE(spd_lp4_ts_mtb));
        spd_insert_map(si, SPD_KEY_FTB, ftb, spd_lp4_ts_ftb,
            ARRAY_SIZE(spd_lp4_ts_ftb));
}

/*
 * The first byte of CAS values is non-uniform. The second byte onwards begins
 * at CL=16 and each bit indicates a CL two apart. We use this array of all CAS
 * values as most of them aren't actually defined by the spec.
 */
static const uint32_t spd_lp4_cas_map[32] = {
        [0] = 3,
        [1] = 6,
        [2] = 8,
        [3] = 9,
        [4] = 10,
        [5] = 11,
        [6] = 12,
        [7] = 14,
        [8] = 16,
        [9] = 18,
        [10] = 20,
        [11] = 22,
        [12] = 24,
        [13] = 26,
        [14] = 28,
        [15] = 30,
        [16] = 32,
        [18] = 36,
        [20] = 40,
        [22] = 44
};

static void
spd_parse_lp4_cas(spd_info_t *si, uint32_t off, uint32_t len,
    const char *key)
{
        uint32_t cas[32] = { 0 };
        uint_t ncas = 0;

        ASSERT3U(len, ==, 4);
        for (uint32_t byte = 0; byte < MIN(len, 4); byte++) {
                uint32_t data = si->si_data[off + byte];
                uint32_t nbits = NBBY;

                for (uint32_t i = 0; i < nbits; i++) {
                        if (bitx8(data, i, i) == 1) {
                                uint8_t pos = i + byte * NBBY;
                                if (spd_lp4_cas_map[pos] != 0) {
                                        cas[ncas] = spd_lp4_cas_map[pos];
                                        ncas++;
                                } else {
                                        spd_nvl_err(si, key, SPD_ERROR_BAD_DATA,
                                            "invalid CAS byte %u/bit %u found",
                                            byte, i);
                                        return;
                                }
                        }
                }
        }

        spd_nvl_insert_u32_array(si, key, cas, ncas);
}

static void
spd_parse_lp4_rwlat(spd_info_t *si, uint32_t off, uint32_t len,
    const char *key)
{
        const uint8_t data = si->si_data[off];
        const uint8_t wr = SPD_LP4_RWLAT_WRITE(data);
        const uint8_t rd = SPD_LP4_RWLAT_READ(data);
        spd_lp_rwlat_t rwlat = 0;

        switch (wr) {
        case SPD_LP4_RWLAT_WRITE_A:
                rwlat |= SPD_LP_RWLAT_WRITE_A;
                break;
        case SPD_LP4_RWLAT_WRITE_B:
                rwlat |= SPD_LP_RWLAT_WRITE_B;
                break;
        default:
                spd_nvl_err(si, key, SPD_ERROR_NO_XLATE, "unknown write "
                    "latency set value: 0x%x", wr);
                return;
        }

        switch (rd) {
        case SPD_LP4_RWLAT_DBIRD_DIS:
                break;
        case SPD_LP4_RWLAT_DBIRD_EN:
                rwlat |= SPD_LP_RWLAT_DBIRD_EN;
                break;
        default:
                spd_nvl_err(si, key, SPD_ERROR_NO_XLATE, "unknown read "
                    "latency mode value: 0x%x", rd);
                return;
        }

        spd_nvl_insert_u32(si, key, rwlat);
}

static const spd_parse_t spd_lp4_base[] = {
        { .sp_off = SPD_LP4_NBYTES, .sp_parse = spd_parse_lp4_nbytes },
        { .sp_off = SPD_LP4_SPD_REV, .sp_parse = spd_parse_rev },
        /*
         * We have previously validated that the DRAM type is something that we
         * understand. We pass through the raw enum to users here.
         */
        { .sp_off = SPD_LP4_DRAM_TYPE, .sp_key = SPD_KEY_DRAM_TYPE,
            .sp_parse = spd_parse_raw_u8 },

        { .sp_off = SPD_LP4_MOD_TYPE, .sp_parse = spd_parse_lp4_mod_type },
        { .sp_off = SPD_LP4_DENSITY, .sp_parse = spd_parse_lp4_density },
        { .sp_off = SPD_LP4_ADDR, .sp_parse = spd_parse_lp4_addr },
        { .sp_off = SPD_LP4_PKG, .sp_parse = spd_parse_lp4_pkg },
        { .sp_off = SPD_LP4_OPT_FEAT, .sp_parse = spd_parse_lp4_feat },
        { .sp_off = SPD_LP4_OPT_FEAT2, .sp_parse = spd_parse_lp4_feat2 },
        { .sp_off = SPD_LP4_MOD_ORG, .sp_parse = spd_parse_lp4_mod_org },
        { .sp_off = SPD_LP4_BUS_WIDTH, .sp_parse = spd_parse_lp4_bus_width },
        { .sp_off = SPD_LP4_MOD_THERM, .sp_parse = spd_parse_lp4_therm },
        { .sp_off = SPD_LP4_SIGLOAD, .sp_parse = spd_parse_lp4_sigload },
        { .sp_off = SPD_LP4_TIMEBASE, .sp_parse = spd_parse_lp4_timebase },
        { .sp_off = SPD_LP4_TCKAVG_MIN, .sp_key = SPD_KEY_TCKAVG_MIN,
            .sp_len = SPD_LP4_TCKAVG_MIN_FINE - SPD_LP4_TCKAVG_MIN + 1,
            .sp_parse = spd_parse_mtb_ftb_time_pair },
        { .sp_off = SPD_LP4_TCKAVG_MAX, .sp_key = SPD_KEY_TCKAVG_MAX,
            .sp_len = SPD_LP4_TCKAVG_MAX_FINE - SPD_LP4_TCKAVG_MAX + 1,
            .sp_parse = spd_parse_mtb_ftb_time_pair },
        { .sp_off = SPD_LP4_CAS_SUP0, .sp_key = SPD_KEY_CAS,
            .sp_len = SPD_LP4_CAS_SUP3 - SPD_LP4_CAS_SUP0 + 1,
            .sp_parse = spd_parse_lp4_cas },
        { .sp_off = SPD_LP5_TAA_MIN, .sp_key = SPD_KEY_TAA_MIN,
            .sp_len = SPD_LP5_TAA_MIN_FINE - SPD_LP5_TAA_MIN + 1,
            .sp_parse = spd_parse_mtb_ftb_time_pair },
        { .sp_off = SPD_LP4_RWLAT, .sp_key = SPD_KEY_LP_RWLAT,
            .sp_parse = spd_parse_lp4_rwlat },
        { .sp_off = SPD_LP5_TRCD_MIN, .sp_key = SPD_KEY_TRCD_MIN,
            .sp_len = SPD_LP5_TRCD_MIN_FINE - SPD_LP5_TRCD_MIN + 1,
            .sp_parse = spd_parse_mtb_ftb_time_pair },
        { .sp_off = SPD_LP5_TRPAB_MIN, .sp_key = SPD_KEY_TRPAB_MIN,
            .sp_len = SPD_LP5_TRPAB_MIN_FINE - SPD_LP5_TRPAB_MIN + 1,
            .sp_parse = spd_parse_mtb_ftb_time_pair },
        { .sp_off = SPD_LP5_TRPPB_MIN, .sp_key = SPD_KEY_TRPPB_MIN,
            .sp_len = SPD_LP5_TRPPB_MIN_FINE - SPD_LP5_TRPPB_MIN + 1,
            .sp_parse = spd_parse_mtb_ftb_time_pair },
        { .sp_off = SPD_LP5_TRPPB_MIN, .sp_key = SPD_KEY_TRPPB_MIN,
            .sp_len = SPD_LP5_TRPPB_MIN_FINE - SPD_LP5_TRPPB_MIN + 1,
            .sp_parse = spd_parse_mtb_ftb_time_pair },
        { .sp_off = SPD_LP4_TRFCAB_MIN_LO, .sp_key = SPD_KEY_TRFCAB_MIN,
            .sp_len = 2, .sp_parse = spd_parse_mtb_pair },
        { .sp_off = SPD_LP4_TRFCPB_MIN_LO, .sp_key = SPD_KEY_TRFCPB_MIN,
            .sp_len = 2, .sp_parse = spd_parse_mtb_pair },
        { .sp_off = SPD_LP4_MAP_DQ0, .sp_key = SPD_KEY_DDR4_MAP_DQ0,
            .sp_parse = spd_parse_ddr4_nib_map },
        { .sp_off = SPD_LP4_MAP_DQ4, .sp_key = SPD_KEY_DDR4_MAP_DQ4,
            .sp_parse = spd_parse_ddr4_nib_map },
        { .sp_off = SPD_LP4_MAP_DQ8, .sp_key = SPD_KEY_DDR4_MAP_DQ8,
            .sp_parse = spd_parse_ddr4_nib_map },
        { .sp_off = SPD_LP4_MAP_DQ12, .sp_key = SPD_KEY_DDR4_MAP_DQ12,
            .sp_parse = spd_parse_ddr4_nib_map },
        { .sp_off = SPD_LP4_MAP_DQ16, .sp_key = SPD_KEY_DDR4_MAP_DQ16,
            .sp_parse = spd_parse_ddr4_nib_map },
        { .sp_off = SPD_LP4_MAP_DQ20, .sp_key = SPD_KEY_DDR4_MAP_DQ20,
            .sp_parse = spd_parse_ddr4_nib_map },
        { .sp_off = SPD_LP4_MAP_DQ24, .sp_key = SPD_KEY_DDR4_MAP_DQ24,
            .sp_parse = spd_parse_ddr4_nib_map },
        { .sp_off = SPD_LP4_MAP_DQ28, .sp_key = SPD_KEY_DDR4_MAP_DQ28,
            .sp_parse = spd_parse_ddr4_nib_map },
        { .sp_off = SPD_LP4_MAP_CB0, .sp_key = SPD_KEY_DDR4_MAP_CB0,
            .sp_parse = spd_parse_ddr4_nib_map },
        { .sp_off = SPD_LP4_MAP_CB4, .sp_key = SPD_KEY_DDR4_MAP_CB4,
            .sp_parse = spd_parse_ddr4_nib_map },
        { .sp_off = SPD_LP4_MAP_DQ32, .sp_key = SPD_KEY_DDR4_MAP_DQ32,
            .sp_parse = spd_parse_ddr4_nib_map },
        { .sp_off = SPD_LP4_MAP_DQ36, .sp_key = SPD_KEY_DDR4_MAP_DQ36,
            .sp_parse = spd_parse_ddr4_nib_map },
        { .sp_off = SPD_LP4_MAP_DQ40, .sp_key = SPD_KEY_DDR4_MAP_DQ40,
            .sp_parse = spd_parse_ddr4_nib_map },
        { .sp_off = SPD_LP4_MAP_DQ44, .sp_key = SPD_KEY_DDR4_MAP_DQ44,
            .sp_parse = spd_parse_ddr4_nib_map },
        { .sp_off = SPD_LP4_MAP_DQ48, .sp_key = SPD_KEY_DDR4_MAP_DQ48,
            .sp_parse = spd_parse_ddr4_nib_map },
        { .sp_off = SPD_LP4_MAP_DQ52, .sp_key = SPD_KEY_DDR4_MAP_DQ52,
            .sp_parse = spd_parse_ddr4_nib_map },
        { .sp_off = SPD_LP4_MAP_DQ56, .sp_key = SPD_KEY_DDR4_MAP_DQ56,
            .sp_parse = spd_parse_ddr4_nib_map },
        { .sp_off = SPD_LP4_MAP_DQ60, .sp_key = SPD_KEY_DDR4_MAP_DQ60,
            .sp_parse = spd_parse_ddr4_nib_map },
        { .sp_len = SPD_DDR4_CRC_MSB + 1, .sp_key = SPD_KEY_CRC_DDR4_BASE,
            .sp_parse = spd_parse_crc },
        { .sp_len = SPD_LP4_CRC_MSB + 1, .sp_key = SPD_KEY_CRC_DDR4_BASE,
            .sp_parse = spd_parse_crc },
};

static const spd_parse_t spd_lp4_lpdimm[] = {
        { .sp_off = SPD_LP4_LPDIMM_HEIGHT, .sp_key = SPD_KEY_MOD_HEIGHT,
            .sp_parse = spd_parse_height },
        { .sp_off = SPD_LP4_LPDIMM_THICK, .sp_parse = spd_parse_thickness },
        { .sp_off = SPD_LP4_LPDIMM_REF, .sp_parse = spd_parse_ddr4_design },
        { .sp_off = SPD_LP4_BLK1_CRC_START, .sp_len = SPD_LP4_BLK1_CRC_MSB +
            1 - SPD_LP4_BLK1_CRC_START, .sp_key = SPD_KEY_CRC_DDR4_BLK1,
            .sp_parse = spd_parse_crc }
};

static void
spd_parse_lp4_mod_specific(spd_info_t *si)
{
        uint32_t type;

        if (nvlist_lookup_uint32(si->si_nvl, SPD_KEY_MOD_TYPE, &type) != 0)
                return;

        switch (type) {
        case SPD_MOD_TYPE_LPDIMM:
                spd_parse(si, spd_lp4_lpdimm, ARRAY_SIZE(spd_lp4_lpdimm));
                break;
        default:
                break;
        }
}

void
spd_parse_lp4(spd_info_t *si)
{
        if (SPD_LP4_SPD_REV_ENC(si->si_data[SPD_LP4_SPD_REV]) !=
            SPD_LP4_SPD_REV_V1) {
                si->si_error = LIBJEDEC_SPD_UNSUP_REV;
                return;
        }

        spd_parse(si, spd_lp4_base, ARRAY_SIZE(spd_lp4_base));
        spd_parse_lp4_mod_specific(si);
        spd_parse_ddr4_mfg(si);
}