⚙️ Langage d'Assemblage ARM CORTEX - Semestre 6

Année Universitaire : 2022-2023
Semestre : 6
Crédits : 2 ECTS
Spécialité : Systèmes Embarqués et Programmation Bas Niveau


PART A - Présentation Générale du Cours

Vue d'ensemble

Ce cours enseigne la programmation en langage assembleur pour les microcontrôleurs ARM Cortex-M (architecture 32 bits). Il couvre l'architecture du processeur, le jeu d'instructions Thumb-2, et la programmation bas niveau pour systèmes embarqués. Les travaux pratiques utilisent des STM32F103 (Cortex-M3) avec l'environnement Keil µVision.

Objectifs pédagogiques :

  • Comprendre l'architecture ARM Cortex-M (registres, pipeline, mémoire)
  • Maîtriser le jeu d'instructions Thumb-2
  • Programmer en assembleur (boucles, fonctions, accès périphériques)
  • Interfacer avec du matériel (GPIO, UART, timers)
  • Optimiser le code pour performances et taille

Position dans le cursus

Ce cours complète et approfondit :

  • Architecture Informatique Matérielle (S5) : bases de l'architecture processeur
  • Architecture Matérielle (S6) : ARM vs x86, pipeline, caches
  • Microcontrôleur (S6) : périphériques et programmation en C

Il prépare aux applications :

  • Systèmes embarqués temps réel : code critique et optimisé
  • Développement de drivers : accès direct au matériel
  • Bootloaders et firmware : code de bas niveau

PART B - Expérience Personnelle et Contexte d'Apprentissage

Organisation et ressources

Le module était structuré en 4 séquences progressives :

1. Séquence 1 : Architecture ARM Cortex-M

  • Organisation mémoire et registres
  • Pipeline et modes d'exécution
  • Conventions d'appel AAPCS

2. Séquence 2 : Jeu d'instructions de base

  • Instructions arithmétiques et logiques
  • Déplacements et chargements mémoire
  • Branchements et boucles

3. Séquence 3 : Sous-programmes et pile

  • Appels de fonctions (BL, BX)
  • Gestion de la pile (PUSH, POP)
  • Passage de paramètres

4. Séquence 4 : Accès périphériques

  • Registres memory-mapped
  • Contrôle GPIO
  • Communication UART
  • Interruptions
Registres ARM Cortex-M

Figure : Organisation des registres ARM Cortex-M - R0-R15, SP, LR, PC et PSR

Environnement de développement :

  • IDE : Keil µVision 5
  • Cible : STM32F103RB (Cortex-M3, 128 KB Flash, 20 KB RAM)
  • Simulateur : simulation cycle-accurate sans carte physique
  • Débuggage : breakpoints, registres, mémoire, désassemblage

Déroulement des TDs et TPs

TD2 : Conversion et affichage

Objectif : lire 4 chiffres au clavier, les convertir en entier, et afficher le résultat.

Fonctions à implémenter :

; Lecture de 4 caractères via UART
BL GetKey_Echo      ; Lit un caractère et l'affiche
STRB R0,[R7]        ; Stocke dans la mémoire RAM

; Conversion ASCII → nombre
LDRB R3,[R7],#1     ; Charge caractère
SUB R3,#'0'         ; Convertit '0'-'9' en 0-9
MOV R4,#10
MUL R3,R3,R4        ; Décalage décimal

; Affichage d'une chaîne
Affichaine PROC
    LDRB R0,[R3],#1  ; Charge caractère
    CBZ R0,Fin       ; Si 0, fin de chaîne
    BL SendChar      ; Envoie via UART
    B Car_suivant    ; Boucle
Fin_Chaine
    POP {R0,PC}      ; Retour
    ENDP

Concepts clés :

  • Adressage indirect avec post-incrément : [R7],#1
  • Convention d'appel : paramètres dans R0-R3
  • Sauvegarde contexte : PUSH/POP avec LR

TD3 : Manipulation de tableaux

Travail sur tableaux d'entiers : tri, recherche, calculs statistiques.

Exemple : recherche du maximum

; R0 = adresse du tableau
; R1 = taille du tableau
; Retour : R2 = valeur maximale

Recherche_Max PROC
    PUSH {R3,R4,LR}
    LDR R2,[R0],#4   ; Premier élément = max initial
    SUBS R1,#1       ; Compteur -1

Boucle
    LDR R3,[R0],#4   ; Élément suivant
    CMP R3,R2
    BLE Suivant      ; Si <= max, passe au suivant
    MOV R2,R3        ; Nouveau max
Suivant
    SUBS R1,#1       ; Décrément compteur
    BNE Boucle       ; Tant que != 0

    POP {R3,R4,PC}
    ENDP

TD4 : Fonctions mathématiques

Implémentation d'opérations complexes (division, racine carrée par approximations successives).

TP : Projet "Roue Magique"

Projet fil rouge en plusieurs étapes pour créer un jeu de lumières avec LEDs.

Étape 1 : Contrôle d'une LED

; Allumer une LED sur PC10
Allume_LED PROC
    LDR R0,=0x40010C10  ; Adresse GPIOC_BSRR (Bit Set Reset Register)
    MOV R1,#(0x01 << 10) ; Bit 10 pour PC10
    STR R1,[R0]          ; Écriture → LED allumée
    BX LR                ; Retour
    ENDP

; Éteindre la LED
Eteint_LED PROC
    LDR R2,=0x40010C14  ; Adresse GPIOC_BRR (Bit Reset Register)
    MOV R3,#(0x01 << 10)
    STR R3,[R2]
    BX LR
    ENDP

