April 2, 2012

U-Boot conventions for i.MX6 (Nitrogen6X and SabreLite)

A little convention make things easy to use, but only if you know the convention.

In this blog post, we’ll explain three key environment variable conventions used on the Nitrogen6X (and Sabre Lite before it).

For the impatient:

  • Both of these devices ship with U-Boot configured to load and run a boot script named 6q_upgrade from the root of either SD card.
  • A pre-configured command upgradeu can be used to upgrade U-Boot in serial flash
  • Another pre-configured command clearenv will erase the environment variables in serial flash.

For those that want an explanation:

Both of these boards boot to SPI EEPROM (serial flash), and both U-Boot and its’ environment variables are stored there.

Convention for many modern ARM boards, including i.MX in recent years has been to boot directly to SD card, either by using unformatted areas of the disk or carefully placing special files in prescribed areas of a filesystem.

We jokingly refer to this as a game of “Hide the U-Boot”, and don’t want to play.

Instead, we added a serial flash chip and store the boot loader there. The flash device size is 2MB, which is plenty of space for the boot loader and environment, but not enough for a complete O/S, and we defer that to SD card, which brings us to the first environment variable:

bootcmd

We’ve pre-configured the bootcmd environment variable to load and launch a boot script from the root directory of either SD card slot by using the looping features of the Hush parser:

for disk in 0 1 ; do
	mmc dev ${disk} ;
        for fs in fat ext2 ; do
                ${fs}load mmc ${disk}:1 10008000 /6q_bootscript &&
                source 10008000 ;
        done ;
done

If you’re not familiar with U-Boot boot scripts, we wrote a backgrounder in this blog post.

To make compiling boot scripts a bit easier, we created a web-based tool to compile them for you. There’s also a bash snippet to make this easy from the command-line.

upgradeu

Once you’re running U-Boot from serial EEPROM, the question is how do you upgrade it?

The easy answer is that you ask U-Boot to do it:

MX6Q SABRELITE U-Boot > run upgradeu
...bunch of debug spew
## Executing script at 10008000
check U-Boot
Loading file "u-boot.bin" from mmc device 1:1 (xxb1)
179632 bytes read
...more debug spew...
SUCCESS
Total of 179632 bytes were the same
------- U-Boot versions match

If it’s not entirely obvious to you what this example illustrated, let’s dig a little deeper.

If you run print at the U-Boot prompt, you’ll see that upgradeu is defined to contain some U-Boot commands:

MX6Q SABRELITE U-Boot > print
bootdelay=3
...
upgradeu=for disk in 0 1 ; do mmc dev ${disk} ;for fs in fat ext2 ; do ${fs}load mmc ${disk}:1 10008000 /6q_upgrade && source 10008000 ; done ; ...
Environment size: 908/8188 bytes

That’s a pretty long command-line, but is essentially the same loop as we described in the bootcmd section of this post. It will try to read a script named 6q_upgrade from either SD card using either the FAT or ext2/3/4 filesystem and source it. The source command simply executes a boot script.

If you’ve followed along this far, you may have just realized that we haven’t really followed either the full boot path or the upgrade path because we’ve waved our hands over what the script does. We’ll address that in a later section.

clearenv

The final convention may be the most useful for the programmer. If a Sabre Lite or Nitrogen6X board spends much time on your desk, you’ll invariably set something in the U-Boot environment. Perhaps you’re bypassing all of these conventions and booting over NFS (the topic of another blog post). Maybe you’re booting to a SATA drive or a USB stick or tweaking some kernel parameters.

If so, you may ultimately want to put things back the way you found them before you hand the device off to someone else. If you know the layout of the serial EEPROM, you can simply erase the sector(s) containing the environment variables and go back to the compiled-in defaults.

I’m assuming you don’t (know or remember the flash layout) because I don’t and I helped come up with it. This is where the clearenv variable or command comes into play. Going back to the print example will tell you:

MX6Q SABRELITE U-Boot > print
bootdelay=3
...
clearenv=sf probe 1 && sf erase 0xc0000 0x2000 && echo restored environment to factory default
...
Environment size: 908/8188 bytes

Aha! The environment is stored at offset 0xc0000 (768k) and is 0x2000 (8192) bytes long!

And if you want to clear it, you can simply run run clearenv from the shell.

The gory details


Unless you’re as much of a U-Boot geek as I am, you’re probably wishing you’d stopped at the for the impatient section, but you may someday need the details, so I’m going to describe a handful of additional things that will help in the use and deployment of boot scripts for your use.

To begin with, I promised the details of the boot script itself, but I lied.

I can’t really give that to you because the things needed really depend on your environment. If you’ve spent time with various embedded Linux distributions, you’ll find that many of them require different sorts of kernel command-line parameters. Also, some distributions, like Android require the use of a RAM disk, but others don’t. These differences (additions) are best expressed in the actual userspace image, so we add them there.

Here’s an example of what we shipped on SabreLite boards with Ubuntu:

Ubuntu boot script

set bootargs $bootargs root=/dev/mmcblk0p1 rootwait fixrtc ;
ext2load mmc ${disk}:1 10800000 /boot/uImage && bootm 10800000 ;
echo "Error loading kernel image"

