ARM Cortex-M Thumb Mode Restrictions on PC Access
The Program Counter (PC) in ARM architectures is a critical register that holds the address of the next instruction to be executed. Accessing the PC directly in assembly code can be useful for various purposes, such as implementing position-independent code, debugging, or dynamic branching. However, the ability to access the PC directly is heavily dependent on the processor’s operating state and the specific ARM architecture version being used. In ARMv7 and earlier architectures, the processor can operate in two primary states: ARM state and Thumb state. ARM state uses 32-bit instructions, while Thumb state uses 16-bit instructions (with Thumb-2 extending this to a mix of 16-bit and 32-bit instructions). The Thumb state is particularly common in ARM Cortex-M series processors, which are widely used in embedded systems due to their efficiency and compact code size.
In Thumb mode, certain operations involving the PC are restricted or undefined. Specifically, the STR
(Store Register) instruction cannot directly use the PC as the source register. This restriction is highlighted in the ARM Architecture Reference Manual, which states that storing the PC using STR
in Thumb mode is undefined behavior. This limitation arises because the Thumb instruction set is optimized for size and simplicity, and direct manipulation of the PC can lead to unpredictable results, especially in pipelined architectures where the PC value may not align with the programmer’s expectations.
The error message "Error: r15(pc) not allowed here — str R15,[R1,#0]
" is a direct consequence of this restriction. When the assembler encounters an attempt to store the PC using the STR
instruction in Thumb mode, it flags the operation as invalid. This behavior is consistent across most ARM Cortex-M processors, including the Cortex-M0, Cortex-M3, and Cortex-M4, which are commonly used in embedded systems.
Thumb Mode Constraints and ARM State Transition Mechanisms
The inability to store the PC directly in Thumb mode is not a hardware bug but a deliberate design choice to maintain the simplicity and efficiency of the Thumb instruction set. However, this restriction can be circumvented by switching the processor to ARM state, where the STR
instruction can legally use the PC as the source register. The ARMv7 architecture provides mechanisms for transitioning between ARM and Thumb states using specific branch instructions, such as BX
(Branch and Exchange) and BLX
(Branch with Link and Exchange). These instructions allow the processor to switch states based on the value of the least significant bit (LSB) of the target address. If the LSB is set to 1, the processor switches to Thumb state; if it is set to 0, the processor switches to ARM state.
The T-bit in the Program Status Register (PSR) indicates the current operating state of the processor. When the T-bit is set, the processor is in Thumb state; when it is cleared, the processor is in ARM state. The BX
and BLX
instructions modify the T-bit as part of their operation, enabling seamless transitions between the two states. For example, to switch from Thumb state to ARM state, you can use the following sequence of instructions:
LDR R0, =ARM_CODE_ADDRESS ; Load the address of the ARM code
BX R0 ; Branch to the ARM code, switching states
In this example, ARM_CODE_ADDRESS
must be aligned to a 4-byte boundary (i.e., its LSB must be 0) to ensure that the processor switches to ARM state. Once in ARM state, you can safely use the STR
instruction to store the PC:
LDR R1, =0x20000000 ; Load the memory address
STR R15, [R1, #0] ; Store the PC to the memory address
After performing the necessary operations in ARM state, you can switch back to Thumb state using a similar mechanism:
LDR R0, =THUMB_CODE_ADDRESS ; Load the address of the Thumb code
BX R0 ; Branch to the Thumb code, switching states
Here, THUMB_CODE_ADDRESS
must have its LSB set to 1 to ensure that the processor switches back to Thumb state.
Implementing PC Storage in Thumb Mode Using Indirect Methods
If switching to ARM state is not feasible or desirable, there are alternative methods for storing the PC in Thumb mode. One common approach is to use a combination of data processing instructions and indirect addressing to achieve the desired result. For example, you can use the ADD
instruction to copy the value of the PC to a general-purpose register and then store the value from that register to memory:
LDR R1, =0x20000000 ; Load the memory address
ADD R2, R15, #0 ; Copy the PC to R2
STR R2, [R1, #0] ; Store the value of R2 to the memory address
In this example, the ADD
instruction is used to copy the value of the PC to register R2
. The STR
instruction then stores the value of R2
to the specified memory address. This approach avoids the direct use of the PC in the STR
instruction, thereby complying with the Thumb mode restrictions.
Another approach is to use the LDR
(Load Register) instruction with the PC as the destination register. This technique is often used for implementing position-independent code or dynamic branching. For example:
LDR R1, =0x20000000 ; Load the memory address
LDR R2, [R15, #0] ; Load the value of the PC into R2
STR R2, [R1, #0] ; Store the value of R2 to the memory address
In this example, the LDR
instruction is used to load the value of the PC into register R2
, which is then stored to memory using the STR
instruction. This method is particularly useful in scenarios where the PC value needs to be manipulated or inspected before being stored.
For more advanced use cases, such as implementing function pointers or jump tables, you can use the LDM
(Load Multiple) or POP
instructions with the PC included in the list of registers to be loaded. These instructions allow you to load multiple registers from memory, including the PC, effectively performing a branch operation. For example:
LDR R1, =JUMP_TABLE ; Load the address of the jump table
LDM R1, {R2, R3, R15} ; Load R2, R3, and PC from the jump table
In this example, the LDM
instruction loads the values of R2
, R3
, and the PC from the specified memory address. This technique is commonly used in embedded systems to implement dynamic function dispatch or state machines.
Summary of Best Practices and Recommendations
When working with the Program Counter (PC) in ARM architectures, it is essential to understand the constraints and capabilities of the processor’s operating state. In Thumb mode, direct access to the PC using the STR
instruction is restricted, but this limitation can be overcome by switching to ARM state or using indirect methods such as data processing instructions or load/store operations with intermediate registers. The following table summarizes the key considerations and recommended approaches for storing the PC in different scenarios:
Scenario | Recommended Approach |
---|---|
Storing PC in Thumb mode | Use ADD to copy PC to a general-purpose register, then store the register value |
Switching to ARM state | Use BX or BLX with an aligned address to transition to ARM state |
Implementing dynamic branching | Use LDR with PC as the destination or LDM /POP with PC in the register list |
Debugging or inspection | Use indirect methods to copy and inspect the PC value without direct manipulation |
By following these best practices, you can effectively manage the Program Counter in ARM architectures while adhering to the constraints of the Thumb instruction set. Whether you are developing firmware for ARM Cortex-M microcontrollers or optimizing performance-critical code, understanding these nuances is crucial for achieving reliable and efficient system implementations.