root/usr/src/lib/libjedec/common/libjedec_spd_ddr4.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
 */

/*
 * DDR4-specific SPD processing logic. For an overview of the processing design
 * please see libjedec_spd.c. Note, this currently does not handle NVDIMMs.
 */

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

static const spd_value_map_t spd_ddr4_nbytes_used_map[] = {
        { SPD_DDR4_NBYTES_USED_UNDEF, 0, true },
        { SPD_DDR4_NBYTES_USED_128, 128, false },
        { SPD_DDR4_NBYTES_USED_256, 256, false },
        { SPD_DDR4_NBYTES_USED_384, 384, false },
        { SPD_DDR4_NBYTES_USED_512, 512, false }
};

static const spd_value_map_t spd_ddr4_nbytes_total_map[] = {
        { SPD_DDR4_NBYTES_TOTAL_UNDEF, 0, true },
        { SPD_DDR4_NBYTES_TOTAL_256, 256, false },
        { SPD_DDR4_NBYTES_TOTAL_512, 512, false }
};

static void
spd_parse_ddr4_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_DDR4_NBYTES_USED(data);
        const uint8_t total = SPD_DDR4_NBYTES_TOTAL(data);

        spd_insert_map(si, SPD_KEY_NBYTES_USED, used, spd_ddr4_nbytes_used_map,
            ARRAY_SIZE(spd_ddr4_nbytes_used_map));
        spd_insert_map(si, SPD_KEY_NBYTES_TOTAL, total,
            spd_ddr4_nbytes_total_map, ARRAY_SIZE(spd_ddr4_nbytes_total_map));

        /*
         * Unlike DDR5, there is no specific definition to indicate that the SPD
         * is present or what type of device it is. There is only one standard
         * DDR4 EEPROM, EE1004, so we note that it's here when we process this.
         */
        spd_upsert_flag(si, SPD_KEY_DEVS, SPD_DEVICE_SPD);
        spd_nvl_insert_u32(si, SPD_KEY_DEV_SPD_TYPE, SPD_SPD_T_EE1004);
}

/*
 * DDR4 has a type value that we leave out: SPD_DDR4_MOD_TYPE_TYPE_EXT. The
 * external type says to look in another register; however, all types in that
 * register are reserved. So we just let it be flagged as an unknown value right
 * now. Which is mostly kind of right.
 */
static const spd_value_map_t spd_ddr4_mod_type_map[] = {
        { SPD_DDR4_MOD_TYPE_TYPE_RDIMM, SPD_MOD_TYPE_RDIMM, false },
        { SPD_DDR4_MOD_TYPE_TYPE_UDIMM, SPD_MOD_TYPE_UDIMM, false },
        { SPD_DDR4_MOD_TYPE_TYPE_SODIMM, SPD_MOD_TYPE_SODIMM, false },
        { SPD_DDR4_MOD_TYPE_TYPE_LRDIMM, SPD_MOD_TYPE_LRDIMM, false },
        { SPD_DDR4_MOD_TYPE_TYPE_MINI_RDIMM, SPD_MOD_TYPE_MINI_RDIMM, false },
        { SPD_DDR4_MOD_TYPE_TYPE_MINI_UDIMM, SPD_MOD_TYPE_MINI_UDIMM, false },
        { SPD_DDR4_MOD_TYPE_TYPE_72b_SORDIMM, SPD_MOD_TYPE_72b_SO_RDIMM,
            false },
        { SPD_DDR4_MOD_TYPE_TYPE_72b_SOUDIMM, SPD_MOD_TYPE_72b_SO_UDIMM,
            false },
        { SPD_DDR4_MOD_TYPE_TYPE_16b_SODIMM, SPD_MOD_TYPE_16b_SO_DIMM, false },
        { SPD_DDR4_MOD_TYPE_TYPE_32b_SODIMM, SPD_MOD_TYPE_32b_SO_DIMM, false }
};

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

static const spd_value_map_t spd_ddr4_mod_hybrid_map[] = {
        { SPD_DDR4_MOD_TYPE_HYBRID_NVDIMM_NF, SPD_MOD_TYPE_NVDIMM_N, false },
        { SPD_DDR4_MOD_TYPE_HYBRID_NVDIMM_P, SPD_MOD_TYPE_NVDIMM_P, false },
        { SPD_DDR4_MOD_TYPE_HYBRID_NVDIMM_H, SPD_MOD_TYPE_NVDIMM_H, false }
};

static void
spd_parse_ddr4_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_DDR4_MOD_TYPE_TYPE(data);
        const uint8_t is_hyb = SPD_DDR4_MOD_TYPE_ISHYBRID(data);
        const uint8_t hybrid = SPD_DDR4_MOD_TYPE_HYBRID(data);

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

        if (is_hyb != 0) {
                spd_insert_map(si, SPD_KEY_MOD_NVDIMM_TYPE, hybrid,
                    spd_ddr4_mod_hybrid_map,
                    ARRAY_SIZE(spd_ddr4_mod_hybrid_map));
        }

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

static const spd_value_map64_t spd_ddr4_density_map[] = {
        { SPD_DDR4_DENSITY_DENSITY_256Mb, 256ULL * 1024ULL * 1024ULL, false },
        { SPD_DDR4_DENSITY_DENSITY_512Mb, 512ULL * 1024ULL * 1024ULL, false },
        { SPD_DDR4_DENSITY_DENSITY_1Gb, 1024ULL * 1024ULL * 1024ULL, false },
        { SPD_DDR4_DENSITY_DENSITY_2Gb, 2ULL * 1024ULL * 1024ULL * 1024ULL,
            false },
        { SPD_DDR4_DENSITY_DENSITY_4Gb, 4ULL * 1024ULL * 1024ULL * 1024ULL,
            false },
        { SPD_DDR4_DENSITY_DENSITY_8Gb, 8ULL * 1024ULL * 1024ULL * 1024ULL,
            false },
        { SPD_DDR4_DENSITY_DENSITY_16Gb, 16ULL * 1024ULL * 1024ULL * 1024ULL,
            false },
        { SPD_DDR4_DENSITY_DENSITY_32Gb, 32ULL * 1024ULL * 1024ULL * 1024ULL,
            false },
        { SPD_DDR4_DENSITY_DENSITY_12Gb, 12ULL * 1024ULL * 1024ULL * 1024ULL,
            false },
        { SPD_DDR4_DENSITY_DENSITY_24Gb, 24ULL * 1024ULL * 1024ULL * 1024ULL,
            false },
};

static const spd_value_range_t spd_ddr4_nbgrp_range = {
        .svr_max = SPD_DDR4_DENSITY_NBG_BITS_MAX
};

static const spd_value_range_t spd_ddr4_nba_range = {
        .svr_max = SPD_DDR4_DENSITY_NBA_BITS_MAX,
        .svr_base = SPD_DDR4_DENSITY_NBA_BITS_BASE
};

static void
spd_parse_ddr4_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_DDR4_DENSITY_NBG_BITS(data);
        const uint8_t nbank = SPD_DDR4_DENSITY_NBA_BITS(data);
        const uint8_t dens = SPD_DDR4_DENSITY_DENSITY(data);

        spd_insert_range(si, SPD_KEY_NBGRP_BITS, nbg, &spd_ddr4_nbgrp_range);
        spd_insert_range(si, SPD_KEY_NBANK_BITS, nbank, &spd_ddr4_nba_range);
        spd_insert_map64(si, SPD_KEY_DIE_SIZE, dens, spd_ddr4_density_map,
            ARRAY_SIZE(spd_ddr4_density_map));
}

static const spd_value_range_t spd_ddr4_nrow_range = {
        .svr_max = SPD_DDR4_ADDR_NROWS_MAX,
        .svr_base = SPD_DDR4_ADDR_NROWS_BASE
};

static const spd_value_range_t spd_ddr4_ncol_range = {
        .svr_max = SPD_DDR4_ADDR_NCOLS_MAX,
        .svr_base = SPD_DDR4_ADDR_NCOLS_BASE
};

