Architecture technique — OricTel
Vue d'ensemble
OricTel est composé de deux sous-systèmes indépendants :
- Programme Oric (6502 C/ASM) — le terminal Minitel.
- Bridge Python — passerelle WebSocket ↔ TCP (chemin secondaire).
Le montage de référence est aujourd'hui LOCI + PicoWiFiModemUSB : le modem AT ouvre lui-même les sockets TCP vers le serveur Minitel, le bridge n'est plus dans le chemin. Celui-ci reste utile pour les serveurs exposés uniquement en WebSocket.
Flux de données
Serveur Minitel (ws://3617.fr/ws)
|
| WebSocket (frames binaires, octets Vidéotex bruts)
v
+-------------------+
| orictel_bridge.py | Python asyncio
| Port TCP 3615 | Relais bidirectionnel transparent
+-------------------+
|
| TCP socket (octets bruts, pas de trame)
v
+-------------------+
| Phosphoric | Émulateur Oric
| Backend ACIA | --serial picowifi / com: / tcp:
| Modem AT 8N1 | 1200 bauds (Contrôle $18, Commande $0B)
+-------------------+
|
| Registres ACIA 6551 @ $0380-$0383 (base LOCI)
| Timing cycle-exact
v
+-------------------+
| Programme OricTel | code 6502 (cc65)
| |
| serial_asm.s | <-- Driver ACIA bas niveau (polling, SEI/PLP)
| serial.c/tx.c | <-- Couche fine + file d'émission non bloquante
| at_modem.c | <-- Commandes AT, raccrochage, veille de porteuse
| videotex.c | <-- Machine à états protocole
| display.c | <-- Rendu HIRES (budget 1 ligne/passe)
| display_asm.s | <-- Blits assembleur (cellule, course, double hauteur)
| fonts.c | <-- Tables G0 / G1 / G2
| keyboard.c | <-- Scan clavier + mapping Minitel
| ui.c | <-- Helpers de menus (testables hôte)
| main.c | <-- Boucle principale, menus, session
+-------------------+
main.c est un cycle menus → connexion → session : la session ne se quitte que par ESC
(confirmé dans la barre de statut, sans effacer la page), qui raccroche (at_hangup) et repart au menu
Mode de connexion avec vtx_init. Les écrans d'échec de connexion et de perte de porteuse rendent la
main sur ESC. ESC sur le menu principal sort d'OricTel par jmp ($FFFC) (vecteur de reset,
indépendant de la ROM 1.0/1.1).
Montage de référence (matériel)
Oric --> ACIA 6551 $0380 (cartouche LOCI)
|
| firmware RP2040 : anneau RX de 32 OCTETS devant le registre
v
USB-CDC --> PicoWiFiModemUSB --> WiFi --> serveur Minitel (TCP)
L'anneau de 32 octets est un élément de conception important : le 6551 nu ne tamponne qu'un octet, et OricTel
n'utilise aucune IRQ. C'est ce tampon qui donne au logiciel une tolérance d'environ 250 000 cycles (30 octets à
1200 bauds) entre deux lectures. Émuler ce montage sans --serial-buffer est donc plus
pessimiste que le matériel réel.
Carte mémoire Oric
$0000-$00FB Zero Page (cc65 : $00E2-$00FB)
$0100-$01FF Pile 6502
$0200-$02FF Variables système
$0300-$030F VIA 6522 (miroir $0300-$03FF)
$0380-$0383 ACIA 6551 (série, base LOCI)
$0400-$0500 Zone système Oric
$0501-$97FF CODE + DATA + BSS OricTel (~37 Ko ; BSS borné à $9800)
$9800-$9BFF Jeu de caractères des lignes texte (copie de font_g0)
$9C00-$9FFF Pile cc65 (1 Ko ; relevé < 32 octets en session)
$A000-$BF3F Framebuffer HIRES (8000 octets)
$BF68-$BFDF 3 lignes texte sous le HIRES = barre de statut
$C000-$FFFF ROM (16 Ko)
Registres ACIA 6551 (côté Oric)
| Adresse | Lecture | Écriture |
|---|---|---|
$0380 | Donnée reçue | Donnée à envoyer |
$0381 | Registre statut | Reset programme |
$0382 | Registre commande | Registre commande |
$0383 | Registre contrôle | Registre contrôle |
Configuration effective (base LOCI $0380)
- Contrôle :
$18= 1200 bauds, 8N1, horloge interne - Commande :
$0B= DTR, IRQ RX désactivée, TIC=10 (RTS bas sans IRQ TX)
Piège datasheet 6551 : TIC=01 (valeurs $05/$07) active l'IRQ TX ; TDRE
étant levé en permanence, l'ACIA réassertait /IRQ sans fin et la machine gelait (régressions v0.3.3/v0.3.4).
L'ancienne configuration V23 7E1 ($28/$69) ne correspond plus à aucun montage
supporté : le PicoWiFiModemUSB dialogue en 8N1.
Bits du registre Status
- Bit 3 (RDRF=
$08) : donnée reçue disponible - Bit 4 (TDRE=
$10) : transmetteur prêt - Bit 5 (
$20) : /DCD, logique inversée — sur LOCI ce bit ne suit que le montage USB et DTR, jamais l'état de l'appel ; la perte de porteuse est détectée par la réponseNO CARRIERdu modem, confirmée par 4 s de silence
Barre de statut (3 lignes texte)
En HIRES, l'Oric affiche encore 3 lignes texte sous les 200 lignes graphiques ($BF68-$BFDF).
Elles étaient cachées parce que leur jeu de caractères, que la ROM place en $9800-$9FFF, était
écrasé par la pile C. La pile est ramenée à 1 Ko en $9C00-$9FFF (zone du jeu alternatif, jamais
sélectionné) et display_init copie font_g0 en $9900-$9BFF. Ligne 0
(cyan) : indicateur C/F, serveur, chrono mm:ss, mode de rendu, rappel
ESC ; ligne 1 (jaune) : messages transitoires ; ligne 2 (blanc) : aide des touches. L'indicateur de liaison
quitte ainsi la ligne 0 de la page Vidéotex, qui appartient au serveur.
Base de temps : Timer 2 du VIA 6522
La boucle de session n'a pas d'ISR et son itération n'a pas de durée fixe. Les délais (confirmation de perte
de porteuse 4 s, retour de l'indicateur à F après 30 s) sont donc comptés sur T2
($0308/$0309), lu à chaque itération : la ROM ne s'en sert que pour la cassette, il décompte en
continu à 1 MHz, et l'on accumule les cycles écoulés en tics de 10 ms. OricTel l'arme lui-même et interdit
son IRQ (IER = $20). Vérifié par make test-carrier : 4,0 s ± 0,1 sur ROM 1.0 et 1.1.
Protocole Vidéotex — Machine à états
NORMAL ──ESC($1B)──> ESC_SEQ
──US($1F)───> US_ROW
──SO($0E)───> (basculer G1)
──SI($0F)───> (basculer G0)
──$20-$7F──> (afficher caractère)
──$08-$0B──> (déplacement curseur)
──$0C─────> (effacer écran)
──$0D─────> (retour chariot)
ESC_SEQ ──$40-$47──> (couleur encre) -> NORMAL
──$50-$57──> (couleur fond) -> NORMAL
──$48/$49──> (flash on/off) -> NORMAL
──$4C-$4F──> (taille) -> NORMAL
──$5B─────> CSI
──$39─────> PRO
US_ROW ──$40-$57──> US_COL (mémorise ligne)
US_COL ──$41-$68──> NORMAL (positionne curseur)
CSI ──params+lettre──> NORMAL (commande ANSI-like)
Affichage HIRES
- Résolution : 240×200 pixels = 40 colonnes × 25 lignes
- Chaque cellule : 6×8 pixels
- 3 jeux de caractères :
- G0 : alphanumérique (ASCII Minitel, accents français)
- G1 : mosaïques semi-graphiques (2×3 blocs = 64 motifs)
- G2 : caractères supplémentaires (diacritiques)
Attributs par cellule
- Couleur encre (0-7)
- Couleur fond (0-7)
- Clignotement (flash)
- Inversion vidéo
- Soulignement / séparation mosaïque
- Taille (normal, double hauteur, double largeur, double taille)
- Masquage (concealed)
Rendu optimisé (plages « dirty », budget mesuré)
Chaque ligne modifiée est marquée « dirty » avec la plage de colonnes touchée. Le rendu n'est pas cadencé
sur la trame vidéo : il est budgété à une ligne par passe depuis la boucle principale, qui
rappelle display_render() tant qu'il reste des lignes sales et qu'aucun octet n'attend en
réception. Pendant une passe la réception n'est pas relue : si la passe dépasse la tolérance du tampon RX, des
octets sont perdus.
Coûts mesurés (make bench-render, cycles 6502 réels)
| Région | Coût | En temps-octet à 1200 bauds |
|---|---|---|
passe display_render() (1 ligne) | ~151 000 | 18,8 |
| ligne hybride réaliste | ~79 000 | 10,1 |
| ligne double hauteur | ~134 000 | 16,7 |
| mise à jour d'une cellule | ~30 000 | 3,6 |
vtx_process() (40 caractères) | ~105 000 | 13,2 |
Budget d'un octet à 1200 bauds : 8 333 cycles. Tolérance du montage réel : ~250 000 cycles (anneau de 32 octets du firmware LOCI). Le banc affiche aussi l'empreinte SHA-256 du framebuffer après une page canonique : toute optimisation sans effet visuel doit la laisser inchangée.
Chemins rapides assembleur (display_asm.s)
| Routine | Rôle |
|---|---|
blit_cell8 | une cellule taille normale (~190 cycles contre ~2 000 en C) |
blit_run | course de cellules éligibles ; s'arrête sur toute taille non normale |
blit_cell4x2 | double hauteur : 4 lignes source étirées sur 8 lignes pixel |
display_clear | remplissage du framebuffer par pages (~5 ms) |
Piège cc65 : ctx->screen[row][col] génère une multiplication 16 bits par 240 à chaque accès, et
un pointeur paramètre est rechargé depuis la pile logicielle à chaque déréférencement. Les boucles chaudes
hissent donc un pointeur de ligne, et videotex.c passe par un pointeur de contexte de portée fichier
plus des tables d'offsets : 4,0× sur le rendu, 1,50× sur le décodeur.
Reproduit depuis docs/ARCHITECTURE.md (version de référence à jour sur GitHub).