PORTB = v ou &PORTB = v

Bonjour,

Pour éteindre la led sur une Uno, si je ne suis pas pressé, j’utilise:

digitalWrite(13, LOW);

(j’emploie aussi LEDBUILTIN, mais pas ici)

Si je suis pressé je passe à

PORTB |= 0x11011111

Cela fonctionne.

Pour éviter de chercher le port et la valeur, j’ai regardé ce qui se passe dans la bibliothèque digitalWriteFast.h, et j’ai regardé par quoi est remplacé

digitalWriteFast(13, LOW);

Voici les extraits pour la uno pour suivre cette ligne.

#define digitalWriteFast(P, V) \
  BIT_WRITE(*__digitalPinToPortReg(P), __digitalPinToBit(P), (V)); 
  

#define __digitalPinToPortReg(P) \
(((P) >= 0 && (P) <= 7) ? &PORTD : (((P) >= 8 && (P) <= 13) ? &PORTB : &PORTC))


#define __digitalPinToBit(P) \
(((P) >= 0 && (P) <= 7) ? (P) : (((P) >= 8 && (P) <= 13) ? (P) - 8 : (P) - 14))


# define BIT_WRITE(value, bit, bitvalue) (bitvalue ? BIT_SET(value, bit) : BIT_CLEAR(value, bit))


# define BIT_CLEAR(value, bit)           ((value) &= ~(1UL << (bit)))

En mixant le tout, j’obtiens:

((&PORTB) &=  ~(1UL << (5)))
  1. Si PORTB fonctionne, pourquoi &PORTB fonctionnerait?

  2. 1UL << 5 donne un entier long, pourquoi prendre un long? Quelle différence avec 1<<5 sachant que l’on a besoin d’un octet?

  3. Quelle est la différence entre 1UL<<n et 1L<<n? Pour moi quand on fait des décalages vers la droite, on introduit des 0 à gauche si le nombre est non signé, et le bit de signe si le nombre est signé. Mais dans le décalage à gauche, cela ne change rien au résultat.


Bibliothèque complète en pièce jointe car trop importante pour être mise dans le texte.

digitalWriteFast.h (11.1 KB)

A force d’y penser toute la nuit, j’ai trouvé la réponse à la première question:
BIT_WRITE(*__digitalPinToPortReg(P), __digitalPinToBit(P), (V));
Il est demandé un passage par adresse.

Mais je ne comprends pas bien pourquoi utiliser
((*&PORTB) &= ~(1UL << (5)))
plutôt que
((PORTB) &= ~(1UL << (5)))

Les points 2 et 3 restent entiers:
2) 1UL << 5 donne un entier long, pourquoi prendre un long? Quelle différence avec 1<<5 sachant que l’on a besoin d’un octet?

  1. Quelle est la différence entre 1UL<<n et 1L<<n? Pour moi quand on fait des décalages vers la droite, on introduit des 0 à gauche si le nombre est non signé, et le bit de signe si le nombre est signé. Mais dans le décalage à gauche, cela ne change rien au résultat.

PORTX n'est pas défini par Wiring/Arduino mais par Atmel directement.
L'IDE arduino inclu l'avr-libc , avr-gcc, avrdude et un certains nombre de fichiers de définition d'Atmel.

Regarde comment PORTB est défini (avec son adresse en mémoire) dans le fichier iom correspondant au microcontroleur (iom328p pour un atmega328p)

Un registre de GPIO se manipule par adresse.

PORTB représente donc le contenu d'un registre dont l'adresse est connue.
C'est un peu comme si l'on définissait PORTB comme *((uint8_t *)0x28)

Donc *&PORTB est égal à PORTB comme pour n'importe quelle variable.

pourquoi prendre un long ?

La librairie ARDUINO est multiplateforme.
Certains processeurs ont des registres 32 bits.

Ils ont fait ce choix pour pouvoir travailler avec la macro __digitalPinToPortReg().

Quand vous écrivez direct à la main, vous pouvez faire directement [color=blue]PORTB &= ....;[/color] (ou [color=blue]PINB = ...;[/color] si vous voulez basculer)

  1. 1UL << 5 donne un entier long, pourquoi prendre un long? Quelle différence avec 1<<5 sachant que l’on a besoin d’un octet?

ça permet sans doute la portabilité vers des machines qui ont des registres 32 bits. Sur votre UNO ce sera tronqué ensuite à l’octet de poids faible.

Merci,

Ils ont fait ce choix pour pouvoir travailler avec la macro __digitalPinToPortReg().
Un registre de GPIO se manipule par adresse

En fait a force de faire des remplacements, avec la bibliothèque digitalWriteFast finit par
((*&PORTB) &= ~(1UL << (5)))

__digitalPinToPortReg(13) est remplacée par &PORTB on aurai pu passer directement sans *&

