Enable SPL with HAB for i.MX6

A recent task assigned to me was to support both 1GiB and 2GiB RAM for a i.MX6 product in a single bootloader and it ended up by enabling SPL with pre-existing HAB feature for it.

Two modes of progam image

There’re two modes in a program image supported by i.MX6: DCD and plugin.

The DCD (Device Configuration Data) is a configuration information contained in the program image (external to the ROM) that the ROM interprets to configure various peripherals on the chip. For example, mx6q_4x_mt41j128.cfg is used on mx6sabresd board for 1GiB RAM.

On the other hand, plugin achieves the same functionality as DCD by using a limited set of assembly language. Plugin source code must be in plugin.S of board directory as required by Makefile. For example, plugin.S used on the same board.

The target product was using DCD and also had a redundant plugin.S in its board directory. So can I use any of them to support two different DDR configurations in one single bootloader?

Neither DCD nor plugin

The program image supported by i.MX6 must have a Image Vector Table (IVT) with the following format:

As you can see, DCD is optional, which means you can have zero or one DCD in a program image, but not two. And DCD doesn’t support any branching logic to support two sets of DDR configurations in one DCD file. So DCD won’t work.

On the other hand, plugin may work but it’s written in assembly language. Not sure if the assembly language used in plugin supports branching, and after all, come on, who wants to write assembly in 2023?!

SPL for rescue

SPL (Secondary Program Loader) is the common practice if you want to support different configurations of peripherals (including DDR) in one bootloader.

If SPL is enabled, the bootloader will be split into two parts: SPL and U-Boot proper. By setting the text base of SPL to On-Chip RAM (OCRAM), the ROM will load SPL into OCRAM and run it. SPL initializes the peripherals then loads U-Boot proper into DDR.

Enable SPL

First thing first, if you’re using Yocto to build U-Boot like me, please remember set UBOOT_MAKE_TARGET to "" (empty) or "all". Otherwise, Yocto will build only U-Boot proper by default because the default make target is u-boot.imx for i.MX6, even if you have already enabled CONFIG_SPL in U-Boot! I’ve wasted so much time to figure this out, and if my tip saves you even 10 minutes, please do let me know!

Besides, also set UBOOT_SUFFIX and SPL_BINARY:

UBOOT_MAKE_TARGET = "all"
UBOOT_SUFFIX = "img"
SPL_BINARY = "SPL"

When I first run make menuconfig in the root directory of U-Boot, I couldn’t enable CONFIG_SPL because it depends on CONFIG_SUPPORT_SPL, so I had to add select SUPPORT_SPL to Kconfig file of the target board.

diff --git a/arch/arm/mach-imx/mx6/Kconfig b/arch/arm/mach-imx/mx6/Kconfig
index 710d89dc74..b392580fe1 100644
--- a/arch/arm/mach-imx/mx6/Kconfig
+++ b/arch/arm/mach-imx/mx6/Kconfig
@@ -445,6 +445,7 @@ config TARGET_BOARD_IMX6
 	bool "target-board-imx6"
 	select TARGET_MX6SABRESD_COMMON
 	depends on MX6Q
+	select SUPPORT_SPL
 
 config TARGET_MX6QPSABRESD
 	bool "mx6qpsabresd"

On top of that, I enabled a bunch of SPL related config items shown below:

CONFIG_SYS_MALLOC_F_LEN=0x4000
CONFIG_SPL_GPIO=y
CONFIG_SPL_LIBCOMMON_SUPPORT=y
CONFIG_SPL_LIBGENERIC_SUPPORT=y
CONFIG_SPL_TEXT_BASE=0x00908000
CONFIG_SPL_MMC=y
CONFIG_SPL_SERIAL=y
CONFIG_SPL_DRIVERS_MISC=y
CONFIG_SPL=y
CONFIG_SPL_PAYLOAD="u-boot-ivt.img"
CONFIG_SPL_BOARD_INIT=y
CONFIG_SPL_SYS_MALLOC_SIMPLE=y
CONFIG_SPL_SEPARATE_BSS=y
# CONFIG_SPL_BANNER_PRINT is not set
CONFIG_SPL_USB_HOST=y
CONFIG_SPL_USB_GADGET=y
CONFIG_SPL_USB_SDP_SUPPORT=y
CONFIG_SPL_WATCHDOG=y
CONFIG_CRC32_VERIFY=y
CONFIG_SPL_OF_CONTROL=y
CONFIG_SDP_LOADADDR=0x177fffc0

