problème avec la fonction map()

Bonjour à tous !
j'ai des résultats étonnants avec la fonction map()

  uint32_t val=map(65535,0,65535,62500 ,375);
  Serial.print("val : ");Serial.println(val);

me renvoie 65912

  uint16_t val=map(65535,0,65535,62500 ,375);//en microsecondes
  Serial.print("val : ");Serial.println(val);

me renvoie 376

Je ne comprend pas, il me semble que dans les deux cas la fonction devrait renvoyer 375.
Des idées ??

map ne fait pas de "constrain" - c'est simplement une fonction affine. vous donnez deux points, ça calcule la droite qui passe par ces deux points et ensuite si le nombre que vous donnez n'est pas dans les bornes d'entrée, le résultat n'est pas dans les bornes de sortie.

Ensuite il y a la taille max représentable qui peut jouer, si vous ne précisez rien, les nombre sont des int et 65535 ne rentre pas dans un int...

si vous voulez des long mettez 65535L

ensuite quand vous allez titillez "les grands nombres" comme vous êtes en calcul entier (cf la doc), vous rentrez dans les erreurs d'arrondis ou débordement lors des calculs

il vaut mieux écrire votre fonction en float si vous voulez éviter ce challenge, mais ce sera plus lent

float mapFloat(float x, float in_min, float in_max, float out_min, float out_max) {
  return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}

void setup() {
  Serial.begin(115200);
  
  int32_t val32 = mapFloat(65535L, 0, 65535L, 62500L, 375L);
  Serial.print("int32_t val : "); Serial.println(val32);

  int16_t val16 = mapFloat(65535L, 0, 65535L, 62500L , 375L); //en microsecondes
  Serial.print("int16_t val : "); Serial.println(val16);
}

void loop() {}

vous donnera bien 375 dans les 2 cas

J-M-L a donné une solution pour que cela fonctionne. Mais cela n'explique pas pourquoi le code de départ ne fonctionne pas.

La fonction map est définie par:
long map(long x, long in_min, long in_max, long out_min, long out_max) {
return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}

et que l'on l'appelle par
uint32_t val=map(65535,0,65535,62500 ,375);
ou par
uint32_t val=map(65535ul,0ul,65535ul,62500ul ,375ul);
ne changera rien au problème car les nombres sont de toutes façon transformés en long. La conversion peut éventuellement prendre un peu plus de temps.

Quand on fait le calcul avec ces données, on va dépasser la capacité des entiers longs dans la multiplication du numérateur (x - in_min) * (out_max - out_min) . Ce n'est pas très grave sauf que l'opération globale est environ (j'ai laissé tomber le signe - pour l'instant):
65535 * 62125 / 65535
et si JE fais cette opération, je vais trouver 62125. Mais la simplification par 65535 ne peut être faite par le compilateur. il va donc faire la multiplication, et comme cela dépasse la vraie opération sera:
[65535 * 62125 -0x1.000.000]/ 65535 soit encore
[65535 * 62125] / 65535 - 0x1.000.000/ 65535
La première partie va bien donner 62125, mais 0x1.000.000 n'est pas divisible par 65535 et va donner un arrondi que l'on va retrouver dans les deux résultats finaux.

Si on passe des int16_t, comme on a enlevé 0x1.000.000/ 65535 en plus, le résultat va de nouveau déborder quand o le mettra dans le int ce qui va compenser le premier débordement. On se retrouve avec la première valeur mais avec l'erreur d'arrondi soit 376 au lieu de 375.

Si on passe par les uint32_t, on ne va pas avoir le deuxième débordement, et on a donc la même valeur au débordement des int près, c'est à dire modulo 65536
65536+376 donne bien le deuxième résultat.

La fonction map est faite pour convertir un résultat de la conversion analogique 0/1024 dans une autre échelle, et cela ne devrait pas déborder. Le débordement n'est pas prévu. Il manque une petite phrase:
le produit de la plage d'entrée par la plage de sortie doit tenir dans un long (max 0x7000.000)

