| 
	 
      | 
     |||||
|  
         Assembleur contre C et C++ Le duel...  | 
      ![]()  | 
    
Maj : 26/11/21
![]()
 Les deux protagonistes
Et bien non cela n'a rien d'un duel, ce n'est un comparatif  élémentaire des deux outils fondamentaux, bases de la programmation.
Il en existe bien d'autres, mais la connaissance de ces piliers ouvre l'esprit pour aborder facilement tous les autres langages  par la suite. Les grands principes de la programmation sont valables pour  tous les langages.
![]()
 Le compilateur C
Avant l'assembleur, les programmes ne s'écrivaient qu'avec la liste des commandes du processeur sous les yeux, en rentrant directement les codes des opérations et en calculant à la main les sauts et les adresses nécessaires, Cela était très lourd, l'assembleur a été une grande facilité, car il a remplacé les obscurs codes machines par des mots faciles à retenir et il a permis de calculer automatiquement les différentes positions mémoire.
Le C est un langage très bien écrit, d'une puissance  considérable. Il est secondé par le C++ (plus complexe) qui ouvre le monde illimité de  la programmation objet.
  Un programme développé en C ou C++sera (presque) transférable sur n'importe  quelle plate-forme matérielle du plus petit microcontrôleur au plus gros  ordinateur, en tenant bien évidemment compte des limites et des  capacités du hardware cible. La  grande richesse des bibliothèques externes permet la programmation rapide  d'applications très complexes.
Cette portabilité est un avantage exceptionnel. Les langages qui semblent d'une approche plus facile pour le débutant, comme les vieux basic ou Pascal ne présentent pas cet atout. 
Attention toutefois, le C++ et le Java évoluent très vite, ils deviennent de plus en plus complexes et pour qui n’est pas spécialiste, certaines subtilités exotiques deviennent incompréhensibles.
Le programmateur du dimanche ne s’aventurera pas dans de telles arcanes.
Qui de l'œuf ou de la poule...
Bien évidemment les premiers langages C ont été développés en  assembleur, et ensuite le serpent se mord la queue, le C a été développé en C...
![]()
 L'assembleur
  C'est le langage roi, au plus près de la machine. Rien ne  peut être plus rapide, plus compact et plus pur qu'une routine assembleur bien  écrite. 
  Programmer en assembleur implique d'avoir en permanence sous  le coude le gros databook (version papier !) correspondant précisément au processeur  choisi. Il faudra s'y référer en permanence pour découvrir tous les détails du  composant, ses adresses internes, le rôle de chacun des nombreux flags des  registres qui commandent les actions. Ce passionnant ouvrage de référence sera  l'outil fondamental, l'environnement assembleur par lui-même ne présentant pas de  difficultés. Le debuggeur associé sera un outil précieux  qui permettra de tester ses routines. Il sera  complété par les nombreuses notes d'exemples du constructeur, pleines de  ressources, car très bien écrites.
