Fazendo medições com o Arduino


A medição de sinais analógicos é muito comum em sistemas embarcados. Assim, as placas Arduino possuem conversores Analógico-Digitais (ADC) que fazem a medição de um sinal analógico, como um sensor ou um potenciômetro, por exemplo, convertendo o sinal analógico, medido, para um sinal digital, ou seja, um número, que pode ser manipulado dentro de seu programa conforme as suas necessidades. No caso das placas Arduino, o sinal medido é um sinal de tensão elétrica.

Conversor Analógico-Digital

Um conversor analógico-digital (ADC) é um dispositivo presente dentro do microcontrolador do Arduino, capaz de converter um sinal de tensão em um número. O Arduino Uno possui um único ADC, mas que é capaz de realizar medições (uma de cada vez) em 6 pinos, apresentados na placa como A0 a A5.

Nós estamos acostumados a trabalhar com o sistema de numeração decimal, que possui 10 algarismos (de 0 a 9) para representar os números. Assim, com um dígito, podemos representar 10 números (de 0 a 9); com dois dígitos, podemos representar 100 números (de 00 a 99) e assim por diante. Genericamente, com n algarismos, podemos representar 10n números (de 0 a 10n-1).

Nos sistemas digitais, os números são armazenados usando o sistema de numeração binário, ou seja, são representados apenas pelos algarismos 0 e 1, também chamados de bits. Assim, com um bit, podemos representar 2 números (0 e 1); com dois bits, podemos representar 4 números (00, 01, 10 e 11); com três bits, podemos representar 8 números (de 000 a 111). De modo geral, com n bits, podemos representar 2n números (de 0 a 2n-1).

O ADC do Arduino tem uma resolução de 10 bits, ou seja, armazena o valor medido em 10 bits. Isso significa que a medição pode ter valores entre 0 e 1023 (ou seja, 0 e 210-1). Se o ADC usar a tensão de alimentação como referência, em um Arduino alimentado com 5V, como o Arduino Uno, o valor máximo do ADC (1023) equivalerá a 5V; o mínimo (0), equivalerá a 0V, e os valores intermediários serão proporcionais às tensões entre 5V e 0V, e podem ser encontrados com uma simples regra de 3. Por exemplo, o valor 200 equivale a 200*5/1023 = 0,97V.

Assim, considerando uma referência de 5V, em um ADC de 10 bits conseguimos detectar uma variação (o equivalente à variação de 1 bit na medida) de cerca de 4,9mV na medida. Se a resolução for de 8 bits, detectamos uma variação de 19mV. Se for de 6 bits, a resolução passa a ser de 78mV, e assim por diante.

É importante saber que, apesar de usar 10 bits para armazenar o valor lido, alguns fatores como interferências, temperatura e até mesmo a taxa de aquisição (velocidade com que os valores são lidos) podem alterar a resolução útil do ADC. Por exemplo, se fizermos medições de dados a uma taxa muito alta, apesar do ADC disponibilizar um número composto por 10 bits, a resolução pode cair, fazendo com que os últimos bits guardem apenas lixo, e apenas os primeiros realmente representem o valor real.

Realizando medições de maneira contínua

Realizar uma medição com o ADC do Arduino é muito simples, basta usar uma função pronta chamada analogRead(), passando como argumento o pino em que o sinal será medido. Essa função permite realizar medições em até 9615Hz, ou seja, 9615 medidas por segundo, aproximadamente. Entretanto, como o envio em tempo real do valor medido toma algum tempo, a taxa de medições pode cair para cerca de 2000 medidas por segundo (comunicando a 115200bps), ou até 170 medidas por segundo (comunicando a 9600bps).

Um programa simples que realiza leituras e envia os valores lidos pela porta serial está disponível neste link [ver código, baixar código], e pode ser visto a seguir:

