/* SPDX-License-Identifier: GPL-2.0-or-later */

/**
 * This code was adapted from src/soc/amd/common/block/pi/amd_late_init.c
 */

#include <fsp/util.h>
#include <memory_info.h>
#include <console/console.h>
#include <cbmem.h>
#include <string.h>
#include <ec/google/chromeec/ec.h>
#include <bootstate.h>
#include <lib.h>
#include <dimm_info_util.h>
#include <dmi_info.h>
#include <device/dram/ddr4.h>
#include <device/dram/lpddr4.h>
#include <device/dram/ddr5.h>

/**
 * Convert DDR clock speed (based on memory type) in MHz to the standard reported speed in MT/s
 */
static uint16_t ddr_speed_mhz_to_reported_mts(uint16_t ddr_type, uint16_t speed)
{
	if (CONFIG(USE_DDR4) && ddr_type == MEMORY_TYPE_DDR4)
		return ddr4_speed_mhz_to_reported_mts(speed);
	else if (CONFIG(USE_LPDDR4) && ddr_type == MEMORY_TYPE_LPDDR4)
		return lpddr4_speed_mhz_to_reported_mts(speed);
	else if (CONFIG(USE_DDR5) && (ddr_type == MEMORY_TYPE_DDR5 ||
			ddr_type == MEMORY_TYPE_LPDDR5))
		return ddr5_speed_mhz_to_reported_mts(speed);

	printk(BIOS_ERR, "Unknown memory type %x\n", ddr_type);
	return 0;
}

/**
 * Return DDR voltage (in mV) based on memory type
 */
static uint16_t ddr_get_voltage(uint16_t ddr_type)
{
	switch (ddr_type) {
	case MEMORY_TYPE_DDR4:
		return 1200;
	case MEMORY_TYPE_LPDDR4:
	case MEMORY_TYPE_DDR5:
		return 1100;
	case MEMORY_TYPE_LPDDR5:
		return 1050;
	default:
		printk(BIOS_ERR, "Unknown memory type %x\n", ddr_type);
		return 0;
	}
}

/**
 * Populate dimm_info using AGESA TYPE17_DMI_INFO.
 */
static void transfer_memory_info(const TYPE17_DMI_INFO *dmi17,
				 struct dimm_info *dimm)
{
	hexstrtobin(dmi17->SerialNumber, dimm->serial, sizeof(dimm->serial));

	dimm->dimm_size = smbios_memory_size_to_mib(dmi17->MemorySize, dmi17->ExtSize);

	dimm->ddr_type = dmi17->MemoryType;

	dimm->configured_speed_mts = ddr_speed_mhz_to_reported_mts(
		dmi17->MemoryType, dmi17->ConfigSpeed);

	dimm->max_speed_mts = ddr_speed_mhz_to_reported_mts(dmi17->MemoryType, dmi17->Speed);

	dimm->rank_per_dimm = dmi17->Attributes;

	dimm->mod_type = smbios_form_factor_to_spd_mod_type(
		(smbios_memory_type)dmi17->MemoryType,
		(smbios_memory_form_factor)dmi17->FormFactor);

	dimm->bus_width = smbios_bus_width_to_spd_width(dmi17->MemoryType, dmi17->TotalWidth,
						dmi17->DataWidth);

	dimm->mod_id = dmi17->ManufacturerIdCode;

	dimm->bank_locator = 0;

	dimm->vdd_voltage = ddr_get_voltage(dmi17->MemoryType);

	strncpy((char *)dimm->module_part_number, dmi17->PartNumber,
		sizeof(dimm->module_part_number) - 1);
}

static void print_dimm_info(const struct dimm_info *dimm)
{
	printk(BIOS_DEBUG,
	       "CBMEM_ID_MEMINFO:\n"
	       "  dimm_size: %u\n"
	       "  ddr_type: 0x%hx\n"
	       "  max_speed_mts: %hu\n"
	       "  config_speed_mts: %hu\n"
	       "  vdd_voltage: %hu\n"
	       "  rank_per_dimm: %hhu\n"
	       "  channel_num: %hhu\n"
	       "  dimm_num: %hhu\n"
	       "  bank_locator: %hhu\n"
	       "  mod_id: %hx\n"
	       "  mod_type: 0x%hhx\n"
	       "  bus_width: %hhu\n"
	       "  serial: %02hhx%02hhx%02hhx%02hhx\n"
	       "  module_part_number(%zu): %s\n",
	       dimm->dimm_size,
	       dimm->ddr_type,
	       dimm->max_speed_mts,
	       dimm->configured_speed_mts,
	       dimm->vdd_voltage,
	       dimm->rank_per_dimm,
	       dimm->channel_num,
	       dimm->dimm_num,
	       dimm->bank_locator,
	       dimm->mod_id,
	       dimm->mod_type,
	       dimm->bus_width,
	       dimm->serial[0],
	       dimm->serial[1],
	       dimm->serial[2],
	       dimm->serial[3],
	       strlen((const char *)dimm->module_part_number),
	       (char *)dimm->module_part_number);
}

