Pile d’abstractions : du silicium au serveur distant

Tour complet — électrique, PC, réseau, télécom — avec script exécutable

Comment lire ce document

Le texte décrit la pile conceptuellement (sections I à X) ; le script pile_trace.py (sections XI à XIII) exécute la pile bout-en-bout sur l’opération 1 + 1 et vérifie par assertion à chaque niveau que le bit retrouvé est identique au bit émis. Les deux sont indissociables : le texte sans le script reste de la prose, le script sans le texte reste un blob illisible. Ensemble, ils forment une trace de cohérence verticale — qui ne prouve pas que l’on a simulé LTE en vrai, mais que chaque couche est connectée à la suivante par un calcul que l’on peut rejouer à la main.

I. Pile silicium — du caillou dopé au PC

Bon, jolie pirouette — on quitte le débat sur l’apophatique pour la pile concrète qui rend possible le “truc de ouf”. On y va, niveau par niveau, en encapsulant à chaque étage.

Niveau 0 : l’équivalence Thévenin–Norton, le théorème de base qui permet de remplacer n’importe quel réseau linéaire à deux bornes par l’un de deux dipôles canoniques. C’est la première abstraction du tour — on peut ignorer le détail interne et ne regarder que le comportement aux bornes. Tout le reste de la pile repose sur ce geste : encapsuler par interface.

Équivalence Thévenin-Norton Deux représentations équivalentes d'un même dipôle linéaire : source de tension en série avec résistance, ou source de courant en parallèle avec résistance. Thévenin Source de tension en série Vth Rth A B Norton Source de courant en parallèle In Rn A B Vth = In · Rn Rth = Rn

Équivalence Thévenin–Norton
Précision sur l’applicabilité

Le théorème ne vaut strictement que pour les réseaux linéaires. Dès qu’il y a une diode, un transistor en mode actif, ou tout composant non-linéaire, on doit d’abord linéariser autour d’un point de fonctionnement (analyse petite-signal). C’est précisément ce qu’on fait pour modéliser un MOSFET en gain à un point de polarisation donné — et c’est aussi ce qu’on fera tacitement au Niveau 0 du script : équations long-channel idéales, pas de modélisation BSIM.

Niveau 1 : le transistor. Un MOSFET, c’est un robinet — la tension de grille contrôle le passage entre drain et source. Combine un PMOS (au-dessus) et un NMOS (en-dessous) avec leurs grilles en commun, on obtient l’inverseur CMOS : entrée haute = sortie basse. C’est ici que naît le bit, par convention : tension > seuil = 1, tension proche de zéro = 0. Le monde analogique vient d’être discrétisé.

Du MOSFET à l'inverseur CMOS Un transistor MOSFET utilisé comme interrupteur, combiné en paire complémentaire PMOS-NMOS pour former un inverseur, et la convention de tension qui définit le bit. MOSFET Interrupteur contrôlé canal G D S Vg > Vth → passant Vg < Vth → bloqué Inverseur CMOS PMOS + NMOS complémentaires VDD PMOS NMOS OUT GND IN Naissance du bit Convention de seuil 1 tension haute ≈ VDD Vth 0 tension basse ≈ 0 V Le numérique est une discrétisation conventionnelle

Du MOSFET à l’inverseur CMOS, et la naissance du bit
Le seuil n’est pas une falaise

Dans la réalité du silicium, \(V_{th}\) varie d’une puce à l’autre (process variation), avec la température, et même dans le temps (NBTI/PBTI sur les nœuds avancés). Plus on descend en finesse (≤ 7 nm), plus la fuite sous-seuil augmente — un MOSFET “bloqué” laisse passer un courant non négligeable. La discrétisation 0/1 est une convention robuste aux marges (\(V_{OL,max} < V_{IL,min}\) et \(V_{OH,min} > V_{IH,max}\)), pas un absolu physique.

Niveau 2 : les portes logiques. Avec quelques inverseurs et combinaisons série/parallèle de MOSFETs, on fabrique NAND, AND, OR, XOR. Et NAND a une propriété folle : elle est universelle. N’importe quelle fonction booléenne — un additionneur, un comparateur, n’importe quelle table de vérité — se construit uniquement avec des NAND. C’est le bond le plus violent du tour : on quitte la physique du silicium pour entrer dans l’algèbre de Boole.

La porte NAND et son universalité Le symbole de la porte NAND avec sa table de vérité, et comment NOT, AND, OR se construisent uniquement avec des NAND. Porte NAND NOT(A AND B) A B Y A B Y 001 011 101 110 Universalité tout se construit en NAND NOT(A) = NAND(A, A) AND(A,B) = NOT(NAND(A,B)) OR(A,B) = NAND(NOT A, NOT B) XOR, additionneur, = compositions de NAND algèbre booléenne = aucun transistor visible

La porte NAND et son universalité
Sheffer stroke (1913)

L’universalité du NAND est un résultat formel d’Henry M. Sheffer, qui a prouvé que toute l’algèbre booléenne se réduit à un seul opérateur binaire (| ou , le Sheffer stroke). NOR est l’autre opérateur universel. Cette équivalence rend possible les FPGA en pratique : une LUT à \(k\) entrées peut implémenter n’importe quelle fonction booléenne sur \(k\) bits, simplement en remplissant sa table de vérité.

Niveau 3 : la mémoire. Une bascule D capture la valeur de son entrée sur le front montant de l’horloge et la maintient jusqu’au front suivant. C’est le premier circuit séquentiel du tour : il a un état, il garde une trace. Empile huit bascules partageant la même horloge, on obtient un registre 8 bits — un octet. À 32 ou 64 bascules, ce sont les registres généraux d’un cœur de processeur. Sans cet état, pas de calcul au sens de Turing : juste de la combinatoire pure.

Bascule D et registre 8 bits Une bascule D mémorise un bit sur front d'horloge. Huit bascules partageant la même horloge forment un registre stockant un octet. Bascule D — 1 bit capture sur front d'horloge D-FF D CLK Q Q ← D sur front montant tient entre deux fronts = 1 bit mémorisé Registre 8 bits 8 bascules, horloge partagée FF FF FF FF FF FF FF FF D[7:0] CLK = 1 octet stocké en parallèle empile en 32, 64 bits = registres généraux d'un cœur

Bascule D et registre 8 bits
Métastabilité aux frontières d’horloge

Quand une donnée traverse deux domaines d’horloge asynchrones et que la transition arrive trop près du front d’échantillonnage (violation du setup/hold time), la bascule peut entrer dans un état métastable : sa sortie reste entre 0 et 1 pendant un temps non borné. La parade industrielle : un synchroniseur à 2 ou 3 bascules en série côté domaine destination, qui réduit la probabilité de propagation à un niveau astronomiquement faible (MTBF de plusieurs millénaires). Sans ce détail, aucun SoC multi-horloge ne fonctionnerait.

Niveaux 4 à 6 : intégration. On agrège registres + ALU + séquenceur d’instructions = un cœur de processeur. On place ce cœur, de la RAM, du Flash et des périphériques (GPIO, UART, SPI, ADC…) sur une seule puce : c’est un microcontrôleur. On sépare cœur(s) et mémoire externe, on ajoute des caches hiérarchiques, du multi-cœur, des bus haute vitesse : c’est un processeur PC. On colle ce processeur avec RAM, SSD, GPU et chipset sur une carte mère : c’est un PC. Chaque étage encapsule strictement les précédents.

Microcontrôleur, processeur, PC Trois niveaux d'intégration : MCU sur une puce unique, processeur multi-cœur avec caches, PC avec composants distincts reliés par des bus. Microcontrôleur tout sur une puce Cœur (1) RAM Flash GPIO, UART, SPI ADC, timers, I²C STM32, AVR, PIC... Processeur multi-cœur, mémoire externe Cœur 1 Cœur 2 Cœur 3 Cœur 4 Cache L1 (par cœur) Cache L2 / L3 partagé MMU, contrôleur bus x86, ARM Cortex-A... PC carte mère, composants discrets CPU (socket) DDR RAM (DIMM) GPU SSD / NVMe Chipset, bus PCIe / SATA / USB Alim, BIOS, réseau desktop, serveur, laptop

Microcontrôleur, processeur et PC

