Quelle est la manière prévue / correcte de gérer les interruptions et d'utiliser l'instruction cpu WFI risc-v?
Je suis très nouveau dans la programmation bare metal et je n'ai jamais eu d'interruptions auparavant, mais j'ai appris sur une carte de développement RISC-V FE310-G002 alimentée par SOC.
J'ai lu à propos de l'instruction RISC-V WFI (Wait for interruption) et dans les manuels, il ne semble pas que vous puissiez vous y fier pour réellement dormir le cœur. Au lieu de cela, cela suggère seulement que l'exécution peut être interrompue sur le système et que l'instruction devrait être traitée plus comme un NOP. Cependant, cela me semble plutôt inutile. Considérez l'extrait de programme ASM suivant:
wfi_loop:
WFI
J wfi_loop
Cela devrait être fait car on ne peut pas compter sur WFI. Cependant, lors de la MRET du gestionnaire d'interruption, vous seriez toujours pris dans la boucle. Il faudrait donc le rendre conditionnel par rapport à une variable globale dont la valeur est mise à jour dans le gestionnaire d'interruption. Cela semble très compliqué.
De plus, si votre implémentation respecte en fait l'instruction WFI et que l'interruption est déclenchée juste avant l'exécution de l'instruction WFI, le cœur entier se bloquera jusqu'à ce qu'une autre interruption soit déclenchée car elle reviendra avant l'instruction WFI.
Il semble que la seule utilisation correcte de l'instruction serait à l'intérieur d'un ordonnanceur du noyau lorsqu'il n'y a pas de travail à faire. Mais même dans ce cas, je ne pense pas que vous voudriez jamais revenir du gestionnaire d'interruption dans un tel code, mais plutôt redémarrer l'algorithme du planificateur depuis le début. Mais ce serait aussi un problème puisque vous auriez en quelque sorte à faire reculer la pile, etc.
Je continue à tourner en rond avec cela dans ma tête et je n'arrive pas à trouver une utilisation sûre. Peut-être, si vous activez de manière atomique les interruptions avec CSRRS, puis appelez immédiatement WFI comme ceci:
CSRRSI zero, mie, 0x80
wfi_loop:
WFI
J wfi_loop
NOP
NOP
Assurez-vous ensuite d'incrémenter le registre mepc de 8 octets avant d'appeler MRET à partir du gestionnaire d'interruption. L'interruption devrait également être désactivée à nouveau dans le registre mie à l'intérieur du gestionnaire d'interruption avant de revenir. Cette solution ne serait sûre que si WFI, J et NOP sont tous codés en tant qu'instructions de 4 octets, que des instructions compressées soient ou non utilisées. Cela dépend également du compteur de programme atteignant l'instruction WFI avant qu'il soit possible que l'interruption soit déclenchée, après avoir été activée par l'instruction CSRRSI. Cela permettrait alors à l'interruption d'être déclenchée dans un endroit sûr dans le code et de revenir de manière à sortir de la boucle qui l'attendait.
Je suppose que j'essaie simplement de comprendre quel comportement je peux attendre du matériel et, par conséquent, comment appeler et revenir correctement des interruptions et utiliser l'instruction WFI?
Réponses
Il devrait y avoir une tâche / thread / processus qui est pour le ralenti, et cela devrait ressembler à votre premier morceau de code.
Étant donné que le thread inactif est configuré pour avoir la priorité la plus basse, si le thread inactif est en cours d'exécution, cela signifie qu'il n'y a aucun autre thread à exécuter ou que tous les autres threads sont bloqués.
Lorsqu'une interruption se produit qui débloque un autre thread, la routine de service d'interruption doit reprendre ce thread bloqué au lieu du thread inactif interrompu.
Notez qu'un thread qui bloque sur IO est lui aussi interrompu - il est interrompu via sa propre utilisation de ecall
. Cette exception est une demande d'E / S et provoque le blocage de ce thread - elle ne peut pas être reprise tant que la demande d'E / S n'est pas satisfaite.
Ainsi, un thread qui est bloqué sur IO est suspendu comme s'il était interrompu - et une interruption d'horloge ou une interruption IO est capable de reprendre un processus différent de celui immédiatement interrompu, ce qui se produira dans le cas où le processus inactif était en cours d'exécution et un événement qu'un processus attendait se produit.
Ce que je fais, c'est utiliser le scratch
csr pour pointer vers le bloc de contexte pour le processus / thread en cours d'exécution. En cas d'interruption, j'enregistre le moins de registres nécessaires pour (commencer à) traiter l'interruption. Si l'interruption entraîne un autre processus / thread qui devient exécutable, alors lors de la reprise d'une interruption, je vérifie les priorités du processus et je peux choisir un changement de contexte au lieu de reprendre ce qui a été interrompu. Si je reprends ce qui a été interrompu, c'est une restauration rapide. Et pour changer de contexte, je termine d'enregistrer le contexte CPU du thread interrompu, puis je reprends un autre processus / thread, en commutant le scratch
registre.
(Pour les interruptions imbriquées, je n'autorise pas les changements de contexte lors de la reprise, mais lors des interruptions après avoir enregistré le contexte actuel, je configure le scratch
csr sur une pile d'interruptions de blocs de contexte avant de réactiver les interruptions de priorité plus élevée. optimisation, nous pouvons supposer qu'un thread inactif écrit personnalisé n'a besoin de rien d'autre que son ordinateur enregistré / restauré.)
Il faudrait donc le rendre conditionnel par rapport à une variable globale dont la valeur est mise à jour dans le gestionnaire d'interruption.
Vous devez le faire quelle que soit la mise en œuvre de wfi
car vous ne savez pas quel événement a provoqué le réveil du cœur.
Vous pouvez avoir n interruptions activées lors de l'exécution wfi
et l'une d'entre elles peut avoir été déclenchée.
wfi
est une optimisation , cela économise de l'énergie jusqu'à ce que quelque chose se passe. Comme vous l'avez noté, le planificateur du système d'exploitation peut se trouver dans la condition qu'aucun thread n'est planifiable (par exemple, ils attendent tous les E / S ou simplement il n'y en a pas) dans ce cas, il doit faire quelque chose comme (avec toute la visibilité nécessaire et la sémantique d'atomicité):
while ( ! is_there_a_schedulable_thread());
C'est juste attendre .
Mais plutôt que de tourner une boucle serrée (ce qui peut nuire aux performances et à la puissance), le planificateur peut utiliser:
while ( ! is_there_a_schedulable_thread())
{
__wfi();
}
Au pire, c'est comme la boucle serrée, au mieux, cela mettra le cœur en pause jusqu'à ce qu'une interruption externe se produise (ce qui signifie que potentiellement une IO a été terminée et qu'un thread peut donc être libre de fonctionner).
Même en l'absence de threads, il vaut mieux se réveiller toutes les x microsecondes (en raison d'une interruption du minuteur) que de gaspiller une boucle d'énergie.
wfi
peut également être utile pour incorporer la programmation si vous avez tout le travail sur les gestionnaires d'interruption (par exemple, lorsqu'un bouton est enfoncé ou similaire).
Dans ce cas, la main
fonction ferait simplement une boucle pour toujours, tout comme le planificateur mais sans condition de sortie.
Une wfi
instruction améliorera considérablement la durée de vie de la batterie.
Vous ne pouvez pas utiliser wfi
partout ou vous pouvez vous retrouver en attente d'une interruption qui ne se produit jamais (en fait, c'est une instruction privilégiée).
Considérez-le comme une optimisation de la coordination avec le matériel.
En particulier, il n'a pas été conçu pour être sûr qu'une interruption a été déclenchée:
void wait_for_int(int int_num)
{
//Leave only interrupt int_num enabled
enable_only_int(int_num);
__wfi();
restore_interrupts();
}
Il pourrait être utilisé de cette façon étant donné une implémentation spécifique de RISC-V, mais comme vous le voyez dans le pseudo-code, ce n'est pas vraiment pratique.
Désactiver toutes les interruptions sauf une est généralement quelque chose qu'un système d'exploitation ne peut pas se permettre.
Une application intégrée pourrait cependant.