/***********************license start***********************************
* Copyright (c) 2003-2017  Cavium Inc. (support@cavium.com). All rights
* reserved.
*
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
*   * Redistributions of source code must retain the above copyright
*     notice, this list of conditions and the following disclaimer.
*
*   * Redistributions in binary form must reproduce the above
*     copyright notice, this list of conditions and the following
*     disclaimer in the documentation and/or other materials provided
*     with the distribution.
*
*   * Neither the name of Cavium Inc. nor the names of
*     its contributors may be used to endorse or promote products
*     derived from this software without specific prior written
*     permission.
*
* This Software, including technical data, may be subject to U.S. export
* control laws, including the U.S. Export Administration Act and its
* associated regulations, and may be subject to export or import
* regulations in other countries.
*
* TO THE MAXIMUM EXTENT PERMITTED BY LAW, THE SOFTWARE IS PROVIDED "AS IS"
* AND WITH ALL FAULTS AND CAVIUM INC. MAKES NO PROMISES, REPRESENTATIONS OR
* WARRANTIES, EITHER EXPRESS, IMPLIED, STATUTORY, OR OTHERWISE, WITH RESPECT
* TO THE SOFTWARE, INCLUDING ITS CONDITION, ITS CONFORMITY TO ANY
* REPRESENTATION OR DESCRIPTION, OR THE EXISTENCE OF ANY LATENT OR PATENT
* DEFECTS, AND CAVIUM SPECIFICALLY DISCLAIMS ALL IMPLIED (IF ANY) WARRANTIES
* OF TITLE, MERCHANTABILITY, NONINFRINGEMENT, FITNESS FOR A PARTICULAR
* PURPOSE, LACK OF VIRUSES, ACCURACY OR COMPLETENESS, QUIET ENJOYMENT,
* QUIET POSSESSION OR CORRESPONDENCE TO DESCRIPTION. THE ENTIRE  RISK
* ARISING OUT OF USE OR PERFORMANCE OF THE SOFTWARE LIES WITH YOU.
***********************license end**************************************/

#include <bdk.h>
#include <libbdk-arch/bdk-csrs-mio_fus.h>
#include <libbdk-dram/bdk-dram-config.h>
#include <libbdk-hal/bdk-config.h>
#include <libbdk-hal/bdk-gpio.h>
#include <libbdk-hal/bdk-l2c.h>
#include <libbdk-hal/bdk-utils.h>
#include <libbdk-os/bdk-init.h>
#include <libdram/libdram-config.h>
#include "dram-internal.h"
#include <stddef.h>                /* for NULL */
#include <lame_string.h>        /* for strtol() and friends */


/* This global variable is accessed through dram_is_verbose() to determine
   the verbosity level. Use that function instead of setting it directly */
dram_verbosity_t dram_verbosity = VBL_OFF; /* init this here so we could set a non-zero default */

static uint32_t measured_ddr_hertz[BDK_NUMA_MAX_NODES];

/* The various DRAM configs in the libdram/configs directory need space
   to store the DRAM config. Since only one config is ever in active use
   at a time, store the configs in __libdram_global_cfg. In a multi-node
   setup, independent calls to get the DRAM config will load first node 0's
   config, then node 1's */
dram_config_t __libdram_global_cfg;

static void bdk_dram_clear_mem(bdk_node_t node)
{
     uint64_t mbytes = bdk_dram_get_size_mbytes(node);
     uint64_t skip = (node == bdk_numa_master()) ? bdk_dram_get_top_of_bdk() : 0;
     uint64_t len =  (mbytes << 20) - skip;

     BDK_TRACE(DRAM, "N%d: Clearing DRAM\n", node);
     if (skip)
     {
         /* All memory below skip may contain valid data, so we can't clear
            it. We still need to make sure all cache lines in this area are
            fully dirty so that ECC bits will be updated on store. A single
            write to the cache line isn't good enough because partial LMC
            writes may be enabled */
         ddr_print("N%d: Rewriting DRAM: start 0 length 0x%llx\n", node, skip);
         volatile uint64_t *ptr = bdk_phys_to_ptr(bdk_numa_get_address(node, 0));
         uint64_t *end = bdk_phys_to_ptr(bdk_numa_get_address(node, skip));
         while (ptr < end)
         {
             *ptr = *ptr;
             ptr++;
         }
     }
     ddr_print("N%d: Clearing DRAM: start 0x%llx length 0x%llx\n", node, skip, len);
     bdk_zero_memory(bdk_phys_to_ptr(bdk_numa_get_address(node, skip)), len);
     BDK_TRACE(DRAM, "N%d: DRAM clear complete\n", node);
}