vileroi:
Mais cela n'explique pas pourquoi le code de départ ne fonctionne pas. et que l'on l'appelle par
uint32_t val=map(65535,0,65535,62500 ,375);
ou par
uint32_t val=map(65535ul,0ul,65535ul,62500ul ,375ul);
ne changera rien au problème car les nombres sont de toutes façon transformés en long.

D'une part ça vous n'en savez rien.. l'optimiser de code peut voir qu'il y a des constantes partout et que le code de la fonction appelée est simple. Il peut donc décider de substituer à l'appel de fonction le calcul direct avec les constantes qu'il va faire en nombre entier puisque c'est ce que veut le standard C++, voire réaliser le calcul lui même et juste mettre le résultat dans la variable dans le code. en mettant des L vous forcez alors le calcul du pré-processeur en Long, ce serait toujours ça de pris...

Mais ici on a un souci de débordement, compilez cela

void setup() {
  Serial.begin(115200);
  Serial.print(F("Calcul -> ")); Serial.println((65535L - 0) * (375L - 62500L) / (65535L - 0) + 62500L);
}

void loop() {}

et vous aurez un [color=orange]warning: integer overflow in expression[/color]

pourquoi ? parce que (65535L - 0) * (375L - 62500L) c'est trop grand pour un LONG....

D'une part ça vous n'en savez rien.

Si parce que j'ai fait l'essai et j'ai le même résultat.

et vous aurez un warning: integer overflow in expression

Dans ce cas, je suis quasiment sûr que le calcul est fait à la compilation. Mais je n'ai pas de messages d'avertissement sous l'IDE et le résultat affiché est bien 65912. Le calcul porte sur des long, le résultat s'affiche comme un long.

ok ok...
Merci beaucoup pour ces réponses rapides et détaillées.
Donc soit je fais avec des uint16_t et le double débordement (à vérifier si ça fonctionne avec toutes le valeurs de 0 à 65535) en acceptant l'imprécision,
Soit je converti en floats ce qui va ralentir considérablement l’exécution du code. Mais il se trouve que la rapidité est très importante pour ce que je développe en ce moment...

que ce soit sur 16 ou 32 bits pour stocker le résultat, votre souci principal c’est que le calcul à effectuer pour la fonction affine est

[color=red](65535L - 0) * (375L - 62500L)[/color] / (65535L - 0) + 62500L

et que chemin faisant, en calculant (65535L - 0) * (375L - 62500L) on devrait avoir -4 071 361 875 en valeur intermédiaire mais le plus grand long représentable sur votre arduino est un -231 = -2 147 483 648… Donc ça déborde. à partir de là plus rien ne fonctionne

vous jouez trop près des bornes… n’y a-t’il pas moyen de changer de domaine tout en restant en calcul entier ?

vileroi:
Si parce que j'ai fait l'essai et j'ai le même résultat.

OK c'est une bonne preuve ça :slight_smile:

J’ai écrit cette petite boucle pour vérifier le “double débordement” :

  for (uint32_t i=0;i<=65535;i++)
  {
    uint16_t val=map(i,0,65535,62500 ,375);
    Serial.print(i);
    Serial.print(" : ");
    Serial.println(val);
  }

ça fonctionne…
mais 62500 et 375 vont aussi devenir des variables…
Danger !

je me suis amusé avec ce code

void setup() {
  Serial.begin(2000000); // 2 millions de bauds OK avec l'ordi

  for (uint32_t i = 0; i <= 65535; i++)  {
    Serial.print(i);
    Serial.write('\t');
    Serial.println(map(i, 0, 65535, 62500 , 375));
  }
}

void loop() {}

en traçant la valeur "mappée" sur un graphe on voit très bien le moment où ça part en sucette

g1.pdf (832 KB)

ce que l'on voit aussi dans les données imprimées

34565 29734
34566 29733
34567 [color=green]29732[/color]
34568 [color=red]95267[/color]
34569 95266
34570 95265
34571 95264

g1.pdf (832 KB)

int64_t map64bits(int64_t x, int64_t in_min, int64_t in_max, int64_t out_min, int64_t out_max) 
{
  return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}

