TickAttack: um gerenciador de tarefas simples para um ARM Cortex-M0

TickAttack

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.

Diagrama do núcleo/Kernel e tarefas do gerenciador de tarefas TickAttack para PIC 8-bits
Figura 1 – Diagrama do núcleo/Kernel e tarefas do gerenciador de tarefas para PIC 8-bits

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:

  1. 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;
  2. 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).

Exemplo de aplicação na eval board STM32072RB rodando o TickAttack
Exemplo de aplicação na eval board STM32072RB

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

TickAttack - gerenciador de tarefas para ARM Cortex-M0

Exemplo de aplicação com TickAttack
Licença Creative Commons Esta obra está licenciada com uma Licença Creative Commons Atribuição-CompartilhaIgual 4.0 Internacional.
Comentários:
Notificações
Notificar
2 Comentários
recentes
antigos mais votados
Inline Feedbacks
View all comments
Eder
Eder
27/07/2017 09:19

Parabéns pelo artigo Pedro e obrigado!

phfbertoleti
phfbertoleti
Reply to  Eder
27/07/2017 13:35

Eder, muito obrigado! Eu que agradeço o apoio, trabalho e atenção!

Home » Hardware » Microcontroladores » TickAttack: um gerenciador de tarefas simples para um ARM Cortex-M0

EM DESTAQUE

WEBINARS

VEJA TAMBÉM

JUNTE-SE HOJE À COMUNIDADE EMBARCADOS

Talvez você goste: