Ao fazer um projeto utilizando sistemas embarcados, deve-se garantir sobretudo que as chances de travamento e falhas sejam as menores possíveis. Afinal, um sistema embarcado normalmente está envolvido em operações em que funcionam por anos a fio, podendo ser altamente críticas ou não. Para conseguir projetar um sistema embarcado mais imune a erros e falhas, uma ótima pedida é isolar as funcionalidades e fazer com que uma não interfira diretamente na outra.
Uma forma de fazer isso é utilizar a abordagem de escalonamento / agendamento de tarefas e um núcleo principal. Este artigo fala sobre um projeto nesta abordagem: TickAttack. O TickAttack trata-se de um gerenciador de tarefas simples a ser aplicado, com ótimo desempenho, a um microcontrolador ARM Cortex-M0.
Definição do TickAttack
A ideia original veio de um projeto mais simples, de minha autoria, destinado ao uso em microcontroladores Microchip PIC de 8 bits. Tal projeto e artigo podem ser vistos em detalhes neste link. Tal projeto consistia no desenvolvimento de um núcleo (que nada mais é que um simples Kernel cooperativo) e tarefas diversas, executadas segundo temporização das mesmas. Observe o diagrama do núcleo / Kernel e tarefas na figura 1.
O TickAttack, portanto, consiste em uma portabilidade do gerenciador em questão. O “Tick” do nome veio do seguinte: pelo fato do gerenciador ser orientado à temporização (utilizando das interrupções de Timer, os Timer Ticks, como mecanismo de disparo das tarefas).
O TickAttack conta com as seguintes melhorias e modificações em relação ao gerenciador de tarefas original:
- Melhor encapsulamento de funções / métodos e variáveis do Kernel;
- Foi acrescentado um mecanismo para melhor gerenciar dados de memória compartilhada entre tarefas;
- Foi acrescentado um mecanismo para medir o tempo gasto (em ms) de cada tarefa, destinado a avaliações de desempenho do TickAttack;
- Preparado para uso em microcontroladores ARM Cortex-M0 (no caso, a primeira versão foi preparada para microcontroladores STM32F0).
Em suma, o TickAttack trata-se de uma versão melhorada do gerenciador.
Kernel – TickAttack
Em termos de mecanismo / forma de funcionamento, quase nada mudou em relação ao gerenciador de tarefas original. O que merece destaque / sofreu modificações é de onde são “tirados” os Timers Ticks e como o Kernel organiza seus métodos e variáveis e as encapsula.
No TickAttack, o Timer Tick é exatamente o SysTick. O SysTick consiste em um Timer Tick gerado a cada milissegundo. O interessante é que este Timer Tick já é previsto em vários HAL (Hardware Abstract Layer) de vários fabricantes de microcontroladores ARM, o que leva a uma economia dos Timers disponíveis no microcontrolador (já que o SysTick não utiliza nenhum dos Timers disponíveis ao programador / desenvolvedor / de uso geral).
Outra informação interessante é que os RTOS de mercado normalmente utilizam o SysTick, o que dá indícios que o TickAttack está no caminho certo.
No TickAttack, tudo que é relevante para o Kernel funcionar está agora encapsulado num único “pacote” (chamado aqui de SetupGerenciadorDeTarefas), facilitando seu uso e isolando-o melhor das demais variáveis de escopo global do projeto. Observe o Header File do Kernel / gerenciador de tarefas do TickAttack:
//Módulo: Header do gerenciador de tarefas
//Descrição: Header do módulo responsável por fazer o gerenciamento de todas as tarefas do firmware.
// A execução das tarefas é orientada à tempozização, compondo um kernel cooperativo.
//includes
#include "stm32f0xx_hal.h"
#ifndef GERENCIADORTAREFAS_H
#define GERENCIADORTAREFAS_H
//defines gerais
#define SIM 1
#define NAO 0
#define INDEX_TAREFA_1 0
#define INDEX_TAREFA_2 1
#define INDEX_TAREFA_3 2
#define INDEX_TAREFA_4 3
#define INDEX_TAREFA_5 4
#define NUMERO_DE_TAREFAS 5
//tempos de execução de tarefas arbitrários. Deve-se modificar para o uso ao qual for destinado.
#define TEMPO_PARA_EXECUTAR_TAREFA1 500 //ms
#define TEMPO_PARA_EXECUTAR_TAREFA2 500 //ms
#define TEMPO_PARA_EXECUTAR_TAREFA3 500 //ms
#define TEMPO_PARA_EXECUTAR_TAREFA4 500 //ms
#define TEMPO_PARA_EXECUTAR_TAREFA5 500 //ms
#define TIMEOUT_DE_TAREFA 1000 //ms
//typedefs
//ponteiro de função (utilizado para executar uma tarefa)
typedef void (*pFunc)(void);
typedef struct
{
unsigned int TempoExecutarTask;
unsigned int TempoDeExecucaoTarefas[NUMERO_DE_TAREFAS]; //Tempos em que cada tarefa deve ser executada
unsigned int TempoParaExecutarTarefas[NUMERO_DE_TAREFAS]; //Tempos atuais de execução das tarefas (periodicidade)
unsigned long TempoGastoPorTarefa[NUMERO_DE_TAREFAS]; //Tempo (em ms) gasto por cada tarefa
unsigned long int HaTarefaEmExecucao; //indica se há alguma tarefa em execução
unsigned long int TimeoutDaTarefa; //contabiliza o tempo de execução da tarefa corrente (para gerenciar timeout)
pFunc TarefasAgendadas[NUMERO_DE_TAREFAS]; //Ponteiros de funções (um para cada tarefa)
}TSetupGerenciadorDeTarefas;
#ifdef DEF_GERENCIADORTAREFAS
#define GERTAR /**/
#else
#define GERTAR extern
#endif
//variáveis globais
GERTAR TSetupGerenciadorDeTarefas SetupGerenciadorDeTarefas; //"objeto" de setup do gerenciador de tarefas
#endif
//prototypes globais
void IniciaGerenciadorTarefas(void);
void ExecutaTarefa(void);
Agora, o código-fonte (.c) do Kernel / gerenciador de tarefas:
//Módulo: Gerenciador de tarefas
//Descrição: Módulo responsável por fazer o gerenciamento de todas as tarefas do firmware.
// A execução das tarefas é orientada à tempozização, compondo um kernel cooperativo.
//include guard
#define DEF_GERENCIADORTAREFAS
//includes
#include "GerenciadorTarefas.h"
#include "stm32f0xx_hal.h"
#include "DadosCompartilhados.h"
//typedefs
//variáveis locais
//variaveis externas
//prototypes locais
void Tarefa1(void);
void Tarefa2(void);
void Tarefa3(void);
void Tarefa4(void);
void Tarefa5(void);
unsigned long ObtemTick(void);
unsigned long TempoGastoTarefa(unsigned long TickInicialTarefa, unsigned long TickFinalTarefa);
//implementações:
//Função: inicialização do gerenciador de tarefas
//Descrição: faz a inicialização do gerenciador de tarefas: escaloma as tarefas
// e suas respectivas temporizações. Além disso, configura o timeout
// da execução de tarefas também.
// IMPORTANTE: todas as tarefas devem ter parÂmetros e retorno do tipo void
//Parâmetros: nenhum
//Retorno: nenhum
void IniciaGerenciadorTarefas(void)
{
//Inicialização dos ponteiros de funções (tarefas)
SetupGerenciadorDeTarefas.TarefasAgendadas[INDEX_TAREFA_1] = Tarefa1;
SetupGerenciadorDeTarefas.TarefasAgendadas[INDEX_TAREFA_2] = Tarefa2;
SetupGerenciadorDeTarefas.TarefasAgendadas[INDEX_TAREFA_3] = Tarefa3;
SetupGerenciadorDeTarefas.TarefasAgendadas[INDEX_TAREFA_4] = Tarefa4;
SetupGerenciadorDeTarefas.TarefasAgendadas[INDEX_TAREFA_5] = Tarefa5;
//Inicialização dos tempos de execução de cada tarefa
SetupGerenciadorDeTarefas.TempoDeExecucaoTarefas[INDEX_TAREFA_1] = TEMPO_PARA_EXECUTAR_TAREFA1;
SetupGerenciadorDeTarefas.TempoDeExecucaoTarefas[INDEX_TAREFA_2] = TEMPO_PARA_EXECUTAR_TAREFA2;
SetupGerenciadorDeTarefas.TempoDeExecucaoTarefas[INDEX_TAREFA_3] = TEMPO_PARA_EXECUTAR_TAREFA3;
SetupGerenciadorDeTarefas.TempoDeExecucaoTarefas[INDEX_TAREFA_4] = TEMPO_PARA_EXECUTAR_TAREFA4;
SetupGerenciadorDeTarefas.TempoDeExecucaoTarefas[INDEX_TAREFA_5] = TEMPO_PARA_EXECUTAR_TAREFA5;
//Inicializa tempo restante para cada tarefa executar
SetupGerenciadorDeTarefas.TempoParaExecutarTarefas[INDEX_TAREFA_1] = TEMPO_PARA_EXECUTAR_TAREFA1;
SetupGerenciadorDeTarefas.TempoParaExecutarTarefas[INDEX_TAREFA_2] = TEMPO_PARA_EXECUTAR_TAREFA2;
SetupGerenciadorDeTarefas.TempoParaExecutarTarefas[INDEX_TAREFA_3] = TEMPO_PARA_EXECUTAR_TAREFA3;
SetupGerenciadorDeTarefas.TempoParaExecutarTarefas[INDEX_TAREFA_4] = TEMPO_PARA_EXECUTAR_TAREFA4;
SetupGerenciadorDeTarefas.TempoParaExecutarTarefas[INDEX_TAREFA_5] = TEMPO_PARA_EXECUTAR_TAREFA5;
//nenhuma tarefa está em execução no momento
SetupGerenciadorDeTarefas.TimeoutDaTarefa = NAO;
}
//Função: Execução de tarefa
//Descrição: verifica se deve executar alguma tarefa. Em caso positivo, a execução é feita
//Parâmetros: nenhum
//Retorno: nenhum
void ExecutaTarefa(void)
{
long i;
unsigned long TickInicialTarefa;
unsigned long TickFinalTarefa;
for (i=0; i<NUMERO_DE_TAREFAS; i++)
{
//verifica se está na hora de executar alguma tarefa
if ((SetupGerenciadorDeTarefas.TarefasAgendadas[i] != 0) && (SetupGerenciadorDeTarefas.TempoParaExecutarTarefas[i] == 0))
{
//obtem o valor do tick
TickInicialTarefa = ObtemTick();
//executa a tarefa
SetupGerenciadorDeTarefas.HaTarefaEmExecucao = SIM;
SetupGerenciadorDeTarefas.TimeoutDaTarefa = TIMEOUT_DE_TAREFA;
SetupGerenciadorDeTarefas.TarefasAgendadas[i](); //executa a tarefa agendada
SetupGerenciadorDeTarefas.HaTarefaEmExecucao = NAO;
SetupGerenciadorDeTarefas.TempoParaExecutarTarefas[i] = SetupGerenciadorDeTarefas.TempoDeExecucaoTarefas[i]; //reagendamento da tarefa
//contabiliza tempo de execução da tarefa (em ms)
TickFinalTarefa = ObtemTick();
SetupGerenciadorDeTarefas.TempoGastoPorTarefa[i] = TempoGastoTarefa(TickInicialTarefa,TickFinalTarefa);
}
}
}
/*
* As tarefas e número de tarefas aqui colocadas são arbitrários.
* Estes devem ser modificados para o uso desejado.
*/
//Função: tarefa 1
//Descrição: contém rotinas da tarefa 1
//Parâmetros: nenhum
//Retorno: nenhum
void Tarefa1(void)
{
}
//Função: tarefa 2
//Descrição: contém rotinas da tarefa 2
//Parâmetros: nenhum
//Retorno: nenhum
void Tarefa2(void)
{
}
//Função: tarefa 3
//Descrição: contém rotinas da tarefa 3
//Parâmetros: nenhum
//Retorno: nenhum
void Tarefa3(void)
{
}
//Função: tarefa 4
//Descrição: contém rotinas da tarefa 4
//Parâmetros: nenhum
//Retorno: nenhum
void Tarefa4(void)
{
}
//Função: tarefa 5
//Descrição: contém rotinas da tarefa 5
//Parâmetros: nenhum
//Retorno: nenhum
void Tarefa5(void)
{
}
//Função: obtem valor atual do tick
//Parâmetros: nenhum
//Retorno: valor atual do tick
unsigned long ObtemTick(void)
{
return HAL_GetTick();
}
//Função: calcula tempo gasto numa tarefa
//Parâmetros: tempos inicial e final (em ms) de uma tarefa
//Retorno: tempo gasto numa tarefa (em ms)
unsigned long TempoGastoTarefa(unsigned long TickInicialTarefa, unsigned long TickFinalTarefa)
{
if (TickFinalTarefa > TickInicialTarefa)
return (TickFinalTarefa-TickInicialTarefa);
else
return (0xFFFFFFFF-TickInicialTarefa) + TickFinalTarefa + 1;
}
Mecanismo para gerenciar dados compartilhados entre tarefas
Quando é utilizada a abordagem mais modularizada de um sistema embarcado (como a do gerenciador aqui retratado, modularizado basicamente em núcleo/Kernel cooperativo e tarefas), é de fundamental importância que o acesso a dados em geral (memória RAM) seja controlado. Se a devida atenção a isto não for dada, inúmeros problemas de difícil diagnóstico e/ou solução podem ocorrer, como por exemplo:
- Invasão de memória: variáveis (ou espaços de memória RAM) são indevidamente sobrescritos, afetando todas as tarefas que dependem destes dados;
- Utilização de variáveis globais e locais com mesmo nome: embora pareça absurdo, há compiladores que não “reclamam” (ou que estão com suas configurações de warning em níveis baixos) se for declarada uma variável local e global com mesmo nome. Se isto ocorrer, é confusão na certa: uma tarefa / processo pode “pensar” escrever ou ler numa variável e, na verdade, estar realizando a ação em outra, causando um caos na lógica de seu sistema como um todo;
- Utilização de variáveis de mesmo nome em módulos diferentes: aqui o caso é similar ao anterior, porém com um agravante: por módulos distintos interferirem entre si, o problema / transtorno pode ser bem maior.
Para mais informações sobre controles de acesso à memória RAM, recomendo a leitura do artigo “Porque evitar as variáveis globais e como“, de Lincoln Uehara.
No TickAttack, há também um mecanismo para gerenciar dados que são compartilhados entre as tarefas. Embora muito simples se comparado a alguns de mercado, este garante que problemas comuns em acesso compartilhados não ocorram.
Basicamente, o mecanismo desenvolvido trata-se de gets e sets (funções de leitura e escrita, respectivamente) para informações de uso comum e o encapsulamento das mesmas em uma só estrutura, por fins de organização. Portanto, se você desejar acrescentar ou remover alguma informação de uso comum, será necessário fazer isto no arquivo .h (DadosCompartilhados.h) deste módulo e acrescentar ou remover os gets e sets correspondentes no arquivo .c (DadosCompartilhados.c). Observe o código-fonte neles contidos a seguir:
Arquivo DadosCompartilhados.c:
//Módulo: Dados compartilhados
//Descrição: Módulo de gerenciamento de dados compartilhados entre as tarefas
//include guard
#define DEF_DADOSCOMPARTILHADOS
//includes
#include <string.h>
#include "stm32f0xx_hal.h"
#include "DadosCompartilhados.h"
//defines
//typedefs
//variáveis locais
//variaveis externas
//prototypes locais
//implementações:
//Função: inicialização dos dados compartilhados
//Descrição: inicializa todos os dados compartilhados
//Parâmetros: nenhum
//Retorno: nenhum
void InicializaDadosCompartilhados(void)
{
memset((unsigned char *)&ConjuntoDadosCompartilhados,0,sizeof(TDadosCompartilhados));
}
/*
* Gets e Sets
*/
//Função: Escreve valor de leitura de ADC
//Parâmetros: valor da leitura
//Retorno: nenhum
void SetLeituraADC(long ValorLeitura)
{
ConjuntoDadosCompartilhados.LeituraADC = ValorLeitura;
}
//Função: Le valor da ultima leitura de ADC
//Parâmetros: nenhum
//Retorno: valor da ultima leitura de ADC
long GetLeituraADC(long ValorLeitura)
{
return ConjuntoDadosCompartilhados.LeituraADC;
}
Arquivo DadosCompartilhados.h:
//Módulo: Header dos dados compartilhados
//Descrição: Header dos dados compartilhados entre todas as tarefas
//includes
#include "stm32f0xx_hal.h"
//defines gerais
#ifndef DADOSCOMPARTILHADOS_H
#define DADOSCOMPARTILHADOS_H
//typedefs
typedef struct
{
long LeituraADC;
}TDadosCompartilhados;
#ifdef DEF_DADOSCOMPARTILHADOS
#define DADOSCOMP /**/
#else
#define DADOSCOMP extern
#endif
//variaveis globais
DADOSCOMP TDadosCompartilhados ConjuntoDadosCompartilhados;
#endif
//prototypes globais
void InicializaDadosCompartilhados(void);
void SetLeituraADC(long ValorLeitura);
long GetLeituraADC(long ValorLeitura);
Medição do tempo gasto em cada tarefa
Outra melhoria acrescentada no TickAttack em relação ao gerenciador de tarefas original foi a possibilidade de se medir o tempo gasto em cada tarefa (em ms). Trata-se de um recurso muito interessante, pois reflete diretamente o desempenho do projeto como um todo e, além disso, dá indícios de onde no seu código é preciso melhorar o algoritmo/lógica de programação e codificação.
Analogamente aos outros mecanismos, isto também é feito de maneira simples, porém eficaz:
- Antes da execução de uma tarefa, é armazenado o valor do Tick (Tick Inicial) do Timer que temporiza o gerenciador de tarefas (nesse caso, obtido diretamente do SysTick). Este “Tick” consiste no número de milisegundos que o microcontrolador está executando o software, desde o momento que foi ligado;
- Uma vez terminada a tarefa, outro valor de Tick é requisitado (Tick Final) e a subtração de Tick Final e Tick Inicial define o tempo gasto (em milisegundos) da tarefa em questão.
Em termos de codificação, há um array com N posições, cada uma contendo a última temporização de cada tarefa, permitindo assim acompanhamento em real-time dos tempos de execução.
Este mecanismo já está embutido no código do Núcleo / Kernel cooperativo, sendo que suas partes de destaque são:
//obtem o valor do tick
TickInicialTarefa = ObtemTick();
//executa a tarefa
SetupGerenciadorDeTarefas.HaTarefaEmExecucao = SIM;
SetupGerenciadorDeTarefas.TimeoutDaTarefa = TIMEOUT_DE_TAREFA;
SetupGerenciadorDeTarefas.TarefasAgendadas[i](); //executa a tarefa agendada
SetupGerenciadorDeTarefas.HaTarefaEmExecucao = NAO;
SetupGerenciadorDeTarefas.TempoParaExecutarTarefas[i] = SetupGerenciadorDeTarefas.TempoDeExecucaoTarefas[i]; //reagendamento da tarefa
//contabiliza tempo de execução da tarefa (em ms)
TickFinalTarefa = ObtemTick();
SetupGerenciadorDeTarefas.TempoGastoPorTarefa[i] = TempoGastoTarefa(TickInicialTarefa,TickFinalTarefa);
.....
//Função: obtem valor atual do tick
//Parâmetros: nenhum
//Retorno: valor atual do tick
unsigned long ObtemTick(void)
{
return HAL_GetTick();
}
//Função: calcula tempo gasto numa tarefa
//Parâmetros: tempos inicial e final (em ms) de uma tarefa
//Retorno: tempo gasto numa tarefa (em ms)
unsigned long TempoGastoTarefa(unsigned long TickInicialTarefa, unsigned long TickFinalTarefa)
{
if (TickFinalTarefa > TickInicialTarefa)
return (TickFinalTarefa-TickInicialTarefa);
else
return (0xFFFFFFFF-TickInicialTarefa) + TickFinalTarefa + 1;
}
Código-fonte do projeto
O projeto do TickAttack feito no MDK Keil 5 e já preparado para rodar na evaluation board STM32F072RBDISCOVERY pode ser acessado através de meu GitHub. Para isso, clique aqui. Baixe e use como quiser!
Um exemplo de aplicação
Abaixo, segue um exemplo de aplicação do TickAttack com componentes da própria evaluation board STM32072RBDISCOVERY.
Nele, os quatro LEDs da placa piscam de forma independente (ou seja, cada um é controlado por uma tarefa distinta), sendo que os LEDs alteram seus estados a cada 1000ms (LED superior, vermelho), 500ms (LED inferior, azul), 333ms (LED à esquerda, laranja) e 250ms (LED à direita, verde).

Agradecimentos
Agradeço ao Eder Tadeu, meu amigo e articulista do Embarcados, pela ajuda, revisão, sugestões de implementação e testes do TickAttack.
Referências







Parabéns pelo artigo Pedro e obrigado!
Eder, muito obrigado! Eu que agradeço o apoio, trabalho e atenção!