For the needs of mass production of an industrial device built by one of our clients, LABCSMART was asked to set up a means of manufacturing requiring the least possible human intervention, in order to automate the production chain. These products of two different types were based on i.MX6 and i.MX8 respectively. We therefore had to go around the means made available by both the community and by NXP in order to achieve the desired objective. Added to our expertise, it was possible to set up the mechanisms that we describe in this blog post.
The choice finally fell on NXP’s UUU (Universal Update Utility) tool as well as a series of patches developed internally at LABCSMART. In order to present the context, Yocto was the build system used, and naturally, the mainline versions of U-boot and the Linux kernel were, although these had very little (if any) impact on the implemented mechanism.
Table of Contents
- Integration of the manufacturing tool
- Tweaking U-boot configuration
- i.MX6 related tweaks
- Put everything together
- i.MX8 related tweaks
- Conclusion
Integration of the manufacturing tool
When an i.MX SoC has its boot strap pins set for USB Serial Download, it exposes a raw USB interface and waits for firmware over the SDP (Serial Download Protocol). UUU – Universal Update Utility, the successor to mfgtools — speaks this protocol from the host. It connects over USB OTG, uploads a first-stage binary, transitions the SoC into Fastboot mode, then drives the entire flashing sequence from a script file called uuu.auto.
The diagram below summarizes the communication path.
Figure 1 — Manufacturing flow: UUU drives the i.MX SoC over USB from Serial Download through Fastboot.Below is the Yocto recipe that builds UUU statically, so the resulting binary runs on any Linux host without additional dependencies:
SUMMARY = "A Daemon wait for NXP mfgtools host's command"
LICENSE = "GPLv2"
LIC_FILES_CHKSUM = "file://LICENSE;md5=38ec0c18112e9a92cffc4951661e85a5"
inherit cmake
inherit native deploy
DEPENDS = "libusb1-native zlib-native bzip2-native lzip-native openssl-native"
SRC_URI = " \
git://github.com/NXPmicro/mfgtools.git;protocol=https \
"
SRCREV = "e56424c825752cbc23a34fc685d9d958adc30e62"
S = "${WORKDIR}/git"
COMPATIBLE_HOST = "x86_64.*-linux"
BBCLASSEXTEND = "native nativesdk"
EXTRA_OECMAKE = "-DSTATIC=ON"
do_install() {
install -d ${bindir}
install -m 0755 ${S}/../build/uuu/uuu ${bindir}
}
do_deploy() {
install -m 0755 ${S}/../build/uuu/uuu ${DEPLOYDIR}
}
addtask deploy before do_build after do_compile
The EXTRA_OECMAKE = "-DSTATIC=ON" flag drives the static build. With UUU in place, we can now focus on the bootloader – the main actor on the device side.
Tweaking U-boot configuration
Although SPL and U-Boot proper share the same source tree, they produce separate binaries. In our scenario both are concatenated into a single u-boot-with-spl.imx file. Execution still proceeds in two stages: the Boot ROM loads the SPL from its fixed offset (0x400), the SPL initializes DRAM, then jumps to U-Boot proper at its own offset further in the same file. That layout is shown below – it directly determines the RAM addresses used in the UUU script.
Figure 2 — Binary layout of u-boot-with-spl.imx: NXP header, SPL at 0x400, padding, U-Boot proper at 0x11000.
These offsets are dictated by the i.MX6 Boot ROM, which always looks for the first-stage binary at 0x400 on the storage medium. With the layout understood, here are the SPL-related Kconfig options to enable:
CONFIG_SPL_SERIAL=y CONFIG_SPL_USB_HOST=y CONFIG_SPL_USB_GADGET=y CONFIG_SPL_USB_SDP_SUPPORT=y
These enable USB gadget, host, and SDP support in the SPL. CONFIG_SPL_USB_SDP_SUPPORT is particularly important: when set, the SPL detects at runtime whether the board booted in recovery mode and, if so, immediately enters an SDP loop waiting for host commands instead of jumping to U-Boot proper. This single option is what allows one binary to handle both normal and manufacturing boot.
For U-Boot proper, the following options enable Fastboot and the UUU-specific command extensions:
CONFIG_CMD_FASTBOOT=y CONFIG_USB_FUNCTION_FASTBOOT=y CONFIG_FASTBOOT_BUF_ADDR=0x12c00000 CONFIG_FASTBOOT_FLASH=y CONFIG_FASTBOOT_UUU_SUPPORT=y CONFIG_FASTBOOT_FLASH_MMC_DEV=3 CONFIG_FASTBOOT_MMC_BOOT_SUPPORT=y CONFIG_FASTBOOT_MMC_BOOT1_NAME="foo_mmc0boot0" CONFIG_FASTBOOT_MMC_BOOT2_NAME="foo_mmc0boot1" CONFIG_FASTBOOT_MMC_USER_SUPPORT=y CONFIG_FASTBOOT_MMC_USER_NAME="foo_emmc" CONFIG_FASTBOOT_CMD_OEM_PARTCONF=y CONFIG_USB_GADGET=y CONFIG_USB_GADGET_MANUFACTURER="foo" CONFIG_USB_GADGET_VENDOR_NUM=0x0525 CONFIG_USB_GADGET_PRODUCT_NUM=0xa4a5 CONFIG_CI_UDC=y
Key options worth explaining: CONFIG_FASTBOOT_UUU_SUPPORT unlocks the UCmd and ACmd extensions that let UUU send and execute arbitrary U-Boot shell commands over Fastboot – this is what the FB: ucmd lines in the UUU script rely on. CONFIG_CI_UDC activates the ChipIdea USB device controller driver. When the SoC re-enumerates after SPL resets the gadget controller, the host sees the new VID/PID defined by CONFIG_USB_GADGET_VENDOR_NUM / CONFIG_USB_GADGET_PRODUCT_NUM — 0x0525/0xa4a5 is the default recognized by UUU for i.MX devices. The BOOT1, BOOT2 and USER name options define the partition identifiers referenced in the UUU script to target eMMC hardware boot partitions and user area respectively.
With configuration complete, we avoid maintaining two separate bootloader binaries. The same image handles both normal production boot and manufacturing mode by inspecting its own boot mode at runtime.
i.MX6 related tweaks
The runtime detection is implemented in board_late_init(). When U-Boot proper detects it was loaded over USB, it overrides bootcmd to run fastboot 0 immediately, bypassing the normal boot sequence. The diagram below shows the decision tree for a single bootloader binary handling both paths.
Figure 3: Single-binary boot decision: same image, two runtime paths.
The patch goes into the board-specific file at board/<vendor>/[<board_name>/]<board_name>.c:
---
board/solidrun/mx6cuboxi/mx6cuboxi.c | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/board/solidrun/mx6cuboxi/mx6cuboxi.c b/board/solidrun/mx6cuboxi/mx6cuboxi.c
index 07df6e7f15..26028f71e6 100644
--- a/board/solidrun/mx6cuboxi/mx6cuboxi.c
+++ b/board/solidrun/mx6cuboxi/mx6cuboxi.c
@@ -185,6 +185,16 @@ int board_late_init(void)
+#ifdef CONFIG_CMD_FASTBOOT
+ if (is_boot_from_usb()) {
+ printf("Boot from USB for uuu\n");
+ run_command("mmc dev 3", 0);
+ env_set("bootdelay", "0");
+ env_set("bootcmd", "fastboot 0");
+
+ }
+#endif /* CONFIG_CMD_FASTBOOT */
+
The is_boot_from_usb() function is already implemented in U-Boot for i.MX SoCs – no need to write it from scratch. Once USB boot is detected, bootdelay is zeroed so the autoboot countdown is skipped, and bootcmd is overridden to run fastboot 0 unconditionally.
With the detection in place, the UUU script needs two values: U-Boot’s load address and U-Boot proper’s offset within the concatenated binary. The load address is extracted from the Yocto build log:
$ grep -rn "u-boot-dtb.img" log.do_compile.17491:778: ./tools/mkimage -A arm -T firmware -C none -O u-boot -a 0x17800000 -e 0x17800000 -n "U-Boot 2022.01"" for <YOUR BOARD> board" -d u-boot.bin u-boot-dtb.img >/dev/null && cat /dev/null $
Or directly from the image header:
$ file u-boot-dtb.img u-boot-dtb.img: u-boot legacy uImage, U-Boot 2022.01 for foo_imx6 boa¸, Firmware/ARM, Firmware Image (Not compressed), 498284 bytes, Mon Jan 10 18:46:34 2022, Load Address: 0x17800000, Entry Point: 0x17800000, Header CRC: 0xCED54C75, Data CRC: 0x3EF2F095 $
The load address is 0x17800000. To find U-Boot proper’s offset inside u-boot-with-spl.imx, search for the U-Boot magic number defined in include/image.h:
#define IH_MAGIC 0x27051956 /* Image Magic Number */
In hexdump -C output this appears as 27 05 19 56. First locate it in u-boot.img to extract a unique surrounding pattern:
# hexdump -C u-boot.img | grep "27 05 19 56" 00000000 27 05 19 56 ac 19 b5 65 61 dc 7f 0a 00 08 20 e8 |'..V...ea..... .| 00003360 97 89 84 17 06 89 84 17 27 05 19 56 7d e6 84 17 |........'..V}...| 00004bb0 02 20 08 bd 01 20 fc e7 27 05 19 56 80 6c 00 38 |. ... ..'..V.l.8| 00004d90 d0 8b 85 17 55 19 85 17 3b 6c 84 17 27 05 19 56 |....U...;l..'..V| 00006b60 27 05 19 56 7a e6 84 17 8f e6 84 17 38 89 84 17 |'..Vz.......8...| 00006f70 30 e9 84 17 27 05 19 56 71 e8 84 17 83 e8 84 17 |0...'..Vq.......| 0000ed60 f6 9b 84 17 27 05 19 56 7d e6 84 17 92 e6 84 17 |....'..V}.......| #
The magic appears at offset 0x00000000 of u-boot.img with the unique sequence 27 05 19 56 ac 19 b5 65. Now use that exact pattern in the concatenated binary:
# hexdump -C u-boot-with-spl.imx | grep "27 05 19 56 ac 19 b5 65" 00011000 27 05 19 56 ac 19 b5 65 61 dc 7f 0a 00 08 20 e8 |'..V...ea..... .| #
U-Boot proper sits at offset 0x11000 — consistent with Figure 2. Since the image has a 64-byte header, the actual RAM load address is 0x17800000 - 64 = 0x177fffc0. Subtracting the file offset: 0x177fffc0 - 0x11000 = 0x177eefc0. This yields the two critical addresses in the UUU script:
SDPV: write -f u-boot-with-spl.imx -addr 0x177eefc0 SDPV: jump -addr 0x177fffc0
On physical storage, the i.MX6 Boot ROM expects the SPL at 0x400 (1 KB); U-Boot proper follows at 0x11000, which is 68 KB from the start — explaining the seek=1 / seek=69 convention used with dd. The complete UUU script for i.MX6 is:
uuu_version 1.4.193
SDP: boot -f u-boot-with-spl.imx
CFG: SDPU: -chip MX6D -vid 0x0525 -pid 0xb4a4
SDPV: write -f u-boot-with-spl.imx -addr 0x177eefc0
SDPV: jump -addr 0x177fffc0
FB: ucmd setenv fastboot_buffer 0x12c00000
FB: download -f u-boot-with-spl.imx
# select boot0 hw partition and flash it
FB: ucmd mmc dev 3 1
FB: ucmd mmc erase 0 0x4000
FB: ucmd mmc write ${fastboot_buffer} 0x2 0x${UBOOT_HEX_SIZE_IN_SECTORS}
# select boot2 hw partition and flash it
FB: ucmd mmc dev 3 2
FB: ucmd mmc erase 0 0x4000
FB: ucmd mmc write ${fastboot_buffer} 0x2 0x${UBOOT_HEX_SIZE_IN_SECTORS}
FB: ucmd mmc partconf 3 1 1 0
# Now flash the rootfs on user partition
FB: flash -raw2sparse foo_emmc mmc.img
FB: ucmd fuse prog -y 0 5 0x1860
FB: ucmd fuse prog -y 0 6 0x10
#FB: acmd reset
FB: done
SDPU: done
The eMMC layout after this script runs is shown below. U-Boot goes into both hardware boot partitions at the 1 KB offset the Boot ROM requires; the root filesystem fills the user area; fuse programming permanently commits the boot source.
Figure 4 — eMMC layout after manufacturing: U-Boot in both boot partitions at 1 KB, rootfs in user area, fuse locks the boot source.
A few notes on key script commands. Writing U-Boot at sector offset 0x2 (1 KB) rather than using FB: flash is deliberate: the standard Fastboot flash command always writes from offset 0, but the Boot ROM requires U-Boot at 1 KB. mmc partconf 3 1 1 0 marks boot0 as the active hardware boot partition. The fuse values 0x1860 / 0x10 are board-specific — they configure the SoC to boot from usdhc4 where the eMMC is connected, and must be verified against actual hardware before use.
Put everything together
A Yocto class ties everything together: it generates the UUU script with the correct binary size substituted in, copies all artifacts, and packages them into a self-contained zip that the production operator runs directly — no host-side installation required.
MFG_DIR = "${MACHINE}-manufacturing-${DATETIME}"
MFG_DIR_LINK = "${MACHINE}-manufacturing"
BUNDLE_NAME = "${MACHINE}-manufacturing-bundle"
MFG_DIR[vardepsexclude] = "DATETIME"
do_generate_uuu_configuration() {
UBOOT_HEX_SIZE_IN_SECTORS=$(printf "%X\n" $(stat -L -c %b ${DEPLOY_DIR_IMAGE}/${UBOOT_BINARY}))
cat > uuu.auto<< EOF
uuu_version 1.4.193
[...]
FB: ucmd mmc write \${fastboot_buffer} 0x2 0x${UBOOT_HEX_SIZE_IN_SECTORS}
# select boot2 hw partition and flash it
[...]
FB: ucmd mmc write \${fastboot_buffer} 0x2 0x${UBOOT_HEX_SIZE_IN_SECTORS}
FB: ucmd mmc partconf 3 1 1 0
[...]
#FB: acmd reset
FB: done
SDPU: done
EOF
}
do_manufacturing_bundle() {
cd ${DEPLOY_DIR_IMAGE}
install -m 755 -d ${MFG_DIR}
install -m 755 -d ${MFG_DIR}/${BUNDLE_NAME}
install -m 644 ${PN}-${MACHINE}.wic ${MFG_DIR}/${BUNDLE_NAME}/mmc.img
install -m 644 ${UBOOT_BINARY} ${MFG_DIR}/${BUNDLE_NAME}
do_generate_uuu_configuration
install -m 644 uuu.auto ${MFG_DIR}/${BUNDLE_NAME}
install -m 755 ${STAGING_BINDIR_NATIVE}/uuu ${MFG_DIR}
# let's create the bundle zip
cd ${MFG_DIR} && zip -r -j ${BUNDLE_NAME}.zip . && cd - && rm -rf ${MFG_DIR}/${BUNDLE_NAME}
ln -snf ${MFG_DIR} ${MFG_DIR_LINK}
}
addtask do_manufacturing_bundle before do_build after do_image_complete
IMAGE_FSTYPES += "wic"
EXTRA_IMAGEDEPENDS += " \
virtual/bootloader \
uuu-native \
zip-native \
"
do_manufacturing_bundle[depends] += "uuu-native:do_deploy"
do_manufacturing_bundle[depends] += "${PN}:do_image_wic"
do_generate_uuu_configuration uses stat -c %b to get binary size in 512-byte blocks and formats it as hex with printf "%X" — because U-Boot’s mmc write only accepts a hex sector count. To trigger a manufacturing run, boot the board in recovery mode and execute:
sudo ./uuu <foo>_manufacturing_bundle.zip
UUU detects the archive format automatically and loads uuu.auto from it. On first run, watch the serial output. Before the SPL patch described below, the output is:
U-Boot SPL 2022.01 (Jan 10 2022 - 18:46:34 +0000) DDR3: calibration done Trying to boot from MMC1 Card did not respond to voltage select! : -110 spl: mmc init failed with error: -95 SPL: failed to boot from all boot devices ### ERROR ### Please RESET the board ###
The Boot ROM loaded and executed the SPL successfully, but spl_boot_device() returned BOOT_DEVICE_MMC1 — the SD card interface — instead of the eMMC on SDHC2. The fix is a one-line patch to arch/arm/mach-imx/spl.c:
---
arch/arm/mach-imx/spl.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/arch/arm/mach-imx/spl.c b/arch/arm/mach-imx/spl.c
index 427b7f7859..050166f6c5 100644
--- a/arch/arm/mach-imx/spl.c
+++ b/arch/arm/mach-imx/spl.c
@@ -98,7 +98,7 @@ u32 spl_boot_device(void)
/* MMC/eMMC: 8.5.3 */
case IMX6_BMODE_MMC:
case IMX6_BMODE_EMMC:
- return BOOT_DEVICE_MMC1;
+ return BOOT_DEVICE_MMC2;
/* NAND Flash: 8.5.2, Table 8-10 */
case IMX6_BMODE_NAND_MIN ... IMX6_BMODE_NAND_MAX:
return BOOT_DEVICE_NAND;
--
2.17.1
After rebuilding and re-running the manufacturing bundle, the serial output confirms success:
U-Boot SPL 2022.01 (Jan 10 2022 - 18:46:34 +0000) DDR3: calibration done Trying to boot from MMC2 [...] U-Boot 2022.01 (Jan 10 2022 - 18:46:34 +0000) CPU: Freescale i.MX6SOLO rev1.2 at 792 MHz Reset cause: POR Model: Foo Board bootloader - all versions Board: Foo i.MX6 DRAM: 512 MiB MMC: FSL_SDHC: 2, FSL_SDHC: 3 Loading Environment from MMC... OK In: serial Out: serial Err: serial Net: eth0: ethernet@2188000 Hit any key to stop autoboot: 0
MMC2 is now selected and U-Boot proper loads cleanly. The 3-second countdown confirms the board is ready for production use.
i.MX8 related tweaks
The i.MX8QXP variant stores the bootloader in SPI NOR flash rather than eMMC boot partitions. The UUU script therefore uses SDPS: (the i.MX8 protocol variant) instead of SDP:, and replaces the eMMC boot-partition writes with SPI NOR erase/write sequences. The bootloader Kconfig configuration stays identical to i.MX6.
uuu_version 1.0.1
# Send a bootloader image with "fastboot" compatible u-boot to run in RAM
SDPS: boot -f flash_ddr_uboot.bin
# Program the SPI-NOR Flash Chip
FB: ucmd setenv fastboot_buffer ${loadaddr}
FB: download -f flash_spinor_all.bin
FB: ucmd sf probe
FB[-t 40000]: ucmd sf erase 0 +${fastboot_bytes}
FB[-t 20000]: ucmd sf write ${fastboot_buffer} 0 ${fastboot_bytes}
FB[-t 40000]: ucmd sf erase 0x00400000 +${fastboot_bytes}
FB[-t 20000]: ucmd sf write ${fastboot_buffer} 0x00400000 ${fastboot_bytes}
# Program the eMMC Flash Chip
FB: ucmd setenv fastboot_dev mmc
FB: ucmd setenv mmcdev ${emmc_dev}
# Use mmc device 0 partition 0
# Partition 0 is "User Area"
FB: ucmd mmc dev ${emmc_dev} 0
# Single image flash
FB: flash -raw2sparse all mmc.img
FB: ucmd mmc partconf ${emmc_dev} 1 7 0
FB: done
#===============================================================================
The first notable difference is FB: ucmd setenv fastboot_buffer ${loadaddr}: this overrides the buffer address to pick up ${loadaddr} from the U-Boot environment rather than the hardcoded 0x12C00000 set in the i.MX6 configuration – the correct i.MX8 value is 0x42800000. The SPI NOR is written twice at different offsets as a redundant copy. Since the board always boots from SPI NOR, no eMMC boot-partition writes or fuse programming are needed. The do_manufacturing_bundle() implementation must be adapted to populate ${MFG_DIR}/${BUNDLE_NAME} with the i.MX8-specific files before the zip is created.
Conclusion
In this blog post we have learnt how to implement the software part of the factory flashing on an embedded Linux system with U-boot as the bootloader, designed with two NXP chips which are the i.MX6 and i.MX8. We have seen how to configure and how to tweak the bootloader. We could have also used the UMS (USB Mass Storage gadget) for this, but it would have been necessary to manually switch between the eMMC hardware partitions. The mechanism described in this post can easily be adapted to other SoCs (from different vendors), provided that you have the host tools to make it automatic.
And you — what method do you use? Post in comments.


