Como o seu produto ‘dá um tempinho’?

No nosso segundo post da arquitetura Inter e Intra montamos as primeiras interfaces do nosso projeto e uma destas interfaces é o do módulo de timer, para contagem de tempo.  ⏱

Vou aproveitar esta oportunidade para falar um pouco sobre as diferentes maneiras de realizar contagem de tempo em nossos projetos embarcados, falar um pouco dos prós e contras de cada uma e finalizar com o que eu geralmente faço. Vamos lá!

Contagem de tempo?

Sim! Quando digo ‘contagem de tempo’, me refiro à uma funcionalidade básica que 99,999% dos projetos precisam, que é de quando as nossas lógicas precisam esperar um tempinho antes de realizar alguma ação.

Por exemplo, uma atividade de piscar um LED consiste basicamente de dois passos, que ficam se repetindo:
  1. Esperar um tempo, do período de piscar.
  2. Trocar o nível lógico do pino ligado ao LED.
ezgif.com-gif-maker
Hello World!

Este artigo foca no primeiro passo, na atividade de ‘esperar um tempo’.

Quais as maneiras erradas de fazer isto?

Primeiro vou listar algumas maneiras que, apesar de existirem alguns casos bem específicos aonde elas são as melhores escolhas, o uso delas deveria ser totalmente desencorajado.

O famoso while

Ahh, quem nunca colocou uma ‘esperinha’ no código com um bom e velho while? 👀
void waitSomeTime(void)
{
    uint32_t i = 0;
    while(i < 10000) { i++; }
}
Apesar de parecer simples e prático, não se enganem, isto tem váaarios problemas:
  • O tempo de espera fica em função do clock do processador, quais as instruções geradas pelo compilador, etc.
  • O tempo de espera pode ser maior caso aconteçam interrupções.
    • Nãaao, você não está usando essas esperas usando while dentro de interrupções, né? 🤔
  • O processador fica alocado para operar este laço, enquanto poderia estar sendo usado para outro uso ‘útil’ (ou ficar em low power mode e economizar energia).
  • O módulo em espera fica bloqueado, sem a capacidade de realizar outra tarefa enquanto esta contagem não acaba.
  • O tempo de espera não é testável.

Delays de RTOS

Uma das funções básicas de todo RTOS é de prover a capacidade de suspender uma thread por um tempo, permitindo que façamos as nossas esperas. Um grande exemplo é o famoso vTaskDelay do FreeRTOS.
Esse delay é uma grande evolução quando comparado a usar laços while, pois:
  • O tempo de espera é mais independente do clock do microcontrolador.
  • A thread fica bloqueada, permitindo que os recursos da sua plataforma sejam melhor usados em outras threads (ou até mesmo fazer algum low power básico)
  • O tempo de espera é testável.
Porém, usar deste approach para aguardar períodos de tempo ainda não é recomendado:
  • O módulo em espera fica bloqueado, sem a capacidade de realizar outra tarefa enquanto esta contagem não acaba.
  • O erro do tempo de espera pode ser significativo, pois fica baseado no período de tick do RTOS.
  • Seu uso encoraja o seu módulo a não ser implementado orientado a eventos, o que dificulta muito testar ele usando testes unitários.
  • Esta opção não está disponível para projetos baremetal.

Quais as maneiras certas de fazer isto?

Para esperar este tempo corretamente usamos de partes especializadas nisto e eles nos informam, através de um callback, quando este tempo passou. 

É como se pedíssemos para eles…

“Ôoo módulo, me avise aqui [aponta para callback] quando tanto tempo [mostra o valor] passar?” 
… e eles se encarregam de contar este tempo. Passamos para eles um callback do nosso módulo de sistema e, assim, nossos módulos podem criar eventos apropriados, pelos seus módulos de sistema.
Temos três possibilidades de fazer isso:
  • Drivers que utilizam de periféricos de timers.
  • Serviço de timer dos RTOSes.
  • Drivers que utilizam de periféricos de RTC.

Drivers de Timers

A nossa primeira alternativa é de implementar um driver que utiliza de um periférico de tempo do microcontrolador. Todo micro fornece pelo menos um periférico para se fazer isso. Inclusive, é esse approach que estou usando no nosso projeto Inter e Intra.
Prós:
  • Acesso direto a um periférico e a sua notificação geralmente é por interrupção.
  • Total liberdade para escolher a resolução da contagem de tempo, dependendo apenas da capacidade do periférico.
  • Pode funcionar em modos de low power.
