Fonction digitalWrite/Read vraiment "fast"

Bonjour,

Cela faisait un certain temps que je me disais qu'il faudrait que approfondisse ceci :

page que j'avais trouvé un peu au hasard.

J'ai donc décidé de l'appliquer à l'amélioration de la vitesse d'exécution des fonctions pinMode, digitalRead et digitalWrite.

Remarque préalable : les fichiers sources que je partage ne sont pas organisés selon le mode très particulier des "bibliothèques arduino".
C'est faisable mais je n'ai pas envie de le faire, donc si cela vous tente ne vous gênez pas.
Dans l'état actuel les fichiers se placent dans le même répertoire que le fichier ino.
C'est pour cela que le fichier *.h est inclu avec des guillemets et non pas avec des crochets.

Etat des lieux : la lenteur des fonctions Wiring a deux origines :

  • Une facilement éliminable : supprimer les nombreux contrôles anti-conneries. C'est ce qui est fait dans la version digitalFast. On ne gagne au mieux qu'un rapport 2.
  • L'autre inévitable : la conversion de la dénomination Arduino en dénomination Atmel.
    Ce pauvre micro avr ne comprend que le langage de son papa Atmel, il lui faut un dictionnaire arduino/Atmel.
    Tous mes autres essais ont toujours buté sur cette foutue conversion.

La solution que j'ai retenu :

  1. demander à mon cerveau de faire la conversion Arduino-> Atmel.
    Il suffit d'avoir une antisèche qui dit que par exemple la pin 13 arduino appartient au port B et a le rang 5 dans ce port. L'anti-sèche est incluse dans le fichier zip en pièce jointe.

  2. réécrire des fonctions digitalWrite/Read. Ce qui est autorisé tant qu'elles n'ont pas le même nombre et/ou le même type de variable en paramètre.

Fichiers inclu : digital2.h

/*  **********************************************************************************
   Nouvelle version de digitalWrite/Read
   Nécessite de faire soi même la conversion dénomination Arduino des IO en dénomination Atmel
   Licence complètement libre domaine public
 * ***********************************************************************************/
#ifndef DIGITAL2_H
#define DIGITAL2_H

#include <inttypes.h>

void pinMode(volatile uint8_t *ddr, uint8_t rang, uint8_t sens);

void digitalWrite(volatile uint8_t *port, uint8_t rang, uint8_t valeur);

uint8_t digitalRead(volatile uint8_t *pin, uint8_t rang);

#endif     //DIGITAL2_H

Fichier digital2.cpp

/*  *****************************************************************************************************
 * Nouvelle version de digitalWrite/Read
 * 
 * Nécessite de faire soi même la conversion dénomination Arduino des IO en dénomination Atmel
 * 
 * Licence complètement libre domaine public
 * *************************************************************/
#include "digital2.h"
#include <inttypes.h>

void pinMode(volatile uint8_t *ddr, uint8_t rang, uint8_t sens)
{
  if (sens) {*ddr |=  (1<<rang) ; }
  else      {*ddr &= ~(1<<rang) ; }
}
void digitalWrite(volatile uint8_t *port, uint8_t rang, uint8_t valeur)
{ 
  if (valeur)  { *port |=  (1<<rang) ; }
  else          {*port &= ~(1<<rang) ; }
}
uint8_t digitalRead(volatile uint8_t *pin, uint8_t rang)
{
  uint8_t lecture;
  lecture = (*pin &= (1<<rang)) >> rang ;
  return lecture;
}

NB : je n'ai pas jugé utile de gérer l'activation des pull-up, la procédure est trop simple :
1 ) configurer la broche en entrée
2) y écrire un 1
et c'est tout !

Le fichier ino pour la mesure des temps d’exécution, dérivé du bien connu "clignote.ino".

#include "digital2.h"
uint8_t mesure ;
uint8_t mesure0;
uint8_t mesure1;

void setup() 
{
  Serial.begin(115200);
  TCCR2B = 1 ; //Timer 16MHz-> compte les cycles horloge.
  mesure = 0;
  TCNT2 =0 ;  
  pinMode(&DDRB, 5, 1 ) ;  // choisir de mettre en service la nouvelle ou l'ancienne forme
  //pinMode(13, OUTPUT);
  mesure = TCNT2;
  Serial.println("\n\n MESURES ");
  Serial.print("Config du sens : Nbre de cycle = "); Serial.println(mesure);
}
void loop() 
{
  TCNT2 =0 ;
  digitalWrite(&PORTB,5,1) ; // choisir de mettre en service la nouvelle ou l'ancienne forme  //digitalWrite(13,1);
  mesure1 = TCNT2;
  delay(500);
  
  TCNT2 = 0 ;
  digitalWrite(&PORTB,5,0) ; // choisir de mettre en service la nouvelle ou l'ancienne forme  
  //digitalWrite(13,0);
  mesure0 = TCNT2 ;

  Serial.print("\n Ecriture d'un 1 = "); Serial.print(mesure1);
  Serial.print("\n Ecriture d'un 0 = "); Serial.print(mesure0);
  delay(500);

}

Et pour finir les résultats.
Les résultats sont donnés en nombre de cycles horloge. Pour ce faire le prédiviseur du timer 2 a été positionné à 1 qui correspond à une horloge de 16 MHz. Le compteur du timer2 subit une raz systématique avant chaque opération de comptage.

Dernier commentaire avant résultats : par rapport aux mesures que j'avais fait il y a 1 ou 2 ans on constate une amélioration grâce aux nouvelles version de GCC et d'AVR-GCC.

