terça-feira, 17 de setembro de 2013

Microcontroladores

Não pretendo fazer uma introdução formal aos microcontroladores, minha experiência com eles é limitada e essencialmente prática. Além disso, textos introdutórios de excelente qualidade sobre o assunto são facilmente encontrados na internet. Aqui serão discutidos alguns tópicos práticos relacionados a características da programação de microcontroladores que talvez os programadores acostumados a programar computadores não estejam habituados. O código usado como exemplo usa a biblioteca do Wixel que contém o CC2511F32 da Texas Instruments. Mas mesmo para quem usar outro microcontrolador, penso que serão de muita ajuda para os iniciantes porque os conceitos são basicamente os mesmo para qualquer microcontrolador.

Antes de tratar sobre a programação é importante conhecer alguns conceitos básicos usados na arquitetura dos microcontroladores então sugiro enfaticamente uma pesquisa introdutória sobre o assunto. Abaixo alguns links:

Loop infinito

A primeira coisa a saber sobre programação de microcontroladores é que o programa está sempre rodando dentro de um loop infinito. Quando o microcontrolador é ligado um código de inicialização é executado e em seguida o programa entra em um loop infinito que só termina quando a energia é cortada. Normalmente dentro do loop estão chamadas a funções que monitoram o estado de alguma porta, pino, timer ou qualquer periférico disponível e conforme o caso executam alguma ação ou retornam sem fazer nada,

É preciso lembrar que essas funções serão chamadas uma vez a cada iteração do loop, mas vão (ou deveriam) ocorrer milhares de iterações por segundo. O comportamento esperado é que na grande maioria das chamadas essas funções retornem sem nenhuma ação, então é uma boa estratégia iniciar a função identificando o mais cedo possível essa condição e retornar executando o mínimo de código possível.

Funções não bloqueantes

Quanto mais iterações por segundo o loop principal conseguir fazer, mais precisas serão as medidas feitas nos periféricos e mais imediatas serão as ações de resposta do microcontrolador. Por isso é importante evitar escrever código que mantenha o fluxo de processamento preso em uma instrução ou loop por muito tempo. Por exemplo, instruções que fazem o processamento "esperar" determinada quantidade de tempo ( do tipo delay(tempo) ), devem ser evitadas.

Para entender o problema de usar usar funções bloqueantes, vamos considerar uma aplicação bem simples que envolva alguns pinos. Suponha que o pino P1 deva ser monitorado e que o funcionamento normal do sistema exija este pino em nível alto. Sempre que o pino P1 ficar em nível baixo, um alarme conectado ao pino P2 tem que ser ligado e mantido assim até o pino P1 voltar ao nível alto. O programa para esta especificação poderia ser algo como:


#define mnPin    P1
#define pinAlarm P2
 
void main()
{
    // inicializacão
    setDigitalInput(mnPin, HIGH_IMPEDANCE);
    setDigitalOutput(pinAlarm, LOW);
 
    // loop principal
    while(1)
    {
        // monitoramento
        pinAlarm = !mnPin ;
    }
}
 
Apesar de simples e básico, o código acima é extremamente eficiente e preciso. No instante em que P1 estiver em nível baixo, o pino de alarme é ativado. Isso será verificado milhares de vezes por segundo, então a reação será praticamente imediata.

Entretanto, qualquer microcontrolador atual pode dar conta de muito mais código do que o programa acima, então vamos acrescentar mais uma funcionalidade à aplicação: um led de status. A função desse led é indicar que o microcontrolador está ligado e funcionando, para isso faremos ele piscar em ciclos de 1 segundo, ficando meio segundo apagado e meio segundo aceso. Assumindo que seja usado o pino P3 para ativar o led, poderia ser feita uma implementação como o programa abaixo:


#define mnPin     P1
#define pinAlarm  P2
#define pinStatus P3
 
#define on  1
#define off 0
 
void main()
{
    // inicializacão
    setDigitalInput(mnPin, HIGH_IMPEDANCE);
    setDigitalOutput(pinAlarm, LOW);
    setDigitalOutput(pinStatus, LOW);
 
    // loop principal
    while(1)
    {
        // monitoramento
        pinAlarm = !mnPin;
 
        // led de status
        pinStatus = on;
        delay(500);
        pinStatus = off;
        delay(500);
    }
}
 
A função delay() faz com que o processador espere a quantidade de milissegundos indicada no argumento. A cada iteração do loop principal, o programa agora verifica o pino monitorado, em seguida liga o led de status e espera meio segundo. A seguir o led de status é desligado e novamente há uma espera de meio segundo. Este código faz o que é pedido, mas agora cada iteração demora pelo menos um segundo para ser completada. Isso significa que se o pino P1 for para o nível baixo logo após ele ser verificado, pode demorar até um segundo para que o alarme seja ativado. Essa demora pode ser aceitável em algumas aplicações, mas também pode inviabilizar outras. Suponha que o alarme signifique uma sobrecarga elétrica no sistema e o mesmo sinal que ative o alarme também desligue o dispositivo sobrecarregado. Nesse caso, um segundo é tempo mais que suficiente para torrar o dispositivo que deveria estar sendo protegido.

Mas então como fazer para piscar o led de um modo não-bloqueante? Veja o código abaixo:


#define mnPin     P1
#define pinAlarm  P2
#define pinStatus P3
 
void blinkStatusLed()
{   static uint32 nextToggle = 0;
 
    if(getMs() >= nextToggle)
    {
        pinStatus = !pinStatus;
        nextToggle = getMs()+500;
    }
}
 
void main()
{
    // inicializacão
    setDigitalInput(mnPin, HIGH_IMPEDANCE);
    setDigitalOutput(pinAlarm, LOW);
    setDigitalOutput(pinStatus, LOW);
 
    // loop principal
    while(1)
    {
        // monitoramento
        pinAlarm = !mnPin;
 
        // led de status
        blinkStatusLed();
    }
}
 
