Question:
Implémentation I2C non bloquante sur STM32
Alexey Malev
2016-10-24 14:22:05 UTC
view on stackexchange narkive permalink

La plupart des articles que je suis capable de trouver qui sont essentiellement "I2C pour les nuls sur le microcontrôleur XXX" suggèrent de bloquer le CPU en attendant les événements de confirmation.Pour exemple, (les commentaires et la description sont en russe, mais cela n'a pas d'importance).Voici l'exemple de "blocage" dont je parle:

  I2C_GenerateSTART (HMC5883L_I2C, ENABLE);
/ * attendre la confirmation * /
while (! I2C_CheckEvent (HMC5883L_I2C, I2C_EVENT_MASTER_MODE_SELECT));
 

Ces boucles while sont là après chaque opération, essentiellement.Enfin, ma question est la suivante: comment implémenter I2C sans «accrocher» MCU avec ces boucles while?À un niveau très élevé, je comprends que je dois utiliser des interruptions au lieu de whiles, mais je ne peux pas trouver d'exemple de code.Pouvez-vous m'aider s'il vous plaît?

Notez que vous pouvez également utiliser DMA avec I²C pour gagner en performances et réduire le nombre d'interruptions à gérer.
@AlexeyMalev Voici un exemple d'implémentation I2C non bloquante pour AVR (bien que des principes similaires s'appliquent pour STM32): https://github.com/scttnlsn/avr-twi Cette implémentation particulière utilise des interruptions pour gérer une machine d'état et appeler une fonction de rappel lorsquela transmission est terminée.
Cinq réponses:
jonk
2016-10-24 14:46:21 UTC
view on stackexchange narkive permalink

