Osciloscópio com Arduino: como montei meu (quase-)osciloscópio

Um (quase-)osciloscópio com Arduino, display LCD gráfico, 2 potenciômetros, 1 botão e um resistor, que cabe em uma mini-protoboard.

Rearranjando os componentes do meu jogo de paredão com Arduino e display Nokia, e acrescentando um botão e um potenciômetro, criei um (quase-)osciloscópio bastante funcional, que você pode modificar (o código está no Github) para as suas finalidades.

O osciloscópio é um dos instrumentos essenciais de um laboratório de eletrônica bem equipado, e geralmente consiste em um display que plota continuamente um (ou mais) pontos ou linhas representando a tensão lida na sua entrada.

Meu (quase-)osciloscópio atende de forma bem restrita à definição acima, e assim não me sinto à vontade em chamá-lo de osciloscópio. Por ler diretamente os valores de uma porta analógica do Arduino, ele fica restrito à capacidade dela, normalmente restrita a tensões positivas entre 0 e 5V (você pode aumentar essa faixa usando circuitos externos tais como divisores de tensão, se desejar).

Ele também não tem suporte a alguns recursos comuns em osciloscópios atuais, como seleção dinâmica da frequência/período, acompanhamento simultâneo de mais de uma entrada, entre outros – e alguns desses talvez entrem em uma futura versão do programa, se eu tiver a demanda. Em especial, ele não tenta fazer uma relação entre o valor lido na porta e a tensão externa correspondente a ele.

Meu (quase-)osciloscópio é um (não-)osciloscópio que plota os valores lidos de uma porta analógica, com controle dinâmico de centro e amplitude do gráfico.

O que ele faz, atendendo plenamente à minha demanda atual, é exibir um gráfico do valor lido da porta (atualizado ~10 vezes por segundo), oferecendo duas maneiras de selecionar dinamicamente os parâmetros do gráfico. Uma delas é manual, girando um par de potenciômetros que definem qual valor corresponde ao centro do eixo Y do gráfico (default: 512) e qual a amplitude exibida na tela (default: 1024).

A outra é automática: pressionando um botão que define – a partir das 80 leituras mais recentes – o melhor centro e amplitude para visualizar os dados atualmente em exibição.

Além do gráfico contínuo, que preenche a tela a cada ~8 segundos, são exibidos permanentemente no alto da tela os valores mínimo, central e máximo da faixa exibida no eixo Y, e na parte de baixo da tela um acompanhamento do valor mais recente lido e plotado.

Como montar o (quase-)osciloscópio com Arduino

Como você deve ter notado nas imagens acima, o número de cabos na montagem em protoboard parece grande, mas em parte isso se deve à minha escolha da montagem em uma mini-protoboard, para deixá-lo montado por uns tempos antes de fixar as conexões em alguma versão mais permanente.

Mas as conexões são simples, como veremos a seguir.

Para os potenciômetros que eu usei, o pino esquerdo (use como referência a imagem acima) é conectado ao GND, e o pino direito é conectado ao pino AREF (ou 3V3). O pino central de cada um deles é conectado a um pino analógico do Arduino – na minha montagem, o pino central do potenciômetro da esquerda foi conectado ao pino A2 do Arduino, e o pino central do potenciômetro da direita foi conectado ao pino A4 do Arduino.

Eu usei um botão bem simples, com 2 terminais e NA (normalmente aberto). Um dos terminais é conectado ao GND, e o outro a um pino digital do Arduino (o 12, no meu caso).

Para ter dados significativos para plotar, basta conectar um sensor a uma porta analógica (a A0, no meu caso). Meu escolhido foi um LDR, um resistor cuja resistência varia conforme a incidência de luz, servindo portanto para detectar a luminosidade. Usei nele uma configuração com pull-up, ilustrada no diagrama acima: um de seus terminais vai conectado ao GND do Arduino, o outro vai conectado tanto ao pino A0 do Arduino quanto a um resistor de 10KΩ ligado ao pino AREF ou 3V3 do Arduino.

Para completar, só falta descrever a conexão do módulo display LCD 84x48, que um dia já foi parte de um celular Nokia 5110. Já expliquei detalhadamente sobre ele no artigo anterior, portanto agora vou apenas listar as conexões de pinos que usei:

  • pino 1 (VCC) do display ➡ pino 3V3 do Arduino
  • pino 2 (GND) do display ➡ pino GND do Arduino
  • pino 3 (SCE) do display ➡ pino 10 do Arduino
  • pino 4 (RST) do display ➡ pino 9 do Arduino
  • pino 5 (D/C) do display ➡ pino 8 do Arduino
  • pino 6 (DN) do display ➡ pino 11 do Arduino
  • pino 7 (SCLK) do display ➡ pino 13 do Arduino
  • pino 8 (LED) do display ➡ deixei desconectado (sem backlight)