static void
spd_parse_ddr4_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_DDR4_ADDR_NROWS(data);
        const uint8_t ncols = SPD_DDR4_ADDR_NCOLS(data);

        spd_insert_range(si, SPD_KEY_NROW_BITS, nrows, &spd_ddr4_nrow_range);
        spd_insert_range(si, SPD_KEY_NCOL_BITS, ncols, &spd_ddr4_ncol_range);
}

static const spd_value_map_t spd_ddr4_sl_map[] = {
        { SPD_DDR4_PKG_SIG_LOAD_UNSPEC, SPD_SL_UNSPECIFIED, false },
        { SPD_DDR4_PKG_SIG_LOAD_MULTI, SPD_SL_MUTLI_STACK, false },
        { SPD_DDR4_PKG_SIG_LOAD_SINGLE, SPD_SL_3DS, false }
};

static void
spd_parse_ddr4_pkg_common(spd_info_t *si, uint8_t data, const char *die_key,
    const char *sl_key)
{
        const uint8_t ndie = SPD_DDR4_PKG_DIE_CNT(data) +
            SPD_DDR4_PKG_DIE_CNT_BASE;
        const uint8_t sl = SPD_DDR4_PKG_SIG_LOAD(data);

        spd_nvl_insert_u32(si, die_key, ndie);
        spd_insert_map(si, sl_key, sl, spd_ddr4_sl_map,
            ARRAY_SIZE(spd_ddr4_sl_map));
}

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

        if (SPD_DDR4_PKG_TYPE(data) == SPD_DDR4_PKG_TYPE_NOT) {
                spd_nvl_insert_key(si, SPD_KEY_PKG_NOT_MONO);
        }

        return (spd_parse_ddr4_pkg_common(si, si->si_data[off],
            SPD_KEY_PKG_NDIE, SPD_KEY_PKG_SL));
}

static void
spd_parse_ddr4_sec_pkg(spd_info_t *si, uint32_t off, uint32_t len,
    const char *key)
{
        ASSERT3U(off, >=, SPD_DDR4_PRI_PKG);

        if (SPD_DDR4_PKG_TYPE(si->si_data[SPD_DDR4_PRI_PKG]) ==
            SPD_DDR4_PKG_TYPE_MONO) {
                return;
        }

        return (spd_parse_ddr4_pkg_common(si, si->si_data[off],
            SPD_KEY_SEC_PKG_NDIE, SPD_KEY_SEC_PKG_SL));
}

static const spd_value_map_t spd_ddr4_maw_map[] = {
        { SPD_DDR4_OPT_FEAT_MAW_8192X, 8192, false },
        { SPD_DDR4_OPT_FEAT_MAW_4096X, 4096, false },
        { SPD_DDR4_OPT_FEAT_MAW_2048X, 2048, false }
};

static const spd_value_map_t spd_ddr4_mac_map[] = {
        { SPD_DDR4_OPT_FEAT_MAC_UNTESTED, 0, true},
        { SPD_DDR4_OPT_FEAT_MAC_700K, 700000, false },
        { SPD_DDR4_OPT_FEAT_MAC_600K, 600000, false },
        { SPD_DDR4_OPT_FEAT_MAC_500K, 500000, false },
        { SPD_DDR4_OPT_FEAT_MAC_400K, 400000, false },
        { SPD_DDR4_OPT_FEAT_MAC_300K, 300000, false },
        { SPD_DDR4_OPT_FEAT_MAC_200K, 200000, false },
        { SPD_DDR4_OPT_FEAT_MAC_UNLIMITED, SPD_KEY_MAC_UNLIMITED, false }
};

static void
spd_parse_ddr4_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_ddr4_maw_map,
            ARRAY_SIZE(spd_ddr4_maw_map));
        spd_insert_map(si, SPD_KEY_MAC, mac, spd_ddr4_mac_map,
            ARRAY_SIZE(spd_ddr4_mac_map));
}

static void
spd_parse_ddr4_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_DDR4_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_DDR4_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_DDR4_OPT_FEAT2_SOFT_PPR(data))
                flags |= SPD_PPR_F_SOFT_PPR;
        if (SPD_DDR4_OPT_FEAT2_MBIST_PPR(data))
                flags |= SPD_PPR_F_MBIST_PPR;
        spd_nvl_insert_u32(si, SPD_KEY_PPR, flags);
}

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

        if (SPD_DDR4_VOLT_V1P2_OPER(data) == 0)
                return;
        spd_nvl_insert_u32_array(si, key, volts, ARRAY_SIZE(volts));
}

static const spd_value_map_t spd_ddr4_dram_width[] = {
        { SPD_DDR4_MOD_ORG_WIDTH_4b, 4, false },
        { SPD_DDR4_MOD_ORG_WIDTH_8b, 8, false },
        { SPD_DDR4_MOD_ORG_WIDTH_16b, 16, false },
        { SPD_DDR4_MOD_ORG_WIDTH_32b, 32, false }
};

static const spd_value_range_t spd_ddr4_nrank_range = {
        .svr_base = SPD_DDR4_MOD_ORG_NPKG_RANK_BASE
};

static void
spd_parse_ddr4_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 mix = SPD_DDR4_MOD_ORG_RANK_MIX(data);
        const uint8_t nranks = SPD_DDR4_MOD_ORG_NPKG_RANK(data);
        const uint8_t width = SPD_DDR4_MOD_ORG_WIDTH(data);

        if (mix == SPD_DDR4_MOD_ORG_RANK_MIX_ASYM)
                spd_nvl_insert_key(si, SPD_KEY_RANK_ASYM);
        spd_insert_range(si, SPD_KEY_NRANKS, nranks, &spd_ddr4_nrank_range);
        spd_insert_map(si, SPD_KEY_DRAM_WIDTH, width, spd_ddr4_dram_width,
            ARRAY_SIZE(spd_ddr4_dram_width));
}

static const spd_value_map_t spd_ddr4_ext_width[] = {
        { SPD_DDR4_MOD_BUS_WIDTH_EXT_NONE, 0, false },
        { SPD_DDR4_MOD_BUS_WIDTH_EXT_8b, 8, false }
};

static const spd_value_map_t spd_ddr4_pri_width[] = {
        { SPD_DDR4_MOD_BUS_WIDTH_PRI_8b, 8, false },
        { SPD_DDR4_MOD_BUS_WIDTH_PRI_16b, 16, false },
        { SPD_DDR4_MOD_BUS_WIDTH_PRI_32b, 32, false },
        { SPD_DDR4_MOD_BUS_WIDTH_PRI_64b, 64, false },
};

static void
spd_parse_ddr4_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 ext = SPD_DDR4_MOD_BUS_WIDTH_EXT(data);
        const uint8_t pri = SPD_DDR4_MOD_BUS_WIDTH_PRI(data);

        /*
         * DDR4 is simpler than LPDDRx and DDR5. It only has a single channel
         * and each DRAM is only connected to one channel.
         */
        spd_nvl_insert_u32(si, SPD_KEY_NSUBCHAN, 1);
        spd_nvl_insert_u32(si, SPD_KEY_DRAM_NCHAN, 1);
        spd_insert_map(si, SPD_KEY_DATA_WIDTH, pri, spd_ddr4_pri_width,
            ARRAY_SIZE(spd_ddr4_pri_width));
        spd_insert_map(si, SPD_KEY_ECC_WIDTH, ext, spd_ddr4_ext_width,
            ARRAY_SIZE(spd_ddr4_ext_width));
}

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

        /*
         * In DDR4, there is only a single standard temperature device. It is
         * often integrated into the EEPROM, but from a JEDEC perspective these
         * each have their own device type.
         */
        if (SPD_DDR4_MOD_THERM_PRES(data) != 0) {
                spd_upsert_flag(si, key, SPD_DEVICE_TEMP_1);
                spd_nvl_insert_u32(si, SPD_KEY_DEV_TEMP_TYPE,
                    SPD_TEMP_T_TSE2004av);
        }
}

static const spd_value_map_t spd_ddr4_ts_mtb[] = {
        { SPD_DDR4_TIMEBASE_MTB_125ps, SPD_DDR4_MTB_PS, false }
};

