ARM IPLs

Updated: April 19, 2023

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 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 looks after 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 on a board 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:

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 the Timer related information */
    mx6sx_init_qtime();

    /* Init 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:

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:

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 to 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:

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:

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:

If you are copying a compressed image into RAM, make sure you leave enough room for the entire extracted image between the image's 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 image_setup() has copied into RAM the part of the OS image needed to continue the boot process, the main() function calls image_start(), which 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 (see Startup Programs).