Forum Programmation.c Volatile, struct et interruptions.

Posté par  . Licence CC By‑SA.
Étiquettes : aucune
2
13
jan.
2016

Bonsoir à tous.

Je suis en train de me poser plusieurs questions à propos de la façon de déclarer des variables devant être modifiées par une interruption.

Je dispose d'une structure de ce type :

struct buffer_t {
byte *buff;
int size;
int head=0;
int tail=0;
byte status=BUFF_EMPTY;
};

Cette structure représente un buffer circulaire rempli par une routine d'interruption, et (probablement) vidé par une autre routine d'interruption (pour l'instant je considère que ce n'est pas le cas).

Je sais que lorsqu'on utilise une variable devant être modifiée par une interruption, on déclare cette variable volatile.

Quand il s'agit d'une seule variable c'est simple. Par contre, quid d'une structure ?

Personnellement, je pense que je dois déclarer volatile les éléments de la structure pouvant être modifiés par les interruptions. Dans mon cas il s'agit de:

  • head : modifié par l'interruption qui vide mon buffer si j'utilise une routine d'interruption pour le vider. Si ce n'est pas le cas, pas besoin de la déclarer volatile.
  • tail : modifié par l'interruption qui remplit le buffer.
  • status : modifié également par la lecture ou l'écriture d'un élément dans mon buffer.