Étape 2 : Lecture d'un bouton

; Lire l'état d'un bouton sur PB8
Capteur PROC
    LDR R4,=0x40010808   ; Adresse GPIOB_IDR (Input Data Register)
    LDRH R5,[R4]         ; Lecture 16 bits
    AND R5,R5,#0x0100    ; Masque bit 8
    CMP R5,#0
    BEQ Bouton_Presse    ; Si 0, bouton appuyé
    BX LR
    ENDP

Étape 3 : Séquence de LEDs

Faire clignoter plusieurs LEDs en séquence (chenillard).

Étape 4 : Interruptions et timers

Utiliser un timer pour créer des délais précis sans attente active.

Difficultés rencontrées

Gestion de la pile : Oublier de faire POP après PUSH provoque des erreurs de retour de fonction. Le LR (Link Register) doit être sauvegardé si la fonction appelle d'autres fonctions.

Adressage mémoire : Confusion entre adresse et valeur. LDR R0,=0x2000 charge l'adresse, LDR R0,[R1] charge la valeur à l'adresse contenue dans R1.

Flags de condition : Les instructions avec 'S' (ADDS, SUBS) modifient les flags NZCV. Nécessaire avant CMP pour les branchements conditionnels.

Registres de périphériques : Adresses spécifiques à mémoriser ou utiliser des fichiers include. Documentation du STM32 indispensable.


PART C - Aspects Techniques Détaillés

1. Architecture ARM Cortex-M3

Banc de registres :

Le Cortex-M3 dispose de 16 registres 32 bits :

RegistreNomUsage
R0-R12Registres générauxCalculs, données temporaires
R13 (SP)Stack PointerPointeur de pile
R14 (LR)Link RegisterAdresse de retour de fonction
R15 (PC)Program CounterAdresse de l'instruction en cours

Registres spéciaux :

  • PSR (Program Status Register) : flags N, Z, C, V, mode, exceptions
  • PRIMASK : masque global d'interruptions
  • CONTROL : sélection de pile, niveau de privilège

Organisation mémoire :

RégionAdressesUsage
Code0x00000000 - 0x1FFFFFFFFlash, instructions
SRAM0x20000000 - 0x3FFFFFFFRAM, données
Périphériques0x40000000 - 0x5FFFFFFFRegistres memory-mapped
Système0xE0000000 - 0xFFFFFFFFNVIC, SysTick, debug

Pipeline 3 étages :

  1. Fetch : lecture instruction
  2. Decode : décodage
  3. Execute : exécution

Conséquence : le PC pointe toujours 2 instructions en avance (PC+4 en Thumb-2).

2. Jeu d'Instructions Thumb-2

Instructions arithmétiques :

InstructionSyntaxeDescription
ADDADD Rd, Rn, RmRd = Rn + Rm
SUBSUB Rd, Rn, RmRd = Rn - Rm
MULMUL Rd, Rn, RmRd = Rn × Rm
SDIVSDIV Rd, Rn, RmRd = Rn / Rm (signé)
UDIVUDIV Rd, Rn, RmRd = Rn / Rm (non signé)

Variantes avec flags : ADDS, SUBS (modifient NZCV).

Instructions logiques :

InstructionDescription
AND Rd, Rn, RmET logique
ORR Rd, Rn, RmOU logique
EOR Rd, Rn, RmOU exclusif (XOR)
BIC Rd, Rn, RmBit Clear (Rd = Rn AND NOT Rm)
MVN Rd, RmNOT (inversion)

Décalages :

InstructionDescription
LSL Rd, Rn, #nLogical Shift Left (décalage gauche)
LSR Rd, Rn, #nLogical Shift Right (décalage droite, complète avec 0)
ASR Rd, Rn, #nArithmetic Shift Right (conserve le signe)
ROR Rd, Rn, #nRotate Right (rotation)

Déplacements de données :

MOV R0,#42          ; R0 = 42 (immédiat)
MOV R1,R2           ; R1 = R2 (copie)
MOVW R0,#0x1234     ; Charge 16 bits bas
MOVT R0,#0x5678     ; Charge 16 bits haut → R0 = 0x56781234

3. Accès Mémoire

Instructions de chargement :

InstructionTailleDescription
LDR Rd,[Rn]32 bitsCharge mot
LDRH Rd,[Rn]16 bitsCharge demi-mot (halfword)
LDRB Rd,[Rn]8 bitsCharge octet (byte)
LDRSB Rd,[Rn]8 bits signéCharge octet avec extension de signe
LDRSH Rd,[Rn]16 bits signéCharge demi-mot avec extension de signe

Instructions de stockage :

InstructionTailleDescription
STR Rd,[Rn]32 bitsStocke mot
STRH Rd,[Rn]16 bitsStocke demi-mot
STRB Rd,[Rn]8 bitsStocke octet

Modes d'adressage :

; Direct
LDR R0,[R1]         ; R0 = mem[R1]