// Exemplo de utilização do ADC do Arduino usando a função analogRead(),
// realizando as medições e enviando os valores medidos pela porta serial.
//
// A IDE do Arduino fornece uma função pronta para uso do ADC, chamada
// analogRead(), que permite realizar medições em até 9615Hz, ou seja,
// 9615 medidas por segundo, aproximadamente. Entretanto, como o envio
// em tempo real do valor medido toma algum tempo, a taxa de medições
// pode cair para cerca de 2000 medidas por segundo (comunicando a
// 115200bps), ou até 170 medidas por segundo (comunicando a 9600bps).
//
// Criado por: Erick Dario León Bueno de Camargo, 2023


int medida;

////////////////////////////////////////////////////////////////////////////////
// Esta função só roda uma vez, no início
void setup() {
  Serial.begin(115200);
  Serial.println("Configuracao_ok!");
}

////////////////////////////////////////////////////////////////////////////////
// Esta função se repete indefinidamente
void loop() {
  medida = analogRead(A0);
  Serial.println(medida);
}

Como podemos ver, esse programa simplesmente realiza uma medida no pino A0, na linha 26, e envia o valor lido (entre 0 e 1023) pela porta serial, na linha 27, repetindo esse processo indefinidamente. Entretanto, apenas com essa abordagem, não podemos controlar com que taxa (velocidade) as medidas são feitas.

Realizando medições de maneira contínua a uma taxa conhecida

Muitas vezes, principalmente em aplicações biomédicas, precisamos saber a taxa com que os valores são lidos, para posteriormente poder extrair informações relevantes dos dados, como frequência de um sinal periódico, por exemplo.

A IDE do Arduino disponibiliza diversas funções para trabalhar com tempo. Uma delas é a função micros(), que retorna a quanto tempo, em microssegundos, sua placa Arduino está ligada. Sabendo a taxa com que queremos que os dados sejam medidos, e usando a função micros(), é possível controlar com relativa precisão a taxa com que os dados são medidos, como mostra o exemplo a seguir, disponível neste link [ver código, baixar código].


// Exemplo de utilização do ADC do Arduino usando a função analogRead(),
// realizando as medições em uma taxa de amostragem conhecida e enviando
// continuamente os valores medidos pela porta serial.
//
// A IDE do Arduino fornece uma função pronta para uso do ADC, chamada
// analogRead(), que permite realizar medições em até 9615Hz, ou seja,
// 9615 medidas por segundo, aproximadamente. Entretanto, como o envio
// em tempo real do valor medido toma algum tempo, a taxa de medições
// pode cair para cerca de 2000 medidas por segundo (comunicando a
// 115200bps), ou até 170 medidas por segundo (comunicando a 9600bps).
//
// Criado por: Erick Dario León Bueno de Camargo, 2023

// Frequencia de amostragem desejada, em Hz. Usar valores entre 1 e 2000:
long frequencia = 1000;

unsigned long atraso_us = (1000000.0/frequencia);
int medida;
unsigned long tempo_atual, tempo_anterior;

////////////////////////////////////////////////////////////////////////////////
// Esta função só roda uma vez, no início
void setup() {
  Serial.begin(115200);
  Serial.println("Configuracao_ok!");
  tempo_anterior = micros();
}

////////////////////////////////////////////////////////////////////////////////
// Esta função se repete indefinidamente
void loop() {
  tempo_atual = micros();

  // o abs() evita problemas com overflow da função micros()
  if (abs(tempo_atual - tempo_anterior) > atraso_us) {
    tempo_anterior = tempo_atual;
    medida = analogRead(A0);
    Serial.println(medida);
  }
}

Neste exemplo, na linha 16, colocamos a taxa (em Hz, ou medições por segundo) com que queremos que os dados sejam lidos. Aqui podemos usar valores entre 1 e 2000 medições por segundo.

Na linha 18, é calculado o intervalo de tempo que deve ter cada medição, em microssegundos. Na linha 33, usando a função micros(), guardamos a quanto tempo a placa está ligada, e na linha 36, verificamos quanto tempo passou desde a última leitura. Se ultrapassar o tempo de uma medição, realizamos uma nova medição (linha 38) e enviamos o dado medido pela porta serial (linha 39).