static void bdk_dram_clear_ecc(bdk_node_t node)
{
    /* Clear any DRAM errors set during init */
    BDK_TRACE(DRAM, "N%d: Clearing LMC ECC errors\n", node);
    int num_lmc = __bdk_dram_get_num_lmc(node);
    for (int lmc = 0; lmc < num_lmc; lmc++) {
        DRAM_CSR_WRITE(node, BDK_LMCX_INT(lmc), BDK_CSR_READ(node, BDK_LMCX_INT(lmc)));
    }
}

static void bdk_dram_enable_ecc_reporting(bdk_node_t node)
{
    /* Enable LMC ECC error HW reporting */
    int num_lmc = __bdk_dram_get_num_lmc(node);

    BDK_TRACE(DRAM, "N%d: Enable LMC ECC error reporting\n", node);

    for (int lmc = 0; lmc < num_lmc; lmc++) {

        // NOTE: this must be done for pass 2.x
        // enable ECC interrupts to allow ECC error info in LMCX_INT
        if (! CAVIUM_IS_MODEL(CAVIUM_CN88XX_PASS1_X)) { // added 81xx and 83xx
            DRAM_CSR_WRITE(node, BDK_LMCX_INT_ENA_W1S(lmc), -1ULL);
            BDK_CSR_INIT(lmc_int_ena_w1s, node, BDK_LMCX_INT_ENA_W1S(lmc));
            ddr_print("N%d.LMC%d: %-36s : 0x%08llx\n",
                      node, lmc, "LMC_INT_ENA_W1S", lmc_int_ena_w1s.u);
        }
    }
}

static void bdk_dram_disable_ecc_reporting(bdk_node_t node)
{
    /* Disable LMC ECC error HW reporting */
    int num_lmc = __bdk_dram_get_num_lmc(node);

    BDK_TRACE(DRAM, "N%d: Disable LMC ECC error reporting\n", node);

    for (int lmc = 0; lmc < num_lmc; lmc++) {

        // NOTE: this must be done for pass 2.x
        // disable ECC interrupts to prevent ECC error info in LMCX_INT
        if (! CAVIUM_IS_MODEL(CAVIUM_CN88XX_PASS1_X)) { // added 81xx and 83xx
            DRAM_CSR_WRITE(node, BDK_LMCX_INT_ENA_W1C(lmc), -1ULL);
            BDK_CSR_INIT(lmc_int_ena_w1c, node, BDK_LMCX_INT_ENA_W1C(lmc));
            ddr_print("N%d.LMC%d: %-36s : 0x%08llx\n",
                      node, lmc, "LMC_INT_ENA_W1C", lmc_int_ena_w1c.u);
        }
    }
}