Actions Nouvelle version Version WiringArduino
Configuration de la pin 13 en sortie 3 cycles horloge 42 cycles horloge
Écriture d'un 1 sur la pin 13 3 cycles horloge 53 cycles horloge
Écriture d'un 0 sur la pin 13 3 cycles horloge 55 cycles horloge

Les fonctions Wiring/arduino suffisent le plus souvent et leur forme est plus conviviale mais cette nouvelle version près de 20 fois plus rapide sera bien utile dans des cas où la rapidité est exigée.

Antisèche :
Elle est incluse dans le fichier zip.

Occupation mémoire en octets selon les versions digitalXY

Fonctions Flash Ram
Nouvelles 2058 277
Arduino 2342 277

Gain de 284 octets en flash avec les nouvelles fonctions soit 0,9% de l'espace total pour un 328p, c'est toujours bon à prendre.

numerique_rapide.zip (236 KB)

Ce script est fantastique, je l'utilise régulièrement depuis un moment et suis étonné que personne n'ait commenté ni remercié pour ce partage.

Voilà qui est chose faite.

Toutefois, le titre mentionne "READ" mais il ne fonctionne pas chez moi avec digitalRead (Arduino v1.8.5)

En tout cas, bravo.

Bonjour,

extrait du datasheet : CBI and SBI instructions work with registers 0x00 to 0x1F only

DDRB et PORTB étant localisés aux adresses 0x04 et 0x05, ces instructions peuvent être utilisées pour (entre autres) effectuer un "digitalWrite" ou un "pinMode" en 2 cycles d'horloge

donc les + pressés pourront incorporer le bout d'assembleur qui va bien dans leur code
(j'espère que ça compile comme il faut)

trimarco232:
Bonjour,

extrait du datasheet : CBI and SBI instructions work with registers 0x00 to 0x1F only

DDRB et PORTB étant localisés aux adresses 0x04 et 0x05, ces instructions peuvent être utilisées pour (entre autres) effectuer un "digitalWrite" ou un "pinMode" en 2 cycles d'horloge

donc les + pressés pourront incorporer le bout d'assembleur qui va bien dans leur code
(j'espère que ça compile comme il faut)

Salut,

Tu as l'équivalent en C: bitSet(PORTB, 5); et bitClear(PORTB, 5);
Deux cycles aussi.

Et pour les plus pressé: PORTB = 0b00100000; et PORTB = 0b00000000;
Les huit broches du port B en un cycle. :slight_smile:

Beau déterrage de post.

Bien sur que le plus efficace est d'utiliser les registres.
Je l'avais déjà testé mais ici ce n'était pas mon but quand je me suis "amusé " à écrire ces fonctions.
Car au départ c'était bien un amusement.
Primitivement elles sont destinées à ceux qui veulent rester dans l'ambiance "Wiring/arduino".

La curiosité m'a fait mesurer le temps de "réponse" des fonctions d'écriture ou lecture sur une E/S d'un atmega328p.
J'ai fait le constat que les fonctions digitalXXX prenaient plus de 60 cycles horloges pour les E/S non capables de faire de la PWM et presque 70 pour les E/S capables de faire de la PWM.
Quant aux fonctions digitalFastXXX elles prennent encore une trentaine de cycles horloge alors qu'avec les registres cela variait de 8 à 6 en fonction de la version du compilateur GCC et avr-GCC.
Au final j'ai pu mettre en évidence que la bibliothèque digitalFasrxxx ne traitait pas la principale cause de lenteur des fonctions Wiring (fonctions qu'arduino a copié mot à mot).

Les fonctions digitalFastXXX ne suppriment que les contrôles que j'appelle "anti conneries" comme vérifier que la PWM n'est pas en service avant d'écrire dans l' E/S.
La principale cause de lenteur est la conversion entre les références Atmel et les références Wiring (arduino).
Dans les fonctions proposées cette conversion est, si je me rappelle ce que j'ai fait, à la charge du programmeur.

J'ai aussi fait un autre "amusement " : écrire une classe inspirée des classes Mbed où je faisais la conversion Atmel/Wiring dans le constructeur de la classe.
Cela fonctionnait bien, la rapidité était là et l'usage de la numérotation Wiring/arduino était conservé.
Le défaut est qu'à chaque instanciation d'un objet E/S cela occupait de la mémoire en flash alors qu'avec la solution fonctions l'occupation mémoire ne dépendait pas du nombre de d'E/S concernée.

PS : digitalRead fonctionnait, je ne peux pas exclure que, vu ma tendance assez bordélique, j'ai pu ne pas publier le dernier fichier.
Comme électronicien je n'ai pas un grand niveau en programmation. Il ne devrait pas être trop difficile de corriger les fichiers : il n'y a aucune astuce j'en suis bien incapable.

Tu as l'équivalent en C: bitSet(PORTB, 5); et bitClear(PORTB, 5);
Deux cycles aussi.

ok, merci

pour les plus pressé: PORTB = 0b00100000; et PORTB = 0b00000000;
Les huit broches du port B en un cycle

exact,
PINB = 0b00100000 peut aussi le faire :
c'est moins risqué car on ne touche pas aux autres pins
c'est + risqué si on n'est pas certain de l'état de la pin avant l'instruction

exemple, pour faire bagoter 3 x la pin vite fait, on pourrait écrire :
bitSet(PORTB, 5); // ↑ 2 cycles
PINB = 0b00100000 ; // ↓ 1 cycle
PINB = 0b00100000 ; // ↑ 1 cycle
PINB = 0b00100000 ; // ↓ 1 cycle
PINB = 0b00100000 ; // ↑ 1 cycle
PINB = 0b00100000 ; // ↓ 1 cycle