No modelo de máquina de estados, assim que uma função termina, o processador automaticamente passa para a próxima tarefa, independente de questões temporais. No entanto, passa a ser extremamente complicado fazer qualquer garantia de tempo de execução, pois além dos tempos serem regidos pelo tamanho da tarefa, estes tempos podem ainda variar. Um bom exemplo é a recepção de comandos pela serial. Nem sempre o sistema recebe bytes e, mesmo quando recebe, ele precisa aguardar todos os bytes chegarem para processar a mensagem. Assim, a função de recepção tem pelo menos três durações diferentes: quando não há nada na serial, quando há informação na serial a ser salva e quando há uma mensagem para ser processada.
Uma característica desejada nos sistemas embarcados é que as funções tivessem um tempo determinado de execução. Deste modo, todo o sistema se tornaria mais previsível.
A maneira mais simples de realizar este procedimento é fazer com que, nas vezes que a função for executada e terminar antes, o sistema faça com que ela aguarde até um determinado valor de tempo. Este valor tem que ser maior que o pior caso de execução da função.
Apesar de não ser ideal, já que o processador irá ficar tempo ocioso apenas para padronizar os tempos de execução das funções, o determinismo atingido por esse método traz imensas vantagens no desenvolvimento. Outra vantagem é que é possível criar uma arquitetura que faça essa garantia consumindo poucos recursos, ou seja, esta é uma abordagem possível de se implementar em sistemas que não são capazes de suportar um sistema de tempo real como o FreeRTOS.
Para isso basta ter acesso à um temporizador em hardware. Basta então inicializar a contagem no top-slot da arquitetura de máquina de estados e aguardar o fim da contagem no bottom-slot. Deste modo, toda vez que um slot terminar, o sistema ficará aguardando o tempo definido antes de iniciar o próximo slot.
void main(void){
//declaração das variáveis
char slot;
//funções de inicialização
InicializaDisplay();
InicializaTimer();
for(;;){ //início do loop infinito
//*************** início do top-slot ******************
ResetaTimer(5000); //5 ms para cada slot
AtualizaDisplay();
//**************** fim do top-slot ******************
//*********** início da máquina de estado ************
switch(slot){
case 0:
LeTeclado();
slot = 1;
break;
case 1:
RecebeSerial();
slot = 2;
break;
case 2:
EnviaSerial();
slot = 0;
break;
default:
slot = 0;
break;
}
//************ fim da máquina de estado **************
//************** início do bottom-slot *****************
AguardaTimer();
//*************** fim do bottom-slot *****************
} //fim loop infinito (!?)
}
No exemplo apresentado é inserida a função AguardaTimer() no bottom-slot de modo que a próxima função só executará em 5 (ms).
Como podemos notar, se a função ultrapassar 5 (ms) todo o cronograma será afetado. É necessário então garantir que todo e cada slot será executado em menos de 5 (ms). Isto pode ser ser feito através de testes de bancada.
Com base no código acima e supondo que a tarefa 1 (LeTeclado(), S.1) gasta um tempo de 2.0(ms), a tarefa 2 (RecebeSerial() , S.2) consuma 3.1 (ms), a tarefa (EnviaSerial(), S.3) apenas 1.2 (ms), com um top-slot de 0.5 (ms) e um o bottom-slot de 0.3 (ms) temos a figura abaixo representando a linha temporal de execução do sistema.

Podemos notar que para o ciclo do primeiro slot são gastos 0.5+2.0+0.3 = 2.8(ms). Deste modo o sistema fica aguardando na função AguardaTimer() durante 2.2 (ms) sem realizar nenhum processamento útil. Para o segundo slot temos um tempo livre de (5-(0.5+3.1+0.3))=1.1 (ms). O terceiro slot é o que menos consome tempo de processamento, possuindo um tempo livre de (5-(0.5+1.2+0.3))=3.0 (ms).
Utilização do tempo livre para interrupções
Dependendo do tempo escolhido para o slot e do tempo consumido pela função, podem existir espaços vagos na linha de tempo do processador. A figura abaixo apresenta uma linha de tempo de um sistema que possui apenas 1 slot cujo temporizador foi configurado para 8 (ms).

A cada ciclo “sobram” 3(ms). Este tempo pode ser considerado perdido caso exista apenas a tarefa S.1 no sistema. Nesta situação é até mesmo possível diminuir o tempo do temporizador para 5 (ms). Isto permitiria um acréscimo na frequência de execução da tarefa S.1 que era de 125(Hz) para 200(Hz), ou 60%.
No entanto, não havendo tempo livre, qualquer alteração acabaria com o determinismo além de impossibilitar o uso de interrupção sem atrapalhar a frequência de execução. Portanto, esse tempo livre é importante por dois motivos: evitar que pequenas alterações não esperadas na execução das funções impactem na taxa de execução e, mais importante ainda, permitir que as interrupções aconteçam sem causar problema na temporização das tarefas.
A figura abaixo demonstra o mesmo sistema sendo interrompido através de interrupções assíncronas. Neste caso, a interrupção “rouba” 1(ms) a cada iteração da máquina de estados.

Como cada interrupção gasta um tempo de 1 (ms) e temos um tempo livre de 3 (ms) em cada ciclo, basta garantir que os eventos que geram a interrupção não ultrapassem a frequência de 3 eventos a cada 8 (ms). Esta análise deve ser feita para assegurar que o sistema, apesar de “perder” tempo de processamento, tenha um modo simples de garantir o determinismo na execução das funções.
O “perder” está entre aspas pois o tempo que o processador fica aguardando no bottom-slot é uma ótima oportunidade para colocar o sistema em baixo consumo de energia e permitir que ele seja acordado com a interrupção do timer. Indo por este caminho de economia de energia, o objetivo passa de “ter uma alta taxa de execução sem tempo ocioso” para “possuir o máximo de tempo livre”, reduzindo efetivamente o consumo.









Muito bom o artigo. Pena que as imagens deixaram de aparecer no artigo, testei em 2 navegadores e não foi possível visualizá-las. Obrigado.
Muito bom Rodrigo Parabéns! Ficamos no aguardo dos próximos.
Muito bom Rodrigo!! Quais seriam as referencias bibliográficas dessa série de artigos?
Obrigado Marcelo!
Infelizmente não tenho um livro que fale especificamente sobre arquiteturas para lhe indicar. Boa parte das referências e exemplos que utilizei tirei de códigos fonte no github ou de projetos. Alguns livros interessantes e que comentam um pouco sobre isso são o “The art of programming embedded systems” do Jack Ganssle, e o Embedded systems design and applications with the 68HC12 and HCS12 do Steven Barrett.
Obrigado Rodrigo pelas referencias passadas. Gostei muito da abordagem que foi dada ao a assunto e a organização lógica. Novamente Parabéns!
Muito bom Rodrigo!
Gostei muito dessa série artigos, acho que seria sensacional esse tipo de abordagem nas universades afim de demonstrar diferentes visões de arquitetura de sw para embarcados. E não apenas o tradicional while(1);
Abs.
Felipe
Até o tradicional while(1) eu não gosto muito, parece que o cara quis fazer um while(i) mas digitou errado. Os meus alunos gostam mesmo é quando eu uso forever. =)
for(;;) //ever
{
}
Excelente material! Parabéns!
muito bom
Obrigado Mateus!