// this routine simply makes the calls to the tuning routines and returns any errors
static int bdk_libdram_tune_node(int node)
{
    int errs, tot_errs;
    int do_dllro_hw = CONFIG(CAVIUM_BDK_DDR_TUNE_HW_OFFSETS);
    int do_dllwo = CONFIG(CAVIUM_BDK_DDR_TUNE_WRITE_OFFSETS);
    int do_eccdll = CONFIG(CAVIUM_BDK_DDR_TUNE_ECC_ENABLE);
    BDK_CSR_INIT(lmc_config, node, BDK_LMCX_CONFIG(0)); // FIXME: probe LMC0
    do_eccdll = (lmc_config.s.ecc_ena != 0); // change to ON if ECC enabled

    // Automatically tune the data byte DLL read offsets
    // always done by default, but allow use of HW-assist
    // NOTE: HW-assist will also tune the ECC byte

    BDK_TRACE(DRAM, "N%d: Starting DLL Read Offset Tuning for LMCs\n", node);
    if (!do_dllro_hw || (lmc_config.s.mode32b != 0)) {
        errs = perform_dll_offset_tuning(node, 2, /* tune */1);
    } else {
        errs = perform_HW_dll_offset_tuning(node, /* read */2, 0x0A/* all bytelanes */);
    }
    BDK_TRACE(DRAM, "N%d: Finished DLL Read Offset Tuning for LMCs, %d errors)\n",
              node, errs);
    tot_errs = errs;

    // disabled by default for now, does not seem to be needed?
    // Automatically tune the data byte DLL write offsets
    // allow override of default setting
    if (do_dllwo) {
        BDK_TRACE(DRAM, "N%d: Starting DLL Write Offset Tuning for LMCs\n", node);
        errs = perform_dll_offset_tuning(node, /* write */1, /* tune */1);
        BDK_TRACE(DRAM, "N%d: Finished DLL Write Offset Tuning for LMCs, %d errors)\n",
               node, errs);
        tot_errs += errs;
    }

    // disabled by default for now, does not seem to be needed much?
    // Automatically tune the ECC byte DLL read offsets
    // FIXME? allow override of the filtering
    // FIXME? allow programmatic override, not via envvar?
    if (do_eccdll && !do_dllro_hw && (lmc_config.s.mode32b == 0)) { // do not do HW-assist twice for ECC
        BDK_TRACE(DRAM, "N%d: Starting ECC DLL Read Offset Tuning for LMCs\n", node);
        errs = perform_HW_dll_offset_tuning(node, 2, 8/* ECC bytelane */);
        BDK_TRACE(DRAM, "N%d: Finished ECC DLL Read Offset Tuning for LMCs, %d errors\n",
                  node, errs);
        tot_errs += errs;
    }

    return tot_errs;
}

// this routine makes the calls to the tuning routines when criteria are met
// intended to be called for automated tuning, to apply filtering...

#define IS_DDR4  1
#define IS_DDR3  0
#define IS_RDIMM 1
#define IS_UDIMM 0
#define IS_1SLOT 1
#define IS_2SLOT 0

// FIXME: DDR3 is not tuned
static const uint32_t ddr_speed_filter[2][2][2] = {
    [IS_DDR4] = {
        [IS_RDIMM] = {
            [IS_1SLOT] =  940,
            [IS_2SLOT] =  800
        },
        [IS_UDIMM] = {
            [IS_1SLOT] = 1050,
            [IS_2SLOT] =  940
        },
    },
    [IS_DDR3] = {
        [IS_RDIMM] = {
            [IS_1SLOT] =    0, // disabled
            [IS_2SLOT] =    0  // disabled
        },
        [IS_UDIMM] = {
            [IS_1SLOT] =    0, // disabled
            [IS_2SLOT] =    0  // disabled
        }
    }
};