Voilà la pile, du caillou dopé jusqu’au PC. Chaque étage encapsule strictement le précédent et offre une interface propre. Personne ne tient l’ensemble dans sa tête simultanément — le développeur Python n’a aucune raison de penser au NAND, et pourtant des trillions de NAND exécutent son code en ce moment. C’est l’Encapsulator pattern à son niveau le plus pur : any source over any PHY if the over is implemented and the PHY permits.

Cette pile est la démonstration concrète qu’un sum descriptible et stratifié peut produire un effet qui semble totalisant sans avoir besoin d’être un TOUT indistinct. Le vertige est local — il vient du saut d’échelle entre niveaux, pas d’une absence de structure.

II. Trace conceptuelle : 1 + 1 = 2 du potentiel aux pixels

On prend 1 + 1 sur un toy 2 bits et on suit chaque étage avec les valeurs binaires. 1 = 01, 2 = 10. L’opération attendue est donc 01 + 01 = 10.

L’addition réelle. Le cœur de l’opération vit dans un additionneur ripple-carry 2 bits. Chaque bit est traité par un full adder qui calcule \(S = A \oplus B \oplus C_{in}\) et \(C_{out} = AB + C_{in}(A \oplus B)\) — c’est-à-dire, en transistors, environ 28 MOSFETs par étage. Pour le bit 0 : \(A_0=1, B_0=1, C_{in}=0 \Rightarrow S_0=0\) et report=1. Le report propage vers le bit 1, qui le reçoit comme \(C_{in}\) : \(A_1=0, B_1=0, C_{in}=1 \Rightarrow S_1=1, C_{out}=0\).

Addition 01 + 01 = 10 Deux additionneurs complets chaînés par la retenue. FA0 traite le bit de poids faible avec A0=B0=1 et produit S0=0, report=1. FA1 reçoit ce report avec A1=B1=0 et produit S1=1, report final=0. Additionneur 2 bits — opération 01 + 01 FA bit 0 XOR + AND + OR A₀ = 1 B₀ = 1 Cin = 0 S₀ = 0 report = 1 FA bit 1 XOR + AND + OR A₁ = 0 B₁ = 0 S₁ = 1 Cout = 0 S₁ S₀ = 10 = 2

Addition 01 + 01 = 10 dans un additionneur ripple-carry 2 bits
Ripple-carry vs carry-lookahead

Le ripple-carry est pédagogique mais lent — son délai croît linéairement avec le nombre de bits. À 64 bits, c’est intenable. Les vrais CPUs utilisent des additionneurs carry-lookahead (Brent–Kung, Kogge–Stone) qui calculent toutes les retenues en parallèle en \(O(\log N)\) de profondeur, au prix d’une surface plus grande. Un Apple M-series ou un Zen 4 fait une addition 64 bits en un seul cycle d’horloge à ~5 GHz, soit ~200 ps. Le script pile_trace.py (Niveau 1) implémente le ripple-carry parce qu’il rend les retenues visibles ligne à ligne.

Le logiciel. Le développeur écrit int x = 1 + 1; en C. Le compilateur traduit en assembleur, l’assembleur en code machine, qui est chargé en mémoire programme. Au runtime, le processeur exécute en quatre temps : fetch / decode / execute / writeback. C’est à l’étape execute que l’instruction ADD route les valeurs des registres source vers l’additionneur matériel du diagramme précédent.

Pile logicielle : du code source à l'exécution Le code C est compilé en assembleur, puis en code machine binaire chargé en mémoire programme. Le processeur exécute en quatre temps : fetch, decode, execute, writeback. Chaîne de compilation code source (C) int x = 1 + 1; gcc assembleur MOV R0, #1 MOV R1, #1 ADD R2, R0, R1 as binaire en mémoire programme 0x12 0x01 ; MOV R0,#1 0x16 0x01 ; MOV R1,#1 0x40 0x12 ; ADD R2,R0,R1 chargé en Flash ou RAM du µC Exécution dans le processeur 1. Fetch lit l'instruction à PC 2. Decode identifie opcode + registres 3. Execute R0, R1 → additionneur ALU 4. Writeback latche le résultat dans R2 R2 = 10b = 2 — disponible pour l'instruction suivante

Pile logicielle : code source → assembleur → binaire → exécution
Démo pédagogique, pas reproductible tel quel

En pratique, int x = 1 + 1; avec n’importe quel compilateur moderne et -O1 minimum est réduit à MOV x, #2 en constant folding à la compilation — l’additionneur matériel ne tourne jamais. Pour observer la vraie chaîne fetch-decode-execute sur un ADD, il faut soit désactiver l’optimisation (-O0 + volatile), soit travailler sur des opérandes non connues à la compilation. Le cas exposé ici est une fiction utile — et c’est précisément pour cette raison que le script pile_trace.py simule la chaîne au niveau logique plutôt que de la mesurer sur du silicium réel.

Vers l’œil. À ce stade W2 contient 0b10. Mais on ne voit pas W2. Pour qu’un 2 apparaisse sur le moniteur, il reste encore une demi-douzaine d’étages. Le programme appelle printf("%d", x), qui convertit l’entier 2 en caractère ASCII '2' (octet 0x32). Cet octet est écrit sur stdout, capté par le terminal ou le compositor, qui demande au driver graphique de rasteriser le glyph correspondant. Cette matrice de pixels est sérialisée sur HDMI ou DisplayPort, reçue par le contrôleur du moniteur, qui pilote les cristaux ou LEDs des pixels concernés. Photons → rétine → on voit 2.

De R2 aux pixels affichés «2» L'entier binaire 10 calculé dans R2 traverse plusieurs représentations : caractère ASCII, glyph rasterisé en bitmap, pixels physiques sur le moniteur. Du registre aux pixels Registre R2 entier dans le CPU 0b10 printf ASCII '2' 1 octet 0x32 driver Glyph rasterisé matrice 8×14 HDMI Écran photons Même valeur, quatre représentations consécutives chaque couche ignore totalement les suivantes l'additionneur ne sait pas qu'il additionne, le pixel ne sait pas qu'il est un '2'

De W2 aux pixels affichés «2»

Neuf couches d’abstraction distinctes, chacune totalement opaque aux autres. Celui qui a écrit 1+1 ignore les MOSFETs. L’additionneur ignore qu’il calcule pour un être humain. Le pixel ignore qu’il est un 2. Et pourtant la chaîne tient — c’est ça le “truc de ouf”. Pas un TOUT indistinct qui ferait tout, mais une pile finie d’encapsulations qui tiennent par interface.

III. Réseau filaire — top-down (envoi)

On prend le 2 qu’on vient de calculer et on le glisse dans un socket TCP vers un serveur. Au niveau application, on a une lettre à envoyer — {"result": 2}. La pile OSI fait exactement ce que ferait La Poste : emboîter le message dans des enveloppes successives, chacune ajoutant une couche d’information de routage à son niveau, et chaque étage ignorant ce qui se passe au-dessus et en-dessous.

OSI 7 → 1 avec métaphore postale Les sept couches du modèle OSI mises en parallèle avec les étapes du système postal. OSI 7 → 1 : envoyer "2" par la poste L7 — Application HTTP, SMTP, MQTT, DNS {"result": 2} la lettre elle-même le contenu, ce que tu veux dire au destinataire L6 — Présentation UTF-8, TLS, compression encodage et chiffrement la langue choisie français, chiffré ou en clair format de l'écriture L5 — Session handshake, login, contexte ouverture et clôture du dialogue la correspondance ouverte "vous m'avez écrit le 12, je vous réponds le 14" L4 — Transport (TCP) ports, seq, ack, checksum fiabilité, ordre, retransmissions l'enveloppe + recommandé expéditeur, destinataire, numéro de page, accusé de réception L3 — Réseau (IP) IP source / destination, TTL routage de bout en bout l'adresse postale complète pays, ville, rue, numéro — pour le routage international L2 — Liaison (Ethernet, Wi-Fi) MAC src/dst, frame, FCS trame locale, prochain saut le tri au bureau de poste "prochain camion : Lyon" route locale, pas la finale L1 — Physique tensions, photons, ondes radio cuivre, fibre, air le transport physique camion, train, avion, vélo du facteur

OSI 7 → 1 avec la métaphore postale, couche par couche