static const spd_value_map_t spd_ddr4_ts_ftb[] = {
        { SPD_DDR4_TIMEBASE_FTB_1ps, SPD_DDR4_FTB_PS, false }
};

static void
spd_parse_ddr4_ts(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_DDR4_TIMEBASE_MTB(data);
        const uint8_t ftb = SPD_DDR4_TIMEBASE_FTB(data);


        spd_insert_map(si, SPD_KEY_MTB, mtb, spd_ddr4_ts_mtb,
            ARRAY_SIZE(spd_ddr4_ts_mtb));
        spd_insert_map(si, SPD_KEY_FTB, ftb, spd_ddr4_ts_ftb,
            ARRAY_SIZE(spd_ddr4_ts_ftb));
}

/*
 * t~RAS~ consists of the upper nibble at off and the MTB at off + 1.
 */
static void
spd_parse_ddr4_tras(spd_info_t *si, uint32_t off, uint32_t len,
    const char *key)
{
        const uint8_t ras_nib = SPD_DDR4_RAS_RC_UPPER_RAS(si->si_data[off]);
        ASSERT3U(len, ==, 2);

        return (spd_parse_ddr_time(si, key, ras_nib, si->si_data[off + 1], 0));
}

/*
 * t~RC~ consists of an upper 4-bit nibble at off. Its MTB is at off + 2. The
 * FTB is at off + len - 1.
 */
static void
spd_parse_ddr4_trc(spd_info_t *si, uint32_t off, uint32_t len,
    const char *key)
{
        const uint8_t rc_nib = SPD_DDR4_RAS_RC_UPPER_RC(si->si_data[off]);

        return (spd_parse_ddr_time(si, key, rc_nib, si->si_data[off + 2],
            si->si_data[off + len - 1]));
}

/*
 * Upper nibble in off, MTB in off + 1, no FTB.
 */
static void
spd_parse_ddr4_tfaw(spd_info_t *si, uint32_t off, uint32_t len,
    const char *key)
{
        const uint8_t faw_nib = SPD_DDR4_TFAW_UPPER_FAW(si->si_data[off]);
        return (spd_parse_ddr_time(si, key, faw_nib, si->si_data[off + 1], 0));
}

static void
spd_parse_ddr4_twr(spd_info_t *si, uint32_t off, uint32_t len,
    const char *key)
{
        const uint8_t twr_nib = SPD_DDR4_TWR_MIN_UPPER_TWR(si->si_data[off]);
        return (spd_parse_ddr_time(si, key, twr_nib, si->si_data[off + 1], 0));
}

static void
spd_parse_ddr4_twtrs(spd_info_t *si, uint32_t off, uint32_t len,
    const char *key)
{
        const uint8_t twtrs_nib = SPD_DDR4_TWRT_UPPER_TWRS(si->si_data[off]);
        return (spd_parse_ddr_time(si, key, twtrs_nib, si->si_data[off + 1],
            0));
}

static void
spd_parse_ddr4_twtrl(spd_info_t *si, uint32_t off, uint32_t len,
    const char *key)
{
        const uint8_t twtrl_nib = SPD_DDR4_TWRT_UPPER_TWRL(si->si_data[off]);
        return (spd_parse_ddr_time(si, key, twtrl_nib, si->si_data[off + 2],
            0));
}

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

        ASSERT3U(len, ==, 4);
        if (SPD_DDR4_CAS_SUP3_RANGE(si->si_data[off + 3]) ==
            SPD_DDR4_CAS_SUP3_RANGE_7) {
                cas_base = 7;
        } else {
                cas_base = 23;
        }

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

                /*
                 * The last byte reserves the last two bits.
                 */
                if (byte == len - 1)
                        nbits -= 2;

                for (uint32_t i = 0; i < nbits; i++) {
                        if (bitx8(data, i, i) == 1) {
                                cas[ncas] = cas_base + i + NBBY * byte;
                                ncas++;
                        }
                }
        }

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

static const uint32_t spd_ddr4_nib_map[0x18][0x4] = {
        { 0, 1, 2, 3 },
        { 0, 1, 3, 2 },
        { 0, 2, 1, 3 },
        { 0, 2, 3, 1 },
        { 0, 3, 1, 2 },
        { 0, 3, 2, 1 },
        { 1, 0, 2, 3 },
        { 1, 0, 3, 2 },
        { 1, 2, 0, 3 },
        { 1, 2, 3, 0 },
        { 1, 3, 0, 2 },
        { 1, 3, 2, 0 },
        { 2, 0, 1, 3 },
        { 2, 0, 3, 1 },
        { 2, 1, 0, 3 },
        { 2, 1, 3, 0 },
        { 2, 3, 0, 1 },
        { 2, 3, 1, 0 },
        { 3, 0, 1, 2 },
        { 3, 0, 2, 1 },
        { 3, 1, 0, 2 },
        { 3, 1, 2, 0 },
        { 3, 2, 0, 1 },
        { 3, 2, 1, 0 }
};

/*
 * This function is shared between LPDDR3/4 and DDR4. They have the same values.
 */
void
spd_parse_ddr4_nib_map(spd_info_t *si, uint32_t off, uint32_t len,
    const char *key)
{
        const uint8_t data = si->si_data[off];
        const uint8_t pkg = SPD_DDR4_MAP_PKG(data);
        const uint8_t nib = SPD_DDR4_MAP_NIBBLE(data);
        uint8_t idx = SPD_DDR4_MAP_IDX(data);
        uint32_t bits[4];

        /*
         * Because there is only a single legal value we don't make a specific
         * nvlist key for it; however, if it is incorrect we will complain about
         * it!
         */
        if (pkg != SPD_DDR4_MAP_PKG_FLIP) {
                spd_nvl_err(si, key, SPD_ERROR_NO_XLATE,
                    "encountered bad package value: 0x%x", pkg);
        }

        if (idx == SPD_DDR4_MAP_IDX_UNSPEC)
                return;
        idx--;

        if (idx >= ARRAY_SIZE(spd_ddr4_nib_map)) {
                spd_nvl_err(si, key, SPD_ERROR_NO_XLATE,
                    "encountered bad nibble mapping value: 0x%x", idx);
                return;
        }

        if (nib == 1) {
                bits[0] = spd_ddr4_nib_map[idx][0] + 4;
                bits[1] = spd_ddr4_nib_map[idx][1] + 4;
                bits[2] = spd_ddr4_nib_map[idx][2] + 4;
                bits[3] = spd_ddr4_nib_map[idx][3] + 4;
        } else {
                bits[0] = spd_ddr4_nib_map[idx][0];
                bits[1] = spd_ddr4_nib_map[idx][1];
                bits[2] = spd_ddr4_nib_map[idx][2];
                bits[3] = spd_ddr4_nib_map[idx][3];
        };

        spd_nvl_insert_u32_array(si, key, bits, ARRAY_SIZE(bits));
}