static int bdk_libdram_maybe_tune_node(int node)
{
    const char *str;

    // FIXME: allow an override here so that all configs can be tuned or none
    // If the envvar is defined, always either force it or avoid it accordingly
    if ((str = getenv("ddr_tune_all_configs")) != NULL) {
        int tune_it = !!strtoul(str, NULL, 0);
        printf("N%d: DRAM auto-tuning %s.\n", node, (tune_it) ? "forced" : "avoided");
        return (tune_it) ? bdk_libdram_tune_node(node) : 0;
    }

    // filter the tuning calls here...
    // determine if we should/can run automatically for this configuration
    //
    // FIXME: tune only when the configuration indicates it will help:
    //    DDR type, RDIMM or UDIMM, 1-slot or 2-slot, and speed
    //
    uint32_t ddr_speed = divide_nint(libdram_get_freq_from_pll(node, 0), 1000000); // sample LMC0
    BDK_CSR_INIT(lmc_config, node, BDK_LMCX_CONFIG(0)); // sample LMC0

    int is_ddr4  = !!__bdk_dram_is_ddr4(node, 0);
    int is_rdimm = !!__bdk_dram_is_rdimm(node, 0);
    int is_1slot = !!(lmc_config.s.init_status < 4); // HACK, should do better
    int do_tune = 0;

    uint32_t ddr_min_speed = ddr_speed_filter[is_ddr4][is_rdimm][is_1slot];
    do_tune = (ddr_min_speed && (ddr_speed > ddr_min_speed));

    ddr_print("N%d: DDR%d %cDIMM %d-slot at %d MHz %s eligible for auto-tuning.\n",
              node, (is_ddr4)?4:3, (is_rdimm)?'R':'U', (is_1slot)?1:2,
              ddr_speed, (do_tune)?"is":"is not");

    // call the tuning routines, done filtering...
    return ((do_tune) ? bdk_libdram_tune_node(node) : 0);
}

/**
 * This is the main DRAM init function. Users of libdram should call this function,
 * avoiding the other internal function. As a rule, functions starting with
 * "libdram_*" are part of the external API and should be used.
 *
 * @param node   Node to initialize. This may not be the same node as the one running the code
 * @param dram_config
 *               DRAM configuration to use
 * @param ddr_clock_override
 *               If non-zeo, this overrides the DRAM clock speed in the config structure. This
 *               allows quickly testing of different DRAM speeds without modifying the basic
 *               config. If zero, the DRAM speed in the config is used.
 *
 * @return Amount of memory in MB. Zero or negative is a failure.
 */