Esta abordagem funciona muito bem quando queremos coletar dados em uma taxa relativamente pequena (entre 1 e 2000 amostras por segundo), e quando não dependemos de uma taxa muito precisa. Na verdade, conseguimos uma taxa com menos de 1% de erro do valor ajustado neste exemplo.

O motivo de não conseguirmos uma taxa maior de 2000 amostras por segundos é que, a cada medida que fazemos, enviamos o valor medido pela serial, e isso toma muito tempo do microcontrolador.

Realizando medições por blocos a uma taxa conhecida

Uma maneira de evitar o tempo gasto no envio pela porta serial durante as medidas é, em um primeiro momento, realizar todas as medidas que queremos, e só então enviá-las pela porta serial. Desde que o Arduino tenha memória suficiente para guardar todos os pontos que queremos, essa é uma ótima opção.

Um exemplo está disponível neste link [ver código, baixar código], e pode ser visto a seguir.

// Exemplo de utilização do ADC do Arduino usando a função analogRead(),
// realizando as medições em uma taxa de amostragem conhecida.
//
// A IDE do Arduino fornece uma função pronta para uso do ADC,
// chamada analogRead(), que permite realizar medições em até
// 9615Hz, ou seja, 9615 medidas por segundo, aproximadamente.
//
// Criado por: Erick Dario León Bueno de Camargo, 2023

#define NRO_MEDIDAS 500

int vetor_medidas[NRO_MEDIDAS];

////////////////////////////////////////////////////////////////////////////////
// Realiza as medidas do ADC, guardando os valores medidos em vetor_medidas[].
void mede_adc(long frequencia){
  long atraso_us = (1000000.0/frequencia)-104.0; // A função analogRead() leva aprox. 104us para rodar
  if (atraso_us<0) atraso_us=0;
  for (int idx = 0; idx < NRO_MEDIDAS; idx++){
    vetor_medidas[idx] = analogRead(A0);
    if (atraso_us < 16000){
      delayMicroseconds(atraso_us);
    }
    else{
      delay(atraso_us/1000);
    }
  }
}

////////////////////////////////////////////////////////////////////////////////
// Esta função só roda uma vez, no início
void setup() {
  Serial.begin(115200);
  Serial.println("Configuracao_ok!");
}

////////////////////////////////////////////////////////////////////////////////
// Esta função se repete indefinidamente
void loop() {
  // realiza as medidas
  mede_adc(1000); // realiza as medidas a 1kHz, ou 1000Hz

  // envia as medidas pela porta serial
  for (int idx = 0; idx < NRO_MEDIDAS; idx++){
    float medida = vetor_medidas[idx]*5.0/1023.0;
    Serial.println(medida);
  }

  // aguarda 1 segundo (1000ms)
  delay(1000);
}

Neste exemplo, realizamos a medição de 500 valores por vez, o que é definido na linha 10. Na linha 12, criamos um vetor onde os dados serão gravados.

Nas linhas 16 a 28, temos a função que realiza as medições em uma determinada frequência (ou taxa). Na linha 17, calculamos o intervalo de tempo de cada medição, já subtraindo o tempo gasto pela função analogRead(). Em seguida, realizamos as medições, aguardando o tempo desejado (linha 22 para intervalos pequenos, em microssegundos, ou linha 25 para intervalos maiores, em milissegundos) para que a frequência de aquisição desejada seja atendida, e guardamos os valores no vetor de medidas (linha 20).

Já no loop principal, na linha 41, realizamos as medidas. Em seguida, na linha 45, convertemos o valor medido pelo ADC para volts, e na linha 46 enviamos o valor, já em volts, pela porta serial.

Caso desejado, podemos definir um intervalo de tempo antes de realizar o próximo conjunto de medições, como é feito na linha 50.

Neste exemplo, é possível atingir taxas de aquisição de aproximadamente 9600 amostras por segundo, já que não paramos para enviar dados pela porta serial entre as medições de um mesmo bloco.

Realizando medições por blocos a uma taxa conhecida usando registradores

