Estruturação de dados e mensagens entre camadas para iniciantes

Confira o conceito de estruturação de dados e como utilizar esse recurso para uma melhor comunicação de mensagens entre as camadas de software embarcado.
estruturação de dados

Em se tratando de programação de software embarcado, conforme manda o bom senso e a experiência, a organização é tudo. A fim de organizar melhor dados e informações importantes, recorrer à estruturação de dados sempre é uma boa ideia quando se busca um código enxuto e eficaz.

Sendo assim, este artigo tem como objetivo explicar o que é estruturação básica de dados (desde as definições formais até a “mão na massa”) e como utilizar esse recurso para uma melhor comunicação/troca de mensagens entre as camadas de um software embarcado.

Pré-requisitos

Para compreender de forma satisfatória este artigo, é necessário:

  • Saber como declarar variáveis locais e globais;
  • Conhecer os tipos básicos de variáveis (int, char, short, float, etc.) em C++;
  • Ter noção de funções (passagem de parâmetros e retorno de dados);
  • Saber o que é debouncing.

Além disso, recomendo fortemente a leitura de meu artigo anterior, Arquitetura de software em camadas para iniciantes, pois assim ficará mais familiar o conceito de camadas em uma arquitetura de software embarcado, conceito o qual irei me referir neste artigo.

Como o foco deste artigo é para quem está começando nos softwares embarcados (além da conhecida abrangência do Arduino entre os que estão iniciando seus estudos e trabalho em softwares embarcados), todos os exemplos feitos aqui serão para Arduino.

Afinal, o que é estruturação de dados?

Para me ajudar a responder essa pergunta, irei recorrer a uma situação rotineira: fazer uma viagem.

Ao fazer uma viagem, qualquer um de nós leva coisas conosco. Essas coisas podem ser de qualquer tipo: roupas, sapatos, equipamentos eletrônicos, chapéu, acessórios, etc. Como não seria nada prático carregar cada item isoladamente conosco (ou, do mesmo modo, colocar cada coisa em uma sacola tornaria o transporte tão complicado quanto), nós colocamos tudo em compartimentos grandes, ou seja, colocamos os objetos em malas e mochilas. Fazemos isso pois assim poderemos nos deslocar movendo todos os itens da viagem somente movendo um (ou alguns) compartimentos, pois este(s) contém tudo o que precisamos.

Pois bem, com dados/variáveis em softwares embarcados, a situação é exatamente a mesma! No desenvolvimento de softwares embarcados, as coisas que levamos na viagem são dados/variáveis, as malas/mochilas são estruturas de dados e a viagem em si é o transporte de dados entre uma camada do software embarcado para outra.

O mesmo ocorre quanto ao tamanho deste pacote de dados. Na vida cotidiana, o peso total desta mala/mochila é a somatória do peso de cada coisa dentro dela. No software embarcado, o tamanho (bytes) de um pacote de dados é igual à soma dos dados/variáveis que nele constam.

Em suma, estruturar dados é organizá-los de forma que seja possível enviá-los ou recebê-los de outras camadas de uma só vez, em um único pacote de dados.

Como estruturar dados?

Para isto, os dados são agrupados na forma de estrutura. Dentro da estrutura declaramos as variáveis que desejamos agrupar. Um exemplo de como declarar e utilizar uma estrutura pode ser visto abaixo:

//esqueleto da estrutura (define os dados que estão contidos nesta estrutura)
struct Cadastro {       
    char Nome[50];       //50 bytes
    char Endereco[50];   //50 bytes
    char Telefone[10];   //10 bytes      
    int DiaNascimento;   //2 bytes 
    int MesNascimento;   //2 bytes 
    int AnoNascimento;   //2 bytes 
};  
   
void setup() {
    Serial.begin(9600);
}


void loop() {
    //variável local que contém dentro de si todos os elementos da estrutura
    struct Cadastro VariavelDeCadastro;  //tal variável possui tamanho de 116 bytes
                                        
    //preenchimento dos elementos da variável de estrutura
    sprintf(VariavelDeCadastro.Nome, "Pedro Bertoleti"); 
    sprintf(VariavelDeCadastro.Endereco, "Rua teste, numero 123 - São Paulo-SP");     
    VariavelDeCadastro.DiaNascimento=8;
    VariavelDeCadastro.MesNascimento=8;
    VariavelDeCadastro.AnoNascimento=1986;
    
    Serial.println("Nome: ");
    Serial.println(VariavelDeCadastro.Nome);
    Serial.println("Endereço: ");
    Serial.println(VariavelDeCadastro.Endereco);
    Serial.println("Dia do nascimento: ");
    Serial.println(VariavelDeCadastro.DiaNascimento);
    Serial.println("Mês do nascimento: ");
    Serial.println(VariavelDeCadastro.MesNascimento);
    Serial.println("Ano do nascimento: ");    
    Serial.println(VariavelDeCadastro.AnoNascimento);
}

No exemplo acima, nota-se que a variável “VariavelDeCadastro”, feita a partir do esqueleto/struct “Cadastro” manipula seus elementos/campos através de um ponto (NOMEDAVARIAVEL.CAMPODESEJADO). Assim, é possível ler o conteúdo dos elementos ou escrever nos mesmos como se faz com variáveis convencionais.

A saída de dados no vista no Serial Monitor é contínua (são repetidos os mesmos dados indefinidamente), e um fragmento da saída é exibido na figura abaixo:

Estruturação de dados - Saída no Serial Monitor
Figura 1 – Saída de dados no Serial Monitor

Neste contexto, a variável “VariavelDeCadastro” pode ser considerada como um pacote de dados, e como qualquer outro tipo de variável, este pacote pode ser passado como parâmetro para outra função. Para ilustrar a passagem de parâmetros deste tipo, observe o exemplo abaixo. Ele tem exatamente o mesmo objetivo do exemplo 1, porém nele é mostrada como é feita a passagem de parâmetros onde o parâmetro é um pacote de dados.

//esqueleto da estrutura (define os dados que estão contidos nestaestrutura
struct Cadastro {       
    char Nome[50];       //50 bytes
    char Endereco[50];   //50 bytes
    char Telefone[10];   //10 bytes      
    int DiaNascimento;   //2 bytes 
    int MesNascimento;   //2 bytes 
    int AnoNascimento;   //2 bytes 
};  
   
void setup() {
    Serial.begin(9600);
}

//loop(), neste exemplo, equivale à camada menos especialista
void loop() {
    //variável local que contém dentro de si todos os elementos da estrutura
    struct Cadastro VariavelDeCadastro;  //tal variável possui tamanho de 116 bytes
                                         
    //preenchimento dos elementos da variável de estrutura (pacote de dados)
    sprintf(VariavelDeCadastro.Nome, "Pedro Bertoleti"); 
    sprintf(VariavelDeCadastro.Endereco, "Rua teste, numero 123 - São Paulo-SP");     
    VariavelDeCadastro.DiaNascimento=8;
    VariavelDeCadastro.MesNascimento=8;
    VariavelDeCadastro.AnoNascimento=1986;

    //todo o pacote de dados é passado de uma única vez para a camada mais especialista	
    EnviaCamposCadastroParaSerial(VariavelDeCadastro);      
}

//EnviaCamposCadastroParaSerial() trata-se da camada mais especialista
void EnviaCamposCadastroParaSerial(struct Cadastro CadastroRecebido)
{
    
    Serial.println("Nome: ");
    Serial.println(CadastroRecebido.Nome);
    Serial.println("Endereço: ");
    Serial.println(CadastroRecebido.Endereco);
    Serial.println("Dia do nascimento: ");
    Serial.println(CadastroRecebido.DiaNascimento);
    Serial.println("Mês do nascimento: ");
    Serial.println(CadastroRecebido.MesNascimento);
    Serial.println("Ano do nascimento: ");    
    Serial.println(CadastroRecebido.AnoNascimento);
}

Desta forma, com uma única passagem de parâmetros podem ser passadas N variáveis, o que deixa o código mais limpo e fácil de ser entendido do que se fossem passadas as variáveis uma a uma. No exemplo dado, foram passados 6 parâmetros em um só parâmetro, o pacote de dados.

Outro fato interessante é que, neste caso, o software foi dividido em camadas, sendo a camada mais especialista a função EnviaCamposCadastroParaSerial() e a camada menos especialista a própria função loop(). Logo, pode-se observar que uma mensagem pode ser passada de uma camada a outra de forma limpa, enxuta e eficaz.

Muito interessante! Mas o que mais é possível fazer com dados estruturados?

A resposta desta pergunta é: o que você conseguir imaginar. A estruturação de dados é um recurso muito poderoso e, com criatividade, é possível resolver problemas complexos com a estruturação de dados adequada. Citarei a seguir um outro exemplo de aplicação de estruturação de dados aplicado a um caso muito comum a nós desenvolvedores (profissionais ou hobbyistas): mostrar telas diferentes em um display.

Embora seja uma simples tarefa, dependendo da complexidade do software embarcado que está sendo desenvolvido, pode ser necessário usar vários tipos telas para exibição num display. Isto ocorre pois, na grande maioria dos produtos embarcados que possuem display, há diversos tipos de telas a serem desenhadas. Isto pode parecer simples no começo, mas à medida que o projeto evolui, pode se tornar uma dor de cabeça desenhar vários tipos de telas toda hora, assim como pode tornar o código extenso e “gastão” (muitos trechos repetidos fazendo a mesma tarefa).

Considerando este caso, irei mostrar um exemplo de como estruturar telas. Ou seja, todos os dados que serão utilizados para a desenhar a tela estarão em um só pacote de dados, e a tela é desenhada em uma única função (camada mais especialista de display). Esta função irá ser responsável por interpretar os dados do pacote de dados e desenhar a tela conforme é solicitado. Há também uma camada especialista de teclado, responsável por fazer a varredura de teclado (com debouncing) e retornar à camada menos especialista a tecla pressionada. Desta forma, além de utilizar de arquitetura de software em camadas e fazer bom uso da estruturação de dados, tem-se economia de código.

Para fazer este exemplo, foi utilizado:

  • Arduino Duemilanove;
  • Shield de display LCD com teclado integrado (exatamente igual este).

O exemplo funciona da seguinte maneira: primeiramente, é exibida uma tela inicial (chamada de Splash Screen). A seguir, é consultado continuamente qual tecla foi pressionada e para cada tela há uma ação correspondente. As ações para cada tecla pressionada são exibidas abaixo:

  • Tecla up:  mostra uma tela com um texto e faz scroll para esquerda;
  • Tecla down: mostra uma tela com texto que pisca 5 vezes;
  • Tecla left: mostra uma tela com texto simples/estático;
  • Tecla right: mostra uma tela com um texto e faz scroll para direita.

Uma foto do Arduino ligado ao Shield pode ser vista abaixo:

Estruturacao de dados - Arduino com shield
Figura 2 – Arduino com shield

O código do exemplo pode ser visto abaixo.

Observação: O debouncing do teclado possui uma rotina bem versátil (e não tão intuitiva), a qual já utilizei com sucesso antes neste tipo de teclado (por isso a utilizei aqui). Portanto, preste atenção nos defines do código que dizem respeito ao debouncing para ajustá-lo conforme sua necessidade.

#include <LiquidCrystal.h>;

// --------------------------------------
// Defines
//---------------------------------------
//defines que serão úteis para a camada menos especialista (loop) na identificação do botão pressionado
#define TECLA_ESQUERDA                 1
#define TECLA_DIREITA                  2
#define TECLA_CIMA                     3
#define TECLA_BAIXO                    4
#define NENHUMA_TECLA_PRESSIONADA      0

//define do tempo de debouncing das teclas (em microssegundos)
#define TEMPO_DEBOUNCING               500    //quanto maior, melhor o debouncing e mais lenta a leitura
#define NUMERO_VARREDURAS             20      //quanto maior, melhor o debouncing e mais lenta a leitura

//defines gerais
#define SIM                           1
#define NAO                           0