int libdram_config(int node, const dram_config_t *dram_config, int ddr_clock_override)
{
    /* Boards may need to mux the TWSI connection between THUNDERX and the BMC.
       This allows the BMC to monitor DIMM temeratures and health */
    int gpio_select = bdk_config_get_int(BDK_CONFIG_DRAM_CONFIG_GPIO);
    if (gpio_select != -1)
        bdk_gpio_initialize(bdk_numa_master(), gpio_select, 1, 1);

    /* Read all the SPDs and store them in the device tree. They are needed by
       later software to populate SMBIOS information */
    for (int lmc = 0; lmc < 4; lmc++)
        for (int dimm = 0; dimm < DDR_CFG_T_MAX_DIMMS; dimm++)
            read_entire_spd(node, (dram_config_t *)dram_config, lmc, dimm);

    const ddr_configuration_t *ddr_config = dram_config->config;
    int ddr_clock_hertz = (ddr_clock_override) ? ddr_clock_override : dram_config->ddr_clock_hertz;
    if (ddr_clock_hertz == 0)  // 0 == AUTO
    {
        ddr_clock_hertz = dram_get_default_spd_speed(node, ddr_config);
        if (ddr_clock_hertz < 0) {
            printf("N%d: DRAM init: AUTO clock ILLEGAL configuration\n", node);
            return -1;
        }
    }
    int errs;

    // At this point, we only know the desired clock rate (ddr_clock_hertz).
    // We do not know whether we are configuring RDIMMs.
    // We also do not yet know if 100MHz alternate refclk is actually available.
    // so, if we are being asked for 2133MT/s or better, we still need to do:
    // 1. probe for RDIMMs (if not, 50MHz refclk is good enough)
    // 2. determine if 100MHz refclk is there, and switch to it before starting any configuration
    //
    // NOTES:
    // 1. dclk_alt_refclk_sel need only be set on LMC0 (see above disabled code)
    // 2. I think we need to first probe to see if we need it, and configure it then if dictated use
    // 3. then go on to configure at the selected refclk
    int ddr_refclk_hertz = bdk_clock_get_rate(node, BDK_CLOCK_MAIN_REF);
    int alt_refclk = bdk_config_get_int(BDK_CONFIG_DDR_ALT_REFCLK, node);

    char *str = getenv("ddr_100mhz_refclk");
    if (str) { // if the envvar was found, force it to that setting
        int do_100mhz = !!strtoul(str, NULL, 0);
        alt_refclk = (do_100mhz) ? 100 : 50;
    }

    dram_verbosity = bdk_config_get_int(BDK_CONFIG_DRAM_VERBOSE);

    // Here we check for fuses that limit the number of LMCs we can configure,
    //  but only on 83XX and 88XX...
    int lmc_limit = 4;
    if (CAVIUM_IS_MODEL(CAVIUM_CN88XX) || CAVIUM_IS_MODEL(CAVIUM_CN83XX)) {
        BDK_CSR_INIT(mio_fus_dat2, node, BDK_MIO_FUS_DAT2);
        if (mio_fus_dat2.s.lmc_half) {
            lmc_limit = (CAVIUM_IS_MODEL(CAVIUM_CN88XX)) ? 2 : 1; // limit LMCs to half present
            error_print("Only %d LMC(s)s supported for this Thunder model\n", lmc_limit);
        }
    }

    /* We need to calculate the interface mask based on the provided SPD
       addresses/contents */
    uint32_t interface_mask = 0;
    for (int i = 0; i < lmc_limit; i++)
    {
        // We need to check only DIMM 0 of each LMC for possible presence of the LMC.
        // This trusts that the board database is correctly configured.
        // Empty DIMM slots in present LMCs will be detected later.
        if (ddr_config[i].dimm_config_table[0].spd_addr ||
            ddr_config[i].dimm_config_table[0].spd_ptr)
            interface_mask |= 1 << i;

        // we know whether alternate refclk is always wanted
        // we also know already if we want 2133 MT/s
        // if alt refclk not always wanted, then probe DDR and DIMM type
        // if DDR4 and RDIMMs, then set desired refclk to 100MHz, otherwise to default (50MHz)
        // depend on ddr_initialize() to do the refclk selection and validation
        if (i == 0) { // only check for LMC0
            if (alt_refclk) { // if alternate refclk was specified, let it override everything
                ddr_refclk_hertz = alt_refclk * 1000000;
                ddr_print("N%d: DRAM init: %d MHz refclk is REQUESTED ALWAYS\n", node, alt_refclk);
            } else if (ddr_clock_hertz > 1000000000) { // if more than 2000 MT/s
                int ddr_type = get_ddr_type(node, &ddr_config[0].dimm_config_table[0]);
                int spd_dimm_type = get_dimm_module_type(node, &ddr_config[0].dimm_config_table[0], ddr_type);
                // is DDR4 and RDIMM just to be sure
                if ((ddr_type == DDR4_DRAM) &&
                    ((spd_dimm_type == 1) || (spd_dimm_type == 5) || (spd_dimm_type == 8))) {
                    ddr_refclk_hertz = 100000000; // yes, we require 100MHz refclk, so set it
                    ddr_print("N%d: DRAM init: 100 MHz refclk is REQUIRED\n", node);
                }
            } // if (ddr_clock_hertz > 1000000000)
        } // if (i == 0)
    }

    BDK_TRACE(DRAM, "N%d: DRAM init started (hertz=%d, refclk=%d, config=%p)\n",
              node, ddr_clock_hertz, ddr_refclk_hertz, dram_config);
    debug_print("N%d: DRAM init started (hertz=%d, refclk=%d, config=%p)\n",
                node, ddr_clock_hertz, ddr_refclk_hertz, dram_config);

    BDK_TRACE(DRAM, "N%d: Calling DRAM init\n", node);
    measured_ddr_hertz[node] = 0;
    int mbytes = octeon_ddr_initialize(node,
        bdk_clock_get_rate(node, BDK_CLOCK_RCLK),
        ddr_clock_hertz,
        ddr_refclk_hertz,
        interface_mask,
        ddr_config,
        &measured_ddr_hertz[node],
        0,
        0,
        0);
    BDK_TRACE(DRAM, "N%d: DRAM init returned %d, measured %u Hz\n",
              node, mbytes, measured_ddr_hertz[node]);

    // do not tune or mess with memory if there was an init problem...
    if (mbytes > 0) {

        bdk_dram_disable_ecc_reporting(node);

        // call the tuning routines, with filtering...
        BDK_TRACE(DRAM, "N%d: Calling DRAM tuning\n", node);
        errs = bdk_libdram_maybe_tune_node(node);
        BDK_TRACE(DRAM, "N%d: DRAM tuning returned %d errors\n",
                  node, errs);

        // finally, clear memory and any left-over ECC errors
        bdk_dram_clear_mem(node);
        bdk_dram_clear_ecc(node);

        bdk_dram_enable_ecc_reporting(node);
    }

    /* Boards may need to mux the TWSI connection between THUNDERX and the BMC.
       This allows the BMC to monitor DIMM temeratures and health */
    if (gpio_select != -1)
        bdk_gpio_initialize(bdk_numa_master(), gpio_select, 1, 0);

    return mbytes;
}