A função getMs() faz parte da biblioteca do Wixel e retorna a quantidade de milissegundos decorridos desde que o microcontrolador foi ligado. Uma função com essa funcionalidade existe nas bibliotecas da maioria dos microcontroladores, talvez com outro nome. De qualquer forma, a função permite medir intervalos de tempo em milissegundos.

A nova implementação do programa acrescenta a função blinkStatusLed() que cuida de piscar o led de status. Veja que não há mais chamadas à função delay(). Em vez disso, a variável estática nextToggle armazena quando deverá ser feita a próxima mudança de estado do led somando o valor 500 ao valor retornado por getMs() cada vez que o led muda de estado.

A grande diferença entre esta implementação e a anterior, é que novamente o pino será monitorado milhares de vezes por segundo, já que não existe mais nenhuma espera no processamento. Apesar disso, o led irá piscar em ciclos de 1 segundo, conforme foi especificado.

Led de status piscante

Uma maneira simples de resolver a questão do tópico anterior seria fazer com que o led de status fique sempre aceso em vez de piscar. Isso tornaria o código muito mais simples. Na verdade, o led poderia ser ativado ainda na inicialização e não seria necessário nenhum código dentro do loop principal. Isso simplifica o programa, mas retira uma importante informação de debug. A função do led de status não é só indicar que o microcontrolador está ligado, mas ele também deve informar se a execução está seguindo seu fluxo normal ou se ficou presa em algum loop ou função. Se o led de status está configurado para piscar e em determinado momento ele passa a ficar sempre aceso ou sempre apagado, isso indica que o programa pode ter travado em algum ponto e não está mais seguindo o fluxo normal de processamento. Um led de status que fique sempre aceso não daria essa informação.

Medir tempo com operações bit-a-bit

Como citado, a maioria dos microcontroladores oferece em suas bibliotecas uma função que informa a quantidade de milissegundos decorridos desde a ativação. O valor retornado por essa função é sempre um inteiro positivo. Se analisarmos o comportamento dos bits que compõe esse valor em binário, vamos perceber que cada bit muda de estado em intervalos de tempo bem determinados. Apenas como referência, vamos chamar o bit menos significativo de bit 0, o segundo bit menos significativo de bit 1 e assim por diante.

Se em determinado momento o bit 0 desse valor for 1, no milissegundo seguinte ele será 0 e no próximo milissegundo ele será 1 novamente e assim por diante. Então ele muda de estado a cada milissegundo. Já o bit 1 só vai mudar de estado a cada dois milissegundos, o bit 2 muda de estado a cada 4 milissegundos, o bit 3 muda a cada oito milissegundos e assim por diante dobrando o intervalo a cada incremento de ordem do bit.

Então se precisarmos medir algum intervalo de tempo fixo (por exemplo para a mudança de estado de um led de status) e se esse intervalo for próximo de alguma potência de dois, é muito mais eficiente, rápido e econômico usar os bits do contador de milissegundos do que criar uma variável estática, somar uma valor a ela, comparar o valor da variável com o valor do contador e etc, como foi sugerido acima.

No exemplo anterior foi implementado um código para piscar o led de status a cada meio segundo usando uma variável estática. Meio segundo significa 500 milissegundos e 500 não é uma potência de dois, mas 512 é (2 elevado a 9) e também é um valor próximo o suficiente de 500 para ser aceitável como aproximação nesta aplicação. Veja abaixo uma nova implementação da aplicação do exemplo, agora usando o contador de milissegundos para piscar o led:


#define mnPin     P1
#define pinAlarm  P2
#define pinStatus P3
 
void main()
{
    // inicializacão
    setDigitalInput(mnPin, HIGH_IMPEDANCE);
    setDigitalOutput(pinAlarm, LOW);
    setDigitalOutput(pinStatus, LOW);
 
    // loop principal
    while(1)
    {
        // monitoramento
        pinAlarm = !mnPin;
 
        // led de status
        pinStatus = (getMs() >> 8) & 1;  // intervalos de 512 ms
    }
}
 
Operações bit-a-bit ocupam pouca memória e são extremamente rápidas, então este código é muito mais compacto e eficiente do que a versão anterior que usa a função blinkStatusLed(). Além disso, continua sendo não bloqueante.

Heart beat - Batida de coração

Podemos usar um bit do contador de milissegundos para medir um intervalo de tempo específico, como mostrado no tópico anterior, mas combinando vários bits do contador é possível produzir padrões bem interessantes simulando ritmos, por exemplo como o ritmo das batidas de um coração. O princípio é o mesmo da medição de intervalos, só que agora vamos usar mais de um bit para calcular o estado do led de status. Veja o código abaixo:

uint32 ms;
 
ms = getMs();
pinStatus = (ms >> 8) & (ms >> 6) & 1;

Agora, para que o pino de status fique em nivel alto, é preciso que o bit 9 e o bit 7 do contador estejam ambos com o valor 1. Veja que o bit 9 muda de estado a cada 512 milissegundos, então quando o esse bit estiver zerado o led ficará apagado não importa o estado do bit 7. Já o bit 7 muda de estado a cada 128 milissegundos, então no período em que o bit 9 assume o valor 1 (a cada 512 ms) o bit 7 muda de estado 4 vezes produzindo duas piscadas. Então o led vai dar duas piscadas, ficar meio segundo apagado e depois mais duas piscadas, etc. O efeito lembra bem uma batida de coração. Outros ritmos podem produzidos fazendo combinações diferentes de bits do contador de milissegundos.

Nenhum comentário:

Postar um comentário