ARM IPLs

QNX SDP8.0Building Embedded SystemsConfigurationDeveloper

IPLs for ARM are board-specific. This means that a great deal of work is usually required to adapt an IPL written for one ARM board so it can be used on another ARM board.

QNX develops IPLs for the ARM platforms it supports and provides the source code and binaries in its BSPs for these platforms.

About ARM IPLs

Most ARM boards have a small segment of ROM code built directly into their SoC. Unlike with the initialization firmware on x86 boards, you can't modify or update this ROM code—all changes must be made in software. Typically, this ROM code does only minimal hardware initialization before handing control to software (an IPL).

IPLs for ARM boards are in two parts. Both IPL parts are stored in the same location on the boot device. The first part is written in assembly, typically in a _start routine. The second part is written in C. Since an ARM IPL is board-specific, its source code files are usually included in the BSP. The make process used to create the BSP compiles the customized source code from the BSP directories.

Some boards include the U-Boot boot loader, which you can use instead of a QNX IPL, especially during the initial stages of a project when just getting a system booted and running is more important that optimizing boot times.

Initializations

The name of the first part of an ARM IPL usually begins with init. Written in assembly, it looks after hardware initializations. These tasks may include:
  • configuring the chip selects and/or the PCI controller, the clocks, and the memory controller
  • setting Power Management IC (PMIC) voltage rails via I2C
  • setting GPIO pins to control reset pins
  • configuring a timer to accurately measure delays

When it completes its initializations and the system DRAM is available, the assembly code sets up a stack. The stack allows the second part of the IPL (the main() function) to get, validate, and load the IFS.

Depending on what the board firmware and the assembly part of the IPL manage, the main() function may have to complete the initialization tasks that the assembly part didn't complete. These can include anything from setting clocks to initializing the debug output; for example:
int main(int argc, char **argv, char **envv)
{
    ...
    /* Initialize debugging output */
    select_debug(debug_devices, sizeof(debug_devices));

     /* Initialize Timer-related information */
    mx6sx_init_qtime();

    /* Initialize Clocks (must happen after timer is initialized) */
    mx6sx_init_clocks(); 
    ...
}

Preparing for startup

The second part of an ARM IPL is written in C. The source file for this part of the IPL is always called main.c and it contains the main() function. This function calls other functions to:
  • Perform initializations not completed in the first part of the IPL
  • Locate and prepare the bootable OS image (i.e., the IFS)
  • Hand control to the startup code in the OS image

Getting the OS image

After it completes required initializations, the main.c function gets the IFS and copies it into RAM. This step may involve a variety of scenarios, including:
  • Using eXecute In Place (XIP), if the OS image is on linearly mapped media and not compressed.
  • Copying the entire IFS into RAM, if the OS image is on non-linearly mapped media, it's compressed, or both.
  • Calling a function (e.g., an image_download_*() function) to download the OS image and copy it into RAM.
  • Making the copying of the IFS into RAM the responsibility of the startup code, and doing only the minimum needed to allow the startup code to run.
Note: Using the startup code to copy the IFS into RAM is particularly useful for systems that must complete specific tasks within very strict time limits. For example, an automotive system that needs to start communication with the CAN Bus or launch a rearview camera within n milliseconds. The IPL can copy only the minimum required for the startup, which then takes the shortest possible route to get the required software launched, while copying the full IFS into RAM in the background (see the Boot Optimization Guide).

Selecting the bootable image — image_scan()

If the OS image is on a linearly mapped device and not compressed, the IPL can use XIP to execute the startup code before copying the image into RAM. In all other cases, or if you don't want to use XIP, you need to copy the image into RAM before scanning it for OS image signatures.

When the OS image is in a location where it can execute (RAM, or linearly mapped storage for XIP), the main() function passes the image_scan() function a start address and an end address to search in memory for valid OS image signatures (see Image validation in this chapter). For instance, the following code returns the start address of an OS image on a linearly mapped device:
/* Image is located at 0x2840000 */
image = image_scan(0x2840000, 0x2841000);

If image_scan() finds one or more OS images within the specified address range, it returns the start address of the newest bootable image. If a valid image can't be found, image_scan() returns -1.

The function doesn't have a specific recovery strategy to use if it doesn't find a valid image. Implementing a recovery mechanism is up to the code in main(). Possible strategies include:
  • Using a primary IFS and secondary IFS. If the primary IFS fails, call image_scan() again, excluding the location of the primary IFS from the scan so the function chooses the secondary IFS.
  • Some boards may support keeping a secondary IFS on an alternative media device (e.g., a small SPI NOR chip), and can be configured to fall back to this secondary IFS if the primary one fails.

Patching the startup header with the startup address — image_setup()

After image_scan() has found and validated an OS image, the IPL main() function calls image_setup() to examine the startup header structure, then patches its startup_vaddr member with the address to which the IFS has been copied into RAM.

The startup code is responsible for copying the IFS to its final destination in RAM, but the startup code can't have knowledge of paged devices (e.g., serial, disk, parallel, network). With these devices, the main() function must copy the image to a location that's linearly accessible by the startup code:
  • If the image is not compressed, then the IPL can copy it from the paged device directly to its final location in RAM. The startup code compares the addresses and realizes that the image doesn't need to be copied to a temporary location.
  • If the image is compressed, then the IPL must copy it to a temporary location, from which the startup code can extract it to its final location in RAM.

When image_setup() returns, the startup code should be in RAM (where it can execute), and the address of the OS image should be in the startup_header structure's startup_vaddr member.

CAUTION:

When copying a compressed image into RAM, make sure you leave enough room for the entire extracted image between its final start address and the temporary location. If there is insuffcient room, you may extract the image onto itself, with unpredictable (and likely undesirable) results.

Remember that if you add or change components, the image size may change. Check your arithmetic.

Jumping to the startup entry point — image_start()

After the part of the OS image needed for the boot process has been copied into RAM, the main() function calls image_start(). This latter function jumps to the startup's entry point, which is the address stored in the startup header's startup_vaddr member. At this point, the IPL's work is done, and the startup code takes over.

Page updated: