Construire un CPU de zéro dans un simulateur
Un parcours cohérent et testable pour un CPU pédagogique 8 bits doté de registres, d'une ALU, d'un bus partagé, d'une RAM octet par octet, d'instructions de 16 bits, de registres d'état et d'une unité de contrôle micro-séquencée.
En bref : ce guide définit une architecture pédagogique complète avant de tracer les fils : un chemin de données et un espace d’adressage de 8 bits, une RAM unifiée organisée en octets, des instructions de 16 bits récupérées en deux octets, quatre registres généraux, une ALU et des indicateurs, un bus partagé unique et un contrôleur micro-séquencé. Construisez et vérifiez chaque sous-système, imposez un seul pilote de bus à la fois, puis exécutez un programme de cinq instructions qui calcule
5 + 3et stocke8en mémoire.
« Construire un CPU » peut désigner aussi bien le raccordement de blocs prêts à l’emploi que la conception de transistors. Ce tutoriel se situe entre ces deux extrêmes. Vous construirez un processeur à partir de blocs logiques numériques — registres, multiplexeurs, décodeurs, ALU, mémoire et mémoire de contrôle — tout en traitant les portes internes de chaque bloc comme une conception testable séparément.
L’objectif n’est pas d’imiter un cœur commercial moderne. Il est d’exposer le contrat que toute implémentation d’instruction doit respecter :
- récupérer les bons octets de l’instruction ;
- les conserver pendant le décodage ;
- déplacer exactement les opérandes prévus ;
- effectuer l’opération définie ;
- ne mettre à jour que l’état architectural autorisé ; et
- sélectionner l’adresse correcte de l’instruction suivante.
1. Figer d’abord l’architecture
De nombreux projets de CPU échouent parce que la largeur mémoire, la largeur des instructions et l’incrément du PC changent en cours de construction. Utilisez cette spécification sans la modifier :
| Propriété | Définition du CPU pédagogique |
|---|---|
| Largeur des données | 8 bits |
| Largeur des adresses | 8 bits |
| Mémoire | 256 emplacements adressables par octet, instructions et données unifiées |
| Largeur des instructions | 16 bits, stockées sous forme de deux octets consécutifs |
| Ordre des octets d’instruction | Octet de poids fort en premier à l’adresse de début de l’instruction |
| Avancement séquentiel du PC | Deux incréments d’un octet pendant le fetch |
| Registres généraux | Quatre registres 8 bits : R0–R3 |
| Verrous d’opérandes | A et B, 8 bits chacun |
| Registre d’instruction | 16 bits |
| Indicateurs | Z (zéro), N (négatif), C (retenue), V (débordement signé) |
| Interconnexion | Un bus pédagogique de 8 bits |
| Contrôle | Compteur de micro-étapes et ROM de contrôle |
Une adresse sur 8 bits sélectionne 256 emplacements ; la mémoire contient 256 octets puisque cette architecture définit explicitement chaque emplacement comme un octet. Les adresses de début d’instruction sont normalement paires, car chaque instruction occupe deux octets.
Schéma fonctionnel
┌──────────────────────┐
│ control ROM + │
│ microstep sequencer │
└──────────┬───────────┘
│ control signals
▼
┌────┐ ┌─────┐ ┌────────────── 8-bit shared bus ───────────────┐
│ PC │──▶│ MAR │ │ │ │ │ │ │ │
└────┘ └──┬──┘ ┌▼──┐ ┌▼──┐ ┌▼──┐ ┌▼──┐ ┌▼──┐ ┌▼──┐ ┌▼───┐
│ │MDR│ │ R0│ │ R1│ │ R2│ │ R3│ │ A │ │ B │
▼ └─┬─┘ └───┘ └───┘ └───┘ └───┘ └┬──┘ └┬───┘
┌────────┐ │ └──┬───┘
│256 × 8 │ │ ┌────▼────┐
│ RAM │ │ │ 8-bit │
└────────┘ │ │ ALU │
│ └────┬────┘
└──────▶ 16-bit IR ◀────────────────────┘
│
▼
opcode / fields
Le dessin est conceptuel. L’IR charge deux octets distincts depuis le bus et le résultat de l’ALU atteint le bus par son propre chemin de sortie sélectionné.
Ouvrez le Séquenceur d’instructions comme référence, mais considérez l’architecture écrite ici comme l’autorité de l’exercice. Si le modèle utilise une convention interne différente, documentez cette différence au lieu de combiner silencieusement les deux.
2. Définir les formats d’instruction
L’ISA personnalisée utilise deux formats de 16 bits.
Format registre
15 12 11 8 7 4 3 0
┌───────────────┬────────────────┬────────────────┬────────────────┐
│ opcode [3:0] │ RA [3:0] │ RB [3:0] │ RD [3:0] │
└───────────────┴────────────────┴────────────────┴────────────────┘
Format immédiat/adresse
15 12 11 8 7 0
┌───────────────┬────────────────┬─────────────────────────────────┐
│ opcode [3:0] │ RD [3:0] │ immediate / address [7:0] │
└───────────────┴────────────────┴─────────────────────────────────┘
Seuls les codes de registre 0–3 sont légaux sur cette machine à quatre registres. Le décodeur doit rejeter les valeurs 4–15 ou déclencher un piège sûr, plutôt que de sélectionner accidentellement une ligne de contrôle.
| Opcode | Mnemonic | Format | Effet défini |
|---|---|---|---|
0 | NOP | — | Aucun changement d’état architectural, hormis le PC déjà avancé |
1 | LDI | Immédiat | RD ← imm8 |
2 | LD | Immédiat | RD ← RAM[addr8] |
3 | ST | Immédiat | RAM[addr8] ← RD |
4 | ADD | Registre | RD ← RA + RB ; mise à jour des indicateurs |
5 | SUB | Registre | RD ← RA − RB ; mise à jour des indicateurs |
6 | JMP | Immédiat | PC ← addr8 |
7 | JZ | Immédiat | Si Z = 1, PC ← addr8 |
8 | HLT | — | Passage du contrôleur à l’état arrêté |
Tous les autres opcodes sont illégaux. Ajoutez une voie explicite illegal_instruction, ou mappez-les vers un piège/arrêt sûr pendant le débogage.
3. Construire les registres sans hacher l’horloge
Un registre 8 bits est constitué de huit bascules D déclenchées sur front, partageant une horloge et une validation logique de chargement.
Pour chaque bit :
Implémentez cette équation avec un multiplexeur 2:1 avant l’entrée D, ou utilisez la fonction de validation d’horloge intégrée au composant. Gardez tous les éléments d’état sur la même horloge globale.
Ne créez pas une horloge locale en faisant un ET entre l’horloge globale et load. Une porte d’horloge non maîtrisée peut introduire du skew et des glitches dans le matériel. Les cellules intégrées de contrôle d’horloge et les ressources d’horloge des FPGA sont des mécanismes distincts qui tiennent compte du timing.
Chaque registre connecté au bus a besoin :
- d’une entrée 8 bits provenant du bus ;
- d’une sortie stockée de 8 bits ;
- d’un contrôle
load_x; et - d’une voie
oe_xautorisée à piloter le bus pédagogique.
Le PC a en plus une voie d’incrémentation et une voie de chargement explicite. L’IR doit disposer de chargements indépendants pour l’octet de poids fort et l’octet de poids faible. Le MAR contient une adresse mémoire ; le MDR capture ou fournit un octet de données mémoire.
Vérifier un registre avant de le dupliquer
Testez les cas suivants :
load = 0: plusieurs cycles laissent Q inchangé.load = 1: Q capture le bus sur le front actif.- Les données changent en dehors du front : Q ne change qu’au prochain front activé.
- Si un reset est fourni : Q atteint l’état documenté.
- Sortie désactivée : le registre ne pilote pas le bus.
Un simulateur numérique peut vérifier la séquence logique de ce modèle. Il ne reproduit pas la métastabilité analogique et ne prouve pas les temps physiques de setup et de maintien.
4. Construire l’ALU et définir chaque indicateur
Utilisez A et B comme verrous d’opérandes stables. L’ALU est combinatoire : elle ne doit pas contenir d’état architectural caché.
alu_op | Opération | Résultat |
|---|---|---|
000 | ADD | A + B |
001 | SUB | A − B |
010 | AND | A & B |
011 | OR | `A |
100 | XOR | A ^ B |
101 | NOT | ~A |
110 | SHL | A << 1, remplissage par zéro |
111 | SHR | A >> 1 logique, remplissage par zéro |
Le guide de l’ALU détaille les voies et le multiplexeur de sortie. La conception arithmétique de base comprend :
- une chaîne de full-adders à huit étages ;
- une soustraction réalisée par
A + (~B) + 1; - des portes bit à bit en parallèle ;
- un câblage de décaleur fixe d’un bit ; et
- un multiplexeur de sortie sélectionné par
alu_op.
Définition des indicateurs
Définissez les indicateurs comme une partie de l’ISA, notamment ce que les instructions logiques et de décalage font à C et V.
- Z : 1 lorsque chaque bit du résultat vaut zéro.
- N : bit 7 du résultat, interprété comme bit de signe uniquement pour les opérations signées.
- C pour ADD : retenue sortante du bit 7.
- C pour SUB : choisissez et documentez soit « pas d’emprunt », soit « emprunt ». Ce guide utilise pas d’emprunt, conformément à la retenue sortante de l’additionneur.
- V pour ADD : les opérandes ont le même signe et le résultat a le signe opposé.
- V pour SUB : les opérandes ont des signes différents et le signe du résultat diffère de celui de A.
La formule équivalente XOR entre la retenue entrante et la retenue sortante s’applique à l’addition et à la soustraction en complément à deux lorsque la voie de soustraction utilise B inversé et une retenue d’entrée. Consultez les indicateurs du CPU pour des exemples détaillés.
Cette ISA pédagogique met à jour Z, N, C et V sur ADD et SUB. Pour les opérations logiques et de décalage, choisissez une politique documentée — conserver C/V ou leur attribuer de nouvelles significations — avant d’écrire le microcode. « La valeur que les fils contiennent par hasard » n’est pas une politique valide.
Tableau de vérification de l’ALU
Au minimum, testez :
| Cas | Point à vérifier |
|---|---|
0x00 + 0x00 | Z |
0xFF + 0x01 | retenue non signée et zéro |
0x7F + 0x01 | débordement signé et N |
0x80 + 0x80 | retenue et débordement signé |
0x00 − 0x01 | convention d’emprunt et N |
0x80 − 0x01 | débordement signé |
| identités AND/OR/XOR | voies bit à bit |
décalages de 0x81 | direction, bit de remplissage, politique du bit éjecté |
5. Construire le bus partagé
Le bus pédagogique est composé de huit fils et de plusieurs sources possibles. Un invariant central s’applique :
Les pilotes possibles comprennent PC, MDR, R0–R3, A, B, un octet de l’IR et le résultat de l’ALU. La liste exacte est plus longue que « le nombre de registres » : énumérez donc chaque source dans la spécification de contrôle.
Dans DigiSim, des buffers trois états peuvent modéliser un bus partagé :
- activer exactement une source pendant un transfert ;
- activer un ou plusieurs signaux de chargement de destination pour le front actif suivant ; et
- ne jamais capturer le bus lorsqu’aucune source ne le pilote.
La contention est une erreur de conception. Un conflit de sorties physiques peut provoquer une tension incorrecte et un courant excessif ; la conséquence dépend de la technologie et de la durée, donc « la puce brûle toujours » n’est pas une règle utile.
Les matrices FPGA modernes implémentent généralement les bus partagés internes avec des multiplexeurs plutôt qu’avec un véritable câblage trois états interne. Les styles ASIC varient également. Le bus trois états reste un modèle pédagogique clair de la propriété et du transfert.
Test d’arbitrage du bus
Placez des valeurs connues dans R0 et R1 :
- R0 =
0xAA - R1 =
0x55
Vérifiez R0 seul, R1 seul, l’absence de pilote et les deux pilotes. Les deux derniers cas doivent être traités comme des états invalides du contrôleur. Ajoutez ensuite une assertion ou un indicateur visible afin que les erreurs ultérieures de microcode soient détectées immédiatement.
6. Connecter une mémoire organisée en octets
Utilisez le composant RAM du simulateur comme modèle de mémoire architecturale. Ne prétendez pas que la RAM physique est littéralement un tableau de verrous D : SRAM, DRAM, block RAM et tableaux de registres synthétisés emploient des cellules de stockage et des timings différents.
L’interface de ce CPU est la suivante :
ADDR[7:0], piloté par MAR ;DATA[7:0], transféré par MDR ou par l’interface de bus contrôlée ;mem_read;mem_write; et- un
readyfacultatif si le modèle prend en charge une latence variable.
Ce parcours suppose qu’une lecture peut être demandée et capturée en une micro-étape dédiée. Si la mémoire choisie est synchrone ou présente une latence visible, insérez un état d’attente et ne capturez les données que lorsqu’elles sont valides.
Les instructions et les données partagent les 256 octets. Une architecture Harvard à mémoires séparées est également valide, mais ce serait une architecture et un calendrier de fetch différents.
Pour distinguer les abstractions de stockage inscriptible et non volatile/constant, consultez RAM ou ROM.
7. Construire le contrôleur micro-séquencé
Le contrôleur transforme un opcode, une micro-étape, les indicateurs et l’état machine en :
- validations de sortie du bus ;
- validations de chargement des registres ;
- opération de l’ALU ;
- commandes de lecture/écriture mémoire ;
- commandes d’incrémentation/chargement du PC ;
- chargement des indicateurs ;
- chargement de l’octet haut/bas de l’IR ; et
- commande de prochaine micro-étape ou de fin d’instruction.
Une implémentation peut utiliser une ROM de contrôle :
control address = {opcode, microstep}
control word = {oe signals, load signals, alu_op, memory controls, next control}
Les indicateurs peuvent conditionner certaines actions — par exemple load_pc = microcode_load_pc AND Z pour JZ — ou participer à l’adresse de la mémoire de contrôle. Une logique explicite d’état suivant est souvent plus claire que de supposer que le compteur de micro-étapes s’incrémente toujours.
Il s’agit d’un contrôleur microprogrammé pédagogique. Les processeurs réels combinent de diverses façons décodage combinatoire, mémoires de contrôle programmables, machines à états et séquenceurs. N’utilisez pas l’étiquette RISC/CISC comme raccourci pour décrire l’implémentation.
Valeurs par défaut sûres
Chaque entrée inutilisée de la ROM de contrôle doit :
- désactiver tous les pilotes du bus ;
- désactiver les écritures mémoire ;
- désactiver les écritures des registres architecturaux ; et
- entrer dans une voie définie d’instruction illégale ou d’arrêt.
Un mot de contrôle non initialisé ne doit jamais devenir « écrire des données arbitraires dans une adresse mémoire arbitraire ».
8. Récupérer systématiquement les deux octets de l’instruction
Comme la RAM et le chemin de données ont une largeur de huit bits alors que chaque instruction fait 16 bits, le fetch nécessite deux lectures d’octet. L’octet haut est stocké en premier, par définition.
En supposant qu’une seule micro-étape suffise pour la lecture de la RAM choisie :
| Étape | Transfert de registre | Contrôles importants |
|---|---|---|
| F0 | MAR ← PC | oe_pc, load_mar |
| F1 | MDR ← RAM[MAR] ; PC ← PC + 1 | mem_read, load_mdr, inc_pc |
| F2 | IR[15:8] ← MDR | oe_mdr, load_ir_hi |
| F3 | MAR ← PC | oe_pc, load_mar |
| F4 | MDR ← RAM[MAR] ; PC ← PC + 1 | mem_read, load_mdr, inc_pc |
| F5 | IR[7:0] ← MDR | oe_mdr, load_ir_lo |
Après F5, l’IR contient une instruction complète et le PC pointe déjà vers l’instruction suivante. Le décodage sélectionne la séquence d’exécution.
Si la RAM nécessite une attente entre l’adresse et la donnée, divisez F1 et F4 en étapes de demande, d’attente et de capture. Ne dissimulez jamais un fetch 16 bits dans un transfert de largeur 8 bits.
9. Définir les micro-opérations d’exécution
ADD RD, RA, RB
| Étape | Transfert |
|---|---|
| E0 | A ← R[RA] |
| E1 | B ← R[RB] |
| E2 | R[RD] ← A + B ; FLAGS ← ALU flags |
| E3 | Fin d’instruction ; retour à F0 |
RA, RB et RD sélectionnent l’un des registres R0–R3 par des validations décodées. Les codes de registre invalides doivent suivre la voie d’instruction illégale.
LDI RD, imm8
| Étape | Transfert |
|---|---|
| E0 | R[RD] ← IR[7:0] |
| E1 | Fin d’instruction |
LD RD, addr8
| Étape | Transfert |
|---|---|
| E0 | MAR ← IR[7:0] |
| E1 | MDR ← RAM[MAR] |
| E2 | R[RD] ← MDR |
| E3 | Fin d’instruction |
ST RD, addr8
| Étape | Transfert |
|---|---|
| E0 | MAR ← IR[7:0] |
| E1 | MDR ← R[RD] |
| E2 | RAM[MAR] ← MDR |
| E3 | Fin d’instruction |
JMP et JZ
- JMP :
PC ← IR[7:0], puis fin. - JZ : ne charger le PC que lorsque Z vaut 1, puis terminer dans tous les cas.
HLT
Entrez dans un état stable du contrôleur qui n’émet aucune écriture. Arrêter l’horloge pilotée manuellement du simulateur est pratique, mais l’état architectural doit rester bien défini même si des fronts d’horloge continuent d’arriver.
10. Assembler manuellement et charger le programme de test
Le programme charge 5 et 3, les additionne dans R2, stocke le résultat à l’adresse 0x10, puis s’arrête.
| Adresse de début | Instruction | Mot 16 bits | Octets en mémoire |
|---|---|---|---|
0x00 | LDI R0, #5 | 0x1005 | 0x10 0x05 |
0x02 | LDI R1, #3 | 0x1103 | 0x11 0x03 |
0x04 | ADD R2, R0, R1 | 0x4012 | 0x40 0x12 |
0x06 | ST R2, 0x10 | 0x3210 | 0x32 0x10 |
0x08 | HLT | 0x8000 | 0x80 0x00 |
Chargez ces dix octets aux adresses 0x00 à 0x09, puis réinitialisez :
- PC =
0x00 - micro-étape du contrôleur = F0
- halted = 0
- tous les pilotes de bus désactivés, sauf lorsque le mot de contrôle en sélectionne un
Avancez d’un cycle à la fois sur le premier fetch et vérifiez :
- après F2, l’octet haut de l’IR vaut
0x10; - après F5, l’IR vaut
0x1005; - le PC vaut
0x02; - LDI place
0x05dans R0 ; et - aucun autre registre ni emplacement mémoire ne change.
À l’arrêt :
- R0 =
0x05 - R1 =
0x03 - R2 =
0x08 - RAM[
0x10] =0x08 - PC =
0x0A
Ces valeurs finales sont nécessaires, mais pas suffisantes. Un CPU défectueux pourrait corrompre un autre emplacement tout en produisant ces résultats.
11. Vérifier les invariants, pas seulement le résultat final
Ajoutez des contrôles pour :
- ne jamais avoir plus d’une validation de sortie du bus à la fois ;
- n’activer l’écriture mémoire que pendant l’étape d’écriture de ST ;
- ne charger les octets de l’IR que pendant F2 et F5 ;
- incrémenter le PC exactement deux fois lors de chaque fetch normal ;
- n’écrire les indicateurs que pour les instructions définies comme les mettant à jour ;
- faire en sorte qu’un opcode ou un code de registre invalide n’entraîne aucune écriture involontaire ;
- faire en sorte que l’état arrêté n’ait aucun effet architectural ; et
- faire en sorte que le reset atteigne un état de contrôleur connu.
Pour chaque instruction, enregistrez une trace de :
clock, microstep, PC, IR, bus owner, bus value,
register loads, memory read/write, ALU op, flags
Lorsqu’un test échoue, trouvez le premier cycle qui diverge de la trace attendue. Déboguer depuis la dernière valeur erronée vous fait généralement remonter plusieurs étapes trop tard.
Le guide de débogage à l’oscilloscope explique comment inspecter les formes d’onde modélisées sans les confondre avec une validation du timing physique.
12. Pannes courantes
| Symptôme | Cause probable | Première vérification |
|---|---|---|
| L’IR contient des octets inversés | L’ordre des octets diffère entre le chargeur et le fetch | Adresses PC et PC+1 |
| Le PC avance de quatre | L’incrément se produit deux fois par octet ou des hypothèses de largeur ont été mélangées | Trace F0–F5 |
| Le bus est en contention | Deux validations de sortie partagent un bit de contrôle | Mot de contrôle décodé |
| Le bus n’est pas piloté lors de la capture | Validation de la source manquante | Assertion du propriétaire du bus |
| ADD écrit dans le mauvais registre | Décodage du champ RD ou ordre des codes de registre incorrect | IR[3:0] et lignes de chargement |
| ST corrompt la mémoire des instructions | Adresse/immédiat ou timing de mem_write incorrect | MAR et ST E2 |
| Le branchement utilise un Z obsolète | Les indicateurs n’ont pas été écrits ou JZ échantillonne le mauvais état | ADD/SUB E2 et JZ |
| Le contrôleur ne revient jamais au fetch | Entrée de fin/état suivant absente | Transition de micro-étape |
| Un opcode illégal exécute une opération | ROM de contrôle non initialisée de manière sûre | Mot de contrôle par défaut |
13. Ce que ce CPU enseigne — et ce qu’il n’enseigne pas
Cette conception enseigne le transfert de registres, le codage des instructions, la propriété du bus, les indicateurs de l’ALU, le séquencement mémoire et l’état du contrôleur.
Elle ne modélise pas :
- la RAM ou la logique au niveau transistor ;
- la hiérarchie de cache ou la mémoire virtuelle ;
- l’exécution spéculative ou dans le désordre ;
- le comportement analogique de l’horloge ou la métastabilité ;
- la propagation physique, le routage, la puissance ou les limites thermiques ; ni
- une architecture de sécurité ou d’exceptions destinée à la production.
Son bus partagé unique est volontairement facile à observer ; ce n’est pas une affirmation sur la topologie interne d’un CPU moderne.
Extensions dans un ordre sûr
- Ajouter des tests d’instructions automatisés. Vérifiez chaque opcode et chaque encodage illégal avant d’ajouter des fonctionnalités.
- Ajouter AND, OR, XOR et les décalages. Définissez d’abord le comportement des indicateurs, puis ajoutez les séquences de contrôle.
- Ajouter un branchement conditionnel par comparaison directe. Cela met en évidence la différence entre les ISA fondées sur les indicateurs et celles qui comparent puis branchent.
- Ajouter un pointeur de pile et l’appel/retour. Spécifiez la croissance de la pile, l’ordre des octets et le comportement en cas de dépassement.
- Ajouter une machine à états d’interruptions. Sauvegardez l’adresse de retour et les indicateurs avant de charger un vecteur ; définissez la priorité et le masquage.
- Séparer les mémoires d’instructions et de données. Comparez le nouveau calendrier de fetch avec la conception à mémoire unifiée.
- Ne mettre en pipeline qu’après validation de la référence multicycle. Définissez alors la validité des étages, les arrêts, le forwarding, les vidages et les exceptions.
Consultez l’analyse approfondie fetch–decode–execute et le guide du registre d’instruction et de l’étape de décodage avant de tenter l’extension en pipeline.
Essayez
Utilisez le Séquenceur d’instructions pour vous orienter, le système ALU 8 bits pour vérifier séparément l’arithmétique et le registre des indicateurs du CPU pour tester le comportement d’état. Gardez une architecture écrite et un tableau de micro-opérations à côté du schéma.
Le jalon important n’est pas de voir 8 une seule fois. C’est de pouvoir expliquer pourquoi chaque changement d’état a été autorisé, le reproduire dans une trace et prouver qu’aucun autre état n’a changé.