/**
 * This is the main DRAM tuning function. Users of libdram should call this function,
 * avoiding the other internal function. As a rule, functions starting with
 * "libdram_*" are part of the external API and should be used.
 *
 * @param node   Node to tune. This may not be the same node as the one running the code
 *
 * @return Success or Fail
 */
int libdram_tune(int node)
{
    int tot_errs;
    int l2c_is_locked = bdk_l2c_is_locked(node);

    dram_verbosity = bdk_config_get_int(BDK_CONFIG_DRAM_VERBOSE);

    // the only way this entry point should be called is from a MENU item,
    // so, enable any non-running cores on this node, and leave them
    // running at the end...
    ddr_print("N%d: %s: Starting cores (mask was 0x%llx)\n",
              node, __func__, bdk_get_running_coremask(node));
    bdk_init_cores(node, ~0ULL);

    // must test for L2C locked here, cannot go on with it unlocked
    // FIXME: but we only need to worry about Node 0???
    if (node == 0) {
        if (!l2c_is_locked) { // is unlocked, must lock it now
            ddr_print("N%d: %s: L2C was unlocked - locking it now\n", node, __func__);
            // FIXME: this should be common-ized; it currently matches bdk_init()...
            bdk_l2c_lock_mem_region(node, 0, bdk_l2c_get_cache_size_bytes(node) * 3 / 4);
        } else {
            ddr_print("N%d: %s: L2C was already locked - continuing\n", node, __func__);
        }
    } else {
        ddr_print("N%d: %s: non-zero node, not worrying about L2C lock status\n", node, __func__);
    }

    // call the tuning routines, no filtering...
    tot_errs = bdk_libdram_tune_node(node);

    // FIXME: only for node 0, unlock L2C if it was unlocked before...
    if (node == 0) {
        if (!l2c_is_locked) { // it was Node 0 and unlocked, must re-unlock it now
            ddr_print("N%d: Node 0 L2C was unlocked before - unlocking it now\n", node);
            // FIXME: this should be common-ized; it currently matches bdk_init()...
            bdk_l2c_unlock_mem_region(node, 0, bdk_l2c_get_cache_size_bytes(node) * 3 / 4);
        } else {
            ddr_print("N%d: %s: L2C was already locked - leaving it locked\n", node, __func__);
        }
    } else {
        ddr_print("N%d: %s: non-zero node, not worrying about L2C lock status\n", node, __func__);
    }

    // make sure to clear memory and any ECC errs when done...
    bdk_dram_clear_mem(node);
    bdk_dram_clear_ecc(node);

    return tot_errs;
}

/**
 * This is the main function for DRAM margining of Write Voltage.
 * Users of libdram should call this function,
 * avoiding the other internal function. As a rule, functions starting with
 * "libdram_*" are part of the external API and should be used.
 *
 * @param node   Node to test. This may not be the same node as the one running the code
 *
 * @return Success or Fail
 */
static
int libdram_margin_write_voltage(int node)
{
    int tot_errs;

    // call the margining routine
    tot_errs = perform_margin_write_voltage(node);

    // make sure to clear memory and any ECC errs when done...
    bdk_dram_clear_mem(node);
    bdk_dram_clear_ecc(node);

    return tot_errs;
}

