Sensores de distância são úteis em diversas aplicações. E um bom sensor disponível é o VL53L0X, que é um sensor de distância a laser. Portanto, vamos aprender a utilizá-lo com o Arduino em diferentes configurações.

Informações básicas

O que é

O VL53L0X é um sensor que é capaz de medir distância por meio de um laser infravermelho. É possível encontrar informações dizendo que ele identifica gestos, mas isto é feito por meio da medição da distância.

Enfim… Este sensor é encontrado para comprar já montado em uma plaquinha, como mostra na imagem abaixo (o post será focado nela):

Vl53L0x sensor de distância a lser
O sensor em si é apenas o ci que está no meio da placa da imagem.
O sensor trabalha de 2.6 a 3.5V, mas a plaquinha possui um regulador de tensão que nos permite alimentá-lo com 5V. Portanto, fica mais fácil utilizá-lo.

Uma informação interessante a ser dita é sobre o possível risco que este produto apresenta, que é o de causar dano aos olhos. Teoricamente, olhar o sensor diretamente seria o equivalente a olhar o laser destas canetas laser. E, dependendo da potência do laser, você pode ficar até cego com pouco tempo de exposição.

Entretanto, o sensor VL53L0X não oferece este risco e está de acordo com uma norma que trata disto (IEC 60825-1:2014). De qualquer forma, ainda assim não é uma boa prática olhá-lo diretamente, pois evita qualquer desgaste por menor que seja.

E esta informação eu tirei do datasheet do sensor, o qual possui informações mais completas caso você queira aprender mais.

Características

  • Distância de medição máxima: cerca de 2 metros.
    • A resolução dele está na casa dos milímetros.
  • A acurácia do sensor depende de alguns fatores diferentes, mas ela pode ser +-3% ou +-5%.
  • Mede distâncias até mesmo se a superfície do objeto estiver inclinada em relação ao sensor.
  • Apresenta pequenas diferenças de distância dependendo da cor do objeto (devido ao índice de reflexão).
  • Interface I2C com endereço programável.

Modos de operação e perfis de medição

O sensor é capaz de operar em 3 modos distintos:

  • Single ranging

A medição é feita uma única vez quando solicitada e após isto o sensor fica em standby.

  • Continuous ranging

A medição é feita de forma contínua: assim que uma termina, outra já começa na sequência com o menor atraso possível.

  • Timed ranging

Mesma coisa do modo anterior. A diferença é que entre uma medição e outra, pode ser definido um atraso (delay).

Perfis de medição

Existem 4 perfis de medição disponíveis:

  • Default mode

Perfil padrão que apresenta um tempo de resposta de 30ms e permite medir até 120cm.

  • High speed

Perfil que apresenta um tempo de resposta de 20ms.

  • High accuracy

É o perfil que apresenta um tempo de resposta de 200ms, mas uma acurácia de +-3%.

  • Long range

Perfil que permite medir até 200cm, mas com uma acurácia de +-5%.

O modo de longo alcance é recomendado apenas em ambientes sem interferência externa de luz infravermelha. Pois, se ele for utilizado em ambientes com interferência, a medição será um pouco instável e imprecisa (veremos isto mais a frente).

E o perfil “Long range” pode ser combinado com o “High speed” ou o “High accuracy”. O mesmo vale para o perfil “Default mode”.

Pinos

O sensor VL53L0XV2 (plaquinha da imagem anterior) apresenta 6 pinos, que são:

  • Vcc – Alimentação positiva: Alimentado com 5V.
  • Gnd – Alimentação negativa.
  • SCL: Clock da comunicação I2C.
  • SDA: Dados da comunicação I2C.
  • GPIO1: Pino de interrupção que é ativado quando uma medição é completada.
    • Pode ser utilizado para alertar o microcontrolador quando a medição for completada (interrupção).
  • XSHUT: Pino para “desligar” o sensor.
    • A máxima tensão que pode ser aplicada neste pino é 3.6V, então cuidado na hora de ligar no Arduino.

VL53L0X com o Arduino

O sensor se comunica via I2C e necessita de uma série de configurações em seus registradores para funcionar corretamente. E, para facilitar a vida, vou utilizar uma biblioteca que já lida com isto tudo.

Na hora de falar dos resultados do sensor, vou misturar um pouco o conceito de estabilidade das medições com o conceito de acurácia (proximidade entre o valor real e o medido). Mas é para ilustrar o comportamento do sensor.

Biblioteca

A biblioteca que vou utilizar pode ser encontrada na própria IDE do Arduino:

  • Acesse o menu: Sketch -> Incluir biblioteca -> Gerenciar bibliotecas

Feito isto, instale a seguinte biblioteca:

Biblioteca do VL53L0X

Circuito

Anteriormente, comentei sobre os pinos do VL53L0X, mas agora vamos ver como ele se liga ao Arduino.

Ligação do VL53L0x com o Arduino

Não tem muito mistério: Basta alimentar o sensor (pinos Vcc e GND com 5v e GND respectivamente). Além disto, é só conectar o pino SCL na porta analógica A5 do Arduino e o SDA na porta analógica A4. Se você estiver utilizando outro Arduino, é só verificar quais são os pinos correspondentes da comunicação I2C (SDA e SCL).

Os demais pinos (GPIO1 e XSHUT) não são necessários para o funcionamento básico do sistema. O GPIO1 é para lidar com interrupção, e não pretendo mostrar isto neste post. E o XSHUT serve para “desligar” o sensor, e isto será necessário mais a frente.

Medição simples

Para mostrar como lidar com o sensor, pretendo mostrar apenas o modo Single ranging (medição única).

Os exemplos que vem junto à biblioteca do sensor são extremamente úteis e já servem para te dar um norte sobre como fazer o sensor funcionar. De qualquer forma, pretendo mostrar os principais aspectos da biblioteca.

O código abaixo mostra o mínimo que você precisa fazer para ler distância com o sensor no perfil “Default”. O monitor serial está sendo utilizado apenas para exibir as leituras do sensor. Leia os comentários para entender o funcionamento.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <Wire.h>
#include <VL53L0X.h>

// Cria uma instancia do sensor
VL53L0X sensor;

void setup()
{
  // Inicializa a comunicação serial
  Serial.begin(9600);
  // Inicializa a comunicação I2C
  Wire.begin();

  // Inicializa o sensor
  sensor.init();
  // Define um timeout de 500mS para a leitura do sensor
  // Em caso de erro, este será o tempo máximo de espera da resposta do sensor
  sensor.setTimeout(500);
}

void loop()
{
  // Faz a medição da distância e retorna um valor em milímetros
  int dist = sensor.readRangeSingleMillimeters();
 
  // Imprime no monitor serial
  Serial.println(dist);
}

Medição de longas distâncias

O código anterior só permite medir distâncias máximas de 100cm (próximo disto). E, para conseguir medir até pouco mais de 2m, você tem que habilitar o perfil de longo alcance.

Isto é feito adicionando os seguintes comandos após a inicialização (dentro do void setup, após definir o timeout):

1
2
3
sensor.setSignalRateLimit(0.1);
sensor.setVcselPulsePeriod(VL53L0X::VcselPeriodPreRange, 18);
sensor.setVcselPulsePeriod(VL53L0X::VcselPeriodFinalRange, 14);

De acordo com meus testes, a distância máxima foi de 2200mm (2,2 metros). E, nesta distância, o sensor fica oscilando a medição em +-90mm. Ou seja, não tem uma boa acurácia.

Medição de exatidão

Para melhorar a exatidão da medição, é possível habilitar o perfil de alta acurácia. Isto é feito aumentando o tempo no qual o sensor faz a medição. E o comando para fazer esta alteração é o seguinte:

O comando deve ser adicionado dentro do void setup, após definir o timeout.

1
sensor.setMeasurementTimingBudget(200000);