Contras:
  • Poucos periféricos disponíveis, limitando quantos usuários podem pedir para contar tempo.
  • Não é capaz de contar longos períodos de tempo.
  • Dependente do microcontrolador. Se você trocar de micro, terá que trabalhar em outro driver.

Timers dos RTOSes

Assim como RTOSes fornecem funções para delay, a maioria deles têm disponível módulos de timers, os quais fornecem exatamente as funcionalidades que estamos discutindo aqui. Eles já usam de um periférico de tempo para realizarem scheduling, porque não expandir seu uso para usar como timer, não é mesmo?
Prós:
  • Simples de utilizar. O RTOS se encarrega de contar o tempo para você, usando do periférico de tempo que lhe foi alocado para o seu scheduling.
  • Não é dependente do microcontrolador. Se o RTOS está portado para ele, seu timer funciona.
  • Suporte à inúmeros clientes.
  • Capacidade de contar longos períodos de tempo.
Contras:
  • O erro da contagem pode ser significativo, pois…
    • … depende do RTOS de te notificar dela.
    • … a resolução da contagem de tempo é com base no período do tick do RTOS.
  • Não funciona em projetos baremetal.
  • Geralmente, não funciona em operações low power.

Drivers de RTC (Real Time Clock)

Similar à alternativa de usar drivers de contagem de tempo, mas aqui é usado o periférico de RTC e a sua função de alarme, para notificar quando se chega à uma certa data e hora.
Prós:
  • Acesso direto a um periférico e a sua notificação geralmente é por interrupção.
  • Pode contar períodos de tempo muito, muito muito longos.
    • Precisa contar até o ano de 2090? Pode deixar! 🙃
  • Opera em modos de low power com uma mão nas costas.
Contras:
  • O passo deste periférico é em segundos, então tempos de espera abaixo deste valor não são possíveis.
  • Assim como o passo é de um segundo, o erro da sua contagem poderá ser de até um segundo.
  • Se a data e hora do seu microcontrolador for alterada, o seu timer terá que se reajustar.
  • Poucos alarmes disponíveis, limitando quantos usuários podem pedir para contar tempo.
  • Dependente do microcontrolador. Se você trocar de modelo, terá que desenvolver outro driver.

Tantas opções! Como você faz, Andre?

E agora?
Bem, conforme eu sempre digo, os módulos do seu projeto não devem ser dependentes das implementações dos outros módulos. Eles devem ser dependentes apenas de um cabeçalho público e as implementações devem atender à este cabeçalho.
Além do mais, não existe uma opção que supera todas as outras – cada uma tem as suas vantagens e desvantagens. Assim, conforme os seus projetos forem sendo desenvolvidos você provavelmente precisará utilizar de diferentes tipos de timers, então não vai ser legal você ficar preso à apenas um ou outro tipo deles. 👍
Tendo dito tudo isso, o que te falo é… tenha todos eles disponíveis! Faça assim:
  • Crie cabeçalhos públicos para os timers por: periférico, por RTOS, por RTC.
  • Crie um cabeçalho público geral para um outro módulo que recebe pedidos de contagem de tempo de clientes e ele atua no módulo necessário para a operação – um dos três criados acima.
  • Implemente os módulos necessários para atender estes cabeçalhos, lhe provendo assim a habilidade de contar tempo de diferentes maneiras!
A figura abaixo ilustra o que quero dizer:
Assim, os outros módulos do seu produto podem usar o módulo principal para as contagens de tempo em geral. Se algum módulo, por algum motivo, precisa usar um tipo de timer específico… sem problemas, basta usar aquele módulo diretamente! Temos total flexibilidade para usarmos dos nossos timers!
Para finalizar, importante salientar que não precisa fazer todo este trabalho de uma só vez. Eu te recomendo ir por partes, gradualmente implementando os módulos e interfaces conforme a sua necessidade. Enquanto você usar as interfaces públicas para abstrair o seu trabalho, os retrabalhos devem ficar em um mínimo. 🙂
E você? Como você faz nos seus projetos embarcados? Dúvidas, comentários? Participe e até a próxima!