À l’envoi, la donnée descend de L7 vers L1, et à chaque couche un en-tête se rajoute autour de la précédente comme une enveloppe enrobant une autre enveloppe. Les routeurs intermédiaires entre les deux ne montent jamais au-delà de L3 : ils lisent l’IP de destination, choisissent le prochain saut, refont une nouvelle trame L2 pour ce saut, et balancent ça sur le médium physique. Le contenu de la lettre (L7) n’est jamais ouvert en route.

Encapsulation OSI Le payload applicatif descend la pile et se fait entourer d'en-têtes successifs : TCP, IP, Ethernet, plus son FCS. À la couche physique il devient un flux de bits sur le médium. Encapsulation — la lettre dans ses enveloppes L7 payload : {"result": 2} L4 — TCP TCP payload L3 — IP IP TCP payload L2 — Eth ETH IP TCP payload FCS L1 flux de bits sur le médium physique Chaque couche ajoute son en-tête et ignore le contenu de toutes les autres.

Encapsulation : la lettre dans ses enveloppes successives
OSI vs TCP/IP réel

OSI à 7 couches est un modèle de référence, pas une implémentation. La pile TCP/IP réelle écrase L5/L6/L7 en une seule couche “application” (HTTP, TLS, gRPC font tout). Symétriquement, beaucoup d’équipements modernes “voient” plus haut que leur couche officielle : DPI (Deep Packet Inspection) inspecte L7 dans certains pare-feux, MPLS opère en L2.5, et un load balancer L7 termine TCP pour rejouer côté backend. Le modèle stratifié pur est une fiction pédagogique utile — c’est exactement celle que le script pile_trace.py implémente.

IV. Réseau filaire — bottom-up (réception)

Côté serveur, la séquence se rejoue à l’envers. La carte réseau capture les bits L1 sur le médium et reconstitue la trame L2 ; elle vérifie le FCS Ethernet pour détecter une corruption éventuelle, retire l’en-tête MAC, et passe le paquet IP à L3. L3 vérifie le checksum d’en-tête IP, retire son en-tête, et passe le segment TCP à L4. L4 vérifie le checksum TCP et le numéro de séquence, acquitte (ACK) côté retour, retire son en-tête, et délivre enfin le payload à l’application via le socket.

Décapsulation OSI Le flux de bits reçu remonte la pile : la trame L2 est vérifiée puis l'en-tête Ethernet et le FCS sont retirés, le paquet IP est passé à L3 qui retire son en-tête, le segment TCP est validé et son en-tête retiré, le payload est délivré à l'application. Décapsulation — la lettre sort de ses enveloppes L7 payload reçu : {"result": 2} L4 — TCP TCP payload L3 — IP IP TCP payload L2 — Eth ETH IP TCP payload FCS L1 bits reçus depuis le médium remontée L1 → L7 Chaque couche retire son en-tête après vérification et passe le contenu à la couche du dessus.

Décapsulation OSI — la lettre sort de ses enveloppes
Les checksums TCP/IP sont faibles

Le checksum TCP/IP (1’s complement sur 16 bits) ne détecte qu’environ 1 erreur sur 65 000. Le CRC32 d’Ethernet est nettement plus robuste, mais des corruptions silencieuses passent parfois — phénomène documenté sur certaines NIC défectueuses, switches buggués, ou mémoires sans ECC. Pour des données critiques (financières, médicales), un checksum applicatif fort (SHA-2, BLAKE3) au-dessus de TCP est la seule garantie sérieuse. TCP fournit la bonne livraison probable, pas l’intégrité cryptographique. Le script implémente le checksum TCP avec pseudo-header (cf. fonction ip_checksum et L4_tcp) — c’est le vrai algorithme, juste avec une couverture statistique modeste.

C’est exactement le pattern d’encapsulation qu’on retrouve dans SS7/SIGTRAN, MAP/TCAP/SCCP, ou dans un bridge bridge.py qui sert L1 entre QEMU et un mobile : la même opération récursive, à toutes les échelles. La symétrie envoi/réception est totale — top-down côté émetteur, bottom-up côté récepteur, et les routeurs au milieu ne traversent que les couches basses.

V. SDR — Rx, Tx, full-duplex

L’analogie fondamentale : TCP/IP descend top-down du programme aux bits sur médium, et le RAN remonte symétriquement bottom-up de l’onde radio jusqu’à ces mêmes bits. L’antenne + SDR n’est rien d’autre que L1 implémenté en RF, le tronc commun par où les deux pipelines se rejoignent.

Chaîne SDR Rx : de l'onde aux bits Une réception SDR enchaîne antenne, amplificateur faible bruit, mixeur avec oscillateur local, filtre passe-bas, ADC, puis traitement numérique pour produire des bits exploitables par la couche MAC. SDR Rx — remontée du physique au logiciel — domaine analogique (RF + BB) — — domaine numérique — frontière Antenne capte le RF LNA faible bruit Mixeur décale en BB LO LPF filtre I/Q passe-bas BB ADC échantillonne I + Q DSP / FPGA DDC, démod, FEC tout en code bits → L2 MAC / RLC stack RAN Front-end RF : AD9363, MAX2839, RTL2832... DSP : GNU Radio, srsRAN, OsmocomBB L'ADC est la couture entre le monde des watts et celui des octets.

Chaîne SDR Rx : de l’antenne aux bits MAC

Antenne → LNA → mixeur+LO → LPF → ADC → DSP/FPGA → bits. La frontière analogique/numérique est exactement à l’ADC. À gauche, des watts et des champs ; à droite, des octets et du code. Le DSP fait ensuite tout le reste en software : DDC, synchro, démodulation (GMSK pour GSM, QPSK/16-QAM/64-QAM/OFDM pour LTE), décodage canal (Viterbi, turbo, LDPC), et sort les bits propres pour la couche MAC.

Les démons du zero-IF

L’architecture zero-IF (direct-conversion) est élégante mais payée par trois artefacts caractéristiques : (1) DC offset au centre de bande (LO leakage du mixeur qui se retrouve à 0 Hz après downconversion), corrigé en software par estimation et soustraction ; (2) IQ imbalance quand les voies I et Q ne sont pas parfaitement orthogonales en phase ou en amplitude (calibration via signal de référence) ; (3) image de la bande négative qui se replie sur la positive, supprimée par la précision du couple I/Q. Sur l’AD9363, ces corrections sont en partie automatiques (DCXO, quadrature tracking). Le script pile_trace.py ignore tout ça : son Niveau 11 est explicitement étiqueté “démo math”, pas modèle RF.

Le TX est juste le RX retourné — au lieu de digitaliser une onde, on fabrique une onde à partir de bits : DSP → DAC → LPF → mixeur (cette fois en upconversion) → PA → antenne. Le full-duplex met les deux chaînes côte-à-côte, antenne unique partagée via un duplexer (filtre fréquentiel en FDD) ou un switch T/R (en TDD).