Dans la bib, j’ai enlevé * pour m’assurer que c’était bien le bon code que j’utilisais. Cela ne se compile pas, c’est normal. Puis j’ai retiré les & devant les PORTB, PORTC et PORTD, et cela refonctionne…

Pour le long, effectivement cette bibliothèque fonctionnerait avec une Due, mais je n’ai pas vu si digitalWriteFast fonctionne sous Due, je n’en vois pas les lignes comme pour les autres cartes.
Ilest vrai que si on doit prendre un long, un signé n’a pas de sens.

Je n’ai pas fait les test pour l’instant, je n’arrive plus à lancer l’IDE…

Dans la bib, j'ai enlevé * pour m'assurer que c'était bien le bon code que j'utilisais. Cela ne se compile pas, c'est normal. Puis j'ai retiré les & devant les PORTB, PORTC et PORTD, et cela refonctionne...

Comme je disais :

PORTB représente la valeur de PORTB.
&PORTB représente l'adresse de PORTB.
Donc *&PORTB représente le contenu de l'adresse de PORTB, donc *&PORTB = PORTB.

Si tu veux aller vite, AMHA, le mieux est de t'inspirer de la bibliothèque pour DH22 écrite par Rob Tillaart.

La bibliothèque digitalFast ne fait que supprimer les contrôles anti-conneries, la grosse perte de temps est dans la série de macro Wiring/arduino pour passer de la numérotation Arduino à celle d'Atmel qui est la seule que le micro comprends.
Cette conversion se fait systématiquement à chaque appel de la fonction.

L'idée de Rob est de faire une classe et de faire cette correspondance dans le constructeur ce qui fait que cette correspondance n'a lieu qu'une seule fois à la création de l'objet et après c'est aussi rapide que de manipuler directement les registres.
Cela permet de conserver le coté pratique de la dénomination Wiring/arduino tout en obtenant la rapidité de la manipulation des registres.
Cela fonctionne très bien mais occupe un peut plus d'octets en mémoire qu'une fonction qui refait la conversion à chaque appel.

Mais là tu n'es plus universel, c'est du dédié à chaque type de microcontrôleur, d'un autre coté il est difficile de faire autrement si tu veux optimiser aux petits oignons.

hello
ce comptage de cycle va te rappeler quelquechose :slight_smile:

int nombre_cycle = 0;
void setup() {
  Serial.begin(1000000);
  PORTB = 0;
  Serial.print("PORTB= "); Serial.println(PORTB);
  TCCR2B = 1 ; //Timer 2 lancé avec diviseur = 1
  TCNT2 = 0 ; // Raz du compteur
  digitalWrite(13, 1);  //2500.0 nano    0.00000250
  nombre_cycle = TCNT2 ;
  Serial.print("digitalWrite(13,1); ");
  Serial.print((nombre_cycle * 62.5)); Serial.print(" nanosecondes  ");
  Serial.print("PORTB= "); Serial.println(PORTB);
  TCNT2 = 0;
  PORTB &= 0x11011111;    // 250.0 nano    0.00000250
  nombre_cycle = TCNT2 ;
  Serial.print("PORTB &= 0x11011111; ");
  Serial.print((nombre_cycle * 62.5)); Serial.print(" nanosecondes  ");
  Serial.print("PORTB= "); Serial.println(PORTB);
  PORTB = 0;
  Serial.print("PORTB= "); Serial.println(PORTB);
  TCNT2 = 0;
  asm("sbi 0x05,5");
  nombre_cycle = TCNT2 ;
  Serial.print("sbi 0x05,5 ");
  Serial.print((nombre_cycle * 62.5)); Serial.print(" nanosecondes  ");
  Serial.print("PORTB= "); Serial.println(PORTB);
  TCNT2 = 0;
  asm("cbi 0x05,5");    // 187.5 nano    0.000000187
  nombre_cycle = TCNT2 ;
  Serial.print("cbi 0x05,5 ");
  Serial.print((nombre_cycle * 62.5)); Serial.print(" nanosecondes  ");
  Serial.print("PORTB= "); Serial.println(PORTB);
}

void loop() {}

Merci pour toutes les remarques.

Pour répondre à Henri, J'ai bien vu que *&PORTB c'est pareil que PORTB, et c'est pourquoi j'ai modifié la librairie en supprimant * et &. Cela fonctionne pareil, mais pourquoi chercher *&

La bibliothèque digitalFast ne fait que supprimer les contrôles anti-conneries, la grosse perte de temps est dans la série de macro Wiring/arduino pour passer de la numérotation Arduino à celle d'Atmel qui est la seule que le micro comprends.
Cette conversion se fait systématiquement à chaque appel de la fonction.

Pas d'accord, dans la bibliothèque digitalFast, la conversion est faite avant la compilation et digitalWriteFast(13,5); ne prend que 2 cycles. Mais on ne peut y mette que des constantes. Du coup, il n'y a pas la perte de temps.

Je vais regarder le DHT22, mais il va falloir avant que je réinstalle L'ide.

Encore que dans ce que je viens d'écrire, les sorties sont mises à 0 en début de programme, et pour les mettre à 1 ou à 0, j'utilise l'équivalent de PINB=B00100000; qui est plus rapide. En fait je pourrais utiliser PORTB=B00100000; en fonction du câblage que je fais.

ce comptage de cycle va te rappeler quelque chose

Oui, j'utilise aussi un chronomètre à instructions.

Mais ce programme me pose des questions (ah, il y a u n0x00100000 au lieu d'un B00100000, mais je suppose que c'est pour voir si j'allais décortiquer le code). J'ai rajouté:

PORTB = 0;
  Serial.print("PORTB= "); Serial.println(PORTB);
  
  TCNT2 = 0;
  PINB=B00100000;    // 187.5 nano    0.000000187
  nombre_cycle = TCNT2 ;
  Serial.print("PINB=B00100000 ");
  Serial.print((nombre_cycle * 62.5)); Serial.print(" nanosecondes  ");
  Serial.print("PORTB= "); Serial.println(PORTB);
  
  TCNT2 = 0;
  PINB=B00100000;    // 187.5 nano    0.000000187
  nombre_cycle = TCNT2 ;
  Serial.print("PINB=B00100000 ");
  Serial.print((nombre_cycle * 62.5)); Serial.print(" nanosecondes  ");
  Serial.print("PORTB= "); Serial.println(PORTB);
  
  TCNT2 = 0;
  PINB=B00100000;    // 187.5 nano    0.000000187
  nombre_cycle = TCNT2 ;
  Serial.print("PINB=B00100000 ");
  Serial.print((nombre_cycle * 62.5)); Serial.print(" nanosecondes  ");
  Serial.print("PORTB= "); Serial.println(PORTB);
  
  TCNT2 = 0;
  PINB=B00100000;    // 187.5 nano    0.000000187
  nombre_cycle = TCNT2 ;
  Serial.print("PINB=B00100000 ");
  Serial.print((nombre_cycle * 62.5)); Serial.print(" nanosecondes  ");
  Serial.print("PORTB= "); Serial.println(PORTB);

C'est bien 4 fois la même chose, et je trouve 3cycles, 2 cycles, 2 cycles, 2 cycles. Pourquoi la première fois il y a un cycle e plus? Encore une farce du compilateur? Je m'attendais éventuellement à tomber sur un chronométrage incluant l'int 0, mais je suppose que ce n'est pas ça
Dans ce chronométrage, on a un cycle mesuré en trop que l'on pourrait retirer, par exemple en initialisant le compteur à 1 au lieu de 0.

Mais là tu n'es plus universel, c'est du dédié à chaque type de microcontrôleur, d'un autre coté il est difficile de faire autrement si tu veux optimiser aux petits oignons.

De toutes façons si je veux pouvoir utiliser plusieurs micros, rien ne m'empêche de de mettre des conditions à la compilation. Mais si je veux vraiment être universel, il faudrait au moins que je fasse les essais sur toutes les cartes, ce que je n'ai pas. Je peux n'être compatible qu'avec les cartes que j'ai.

Pour le nombre de cycle je ne sais pas analyser le code, je travaille avec le compteur d'un timer 8 bits dont je cale le prédiviseur à 1 pour que le compteur donne directement une mesure en cycle horloge.
De mémoire, (elle va encore pas trop mal même si parfois......) :

  • digitaWrite/Read de Wiring/arduino : 62 cycles horloge, plus et surtout instable pour les sorties PWM
  • digitalFast 32 cycles horloge soit encore la moitiè de la fonction Wiring comme je l'ai indiqué. C'est une fonction qui fait la conversion à chaque appel.
  • Manipulation des registres : 8 cycles horloge.

Nota : ces résultats sont dépendants de la version de l'avr-gcc et du niveau de ses optimisations.
Les premières mesures que j'ai effectuées avec la version antedilluvienne d'avr_gcc qui était inclue dans l'IDE de l'époque donnaient des résultats pires.
J'ai pu constater ce fait parce qu'en paralelle j'avais essayé d'utiliser Eclipse qui lui utilisait la dernière version de l'avr-gcc disponible sur ma Debian.
C'est pour cela que je crois plus le compteur du timer que l'analyse théorique du code.

C'est pour cela que je crois plus le compteur du timer que l'analyse théorique du code.

ça dépend quel code... si vous regardez l'assembleur il suffit de compter les cycles par instruction.

ce que tu fais avec le timer, c'est du bricolage ; l'assembleur, c'est le juge de paix
(au pire on peut importer le code dans atmel studio pour le simuler en faisant tourner le stopwatch : ça permet d'avoir la durée de 1000 lignes au cycle prêt)

en regardant l'assembleur, tu auras la réponse à toutes tes questions - sauf peut-être à "pourquoi tu te poses toutes ces questions"

l'instruction assembleur OUT ne fonctionne que depuis un registre, pas depuis une constante ; une fois que le registre est chargé avec la constante, ça va + vite ...

J-M-L:
ça dépend quel code... si vous regardez l'assembleur il suffit de compter les cycles par instruction.

Je n'ai jamais dit le contraire, j'ai même dis que je ne sais pas le faire et j'ajoute que je ne saurai jamais le faire parce que je n'ai pas envie d'apprendre l'assembleur. Je ne suis pas et je ne serai jamais un professionnel de la programmation.

Vous préférez mettre un condensateur anti-rebond qui traite le problème à l'origine et qui protège le restant de l'électronique ou vous préférez "bricoler" en promenant le problème dans votre montage et aligner les lignes de code que je m'accorde le droit de trouver inutiles ?

Avec un avr ma façon de procéder m'a toujours donner le niveau de précision suffisant. Elle est accessible à tous sur ce forum, je n'en dirai pas autant pour l'assembleur.
Et contrairement à millis ou à micro elle n'induit pas de temps de taitement supérieur à la différence de temps que l'on veut mesurer.

Votre point était assez général, je pensais que vous parliez du code avant compilation en C++ puisque vous évoquiez la différence de performance entre 2 compilos

Mais oui je suis d’accord faut garder l’esprit ouvert.

puisque vous évoquiez la différence de performance entre 2 compilos

C'était un simple constat la version d'avr-gcc livrée par arduino était bien plus vieille que la version stable officielle et les résultats était très variables.
J'avais obtenu les meilleurs résultats avec Eclipse avec une optimisation du compilateur sur la taille de l'exécutable et non pas sur la rapidité, je tiens à le signaler.
Les sources de l'IDE provenaient du paquet Debian. Le problème avec Eclipse c'est que je n'ai jamais compris comment on pouvait faire un modèle réutilisable. En fait j'ai cru comprendre qu'il fallait écrire un plug-in donc hors de question pour moi.

Certains, dont ici Barbudor, avaient essayé la dernière version de l'IDE en utilisant la dernière version d' avr-gcc et cela ne fonctionnait pas alors que pour ceux qui utilisaient la version mis en paquet par Debian cela fonctionnait.

A partir de maintenant je fais des suppositions :
A l'époque j'avais pu voir que dans l'IDE il n'était toujours pas question de la macro "ISR" mais toujours de "SIGNAL" qui était "deprecated" depuis plus de 10 ans. Apparemment le "Search & Replace" était inconnu d'Arduino.
J'ai vu ce point mais il ne devait pas être le seul et avec trop de "deprecated" cela ne fonctionnait pas avec un avr-gcc à jour.
Ce travail devait être fait par le responsable Debian pour qui il était inenvisageable d'utiliser une autre version que celle de la distribution.

ça dépend quel code… si vous regardez l’assembleur il suffit de compter les cycles par instruction.

Pour le comptage du temps d’une instruction, chacun a sa méthode préférée, et chacun utilise la sienne. Il vaut mieux un outil moins bon qu’un outil dont on ne sait pas se servir.

J’utilise souvent en parallèle mon chronomètre et la liste des instructions avec leur temps pour les instructions extrêmement simples. Mais si c’est pour tester si un calcul se fait plus vite avec des long ou des float, ou si il vaut mieux mettre if ou case… c’est plus compliqué. Je suis en C et pas en assembleur, et je ne sais pas encore lire les fichiers objets (sans doute parce que je n’en ai pas vraiment besoin), et le chronométrage est un outil simple.

De mémoire, (elle va encore pas trop mal même si parfois…) :

  • digitaWrite/Read de Wiring/arduino : 62 cycles horloge, plus et surtout instable pour les sorties PWM
  • digitalFast 32 cycles horloge soit encore la moitiè de la fonction Wiring comme je l’ai indiqué. C’est une fonction qui fait la conversion à chaque appel.
  • Manipulation des registres : 8 cycles horloge.

Je sais que le digitalWrite est lent, mais quand je l’utilise, je n’ai pas besoin de vitesse. C’est en tout cas plus lisible par un tiers.

Pour le digitalWriteFast de digitalWriteFast.h, j’obtiens 2 cycles, exactement comme PORTB=0x20;
Peut être ce n’est pas la même bibliothèque.

Et contrairement à millis ou à micro elle n’induit pas de temps de taitement supérieur à la différence de temps que l’on veut mesurer.

Perso, j’utilise micros(); et une instruction vide me retourne 0 cycle, un nop 1 cycle, un PORTB=0x20; 2 cycles, tout va bien.