/**
 * This is the main function for DRAM margining of Read Voltage.
 * Users of libdram should call this function,
 * avoiding the other internal function. As a rule, functions starting with
 * "libdram_*" are part of the external API and should be used.
 *
 * @param node   Node to test. This may not be the same node as the one running the code
 *
 * @return Success or Fail
 */
static
int libdram_margin_read_voltage(int node)
{
    int tot_errs;

    // call the margining routine
    tot_errs = perform_margin_read_voltage(node);

    // make sure to clear memory and any ECC errs when done...
    bdk_dram_clear_mem(node);
    bdk_dram_clear_ecc(node);

    return tot_errs;
}

/**
 * This is the main function for DRAM margining of Write Timing.
 * Users of libdram should call this function,
 * avoiding the other internal function. As a rule, functions starting with
 * "libdram_*" are part of the external API and should be used.
 *
 * @param node   Node to test. This may not be the same node as the one running the code
 *
 * @return Success or Fail
 */
static
int libdram_margin_write_timing(int node)
{
    int tot_errs;

    // call the tuning routine, tell it we are margining not tuning...
    tot_errs = perform_dll_offset_tuning(node, /* write offsets */1, /* margin */0);

    // make sure to clear memory and any ECC errs when done...
    bdk_dram_clear_mem(node);
    bdk_dram_clear_ecc(node);

    return tot_errs;
}

/**
 * This is the main function for DRAM margining of Read Timing.
 * Users of libdram should call this function,
 * avoiding the other internal function. As a rule, functions starting with
 * "libdram_*" are part of the external API and should be used.
 *
 * @param node   Node to test. This may not be the same node as the one running the code
 *
 * @return Success or Fail
 */
static
int libdram_margin_read_timing(int node)
{
    int tot_errs;

    // call the tuning routine, tell it we are margining not tuning...
    tot_errs = perform_dll_offset_tuning(node, /* read offsets */2, /* margin */0);

    // make sure to clear memory and any ECC errs when done...
    bdk_dram_clear_mem(node);
    bdk_dram_clear_ecc(node);

    return tot_errs;
}

/**
 * This is the main function for all DRAM margining.
 * Users of libdram should call this function,
 * avoiding the other internal function. As a rule, functions starting with
 * "libdram_*" are part of the external API and should be used.
 *
 * @param node   Node to test. This may not be the same node as the one running the code
 *
 * @return Success or Fail
 */
