U-Boot handling MAC address

There’s several ways for U-Boot to store the Ethernet MAC address in non-volatile memory set at production time. In this post, we’ll analyze the different approaches and point to solutions how to implement them.

MAC address

The media access control address MAC address is a unique identifier composed of six octets that network devices use to identify themselves on a network. These addresses are generally assigned at production time and saved to a ROM device or to a non-volatile memory device such as flash that is read at boot time. We’ll look at storing the MAC address in a flash device.

Storage

In general, the MAC address will be stored in an on-board flash device, using a digital interface such as SPI or I2C accessible to the embedded processor. Sometimes, this device uses a non-standard interface that is accessed using a custom interface, and this would make it more complicated for U-Boot to read it, requiring custom code. Even rarer and also more complicated, is using a custom PL interface to read the device, also requiring a custom solution. In order from simplest to hardest to implement thus:

  • U-boot environment partition.
  • I2C interface to supported flash device.
  • SPI interface to supported flash device.
  • SPI interface to unsupported flash device.
  • Custom PS interface to flash device.
  • Custom PL interface to flash device.

U-Boot

The bootloader has built-in support for storing the MAC address in its flash-based environment partition, if present. The U-Boot variable ethaddr is used to hold the Ethernet MAC address.

UBoot> printenv ethaddr
00:1E:02:03:04:05

This variable can be modified using the setenv command, but this is only a volatile value thus far and will be lost if power goes down.

UBoot> setenv ethaddr 01:02:03:04:05:06

To save this MAC address to U-Boot’s environment flash partition, the saveenv command is used. Note that all of U-Boot variables are saved to this partition.

UBoot> saveenv

It’s important to note that U-Boot will only write the ethaddr variable to the partition once. It will refuse to overwrite a previously written value. In this case, it’s best to format the U-Boot partition (i.e. using “run eraseenv” or some other way) and re-save it again.

You can tell when U-Boot is reading its environment partition when it prints “Loading environment from …”. When it doesn’t, it prints “Warning: bad CRC, using default environment” at boot time. You can force U-Boot to use the default environment instead, using the command “env default -a”.

Linux uses a couple of binaries that can read U-Boot’s environment partition and even set variables. The Petalinux (i.e. yocto) package u-boot-fw-utils contains the fw_printenv, fw_saveenv and fw_setenv binaries.

Fixing unknown saveenv command

In previous to 2020.1, the saveenv command was available by default and somehow this has changed. To re-enable the saveenv command we need to add these configuration settings in meta-user/recipes-bsp/u-boot/files/

CONFIG_ENV_IS_IN_SPI_FLASH=y
CONFIG_ENV_SIZE=0x40000
CONFIG_ENV_SECT_SIZE=0x20000
CONFIG_ENV_OFFSET=0x2600000

It should be noted that CONFIG_ENV_SIZE is not taken into account when saving the environment partition, as the variable script_size_f does not change from its default 0x80000 value. The file u-boot-xlnx/include/configs/xilinx_zynqmp.h must be patched to change this default, to match the QSPI bootenv partition size.

diff --git a/include/configs/xilinx_zynqmp.h b/include/configs/xilinx_zynqmp.h
index a3461214c9..3361474bcc 100644
--- a/include/configs/xilinx_zynqmp.h
+++ b/include/configs/xilinx_zynqmp.h
@@ -116,7 +116,7 @@
 	"kernel_addr_r=0x18000000\0" \
 	"scriptaddr=0x20000000\0" \
 	"ramdisk_addr_r=0x02100000\0" \
-	"script_size_f=0x80000\0" \
+	"script_size_f=0x40000\0" \

Reading from flash

U-Boot also has built-in access to I2C EEPROM/flash storage and can automatically fetch the six octets from a given offset at boot time. In the U-Boot configuration, the I2C_EEPROM setting enables a set of parameters to address the EEPROM device, such as I2C address, bus, size, etc. For Zynq/MP, the ZYNQ_GEM_I2C_MAC_OFFSET defines the memory offset where to read the octets from, using the built-in I2C controller.

QSPI flash