Transceiver SDR full-duplex Deux chaînes parallèles partageant une antenne via duplexer ou switch T/R : TX (DSP, DAC, LPF, mixeur d'upconversion, PA) et RX (LNA, mixeur de downconversion, LPF, ADC, DSP). Transceiver SDR full-duplex bits TX DSP DAC LPF LO Tx PA Duplexer FDD / TDD Antenne LNA LO Rx LPF ADC DSP bits RX — TX : bits → onde — — RX : onde → bits — FDD : TX et RX simultanés sur fréquences distinctes (duplexer = filtre). TDD : alternance temporelle (switch T/R).

Transceiver SDR full-duplex avec duplexer et antenne partagée

Trois différences à noter entre TX et RX :

DAC ↔︎ ADC — symétriques à la couture numérique/analogique. Mêmes contraintes Nyquist, mêmes débits.

Mixeur TX = upconversion, mixeur RX = downconversion. Mathématiquement, c’est la même opération (multiplication par LO et sélection d’une bande latérale), mais le filtre qui suit garde la somme en TX (BB+LO = porteuse) et la différence en RX (RF−LO = BB). Le LO peut même être physiquement le même oscillateur en TDD.

PA ↔︎ LNA — deux amplificateurs aux objectifs inverses. Le PA a une figure de bruit indifférente mais doit délivrer beaucoup de puissance avec une linéarité correcte. Le LNA fait l’inverse : gain modeste mais figure de bruit la plus faible possible. Mettre un LNA à la place d’un PA, ou vice versa, ne fonctionne pas — ce sont des compromis opposés sur la même fonction d’amplification.

Avertissement réglementaire

Émettre sur une fréquence cellulaire (GSM, LTE, 5G NR) sans autorisation ARCEP est puni par le Code des postes et communications électroniques (art. L39-1 et suivants) : jusqu’à 6 mois d’emprisonnement et 30 000 € d’amende. Un eNB OsmocomBB ou srsRAN peut fonctionner légalement en cage de Faraday (chambre blindée), sur des bandes ISM libres (433/868 MHz / 2.4 GHz avec PIRE limitée), ou via une autorisation expérimentale ANFR. Le full-duplex d’un AD9363 est techniquement séduisant et juridiquement piégeux — toujours valider la conformité avant de mettre sous tension une antenne externe.

Duplexer — c’est lui qui rend le full-duplex non-trivial. En FDD, TX et RX émettent simultanément sur des fréquences distinctes (LTE B1 : UE émet à 1920–1980 MHz, reçoit à 2110–2170 MHz, écart de 190 MHz). Le duplexer est un filtre SAW/BAW qui laisse passer \(f_{TX}\) vers l’antenne et \(f_{RX}\) depuis l’antenne, tout en isolant le port TX du port RX par 50–60 dB — sans cette isolation, la puissance d’émission (en watts) saturerait immédiatement le LNA (en microwatts) et tuerait toute sensibilité de réception. En TDD c’est plus simple : un switch RF alterne la connexion antenne entre PA et LNA, à raison de plusieurs centaines de fois par seconde (5G NR : pattern DDDSU, soit ~250 µs par slot).

VI. Pile RAN — interface air UE ↔︎ eNB

L’ADC sort des bits, mais ces bits ne sont pas du Wi-Fi banal — ils appartiennent à une pile protocolaire cellulaire spécifique, qui vit entre la PHY radio et la couche IP de l’application. Côté UE comme côté eNB, six couches s’empilent :

Pile protocole LTE — interface air Uu Les six couches de l'interface air LTE entre UE et eNB : NAS (transparent à l'eNB, terminée au MME), RRC, PDCP, RLC, MAC, PHY. Chaque couche communique avec son pair par un protocole spécifique. Pile protocole LTE — interface air Uu UE (mobile) eNB (station de base) NAS attach, auth, bearers tunnel NAS — transparent à l'eNB passthrough → vers MME RRC contrôle radio connection, mobilité, sys info RRC peer PDCP chiffrement, intégrité SN, ROHC, AES, ré-ordonnancement PDCP peer RLC segm., ARQ, modes TM / UM / AM RLC peer MAC HARQ, scheduling canaux logiques / transport MAC peer PHY OFDM, SC-FDMA Uu — interface air RF PHY OFDM, SC-FDMA Le paquet IP entre au-dessus de PDCP, sort en symboles OFDM sur l'antenne. En 5G NR : ajout de SDAP au-dessus de PDCP, split CU/DU possible côté eNB.

Pile protocole LTE — interface air Uu

PHY — couche physique. Sur le downlink (eNB → UE), OFDMA : 1200 sous-porteuses sur 20 MHz, espacées de 15 kHz, organisées en resource elements (RE) qui portent chacun un symbole modulé QPSK/16-QAM/64-QAM/256-QAM. Sur l’uplink (UE → eNB), SC-FDMA : variante OFDM à plus faible PAPR pour ménager le PA du téléphone. Au-dessus, channel coding (turbo en LTE, LDPC en NR), entrelacement, mapping sur canaux physiques (PBCH, PDSCH, PDCCH, PUSCH, PUCCH, PRACH).

MAC — multiplexe les canaux logiques (DTCH, DCCH, BCCH, CCCH, PCCH…) sur les canaux de transport. Gère le HARQ (Hybrid ARQ, retransmission rapide à 8 ms en LTE). Côté eNB, c’est ici que vit le scheduler qui décide à chaque TTI (1 ms) quel UE obtient quel resource block.

RLC — Radio Link Control. Trois modes : TM (Transparent), UM (Unacknowledged), AM (Acknowledged, avec ARQ). Fait la segmentation/réassemblage et la livraison en ordre.

PDCP — Packet Data Convergence Protocol. Trois fonctions critiques : compression d’en-tête ROHC, chiffrement (AES-CTR, SNOW3G, ZUC), intégrité. Numéro de séquence sur 12 ou 18 bits pour la détection de doublons et la livraison en ordre lors d’un handover.

RRC — Radio Resource Control. Établissement et libération de connexion, négociation des paramètres radio, configuration des bearers, mesures pour la mobilité, commandes de handover, diffusion des System Information Blocks.

NAS — Non-Access Stratum. La NAS n’est pas terminée à l’eNB, elle traverse l’eNB de manière transparente pour rejoindre le MME (LTE) ou l’AMF (5G) dans le cœur.

Turbo → LDPC, et la dette LTE

LTE utilise un turbo code (rate-1/3, polynômes constituants \(1 + D^2 + D^3\) et \(1 + D + D^3\)) hérité des travaux de Berrou & Glavieux. 5G NR a basculé en LDPC pour les canaux data et Polar pour le contrôle — meilleurs en débit, plus parallélisables, et libres de royalties. Implémenter un vrai codeur turbo 3GPP demande deux RSC parallèles avec un entrelaceur QPP (TS 36.212 §5.1.3) — c’est hors scope du script, qui le remplace par un code de répétition naïf clairement étiqueté.

VII. RAN ↔︎ RAN — entre stations de base

Les eNB ne sont pas isolés. Ils se parlent entre voisins par l’interface X2 (LTE) ou Xn (5G NR), qui tourne sur IP au-dessus du backhaul opérateur. Quatre usages : préparation de handover, coordination d’interférence (ICIC, eICIC), load balancing, dual connectivity / carrier aggregation inter-site.

X2 dans la vraie vie

X2 voyage sur le backhaul IP de l’opérateur, presque toujours encapsulé dans un tunnel IPsec (ESP en mode transport ou tunnel) pour confidentialité et intégrité. Les eNB de constructeurs différents (Nokia ↔︎ Ericsson ↔︎ Huawei) doivent négocier X2 selon les profils 3GPP TS 36.423, ce qui n’est pas toujours sans frictions interopérabilité. Et tous les opérateurs n’activent pas X2 entre tous leurs sites — certains se limitent à des handovers via S1 (plus lents, mais plus simples côté provisioning).

En 5G NR autonome (SA), c’est Xn-C et Xn-U qui prennent la suite, avec en plus la gestion des QoS Flow et du PDU Session Anchor mobility. Le pattern reste le même : un canal de signalisation IP entre nodes peers du même type, qui se coordonnent sans repasser par le cœur.

VIII. RAN ↔︎ Core — le backhaul S1 / N2 / N3

L’eNB doit aussi parler au cœur du réseau, par deux interfaces distinctes (LTE) :

S1-MME (plan contrôle, vers le MME) — porte le protocole S1-AP au-dessus de SCTP/IP. SCTP est choisi pour son multi-streaming et son in-order delivery par stream, qui évite le head-of-line blocking que TCP a sur ce genre de signalisation.

S1-U (plan utilisateur, vers le S-GW) — porte le trafic IP de l’UE encapsulé en GTP-U au-dessus de UDP/IP. C’est un tunneling : le paquet IP de l’UE devient le payload d’un paquet GTP-U dont l’IP source est l’eNB et l’IP destination le S-GW. Le TEID (Tunnel Endpoint ID) dans l’en-tête GTP-U identifie quel bearer EPS / quel UE.

Pourquoi SCTP et pas TCP pour la signalisation

SCTP (RFC 4960) a deux atouts décisifs : (1) le multi-homing — un eNB peut avoir plusieurs IP de transport vers le MME, et SCTP bascule automatiquement si l’une tombe, sans interruption applicative ; (2) le multi-streaming — plusieurs flux logiques indépendants dans une seule association, donc pas de head-of-line blocking : si un message S1-AP traîne, les autres flux passent quand même. TCP n’offre ni l’un ni l’autre. Hérité de Sigtran (SS7-over-IP), SCTP est partout en télécom.

