Agora vamos mostrar a programação do Arduino e o uso da linguagem Tcl/Tk no desenvolvimento de um programa (com interface gráfica - GUI) para controlar o funcionamento da bomba centrífuga e da válvula solenóide, e monitorar a vazão gravimétrica no reservatório superior, através da comunicação serial com o Arduino.
Implementamos o código no Arduino para permitir a comunicação serial com o PC através de comandos com o formato: [comando];[pino];[valor]
[comando]: SET (digital (0-1) ou analog (PWM 0-255)) ou GET (analogRead or digitalRead)
[pino]: D2, D3 ... D12, A0, A1 ... A5
[valor]: 0-1, 0-255
O comando SET é usado nos pinos configurados como OUTPUT. E o comando GET é usado com os pinos analógicos e digitais configuradas como INPUT.
Exemplos de comando:
SET;D2;0 - definir o pino digital “D2” como “0” (LOW)
SET;D3;255 - definir o pino digital “D3” com o valor “255” no modo PWM
GET;A0 - retorna a leitura do pino analógico “A0”
GET;D1 - retorna a leitura do pino digital “D1”
Versão final do Sketch: (Fonte: Building a Respirometer using Finite State Machine (FSM), Tcl/Tk and Arduino)
/* Final version of the sketch to interpret the commands SET and GET 07/02/2014 */ char message[15]; char *cmd; char *pin; char *value; char c; byte length; byte length_message; void setup() { Serial.begin(9600); Serial.flush(); //Available pins //D2, D3 (PWM), D4, D5(PWM), D6(PWM), D7, D8, D9(PWM), D10(PWM), D11(PWM) e D12 //A0, A1, A2, A3, A4 e A5 //Pins 2-5 are configured as digital input pins, //and pins 9-12 are configured as digital output pins. pinMode(9, OUTPUT); pinMode(10, OUTPUT); pinMode(11, OUTPUT); pinMode(12, OUTPUT); pinMode(2, INPUT); digitalWrite(2,HIGH); // enable pull-up resistors pinMode(3, INPUT); digitalWrite(3,HIGH); // enable pull-up resistors pinMode(4, INPUT); digitalWrite(4,HIGH); // enable pull-up resistors pinMode(5, INPUT); digitalWrite(5,HIGH); // enable pull-up resistors } void loop() { for (int i = 0; i < 15; i++) { message[i] = 0; } if (Serial.available() > 0) { //Wait to receive the message //with 9600 baud (9600 bits/s = (9600/8) bytes / sec //1200 bytes/s = 1200 characters/s = 1200 caractere/1000ms //therefore each character takes approx. 1.2 ms to be transmitted //50 ms are therefore sufficient for receiving approx. 40 characters delay(50); length_message = Serial.available(); if (length_message >= 15) { Serial.print("message larger than the limit (15)!"); Serial.println(length_message); for (int i = 0; i < length_message; i++) { c = Serial.read(); } } else { for (int i = 0; i < length_message; i++) { c = Serial.read(); if (c != '\n' && c != '\r') { message[i] = c; } } //function which returns the array size //but does not count the null '\0' end-of-string character length = strlen(message); message[length] = '\0'; // message[length] = 0 cmd = strtok(message, ";"); if ( (strcmp(cmd, "SET") == 0) || (strcmp(cmd, "set") == 0) ) { pin = strtok(NULL, ";"); value = strtok(NULL, ";"); setPin(pin, value); } else if (strcmp(cmd, "GET") == 0 || strcmp(cmd, "get") == 0) { pin = strtok(NULL, ";"); getPin(pin); } else { Serial.println("unknown command"); } } } } void setPin(char *pin, char *val) { if ((pin[0] == 'D') || (pin[0] == 'd')) { byte numPin = strtol(pin+1, NULL, 10); //O valor do pino digital pode ser HIGH 1 ou LOW 0 //e a funcao atoi converte a string contida no ponteiro //val em um inteiro (ou byte) //The value of the digital pins can be HIGH 1 or LOW 0 //and the function atoi converts the string contained //in the pointer val to an integer (or byte) byte state = atoi(val); if ( state > 1) { analogWrite(numPin, state); } else { digitalWrite(numPin, state); } //Outra opcao seria //if ( (strcmp(val, "HIGH") == 0) || (strcmp(val, "high") == 0) ) { //digitalWrite(numPin, HIGH); //} else if ( (strcmp(val, "LOW") == 0) || (strcmp(val, "low") == 0) ) { //digitalWrite(numPin, LOW); //} Serial.print("pin:"); Serial.print(pin); Serial.print(";"); Serial.print("set:"); Serial.println(state); } } void getPin(char *pin) { byte numPin = strtol(pin+1, NULL, 10); if ((pin[0] == 'D') || (pin[0] == 'd')) { byte state = digitalRead(numPin); Serial.print("pin:"); Serial.print(pin); Serial.print(";"); Serial.print("state:"); Serial.println(state); } else if ((pin[0] == 'A') || (pin[0] == 'a')) { int readout = analogRead(numPin); Serial.print("pin:"); Serial.print(pin); Serial.print(";"); Serial.print("readout:"); Serial.println(readout); } }
Tcl (pronuncia-se ticol, a abreviação de Tool Comand Language) é uma linguagem de script muito útil que roda na maioria das plataformas UNIX e Windows. Combinada com a biblioteca gráfica Tk (pronuncia-se tikei), a qual fornece os Widgets (elementos gráficos), a Tcl pode ser usada para gerar programas com interfaces gráficas com o usuário.
Vamos aproveitar os recursos da Tcl/Tk e criar inicialmente uma interface gráfica com apenas dois botões para ligar e desligar a bomba centrífuga.
Para abrir um arquivo: open [nome_do_dispositivo]
No caso da primeira porta serial do Linux: open /dev/ttyS0
E no Windows: open com1:
Mas para identificar qual o nome do dispositivo serial que o sistema associa ao Arduino observe a saída do comando dmesg:
New USB device found, idVendor=2341, idProduct=0043 usb 2-1: New USB device strings: Mfr=1, Product=2, SerialNumber=220 usb 2-1: Manufacturer: Arduino (www.arduino.cc) usb 2-1: SerialNumber: 952333531313518012F1 usb 2-1: configuration #1 chosen from 1 choice cdc_acm 2-1:1.0: ttyACM1: USB ACM device
A linha
cdc_acm 2-1:1.0: ttyACM1: USB ACM device
Indica que o dispositivo serial está associado ao arquivo /dev/ttyACM1
e portanto o comando seria open /dev/ttyACM1
Mas ao abrir um canal de comunicação com o comando open é preciso especificar o modo de acesso, ou seja, se o arquivo será aberto apenas para leitura “r”, escrita “w” ou leitura e escrita “r+”.
E portanto o comando fica:
open
/dev/ttyACM1
r+
O canal aberto pelo comando open recebe um nome gerado "aleatoriamente" pela Tcl. Para facilitar a manipulação deste canal, associamos o nome do canal aberto a uma variável de fácil identificação com o comando set:
set
[nome_do_canal] [ open nome_do_arquivo modo_de_acesso]
set
porta
[ open /dev/ttyACM1
r+
]
E para se comunicar corretamente com o dispositivo é necessário configurar a porta serial com os mesmos parâmetros de comunicação serial usados pelo Arduino, ou seja:
9600 bps, 8 bits de dados, sem paridade e 1 bit de parada.
Para fazer isso usamos o comando fconfigure.
set
porta
[ open /dev/ttyACM1
r+
]fconfigure
$porta
-mode 9600,n,8,1
É possível usar velocidades maiores do que 9600 bps.
Para escrever dados em arquivos usamos o comando puts.
Por exemplo, o comando:
puts
$porta
"SET;d10;1"
envia a string “SET;d10;1” pela porta USB na qual está conectado o Arduino.
O comando “SET;d10;1” é executado pelo Arduino e define o valor “1” (HIGH) para o pino 10, no qual está ligado o “gate” do MOSFET.(Ver figura).
Para rodar o programa podemos digitar linha por linha em um interpretador interativo ou, usando um editor de texto puro, criar um arquivo com a extensão “tcl” para executar todos os comandos do programa quando for chamado:
set
porta
[ open /dev/ttyACM1
r+
]fconfigure
$porta
-mode 9600,n,8,1
puts
$porta
"SET;d10;1"
Para isso devemos incluir nas primeiras linhas do arquivo as seguintes linhas:
#!/bin/sh #A próxima linha reinicia usando o wish \ exec wish "$0" "$@"
Existem outras opções de cabeçalho.
E em seguida podemos salvar o arquivo com o nome controle_sistema.tcl
, para ficar fácil de lembrar.
No Linux é necessário tornar o arquivo “executável” e para isso podemos usar o comando:
chmod
+x
controle_sistema.tcl
E o script “executável” controle_sistema.tcl
poderá ser executado a partir da linha de comando como qualquer outro programa.
Conteúdo do programa até o momento:
#!/bin/sh #A próxima linha reinicia usando o wish \ exec wish "$0" "$@" set porta [ open /dev/ttyACM1 r+ ] fconfigure $porta -mode 9600,n,8,1 puts $porta "SET;d10;1"
Agora vamos criar uma interface gráfica para o programa controle_sistema.tcl
com apenas dois botões para ligar e desligar a bomba.
Agora vamos usar o Toolkit (Kit de Ferramentas/Utilitários) Tk que é um complemento da Tcl, permitindo criar os Widgets (elementos gráficos) da interface gráfica com o usuário (GUI).
Para usar o Tk é importante entender quatro aspectos básicos:
Interpretador
Hierarquia
Eventos
Posicionamento dos widgets na tela
Um script que use apenas códigos Tcl pode ser executado com um interpretador tcl ou tclsh, dependendo da distribuição. Para usar Tk usamos outro interpretador, chamado wish. Por simplicidade já incluímos a chamada ao interpretador wish na primeira linha do nosso script. Se lembra?
Em Tk existe uma ordem para criar os objetos gráficos. O primeiro objeto gráfico é representado pelo caractere ponto “.”.
Esse primeiro objeto gráfico é a sua janela principal, ou janela “pai”, dentro da qual são colocados todos ou outros objetos “filhos”.
O comando seguinte cria um elemento gráfico do tipo “button” (botão) com o nome “.ligar”:
button
.ligar
-text
"Ligar"
-command
ligar
Repare no “.” antes do nome “.ligar”.
Em seguida é definido o texto “Ligar” que vai ser exibido no botão usando a opção “-text”.
São ações que devem ser executadas quando o usuário clicar no botão, pressionar uma tecla ou interagir com outros elementos gráficos.
As ações também podem ser desencadeadas por eventos externos, independente do usuário.
Com a opção “-command” definimos que o procedimento chamado “ligar” deverá ser executado quando o usuário clicar no botão.
button
.ligar
-text
"Ligar"
-command
ligar
Em Tk, existem três “gerenciadores de posição”: pack, grid e place. Cada qual posicionando o widget de forma diferenciada.
Vamos usar o gerenciador pack para exibir o botão “.ligar” com o comando:
pack
.ligar
button .ligar -text "Ligar" -command ligar button .desligar -text "Desligar" -command desligar pack .ligar .desligar -side left
Com a inclusão dos comandos para a criação dos botões o programa, por enquanto, está assim:
#!/bin/sh #A próxima linha reinicia usando o wish \ exec wish "$0" "$@" set porta [ open /dev/ttyACM1 r+ ] fconfigure $porta -mode 9600,n,8,1 puts $porta "SET;d10;1" button .ligar -text "Ligar" -command ligar button .desligar -text "Desligar" -command desligar pack .ligar .desligar -side left
Agora vamos criar os procedimentos “ligar” e “desligar” que serão executados quando os respectivos botões forem clicados.
O comando proc registra um procedimento (também chamada de função ou sub-rotina) por nome e permite que ele seja executado como qualquer outro comando em Tcl. A sintaxe é:
proc nome_do_procedimento {argumentos} { instruções }
Procedimento “ligar”:
proc ligar {} { global porta puts $porta "SET;d10;1" flush $porta }
Procediment “desligar”:
proc desligar {} { global porta puts $porta "SET;d10;0" flush $porta }
A variável “porta” foi definida como uma variável global mas antes vamos entender o conceito de “Escopo de Variáveis”.
Em linguagens de programação, “escopo” se refere ao alcance que uma variável possui, ou seja, de onde a variável pode ser acessada, trata da visibilidade de uma variável.
O escopo pode ser “Global”, “Local” ou definido entro de um “Namespace”.
Uma variável definida com o escopo “global” pode ser lida e modificada em qualquer parte do programa.
Uma variável com o escopo “local” está confinada em um bloco de código (procedimento/função/sub-rotina) e só pode ser manipulada dentro do bloco de código.
Conforme o diagrama seguinte a variável “A” é uma variável Global pois pode ser acessada em qualquer parte do programa enquanto que as variáveis “B” e “C” são variáveis Locais e podem ser manipuladas apenas dentro dos respectivos procedimentos (1 e 2).
Figura 67. Diagrama esquemático da visibilidade da variável global (A) e das variáveis locais (B e C).
A variável porta
foi criada no código principal do nosso programa e por isso é uma variável “global”. E para ser acessada pelos procedimentos ligar e desligar precisa ser declarada como “global” dentro do procedimento (global porta).
O comando:
puts
$porta
"SET;d10;1"
envia a string “SET;d10;1” pela porta USB na qual está conectado o Arduino.
E finalmente o comando flush $porta
envia todos os dados que estiverem no buffer de saída.
Mas podemos dispensar o comando flush e acrescentar no código principal, logo após comando de abertura da porta serial, o comando:
fconfigure
$porta
-buffering
none
Como está o programa até o momento?
#!/bin/sh #A próxima linha reinicia usando o wish \ exec wish "$0" "$@" set porta [ open /dev/ttyACM0 r+ ] fconfigure $porta -mode 9600,n,8,1 fconfigure $porta -buffering none button .ligar -text "Ligar" -command ligar button .desligar -text "Desligar" -command desligar pack .ligar .desligar -side left proc ligar {} { global porta puts $porta "SET;d10;1" } proc desligar {} { global porta puts $porta "SET;d10;0" }
Vamos fazer algumas modificações no programa e incluir botões para o controle da válvula solenóide:
button .ligar_bomba -text "Ligar Bomba" -command ligar_bomba button .desligar_bomba -text "Desligar Bomba" -command desligar_bomba button .ligar_valvula -text "Ligar Vávula" -command ligar_valvula button .desligar_valvula -text "Desligar Válvula" -command desligar_valvula pack .ligar_bomba .desligar_bomba .ligar_valvula .desligar_valvula -side left
E nos procedimentos para ligar/desligar a bomba e a válvula.
proc ligar_bomba {} { global porta puts $porta "SET;d10;67" } proc desligar_bomba {} { global porta puts $porta "SET;d10;0" } proc ligar_valvula {} { global porta puts $porta "SET;d9;1" } proc desligar_valvula {} { global porta puts $porta "SET;d9;0" }
Qual a diferença entre os procedimentos para a bomba e a válvula?
Como está o programa até o momento?
#!/bin/sh #A próxima linha reinicia usando o wish \ exec wish "$0" "$@" set porta [ open /dev/ttyACM0 r+ ] fconfigure $porta -mode 9600,n,8,1 fconfigure $porta -buffering none button .ligar_bomba -text "Ligar Bomba" -command ligar_bomba button .desligar_bomba -text "Desligar Bomba" -command desligar_bomba button .ligar_valvula -text "Ligar Vávula" -command ligar_valvula button .desligar_valvula -text "Desligar Válvula" -command desligar_valvula pack .ligar_bomba .desligar_bomba .ligar_valvula .desligar_valvula -side left proc ligar_bomba {} { global porta puts $porta "SET;d10;1" } proc desligar_bomba {} { global porta puts $porta "SET;d10;0" } proc ligar_valvula {} { global porta puts $porta "SET;d9;1" } proc desligar_valvula {} { global porta puts $porta "SET;d9;0" }
E como está a interface gráfica?
Programa após incluir comandos para criar o botão de fechamento do programa.
#!/bin/sh #A próxima linha reinicia usando o wish \ exec wish "$0" "$@" set porta [ open /dev/ttyACM0 r+ ] fconfigure $porta -mode 9600,n,8,1 fconfigure $porta -buffering none button .ligar_bomba -text "Ligar Bomba" -command ligar_bomba button .desligar_bomba -text "Desligar Bomba" -command desligar_bomba button .ligar_valvula -text "Ligar Vávula" -command ligar_valvula button .desligar_valvula -text "Desligar Válvula" -command desligar_valvula button .fechar -text "Fechar Programa" -command fechar_programa pack .ligar_bomba .desligar_bomba .ligar_valvula .desligar_valvula .fechar -side left proc ligar_bomba {} { global porta puts $porta "SET;d10;1" } proc desligar_bomba {} { global porta puts $porta "SET;d10;0" } proc ligar_valvula {} { global porta puts $porta "SET;d9;1" } proc desligar_valvula {} { global porta puts $porta "SET;d9;0" } proc fechar_programa {} { global porta close $porta exit }
As linhas seguintes criam o widget do tipo text chamado “.tela.dados”, do tipo texto dentro do frame “.tela” onde serão exibidos os resultados das leituras do pino A0.
frame .tela pack .tela text .tela.dados -relief sunken -yscrollcommand {.rolagem set} -height 15 -width 110 pack .tela.dados -side left -fill y
A opção “relief” define o tipo de relevo do widget. Os valores possíveis são: flat, groove, raised, ridgen e sunken.
A opção “yscrollcommand” define qual o nome da barra de rolagem (scrollbar) vertical que será usada para “rolar” a tela do texto no sentido vertical. A opção “height” apenas define a altura da janela de texto.
Em seguida incluímos os comandos para criar um scrollbar (barra de rolagem) dentro do frame “.tela” chamada “.tela.rolagem” que permitirá visualizar (rolar) todo o texto.
scrollbar .tela.rolagem -command {.tela yview} pack .tela.rolagem -expand yes -fill both
Salvar os novos comandos e chamar o programa de monitor_arduino_03.tcl
.
Figura 70. Tela do programa monitor_arduino_03.tcl incluindo os botões de controle e o texto para exibição das leituras do sensor.
O comando set dentro da opção "yscrollcommand" serve para passar para a barra de rolagem a área visível atual do widget que está associado a ela.
text .meu_texto -height 20 -yscrollcommand { .minha_barra_de_rolagem set } scrollbar .minha_barra_de_rolagem -command { .meu_texto yview } pack .meu_text -side left pack .minha_barra_de_rolagem -fill both -expand yes
A opção “command” do widget scrollbar define qual widget ele deve “rolar” e em qual sentido, “yview” (vertical) ou “xview” (horizontal).
Incluindo os botões para iniciar e parar as leituras de vazão.
button .iniciar_leitura -text "Iniciar Leitura" -command iniciar_leitura button .parar_leitura -text "Parar Leitura" -command parar_leitura pack .ligar_bomba .desligar_bomba .ligar_valvula .desligar_valvula .iniciar_leitura .parar_leitura .fechar -side left
E os eventos iniciar_leitura e parar_leitura:
proc iniciar_leitura {} { global porta fileevent $porta readable [list ler_porta $porta] } proc parar_leitura {} { global porta fileevent $porta readable "" }
Um canal na Tcl, por padrão, vai ficar bloqueado se você tentar ler mais dados do que existem no canal, e gerar uma mensagem de erro se você tentar ler ou escrever dados em um canal que já tenha sido fechado.
Para evitar isso, existe o comando fileevent que executa um comando (definido por você) quando um canal puder ser lido ou escrito. Ele recebe o nome de um comando (definido por você) e um parâmetro que informa em qual situação o comando deve ser executado: se quando o canal puder ser lido (readable) ou escrito (writable).
Estrutura geral do comando fileevent para executar o meu_procedimento quando meu_canal
puder ser lido:
fileevent
$meu_canal
readable
[list meu_procedimento
]
Por exemplo, no procedimento iniciar_leitura a linha:
fileevent
$porta
readable
[list ler_porta $porta
]
significa que o procedimento ler_porta deve ser executado sempre que o canal porta estiver legível.
Agora precisamos criar o procedimento ler_porta.
O procedimento ler_porta é chamado sempre que “chegar” algum dado pelo porta serial, e recebe como argumento a variável que armazena o identificar do canal para a porta serial (porta
).
Dentro da sub-rotina ler_porta implementamos os comandos necessários para “extrair” a informação desejada.
proc ler_porta { canal } { if { [eof $canal ] } { <------ Testa se já chegou no fim puts stderr "Fechando $canal" <------ Se "é o fim", envia mensagem catch { close $canal } <------ Fecha o canal return <----- Sai do procedimento } set dados [gets $canal] <----- Mas se ainda não "é o fim", então lê uma linha do canal e armazena na variável "dados". .tela.dados insert end "$dados\n" <----- Insere a leitura na janelade texto .tela.dados see end <---- Rola a janela e posiciona o cursor na última linha }
O conteúdo da variável dados
é exibido na janela do tipo text chamada “.tela.dados” com o comando:
.tela.dados insert end "$dados\n"
No entanto vamos lembrar que o programa que foi gravado no Arduino só envia informações após receber um comando de solitação. (Ex: GET;A0 - retorna a leitura do pino analógico "A0")
Por isso vamos inclur uma rotina, que chamaremos de ler_peso e será responsável pelo envio de comandos para o Arduino solicitando o valor das leituras de tensão do pino analógico conectado ao circuito do SRF.
O procedimento ler_peso envia o comando “get;a0” para a placa arduino e, se avariável lendo
for diferente de “0”, usa o comando after para programar uma nova chamada da sub-rotina ler_peso 500 milissegundos mais tarde.
proc ler_peso { canal } { global lendo puts $canal "get;a0" if {$lendo} { after 500 [list ler_peso $canal] } }
A única finalidade da rotina ler_peso é solicitar à placa Arduino o envio de dados que serão processados pela rotina ler_porta.
O comando after possui a seguinte sintaxe:
after
tempo
[comando1 comando2
]
Ele é usado para atrasar a execução do programa durante o intervalo definido por “tempo” (em milissegundos) ou para executar uma lista de comandos ([comando1 comando2]) após o intervalo definido por “tempo”.
Com a criação do procedimento (sub-rotina) ler_peso vamos incluir nas rotinas iniciar_leitura e parar_leitura alguns comandos para iniciar e encerrar o envio de dados respectivamente:
proc iniciar_leitura {} { global porta lendo fileevent $porta readable [list ler_porta $porta] set lendo 1 ler_peso $porta } proc parar_leitura {} { global porta lendo fileevent $porta readable "" set lendo 0 }
O código implementado no Arduino retorna uma string do tipo:
pin:a0;readout:611
Para “extrair” o valor numérico podemos usar o comando split que divide uma string em uma lista.
Isso também poderia ser feito com o uso de Expressões Regulares.
Rotina ler_porta com os comandos, e comentários, para extrair as leituras analógicas:
proc ler_porta { canal } { if { [eof $canal ] } { puts stderr "Fechando $canal" catch { close $canal } return } set dados [gets $canal] #Remove alguns caracteres não imprimíveis enviados pelo Arduino if {$dados == ""} { return } #Divide a string "dados" (Ex: pin:a0;readout:611) #usando o caracter ";" como separador de campos #e retorna a lista com 2 elementos {pin:a0 readout:611} set lista [split $dados ";"] #Retorna o segundo elemento da lista (readout:611) set leitura [lindex $lista 1] #Divide novamente a string "leitura" (readout:611) #em uma lista {readout 611} set lista [split $leitura ":"] #Retorna apenas o segundo elemento da lista (611) set numero [lindex $lista 1] #E exibe na janela de texto .tela.dados insert end "$numero\n" #Rola a janela de texto para exibir a última linha .tela.dados see end }
Simplificando as linhas com o comando split
set leitura [lindex [split $dados ";"] 1]
set numero [lindex [split $leitura ":"] 1]
Agora imagine que você queira registrar também o intervalo de tempo de cada leitura para poder calcular a vazão e produzir gráficos Peso X Tempo.
Neste caso vamos criar uma variável que vai armazenar o instante inicial da aquisição e os intervalos de tempo de cada leitura.
Dentro do procedimento iniciar_leitura vamos declarar a variável global t0
e inicializar esta variável com o tempo do sistema em milisegundos:
set
t0 [clock clicks -milliseconds]
E no procedimento ler_porta vamos declarar a variável global t0
, obter o tempo atual em milisegundos e armazenar o resultado na variável tn
com o comando:
set
tn [clock clicks -milliseconds]
Com o tempo inicial e final calculamos o intervalo de tempo (segundos) decorrido desde o início das leituras com o comando:
set
tn
[expr ($tn - $t0)/1000.0
]
E inserimos na janela de texto os intervalos de tempo e as leituras do sensor:
.tela.dados insert end "$tn $numero\n"
Podemos também formatar as leituras de tempo definindo apenas 1 casa decimal com o comando:
set
tn
[format "%.1f" $tn]
Ou com 2 casas decimais:
set
tn
[format "%.2f" $tn]
Como ficou a rotina “ler_porta”:
proc ler_porta { canal } { global t0 if { [eof $canal ] } { puts stderr "Fechando $canal" catch { close $canal } return } set dados [gets $canal] if {$dados == ""} { return } set tn [clock clicks -milliseconds] set tn [expr ($tn - $t0)/1000.0] set tn [format "%.1f" $tn] set leitura [lindex [split $dados ";"] 1] set numero [lindex [split $leitura ":"] 1] .tela.dados insert end "$tn $numero\n" .tela.dados see end }
Com os recursos, até então implementados, do programa de controle foi possível fazer os primeiros testes de calibração [7] do sensor SRF adicionando volumes conhecidos de água no reservatório superior e registrando as leituras do Arduino.
O conversor Analógico-Digital (AD) da placa Arduino possui uma resolução [8] de 10 bits, ou seja, consegue converter a escala de 0-5V com uma resolução de:
210 = 1024
resolução = 5 V / 1024 = 0,00488 ou 4,88 mV
Ou seja, o conversor AD do Arduino faz a leitura de tensão e converte em um número de 0 a 1023, e cada unidade de leitura corresponde a 4,88 mV. (0 = 0 V, 1023 = 5V)
O código que gravamos no Arduino está retornando apenas as leituras puras do conversor AD (0-1023) feitas pelo pino A0 conforme o curcuito para o SRF.
Usando apenas esses números, sem fazer a conversão em mV, tentei fazer uma calibração, bem simplificada, do SRF adicionando incrementos de 100 mL no reservatório superior e registrando as leituras do sensor.
Tabela 3. Tabela de Calibração
Volume(mL) | Leituras |
---|---|
0 | 600 |
0 | 608 |
100 | 686 |
100 | 703 |
200 | 748 |
300 | 775 |
400 | 812 |
400 | 829 |
500 | 838 |
500 | 857 |
600 | 864 |
600 | 874 |
Observei que após a adição de água as leituras demoram muito (alguns minutos) para estabilizar em torno do novo valor. Por isso, em alguns casos, fiz leituras em duplicata.
Esse grande “tempo de resposta” dificulta o uso desse tipo de sensor para medidas de vazão. Apesar dessa limitação resolvi continuar a usar o SRF para implementar as rotinas necessárias na interface em Tcl/Tk.
Mas ao mesmo tempo, comecei também a trabalhar na idéia de Hackear a célula de carga de uma balança para uso doméstico.
Ao colocar em um gráfico (usando o Calc do OpenOffice) é possível identificar uma relação exponencial.
Mas conforme o Datasheet deste sensor a relação de resistência em função da forçao é melhor representada por um gráfico log-log.
Tabela 4. Tabela de Calibração com os logaritmos dos Volumes e Leituras
Volume(mL) | log(Volume) | Leitura | log(Leitura) |
---|---|---|---|
0 | - | 600 | - |
0 | - | 608 | - |
200 | 2,3010 | 748 | 2,8739 |
300 | 2,4771 | 775 | 2,8893 |
400 | 2,6021 | 812 | 2,9096 |
400 | 2,6020 | 829 | 2,9185 |
500 | 2,6990 | 838 | 2,9232 |
500 | 2,6990 | 857 | 2,9330 |
600 | 2,7781 | 864 | 2,9365 |
600 | 2,7781 | 874 | 2,9415 |
Alguns instantes depois fiz outra calibração, mas dessa vez usando apenas as leituras iniciais sem esperar a estabilização do SRF, e obtive outra curva de calibração (log(volume) = 9,90 * log10(leitura) - 26,39).
Essa equação foi usada no procedimento convert_leitura_volume para o cálculo do volume a partir das leituras analógicas.
#Procedimento para converter a leitura analógica em #em volume usando a equação de calibração #log(volume) = 9,901 * log10(leitura) - 26,385 #volume = 10^(9,901 * log10(leitura) - 26,385) #[log base 10 log10 (float)] #Ex: set two [expr log10(100)] #Potenciação: [pow (float, float)]] #Ex: set eight [expr pow(2, 3)] proc convert_leitura_volume { leitura } { set log_volume [expr 9.901 * [expr log10($leitura)] - 26.385] set volume [expr pow(10, $log_volume)] return $volume }
Esse procedimento foi usado para o cálculo do volume dentro da rotina ler_porta com o comando:
set
volume
[convert_leitura_volume $numero
]
E finalmente o cálculo da vazão é feito pela rotina calc_vazao:
#Calcula vazao como ml/s proc calc_vazao { ult_volume volume } { global int_leitura set vazao [expr ($volume - $ult_volume)/($int_leitura/1000)] return [format "%.1f" $vazao] }
O procedimento calc_vazao também é chamado por ler_porta dentro do teste if:
... if {[info exists ult_volume]} { set vazao [calc_vazao $ult_volume $volume] .tela.dados insert end "$tn $numero $volume $vazao\n" .tela.dados see end } else { .tela.dados insert end "T(s) L V(mL) Q(mL/s)\n" .tela.dados insert end "$tn $numero $volume\n" .tela.dados see end } set ult_volume $volume }
E para facilitar as medidas de vazão escrevi o procedimento medir_vazao:
proc medir_vazao {} { ligar_bomba set espera 1 after 30000 [list set espera 0] vwait espera desligar_bomba }
E finalmente para testar a capacidade do SRF para medidas de vazão configurei a bomba para operar com baixa vazão (puts $porta "SET;d10;60") no modo PWM.
Deixei a bomba e válvula ligadas durante 1 hora para avaliar o eventual aquecimento da bomba, válvula e componentes do circuito, mas não observei mudança significativa de temperatura.
Leituras com as medidas de vazão.
Início medida de vazão
T(s) L V(mL) Q(mL/s)
124.1 815 274.4 1.6
128.1 817 281.1 1.7
132.1 819 288.0 1.7
136.1 820 291.5 0.9
140.1 823 302.3 2.7
144.1 826 313.3 2.8
148.1 828 320.9 1.9
152.1 831 332.6 2.9
156.1 833 340.6 2.0
160.1 836 353.0 3.1
164.1 838 361.4 2.1
168.1 839 365.7 1.1
172.1 841 374.5 2.2
176.1 843 383.4 2.2
180.1 845 392.5 2.3
184.1 847 401.8 2.3
188.1 849 411.3 2.4
192.1 850 416.1 1.2
196.1 852 425.9 2.4
200.1 852 425.9 0.0
204.1 854 435.9 2.5
208.1 856 446.1 2.6
212.1 857 451.3 1.3
216.1 858 456.5 1.3
220.1 859 461.8 1.3
Pode-se observar que as medidas de vazão oscilam muito. E portanto cheguei à conclusão que este tipo de transdutor não é adequado para medidas de vazão gravimétrica.
A partir de agora vou avaliar a possibilidade de hackear uma balança digital para uso doméstico e aproveitar a célula de carga para medidas de massa, e vazão, com maior precisão.
Essas atividades estão documentadas na seção Hackeando uma Balança Digital.
#!/bin/sh #A próxima linha reinicia usando o wish \ exec wish "$0" "$@" set porta [ open /dev/ttyACM0 r+ ] fconfigure $porta -mode 9600,n,8,1 fconfigure $porta -buffering none -blocking 0 #Intervalo de leituras set int_leitura 4000 button .ligar_bomba -text "Ligar Bomba" -command ligar_bomba button .desligar_bomba -text "Desligar Bomba" -command desligar_bomba button .ligar_valvula -text "Ligar Vávula" -command ligar_valvula button .desligar_valvula -text "Desligar Válvula" -command desligar_valvula button .iniciar_leitura -text "Iniciar Leitura" -command iniciar_leitura button .parar_leitura -text "Parar Leitura" -command parar_leitura button .medir_vazao -text "Medir Vazao" -command medir_vazao button .fechar -text "Fechar Programa" -command fechar_programa frame .tela pack .tela text .tela.dados -relief sunken -yscrollcommand {.tela.rolagem set} -height 15 -width 125 pack .tela.dados -side left -fill y scrollbar .tela.rolagem -command {.tela.dados yview} pack .tela.rolagem -expand yes -fill both pack .ligar_bomba .desligar_bomba .ligar_valvula .desligar_valvula .iniciar_leitura .parar_leitura .medir_vazao .fechar -side left proc ligar_bomba {} { global porta #puts $porta "SET;d10;1" puts $porta "SET;d10;60" } proc desligar_bomba {} { global porta puts $porta "SET;d10;0" } proc ligar_valvula {} { global porta puts $porta "SET;d9;1" } proc desligar_valvula {} { global porta puts $porta "SET;d9;0" } proc fechar_programa {} { global porta close $porta exit } proc iniciar_leitura {} { global porta lendo t0 set t0 [clock clicks -milliseconds] fileevent $porta readable [list ler_porta $porta] set lendo 1 ler_peso $porta } proc parar_leitura {} { global porta lendo fileevent $porta readable "" set lendo 0 } proc ler_porta { canal } { global t0 ult_volume if { [eof $canal ] } { puts stderr "Fechando $canal" catch { close $canal } return } set dados [gets $canal] if {$dados == ""} { return } set tn [clock clicks -milliseconds] set tn [expr ($tn - $t0)/1000.0] set tn [format "%.1f" $tn] set lista [split $dados ";"] puts "lista -> $lista" set leitura [lindex $lista 1] puts "leitura -> $leitura" set lista [split $leitura ":"] puts "lista -> $lista" set numero [lindex $lista 1] puts "numero -> $numero" # set leitura [lindex [split $dados ";"] 1] # set numero [lindex [split $leitura ":"] 1] set volume [convert_leitura_volume $numero] set volume [format "%.1f" $volume] if {[info exists ult_volume]} { set vazao [calc_vazao $ult_volume $volume] .tela.dados insert end "$tn $numero $volume $vazao\n" .tela.dados see end } else { .tela.dados insert end "T(s) L V(mL) Q(mL/s)\n" .tela.dados insert end "$tn $numero $volume\n" .tela.dados see end } set ult_volume $volume } proc ler_peso { canal } { global lendo int_leitura puts $canal "get;a0" if {$lendo} { after $int_leitura [list ler_peso $canal] } } #Calcula vazao como ml/s proc calc_vazao { ult_volume volume } { global int_leitura set vazao [expr ($volume - $ult_volume)/($int_leitura/1000)] return [format "%.1f" $vazao] } #Procedimento para converter a leitura analógica em #em volume usando a equação de calibração #log(volume) = 9,901 * log10(leitura) - 26,385 #volume = 10^(9,901 * log10(leitura) - 26,385) #log base 10 log10 (float) # set two [expr log10(100)] #power pow (float, float) # set eight [expr pow(2, 3)] proc convert_leitura_volume { leitura } { set log_volume [expr 9.901 * [expr log10($leitura)] - 26.385] set volume [expr pow(10, $log_volume)] return $volume } proc medir_vazao {} { ligar_bomba set espera 1 after 30000 [list set espera 0] vwait espera desligar_bomba }
[7] Calibração (ou aferição) Conjunto de operações que estabelece, sob condições especificadas, a relação entre os valores indicados por um instrumento de medição ou sistema de medição ou valores representados por uma medida materializada ou um material de referência, e os valores correspondentes das grandezas estabelecidas por padrões.(Fonte: glossario-de-metrologia.pdf)
[8] Para entender o conceito de resolução na conversão AD consulte o tutorial http://emc5710.lago.prof.ufsc.br/arquivos/5conversao AD 122.pdf