A close look at the anatomy of the U-Boot environment.
Das U-Boot is a popular bootloader for embedded systems. This wide adoption of U-Boot is hardly surprising given the number of architectures and platforms it supports. Additionally, U-Boot has a flexible compile-time configuration setup. You can select different features and drivers via config options and build a custom bootloader image for your platform. U-Boot's flexibility is extended at runtime as well. Using U-Boot environment variables, you can influence the program execution flow.
U-Boot comes with a CLI (command-line interpreter), which has basic scripting capabilities. This scripting ability combined with the U-Boot environment variables can be used to create some powerful booting scenarios. The ability to manipulate program behavior using environment variables is beneficial for both development and production setups alike. During development, people strive to test all possible paths for loading and booting images for their platforms. So, you may try to load a Linux kernel image from a local storage (Flash, SDcard, USB, eMMC and so on), or access it over the network (NFS, TFTP and so on).
U-Boot simply makes your life easier as a developer. You just need to tweak the scripts combining environment variables in a fruitful way. Production images also need some versatility. When a product's OS images need an upgrade, the bootloader must be configurable to fetch the images from different sources.
U-Boot has a number of system variables that you can modify to achieve your desired results. For example, on certain systems, initrd images loaded on top of DDR may not be accessible to the Linux kernel. To counter this, you can instruct U-Boot to load initrd at a lower DDR address. You can do this by setting the initrd_high environment variable.
Another common situation during development is the presence of different network configurations. On your home setup, you may be working on a static IP configuration using NFS. But, when you are out for a demo at a client location, you only have DHCP available with images kept on a TFTP server. U-Boot is highly configurable for such scenarios because it provides so many options. You can change the network configuration, modify the IP addresses of image servers and gateway servers with the help of environment variables. You could assign a console over serial port, or you could use netconsole or usbtty if you prefer.
A simple answer to that question would be “a collection of name=value pairs”. Here, “name” refers to the name of the environment variable to which you want to assign some “value”. This “value” could be of any type: string, hexadecimal, boolean and so forth. Whatever type the value is, it is converted into a string before being stored in a linearized environment data block. Each environment variable pair (“name=value”) would be stored as a null-terminated string. So, the collection of many environment variables is nothing but a null-separated list with a double-null terminator. Figure 1 illustrates how a list of strings is actually stored. The left-hand side is just a logical representation of environment variables, whereas the right-hand side shows that the variables have been flattened and written in a serialized form.
U-Boot has two types of persistent environments.
1) Default Environment (Compiled-In, Read-Only):
Every U-Boot binary has a default built-in environment of its own (Figure 2a). During compilation, a character array called default_environment is embedded into the U-Boot image. This character array stores the environment variables as a list of null-terminated strings with a double-null terminator. The contents of this array are populated conditionally based on the config options selected for your board. Environment variables that are commonly used can be enabled by defining the corresponding CONFIGs in your board's config file (include/configs/<YOUR_BOARD>.h). Figure 3 lists some commonly used options, which, once defined, would make their way into the default environment of your board.
Apart from the standard variables used across boards, you may want to add certain environment variables that are specific to your board or that just are convenient for you. You may, for instance, want to embed the revision number of the board into this environment. You could do that by defining all of these variables in a macro called CONFIG_EXTRA_ENV_SETTINGS in your board's config file:
#define CONFIG_EXTRA_ENV_SETTINGS \ "board=" XSTR(BOARD) "\0" \ "load_addr=" XSTR(CONFIG_SYS_LOAD_ADDR) "\0"
Remember that the default environment is “read-only”, as it is part of the U-Boot image itself. Vendors normally keep some essential system variables as part of this environment.
There are some good reasons to keep a default environment as part of the image:
Because it is read-only, you always have a default state to revert to.
During early bootup, a user-supplied environment (defined next) may be inaccessible or must not be used due to security concerns.
A user-supplied environment may be inaccessible due to a storage device malfunction or environment data corruption.
You shouldn't keep too much data in this default environment, as it directly adds to the weight of the binary. Keep only critical system variables in this environment.
2) User-Supplied Environment (Flashed in External Storage, Writable):
Typically, vendors flash an environment data image to external storage present on your board. The format of this pre-built environment is again the same—that is, a linearized list of strings, but there is a 4-byte CRC header prefixed to it. This CRC is computed over the environment data. Figure 2b illustrates such an environment blob with CRC data, followed by valid environment data and an invalid one after that. The total size of this environment data is fixed to CONFIG_ENV_SIZE during compilation. So, if your environment usage exceeds this size, you would need to recompile your U-Boot binary after increasing CONFIG_ENV_SIZE. If you do not increase the size, U-Boot will refuse to save the environment variables.
You may decide to keep this environment in external storage, but you must configure the board's config accordingly. U-Boot must know which storage method (and at what offset) will be used to hold the user environment. U-Boot provides a number of options to configure the location of the environment data. U-Boot has the infrastructure to access environment data stored in serial flashes, NVRAM, NAND, dataflash, MMC and even UBI volumes. See the U-Boot documentation for more information on how to use these CONFIG options. Since the default environment size has to be minimized, most of the environment variables are stored here. Certain storage technologies like raw NAND flashes are inherently unreliable. To combat such possibilities (including power failure), and for robustness in general, you also can configure a redundant user environment. You can configure the location and size of this duplicate environment data as well in your board's config.
Out of the two default environments (default and user), only the user is writable. So, whenever you modify a variable and issue a saveenv command, that variable ends up in the user environment.
When you do a saveenv, U-Boot does the following:
Sorts the list of current environment variables.
Converts them to a linearized list of strings.
Computes CRC over this data and burns the env back at its fixed location in storage.
U-Boot provides a utility named mkenvimage that can be used to generate an environment blob suitable to be flashed. mkenvimage needs at least two inputs to create the blob:
Environment variables in a text file (only one env “name=value” string on each line).
The size of the environment blob in bytes (remember, this must comply with the CONFIG_ENV_SIZE you have defined in your board's config).
For example, if my env data file is called my_env_data.txt, and the size of my desired env blob is 16384 (16 KiB), I would use the following command:
$./tools/mkenvimage -s 16384 -o env_blob my_env_data.txt
You can see the dump of the env blob using the od command:
$ od -t x1c env_blob 0000000 0d d2 49 96 62 61 75 64 72 61 74 65 3d 31 31 35 \r 322 I 226 b a u d r a t e = 1 1 5 0000020 32 30 30 00 62 6f 6f 74 64 65 6c 61 79 3d 31 30 2 0 0 \0 b o o t d e l a y = 1 0 0000040 00 6c 6f 61 64 5f 61 64 64 72 3d 30 78 34 30 30 \0 l o a d _ a d d r = 0 x 4 0 0 0000060 30 30 30 30 30 00 00 00 00 00 00 00 00 00 00 00 0 0 0 0 0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 0000100 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 * 0040000
This environment data blob must be flashed at the predefined offset in the storage device. You can use U-Boot, Linux or any other flasher to burn this blob.
During an early boot when U-Boot has not relocated to RAM, it uses the linearized form of environment data (as shown in Figure 1). But once U-Boot has relocated to RAM, this linearized form is no longer used. Instead, U-Boot imports all such env data stored from persistent storage into a RAM-resident hashtable. If the user-supplied environment is good (that is, the CRC is okay), it is imported from Flash to RAM. Otherwise, U-Boot imports the default compiled-in environment to this hashtable. Figure 4 shows how the user environment is imported in to the hashtable, whereas U-Boot along with its default environment relocates to top of the RAM. If the user environment is corrupt or inaccessible, U-Boot would import the default environment in to the hashtable.
The use of the RAM-resident data structure (hashtable) is important for various reasons:
It boosts performance, as you are manipulating variables in RAM and not in Flash.
You have to manipulate data only in RAM and need not access some slow Flash driver (and deal with the associated complexity).
It allows U-Boot to deploy type checks and access control attributes on different variables while still keeping the persistent storage form simple (a linear list).
Once the environment has relocated to RAM (into the hashtable), all commands operating on environment variables will be working only on this hashtable. U-Boot does not touch the environment variables stored in the persistent storage at all (unless it needs to save the env).
Each environment variable entry inside the hashtable is represented by a data structure called a struct entry (Figure 5). Apart from the members key and data, which correspond to “name” and “value” in the linearized representation of data, you have members called callback and flags. flags is of integer type and is used to implement type check and access control. callbacks is the callback function associated with this environment variable. If defined, this callback handler would be invoked whenever any operation (like add, delete or modify) is performed on this environment variable.
At times you may want to do your own runtime configurations. You may want to react (accept, reject or produce some side effects) to the changes done to some environment variable. For such use cases, U-Boot provides a mechanism of deploying callback handlers. You can associate a callback function with an environment variable. As a first step, you have to register a callback handler with U-Boot. This function would be called whenever you do any modifications to the environment variable. Figure 6 shows sample code to register a callback with U-Boot. You can place such handler code in your board-specific file. The macro U_BOOT_ENV_CALLBACK registers the callback function on_change_foo with the handler named foo_h. Your handler is now registered with U-Boot with the name “foo”.
Now, you need to establish a link to this registered handler with an environment variable. Take a look at the struct entry in Figure 6. You can see that there is a member named callback (a function pointer) for each environment variable. U-Boot would invoke this handler before committing the modified environment variable to the hashtable. You can make this association of callback handler with the environment variable either at compile time or at runtime. For compile-time association, you need to define the config option #define CONFIG_ENV_CALLBACK_LIST_DEFAULT foo:foo_h in your board's config file.
You also can do runtime association as depicted in Figure 7. Here I have created a new environment variable .callbacks, which is a standard U-Boot system variable to make such associations. I have deployed a handler named foo_h for the environment variable foo. Once this registration is done, whenever you do some modification to variable foo, the function on_change_foo() would be invoked. You now can deliver your reaction to different types of actions (env_op_create, env_op_overwrite or env_op_delete).
U-Boot already deploys similar handlers for managing console changes, splash images and so on.
There are certain environment variables that you want to use but do not want to do casual modifications. For example, say you have an environment variable serial#; you definitely want this variable to be read-only, and you want automatic rejection of any attempt to change it. Another such example is the MAC address of the device. Again, you want to keep that variable as read-only or, at worst, write-once. U-Boot supports different access modifiers: any, read-only, write-once and change-default (Figure 5). The U-Boot hashtable representation of environment variables has a member int flags (Figure 5). The member flags is used to keep a bitmap specifying the access permission associated with variable. So, whenever any modification attempt is done on variable, it must comply with the access permission; otherwise, U-Boot will reject the changes.
Another problem faced by users is the basic sanity check of environment variable type. Since the linearized form of environment keeps only strings, U-Boot needs to make sure that it can do some kind of type check before assigning a value to a variable. To address this issue, U-Boot makes use of some predefined types, such as “string”, “decimal”, “hexadecimal”, “boolean”, “IP address” and “MAC address”. There are corresponding codes for these type modifiers: “s”, “d”, “x”, “b”, “i” and “m”. Again, U-Boot stores this type information of variables in flags as a bitmap (Figure 5).
You can associate “type” and “access control” to a variable either at compile time or at runtime. For compile-time association, you need to define a config #define CONFIG_ENV_FLAGS_LIST_DEFAULT foo:sr in your board file. For runtime association, you can define an environment variable .flags (Figure 8). Here, I am associating an environment variable foo with type s (meaning the value is a string) and access control r (meaning it is read-only). Once deployed, if you try to modify the variable foo, U-Boot will reject your request. Also, if a value is not of specified “type”, your update to the environment variable will fail.
Environment variables like MAC address make use of type m. This will make U-Boot do a sanity check on the value entered by the user to confirm whether the value is indeed a valid MAC address.
U-Boot environment variables can be added, modified or deleted from Linux as well. U-Boot provides a set of utilities called fw_printenv and fw_setenv to do the job. First, you need to compile these utilities for Linux. Figure 9 shows the compilation steps for the utility. Here, I am cross-compiling it for the ARM platform. It is a multi-call binary. So, you need to make a symlink named fw_setenv to the binary fw_printenv.
To modify the environment, you first need to boot in to Linux on the target board. Next, you need to create a file called /etc/fw_env.config. This file contains all the information needed to specify the location of the environment data blob. Figure 10 shows my configuration file. I kept my environment in an SPI Flash, which appeared as /dev/mtd0 to my kernel. My environment blob was configured at an offset of 0x80000 from the beginning of Flash and had a size of 0x40000. The size of each sector of my Flash is 0x10000. This is all the information I needed to provide in order for the environment manipulation utilities to work.
As soon as I keyed in the command fw_printenv, I could see the variables that I saved in the U-Boot user environment appearing on my console.
You also can set the environment variables using fw_setenv. As shown in Figure 10, I make use of a text file (list.txt) containing the variables I want to set. The format is simple. The first whitespace after a name acts as a delimiter, and the characters until the end of line thereafter are considered the value for the key.
You can verify that the variables have been set by executing fw_printenv. These variables now would be visible from U-Boot as well.
Sometimes after a lot of environment variable changes, you can corrupt the state. To restore sanity and get the original values of the default environment, you can use the env command:
env default [-f] var [...]
The above command would forcibly reset the specified variables to a value from the default environment.
To restore the complete environment from the default, invoke the following command:
env default -a
The env command is very powerful; you can use it import/export environment data from/to RAM.
The U-Boot environment can act as a very useful runtime configuration tool. When combined with scripting, it can make the arduous task of development and testing boot scenarios much simpler and more fun to do.