RÉSUMÉ DÉTAILLÉ – ARCHITECTURE DES ORDINATEURS
1. QU’EST-CE QU’UN ORDINATEUR ?
Un ordinateur est une machine électronique programmable capable
d’exécuter une suite d’instructions (programme) stockées en mémoire
afin de traiter des données.
Il fonctionne uniquement avec des signaux électriques représentant
des bits (0 ou 1). Toute information (nombre, texte, image, son,
instruction machine) est encodée sous forme binaire.
Un ordinateur transforme donc :
Programme (abstrait, écrit en C++ par exemple)
→ Instructions machine (binaire)
→ Signaux électriques
→ Actions physiques (affichage, moteur, communication)
1.1 Grandes catégories de systèmes
1) Microprocesseur (CPU seul)
Un microprocesseur :
Exécute les instructions
Ne contient PAS de mémoire interne importante
Nécessite :
o RAM externe
o Mémoire de stockage
o Contrôleurs I/O
o Alimentation régulée
On le retrouve dans :
PC
Serveurs
Téléphones (SoC plus complexe)
Caractéristique principale : usage général.
2) Microcontrôleur (MCU)
Un microcontrôleur intègre sur une seule puce :
CPU
RAM
Mémoire Flash
Périphériques (ADC, timers, UART, SPI, I²C)
Avantages :
Faible consommation
Faible coût
Taille compacte
Très prédictible en temps réel
Exemple : ATmega328P (Arduino Uno)
3) Systèmes embarqués
Un système embarqué est un système informatique conçu pour
une tâche précise.
Exemples :
ABS automobile
Lave-vaisselle
Robot industriel
Contrôleur de moteur
Caractéristiques :
Optimisé pour une fonction
Ressources limitées
Forte contrainte temps réel
1.2 Les 3 composantes fondamentales
Tout système informatique contient :
1) CPU (Unité centrale)
Rôle :
Récupère instruction en mémoire
La décode
L’exécute
Il contient :
Registres
ALU
Unité de contrôle
Compteur de programme (PC)
2) Mémoire
Stocke :
Programme
Données
Types :
Registres (ultra rapide)
Cache
RAM
Flash
SSD
3) Périphériques
Interface avec le monde réel :
Entrées :
Capteurs
Boutons
Réseau
Sorties :
LED
Moteur
Écran
4) Les bus
Trois bus principaux :
Bus d’adresses
Permet au CPU de sélectionner une case mémoire.
Bus de données
Transporte les données.
Bus de contrôle
Indique :
Lecture
Écriture
Interruption
1.3 Architectures : Von Neumann vs Harvard
Architecture Von Neumann
Une seule mémoire pour code et données
Un seul bus mémoire
Avantages :
Flexible
Code auto-modifiable
Inconvénient :
Goulot d’étranglement (CPU ne peut pas lire instruction ET
donnée simultanément)
Architecture Harvard
Mémoire code séparée
Mémoire données séparée
Bus séparés
Avantages :
Accès parallèle
Plus rapide
Plus sécuritaire
Inconvénient :
Moins flexible
Harvard modifiée (AVR)
Flash → programme
SRAM → données
Instructions spéciales (LPM) pour lire Flash
2. BINAIRE ET HEXADÉCIMAL
Bits et octets
1 bit = 0 ou 1
8 bits = 1 octet
1 octet = 2⁸ = 256 valeurs possibles
Conversion binaire → décimal
Exemple :
10110100₂
= 1×2⁷ + 0×2⁶ + 1×2⁵ + 1×2⁴ + 0×2³ + 1×2² + 0×2¹ + 0×2⁰
= 128 + 32 + 16 + 4
= 180
Hexadécimal
Base 16 :
0–9, A–F
1 chiffre hex = 4 bits
1011₂ = B
0100₂ = 4
10110100₂ = B4₁₆
Pourquoi utiliser l’hex ?
Compact
Lisible
Correspond directement aux bits
Exemple :
Adresse 32 bits :
0x0800FFFC
3. CPU – FONCTIONNEMENT INTERNE
3.1 Cycle Fetch-Decode-Execute
Chaque instruction passe par :
1. Fetch → lire instruction mémoire
2. Decode → interpréter
3. Execute → effectuer opération
4. Write-back → écrire résultat
Le compteur de programme (PC) contient l’adresse de la prochaine
instruction. (Est un pointeur)
3.2 Jeu d’instructions (ISA)
Définit :
Instructions disponibles
Format binaire
RISC
Instructions simples
Exécution rapide
Exemple : ARM, AVR
CISC
Instructions complexes
Exemple : x86
3.3 Registres
Mémoire interne ultra-rapide
AVR : 32 registres 8 bits
Pourquoi importants ?
Accès RAM = plusieurs cycles
Accès registre = 1 cycle
Toute opération doit passer par registres.
3.4 ALU
Effectue :
Addition
Soustraction
AND
OR
XOR
Décalages
Met à jour les flags :
Z → zéro
C → retenue
N → négatif
V → overflow
3.5 Horloge
Fréquence = nombre de cycles par seconde
Arduino Uno = 16 MHz
PC moderne = >5 GHz
Une instruction peut prendre plusieurs cycles.
3.6 Pipeline
Découpe instruction en étapes :
Fetch
Decode
Execute
Memory
Write-back
Permet exécution parallèle partielle.
Améliore débit mais complexifie gestion branchements.
3.7 Stack et mémoire programme
Stack (pile)
Contient :
Variables locales
Adresse de retour
Contexte
Croît vers le bas.
Heap (tas)
Allocation dynamique :
malloc()
free()
Danger :
Stack et heap peuvent entrer en collision.
4. MÉMOIRE
4.1 Hiérarchie mémoire
Du plus rapide au plus lent :
Registres
Cache
RAM
SSD
Compromis :
Vitesse ↔ Capacité ↔ Coût
AVR :
Pas de cache
Comportement déterministe
4.2 Espaces mémoire AVR
Mémoire données :
0x0000–0x001F → registres
0x0020–0x005F → I/O
0x0100–0x08FF → SRAM
Mémoire programme :
Flash séparée
5. ENCODAGE NUMÉRIQUE
5.1 Endianness (Boutisme)
Little-endian
Octet faible en premier
Intel, ARM, AVR
Big-endian
Octet fort en premier
TCP/IP
Reconstruction portable :
Little-endian :
uint16_t v = buffer[0] | (buffer[1] << 8);
Big-endian :
uint16_t v = (buffer[0] << 8) | buffer[1];
5.2 IEEE 754 – Float
Structure 32 bits :
1 bit signe
8 bits exposant
23 bits mantisse
Valeur :
(-1)^s × 1.m × 2^(exp - 127)
Valeurs spéciales :
Exp = 255, mantisse = 0 → infini
Exp = 255, mantisse ≠ 0 → NaN
Float ≈ 7 chiffres significatifs
Ne jamais comparer avec ==
Toujours utiliser epsilon.
5.3 ASCII et UTF-8
ASCII :
7 bits
128 caractères
UTF-8 :
1 à 4 octets
Compatible ASCII
Attention :
Nombre de caractères ≠ nombre d’octets en UTF-8
6. PÉRIPHÉRIQUES
6.1 Principe général
Le CPU interagit avec périphériques via registres de contrôle.
Lire/écrire dans ces registres configure et contrôle le périphérique.
6.2 Protocoles
UART :
Asynchrone
Bits start/stop
SPI :
Synchrone
Rapide
I²C :
Bus 2 fils
Synchrone
Adressable
6.3 Memory-mapped I/O
Registres périphériques placés dans espace mémoire.
Écriture à une adresse spéciale = commande matériel.
AVR :
Hybrid :
o IN/OUT rapides
o Memory-mapped
7. Machine de Turing – Fondement théorique de l’informatique
La machine de Turing, proposée par Alan Turing en 1936, est un
modèle mathématique abstrait servant à définir formellement ce
qu’est un calcul.
Elle ne représente pas un ordinateur réel, mais un modèle théorique
permettant de comprendre les limites fondamentales du calcul.
7.1 Structure d’une machine de Turing
Elle est composée de :
1) Un ruban infini
Divisé en cases
Chaque case contient un symbole (0, 1 ou symbole vide)
Représente la mémoire
Contrairement à la RAM moderne (accès aléatoire), la machine de
Turing accède au ruban de manière séquentielle.
2) Une tête de lecture/écriture
Elle peut :
Lire le symbole courant
Écrire un symbole
Se déplacer à gauche ou à droite
Elle représente le processeur (CPU).
3) Un ensemble fini d’états
La machine possède :
Un nombre fini d’états internes
Une table de transition
Chaque règle indique :
SI (état courant, symbole lu)
ALORS (écrire symbole, déplacer tête, changer d’état)
Exemple simple de transition
État q0, lit 1 →
écrire 0
aller à droite
passer à état q1
C’est l’équivalent d’une instruction machine.
Pourquoi c’est fondamental ?
Alan Turing a démontré que :
Toute fonction calculable peut être exécutée par une machine de
Turing.
C’est la thèse de Church-Turing.
Cela signifie :
Tout algorithme programmable
Tout logiciel moderne
Tout microcontrôleur
Tout PC
Peut être modélisé comme une machine de Turing.
Équivalence avec un ordinateur moderne
Ordinateur
Machine de Turing
réel
Ruban RAM
Tête de lecture CPU
États internes Registres
Table de transition Programme
Déplacement Incrémentation
gauche/droite du PC
La différence majeure :
Machine de Turing → accès séquentiel
Ordinateur moderne → accès aléatoire (RAM)
Cela améliore énormément la performance, mais ne change pas la
puissance théorique.
Machine de Turing universelle
Turing a aussi défini une machine capable de simuler toutes les autres
machines de Turing.
C’est le concept fondamental du :
➡ Ordinateur programmable universel
Cela signifie :
Le matériel ne change pas
Seul le programme change
Un Arduino, un PC ou un superordinateur sont tous universels.
Limites du calcul – Problème de l’arrêt
Turing a démontré qu’il est impossible d’écrire un programme capable
de déterminer, pour tout autre programme, s’il va s’arrêter ou non.
C’est le problème de l’arrêt (Halting Problem).
Conséquence :
Il existe des problèmes mathématiquement indécidables.
Cela montre que :
Même les ordinateurs parfaits ont des limites fondamentales.
Complexité et ressources
Même si un problème est calculable (Turing-complet), il peut être :
Trop long à calculer
Nécessiter trop de mémoire
Exemple :
Certains problèmes exponentiels deviennent impossibles à résoudre en
pratique.
Donc :
Calculable ≠ faisable efficacement.
Pourquoi c’est important en architecture ?
La machine de Turing permet de comprendre que :
1. Tous les processeurs peuvent exécuter les mêmes algorithmes.
2. Les différences d’architecture influencent seulement :
o La performance
o La consommation
o L’efficacité
Mais pas la puissance théorique.
Lien avec les systèmes embarqués
Même un microcontrôleur 8 bits comme l’AVR est Turing-complet si :
Il a suffisamment de mémoire
Il peut exécuter des boucles
Cela signifie qu’en théorie :
Un Arduino peut exécuter n’importe quel algorithme, mais plus
lentement qu’un PC.
8. Machine est little-endian et big-endian
bit7 bit6 bit5 bit4 bit3 bit2 bit1 bit0
0 1 0 1 0 1 1 0
bit0 = bit le moins significatif (LSB)
bit7 = bit le plus significatif (MSB)
0x12345678
0x12 = octet de poids fort (MSB)
0x78 = octet de poids faible (LSB)
Le poids faible correspond à la partie droite du nombre.
Exemple little-endian :
La machine est little-endian car…
La valeur est : 0x12345678
En mémoire, on observe :
[0] = 0x78
[1] = 0x56
[2] = 0x34
[3] = 0x12
C’est la définition du little-endian : L’octet le moins significatif est
stocké en premier en mémoire.
Exemple Big-endian:
La valeur est : 0x12345678
En mémoire, on observe :
[0] = 0x12
[1] = 0x34
[2] = 0x56
[3] = 0x78
Big-endian signifie l’octet de poids fort (Most Significant Byte – MSB)
est stocké en premier en mémoire.
Laboratoire :
a) Que signifie #include <iostream> ?
La directive #include <iostream> indique au compilateur d’inclure la
bibliothèque standard d’entrée/sortie du C++.
La bibliothèque iostream permet d’utiliser des objets comme std::cout
(affichage à l’écran) et std::cin (lecture au clavier).
Sans cette ligne, le compilateur ne reconnaîtrait pas std::cout.
b) À quoi sert return 0; à la fin de main ?
L’instruction return 0; indique que le programme s’est terminé correctement.
La valeur 0 signifie qu’il n’y a eu aucune erreur lors de l’exécution.
Si une autre valeur est retournée (ex. return 1;), cela peut indiquer une erreur.
c) Est-ce qu’il est possible d’écrire la ligne std::cout … sans la mettre dans la
fonction main ? Pourquoi ?
Non, ce n’est pas possible. En C++, toutes les instructions exécutables doivent se
trouver à l’intérieur d’une fonction. La fonction main est le point d’entrée du
programme : c’est elle qui démarre l’exécution. Si on écrit std::cout en dehors
d’une fonction, le programme ne compilera pas, car les instructions ne peuvent
pas être placées directement dans la zone globale.
d) Quelle est la différence entre int16_t et uint16_t ?
int16_t est un entier signé sur 16 bits.
uint16_t est un entier non signé sur 16 bits.
La différence principale est donc que int16_t accepte les nombres négatifs,
tandis que uint16_t accepte seulement des nombres positifs.
e) Pourquoi utilise-t-on <cstdint> plutôt que simplement int ?
La bibliothèque <cstdint> permet d’utiliser des types de taille fixe comme
int16_t, uint16_t, int32_t, etc.
Le type int n’a pas une taille garantie : selon le système, il peut être 16 bits, 32
bits ou même 64 bits.
En utilisant <cstdint>, on s’assure que le type a exactement le nombre de bits
voulu.
f) Que signifie le f après 23.5f ?
Le f indique que la valeur est de type float (nombre réel en simple précision).
Sans le f, le nombre 23.5 est considéré par défaut comme un double (double
précision). Donc 23.5f signifie que la constante est explicitement un float.
g) Pourquoi faut-il caster octet en (int) pour l’affichage en hexadécimal ?
Le type uint8_t est généralement défini comme un alias de unsigned char.
Or, lorsqu’on affiche un unsigned char avec std::cout, il est interprété comme
un caractère ASCII et non comme un nombre.
Donc, si on écrit :
std::cout << std::hex << octet;
le programme risque d’afficher un caractère (par exemple ´´´´) au lieu de la valeur
numérique en hexadécimal.
En faisant un cast :
(int)octet
on force la conversion vers un entier (int).
Ainsi, std::cout l’affiche comme une valeur numérique (par exemple b4) et
non comme un caractère.
h) Quelle est la taille réelle en mémoire obtenue avec sizeof ?
struct DonneesAxe {
int16_t position; // centièmes de degré
int16_t vitesse; // dixièmes de degré/seconde
uint16_t courant; // milliampères
};
Chaque membre est un entier de 16 bits.
int16_t = 2 octets
int16_t = 2 octets
uint16_t = 2 octets
Total théorique : 2 + 2 + 2 = 6 octets
i) La taille de DonneesBras est-elle exactement sizeof(uint8_t) + 6 *
sizeof(DonneesAxe) ? Pourquoi ?
struct DonneesAxe {
int16_t position;
int16_t vitesse;
uint16_t courant;
};
struct DonneesBras {
uint8_t sequence; // numéro de trame
DonneesAxe axes[6]; // tableau de 6 axes
};
Calcul théorique :
uint8_t sequence = 1 octet
DonneesAxe = 6 octets
6 axes = 6 × 6 = 36 octets
Total théorique :
1 + 36 = 37 octets
Non, car elle donne 38 octets à cause du padding.
0 1 2 3 4 5 6 7
⬜ ⬛ ⬜ ⬜ ⬜ ⬜ ⬜ ⬜ char + Padding + axe 1
⬜⬜⬜⬜⬜⬜ axe 2
⬜⬜⬜⬜⬜⬜ axe 3
⬜⬜⬜⬜⬜⬜ axe 4
⬜⬜⬜⬜⬜⬜ axe 5
⬜⬜⬜⬜⬜⬜ axe 6
j) Dessinez la disposition mémoire de chaque structure en indiquant les octets
de padding et Quelle version est la plus compacte ? Pourquoi ? Est-ce toujours
la plus performante ?
Le padding, ce sont des octets vides ajoutés automatiquement par le compilateur
dans une structure.
Pourquoi le processeur veut ça ?
Parce que :
Accès aligné = 1 seul cycle mémoire
Accès non aligné = plusieurs cycles
Donc sans padding :
plus lent
parfois même interdit sur certains processeurs embarqués
L’alignement veut dire :
👉 certains types doivent commencer à des adresses spécifiques.
Par exemple :
Doit commencer à une
Type Taille
adresse multiple de
uint8_t 1 1 (n’importe où)
uint16_t 2 2
Doit commencer à une
Type Taille
adresse multiple de
uint32_t 4 4
uint64_t 8 8
Exemple :
struct Test {
uint8_t a; // adresse 0
uint32_t b; // ?
};
Sans padding :
0:a
1:b
2:b
3:b
4:b
Mais 1 n’est pas multiple de 4 ❌
Donc le compilateur ajoute :
0:a
1 : padding
2 : padding
3 : padding
4:b
5:b
6:b
7:b
Maintenant b commence à 4 ✔
struct VersionA {
uint8_t a; // 1 octet
uint32_t b; // 4 octets
uint8_t c; // 1 octet
};
Octet 0 : a
Octets 1-3 : padding
Octets 4-7 : b
Octet 8 : c
Octets 9-11 : padding
a occupe 1 octet
3 octets de padding sont ajoutés pour aligner b sur un multiple de 4
b occupe 4 octets
c occupe 1 octet
3 octets de padding sont ajoutés pour que la structure ait une taille multiple de 4
➡ Taille totale : 12 octets
struct VersionB {
uint8_t a; // 1 octet
uint8_t c; // 1 octet
uint32_t b; // 4 octets
};
Octet 0 : a
Octet 1 : c
Octets 2-3 : padding
Octets 4-7 : b
a et c occupent 2 octets
2 octets de padding sont ajoutés pour aligner b
b occupe 4 octets
La taille totale est déjà multiple de 4
➡ Taille totale : 8 octets
#pragma pack(push, 1)
struct VersionC {
uint32_t b;
uint8_t a;
uint8_t c;
};
#pragma pack(pop)
Explication :
Le #pragma pack(1) désactive le padding.
Les membres sont placés directement les uns après les autres.
➡ Taille totale : 6 octets
La version la plus compacte est VersionC (6 octets), car le #pragma pack(1)
supprime complètement le padding.
Le padding est performant pour augmenter vitesse. Annuler le padding sauve de la
mémoire mais ralenti programme.
k) Que se passe-t-il si on oublie delete[] ? (Exercice 3.4)
Si on oublie delete[], la mémoire allouée avec new[] n’est jamais libérée.
➡ Cela crée une fuite mémoire (memory leak).
o new → delete
o new[] → delete[]
1. Pourquoi doit-on caster en char* lors de l'utilisation de \`read()\` et \`write()\`
avec des données binaires ?
R : Pour éviter les erreurs de compilation
Les fonctions :
std::istream::read(char* s, std::streamsize n);
std::ostream::write(const char* s, std::streamsize n);
attendent un pointeur vers char.
Si tu as par exemple :
std::vector<uint8_t> buffer;
alors :
[Link]()
retourne un uint8_t*, pas un char*.
Le cast (ou \`reinterpret_cast\`) indique qu'on veut lire/écrire les octets bruts
de n'importe quel type.
2. Qu'affichera ce code ?
uint16_t valeur = 300;
uint8_t petit = valeur;
std::cout << (int)petit;
R : 44 car 8 bits ne peuvent contenir que des valeurs entre 0 et 255.
uint8_t a = 250;
uint8_t b = 10;
uint8_t c = a + b;
std::cout << (int)c;
R:4
-++
+++
3. Pourquoi doit-on caster uint8_t en int pour l'afficher en hexadécimal avec
std::cout ?
R : On caste uint8_t en int parce que uint8_t est traité comme un unsigned
char, et std::cout affiche les char comme des caractères ASCII, pas comme
des nombres.
4. Ce code produit potentiellement des résultats incorrects.
Pourquoi ?
struct Paquet {
uint8_t type;
uint32_t donnees;
};
uint8_t buffer[5] = {0x01, 0x12, 0x34, 0x56, 0x78};
Paquet* p = reinterpret_cast(buffer);
std::cout << std::hex << p->donnees;
Il faut ecrire : Paquet* p = reinterpret_cast<Paquet*>(buffer);
R : Ce code peut produire des résultats incorrects à cause du padding : le
compilateur peut ajouter des octets invisibles entre type et donnees pour
aligner le uint32_t sur 4 octets. Le reinterpret_cast lit au-delà des données
valides du buffer. Il faut #pragma pack(push, 1) ou équivalent pour une
structure de 5 octets.
Pourquoi faut “compacter” la structure ?
Parce que ton buffer contient exactement 5 octets, mais ta structure peut en
prendre 8 en mémoire.
Ici, pas besoin de nommer explicitement le "pragma pack", simplement
comprendre qu'il faut indiquer au compilateur que la structure doit être
"compactée".
5. Quelle est la différence fondamentale entre une conversion de type et une
réinterprétation de bits ?
Une conversion de type transforme une valeur vers un autre type (ex. int vers
float, la valeur est recalculée). Une réinterprétation de bits ne change pas les
bits en mémoire, elle change seulement la façon dont ils sont interprétés par le
programme. La réinterprétation est dangereuse car elle ne garantit pas un
résultat cohérent.
6. Qu'est-ce qu'un "file descriptor" (descripteur de fichier) ?
R : Un entier identifiant un fichier ouvert
7. Décrivez les trois étapes du cycle fetch-decode-execute.
Fetch (recherche) : Récupère l'instruction depuis la mémoire (ou cache)
Decode (décodage) : Il analyse l’instruction pour déterminer quelle opération doit
être effectuée et sur quelles données. (Sélectionne registres, configure ALU)
Execute (exécution) : Exécute l'opération dans l'ALU ou autre unité fonctionnelle.
Le compteur d'instructions (Program Counter) est incrémenté pour pointer vers
l'instruction suivante.
8. Décrivez la hiérarchie mémoire d'un ordinateur moderne, du plus rapide au
plus lent.
Registres → Cache (L1, L2, L...) → RAM → Stockage (SSD/HDD). Plus une
mémoire est rapide, plus elle est chère et petite. Cette hiérarchie optimise le
compromis entre vitesse, capacité et coût.
9. Quel type devrait-on utiliser pour stocker une valeur monétaire précise (ex:
prix en dollars) ?
R : On utilise un entier (ex. stocker 10,99 $ comme 1099 cents) pour éviter les
erreurs d’arrondi des nombres flottants, car float et double ne représentent pas
exactement les décimales en base 2.
10. Les chiffres ASCII ('0' à '9') sont-ils encodés de manière séquentielle ?
Pourquoi est-ce utile ?
C’est utile parce qu’on peut facilement convertir un caractère en entier avec
une simple opération comme : int chiffre = caractere - '0';
11. Pourquoi une chaîne UTF-8 ne compte-t-elle pas nécessairement le même
nombre d'octets que le nombre de caractères ?
R : Parce que UTF-8 est un encodage à longueur variable : un caractère
peut occuper de 1 à 4 octets selon le symbole (ex. lettres accentuées, emojis,
caractères asiatiques). UTF-8 est dominant pour tous les pays et toutes les
langues sur Internet, il est utilisé dans la plupart des standards, est souvent le
seul encodage autorisé et est pris en charge par tous les systèmes
d’exploitation et langages de programmation modernes.
12. Vous devez lire une structure de données de 5 octets depuis un fichier binaire
créé par un autre système. Votre structure C++ fait 8 octets à cause du
padding. Quelle approche devriez-vous utiliser ?
R : Forcer la structure à être compacte (sans padding) avec une directive de
compilation, OU lire les champs individuellement octet par octet plutôt que la
structure complète. Le padding automatique est utile pour la performance
mais incompatible avec les formats binaires externes définis.
13. Si ptr est un pointeur vers uint32_t et a la valeur 0x1000, quelle sera la valeur
de ptr + 3 ?
R : 0x100C
Parce que ptr pointe vers un uint32_t (4 octets).
Donc ptr + 3 avance de 3 × 4 octets = 12 octets → 0x1000 + 0xC = 0x100C.
Comme ptr pointe vers un uint32_t : 1 déplacement = 4 octets
Donc :
ptr + 3 = 3 × 4 octets = 12 octets
12 en hexadécimal = 0xC
14. Identifiez le problème dans ce code :
uint8_t* creer_buffer()
{
uint8_t buffer[100];
// ... initialisation ...
return buffer;
}
R : Le problème est que buffer est une variable locale créée sur la pile. Quand
la fonction se termine, cette mémoire est détruite, donc le pointeur retourné
pointe vers une zone invalide. Utiliser ce pointeur plus tard peut causer un
comportement imprévisible. Il faudrait allouer dynamiquement avec new[] ou
utiliser un std::vector. Les variables locales sont généralement allouées sur
la pile (stack), une zone mémoire gérée automatiquement qui grandit et
rétrécit au fil des appels de fonctions.
int* creer()
{
static int x = 5;
return &x;
}
R : code valide
std::vector<uint8_t> f()
{
std::vector<uint8_t> v(100);
return v;
}
R : code valide
15. Pourquoi utilise-t-on delete[] et non delete pour libérer un tableau alloué
avec new[] ?
R : Parce que new[] crée un tableau complet d’éléments, donc delete[] est
nécessaire pour libérer correctement tous les éléments, alors que delete ne
libère qu’un seul objet.
16. Quelle opération permet d'extraire le bit 5 (en comptant de 0) d'un octet ?
R : (octet & 0x20) >> 5
Parce que 0x20 correspond au bit 5 (0010 0000), on fait un AND pour l’isoler
puis on décale à droite de 5 pour obtenir 0 ou 1.
Oui, dans la plupart des cas on le ramène à droite (bit 0)
On fait ça pour obtenir simplement 0 ou 1, ce qui permet de l’utiliser
facilement dans une condition ou un calcul.
Extraire un bit sert à vérifier l’état d’un seul bit précis dans une variable.
17. Une trame contient un champ longueur indiquant le nombre d'octets de
données utiles. Où devrait-on placer ce champ ?
R : Le champ longueur doit être au début (après la synchro, si présente) pour
savoir combien d'octets lire avant de traiter la trame. Sinon, impossible de
connaître à l'avance la quantité d'octets à lire, à moins d'avoir un code de
synchronisation de fin unique.
18. Quel type de mémoire est utilisé pour l'interaction avec des périphériques
en utilisant des adresses mémoire ?
R : mémoire mappée en entrée/sortie
19. Quels sont les trois composants fondamentaux d'un système informatique
?
CPU Mémoire Périphérique
20. Quel composant matériel est responsable de la gestion des adresses
virtuelles en mémoire virtuelle?
R: Unité de gestion de mémoire (MMU)
21. Quel terme décrit un système informatique conçu pour une tâche
spécifique, souvent avec des ressources limitées ?
Réponse attendue: Système embarqué
22. Déterminer la taille d’un fichier
[Link](0, std::ios::end); // Aller à la fin
size_t taille = [Link](); // Position = taille
[Link](0, std::ios::beg); // Revenir au début
23. Espace mémoire d’un programme
.text : code du programme
.data : variables globales/statique initialisées
.bss : variables globales/statique non initialisées
stack (pile) : variables locales, appels de fonctions
heap (tas) : mémoire dynamique
24. appels système (surtout sur PC/Linux)
Un appel système est une demande faite par un programme au système
d’exploitation pour faire une action que le programme ne peut pas faire
tout seul.
Le programme demande au système d’exploitation, par exemple, de :
ouvrir un fichier
écrire à l’écran
lire le clavier
utiliser le réseau
accéder à un périphérique
créer un processus
25. Quelle est la différence entre microprocesseur, microcontrôleur et système
Microprocesseur : CPU seul.
Microcontrôleur : CPU + mémoire + périphériques sur une puce.
Système embarqué : système informatique pour une tâche précise.
26. C’est quoi la stack ? C’est quoi le heap ?
pile : mémoire des variables locales et appels de fonctions.
tas : mémoire allouée dynamiquement.
27. À quoi sert la compilation ? À quoi sert le linking ?
Compilation : transforme le code source en code objet/machine.
Linking : assemble les fichiers objets et bibliothèques en exécutable.
28. Quelle est la différence entre memory-mapped I/O et port-mapped I/O ?
Memory-mapped I/O : périphériques accessibles comme des adresses
mémoire.
Port-mapped I/O : périphériques accessibles par des ports spéciaux et
instructions spéciale
29. Protocoles
UART :
Asynchrone
Bits start/stop
SPI :
Synchrone
Rapide
I²C :
Bus 2 fils
Synchrone
Adressable
30. Le CPU interagit avec périphériques via registres de
contrôle.
Lire/écrire dans ces registres configure et contrôle le
périphérique.
31. Fiche express à mémoriser
CPU = exécute les instructions
Mémoire = stocke code et données
Périphériques = lien avec le monde extérieur
Von Neumann = code et données ensemble
Harvard = code et données séparés
Registres = mémoire la plus rapide
ALU = fait les calculs
Stack = variables locales, appels de fonctions
Heap = mémoire dynamique
Pointeur = adresse mémoire
Référence = alias d’une variable
Struct = groupe de variables
Padding = octets ajoutés pour alignement
Little-endian = octet faible en premier
IEEE 754 = format des floats
ASCII = encodage simple de caractères
Compilation = code source → objet
Linking = objets → exécutable
32. Interruptions
Un événement matériel interrompt le CPU pour traiter quelque chose
d’urgent.
Exemple : réception série, bouton pressé, minuterie
33. Memory-mapped I/O : Les registres des périphériques sont placés à des
adresses mémoire précises.
34. uint8_t buffer[10] = {0xAA, 0x55, 0x01, 0x02, 0x03};
uint8_t* ptr = buffer; // ptr pointe vers buffer[0]
std::cout << *ptr; // Affiche : 0xAA (170 en décimal)
std::cout << *(ptr + 1); // Affiche : 0x55 (85 en décimal)
std::cout << ptr[2]; // Affiche : 0x01 — notation équivalente
35. Alias :
int valeur = 42;
int& ref = valeur; // ref est un alias de valeur
ref = 100; // Modifie valeur
std::cout << valeur; // Affiche : 100 // Une référence ne peut pas être « réassignée
» int autre = 50; ref = autre; // Copie la valeur de autre dans valeur (via ref) // ref
réfère toujours à valeur, pas à autre
36. Exercice plus difficile – Machine de Turing
On considère la table de transition suivante :
Éta
Symb Nou
t Symb Déplace
ole vel
act ole lu ment
écrit état
uel
Q0 0 0 Droite Q0
Q0 1 1 Droite Q0
Q0 # # Gauche Q1
Q1 0 1 Gauche Q2
Q1 1 0 Gauche Q2
Q2 0 0 Gauche Q1
Q2 1 1 Gauche Q1
Q1 B B Aucun HAL
Éta
Symb Nou
t Symb Déplace
ole vel
act ole lu ment
écrit état
uel
HAL
Q2 B B Aucun
T
Ruban initial
B101100#
La tête commence sur le premier 1 en état Q0.
Question
Quel sera le contenu final du ruban après l’arrêt de la machine ?
Indice
En Q0, la machine avance jusqu’à #
Ensuite elle revient vers la gauche
Mais cette fois, elle change un bit sur deux seulement
Solution :
Étapes
En partant du dernier bit :
dernier 0 en Q1 → devient 1, puis gauche, état Q2
0 en Q2 → reste 0, puis gauche, état Q1
1 en Q1 → devient 0, puis gauche, état Q2
1 en Q2 → reste 1, puis gauche, état Q1
0 en Q1 → devient 1, puis gauche, état Q2
1 en Q2 → reste 1, puis gauche, état Q1
B → arrêt
B111001#