ARM IPLs

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 that it can be used on another ARM board.

QNX develops IPLs for the ARM platforms it supports and provides the source code as well as the 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 the 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 an _start routine. The second part is written in C.

Since an IPL for an ARM board 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 initialization. These tasks may include configuring the chip selects and/or the PCI controller, the clocks, and the memory controller; setting Power Management IC (PMIC (Power Management IC) voltage rails via I2C; setting some GPIO pins to control reset pins; and configuring a timer to accurately measure delays.

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

Depending on what the board firmware and the assembly portion of the IPL look after, the main() function may have to complete the initialization tasks the assembly portion of the IPL didn't complete. These initializatins can include anything from setting clocks to initializing the debug output. For example:

int main(int argc, char **argv, char **envv)
{
    /*
    * Initialise 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 fucntions to look after:

Get the OS image

After it completes required initializations, the main.c 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 rear-view 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).

Select the bootable image — image_scan()

If the OS image is on a linearly-mapped device, 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, copy the image into RAM before scanning it for a valid OS image.

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 memory for valid OS image signatures (see Image validation in this chapter). For example, with the OS in memory on a linearly-mapped device, the following would return its start address:

/*
* 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 in RAM of the newest bootable image.

The image_scan() function doesn't have a specific recovery strategy it uses if it doesn't find a valid image in the memory it is instructed to scan. It returns either the address of a valid image, or -1 if it fails to find a valid image.

Implementing a recovery mechanism is up to the code in the main() function. Possible strategies include:

Patch 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 patch its startup_vaddr member with the address to which the IFS has been copied into RAM.

The startup code is responsible for copying the image filesystem 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, it is the IPL (i.e., main()) that 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.

Jump 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 (the address stored in the startup header's startup_vaddr member). At this point, the IPL's work is done, and the OS startup code takes over (see Startup Programs).