December 11, 2025

EL3 registers used by the GICv3 driver

We can start with SRE_EL3 since this is the first thing that tripped us up. We find this code in gicv3_cpuif_enable() in gicv3_main.c
	/* Disable the legacy interrupt bypass */
    icc_sre_el3 = ICC_SRE_DIB_BIT | ICC_SRE_DFB_BIT;

    /*
     * Enable system register access for EL3 and allow lower exception
     * levels to configure the same for themselves. If the legacy mode is
     * not supported, the SRE bit is RAO/WI
     */
    icc_sre_el3 |= (ICC_SRE_EN_BIT | ICC_SRE_SRE_BIT);
    write_icc_sre_el3(read_icc_sre_el3() | icc_sre_el3);

    scr_el3 = read_scr_el3();
Note the routine read_icc_sre_el3() You can look high and low for this and you won't find it. Ctags cannot find it, even in the original ATF sources.

Both the read and write accessor functions get magically generated in arch_helpers.h by this line of code:

DEFINE_RENAME_SYSREG_RW_FUNCS(icc_sre_el1, ICC_SRE_EL1)
DEFINE_RENAME_SYSREG_RW_FUNCS(icc_sre_el2, ICC_SRE_EL2)
DEFINE_RENAME_SYSREG_RW_FUNCS(icc_sre_el3, ICC_SRE_EL3)
I include the EL1 and EL2 versions also, just to be clear that they exist.
The macros are:
/* Define read & write function for renamed system register */
#define DEFINE_RENAME_SYSREG_RW_FUNCS(_name, _reg_name) \
    _DEFINE_SYSREG_READ_FUNC(_name, _reg_name)  \
    _DEFINE_SYSREG_WRITE_FUNC(_name, _reg_name)
	#define _DEFINE_SYSREG_READ_FUNC(_name, _reg_name)      \
