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 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:
- 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 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:
- 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, such as one of the image_download_*() functions, 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.
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 isn't compressed, then the IPL can copy the image from the paged device directly to its final location in RAM. The startup code will compare the addresses and realize that the image doesn't need to be copied.
- If the image is compressed, then the IPL must copy the image 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.
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
).
