Enabling U-Boot with SPL QSPI Boot on i.MX6ULL

2022-01-13

Introduction & Overview

The following are the notes of getting U-Boot with SPL to boot from QSPI Flash on i.MX6ULL platform.

SPL is a stripped-down version of the full U-Boot, typically being loaded into on-chip SRAM. It is responsible for initializing the system (power, SDRAM, storage, etc.), loading the full U-Boot into SDRAM, and then jumping to the full U-Boot. (There is also a falcon mode, where SPL directly loads the kernel and jumps to the kernel.) Typically one doesn't need to use SPL on i.MX6 platform. The boot ROM is quite capable and extensible, to a point you may not even need the U-Boot at all.

In my case, I am using a board that requires the PMIC to be initialized before initializing DRAM. This could be implemented by using the plugin binary on the i.MX6 platform (essentially a small piece of code that would run before loading the full image), however in that case I would be completely on my own to figure out IO multiplexing, I2C controller programming, etc. An easier way is to use the SPL to set that up: it's designed to do that, after all. On top of that, I want the whole thing to run from SPI Flash.

Overall, I want the following features:

I need the U-Boot + SPL to boot from the flash, but I also need to figure out how to set up the eFuse to let the processor try to boot from QSPI also write the U-Boot image into the empty flash. The plan would then be:

  1. Load SPL into the processor's internal SRAM via USB and run the SPL to initialize the PMIC and DDR
  2. Load the U-Boot into the DDR using SPL over USB and run the U-Boot
  3. In the U-Boot, program the eFUSE
  4. In the U-Boot, receive the U-Boot image over USB and write it into the flash.

The flash layout would look like this:

---- 0x00000 / Start ----
Empty (reserved for partition table)
---- 0x00400 / 1KB ----
QuadSPI Configuration Parameters Table, to be loaded by BROM
---- 0x01000 / 4KB ----
U-Boot SPL in imx image format, to be loaded by BROM
---- 0x12000 / 72KB ----
U-Boot in leagcy uImage format, to be loaded by SPL
---- 0xA0000 / 640KB ----
U-Boot environment variables
---- 0xA2000 / 648KB ----

Let's go over these steps.

SPL over USB

When there is no valid image in the flash, the processor enters the recovery mode, allowing download images over USB and boot. Freescale/NXP provided a tool called uuu for this.

Typically, if one has a U-Boot without SPL, then everything needed is to load the U-Boot into the processor using uuu and done. But what if one tries to do the same with U-Boot with SPL?

U-Boot SPL 2020.04-49498-g8a36c01ac6-dirty
SPL: Unsupported Boot Device!
SPL: failed to boot from all boot devices
### ERROR ### Please RESET the board ###

This is because in this case, only the SPL is loaded into the processor. The U-Boot is nowhere to be found so SPL couldn't do anything. This makes sense: before SPL is executed, the uuu couldn't do much as the DDR RAM hasn't been initialized yet. Once SPL is up, the processor is no longer in the recovery mode, so uuu couldn't load the full U-Boot.

I will discuss the solution later, but at this point, I will need to modify the SPL first to initialize the PMIC before continuing. Otherwise, it won't boot after all.

I2C R/W in SPL

This part is also a bit tricky: U-Boot switched to a new set of driver interface called DM (device model). The switch-over from legacy driver to DM driver happened roughly in the 2014-2016 time frame. While the full U-Boot now fully uses the DM, the SPL may or may not use DM. I have tried to enable DM for SPL, however, I encountered some issues with device tree for SPL. I am also running out of internal RAM space for SPL, so I will leave CONFIG_SPL_DM off for now. But in this section I am still going to show how to do I2C access using both legacy driver and DM driver:

    int ret;
#ifdef CONFIG_DM_I2C
    struct udevice *bus, *dev;
#endif

#ifdef CONFIG_DM_I2C
    ret = uclass_get_device_by_seq(UCLASS_I2C, 0, &bus);
#else
    ret = i2c_set_bus_num(0);
#endif
    if (ret) {
        printf("Failed to get I2C bus: %d\n", ret);
        return;
    }

#ifdef CONFIG_DM_I2C
    ret = dm_i2c_probe(bus, DEVICE_I2C_ADDR, DM_I2C_CHIP_RD_ADDRESS |
            DM_I2C_CHIP_WR_ADDRESS, &dev);
#else
    ret = i2c_probe(DEVICE_I2C_ADDR);
#endif
    if (ret) {
        printf("Device didn't respond! %d\n", ret);
        return;
    }