static const spd_parse_t spd_ddr4_common[] = {
        { .sp_off = SPD_DDR4_NBYTES, .sp_parse = spd_parse_ddr4_nbytes },
        { .sp_off = SPD_DDR4_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_DDR4_DRAM_TYPE, .sp_key = SPD_KEY_DRAM_TYPE,
            .sp_parse = spd_parse_raw_u8 },
        { .sp_off = SPD_DDR4_MOD_TYPE, .sp_parse = spd_parse_ddr4_mod_type },
        { .sp_off = SPD_DDR4_DENSITY, .sp_parse = spd_parse_ddr4_density },
        { .sp_off = SPD_DDR4_ADDR, .sp_parse = spd_parse_ddr4_addr },
        { .sp_off = SPD_DDR4_PRI_PKG, .sp_parse = spd_parse_ddr4_pri_pkg },
        { .sp_off = SPD_DDR4_SEC_PKG, .sp_parse = spd_parse_ddr4_sec_pkg },
        { .sp_off = SPD_DDR4_OPT_FEAT, .sp_parse = spd_parse_ddr4_feat },
        { .sp_off = SPD_DDR4_OPT_FEAT2, .sp_parse = spd_parse_ddr4_feat2 },
        { .sp_off = SPD_DDR4_VOLT, .sp_key = SPD_KEY_NOM_VDD,
            .sp_parse = spd_parse_ddr4_volt },
        { .sp_off = SPD_DDR4_MOD_ORG, .sp_parse = spd_parse_ddr4_mod_org },
        { .sp_off = SPD_DDR4_MOD_BUS_WIDTH,
            .sp_parse = spd_parse_ddr4_bus_width },
        { .sp_off = SPD_DDR4_MOD_THERM, .sp_key = SPD_KEY_DEVS,
            .sp_parse = spd_parse_ddr4_therm },
        /*
         * Because there is only one set of valid time bases, we assume that
         * as part of the rest of the time construction.
         */
        { .sp_off = SPD_DDR4_TIMEBASE, .sp_parse = spd_parse_ddr4_ts },
        { .sp_off = SPD_DDR4_TCKAVG_MIN, .sp_key = SPD_KEY_TCKAVG_MIN,
            .sp_len = SPD_DDR4_TCKAVG_MIN_FINE - SPD_DDR4_TCKAVG_MIN + 1,
            .sp_parse = spd_parse_mtb_ftb_time_pair },
        { .sp_off = SPD_DDR4_TCKAVG_MAX, .sp_key = SPD_KEY_TCKAVG_MAX,
            .sp_len = SPD_DDR4_TCKAVG_MAX_FINE - SPD_DDR4_TCKAVG_MAX + 1,
            .sp_parse = spd_parse_mtb_ftb_time_pair },
        { .sp_off = SPD_DDR4_CAS_SUP0, .sp_key = SPD_KEY_CAS,
            .sp_len = SPD_DDR4_CAS_SUP3 - SPD_DDR4_CAS_SUP0 + 1,
            .sp_parse = spd_parse_ddr4_cas },
        { .sp_off = SPD_DDR4_TAA_MIN, .sp_key = SPD_KEY_TAA_MIN,
            .sp_len = SPD_DDR4_TAA_MIN_FINE - SPD_DDR4_TAA_MIN + 1,
            .sp_parse = spd_parse_mtb_ftb_time_pair },
        { .sp_off = SPD_DDR4_TRCD_MIN, .sp_key = SPD_KEY_TRCD_MIN,
            .sp_len = SPD_DDR4_TRCD_MIN_FINE - SPD_DDR4_TRCD_MIN + 1,
            .sp_parse = spd_parse_mtb_ftb_time_pair },
        { .sp_off = SPD_DDR4_TRP_MIN, .sp_key = SPD_KEY_TRP_MIN,
            .sp_len = SPD_DDR4_TRP_MIN_FINE - SPD_DDR4_TRP_MIN + 1,
            .sp_parse = spd_parse_mtb_ftb_time_pair },
        { .sp_off = SPD_DDR4_RAS_RC_UPPER, .sp_len = 2,
            .sp_key = SPD_KEY_TRAS_MIN, .sp_parse = spd_parse_ddr4_tras },
        { .sp_off = SPD_DDR4_RAS_RC_UPPER, .sp_key = SPD_KEY_TRC_MIN,
            .sp_len = SPD_DDR4_TRC_MIN_FINE - SPD_DDR4_RAS_RC_UPPER + 1,
            .sp_parse = spd_parse_ddr4_trc },
        { .sp_off = SPD_DDR4_TRFC1_MIN_LSB, .sp_len = 2,
            .sp_key = SPD_KEY_TRFC1_MIN, .sp_parse = spd_parse_mtb_pair },
        { .sp_off = SPD_DDR4_TRFC2_MIN_LSB, .sp_len = 2,
            .sp_key = SPD_KEY_TRFC2_MIN, .sp_parse = spd_parse_mtb_pair },
        { .sp_off = SPD_DDR4_TRFC4_MIN_LSB, .sp_len = 2,
            .sp_key = SPD_KEY_TRFC4_MIN, .sp_parse = spd_parse_mtb_pair },
        { .sp_off = SPD_DDR4_TFAW_UPPER, .sp_len = 2, .sp_key = SPD_KEY_TFAW,
            .sp_parse = spd_parse_ddr4_tfaw },
        { .sp_off = SPD_DDR4_TRRDS_MIN, .sp_key = SPD_KEY_TRRD_S_MIN,
            .sp_len = SPD_DDR4_TRRDS_MIN_FINE - SPD_DDR4_TRRDS_MIN + 1,
            .sp_parse = spd_parse_mtb_ftb_time_pair },
        { .sp_off = SPD_DDR4_TRRDL_MIN, .sp_key = SPD_KEY_TRRD_L_MIN,
            .sp_len = SPD_DDR4_TRRDL_MIN_FINE - SPD_DDR4_TRRDL_MIN + 1,
            .sp_parse = spd_parse_mtb_ftb_time_pair },
        { .sp_off = SPD_DDR4_TCCDL_MIN, .sp_key = SPD_KEY_TCCD_L_MIN,
            .sp_len = SPD_DDR4_TCCDL_MIN_FINE - SPD_DDR4_TCCDL_MIN + 1,
            .sp_parse = spd_parse_mtb_ftb_time_pair },
        { .sp_off = SPD_DDR4_TWR_MIN_UPPER, .sp_len = 2,
            .sp_key = SPD_KEY_TWR_MIN, .sp_parse = spd_parse_ddr4_twr },
        { .sp_off = SPD_DDR4_TWRT_UPPER, .sp_len = 2,
            .sp_key = SPD_KEY_TWTRS_MIN, .sp_parse = spd_parse_ddr4_twtrs },
        { .sp_off = SPD_DDR4_TWRT_UPPER, .sp_len = 3,
            .sp_key = SPD_KEY_TWTRL_MIN, .sp_parse = spd_parse_ddr4_twtrl },
        { .sp_off = SPD_DDR4_MAP_DQ0, .sp_key = SPD_KEY_DDR4_MAP_DQ0,
            .sp_parse = spd_parse_ddr4_nib_map },
        { .sp_off = SPD_DDR4_MAP_DQ4, .sp_key = SPD_KEY_DDR4_MAP_DQ4,
            .sp_parse = spd_parse_ddr4_nib_map },
        { .sp_off = SPD_DDR4_MAP_DQ8, .sp_key = SPD_KEY_DDR4_MAP_DQ8,
            .sp_parse = spd_parse_ddr4_nib_map },
        { .sp_off = SPD_DDR4_MAP_DQ12, .sp_key = SPD_KEY_DDR4_MAP_DQ12,
            .sp_parse = spd_parse_ddr4_nib_map },
        { .sp_off = SPD_DDR4_MAP_DQ16, .sp_key = SPD_KEY_DDR4_MAP_DQ16,
            .sp_parse = spd_parse_ddr4_nib_map },
        { .sp_off = SPD_DDR4_MAP_DQ20, .sp_key = SPD_KEY_DDR4_MAP_DQ20,
            .sp_parse = spd_parse_ddr4_nib_map },
        { .sp_off = SPD_DDR4_MAP_DQ24, .sp_key = SPD_KEY_DDR4_MAP_DQ24,
            .sp_parse = spd_parse_ddr4_nib_map },
        { .sp_off = SPD_DDR4_MAP_DQ28, .sp_key = SPD_KEY_DDR4_MAP_DQ28,
            .sp_parse = spd_parse_ddr4_nib_map },
        { .sp_off = SPD_DDR4_MAP_CB0, .sp_key = SPD_KEY_DDR4_MAP_CB0,
            .sp_parse = spd_parse_ddr4_nib_map },
        { .sp_off = SPD_DDR4_MAP_CB4, .sp_key = SPD_KEY_DDR4_MAP_CB4,
            .sp_parse = spd_parse_ddr4_nib_map },
        { .sp_off = SPD_DDR4_MAP_DQ32, .sp_key = SPD_KEY_DDR4_MAP_DQ32,
            .sp_parse = spd_parse_ddr4_nib_map },
        { .sp_off = SPD_DDR4_MAP_DQ36, .sp_key = SPD_KEY_DDR4_MAP_DQ36,
            .sp_parse = spd_parse_ddr4_nib_map },
        { .sp_off = SPD_DDR4_MAP_DQ40, .sp_key = SPD_KEY_DDR4_MAP_DQ40,
            .sp_parse = spd_parse_ddr4_nib_map },
        { .sp_off = SPD_DDR4_MAP_DQ44, .sp_key = SPD_KEY_DDR4_MAP_DQ44,
            .sp_parse = spd_parse_ddr4_nib_map },
        { .sp_off = SPD_DDR4_MAP_DQ48, .sp_key = SPD_KEY_DDR4_MAP_DQ48,
            .sp_parse = spd_parse_ddr4_nib_map },
        { .sp_off = SPD_DDR4_MAP_DQ52, .sp_key = SPD_KEY_DDR4_MAP_DQ52,
            .sp_parse = spd_parse_ddr4_nib_map },
        { .sp_off = SPD_DDR4_MAP_DQ56, .sp_key = SPD_KEY_DDR4_MAP_DQ56,
            .sp_parse = spd_parse_ddr4_nib_map },
        { .sp_off = SPD_DDR4_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 },
};

