Notation pour la représentation en virgule fixe

Je cherche une notation compréhensible pour définir une représentation numérique à virgule fixe. La notation devrait pouvoir définir à la fois un facteur de puissance de deux (utilisant des bits fractionnaires) et un facteur générique (parfois, je suis obligé de l’utiliser, bien que moins efficace). Et aussi un décalage optionnel devrait être défini.
Je connais déjà quelques notations possibles, mais toutes semblent être limitées à des applications spécifiques.

  • Par exemple, la notation Simulink répondrait parfaitement à mes besoins, mais elle n’est connue que dans le monde Simulink. De plus, l’utilisation surchargée de la fonction fixdt () n’est pas lisible.

  • TI définit un format Q vraiment compact, mais le signe est implicite et ne gère pas un facteur générique (c’est-à-dire pas une puissance de deux).

  • ASAM utilise une fonction rationnelle à 6 coefficients générique avec des polynômes de numérateur et de dénominateur au 2e degré ( COMPU_METHOD ). Très générique, mais pas si sympathique.

Voir aussi la discussion sur Wikipedia .

La question concerne uniquement la notation (pas l’efficacité de la représentation ni la manipulation à virgule fixe). C’est donc une question de lisibilité du code, de maintenabilité et de testabilité.

    Ah oui. Avoir de bonnes annotations de nommage est absolument essentiel pour ne pas introduire de bugs avec l’arithmétique en virgule fixe. J’utilise une version explicite de la notation Q, qui gère toute division entre M et N en ajoutant _Q_ au nom de la variable. Cela permet également d’inclure la signature. Il n’y a pas de pénalité de performances d’exécution pour cela. Exemple:

     uint8_t length_Q2_6; // unsigned, 2 bit integer, 6 bit fraction int32_t sensor_calibration_Q10_21; // signed (1 bit), 10 bit integer, 21 bit fraction. /* * Calculations with the bc program (with '-l' argument): * * sqrt(3) * 1.73205080756887729352 * * obase=16 * sqrt(3) * 1.BB67AE8584CAA73B0 */ const uint32_t SQRT_3_Q7_25 = 1 << 25 | 0xBB67AE85U >> 7; /* Unsigned shift super important here! */ 

    Au cas où quelqu’un n’aurait pas bien compris pourquoi une telle annotation est extrêmement importante, pouvez-vous repérer la présence éventuelle d’un bogue dans les deux exemples suivants?

    Exemple 1:

     speed_fraction = fix32_udiv(25, speed_percent << 25, 100 << 25); squared_speed = fix32_umul(25, speed_fraction, speed_fraction); tmp1 = fix32_umul(25, squared_speed, SQRT_3); tmp2 = fix32_umul(12, tmp1 >> (25-12), motor_volt << 12); 

    Exemple 2:

     speed_fraction_Q7_25 = fix32_udiv(25, speed_percent << 25, 100 << 25); squared_speed_Q7_25 = fix32_umul(25, speed_fraction_Q7_25, speed_fraction_Q7_25); tmp1_Q7_25 = fix32_umul(25, squared_speed_Q7_25, SQRT_3_Q1_31); tmp2_Q20_12 = fix32_umul(12, tmp1_Q7_25 >> (25-12), motor_volt << 12); 

    Imaginez si un fichier contenait #define SQRT_3 (1 << 25 | 0xBB67AE85U >> 7) et un autre fichier contenait #define SQRT_3 (1 << 31 | 0xBB67AE85U >> 1) et que le code était déplacé entre ces fichiers. Par exemple, 1 a une grande chance de passer inaperçu et d’introduire le bogue présent dans l’exemple 2, qui est fait ici délibérément et a zéro chance d’être fait accidentellement.

    En fait, le format Q est la représentation la plus utilisée dans les applications commerciales: vous utilisez lorsque vous devez traiter des nombres fractionnaires FAST et que votre processeur ne possède pas de FPU (unité à virgule flottante). Il ne peut pas utiliser les types de données float et double de manière native. imiter pour eux des instructions très coûteuses.

    vous utilisez généralement le format Q pour représenter uniquement la partie décimale, bien que cela ne soit pas obligatoire, vous obtenez plus de précision pour votre représentation. Voici ce que vous devez considérer:

    • nombre de bits que vous utilisez (Q15 utilise 16 types de données bit, généralement short)
    • le premier bit est le bit de signe (il vous rest 15 bits sur 16 bits)
    • le rest des bits sont utilisés pour stocker la partie décimale de votre nombre.
    • puisque vous représentez des nombres fractionnaires, votre valeur est quelque part dans [0,1)
    • vous pouvez également choisir d’utiliser certains bits pour la partie entière, mais vous perdriez en précision. Par exemple, si vous souhaitez représenter 3,3 au format Q, vous aurez besoin d’un bit pour le signe, de 2 bits pour la partie entière et il vous rest 13 bits pour la partie décimale (en supposant que vous utilisez une représentation 16 bits) -> ce format s’appelle 2Q13

    Exemple: supposons que vous vouliez représenter 0,3 au format Q15; vous appliquez la règle de trois:

      1 = 2^15 = 32768 = 0x8000 0.3 = X ------------- X = 0.3*32768 = 9830 = 0x666 

    Vous avez perdu la précision en faisant cela, mais au moins le calcul est rapide maintenant.

    En C, vous ne pouvez pas utiliser un type défini par l’utilisateur comme un type intégré. Si vous voulez faire cela, vous devez utiliser C ++. Dans cette langue, vous pouvez définir une classe pour votre type de point fixe, surcharger tous les opérateurs arithmétiques (+, -, *, /,%, + =, – =, * =, / =,% =, -, ++ , cast vers d’autres types), de sorte que l’utilisation des instances de cette classe se comporte vraiment comme les types prédéfinis.

    En C, vous devez faire ce que vous voulez explicitement. Il y a deux approches de base.


    Approche 1: Effectuez les ajustements du point fixe dans le code utilisateur.
    Ceci est sans frais généraux, mais vous devez vous rappeler de faire les ajustements corrects. Je pense qu’il est plus facile d’append simplement le nombre de bits de points passés à la fin du nom de la variable, car le système de types ne vous fera pas grand bien, même si vous typedef toutes les positions de points que vous utilisez. Voici un exemple:

     int64_t a_7 = (int64_t)(7.3*(1<<7)); //a variable with 7 past point bits int64_t b_5 = (int64_t)(3.78*(1<<5)); //a variable with 5 past point bits int64_t sum_7 = a_7 + (b_5 << 2); //to add those two variables, we need to adjust the point position in b int64_t product_12 = a_7 * b_5; //the product produces a number with 12 past point bits 

    Bien sûr, c'est très compliqué, mais au moins, vous pouvez facilement vérifier à tout moment si le réglage du point est correct.


    Approche 2: Définissez une structure pour vos nombres à virgule fixe et encapsulez l’arithmétique dans un tas de fonctions. Comme ça:

     typedef struct FixedPoint { int64_t data; uint8_t pointPosition; } FixedPoint; FixedPoint fixed_add(FixedPoint a, FixedPoint b) { if(a.pointPosition >= b.PointPosition) { return (FixedPoint){ .data = a.data + (b.data << a.pointPosition - b.pointPosition), .pointPosition = a.pointPosition }; } else { return (FixedPoint){ .data = (a.data << b.pointPosition - a.pointPosition) + b.data, .pointPosition = b.pointPosition }; } } 

    Cette approche est un peu plus propre dans l'utilisation, cependant, elle introduit des frais généraux importants. Ces frais généraux comprennent:

    1. La fonction appelle.

    2. La copie des structures pour la transmission de paramètre et de résultat, ou le pointeur déréférencie si vous utilisez des pointeurs.

    3. La nécessité de calculer les ajustements de points au moment de l'exécution.

    Cela ressemble beaucoup à la surcharge d'une classe C ++ sans modèles. L'utilisation de modèles ramènerait certaines décisions sur le temps de compilation, au prix d'une perte de flexibilité.

    Cette approche basée sur les objects est probablement la plus flexible et vous permet d’append un support pour les positions de points non binarys de manière transparente.