Monitoramento de água com IoT - Parte 2

monitoramento de água
Este post faz parte da série Monitoramento de água com IoT. Leia também os outros posts da série:

Esta é a segunda parte da série de artigos que descrevem um projeto de monitoramento de água com IoT.  A série é composta de três artigos, sendo eles: 

  1. Parte 1 - objetiva introduzir o leitor ao que o projeto consiste e em como configurar a comunicação ZigBEE utilizada no projeto;
  2. Parte 2 - visa apresentar a parte bare-metal da solução, com explicações detalhadas do código e conceitos e estratégias de desenvolvimento;
  3. Parte 3 - parte final da série, destinada a explicar em detalhes a parte de Linux Embarcado do projeto e interação com o usuário via Internet (aqui o conceito de IoT é dominante).

 

Este artigo tornou-se viável graças ao apoio da Eletrogate, a qual forneceu o sensor de fluxo de água utilizado no projeto.

 

 

Pré-requisitos

 

Devido à multidisciplinaridade deste projeto, para uma boa compreensão dos conteúdos desta série de artigos são desejáveis os seguintes pré-requisitos: 

  • Conhecimento básico de Linux Embarcado;
  • Conhecimento de linguagem de programação C;
  • Conhecimento de linguagem de programação Python;
  • Conhecimentos de MQTT (recomendo fortemente a leitura do artigo MQTT e Intel Edison);
  • Conhecimento em desenvolvimento de sistemas embarcados bare-metal (ter noção das principais técnicas e conceitos desse tipo de sistema, tais como: interrupções externas, timers, comunicação serial por interrupção, watchdog timer, etc.).

 

 

No que consiste esta parte do sistema?

 

Esta parte do sistema corresponde à parte do sistema dedicada exclusivamente à medição, cálculo e publicação da vazão instantânea e consumo acumulado de água. Ou seja, esta parte do sistema é responsável por: 

  • Fazer a leitura do sensor de fluxo de água;
  • Calcular vazão instantânea e consumo acumulado, incluindo salvamento periódico do consumo acumulado em memória não-volátil (EEPROM do microcontrolador);
  • Permitir entrar e sair em modo de calibração de sensor via comando (um comando para entrada e outro para saída do modo de calibração);
  • Responder a solicitações de informação de consumo acumulado, vazão instantânea e versão do equipamento.

 

Tal parte do sistema foi escolhida ser bare-metal pelas devidas necessidades: 

  • Dedicação exclusiva a tarefas que não são muito complexas e que não demandam um sistema operacional;
  • Melhor definição da arquitetura do sistema (sendo a camada mais especialista);
  • Baixo consumo energético;
  • Ter pouca ou nenhuma interação com usuário final;
  • Funcionar em locais "adversos" (grandes diferenças de temperatura, locais sem acesso à internet e locais de difícil acesso);
  • Possibilidade zero de travamento/crash;
  • Possuir um Breathing Light, que em condições normais de funcionamento pisca a uma frequência de 1Hz, indicando que o sistema está OK;
  • Possuir mecanismo de auto-reset em caso extremo (travamento).

 

Em suma, esta parte do sistema foi projetada para ser robusta, ter funcionamento o mais dedicado possível e para ser instalada e "esquecida" lá realizando sua função de medição dos dados importantes ao sistema.

 

Devido à sua importância, tal sistema precisa ser muito bem desenvolvido e testado, assim como bem aferido. Para garantir isso, durante o desenvolvimento foram utilizadas técnicas de programação aplicadas a sistemas bare-metal, as quais serão explicadas em detalhes.

 

 

Medição de vazão e consumo de água com IoT - sensoriamento

 

A parte bare-metal, conforme foi dito, possui apenas o sensor de fluxo de água como sensor. Tal sensor corresponde ao visto na figura 1.

 

Água com IoT: Sensor de fluxo de água (1/2 polegada)
Figura 1 - Sensor de fluxo de água (1/2 polegada).

 

Esse sensor pode funcionar em uma vazão de até 30 L/min e fornece uma saída pulsada, com frequência diretamente proporcional à vazão de água.

 

Importante: Como o número de pulsos por litro pode variar muito da instalação (conexões utilizadas, inclinação, etc.) e de onde ele foi instalado, é de extrema importância que o número de pulsos/1 litro seja calibrado (tal passo será visto com mais detalhes à frente neste artigo).

 

 