static const spd_parse_t spd_ddr4_mfg[] = {
        { .sp_off = SPD_DDR4_MOD_MFG_ID0, .sp_len = 2,
            .sp_key = SPD_KEY_MFG_MOD_MFG_ID,
            .sp_parse = spd_parse_jedec_id },
        { .sp_off = SPD_DDR4_MOD_MFG_ID0, .sp_len = 2,
            .sp_key = SPD_KEY_MFG_MOD_MFG_NAME,
            .sp_parse = spd_parse_jedec_id_str },
        { .sp_off = SPD_DDR4_DRAM_MFG_ID0, .sp_len = 2,
            .sp_key = SPD_KEY_MFG_DRAM_MFG_ID,
            .sp_parse = spd_parse_jedec_id },
        { .sp_off = SPD_DDR4_DRAM_MFG_ID0, .sp_len = 2,
            .sp_key = SPD_KEY_MFG_DRAM_MFG_NAME,
            .sp_parse = spd_parse_jedec_id_str },
        { .sp_off = SPD_DDR4_MOD_MFG_LOC, .sp_key = SPD_KEY_MFG_MOD_LOC_ID,
            .sp_parse = spd_parse_raw_u8 },
        { .sp_off = SPD_DDR4_MOD_MFG_YEAR, .sp_key = SPD_KEY_MFG_MOD_YEAR,
            .sp_parse = spd_parse_hex_string },
        { .sp_off = SPD_DDR4_MOD_MFG_WEEK, .sp_key = SPD_KEY_MFG_MOD_WEEK,
            .sp_parse = spd_parse_hex_string },
        { .sp_off = SPD_DDR4_MOD_SN, .sp_len = SPD_DDR4_MOD_SN_LEN,
            .sp_key = SPD_KEY_MFG_MOD_SN, .sp_parse = spd_parse_hex_string },
        { .sp_off = SPD_DDR4_MOD_PN, .sp_len = SPD_DDR4_MOD_PN_LEN,
            .sp_key = SPD_KEY_MFG_MOD_PN, .sp_parse = spd_parse_string },
        { .sp_off = SPD_DDR4_MOD_REV, .sp_key = SPD_KEY_MFG_MOD_REV,
            .sp_parse = spd_parse_dram_step },
        { .sp_off = SPD_DDR4_DRAM_STEP, .sp_key = SPD_KEY_MFG_DRAM_STEP,
            .sp_parse = spd_parse_dram_step },
};

/*
 * The offsets and values for design information are identical across DDR4 and
 * the LPDDR3/4/4X SPD data.
 */
void
spd_parse_ddr4_design(spd_info_t *si, uint32_t off, uint32_t len,
    const char *key)
{
        ASSERT3U(off, >=, SPD_DDR4_RDIMM_HEIGHT);
        return (spd_parse_design(si, off, SPD_DDR4_RDIMM_HEIGHT));
}

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

        if (SPD_DDR4_RDIMM_MAP_R1(data) != 0)
                spd_nvl_insert_key(si, SPD_KEY_MOD_EDGE_MIRROR);
}

/*
 * DDR4 UDIMM specific processing.
 */
static const spd_parse_t spd_ddr4_udimm[] = {
        { .sp_off = SPD_DDR4_UDIMM_HEIGHT, .sp_key = SPD_KEY_MOD_HEIGHT,
            .sp_parse = spd_parse_height },
        { .sp_off = SPD_DDR4_UDIMM_THICK, .sp_parse = spd_parse_thickness },
        { .sp_off = SPD_DDR4_UDIMM_REF, .sp_parse = spd_parse_ddr4_design },
        { .sp_off = SPD_DDR4_UDIMM_MAP, .sp_parse = spd_parse_ddr4_edge },
        { .sp_off = SPD_DDR4_BLK1_CRC_START, .sp_len = SPD_DDR4_BLK1_CRC_MSB +
            1 - SPD_DDR4_BLK1_CRC_START, .sp_key = SPD_KEY_CRC_DDR4_BLK1,
            .sp_parse = spd_parse_crc }
};

/*
 * DDR4 RDIMM specific processing.
 */
static const spd_value_map_t spd_ddr4_rcd_type_map[] = {
        { SPD_DDR4_RDIMM_ATTR_TYPE_RCD01, SPD_RCD_T_DDR4RCD01, false },
        { SPD_DDR4_RDIMM_ATTR_TYPE_RCD02, SPD_RCD_T_DDR4RCD02, false },
};

static void
spd_parse_ddr4_rdimm_attr(spd_info_t *si, uint32_t off, uint32_t len,
    const char *key)
{
        const uint8_t data = si->si_data[off];
        const uint8_t rcd = SPD_DDR4_RDIMM_ATTR_TYPE(data);
        const uint8_t nrow = 1 << (SPD_DDR4_RDIMM_ATTR_NROWS(data) - 1);
        const uint8_t nreg = 1 << (SPD_DDR4_RDIMM_ATTR_NREGS(data) - 1);

        spd_upsert_flag(si, SPD_KEY_DEVS, SPD_DEVICE_RCD);
        spd_insert_map(si, SPD_KEY_DEV_RCD_TYPE, rcd,
            spd_ddr4_rcd_type_map, ARRAY_SIZE(spd_ddr4_rcd_type_map));
        if (nrow != 0)
                spd_nvl_insert_u32(si, SPD_KEY_MOD_NROWS, nrow);
        if (nreg != 0)
                spd_nvl_insert_u32(si, SPD_KEY_MOD_NREGS, nreg);
}

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

        if (SPD_DDR4_RDIMM_THERM_IMPL(data) != 0)
                spd_upsert_flag(si, SPD_KEY_DEVS, SPD_DEVICE_HS);
}

static void
spd_parse_ddr4_rdimm_rcd_mfg(spd_info_t *si, uint32_t off, uint32_t len,
    const char *key)
{
        ASSERT3U(len, ==, 2);

        spd_parse_jedec_id(si, off, 2, SPD_KEY_DEV_RCD_MFG);
        spd_parse_jedec_id_str(si, off, 2, SPD_KEY_DEV_RCD_MFG_NAME);
}

static const spd_value_map_t spd_ddr4_rdimm_ods_map[] = {
        { SPD_DDR4_RDIMM_ODS0_LIGHT, SPD_DRIVE_LIGHT, false },
        { SPD_DDR4_RDIMM_ODS0_MODERATE, SPD_DRIVE_MODERATE, false },
        { SPD_DDR4_RDIMM_ODS0_STRONG, SPD_DRIVE_STRONG, false },
        { SPD_DDR4_RDIMM_ODS0_VERY_STRONG, SPD_DRIVE_VERY_STRONG, false },
};