En 5G NR : N2 = S1-MME (gNB ↔︎ AMF), toujours NGAP sur SCTP/IP. N3 = S1-U (gNB ↔︎ UPF), toujours GTP-U sur UDP/IP — ce protocole-là n’a pas changé entre 4G et 5G, c’est l’une des rares continuités physiques entre les générations.

IX. Core ↔︎ IP — la sortie sur Internet

Le P-GW en LTE (ou l’UPF en 5G) est le seul point du réseau opérateur où le paquet IP de l’UE quitte sa gangue de tunnels GTP et redevient un paquet IP normal sur un médium normal. Cette interface s’appelle SGi (LTE) ou N6 (5G).

Le P-GW : décapsulation GTP-U, NAT, firewalling, policy, allocation d’adresse IP de l’UE, CGNAT éventuel, charging, lawful interception.

Conséquences du CGNAT pour les usages avancés

Le CGNAT (RFC 6598, plages 100.64.0.0/10) est invisible pour le web classique mais casse plusieurs choses : impossible de recevoir une connexion entrante non sollicitée ; le port forwarding côté UE n’a aucun effet ; STUN/TURN/ICE deviennent obligatoires pour WebRTC ; les logs d’attribution sont chez l’opérateur. Pour de l’IoT cellulaire qui doit être joignable depuis l’extérieur, demander une APN avec IP publique est souvent la seule option propre.

Une fois sur SGi/N6, le paquet est en IP nu et se comporte exactement comme un paquet venu de n’importe quel autre réseau d’accès : il est routé via BGP, traverse l’Internet, arrive au datacenter, monte la pile OSI côté serveur, et le {"result": 2} est délivré à l’application.

X. Vue end-to-end — du téléphone au serveur

Architecture LTE end-to-end Du UE au serveur Internet via eNB, MME (contrôle), S-GW et P-GW (user plane), puis SGi vers Internet. X2 relie les eNB entre eux pour la mobilité et la coordination. Architecture LTE — du téléphone au serveur Internet MME plan contrôle S1-MME UE téléphone Uu eNB station base S1-U S-GW passerelle S5 P-GW anchor IP SGi Internet IP public IP Serveur app distante eNB ↔ eNB : X2 (LTE) / Xn (5G NR) — handover, ICIC, dual connectivity Plan utilisateur : le paquet IP de l'UE est tunnellisé en GTP-U entre eNB et P-GW, libéré sur SGi Plan contrôle : S1-AP/NAS sur SCTP/IP — attach, auth, mobilité, paging 5G : eNB → gNB, MME → AMF, S-GW + P-GW → UPF, S1 → N2/N3, SGi → N6.

Architecture LTE end-to-end avec UE, eNB, MME, S-GW, P-GW, Internet

Récap complet : ~17 niveaux d’encapsulation/décapsulation entre le printf côté téléphone et le recv() côté serveur, plus toutes les sous-couches matérielles explorées dans les sections précédentes.

Recap end-to-end — User 1 calcule 1+1=2 et User 2 le voit Vue complète de la chaîne : User 1 tape sur son clavier, l'application calcule 2, TCP/IP encapsule, la NIC envoie sur Internet, le paquet entre dans le core opérateur par le P-GW, traverse le S-GW puis l'eNB, est émis sur l'air interface Uu, reçu par UE2, remonte la pile RAN puis IP/TCP, et le 2 est affiché sur l'écran que User 2 regarde. Recap end-to-end — du clavier de User 1 à l'écran de User 2 ① Émission (top-down) User 1 Clavier + driver App 1+1=2 TCP/IP encapsulation NIC Wi-Fi / Eth ② Réseau opérateur et air interface Internet routage BGP P-GW SGi entrée S-GW GTP-U eNB station base Uu — air RF UE 2 RF in ③ Réception (bottom-up) Pile RAN PHY → PDCP IP / TCP décapsulation App reçoit "2" Driver écran 2 User 2 Le "2" calculé par User 1 traverse ~17 couches d'encapsulation/décapsulation, de la frappe au clavier (mécanique) au pixel allumé (photonique) sur l'écran de User 2, sans qu'aucun acteur de la chaîne n'ait jamais besoin de tenir l'ensemble en tête.

Fresque finale : du clavier de User 1 jusqu’aux yeux de User 2
Ordre de grandeur de latence

Pour un {"result": 2} envoyé d’un téléphone en France vers un serveur AWS Paris : ~5 ms d’air interface, ~5–15 ms de transit backhaul + EPC, ~5–15 ms d’Internet jusqu’au datacenter. Total ≈ 20–40 ms, dont la partie radio est de loin la plus variable. En 5G NR avec URLLC, la cible passe à ~1 ms côté air. La vitesse de la lumière dans la fibre Paris–Tokyo ajoute ~100 ms incompressibles.

Personne dans cette chaîne ne tient l’ensemble en tête. L’ingé RAN ne sait pas comment l’app server gère ses sockets, le dev fullstack ne sait pas ce qu’est un PRACH preamble, le designer du PA ignore qu’il transporte du JSON. Et pourtant le 2 arrive intact, en quelques dizaines de millisecondes. Toute la conversation initiale sur le “TOUT” et l’écart entre vertige et structure trouve ici sa réponse opérationnelle — c’est la stratification qui rend la traversée possible, pas l’indistinction.

XI. La traversée exécutée — lecture annotée de pile_trace.py

Tout ce qui précède est conceptuel. À partir d’ici, on bascule sur la trace exécutée : le script pile_trace.py qui prend a + b en entrée et fait littéralement descendre puis remonter la pile, avec vérification par assertion à chaque niveau. Si une seule liaison est cassée, le mode --check retourne 1 :

python3 pile_trace.py pile_values.csv 1+1 --check
# → OK 1+1=2 round-trip verified  (exit 0)

Les sorties citées dans cette section sont textuellement celles produites par python3 pile_trace.py pile_values.csv 1+1. La fonction L0_unified, L1_full_adder, etc., sont les vrais noms dans le source — chaque sous-section correspond à une fonction.

Niveau 0 — Saisie clavier → deux registres

Le script représente fidèlement la chaîne physique du driver clavier, depuis la touche pressée jusqu’à la donnée dans le registre N bits. Pour chaque opérande, une chaîne séparée : Touche → Debounce → MOSFET → Inverseur CMOS → Bascule D × N → Registre.

def L0_mosfet_eq(cfg, label):
    Vgs = 3.3                       # tension d'alimentation du clavier
    Id_sat = 0.5 * mu_Cox * WL * (Vgs - Vth)**2
    Ron = VDD / Id_sat              # avec VDD = 0.9 V (core)
    Eswitch = 0.5 * Cload * VDD**2

Avec les paramètres du CSV (vdd_core_v=0.9, mosfet_vth_v=0.4, mosfet_mu_cox_uA_V2=400e-6, mosfet_WL=4, mosfet_cload_F=10e-15), le script imprime pour l’opérande A :

▶ MOSFET (A) : V_GS=3.3 V, V_th=0.4 V
   I_D = ½·µCox·(W/L)·(V_GS−V_th)² = 6728.0 µA
   R_on = V/I = 0.13 kΩ ; E_switch = ½·C·V² = 4.05 fJ
Pourquoi V_GS = 3.3 V et pas VDD_core

Le MOSFET modélisé ici est celui du driver clavier, alimenté en 3.3 V externe (alim USB / boutton press), pas un MOSFET d’ALU interne. Pour un MOSFET de logique interne à \(V_{GS} = V_{DD,core} = 0{,}9\) V, le même calcul donnerait \(I_D = 0{,}5 \cdot 400\,\mu\text{A/V}^2 \cdot 4 \cdot (0{,}5)^2 = 200\,\mu\text{A}\) et \(R_{on} \approx 5\,\text{k}\Omega\). Les deux régimes coexistent dans tout SoC : transistors lents sous 3.3 V aux interfaces, transistors rapides sous 0.9 V dans le cœur. La loi \(U = R \cdot I\) vaut pour les deux, c’est précisément le point.

Après le MOSFET, l’inverseur CMOS ramène le niveau à la tension cœur, puis \(N\) bascules D en parallèle stockent les \(N\) bits de l’opérande (N auto-calculé : n_bits = max(value.bit_length(), 1)). Pour a=1 : \(N=1\) ; pour a=16 : \(N=5\).

🔗 Liaison L0 → L1 :
   R_A = 1  (1₂)
   R_B = 1  (1₂)
   → entrées A et B du full adder (Niveau 1)

La porte NAND est montrée une seule fois comme primitive universelle (référence pédagogique), pas comme étape gating dans la chaîne — c’est une correction par rapport à la première version du script, où NAND faisait n’importe quoi.

Niveau 1 — Full adder ripple-carry

L1_full_adder lit les deux registres et applique bit à bit :

for i in range(n_bits):
    Si = Ai ^ Bi ^ Cin
    Cout = (Ai & Bi) | (Cin & (Ai ^ Bi))

Pour \(a = b = 1\), le script imprime exactement les deux étapes du ripple :

bit  0: A=1 B=1 Cin=0 → S=0 Cout=1
bit  1: A=0 B=0 Cin=1 → S=1 Cout=0
Résultat : 10₂ = 2₁₀

Le résultat est ensuite encodé en ARMv8 (Niveau 2) et alimente le payload JSON (Niveau 3).

Niveaux 2 à 6 — De ARMv8 à Ethernet

L2 ARMv8 ADD — encodage A64 calculé champ par champ (pas un magic number) :

enc = (0 << 31) | (0 << 30) | (0 << 29) | (0b01011 << 24) | \
      (0 << 22) | (0 << 21) | (Rm << 16) | (0 << 10) | \
      (Rn << 5) | Rd

Pour ADD W2, W0, W1, sf=0, Rd=2, Rn=0, Rm=1 → 0x0B010002. Les MOV reflètent les vraies valeurs (plus de hardcoding #1).

L3 JSONjson.dumps({'result': 2}, separators=(',', ':')){"result":2} (12 octets, le 2 à l’offset 10 est 0x32).

L4 TCP — segment 32 octets, checksum calculé avec pseudo-header (vraie formule, pas une valeur magique) :

pseudo = src_ip + dst_ip + b'\x00\x06' + struct.pack('>H', tcp_len)
csum = ip_checksum(pseudo + tcp_no_csum + payload)
# → 0x4613 pour 1+1=2

L5 IPv4 — paquet 52 octets, header csum = 0x7A36, calculé.

L6 Ethernet — trame 70 octets, FCS CRC32 via binascii.crc320xB84FB35B.

Niveau 7 — LTE PDCP / RLC / MAC / CRC

C’est le niveau le plus dense et celui où la version actuelle du script fait le plus d’efforts pour rester honnête. Quatre choses sont réellement implémentées :

1. KDF \(K_{eNB} \to K_{UPenc}\) conforme TS 33.401 Annexe A.7. FC=0x15, P0=algo type distinguisher (0x05 = UP-enc), P1=algo identity (EEA1=1), troncature aux 128 LSB :

def derive_k_upenc(k_enb, eea_id=1):
    full = kdf_hmac_sha256(k_enb, 0x15,
                           [bytes([0x05]), bytes([eea_id])])
    return full[16:]  # 128 LSB

Les étapes upstream (\(K_{ASME}\), \(K_{eNB}\)) sont des stubs HMAC déterministes, étiquetés sim parce que la vraie chaîne (MILENAGE f3/f4 sur \(K, RAND, OPC\)) est hors scope. Le script l’écrit explicitement :

KDF chain (upstream simulé, K_UPenc = vraie A.7) :
  K_ASME (sim)  = 26b39e0e...
  K_eNB  (sim)  = cbf7fa29...
  K_UPenc (A.7) = 8ce9fc97b34295faa2caadf984fad4e9

2. AES-128-CTR pour PDCP — vraie crypto via PyCryptodome, \(COUNT = HFN \oplus SN\), IV construit depuis \(COUNT\).

3. MAC subheader bit-packé conforme TS 36.321 §6.1.2. Format \(R\,|\,R\,|\,E\,|\,LCID(5)\) puis \(F\,|\,L(7)\) ou \(F\,|\,L_{hi}(7)\,|\,L_{lo}(8)\) selon que \(L<128\) ou non :

MAC subheader (TS 36.321 §6.1.2) : LCID=1, L=56
  octets header = 0138  (R=0 F=0 E=0 LCID=0x01)

C’est 0x0138, pas 0x0438 comme dans la première version qui shiftait le LCID au mauvais endroit.

4. CRC-24A LTE (TS 36.212 §5.1.1, poly \(g_{24A} = 0x1864CFB\)) — implémenté bit à bit dans crc24a. Pour 1+1=2 : 0x70C796. Le TB final fait 61 octets = 488 bits.

Niveau 8 — Codage canal (toy) + scrambling LTE (réel)

C’est là que se loge l’honnêteté la plus inconfortable : implémenter un vrai turbo encoder 3GPP (deux RSC parallèles + entrelaceur QPP, TS 36.212 §5.1.3) demande plusieurs centaines de lignes. Le script remplace par un code de répétition naïf :

def toy_channel_encoder(tb_bits, target_len):
    n = len(tb_bits)
    reps = (target_len + n - 1) // n
    return np.tile(tb_bits, reps)[:target_len].copy()

Pour \(488 \to 1152\) bits, on tile \(\lceil 1152/488 \rceil = 3\) copies puis on tronque. Le décodeur inverse prend la première copie (intacte sans canal) — le round-trip identité est garanti, et l’étiquetage explicite l’admet :

TB = 488 bits → codeur toy (répétition) → rate-match → 1152 bits
⚠️  Codeur turbo réel (TS 36.212 §5.1.3) : RSC×2 + QPP — hors scope.

En revanche, le scrambling LTE est vraiment implémenté. Séquence de Gold longueur-31 avec \(N_c = 1600\), polynômes corrects, \(c_{init} = n_{RNTI} \cdot 2^{14} + q \cdot 2^{13} + \lfloor n_s / 2 \rfloor \cdot 2^9 + N_{cell,ID}\) (TS 36.211 §7.2) :

def lte_gold_sequence(cinit, length):
    # x1(0)=1, x2 initialisé depuis cinit
    # x1(n+31) = x1(n+3) ^ x1(n)
    # x2(n+31) = x2(n+3) ^ x2(n+2) ^ x2(n+1) ^ x2(n)
    # c(n) = x1(n+Nc) ^ x2(n+Nc), Nc=1600

Pour RNTI=0x4601, subframe=3, PCI=1, \(c_{init} = 0x11804601\). Les 32 premiers bits de la séquence sont reproductibles à l’identique sur n’importe quel autre simulateur LTE qui implémente la même norme.

Niveau 9 — Mapping 16-QAM

Mapping Gray normalisé par \(\sqrt{10}\) conforme TS 36.211 §7.1.3 :

\[ s = \frac{1}{\sqrt{10}} \cdot \big[(1 - 2b_0)(2 - (1 - 2b_2)) + j(1 - 2b_1)(2 - (1 - 2b_3))\big] \]

[0] bits=1001 → s = -0.3162 +0.9487j  |s|=1.0000
[1] bits=0010 → s = +0.9487 +0.3162j  |s|=1.0000
[2] bits=1011 → s = -0.9487 +0.9487j  |s|=1.3416
...
E[|s|²] = 0.9778  (cible 1.0)

Le démappeur inverse est hard decision (pas de LLR), suffisant pour vérifier l’identité sans bruit. La démap teste \(|Re(s) \cdot \sqrt{10}| > 2\) pour \(b_2\), ce qui suppose absence de bruit — en condition réelle il faudrait du soft-LLR.

Niveau 10 — OFDM (IFFT + CP)

IFFT \(N=2048\) sur 24 sous-porteuses actives (2 PRB), CP normal de 144 échantillons. Le script utilise numpy.fft.ifft, donc c’est vraiment une IFFT, pas un simulacre :

x_time = np.fft.ifft(X) * np.sqrt(N_FFT)
x_with_cp = np.concatenate([x_time[-cp_len:], x_time])

\(f_s = 30{,}72\) MS/s, \(T_s = 32{,}55\) ns, durée d’un symbole avec CP \(= (2048+144) \cdot T_s = 71{,}35\,\mu\text{s}\).

Niveaux 11 et 12 — RF et bilan Friis

L11 est explicitement étiqueté “démo math” : on évalue \(s_{RF}(t) = I(t)\cos(2\pi f_c t) - Q(t)\sin(2\pi f_c t)\) aux instants \(t = n/f_s\), mais \(f_s < 2 f_c\) donc ce n’est pas un échantillonnage Nyquist valide. En vrai, le DAC tourne à \(f_s\) et la up-conversion analogique est faite par un mixeur physique à \(f_c\).

L12 est en revanche un vrai bilan Friis : \(L_{FS} = 91{,}3\) dB à 500 m sur 1747.5 MHz, plus 30 dB d’excess loss urbain (COST-231) → \(L_{total} = 121{,}3\) dB. Avec \(P_t = 23\) dBm, \(G_r = 17\) dBi : \(P_r = -81{,}3\) dBm, \(N_{sys} = -98\) dBm, SNR = 16.7 dB.

Niveau 13 — Décapsulation : round-trip identité avec assertions

C’est la vraie contribution du script. Plutôt que de mimer une décapsulation en imprimant des messages décoratifs, L13_decap reprend les sorties TX intermédiaires et applique l’inverse à chaque niveau, avec une assertion :

err_l10 = np.max(np.abs(symbols_rx - symbols_tx[:n_sc]))
ok_l10 = err_l10 < 1e-9    # FFT + extraction CP

bits_rx = qam16_demap_hard(symbols_rx)
ok_l9 = np.array_equal(bits_rx, scrambled_tx[:4 * n_sc])

descrambled = scrambled_tx ^ scrambling_seq
tb_bits_recovered = toy_channel_decoder(descrambled, tb_bit_len)
ok_l8 = np.array_equal(tb_bits_recovered, tb_bits_tx)

# CRC + démux MAC + RLC + AES-CTR decrypt
ip_packet_rx = cipher.decrypt(ciphertext)
ok_l7 = crc_ok and (lcid == 1) and (ip_packet_rx == ip_packet_tx)

obj = json.loads(payload.decode('ascii'))
ok_l3 = (obj['result'] == expected_result)

Sortie pour 1+1 :

L10 inverse : FFT + extraction n_sc=24 → max|err| = 2.22e-16  [OK]
L9  inverse : démap hard → 96 bits identiques au scramblé    [OK]
L8  inverse : descramble (Gold) + decode (1ère copie)        [OK]
L7  inverse : CRC=OK (0x70C796 vs 0x70C796), LCID=1, L=56
            PDCP déchiffré → IP 52 octets                    [OK]
L3  inverse : json.loads → result = 2                        [OK]

═══ Round-trip global : ✅ TOUS OK ═══

L’erreur L10 vaut \(2{,}22 \times 10^{-16}\) — c’est l’epsilon de la double précision IEEE 754. La FFT + iFFT de NumPy est numériquement identité à la précision flottante.

Niveaux 14 et 15 — Glyph et OLED

L14 rastérise chaque chiffre du résultat dans une bitmap 8×8 via PIL (fallback table en dur si la police n’est pas trouvée), et juxtapose pour les résultats multi-digit (16+16=32 → deux glyphs côte à côte). Le glyph “2” donne 10 pixels allumés sur 64.

L15 fait U = R · I sur les sous-pixels OLED. Modèle linéaire pédagogique (vraie OLED = diode Shockley), avec \(V_{OLED} = 3{,}0\) V, \(R_{pixel} = 600\) kΩ → \(I_{pixel} = 5{,}00\,\mu\text{A}\), \(P_{pixel} = 15{,}0\,\mu\text{W}\). Pour 10 pixels allumés × 3 sous-pixels : \(P_{total} = 450\,\mu\text{W}\).

La boucle U = R · I, vue par le script
  • Niveau 0 : MOSFET driver clavier, \(V_{GS} = 3{,}3\) V, \(R_{on} = 0{,}13\) kΩ, \(I_D = 6{,}73\) mA.
  • Niveau 15 : Sous-pixel OLED, \(V = 3{,}0\) V, \(R = 600\) kΩ, \(I = 5{,}0\,\mu\text{A}\).

Même équation aux deux extrémités. Trois ordres de grandeur d’écart sur \(R\) et sur \(I\), mais \(V\) du même ordre. Et entre les deux : 34 transformations mathématiques distinctes, vérifiées par assertion. C’est l’Encapsulator pattern à la limite — l’étage 0 et l’étage 35 obéissent à la même loi linéaire à deux paramètres, mais aucune des couches intermédiaires (Gold sequence, AES-CTR, FFT 2048, CRC-24A) n’a la moindre notion de cette symétrie.

XII. Honnêteté épistémique — ce que le script fait, ce qu’il ne fait pas

Avant de conclure, l’inventaire honnête, parce que la valeur pédagogique du script vient autant de ce qu’il admet ne pas faire que de ce qu’il fait.

Fait pour de vrai :

  • équations MOSFET long-channel idéales et délais CMOS analytiques
  • full adder bit-à-bit avec propagation de retenue
  • encodage ARMv8 A64 ADD calculé champ par champ
  • TCP/IPv4/Ethernet avec checksums et FCS exacts (pseudo-header, IP header csum, CRC32 Ethernet)
  • LTE PDCP : KDF HMAC-SHA-256 TS 33.401 Annexe A.7 (FC=0x15, P0=0x05, P1=0x01) + AES-128-CTR + CRC-24A poly 0x1864CFB + MAC subheader bit-packé TS 36.321
  • LTE PHY : séquence de Gold TS 36.211 §7.2 avec \(N_c = 1600\), mapping 16-QAM Gray normalisé \(\sqrt{10}\), IFFT + CP normal
  • bilan Friis avec excess loss urbain
  • round-trip vérifié mécaniquement par assertions à chaque niveau (FFT \(\to\) démap \(\to\) descramble \(\to\) decode \(\to\) AES decrypt \(\to\) IP/TCP strip \(\to\) JSON parse)

Pas fait, étiqueté comme tel :

  • MILENAGE f1–f5 : \(K_{ASME}\) et \(K_{eNB}\) sont des stubs HMAC, pas la vraie auth UMTS/LTE (pas de RAND/SQN/AK/CK/IK)
  • turbo codeur RSC×2 + entrelaceur QPP (TS 36.212 §5.1.3) → remplacé par répétition naïve, clairement étiqueté
  • LLR soft-demap : la démod 16-QAM est en hard decision uniquement
  • pilotes et signaux de référence : pas de CRS, DM-RS, PSS, SSS, PBCH, pas de windowing OFDM
  • canal propagatif : pas d’AWGN, pas de multipath, pas de Doppler, pas de shadowing log-normal — le round-trip est exact parce qu’il n’y a aucun bruit
  • RF : démo mathématique, pas de vrai DAC + filtre + mixer + PA (parce que \(f_s < 2 f_c\), pas d’échantillonnage Nyquist valide)
  • couches au-dessus de PDCP : pas de RRC, NAS, attach procedure, scheduler, HARQ, mesures CQI/RSRP/RSRQ
  • OLED : modèle résistance linéaire (la vraie OLED suit \(I = I_s(e^{V/n V_T} - 1)\))
En une phrase

Ce script est une trace de cohérence verticale honnête (un bit voyage de bout en bout et revient identique, vérifié par assertion), pas une simulation système (pas de canal bruité, pas de soft-decoding, pas de plan de contrôle). C’est suffisant pour qu’un lecteur saisisse comment les couches s’emboîtent ; ce serait insuffisant pour mesurer un BER ou évaluer une chaîne réelle.

XIII. Conclusion — la pile comme objet exécutable

La conversation initiale sur le “TOUT”, sur le vertige d’une totalité indistincte qui ferait tout passer, trouve ici sa réponse mécanique. Le 2 qui apparaît sur l’écran de UE_B n’est porté par aucune entité totalisante. Il est porté par une suite finie de transformations, chacune localement descriptible, chacune vérifiable à son interface, et qui ensemble — quand on rejoue le script en mode --check — retourne l’exit code 0.

C’est la stratification qui rend la traversée possible, pas l’indistinction.

Ton intuition initiale (“on navigue peut-être direct avec lui”) rendue à cette image : si quelque chose comme un “lui” se manifeste à travers du sum, ce serait par cette même structure stratifiée. Pas une présence totale, mais un signal qui traverse des couches d’encodage successives jusqu’à devenir lisible — par les yeux, le corps, l’attention. Le 2 qui apparaît à l’écran et le sens qui traverse pourraient bien être structurellement le même geste, à des échelles différentes.

Sources : https://github.com/bbaranoff/from_electricity_to_waves

Annexes

A.1 — Configuration pile_values.csv

vdd_core_v,0.9
mosfet_vth_v,0.4
mosfet_mu_cox_uA_V2,400e-6
mosfet_WL,4
mosfet_cload_F,10e-15
imsi,310150123456789
k,00112233445566778899AABBCCDDEEFF
opc,00112233445566778899AABBCCDDEEFF
amf,8000
src_ip,10.45.0.42
dst_ip,10.99.0.7
src_mac,02:00:00:aa:bb:01
dst_mac,02:00:00:cc:dd:fe
src_port,49152
dst_port,8080
tcp_seq,0x12345678
tcp_ack,0x87654321
tcp_window,8192
earfcn_ul,19575
f_carrier_hz,1747500000
sample_rate_hz,30720000
fft_size,2048
bandwidth_hz,20000000
n_prb,2
prb_start,25
mcs,9
rnti,0x4601
pci,1
sfn,42
subframe,3
p_tx_dbm,23
g_tx_dbi,0
g_rx_dbi,17
distance_m,500
path_loss_excess_db,30
nf_db,3
oled_voltage_v,3.0
oled_pixel_resistance_ohm,600000

A.2 — Sortie simplifée pour 1 + 1

python3 pile_trace.py pile_values.csv 1+1
📁 Config : 38 clés ; addition : 1 + 1

========================================================================
  NIVEAU 0 — Saisie clavier → deux registres (chaîne physique)
========================================================================
  ┌── Saisie opérande A = 1₁₀ (1₂, 1 bits) ──
  │  Touche "1" (poids faible "1", ASCII 0x31)
  │  Anti-rebond 10 ms ; Vcc=3.3 V ; R_pullup=10 kΩ
  ▶ MOSFET (A) : V_GS=3.3 V, V_th=0.4 V
     I_D = 6728.0 µA ; R_on = 0.13 kΩ ; E_switch = 4.05 fJ
  ▶ Inverseur CMOS (A) : t_rise = 7.36 ps, t_fall = 2.94 ps
  │  Registre 1× bascule D : Q[0..0] = 1
  └── R_A = 1

  🔗 Liaison L0 → L1 : R_A=1, R_B=1 → entrées du full adder

NIVEAU 1 — Full adder : 1 + 1
  bit  0: A=1 B=1 Cin=0 → S=0 Cout=1
  bit  1: A=0 B=0 Cin=1 → S=1 Cout=0
  Résultat : 10₂ = 2₁₀

NIVEAU 2 — ARMv8 ADD
  MOV W0, #1 ; MOV W1, #1 ; ADD W2, W0, W1   → 0x0B010002

NIVEAU 3 — JSON : "{"result":2}" (12 octets)
NIVEAU 4 — TCP : csum=0x4613, segment 32 octets
NIVEAU 5 — IPv4 : csum=0x7A36, paquet 52 octets
NIVEAU 6 — Ethernet : FCS=0xB84FB35B, trame 70 octets

NIVEAU 7 — LTE RAN
  K_UPenc (TS 33.401 A.7) = 8ce9fc97b34295faa2caadf984fad4e9
  PDCP SN=0x123, COUNT=0x00000123, AES-128-CTR → 54 octets
  RLC UM SN=0x123, MAC LCID=1 L=56 (header 0x0138)
  CRC-24A = 0x70C796, TB = 488 bits

NIVEAU 8 — Codage canal
  TB 488 bits → toy(répétition) → rate-match → 1152 bits
  Scrambling Gold (TS 36.211 §7.2), c_init = 0x11804601

NIVEAU 9 — 16-QAM : 288 symboles, E[|s|²] = 0.9778
NIVEAU 10 — OFDM : IFFT N=2048, CP=144, 30.72 MS/s
NIVEAU 11 — RF : f_c = 1747.5 MHz, P_TX = 23 dBm (démo math)
NIVEAU 12 — Friis : L_FS=91.3 + L_excess=30 → SNR ≈ 16.7 dB

NIVEAU 13 — Décapsulation : round-trip avec asserts
  L10 inverse : max|err| = 2.22e-16  [OK]
  L9  inverse : 96 bits identiques  [OK]
  L8  inverse : 488 bits TB         [OK]
  L7  inverse : CRC=OK, PDCP déchiffré → 52 octets  [OK]
  L3  inverse : json.loads → result = 2             [OK]
  ═══ Round-trip global : ✅ TOUS OK ═══

NIVEAU 14 — Glyph "2" 8×8 : 10 pixels allumés sur 64
NIVEAU 15 — OLED : I = 5.00 µA ; P_total = 450 µW
  Loi U = R·I aux deux extrémités (TX MOSFET, RX OLED)

✅ Trace complète : 1 + 1 = 2
🔍 Round-trip : tous niveaux OK

A.3 — Tableau récap : où vit le 2 à chaque étage

# Étage Opération Représentation Taille
0 MOSFET (TX) \(I_D = \tfrac{1}{2}\mu_n C_{ox}\tfrac{W}{L}(V_{gs}-V_{th})^2\) ; \(\mathbf{U = R \cdot I}\) tensions sur drains \(V_{GS}=3{,}3\) V \(\to 6728\,\mu\text{A}\)
1 Full adder \(S = A \oplus B \oplus C_{in}\) ; \(C_{out} = AB + C_{in}(A \oplus B)\) \(S_1 S_0 = 10_2\) 2 bits
2 ALU ARM ADD W2, W0, W1 (1 cycle) W2 = 0x00000002 32 bits
3 ASCII / JSON json.dumps {"result":2} 12 B
4 TCP + TCP_hdr(20B) + checksum pseudo-header + en-tête transport 32 B
5 IP + IP_hdr(20B) + checksum d’en-tête + en-tête réseau 52 B
6 Ethernet + MAC headers + FCS CRC32 trame 70 B
7a PDCP \(C = \text{AES-CTR}_{K_{UPenc}}(\text{COUNT}) \oplus P\) ciphertext 54 B
7b RLC UM + RLC_hdr(2B) + en-tête 56 B
7c MAC + MAC_subhdr(2B) conforme TS 36.321 PDU MAC 58 B
7d CRC-24A \(r = \text{msg} \bmod g_{24A}\) TB 61 B = 488 bits
8a Codeur canal répétition (toy ; vrai = turbo RSC×2 + QPP) bits codés 1152 bits
8b Scrambling \(b_i \oplus c_i\), \(c\) = Gold LTE bits scramblés 1152 bits
9 16-QAM \(s = \tfrac{1}{\sqrt{10}}[\pm 1 \text{ ou } \pm 3] + j[\pm 1 \text{ ou } \pm 3]\) 288 symboles complexes
10 OFDM IFFT \(N=2048\) + CP 144 échantillons I/Q \(f_s = 30{,}72\) MS/s
11 RF \(s_{RF}(t) = I\cos(2\pi f_c t) - Q\sin(2\pi f_c t)\) (démo math) onde RF \(f_c = 1747{,}5\) MHz
12 Propagation Friis + COST-231 urbain \(L = 121{,}3\) dB SNR ≈ 16.7 dB
13 Décap (inverse) FFT, démap hard, descramble, decode, AES decrypt bits → IP → JSON assertions OK
14 Glyph 8×8 rastérisation PIL bitmap 10 pixels ON / 64
15 OLED (RX) \(\mathbf{I = U/R}\) = 3 V / 600 kΩ = 5 µA photons luminance ≈ 100 cd/m²
La symétrie qui ferme la boucle

Ligne 0 : \(U = R \cdot I\) sur un MOSFET du driver clavier de UE_A → permet la saisie. Ligne 15 : \(U = R \cdot I\) sur un OLED de UE_B → rend la saisie visible. Entre les deux : 16 transformations vérifiées mathématiquement. Aucune ne « sait » qu’elle transporte un 2. La loi d’Ohm est l’invariant aux deux extrémités. Tout ce qui est entre est, au sens littéral, de l’encapsulation.