mb/prodrive/atlas: Implement initial VPD support

Atlas stores VPD (Vital Product Data) in an I2C EEPROM, which is only
connected to the EC. In order for the host (x86) to be able to access
the VPD, the EC reads the EEPROM contents into a buffer in EC RAM and
provides the host with read-only access to this EC RAM buffer through
EMI (Embedded Memory Interface) 0.

The VPD layout is designed to be extensible yet backwards compatible.
The code in coreboot uses the revision field to know which fields are
valid, and will populate the rest with fallback values.

Use the serial number and part number in VPD to populate SMBIOS tables.

Change-Id: I2d3d70fee22548daa73ef98af56c98e950dc5e9d
Signed-off-by: Angel Pons <th3fanbus@gmail.com>
Reviewed-on: https://review.coreboot.org/c/coreboot/+/73937
Tested-by: build bot (Jenkins) <no-reply@coreboot.org>
Reviewed-by: Lean Sheng Tan <sheng.tan@9elements.com>
diff --git a/src/mainboard/prodrive/atlas/Makefile.inc b/src/mainboard/prodrive/atlas/Makefile.inc
index 3826c8f..6c11876 100644
--- a/src/mainboard/prodrive/atlas/Makefile.inc
+++ b/src/mainboard/prodrive/atlas/Makefile.inc
@@ -1,6 +1,7 @@
 ## SPDX-License-Identifier: GPL-2.0-only
 
 all-y += ec.c
+all-y += vpd.c
 
 bootblock-y += bootblock.c
 bootblock-y += early_gpio.c
@@ -9,3 +10,4 @@
 
 ramstage-y += gpio.c
 ramstage-y += mainboard.c
+ramstage-y += smbios.c
diff --git a/src/mainboard/prodrive/atlas/mainboard.c b/src/mainboard/prodrive/atlas/mainboard.c
index 227e30a..db808ce 100644
--- a/src/mainboard/prodrive/atlas/mainboard.c
+++ b/src/mainboard/prodrive/atlas/mainboard.c
@@ -1,14 +1,14 @@
 /* SPDX-License-Identifier: GPL-2.0-only */
 
-#include <device/device.h>
 #include <console/console.h>
-#include <stdint.h>
+#include <device/device.h>
 #include <gpio.h>
-#include <arch/io.h>
-#include <string.h>
 #include <smbios.h>
+#include <string.h>
+#include <types.h>
 
 #include "gpio.h"
+#include "vpd.h"
 
 void smbios_fill_dimm_locator(const struct dimm_info *dimm, struct smbios_type17 *t)
 {
@@ -53,6 +53,25 @@
 	printk(BIOS_INFO, "HSID: 0x%x\n", get_hsid());
 }
 
+static const char *get_formatted_pn(void)
+{
+	static char buffer[32 + ATLAS_SN_PN_LENGTH] = {0};
+	const char *prefix = "P/N: ";
+	snprintf(buffer, sizeof(buffer), "%s%s", prefix, get_emi_eeprom_vpd()->part_number);
+	return buffer;
+}
+
+static void mainboard_smbios_strings(struct device *dev, struct smbios_type11 *t)
+{
+	t->count = smbios_add_string(t->eos, get_formatted_pn());
+}
+
+static void mainboard_enable(struct device *dev)
+{
+	dev->ops->get_smbios_strings = mainboard_smbios_strings;
+}
+
 struct chip_operations mainboard_ops = {
-	.init = mainboard_init,
+	.init       = mainboard_init,
+	.enable_dev = mainboard_enable,
 };
