System control

This exercise looks at the instructions for accessing system and special registers. System and special registers control the operation of the processor, for example cache configuration. Typically, system and special registers are programmed in start-up code, before switching to a C environment.

In Data processing and flow control and Access memory a startup.s file was provided, containing a very minimal reset handler. In this exercise, you will implement this file yourself. If you have a problem, look at the startup.s that is provided for the other examples as a reference. 

Get started

Like with Access memory, a framework project is provided to get you started. Follow these steps:

  1. Import the 3_sys_regs project into the Arm data side.
  2. The imported project then appears in the Project Explorer pane, as you can see in this screenshot:

    Within the **sys_reg**

    • main.c
      A simple “hello world” C program.
    • startup.s
      This is the startup code that we will complete in the exercise.
    Implement the startup code
  3. Open startup.s.
  4. The framework code is shown here:

    .global start64
      .type start64, @function
      // Check which core is running
      // ----------------------------
      // Core should continue to execute
      // All other cores should be put into sleep (WFI)
      // Your code here
      // Disable trapping of CPTR_EL2 accesses or use of Adv.SIMD/FPU
      // -------------------------------------------------------------
      // Your code here
      // Install EL3 vector table
      // -------------------------
      // Your code here
      // The effect of changes to the system registers are
      // only guaranteed to be visible after a context
      // synchronization event.  See the Barriers guide
      // Branch to scatter loading and C library init code
      // -------------------------------------------------
      .global  __main
      B        __main

    This example runs in EL3. For this exercise, we do not consider what would be necessary to switch Exception levels. There are three pieces of functionality that we need to implement:

    • Detection of which core is being run on
      We will run the example on a multiprocessor model, but the example is not written to be multi-threaded and needs to just run on core 0 (affinity The startup code needs to check the ID of the core. If the ID of the core is not then software should put the core to sleep using the WFI instruction.
    • Clearing floating-point trap
      The floating-point traps are Unknown at reset, but the C compiler assumes that floating-point operations are available before the C library initialization code (__main) is called. The startup code needs to ensure that the traps on accesses to the FPU are cleared.
    • Install the EL3 vector table
      Unlike earlier versions of the Arm architecture, in AArch64 there is no default vector table location. Software must install a vector table before the first exceptions are generated. This example does not use exceptions itself, but it is good practice to install a simple vector table to capture unexpected exceptions.
    Floating point traps

    The comment in startup.s tells us that the register that controls the FPU traps is CPTR_EL3. Let’s start by looking at the register description.

    The register contains several trap controls:

    • TCPAC - Trap lower Exception level accesses to CPTR_EL2 and CPACR_EL1
    • TAM - Trap lower Exception level accesses to AMU
    • TTA - Trap accesses to trace registers
    • TFP - Trap EL3 use of FP registers

    If you read the description of each field, you will find that a value of 0 means do not trap. Therefore, in this case we want to set the register to 0.

  5. Write a sequence which will set CPTR_EL3 to 0.
  6. Detect which core the software is running on

    Each core has a unique affinity number, formatted as four 8-bit fields, as you can see here:


    The affinity of a core can be read from the MPIDR_EL1 register. Unlike Data processing and flow control and Access memory, this exercise uses a model that contains multiple cores. However, the software is only written to run on one core. This means that the startup code must check which core it is running on, and if it is not core, the code should put the core to sleep using a WFI.

  7. Implement code that reads MPIDR_EL1 and check the affinity value, putting the core to sleep if not
  8. Things to consider:

    • What is the format of the MPIDR_EL1 register?
    • How will you extract and compare the full affinity value?
    • What will happen if the secondary cores are unexpectedly woken from standby?
    Install a vector table

    The provided project includes a simple vector table at the end of startup.s. The format of the table, and how exceptions are handled more generally, is beyond the scope of these exercises. For more information, refer to Exception model.

    For this exercise, we need to write the address of the vector table into the Vector Table Base Address register (VBAR_EL3). To do this, we need to know the address of the vector table. Let’s look at the vector table in the project, which is shown in the following code:

      .global vector_table
    // ------------------------------------------------------------
    // Current EL with SP0
    // ------------------------------------------------------------
      .balign 128
      B        .                    //        Synchronous

    The label vector_table marks the start of the table. We need to write the address of this label to VBAR_EL3.

    There are two pseudo instructions which allow you to get the address of a label:

    • ADR Xd, <label>
    • LDR Xd, =<label>

    ADR only works for labels that are within the same compilation unit. LDR can also be used for imported global symbols.

  9. Complete the code to set the EL3 vector table location, using either of these instructions.
  10. Note: There are two similar operations for LDR:

    • LDR Xd, <label> Returns in Xd the value at <label>
    • LDR Xd, =<label> Returns in Xd the address of <label>
    Run the completed image

    When you have completed the function, you can test it using the Fixed Virtual Platform (FVP) models that are provided with Arm Development Studio.

  11. Right-click on the project and select Build Project to build the project.
  12. Like in Access memory, the Console tab shows the build messages. If the project builds successfully, the output will look like what you can see in the following screenshot:

  13. Check for any errors. If there are any, correct them and try to rebuild the project.
  14. Note: If you look at the link command that is being issued for the image, it includes “—entry=start64”. This tells the compiler to set the entry point of the image to the label start64, which is the beginning of the startup code. The entry point is the address that the PC will be set to when the image is loaded into the simulation.

    When you have successfully built your image, you can try it out. Follow these steps:

  15. Use the A64 – sys reg.launch script in the project to launch the model.
  16. The model used for this exercise contains a dual-core Cortex-A72 processor and a quad-core Cortex-A53 processor. The affinity values for these cores are:

    • Cortex-A72, core 0
    • Cortex-A72, core 1

    • Cortex-A53, core 0
    • Cortex-A53, core 1
    • Cortex-A53, core 2
    • Cortex-A53, core 3

    The debugger is configured to connect to the two Cortex-A72 cores, as you can see in the following screenshot:

    The different debugger panes, like Source view and Register view, can only show information for one core at a time. The Debug Control pane selects which core’s information is currently being displayed. In the preceding screenshot, you can see that core 0, ARM_Cortex-A72_0, is selected. If core 1 is selected, the Debug Control pane looks like what you can see in this screenshot:

    Using the Register pane, we can manually check the MPIR_EL1 (AArch64 -> System -> ID) value that is reported by each core:

    • ARM_Cortex-A72_0: 0x0000000080000000 -> affinity
    • ARM_Cortex-A72_1: 0x0000000080000001 -> affinity
  17. Step through the first few instructions of the image that is connected to core 0, to confirm that it is correctly checking the ID.
  18. Disconnect and re-launch the model, this time stepping through the image on core 1 to compare the results.
  19. When you are satisfied that your code is working, run the image. The output from the simulator is shown in the Target Console tab. The output for a successful run looks like this code:

    terminal_0: Listening for serial connection on port 5000
    terminal_1: Listening for serial connection on port 5001
    terminal_2: Listening for serial connection on port 5002
    terminal_3: Listening for serial connection on port 5003
    CADI server started listening to port 7000
    Info: FVP_Base_Cortex_A72x2_A53x4: CADI Debug Server started for ARM Models...
    CADI server is reported on port 7000
    Hello world

    The startup file for this example is basic, and a real image would perform more initialization. To explore this topic, see the Bare Metal Boot guide.

Previous Next