Fonctionne !
Je ne pensait pas qu'on pouvait utiliser des valeurs de 64 bit avec un AVR et l'IDE arduino.

A oui, bonne idée de monitorer avec le plotter !

Mais ce n'est pas le plotter...

non c'est excel :slight_smile:

vous pouvez récrire la fonction en int64_t oui bien sûr mais sur un processeur 8 bits c'est plus lent qu'en float (mais aussi plus précis sans doute)

testez cela:

int64_t map64bits(int64_t x, int64_t in_min, int64_t in_max, int64_t out_min, int64_t out_max)
{
  return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}

float mapFloat(float x, float in_min, float in_max, float out_min, float out_max) {
  return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}

void setup() {
  unsigned long tDebut, tFin;
  volatile long x = 65535; // pour éviter l'optimisation du compilateur
  
  Serial.begin(115200);

  tDebut = micros();
  int32_t valFloat = mapFloat(x, 0, 65535, 62500, 375);
  tFin = micros() ;
  Serial.print(F("en Float : ")); Serial.print(valFloat);
  Serial.print(F(" en ")); Serial.print(tFin - tDebut); Serial.println(F(" µs."));

  tDebut = micros();
  int32_t val64 = map64bits(x, 0, 65535, 62500, 375);
  tFin = micros();
  Serial.print(F("en 64 bits : ")); Serial.print(val64);
  Serial.print(F(" en ")); Serial.print(tFin - tDebut); Serial.println(F(" µs."));

}

void loop() {}

le moniteur série (à 115200 bauds) affichera

[color=purple]
en Float : 375 en 60 µs.
en 64 bits : 375 en 84 µs.
[/color]

--> 40% plus couteux donc en 64 bits

et si vous enlevez le mot clé volatile dans la déclaration de x vous verrez que le calcul se fait instantanément (en 4µs) c'est parce que le compilateur a effectué le calcul pour vous à la compilation et aucun calcul n'est fait sur l'Arduino

Merci beaucoup, c'est exactement le test que je voulais faire !
Mais je ne savais pas qu'il fallait utiliser volatile dans ce cas.

Mais alors !
Chez moi un copié/collé de ton code donne 4us pour les deux !

Avec volatile ??

Bon le code suivant :

int64_t map64bits(int64_t x, int64_t in_min, int64_t in_max, int64_t out_min, int64_t out_max)
{
  return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}

float mapFloat(float x, float in_min, float in_max, float out_min, float out_max) {
  return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}

void setup() {
  unsigned long tDebut, tFin;
  volatile long x = 65535; // pour éviter l'optimisation du compilateur
 
  Serial.begin(115200);

  tDebut = micros();
  volatile int32_t valFloat = mapFloat(x, 0, 65535, 62500, 375);
  tFin = micros() ;
  Serial.print(F("en Float : ")); Serial.print(valFloat);
  Serial.print(F(" en ")); Serial.print(tFin - tDebut); Serial.println(F(" µs."));

  tDebut = micros();
  volatile int32_t val64 = map64bits(x, 0, 65535, 62500, 375);
  tFin = micros();
  Serial.print(F("en 64 bits : ")); Serial.print(val64);
  Serial.print(F(" en ")); Serial.print(tFin - tDebut); Serial.println(F(" µs."));

  tDebut = micros();
  volatile int16_t val = map(x, 0, 65535, 62500, 375);
  tFin = micros();
  Serial.print(F("map ordinaire : ")); Serial.print(val);
  Serial.print(F(" en ")); Serial.print(tFin - tDebut); Serial.println(F(" µs."));
  

}

void loop() {}

Me donne :
en Float : 375 en 60 µs.
en 64 bits : 375 en 84 µs.
map ordinaire : 376 en 60 µs.

Q'en penser ?
Le map ordinaire ne serait pas plus rapide que le float ?
60 µs c'est long non ?

Oui copié/collé.
Mais j'ai rajouté volatile à la variable à calculer et là ça va.

60 µs .. tout est relatif... quel est votre besoin ?