IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Navigation

Inscrivez-vous gratuitement
pour pouvoir participer, suivre les réponses en temps réel, voter pour les messages, poser vos propres questions et recevoir la newsletter

Arduino Discussion :

signed int : à éviter ?


Sujet :

Arduino

  1. #1
    Membre éprouvé Avatar de electroremy
    Homme Profil pro
    Ingénieur sécurité
    Inscrit en
    Juin 2007
    Messages
    934
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 43
    Localisation : France, Doubs (Franche Comté)

    Informations professionnelles :
    Activité : Ingénieur sécurité
    Secteur : Industrie

    Informations forums :
    Inscription : Juin 2007
    Messages : 934
    Points : 1 274
    Points
    1 274
    Par défaut signed int : à éviter ?
    Bonjour,

    Toujours avec mon projet Arduino UNO / Shield Ethernet / Ecran TFT ILI9341 + dalle tactile XPT 2046, mon travail d'optimisation porte ses fruits :
    - code plus court de 5000 octets (sur 32K c'est pas négligeable), j'ai maintenant 10K de libres pour d'autres fonctionnalités
    - affichage du texte 2,5X plus rapide et des bitmaps 3X plus rapide
    Ceci alors que je suis parti d'une librairie déjà optimisée PDQ GFX par rapport à la Adafruit d'origine.

    J'ai fait une remarque : les instructions sur les types 'int' sont très consommatrices ; une affectation comme celle-là :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
     
    int A;
    int B;
    ...
    ...
    A = B;
    bouffe 24 octets de ROM dans le code.

    Un type 'int' c'est un entier signé, il y a plein de vérification que le microcontrôleur dois faire...

    Ce qui n'est pas le cas avec un unsigned int

    Problème : le c++ Arduino ressemble tellement au c++ classique que les gens ont écrit des programmes de la même façon.

    Sur un PC, les instructions sur les entiers signés sont câblés, mais pas toujours dans les microcontrôleurs.

    Du coup, lorsqu'on utilise des entiers signés sur un microcontrôleur, c'est le carnage car le compilateur doit ajouter plein de code.

    En épluchant mon code et le code des bibliothèques et des fonctions du core, on voit que l'emploi de ces entiers signés est souvent injustifié :
    - soit le code n'utilise que des valeurs positives
    - soit on utilise juste une valeur négative (typiquement '-1') pour renvoyer une erreur

    Parfois c'est pire, pour des valeurs toujours comprises entre 0 et 255 le code utilise un 'int' au lieu d'un byte.

    Donc la prochaine étape pour moi c'est de partir à la chasse au 'int'

    Ce n'est pas toujours évident et la question que je me pose est de savoir si on peut forcer la copie directe sans code de vérification (comme le cas de mon exemple).

    Si je copie un 'int' dans un autre 'int' il ne peut pas y avoir de débordement ! Le contrôle a déjà été fait lorsque le 'int' source a été affecté...

    De même, forcer une copie directe d'un unsigned int dans un int, quand on est certain que le unsigned int a une valeur qui ne sera pas interprétée comme un nombre négatif...

    A bientôt
    Quand deux personnes échangent un euro, chacun repart avec un euro.
    Quand deux personnes échangent une idée, chacun repart avec deux idées.

  2. #2
    Modérateur

    Homme Profil pro
    Ingénieur électricien
    Inscrit en
    Septembre 2008
    Messages
    1 267
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 41
    Localisation : Suisse

    Informations professionnelles :
    Activité : Ingénieur électricien

    Informations forums :
    Inscription : Septembre 2008
    Messages : 1 267
    Points : 4 829
    Points
    4 829
    Par défaut
    Bonsoir Rémy

    Intuitivement je dirais que les AVR sont autant efficace en signé qu'en non signé. Les différents tests et opérations sont câblées pour faire les deux types de variable, y compris pour les retenues.

    Par contre c'est la longueur des variable qui influe énormément.
    Mettre un int au lieu d'un Byte c'est un facteur 4 de base.
    Puis sur des algorithmes simple ne nécessitant que quelques variables (jusqu'à 8), en Byte le compilateur pourra le faire sans passer par la RAM, uniquement en restant dans les registres, alors qu'en 32bits, il faudra quasi systématiquement aller lire et écrire dans la RAM. Une opération de base c'est 1 cycle et 2 octets de ROM, pour du byte stocké en registre. 4 cycles et 8 octets de ROM pour du INT en registre (mais là c'est limité à 2 variables), lire ou écrire un INT en RAM (vers ou depuis les registres) c'est 8 cycles et 16 octets de ROM.

    Pour l'analyse:
    • On peut demander de lire le code assembleur par l’intermédiaire du fichier .lss généré à la compilation. En cas de compilation en mode debug, il y a même le code C/C++ original en commentaire.
    • Il existe un service en ligne qui permet de voir le code ASM issu d'une compilation: https://godbolt.org/ (en alternative au premier point)
    • Puis pour savoir ce que fait chaque instruction ASM, la place qu'elle occupe en ROM et sa durée d’exécution, il y a l'AVR Instruction Set Manual.
    • Une autre référence pour cette analyse c'est le Atmel AT1886: Mixing Assembly and C with AVRGCC qui explique comment le compilateur GCC AVR utilise les registres.


    Bonne suite

    Delias

  3. #3
    Expert confirmé

    Homme Profil pro
    mad scientist :)
    Inscrit en
    Septembre 2019
    Messages
    2 711
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Paris (Île de France)

    Informations professionnelles :
    Activité : mad scientist :)

    Informations forums :
    Inscription : Septembre 2019
    Messages : 2 711
    Points : 5 390
    Points
    5 390
    Par défaut
    Un type 'int' c'est un entier signé, il y a plein de vérification que le microcontrôleur dois faire...
    Ce qui n'est pas le cas avec un unsigned int
    Non pas avec ce type de code.

    les calculs sur 16 bits sur un micro-contrôleur 8 bit sont forcément plus coûteux que si l'on peut utiliser des variables sur un octet puisqu'il faut injecter du code intermédiaire, mais les opérations signées ou non signées sont gérées de manière "identique" (en nombre d'instructions) au niveau du micro.

    si vous prenez le petit code ci dessous (variables en volatile pour éviter les optimisations du compilateur)
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    volatile int16_t a, b, c;
    //volatile uint16_t a, b, c;
    //volatile int8_t a, b, c;
    //volatile uint8_t a, b, c;
     
    void setup()
    {
      a = 5;
      b = 8;
    }
     
    void loop() {
      c = a + b;
      delay(2000);
    }

    int16_t => Le croquis utilise 644 octets et Les variables globales utilisent 15 octets
    uint16_t => Le croquis utilise 644 octets et Les variables globales utilisent 15 octets
    int8_t => Le croquis utilise 618 octets et Les variables globales utilisent 12 octets
    uint8_t => Le croquis utilise 618 octets et Les variables globales utilisent 12 octets

    on voit bien qu'on a la même taille de code et de mémoire RAM et que ça ne dépend que du fait qu'on traite des entiers dur 8 bits ou 16 bits

    Par contre, si vous insérez un Serial.print() vous verrez des impacts sur la taille mémoire parce que la fonction print() qui est appelée n'est pas la même pour un entier signé ou non signé et donc le compilateur n'injecte pas le même code à compiler. Mais ce n'est pas lié à vos calculs, plus au type de code qui doit différencier un entier signé d'un entier non signé.

  4. #4
    Membre éprouvé Avatar de electroremy
    Homme Profil pro
    Ingénieur sécurité
    Inscrit en
    Juin 2007
    Messages
    934
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 43
    Localisation : France, Doubs (Franche Comté)

    Informations professionnelles :
    Activité : Ingénieur sécurité
    Secteur : Industrie

    Informations forums :
    Inscription : Juin 2007
    Messages : 934
    Points : 1 274
    Points
    1 274
    Par défaut
    Citation Envoyé par Jay M Voir le message
    les calculs sur 16 bits sur un micro-contrôleur 8 bit sont forcément plus coûteux que si l'on peut utiliser des variables sur un octet puisqu'il faut injecter du code intermédiaire, mais les opérations signées ou non signées sont gérées de manière "identique" (en nombre d'instructions) au niveau du micro.

    Par contre, si vous insérez un Serial.print() vous verrez des impacts sur la taille mémoire parce que la fonction print() qui est appelée n'est pas la même pour un entier signé ou non signé et donc le compilateur n'injecte pas le même code à compiler. Mais ce n'est pas lié à vos calculs, plus au type de code qui doit différencier un entier signé d'un entier non signé.
    OK je comprend mieux...

    J'ai utilisé l'astuce décrite ici https://forum.arduino.cc/index.php?topic=203262.0 pour avoir un fichier .txt élaboré à partir du fichier .elf

    Il me décrit de quelle façon le code C++ est compilé en assembleur, en montrant le code assembleur en version code et en version binaire

    C'est très intéressant !

    C'est assez impressionnant de voir que de "petites fonctions" demandent pas mal code.

    Par exemple, void delay(unsigned long ms) :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    void delay(unsigned long ms)
    {
         772:	8f 92       	push	r8
         774:	9f 92       	push	r9
         776:	af 92       	push	r10
         778:	bf 92       	push	r11
         77a:	cf 92       	push	r12
         77c:	df 92       	push	r13
         77e:	ef 92       	push	r14
         780:	ff 92       	push	r15
         782:	6b 01       	movw	r12, r22
         784:	7c 01       	movw	r14, r24
    	uint32_t start = micros();
         786:	0e 94 94 03 	call	0x728	; 0x728 <micros>
         78a:	4b 01       	movw	r8, r22
         78c:	5c 01       	movw	r10, r24
     
    	while (ms > 0) {
    		yield();
    		while ( ms > 0 && (micros() - start) >= 1000) {
         78e:	c1 14       	cp	r12, r1
         790:	d1 04       	cpc	r13, r1
         792:	e1 04       	cpc	r14, r1
         794:	f1 04       	cpc	r15, r1
         796:	b9 f0       	breq	.+46     	; 0x7c6 <delay+0x54>
         798:	0e 94 94 03 	call	0x728	; 0x728 <micros>
         79c:	68 19       	sub	r22, r8
         79e:	79 09       	sbc	r23, r9
         7a0:	8a 09       	sbc	r24, r10
         7a2:	9b 09       	sbc	r25, r11
         7a4:	68 3e       	cpi	r22, 0xE8	; 232
         7a6:	73 40       	sbci	r23, 0x03	; 3
         7a8:	81 05       	cpc	r24, r1
         7aa:	91 05       	cpc	r25, r1
         7ac:	80 f3       	brcs	.-32     	; 0x78e <delay+0x1c>
    			ms--;
         7ae:	21 e0       	ldi	r18, 0x01	; 1
         7b0:	c2 1a       	sub	r12, r18
         7b2:	d1 08       	sbc	r13, r1
         7b4:	e1 08       	sbc	r14, r1
         7b6:	f1 08       	sbc	r15, r1
    			start += 1000;
         7b8:	88 ee       	ldi	r24, 0xE8	; 232
         7ba:	88 0e       	add	r8, r24
         7bc:	83 e0       	ldi	r24, 0x03	; 3
         7be:	98 1e       	adc	r9, r24
         7c0:	a1 1c       	adc	r10, r1
         7c2:	b1 1c       	adc	r11, r1
         7c4:	e4 cf       	rjmp	.-56     	; 0x78e <delay+0x1c>
    		}
    	}
    }
         7c6:	ff 90       	pop	r15
         7c8:	ef 90       	pop	r14
         7ca:	df 90       	pop	r13
         7cc:	cf 90       	pop	r12
         7ce:	bf 90       	pop	r11
         7d0:	af 90       	pop	r10
         7d2:	9f 90       	pop	r9
         7d4:	8f 90       	pop	r8
         7d6:	08 95       	ret
    En assembleur 8051, pour faire des pauses, j'avais :
    - une toute petite fonction pause courte (une boucle "dans le vide")
    - une toute petite fonction pause moyenne qui était une boucle qui appelait pause courte
    - et une toute petite fonction pause longue, une boucle qui appelait pause moyenne

    Là, il y a quand même une sacrée tartine de code je trouve.

    On peut voir ce que coute une incrémentation sur un 'int' (Buffi est une variable globale 'int') :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    int LireUInt9() {
      // Entier compris entre 0 et 510 - Occupe un octet si inférieur Ã* 255
      int tmp;
      Buffi++;
         e72:	20 91 e6 05 	lds	r18, 0x05E6	; 0x8005e6 <Buffi>
         e76:	30 91 e7 05 	lds	r19, 0x05E7	; 0x8005e7 <Buffi+0x1>
         e7a:	c9 01       	movw	r24, r18
         e7c:	01 96       	adiw	r24, 0x01	; 1
         e7e:	90 93 e7 05 	sts	0x05E7, r25	; 0x8005e7 <Buffi+0x1>
         e82:	80 93 e6 05 	sts	0x05E6, r24	; 0x8005e6 <Buffi>
      tmp = (byte)Buffer_HTTP[Buffi];
         e86:	8a 51       	subi	r24, 0x1A	; 26
         e88:	9c 4f       	sbci	r25, 0xFC	; 252
         e8a:	fc 01       	movw	r30, r24
         e8c:	80 81       	ld	r24, Z
      if (tmp==255) {
         e8e:	8f 3f       	cpi	r24, 0xFF	; 255
         e90:	11 f0       	breq	.+4      	; 0xe96 <_Z9LireUInt9v+0x24>
    Une incrémentation sur un 'byte' coute bien moins chère (ici ReadedPinIndex est une variable globale de type 'byte') :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    void LireEntree(byte mode) {
         ed0:	ff 92       	push	r15
         ed2:	0f 93       	push	r16
         ed4:	1f 93       	push	r17
         ed6:	cf 93       	push	r28
         ed8:	df 93       	push	r29
      byte pin;
      Status = STATUS_4_ReadPin;
         eda:	94 e0       	ldi	r25, 0x04	; 4
         edc:	90 93 f8 05 	sts	0x05F8, r25	; 0x8005f8 <Status>
      if (ReadedPinIndex<MaxReadedPin) {
         ee0:	10 91 f7 05 	lds	r17, 0x05F7	; 0x8005f7 <ReadedPinIndex>
         ee4:	1a 30       	cpi	r17, 0x0A	; 10
         ee6:	e0 f4       	brcc	.+56     	; 0xf20 <_Z10LireEntreeh+0x50>
         ee8:	f8 2e       	mov	r15, r24
        //if (mode==LireEntreeLumiere) {
        //  pin = AMBIANT_LIGHT;
        //}else{
          pin = LireUInt8();
         eea:	0e 94 59 07 	call	0xeb2	; 0xeb2 <_Z9LireUInt8v>
         eee:	08 2f       	mov	r16, r24
        //}
        pinMode(pin, INPUT);
         ef0:	60 e0       	ldi	r22, 0x00	; 0
         ef2:	0e 94 f1 02 	call	0x5e2	; 0x5e2 <pinMode>
        ReadedPin[ReadedPinIndex] = pin;
         ef6:	c1 2f       	mov	r28, r17
         ef8:	d0 e0       	ldi	r29, 0x00	; 0
         efa:	fe 01       	movw	r30, r28
         efc:	e4 52       	subi	r30, 0x24	; 36
         efe:	fc 4f       	sbci	r31, 0xFC	; 252
         f00:	00 83       	st	Z, r16
         f02:	cc 0f       	add	r28, r28
         f04:	dd 1f       	adc	r29, r29
        if (mode==LireEntreeNumerique) {
         f06:	82 e0       	ldi	r24, 0x02	; 2
         f08:	f8 12       	cpse	r15, r24
         f0a:	10 c0       	rjmp	.+32     	; 0xf2c <_Z10LireEntreeh+0x5c>
          ReadedPinValue[ReadedPinIndex] = digitalRead(pin);
         f0c:	80 2f       	mov	r24, r16
         f0e:	0e 94 9a 02 	call	0x534	; 0x534 <digitalRead>
        }else{
          ReadedPinValue[ReadedPinIndex] = analogRead(pin);
         f12:	c8 53       	subi	r28, 0x38	; 56
         f14:	dc 4f       	sbci	r29, 0xFC	; 252
         f16:	99 83       	std	Y+1, r25	; 0x01
         f18:	88 83       	st	Y, r24
        }
        ReadedPinIndex++;
         f1a:	1f 5f       	subi	r17, 0xFF	; 255
         f1c:	10 93 f7 05 	sts	0x05F7, r17	; 0x8005f7 <ReadedPinIndex>
      }
    }
    Voici mes fameuses affectations 'int' = 'int' que je juge gourmandes :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    					  GLOBAL_X2 = GLOBAL_W1;
        3762:	80 91 8a 03 	lds	r24, 0x038A	; 0x80038a <GLOBAL_W1>
        3766:	90 91 8b 03 	lds	r25, 0x038B	; 0x80038b <GLOBAL_W1+0x1>
        376a:	90 93 9b 03 	sts	0x039B, r25	; 0x80039b <GLOBAL_X2+0x1>
        376e:	80 93 9a 03 	sts	0x039A, r24	; 0x80039a <GLOBAL_X2>
    					  GLOBAL_Y2 = GLOBAL_H1;
        3772:	80 91 88 03 	lds	r24, 0x0388	; 0x800388 <GLOBAL_H1>
        3776:	90 91 89 03 	lds	r25, 0x0389	; 0x800389 <GLOBAL_H1+0x1>
        377a:	90 93 99 03 	sts	0x0399, r25	; 0x800399 <GLOBAL_Y2+0x1>
        377e:	80 93 98 03 	sts	0x0398, r24	; 0x800398 <GLOBAL_Y2>
    					  GLOBAL_X3 = GLOBAL_R1;
        3782:	80 91 86 03 	lds	r24, 0x0386	; 0x800386 <GLOBAL_R1>
        3786:	90 91 87 03 	lds	r25, 0x0387	; 0x800387 <GLOBAL_R1+0x1>
        378a:	90 93 97 03 	sts	0x0397, r25	; 0x800397 <GLOBAL_X3+0x1>
        378e:	80 93 96 03 	sts	0x0396, r24	; 0x800396 <GLOBAL_X3>
    L'appel d'une fonction avec des 'int' en paramètre a pu se faire de façon plus "compacte" en utilisant des registres, qui seront ensuite utilisés dans la fonction :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
                tft.fillRect(GLOBAL_X1, GLOBAL_Y1, GLOBAL_X2, GLOBAL_Y2);
        36f0:	20 91 98 03 	lds	r18, 0x0398	; 0x800398 <GLOBAL_Y2>
        36f4:	30 91 99 03 	lds	r19, 0x0399	; 0x800399 <GLOBAL_Y2+0x1>
        36f8:	40 91 9a 03 	lds	r20, 0x039A	; 0x80039a <GLOBAL_X2>
        36fc:	50 91 9b 03 	lds	r21, 0x039B	; 0x80039b <GLOBAL_X2+0x1>
        3700:	60 91 9c 03 	lds	r22, 0x039C	; 0x80039c <GLOBAL_Y1>
        3704:	70 91 9d 03 	lds	r23, 0x039D	; 0x80039d <GLOBAL_Y1+0x1>
        3708:	80 91 9e 03 	lds	r24, 0x039E	; 0x80039e <GLOBAL_X1>
        370c:	90 91 9f 03 	lds	r25, 0x039F	; 0x80039f <GLOBAL_X1+0x1>
        3710:	14 c3       	rjmp	.+1576   	; 0x3d3a <_Z25Network_Contacter_Serveurv+0xc22>
    Dans mon travail d'optimisation j'ai regroupé et réduit le nombre de variables utilisées en me disant que cela permet au compilateur d'utiliser les registres le plus souvent possible.

    En fait dans mon code, j'ai quelques variables globales de type 'int' qui sont utilisées partout dans mon sketch, dans la librairie GFX et la librairie ILI9341 :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    int GLOBAL_X1; // X ou X1
    int GLOBAL_Y1; // Y ou Y1
    int GLOBAL_X2; // X2 ou W
    int GLOBAL_Y2; // Y2 ou H
    int GLOBAL_X3; // X3 ou L ou R
    int GLOBAL_Y3; // Y3
    unsigned int GLOBAL_Couleur;
    unsigned int GLOBAL_CouleurBG;
    Je me disait que ça serait pas mal si le compilateur pouvait utiliser en permanence des registres pour ces variables, quitte à monopoliser une banque de registres pour quelques variables globales judicieusement choisies.
    C'est comme ça que je faisais lorsque je programmais en assembleur 8051; mes variables "stratégiques" étaient des registres, pas des variables en RAM.
    Evidemment ça consomme des registres qui ne pourront pas servir pour autre chose, il faut être raisonnable dans ce type de démarche.

    Je me demande si c'est possible de "forcer" une variable en registre au niveau de sa déclaration dans le code en C++ ?

    De cette façon, certaines fonctions seraient beaucoup plus courtes.

    Par exemple, la fonction "DrawTriangle" ci-dessous se limiterais aux trois appels de fonctions sans paramètres :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    template<class HW>
    void PDQ_GFX<HW>::drawTriangle()
    {
    	HW::drawLine(GLOBAL_X1, GLOBAL_Y1, GLOBAL_X2, GLOBAL_Y2);
        3fb8:	80 90 98 03 	lds	r8, 0x0398	; 0x800398 <GLOBAL_Y2>
        3fbc:	90 90 99 03 	lds	r9, 0x0399	; 0x800399 <GLOBAL_Y2+0x1>
        3fc0:	a0 90 9a 03 	lds	r10, 0x039A	; 0x80039a <GLOBAL_X2>
        3fc4:	b0 90 9b 03 	lds	r11, 0x039B	; 0x80039b <GLOBAL_X2+0x1>
        3fc8:	c0 90 9c 03 	lds	r12, 0x039C	; 0x80039c <GLOBAL_Y1>
        3fcc:	d0 90 9d 03 	lds	r13, 0x039D	; 0x80039d <GLOBAL_Y1+0x1>
        3fd0:	60 90 9e 03 	lds	r6, 0x039E	; 0x80039e <GLOBAL_X1>
        3fd4:	70 90 9f 03 	lds	r7, 0x039F	; 0x80039f <GLOBAL_X1+0x1>
        3fd8:	94 01       	movw	r18, r8
        3fda:	a5 01       	movw	r20, r10
        3fdc:	b6 01       	movw	r22, r12
        3fde:	c3 01       	movw	r24, r6
        3fe0:	0e 94 cd 07 	call	0xf9a	; 0xf9a <_ZN11PDQ_ILI93418drawLineEiiii>
    	HW::drawLine(GLOBAL_X2, GLOBAL_Y2, GLOBAL_X3, GLOBAL_Y3);
        3fe4:	e0 90 94 03 	lds	r14, 0x0394	; 0x800394 <GLOBAL_Y3>
        3fe8:	f0 90 95 03 	lds	r15, 0x0395	; 0x800395 <GLOBAL_Y3+0x1>
        3fec:	00 91 96 03 	lds	r16, 0x0396	; 0x800396 <GLOBAL_X3>
        3ff0:	10 91 97 03 	lds	r17, 0x0397	; 0x800397 <GLOBAL_X3+0x1>
        3ff4:	97 01       	movw	r18, r14
        3ff6:	a8 01       	movw	r20, r16
        3ff8:	b4 01       	movw	r22, r8
        3ffa:	c5 01       	movw	r24, r10
        3ffc:	0e 94 cd 07 	call	0xf9a	; 0xf9a <_ZN11PDQ_ILI93418drawLineEiiii>
    	HW::drawLine(GLOBAL_X3, GLOBAL_Y3, GLOBAL_X1, GLOBAL_Y1);
        4000:	96 01       	movw	r18, r12
        4002:	a3 01       	movw	r20, r6
        4004:	b7 01       	movw	r22, r14
        4006:	c8 01       	movw	r24, r16
        4008:	70 cd       	rjmp	.-1312   	; 0x3aea <_Z25Network_Contacter_Serveurv+0x9d2>
    A bientôt
    Quand deux personnes échangent un euro, chacun repart avec un euro.
    Quand deux personnes échangent une idée, chacun repart avec deux idées.

  5. #5
    Membre éprouvé Avatar de electroremy
    Homme Profil pro
    Ingénieur sécurité
    Inscrit en
    Juin 2007
    Messages
    934
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 43
    Localisation : France, Doubs (Franche Comté)

    Informations professionnelles :
    Activité : Ingénieur sécurité
    Secteur : Industrie

    Informations forums :
    Inscription : Juin 2007
    Messages : 934
    Points : 1 274
    Points
    1 274
    Par défaut
    Et voici une bizarrerie :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
        // Affichage de debug : -----------------------------------------------------------------------------
        GLOBAL_TextSize = 1;
        4390:	81 e0       	ldi	r24, 0x01	; 1
        4392:	80 93 ec 05 	sts	0x05EC, r24	; 0x8005ec <GLOBAL_TextSize>
        GLOBAL_Couleur = 0xFFFF;
        4396:	8f ef       	ldi	r24, 0xFF	; 255
        4398:	9f ef       	ldi	r25, 0xFF	; 255
        439a:	90 93 eb 05 	sts	0x05EB, r25	; 0x8005eb <GLOBAL_Couleur+0x1>
        439e:	80 93 ea 05 	sts	0x05EA, r24	; 0x8005ea <GLOBAL_Couleur>
    	GLOBAL_CouleurBG = 0;
        43a2:	10 92 e9 05 	sts	0x05E9, r1	; 0x8005e9 <GLOBAL_CouleurBG+0x1>
        43a6:	10 92 e8 05 	sts	0x05E8, r1	; 0x8005e8 <GLOBAL_CouleurBG>
    Pourquoi GLOBAL_Couleur = 0xFFFF utilise deux registres alors que GLOBAL_CouleurBG = 0x0000 un seul ?
    Quand deux personnes échangent un euro, chacun repart avec un euro.
    Quand deux personnes échangent une idée, chacun repart avec deux idées.

  6. #6
    Expert confirmé

    Homme Profil pro
    mad scientist :)
    Inscrit en
    Septembre 2019
    Messages
    2 711
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Paris (Île de France)

    Informations professionnelles :
    Activité : mad scientist :)

    Informations forums :
    Inscription : Septembre 2019
    Messages : 2 711
    Points : 5 390
    Points
    5 390
    Par défaut
    Oui c’est parce que r1 est réputé contenir 0 et donc le compilateur ne charge pas deux registres pour faire le ldi (qui ne fonctionne qu’avec les registres 16 à 31)
    Si vous mettez autre chose que 0 vous verrez sans doute aussi l’usage de 2 registres (je ne peux pas tester pour le moment)

    Ps: r1 est mis à 0x00 par le code de démarrage généré implicitement par gcc et est ensuite supposé par le compilateur contenir 0x00 pour toujours. Par conséquent, partout où 0x00 est nécessaire mais qu'un littéral ne peut pas être utilisé, gcc utilise simplement r1.

    J’ai retrouvé le lien
    Fixed Registers

    Fixed Registers are registers that won't be allocated by GCC's register allocator. Registers R0 and R1 are fixed and used implicitly while printing out assembler instructions:

    R0
    is used as scratch register that need not to be restored after its usage. It must be saved and restored in interrupt service routine's (ISR) prologue and epilogue. In inline assembler you can use __tmp_reg__ for the scratch register.
    R1
    always contains zero. During an insn the content might be destroyed, e.g. by a MUL instruction that uses R0/R1 as implicit output register. If an insn destroys R1, the insn must restore R1 to zero afterwards. This register must be saved in ISR prologues and must then be set to zero because R1 might contain values other than zero. The ISR epilogue restores the value. In inline assembler you can use __zero_reg__ for the zero register.
    T
    the T flag in the status register (SREG) is used in the same way like the temporary scratch register R0.

  7. #7
    Membre éprouvé Avatar de electroremy
    Homme Profil pro
    Ingénieur sécurité
    Inscrit en
    Juin 2007
    Messages
    934
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 43
    Localisation : France, Doubs (Franche Comté)

    Informations professionnelles :
    Activité : Ingénieur sécurité
    Secteur : Industrie

    Informations forums :
    Inscription : Juin 2007
    Messages : 934
    Points : 1 274
    Points
    1 274
    Par défaut
    Tout s'explique !

    Le zéro est donc "un nombre magique" efficace !

    Typiquement, si j'ai une fonction qui retourne un résultat ou bien une valeur précise en cas d'erreur, on a plutôt intérêt à ce que cette valeur précise soit zéro (sauf si zéro fait partie de la plage utile).
    Ca vaudrait le coup qu'il existe un registre contenant en permanence "-1", valeur très utilisée par les programmateurs C/C++

    Après, dans l'affectation entier=0xWXYZ, le compilateur pourrait tester si WX = YZ et n'utiliser qu'un seul registre au lieu de deux.

    Cela ferait gagner deux octets à chaque affectation entier=0xFFFF (assez courantes) ou 0xXYXY

    Est-il possible (par exemple avec des pointeurs) de travailler directement sur le MSB et le LSB d'un entier sur 16 bits, sans faire couiner le compilateur c++, ni se voir rajouter des tartines de code de vérification à la compilation ?

    Dans mon code j'ai pas mal d'entiers qui, la plupart du temps, ne reçoivent que des valeurs comprises entre 0 et 255.

    L'afficheur fait 240 * 320 pixels. En fonction de l'orientation choisie, on a donc soit des coordonnées X soit des coordonnées Y toujours comprises entre 0 et 239.
    Stocker ces valeurs dans des entiers gaspille de la RAM et du code.
    Et l'autre coordonnée est très souvent inférieure à 255.

    Partant de ce constat, j'ai déjà optimisé le stockage de mes données dans le buffer ainsi que les échanges réseaux, en créant un type "uInt9".
    Si la valeur est inférieure à 255, je stocke la valeur sur un octet.
    Si la valeur est supérieure ou égale à 255, je stocke la valeur sur deux octets : le premier vaut 255, le second (255-valeur).
    Cela permet de stocker des nombres compris entre 0 et 510 de façon efficace.
    Un simple "if" avec une addition ou une soustraction permet de convertir les valeurs dans un sens ou dans l'autre.

    Et sinon... est-il possible de "forcer" des variables globales à être en permanence dans un registre et jamais en RAM ?

    A bientôt
    Quand deux personnes échangent un euro, chacun repart avec un euro.
    Quand deux personnes échangent une idée, chacun repart avec deux idées.

  8. #8
    Expert confirmé

    Homme Profil pro
    mad scientist :)
    Inscrit en
    Septembre 2019
    Messages
    2 711
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Paris (Île de France)

    Informations professionnelles :
    Activité : mad scientist :)

    Informations forums :
    Inscription : Septembre 2019
    Messages : 2 711
    Points : 5 390
    Points
    5 390
    Par défaut
    Bloquer des registres est pénalisant pour le reste des optimisations de code. -1 est utilisé effectivement mais pas autant que 0 et donc c’est un choix qui a été fait. Oui le compilo pourrait tester si dans une affectation sur 2 octets les 2 valeurs sont identiques pour gagner un cycle d’horloge. Le compilo pour avr est en retard de plusieurs guerres (on en est à gcc v10 et avr-gcc est fondé sur la v5 de mémoire) et ne sera d’ailleurs plus maintenu (support de avr déprécié)

    Fut un temps (déprécié avec C++ 11 suite à des décisions de 2009 cf « 809. Deprecation of the register keyword*») on avait le mot clé register pour donner une indication au compilo que cette variable allait être très utilisée et qu’il serait pas mal d’avoir un accès rapide (et donc de suggérer de mettre cette variable dans un registre) mais ce n’était qu’une indication et au final le compilateur Faisait ce qu’il voulait et il est souvent meilleur que vous pour optimiser les choses (à effort raisonnable).

    Oui c’est facile en C ou C++ de travailler sur le LSB ou MSB, un pointeur casté en uint8_t* permet de faire ce qu’on veut
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    uint16_t valeur = 0x1234;
    uint8_t* lsb= (uint8_t*) &valeur; // en architecture little endian 
    uint8_t* msb= lsb+1;
    Si la valeur est supérieure ou égale à 255, je stocke la valeur sur deux octets : le premier vaut 255, le second (255-valeur).
    Cela permet de stocker des nombres compris entre 0 et 510 de façon efficace.
    Euh je ne comprends pas pourquoi c’est efficace, si vous avez deux octets, stockez en binaire la vraie valeur et vous allez au delà de 510 sans avoir à jongler avec des additions ou soustractions. En plus quand on fait des calculs sur des positions de pixels, les maths peuvent dépasser le stockage sur un octet donc faut faire attention. (Ou alors vous voulez dire que c’est condensé sur 9 bits et pas deux octets complets ?)

  9. #9
    Membre éprouvé Avatar de electroremy
    Homme Profil pro
    Ingénieur sécurité
    Inscrit en
    Juin 2007
    Messages
    934
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 43
    Localisation : France, Doubs (Franche Comté)

    Informations professionnelles :
    Activité : Ingénieur sécurité
    Secteur : Industrie

    Informations forums :
    Inscription : Juin 2007
    Messages : 934
    Points : 1 274
    Points
    1 274
    Par défaut
    Je vois...

    Le compilateur privilégie l'utilisation de registres pour les variables locales des fonctions, surtout quand les fonctions sont petites.
    Lorsqu'une variable a une portée trop grande, le compilateur considère qu'elle doit être placée en RAM, même si elle est utilisée très souvent.

    Voici un exemple.
    La fonction DessinerBouton sert à... dessiner un bouton, enfin juste le cadre du bouton en relief.
    Si son paramètre 'Forcer' est vrai, la fonction utilise les coordonnées définies par GLOBAL_X1, GLOBAL_Y1, GLOBAL_X2, GLOBAL_Y2
    Sinon, la fonction regarde le contenu de la variable GLOBAL_Button_Offset :
    - si c'est zéro, on ne dessine rien
    - si c'est une valeur autre que zéro, on dessine le bouton en utilisant la valeur comme décalage

    Dans mon application, je peut donner une valeur à GLOBAL_Button_Offset pour dessiner des boutons automatiquement à chaque fois que je dessine un texte ou un bitmap, très utile quand par exemple il faut dessiner un clavier (il suffit de faire un printText avec le texte correspondant à chaque touche).


    Code utilisant uniquement les variables globales :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    void DessinerBouton(bool Forcer = false) { 
    	//Auparavent, si Forcer=true, les valeurs à utiliser sont directement celles de GLOBAL_X1, GLOBAL_Y1, GLOBAL_X2, GLOBAL_Y2
    	if (!Forcer) {
    		if (GLOBAL_Button_Offset==0) {
    			return;
    		} else {
    			//Auparavent, les fonctions DrawText ou DrawBitmap ont définit GLOBAL_X1, GLOBAL_Y1 comme coin supérieur gauche et GLOBAL_X2, GLOBAL_Y2 comme coin inférieur droit
    			GLOBAL_X2 += 2 * GLOBAL_Button_Offset - GLOBAL_X1;
    			GLOBAL_Y2 += 2 * GLOBAL_Button_Offset - GLOBAL_Y1;
    			GLOBAL_X1 -= - GLOBAL_Button_Offset;
    			GLOBAL_Y1 -= - GLOBAL_Button_Offset;
    		}
    	}
    	GLOBAL_Couleur = Couleur[4]; // 4 : Bord supérieur gauche
    	tft.drawFastHLine(GLOBAL_X1, GLOBAL_Y1, GLOBAL_X2 - 1); 
    	tft.drawFastVLine(GLOBAL_X1, GLOBAL_Y1, GLOBAL_Y2 - 1);
    	GLOBAL_Couleur = Couleur[6]; // 6 : Bord inférieur droit extérieur
    	tft.drawFastHLine(GLOBAL_X1 + 1, GLOBAL_Y1 + GLOBAL_Y2 - 2, GLOBAL_X2 - 2); 
    	tft.drawFastVLine(GLOBAL_X1 + GLOBAL_X2 - 2, GLOBAL_Y1 + 1, GLOBAL_Y2 - 2);
    	GLOBAL_Couleur = Couleur[5]; // 5 : Bord inférieur droit extérieur
    	tft.drawFastHLine(GLOBAL_X1, GLOBAL_Y1 + GLOBAL_Y2 - 1, GLOBAL_X2); 
    	tft.drawFastVLine(GLOBAL_X1 + GLOBAL_X2 - 1, GLOBAL_Y1, GLOBAL_Y2);
    }
    Code utilisant des variables locales, à priori moins optimal :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    void DessinerBouton(bool Forcer = false) { // 22880 octets
    	int X,Y,W,H; // Le compilateur utilise des registres pour ces variables ce qui explique l'économie de code
    	W = GLOBAL_X2;
    	H = GLOBAL_Y2;
    	X = GLOBAL_X1;
    	Y = GLOBAL_Y1;
    	if (GLOBAL_Button_Offset==0) {
    		if (!Forcer) return;
    	} else {
    		//Auparavent, les fonctions DrawText ou DrawBitmap ont définit GLOBAL_X1, GLOBAL_Y1 comme coin supérieur gauche et GLOBAL_X2, GLOBAL_Y2 comme coin inférieur droit
    		W += 2 * GLOBAL_Button_Offset - GLOBAL_X1;
    		H += 2 * GLOBAL_Button_Offset - GLOBAL_Y1;
    		X -= GLOBAL_Button_Offset;
    		Y -= GLOBAL_Button_Offset;
    	}
    	GLOBAL_Couleur = Couleur[4]; // 4 : Bord supérieur gauche
    	tft.drawFastHLine(X, Y, W - 1); 
    	tft.drawFastVLine(X, Y, H - 1);
    	GLOBAL_Couleur = Couleur[6]; // 6 : Bord inférieur droit extérieur
    	tft.drawFastHLine(X + 1, Y + H - 2, W - 2); 
    	tft.drawFastVLine(X + W - 2, Y + 1, H - 2);
    	GLOBAL_Couleur = Couleur[5]; // 5 : Bord inférieur droit extérieur
    	tft.drawFastHLine(X, Y + H - 1, W); 
    	tft.drawFastVLine(X + W - 1, Y, H);
    }
    Hé bien le code avec les variables locales occupe 48 octets de moins à la compilation, car les variables locales X,Y,W et H ne sont jamais stockées en RAM mais directement dans les registres

    Alors oui, le mot clef REGISTER m'intéresse... il permettrait à mon code d'être efficace et rapide.

    Surtout que l'AVR a beaucoup de registres et que je pourrais me contenter juste d'une poignées de variables forcées en registre.

    Je vais finir par programmer l'Arduino en assembleur, mais il va falloir que je convertisse non seulement le code de mon sketch mais aussi les bibliothèques et les fonctions du core...

    Après cela fait un bon passe-temps

    Toutes ces expérimentations me donneraient presque envie d'imaginer mon propre compilateur, mais la tâche est titanesque vu la complexité des AVR.
    Ce genre de projet serait plus facile avec un bon vieux 8051 genre AT89C2051, on peut se bricoler un pseudo éditeur / compilateur BASIC dont les instructions correspondent à des macros.
    Quand deux personnes échangent un euro, chacun repart avec un euro.
    Quand deux personnes échangent une idée, chacun repart avec deux idées.

  10. #10
    Expert confirmé

    Homme Profil pro
    mad scientist :)
    Inscrit en
    Septembre 2019
    Messages
    2 711
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Paris (Île de France)

    Informations professionnelles :
    Activité : mad scientist :)

    Informations forums :
    Inscription : Septembre 2019
    Messages : 2 711
    Points : 5 390
    Points
    5 390
    Par défaut
    Vous pouvez mélanger l’assembleur et le code c++

    Sinon vous ne devez pas raisonner avec un cas particulier - un compilateur doit servir le cas général.

    Il n’y a pas tant de registres que cela, 32 ça semble beaucoup mais ca va vite dès que vous avez des données qui ne tiennent pas sur un octet et que toutes les instructions en assembleur travaillent uniquement sur un octet.

    Alors oui, le mot clef REGISTER m'intéresse... il permettrait à mon code d'être efficace et rapide.
    sauf qu’il est déprécié... le compilateur va se débrouiller. Vous pouvez aider en réfléchissant bien à la structure du code.


    Une des problèmes des variables globales c’est que le code n’est plus ré-entrant, la fonction ne peut pas s’appeler elle même en recursion ou lors d’une interruption on risque de mettre le bazar. Parfois c’est utile. Quand on a de nombreuses variables à une fonction qui ne les modifie pas, il faut penser au passage par référence.

  11. #11
    Membre éprouvé Avatar de electroremy
    Homme Profil pro
    Ingénieur sécurité
    Inscrit en
    Juin 2007
    Messages
    934
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 43
    Localisation : France, Doubs (Franche Comté)

    Informations professionnelles :
    Activité : Ingénieur sécurité
    Secteur : Industrie

    Informations forums :
    Inscription : Juin 2007
    Messages : 934
    Points : 1 274
    Points
    1 274
    Par défaut
    Citation Envoyé par Jay M Voir le message
    Sinon vous ne devez pas raisonner avec un cas particulier - un compilateur doit servir le cas général.
    Oui. J'ai fait pas mal de tests...

    L'utilisation de mes variables globales m'a fait gagné pas mal de code sur l'ensemble du projet. Ca reste, pour mon projet, un choix pertinent.

    Au final, je n'ai eu que deux fonctions dans lesquelles j'ai eu un gain en remettant des variables locales : celle donnée en exemple et fillTriangle.

    Le gain est de 100 octets (pas négligeable). Il y a aussi un léger gain en vitesse.

    L'explication est simple : ces deux fonctions faisaient beaucoup de calculs et d'échanges sur les variables globales, donc repasser par des variables locales que le compilateur remplace par des registre aboutit à un code plus court et plus rapide.

    Citation Envoyé par Jay M Voir le message
    Une des problèmes des variables globales c’est que le code n’est plus ré-entrant, la fonction ne peut pas s’appeler elle même en recursion ou lors d’une interruption on risque de mettre le bazar. Parfois c’est utile. Quand on a de nombreuses variables à une fonction qui ne les modifie pas, il faut penser au passage par référence.
    Les fonctions récursives sur Arduino je préfère éviter : quantité de RAM limitée, pas de garbage collector, trop utiliser la pile n'est pas une bonne idée.

    Le passage par référence d'un entier sur 16 bits est-il mieux ? Sur les AVR, la RAM fait 2 ko pour le UNO et 8 ko sur le MEGA, un pointeur réclame 11 à 13 bits.
    Autant dire que le pointeur occupe autant d'espace mémoire que la variable sur laquelle il pointe.
    Le pointeur d'un byte prend plus de place que la variable elle-même

    A bientôt
    Quand deux personnes échangent un euro, chacun repart avec un euro.
    Quand deux personnes échangent une idée, chacun repart avec deux idées.

  12. #12
    Expert confirmé

    Homme Profil pro
    mad scientist :)
    Inscrit en
    Septembre 2019
    Messages
    2 711
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Paris (Île de France)

    Informations professionnelles :
    Activité : mad scientist :)

    Informations forums :
    Inscription : Septembre 2019
    Messages : 2 711
    Points : 5 390
    Points
    5 390
    Par défaut
    Citation Envoyé par electroremy Voir le message
    Oui. J'ai fait pas mal de tests...
    Les fonctions récursives sur Arduino je préfère éviter : quantité de RAM limitée, pas de garbage collector, trop utiliser la pile n'est pas une bonne idée.
    ça dépend de la situation et du contrôle que l'on a sur la récession bien sûr et le compilateur n'empile que les registres nécessaires donc ça se gère. Bien sûr il faut faire attention et il faut que la récursivité apporte un plus par rapport à la version itérative.

    Citation Envoyé par electroremy Voir le message
    Le passage par référence d'un entier sur 16 bits est-il mieux ? Sur les AVR, la RAM fait 2 ko pour le UNO et 8 ko sur le MEGA, un pointeur réclame 11 à 13 bits.
    Autant dire que le pointeur occupe autant d'espace mémoire que la variable sur laquelle il pointe.
    Le pointeur d'un byte prend plus de place que la variable elle-même
    Tout à fait, de très bons points.
    Là encore c'est à utiliser à bon escient; une référence sur une instance, une structure par exemple. il faut que le jeu en vaille la chandelle.

    Notez qu'il existe des registres spéciaux en R26...R31 que l'on peut combiner par 2 (X,Y et Z) pour faire un pointeur 16 bits sur les données. Le compilateur peut en tirer partie avec différents modes d'adressage avec déplacement fixe, incrémentation et décrémentation automatique pour le parcours de tableau.

  13. #13
    Membre éprouvé Avatar de electroremy
    Homme Profil pro
    Ingénieur sécurité
    Inscrit en
    Juin 2007
    Messages
    934
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 43
    Localisation : France, Doubs (Franche Comté)

    Informations professionnelles :
    Activité : Ingénieur sécurité
    Secteur : Industrie

    Informations forums :
    Inscription : Juin 2007
    Messages : 934
    Points : 1 274
    Points
    1 274
    Par défaut
    Citation Envoyé par Jay M Voir le message
    Notez qu'il existe des registres spéciaux en R26...R31 que l'on peut combiner par 2 (X,Y et Z) pour faire un pointeur 16 bits sur les données. Le compilateur peut en tirer partie avec différents modes d'adressage avec déplacement fixe, incrémentation et décrémentation automatique pour le parcours de tableau.
    Le 8051 aussi a un pointeur 16 bits, il est indispensable pour chercher des données en ROM (équivalent de PROGMEM)

    Je suppose que le compilateur utilise de lui même ces registres quand on manipule des tableaux.

    J'utilise deux gros buffers (tableaux de char) :
    - un pour stocker et la requête envoyée par le client et les données reçues par le serveur ; le contenu de ce buffer change à chaque échange réseau
    - un autre pour stocker des données sur le long terme (typiquement des données d'initialisation envoyées par le serveur au client lorsque le client vient de s'allumer)

    Dans mon sketch, à par ces deux buffers, j'ai très peu de variables.
    Pour afficher à l'écran les "instructions" envoyées par le serveur, mon code :
    - parcours le buffer
    - pour chaque instruction stockée dans le buffer, extrait les paramètres (couleur, coordonnée X/Y...) qui suivent l'instruction dans le buffer
    - les chaines de caractères ne sont pas extraites mais laissées dans le buffer et imprimées avec une instruction du style tft.print(buffer + index)
    - on passe à l'instruction suivante, le principe c'est de "dé-bufferiser" les données instruction par instruction juste nécessaires au fur et à mesure

    Une astuce, au moment ou je reçois les données du serveur, est d'identifier les données destinées au buffer "long terme" et de les mettre directement dedans. De cette façon, on évite un stockage "en double". Il serait très stupide en effet de stocker les données dans un buffer pour ensuite les copier dans un autre.

    Le buffer "long terme" sert notamment à stocker :
    - des bitmaps
    - des chaines de caractères souvent utilisées

    C'est dans ces buffers que j'utilise mon fameux type de données "Int9", c'est à dire un entier pouvant avoir des valeurs allant de 0 à 510 et qui n'occupe qu'un seul octet si sa valeur est inférieure à 255. Cela fait une "compression" de données très efficace pour les coordonnées X/Y et les longueurs, surtout pour un afficheur 240 x 320 pour lequel très peu de coordonnées nécessitent deux octets pour le stockage.
    J'ai des instructions qui permettent de réutiliser la longueur et la hauteur de l'instruction précédente car souvent, dans une interface graphique, on dessine une série d'objets ayant des dimensions identiques.
    J'ai des instructions qui permettent de définir des statuts "modificateurs", par exemple une instruction qui met un sémaphore "bouton" à '1' et ensuite tous les textes et bitmaps qu'on affiche ont automatiquement un cadre en relief dessiné autour.

    Cette "bufferisation" permet de faire plein de choses avec assez peu de RAM.

    Après... le débogage est sportif et les plantages spectaculaires le moindre "décalage", octet oublié ou en trop envoyé par le serveur génère un sacré carnage car les données suivantes sont interprétées à tord comme des instructions

    La communication se fait en TCP/IP, et en plus j'ai rajouté une couche de sûreté en contrôlant la longueur des données et en calculant un CRC.
    En cas d'erreur, le client ou le serveur fait une requête ou une réponse spéciale qui demande le renvoi des données précédentes, en précisant le code d'erreur.

    Si, au moment de calculer le CRC (moment qui ne peut se faire qu'à la fin de la réception des données) je détecte une erreur et que le buffer "long terme" a été modifié, alors un code d'erreur spécifique est envoyé pour dire au serveur que le buffer "long terme" est corrompu et qu'il faut renvoyer toutes les données qu'il est censé contenir.

    Le plus important : serveur et client émettent une erreur s'il y a trop de données pour éviter un buffer overflow.

    Avec toutes ces précautions, si les bugs génèrent des affichages "spectaculaires", aucun ne plante ni l'Arduino ni le programme.
    C'est donc capricieux mais fiable.
    Il suffit qu'au prochain échange des données correctes soient reçues et l'affichage redevient normal.

    Une programmation orientée objet, où on instancierait une classe à chaque instruction reçue, contenant les paramètres, avec du polymorphisme d'héritage en bonne et due forme, ça serait plus propre mais ça ne tiendrais jamais dans un Arduino UNO (même un MEGA aurait du mal)

    A bientôt
    Quand deux personnes échangent un euro, chacun repart avec un euro.
    Quand deux personnes échangent une idée, chacun repart avec deux idées.

  14. #14
    Expert confirmé

    Homme Profil pro
    mad scientist :)
    Inscrit en
    Septembre 2019
    Messages
    2 711
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Paris (Île de France)

    Informations professionnelles :
    Activité : mad scientist :)

    Informations forums :
    Inscription : Septembre 2019
    Messages : 2 711
    Points : 5 390
    Points
    5 390
    Par défaut
    Citation Envoyé par electroremy Voir le message
    Une programmation orientée objet, où on instancierait une classe à chaque instruction reçue, contenant les
    paramètres, avec du polymorphisme d'héritage en bonne et due forme, ça serait plus propre mais ça ne tiendrais jamais dans un Arduino UNO (même un MEGA aurait du mal)
    Pas aussi sûr que vous qu’utiliser une classe bien fichue (pas un truc super profond en héritage etc) soit bcp plus gourmand... ça reste une structure de données en mémoire et vous bénéficiez de l’abstraction pour simplifier le code.

    Concernant votre Int9 Je suis curieux de savoir comment vous indiquez qu’une coordonnée est sur 2 octets versus un ?

  15. #15
    Membre éprouvé Avatar de electroremy
    Homme Profil pro
    Ingénieur sécurité
    Inscrit en
    Juin 2007
    Messages
    934
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 43
    Localisation : France, Doubs (Franche Comté)

    Informations professionnelles :
    Activité : Ingénieur sécurité
    Secteur : Industrie

    Informations forums :
    Inscription : Juin 2007
    Messages : 934
    Points : 1 274
    Points
    1 274
    Par défaut
    Citation Envoyé par Jay M Voir le message
    Concernant votre Int9 Je suis curieux de savoir comment vous indiquez qu’une coordonnée est sur 2 octets versus un ?
    C'est pas très compliqué :
    - si la valeur est comprise entre 0 et 254, on stocke sur un octet
    - si la valeur est supérieure à 254, on définit le premier octet à 255 et ensuite le deuxième comme étant égal à valeur - 255

    Exemple :
    126 est stocké dans un octet de valeur 126
    260 est stocké dans deux octets : le premier qui vaut 255, le second qui vaut 5

    Le serveur qui envoie les données teste si la valeur est supérieure ou égale à 255.
    Si non, il stocke la valeur sur un octet (comme le serait un type unsigned byte)
    Si oui, il stocke un premier octet de valeur 255, puis un second octet qui contient la valeur initiale moins 255.

    Le client qui reçoit les données les analyse comme suit :
    Lecture d'un octet et stockage dans un entier
    Si valeur < 255 c'est terminé
    Sinon, alors on ajoute à l'entier qu'on vient de lire la valeur lue du second octet

    Ca peut sembler alambiqué mais c'est rapide et efficace. Les fonctions Lecture() et Ecriture() sont courtes et rapides.
    Le gain est loin d'être négligeable.
    On gagne presque 40% sur la taille des échanges, ça compte étant donné que mon Buffer ne fait que 512 octets.

    En utilisant astucieusement ce principe et mes fameuses instructions, dessiner un clavier de 10 touches ne prend que 60 octets dans le Buffer.
    En fait, j'ai fait ce qu'on appelle une compression de code.

    Cela se trouve, pour faire mon menu "setup" afin de calibrer l'écran et de choisir d'autres paramètres sans devoir reflasher l'Arduino, je vais me servir de mon code et définir des données de "calibration" en PROGMEM qu'il me suffira de copier dans le buffer. Ca sera certainement moins gourmand en ROM que de faire une fonction de calibration qui devra appeler les fonctions graphiques.

    Une "grosse" réponse du serveur qui rempli le buffer de 512 octets permet de remplir l'écran TFT en entier avec plein de formes et de textes de toutes les couleurs :
    - la détection de la frappe sur l'écran tactile, l’envoi de la requête au serveur, la réception de la réponse et le contrôle d'erreur prend au total 20 millisecondes
    - le plus lent c'est l'affichage qui prend jusque 400 millisecondes en fonction de la complexité et de la taille de ce qui est dessiné.

    Etant donné le principe de fonctionnement du contrôleur TFT ILI9341, c'est l'effacement de l'écran la fonction la plus lente, 170 millisecondes, car l'écran TFT demande qu'on envoi via le bus SPI la couleur de chaque pixel que l'on veut écrire dans une fenêtre rectangulaire.
    La librairie Adafruit GFX utilisait bêtement une fonction DrawPixel qui, pour chaque pixel, définissant une fenêtre rectangulaire et envoyait la couleur. De plus, Adafruit n'utilisait pas le hardware SPI mais un (mauvais) bitbanging. Résultat : l'effacement de l'écran avec cette bibliothèque prenait presque 3 secondes. Cette bibliothèque occupait beaucoup de place en ROM

    C'est vraiment vraiment dommage que le ILI9341 n'ai pas juste une petite instruction "remplir toute la fenêtre rectangulaire avec cette couleur"
    Je rêve de flasher le ILI9314 pour ajouter cette fonction qui rendrait l'utilisation de cet afficheur beaucoup plus rapide.

    400 ms maximum cela reste assez rapide pour une interface homme machine.
    Surtout qu'on n'est pas obligé de redessiner entièrement l'écran à chaque action de l'utilisateur, on peut se contenter de mettre à jour une partie seulement. C'est alors quasi instantané.

    Finalement, ce projet m'aura emmené assez loin avec l'Arduino UNO.

    Avec une carte plus puissante, je n'aurais pas eu besoin de toutes ces optimisations, ça aurai été plus facile mais moins intéressant.
    Quand deux personnes échangent un euro, chacun repart avec un euro.
    Quand deux personnes échangent une idée, chacun repart avec deux idées.

  16. #16
    Expert confirmé

    Homme Profil pro
    mad scientist :)
    Inscrit en
    Septembre 2019
    Messages
    2 711
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Paris (Île de France)

    Informations professionnelles :
    Activité : mad scientist :)

    Informations forums :
    Inscription : Septembre 2019
    Messages : 2 711
    Points : 5 390
    Points
    5 390
    Par défaut
    d'accord - oui c'est efficace.

    C'est un principe similaire qui est utilisé pour coder les caractères en UTF8. on code sur un octet les 127 caractères standard ASCII (comme cela il y a compatibilité) et si on dépasse alors "certains bits connus" définissent si le caractère qui arrive tient sur 2 ou plusieurs octets

    il y a d'autres techniques, par exemple une qui consiste à insérer un marqueur (255 comme vous par exemple) pour marquer le début d'une suite de "grands chiffres" et un marqueur de fin de série. ça sert si statistiquement vous avez de nombreuses valeurs consécutives qui sont grandes alors ça va compresser plus.

    exemple: pour envoyer 32 355 355 355 355 355 355 20 vous envoyez 32 255 100 255 100 255 100 255 100 255 100 255 100 20, et avec l'autre approche on envoie 32 255 100 100 100 100 100 100 255 20 donc on est gagnant - mais si vous avez une alternance de petits et grands alors le marqueur de fin vous coûte plus cher. une technique consiste alors à avoir un premier octet dans la description du bitmap qui décrit sa stratégie de codage et c'est lorsqu'on génère le bitmap que l'on retient celui qui est le plus condensé. (une autre option consiste à déclarer le nombre de grandes valeurs après le marqueur et on n'a pas le marqueur de fin, ça limite à 255 valeurs pour une trame de grands nombres alors). On se sert d'approches de ce genre en streaming de flux de données. (ensuite il y a des algos qui travaillent au niveau du bit au lieu de l'octet)

    Vous avez fait un beau boulot - bravo !!

  17. #17
    Membre éprouvé Avatar de electroremy
    Homme Profil pro
    Ingénieur sécurité
    Inscrit en
    Juin 2007
    Messages
    934
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 43
    Localisation : France, Doubs (Franche Comté)

    Informations professionnelles :
    Activité : Ingénieur sécurité
    Secteur : Industrie

    Informations forums :
    Inscription : Juin 2007
    Messages : 934
    Points : 1 274
    Points
    1 274
    Par défaut
    J'ai aussi optimisé le stockage des bitmaps des polices de caractères, en mettant tous les bits les uns à la suite des autres (pas de bits perdus si une ligne n'a pas une taille multiple de 8).
    Pour les polices de caractère, je n'ai stocké que les caractères utiles (codes ascii entre 32 et 142).

    Citation Envoyé par Jay M Voir le message
    il y a d'autres techniques, par exemple une qui consiste à insérer un marqueur (255 comme vous par exemple) pour marquer le début d'une suite de "grands chiffres" et un marqueur de fin de série. ça sert si statistiquement vous avez de nombreuses valeurs consécutives qui sont grandes alors ça va compresser plus.
    C'est pas bête du tout, mais en pratique ça arrive jamais ou rarement dans mon application.
    J'avais pensé à aller plus loin dans la compression, mais vu le nombre et le type de données échangées ça ne valait pas le coup.
    Pour les polices de caractères j'ai analysé si des valeurs étaient fréquentes mais je n'ai rien eu de probant. Le code de décompression aurait probablement pris plus de place en ROM que l'économie de stockage, sans parler du temps d’exécution.

    Il y a fort longtemps, je m'étais amusé sur Amiga 1200 à faire moi-même, en GFA Basic :
    - un format bitmap compressé
    - une police de caractère bitmap multicolore
    Les gens de Hackaday ont fait une compression bitmap de leur logo pour l'afficher en grand avec une quantité de mémoire limitée.

    J'ai aussi une fonction "dégradé de couleur".
    Compte tenu du principe de fonctionnement de l'afficheur, le dégradé est plus facile et plus rapide à aire dans le sens vertical que horizontal.
    Je pourrais assez facilement et avec une durée d’exécution raisonnable dessiner du texte en couleur "dégradé", j'ajouterais cette fonction en dernier s'il reste de la ROM.

    J'espère réussir, avec les 9Ko de ROM qu'il me reste :
    - faire le menu "setup" permettant la calibration de l'écran tactile et d'autres réglages
    - intégrer une deuxième police de caractère bitmap plus grande, car la petite police 5 x 7 quand elle est affichée en taille 2 ou 3 est assez laide et pas très lisible.

    Je compte publier le projet quand il sera terminé.

    Il sera polyvalent, car ce n'est rien d'autre qu'un afficheur TFT tactile pilotable par Ethernet.
    On peut également lire ou écrire les broches d'I/O disponibles via Ethernet.
    Une photodiode permet d'ajuster le rétroéclairage à la luminosité ambiante, et dans chaque requête au serveur la valeur de la luminosité mesurée est incluse, ça fait un capteur intégré.

    Il fonctionne en client, ce qui permet d'en avoir plusieurs reliés à un serveur.
    Une broche d'I/O est utilisée en "IRQ" pour permettre au serveur de forcer le client à faire une requête.
    L'astuce c'est que la pseudo interruption est déclenchée sur changement d'état (passage de 0 à 1 ou de 1 à 0). De cette façon, il n'y a aucun problème de timing, d'aquitement ni de réinitialisation.
    La communication Ethernet n'utilise que deux paires. Il reste 4 fils plus la masse, de quoi alimenter la carte, raccorder la fameuse broche IRQ et aussi gérer le reset de l'ensemble du système en cas de plantage. Le gros avantage de l'Arduino UNO c'est son connecteur d'alimentation 9V avec régulateur 5V / 3,3V integré, cela permet d'alimenter l'Arduino à distance. Les cartes qui exigent une alimentation 5V stable imposent d'avoir une alimentation à proximité.

    Je ne sais pas encore si je ferais un watchdog que pour le serveur ou pour toutes les cartes.
    Je sais déjà que je ferais une extinction automatique des cartes clients pour économiser l'énergie.

    On pourrait imaginer une version "non tactile".
    Sauf que dans ce cas de figure, il serait plus pertinent que la carte fonctionne en serveur et pas en client, car on pourrait se passer de la fameuse broche IRQ. Mais c'est ennuyeux, car ce sera difficile d'avoir sur un même système des cartes tactiles et non tactiles.
    Quand deux personnes échangent un euro, chacun repart avec un euro.
    Quand deux personnes échangent une idée, chacun repart avec deux idées.

  18. #18
    Expert confirmé

    Homme Profil pro
    mad scientist :)
    Inscrit en
    Septembre 2019
    Messages
    2 711
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Paris (Île de France)

    Informations professionnelles :
    Activité : mad scientist :)

    Informations forums :
    Inscription : Septembre 2019
    Messages : 2 711
    Points : 5 390
    Points
    5 390
    Par défaut
    super exploration - je suis sûr que si vous postez le résultat ça pourra servir à d'autres !

  19. #19
    Membre éprouvé
    Femme Profil pro
    ..
    Inscrit en
    Décembre 2019
    Messages
    562
    Détails du profil
    Informations personnelles :
    Sexe : Femme
    Âge : 94
    Localisation : Autre

    Informations professionnelles :
    Activité : ..

    Informations forums :
    Inscription : Décembre 2019
    Messages : 562
    Points : 1 253
    Points
    1 253
    Par défaut
    Citation Envoyé par Jay M Voir le message
    Alors oui, le mot clef REGISTER m'intéresse... il permettrait à mon code d'être efficace et rapide.
    sauf qu’il est déprécié... le compilateur va se débrouiller.
    Salut,

    Si je me souviens bien, déprécié uniquement lorsqu'il est relatif aux fonctions et blocs de code.
    La "version globale" (named register storage class specifier, je crois) qui est une extension du C pour l'embarqué reste toujours valide, cf ISO TR18035, 37 ou 15, je ne sais plus. Et en parlant d'embarqué, il est à l'honneur cette année au CppCon, c'est dans une quinzaine de jours

  20. #20
    Expert confirmé

    Homme Profil pro
    mad scientist :)
    Inscrit en
    Septembre 2019
    Messages
    2 711
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Paris (Île de France)

    Informations professionnelles :
    Activité : mad scientist :)

    Informations forums :
    Inscription : Septembre 2019
    Messages : 2 711
    Points : 5 390
    Points
    5 390
    Par défaut
    extension du C pour l'embarqué
    oui - mais le compilo de base c'est C++

Discussions similaires

  1. Réponses: 17
    Dernier message: 12/04/2019, 16h18
  2. Conversion int vers byte non signé
    Par grunk dans le forum Débuter avec Java
    Réponses: 5
    Dernier message: 09/06/2011, 16h24
  3. [.COM] Réserver de la RAM fct 48h int 21h
    Par bulerias dans le forum x86 16-bits
    Réponses: 5
    Dernier message: 06/12/2010, 14h33
  4. peut-on éviter les spams "locaux" auto-signés par dkim ?
    Par germaino_0 dans le forum Sécurité
    Réponses: 6
    Dernier message: 12/07/2010, 16h12
  5. Réponses: 11
    Dernier message: 14/12/2005, 13h45

Partager

Partager
  • Envoyer la discussion sur Viadeo
  • Envoyer la discussion sur Twitter
  • Envoyer la discussion sur Google
  • Envoyer la discussion sur Facebook
  • Envoyer la discussion sur Digg
  • Envoyer la discussion sur Delicious
  • Envoyer la discussion sur MySpace
  • Envoyer la discussion sur Yahoo