A função analogRead() facilita muito a vida do programador quando queremos utilizar o ADC do Arduino. Internamente, essa função acessa regiões específicas da memória do microcontrolador, chamadas de registradores, que são acessadas tanto pelo software quanto pelo hardware da sua placa. Os registradores são usados tanto para configurar e comandar o ADC, quanto para pegar os valores medidos por ele.

Apesar de facilitar nossa vida, a função analogRead() não fornece meios para alterarmos as configurações do ADC, para, por exemplo, realizar medidas mais rapidamente. Isso acontece porque ela já é otimizada para aproveitarmos ao máximo a resolução do ADC. Medidas mais rápidas podem (e provavelmente irão) diminuir a resolução útil do ADC. Quanto mais rápido realizamos medições, menor é a precisão da medição. Entretanto, se não precisamos de todos os 10 bits de resolução do ADC, podemos, em alguns casos, aumentar a taxa de aquisição, chegando a até 150000 amostras por segundo, cerca de 15 vezes mais rápido do que com a função analogRead().

Pensando nisso, foi desenvolvida uma biblioteca, que pode ser obtida neste link [ver página do projeto, ver código, baixar código], que acessa os registradores e permite a configuração do ADC para que seja possível realizar medidas mais rapidamente.

Do como como a biblioteca foi escrita, os valores são coletados em um modo chamado Free Running Mode, que faz com que o ADC inicie a coleta do próximo dado imediatamente após a coleta do dado anterior, independente de outras tarefas que o microcontrolador possa estar executando ao mesmo tempo. Isso faz com que a taxa de aquisição seja muito mais precisa que nos exemplos anteriores, e também permite a coleta em taxas muito maiores.

O quanto perdemos em resolução ao fazer coletas em taxas maiores? Essa é uma pergunta de difícil resposta. Isso depende de diversos fatores, mas principalmente da impedância de saída do que você está medindo e da velocidade com que o sinal sendo medido varia, entre outras coisas. Mas, para aplicações caseiras, geralmente é aceitável realizar as medidas com relativa precisão mesmo com taxas mais altas.

Exemplos de uso dessa biblioteca podem ser obtidos neste link (para baixar um código quer está sendo visualizado no GitHub, basta abrir o arquivo no site e clicar no botão “Raw”, logo acima do código). Um exemplo de uso realizando a leitura de 8 bits pode ser visto a seguir.

// Exemplo de utilização da biblioteca fast_ADC_lib.h com 8 bits
// de resolução do ADC.
//
// Programa de exemplo de leitura de dados pelo ADC do Arduino
// utilizando a biblioteca fast_ADC_lib.h.
//
// A leitura dos dados é feita utilizando-se os registradores
// do Arduino, o que permite uma coleta de dados muito mais
// rápida do que usando a função AnalogRead();
//
// A taxa de aquisição é controlada pelo pre-scaler do ADC,
// passada à função adc_setup_8bit() (ou adc_setup_10bit()),
// conforme a lista abaixo (taxas teóricas):
// - PS_4: 307.7 kHz (taxa real: aprox. 196.6 kHz) (usar 8 bits)
// - PS_8: 153.8 kHz (taxa real: aprox. 153.17 kHz) (usar 8 bits)
// - PS_16: 76.9 kHz (taxa real: aprox. 76.77 kHz)
// - PS_32: 38.5 kHz (taxa real: aprox. 38.38 kHz)
// - PS_64: 19.2 kHz (taxa real: aprox. 19.19 kHz)
// - PS_128: 9.6 kHz (taxa real: aprox. 9.596 kHz)
//
// Para taxas acima de 76,9 kHz (PS_16), o ADC perde resolução, e
// o uso dos 10 bits é desnecessário (usar 8 bits nesses casos).
//
// Criado por: Erick Dario León Bueno de Camargo, 2023

#include "fast_ADC_lib.h"

#define NRO_MEDIDAS 500

byte vetor_medidas[NRO_MEDIDAS];

////////////////////////////////////////////////////////////////////////////////
// Esta função só roda uma vez, no início
void setup() {
  adc_setup_8bit(A0,PS_8,NRO_MEDIDAS,vetor_medidas);

  Serial.begin(115200);
  Serial.println("Configuracao_ok!");
}