Duas observações importantes:

  1. O display que eu usei é feito para operar em nível lógico de 3,3V. Eu o conectei a um Uno Plus, clone do Arduino Uno que permite selecionar o nível lógico de 3,3V. A maior parte dos Arduinos opera com nível lógico de 5V, e neste caso o fabricante recomenda usar resistores de 10KΩ entre os pinos do Arduino e os pinos 4, 5, 6 e 7 do módulo, e um resistor de 1KΩ entre o pino do Arduino e o pino 3 do módulo, como vimos no artigo anterior.
  2. Existem diversas variações de módulo display Nokia 5110 à venda, algumas com pinagens diferentes da descrita acima, e outras com níveis lógicos ajustáveis ou diferentes. Consulte a documentação do seu módulo específico e faça as adequações necessárias.

Como programar o (quase)-osciloscópio no Arduino

Publiquei o código completo no meu Github; abaixo você vê a versão corrente na data da publicação deste artigo, colorizada como de hábito, para facilitar as explicações que vêm logo a seguir. Vale também olhar as explicações do código do artigo anterior, pois são bastante complementares, em especial quanto ao uso do display, e foram apresentadas com maior detalhamento.

// nokia-monitor-grafico
// um nao-osciloscopio em um Arduino com display Nokia 5110
// (c) Augusto Campos 2015 - BR-Arduino.org
// licenca ao final deste arquivo.
// Usa 2 potenciometros (um para definir o centro do eixo Y
// e o outro para definir a amplitude) e um botao para 
// selecionar automaticamente os melhores parametros de
// exibicao.


#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_PCD8544.h>


// configuracoes de hardware
const byte pinoBotao=12;
const byte pot1Analogico=A2;
const int pot1max=1007; // valor lido no pino do potenciometro quando girado ate o maximo
const byte pot2Analogico=A4;
const int pot2max=1007;

// SPI via software:
// pino 13 - Serial clock out (SCLK;MOSI)
// pino 11 - Serial data out (DN;DIN)
// pino 8 - Data/Command select (D/C)
// pino 10 - LCD chip select (CS;SCE)
// pino 9 - LCD reset (RST)
Adafruit_PCD8544 display = Adafruit_PCD8544(13, 11, 8, 10, 9);


void setup()   {
  display.begin();
  display.clearDisplay();
  display.setContrast(50);
  display.display();
  pinMode(pinoBotao,INPUT_PULLUP);
}

int X=0;
int Y=0;
int cMin;
int cMax;

int oldX=-1;
int oldY=-1;
int cY;
int cCentro;
int amplitude;
int old_cMin;
int old_cMax;
int linha[40];
int extremoAlto=0;
int extremoBaixo=1024;
int old_extremoAlto=1024;
int old_extremoBaixo=0;
unsigned long botaoUltimoM=0;
int pot1val;
int pot2val;
int old_pot1val;
int old_pot2val;
boolean botaoMudou=true;

void plotaPonto(int YY, boolean limpar=true) {
  const byte xMax=80;
  if (limpar) {
    if (X<xMax-10) display.drawLine(X+10,5,X+10,40,WHITE);
    if (X==0) display.fillRect(X,5,X+10,40,WHITE);
    linha[X]=YY;
    if (YY>extremoAlto) extremoAlto=YY;
    if (YY<extremoBaixo) extremoBaixo=YY;
    if (X==40) {
      old_extremoAlto=extremoAlto;
      extremoAlto=0;
      old_extremoBaixo=extremoBaixo;
      extremoBaixo=1024;
    }  
  }  
  if (cMax-cMin!=1023) {
    cY=constrain(YY,cMin,cMax)-cMin;
    cY=map(cY,0,cMax-cMin,0,30);
  }  
  else cY=map(YY,0,1024,0,35);
  if (X>0) display.drawLine(oldX,oldY,X,39-cY,BLACK);
  else display.drawPixel(X,39-cY,BLACK);
  X++;
  if (X==80) X=0;
  oldX=X;
  oldY=39-cY;
}


