⚙️ 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
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 :
| Registre | Nom | Usage |
|---|---|---|
| R0-R12 | Registres généraux | Calculs, données temporaires |
| R13 (SP) | Stack Pointer | Pointeur de pile |
| R14 (LR) | Link Register | Adresse de retour de fonction |
| R15 (PC) | Program Counter | Adresse 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égion | Adresses | Usage |
|---|---|---|
| Code | 0x00000000 - 0x1FFFFFFF | Flash, instructions |
| SRAM | 0x20000000 - 0x3FFFFFFF | RAM, données |
| Périphériques | 0x40000000 - 0x5FFFFFFF | Registres memory-mapped |
| Système | 0xE0000000 - 0xFFFFFFFF | NVIC, SysTick, debug |
Pipeline 3 étages :
- Fetch : lecture instruction
- Decode : décodage
- 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 :
| Instruction | Syntaxe | Description |
|---|---|---|
| ADD | ADD Rd, Rn, Rm | Rd = Rn + Rm |
| SUB | SUB Rd, Rn, Rm | Rd = Rn - Rm |
| MUL | MUL Rd, Rn, Rm | Rd = Rn × Rm |
| SDIV | SDIV Rd, Rn, Rm | Rd = Rn / Rm (signé) |
| UDIV | UDIV Rd, Rn, Rm | Rd = Rn / Rm (non signé) |
Variantes avec flags : ADDS, SUBS (modifient NZCV).
Instructions logiques :
| Instruction | Description |
|---|---|
| AND Rd, Rn, Rm | ET logique |
| ORR Rd, Rn, Rm | OU logique |
| EOR Rd, Rn, Rm | OU exclusif (XOR) |
| BIC Rd, Rn, Rm | Bit Clear (Rd = Rn AND NOT Rm) |
| MVN Rd, Rm | NOT (inversion) |
Décalages :
| Instruction | Description |
|---|---|
| LSL Rd, Rn, #n | Logical Shift Left (décalage gauche) |
| LSR Rd, Rn, #n | Logical Shift Right (décalage droite, complète avec 0) |
| ASR Rd, Rn, #n | Arithmetic Shift Right (conserve le signe) |
| ROR Rd, Rn, #n | Rotate 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 :
| Instruction | Taille | Description |
|---|---|---|
| LDR Rd,[Rn] | 32 bits | Charge mot |
| LDRH Rd,[Rn] | 16 bits | Charge demi-mot (halfword) |
| LDRB Rd,[Rn] | 8 bits | Charge 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 :
| Instruction | Taille | Description |
|---|---|---|
| STR Rd,[Rn] | 32 bits | Stocke mot |
| STRH Rd,[Rn] | 16 bits | Stocke demi-mot |
| STRB Rd,[Rn] | 8 bits | Stocke 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).
| Suffixe | Condition | Flags |
|---|---|---|
| EQ | Égal | Z=1 |
| NE | Non égal | Z=0 |
| GT | Supérieur (signé) | Z=0 ET N=V |
| LT | Inférieur (signé) | N≠V |
| GE | Supérieur ou égal (signé) | N=V |
| LE | Inférieur ou égal (signé) | Z=1 OU N≠V |
| HI | Supérieur (non signé) | C=1 ET Z=0 |
| LS | Infé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) :
| Registre | Usage | Sauvegarde |
|---|---|---|
| R0-R3 | Paramètres et retour | Caller-saved |
| R4-R11 | Variables locales | Callee-saved |
| R12 | Temporaire | Caller-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 :
| Registre | Adresse | Usage |
|---|---|---|
| CRL | 0x40011000 | Configuration broches 0-7 |
| CRH | 0x40011004 | Configuration broches 8-15 |
| IDR | 0x40011008 | Input Data Register (lecture) |
| ODR | 0x4001100C | Output Data Register (écriture) |
| BSRR | 0x40011010 | Bit Set/Reset Register |
| BRR | 0x40011014 | Bit 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 :
- Architecture Matérielle - S6 : pipeline, cache
- Microcontrôleur - S6 : périphériques, C embarqué
- Électronique Fonctions Numériques - S6 : bus I2C/SPI
- Temps Réel - S8 : contraintes temporelles
📚 Documents de Cours
📖 Séquence 1 - Introduction ARM
Introduction à l'architecture ARM Cortex-M, registres, jeu d'instructions de base et organisation mémoire.
📖 Séquence 2 - Instructions ARM
Jeu d'instructions ARM complet : arithmétique, logique, branchements, et modes d'adressage.
📖 Séquence 3 - Fonctions et Pile
Convention d'appel AAPCS, gestion de la pile, prologue/épilogue de fonctions et passage de paramètres.
📖 Référence Jeu d'Instructions
Tableau synthétique complet du jeu d'instructions ARM Cortex-M avec syntaxe et exemples.
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
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:
| Register | Name | Usage |
|---|---|---|
| R0-R12 | General purpose registers | Calculations, temporary data |
| R13 (SP) | Stack Pointer | Stack pointer |
| R14 (LR) | Link Register | Function return address |
| R15 (PC) | Program Counter | Current 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:
| Region | Addresses | Usage |
|---|---|---|
| Code | 0x00000000 - 0x1FFFFFFF | Flash, instructions |
| SRAM | 0x20000000 - 0x3FFFFFFF | RAM, data |
| Peripherals | 0x40000000 - 0x5FFFFFFF | Memory-mapped registers |
| System | 0xE0000000 - 0xFFFFFFFF | NVIC, SysTick, debug |
3-stage pipeline:
- Fetch: instruction read
- Decode: decoding
- Execute: execution
Consequence: the PC always points 2 instructions ahead (PC+4 in Thumb-2).
2. Thumb-2 Instruction Set
Arithmetic instructions:
| Instruction | Syntax | Description |
|---|---|---|
| ADD | ADD Rd, Rn, Rm | Rd = Rn + Rm |
| SUB | SUB Rd, Rn, Rm | Rd = Rn - Rm |
| MUL | MUL Rd, Rn, Rm | Rd = Rn × Rm |
| SDIV | SDIV Rd, Rn, Rm | Rd = Rn / Rm (signed) |
| UDIV | UDIV Rd, Rn, Rm | Rd = Rn / Rm (unsigned) |
Variants with flags: ADDS, SUBS (modify NZCV).
Logic instructions:
| Instruction | Description |
|---|---|
| AND Rd, Rn, Rm | Logical AND |
| ORR Rd, Rn, Rm | Logical OR |
| EOR Rd, Rn, Rm | Exclusive OR (XOR) |
| BIC Rd, Rn, Rm | Bit Clear (Rd = Rn AND NOT Rm) |
| MVN Rd, Rm | NOT (inversion) |
Shifts:
| Instruction | Description |
|---|---|
| LSL Rd, Rn, #n | Logical Shift Left |
| LSR Rd, Rn, #n | Logical Shift Right (fills with 0) |
| ASR Rd, Rn, #n | Arithmetic Shift Right (preserves sign) |
| ROR Rd, Rn, #n | Rotate 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:
| Instruction | Size | Description |
|---|---|---|
| LDR Rd,[Rn] | 32 bits | Load word |
| LDRH Rd,[Rn] | 16 bits | Load halfword |
| LDRB Rd,[Rn] | 8 bits | Load byte |
| LDRSB Rd,[Rn] | 8 bits signed | Load byte with sign extension |
| LDRSH Rd,[Rn] | 16 bits signed | Load halfword with sign extension |
Store instructions:
| Instruction | Size | Description |
|---|---|---|
| STR Rd,[Rn] | 32 bits | Store word |
| STRH Rd,[Rn] | 16 bits | Store halfword |
| STRB Rd,[Rn] | 8 bits | Store 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).
| Suffix | Condition | Flags |
|---|---|---|
| EQ | Equal | Z=1 |
| NE | Not equal | Z=0 |
| GT | Greater than (signed) | Z=0 AND N=V |
| LT | Less than (signed) | N!=V |
| GE | Greater than or equal (signed) | N=V |
| LE | Less than or equal (signed) | Z=1 OR N!=V |
| HI | Higher (unsigned) | C=1 AND Z=0 |
| LS | Lower 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):
| Register | Usage | Preservation |
|---|---|---|
| R0-R3 | Parameters and return | Caller-saved |
| R4-R11 | Local variables | Callee-saved |
| R12 | Temporary | Caller-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:
| Register | Address | Usage |
|---|---|---|
| CRL | 0x40011000 | Configuration for pins 0-7 |
| CRH | 0x40011004 | Configuration for pins 8-15 |
| IDR | 0x40011008 | Input Data Register (read) |
| ODR | 0x4001100C | Output Data Register (write) |
| BSRR | 0x40011010 | Bit Set/Reset Register |
| BRR | 0x40011014 | Bit 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:
- Hardware Architecture - S6: pipeline, cache
- Microcontroller - S6: peripherals, embedded C
- Digital Electronics - S6: I2C/SPI bus
- Real-Time Systems - S8: timing constraints
📚 Course Documents
📖 Sequence 1 - ARM Introduction
Introduction to ARM Cortex-M architecture, registers, basic instruction set and memory organization.
📖 Sequence 2 - ARM Instructions
Complete ARM instruction set: arithmetic, logic, branches, and addressing modes.
📖 Sequence 3 - Functions and Stack
AAPCS calling convention, stack management, function prologue/epilogue and parameter passing.
📖 Instruction Set Reference
Complete summary table of the ARM Cortex-M instruction set with syntax and examples.
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.