A real embedded system will need initialization before any other code is executed. You will create a minimal reset handler, putting all but one processor to sleep, and executing the application on just one processor.
Create a new file, startup_el3.s
, with the following contents:
.section BOOT,"ax" // Define an executable ELF section, BOOT
.global el3_entry
.type el3_entry, "function"
el3_entry:
MRS x0, MPIDR_EL1 // Read Affinity register
AND x0, x0, #0xFFFF // Mask off Aff0/Aff1 fields
CBZ x0, boot // Branch to boot if Aff0/Aff1 are zero (Core 0 of Cluster 0)
sleep: // Else put processor to sleep
WFI
B sleep
boot:
MSR CPTR_EL3, xzr // Clear all trap bits
// Branch to scatter loading and C library init code
.global __main
B __main
Build the startup code with:
armclang -c -g --target=aarch64-arm-none-eabi -march=armv8-a startup_el3.s
The compiler identifies .s
files as assembler source.
The
MPIDR_EL1
register provides a CPU identification mechanism. The Aff0
and Aff1
bitfields let us check which numbered processor in a cluster the code is running on. This startup code sends all but one processor to sleep.
Setting CPTR_EL3 to zero disables various instruction traps which allows the C library init code to proceed.
Modify the scatter file so that the startup code goes into the root region ROM_EXEC
. This must be located as the FIRST
section in the region, so that it is at exactly 0x0
, and so is executed when the processors start.
ROM_EXEC +0x0
{
startup_el3.o (BOOT, +FIRST)
* (InRoot$$Sections)
* (+RO)
}
Link the objects, specifying the symbol el3_entry
as the entry point.
armlink --scatter=scatter.txt hello.o startup_el3.o -o hello.axf --entry=el3_entry
The entry point is used by the linker to determine which code is necessary to keep. It is also used by debuggers to know where to start execution from.
You can now successfully execute on the FVP without the additional pctl.startup
parameter from before.
FVP_Base_AEMv8 -a hello.axf
A single “Hello World!” message is displayed.
Hello World!