Utilizando a medição de alta acurácia e o perfil de longo alcance, a oscilação caiu de +-90mm para +-20mm. Apesar desta melhora, o intervalo entre as medições ficou maior.

Por outro lado, no modo Default, a medição oscila +-20mm. E, quando aplico o perfil de alta acurácia, ela passa a oscilar +-6mm. Ou seja, o resultado é realmente melhor.

Medição rápida

Para acelerar o tempo da medição, é possível habilitar o perfil de alta velocidade. Isto é feito diminuindo o tempo no qual o sensor faz a medição. E o comando para fazer esta alteração é o seguinte:

O comando deve ser adicionado dentro do void setup, após definir o timeout.

1
sensor.setMeasurementTimingBudget(20000);

As medições ficam mais rápidas, como era de se esperar. Mas, a acurácia cai e as medições passaram a oscilar +-120mm.

Interferência da luz do sol no VL53L0X

Este sensor sofre interferência da luz solar e isto pode atrapalhar as medições de distância. Entretanto, o VL53L0X apresenta um comportamento muito superior (em termos de funcionamento) a um sensor de obstáculo infravermelho quando exposto à luz solar.

Para demonstrar a interferência, fiz um teste medindo a distância no perfil de longo alcance com a janela fechada e com a janela aberta. Em nenhum momento do teste, houve incidência direta da luz solar no sensor. Para isto, fiz um outro teste, onde pude verificar que o sensor apresenta uma alta instabilidade quando exposta ao sol diretamente.

O teste consistiu em colocar o sensor próximo à janela apontado para o teto, onde ele iria medir distâncias em torno de 2 metros. Esta região perto da janela não estava recebendo luz direta do sol.

Sem filtrar as medições

Apliquei o código que está mostrado abaixo, que apenas faz as medições da distância no perfil de longo alcance sem filtrar os dados.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <Wire.h>
#include <VL53L0X.h>

VL53L0X sensor;

void setup()
{
  Serial.begin(9600);
  Wire.begin();

  sensor.init();
  sensor.setTimeout(500);

  // Perfil de longo alcance
  sensor.setSignalRateLimit(0.1);
  sensor.setVcselPulsePeriod(VL53L0X::VcselPeriodPreRange, 18);
  sensor.setVcselPulsePeriod(VL53L0X::VcselPeriodFinalRange, 14);
}

void loop()
{
  int dist = sensor.readRangeSingleMillimeters();
 
  Serial.println(dist);
}

Resultado sem filtrar as medições

Medindo distancia sem filtrar com a janela fechada
Janela fechada
Medindo distancia sem filtrar com a janela aberta
Janela aberta

O valor de distância de ~8000mm é obtido quando o sensor chega no tempo limite de espera da medição. Portanto, é quando ele não é capaz de informar a distância (como se não existissem objetos na frente dele).

É perceptível que, mesmo com incidência solar indireta, o sensor sofre grande influência. Mesmo com a janela fechada, houve alguns picos em que o sensor não foi capaz de obter uma medição. O mesmo procedimento feito durante a noite não apresenta o mesmo comportamento, pois não há mais interferência significativa.

Quando a janela foi aberta, praticamente não da para saber qual é a distância certa, pois o sensor fica constantemente perdendo a medição (quando ele mede ~8000mm).

Filtrado as medições

Para resolver o problema anterior, podemos aplicar um método simples para filtrar as medições. O método consiste apenas em ignorar o valor de ~8000mm quando ele for retornado pelo sensor. Desta forma, o programa irá considerar o último valor válido lido e irá “descartar” o valor de ~8000mm.

Este procedimento foi feito na função “filtrar_sinal” do código abaixo. Além do que comentei acima, adicionei um “timeout” de 1 segundo: se o sensor retornar apenas ~8000mm durante 1 segundo, então ele para de descartar este valor. Leia os comentários do código para entender.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <Wire.h>
#include <VL53L0X.h>

VL53L0X sensor;

// Variaveis para armazenar a distancia atual e o ultimo valor lido
int dist = 0, dist_old = 0;
// Variavel para armazenar o tempo na parte do timeout
unsigned long timeout = 0;