void replota() {
  int Xsave=X;
  X=0;
  display.fillRect(0,8,84,40,WHITE);
  for (byte i=0;i<Xsave;i++) {
    plotaPonto(linha[i],false);
  }  
  X=Xsave;
}

void autoEscala() {
  display.setCursor(75,40);
  display.print("x");
  int _dAmp;
  if (X<10) {
    _dAmp=(old_extremoAlto-old_extremoBaixo)/20;  
    if (_dAmp<30) _dAmp=30;  
    cMin=old_extremoBaixo-_dAmp;
    cMax=old_extremoAlto+_dAmp;
  } else {
    _dAmp=(extremoAlto-extremoBaixo)/20;
    if (_dAmp<30) _dAmp=30;  
    cMin=extremoBaixo-_dAmp;
    cMax=extremoAlto+_dAmp;  
  }  
  cCentro=cMin+((cMax-cMin)/2);
  old_cMin=cMin;
  old_cMax=cMax;
  replota();
}  


void atualizaEscala() {
  cCentro=1024-int(pot1val*1.0/pot1max*1024);
  int folga=cCentro;
  if ((1024-cCentro)<folga) folga=1024-cCentro;
  amplitude=1024-int(pot2val*1.0/pot2max*1024);
  if (amplitude > (2*folga)) amplitude=2*folga;
  cMin=cCentro-amplitude/2;
  cMax=cCentro+amplitude/2;
  if (abs((old_cMin-cMin)>2) | (abs(old_cMax-cMax)>2)) {
    if (X>0) replota();
  }  
  old_cMin=cMin;
  old_cMax=cMax;
}  

void processaEscala() {
  if ((digitalRead(pinoBotao)==LOW) & ((abs(millis()-botaoUltimoM))>200)) {
    if (botaoMudou==true) {
      autoEscala();
      botaoUltimoM=millis();
    }  
    botaoMudou=false;
  }  
  if (!botaoMudou & ((digitalRead(pinoBotao)==HIGH))) botaoMudou=true;
  if ( (abs(old_pot1val-pot1val)>10) | (abs(old_pot2val-pot2val)>10) ) {
    atualizaEscala();   
    old_pot1val=pot1val;
    old_pot2val=pot2val;
  }  
}


void atualizaPlacares() { 
  display.fillRect(0,40,84,8,WHITE);
  display.setCursor(5,40);
  display.print(cY);
  display.setCursor(40,40);
  display.print(Y);
  display.fillRect(0,0,84,8,WHITE);
  display.setCursor(0,0);
  display.print(cMin);
  display.setCursor(32,0);
  display.print(cCentro);
  display.setCursor(58,0);
  display.print(cMax);
}  


void loop() {
  delay(100);
  Y=analogRead(A0);
  plotaPonto(Y);
  pot1val=analogRead(pot1Analogico);
  pot2val=analogRead(pot2Analogico);
  processaEscala();
  atualizaPlacares();
  display.display(); 
}

/*
© 2015 Augusto Campos http://augustocampos.net/ (13.04.2015)
Licensed under the Apache License, Version 2.0 (the "License"); 
you may not use this file except in compliance with the License. 
You may obtain a copy of the License at 
http://www.apache.org/licenses/LICENSE-2.0 

Unless required by applicable law or agreed to in writing, software 
distributed under the License is distributed on an "AS IS" BASIS, 
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
See the License for the specific language governing permissions and 
limitations under the License. 
*/

De cima para baixo, começamos pelo trecho em salmão, que tem a inclusão das bibliotecas de comunicação e de suporte ao display, seguida da definição de constantes de posição e calibração do botão e dos potenciômetros, da importante instanciação do objeto display (quando informamos para a biblioteca quais os pinos do Arduino que estão conectados aos pinos relevantes do LCD), e do setup propriamente dito, que se restringe a deixar a tela pronta para uso e definir o modo do pino conectado ao botão (os pinos em uso pelos potenciômetros são analógicos e não precisam ser inicializados, e os do display são inicializados pela biblioteca).

A seguir, em verde, temos a definição das variáveis globais que governam quase tudo que acontece no (não-)osciloscópio. Por exemplo: X é a coluna da tela em que será plotado o próximo ponto, cMin e cMax são os limites da amplitude do display, e linha[40] é onde fica gravada a série de pontos da volta atual do gráfico. Você verá as referências às demais quando estudar as funções a seguir.

