Melhor padrão para WFI (espera por interrupção) nos microcontroladores Cortex (ARM)

18

Estou pensando em desenvolver software alimentado por bateria usando os controladores EFM Gekko (http://energymicro.com/) e gostaria que o controlador estivesse dormindo sempre que não houvesse nada útil para ele fazer. A instrução WFI (Wait For Interrupt) é usada para esse fim; colocará o processador em suspensão até que ocorra uma interrupção.

Se o sono estivesse comprometido armazenando algo em algum lugar, seria possível usar operações de carregamento exclusivo / armazenamento exclusivo para fazer algo como:

  // dont_sleep é carregado com 2 sempre que algo acontece
  // deve forçar o loop principal a alternar pelo menos uma vez. Se uma interrupção
  // ocorre que faz com que seja redefinido para 2 durante a seguinte instrução,
  // o comportamento será como se a interrupção tivesse acontecido depois dele.

  loja_exclusiva (carga_exclusiva (não dorme) >> 1);

  while (! dont_sleep)
  {
    // Se ocorrer uma interrupção entre a próxima instrução e store_exclusive, não durma
    load_exclusive (SLEEP_TRIGGER);
    if (! dont_sleep)             
      loja_exclusiva (SLEEP_TRIGGER);
  }

Se uma interrupção ocorrer entre as operações load_exclusive e store_exclusive, o efeito seria pular a store_exclusive, fazendo com que o sistema execute o loop mais uma vez (para ver se a interrupção havia definido dont_sleep). Infelizmente, o Gekko usa uma instrução WFI em vez de um endereço de gravação para acionar o modo de suspensão; escrevendo código como

  if (! dont_sleep)
    WFI ();

correria o risco de que uma interrupção pudesse ocorrer entre o 'if' e o 'wfi' e definir dont_sleep, mas o wfi iria em frente e seria executado de qualquer maneira. Qual é o melhor padrão para evitar isso? Defina PRIMASK como 1 para impedir que as interrupções interrompam o processador antes de executar o WFI e limpe-o imediatamente depois? Ou existe algum truque melhor?

EDITAR

Eu estou pensando sobre o bit Event. Pela descrição geral, ele gostaria que fosse destinado ao suporte a vários processadores, mas estava se perguntando se algo como o seguinte poderia funcionar:

  if (não dorme)
    SEV (); / * Tornará o seguinte sinalizador de evento claro do WFE, mas não será interrompido * /
  WFE ();

Cada interrupção que define não_sleep também deve executar uma instrução SEV; portanto, se a interrupção ocorrer após o teste "se", o WFE limpará o sinalizador de evento, mas não entrará no modo de suspensão. Isso soa como um bom paradigma?

supercat
fonte
11
A instrução WFI não coloca o núcleo em suspensão se sua condição de ativação for verdadeira quando a instrução for executada. Por exemplo, se houver um IRQ claro quando o WFI for executado, ele funcionará como um NOP.
Mark
@ Mark: O problema seria que, se uma interrupção for realizada entre o "if (! Dont_sleep)" e o "WFI", a condição de interrupção não estará mais pendente quando o WFI for executado, mas a interrupção pode ter definido dont_sleep porque fez algo que justificaria o loop principal executando outra iteração. Em um aplicativo Cypress PSOC meu, qualquer interrupção que deva causar uma ativação prolongada agitaria a pilha se o código da linha principal estivesse prestes a dormir, mas isso parece bastante nojento e eu entendo que o ARM desencoraja essas manipulações de pilha.
supercat
@supercat A interrupção pode ou não ser limpa quando o WFI é executado. Cabe a você e quando / onde você escolhe limpar a interrupção. Livre-se da variável dont_sleep e use uma interrupção mascarada para indicar quando você quer ficar acordado ou dormindo. Você pode simplesmente se livrar da instrução if todos juntos e deixar o WFI no final do loop principal. Se você atendeu a todas as solicitações, limpe o IRQ para poder dormir. Se você precisar ficar acordado, ative o IRQ, mascarado, para que nada aconteça, mas quando o WFI tentar executá-lo, o NOP será ativado.
Mark
2
@supercat Em um nível mais fundamental, parece que você está tentando misturar um design orientado a interrupções com um design de 'loop principal principal', que geralmente não é crítico em termos de tempo, geralmente baseado em pesquisas e tem interrupções mínimas. Misturar isso pode ficar feio e rápido. Se possível, escolha um paradigma de design ou outro para usar. Lembre-se de que, com os modernos controladores de interrupção, você basicamente realiza multitarefas preemptivas entre interrupções e o que equivale a filas de tarefas (atenda uma interrupção, depois a próxima prioridade mais alta etc.). Use isso a seu favor.
Mark
@ Mark: Eu desenvolvi um sistema que usava muito bem um PIC 18x em um aplicativo alimentado por bateria; por causa das limitações da pilha, ele não aguentava muito em uma interrupção; portanto, a grande maioria das coisas é tratada no loop principal de maneira conveniente. Geralmente funciona muito bem, embora existam alguns pontos em que as coisas ficam bloqueadas por um segundo ou dois por causa de operações de longa duração. Se eu migrar para um ARM, posso usar um RTOS simples para facilitar a divisão das operações de longa execução, mas não tenho certeza se devo usar multitarefa preemptiva ou cooperativa.
supercat

Respostas:

3

Eu não entendi completamente a dont_sleepcoisa, mas uma coisa que você pode tentar é fazer o "trabalho principal" no manipulador PendSV, definido com a menor prioridade. Em seguida, agende um PendSV de outros manipuladores sempre que precisar de algo. Veja aqui como fazê-lo (é para M1, mas M3 não é muito diferente).

Outra coisa que você pode usar (talvez em conjunto com a abordagem anterior) é o recurso de suspensão na saída. Se você ativá-lo, o processador entrará em suspensão após sair do último manipulador ISR, sem que você precise chamar o WFI. Veja alguns exemplos aqui .

Igor Skochinsky
fonte
5
a instrução WFI não exige que interrupções sejam ativadas para ativar o processador, os bits F e I no CPSR são ignorados.
Mark
11
@ Mark Devo ter perdido essa parte dos documentos, você tem alguns links / indicadores sobre isso? O que acontece com o sinal de interrupção que despertou o núcleo? Ele permanece pendente até a interrupção ser ativada novamente?
Igor Skochinsky
O manual de referência do ASM está aqui: infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0489c/… informações mais específicas para o córtex-m3 estão aqui: infocenter.arm.com/help/ index.jsp? topic = / com.arm.doc.dui0552a /… em suma, quando uma interrupção mascarada fica pendente, o núcleo é ativado e continua a operação após a instrução WFI. Se você tentasse emitir outro WFI sem esclarecer que a interrupção pendente, o WFI atuaria como um NOP (o núcleo não funcionaria porque a condição de ativação do WFI é verdadeira).
Mark
@ Mark: Uma coisa que eu estava pensando seria ter qualquer manipulador de interrupção que define dont_sleep também executar uma instrução SEV ("Set Event") e, em seguida, usar WFE ("Wait For Event") em vez de WFI. Os exemplos de Gekko parecem usar o WFI, mas acho que o WFE também pode funcionar. Alguma ideia?
supercat
10

Coloque-o dentro de uma seção crítica. Os ISRs não serão executados, portanto você não corre o risco de alterar o dont_sleep antes do WFI, mas eles ainda ativam o processador e os ISRs serão executados assim que a seção crítica terminar.

uint8 interruptStatus;
interruptStatus = EnterCriticalSection();
if (!dont_sleep)
  WFI();
ExitCriticalSection(interruptStatus);

Seu ambiente de desenvolvimento provavelmente possui funções críticas de seção, mas é mais ou menos assim:

EnterCriticalSection é:

MRS r0, PRIMASK /* Save interrupt state. */
CPSID i /* Turn off interrupts. */
BX lr /* Return. */

ExitCriticalSection é:

MSR PRIMASK, r0 /* Restore interrupt states. */
BX lr /* Return. */
Kenzi
fonte
2
Curiosamente, várias bibliotecas ARM usam uma implementação de seção crítica que usa um contador global em vez de preservar o status localmente. Acho que isso é incompreensível, pois a abordagem do contador é mais complicada e só funcionará se todo o código do sistema usar o mesmo contador.
Supercat
11
As interrupções não serão desativadas até sairmos da seção crítica? Nesse caso, o WFI não fará com que a CPU aguarde indefinidamente?
Corneliu Zuzu
11
A resposta do @Kenzi Shrimp aponta para um tópico de discussão do Linux que responde à minha pergunta anterior. Eu editei a resposta dele e a sua para esclarecer isso.
Corneliu Zuzu
@CorneliuZuzu Editar a resposta de outra pessoa para interpor sua própria discussão não é uma ótima idéia. Adicionar uma citação para melhorar uma resposta "somente link" é uma questão diferente. Se você tem uma pergunta real do seu ganho, talvez faça uma pergunta e faça um link para esta.
Sean Houlihane
11
@SeanHoulihane Não invalidei nem removi nada da resposta dela. Um breve esclarecimento sobre por que isso funciona não é uma discussão separada. Honestamente, não acho que essa resposta mereça uma votação positiva sem o esclarecimento da WFI, mas ela merece mais com ela.
Corneliu Zuzu
7

Sua ideia é boa, é exatamente isso que o Linux implementa. Veja aqui .

Citação útil do tópico de discussão acima mencionado para esclarecer por que o WFI funciona mesmo com as interrupções desabilitadas:

Se você pretende ficar ocioso até a próxima interrupção, precisa fazer alguma preparação. Durante essa preparação, uma interrupção pode se tornar ativa. Essa interrupção pode ser um evento de ativação que você está procurando.

Não importa o quão bom seja o seu código, se você não desativar as interrupções, você sempre terá uma corrida entre preparar-se para dormir e realmente dormir, o que resulta em perda de eventos de despertar.

É por isso que todas as CPUs ARM que eu conheço serão ativadas mesmo se estiverem mascaradas na CPU principal (bit CPSR I).

Qualquer outra coisa e você deve esquecer de usar o modo ocioso.

Camarão
fonte
11
Você está se referindo à desativação de interrupções no momento da instrução WFI ou WFE? Você vê alguma distinção significativa entre o uso de WFI ou WFE para esse fim?
supercat
11
@ supercat: Eu definitivamente usaria o WFI. O WFE IMO é principalmente para dicas de sincronização entre núcleos no sistema multicore (por exemplo, fazer o WFE no spinlock, tendo falha e emitindo SEV após sair do spinlock). Além disso, o WFE leva em consideração o sinalizador de máscara de interrupção, portanto não é tão útil aqui quanto o WFI. Esse padrão realmente funciona bem no Linux.
Camarão
2

Assumindo que:

  1. O encadeamento principal executa tarefas em segundo plano
  2. As interrupções executam apenas tarefas de alta prioridade e nenhuma tarefa em segundo plano
  3. O thread principal pode ser interrompido a qualquer momento (normalmente não mascara interrupções)

A solução é usar o PRIMASK para bloquear interrupções entre a validação do sinalizador e o WFI:

mask_interrupts();
if (!dont_sleep)
    wfi();
unmask_interrupts();
hdante
fonte
0

E o modo de suspensão ao sair? Isso entra automaticamente no modo de suspensão sempre que um manipulador de IRQ sai, portanto não há realmente nenhum "modo normal" em execução após a configuração. Um IRQ acontece, ele acorda e executa o manipulador e volta a dormir. Não é necessário WFI.

faturar
fonte
2
Como lidar melhor com o fato de que o tipo de suspensão no qual o processador deve cair pode variar com base em algo que acontece durante uma interrupção? Por exemplo, um evento de troca de pinos pode indicar que os dados seriais podem estar chegando e, portanto, o processador deve manter o oscilador do relógio principal em execução enquanto aguarda os dados. Se o loop principal limpa o sinalizador de evento, examina o que está acontecendo e coloca o processador em um modo de suspensão apropriado com um WFI, qualquer interrupção que possa ter afetado qual modo seria apropriado
definiria o
... e abortar o sono. Ter um manipulador de loop principal controlando o modo de suspensão parece mais limpo do que ter que se preocupar com isso a cada interrupção. Ter que "girar" essa parte do loop principal em cada interrupção pode não ser idealmente eficiente, mas não deve ser muito ruim, especialmente se todas as interrupções que podem afetar o comportamento do sono atingirem alguma sinalização.
Supercat