If you look closely, you’ll see that it’s adding a couple of things to the bootargs environment variable, then loading the kernel from /boot/uImage and booting it using bootm and that it’s printing an error message in case something fails. The bootm command will normally not return.

Also note that this script uses the disk variable that was defined in the loop from which it was called, so the boot script must reside on the same disk as the kernel.

Android boot script

Here’s another example of loading Android ICS on an i.MX6:

${fs}load mmc ${disk}:1 10800000 uimage &&
${fs}load mmc ${disk}:1 12800000 uramdisk.img &&
bootm 10800000 12800000 ;
echo "Error loading kernel image"

This one doesn’t add anything to the bootargs, and uses the && operators liberally, but the primary thing it does differently from the Ubuntu script is load a ram disk (uramdisk.img). It also uses the fs parameter from the calling loop in the same way the Ubuntu script used the disk variable.

A lot more could be done inside this boot script. The Android boot script could (and probably should) handle recovery mode. Both of them should probably loop in the case of errors, although we don’t yet have display support in i.MX6 U-Boot.

We should also add support for SATA into our default environment, but those are topics for another day and perhaps another blog post.

Upgrade script

The content of the 6q_upgrade script is a little more complex, but shouldn’t be too surprising. It tries to load a file named u-boot.bin from the same partition in which the script was found and compares it against the content of the serial EEPROM. If the two are different, it will wait for 10 seconds before (re)programming the flash to match the content of the SD card. It will also perform a verification pass before telling you that things are good:

echo "check U-Boot" ;
if ${fs}load mmc ${disk}:1 12000000 u-boot.bin ; then
      echo "read $filesize bytes from SD card" ;
      if sf probe 1 27000000 ; then
	   echo "probed SPI ROM" ;
           if sf read 0x12400000 0 $filesize ; then
               if cmp.b 0x12000000 0x12400000 $filesize ; then
                   echo "------- U-Boot versions match" ;
               else
                   echo "Need U-Boot upgrade" ;
                   echo "Program in 10 seconds" ;
                   for n in 9 8 7 6 5 4 3 2 1 0 ; do
                        echo $n ;
                        sleep 1 ;
                   done
		   echo "erasing" ;
                   sf erase 0 0x40000 ;
		   # two steps to prevent bricking
		   echo "programming" ;
                   sf write 0x12000000 0 $filesize ;
		   echo "verifying" ;
                   if sf read 0x12400000 0 $filesize ; then
                       if cmp.b 0x12000000 0x12400000 $filesize ; then
                           while echo "---- U-Boot upgraded. reset" ; do
				sleep 120
			   done
                       else
                           echo "Read verification error" ;
                       fi
                   else
                        echo "Error re-reading EEPROM" ;
                   fi
               fi
           else
               echo "Error reading boot loader from EEPROM" ;
           fi
      else
           echo "Error initializing EEPROM" ;
      fi ;
else
     echo "No U-Boot image found on SD card" ;
fi

Recap and best practices

I should probably move this section to the top of the document, but here’s some additional rationale for how and why we use the boot script scheme.

Two-file U-Boot upgrades

To build an SD card to upgrade U-Boot, you can copy just two files (6q_upgrade and u-boot.bin)to a fresh, out-of-the-box SD card (which tend to come formatted as FAT32), insert the card and run run upgradeu.

Single partition Ubuntu

To run Ubuntu, LTIB or Timesys single-partition images, you can create an SD card by simply formatting an ext2/3/4 partition and copy your filesystem image, boot script and kernel using cp. There’s no need to use dd or any other special formatting commands.

Windows CE, too

I guess I should say Windows Embedded 7. As the name implies, U-Boot is the Universal Boot Loader, and it can load and launch Windows Embedded using a boot script in a very similar manner as Linux. In this environment, you shouldn’t need to format the SD card at all: FAT32 should work.

And NFS

It may not be clear, but the use of a boot script on SD can also be useful in booting over NFS. This may sound like an oxymoron, but it’s often more convenient to write a network load script and copy it to an SD card than it is to hook up a serial port and tweak settings there.

This is especially true for those that infrequently use a serial port or when devices are enclosed in a manner that makes access to the serial port difficult.

Best practices

If U-Boot and environment variables are all stored on SD card, you don’t have much choice about what goes where, and environment variables are often used and changed to reflect the combination of distribution-related variables like whether or not a RAM disk is loaded and machine-specific variables like ethernet addresses or display settings.

When the two are separated as they are in SabreLite and Nitrogen6 boards, you should try to use environment variables for only those things which are tied to the board, such as the type of display. Any distribution-related bootargs should be added by the boot script.

If we haven’t yet convinced you that this is a good idea, I hope that at least you’ll understand what you’re seeing when you receive a board on your desk. At that point, I’ll refer you to the Linaro way, which allows you to override all of this and play “Hide the U-Boot” using a small shim in SPI ROM.

For what it’s worth, you can load and program that using the upgradeu command and the 6q_upgrade script by naming the shim u-boot.bin.