When programming for the Cortex-M3 processor, it is important to keep the processor in Thumb mode rather than ARM mode. Thumb mode uses 16-bit Thumb instructions which take up less memory space than 32-bit ARM instructions. Staying in Thumb mode ensures your code remains compact and efficient. Here are some techniques to keep the Cortex-M3 locked in Thumb mode.
Set the CPSR T-Bit on Reset
The simplest way to keep the Cortex-M3 in Thumb mode is to set the T-bit in the CPSR (Current Program Status Register) during processor reset. The T-bit controls whether the processor is in Thumb (T=1) or ARM (T=0) state. By default, the T-bit resets to 0 on power on for Cortex-M3. To change this, update your reset handler code to set the T-bit to 1 early in the reset process. For example: void Reset_Handler() { // Set T-bit to enable Thumb mode __set_CONTROL(0x00000001); // Rest of reset code }
Setting the T-bit on reset locks the processor into Thumb state from the start, preventing accidental switching to ARM mode later on. This is the recommended approach for Cortex-M3 programming to maximize code density.
Use BLX Instructions for Function Calls
The BL and BLX instructions are used for function calls in ARM code. BL performs a branch with link and always stays in the same instruction set state. BLX performs a branch with link, and can switch modes. When calling between Thumb and ARM code, you must use BLX rather than BL to change processor states automatically. Using BLX for all function calls ensures the T-bit remains set properly throughout a program. // Calling a Thumb function from ARM code BLX ThumbFunc // Calling an ARM function from Thumb code BLX ARMFunc
This avoids accidentally dropping into ARM mode by using BL, which can happen if libraries contain mixed ARM and Thumb code. Getting into ARM mode can be disastrous for code size, so strictly using BLX is good practice.
Avoid BX Instructions Outside Veneers
The BX instruction can explicitly change the instruction set state from ARM to Thumb or vice versa. Avoid using BX outside of interworking veneers, as this can unintentionally switch modes. Interworking veneers handle transitions between states properly, whereas arbitrary BX instructions can corrupt the T-bit. // In Thumb code BX ARMFunc ; DON’T DO THIS! // Use veneers instead BLX ARMFuncStub ; Thumb to ARM veneer
Veneers contain a small stub that performs a proper BLX when switching instruction sets. This protects the T-bit throughout the rest of the code. Avoiding extraneous BX instructions ensures Thumb mode remains unchanged.
Use Assembler Directives to Lock Sections
Most assemblers and compilers provide directives to lock sections into Thumb or ARM state. For Cortex-M3, add .thumb or .thumb_func to force functions into Thumb mode. For example: .thumb .thumb_func void MyThumbFunc(void) { // Thumb instructions }
This prevents a function from accidentally ending up in ARM state if the linker mixes code sections. Apply .thumb directives to source files and key functions to robustly stay in Thumb state.
Configure Linker Script Memory Regions as Thumb
Linker scripts define how code and data sections are placed into memory. To keep regions Thumb only, add a “.thumb” prefix to the region name. For example: .thumb.text : { *(.thumb.text*) }
This forces all code linked into the “text” region to contain only Thumb instructions. Having Thumb-only memory regions prevents ARM code from being mixed in.
Validate T-Bit Settings During Debugging
Debuggers allow you to view and manipulate processor registers like the CPSR during execution. When halted, check that the T-bit is set to confirm Thumb mode is active. The debugger can also set breakpoints if the T-bit gets improperly cleared to catch errors early. Watching the CPSR ensures the processor stays in Thumb state as expected. (gdb) print $cpsr $1 = 0x1000000 (T-bit is set)
Proper use of BLX, BX avoidance, assembler directives, linker regions, and debugger validation combines to create robust, Thumb mode only applications on Cortex-M3.
Use Inline Assembly to Set T-Bit
For C code, inline assembly can explicitly set the T-bit when necessary. Insert asm code to change the CPSR control register and lock in Thumb mode. asm volatile ( “mov r0, #0x1” “\n” “msr CONTROL, r0” “\n” );
This is useful if transitioning from higher level ARM code down to Thumb functions. The inline asm ensures Thumb mode is set properly before hitting any Thumb instructions.
Configure LOC Counter for Thumb Mode
The LOC (Length of Code) counter tracks code size during JTAG debug. Make sure LOC counts bytes as Thumb (2-byte) instructions rather than 4-byte ARM instructions. For example, in GDB: monitor LOC 2
This sets the multiplier to 2 for Thumb mode. Confirming LOC counts properly ensures BREAK and other debug commands work as intended in Thumb state.
Validate Thumb-2 Support in Tools
Thumb-2 extended Thumb with some 16- and 32-bit instructions on Cortex-M3. Make sure your toolchain – assembler, compiler, linker, debugger – fully supports Thumb-2 extensions. Using legacy Thumb-only tools can result in incorrect code generation and T-bit handling. Check documentation and confirm Thumb-2 compatibility is explicitly listed.
Reserve Some ARM Space in Memory Map
While your Cortex-M3 code will be Thumb, reserve space in the memory map for some ARM mode exception handlers. The low level ARM exception code helps transition to Thumb cleanly. Locate this ARM code in the memory map by using linker region directives. .arm.text : { *(.arm.text*) }
Put these handlers in a known ARM-only region. The rest of the app code can safely stay in Thumb mode.
Use UAL Syntax in Assembly Code
UAL (Unified Assembly Language) provides syntax compatible with both ARM and Thumb assembly. By using UAL conventions like using R registers instead of RN, assembly code can be assembled into either mode. This makes code reuse easier when using both Thumb and ARM. ADD R0, R1, R2 ; UAL syntax
The assembler will convert UAL instructions into the proper Thumb or ARM encodings based on section directives. UAL allows writing generic code sections that can be used in either mode.
Order Code Sections from ARM to Thumb
When organizing code sections, list ARM first followed by Thumb during linking. Linker section placement order matters for proper interworking veneers. Listing ARM code first simplifies transitioning into Thumb mode when sections get linked together. ARM_Code1 ARM_Code2 … Thumb_Code1 Thumb_Code2 …
Following this order guarantees ARM code appears at lower addresses than Thumb code in the final binary. This convention produces the shortest interworking veneers.
Use Assembly Wrappers Around ARM Functions
For legacy ARM library functions, create a thin wrapper in assembly to transition modes properly: .thumb .thumb_func .global ThumbFunc ThumbFunc: bx ARMFunc ; switch to ARM nop .arm ARMFunc: // ARM instructions
This guarantees Thumb mode leading into and out of ARM code segments. The wrappers isolate ARM so it does not corrupt surrounding Thumb areas.
Debug Faulty Veneers Causing Mode Switching
If switching between Thumb and ARM code, debug any interworking veneers that may be buggy. Faulty veneers are a common source of improperly changed T-bit values. Use breakpoint debugging or log trace records to ensure veneers operate correctly.
Veneers should follow this proper sequence: Thumb: BLX ARMStub BX LR ARMStub: BX ThumbFunc
Confirm this flow is followed to rule out veneer issues when tracking down mode switching problems.
Limit Inline Assembly Code Sections
Excessive use of inline assembly code can inadvertently toggle the T-bit. Try to minimize inline asm statements and consolidate them into named functions. This contained approach reduces the likelihood of inline asm accidentally enabling ARM mode. // Avoid littering code with asm blocks asm { ARM instructions… } // Instead use contained asm functions void asmFunc() { asm { ARM instructions … } }
Proper encapsulation limits the reach of inline asm, making the code easier to manage in Thumb mode.
Use Compiler Optimization Pragmas
Compiler optimizers can generate incorrect code that improperly changes modes. Use pragmas or command line options to selectively disable optimizations that could impact the T-bit: #pragma O0 // disable optimization for function -O0 // disable optimization for file
Diagnosing flags that introduce erroneous BX or BL instructions will ensure the compiler preserves Thumb mode correctly.
Reserve unused PC Bits to Validate Thumb Mode
When returning from an exception the PC is checked for the bit pattern 0bXXXX0000. Since bits 4:0 are all zero in Thumb, we can use bits 14:5 as a signature to verify Thumb mode on exception return. Set your own signature value in the unused bits. ldr pc, [sp], #4 // Exception return // Check reserved PC bits ands pc, #0x7FE0 bne goodThumbPc // Invalid PC, handle error
This technique provides runtime validation of the PC value and Thumb state integrity. Having your own signature encodes an extra level of protection.
Summary
In summary, keeping Cortex-M3 locked in Thumb mode requires utilizing:
- T-bit configuration in reset handler and linker scripts
- BLX instructions for function calls
- Thumb assembly directives on code
- Validator debugger checks on T-bit
- Proper veneer sequences when transitioning
- Reserved PC bit patterns to check mode
Following ARM recommended programming practices strictly, along with robust debugging, ensures Thumb mode remains enabled reliably in Cortex-M3 applications. The result is compact, efficient code that maximizes the performance and capability of the Cortex-M3 processor.