diff --git a/src/mainboard/prodrive/atlas/smbios.c b/src/mainboard/prodrive/atlas/smbios.c
new file mode 100644
index 0000000..1167a80
--- /dev/null
+++ b/src/mainboard/prodrive/atlas/smbios.c
@@ -0,0 +1,10 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+
+#include <smbios.h>
+
+#include "vpd.h"
+
+const char *smbios_mainboard_serial_number(void)
+{
+	return get_emi_eeprom_vpd()->serial_number;
+}
diff --git a/src/mainboard/prodrive/atlas/vpd.c b/src/mainboard/prodrive/atlas/vpd.c
new file mode 100644
index 0000000..12371f7
--- /dev/null
+++ b/src/mainboard/prodrive/atlas/vpd.c
@@ -0,0 +1,60 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+
+#include <console/console.h>
+#include <string.h>
+#include <types.h>
+
+#include "ec.h"
+#include "mainboard.h"
+#include "vpd.h"
+
+const union emi_eeprom_vpd *get_emi_eeprom_vpd(void)
+{
+	static union emi_eeprom_vpd vpd = {0};
+
+	/* Check if cached VPD is valid */
+	if (vpd.header.revision == VPD_LATEST_REVISION)
+		return &vpd;
+
+	ec_emi_read(vpd.raw, EMI_0_IO_BASE_ADDR, 0, 0, sizeof(vpd.raw));
+
+	/* If the magic value doesn't match, consider EEPROM VPD unreliable */
+	if (vpd.header.magic != VPD_MAGIC) {
+		printk(BIOS_WARNING, "Atlas VPD: Bad magic value, using fallback defaults\n");
+		vpd.header.revision = 0;
+	} else {
+		printk(BIOS_DEBUG, "Atlas VPD: Got revision %u from EC\n", vpd.header.revision);
+	}
+
+	/*
+	 * For backwards compatibility, if the VPD from the EC is an older
+	 * version, uprev it to the latest version coreboot knows about by
+	 * filling in the remaining fields with default values. Should the
+	 * EC provide a newer VPD revision, coreboot would downgrade it to
+	 * the latest version it knows about as the VPD layout is designed
+	 * to be backwards compatible.
+	 *
+	 * Whenever the value of `VPD_LATEST_REVISION` is incremented, add
+	 * a new `case` label just before the `default` label that matches
+	 * the second latest revision to initialise the newly-added fields
+	 * of the VPD structure with a reasonable fallback value. Note the
+	 * intentional falling through.
+	 */
+	switch (vpd.header.revision) {
+	case 0:
+		memset(vpd.raw, 0, sizeof(vpd.raw));
+		vpd.header.magic = VPD_MAGIC;
+		vpd.serial_number[0] = '\0';
+		vpd.part_number[0] = '\0';
+		vpd.profile = ATLAS_PROF_UNPROGRAMMED;
+		__fallthrough;
+	default:
+		/* Ensure serial numbers are NULL-terminated, update revision last */
+		vpd.serial_number[ATLAS_SN_PN_LENGTH - 1] = '\0';
+		vpd.part_number[ATLAS_SN_PN_LENGTH - 1] = '\0';
+		vpd.header.revision = VPD_LATEST_REVISION;
+		break;
+	}
+
+	return &vpd;
+}
diff --git a/src/mainboard/prodrive/atlas/vpd.h b/src/mainboard/prodrive/atlas/vpd.h
new file mode 100644
index 0000000..3955c11
--- /dev/null
+++ b/src/mainboard/prodrive/atlas/vpd.h
@@ -0,0 +1,59 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+
+#ifndef ATLAS_VPD_H
+#define ATLAS_VPD_H
+
+/*
+ * The Atlas COMe module stores some non-volatile vital product data in
+ * an EC-attached I2C EEPROM. The EC firmware reads the EEPROM contents
+ * and provides them to the host via EMI (Embedded Memory Interface) 0.
+ */
+
+#include <types.h>
+
+#define VPD_MAGIC	0x56504453  /* 'VPDS' */
+
+/*
+ * Increment this value whenever new fields are added to the structures.
+ * Furthermore, adapt the `get_emi_eeprom_vpd()` function accordingly to
+ * provide fallback values for newly-added fields.
+ */
+#define VPD_LATEST_REVISION	1
+
+struct __packed emi_eeprom_vpd_header {
+	uint32_t magic;
+	uint8_t revision;
+	uint8_t _rfu[15];	/* Reserved for Future Use */
+};
+
+/* For backwards compatibility reasons, do NOT reuse enum values! */
+enum atlas_profile {
+	ATLAS_PROF_UNPROGRAMMED		= 0,	/* EEPROM not initialised */
+	ATLAS_PROF_DEFAULT		= 1,
+	ATLAS_PROF_REALTIME_PERFORMANCE	= 2,
+	ATLAS_PROF_THEMIS_LED_CONFIG	= 3,
+};
+
+#define ATLAS_SN_PN_LENGTH	20
+
+#define EMI_EEPROM_LAYOUT_LENGTH (			\
+		sizeof(struct emi_eeprom_vpd_header) +	\
+		ATLAS_SN_PN_LENGTH +			\
+		ATLAS_SN_PN_LENGTH +			\
+		sizeof(uint16_t)			\
+	)
+
+union emi_eeprom_vpd {
+	struct __packed {
+		struct emi_eeprom_vpd_header header;
+		char serial_number[ATLAS_SN_PN_LENGTH];	/* xx-xx-xxx-xxx */
+		char part_number[ATLAS_SN_PN_LENGTH];	/* xxx-xxxx-xxxx.Rxx */
+		uint16_t profile;
+	};
+	uint8_t raw[EMI_EEPROM_LAYOUT_LENGTH];
+};
+
+/* Always returns a non-NULL pointer to valid data */
+const union emi_eeprom_vpd *get_emi_eeprom_vpd(void);
+
+#endif /* ATLAS_VPD_H */