void setup()
{
  Serial.begin(9600);
  Wire.begin();

  sensor.init();
  sensor.setTimeout(500);

  // Perfil de longo alcance
  sensor.setSignalRateLimit(0.1);
  sensor.setVcselPulsePeriod(VL53L0X::VcselPeriodPreRange, 18);
  sensor.setVcselPulsePeriod(VL53L0X::VcselPeriodFinalRange, 14);
}

void loop()
{
  dist = sensor.readRangeSingleMillimeters();
  filtrar_sinal(); // Filtra o valor de distancia
 
  Serial.println(dist);
}

// Função para filtrar o valor medido
void filtrar_sinal()
{
  // Se a distância medida for maior que 8000 e ainda não tiver passado 1 segundo de timeout
  if (dist > 8000 && ((millis() - timeout) < 1000))
  {
    // Descarta a medição feita e iguala ela à anterior
    dist = dist_old;
  }
  else // Caso contrário (medição < 8000 ou passou do timeout)
  {
    // Não descarta a medição atual e atualiza a medição antiga para a atual
    dist_old = dist;
    timeout = millis(); // Reseta o valor da variável do timeout
  }
}

Resultado filtrando as medições

Medindo distancia com filtro com a janela fechada
Janela fechada
Medindo distancia com filtro com a janela aberta
Janela aberta

Em ambos os casos, com este simples procedimento de filtragem, as medições ficaram livre das oscilações de antes. Portanto, é uma coisa boa de ser feita, a não ser que você vá utilizar o sensor em um ambiente escuro livre da interferência solar.

Ligando dois ou mais sensores

Conforme foi dito no início do post, o endereço I2C do VL53L0X é programável. Desta forma, é possível utilizar diversos sensores ao mesmo tempo no mesmo barramento. E, pretendo mostrar como fazer isto com 2 sensores.

Circuito

A ideia do circuito é interligar os pinos SDA e SCL dos dois sensores, além de alimentar os sensores da mesma forma. E, também será necessário utilizar os pinos XSHUT dos sensores. Este último pode ser conectado em qualquer pino digital do Arduino (cada um em uma entrada digital separada).

Ligação de 2 sensores VL53L0x com o Arduino

Conforme dito anteriormente, a máxima tensão em cima do pino XSHUT deve ser 3.6V e o pino digital do Arduino funciona em 5V. Portanto, não podemos utilizar este pino como saída e aplicar um nível alto. Para isto, teríamos que utilizar um divisor de tensão, mas existe uma solução mais fácil que será explicada no próximo tópico.

Cheguei a utilizar o pino XSHUT com 5V várias vezes e meus sensores não queimaram, mas não é bom arriscar.

Como alterar o endereço I2C do sensor

Os sensores por padrão inicializam com o mesmo endereço I2C e é preciso alterá-los. Para isto, você mantém o pino XSHUT do sensor que você irá alterar em nível alto e desliga todos os outros colocando o pino XSHUT em nível baixo.

Como o Arduino não pode colocar o pino XSHUT em nível alto devido à tensão máxima, é necessário seguir outro caminho: definir o pino como entrada. Ao fazer isto, o pull-up interno do sensor automaticamente faz o pino XSHUT ficar em nível alto (~3,2V).

Com isto, resta apenas utilizar o comando abaixo para alterar o endereço (o valor está em hexadecimal, mas pode ser um número decimal mesmo):

1
2
sensor.setAddress(0x32);
// Basta alterar o valor de 0x32 para o endereço desejado (0-127 decimal)

Após aplicar o comando acima, o endereço do sensor já fica alterado. Depois, é só religar os outros sensores e repetir o procedimento caso seja necessário reconfigurar outro sensor.

Código completo

Leia os comentários para entender.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <Wire.h>
#include <VL53L0X.h>

// Instancia dois sensores
VL53L0X sensor, sensor2;

#define SHUT_1 7
#define SHUT_2 6

