Microcontrolador Sleep Race Condition

11

Dado um microcontrolador que está executando o seguinte código:

volatile bool has_flag = false;

void interrupt(void) //called when an interrupt is received
{
    clear_interrupt_flag(); //clear interrupt flag
    has_flag = true; //signal that we have an interrupt to process
}

int main()
{
    while(1)
    {
        if(has_flag) //if we had an interrupt
        {
            has_flag = false; //clear the interrupt flag
            process(); //process the interrupt
        }
        else
            sleep(); //place the micro to sleep
    }
}

Suponha que a if(has_flag)condição seja avaliada como falsa e estamos prestes a executar a instrução de suspensão. Logo antes de executarmos a instrução sleep, recebemos uma interrupção. Depois de deixarmos a interrupção, executamos a instrução sleep.

Essa sequência de execução não é desejável porque:

  • O microcontrolador foi dormir em vez de acordar e ligar process().
  • O microcontrolador nunca poderá ser ativado se nenhuma interrupção for recebida posteriormente.
  • A chamada para process()é adiada até a próxima interrupção.

Como o código pode ser escrito para impedir que essa condição de corrida ocorra?

Editar

Alguns microcontroladores, como o ATMega, possuem um bit de habilitação para suspensão que impede que essa condição ocorra (obrigado Kvegaoro por apontar isso). O JRoberts oferece um exemplo de implementação que exemplifica esse comportamento.

Outros micros, como os PIC18s, não possuem esse bit e o problema ainda ocorre. No entanto, esses micros são projetados de modo que as interrupções ainda possam ativar o núcleo, independentemente de o bit de ativação de interrupção global estar definido (obrigado supercat por apontar isso). Para essas arquiteturas, a solução é desativar as interrupções globais antes de dormir. Se uma interrupção for acionada imediatamente antes de executar a instrução de suspensão, o manipulador de interrupções não será executado, o núcleo será ativado e, quando as interrupções globais forem reativadas, o manipulador de interrupções será executado. No pseudo-código, a implementação seria assim:

int main()
{
    while(1)
    {
        //clear global interrupt enable bit.
        //if the flag tested below is not set, then we enter
        //sleep with the global interrupt bit cleared, which is
        //the intended behavior.
        disable_global_interrupts();

        if(has_flag) //if we had an interrupt
        {
            has_flag = false; //clear the interrupt flag
            enable_global_interrupts();  //set global interrupt enable bit.

            process(); //process the interrupt
        }
        else
            sleep(); //place the micro to sleep
    }
}
TRISAbits
fonte
Esta é uma questão prática ou teórica?
AndrejaKo
em teoria, você usa um temporizador que acorda uma vez a cada (insira um valor aceitável) ms e depois volta a dormir se nada precisar ser feito.
Grady Player
1
Eu faria interrupt_flagcomo um inte incrementá-lo sempre que houver interrupção. Então mude if(has_flag)para while (interrupts_count)e depois durma. No entanto, a interrupção pode ocorrer depois que você sai do loop while. Se isso for um problema, faça o processamento na própria interrupção?
angelatlarge
1
bem, depende de qual micro você está executando .. Se fosse um ATmega328, você poderia desativar o modo de suspensão na interrupção; portanto, se a condição de corrida descrita ocorrer, a função de suspensão será substituída, retorne novamente e você processará a interrupção com uma pequena latência. Mas também utilizando um temporizador para acordar em um intervalo igual ou menor à sua latência máxima seria uma grande solução também
Kvegaoro
1
@TRISAbits: No PIC 18x, a abordagem que descrevi na minha resposta funciona muito bem (é o meu design normal ao usar essa parte).
Supercat 20/03

Respostas:

9

Geralmente, há algum tipo de suporte de hardware para este caso. Por exemplo, a seiinstrução dos AVRs para ativar as interrupções adia a ativação até que a instrução a seguir seja concluída. Com isso, pode-se fazer:

forever,
   interrupts off;
   if has_flag,
      interrupts on;
      process interrupt;
   else,
      interrupts-on-and-sleep;    # won't be interrupted
   end
end

A interrupção que seria perdida no exemplo, nesse caso, seria interrompida até que o processador conclua sua sequência de suspensão.