Logo após, em vermelho, temos as 2 funções responsáveis pelo gráfico. A primeira, plotaPonto, recebe como parâmetro YY um valor lido na porta analógica, e o converte em um ponto ou linha na tela. Ela é chamada em 2 contextos diferentes: pelo loop() (para postar um ponto lido diretamente da porta analógica, sempre com o parâmetro limpar como true) e pela função replota() (para retraçar a linha após uma alteração da escala, sempre com o parâmetro limpar como false).

Note que o começo da função tem um grande bloco de código executado apenas quando limpar=true, ou seja, quando a função é chamada diretamente a partir do loop(). Nesse bloco, além de limpar o campo do gráfico algumas colunas à frente da coluna atual (definida por X), também é executada a gravação do valor no histórico (linha[]) e ajustando o registro de valores mínimos e máximos, necessários para permitir o ajuste automático da amplitude do gráfico, caso o usuário aperte o botão.

As operações matemáticas essenciais para ajustar o valor lido à amplitude e escala da tela são realizadas pelas funções constrain e map, da biblioteca do Arduino.

Em seguida, as funções constrain() e map() da biblioteca do Arduino são usadas para restringir o valor de YY à amplitude configurada e convertê-lo à escala em uso. Aí é só plotar o ponto (ou linha), ajustar o X para a próxima volta, e pronto :-)

Comparada à complexidade da plotaPonto, a função replota() é trivial: ela simplesmente refaz (chamando múltiplas vezes a plotaPonto) o gráfico na tela, para ajustá-lo quando o usuário muda a escala.

A seguir, em laranja, vêm as 3 funções que gerenciam a escala do gráfico. A primeira delas, autoEscala(), é chamada quando o usuário aperta o botão de seleção automática. Os cálculos dela são simples, ajustando o ponto mínimo e máximo da escala a partir do mínimo e máximo registrados na volta mais recente do gráfico (com uma folga de 10%), e em seguida chamando a já apresentada replota().

A atualizaEscala() faz cálculos similares, mas usa como entrada não o histórico, e sim a posição dos 2 potenciômetros: o da esquerda define o centro, e o da direita define a amplitude.

A técnica de debounce consiste em usar temporizações ou outros métodos para evitar que as instabilidades no contato elétrico façam com que um pressionamento do botão seja percebido como múltiplos pressionamentos.

Já a processaEscala() faz um trabalho mais bruto, sem cálculos. É ela que verifica se o botão foi pressionado, ou se os potenciômetros foram movidos, e chama as duas funções acima, dependendo do caso. A leitura do botão usa uma técnica chamada debounce, para evitar que o processamento ocorra mais de uma vez no caso de o botão permanecer pressionado, ou de ele alternar várias vezes entre pressionado e solto (situação comum no momento em que o contato físico da chave está se estabelecendo ou se soltando).

Em marrom temos a atualizaPlacares(), que herdou seu nome do jogo que eu implementei no mesmo display, e que simplesmente atualiza na tela 5 números de acompanhamento: os extremos e centro da escala, ao alto, e os valores de Y (convertido e original) mais recentemente plotado, abaixo.

O loop() fica simples porque toda a complexidade foi terceirizada para as funções de apoio.

Para finalizar, em fúcsia, temos o loop() do Arduino. Como toda a complexidade foi terceirizada para as funções que já vimos, ele ficou bem simples, limitando-se a ler e plotar periodicamente o valor do sensor, ler os potenciômetros e processar a escala, atualizar os placares e chamar a importante função display.display(), único momento em que as alterações feitas nos demais pontos do programa são realmente atualizadas na tela visível.

Para o futuro (acompanhe no meu Github), tenho em mente adicionar algumas funcionalidades: plotar o valor de uma segunda porta analógica, tornar configurável a frequência/período, oferecer um display de status que possa apontar a média e outras funções estatísticas da série histórica, entre outras. Para o momento, entretanto, estou bem satisfeito com o meu (não-)osciloscópio ;-)

Comentar

Dos leds ao Arduino, ESP8266 e mais

Aprenda eletrônica com as experiências de um geek veterano dos bits e bytes que nunca tinha soldado um led na vida, e resolveu narrar para você o que descobre enquanto explora esse universo – a partir da eletrônica básica, até chegar aos circuitos modernos.

Por Augusto Campos, autor do BR-Linux e Efetividade.net.

Recomendados

Livro recomendado


Artigos já disponíveis

Comunidade Arduino

O BR-Arduino é integrante da comunidade internacional de entusiastas do Arduino, mas não tem relação com os criadores e distribuidores do produto, nem com os detentores das marcas registradas.

Livros recomendados