int libdram_margin(int node)
{
    int ret_rt, ret_wt, ret_rv, ret_wv;
    const char *risk[2] = { "Low Risk", "Needs Review" };
    int l2c_is_locked = bdk_l2c_is_locked(node);

    // for now, no margining on 81xx, until we can reduce the dynamic runtime size...
    if (CAVIUM_IS_MODEL(CAVIUM_CN81XX)) {
        printf("Sorry, margining is not available on 81xx yet...\n");
        return 0;
    }

    dram_verbosity = bdk_config_get_int(BDK_CONFIG_DRAM_VERBOSE);

    // the only way this entry point should be called is from a MENU item,
    // so, enable any non-running cores on this node, and leave them
    // running at the end...
    ddr_print("N%d: %s: Starting cores (mask was 0x%llx)\n",
              node, __func__, bdk_get_running_coremask(node));
    bdk_init_cores(node, ~0ULL);

    // must test for L2C locked here, cannot go on with it unlocked
    // FIXME: but we only need to worry about Node 0???
    if (node == 0) {
        if (!l2c_is_locked) { // is unlocked, must lock it now
            ddr_print("N%d: %s: L2C was unlocked - locking it now\n", node, __func__);
            // FIXME: this should be common-ized; it currently matches bdk_init()...
            bdk_l2c_lock_mem_region(node, 0, bdk_l2c_get_cache_size_bytes(node) * 3 / 4);
        } else {
            ddr_print("N%d: %s: L2C was already locked - continuing\n", node, __func__);
        }
    } else {
        ddr_print("N%d: %s: non-zero node, not worrying about L2C lock status\n", node, __func__);
    }

    debug_print("N%d: Starting DRAM Margin ALL\n", node);
    ret_rt = libdram_margin_read_timing(node);
    ret_wt = libdram_margin_write_timing(node);
    ret_rv = libdram_margin_read_voltage(node);
    ret_wv = libdram_margin_write_voltage(node);
    debug_print("N%d: DRAM Margin ALL finished\n", node);

    /*
      >>> Summary from DDR Margining tool:
      >>> N0: Read Timing Margin   : Low Risk
      >>> N0: Write Timing Margin  : Low Risk
      >>> N0: Read Voltage Margin  : Low Risk
      >>> N0: Write Voltage Margin : Low Risk
     */
    printf("  \n");
    printf("-------------------------------------\n");
    printf("  \n");
    printf("Summary from DDR Margining tool\n");
    printf("N%d: Read Timing Margin   : %s\n", node, risk[!!ret_rt]);
    printf("N%d: Write Timing Margin  : %s\n", node, risk[!!ret_wt]);

    // these may not have been done due to DDR3 and/or THUNDER pass 1.x
    // FIXME? would it be better to print an appropriate message here?
    if (ret_rv != -1) printf("N%d: Read Voltage Margin  : %s\n", node, risk[!!ret_rv]);
    if (ret_wv != -1) printf("N%d: Write Voltage Margin : %s\n", node, risk[!!ret_wv]);

    printf("  \n");
    printf("-------------------------------------\n");
    printf("  \n");

    // FIXME: only for node 0, unlock L2C if it was unlocked before...
    if (node == 0) {
        if (!l2c_is_locked) { // it was Node 0 and unlocked, must re-unlock it now
            ddr_print("N%d: Node 0 L2C was unlocked before - unlocking it now\n", node);
            // FIXME: this should be common-ized; it currently matches bdk_init()...
            bdk_l2c_unlock_mem_region(node, 0, bdk_l2c_get_cache_size_bytes(node) * 3 / 4);
        } else {
            ddr_print("N%d: %s: L2C was already locked - leaving it locked\n", node, __func__);
        }
    } else {
        ddr_print("N%d: %s: non-zero node, not worrying about L2C lock status\n", node, __func__);
    }

    return 0;
}

/**
 * Get the measured DRAM frequency after a call to libdram_config
 *
 * @param node   Node to get frequency for
 *
 * @return Frequency in Hz
 */
uint32_t libdram_get_freq(int node)
{
    return measured_ddr_hertz[node];
}

/**
 * Get the measured DRAM frequency from the DDR_PLL_CTL CSR
 *
 * @param node   Node to get frequency for
 *
 * @return Frequency in Hz
 */
uint32_t libdram_get_freq_from_pll(int node, int lmc)
{
    static const uint8_t _en[] = {1, 2, 3, 4, 5, 6, 7, 8, 10, 12};
    BDK_CSR_INIT(c, node, BDK_LMCX_DDR_PLL_CTL(0));
    // we check the alternate refclk select bit in LMC0 to indicate 100MHz use
    // assumption: the alternate refclk is setup for 100MHz
    uint64_t ddr_ref_hertz = (c.s.dclk_alt_refclk_sel) ? 100000000 : bdk_clock_get_rate(node, BDK_CLOCK_MAIN_REF);
    uint64_t en = _en[c.cn83xx.ddr_ps_en];
    uint64_t calculated_ddr_hertz = ddr_ref_hertz * (c.cn83xx.clkf + 1) / ((c.cn83xx.clkr + 1) * en);
    return calculated_ddr_hertz;
}

#ifndef DRAM_CSR_WRITE_INLINE
void dram_csr_write(bdk_node_t node, const char *csr_name, bdk_csr_type_t type, int busnum, int size, uint64_t address, uint64_t value)
{
    VB_PRT(VBL_CSRS, "N%d: DDR Config %s[%016llx] => %016llx\n", node, csr_name, address, value);
    bdk_csr_write(node, type, busnum, size, address, value);
}
#endif