Bien sûr.

  1. Configurez vos pilotes en termes de paires supérieure et inférieure , séparées par un tampon partagé. La fonction lower est un bit de code piloté par interruption qui répond aux événements d'interruption, s'occupe du travail immédiat nécessaire pour servir le matériel et ajoute des données dans le tampon partagé (s'il s'agit d'un récepteur) ou extrait le bit suivant de données du tampon partagé pour continuer à entretenir le matériel (s'il s'agit d'un émetteur.) La fonction upper est appelée par votre code normal et accepte les données à ajouter à un tampon sortant ou else vérifie et extrait les données du tampon entrant. En associant des éléments comme celui-ci et en fournissant une mémoire tampon adéquate pour vos besoins, cet arrangement peut fonctionner toute la journée sans problème et il dissocie votre code principal de votre maintenance matérielle.
  2. Utilisez une machine d'état dans votre pilote d'interruption. Chaque interruption lit l'état actuel, traite une étape, puis passe à un état différent ou reste dans le même état, puis quitte. Cela peut être aussi complexe ou aussi simple que vous en avez besoin. Souvent, cela est provoqué par un événement de minuterie. Mais il ne doit pas en être ainsi.
  3. Créez un système d'exploitation coopératif. Cela peut être aussi simple que de configurer quelques petites piles allouées à l'aide de malloc () et de permettre aux fonctions d'appeler en coopération une fonction switch () lorsqu'elles ont terminé avec une tâche immédiate pour le moment. Vous configurez un thread distinct pour votre fonction I2C, mais lorsqu'il décide qu'il n'y a rien à faire pour le moment, il appelle simplement switch () pour provoquer un changement de pile vers un autre thread. Cela peut être fait à tour de rôle jusqu'à ce qu'il revienne et revienne de l'appel switch () que vous avez effectué. Ensuite, vous revenez à votre condition while, qui vérifie à nouveau. S'il n'y a toujours rien à faire, les appels while basculent à nouveau (). Etc. De cette façon, votre code n'est pas beaucoup plus complexe à maintenir et il est simple d'insérer des appels switch () partout où vous en ressentez le besoin. Il n'est pas nécessaire non plus de s'inquiéter de la préemption des fonctions de bibliothèque, puisque vous effectuez uniquement la transition entre les piles à une limite d'appel de fonction, il est donc impossible pour une fonction de bibliothèque d'être interrompue. Cela rend la mise en œuvre très simple.
  4. Fils de discussion préemptifs. C'est un peu comme # 3 sauf qu'il n'est pas nécessaire d'appeler la fonction switch (). La préemption a lieu sur la base d'une minuterie, ou un thread peut également choisir librement de libérer son heure. La difficulté ici est de traiter la préemption des routines de bibliothèque et / ou du matériel spécialisé où il peut y avoir des séquences d'instructions spécifiques générées par le compilateur qui ne peuvent pas être interrompues (E / S dos à dos qui doivent récupérer un octet haut suivi d'un octet de poids faible de la même adresse mappée en mémoire, par exemple.)

Je pense cependant que les deux premières options sont probablement vos meilleurs paris. Cependant, beaucoup dépend de combien vous dépendez du code de bibliothèque écrit par d'autres. Il est fort possible que leur code de bibliothèque ne soit pas conçu pour être divisé en composants de niveau supérieur et inférieur. Et il est également fort possible que vous ne puissiez pas les appeler en fonction d'événements de minuterie ou d'autres événements basés sur une machine d'état.

J'ai tendance à supposer que si je n'aime pas la façon dont la bibliothèque fait le travail (boucles d'attente occupées uniquement), alors je suis coincé à écrire mon propre code pour faire le travail.Ce qui signifie que je suis libre d'utiliser l'une des méthodes ci-dessus.

Mais vous devez examiner de près le code de la bibliothèque et voir s'il a déjà des fonctionnalités qui vous permettent d'éviter le comportement d'attente occupé.Il est possible que la bibliothèque prenne en charge quelque chose de différent.C'est donc le premier endroit à vérifier, juste au cas où.Sinon, jouez avec l'idée d'utiliser les fonctions existantes dans le cadre d'un partage de pilotes supérieur / inférieur ou bien comme processus piloté par une machine à états.Il est possible que vous puissiez également résoudre ce problème.

Je n'ai pas d'autres suggestions qui me viennent rapidement à l'esprit, pour le moment.

S'il vous plaît, corrigez-moi si j'ai mal l'idée du n ° 1 - supposons que j'ai un code qui lit certaines données du bus I2C et fait des calculs lourds, par exemple calcule factorielle d'un certain nombre.Comme je n'ai pas de threads ici, mon code principal qui calcule la factorielle devrait parfois "mettre en pause" (au lieu de céder à interrompre le thread de gestion dans un langage de haut niveau) et vérifier s'il y a des données dans le tampon que vous avez mentionné, ajouté là par le gestionnaire d'interruption?
@AlexeyMalev Ce code factoriel devrait probablement être dans votre fonction principale, qui appelle le code de niveau _upper_.Le code de niveau inférieur ne fait pas de choses qui nécessitent une pause au milieu.Si plus de choses se produisent pendant que votre code principal appelle le code _upper_ ou le traite après l'avoir appelé, l'interruption sera toujours traitée et les valeurs dans le tampon seront stockées pour vous.Cela ne s'arrête pas même si vous faites des calculs.Il n'est pas nécessaire de rechercher davantage de données en mémoire tampon si vous n'avez pas terminé l'autre travail.Si vous n'arrivez pas à suivre, vous avez quand même un problème.
@AlexeyMalev Le travail de calcul le plus long devrait être dans votre code principal.Si vous avez des bits plus courts à faire entre les temps, vous pouvez les configurer sur des événements de minuterie qui interrompent votre long code de calcul pour effectuer leurs tâches courtes.Ces tâches courtes chronométrées peuvent appeler la moitié du niveau _upper_ de n'importe quel pilote pour obtenir également des données tamponnées.C'est juste une question de tracer un chronogramme.Laissez suffisamment de temps entre les étapes pour que les calculs les plus longs soient effectués, ainsi que tous les temps d'interruption.C'est votre processus principal.Cependant, vous pouvez utiliser mon option n ° 3 et saler votre code principal avec des appels switch ().
Désolé, mais je dois voter contre.Cette longue réponse ne semble pas fournir d'informations utiles sur * comment * implémenter un statemachine I²C piloté par interruption sur un STM32.
La question était - comment implémenter l'interaction i2c non bloquante, j'ai mentionné les interruptions comme une option.Basé sur l'interruption dans une seule des approches possibles.
@AlexeyMalev Je ne connais pas spécifiquement l'environnement STM32, mais j'ai essayé de fournir des réponses générales qui s'appliquent généralement, ne sont pas bloquantes, et je l'ai fait tout de suite.
@jonk Nono, je faisais référence au commentaire de JimmyB, peu importe :)
followed Monica to Codidact
2016-10-24 14:52:41 UTC
view on stackexchange narkive permalink

Il existe des exemples dans les bibliothèques STM32Cube .Obtenez celui qui convient à votre famille de contrôleurs (par exemple STM32CubeF4 ou STM32CubeL1 ), et recherchez Exemples / I2C / I2C_TwoBoards_ComDMA dans les Projets sous-répertoire.

AnoE
2016-10-24 18:44:59 UTC
view on stackexchange narkive permalink

Raison

Eh bien, la raison est simple: le blocage est simple et à première vue, il semble fonctionner. Malheur à vous si vous voulez faire autre chose en attendant.

Donc, sans entrer dans beaucoup de détails, comme je ne connais pas le STM32, vous pouvez généralement vous sortir de ce problème de deux manières, en fonction de vos besoins.

  I2C_GenerateSTART (HMC5883L_I2C, ENABLE);
/ * attendre la confirmation * /
while (! I2C_CheckEvent (HMC5883L_I2C, I2C_EVENT_MASTER_MODE_SELECT));
 

Conversion en mode non bloquant

Soit vous implémentez un timeout pour toutes vos boucles while . Cela signifie:

  I2C_GenerateSTART (HMC5883L_I2C, ENABLE);
/ * attendre la confirmation * /
static unsigned long start = now ();
while (! I2C_CheckEvent (HMC5883L_I2C, I2C_EVENT_MASTER_MODE_SELECT) && now () - démarrer < TIMEOUT);
if (now () - start > = TIMEOUT) {return ERROR_TIMEOUT; }
 

(Il s'agit bien sûr d'un pseudocode, vous voyez l'idée. N'hésitez pas à optimiser ou à ajuster vos préférences de codage si nécessaire.)

Vous devez vérifier les codes de retour lorsque vous voyagez dans la pile et choisissez le bon endroit où vous effectuez votre gestion du délai d'attente. Notez qu'il est utile de définir également une variable globale i2c_timeout_occured = 1 ou autre afin que vous puissiez rapidement abandonner d'autres appels I2C sans avoir à passer trop d'arguments.

Ce changement est plutôt indolore, espérons-le.

À l'envers

Si, à la place, vous avez vraiment besoin de faire un autre traitement pendant que vous attendez cet événement, alors vous devez vous débarrasser complètement de la boucle while interne. Vous faites cela comme ceci:

  void main_loop () {
   do_i2c_stuff (); // ne doit jamais bloquer
   do_other_stuff ();
   ...
}

// Ne doit jamais bloquer. En supposant que toutes les fonctions I2C _... ne bloquent pas non plus.
void do_i2c_stuff () {
  état int statique = ...;

  if (état == 0) {
    I2C_GenerateSTART (HMC5883L_I2C, ENABLE);
    état = 1;
  } else if (état == 1) {
    if (I2C_CheckEvent (HMC5883L_I2C, I2C_EVENT_MASTER_MODE_SELECT))
      état = 2;
} autre ...
}
 

Ce n'est pas forcément très compliqué, selon votre autre logique. Vous pouvez faire beaucoup avec une mise en retrait / des commentaires / un formatage appropriés pour ne pas perdre de vue ce que vous programmez.

La façon dont cela fonctionne est de créer une machine à états . Si vous regardez votre code d'origine, il ressemble à ceci:

  code non bloquant
while (! nonblocking_function_call1 ());
code non bloquant
while (! nonblocking_function_call2 ());
 

Pour transformer cela en une machine à états, vous avez un état pour chacun:

  état 0: code non bloquant
état 1: nonblocking_function_call1 ()
état 2: code non bloquant
état 3: nonblocking_function_call2 ()
 

Ensuite, comme indiqué dans l'exemple ci-dessus, vous appelez ce code dans une boucle sans fin (votre boucle principale), et n'exécutez que le code correspondant à votre état actuel (suivi dans une variable statique state ) . Le code non bloquant est trivial, il est inchangé par rapport à avant. Le code de blocage est remplacé par une variante qui ne bloque pas, mais ne met à jour que l ' état lorsqu'il est terminé.

Notez que les boucles individuelles while ont disparu; vous les avez remplacées par le fait que vous avez de toute façon votre boucle principale de niveau supérieur, qui appelle votre machine d'état à plusieurs reprises.

Cette solution peut être pénible lorsque vous avez beaucoup de code hérité car vous ne pouvez pas simplement adapter la fonction de blocage la plus interne, comme dans la première solution. Il brille lorsque vous commencez à écrire du nouveau code et que vous procédez de cette façon depuis le début. Combinez-le avec beaucoup d'autres choses qu'un µC pourrait faire (par exemple, attendre que vous appuyiez sur un bouton, etc.); si vous vous habituez à faire de cette façon tout le temps, vous obtenez gratuitement des capacités multitâches arbitraires.

Interruptions

Franchement, pour quelque chose comme ça (c'est-à-dire, simplement me débarrasser du blocage infini), je ferais de mon mieux pour rester à l'écart des interruptions à moins que vous n'ayez des besoins de timing extrêmes.Les interruptions rendent les choses compliquées, rapides, vous n'en avez peut-être pas assez de toute façon, et cela se résumera de toute façon à un code assez similaire, car vous ne voulez pas faire beaucoup plus à l'intérieur de l'interruption, sauf définir quelques indicateurs.

belle solution (sous-optimale) ici - je pensais qu'une sorte d'interruption serait toujours nécessaire.
L'idée était de se débarrasser des boucles «while».J'ai une question sur votre dernier exemple de code - je ne sais pas ce que vous proposez exactement.Le problème est - `I2C_CheckEvent` renvoie` true` après un certain temps et ne bloque pas, ce qui signifie qu'en général il ne suffit pas de l'appeler une fois, ce que vous suggérez (comme je l'ai compris).Pouvez-vous expliquer un peu cela?
@AlexeyMalev, mes solutions se débarrassent des boucles infinies `while`.Le premier les a toujours mais ajoute un délai d'attente.Le second s'en débarrasse complètement (en supposant que vous ayez une boucle de niveau supérieur qui s'exécute en boucle perpétuelle, de toute façon).J'ai ajouté quelques explications, comme demandé.i2C_CheckEvent * est * souvent appelé, mais votre méthode retourne immédiatement, quel que soit le résultat, donnant ainsi aux autres parties de votre programme le temps de s'exécuter également.
Ce n'est pas "non bloquant".Non bloquant signifie que votre CPU se met en veille lorsqu'aucun travail n'est à faire.Cela se fait généralement en utilisant un planificateur permettant à la tâche de dormir sur un sémaphore au lieu de bloquer le processeur dans une boucle while.Lorsque i2c se produit même, la routine d'interruption donne le sémaphore et la tâche qui attendait le sémaphore se réveille et s'exécute à nouveau.CECI est non bloquant.Les solutions décrites dans cet article ne le sont pas.
@Martin, il existe différents aspects du blocage.Ma réponse reflète ma compréhension de la question, qui demande comment éviter de bloquer le flux du programme lors de la saisie d'un appel i2c.OP décrit une variante bloquante, et ma solution décrite est la non bloquante correspondante.Ma solution résout le problème immédiat et est simple à généraliser pour interrompre ou sémaphore si cela est nécessaire.
Sauf que votre première solution n'est pas non bloquante et que votre deuxième solution REQUIERT 100% d'utilisation du processeur pendant qu'une opération i2c est en cours.Les deux solutions ne conviennent donc à aucun code de niveau de production sérieux.
@Martin, Je pense que ma réponse est suffisamment complète pour que le lecteur voie ce que je veux en venir.Je ne veux pas dire que ma solution résout tous les problèmes du monde.Je ne vois rien sur l'économie de puissance du processeur dans la question.Mon approche résout un problème spécifique (comme indiqué) - comment entrelacer ce type de communication avec d'autres traitements.Si vous préférez une solution différente, n'hésitez pas à en rédiger une.Si vous avez des améliorations concrètes sur mon approche qui ne correspondent pas à une réécriture complète, n'hésitez pas à commenter et je vais en tenir compte.
Bence Kaulics
2016-10-24 19:28:22 UTC
view on stackexchange narkive permalink

Voici une liste des requêtes d'interruption \ $ \ small I ^ 2C \ $ disponibles sur un STM32.Comme je ne connais pas la puce exacte que vous utilisez, il est recommandé de vérifier votre manuel de référence s'il y a une différence, mais je doute qu'il y en ait.

enter image description here

Pour rester à votre exemple de code, si une version non bloquante est nécessaire, vous devez activer le bit Start envoyé (Master) événement avec le bit de contrôle ITEVFEN et vérifier le SB indicateur d'événement à l'intérieur de l'ISR.

JimmyB
2016-10-24 20:55:53 UTC
view on stackexchange narkive permalink

En considérant l'extrait de @BenceKaulics d'une fiche technique, le pseudo-code d'une routine de service d'interruption (ISR) pourrait ressembler à ceci:

  i2c_event_isr () {
  commutateur (i2c_event) {

    case master_start_bit_sent:

      send_address (...);
      Pause;

    case master_address_sent:
    case data_byte_finished:

      if (has_more_data ()) {
        send_next_data_byte ();
      } autre {
        send_stop_condition ();
      }

      Pause;
    ...
  }
}
 


Ce Q&R a été automatiquement traduit de la langue anglaise.Le contenu original est disponible sur stackexchange, que nous remercions pour la licence cc by-sa 3.0 sous laquelle il est distribué.
Loading...