Besides reading its environment partition, U-Boot can also be patched to read its MAC address from an arbitrary address from QSPI flash. I got this inspiration from Digilent’s Genesys Github repo Read MAC in flash patch, and updated it for Petalinux 2020.1. Credit where due.

The change involves replacing the zynq_board_read_rom_ethaddr function in U-Boot’s source board/xilinx/common/board.c by a SPI version. This function reads the QSPI offset and returns the octets on the ethaddr argument. The offset is set by the newly patched configuration option CONFIG_ZYNQ_GEM_SPI_OFFSET.

int zynq_board_read_rom_ethaddr(unsigned char *ethaddr)
{
	int ret = -EINVAL;

#if defined(CONFIG_ZYNQ_GEM_SPI_MAC_OFFSET)
	struct spi_flash *flash;
	flash = spi_flash_probe(CONFIG_SF_DEFAULT_BUS,
				CONFIG_SF_DEFAULT_CS,
				CONFIG_SF_DEFAULT_SPEED,
				CONFIG_SF_DEFAULT_MODE);

	if (!flash) {
		printf("no flash\n");
		printf("SPI(bus:%u cs:%u) probe failed\n",
			CONFIG_SF_DEFAULT_BUS,
			CONFIG_SF_DEFAULT_CS);
		return 0;
	}

	if (spi_flash_read(flash, CONFIG_ZYNQ_GEM_SPI_MAC_OFFSET, 6, ethaddr))
		printf("%s:SPI MAC address read failed\n", __func__);
 
	printf("%s: SPI ethaddr: %02X:%02X:%02X:%02X:%02X:%02X\n", __func__, ethaddr[0], ethaddr[1], ethaddr[2], ethaddr[3], ethaddr[4], ethaddr[5]);

	if (flash)
		spi_flash_free(flash);

#endif

	return ret;
}

Then the file containing the setting may be called user_add-SPI-MAC-offset.cfg

CONFIG_ZYNQ_GEM_SPI_MAC_OFFSET=0x2000

And putting it all together, the patch and the configuration settings in meta-user/recipes-bsp/u-boot/u-boot-xlnx_%.bbappend

SRC_URI += "file://platform-top.h \
            file://devtool-fragment.cfg \
            file://user_add-SPI-MAC-offset.cfg \
            file://0001-Read-MAC-address-from-QSPI.patch \

Example U-Boot output:

Linux reading address passed from U-Boot:

It’s interesting to note that if U-Boot has a MAC address in its environment partition, and another redundant MAC address in a SPI offset, it will give priority to its environment value. It will throw a warning message in this case:

ZYNQ GEM: ff0c0000, mdio bus ff0c0000, phyaddr 0, interface rgmii-id
SF: Detected n25q512a with page size 512 Bytes, erase size 128 KiB, total 128 MiB
zynq_board_read_rom_ethaddr: SPI ethaddr: FF:FF:FF:FF:FF:FF

Warning: ethernet@ff0c0000 MAC addresses don't match:
Address in ROM is               ff:ff:ff:ff:ff:ff
Address in environment is       70:b3:d5:1a:70:06

The MAC handoff

Once U-Boot read the MAC address from any source and assigned it to its ethaddr variable, it will be handed off to the kernel. U-Boot will read the FIT image containing the kernel, the rootfs and the device tree, and will patch the device tree MAC value with its ethaddr variable just before executing the kernel. So as far the kernel is concerned, its device tree comes with the MAC address read from flash (or U-Boot environment), as if it was set in the Ethernet entry of the device tree. For example as:

&gem0 {
    local-mac-address = [00 0a 35 00 22 20];
    <...>
};

Conclusion

We analyzed several different approaches for storing a MAC address in non-volatile memory, and how to use U-Boot to retrieve it and pass it on to the kernel at boot time.

Using PS SPI to access flash

The ZynqMP Processing System (PS) counts with two embedded SPI controllers that can be used to access SPI Flash devices....

Previous
QSPI boot then loading from MMC

The flexibility of U-Boot distro boot feature is exercised in this post, where we configure the target to boot from...

Next