Pour chaque famille de processeurs ou microcontrôleur, il existe plusieurs assembleurs. Très souvent le constructeur offre gratuitement un outil de base, mais avec la pratique il est souhaitable d'investir dans un outil commercial plus complet qui permet d'investiguer finement dans un code buggé. Il est valable pour la famille complète, le type précis du composant sera configuré dans le menu.
En assembleur, on se préoccupe du nombre de cycles que prend une routine pour optimiser l'écriture. L'oscilloscope et l'analyseur logique resteront branchés sur le prototype en développement pour s'assurer que les timings prévus sont bien respectés. Avec le debugger (et beaucoup d'expérience...) on disposera ainsi d'une grande puissance pour réaliser des codes très performants.
En assembleur, il faut tout écrire !
  En C, pour envoyer un message sur un périphérique quelconque dénommé  xxx, un :
  xxx.print "bonjour" ; 
  suffit, tout le gros travail est fait par le langage qui comprend d'énormes bibliothèques bien optimisées pour tout faire, et qui seront appelées à la demande pour s'ajouter automatiquement par le linker, à votre code.
  En assembleur, il faudra des jours la première fois pour  mettre en œuvre une telle procédure car tout doit être défini. Cela peut paraître  monstrueux, mais c'est un passionnant jeu de l'esprit.
  En assembleur tout est maîtrisé aux bas niveaux, toutes les variables,  registres, adresses mémoires sont définies individuellement, il y a un contrôle  absolu, du nombre de cycles d'exécutions, du timing...
En C la manière dont l'outil utilisera les ressources systèmes  sont totalement inconnues de l'utilisateur. C'est une boîte noire, des "choses"  entrent d'un côté, d'autres sortent de l'autre...
Pour des routines très rapides utilisant un minimum de  ressources, par exemple un moniteur débuggeur, un loader, un noyau temps réel,  l'assembleur est irremplaçable.
Il serait possible de réaliser cela en C, mais en consommant  plus de ressources et en allant moins vite car les routines assembleur iront à  l'essentiel. Par son côté généraliste, le C ne peut pas atteindre de telles  performance, bien qu'extrêmement performant. La programmation en assembleur est  une véritable passion, c'est le jeu le plus addictif. 
 L'assembleur a toutefois deux inconvénients !
 Spécificité du matériel
Il est spécifique à un type particulier de contrôleur. Cela  n'est pas absolument pas gênant pour qui travaille sur un seul matériel  simultanément, mais pose de gros problèmes quand on développe sur plusieurs familles  pour des clients ou des projets différents, les instructions se mélangent dans  la tête ce qui fait perdre du temps. 
  Chaque famille de processeurs possède un esprit et un langage différent dont il faut s'imprégner.
  Quand on est polyglotte, il est facile de commuter sans  erreur d'une langue à l'autre au gré de ses interlocuteurs, mais quand on est  multi-assembleur c'est moins facile.
  Il faut longtemps pour maîtriser une famille et encore plus  beaucoup de différentes.
  Un logiciel parfaitement au point sur une cible devra être totalement ré-écrit si l'on change de contrôleur, rien n'est récupérable.
 Applications complexes
Le deuxième problème de l'assembleur est plus ennuyeux, il  concerne le développement d'applications très complexes.
  J'ai rencontré mes premiers problèmes après avoir développé  avec passion pendant dix ans en continu, des automates biomédicaux devenus de plus  en plus complexes. Ils étaient basés sur un noyau Perl multitâche que j'avais  entièrement développé  en quelques  dizaines de milliers de lignes. Lorsqu'il a fallu se connecter à de grosses  bases de données sécurisées par de l'Ethernet  puis de l'Internet, la tâche de tout écrire à  la main devenait insurmontable. Faire des manipulations de grosses matrices en assembleur n'est  pas très raisonnable, sauf si l'on cherche la rapidité maximale. Développer une  telle application en C et C++ prendra cent fois moins de temps et sera transposable.
  Il fallait abandonner cette voie bouchée pour passer au C afin de  bénéficier des puissantes bibliothèques existantes et tout remettre à plat.
  Toute l'algorithmique était bien rodée, il fallait  "simplement" tout réécrire. J'ai fait plus de travail fini et propre en  quelques mois en C qu'en dix ans d'assembleur.
  évidemment le nouveau code en C (pour les mêmes  caractéristiques) était plus gros et un peu moins rapide qu'en assembleur, mais cela  n'a pas d'importance car bien que le contrôleur exécute énormément de tâches, il  passe plus de 95% du temps en mode sommeil "low power". La vitesse n'intervient que lorsque l'on fait du temps réel, (exemple : guidage de missiles en poursuite) ou des calculs mathématiques très lourds (exemple : décimales de Pi). 
Aujourd'hui, je pourrais rapidement reprendre mon code en C pour ajouter ou updater des modules, ou changer de plateforme matérielle, mais je suis totalement incapable d’en faire le millième sur mes premiers travaux en assembleur, je n’ai plus les outils avec lesquels cela était développé, j’ai oublié les instructions, le noyau matériel (CPU) n’existe plus…
Il est très important de bien hiérarchiser et commenter son code pour pouvoir le comprendre quand il faudra le reprendre plus tard.
![]()
 Organisation des bibliothèques personnelles
  Une fois la routine ou la macro-commande écrite, elle sera  testée finement et classée pour des réutilisations ultérieures. Il faut faire des blocs  les plus petits possibles pour un réemploi facile. 
Essayez de ne pas  dépasser une petite page pour une belle fonction bien commentée. L'assemblage judicieux de nombreux petits blocs élémentaires  constituera un gros programme. 
Il faut hiérarchiser, c'est à dire réaliser des  fonctions à différents niveaux. Le programme final sera petit car il ne fera appel qu'à  quelques grosses fonctions, c'est une construction arborescente.
Dans  une routine, il ne faut pas faire coexister des appels à des niveaux  différents, si cela se produit, développer le code de plus bas niveau dans une  nouvelle fonction plus petite et l’appeler.
Tous les noms des fonctions doivent être très explicites, utilisez  des noms longs. En lisant le nom de la fonction, vous devez savoir ce qu'elle  fait !
On oublie vite, il sera difficile de comprendre ce que l'on  a écrit quelques années plus tôt, ou par un autre ; s'il faut reprendre un  programme qui n'a pas été très bien commenté et structuré, cela sera très lourd.
  Pour chaque fonction il faut commenter largement l'en-tête, en indiquant précisement à quoi elle sert, quelles sont les variables d'entrée, de sortie, les bugs éventuels connus à traiter ultérieurement. Soyez généreux dans les commentaires en pensant à la maintenance future.
  Au cours des ans j'ai accumulé des masses de routines dans  tous les langages.  Elles sont classées en diverses catégories :
  
Routines "blindées"
  Il s'agit de routines parfaites, totalement  optimisées, la pratique ayant montré qu'elles n'ont pas de bug, très bien commentées. Elles ont subi de sévères batteries  de tests. Il est très peu probable de pouvoir faire mieux. Elles peuvent aussi provenir des bibliothèques du constructeur qui  sont souvent excellentes. Les routines mathématiques en sont de bons exemples.
  Malheureusement,  toutes mes routines n'ont pas une telle qualité !
  
Routines agréées
Fonctionnelles, efficaces, pas de bugs  détectés pour le moment, mais tests trop sommaires. Il faudra passer du temps pour améliorer. Les routines suivantes sont repérées par un commentaire  initial indiquant leur état précaire. En passant plus de temps, il doit être possible de mieux les écrire.
  
Routines jetables
  Elles sont   écrites à la va-vite, pour un  besoin précis, mais sont très douteuses, souvent buggées, à ne jamais  réutiliser (quick and dirty programming). Les conditions aux limites et pour les cas particuliers ne  sont pas testées. Elles  permettent de faire une  maquette sale rapidement, mais les nombreux problèmes engendrés finissent par coûter très  cher et décrédibilisent le programmeur.
  
"Dummies" routines (bidons)
Il s'agit de calcul seulement simulé provisoirement  pour avancer dans un développement. C'est un artifice indispensable pour  préparer une maquette dans le but de présenter un projet à un client. La seule  chose qui l'intéresse est l'aspect visuel et l'ergonomie. Il n'est pas utile que  les calculs internes fonctionnent vraiment pour le moment, une simulation  grossière suffit. Si cela convient le gros morceau sera traité séparément ultérieurement  après validation du concept. Ces particularités seront clairement explicitées et repérées dans le planning de développement.
Cela consiste à bâtir le gros œuvre, la très longue finition  intérieure n’interviendra que plus tard. Pour le moment cela est plutôt un  décor de cinéma fait pour créer l’illusion du réel. 
![]()
 Petit exemple des deux approches
Beaucoup de débutants ont appris le C, ils aimeraient bien écrire 
  en assembleur pour améliorer les performances mais ont peur du monstre 
  et cherchent à transformer automatiquement et sans effort le code C en 
  assembleur.
  Cela est-il vraiment possible ?
  Pour comprendre le problème, il faut étudier comment travaille 
  le compilateur C.  Prenons par exemple une boucle d'attente élémentaire et regardons 
les deux approches.
 
 En C, nous allons écrire
 For (x=0 ; x<1000 ; x++ )
 { pas de code car nous faisons une boucle vide}
 Nous n'avons pas oublié avant de signaler que nous voulions utiliser 
  une variable x en la déclarant : int x ;
  Cette ligne est une boucle d'attente vide, qui va simplement perdre du temps 
  en décomptant 1000 fois.
  Remarque : Attention il n'y aura que 1000 exécutions, pas 1001, car la condition est  "strictement 
  inférieur ".
  Il faut toujours être certain que le résultat est bien celui espéré en fonction des conditions écrites, pour vérifier : 
  For (x=0 ; x<2 ; x++ ) printf ( "\nrésultat 
    = %d ", x);
   Ce code  imprime deux  lignes car dans la boucle For, la variable est incrémentée avant le test.
  Avec une boucle Do...While, il y aura 3 lignes avec x++<2, la variable est incrémentée après le test!
  Avec une boucle Do...While, il y aura 2 lignes avec ++x<2, la variable est incrémentée avant le test!
| main() { int x; for (x=0 ; x<2 ; x++ )  | 
      main()  do  | 
      main()  do  | 
    
résultat = 0  | 
      résultat = 0 
  | 
      résultat = 0 
  | 
    
Revenons à nos moutons, enfin ceux qui ont encore échappé à la fièvre aphteuse.
 
 En assembleur l'écriture est un peu différente.
 En C la déclaration des variables est automatique, il suffit de déclarer 
  <int x ;> et le compilateur se charge d'affecter les cases mémoires 
  dans l'espace disponible sans que l'utilisateur ne s'en préoccupe.
  En assembleur il faudra tout définir et choisir à quelle adresse 
  sera affectée la valeur de ce compteur. Si c'est une boucle longue, l'assembleur 
  étant très rapide, il faudra décompter sur 2 octets (espace 
  de 64 k).
  Il faut donc choisir le type de mémoire dans l'espace interne (sans planter 
  la pile) ou externe, mais les modes d'adressage sont différents, ou utiliser 
  un registre interne privilégié, mais ce n'est pas une bonne idée 
  car il faudra le sauver puis le restaurer (c'est lourd) en entrée puis 
  sortie et s'il y a interruption. Nous allons donc utiliser par exemple un registre "poubelle ", ce qui veut dire qu'il est utilisé par diverses 
  routines qui l'initialisent chaque fois.
;----> Voici une boucle générale, très utilisée 
  dans les programmes (exemple en 8051).
  r2loop: djnz R2,$ ;Boucle générale d'attente, 
  décompte de la valeur chargée
  ret ;
;----> Délai de base 10 ms utilisant l'accumulateur en le sauvant 
  puis en le restaurant.
  del10mil: push ACC ;
    mov A,#0Ah ;Délai réglé à 10 millisecondes 
    del1m1: lcall del01mil ;
    djnz ACC,del1m1 ;
    pop ACC ;
    ret ;
;----> Délai de base 1 ms usage général ne touche que 
  R2 (pour un quartz 11.0592 MHz)
  del01mil: mov R2,#199 ;922 périodes : 1 millisec
    lcall r2loop ;Lcall+ret 4 cycles
    ljmp r2loop ;4+4+4+512+398
Cet exemple est extrêmement optimisé pour être au plus près 
  du processeur, il est évident qu'aucun compilateur C ne pourrait générer 
  ces deux attentes avec si peu de ressources.
  Il devrait charger des bibliothèques générales très 
  lourdes pour arriver à ce résultat.
Le compilateur générera un code qui fonctionne parfaitement, 
  mais en analysant la sortie désassemblée, le débutant sera 
  dérouté par la complexité du résultat.  Tout ce long baratin pour expliquer qu'il ne faut pas rêver sur le traducteur 
  magique de C en assembleur.
  Le C est plus accessible pour le débutant car il l'a appris dans les 
bases de l'informatique. 
Les débutants ont souvent peur d'attaquer l'assembleur. C'est comme 
      le vélo, il y a un petit apprentissage au départ et ensuite 
      cela va tout seul. Je vous joins un projet complet d'évaluation d'une 
      carte avec un Dallas 89c420, pour montrer comment est structuré un 
      programme assembleur en 51 et le règles de base d'écriture. 
      Il utilise de nombreuses astuces que vous découvrirez peu à 
      peu suivant votre niveau. Les routines I2C sont un classique d'origine Philips.  | 
    
       Projet d'évaluation  | 
  
Le 89c420 a été remplacé par le 80c430 qui consomme moins et le 89c440 (plus de mémoire). Le 89c450 est supprimé.
![]()
 Conclusion 
La connaissance de ces deux bases fondamentales ouvre vers  tous les autres langages et les infinies possibilités de la programmation.
Faites clignoter votre première led en utilisant le C et  l'assembleur, ensuite développez suivant vos envies pour découvrir ces deux mondes  passionnants et poursuivre la quête infinie.
Il y a maintenant une multitude de langages qui ont le vent  en poupe, outre C++, Java, Python et de multiples autres, il en sort tous les  jours.
  Il est maintenant désuet et inefficace de programmer en  assembleur à l’ancienne en faisant tout à la main. Adopter un langage moderne  permet de bénéficier d’une masse de drivers et d’outils bien écrits qui  accélèrent considérablement le développement.
Il est très agréable de traverser le désert de Gobi à pied  avec une gourde, mais c’est plus confortable avec un 4*4 polluant.
Voir la page principale  Arduino  ![]()