static inline u_register_t read_ ## _name(void)         \
{                               \
    u_register_t v;                     \
    __asm__ volatile ("mrs %0, " #_reg_name : "=r" (v));    \
    return v;                       \
}

#define _DEFINE_SYSREG_READ_FUNC_NV(_name, _reg_name)       \
static inline u_register_t read_ ## _name(void)         \
{                               \
    u_register_t v;                     \
    __asm__ ("mrs %0, " #_reg_name : "=r" (v));     \
    return v;                       \
}

Access to system registers as functions

Consider a line of code like this (taken from the above).
    write_icc_sre_el3(read_icc_sre_el3() | icc_sre_el3);
Setting up inline accessor functions like this allows clean compact and readable code to be written. I could review places where I have used inline assembly to access system registers and revise my code to use this methodology.

What is the SRE register?

This is a GIC register, accessed as a system register.
It is the "Interrupt Controller System Register Enable register" It has 4 bits, as per this in gicv3.h:
#define ICC_SRE_EN_BIT      BIT_32(3)
#define ICC_SRE_DIB_BIT     BIT_32(2)
#define ICC_SRE_DFB_BIT     BIT_32(1)
#define ICC_SRE_SRE_BIT     BIT_32(0)
The EN bit allows lower EL levels to access this register.
The DIB and DFB bits disable bypass for IRQ and FIQ.
The SRE bit enables system register access to GIC registers.

Note that the code above sets all of these bits. And given that the code shown actually runs at EL3 as part of BL31, this is the state we inherit things in.

The EL2 and EL1 flavors of this have the same bits, except that EL1 neither has nor needs the EN bit.

What is ICC?

As in "read_icc_sre_el2()". Apparently ICC just stands for "interrupt controller", though just what the second C stands for is confusing. Any register with the prefix "ICC" is a system register that is part of the interrupt controller. I think they were just hell-bent on using 3 letter acronyms for everything.

Other el3 specific system registers

Here is the list of things from gicv3_main.c that are "tagged" with el3 names:
IS_IN_EL3()
sre_el3
scr_el3
igrpen1_el3
sctlr_el3
The first is easy, and obvious just by name IS_IN_EL3() -- it returns a boolean value to indicate whether or not we are running in EL3 and is used in assertions. It is used by assert(), it is in arch_helpers.h and boils down to this:
#define IS_IN_EL(x) \
    (GET_EL(read_CurrentEl()) == MODE_EL##x)

#define IS_IN_EL1() IS_IN_EL(1)
#define IS_IN_EL2() IS_IN_EL(2)
#define IS_IN_EL3() IS_IN_EL(3)

SCR_el3

This is the "Secure Configuration Register". It is a general CPU register, not a GIC register. There is no SCR_el2, which sort of makes sense. EL3 runs in secure mode and decides whether lower levels will be secure or not. There is an HCR_el2 register which might be of interest, though I think not.

Code in gicv3_cpuif_enable() manipulates a bit in the SCR register to transition to NS (non-secure) state in order to write to Non secure ICC_SRE_EL1 and ICC_SRE_EL2 registers. then it switches back to secure state. It also writes to the secure ICC_SRE_EL1 register. It writes to these registers to set the SRE bit, so that the switch is made to use system registers to access the GIC.

We won't need to do any of this as we will inherit this setup which will be done by BL31.

I note though in the code in gicv3_cpuif_enable() that it writes to certain el1 specific registers. No doubt the expectation is that things need to be set up for EL1. However the _el1 specific registers that are written are the secure mode versions. In particular "Group0" interrupts are enabled for EL1 -- for secure mode.

IGRPEN1_el3

The bit IGRPEN1_EL3_ENABLE_G1S_BIT gets set (as mentioned just above) in gicv3_cpuif_enable() to enable Group1 secure interrupts.

Both the IGRPEN1_EL3_ENABLE_G1NS_BIT and the IGRPEN1_EL3_ENABLE_G1S_BIT get cleared in gicv3_cpuif_disable(), which I don't particularly care about as I don't forsee disabling the interrupt controller at any time.

page 19 of the GICv3 manual talks about the concept of interrupt grouping.

We have Group0 and Group1. Group0 is handled by the highest active EL. Group 1 is split into secure and non-secure. Group1 secure can be handled at secure EL1 or EL2. Group1 non-secure will be handled at EL2 if a system is using virtualization, or at EL1 if not.

We will have to see how all this sorts out. I expect my code to run in EL2 non-secure. I might be wiser to drop from EL2 to EL1.

SCTLR_el3

This is not a GIC register, but a central ARM cpu register. It has bits to control caches, MMU, and so forth. There indeed are EL2 and EL1 counterparts.

It is used in one line of code, in gicv3_rdistif_probe() as follows:

assert((read_sctlr_el3() & SCTLR_C_BIT) != 0U);
This is called from gicv3_base.c (which I have not even copied to my collection). (See drivers/arm/gic/v3/gicv3_base.c) It is called from gic_pcpu_init() in that file.

This is called from bl31/bl31_main.c where we see:

unsigned int core_pos = plat_my_core_pos();

#if USE_GIC_DRIVER
    /*
     * Initialize the GIC driver as well as per-cpu and global interfaces.
     * Platform has had an opportunity to initialise specifics.
     */
    gic_init(core_pos);
    gic_pcpu_init(core_pos);
    gic_cpuif_enable(core_pos);
#endif /* USE_GIC_DRIVER */

What is up with these frames?

The routine gicv3_rdistif_probe() in gicv3_main.c finds frames, but does nothing with them. Several routines in gicv3_helpers.c probe and discover frames. It seems odd that we could get a clean compile without including gicv3_base.c in the list of files. For now I am going to ignore this and see what trouble we get into.

What about caches and the MMU?

I read the sctlr registers and see:
Current EL = 2
Sctlr_el1 = 30D00800
Sctlr_el2 = 30C51835
MMU enabled
D Cache enabled
I Cache enabled
Notice that the value for el1 is different and not relevant to us, as we are running in EL2. Also plain "sctlr" is meaningless on aarch64, it is entirely an aarch32 thing. There is also an sctlr2_elx (in 3 flavors), which we are not yet worring about.

The above tests bit 2 (the C bit). There is also an I bit (bit 12) that enables the I cache. Everything I read tells me that enabling the D cache (via the C bit) also enables the L2 cache.

It will be interesting to inspect the MMU tables and see how they are set up for the huge address space this 64 bit processor has.


Have any comments? Questions? Drop me a line!

Tom's electronics pages / tom@mmto.org