6. Access Levels and T bit

1. Access Level Switching

As we saw in Section 3, the Cortex-M4 supports two access levels: Privileged and Unprivileged. The access level is determined by the CONTROL register. The CONTROL register is a special register that is only accessible in privileged mode. It has the following bit fields:

control_resgistger

For now, we only care about bit 0, the nPRIV bit. This bit determines the access level of the processor. If the bit is set to 0, the processor is in privileged mode. If the bit is set to 1, the processor is in unprivileged mode. We can access this bit using the MRS (move from special register) and MSR (move to special register) instructions.

void change_access_level_unpriv(void)
{
    //read
    __asm volatile ("MRS R0,CONTROL");
    //modify
    __asm volatile ("ORR R0,R0,#0x01");
    //write
    __asm volatile ("MSR CONTROL,R0");
}

and in the main function. We can call this function to switch to unprivileged mode.

int main(void)
{
    change_access_level_unpriv();
    generate_interrupt();
    for(;;);
}

Where the generate_interrupt() function is defined as:

void generate_interrupt()
{
    uint32_t *pSTIR  = (uint32_t*)0xE000EF00;
    uint32_t *pISER0 = (uint32_t*)0xE000E100;

    //enable IRQ3 interrupt
    *pISER0 |= ( 1 << 3);

    //generate an interrupt from software for IRQ3
    *pSTIR = (3 & 0x1FF);
}

This function triggers an IRQ3 interrupt manually. It uses the memory-mapped registers pSTIR and pISER0 for software trigger and interrupt set-enable, respectively.

  • *pISER0 |= ( 1 << 3); enables the IRQ3 interrupt.
  • *pSTIR = (3 & 0x1FF); triggers IRQ3.

However, since we are already in unprivileged mode, the processor will throw a UsageFault exception. This is because the CONTROL register is only accessible in privileged mode. The processor will enter the UsageFault handler, which is defined as:

void HardFault_Handler(void)
{
    printf("Hard fault detected\n");
    while(1);
}

Build and run the code in debug mode, we can see this fault in the Fault Analyzer window:

fault_analyzer


2. Importance of T bit of EPSR

Introduction

The Execution Program Status Register (EPSR) is a special register in ARM Cortex-M processors that contains various status bits that reflect the state of the processor. The EPSR is not directly accessible, but its individual fields can be accessed or modified through other means like specific instructions or special function calls.

One important bit in the EPSR is the T-bit (Thumb state bit). The T-bit indicates whether the processor is in ARM or Thumb state.

  • If the T-bit is set (1), the processor executes in Thumb state.
  • If it is cleared (0), the processor is in ARM state.

In the context of Cortex-M series processors, they only support the Thumb instruction set, so the T-bit should always be set to 1 for proper operation. If somehow this bit is cleared, and an attempt is made to fetch an ARM instruction, it will result in an exception like a fault condition.

The T-bit typically gets set correctly when branching to an address, provided the least significant bit of the target address is set. For example, using a branch instruction to an odd-numbered address like 0x1001 would set the T-bit, allowing the CPU to correctly interpret the following instructions as Thumb instructions.

Relationship to Program Counter (PC)

The PC holds the address of the next instruction to be executed. In Cortex-M series, when the program loads a value into the PC for a branch or jump, the least significant bit (Bit[0]) of that value determines the state of the T-bit.

Here's what it means in practical terms:

  • If the loaded address has its least significant bit set (1), it indicates that the target code is Thumb code. The processor will automatically set the T-bit, ensuring that the processor will interpret subsequent instructions as Thumb instructions.

  • If, hypothetically, the loaded address has the least significant bit cleared (0), the processor would clear the T-bit. However, since Cortex-M processors only support Thumb mode, doing so would lead to undefined behavior or a fault condition.

  • The compiler usually takes care of setting the least significant bit (LSB) to 1 when generating addresses for function calls, jumps, or interrupt vectors in Thumb mode. This is particularly true for Cortex-M processors that only support Thumb mode. The setting of the LSB happens at compile time. The compiler generates machine code with the proper settings to ensure the processor will be in the right state to execute the next instruction.

    For example, consider a function starting at address 0x2000. In the function table or when performing a branch, this would be recorded as 0x2001 to set the T-bit. However, the function's machine code would still start at 0x2000 and occupy subsequent addresses (0x2002, 0x2004, etc.) without any "gaps."

  • So we don't really need to worry about the T-bit in most cases. However, in rare cases where we need to manually set the PC, we need to ensure that the LSB is set to 1 to set the T-bit.

Example

Here is a simple example to demonstrate the importance of the T-bit.

typedef void (*fun_ptr)(void);

void do_nothing(void) {};

/* This function executes in THREAD MODE+ PRIV ACCESS LEVEL of the processor */
int main(void)
{
    fun_ptr f1 = do_nothing;

    /* storing some address in the function pointer variable */
    fun_ptr f2 = (void*)0x080001e8;

    f1();   // This is okay. The compiler will adjust the LSB to be 1.
    f2();   // This will lead to a fault because the LSB is 0, and ARM Cortex-M does not support that.

    for(;;);
}

void HardFault_Handler(void)
{
    while(1);
}

Here, although the function do_nothing lives at the address 08000204 according to the disassembly:

08000204 <do_nothing>:
typedef void (*fun_ptr)(void);

void do_nothing(void) {};
 8000204:   b480        push    {r7}
 8000206:   af00        add r7, sp, #0
 8000208:   bf00        nop
 800020a:   46bd        mov sp, r7
 800020c:   bc80        pop {r7}
 800020e:   4770        bx  lr

When it is stored in the function pointer f1, the compiler automatically sets the LSB to 1 to set the T-bit. This can be seen in the register window, where the value of f1 is stored in R3:

f1_register

However, when we manually set the function pointer f2 to 0x080001e8, the T-bit is not set. When we call f2(), the processor will try to execute the instruction at 0x080001e8 as an ARM instruction, which will lead to a fault.