void setup()
{
  Serial.begin(9600);
  Wire.begin();

  // Define o sensor 2 como entrada para fazer o pino SHUT_2 ficar em nível alto
  pinMode(SHUT_1, OUTPUT);
  pinMode(SHUT_2, INPUT);

  // "Desliga" o sensor 1
  digitalWrite(SHUT_1, LOW);
  delay(2);

  // Altera o endereço do sensor 2
  sensor2.setAddress(0x32);

  // Religa o sensor 1 definindo ele como entrada
  pinMode(SHUT_1, INPUT);
  // É possível alterar o endereço do sensor 1 apenas com o código abaixo
  // Como o sensor 2 já está com endereço diferente, não é necessário desligá-lo,
  // pois ele não interferirá na comunicação
  //sensor.setAdress(0x31);
 
  // Inicializa sensores
  sensor.init();
  sensor2.init();
  sensor.setTimeout(500);
  sensor2.setTimeout(500);
}

void loop()
{
  // Mede a distância dos dois sensores
  int dist1 = sensor.readRangeSingleMillimeters();
  int dist2 = sensor2.readRangeSingleMillimeters();
 
  Serial.print(dist1);
  Serial.print(" - ");
  Serial.println(dist2);
}

Código de dois sensores com filtro

O código abaixo é complementar ao anterior, pois adiciona uma função para filtrar as medições dos dois sensores. E o método de filtragem é de acordo com o que foi mostrado no tópico sobre a interferência da luz do sol.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
#include <Wire.h>
#include <VL53L0X.h>

VL53L0X sensor, sensor2;

// Define os pinos XSHUT
#define SHUT_1 7
#define SHUT_2 6

int dist1_atual, dist1_old=0;
int dist2_atual, dist2_old=0;
unsigned long timeout1=0, timeout2=0;


void setup()
{
  Serial.begin(9600);
  Wire.begin();

  // Define o sensor 2 como entrada para fazer o pino SHUT_2 ficar em nível alto
  pinMode(SHUT_1, OUTPUT);
  pinMode(SHUT_2, INPUT);

  // "Desliga" o sensor 1
  digitalWrite(SHUT_1, LOW);
  delay(2);

  // Altera o endereço do sensor 2
  sensor2.setAddress(0x32);

  // Religa o sensor 1 definindo ele como entrada
  pinMode(SHUT_1, INPUT);

  // Inicializa sensores
  sensor.init();
  sensor2.init();
  sensor.setTimeout(500);
  sensor2.setTimeout(500);

  // Longo alcance
  sensor.setSignalRateLimit(0.1);
  sensor.setVcselPulsePeriod(VL53L0X::VcselPeriodPreRange, 18);
  sensor.setVcselPulsePeriod(VL53L0X::VcselPeriodFinalRange, 14);

  sensor2.setSignalRateLimit(0.1);
  sensor2.setVcselPulsePeriod(VL53L0X::VcselPeriodPreRange, 18);
  sensor2.setVcselPulsePeriod(VL53L0X::VcselPeriodFinalRange, 14);


  // Alta velocidade
  sensor.setMeasurementTimingBudget(20000);
  sensor2.setMeasurementTimingBudget(20000);
}

void loop()
{
  dist1_atual = sensor.readRangeSingleMillimeters();
  dist2_atual = sensor2.readRangeSingleMillimeters();
 
  filtrar_sinais();

  Serial.print(dist1_atual);
  Serial.print(" - ");
  Serial.println(dist2_atual);
}

void filtrar_sinais(void)
{
  if((dist1_atual > 8000) && ((millis()-timeout1) < 1000))
  {
    dist1_atual = dist1_old;
  }
  else
  {
    dist1_old = dist1_atual;
    timeout1 = millis();
  }

  if((dist2_atual > 8000) && ((millis()-timeout2) < 1000))
  {
    dist2_atual = dist2_old;
  }
  else
  {
    dist2_old = dist2_atual;
    timeout2 = millis();
  }
}