static void print_dmi_info(const TYPE17_DMI_INFO *dmi17)
{
	printk(BIOS_DEBUG,
	       "AGESA TYPE 17 DMI INFO:\n"
	       "  Handle: %hu\n"
	       "  TotalWidth: %hu\n"
	       "  DataWidth: %hu\n"
	       "  MemorySize: %hu\n"
	       "  DeviceSet: %hhu\n"
	       "  Speed: %hu\n"
	       "  ManufacturerIdCode: %llx\n"
	       "  Attributes: %hhu\n"
	       "  ExtSize: %u\n"
	       "  ConfigSpeed: %hu\n"
	       "  MemoryType: 0x%x\n"
	       "  FormFactor: 0x%x\n"
	       "  DeviceLocator: %8s\n"
	       "  BankLocator: %10s\n"
	       "  SerialNumber(%zu): %9s\n"
	       "  PartNumber(%zu): %19s\n",
	       dmi17->Handle,
	       dmi17->TotalWidth,
	       dmi17->DataWidth,
	       dmi17->MemorySize,
	       dmi17->DeviceSet,
	       dmi17->Speed,
	       dmi17->ManufacturerIdCode,
	       dmi17->Attributes,
	       dmi17->ExtSize,
	       dmi17->ConfigSpeed,
	       dmi17->MemoryType,
	       dmi17->FormFactor,
	       dmi17->DeviceLocator,
	       dmi17->BankLocator,
	       strlen((const char *)dmi17->SerialNumber),
	       dmi17->SerialNumber,
	       strlen((const char *)dmi17->PartNumber),
	       dmi17->PartNumber);
}

/**
 * Marshalls dimm info from AMD_FSP_DMI_HOB into CBMEM_ID_MEMINFO
 */
static void prepare_dmi_16_17(void *unused)
{
	const DMI_INFO *dmi_table;
	const TYPE17_DMI_INFO *type17_dmi_info;
	struct memory_info *mem_info;
	struct dimm_info *dimm_info;
	char cbi_part_number[DIMM_INFO_PART_NUMBER_SIZE];
	bool use_cbi_part_number = false;
	size_t dimm_cnt = 0;
	size_t amd_fsp_dmi_hob_size;
	const EFI_GUID amd_fsp_dmi_hob_guid = AMD_FSP_DMI_HOB_GUID;

	printk(BIOS_DEBUG, "Saving dimm info for smbios type 17\n");

	/* Allocate meminfo in cbmem. */
	mem_info = cbmem_add(CBMEM_ID_MEMINFO, sizeof(struct memory_info));
	if (!mem_info) {
		printk(BIOS_ERR,
		       "Failed to add memory info to CBMEM, DMI tables will be incomplete\n");
		return;
	}
	memset(mem_info, 0, sizeof(struct memory_info));

	/* Locate the memory info HOB. */
	dmi_table = fsp_find_extension_hob_by_guid(
		(const uint8_t *)&amd_fsp_dmi_hob_guid, &amd_fsp_dmi_hob_size);

	if (dmi_table == NULL || amd_fsp_dmi_hob_size == 0) {
		printk(BIOS_ERR,
		       "AMD_FSP_DMI_HOB not found, DMI table 17 will be incomplete\n");
		return;
	}
	printk(BIOS_DEBUG, "AMD_FSP_DMI_HOB found\n");

	if (CONFIG(EC_GOOGLE_CHROMEEC)) {
		/* Prefer DRAM part number from CBI. */
		if (google_chromeec_cbi_get_dram_part_num(
			cbi_part_number, sizeof(cbi_part_number)) == 0) {
			use_cbi_part_number = true;
		} else {
			printk(BIOS_ERR, "Could not obtain DRAM part number from CBI\n");
		}
	}

	for (unsigned int channel = 0; channel < AGESA_STRUCT_CHANNELS_PER_SOCKET; channel++) {
		for (unsigned int dimm = 0; dimm < AGESA_STRUCT_DIMMS_PER_CHANNEL; dimm++) {
			type17_dmi_info = &dmi_table->T17[0][channel][dimm];
			/* DIMMs that are present will have a non-zero
			   handle. */
			if (type17_dmi_info->Handle == 0)
				continue;
			print_dmi_info(type17_dmi_info);
			dimm_info = &mem_info->dimm[dimm_cnt];
			dimm_info->channel_num = channel;
			dimm_info->dimm_num = dimm;
			transfer_memory_info(type17_dmi_info, dimm_info);
			if (use_cbi_part_number) {
				/* mem_info is memset to 0 above, so it's
				   safe to assume module_part_number will be
				   null terminated */
				strncpy((char *)dimm_info->module_part_number, cbi_part_number,
					sizeof(dimm_info->module_part_number) - 1);

				/* These ID values match what's used in device/dram/spd.c */
				switch (dimm_info->module_part_number[0]) {
				case 'H':
					dimm_info->mod_id = 0xad00;	// Hynix
					break;
				case 'K':
					dimm_info->mod_id = 0x9801;	// Kingston
					break;
				case 'M':
					dimm_info->mod_id = 0x2c00;	// Micron
					break;
				case 'N':
					dimm_info->mod_id = 0x0b83;	// Nanya
					break;
				}
			}
			print_dimm_info(dimm_info);
			dimm_cnt++;
		}
	}
	mem_info->dimm_cnt = dimm_cnt;

	mem_info->ecc_type = dmi_table->T16.MemoryErrorCorrection;
}

/* AMD_FSP_DMI_HOB is initialized very late, so check it just in time for writing tables. */
BOOT_STATE_INIT_ENTRY(BS_WRITE_TABLES, BS_ON_ENTRY, prepare_dmi_16_17, NULL);