////////////////////////////////////////////////////////////////////////////////
// Esta função se repete indefinidamente
void loop() {
  // realiza as medidas
  read_adc();

  // envia as medidas pela porta serial
  for (int idx = 0; idx < NRO_MEDIDAS; idx++){
    float medida = vetor_medidas[idx]*5.0/255.0;
    Serial.println(medida);
  }

  // aguarda 1 segundo (1000ms)
  delay(1000);
}

O controle da taxa de aquisição é feito a partir de um divisor, chamado de prescaler. Ele define a velocidade com que o ADC irá trabalhar, e consequentemente, com que velocidade as medições serão feitas. É importante saber que o ADC precisa de 13 ciclos de clock para realizar uma única medição, e que o clock do ADC é determinado pelo clock da placa dividido pelo prescaler. Assim, em um Arduino Uno, que possui um clock de 16MHz, se utilizarmos um prescaler de 128, o ADC irá trabalhar com um clock de 16MHz/128 = 125kHz. Como são necessários 13 ciclos de clock para realizar uma medição, a taxa de aquisição de dados será 125kHz/13 = 9,615kHz, ou 9615Hz, que é aproximadamente a taxa obtida usando a função analogRead(). Ela usa esse prescaler porque, segundo o datasheet do microcontrolador do Arduino Uno (o documento oficial do fabricante com as especificações do microcontrolador), a resolução de 10 bits do ADC só é garantida se o clock do ADC estiver entre 50 e 200kHz (e, neste caso, está em 125kHz). Se usarmos um prescaler de 64, obtendo 19190 medidas por segundo aproximadamente, o clock do ADC iria a 250kHz, o que ficaria fora da especificação para garantir a máxima resolução do ADC. Como dito anteriormente, isso não quer dizer necessariamente que sua medição estará incorreta, apenas que a resolução máxima não é garantida.

O prescaler pode ser ajustado em potências de 2, em um valor entre 8 e 128. Na verdade, é possível ajustar em 2 ou 4 também, mas em 2 o ADC para de funcionar, e em 4 o ADC opera no seu limite, realizando amostras a uma taxa fora do especificado. Assim, teoricamente conseguiríamos coletar dados em taxas entre 9,6kHz e 153,8kHz (na prática as taxas obtidas ficam muito próximas desses valores!).

Voltando ao exemplo, na linha 26 incluímos a biblioteca fast_ADC_lib.h. Para que ela funcione, basta baixar o arquivo e copiá-lo para a mesma pasta do seu programa, onde está seu arquivo .ino. Na linha 28 definimos quantas medições queremos fazer por vez. Na linha 30 definimos um vetor para guardar os valores medidos. Note que, aqui, os dados são guardados em uma variável do tipo byte, e não do tipo int como nos exemplos anteriores. Isso é possível pois a variável do tipo byte ocupa apenas 8 bits na memória, e como estamos lendo apenas 8 bits do ADC, e não 10 bits, é possível guardar o valor lido nessa variável, economizando memória do microcontrolador.

Na linha 35 configuramos o ADC. Aqui precisamos passar o pino onde os dados serão medidos (pino A0), o prescaler desejado (no caso, PS_8, ou seja, 8, o que equivale a uma taxa de 153846 medidas por segundo), o número de pontos que serão medidos, e o vetor onde as medições serão guardadas. Na linha 45, realizamos as medições. O programa só continua a execução quando todos os pontos desejados já foram medidos, então é seguro ler os pontos logo em seguida, o que é feito na linha 49, onde os dados medidos são convertidos para volts. Como foram lidos apenas 8 bits, a conversão é feita usando o valor máximo de leitura de 255 (28-1), e não 1023 (210-1), como nos exemplos anteriores. Finalmente, na linha 50, os valores convertidos são enviados pela porta serial, e na linha 54, aguardamos 1 segundo para dar início ao próximo bloco de medições.


Texto por: Erick León

Revisão e testes: Lucas Jácome Cabral


, ,

2 respostas para “Fazendo medições com o Arduino”

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *