calcul à virgule fixe, résolution et interpolation linéaire

calcul à virgule fixe, résolution et interpolation linéaire - Algo - Programmation

Marsh Posté le 13-12-2020 à 16:19:49    

Bonjour les mathématiciens, programmeurs et mathématiciens programmeurs :o ,
 
problème: Je dois faire des calculs pour convertir des données brutes (issues d'un capteur) en valeurs qui ont une signification, genre une valeur brute qui sort d'un convertisseur ADC en température en °C. Le capteur possède deux points de calibration et entre les deux il faut faire une interpolation linéaire.
 
En principe c'est basique, sauf que ça doit tourner sur un petit µC donc exit les float/double. Je cherche à faire les calculs à virgule fixe, mais j'ai du mal à m'en sortir.
 
Faisons simple: J'ai une valeur qui varie disons entre -100 et +100 et je veux deux chiffres après la virgule. Il faut donc 7 bits avant la virgule (soit +-127) et 7 bits après la virgule pour avoir une résolution de 2^-7=0,0078... Ok, soit. Je comprends aussi que si je veux convertir une nombre réel en représentation à virgule fixe je dois multiplier par 2^N avec N le nombre de bits après la virgule et arrondir vers zéro. Ok.
 
Sauf que j'ai des constantes de calibration qui sont calculées avec des multiplications et des additions et là je n'arrive pas à trouver les bonnes formules à virgule fixe (une interpolation linéaire en soit je sais faire! :o ).
 
Voilà mon code de test qui tourne sur PC. Je fais les calculs intermédiaires en int64_t histoire de - dans un premier temps - ne pas m'emmerder avec les overflow etc.
 
Quelqu'un pour m'expliquer où et surtout pourquoi il faut rajouter des multiplications/divisions par des puissances de 2 pour avoir 2 décimales en sortie?
 

Code :
  1. #include <stdlib.h>
  2. #include <stdio.h>
  3. #include <stdint.h>
  4.  
  5. int main(void)
  6. {
  7.    uint16_t T0_degC_x8=0x0A6; //température en °C au premier point de calibration multiplié par 8
  8.    uint16_t T1_degC_x8=0x10F; //t° au deuxième point
  9.    int16_t T0_OUT=0x0003; //valeur brute en sortie du ADC pour temp 0
  10.    int16_t T1_OUT=0x02D0; //idem pour temp 1
  11.    
  12.    typedef int64_t var; //pour pouvoir changer facilement, bricolage...
  13.    
  14.    int16_t ADC_TEMP=0x00A9; //valeur brute qui est sortie du ADC lors d'une mesure
  15.    
  16.    //interpolation linéaire: y=ax+b, a=dY/dX, b=y-ax
  17.    
  18.    var a_t=(T1_degC_x8-T0_degC_x8)/((var)T1_OUT-T0_OUT);
  19.    var b_t=T1_degC_x8-(var)T1_OUT*a_t;
  20.    var b1_t=T0_degC_x8-(var)T0_OUT*a_t;
  21.    //vérification: b0 und b1 doivent être identiques
  22.    printf("a: %ld  b: %ld  b1: %ld  (diff %ld)\n", a_t, b_t, b1_t, (b_t-b1_t));
  23.    
  24.    //valeur exacte (calculée en float): 23.788704°C -> 23,78°C ou 23,79°C en sortie du code à virgule fixe
  25.    
  26.    printf("TEMP: %ld°C\n", ADC_TEMP*a_t+b_t); //n'importe quoi, forcément puisque a est nul dans l'état actuel du code
  27.  
  28.    return 0;
  29. }


 
Comme je disais, en float cela fonctionne très bien, mais je voudrais éviter tout le bazar que le compilateur va rajouter pour faire fonctionner ça sur un petit µC au final.
 
Merci.

Reply

Marsh Posté le 13-12-2020 à 16:19:49   

Reply

Marsh Posté le 14-12-2020 à 14:16:40    

Et si tu multiplies toutes les valeurs en entrée par 100 et tu castes en int pour supprimer l'éventuelle virgule derrière ?
Ensuite, tu fais tous tes calculs avec ces valeurs x100. Pour avoir les vraies valeurs à la fin, tu redivises par 100.
Ca le ferait pas ?


---------------
Astres, outil de help-desk GPL : http://sourceforge.net/projects/astres, ICARE, gestion de conf : http://sourceforge.net/projects/icare, Outil Planeta Calandreta : https://framalibre.org/content/planeta-calandreta
Reply

Marsh Posté le 14-12-2020 à 19:15:13    

Non, la variable a_t sera p.ex. nulle si je fais

Code :
  1. var a_t=(100*(var)T1_degC_x8-100*(var)T0_degC_x8)/(100*(var)T1_OUT-100*(var)T0_OUT);


avec var==int64_t.
 
Ou tu voulais dire autre chose? :o  
 
Le soucis c'est que pour les additions et soustractions c'est simple, mais pour une multiplication déjà c'est plus délicat, car (100*a)*(100*b)=10**4*c, donc il faudrait diviser par 10**4 et non 10**2 à la fin - ce qui en soit n'est pas un problème mais ça montre que cette histoire est un peu plus complexe qu'il n'y paraît. Et mon niveau en maths n'est pas énorme. :( Ca doit pas être bien complexe en soi, mais je m'en sors pas. :(

Reply

Marsh Posté le 14-12-2020 à 19:26:44    

Je le ferais en plusieurs étapes car t'es pas à l'abri d'opti du compilo.
Moi, je ferais un x100 au moment de l'acquisition des données, du genre :
A = 100 * A
B = 100 x B
...
Et ensuite tu fais ton calcul d'interpolation qui va te donner un résultat. Et tout à la fin, au moment d'interpréter ton résultat, tu le divises par 100.


---------------
Astres, outil de help-desk GPL : http://sourceforge.net/projects/astres, ICARE, gestion de conf : http://sourceforge.net/projects/icare, Outil Planeta Calandreta : https://framalibre.org/content/planeta-calandreta
Reply

Marsh Posté le 14-12-2020 à 19:39:28    

Ah oui, l'optimisation, en effet, ça pourrait être un soucis dans ce code test. Bien vu. :jap:  Je vais regarder.

Reply

Marsh Posté le 14-12-2020 à 19:46:19    

Bon, pour tester j'ai mis toutes les valeurs en variable globale volatile int64_t ce qui devrait éviter toute optimisation(??), mais le résultat est n'importe quoi, même si on oublie la virgule. Le soucis c'est que "a" est toujours 0.

 

Le code modifié au cas où quelqu'un y vois une erreur de ma part...

Code :
  1. volatile int64_t T0_degC_x8=0x0A6; //température en °C au premier point de calibration multiplié par 8
  2. volatile int64_t T1_degC_x8=0x10F; //t° au deuxième point
  3. volatile int64_t T0_OUT=0x0003; //valeur brute en sortie du ADC pour temp 0
  4. volatile int64_t T1_OUT=0x02D0; //idem pour temp 1
  5. typedef int64_t var; //pour pouvoir changer facilement, bricolage...
  6. volatile int64_t ADC_TEMP=0x00A9; //valeur brute qui est sortie du ADC lors d'une mesure
  7. int main(void)
  8. {
  9. //interpolation linéaire: y=ax+b, a=dY/dX, b=y-ax
  10.    
  11.     T0_degC_x8*=100;
  12.     T1_degC_x8*=100;
  13.     T0_OUT*=100;
  14.     T1_OUT*=100;
  15.     ADC_TEMP*=100;
  16.    
  17.     var a_t=(T1_degC_x8-T0_degC_x8)/(T1_OUT-T0_OUT);
  18.     var b_t=T1_degC_x8-T1_OUT*a_t;
  19.     var b1_t=T0_degC_x8-T0_OUT*a_t;
  20.     //vérification: b0 und b1 doivent être identique
  21.     printf("a: %ld  b: %ld  b1: %ld  (diff %ld)\n", a_t, b_t, b1_t, (b_t-b1_t));
  22.    
  23.     //valeur exacte (calculée en float): 23.788704°C -> 23,78°C ou 23,79°C en sortie du code à virgule fixe
  24.    
  25.     printf("TEMP: %ld°C\n", (ADC_TEMP*a_t+b_t)/100);
  26.    
  27.     return 0;
  28. }
 

edit: J'ai d'autres formules où un *100 était en effet suffisant au bon endroit car on ne fait que diviser par une puissance de 2, mais là avec cette interpolation j'ai du mal.


Message édité par rat de combat le 14-12-2020 à 19:56:33
Reply

Marsh Posté le 14-12-2020 à 20:22:32    

Si tes constantes de calibration sont connues, pourquoi tu ne mets pas dans les coeffs a et b les valeurs déjà calculées. Ainsi, t'auras plus qu'à faire le calcul d'interpolation avec la valeur relevée.
 
Avec tes valeurs, sans faire le x100 des valeurs, je trouve :
a = 0.1464
b = b1 = 165.5606
 
Par contre, quand je disais de x100 toutes les valeurs, c'était les mesures (les températures) uniquement ! Parce que si tu fais x100 aussi sur les temps, quand tu vas calculer le coeff a, les x100 vont s'annuler pour faire x1, gros malin :D
Du coup, comme tes valeurs sont que des int et que a < 1 (0.1464), ça va caster et tu vas te retrouver avec un a = 0 :/


---------------
Astres, outil de help-desk GPL : http://sourceforge.net/projects/astres, ICARE, gestion de conf : http://sourceforge.net/projects/icare, Outil Planeta Calandreta : https://framalibre.org/content/planeta-calandreta
Reply

Marsh Posté le 14-12-2020 à 20:30:04    

rufo a écrit :

Si tes constantes de calibration sont connues, pourquoi tu ne mets pas dans les coeffs a et b les valeurs déjà calculées. Ainsi, t'auras plus qu'à faire le calcul d'interpolation avec la valeur relevée.

C'est vrai. Le soucis c'est que ces coeffs sont/peuvent être différents pour chaque exemplaire du capteur et je risque d'en avoir plusieurs, du coup ça serait bien si le Master qui dirige tout le bazar à la fin (un µC) peut faire les calculs de a et b pour chaque capteur en automatique. Après je pourrais faire ça une fois à la main pour chaque capteur aussi et si je m'en sors pas ça sera ça, mais bon, c'est aussi pour comprendre... :o
 

Citation :

Par contre, quand je disais de x100 toutes les valeurs, c'était les mesures (les températures) uniquement ! Parce que si tu fais x100 aussi sur les temps, quand tu vas calculer le coeff a, les x100 vont s'annuler pour faire x1, gros malin :D
Du coup, comme tes valeurs sont que des int et que a < 1 (0.1464), ça va caster et tu vas te retrouver avec un a = 0 :/

Je ne suis pas sûr d'avoir compris... Quel temps? Le capteur est un capteur de température, il me sort une valeur ADC brute que je veux transformer en °C en utilisant une interpolation linéaire avec ces fameuses constantes à calculer.
 
En tout cas merci pour ton aide. :jap:  
Je vais regarder ça encore une fois et refléchir à tes propos.

Reply

Marsh Posté le 14-12-2020 à 20:39:01    

Ce ne sont que les valeurs Tx_degC_x8 qu'il faut x100 (donc les valeurs de l'axe des Y), pas les valeurs du dénominateur.
Autant pour moi, j'avais cru que c'était des durées (des temps) en X et pas des valeurs brutes.
Donc, tu gardes tes valeurs brutes et tu fais x100 (en plus du x8 que tu fais déjà, donc faire un x800 en tout) sur les variables Tx_degC_x8.
C'est plus clair ?


---------------
Astres, outil de help-desk GPL : http://sourceforge.net/projects/astres, ICARE, gestion de conf : http://sourceforge.net/projects/icare, Outil Planeta Calandreta : https://framalibre.org/content/planeta-calandreta
Reply

Marsh Posté le 14-12-2020 à 20:49:11    

Les températures sont stockées x8 dans la mémoire de la puce - et en effet, il faut le prendre en compte ce que j'avais un peu oublié. :o

 

Voilà le code test actuel:

Code :
  1. T0_degC_x8*=100;
  2.     T1_degC_x8*=100;
  3.     var a_t=(T1_degC_x8-T0_degC_x8)/(T1_OUT-T0_OUT);
  4.     var b_t=T1_degC_x8-T1_OUT*a_t;
  5.     var b1_t=T0_degC_x8-T0_OUT*a_t;
  6.     //vérification: b0 und b1 doivent être identique
  7.     printf("a: %ld  b: %ld  b1: %ld  (diff %ld)\n", a_t, b_t, b1_t, (b_t-b1_t));
  8.    
  9.     //valeur exacte (calculée en float): 23.788704°C -> 23,78°C ou 23,79°C en sortie du code à virgule fixe
  10.    
  11.     printf("TEMP: %f°C\n", (ADC_TEMP*a_t+b_t)/800.0); //prendre en compte le x8!


(J'ai mis du float en sortie pour tester pour avoir toutes les décimales.)

 

Sauf que la précision n'y est pas encore: Je retrouve 24,23°C alors qu'il faudrait trouver 23,79°C. Certes, la différence n'est pas énorme, mais c'est justement le point clé: Pour avoir deux décimales correctes il ne suffit PAS de multiplier certaines valeurs en entrée par 10**2. Par tâtonnement je trouve qu'il faut multiplier par 10**4 plutôt, mais pourquoi? J'aimerai bien comprendre, si ce n'est pour ma culture générale. Il doit bien y avoir un tas de formules qui permettent de comprendre/calculer ces facteurs précisément en fonction de la précision souhaitée et du genre de calculs (additions, multiplications, ...) à faire? Ca doit être tout con je suppose mais je ne trouve pas.


Message édité par rat de combat le 14-12-2020 à 20:49:59
Reply

Marsh Posté le 14-12-2020 à 20:49:11   

Reply

Marsh Posté le 14-12-2020 à 21:52:35    

Ben parce qu'en multipliant par 10000, t'as une précision de 4 chiffres après la virgule, donc c'est normal que tu obtiennes un meilleur résultat.
Donc, au lien que ton coeff a ait la valeur 0.1464, donc 14 avec un x100 et 164 avec x10000 ce qui fait que dans ce dernier cas, tu perds pas de chiffre après la virgule. :o


---------------
Astres, outil de help-desk GPL : http://sourceforge.net/projects/astres, ICARE, gestion de conf : http://sourceforge.net/projects/icare, Outil Planeta Calandreta : https://framalibre.org/content/planeta-calandreta
Reply

Marsh Posté le 01-01-2021 à 16:22:00    

J'avoue que j'ai toujours du mal avec cette histoire / l'approche par tatonnement ne me plaît pas. J'essaye de me documenter et je viens de tomber sur une présentation de l'Université Paris 6 qui semble compréhensible au niveau maths. Je vais regarder ça...
 
Et un résultat plus spécifique aux AVR: https://wiki.logre.eu/index.php/Calculs_en_virgule_fixe
 

Reply

Marsh Posté le 01-01-2021 à 17:58:27    

Bon, j'ai trouvé comment contourner tout ce casse-tête. Il suffit de bricoler un peu les expressions pour n'avoir qu'une seule division tout à la fin et aucune valeur <1, du coup il suffit de prendre le numérator*100 (soit 10**2) pour avoir 2 décimales. Ce n'est pas parfait, il faudrait arrondir correctement, mais ça me va. Reste a voir la taille des variables.

 
Code :
  1. var dX=T1_OUT-T0_OUT;
  2. var dY=T1_degC_x8-T0_degC_x8;
  3. var x1=T1_OUT;
  4. var y1=T1_degC_x8;
  5. var x=ADC_TEMP;
  6. var num=x*dY+y1*dX-x1*dY;
  7. var denom=dX;
  8. printf("TEMP: %ld°C\n", 100*num/(8*denom)); //prendre en compte le x8!
 

Voilà, j'ai pas compris ces histoires de virgule fixe mais j'ai mon calcul qui fonctionne, ça devra suffire.

 

:jap:


Message édité par rat de combat le 01-01-2021 à 17:59:01
Reply

Sujets relatifs:

Leave a Replay

Make sure you enter the(*)required information where indicate.HTML code is not allowed