board: sifive: unmatched: add initial support for a platform ID EEPROM

Add initial support for the PCB description EEPROM for SiFive HiFive
Unmatched boards.

This implementation is refactored based on Paul Walmsley's porting and
adopt the suggestions from David Abdurachmanov.

Signed-off-by: Paul Walmsley <paul.walmsley@sifive.com>
Signed-off-by: David Abdurachmanov <david.abdurachmanov@sifive.com>
Signed-off-by: Zong Li <zong.li@sifive.com>
Reviewed-by: Leo Yu-Chi Liang <ycliang@andestech.com>
diff --git a/board/sifive/unmatched/hifive-platform-i2c-eeprom.c b/board/sifive/unmatched/hifive-platform-i2c-eeprom.c
new file mode 100644
index 0000000..9a62d32
--- /dev/null
+++ b/board/sifive/unmatched/hifive-platform-i2c-eeprom.c
@@ -0,0 +1,542 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Copyright (C) 2020 SiFive, Inc.
+ *
+ * Based on board/freescale/common/sys_eeprom.c:
+ * Copyright 2006, 2008-2009, 2011 Freescale Semiconductor
+ * York Sun (yorksun@freescale.com)
+ * Haiying Wang (haiying.wang@freescale.com)
+ * Timur Tabi (timur@freescale.com)
+ */
+
+#include <common.h>
+#include <command.h>
+#include <env.h>
+#include <i2c.h>
+#include <init.h>
+#include <linux/ctype.h>
+#include <linux/delay.h>
+#include <u-boot/crc.h>
+
+#ifndef CONFIG_SYS_EEPROM_BUS_NUM
+#error Requires CONFIG_SYS_EEPROM_BUS_NUM to be defined
+#endif
+
+#define FORMAT_VERSION				0x1
+
+/* Options for the manuf_test_status field */
+#define SIFIVE_MANUF_TEST_STATUS_UNKNOWN	0
+#define SIFIVE_MANUF_TEST_STATUS_PASS		1
+#define SIFIVE_MANUF_TEST_STATUS_FAIL		2
+
+/*
+ * BYTES_PER_EEPROM_PAGE: the AT24C02 datasheet says that data can
+ * only be written in page mode, which means 8 bytes at a time
+ */
+#define BYTES_PER_EEPROM_PAGE			8
+
+/*
+ * EEPROM_WRITE_DELAY_MS: the AT24C02 datasheet says it takes up to
+ * 5ms to complete a given write
+ */
+#define EEPROM_WRITE_DELAY_MS			5000
+
+/*
+ * MAGIC_NUMBER_BYTES: number of bytes used by the magic number
+ */
+#define MAGIC_NUMBER_BYTES			4
+
+/*
+ * SERIAL_NUMBER_BYTES: number of bytes used by the board serial
+ * number
+ */
+#define SERIAL_NUMBER_BYTES			16
+
+/*
+ * MAC_ADDR_BYTES: number of bytes used by the Ethernet MAC address
+ */
+#define MAC_ADDR_BYTES				6
+
+/*
+ * MAC_ADDR_STRLEN: length of mac address string
+ */
+#define MAC_ADDR_STRLEN				17
+
+/*
+ * SiFive OUI. Registration Date is 2018-02-15
+ */
+#define SIFIVE_OUI_PREFIX			"70:B3:D5:92:F"
+
+/**
+ * static eeprom: EEPROM layout for the SiFive platform I2C format
+ */
+static struct __attribute__ ((__packed__)) sifive_eeprom {
+	u8 magic[MAGIC_NUMBER_BYTES];
+	u8 format_ver;
+	u16 product_id;
+	u8 pcb_revision;
+	u8 bom_revision;
+	u8 bom_variant;
+	u8 serial[SERIAL_NUMBER_BYTES];
+	u8 manuf_test_status;
+	u8 mac_addr[MAC_ADDR_BYTES];
+	u32 crc;
+} e;
+
+struct sifive_product {
+	u16 id;
+	const char *name;
+};
+
+/* Set to 1 if we've read EEPROM into memory */
+static int has_been_read;
+
+/* Magic number at the first four bytes of EEPROM */
+static const unsigned char magic[MAGIC_NUMBER_BYTES] = { 0xf1, 0x5e, 0x50, 0x45 };
+
+/* Does the magic number match that of a SiFive EEPROM? */
+static inline int is_match_magic(void)
+{
+	return (memcmp(&e.magic, &magic, MAGIC_NUMBER_BYTES) == 0);
+}
+
+/* Calculate the current CRC */
+static inline u32 calculate_crc32(void)
+{
+	return crc32(0, (void *)&e, sizeof(struct sifive_eeprom) - sizeof(e.crc));
+}
+
+/* This function should be called after each update to the EEPROM structure */
+static inline void update_crc(void)
+{
+	e.crc = calculate_crc32();
+}
+
+static struct sifive_product sifive_products[] = {
+	{ 0, "Unknown"},
+	{ 2, "HiFive Unmatched" },
+};
+
+/**
+ * dump_raw_eeprom - display the raw contents of the EEPROM
+ */
+static void dump_raw_eeprom(void)
+{
+	unsigned int i;
+
+	printf("EEPROM dump: (0x%lx bytes)\n", sizeof(e));
+	for (i = 0; i < sizeof(e); i++) {
+		if ((i % 16) == 0)
+			printf("%02X: ", i);
+		printf("%02X ", ((u8 *)&e)[i]);
+		if (((i % 16) == 15) || (i == sizeof(e) - 1))
+			printf("\n");
+	}
+}
+
+/**
+ * show_eeprom - display the contents of the EEPROM
+ */
+static void show_eeprom(void)
+{
+	unsigned int i;
+	u32 crc;
+	const char *product_name = "Unknown";
+	char board_serial[SERIAL_NUMBER_BYTES + 1] = { 0 };
+
+	if (!is_match_magic()) {
+		printf("Not a SiFive HiFive EEPROM data format - magic bytes don't match\n");
+		dump_raw_eeprom();
+		return;
+	};
+
+	snprintf(board_serial, sizeof(board_serial), "%s", e.serial);
+
+	for (i = 0; i < ARRAY_SIZE(sifive_products); i++) {
+		if (sifive_products[i].id == e.product_id) {
+			product_name = sifive_products[i].name;
+			break;
+		}
+	};
+
+	printf("SiFive PCB EEPROM format v%u\n", e.format_ver);
+	printf("Product ID: %04hx (%s)\n", e.product_id, product_name);
+	printf("PCB revision: %x\n", e.pcb_revision);
+	printf("BOM revision: %c\n", e.bom_revision);
+	printf("BOM variant: %x\n", e.bom_variant);
+	printf("Serial number: %s\n", board_serial);
+	printf("Ethernet MAC address: %02x:%02x:%02x:%02x:%02x:%02x\n",
+	       e.mac_addr[0], e.mac_addr[1], e.mac_addr[2],
+	       e.mac_addr[3], e.mac_addr[4], e.mac_addr[5]);
+
+	crc = calculate_crc32();
+	if (crc == e.crc) {
+		printf("CRC: %08x\n", e.crc);
+	} else {
+		printf("CRC: %08x (should be %08x)\n", e.crc, crc);
+		dump_raw_eeprom();
+	}
+}
+
+/**
+ * read_eeprom() - read the EEPROM into memory, if it hasn't been read already
+ */
+static int read_eeprom(void)
+{
+	int ret;
+	struct udevice *dev;
+
+	if (has_been_read)
+		return 0;
+
+	ret = i2c_get_chip_for_busnum(CONFIG_SYS_EEPROM_BUS_NUM,
+				      CONFIG_SYS_I2C_EEPROM_ADDR,
+				      1,
+				      &dev);
+	if (!ret)
+		dm_i2c_read(dev, 0, (void *)&e,
+			    sizeof(struct sifive_eeprom));
+
+	show_eeprom();
+
+	has_been_read = (ret == 0) ? 1 : 0;
+
+	return ret;
+}
+
+/**
+ * prog_eeprom() - write the EEPROM from memory
+ */
+static int prog_eeprom(void)
+{
+	int ret = 0;
+	unsigned int i;
+	void *p;
+
+	if (!is_match_magic()) {
+		printf("Please read the EEPROM ('read_eeprom') and/or initialize the EEPROM ('initialize') first.\n");
+		return 0;
+	}
+
+	for (i = 0, p = &e; i < sizeof(e);
+	     i += BYTES_PER_EEPROM_PAGE, p += BYTES_PER_EEPROM_PAGE) {
+		struct udevice *dev;
+
+		ret = i2c_get_chip_for_busnum(CONFIG_SYS_EEPROM_BUS_NUM,
+					      CONFIG_SYS_I2C_EEPROM_ADDR,
+					      CONFIG_SYS_I2C_EEPROM_ADDR_LEN,
+					      &dev);
+		if (!ret)
+			ret = dm_i2c_write(dev, i, p,
+					   min((int)(sizeof(e) - i),
+					       BYTES_PER_EEPROM_PAGE));
+
+		if (ret)
+			break;
+
+		udelay(EEPROM_WRITE_DELAY_MS);
+	}
+
+	if (!ret) {
+		/* Verify the write by reading back the EEPROM and comparing */
+		struct sifive_eeprom e2;
+		struct udevice *dev;
+
+		ret = i2c_get_chip_for_busnum(CONFIG_SYS_EEPROM_BUS_NUM,
+					      CONFIG_SYS_I2C_EEPROM_ADDR,
+					      CONFIG_SYS_I2C_EEPROM_ADDR_LEN,
+					      &dev);
+		if (!ret)
+			ret = dm_i2c_read(dev, 0, (void *)&e2, sizeof(e2));
+		if (!ret && memcmp(&e, &e2, sizeof(e)))
+			ret = -1;
+	}
+
+	if (ret) {
+		printf("Programming failed.\n");
+		has_been_read = 0;
+		return -1;
+	}
+
+	printf("Programming passed.\n");
+	return 0;
+}
+
+/**
+ * set_mac_address() - stores a MAC address into the local EEPROM copy
+ *
+ * This function takes a pointer to MAC address string
+ * (i.e."XX:XX:XX:XX:XX:XX", where "XX" is a two-digit hex number),
+ * stores it in the MAC address field of the EEPROM local copy, and
+ * updates the local copy of the CRC.
+ */
+static void set_mac_address(char *string)
+{
+	unsigned int i;
+
+	if (strncasecmp(SIFIVE_OUI_PREFIX, string, 13)) {
+		printf("The MAC address doesn't match SiFive OUI %s\n",
+		       SIFIVE_OUI_PREFIX);
+		return;
+	}
+
+	for (i = 0; *string && (i < MAC_ADDR_BYTES); i++) {
+		e.mac_addr[i] = simple_strtoul(string, &string, 16);
+		if (*string == ':')
+			string++;
+	}
+
+	update_crc();
+}
+
+/**
+ * set_manuf_test_status() - stores a test status byte into the in-memory copy
+ *
+ * Takes a pointer to a manufacturing test status string ("unknown",
+ * "pass", "fail") and stores the corresponding numeric ID to the
+ * manuf_test_status field of the EEPROM local copy, and updates the
+ * CRC of the local copy.
+ */
+static void set_manuf_test_status(char *string)
+{
+	if (!strcasecmp(string, "unknown")) {
+		e.manuf_test_status = SIFIVE_MANUF_TEST_STATUS_UNKNOWN;
+	} else if (!strcasecmp(string, "pass")) {
+		e.manuf_test_status = SIFIVE_MANUF_TEST_STATUS_PASS;
+	} else if (!strcasecmp(string, "fail")) {
+		e.manuf_test_status = SIFIVE_MANUF_TEST_STATUS_FAIL;
+	} else {
+		printf("Usage: mac manuf_test_status (unknown|pass|fail)\n");
+		return;
+	}
+
+	update_crc();
+}
+
+/**
+ * set_pcb_revision() - stores a SiFive PCB revision into the local EEPROM copy
+ *
+ * Takes a pointer to a string representing the numeric PCB revision in
+ * decimal ("0" - "255"), stores it in the pcb_revision field of the
+ * EEPROM local copy, and updates the CRC of the local copy.
+ */
+static void set_pcb_revision(char *string)
+{
+	unsigned long p;
+
+	p = simple_strtoul(string, &string, 10);
+	if (p > U8_MAX) {
+		printf("%s must not be greater than %d\n", "PCB revision",
+		       U8_MAX);
+		return;
+	}
+
+	e.pcb_revision = p;
+
+	update_crc();
+}
+
+/**
+ * set_bom_revision() - stores a SiFive BOM revision into the local EEPROM copy
+ *
+ * Takes a pointer to a uppercase ASCII character representing the BOM
+ * revision ("A" - "Z"), stores it in the bom_revision field of the
+ * EEPROM local copy, and updates the CRC of the local copy.
+ */
+static void set_bom_revision(char *string)
+{
+	if (string[0] < 'A' || string[0] > 'Z') {
+		printf("BOM revision must be an uppercase letter between A and Z\n");
+		return;
+	}
+
+	e.bom_revision = string[0];
+
+	update_crc();
+}
+
+/**
+ * set_bom_variant() - stores a SiFive BOM variant into the local EEPROM copy
+ *
+ * Takes a pointer to a string representing the numeric BOM variant in
+ * decimal ("0" - "255"), stores it in the bom_variant field of the
+ * EEPROM local copy, and updates the CRC of the local copy.
+ */
+static void set_bom_variant(char *string)
+{
+	unsigned long p;
+
+	p = simple_strtoul(string, &string, 10);
+	if (p > U8_MAX) {
+		printf("%s must not be greater than %d\n", "BOM variant",
+		       U8_MAX);
+		return;
+	}
+
+	e.bom_variant = p;
+
+	update_crc();
+}
+
+/**
+ * set_product_id() - stores a SiFive product ID into the local EEPROM copy
+ *
+ * Takes a pointer to a string representing the numeric product ID  in
+ * decimal ("0" - "65535"), stores it in the product ID field of the
+ * EEPROM local copy, and updates the CRC of the local copy.
+ */
+static void set_product_id(char *string)
+{
+	unsigned long p;
+
+	p = simple_strtoul(string, &string, 10);
+	if (p > U16_MAX) {
+		printf("%s must not be greater than %d\n", "Product ID",
+		       U16_MAX);
+		return;
+	}
+
+	e.product_id = p;
+
+	update_crc();
+}
+
+/**
+ * set_serial_number() - set the PCB serial number in the in-memory copy
+ *
+ * Set the board serial number in the in-memory EEPROM copy from the supplied
+ * string argument, and update the CRC.
+ */
+static void set_serial_number(char *string)
+{
+	if (strlen(string) > SERIAL_NUMBER_BYTES) {
+		printf("Serial number must not be greater than 16 bytes\n");
+		return;
+	}
+
+	memset(e.serial, 0, sizeof(e.serial));
+	strncpy((char *)e.serial, string, sizeof(e.serial));
+	update_crc();
+}
+
+/**
+ * init_local_copy() - initialize the in-memory EEPROM copy
+ *
+ * Initialize the in-memory EEPROM copy with the magic number.  Must
+ * be done when preparing to initialize a blank EEPROM, or overwrite
+ * one with a corrupted magic number.
+ */
+static void init_local_copy(void)
+{
+	memset(&e, 0, sizeof(e));
+	memcpy(e.magic, magic, sizeof(e.magic));
+	e.format_ver = FORMAT_VERSION;
+	update_crc();
+}
+
+int do_mac(struct cmd_tbl *cmdtp, int flag, int argc, char *const argv[])
+{
+	char *cmd;
+
+	if (argc == 1) {
+		show_eeprom();
+		return 0;
+	}
+
+	if (argc > 3)
+		return cmd_usage(cmdtp);
+
+	cmd = argv[1];
+
+	/* Commands with no argument */
+	if (!strcmp(cmd, "read_eeprom")) {
+		read_eeprom();
+		return 0;
+	} else if (!strcmp(cmd, "initialize")) {
+		init_local_copy();
+		return 0;
+	} else if (!strcmp(cmd, "write_eeprom")) {
+		prog_eeprom();
+		return 0;
+	}
+
+	if (argc != 3)
+		return cmd_usage(cmdtp);
+
+	if (!is_match_magic()) {
+		printf("Please read the EEPROM ('read_eeprom') and/or initialize the EEPROM ('initialize') first.\n");
+		return 0;
+	}
+
+	if (!strcmp(cmd, "serial_number")) {
+		set_serial_number(argv[2]);
+		return 0;
+	} else if (!strcmp(cmd, "manuf_test_status")) {
+		set_manuf_test_status(argv[2]);
+		return 0;
+	} else if (!strcmp(cmd, "mac_address")) {
+		set_mac_address(argv[2]);
+		return 0;
+	} else if (!strcmp(cmd, "pcb_revision")) {
+		set_pcb_revision(argv[2]);
+		return 0;
+	} else if (!strcmp(cmd, "bom_variant")) {
+		set_bom_variant(argv[2]);
+		return 0;
+	} else if (!strcmp(cmd, "bom_revision")) {
+		set_bom_revision(argv[2]);
+		return 0;
+	} else if (!strcmp(cmd, "product_id")) {
+		set_product_id(argv[2]);
+		return 0;
+	}
+
+	return cmd_usage(cmdtp);
+}
+
+/**
+ * mac_read_from_eeprom() - read the MAC address from EEPROM
+ *
+ * This function reads the MAC address from EEPROM and sets the
+ * appropriate environment variables for each one read.
+ *
+ * The environment variables are only set if they haven't been set already.
+ * This ensures that any user-saved variables are never overwritten.
+ *
+ * This function must be called after relocation.
+ */
+int mac_read_from_eeprom(void)
+{
+	u32 crc;
+	char board_serial[SERIAL_NUMBER_BYTES + 1] = { 0 };
+
+	puts("EEPROM: ");
+
+	if (read_eeprom()) {
+		printf("Read failed.\n");
+		return 0;
+	}
+
+	if (!is_match_magic()) {
+		printf("Invalid ID (%02x %02x %02x %02x)\n",
+		       e.magic[0], e.magic[1], e.magic[2], e.magic[3]);
+		dump_raw_eeprom();
+		return 0;
+	}
+
+	crc = calculate_crc32();
+	if (crc != e.crc) {
+		printf("CRC mismatch (%08x != %08x)\n", crc, e.crc);
+		dump_raw_eeprom();
+		return 0;
+	}
+
+	eth_env_set_enetaddr("ethaddr", e.mac_addr);
+
+	if (!env_get("serial#")) {
+		snprintf(board_serial, sizeof(board_serial), "%s", e.serial);
+		env_set("serial#", board_serial);
+	}
+
+	return 0;
+}