3. Access Levels and Operation Modes of the Processor

1. Operation Modes

In ARM Cortex-M processors, the CPU can operate in different privilege levels and states, commonly referred to as "modes." The two main modes are Thread mode and Handler mode:

Thread Mode

  • Purpose: This is the normal programming mode where your application code runs. It's the starting mode after reset.
  • Privilege Levels: Thread mode can operate in either privileged or unprivileged state. In privileged state, the code has unrestricted access to all CPU resources. In unprivileged state, some restrictions apply, like limited access to some system control registers.
  • Typical Uses: Running application tasks, RTOS tasks.

Handler Mode

  • Purpose: Entered when an exception occurs. This includes interrupts, SysTick, fault exceptions, etc.
  • Privilege Levels: Handler mode always runs in privileged state.
  • Stack: Uses a dedicated stack, called the Main Stack, for exception handling. Some processors also allow you to use the Process Stack.
  • Typical Uses: Executing ISR (Interrupt Service Routines), fault handling routines, and system-level tasks.

Switching Between Modes

  • Going from Thread to Handler mode: Triggered by events like interrupts or exceptions.
  • Going from Handler to Thread mode: Typically when an exception return instruction is executed, or when you manually clear the appropriate bit in the control register.

Interrupt Program Status Register (IPSR)

IPSR tell us which exception is currently being serviced. The 8 LSBs indicates the exception number. If the processor is in Thread mode, the IPSR value is 0. For more information, refer to the ARM Cortex-M4 Generic User Guide:

ipsr

You can use the register window of the STM32CubeIDE debugger to view the IPSR value.

In general software development, the concept of modes is not as explicit or critical as it is in embedded systems. Modes in Cortex-M processors are designed to provide a level of security and determinism that is often crucial in real-time and embedded applications.


2. Access Levels

In ARM Cortex-M processors like the Cortex-M4, access levels control the privilege level at which code executes. The Cortex-M4 supports two access levels:

  1. Privileged (PAL): When running in Privileged mode, the code has unrestricted access to all CPU resources. This includes system control space, i.e., it can change control registers, enable/disable interrupts, and so on. Typically, an operating system kernel or a real-time operating system (RTOS) runs in Privileged mode to have full control over the system resources.

  2. Non-Privileged/Unprivileged (NPAL): Code running in Unprivileged mode is restricted in some of the operations it can perform. For example, it can't change the control registers that could affect the overall system behavior, like enabling or disabling interrupts. This mode is generally used for user applications, ensuring they can't accidentally or intentionally compromise the system's stability.

When the processor is in thread mode, the access level is determined by the CONTROL register. It is not possible to go from Unprivileged to Privileged mode directly. Instead, the processor must first enter Handler mode, then switch to Privileged mode. This mechanism helps in isolating critical system functions from user-level tasks, thus enhancing the security and stability of the system.

We will get into more details about access levels in Seciton 6.


3. Core Registers

Navigate to the ARM Cortex-M4 Generic User Guide, Section 2.1.3, to view the registers of the Cortex-M4 core:

core_registers

As you can see, the Cortex-M4 core has a large number of registers. We'll focus on the following registers:

General-Purpose Registers: R0-R12

These are your workhorses for most computational tasks. You'll use these registers to hold data temporarily during calculations, function calls, and other operations. If you're coming from a high-level language, think of these as your temporary variables.

Stack Pointer (SP): R13

This register keeps track of the top of the stack, a special region of memory where data is stored in a last-in, first-out (LIFO) manner. The stack is essential for function calls, storing return addresses, and local variables. Make sure you never overflow the stack; otherwise, you're in for a world of debugging pain!

When you call a function, the address to return to is stored here. It's like a bookmark that says, "once you're done with that function, come back here."

Program Counter (PC): R15

This register points to the next instruction to be executed. Typically, you don't mess with this directly, but it's crucial for understanding the flow of your program.

Special-Purpose Registers

Program Status Register (PSR)

This is an all-in-one register that includes APSR, IPSR, and EPSR. You usually interact with its parts rather than the whole thing.

Application Program Status Register (APSR)

Here you'll find status flags like Zero (Z), Carry (C), and Overflow (V) that are set or cleared by various operations. They're handy for conditional branches.

Interrupt Program Status Register (IPSR)

As shown above, if IPSR is 0, the processor is in Thread mode. Otherwise, it's in Handler mode, and this register tells you which interrupt service routine (ISR) is currently active. If you're debugging and want to know why you're in an ISR, check here.

Execution Program Status Register (EPSR)

This one's interesting because of the T bit, which determines the instruction set being used (ARM or Thumb). Usually, it's set to use Thumb instructions for compactness and efficiency.

Exception Mask Registers

  • Priority Mask Register (PRIMASK): Used to disable all interrupts with configurable priorities. Good for atomic operations.

  • Fault Mask Register (FAULTMASK): Similar to PRIMASK, but for non-configurable fault interrupts. Use this for critical sections.

  • Base Priority Register (BASEPRI): Allows you to mask interrupts below a certain priority level. It's more flexible than PRIMASK.

Control Register (CONTROL)

Here you set up privilege levels and the stack pointer to be used (Process or Main). For example, switching between user and privileged modes is done here.


4. Memory Mapped vs. Non-Memory Mapped Registers

Non-Memory Mapped Registers

The core registers like R0-R15 and the special-purpose ones are non-memory-mapped registers. They are integral to the CPU and are manipulated directly by assembly instructions. For instance, you can't just use standard C code to tweak the value of the stack pointer (R13). However, you can do so with inline assembly code, like this:

__asm volatile("mov r0, sp");

In this example, the stack pointer's value is moved into register R0. You'd typically use inline assembly snippets like this only for very low-level tasks where you need direct hardware control.

Memory Mapped Registers

On the flip side, memory-mapped registers are allocated specific addresses in the memory space. This makes it possible to access and manipulate them using regular C code. Such registers include:

  • Those of the processor's special peripherals like the Nested Vectored Interrupt Controller (NVIC), Memory Protection Unit (MPU), System Control Block (SCB), and Debug components.
  • The registers that control MCU peripherals like General-Purpose I/O (GPIO), Serial Peripheral Interface (SPI), Universal Synchronous Asynchronous Receiver Transmitter (USART), and so on.

For example, to set the 5th bit of a GPIO register:

GPIOA->ODR |= (1 << 5);

In this line, GPIOA->ODR is the memory-mapped register for the output data of GPIO port A. We use the standard bit manipulation techniques in C to set or clear specific bits in these memory-mapped registers.