static void
spd_parse_ddr4_rdimm_ods(spd_info_t *si, uint32_t off, uint32_t len,
    const char *key)
{
        const uint8_t ods0 = si->si_data[off];
        const uint8_t ods1 = si->si_data[off + 1];
        const uint8_t cs = SPD_DDR4_RDIMM_ODS0_CS(ods0);
        const uint8_t ca = SPD_DDR4_RDIMM_ODS0_CA(ods0);
        const uint8_t odt = SPD_DDR4_RDIMM_ODS0_ODT(ods0);
        const uint8_t cke = SPD_DDR4_RDIMM_ODS0_CKE(ods0);
        const uint8_t y1 = SPD_DDR4_RDIMM_ODS1_Y1(ods1);
        const uint8_t y0 = SPD_DDR4_RDIMM_ODS1_Y0(ods1);

        spd_insert_map(si, SPD_KEY_DDR4_RCD_DS_CKE, cke, spd_ddr4_rdimm_ods_map,
            ARRAY_SIZE(spd_ddr4_rdimm_ods_map));
        spd_insert_map(si, SPD_KEY_DDR4_RCD_DS_ODT, odt, spd_ddr4_rdimm_ods_map,
            ARRAY_SIZE(spd_ddr4_rdimm_ods_map));
        spd_insert_map(si, SPD_KEY_DDR4_RCD_DS_CA, ca, spd_ddr4_rdimm_ods_map,
            ARRAY_SIZE(spd_ddr4_rdimm_ods_map));
        spd_insert_map(si, SPD_KEY_DDR4_RCD_DS_CS, cs, spd_ddr4_rdimm_ods_map,
            ARRAY_SIZE(spd_ddr4_rdimm_ods_map));
        spd_insert_map(si, SPD_KEY_DDR4_RCD_DS_Y0, y0, spd_ddr4_rdimm_ods_map,
            ARRAY_SIZE(spd_ddr4_rdimm_ods_map));
        spd_insert_map(si, SPD_KEY_DDR4_RCD_DS_Y1, y1, spd_ddr4_rdimm_ods_map,
            ARRAY_SIZE(spd_ddr4_rdimm_ods_map));

        if (SPD_DDR4_RDIMM_ODS1_SLEW_SUP(ods1) != 0)
                spd_nvl_insert_key(si, SPD_KEY_DDR4_RCD_SLEW);
}

static const spd_parse_t spd_ddr4_rdimm[] = {
        { .sp_off = SPD_DDR4_RDIMM_HEIGHT, .sp_key = SPD_KEY_MOD_HEIGHT,
            .sp_parse = spd_parse_height },
        { .sp_off = SPD_DDR4_RDIMM_THICK, .sp_parse = spd_parse_thickness },
        { .sp_off = SPD_DDR4_RDIMM_REF, .sp_parse = spd_parse_ddr4_design },
        { .sp_off = SPD_DDR4_RDIMM_ATTR,
            .sp_parse = spd_parse_ddr4_rdimm_attr },
        { .sp_off = SPD_DDR4_RDIMM_THERM,
            .sp_parse = spd_parse_ddr4_rdimm_therm },
        { .sp_off = SPD_DDR4_RDIMM_REG_MFG_ID0, .sp_len = 2,
            .sp_parse = spd_parse_ddr4_rdimm_rcd_mfg },
        { .sp_off = SPD_DDR4_RDIMM_REV, .sp_key = SPD_KEY_DEV_RCD_REV,
            .sp_parse = spd_parse_dram_step },
        { .sp_off = SPD_DDR4_RDIMM_MAP, .sp_parse = spd_parse_ddr4_edge },
        { .sp_off = SPD_DDR4_RDIMM_ODS0, .sp_len = 2,
            .sp_parse = spd_parse_ddr4_rdimm_ods },
        { .sp_off = SPD_DDR4_BLK1_CRC_START, .sp_len = SPD_DDR4_BLK1_CRC_MSB +
            1 - SPD_DDR4_BLK1_CRC_START, .sp_key = SPD_KEY_CRC_DDR4_BLK1,
            .sp_parse = spd_parse_crc }
};

/*
 * DDR4 LRDIMM specific processing.
 */
static const spd_value_map_t spd_ddr4_db_type_map[] = {
        { SPD_DDR4_LRDIMM_ATTR_TYPE_RCD01_DB01, SPD_RCD_T_DDR4RCD01, false },
        { SPD_DDR4_LRDIMM_ATTR_TYPE_RCD02_DB02, SPD_RCD_T_DDR4RCD02, false },
};

/*
 * We use value maps for these LRDIMM properties because they're a bit
 * inconsistent and this gets us out of a lot of if statements. The RDIMM code
 * doesn't have this problem because all of the values are valid.
 */
static const spd_value_map_t spd_ddr4_lrdimm_nrows_map[] = {
        { 0, 0, true },
        { 1, 1, false },
        { 2, 2, false }
};

static const spd_value_map_t spd_ddr4_lrdimm_nregs_map[] = {
        { 0, 0, true },
        { 1, 1, false }
};

static void
spd_parse_ddr4_lrdimm_attr(spd_info_t *si, uint32_t off, uint32_t len,
    const char *key)
{
        const uint8_t data = si->si_data[off];
        const uint8_t rcd = SPD_DDR4_LRDIMM_ATTR_TYPE(data);
        const uint8_t nrow = SPD_DDR4_LRDIMM_ATTR_NROWS(data);
        const uint8_t nreg = SPD_DDR4_LRDIMM_ATTR_NREGS(data);

        /*
         * The type defines both the RCD and the DB. The RCD types overlap with
         * RDIMMs.
         */
        spd_upsert_flag(si, SPD_KEY_DEVS, SPD_DEVICE_RCD | SPD_DEVICE_DB);
        spd_insert_map(si, SPD_KEY_DEV_RCD_TYPE, rcd,
            spd_ddr4_rcd_type_map, ARRAY_SIZE(spd_ddr4_rcd_type_map));
        spd_insert_map(si, SPD_KEY_DEV_DB_TYPE, rcd,
            spd_ddr4_db_type_map, ARRAY_SIZE(spd_ddr4_db_type_map));
        spd_insert_map(si, SPD_KEY_MOD_NROWS, nrow, spd_ddr4_lrdimm_nrows_map,
            ARRAY_SIZE(spd_ddr4_lrdimm_nrows_map));
        spd_insert_map(si, SPD_KEY_MOD_NREGS, nreg, spd_ddr4_lrdimm_nregs_map,
            ARRAY_SIZE(spd_ddr4_lrdimm_nregs_map));
}

/*
 * The LRDIMM manufacturer here covers both the register and the data buffer, so
 * we end up setting the same values for both.
 */
static void
spd_parse_ddr4_lrdimm_rcd_mfg(spd_info_t *si, uint32_t off, uint32_t len,
    const char *key)
{
        ASSERT3U(len, ==, 2);

        spd_parse_jedec_id(si, off, 2, SPD_KEY_DEV_RCD_MFG);
        spd_parse_jedec_id_str(si, off, 2, SPD_KEY_DEV_RCD_MFG_NAME);
        spd_parse_jedec_id(si, off, 2, SPD_KEY_DEV_DB_MFG);
        spd_parse_jedec_id_str(si, off, 2, SPD_KEY_DEV_DB_MFG_NAME);
}

static const spd_value_map_t spd_ddr4_lrdimm_ods_map[] = {
        { SPD_DDR4_LRDIMM_ODS1_MODERATE, SPD_DRIVE_MODERATE, false },
        { SPD_DDR4_LRDIMM_ODS1_STRONG, SPD_DRIVE_STRONG, false }
};

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

        spd_insert_map(si, SPD_KEY_DDR4_RCD_DS_BCOM, bcom,
            spd_ddr4_lrdimm_ods_map, ARRAY_SIZE(spd_ddr4_lrdimm_ods_map));
        spd_insert_map(si, SPD_KEY_DDR4_RCD_DS_BCK, bck,
            spd_ddr4_lrdimm_ods_map, ARRAY_SIZE(spd_ddr4_lrdimm_ods_map));
}

/*
 * There are two VrefDQ ranges in the DDR4 specs. These all increase at 0.65%
 * increments, hence our mult as 65.
 */
static const spd_value_range_t spd_ddr4_vrefdq1_range = {
        .svr_base = 6000,
        .svr_mult = 65,
        .svr_max = 9250
};

static const spd_value_range_t spd_ddr4_vrefdq2_range = {
        .svr_base = 4500,
        .svr_mult = 65,
        .svr_max = 7750
};