// --------------------------------------
// Estruturas
//---------------------------------------

//"esqueleto" do pacote de dados que contém informações de como as telas são compostas
struct TelaLCD
{
    char PrimeiraLinha[16];        //texto a ser exibido na primeira linha
    char SegundaLinha[16];         //texto a ser exibido na segunda linha
    char RotacionaParaDireita;     //informa se os textos informados devem ser rotacionados para a direita
    char RotacionaParaEsquerda;    //informa se os textos informados devem ser rotacionados para a direita    
    char DevePiscar;               //informa se o display deve piscar
    char NumeroDeVezesParaPiscar;  //informa o número de vezes que o display deve piscar 
};

// --------------------------------------
// Variáveis globais
//---------------------------------------
LiquidCrystal Lcd16x2(8, 9, 4, 5, 6, 7); // Cria um LCD objeto com estes pinos (padrão do Shield LCD com teclado

// --------------------------------------
// Inicializações de hardware e software 
//---------------------------------------
void setup() {  
  //inicializações do LCD
  Lcd16x2.begin(16, 2); //seta o tipo de display como sendo de 16 colunas por 2 linhas
  Lcd16x2.display(); //liga o display LCD
  Lcd16x2.clear();  //limpa o display
  
  //mostra splashscreen
  MostraSplashScreen();
}

// ----------------------------------------------
// Programa principal (camada menos especialista
//-----------------------------------------------
void loop() {    
    char TeclaLida;  

    TeclaLida = LeTeclado();
    ExibeTelaSolicitada(TeclaLida);  //exibe tela de acordo com a tecla pressionada    
}
 
// ----------------------------------------------------
// Camada mais especialista de display (desenha a tela) 
//-----------------------------------------------------
void DesenhaTela(struct TelaLCD TelaSolicitada) {
    int i;
    
    Lcd16x2.clear();              //limpa display
    Lcd16x2.setCursor(0,0);       //posiciona cursor para escrever primeira linha
    Lcd16x2.print(TelaSolicitada.PrimeiraLinha);
    Lcd16x2.setCursor(0,1);       //posiciona cursor para escrever segunda linha
    Lcd16x2.print(TelaSolicitada.SegundaLinha);
    
    //se deve rotacionar para direita, faz o scroll
    if (TelaSolicitada.RotacionaParaDireita == SIM)
    {
        for(i=0; i<16; i++)
        {
           delay(500);  
           Lcd16x2.scrollDisplayRight();             
        }  
    }
    
    //se deve rotacionar para direita, faz o scroll
    if (TelaSolicitada.RotacionaParaEsquerda == SIM)
    {
        for(i=0; i<16; i++)
        {
            delay(500);
            Lcd16x2.scrollDisplayLeft(); 
        }  
    }
    
    //se deve piscar, pisca o numero de vezes solicitada
    if (TelaSolicitada.DevePiscar == SIM)
    {
        for(i=0; i<TelaSolicitada.NumeroDeVezesParaPiscar; i++)
        {
             Lcd16x2.clear();              //limpa display
             Lcd16x2.setCursor(0,0);       //posiciona cursor para escrever primeira linha
             Lcd16x2.print(TelaSolicitada.PrimeiraLinha);
             Lcd16x2.setCursor(0,1);       //posiciona cursor para escrever segunda linha
             Lcd16x2.print(TelaSolicitada.SegundaLinha); 
             delay(500);
             
             Lcd16x2.clear();              //limpa display
             delay(500);
        }
    }
    
}

// ----------------------------------------------------
// Camada mais especialista de teclado (faz a leitura
// do teclado). Retorna um valor numérico correspondente
// a tecla pressionada (conforme defines deste software)
// Esta camada faz também o debouncing do teclado.
//-----------------------------------------------------
char LeTeclado(void)
{
    int LeituraAnalogica; //como o teclado deste shield utiliza associação de resistores, é feita uma leitura analógica para se determinar qual tecla foi acionada      
    int TeclasLidasNasVarreduras[NUMERO_VARREDURAS];  //armazena o valor das varreduras de teclado
    int TeclaLida;   //armazena o valor da tecla lida (após o debouncing)
    int i;
    
    //varredor de teclado
    for(i=0; i<NUMERO_VARREDURAS; i++)
    {   
       LeituraAnalogica = analogRead(0);   //le o valor resultante da conversão da tensão pelo ADC de 10 bits (0..1023, para um range de tensão de 0V..5V)
       
       if (LeituraAnalogica < 100) 
       {  
          //tecla direita pressionada  
          TeclasLidasNasVarreduras[i] = TECLA_DIREITA;  
       }  
       else if (LeituraAnalogica < 200) {  
         //tecla cima pressionada 
         TeclasLidasNasVarreduras[i] = TECLA_CIMA;  
       }  
       else if (LeituraAnalogica < 400){
         //tecla baixo pressionada  
         TeclasLidasNasVarreduras[i] = TECLA_BAIXO;    
       }  
       else if (LeituraAnalogica < 600){  
         //tecla esqueda pressionada
         TeclasLidasNasVarreduras[i] = TECLA_ESQUERDA;    
       }  
   
       //Se já foi feita alguma varredura e, nela, a tecla pressionada deu diferente da tecla lida nesta varredura, não houve leitura satisfatória
       if ((i > 0) && (TeclasLidasNasVarreduras[i] != TeclasLidasNasVarreduras[i-1]))
           return NENHUMA_TECLA_PRESSIONADA;
       
       //aguarda antes de realizar a segunda varredura (tempo para a tecla parar de trepidar)
       delayMicroseconds(TEMPO_DEBOUNCING);
    }
    
    return TeclasLidasNasVarreduras[0];  //retorna a tecla lida na primeira varredura (que, obrigatoriamente, foi igual a todas as outras leituras
}

// ----------------------------------------------------
// Exibe um splashscreen por 3 segundos
//-----------------------------------------------------
void MostraSplashScreen(void)
{
    struct TelaLCD  SplashScreenProjeto;  

    sprintf(SplashScreenProjeto.PrimeiraLinha,"  Telas no LCD  ");
    sprintf(SplashScreenProjeto.SegundaLinha, "Usando Arduino ");
    SplashScreenProjeto.RotacionaParaDireita  = NAO;
    SplashScreenProjeto.RotacionaParaEsquerda = NAO;
    SplashScreenProjeto.DevePiscar = NAO;
    SplashScreenProjeto.NumeroDeVezesParaPiscar = 0;

    //solicita que a tela informada seja exibida
    DesenhaTela(SplashScreenProjeto);             
    delay(3000);
}