Microcontrolador utilizado

 

Neste sistema foi utilizado o microcontrolador Microchip PIC18F4520. Este trata-se de um microcontrolador de 8 bits, 32 KB de memória Flash, 1536 bytes de memória SRAM, 40 pinos (até 36 I/Os) e 256 bytes de EEPROM interna.  Segue uma foto deste microcontrolador na Figura 2.

 

Água com IoT: PIC18F4520
Figura 2 - PIC 18F4520

 

Este foi escolhido como microcontrolador deste projeto pelos seguinte fatores: 

  • Ser um microcontrolador com bom "suporte" na internet: possui ferramentas de programação em grande quantidade na Internet (e, na maioria das vezes, ferramentas simples) e há um grande número de projetos, exemplos e cursos (grátis e pagos) disponíveis na rede (isto se aplica a todos os microcotroladores Microchip PIC);
  • A série 18F dos PICs permite definir priorização de interrupções, algo de essencial importância neste sistema;
  • Os equipamentos de gravação do microcontrolador são de baixo custo, além de ser plenamenta possível montá-los em casa se quiser economizar uma grana (esta dica se aplica a todos os microcotroladores Microchip PIC);
  • Microcontrolador fácil de achar no mercado;
  • Os microcontroladores PIC são, na minha opinião, os mais fáceis e rápidos pra "botar pra funcionar" (do zero até à prototipagem).

 

Além disso, este microcontrolador possui um bom número de I/O´s, o que pode ser útil em uma expansão futura.

 

Para o desenvolvimento do firmware para este microcontrolador, foram utilizados os seguintes softwares: 

 

 

Comunicação

 

Conforme dito na parte 1 deste artigo, este módulo (sistema bare-metal) comunica-se com o restante do sistema de modo wireless, via ZigBEE. Os dados são enviados e recebidos para/do ZigBEE através da UART nativa do microcontrolador, em um baudrate de 9600 bauds, com 8 bits de dados e sem paridade. 

 

Todos os comandos enviados ao sistema bare-metal seguem uma mesma ordem/estrutura: 

  • STX (0x02), que corresponde ao "byte de sincronia", ou seja, indica que um frame válido está por vir;
  • Opcode (1 byte);
  • Tamanho (1 byte), sendo que este tamanho é somente do buffer;
  • Checksum (1 byte), sendo que o checksum é somente dos dados contidos no buffer;
  • Buffer (máximo de 20 bytes).

 

Quanto aos tipos de comandos que podem ser enviados à placa (opcode e descrição), estes são os seguintes:

Comando

Código / Opcode
(ASCII / Hexadecimal)

Descritivo do comando
Leitura de consumo acumulado'L' / 0x4CRealiza a leitura do consumo acumulado de água (em litros)
Leitura de vazão instantânea'V' / 0x56Realiza a leitura da vazão instantênea (em l/h)
Reset de consumo acumulado'R' / 0x52Zera o consumo total acumulado
Entrada em modo de calibração'E' / 0x45Entra no modo calibração do sensor de fluxo de água*
Saída do modo de calibração'S' / 0x53Sai do modo calibração do sensor de fluxo de água**
Leitura da versão do equipamento'Q' / 0x51Lê a versão do firmware do equipamento (V1.00, por exemplo)

* Ao entrar neste modo, o breathing light irá ficar aceso constantemente. Além disso, enquanto em modo de calibração, o consumo e vazão não são medidos.
** Ao sair deste modo, o breathing light volta a piscar na frequência de 1Hz e o consumo e vazão voltam a ser medidos.

 

Observação: Todos os comandos retornam dados. Este retorno é feito na mesma estrutura/formato do comando recebido, inclusive no mesmo opcode. Quando há dados a serem enviados (leitura de consumo, leitura de vazão e leitura da versão do firmware), estes vêm na parte de buffer do protocolo. 

 

 

Técnicas de programação utilizadas e soluções adotadas

 

O código-fonte final é bem extenso, beirando 600 linhas de código. Portanto, as explicações sobre ele serão feitas em tópicos, sendo cada tópico uma técnica de programação / "estratégia" utilizada para desenvolver este projeto.

 

O link para o código-fonte completo pode ser encontrado no fim desse artigo.

 

 

Base de tempo do sistema

 