Some config items are explained below:

  • Increase CONFIG_SYS_MALLOC_F_LEN to 0x4000 (16 KiB), otherwise “alloc space exhausted” while booting.
  • CONFIG_SPL_TEXT_BASE is set to 0x00908000, which is, as mentioned above, a location in OCRAM (0x00900000 - 0x0097FFFF).
  • CONFIG_SPL_BANNER_PRINT is disabled because the “U-Boot SPL” banner will have some garbage data at the beginning. My guess was the serial port hasn’t been stablized when printing.
  • CONFIG_SPL_USB_HOST, CONFIG_SPL_USB_GADGET and CONFIG_SPL_USB_SDP_SUPPORT are enabled to support serial download in SPL via uuu. See more details below as these changes are not sufficient.
  • CONFIG_CRC32_VERIFY is enabled also for uuu purpose to verify the checksum of downloaded and written bootloader on eMMC.
  • CONFIG_SDP_LOADADDR is set to the value of CONFIG_SYS_TEXT_BASE (0x17800000) minus the length of legacy format image header (0x40 = 64 bytes), so that SDP will download and jump to U-Boot proper at the exact same location as normal boot.

Some other config iteams not shown in the change above:

  • CONFIG_SPL_LOAD_FIT is disabled because we’re using legacy U-Boot image instead of a FIT U-Boot image.
  • Disable CONFIG_SPL_DM to make the SPL binary smaller as well as the source code simpler.
  • The default value of CONFIG_SPL_PAD_TO and CONFIG_SPL_SIZE_LIMIT is 0x11000 (68 KiB). If the result SPL binary along with generated CSF (more details below) is larger than 68 KiB, you can increase the value of these two config items. An alternative is to enable CONFIG_MX6_OCRAM_256KB if you’re using MX6Q/MX6D series of chips, which will set the values to 0x31000 (196 KiB).
  • Set IMX_CONFIG of the target board to arch/arm/mach-imx/spl_sd.cfg, which I think is a dummy DCD file to enable “HAB Blocks” in generated SPL.log file.

Also, the redundant plugin.S was removed to avoid confusion.

After all these changes, there should be SPL and u-boot-ivt.img generated in build directory, which will be used for signing later.

Adapt with different RAM size

Since I have a SPL-enabled bootloader, I can add more stuff in SPL to achieve my task.

Add a spl.c in board directory and only build it when CONFIG_SPL_BUILD is defined. In spl.c, besides usual initialization like the other boards do, the key point is DDR initialization.

To adapt with different RAM size, I have to add two sets of DDR configurations: one for 1GiB and the other for 2GiB (they actually share the same struct mx6dq_iomux_ddr_regs, struct mx6dq_iomux_grp_regs and struct mx6_mmdc_calibration but use different struct mx6_ddr3_cfg), then configure DDR as 2GiB first then call get_ram_size() to get the actual RAM size and re-configure DDR if necessary:

static void spl_dram_init(void)
{
	unsigned long ram_size;

	mx6dq_dram_iocfg(64, &mx6_ddr_ioregs, &mx6_grp_ioregs);
	mx6_dram_cfg(&ddr_sysinfo, &mx6_mmdc_calib, &mem_ddr_2gb);

	/*
	 * Get actual RAM size, so we can adjust DDR size for <2G
	 * memories
	 */
	ram_size = get_ram_size((void *)CONFIG_SYS_SDRAM_BASE, SZ_2G);
	switch (ram_size) {
	case SZ_1G:
		mx6_dram_cfg(&ddr_sysinfo, &mx6_mmdc_calib, &mem_ddr_1gb);
		printf("DRAM:  1 GiB\n");
		break;
	case SZ_2G:
	default:
		printf("DRAM:  2 GiB\n");
		break;
	}
}

Construct a CST-signed bootloader

The main reference of this section is i.MX6, i.MX7 U-Boot HABv4 Secure Boot guide for SPL targets. However, there are some errors in that guide as well as an important missing information.

  • In the diagram of signed SPL image layout, the payload should be u-boot-spl.bin instead of SPL itself.
  • Similarly, in the diagram of signed u-boot-ivt.img image layout, the payload should be u-boot.bin instead of u-boot.img.
  • In 1.4 Signing the images, SPL and u-boot-ivt.img are signed separately, and generated CSF files are appended to SPL and u-boot-ivt.img respectively, then saying “can be flashed into the boot media” without describing how to construct and flash the bootloader.

After searching around the source code, I figured out that this is where CONFIG_SPL_PAD_TO is used for. The signed SPL (containing SPL + CSF) should be padded to CONFIG_SPL_PAD_TO (0x11000 as mentioned earlier), so the signed U-Boot proper (containing u-boot-ivt.img + CSF) will be put at the offset of CONFIG_SPL_PAD_TO in the final bootloader.

The diagram below illustrates the constructed bootloader with separately signed SPL and u-boot-ivt.img:

+-----------------------------+
|                             |
|             SPL             |
|                             |
+-----------------------------+
|                             |
| Command Sequence File (CSF) |
|                             |
+-----------------------------+
|      Padding (optional)     |
+-----------------------------+ <-- CONFIG_SPL_PAD_TO
|                             |         (0x11000)
|                             |
|         u-boot-ivt.img      |
|                             |
|                             |
+-----------------------------+
|                             |
| Command Sequence File (CSF) |
|                             |
+-----------------------------+
|     Padding (optional)      |
+-----------------------------+

Another note I want to mention is, in the generated SPL.log, there is still a line of “DCD Blocks”, which I think it’s a dummy one because we don’t use DCD any more and don’t need to sign DCD blocks. The DCD field in IVT is actually 0.

Serial download via uuu

Before enabling SPL, the process of uuu script was use SDP protocol to download a special USB version of bootloader (whole image) to RAM and run it. From there (in U-Boot), use fastboot protocol to download a normal bootloader and write to eMMC.

With SPL enabled, no need to construct a special USB version of bootloader. The process will be use SDP protocol to only download the SPL part of bootloader to RAM and run it. From there (SPL), use SDP protocol again to download U-Boot part to RAM and run it. Then from U-Boot, same as before, use fastboot protocol to download the whole bootloader and write to eMMC.

As you can see, the supposed process of uuu script before fastboot is the same as a normal boot except the images are from USB instead of eMMC.

The supposed uuu script is shown as below:

# @_image0             | bootloader including SPL and U-Boot proper

# Download SPL to RAM and run
SDP: boot -f _image0

SDPV: delay 1000

# Download U-Boot to RAM and run
# Offset should match CONFIG_SPL_PAD_TO in U-Boot source code
# 0x11000 is the default value defined in include/configs/imx6_spl.h
SDPV: write -f _image0 -skipspl -offset 0x11000
SDPV: jump

# Select target eMMC partition
FB: ucmd setenv fastboot_dev mmc
FB: ucmd setenv mmcdev ${emmc_dev}

# Flash the 1st bootloader
FB: ucmd mmc dev ${emmc_dev} 1
FB: ucmd setenv fastboot_buffer ${loadaddr}
# write 1st bootloader to eMMC offset 2 blocks = 0x400 bytes
FB: write -f _image0 -seek 0x400
FB: crc -f _image0 -seek 0x400

# Flash the 2nd bootloader
# write 2nd bootloader to eMMC offset 1MiB = 0x100000 bytes
FB: write -f _image0 -seek 0x100000
FB: crc -f _image0 -seek 0x100000

FB: Done

Please note that I use FB:CRC command to verify the integrity of bootloader after writing to eMMC. That’s why I enable CONFIG_CRC32_VERIFY.

With this uuu script, I figured out there were some issues in the source code of uuu and my pull requests for fixing them had been merged.

The first pull request is to fix a segfault when boot data is zero in SDP. mkimage set boot data of IVT to zero for firmware_ivt image type, which is the image type for U-Boot proper.

The second pull request is to round up the block number when the file size isn’t divisible by block size in fastboot, so that the last portion of bootlodaer can be correctly written to eMMC in FB:WRITE command and verified in FB:CRC command.

Last but definitely not the least, the VID/PID of USB gadget used in SPL had been changed in upstream commit e330a88a8a, so I had to change them in the defconfig of the target board as well:

diff --git a/configs/defconfig b/configs/defconfig
index f3af995f27..fed771a4fe 100644
--- a/configs/defconfig
+++ b/configs/defconfig
@@ -84,8 +84,8 @@ CONFIG_USB_HOST_ETHER=y
 CONFIG_USB_ETHER_ASIX=y
 CONFIG_USB_GADGET=y
 CONFIG_USB_GADGET_MANUFACTURER="FSL"
-CONFIG_USB_GADGET_VENDOR_NUM=0x0525
-CONFIG_USB_GADGET_PRODUCT_NUM=0xa4a5
+CONFIG_USB_GADGET_VENDOR_NUM=0x1fc9
+CONFIG_USB_GADGET_PRODUCT_NUM=0x0152
 CONFIG_CI_UDC=y
 CONFIG_SDP_LOADADDR=0x177fffc0
 CONFIG_DM_VIDEO=y

Have fun!

comments powered by Disqus