static void
spd_parse_ddr4_vrefdq_common(spd_info_t *si, uint8_t range, uint8_t val,
    const char *key)
{
        if (range == SPD_DDR4_LRDIMM_VERFDQ_RNG_1) {
                spd_insert_range(si, key, val, &spd_ddr4_vrefdq1_range);
        } else {
                ASSERT3U(range, ==, SPD_DDR4_LRDIMM_VERFDQ_RNG_2);
                spd_insert_range(si, key, val, &spd_ddr4_vrefdq2_range);
        }
}

static void
spd_parse_ddr4_lrdimm_vrefdq_r0(spd_info_t *si, uint32_t off, uint32_t len,
    const char *key)
{
        const uint8_t data = si->si_data[off];
        const uint8_t volt = SPD_DDR4_LRDIMM_VREFDQ_V(data);
        const uint8_t range = si->si_data[off + len - 1];

        spd_parse_ddr4_vrefdq_common(si, SPD_DDR4_LRDIMM_VREFDQ_RNG_R0(range),
            volt, key);
}

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

        spd_parse_ddr4_vrefdq_common(si, SPD_DDR4_LRDIMM_VREFDQ_RNG_R1(range),
            volt, key);
}

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

        spd_parse_ddr4_vrefdq_common(si, SPD_DDR4_LRDIMM_VREFDQ_RNG_R2(range),
            volt, key);
}

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

        spd_parse_ddr4_vrefdq_common(si, SPD_DDR4_LRDIMM_VREFDQ_RNG_R3(range),
            volt, key);
}

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

        spd_parse_ddr4_vrefdq_common(si, SPD_DDR4_LRDIMM_VREFDQ_RNG_DB(range),
            data, key);
}

static const spd_value_map_t spd_ddr4_mdq_ds_map[] = {
        { SPD_DDR4_LRDIMM_MDQ_DS_40R, 40, false },
        { SPD_DDR4_LRDIMM_MDQ_DS_34R, 34, false },
        { SPD_DDR4_LRDIMM_MDQ_DS_48R, 48, false },
        { SPD_DDR4_LRDIMM_MDQ_DS_60R, 60, false }
};

static const spd_value_map_t spd_ddr4_rtt_map[] = {
        { SPD_DDR4_LRDIMM_MDQ_RTT_DIS, SPD_TERM_DISABLED, false },
        { SPD_DDR4_LRDIMM_MDQ_RTT_60R, 60, false },
        { SPD_DDR4_LRDIMM_MDQ_RTT_120R, 120, false },
        { SPD_DDR4_LRDIMM_MDQ_RTT_40R, 40, false },
        { SPD_DDR4_LRDIMM_MDQ_RTT_240R, 240, false },
        { SPD_DDR4_LRDIMM_MDQ_RTT_48R, 48, false },
        { SPD_DDR4_LRDIMM_MDQ_RTT_80R, 80, false },
        { SPD_DDR4_LRDIMM_MDQ_RTT_34R, 34, false },
};

static void
spd_parse_ddr4_lrdimm_mdq(spd_info_t *si, uint32_t off, uint32_t len,
    const char *key)
{
        const uint8_t d1866 = si->si_data[off];
        const uint8_t d2400 = si->si_data[off + 1];
        const uint8_t d3200 = si->si_data[off + 2];
        const uint8_t rtt[3] = { SPD_DDR4_LRDIMM_MDQ_RTT(d1866),
            SPD_DDR4_LRDIMM_MDQ_RTT(d2400), SPD_DDR4_LRDIMM_MDQ_RTT(d3200) };
        const uint8_t ds[3] = { SPD_DDR4_LRDIMM_MDQ_DS(d1866),
            SPD_DDR4_LRDIMM_MDQ_DS(d2400), SPD_DDR4_LRDIMM_MDQ_DS(d3200) };

        spd_insert_map_array(si, SPD_KEY_DDR4_MDQ_RTT, rtt, ARRAY_SIZE(rtt),
            spd_ddr4_rtt_map, ARRAY_SIZE(spd_ddr4_rtt_map));
        spd_insert_map_array(si, SPD_KEY_DDR4_MDQ_DS, ds, ARRAY_SIZE(ds),
            spd_ddr4_mdq_ds_map, ARRAY_SIZE(spd_ddr4_mdq_ds_map));
}

static const spd_value_map_t spd_ddr4_dram_ds_map[] = {
        { SPD_DDR4_LRDIMM_DRAM_DS_34R, 34, false },
        { SPD_DDR4_LRDIMM_DRAM_DS_48R, 48, false }
};

static void
spd_parse_ddr4_lrdimm_dram(spd_info_t *si, uint32_t off, uint32_t len,
    const char *key)
{
        const uint8_t data = si->si_data[off];
        const uint8_t ds[3] = {
                SPD_DDR4_LRDIMM_DRAM_DS_1866(data),
                SPD_DDR4_LRDIMM_DRAM_DS_2400(data),
                SPD_DDR4_LRDIMM_DRAM_DS_3200(data)
        };

        spd_insert_map_array(si, SPD_KEY_DDR4_DRAM_DS, ds, ARRAY_SIZE(ds),
            spd_ddr4_dram_ds_map, ARRAY_SIZE(spd_ddr4_dram_ds_map));
}

static const spd_value_map_t spd_ddr4_rtt_wr_map[] = {
        { SPD_DDR4_LRDIMM_ODT_WR_DYN_OFF, SPD_TERM_DISABLED, false },
        { SPD_DDR4_LRDIMM_ODT_WR_120R, 120, false },
        { SPD_DDR4_LRDIMM_ODT_WR_240R, 240, false },
        { SPD_DDR4_LRDIMM_ODT_WR_HIZ, SPD_TERM_HIZ, false },
        { SPD_DDR4_LRDIMM_ODT_WR_80R, 80, false },
};

static void
spd_parse_ddr4_lrdimm_odt(spd_info_t *si, uint32_t off, uint32_t len,
    const char *key)
{
        const uint8_t d1866 = si->si_data[off];
        const uint8_t d2400 = si->si_data[off + 1];
        const uint8_t d3200 = si->si_data[off + 2];
        const uint8_t nom[3] = { SPD_DDR4_LRDIMM_ODT_NOM(d1866),
            SPD_DDR4_LRDIMM_ODT_NOM(d2400), SPD_DDR4_LRDIMM_ODT_NOM(d3200) };
        const uint8_t wr[3] = { SPD_DDR4_LRDIMM_ODT_WR(d1866),
            SPD_DDR4_LRDIMM_ODT_WR(d2400), SPD_DDR4_LRDIMM_ODT_WR(d3200) };

        spd_insert_map_array(si, SPD_KEY_DDR4_RTT_NOM, nom, ARRAY_SIZE(nom),
            spd_ddr4_rtt_map, ARRAY_SIZE(spd_ddr4_rtt_map));
        spd_insert_map_array(si, SPD_KEY_DDR4_RTT_WR, wr, ARRAY_SIZE(wr),
            spd_ddr4_rtt_wr_map, ARRAY_SIZE(spd_ddr4_rtt_wr_map));
}

static void
spd_parse_ddr4_lrdimm_park(spd_info_t *si, uint32_t off, uint32_t len,
    const char *key)
{
        const uint8_t d1866 = si->si_data[off];
        const uint8_t d2400 = si->si_data[off + 1];
        const uint8_t d3200 = si->si_data[off + 2];
        const uint8_t r01[3] = { SPD_DDR4_LRDIMM_PARK_R01(d1866),
            SPD_DDR4_LRDIMM_PARK_R01(d2400), SPD_DDR4_LRDIMM_PARK_R01(d3200) };
        const uint8_t r23[3] = { SPD_DDR4_LRDIMM_PARK_R23(d1866),
            SPD_DDR4_LRDIMM_PARK_R23(d2400), SPD_DDR4_LRDIMM_PARK_R23(d3200) };

        spd_insert_map_array(si, SPD_KEY_DDR4_RTT_PARK_R0, r01, ARRAY_SIZE(r01),
            spd_ddr4_rtt_map, ARRAY_SIZE(spd_ddr4_rtt_map));
        spd_insert_map_array(si, SPD_KEY_DDR4_RTT_PARK_R2, r23, ARRAY_SIZE(r23),
            spd_ddr4_rtt_map, ARRAY_SIZE(spd_ddr4_rtt_map));
}

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

        if (SPD_DDR4_LRDIMM_EQ_DFE_SUP(data) != 0)
                spd_nvl_insert_key(si, SPD_KEY_DDR4_DB_DFE);
        if (SPD_DDR4_LRDIMM_EQ_GA_SUP(data) != 0)
                spd_nvl_insert_key(si, SPD_KEY_DDR4_DB_GAIN);
}