Les éléments size, ainsi que le pointeur sur byte buff ne changent pas, eux. par contre les éléments pointés par buff seront modifiés par la routine qui écrit (mais pas par celle qui lit puisqu'elle n'aura qu'à incrémenter l'index head pour supprimer l'élément de tête de mon buffer). Ma première question : je déclare ça comment ? a première vue je ferais un truc du genre :

typedef volatile byte v_byte;

puis je déclarerais ma structure ainsi :

struct buffer_t {
v_byte *buff;
[ … ]
};

Qu'en pensez vous ?

Enfin, mon buffer sera déclaré comme variable globale de la façon suivante :

struct buffer_t buffer;

Aurai-je besoin de déclarer un pointeur de type volatile sur ce buffer pour pouvoir y accéder dans mon interruption ? A priori, je pense que si je déclare un pointeur, il ne doit pas être de type volatile car cette valeur n'est jamais modifiée durant toute la vie du programme. Et vous, que me conseillez-vous ?

  • # multithread et optimisation

    Posté par  (site web personnel) . Évalué à 2.

    Il me semble qu'il y a deux aspects à distinguer, d'une part prévenir la concurrence malheureuse en multithread et d'autre part prévenir une optimisation malheureuse d'accès à la mémoire vive.

    volatile ne permet d'éviter que les optimisations malheureuses d'accès à la mémoire vive.

    Par contre, cela ne concerne que la variable en question… Ce que je veux dire, c'est que dans le traitement de ce buffer, il y a typiquement un pointeur propre de parcourt initialisé avec le buff de ta structure (et si volatile, toujours par une lecture de la mémoire vive, même si pour l'optimisateur un registre contient toujours la valeur) mais ensuite, que se passera-t-il ?

    Comme son nom l'indique, une interruption est susceptible d'interrompre le fil d'exécution où qu'il en soit (il y a peut-être possibilité de protéger un petit groupe d'instructions, à la manière "atomique", mais je suis pas sûr…). Par exemple, les allocations dynamiques ne devraient, a priori, pas être libérées par le traitement d'une interruption.

    • [^] # Re: multithread et optimisation

      Posté par  . Évalué à 2. Dernière modification le 14 janvier 2016 à 13:57.

      prévenir la concurrence malheureuse en multithread

      Je n'ai pas cette problématique : c'est du développement sur microcontroleur AVR donc pas de multithread.

      prévenir une optimisation malheureuse d'accès à la mémoire vive.

      C'est surtout cet aspect qui m'intéresse aujourd'hui.

      volatile ne permet d'éviter que les optimisations malheureuses d'accès à la mémoire vive.

      Ca tombe bien, c'est ce que je cherche à faire ;)

      Par contre, cela ne concerne que la variable en question…

      C'est ce qui m'a semblé également, mais j'avais un doute d'ou mes questions.

      dans le traitement de ce buffer, il y a typiquement un pointeur propre de parcourt initialisé avec le buff de ta structure

      dans le traitement de ce buffer, il y a typiquement un pointeur propre de parcourt initialisé avec le buff de ta structure

      Dans le parcours, j'accède de façon indirecte via *(buff + head) ou *(buff + tail).

      et si volatile, toujours par une lecture de la mémoire vive, même si pour l'optimisateur un registre contient toujours la valeur)

      Est-ce à dire que même si buff e ne bouge jamais (initialisé tout au début du code), j'ai intérêt à le déclarer volatile quand même ?

      mais ensuite, que se passera-t-il ?

      C'est ce que j'aimerais savoir …

      Comme son nom l'indique, une interruption est susceptible d'interrompre le fil d'exécution où qu'il en soit

      (il y a peut-être possibilité de protéger un petit groupe d'instructions, à la manière "atomique", mais je suis pas sûr…).

      Dans ce cas précis, une interruption ne peut pas être interrompue. Mais malhereusement, il n'y a pas possibilité de générer une interruption logicielle (sauf à passer par un petit hack que je souhaite éviter), sinon, je pourrais passer par une interruption pour protéger le code en question. Mais je vois l'idée.

      En tout cas, merci d'avoir pris un peu de temps pour répondre

      • [^] # Re: multithread et optimisation

        Posté par  (site web personnel) . Évalué à 2.

        La la valeur de buff est stable, alors c'est head et tail qui sont susceptible d'être modifiés lors du remplissage ? Ce serait alors éventuellement ces deux variables qui devraient être volatiles.

        Qu'en penses-tu ?

        Essayes d'imaginer des scénarios…

        Dis-toi que sans volatile, un code peut-être optimisé de sorte à ne pas systématiquement lire ou écrire en mémoire vive mais soit récupérer une valeur toujours présente dans un registre ou postposer l'écriture en mémoire tant que le registre ne doit pas être libéré.

        • [^] # Re: multithread et optimisation

          Posté par  . Évalué à 2. Dernière modification le 14 janvier 2016 à 17:05.

          La la valeur de buff est stable, alors c'est head et tail qui sont susceptible d'être modifiés lors du remplissage ? Ce serait alors éventuellement ces deux variables qui devraient être volatiles.

          Qu'en penses-tu ?

          Je suis d'accord (c'est ce que j'avais mis il me semble dans mon post initial, mais je n'ai peut-être pas été clair, vu l'heure ou je l'ai fait).

          Les scénarios :

          • lors de la phase d'initialisation, je crée mon buffer (j'initialise la structure avec la zone mémoire à remplir/vider, et je positionne les variables tail et head, aisi que le status de mon buffer à Empty).

          • j'initialise les routines d'interruption qui vont remplir le buffer (3 sources différentes).

          • lorsque je reçois une interruption de l'une de ces 3 sources :

            • je lis l'état de 2 entrées
            • je lis le contenu d'un timer (14 bits utiles)
            • je merge les données sur 16 bits
            • je remplis mon buffer.

          Note : quand je remplis mon buffer, je positionne la variable status à NON_EMPTY, et je la positionne à FULL lorsque je n'ai plus de place.

          Pour info, sur mon microcontroleur, les interruptions ne peuvent être interrompues.

          Dans ma routine principale, j'envisage de:
          - tester le taux de remplissage du buffer (vérifier qu'il y a plus de N octets dispo).
          - si le buffer est plein à plus de n%, je démarre la transmission des données sur une UART. L'idée serait de transmettre via interruption tant que le buffer n'est pas vide. Lorsque le buffer est vide, je stoppe le transfert (ais pour cette partie, je ne suis pas sur que ce soit la meilleure façon de faire, je n'ai pas encore les idées tout à fait claires, il est possible que je vienne à emettre via le a boucle principale).

          Donc au final:
          - dans mes interruptions de remplissage, je n'accède qu'à la variable tail (et à mon buffer)
          - dans la boucle principale : je n'ai pas encore déterminé comment détecter le taux de remplissage. Probablement en lisant les variables head, tail et size de ma structure.
          - si j'émets via interruption, je ne modifie que la variable head dans cette interruption. Comme une it ne peut être interrompu, je suis sur qu'il n'y aura rien de modifié pendant le déroulement de celle-ci
          - si j'emets dans ma boucle principale, je suis suceptible d'être interrompu par une interruption de remplissage. Mais comme les variables modifiées pendant l'émission (head) n'est pas modifiée par la routine de remplissage. Par contre cette routile va la lire pour savoir si mon buffer est plein ou non. C'est là qu'il faut que je m'assure que ça ne pose pas de pb.

          Dis-toi que sans volatile, un code peut-être optimisé de sorte à ne pas systématiquement lire ou écrire en mémoire vive mais soit récupérer une valeur toujours présente dans un registre ou postposer l'écriture en mémoire tant que le registre ne doit pas être libéré.

          C'est bien ce que je pensais avoir compris, mais toujours avec un doute quelque part. Merci à toi, tu as levé mes doutes.

          • [^] # E_NOENOUGH_INFO | E_UNNEEDED

            Posté par  . Évalué à 2. Dernière modification le 14 janvier 2016 à 22:28.

            Quelques remarques en attendant que tes algos soit spécifiés et que l'on puisse te donner une réponse précise.

            • si ton interruption ne se fait pas interrompre, un volatile dans le code de l'interruption ne sert effectivement à rien.

            • un volatile dans le code "main" peut-être utile mais certainement pas suffisante (enfin faut voir ton algo toujours). Je conjecture que tu auras quand même besoin d'implémenter un mécanisme de type mutex pour te prémunir des problèmes de TOCTOU.

            • inspecte le code assembleur pour être sûr de savoir ce que ton compilo a produit.

            • mettre volatile est souvent une fausse bonne idée. Je te laisse rechercher les références de Linus sur la LKML ;-) La démonstration suit…

            Admettons que ton main ressemble à cela:

            // pseudocode, addresse du buffer assimilée à zéro,,,
            if (tail < head)
              sz = tail + (bufsz - head);  // le buffer a "wrappé"
            else
              sz  = tail - head;
            
            if (sz >= MIN_COPY) {
              copy(head, copy_size); // gère le wrapping
              move_head(&head, copy_size); // idem
            }
            // loop...
            

            Au niveu de l'interruption:

            //calculer sz comme au dessus, puis calculer l'espace libre
            freesz = bufsz - sz;
            
            if (freesz >= neededsz) {
               while (neededsz--) {
                  *tail = data; // ??
                  if (tail == bufz - 1) // wrap ?
                     tail = 0;
                  else
                     tail++;
               }
            } else {
               *overrun = 1;
            }
            

            NB: c'est une implémentation naïve d'un buffer circulaire. Je n'ai jamais fait ce genre de chose, je te conseille quand même de consulter wikipedia avant ;-)

            Donc nous avons ton interruption qui modifie tail et ton main qui modifie head.

            • Il y a-t-il un risque que l'interruption utilise une mauvaise valeur pour head ? C'est-à-dire entre le momen où tu calcules freesz et le moment où tu modifie tail.
              -> Non je ne crois pas, au pire tu as un overrun du à un freesz sous-estimé.

            • Il y a-t-il un risque que main utilise une mauvaise valeur de tail ? Idem, non je ne crois pas.

            Le problème se situe au moment où l'on calcule sz et freesz !! En fait là tu ne veux surtout pas de volatile : que se passerait-il si tail est accèdée en volatile et qu'une interruption fait wrapper tail et survient juste après que tu aies testé qu'il n'y a pas de wrap ? Oops un freesz < 0… ça sent pas bon hein… ;-)

            Donc bref: tu copies tes volatiles dans des variables locales au début de ta routine pour être certain que tu n'accède pas à tes variables en ram directement. Ton algo doit être conçu pour fonctionner sur des valeurs qui ne reflètent peut-être plus la réalité mais qui son cohérentes entre elles, pas pour fonctionner avec des valeurs certes actuelles mais qui changent au gré du vent/des lignes!!!

            (Ma) conclusion : pas besoin de mutex, surtout pas besoin de volatile, sous réserve que le code "main" soit dans une fonction séparée.

            Maintenant si tu passes à plusieurs producteurs/consommateurs, ben ça se complique : ne le fais pas, utilise plusieurs buffers :p

            PS: je viens de me rendre compte que c'est l'inverse que tu veux faire, soit vider un buffer par une interruption. Cela revient au même.

            • [^] # Re: E_NOENOUGH_INFO | E_UNNEEDED

              Posté par  . Évalué à 2.

              Pour complèter un peu au sujet des mutex.
              Ici il n'y a qu'une variable suceptible d'être modifiée par dessous le tapis -> pas besoin de mutex.

              S'il y avait plusieurs valeurs à lire, alors dans ce cas tu es obligé de trouver un mécanisme de synchronisation, pour t'assurer qu'il n'y aie pas eu d'interruption entre le moment où tu as lu la première valeur et la dernière. Dans ton cas, un simple compteur de génération devrait suffire car tu as la certitude que celui qui incrémente le compeur ne peut pas être interrompu. Sinon tu devras passer par un compare-and-swap, mutex ou rcu (mais bon là on est loin du cadre du dévelopement de micro-controlleurs).

              Au sujet des volatile: le volatile ne sert qu'à "forcer" la lecture en ram, ce qui n'est que (probablement) uniquement nécessaire dans le cas d'une boucle, pas si tu lis les valeurs dans le prologue d'une fonction (car le compilo ne peut pas supposer que telle valeur sera déja présente dans un registre, sauf si tu utilises un compileur qui fait de l'analyse globale avec un -O42 et -f-i-dont-care-about-c-standard).
              Par contre pour le cas d'un compteur de génération, c'est peut-être mieux de le mettre en volatile car tu veux probablement "forcer" une lecture à chaque fois qu'il est utilisé. L'important c'est de bien raisonner en fonction de ton code-flow. Si une lecture détermine un chemin d'exécution particulier alors de deux choses l'une: soit tu ne veux probablement plus réutiliser sa valeur et dans ce cas un volatile est OK, soit tu veux réutiliser sa valeur plus tard et un volatile n'est absolument pas OK! Dans le cas d'un compteur, il faut voir sa valeur comme une valeur à un instant T, chaque fois que tu vas le lire, c'est par définition une valeur différente.

              • [^] # Re: E_NOENOUGH_INFO | E_UNNEEDED

                Posté par  . Évalué à 3.

                Ici il n'y a qu'une variable suceptible d'être modifiée par dessous le tapis -> pas besoin de mutex.

                J'entends bien "par un seul des agents"… Évidemment si plusieurs agents (ou contextes d'exécution) modifient la même variable, il y a besoin d'une synchronisation.

            • [^] # Re: E_NOENOUGH_INFO | E_UNNEEDED

              Posté par  . Évalué à 2.

              Il y a-t-il un risque que l'interruption utilise une mauvaise valeur pour head ? C'est-à-dire entre le momen où tu calcules freesz et le moment où tu modifie tail.
              -> Non je ne crois pas, au pire tu as un overrun du à un freesz sous-estimé.

              Et bien si!

              Il y a-t-il un risque que main utilise une mauvaise valeur de tail ? Idem, non je ne crois pas.

              Et bien si!

              a2g

              • [^] # Re: E_NOENOUGH_INFO | E_UNNEEDED

                Posté par  . Évalué à 2.

                Il y a-t-il un risque que l'interruption utilise une mauvaise valeur pour head ? C'est-à-dire entre le momen où tu calcules freesz et le moment où tu modifie tail.
                -> Non je ne crois pas, au pire tu as un overrun du à un freesz sous-estimé.

                Et bien si!

                Mais à la limite on s'en fiche : je ne calcule pas la taille dans mon cas, je me contente juste de comparer tail et head pour savoir si mon buffer est plein. La seule chose que je risque, c'est de croire que mon buffer est plein alors qu'un espace a été libérée par la routine de lecture, ce qui n'est pas si grave que ça dans mon cas.

                Il y a-t-il un risque que main utilise une mauvaise valeur de tail ? Idem, non je ne crois pas.
                

                Et bien si!

                Là encore je ne pense pas que ce soit grave : dans la boucle, il considèrera que le buffer est vide, alors que l'IT aura ajouté un caractère dans le buffer, et attendra la prochaine itération de la boucle pour voir qu'en fait il y a un caractère à émettre.

                • [^] # Re: E_NOENOUGH_INFO | E_UNNEEDED

                  Posté par  . Évalué à 2.

                  Tu te méprends, cf. mon post plus bas.
                  Je fais référénce au code ci-dessus et les déclarations de ton premier post.
                  C'est au moment où tu vas calculer ton "sz" que ça va partir en live: si tu es en train de le calculer, et que l'écriture en mémoire n'est pas finie (par exemple), que ton IT claque, tu vas calculer un "freesz" qui peut être complètement faux!
                  si tu considère que l'opération "sz = tail + (bufsz - head);" se fait en un coup d'horloge, uniquement par des registres, qu'il n'y a qu'un accès mémoire à sz, alors pourquoi pas!
                  Mais celà n'a pas l'air d'être le cas pour la "plus" simple raison que toutes ces variables sont des 16bits sur un AVR alors que ta cpu est en 8bits.
                  Donc avec le code proposé, tu auras des bugs aléatoirement et un résultat inattendu voir plantage du micro, mais si tu penses que ce n'est pas grave…
                  Je ne peux que t'encourager vivement à lire le fichier .lss généré pour comprendre comment une instruction 'C' est compilée et optimisée

                  a2g

                  ps: par curiosité, quel est le micro que tu utilises et ça fait combien de temps que tu codes sur de l'AVR ou un micro 8bits?

                  • [^] # Re: E_NOENOUGH_INFO | E_UNNEEDED

                    Posté par  . Évalué à 2.

                    Avant de continuer, je vais donner une petite explication sur la façon dont je remplis/vide mon buffer, ce sera plus simple et plus compréhensible. Je n'ai pas le code de gestion du buffer sous la main pour le moment mais voilà en gros ce que je fais :

                    Si tu regardes bien ma structure, j'ai une variable status dans mon buffer. Cette variable peut prendre plusieurts état : vide, non vide (équivalent à non plein), et plein.

                    J'ai défini un truc du genre :

                    #define BUFF_EMPTY 0
                    #define BUFF_NON_EMPTY 1
                    #define BUFF_NON_FULL BUS_NON_EMPTY
                    #define BUFF_FULL 2

                    Lorsque le buffer est vide, cette variable est à l'état vide. Lorsquer je remplis mon buffer, cette variable passe à l'état non-vide. Lorsque je le vide, elle passe à l'état non plein (qui est la même chose que non vide).Si je détecte après ajout d'un élément que mon buffer est plein, je positionne cet indicateur à plein. Si je détecte après vidage d'un élément que mon buffer est vide, je positionne cet indicateur à vide.

                    Pour le remplissage, j'ajoute mon élément dans le buffer, et je modifie l'index tail ensuite Dans le détail ça donne :

                    • je teste la variable status pour voir si mon buffer n'est pas plein. S'il n'est pas plein, je remplis mon buffer en ajoutant l'élément a l'endroit pointé par buff + tail.
                    • Ensuite, Je compare l'index à la taille du buffer (qui elle ne bouge jamais) : si index==taille de buffer-1, je positionne index à 0. Sinon, j'incrémente cet index.
                    • Ensuite je compare head et tail: s'ils sont égaux, ça signifie que mon buffer est plein. Dans ce cas je positionne le status à plein. Sinon, je positionne le status à non plein.

                    pour le vidage je récupère ma valeur, et je modifie mon index head ensuite Le détail :

                    • je teste la variable status pour voir si mon buffer n'est pas vide. S'il n'est pas vide, je lis mon buffer en récupérant l'élément a l'endroit pointé par buff + head.
                    • Ensuite, Je compare l'index tail à la taille du buffer (qui elle ne bouge jamais) : si index=taille de buffer-1, je positionne l'index à 0. Sinon, j'incrémente cet index.
                    • Ensuite je compare head et tail: s'ils sont égaux, ça signifie que mon buffer est vide. Dans ce cas je positionne le status à vide. Sinon, je positionne le status à non vide.

                    C'est au moment où tu vas calculer ton "sz" que ça va partir en live:

                    Comme tu le vois, je ne calcule pas de taille en tant que telle, par contre je pense que la comparaison entre tail et head équivaut à un calcul de taille (il faudrait juste voir comment le compilo génère le code assembleur dans ce cas pour en être sur).

                    si tu es en train de le calculer, et que l'écriture en mémoire n'est pas finie (par exemple), que ton IT claque, tu vas calculer un "freesz" qui peut être complètement faux!

                    C'est pour ça que je me demande si je ne devrais pas vider mon buffer sur interruption, pour m'assurer que les calculs se fassent sans accès concurrent potentiel.

                    Faire un truc du genre tester dans la boucle principale l'état de la variable status de ma structure, et si elle n'est pas vide, j'initie le transfert sur ma liaison série via It. Mais comme je n'ai pas pris le temps de réfléchir plus loin à cette partie, je ne suis pas vraiment sur de mon coup. Comme dit plus haut, il faudrait que je me refasse un petit schema du workflow.

                    Mais celà n'a pas l'air d'être le cas pour la "plus" simple raison que toutes ces variables sont des 16bits sur un AVR alors que ta cpu est en 8bits.

                    Bien vu, je n'avais pas pensé à ce détail.

                    Je ne peux que t'encourager vivement à lire le fichier .lss généré pour comprendre comment une instruction 'C' est compilée et optimisée

                    C'est ce que j'ai l'intention de faire de toute façon.

                    ps: par curiosité, quel est le micro que tu utilises et ça fait combien de temps que tu codes sur de l'AVR ou un micro 8bits?

                    J'utilise un atmega328. Ca fait déjà plusieurs dizaines d'années que je code sur du micro 8 bits (mais pas à plein temps) , mais jusqu'aujourd'hui, je ne faisais que de l'assembleur (sur ATTiny), donc un certain nombre de questions ne se posaient pas (notamment toutes ces histoires d'optimisation du compilateur).

                    • [^] # Re: E_NOENOUGH_INFO | E_UNNEEDED

                      Posté par  . Évalué à 2. Dernière modification le 15 janvier 2016 à 15:57.

                      Euh .. erreur dans cette partie :

                      pour le vidage je récupère ma valeur, et je modifie mon index head ensuite Le détail :

                      • je teste la variable status pour voir si mon buffer n'est pas vide. S'il n'est pas vide, je lis mon buffer en récupérant l'élément a l'endroit pointé par buff + head.
                      • Ensuite, Je compare l'index tail head à la taille du buffer (qui elle ne bouge jamais) : si index=taille de buffer-1, je positionne l'index à 0. Sinon, j'incrémente cet index.
                      • Ensuite je compare head et tail: s'ils sont égaux, ça signifie que mon buffer est vide. Dans ce cas je positionne le status à vide. Sinon, je positionne le status à non vide.

                      Est-ce qu'un modérateur pourrait corriger ? Sinon ça n'a pas de sens.

                    • [^] # Re: E_NOENOUGH_INFO | E_UNNEEDED

                      Posté par  . Évalué à 2.

                      Le problème n'est pas lié au "calcul" mais à l'architecture 8bits pour des calculs 16 bits et au delà.
                      Toute donnée significative de 16bits utilisée (que celà soit écriture ou lecture) dans une interruption doit être protégée ailleurs dans une section "protégée" par désactivation/activation de l'interruption. C'est une règle à laquelle tu n'échapperas pas.
                      voici un extrait d'un code (atxmega) de gestion d'un buffer circulaire de 512 octets aligné sur une adresse en 210, juste la partie réception (exemple pas bien optimisé mais osef) qui utilise des index. (l'utilisation des index est, dans certains cas, mieux optimisable par le compilateur que par pointeur. C'est l'expérience et l'étude du code généré qui le dit, mais ça tu devrais y arriver avec ta connaissance de l'asm)

                      #define DISABLE_ALL_IT        __asm__ __volatile__ ("cli" ::)
                      #define ENABLE_ALL_IT         __asm__ __volatile__ ("sei" ::)
                      
                      #define RX_BUFFER_SIZE        512
                      #define RX_BUFFER_MASK        (RX_BUFFER_SIZE - 1)
                      
                      volatile u16   rxReadIndex;
                      volatile u16   rxWriteIndex;
                      volatile u16   rxAvailableLength;
                      volatile u8    rxBuffer[RX_BUFFER_SIZE] __attribute__ ((aligned (10)));
                      
                      ISR(USARTC0_RXC_vect)
                      {
                         ... Verification of errors(etc.) before storing data!
                         // Store received data
                         while((USARTC0.STATUS & USART_RXCIF_bm) != 0)
                         {
                            rxBuffer[rxWriteIndex++] = USARTC0.DATA;
                            rxWriteIndex &= RX_BUFFER_MASK;
                            rxAvailableLength++;
                         }
                      }
                      
                      u8 getReceivedData (u8* pDst)
                      {
                         u16 i;
                         u16 availableLength;
                      
                         DISABLE_ALL_IT;
                         rxAvailableLength = availableLength;
                         ENABLE_ALL_IT;
                      
                         for (i=0; i<availableLength; i++)
                         {
                            *pDst++ = rxBuffer[rxReadIndex++];
                            rxReadIndex &= RX_BUFFER_MASK;
                         }
                         DISABLE_ALL_IT;
                         rxAvailableLength = 0;
                         ENABLE_ALL_IT;
                      
                         return (u8)i;
                      }
                      
                      etc...

                      C'est clair que de passer de l'assembleur au C n'est pas simple car il y a pleins de subtilités. Ensuite ça va tout seul lorsqu'on connait le fonctionnement du compilateur en C. Ca met du temps…
                      a2g

                      • [^] # Re: E_NOENOUGH_INFO | E_UNNEEDED

                        Posté par  . Évalué à 2.

                        en fait à l'origine, je voulais éviter de désactiver/activer les it dans ma boucle principale pour éviter que ça "bagotte" et de perdre des interruptions, mais en m'arrangeant pour "endormir" mon processeur dans la boucle principale, ça devrait limiter la casse.

                      • [^] # Re: E_NOENOUGH_INFO | E_UNNEEDED

                        Posté par  . Évalué à 2.

                        Ok. C'est plus simple de raisonner sur un code fonctionnel :-)

                        Pourquoi désactiver les interruptions dans getreceiveddata ?

                        Avec mon algorithme 1) copier tail dans une locale 2) calculer sz/freesz; il n'y a pas de problèmes vu que head n'est jamais touché par l'interruption. En gros à mes deux questions tu réponds: "je ne sais pas, j'empêche au cas où…".

                        Pour désactiver les interruptions dans le gestionnaire d'interruption tu as sans-doutes raison d'insister sur ce point, j'imagine qu'un avr est trop petit pour avoir un interrupt controller qui gère les priorités entre les interruptions. Mais cela dépend vraiment de la platforme et de comment qu'on programme l'IC, ça n'est pas toujours nécessaire.

                        À part ça, merci pour partager ton code. J'aime bien l'idée d'utiliser un masque pour gèrer le clamping, peut-être qu'il serait encore plus malin de le définir en puissance de 2 (2 << POWER) pour éviter une bête erreur, fin bon ça c'est du détail…

                        • [^] # Re: E_NOENOUGH_INFO | E_UNNEEDED

                          Posté par  . Évalué à 2. Dernière modification le 15 janvier 2016 à 17:09.

                          Pourquoi désactiver les interruptions dans getreceiveddata ?

                          Ok lu ton message plus bàs, donc ce serait parce que c'est une quantité de 16 bits sur un micro 8 (en plus du fait que tes 2 routines modifient la mêm variable)? Admettons mon algorithme, est-ce vraiment un problème ? Par exemple si on écrit d'abord les bits de poids faibles, et comme la valeur est monotonique, on ne risque jamais d'avoir un "tail" qui pointe trop loin. Au pire on peut peut-être tomber sur un tail < head, ce qui peut facilement être détecté par main. Ça me semble bon, non ? Ou me tromperais-je ?

                          • [^] # Re: E_NOENOUGH_INFO | E_UNNEEDED

                            Posté par  . Évalué à 1.

                            Ok j'ai fini de lire ton sus-cité message dans son intégralité :-P Merci j'ai eu ma réponse et je me coucherai moins ignorant ce soir!

                          • [^] # Re: E_NOENOUGH_INFO | E_UNNEEDED

                            Posté par  . Évalué à 2.

                            Au pire on peut peut-être tomber sur un tail < head, ce qui peut facilement être détecté par main.

                            Je me re-réponds à moi-même: dans ce cas ça ne marche évidemment pas car on utilise tail < head pour détecter un wrap… Désolé pour le bruit.

                        • [^] # Re: E_NOENOUGH_INFO | E_UNNEEDED

                          Posté par  . Évalué à 2.

                          Pour désactiver les interruptions dans le gestionnaire d'interruption tu as sans-doutes raison d'insister sur ce point, j'imagine qu'un avr est trop petit pour avoir un interrupt controller qui gère les priorités entre les interruptions.

                          D'après la doc AVR :
                          When an interrupt occurs, the Global Interrupt Enable I-bit is cleared and all interrupts are disabled. The user software can write logic one to the I-bit to enable nested interrupts. All enabled interrupts can then interrupt the current interrupt routine. The I-bit is automatically set when a Return from Interrupt instruction – RETI – is executed.

                          Les It sont désactivées si tu ne les réactives pas lors du traitement des It.

                          • [^] # Re: E_NOENOUGH_INFO | E_UNNEEDED

                            Posté par  . Évalué à 2.

                            En effet, il faut sauvegarder le SREG avant de désactiver les IT pour ne pas perdre le SREG
                            C'est un exemple à la RACHE. (j'ai même introduit un bug: c'est align(512) pour l'alignement du buffer)
                            sur les softs que j'ai en production actuellement, je n'ai que des buffers de 256bytes pour les AVR, donc moins de données en format 16bits: ce code est extrait d'un prototype qui fonctionne très bien.

                            sur les uC qui gèrent plus que l'interruption UART, j'utilise plutôt des macros du genre:

                            #define AVR_ENTER_CRITICAL_REGION()    volatile u8 saved_sreg = SREG;IT_ALL_DISABLE;
                            #define AVR_LEAVE_CRITICAL_REGION()    SREG = saved_sreg;

                            Je conseille de ne surtout pas réactiver les interruptions dans les routines d'interruption en utilisant ces macros si on ne sait pas gérer les interruptions imbriqués ("nested") et attention à l'explosion de la pile avec la réentrance… etc.

                            a2g

                            • [^] # Re: E_NOENOUGH_INFO | E_UNNEEDED

                              Posté par  . Évalué à 2.

                              En effet, il faut sauvegarder le SREG avant de désactiver les IT pour ne pas perdre le SREG

                              Soit j'ai mal compris, soit sreg est sauvegardé par le hard.

                              Je conseille de ne surtout pas réactiver les interruptions dans les routines d'interruption en utilisant ces macros si on ne sait pas gérer les interruptions imbriqués ("nested") et attention à l'explosion de la pile avec la réentrance… etc

                              Je ne compte pas non plus le faire, c'est déjà assez compliqué comme ça.

                              • [^] # Re: E_NOENOUGH_INFO | E_UNNEEDED

                                Posté par  . Évalué à 2.

                                J'ai vérifié, oui, SREG est sauvegardé.
                                J'ai jeté un coup d'oeil là où je l'ai utilisé: c'est parce que SREG est modifié par du code entre l'entrée et la sortie de la région critique et qu'en sortant de cette région, je voulais récupérer l'ancien SREG (cas particulier)

                    • [^] # Re: E_NOENOUGH_INFO | E_UNNEEDED

                      Posté par  . Évalué à 1.

                      Ton alogithme me semble de loin similaire à ce que j'ai écris, il serait bien que tu l'écrive en formules et que tu le simplifie en suite. Le problème c'est que tu parles d'index, de status, etc. et au final tu te retrouve avec 4 variables écrites par les deux routines, ce qui est une recette pour un désastres et qui t'oblige à suspendre les interruptions (ce qui cf. les messages de a2g n'est sans doutes pas une mauvaise idée).

                      Au final, ton "index" vaut head pour le lecteur et tail pour l'écriveur… sz (== la taillle à lire) est calculée justement en faisant la différence des deux (ou en prennant le cas du wrap). Le lecteur se sert de tail pour avoir une estimmation de jusque où il peut lire, et tail utilise head pour savoir jusqu'où il peut écrire… Bref 2 variables, chacunes écrites une fois par chaque routine. Pas besoin de statuts vide, plein, demi-vide ou demi-plein ;-)

                      • [^] # Re: E_NOENOUGH_INFO | E_UNNEEDED

                        Posté par  . Évalué à 2.

                        Ton alogithme me semble de loin similaire à ce que j'ai écris, il serait bien que tu l'écrive en formules et que tu le simplifie en suite.

                        Comme je l'ai dit c'est un truc "simpliste" que j'ai écrit à la rache vite fait le soir tard, c'est d'ailleurs pour ça que je n'en a vais pas vraiment parlé au début : je sais que je dois y revenir. Ce n'était pas l'objet initial de mon post, mais j'ai tenu à en parler histoire de clarifier certaines choses, notamment le contexte dans lequel je lis/écris.

                        Le problème c'est que tu parles d'index, de status, etc. et au final tu te retrouve avec 4 variables écrites par les deux routines, ce qui est une recette pour un désastres et qui t'oblige à suspendre les interruptions

                        3 variables modifiées par 2 routines : head, tail et status.

                        Pas besoin de statuts vide, plein, demi-vide ou demi-plein ;-)

                        Je t'avoue que celle-là j'en ai un peu honte : comme tu dis il faut que je le reprenne. Je me doutais que je pourrais m'en passer, mais la flemme de réfléchir sur ça lorsque j'ai codé mon truc (a 1h du matin on a pas toujours les idées très claires …). J'ai donc fait un truc "vite fait" parce que je voulais tester un autre truc qui avait besoin d'un buffer de ce genre.

              • [^] # Re: E_NOENOUGH_INFO | E_UNNEEDED

                Posté par  . Évalué à 2.

                Salut a2g,

                Merci pour ton input et tes idées.

                Il y a-t-il un risque que main utilise une mauvaise valeur de tail ? Idem, non je ne crois pas.

                Et bien si!

                Je crois que tu souffres du même problème que j'ai à lire les messages dans leur intégralité: deux lignes plus loin j'écris exactement la même chose que toi concernant sz/freesz avec en substance le même message que tu aides à faire passer du coup, parfait ;-)

                Ici j'évoquais le problème que main pourrait avoir à avoir une valeur de tail moins actuelle: donc soit une estimmation trop petite du nombres des données, soit ne pas détecter un wrapping qui sont tous deux sans conséquence.

                L'objet de mon message étant quand même d'insister sur le fait que mettre volatile à tout bout de champs n'est certainement pas une bonne idée… Bref, pas de volatile du tout avec un code proprement découpé en fonctions couplé d'une inspection de l'assembleur généré est une possibilité tout à fait viable et bug-free !

                • [^] # Re: E_NOENOUGH_INFO | E_UNNEEDED

                  Posté par  . Évalué à 2.

                  [HS]
                  Après avoir eu des problèmes d'optimisation sur du code pour de l'AVR qui allait partir en production il y a bien longtemps, je ne mets dorénavent que des "volatile" pour le code AVR à moins que je ne sache exactement comme le code est optimisé: je préfère utliser d'autres astuces d'optimisation et perdre quelques instructions et coups de clock plutôt que de partir faire le tour du monde pour mettre à jour les produits déployés.
                  Par contre, ayant commencé ma carrière avec de l'assembleur dsp, j'ai gardé l'habitude de maîtriser entièrement toutes les variables, leur utilisation, leur optimisation, le mapping, les garder en "privé", les définir en variables locales ou en registre si je considère qu'elle peut être optimisée.

                  -> celà ne me choque donc pas, l'utilisation des "volatile" quand le code est bien architecturé, bien "modularisé" et bien écrit.
                  [/HS]
                  a2g

            • [^] # Re: E_NOENOUGH_INFO | E_UNNEEDED

              Posté par  . Évalué à 2.

              Quelques remarques en attendant que tes algos soit spécifiés et que l'on puisse te donner une réponse précise.

              si ton interruption ne se fait pas interrompre, un volatile dans le code de l'interruption ne sert effectivement à rien.

              Effectivement, vu que la routine d'it est censée sauvegarder le contexte d'exécution du programme interrompu.

              un volatile dans le code "main" peut-être utile mais certainement pas suffisante (enfin faut voir ton algo toujours). Je conjecture que tu auras quand même besoin d'implémenter un mécanisme de type mutex pour te prémunir des problèmes de TOCTOU.

              C'est pour ça que je n'ai pas encore spécifié mon algo: je "sens" qu'il y a un truc à faire à ce niveau (d'ailleurs tu as mis le doigt dessus, voir plus bas).

              mettre volatile est souvent une fausse bonne idée. Je te laisse rechercher les références de Linus sur la LKML ;-) La démonstration suit…

              Je pars du principe que volatile est utile, mais pas suffisant. si j'ai bien compris, volatile sert à "forcer" le compilateur à insérer une instruction de relecture de la valeur depuis la mémoire (pour des questions d'optimisation, le compilateur ne force pas la relecture de la valeur lorsque celle-ci est déjà dans un des registres du processeur).
              Ca a son utilité lorsqu'une valeur est susceptible d'être écrite par un autre thread entre deux (ou plus de deux ) lectures successives (ou une interruption). Par contre ça ne règle pas les problèmes d'accès concurrents à une valeur, problème résolus par les mécanismes de mutex. L'un ne remplace pas l'autre, les deux ont une utilité différente.

              Admettons que ton main ressemble à cela:

              Ca y ressemble, au détail près que j'ai déporté la gestiuon du buffer dans des fonctions, appeléers par le main ou les routines d'IT.

              Au niveu de l'interruption:

              Tu en es pas très loin, au détail près que je ne calcule pas de taille, je me contente juste de comparer les index head et tail : s'ils ont la même valeur, ça signifie que le buffer est plein.

              NB: c'est une implémentation naïve d'un buffer circulaire. Je n'ai jamais fait ce genre de chose, je te conseille quand même de consulter wikipedia avant ;-)

              Mon implémentation n'est pas non plus très subtile non plus (j'ai fait au plus simple). Mais je peux (et devrai certainement) la modifier un peu (une idée m'est venu suite à vos remarques).

              Donc nous avons ton interruption qui modifie tail et ton main qui modifie head.

              La conclusion est bonne : l'interruption remplit, donc modifie le tail, et le main (et/ou une it) vide, donc modifie le head.

              Il y a-t-il un risque que l'interruption utilise une mauvaise valeur pour head ? C'est-à-dire entre le momen où tu calcules freesz et le moment où tu modifie tail.

              Comme je ne calcule pas (pour le moment) la taille, pas de risque.

              Il y a-t-il un risque que main utilise une mauvaise valeur de tail ? Idem, non je ne crois pas.

              Là par contre c'est peut-être possible. Le main est un classique : une boucle sans fin qui fait des trucs à chaquie itération. Comme je ne calcule pas de taille de buffer lors du remplissage, c'est peuit-être là que je devrais le calculer (pour déterminer le taux de remplissage de mon buffer et déclencher le transfert). Mais je pourrais simplifier en déclenchant le transfert dès que le buffer n'est pas vide.

              Le problème se situe au moment où l'on calcule sz et freesz !!

              Donc bref: tu copies tes volatiles dans des variables locales au début de ta routine pour être certain que tu n'accède pas à tes variables en ram directement.

              Ca me parait une bonne idée.

              Ton algo doit être conçu pour fonctionner sur des valeurs qui ne reflètent peut-être plus la réalité mais qui son cohérentes entre elles, pas pour fonctionner avec des valeurs certes actuelles mais qui changent au gré du vent/des lignes!!!

              Ca me parait logique. Pour moi, le seul moment ou j'ai besoin d'une représentation réelle est lorsque le buffer est vide ou lorsqu'il est plein.

              que se passerait-il si tail est accèdée en volatile et qu'une interruption fait wrapper tail et survient juste après que tu aies testé qu'il n'y a pas de wrap ? Oops un freesz < 0… ça sent pas bon hein… ;-)

              C'est justement pour ça que je n'ai pas encore spécifié définitivement mon algo : j'avais senti cette mauvaise odeur de loin, mais pas eu le temps d'approfondir et de détailler la réflexion.

              (Ma) conclusion :

              pas besoin de mutex,

              Je suis d'accord sur ce point.

              surtout pas besoin de volatile, sous réserve que le code "main" soit dans une fonction séparée

              Là je suis moins d'accord (en fait ça dépendra de mes choix) : je peux en avoir besoin si je calcule le taux d'occupation de mon buffer dans le main. Mais je crois que je vais éviter …

              Maintenant si tu passes à plusieurs producteurs/consommateurs, ben ça se complique : ne le fais pas, utilise plusieurs buffers :p

              Je ne compte pas le faire.

              PS: je viens de me rendre compte que c'est l'inverse que tu veux faire, soit vider un buffer par une interruption. Cela revient au même.

              Pas encore décidé : Ce qui est sur c'est que je remplis via une interruption. Par contre pour vider je me tate : soit je vide dans le main, soit je vide via l'interruption UART. L'avantage de l'interruption, c'est que je suis sur qu'elle ne sera pas interrompue durant son exécution. L'inconvénient : un peu plus de taf.

              en tout cas merci pour ces indications qui me feront avancer. J'ai une ou deux idées que je vous présenterai si ça vous intéresse.

              • [^] # Re: E_NOENOUGH_INFO | E_UNNEEDED

                Posté par  . Évalué à 2.

                en tout cas merci pour ces indications qui me feront avancer. J'ai une ou deux idées que je vous présenterai si ça vous intéresse.

                Tout à fait! Merci d'avance :)

  • # volatile et interruptions

    Posté par  . Évalué à 3.

    L'utilisation du mot clé "volatile" est indispensable à maîtriser lorsqu'on écrit du code embarqué: elle permet à l'utilisateur d'éviter que le compilateur, lorsqu'utilisé avec les optimisations qui vont bien n'en fasse trop. L'exemple le plus courant est le polling sur une pin d'entrée:
    si pin_b0 est le nom de la "variable" qui permet d'accéder à la pin 0 du port B du uC, alors, sans le mot clé "volatile", le code suivant qui permet de détecter le passage à 1 de cette entrée a pour conséquence de faire "disparaitre" la boucle while et de se faire rick-roller et c'est surtout pas ça ce qu'on veut:

    // Code "RACHE certified" before while loop
    isRisingEdgeDetected = false
    while(!pin_b0)
    {
       isRisingEdgeDetected = true
    }
    // Code "RACHE certified" after while loop
    if(!isRisingEdgeDetected)
    {
       // Listen to Rick Roll/Never Gonna Give You Up!
    }

    Jeter un coup d'oeil à sfr_defs.h, io.h (etc.) pour la définition des accès mémoires des pins du uC de type AVR. Pour définir une variable volatile de type "la structure que tu veux définir":

    typedef struct S_MASTRUCTURE t_mastructure;
    #ifndef _S_MASTRUCTURE
    struct S_MASTRUCTURE
    {
      u8*  pBuffer;
      u16  aWord;
      u8   aByte;
    };
    #endif
    volatile t_mastructure maVariableDeTypeMastructure;

    Pour l'embarqué tel que pour un AVR, lorsqu'on veut modifier une variable par le code principal et par une interruption, il faut désactiver les interruptions dans le code principal lorsqu'on y accède.
    exemple, pour modifier aWord par le code principal ET par l'interruption pour un uC de type AVR:

    ISR(USARTC0_RXC_vect)
    {
       // Modify a "shared value"
       maVariableDeTypeMastructure.aWord++;
    }
    
    int main (void)
    {
       // Code "RACHE certified"
       ...
    
       for(;;)
       {
          // Deactivate interruptions
          __asm__ __volatile__ ("cli" ::)
          // Modify a shared variable
          maVariableDeTypeMastructure.aWord+=2;
          // Reactivate interruptions
          __asm__ __volatile__ ("sei" ::)
          // Fantastic code
          ...
       }
       return 0;
    }

    Sans désactiver les interruptions, sur un micro 8bit, on risque l'interruption en plein cours de modification de aWord dans le programme principal et là, c'est le drame!
    Et même si la variable est de type 8bit, parce qu'entre le moment où la variable va être chargée dans un registre et le moment où le registre va être modifié et le moment où il va être réécrit dans la mémoire, il y a des malchances qu'une interruption claque et qu'à la fin, la variable se retrouve avec une valeur totalement indéterminée. Je te laisse imaginer le cas où cette variable est de type pointeur, à côté des dégats que celà peut engendrer, tu préfèrerais être rickrollé!

    Mutex, semaphore et autres mécanismes du même type sont des implémentations pour langage "haut-niveau" (os, multithread, etc.) pour éviter ces problèmes, mais la base est ci-dessus!

    a2g

Suivre le flux des commentaires

Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.