JRobert
fonte
Ótima resposta! O algoritmo que você fornece realmente funciona muito bem em um AVR. Obrigado pela sugestão.
TRISAbits
3

Em muitos microcontroladores, além de poder ativar ou desativar causas de interrupção específicas (geralmente dentro de um módulo controlador de interrupção), existe um sinalizador mestre no núcleo da CPU que determina se as solicitações de interrupção serão aceitas. Muitos microcontroladores sairão do modo de suspensão se uma solicitação de interrupção atingir o núcleo, esteja ele disposto ou não a aceitá-lo.

Nesse projeto, uma abordagem simples para obter um comportamento confiável do sono é fazer com que o loop principal limpe um sinalizador e, em seguida, verifique se sabe algum motivo para o processador estar acordado. Qualquer interrupção que ocorra durante esse período que possa afetar qualquer um desses motivos deve definir o sinalizador. Se o loop principal não encontrou nenhuma causa para permanecer acordado e se o sinalizador não estiver definido, o loop principal deve desativar as interrupções e verificar o sinalizador novamente [talvez depois de algumas instruções NOP, se é possível que uma interrupção fique pendente durante uma instrução de interrupção de desativação pode ser processada após a busca do operando associada à instrução a seguir já ter sido executada]. Se o sinalizador ainda não estiver definido, vá dormir.

Nesse cenário, uma interrupção que ocorre antes que o loop principal desative as interrupções definirá o sinalizador antes do teste final. Uma interrupção que fica pendente demais para ser atendida antes da instrução de suspensão impedirá que o processador entre no modo de suspensão. Ambas as situações estão bem.

Às vezes, dormir na saída é um bom modelo para usar, mas nem todos os aplicativos realmente o "encaixam". Por exemplo, um dispositivo com um LCD que economiza energia pode ser programado com mais facilidade com código parecido com:

void select_view_user(int default_user)
{
  int current_user;
  int ch;
  current_user = default_user;
  do
  {
    lcd_printf(0, "User %d");
    lcd_printf(1, ...whatever... );
    get_key();
    if (key_hit(KEY_UP)) {current_user = (current_user + 1) % MAX_USERS};
    if (key_hit(KEY_DOWN)) {current_user = (current_user + MAX_USERS-1) % MAX_USERS};
    if (key_hit(KEY_ENTER)) view_user(current_user);
  } while(!key_hit(KEY_EXIT | KEY_TIMEOUT));
}

Se nenhum botão for pressionado e nada mais estiver acontecendo, não há razão para o sistema não dormir durante a execução do get_keymétodo. Embora seja possível que as chaves acionem uma interrupção e gerenciem toda a interação da interface do usuário por meio de uma máquina de estado, código como o acima é geralmente a maneira mais lógica de lidar com fluxos de interface do usuário altamente modais, típicos de pequenos microcontroladores.

supercat
fonte
Obrigado supercat pela ótima resposta. Desativar interrupções e depois entrar em suspensão é uma solução fantástica, desde que o núcleo seja ativado de qualquer fonte de interrupção, independentemente de o bit de interrupção global estar definido / limpo. Dei uma olhada no esquema de hardware de interrupção PIC18 e essa solução funcionaria.
TRISAbits
1

Programe o micro para acordar em interrupção.

Os detalhes específicos variam de acordo com o micro que você está usando.

Em seguida, modifique a rotina main ():

int main()
{
    while(1)
    {
        sleep();
        process(); //process the interrupt
    }
}
jwygralak67
fonte
1
A arquitetura Wake-on-Interrupt é assumida na pergunta. Não acho que sua resposta resolva a questão / problema.
angelatlarge
@angelatlarge Ponto aceito. Adicionei um exemplo de código que acho que ajuda.
jwygralak67
@ jwygralak67: Obrigado pela sugestão, mas o código que você fornece apenas move o problema para a rotina process (), que agora precisa testar se a interrupção ocorreu antes de executar o corpo process ().
TRISAbits
2
Se a interrupção não ocorreu, por que estamos acordados?
JRobert
1
@JRobert: Podemos estar acordados de uma interrupção anterior, concluir a rotina process () e, quando terminarmos o teste if (has_flag) e logo antes do sono, receberemos outra interrupção, o que causa o problema que descrevi no questão.
TRISAbits