static const spd_parse_t spd_ddr4_lrdimm[] = {
        { .sp_off = SPD_DDR4_LRDIMM_HEIGHT, .sp_key = SPD_KEY_MOD_HEIGHT,
            .sp_parse = spd_parse_height },
        { .sp_off = SPD_DDR4_LRDIMM_THICK, .sp_parse = spd_parse_thickness },
        { .sp_off = SPD_DDR4_LRDIMM_REF, .sp_parse = spd_parse_ddr4_design },
        { .sp_off = SPD_DDR4_LRDIMM_ATTR,
            .sp_parse = spd_parse_ddr4_lrdimm_attr },
        { .sp_off = SPD_DDR4_LRDIMM_THERM,
            .sp_parse = spd_parse_ddr4_rdimm_therm },
        { .sp_off = SPD_DDR4_LRDIMM_REG_MFG_ID0, .sp_len = 2,
            .sp_parse = spd_parse_ddr4_lrdimm_rcd_mfg },
        { .sp_off = SPD_DDR4_LRDIMM_REV, .sp_key = SPD_KEY_DEV_RCD_REV,
            .sp_parse = spd_parse_dram_step },
        { .sp_off = SPD_DDR4_LRDIMM_MAP, .sp_parse = spd_parse_ddr4_edge },
        /*
         * The LRDIMM output drive strength is equivalent to the RDIMM, so we
         * use that. For ODS1, we fire it a second-time to get just the
         * LRDIMM-specific fields.
         */
        { .sp_off = SPD_DDR4_LRDIMM_ODS0, .sp_len = 2,
            .sp_parse = spd_parse_ddr4_rdimm_ods },
        { .sp_off = SPD_DDR4_LRDIMM_ODS1,
            .sp_parse = spd_parse_ddr4_lrdimm_ods },
        { .sp_off = SPD_DDR4_LRDIMM_DB_REV,  .sp_key = SPD_KEY_DEV_DB_REV,
            .sp_parse = spd_parse_dram_step },
        /*
         * The five VrefDQ values (four ranks and data buffer) require the range
         * byte to determine which base set of values to use. This is why they
         * all have the long length to ensure we account for that.
         */
        { .sp_off = SPD_DDR4_LRDIMM_VREFDQ0, .sp_key = SPD_KEY_DDR4_VREFDQ_R0,
            .sp_len = SPD_DDR4_LRDIMM_VREFDQ_RNG - SPD_DDR4_LRDIMM_VREFDQ0 + 1,
            .sp_parse = spd_parse_ddr4_lrdimm_vrefdq_r0 },
        { .sp_off = SPD_DDR4_LRDIMM_VREFDQ1, .sp_key = SPD_KEY_DDR4_VREFDQ_R1,
            .sp_len = SPD_DDR4_LRDIMM_VREFDQ_RNG - SPD_DDR4_LRDIMM_VREFDQ1 + 1,
            .sp_parse = spd_parse_ddr4_lrdimm_vrefdq_r1 },

        { .sp_off = SPD_DDR4_LRDIMM_VREFDQ2, .sp_key = SPD_KEY_DDR4_VREFDQ_R2,
            .sp_len = SPD_DDR4_LRDIMM_VREFDQ_RNG - SPD_DDR4_LRDIMM_VREFDQ2 + 1,
            .sp_parse = spd_parse_ddr4_lrdimm_vrefdq_r2 },

        { .sp_off = SPD_DDR4_LRDIMM_VREFDQ3, .sp_key = SPD_KEY_DDR4_VREFDQ_R3,
            .sp_len = SPD_DDR4_LRDIMM_VREFDQ_RNG - SPD_DDR4_LRDIMM_VREFDQ3 + 1,
            .sp_parse = spd_parse_ddr4_lrdimm_vrefdq_r3 },

        { .sp_off = SPD_DDR4_LRDIMM_VREFDQ_DB, .sp_key = SPD_KEY_DDR4_VREFDQ_DB,
            .sp_len = SPD_DDR4_LRDIMM_VREFDQ_RNG - SPD_DDR4_LRDIMM_VREFDQ_DB +
            1, .sp_parse = spd_parse_ddr4_lrdimm_vrefdq_db },
        { .sp_off = SPD_DDR4_LRDIMM_MDQ_1866, .sp_len = 3,
            .sp_parse = spd_parse_ddr4_lrdimm_mdq },
        { .sp_off = SPD_DDR4_LRDIMM_DRAM_DS,
            .sp_parse = spd_parse_ddr4_lrdimm_dram },
        { .sp_off = SPD_DDR4_LRDIMM_ODT_1866, .sp_len = 3,
            .sp_parse = spd_parse_ddr4_lrdimm_odt },
        { .sp_off = SPD_DDR4_LRDIMM_PARK_1866, .sp_len = 3,
            .sp_parse = spd_parse_ddr4_lrdimm_park },
        { .sp_off = SPD_DDR4_LRDIMM_EQ, .sp_parse = spd_parse_ddr4_lrdimm_dfe },
        { .sp_off = SPD_DDR4_BLK1_CRC_START, .sp_len = SPD_DDR4_BLK1_CRC_MSB +
            1 - SPD_DDR4_BLK1_CRC_START, .sp_key = SPD_KEY_CRC_DDR4_BLK1,
            .sp_parse = spd_parse_crc }
};

static void
spd_parse_ddr4_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_RDIMM:
        case SPD_MOD_TYPE_MINI_RDIMM:
        case SPD_MOD_TYPE_72b_SO_RDIMM:
                spd_parse(si, spd_ddr4_rdimm, ARRAY_SIZE(spd_ddr4_rdimm));
                break;
        case SPD_MOD_TYPE_LRDIMM:
                spd_parse(si, spd_ddr4_lrdimm, ARRAY_SIZE(spd_ddr4_lrdimm));
                break;
        case SPD_MOD_TYPE_UDIMM:
        case SPD_MOD_TYPE_SODIMM:
        case SPD_MOD_TYPE_MINI_UDIMM:
        case SPD_MOD_TYPE_72b_SO_UDIMM:
        case SPD_MOD_TYPE_16b_SO_DIMM:
        case SPD_MOD_TYPE_32b_SO_DIMM:
                spd_parse(si, spd_ddr4_udimm, ARRAY_SIZE(spd_ddr4_udimm));
                break;
        default:
                break;
        }
}

void
spd_parse_ddr4_mfg(spd_info_t *si)
{
        spd_parse(si, spd_ddr4_mfg, ARRAY_SIZE(spd_ddr4_mfg));
}

/*
 * DDR4 processing.
 *
 *  1. Check that we know the encoding revision of the SPD.
 *  2. Capture the SPD module type information as we already have the dram type
 *     information.
 *  3. Attempt to parse everything. Note that we don't really use the device's
 *     notion of how much data should be present and only will attempt to parse
 *     regions if we have enough data from the user.
 */
void
spd_parse_ddr4(spd_info_t *si)
{
        if (SPD_DDR4_SPD_REV_ENC(si->si_data[SPD_DDR4_SPD_REV]) !=
            SPD_DDR4_SPD_REV_V1) {
                si->si_error = LIBJEDEC_SPD_UNSUP_REV;
                return;
        }

        /*
         * Parse DDR4 common attributes. Some overlay information. Then go
         * through and do the manufacturing info.
         */
        spd_parse(si, spd_ddr4_common, ARRAY_SIZE(spd_ddr4_common));
        spd_parse_ddr4_mod_specific(si);
        spd_parse_ddr4_mfg(si);
}