; Offset
LDR R0,[R1,#8]      ; R0 = mem[R1+8]

; Pré-incrémenté
LDR R0,[R1,#8]!     ; R1 = R1+8, puis R0 = mem[R1]

; Post-incrémenté
LDR R0,[R1],#8      ; R0 = mem[R1], puis R1 = R1+8

; Indexé par registre
LDR R0,[R1,R2]      ; R0 = mem[R1+R2]

; PC-relatif (litteral pool)
LDR R0,=0x20000000  ; Charge adresse via pool constant

4. Contrôle de Flux

Branchements inconditionnels :

B etiquette         ; Branchement simple
BL fonction         ; Branchement avec Link (LR = PC+4)
BX Rm               ; Branchement vers adresse dans Rm
BLX Rm              ; Branchement avec Link vers Rm

Branchements conditionnels :

Basés sur les flags NZCV (Negative, Zero, Carry, oVerflow).

SuffixeConditionFlags
EQÉgalZ=1
NENon égalZ=0
GTSupérieur (signé)Z=0 ET N=V
LTInférieur (signé)N≠V
GESupérieur ou égal (signé)N=V
LEInférieur ou égal (signé)Z=1 OU N≠V
HISupérieur (non signé)C=1 ET Z=0
LSInférieur ou égal (non signé)C=0 OU Z=1

Comparaisons :

CMP R0,R1           ; Compare R0 et R1 (calcule R0-R1, met à jour flags)
CMN R0,R1           ; Compare negative (calcule R0+R1)
TST R0,R1           ; Test bits (AND, met à jour flags sans stocker)
TEQ R0,R1           ; Test equal (XOR, met à jour flags)

Exemple de boucle :

    MOV R0,#10      ; Compteur
Boucle
    ; ... code ...
    SUBS R0,#1      ; Décrément et mise à jour flags
    BNE Boucle      ; Tant que R0 != 0

5. Sous-programmes et Pile

Convention d'appel AAPCS (ARM Architecture Procedure Call Standard) :

RegistreUsageSauvegarde
R0-R3Paramètres et retourCaller-saved
R4-R11Variables localesCallee-saved
R12TemporaireCaller-saved
R13 (SP)Pile-
R14 (LR)Adresse retour-

Appel de fonction :

; Appelant (caller)
    MOV R0,#5       ; Premier paramètre
    MOV R1,#10      ; Deuxième paramètre
    BL MaFonction   ; Appel (LR = adresse retour)
    ; R0 contient le résultat

; Fonction (callee)
MaFonction PROC
    PUSH {R4,LR}    ; Sauvegarde registres utilisés

    ; Calculs utilisant R0, R1, R4
    ADD R4,R0,R1
    MOV R0,R4       ; Résultat dans R0

    POP {R4,PC}     ; Restaure et retourne
    ENDP

Gestion de la pile :

La pile croît vers les adresses décroissantes (full descending).

PUSH {R0-R3,LR}     ; Sauvegarde multiple
; SP = SP - 20 (5 registres × 4 octets)

POP {R0-R3,PC}      ; Restauration et retour
; SP = SP + 20

Équivalent :

  • PUSH {Rx} = STMDB SP!, {Rx} (Store Multiple Decrement Before)
  • POP {Rx} = LDMIA SP!, {Rx} (Load Multiple Increment After)

6. Accès aux Périphériques

Memory-mapped I/O :

Les périphériques sont accessibles via des adresses mémoire spécifiques.

Exemple : GPIO (General Purpose Input/Output)

Registres du GPIOC (Port C) pour STM32F103 :

RegistreAdresseUsage
CRL0x40011000Configuration broches 0-7
CRH0x40011004Configuration broches 8-15
IDR0x40011008Input Data Register (lecture)
ODR0x4001100COutput Data Register (écriture)
BSRR0x40011010Bit Set/Reset Register
BRR0x40011014Bit Reset Register

Configuration d'une broche en sortie :

; Configurer PC10 en sortie push-pull, 50 MHz
    LDR R0,=0x40011004  ; GPIOC_CRH
    LDR R1,[R0]
    BIC R1,#(0xF << 8)  ; Clear bits pour PC10
    ORR R1,#(0x3 << 8)  ; MODE=11 (50MHz), CNF=00 (push-pull)
    STR R1,[R0]

Manipulation de bits :

; Mise à 1 d'un bit (set)
LDR R0,=0x40011010  ; GPIOC_BSRR
MOV R1,#(1 << 10)   ; Bit 10
STR R1,[R0]         ; PC10 = 1

; Mise à 0 d'un bit (reset)
LDR R0,=0x40011014  ; GPIOC_BRR
MOV R1,#(1 << 10)
STR R1,[R0]         ; PC10 = 0

; Lecture d'un bit
LDR R0,=0x40011008  ; GPIOC_IDR
LDR R1,[R0]
TST R1,#(1 << 8)    ; Test bit 8
BEQ Bit_A_Zero      ; Branch if zero

7. Directives Assembleur

Organisation du code :

    AREA MonCode, CODE, READONLY, ALIGN=2
    ; Sections de code

    AREA MesDonnees, DATA, READWRITE
    ; Sections de données

Définition de données :

; Constantes
Valeur  EQU 42          ; Équivalent #define

; Données initialisées
Tableau DCD 1,2,3,4,5   ; Define Constant Data (32 bits)
Chaine  DCB "Hello",0   ; Define Constant Byte (8 bits)
Mot     DCW 0x1234      ; Define Constant Word (16 bits)

; Données non initialisées
Buffer  SPACE 100       ; Réserve 100 octets

Déclarations :

    EXPORT main         ; Rend le symbole visible à l'extérieur
    IMPORT fonction     ; Importe un symbole externe

    PROC                ; Début de procédure
    ENDP                ; Fin de procédure

    END                 ; Fin du fichier source

8. Optimisations

Taille de code :

Instructions Thumb-2 sont 16 ou 32 bits. Préférer les instructions 16 bits quand possible.

; 16 bits
ADDS R0,R1,R2       ; Si R0-R7 et résultat modifie flags

; 32 bits
ADD.W R0,R1,R2      ; Force 32 bits

Boucles efficaces :

; Inefficace (compare à 0 à chaque itération)
    MOV R0,#0
Boucle
    ; ...
    ADD R0,#1
    CMP R0,#100
    BLT Boucle

; Efficace (décrémente et teste zéro implicitement)
    MOV R0,#100
Boucle
    ; ...
    SUBS R0,#1
    BNE Boucle

Éviter les branchements :

Utiliser les instructions conditionnelles quand possible.

; Avec branchement
    CMP R0,R1
    BLE Suivant
    MOV R2,R0
Suivant
    ; ...

; Sans branchement (IT = If-Then)
    CMP R0,R1
    IT GT
    MOVGT R2,R0     ; Exécuté si GT

PART D - Analyse Réflexive et Perspectives

Compétences acquises

Compréhension bas niveau : La programmation en assembleur force à comprendre exactement ce qui se passe au niveau processeur : chaque instruction, chaque accès mémoire, chaque modification de flag. Cette compréhension aide à débugger et optimiser le code C.

Contrôle total du matériel : Accès direct aux registres de périphériques, manipulation bit à bit, timing précis. Essentiel pour développer des drivers ou du code temps réel critique.

Optimisation : Conscience du coût de chaque instruction. Savoir où optimiser (boucles critiques) et quand laisser le compilateur faire le travail.

Points clés à retenir

1. La pile est votre amie (et votre ennemie) : PUSH/POP doivent toujours être équilibrés. Oublier un POP ou faire POP du mauvais registre provoque des bugs difficiles à tracer.

2. LR doit être sauvegardé : Si une fonction appelle d'autres fonctions, LR doit être sauvegardé (PUSH {LR}) et restauré (POP {PC}) pour retourner correctement.

3. Flags de condition : Les instructions avec 'S' (ADDS, SUBS) modifient les flags. Nécessaire pour les branchements conditionnels. Attention à ne pas écraser les flags entre CMP et Bxx.

4. Adressage mémoire : Bien distinguer adresse et valeur. LDR R0,=etiquette charge l'adresse, LDR R0,[R1] charge la valeur à l'adresse contenue dans R1.

5. Documentation indispensable : Avoir toujours sous la main :

  • ARM Architecture Reference Manual (jeu d'instructions)
  • STM32 Reference Manual (adresses périphériques)
  • Cortex-M3 Technical Reference Manual (architecture)

Applications pratiques

Systèmes embarqués critiques : Bootloaders, routines d'initialisation, handlers d'interruption critiques. Code assembleur pour timing précis ou optimisation extrême.

Drivers de périphériques : Accès direct aux registres matériels pour performances maximales. Utile quand les HAL (Hardware Abstraction Layer) sont trop lourds.

Reverse engineering : Comprendre l'assembleur permet d'analyser du code compilé, débugger des problèmes obscurs, ou étudier des malwares.

Inline assembly en C : Intégrer quelques instructions assembleur dans du code C pour optimisations locales.

// Exemple inline assembly GCC
__asm volatile (
    "MOV R0, #42\n"
    "ADD R1, R0, R0\n"
    : "=r" (resultat)
    : "r" (entree)
    : "r0", "r1"
);

Assembleur vs C

Quand utiliser l'assembleur :

  • Code critique en temps (ISR ultra-rapides)
  • Optimisation extrême (boucles intensives)
  • Accès à des fonctionnalités spéciales (instructions SIMD)
  • Bootloaders et code d'initialisation
  • Taille de code très contrainte

Quand préférer le C :

  • Développement rapide et maintenabilité
  • Portabilité entre architectures
  • Code métier complexe
  • 95% des cas !

Les compilateurs modernes optimisent très bien. L'assembleur manuel n'est souvent nécessaire que pour quelques pourcents du code.

Retour d'expérience sur le projet TP

Le projet "Roue Magique" a permis d'appliquer tous les concepts :

  • Étape 1 : contrôle GPIO, découverte des registres
  • Étape 2 : lecture d'entrées, logique conditionnelle
  • Étape 3 : boucles et séquences
  • Étape 4 : timers et interruptions

Progression pédagogique : Chaque étape ajoute de la complexité. On construit progressivement un système complet. Sentiment d'accomplissement en voyant les LEDs s'allumer selon notre code.

Difficultés : Débugger en assembleur est plus difficile qu'en C. Pas de printf facile. Utilisation intensive du debugger et de l'observation des registres.

Limites et ouvertures

Limites du cours :

  • Peu sur les interruptions avancées (NVIC complet)
  • Pas de DMA ou timers complexes
  • Peu de code optimisé (SIMD, DSP)
  • Pas de systèmes d'exploitation temps réel (RTOS)

Ouvertures vers :

  • Embedded C avancé : inline assembly, optimisations
  • RTOS : FreeRTOS, tâches, synchronisation
  • Compilation et linking : fichiers objets, éditeur de liens
  • Architectures avancées : Cortex-M4 DSP, Cortex-M7 cache
  • Sécurité : TrustZone, secure boot

Évolution technologique

Tendances actuelles :

  • Cortex-M33 : TrustZone pour sécurité
  • Cortex-M55 : extensions Helium pour machine learning
  • RISC-V : alternative open-source à ARM
  • Compilation Just-In-Time : pour microcontrôleurs puissants

Outils modernes :

  • CMSIS (Cortex Microcontroller Software Interface Standard)
  • HAL (Hardware Abstraction Layer) générées automatiquement
  • IDE graphiques (STM32CubeIDE, Mbed Studio)
  • Débogage avancé (Segger J-Link, Ozone)

Conclusion

La programmation en assembleur ARM est un passage obligé pour comprendre en profondeur le fonctionnement des microcontrôleurs. Même si 95% du code moderne est écrit en C/C++, connaître l'assembleur permet :

  • De mieux comprendre ce que fait le compilateur
  • D'optimiser le code critique
  • De débugger des problèmes complexes
  • De développer des drivers efficaces
  • D'apprécier la puissance des langages de haut niveau

L'architecture ARM Cortex-M est omniprésente dans les systèmes embarqués modernes (IoT, wearables, drones, automobile). La maîtriser est un atout majeur pour tout ingénieur en systèmes embarqués.

Recommandations :

  • Pratiquer régulièrement (petits programmes, défis)
  • Lire du code assembleur généré par le compilateur (option -S de GCC)
  • Participer à des CTF (Capture The Flag) de reverse engineering
  • Expérimenter avec différentes architectures (ARM, RISC-V, x86)

Liens avec les autres cours :


📚 Documents de Cours

📖 Séquence 1 - Introduction ARM

Introduction à l'architecture ARM Cortex-M, registres, jeu d'instructions de base et organisation mémoire.

📥 Télécharger

📖 Séquence 2 - Instructions ARM

Jeu d'instructions ARM complet : arithmétique, logique, branchements, et modes d'adressage.

📥 Télécharger

📖 Séquence 3 - Fonctions et Pile

Convention d'appel AAPCS, gestion de la pile, prologue/épilogue de fonctions et passage de paramètres.

📥 Télécharger

📖 Référence Jeu d'Instructions

Tableau synthétique complet du jeu d'instructions ARM Cortex-M avec syntaxe et exemples.

📥 Télécharger


Cours enseigné en 2022-2023 à l'INSA Toulouse, Département Génie Électrique et Informatique.

⚙️ ARM CORTEX Assembly Language - Semester 6

Academic Year: 2022-2023
Semester: 6
Credits: 2 ECTS
Specialization: Embedded Systems and Low-Level Programming


PART A - General Course Overview

Overview

This course teaches assembly language programming for ARM Cortex-M microcontrollers (32-bit architecture). It covers processor architecture, the Thumb-2 instruction set, and low-level programming for embedded systems. Lab sessions use STM32F103 (Cortex-M3) with the Keil µVision development environment.

Learning objectives:

  • Understand ARM Cortex-M architecture (registers, pipeline, memory)
  • Master the Thumb-2 instruction set
  • Program in assembly (loops, functions, peripheral access)
  • Interface with hardware (GPIO, UART, timers)
  • Optimize code for performance and size

Position in the curriculum

This course builds upon and deepens:

  • Computer Hardware Architecture (S5): processor architecture fundamentals
  • Hardware Architecture (S6): ARM vs x86, pipeline, caches
  • Microcontroller (S6): peripherals and C programming

It prepares for applications in:

  • Real-time embedded systems: critical and optimized code
  • Driver development: direct hardware access
  • Bootloaders and firmware: low-level code

PART B - Personal Experience and Learning Context

Organization and resources

The module was structured into 4 progressive sequences:

1. Sequence 1: ARM Cortex-M Architecture

  • Memory organization and registers
  • Pipeline and execution modes
  • AAPCS calling conventions

2. Sequence 2: Basic instruction set

  • Arithmetic and logic instructions
  • Data movement and memory load operations
  • Branches and loops

3. Sequence 3: Subroutines and stack

  • Function calls (BL, BX)
  • Stack management (PUSH, POP)
  • Parameter passing

4. Sequence 4: Peripheral access

  • Memory-mapped registers
  • GPIO control
  • UART communication
  • Interrupts
ARM Cortex-M Registers

Figure: ARM Cortex-M register organization - R0-R15, SP, LR, PC and PSR

Development environment:

  • IDE: Keil µVision 5
  • Target: STM32F103RB (Cortex-M3, 128 KB Flash, 20 KB RAM)
  • Simulator: cycle-accurate simulation without physical board
  • Debugging: breakpoints, registers, memory, disassembly

Tutorials and lab sessions

Tutorial 2: Conversion and display

Objective: read 4 digits from the keyboard, convert them to an integer, and display the result.

Functions to implement:

; Reading 4 characters via UART
BL GetKey_Echo      ; Reads a character and echoes it
STRB R0,[R7]        ; Stores in RAM memory

; ASCII → number conversion
LDRB R3,[R7],#1     ; Load character
SUB R3,#'0'         ; Convert '0'-'9' to 0-9
MOV R4,#10
MUL R3,R3,R4        ; Decimal shift

; String display
Affichaine PROC
    LDRB R0,[R3],#1  ; Load character
    CBZ R0,Fin       ; If 0, end of string
    BL SendChar      ; Send via UART
    B Car_suivant    ; Loop
Fin_Chaine
    POP {R0,PC}      ; Return
    ENDP

Key concepts:

  • Indirect addressing with post-increment: [R7],#1
  • Calling convention: parameters in R0-R3
  • Context saving: PUSH/POP with LR

Tutorial 3: Array manipulation

Working with integer arrays: sorting, searching, statistical calculations.

Example: finding the maximum

; R0 = array address
; R1 = array size
; Return: R2 = maximum value

Recherche_Max PROC
    PUSH {R3,R4,LR}
    LDR R2,[R0],#4   ; First element = initial max
    SUBS R1,#1       ; Counter -1

Boucle
    LDR R3,[R0],#4   ; Next element
    CMP R3,R2
    BLE Suivant      ; If <= max, skip to next
    MOV R2,R3        ; New max
Suivant
    SUBS R1,#1       ; Decrement counter
    BNE Boucle       ; While != 0

    POP {R3,R4,PC}
    ENDP

Tutorial 4: Mathematical functions

Implementation of complex operations (division, square root by successive approximations).

Lab project: "Magic Wheel"

A multi-step ongoing project to create an LED light show.

Step 1: Controlling an LED

; Turn on an LED on PC10
Allume_LED PROC
    LDR R0,=0x40010C10  ; GPIOC_BSRR address (Bit Set Reset Register)
    MOV R1,#(0x01 << 10) ; Bit 10 for PC10
    STR R1,[R0]          ; Write → LED on
    BX LR                ; Return
    ENDP

; Turn off the LED
Eteint_LED PROC
    LDR R2,=0x40010C14  ; GPIOC_BRR address (Bit Reset Register)
    MOV R3,#(0x01 << 10)
    STR R3,[R2]
    BX LR
    ENDP

Step 2: Reading a button

; Read button state on PB8
Capteur PROC
    LDR R4,=0x40010808   ; GPIOB_IDR address (Input Data Register)
    LDRH R5,[R4]         ; 16-bit read
    AND R5,R5,#0x0100    ; Mask bit 8
    CMP R5,#0
    BEQ Bouton_Presse    ; If 0, button pressed
    BX LR
    ENDP

Step 3: LED sequence

Blinking multiple LEDs in sequence (chaser pattern).

Step 4: Interrupts and timers

Using a timer to create precise delays without busy-waiting.

Difficulties encountered

Stack management: Forgetting to POP after PUSH causes function return errors. The LR (Link Register) must be saved if the function calls other functions.

Memory addressing: Confusion between address and value. LDR R0,=0x2000 loads the address, LDR R0,[R1] loads the value at the address contained in R1.

Condition flags: Instructions with 'S' (ADDS, SUBS) modify the NZCV flags. Required before CMP for conditional branches.

Peripheral registers: Specific addresses to memorize or use include files. STM32 documentation is essential.


PART C - Detailed Technical Aspects

1. ARM Cortex-M3 Architecture

Register bank:

The Cortex-M3 has 16 32-bit registers:

RegisterNameUsage
R0-R12General purpose registersCalculations, temporary data
R13 (SP)Stack PointerStack pointer
R14 (LR)Link RegisterFunction return address
R15 (PC)Program CounterCurrent instruction address

Special registers:

  • PSR (Program Status Register): N, Z, C, V flags, mode, exceptions
  • PRIMASK: global interrupt mask
  • CONTROL: stack selection, privilege level

Memory organization:

RegionAddressesUsage
Code0x00000000 - 0x1FFFFFFFFlash, instructions
SRAM0x20000000 - 0x3FFFFFFFRAM, data
Peripherals0x40000000 - 0x5FFFFFFFMemory-mapped registers
System0xE0000000 - 0xFFFFFFFFNVIC, SysTick, debug

3-stage pipeline:

  1. Fetch: instruction read
  2. Decode: decoding
  3. Execute: execution

Consequence: the PC always points 2 instructions ahead (PC+4 in Thumb-2).

2. Thumb-2 Instruction Set

Arithmetic instructions:

InstructionSyntaxDescription
ADDADD Rd, Rn, RmRd = Rn + Rm
SUBSUB Rd, Rn, RmRd = Rn - Rm
MULMUL Rd, Rn, RmRd = Rn × Rm
SDIVSDIV Rd, Rn, RmRd = Rn / Rm (signed)
UDIVUDIV Rd, Rn, RmRd = Rn / Rm (unsigned)

Variants with flags: ADDS, SUBS (modify NZCV).

Logic instructions:

InstructionDescription
AND Rd, Rn, RmLogical AND
ORR Rd, Rn, RmLogical OR
EOR Rd, Rn, RmExclusive OR (XOR)
BIC Rd, Rn, RmBit Clear (Rd = Rn AND NOT Rm)
MVN Rd, RmNOT (inversion)

Shifts:

InstructionDescription
LSL Rd, Rn, #nLogical Shift Left
LSR Rd, Rn, #nLogical Shift Right (fills with 0)
ASR Rd, Rn, #nArithmetic Shift Right (preserves sign)
ROR Rd, Rn, #nRotate Right

Data movement:

MOV R0,#42          ; R0 = 42 (immediate)
MOV R1,R2           ; R1 = R2 (copy)
MOVW R0,#0x1234     ; Load lower 16 bits
MOVT R0,#0x5678     ; Load upper 16 bits → R0 = 0x56781234

3. Memory Access

Load instructions:

InstructionSizeDescription
LDR Rd,[Rn]32 bitsLoad word
LDRH Rd,[Rn]16 bitsLoad halfword
LDRB Rd,[Rn]8 bitsLoad byte
LDRSB Rd,[Rn]8 bits signedLoad byte with sign extension
LDRSH Rd,[Rn]16 bits signedLoad halfword with sign extension

Store instructions:

InstructionSizeDescription
STR Rd,[Rn]32 bitsStore word
STRH Rd,[Rn]16 bitsStore halfword
STRB Rd,[Rn]8 bitsStore byte

Addressing modes:

; Direct
LDR R0,[R1]         ; R0 = mem[R1]

; Offset
LDR R0,[R1,#8]      ; R0 = mem[R1+8]

; Pre-indexed
LDR R0,[R1,#8]!     ; R1 = R1+8, then R0 = mem[R1]

; Post-indexed
LDR R0,[R1],#8      ; R0 = mem[R1], then R1 = R1+8

; Register-indexed
LDR R0,[R1,R2]      ; R0 = mem[R1+R2]

; PC-relative (literal pool)
LDR R0,=0x20000000  ; Load address via constant pool

4. Flow Control

Unconditional branches:

B label             ; Simple branch
BL function         ; Branch with Link (LR = PC+4)
BX Rm               ; Branch to address in Rm
BLX Rm              ; Branch with Link to Rm

Conditional branches:

Based on NZCV flags (Negative, Zero, Carry, oVerflow).

SuffixConditionFlags
EQEqualZ=1
NENot equalZ=0
GTGreater than (signed)Z=0 AND N=V
LTLess than (signed)N!=V
GEGreater than or equal (signed)N=V
LELess than or equal (signed)Z=1 OR N!=V
HIHigher (unsigned)C=1 AND Z=0
LSLower or same (unsigned)C=0 OR Z=1

Comparisons:

CMP R0,R1           ; Compare R0 and R1 (computes R0-R1, updates flags)
CMN R0,R1           ; Compare negative (computes R0+R1)
TST R0,R1           ; Test bits (AND, updates flags without storing)
TEQ R0,R1           ; Test equal (XOR, updates flags)

Loop example:

    MOV R0,#10      ; Counter
Boucle
    ; ... code ...
    SUBS R0,#1      ; Decrement and update flags
    BNE Boucle      ; While R0 != 0

5. Subroutines and Stack

AAPCS calling convention (ARM Architecture Procedure Call Standard):

RegisterUsagePreservation
R0-R3Parameters and returnCaller-saved
R4-R11Local variablesCallee-saved
R12TemporaryCaller-saved
R13 (SP)Stack-
R14 (LR)Return address-

Function call:

; Caller
    MOV R0,#5       ; First parameter
    MOV R1,#10      ; Second parameter
    BL MaFonction   ; Call (LR = return address)
    ; R0 contains the result

; Function (callee)
MaFonction PROC
    PUSH {R4,LR}    ; Save used registers

    ; Calculations using R0, R1, R4
    ADD R4,R0,R1
    MOV R0,R4       ; Result in R0

    POP {R4,PC}     ; Restore and return
    ENDP

Stack management:

The stack grows toward lower addresses (full descending).

PUSH {R0-R3,LR}     ; Multiple save
; SP = SP - 20 (5 registers × 4 bytes)

POP {R0-R3,PC}      ; Restore and return
; SP = SP + 20

Equivalent:

  • PUSH {Rx} = STMDB SP!, {Rx} (Store Multiple Decrement Before)
  • POP {Rx} = LDMIA SP!, {Rx} (Load Multiple Increment After)

6. Peripheral Access

Memory-mapped I/O:

Peripherals are accessible via specific memory addresses.

Example: GPIO (General Purpose Input/Output)

GPIOC (Port C) registers for STM32F103:

RegisterAddressUsage
CRL0x40011000Configuration for pins 0-7
CRH0x40011004Configuration for pins 8-15
IDR0x40011008Input Data Register (read)
ODR0x4001100COutput Data Register (write)
BSRR0x40011010Bit Set/Reset Register
BRR0x40011014Bit Reset Register

Configuring a pin as output:

; Configure PC10 as push-pull output, 50 MHz
    LDR R0,=0x40011004  ; GPIOC_CRH
    LDR R1,[R0]
    BIC R1,#(0xF << 8)  ; Clear bits for PC10
    ORR R1,#(0x3 << 8)  ; MODE=11 (50MHz), CNF=00 (push-pull)
    STR R1,[R0]

Bit manipulation:

; Set a bit (set)
LDR R0,=0x40011010  ; GPIOC_BSRR
MOV R1,#(1 << 10)   ; Bit 10
STR R1,[R0]         ; PC10 = 1

; Clear a bit (reset)
LDR R0,=0x40011014  ; GPIOC_BRR
MOV R1,#(1 << 10)
STR R1,[R0]         ; PC10 = 0

; Read a bit
LDR R0,=0x40011008  ; GPIOC_IDR
LDR R1,[R0]
TST R1,#(1 << 8)    ; Test bit 8
BEQ Bit_A_Zero      ; Branch if zero

7. Assembler Directives

Code organization:

    AREA MonCode, CODE, READONLY, ALIGN=2
    ; Code sections

    AREA MesDonnees, DATA, READWRITE
    ; Data sections

Data definitions:

; Constants
Valeur  EQU 42          ; Equivalent to #define

; Initialized data
Tableau DCD 1,2,3,4,5   ; Define Constant Data (32 bits)
Chaine  DCB "Hello",0   ; Define Constant Byte (8 bits)
Mot     DCW 0x1234      ; Define Constant Word (16 bits)

; Uninitialized data
Buffer  SPACE 100       ; Reserve 100 bytes

Declarations:

    EXPORT main         ; Make symbol visible externally
    IMPORT fonction     ; Import an external symbol

    PROC                ; Procedure start
    ENDP                ; Procedure end

    END                 ; End of source file

8. Optimizations

Code size:

Thumb-2 instructions are 16 or 32 bits. Prefer 16-bit instructions when possible.

; 16 bits
ADDS R0,R1,R2       ; If R0-R7 and result modifies flags

; 32 bits
ADD.W R0,R1,R2      ; Force 32 bits

Efficient loops:

; Inefficient (compares to 0 on every iteration)
    MOV R0,#0
Boucle
    ; ...
    ADD R0,#1
    CMP R0,#100
    BLT Boucle

; Efficient (decrement and implicitly test for zero)
    MOV R0,#100
Boucle
    ; ...
    SUBS R0,#1
    BNE Boucle

Avoiding branches:

Use conditional instructions when possible.

; With branch
    CMP R0,R1
    BLE Suivant
    MOV R2,R0
Suivant
    ; ...

; Without branch (IT = If-Then)
    CMP R0,R1
    IT GT
    MOVGT R2,R0     ; Executed if GT

PART D - Reflective Analysis and Perspectives

Skills acquired

Low-level understanding: Assembly programming forces you to understand exactly what happens at the processor level: every instruction, every memory access, every flag modification. This understanding helps debug and optimize C code.

Total hardware control: Direct access to peripheral registers, bit-level manipulation, precise timing. Essential for developing drivers or time-critical real-time code.

Optimization: Awareness of the cost of each instruction. Knowing where to optimize (critical loops) and when to let the compiler do the work.

Key takeaways

1. The stack is your friend (and your enemy): PUSH/POP must always be balanced. Forgetting a POP or popping the wrong register causes bugs that are difficult to trace.

2. LR must be saved: If a function calls other functions, LR must be saved (PUSH {LR}) and restored (POP {PC}) to return correctly.

3. Condition flags: Instructions with 'S' (ADDS, SUBS) modify the flags. Required for conditional branches. Be careful not to overwrite flags between CMP and Bxx.

4. Memory addressing: Clearly distinguish between address and value. LDR R0,=label loads the address, LDR R0,[R1] loads the value at the address contained in R1.

5. Documentation is essential: Always have at hand:

  • ARM Architecture Reference Manual (instruction set)
  • STM32 Reference Manual (peripheral addresses)
  • Cortex-M3 Technical Reference Manual (architecture)

Practical applications

Critical embedded systems: Bootloaders, initialization routines, critical interrupt handlers. Assembly code for precise timing or extreme optimization.

Peripheral drivers: Direct access to hardware registers for maximum performance. Useful when HALs (Hardware Abstraction Layers) are too heavy.

Reverse engineering: Understanding assembly allows analyzing compiled code, debugging obscure issues, or studying malware.

Inline assembly in C: Integrating a few assembly instructions in C code for local optimizations.

// GCC inline assembly example
__asm volatile (
    "MOV R0, #42\n"
    "ADD R1, R0, R0\n"
    : "=r" (result)
    : "r" (input)
    : "r0", "r1"
);

Assembly vs C

When to use assembly:

  • Time-critical code (ultra-fast ISRs)
  • Extreme optimization (intensive loops)
  • Access to special features (SIMD instructions)
  • Bootloaders and initialization code
  • Very constrained code size

When to prefer C:

  • Rapid development and maintainability
  • Portability across architectures
  • Complex business logic
  • 95% of cases!

Modern compilers optimize very well. Hand-written assembly is often only necessary for a few percent of the code.

Lab project feedback

The "Magic Wheel" project allowed applying all concepts:

  • Step 1: GPIO control, register discovery
  • Step 2: input reading, conditional logic
  • Step 3: loops and sequences
  • Step 4: timers and interrupts

Pedagogical progression: Each step adds complexity. We progressively build a complete system. A sense of accomplishment when seeing the LEDs light up according to our code.

Difficulties: Debugging in assembly is harder than in C. No easy printf. Intensive use of the debugger and register observation.

Limitations and future directions

Course limitations:

  • Little coverage of advanced interrupts (full NVIC)
  • No DMA or complex timers
  • Little optimized code (SIMD, DSP)
  • No real-time operating systems (RTOS)

Future directions:

  • Advanced Embedded C: inline assembly, optimizations
  • RTOS: FreeRTOS, tasks, synchronization
  • Compilation and linking: object files, linker
  • Advanced architectures: Cortex-M4 DSP, Cortex-M7 cache
  • Security: TrustZone, secure boot

Technological evolution

Current trends:

  • Cortex-M33: TrustZone for security
  • Cortex-M55: Helium extensions for machine learning
  • RISC-V: open-source alternative to ARM
  • Just-In-Time compilation: for powerful microcontrollers

Modern tools:

  • CMSIS (Cortex Microcontroller Software Interface Standard)
  • HAL (Hardware Abstraction Layer) generated automatically
  • Graphical IDEs (STM32CubeIDE, Mbed Studio)
  • Advanced debugging (Segger J-Link, Ozone)

Conclusion

ARM assembly programming is an essential step for deeply understanding how microcontrollers work. Even though 95% of modern code is written in C/C++, knowing assembly allows you to:

  • Better understand what the compiler does
  • Optimize critical code
  • Debug complex problems
  • Develop efficient drivers
  • Appreciate the power of high-level languages

The ARM Cortex-M architecture is ubiquitous in modern embedded systems (IoT, wearables, drones, automotive). Mastering it is a major asset for any embedded systems engineer.

Recommendations:

  • Practice regularly (small programs, challenges)
  • Read compiler-generated assembly code (GCC -S option)
  • Participate in reverse engineering CTFs (Capture The Flag)
  • Experiment with different architectures (ARM, RISC-V, x86)

Links to other courses:


📚 Course Documents

📖 Sequence 1 - ARM Introduction

Introduction to ARM Cortex-M architecture, registers, basic instruction set and memory organization.

📥 Download

📖 Sequence 2 - ARM Instructions

Complete ARM instruction set: arithmetic, logic, branches, and addressing modes.

📥 Download

📖 Sequence 3 - Functions and Stack

AAPCS calling convention, stack management, function prologue/epilogue and parameter passing.

📥 Download

📖 Instruction Set Reference

Complete summary table of the ARM Cortex-M instruction set with syntax and examples.

📥 Download


Course taught in 2022-2023 at INSA Toulouse, Department of Electrical and Computer Engineering.

Rédigé par Cédric Chanfreau, étudiant en ingénierie à l’INSA Toulouse.Written by Cédric Chanfreau, engineering student at INSA Toulouse.