// ----------------------------------------------------
// Exibe a tela dependendo da tecla pressionada.
// Corresponde a uma camada intermediária.
//-----------------------------------------------------
void ExibeTelaSolicitada(char TeclaLida)
{
    struct TelaLCD  TelaParaMostrar;   //pacote de dados contendo todas as informações para montar a tela 
  
    //dependendo da tecla lida, mostra um determinado tipo de tela
    switch (TeclaLida)
    {
         case TECLA_ESQUERDA:    //solicita exibição de uma tela simples
             //informa todos os dados do pacote de informações da tela
             sprintf(TelaParaMostrar.PrimeiraLinha,"Tela estatica   ");
             sprintf(TelaParaMostrar.SegundaLinha, "LCD 16x2     ");
             TelaParaMostrar.RotacionaParaDireita  = NAO;
             TelaParaMostrar.RotacionaParaEsquerda = NAO;
             TelaParaMostrar.DevePiscar = NAO;
             TelaParaMostrar.NumeroDeVezesParaPiscar = 0;

             //solicita que a tela informada seja exibida
             DesenhaTela(TelaParaMostrar);             
             break;  
         case TECLA_DIREITA:    //solicita exibição de uma tela com scroll para direita
             //informa todos os dados do pacote de informações da tela
             sprintf(TelaParaMostrar.PrimeiraLinha,"Feito por:");
             sprintf(TelaParaMostrar.SegundaLinha, "Pedro Bertoleti");
             TelaParaMostrar.RotacionaParaDireita  = SIM;
             TelaParaMostrar.RotacionaParaEsquerda = NAO;
             TelaParaMostrar.DevePiscar = NAO;
             TelaParaMostrar.NumeroDeVezesParaPiscar = 0;

             //solicita que a tela informada seja exibida
             DesenhaTela(TelaParaMostrar);             
             break;  
         case TECLA_CIMA:      //solicita exibição de uma tela com scroll para esquerda
             //informa todos os dados do pacote de informações da tela
             sprintf(TelaParaMostrar.PrimeiraLinha,"Estruturacao de");
             sprintf(TelaParaMostrar.SegundaLinha, "Dados");
             TelaParaMostrar.RotacionaParaDireita  = NAO;
             TelaParaMostrar.RotacionaParaEsquerda = SIM;
             TelaParaMostrar.DevePiscar = NAO;
             TelaParaMostrar.NumeroDeVezesParaPiscar = 0;

             //solicita que a tela informada seja exibida
             DesenhaTela(TelaParaMostrar);             
             break;  
         case TECLA_BAIXO:         //solicita exibição de uma tela que pisca 5 vezes 
             //informa todos os dados do pacote de informações da tela
             sprintf(TelaParaMostrar.PrimeiraLinha,"Embarcados");
             sprintf(TelaParaMostrar.SegundaLinha, "----------");
             TelaParaMostrar.RotacionaParaDireita  = NAO;
             TelaParaMostrar.RotacionaParaEsquerda = NAO;
             TelaParaMostrar.DevePiscar = SIM;
             TelaParaMostrar.NumeroDeVezesParaPiscar = 5;

             //solicita que a tela informada seja exibida
             DesenhaTela(TelaParaMostrar);             
             break;  
    }  
}

Conforme pode ser visto no exemplo, o software desenvolvido possui três camadas: uma camada menos especialista (representada pelas funções loop() e MostraSplashScreen()), uma camada intermediária (representada pela função ExibeTelaSolicitada()) e, por fim, uma camada mais especialista (representada pelas funções DesenhaTela() e LeTeclado()). Logo, as camadas estão isoladas. Ou seja, se for necessário substituir o display ou teclado, basta alterar as camadas mais especialistas respectivas e o software funcionará normalmente.

Nota-se neste exemplo que toda exibição de tela no display é feita por meio de dados estruturados, logo com apenas uma função é possível desenhar a tela do jeito que quiser.

Conclusão

A estruturação de dados é um recurso muito importante na programação de software embarcado, pois permite condensar em somente uma variável vários tipos de dados diferentes, além de permitir a passagem de todos eles de uma só vez a outras camadas de software.

Conforme visto no último exemplo, se bem utilizada, a estruturação de dados permite que somente uma função construa algo conforme os parâmetros da estrutura passada como parâmetro, o que garante economia de código-fonte e uma melhor organização (já que se um bug for descoberto nesta função, basta corrigi-la e a correção será aplicada a todos os pontos do código em que a função é chamada).

Saiba mais sobre arquitetura de software em sistemas embarcados

Arquitetura de Software em Sistemas Embarcados

Arquitetura de software em camadas para iniciantes

Arquitetura de desenvolvimento de software

Notificações
Notificar
guest
1 Comentário
recentes
antigos mais votados
Inline Feedbacks
View all comments
Rafael Dias
Rafael Dias
11/08/2015 22:59

muito bom. a escolha de uma boa arquitetura de software, dos abstracts data types (ADTs) e as estruturas de dados é que podem determinar se um projeto pode ser extensível e mantenível com o passar do tempo. É importante sempre olharmos para estes aspectos e não cair na armadilha de escrever um firmware/software que serve somente para um projeto,não podendo reutilizar as estruturas de software em outro contexto. Ah, só uma observação… Acho que vc passou uma struct como parâmetro de algumas funções somente para fins didáticos… O recomendado para estruturas grandes é de se passar somente uma referência (ou… Leia mais »

WEBINAR

Visão Computacional para a redução de erros em processos manuais

DATA: 23/09 ÀS 17:00 H