Todas as contabilizações (vazão e consumo acumulado), controle de gravação de dados na EEPROM e controle do breathing light são feitas em uma base de tempo ditada pelo Timer1 (timer de 16-bits do PIC). Abaixo, segue a função de configuração do Timer1 (bastante detalhada nos comentários): 

//função de configuração do Timer1
//parametros: nenhum
//saida: nenhum
void ConfigTimer1(void)
{
    // - Frequencia do oscilador interno (4000000/4)=1Mhz (por default, o PIC funciona a 1/4 da frequencia de clock estabelecida)
	// - Se o Timer1 tem 16 bits, seu valor máximo de contagem é 0xFFFF (65535)	
	// - Com 1MHz de frequencia util, temos que cada ciclo de máquina terá, em segundos: 1 / 1MHz = 0,000001 (1us)
    // - Utilizando o prescaler do microcontrolador em 4 (ou seja, a frequencia util do timer1 é 1/4 da frequencia util do pic), temos:
    //   Periodo minimo "contável" pelo Timer1 =  (1 / (1MHz/4))   = 0,000004 (4us)
    // - Logo, a cada 16 bits contados, teremos: 65536 * 4us =  0,262144s
    // - visando maior precisão, sera feito um timer de 0,2s. Logo:   
    //              0,262144s   ---  65536
    //                 0,20s     ---     x        x = 50000
    // Logo, o valor a ser setao no timer1 é: 65536 - 50000 = 15536
 
    ContadorIntTimer=0;
    setup_timer_1(T1_INTERNAL | T1_DIV_BY_4);
    set_timer1(15536);
    enable_interrupts(INT_TIMER1);      
    enable_interrupts(GLOBAL);
 
}

 

O Timer1 foi configurado e calculado considerando o cristal de 4MHz. Logo, se você quiser reproduzir este projeto com um oscilador (interno ou externo) de frequência diferente, será necessário revisar/alterar esta função.

 

O "tick" do timer, conforme visto, é de 200ms. Desta forma, é preciso fazer, na função de tratamento de interrupção, filtragens para saber quando executar de fato o código da função (planejado para executar de 1 em 1 segundo). Além disso, em situações especiais (quando em modo calibração e quando a placa "acorda" pela primeira vez, ou seja, quando não há calibração feita) a função não deve ser executada por inteiro. Esta filtragem de quando dese ser executado todo o conteúdo da função de tratamento da interrupção do timer está no fragmento abaixo: 

//tratamento da interrupção de timer
#INT_TIMER1
void TrataTimer1()
{
	ContadorIntTimer++;

    if (ContadorIntTimer < 5)   //cada "tick" do timer1 tem 0,2s. Logo, 5 "tiks" equivalem a 1 segundo
    {
        set_timer1(15536);
		return;
	}

	if (EstaEmModoCalibracao == SIM)  //se o equipamento está em modo calibração, nada deve ser feito aqui
	{
		set_timer1(15536);
		return;
	}
	
	if (PulsosPorLitro == 0) //não há calibração do sensor realizada. Nenhum calculo é feito
	{
		set_timer1(15536);
		return;
	}
	
    //1 segundo se passou. Pode executar o restante da função / cálculos

 

 

Melhor aproveitamento possível do processamento

 

Visando um melhor aproveitamento do processamento, o firmware funciona quase que totalmente via interrupção. Mais precisamente, funciona por interrupção: recepção serial, "tick" do timer (que serve de base de tempo), interrupção externa (para leitura dos pulsos do sensor) e cálculo e contabilização da vazão instantânea e consumo acumulado. 

 

Neste projeto, por exemplo, o laço principal faz pouquíssimas tarefas, conforme mostrado abaixo: 

while(1)
	{		
		restart_wdt();  //reinicia watchdog-timer

        if (RecebeuBufferCompleto == RECEPCAO_OK)   //trata buffer recebido (visando otimização de desempenho de interrupção serial,  este tratamento é feito aqui)
		{
			RecebeuBufferCompleto=SEM_RECEPCAO;
            TrataMensagem();
		}
		
		//verifica se deve gravar consumo na EEPROM (visando otimização de desempenho geral,  esta tarefa é feita aqui)
		if ((DeveGravarConsumo == SIM) && (EstaEmModoCalibracao == NAO))
		{
			//deliga todas interrupções
            disable_interrupts(INT_RDA);
            disable_interrupts(INT_EXT);

            //consumo deve ser gravado
			for (i = 0; i < 4; i++) 
			{
				write_eeprom(EnderecoEscritaEEPROM, *((int8*)&ConsumoCalculado + i) ) ; 
				EnderecoEscritaEEPROM++;
			}
			DeveGravarConsumo = NAO;
			TempoSalvamentoConsumo = 0;

			//configura timer e religa interrupções	
            set_timer1(15536);
            ConfigInterrupcaoUART();
            ConfigInterrupcaoEXT();
		}
	}

 

As interrupções foram priorizadas, visando melhor funcionamento do sistema. A priorização, do mais prioritário para o menos prioritário, é a seguinte: Timer, interrupção externa, interrupção de recepção serial. Tal priorização definida com a diretiva de compilação #priority, conforme mostrado abaixo: 

#priority INT_TIMER1, INT_EXT, INT_RDA  //ordem de prioridade das interrupções (ordem decrescente de prioridade)

 

Outro fator importantíssimo para o desempenho do sistema como um todo é o atendimento da interrupção externa (chamada a cada pulso lido, com borda de subida). Via de regra, quanto mais uma interrupção é chamada, mais rápido deve ser o código que ela executa. Sendo assim, considerando a ordem de grandeza da frequência de saída do sensor (100Hz), é altamente desejável que esta interrupção faça o mínimo possível. Logo, neste caso, a função de tratamento da interrupção externa ficou simples conforme mostrado abaixo: 

//tratamento da interrupção externa
#int_EXT 
void  EXT_isr(void) 
{ 
	ContadorPulsos++;    
} 

 

Outra boa prática utilizada para maximização de desempenho é o uso de memset() e memcpy() para inicialização e preenchimento de arrays. Isto é eficiente pois dispensa o uso de estruturas de repetição (for, while e do-while) para esta tarefa e porque faz o preenchimento/cópia diretamente no endereço de memória (ou seja, manipula dados a nível de endereço de memória). No projeto em questão, isto pode ser visto em vários momentos, como aqui: 

memset(BufferAscii,0,TAMANHO_MSG_RESPOSTA);

 

E aqui: 

memcpy(BufferAscii+4,Dado,Tamanho);  //note o "+4", indicando que a cópia será feita do quarto byte em diante da variável "BuferAscii"

 

Ainda, uma eficiente maneira de inicializar estruturas completas de dados usando memset() pode ser vista a seguir: 

memset(&DadosProtocoloLeitorAgua, 0, sizeof(TDadosProtocoloLeitorAgua));   //limpa dados do protocolo

 

 

Máquina de estado de recepção serial

 

Conforme visto no tópico de comunicação, a recepção serial é feita por interrupção. Desta forma, uma eficiente maneira de tratar estes dados recebidos é utilizar uma máquina de estados. Para os que estão começando, recomendo fortemente a leitura deste artigo para melhor compreensão do que é e como é usada uma máquina de estado.

 

Neste projeto é utilizada máquina de estado feita em switch-case (com cinco estados), sendo chamada a cada byte recebido. Os cinco estados estão descritos a seguir:

  1. ESTADO_STX: aguarda chegar um byte STX (0x02), byte este que sinaliza que um pacote válido de informações está por vir. Ao chegar STX, o próximo estado é ESTADO_OPCODE;
  2. ESTADO_OPCODE: aguarda a chegada do byte que indica opcode. Após a chegada deste byte, o próximo estado é ESTADO_TAMANHO;
  3. ESTADO_TAMANHO: aguarda a chegada do byte que informa o tamanho do buffer a ser recebido. Se o tamanho recebido for maior que 20 bytes (limite máximo do buffer aceito), a máquina de estado é reiniciada (faz o próximo estado ser ESTADO_STX) e as informações recebidas são desconsideradas. Após a chegada deste byte, o próximo estado é ESTADO_CHECKSUM;
  4. ESTADO_CHECKSUM: aguarda a chegada do byte de checksum. Se o tamanho do buffer (recebido no estado ESTADO_TAMANHO) for zero, sinaliza que o pacote foi completamente recebido (para posterior tratamento) e reinicia a máquina de estados (faz o próximo estado ser ESTADO_STX). Caso contrário, direciona ao estado de recepção do buffer (ESTADO_BUFFER);
  5. ESTADO_BUFFER: Aqui é aguardada a chegada do buffer (de acordo com o tamanho recebido no estado ESTADO_TAMANHO). Após a recepção, é calculado o checksum do buffer recebido e, caso coincida com o valor recebido no estado ESTADO_CHECKSUM, sinaliza que o pacote foi completamente recebido (para posterior tratamento) e reinicia a máquina de estados (faz o próximo estado ser ESTADO_STX). Caso o checksum não coincida, a máquina de estados é reiniciada e as informações recebidas são desconsideradas.

 

 

Calibração do sensor de fluxo

 

Antes de entrar em modo calibração, é fundamental que todo o encanamento (da entrada de água até a saída pro recipiente) esteja cheio de água. A calibração do sensor de fluxo é feita da seguinte forma:

  • Um comando de entrada em modo de calibração é recebido. O número de pulsos/litro em memória RAM é zerado;
  • O usuário deixa um recipiente de 1 litro ser preenchido (através de uma tubulação que contenha o sensor instalado). Neste tempo, o sistema bare-metal está contando quantos pulsos foram lidos;
  • Um comando de saída do modo de calibração é enviado após o recipiente ser preenchido. Nesta hora, a quantia de pulsos/litro é salva em memória não volátil e seu valor atualizado em RAM. A partir deste ponto, o sistema volta a medir e contabilizar consumo e vazão.

 

Durante o processo de calibração a breathing light mantém-se constantemente acesa, só voltando a piscar após o fim da calibração.

 

 

Consumo acumulado

 

O consumo acumulado de água é contabilizado segundo a segundo em uma variável (memória RAM do microcontrolador), a cada cinco "ticks" do Timer1.

 

 

Gravação de dados em memória não-volátil (EEPROM)

 

A calibração do sensor de fluxo (número de pulsos/litro) e consumo acumulado são salvos na memória EEPROM do microcontrolador. 

 

Cada dado/grandeza salva na EEPROM é salva precedida de uma chave. Neste projeto, a chave é "PedroBertoleti2015" (sem aspas). Isto serve para, na leitura destes dados (na inicialização do sistema, por exemplo), garantir que o que há gravado ali é um dado do sistema realmente (e não lixo, dados inválidos ou dados corrompidos). Caso não houver esta chave gravada, a mesma é gravada e o registro da grandeza correspondente (consumo ou calibração) é zerado.

 

A fim de prolongar a vida útil da memória EEPROM (que, neste microcontrolador, suporta até 100.000 escritas), este salvamento é feito a cada cinco horas. Para alterar este tempo, basta alterar o seguinte define (contido em HeaderLeitor.h): 

#define TEMPO_SALVAMENTO_CONSUMO_SEGUNDOS    18000 //5h

 

 

Alcance da comunicação sem fio

 

O alcance da comunicação sem fio foi de 15 metros em ambiente residencial comum (ou seja, em visada direta isso pode ser ainda maior).

 

Tal alcance pode ser substancialmente aumentado utilizando um XBee Pro como coordenador da rede ZigBEE.

 

 

Projeto na íntegra

 

Para baixar o projeto na íntegra, clique neste link.

 

 

Circuito esquemático

 

O circuito esquemático do projeto é exibido na figura 3.

 

Água com IoT: esquemático da placa
Figura 3 - Esquemático da placa.

 

Onde: 

  • ALM: conector 3 vias da alimentação;
  • SFLX: conector 3 vias para ligação do sensor.

 

Nota-se no esquema que assumi a entrada de alimentação como 5V já regulado. Isto se deve ao fato que utilizei neste projeto um velho carregador de celular, logo não precisei fazer um circuito para alimentação da placa.

 

 

Montagem final

 

Segue na figura 3 a montagem final do hardware do sistema bare-metal.

 

Água com IoT: Montagem final do hardware do sistema bare-metal
Figura 4 - Montagem final do hardware do sistema bare-metal.

 

 

Vídeo - Funcionamento

 

O vídeo foi desenvolvido com os recursos disponíveis, de certa forma limitados e compatíveis com a lógica DIY.

 

Referências

 

Livro: Microcontroladores PIC - Programação em C - Fábio Pereira

 

 

Agradecimentos

 

Eu gostaria de agradecer ao Fábio Souza por todo o apoio e ajuda no software do sistema bare-metal, assim como agradecer Amário Soares pela ajuda com a soldagem do hardware.

Outros artigos da série

<< Monitoramento de água com IoT - Parte 1Monitoramento de água com IoT - Parte 3 >>
Este post faz da série Monitoramento de água com IoT. Leia também os outros posts da série:
NEWSLETTER

Receba os melhores conteúdos sobre sistemas eletrônicos embarcados, dicas, tutoriais e promoções.

Obrigado! Sua inscrição foi um sucesso.

Ops, algo deu errado. Por favor tente novamente.

Licença Creative Commons Esta obra está licenciada com uma Licença Creative Commons Atribuição-CompartilhaIgual 4.0 Internacional.

Pedro Bertoleti
Sou engenheiro eletricista formado pela Faculdade de Engenharia de Guaratinguetá (FEG - UNESP) e trabalho com Android embarcado em Campinas-SP. Curioso e viciado em tecnologia, sempre busco me aprimorar na área de sistemas embarcados (modalidades bare-metal, RTOS, Linux embarcado e Android embarcado).Para mais informações, acesse minha página no Facebook:https://www.facebook.com/pbertoleti

14
Deixe um comentário

avatar
 
8 Comment threads
6 Thread replies
4 Followers
 
Most reacted comment
Hottest comment thread
10 Comment authors
Fábio SouzaMarco MolinaCarlos ConyMarciaPedro Bertoleti Recent comment authors
  Notificações  
recentes antigos mais votados
Notificar
Marco Molina
Membro
Marco Molina

Boa tarde, tentei baixar o projeto na integra no Link, porém diz que não possuo permissão. Como faço para conseguir tal permissão?

Grato

Fábio Souza
Visitante

Olá Marco, por favor tente novamente. Eu testei em dois navegadores aqui e não tiver problema de permisão, mesmo sem estar logado no site.

Carlos Cony
Visitante
Carlos Cony

qual a precisao do sensor? E o erro dele é constante? Tipo sempre tem 10% pra mais de valor calculado? Pergunto isso, pois se o erro for constante posso resolver ver ele via softare.

Marcia
Visitante
Marcia

Olá Pedro, desculpe minha ignorância sobre o assunto, mas gostaria de saber se um sensor de hidrômetro com defeito, altera a leitura de consumo de água?
Obrigada.

Kaline Brandão farias mesquita
Visitante
kaline

Como você sabe qual o opcode entre o sensor e o pic?Não vi nenhuma definição no datasheet que me proporcionasse alguma ideia ou não entendi (mais provável)

Andre Sobral
Membro
Andre Sobral

Estou pesquisando a algum tempo sobre internet das coisas pois pretendo estudar mais a respeito como estudante de engenharia. E confesso que gostei deste artigo pela ótima linguagem e organização dos conteúdos. Pretendo poder trocar ideias sobre o tema.

trackback
Douglas Bastos
Visitante
Douglas Bastos

Mais um excelente artigo desta serie.

phfbertoleti
Visitante
phfbertoleti

Douglas, obrigado!

Roberto Kitahara
Visitante
roberto akira kitahara

Prezado gostei muito da sua linha de pesquisa, estou desenvolvendo um projeto semelheante com arduino http://labdegaragem.com/forum/topics/monitorar-a-medi-o-de-gua-via-web-server-com-arduino?xg_source=activity . Gostaria de saber mais sobre a parte web do projeto, foi uma das partes que eu não consegui implementar. Obrigado

phfbertoleti
Visitante
phfbertoleti

Roberto, bom dia. Primeiramente, muito obrigado pela leitura e por comentar! A parte web do projeto sejá explicada em detalhes na parte 3, porém posso adiantar que ela utiliza um sistema com Linux embarcado (no caso, escolhi a Intel Edison) e se comunica com a web utilizando MQTT. A comunicação por MQTT creio que seja uma boa solução pro seu caso, então recomendo que você leia este artigo aqui: http://embarcados.com.br/mqtt-e-intel-edison/ Aproveitando a mensagem, eu olhei seu projeto e achei interessante a ideia de utilizar um hidrômetro comercial / já homologado como medidor do sistema todo, sobretudo um que garante a… Leia mais »

Roberto Kitahara
Visitante
roberto akira kitahara

Obrigado! vou fazer uns ajustes nesse projeto e incluirei a interrupção externa!