#ifdef CONFIG_DM_I2C
    #define I2C_WRITE(addr) dm_i2c_write(dev, addr, &buf, 1)
    #define I2C_READ(addr) dm_i2c_read(dev, addr, &buf, 1)
#else
    #define I2C_WRITE(addr) i2c_write(0x34, addr, 1, &buf, 1)
    #define I2C_READ(addr) i2c_read(0x34, addr, 1, &buf, 1)
#endif

    uint8_t buf;
    // Set DCDC2 to 1.375V
    buf = 27;
    I2C_WRITE(DEVICE_I2C_ADDR);
    // Read back DCDC2 voltage
    I2C_READ(DEVICE_I2C_ADDR);
    printf("VCORE set to %d mV\n", buf * 25 + 700);

Adding the correct code into SPL and we are ready to continue to figure out how to boot the full U-Boot over USB.

SPL + Full U-Boot over USB

The solution is to instead let uuu talk to the SPL to get the image loaded over USB. This could be done by enabling the following build options for U-Boot SPL:

CONFIG_SPL_USB_HOST_SUPPORT=y
CONFIG_SPL_USB_GADGET=y
CONFIG_SPL_USB_SDP_SUPPORT=y

Note: SPL_USB_HOST_SUPPORT is needed to enable the USB driver.

Now the uuu script could be changed to like this to first load the SPL, then load the full U-Boot into RAM, finally jumps to it:

uuu_version 1.2.135

SDP: boot -f u-boot-with-spl.imx
SDPV: write -f u-boot-with-spl.imx
SDPV: jump -ivt
SDPV: done

This, however, doesn't work. When running one would see something like this:

U-Boot SPL 2020.04-49498-g8a36c01ac6-dirty (Jan 12 2022 - 19:21:01 -0500)                           
Trying to boot from USB SDP                             
SDP: initialize...                                      
SDP: handle requests...                                 
Downloading file of size 435083 to 0x00000000... done   
Jumping to header at 0x00000000
Header Tag is not an IMX image
Error in search header, failed to jump

resetting ..

Apparently, the 0x00000000 address is wrong, that's not even in the DRAM space, so that won't work. Now we have:

So I need to know the 1. the offset of the image, 2. the target address.

The offset could be found by manually inspecting the image file using a hex editor by looking for the 27 05 19 56 (0x56190527) magic in the file (optionally, use something like binwalk to find that for you). For i.MX6ULL, the image offset is 0x11000 into the u-boot-with-spl.imx file.

The load address could be found by directly looking into the u-boot-dtb.img which is generated during the build process:

file u-boot-dtb.img
u-boot-dtb.img: u-boot legacy uImage, U-Boot 2020.04-49498-g8a36c01ac6\270, Firmware/ARM, Firmware Image (Not compressed), 422731 bytes, Thu Jan 13 00:33:21 2022, Load Address: 0x87800000, Entry Point: 0x87800000, Header CRC: 0x3A84243E, Data CRC: 0x0A56A075

So it's expected to be loaded to 0x87800000. Note the image has a 64-byte header, so the address to load the image is 0x87800000-64=0x877FFFC0. Then combine the offset, to have the U-Boot image located at 0x877FFFC0, the file should be loaded into 0x877FFFC0-0x11000=0x877EEFC0. Modify the uuu script as follows:

uuu_version 1.2.135

SDP: boot -f u-boot-with-spl.imx
SDPV: write -f u-boot-with-spl.imx -addr 0x877eefc0
SDPV: jump -ivt -addr 0x877fffc0
SDPV: done

Note: uuu does provide a parameter called -skipspl for the write command. However, it uses the wrong offset for i.MX6 platform. I guess that's only supposed to be used with i.MX8.

Now run that script while the board is in recovery mode, it should boot into U-Boot.

Writing eFUSE

This is the easy part (but surprisingly I didn't find something to copy online, is this too easy so no one writes about it?). i.MX6 ULL RM gives a good description of what need to be done to configure the eFUSE to let it boot from QSPI flash. There aren't that many options, because most of the QSPI options (like DDR mode, speed, mode, etc.) are stored in the flash itself. In most cases one only need these 2 commands:

fuse prog -y 0 5 0x10
fuse prog -y 0 6 0x10
fuse prog -y 0 6 0x08

Note: eFUSE programming process is irreversible. If your board has DIP switches to configure the boot mode using GPIO override, use that instead. My board only supports QSPI boot so I didn't include the DIP switch and relied on eFUSE to configure the boot mode.

QuadSPI configuration parameters table

The QuadSPI configuration parameters table is a binary that the Boot ROM loads from 0x400. This is not part of the u-boot-with-spl.imx, and needs to be crafted individually. Freescale provided some examples in the L4.1.15_1.0.0_ga-mfg-tools. It's important to create the right configuration for the flash, otherwise, it might not boot. Generally one needs to pay attention to the following values:

The LUT is the command sequence that would be used to read values from SPI flash. If you've used other i.MX processors with QSPI or FlexSPI this might be familiar to you. In the i.MX6ULL RM Chapter 42 (QuadSPI), there are several example sequences (In the Serial Flash Devices section). I am going to show here how to convert such an example into binary so it could be used. Use the Table 42-32 Fast Read Quad Output sequence as an example:

Instruction Pad Operand Comment
CMD 0x0 0x0B Fast Read command = 0x0B
ADDR 0x0 0x18 24 Addr bits to be sent on one pad
DUMMY 0x0 0x08 8 Dummy cycles
READ 0x2 0x04 Read 32 Bits on 4 pads
JMP_ON_CS 0x0 0x00 Jump to instruction 0 (CMD)

The instruction opcode is a 6-bit value, and the pad is a 2-bit value, combined they form a byte. The operand is 1 byte. So one instruction is 2 bytes long. They should be saved in 16-bit word little-endian. The opcode table could be found in section 42.5.3.1 Programmable Sequence Engine.

For example, CMD is 1 (000001), combine with pad 0x0 (00), the first byte is 00000100, or 0x04. READ is 7 (000111), combined with pad 0x2 (10), the byte is 00011110, or 0x1E. So we got the full sequence:

0x04 0x0B
0x0A 0x18
0x0C 0x08
0x1E 0x04
0x24 0x00

The Boot ROM expects 16-bit word little-endian, however, the conversion script provided by Freescale expect 32-bit word little-endian in plain text, so it would be written as:

0a18040b
1e040c08
00002400

The conversion script and the config for W25Q32JV could be found here: https://gist.github.com/zephray/af8e0dea5a6e1c5a5bff6ba09338fcb3

Use ./qspi-header.sh qspi-nor-winbond-w25q32jv-config qspi-config.bin to convert it to the binary file to be written into flash.

Loading U-Boot from SPI Flash using SPL

For this, one need to enable the following configs:

CONFIG_SPL_SPI_FLASH_SUPPORT=y
CONFIG_SPL_SPI_SUPPORT=y
CONFIG_SPL_SPI_LOAD=y
CONFIG_SYS_SPI_U_BOOT_OFFS=0x12000

Then it should be able to load U-Boot from SPI Flash. Note if you are using U-Boot 2020.04 or newer like I do, the legacy support (non-DM) in fsl-qspi driver has been removed. So either migrate SPL to use DM or use the fsl-qspi driver from U-Boot 2018.3.

The number 0x12000 comes from the 0x11000 offset within the file, and the 0x1000 offset of the entire file.

Hint: Defining DEBUG in spl.c really helps a lot in terms of debugging.

U-Boot environment

This could be done with the following config:

CONFIG_ENV_SIZE=0x2000
CONFIG_ENV_OFFSET=0xA0000
CONFIG_ENV_SECT_SIZE=0x1000
CONFIG_ENV_IS_IN_SPI_FLASH=y

Putting everything together

Finally, I have a complete uuu script that would flash everything into the SPI flash (not including the eFUSE part):

uuu_version 1.2.135

SDP: boot -f u-boot-with-spl.imx
SDPV: write -f u-boot-with-spl.imx -addr 0x877eefc0
SDPV: jump -ivt -addr 0x877fffc0
FB[-t 60000]: ucmd sf erase 0 0x1000
FB: ucmd setenv fastboot_buffer ${loadaddr}
FB: download -f qspi-config.bin
FB: ucmd sf write ${loadaddr} 0x400 0x200
FB: download -f u-boot-with-spl.imx
FB[-t 30000]: ucmd sf update ${loadaddr} 0x1000 0x${fastboot_bytes}
FB: done

Conclusion

This may sound simple, and that's what I assumed initially: QSPI booting SPL-enabled U-Boot on a common platform like i.MX6 should be well supported, right? But the result is I didn't find any such examples. This combination seems to be quite rare. The information is scattered around in the reference manual, BSP user guides, BSP flashing scripts, U-Boot configuration for other platforms like i.MX8 etc. Eventually, I am able to get it to work, so that's a good ending for the story. I am putting down these things here, as a note for myself, and hopefully helpful to someone else trying to do similar things.