terça-feira, junho 08, 2021

Sensor Ultrassônico HC-SR04 com o Raspberry Pi Pico (usando o PIO)

À medida em que vou entendendo o funcionamento do PIO (Programmable I/O) do Raspberry Pi Pico, mais oportunidades de usá-lo eu vou enxergando. O dispositvo da vez é o sensor de distância HC-SR04, cujo funcionamento já examinamos oito anos atrás (!).


Resumindo para quem teve preguiça de clicar no link, o HC-SR04 emite um sinal ultrassônico quando colocamos um pulso de 10us no pino "trigger" e depois mantêm o pino "echo" em nível alto até receber o eco do sinal (ou passar o tempo máximo de 38ms). Estes valores eu obtive em um datasheet da Elec Freaks, já encontrei alguns códigos que usam valores diferentes.

Antes de olhar o software, vamos ver a conexão ao PiPico. São apenas 4 sinais:

  • Vcc: o datasheet fala 5V. Já vi descrições ligando a 3,3V, para mim não funcionou direito. Como estou alimentando o PiPico pela USB, liguei o VCC ao VBUS.
  • GND: ligando ao GND do PiPico.
  • Trigger: o datasheet fala que é um sinal TTL de entrada, portanto pode ser ligado direto a um pino do PiPico. No meu teste usei o pino 3.
  • Echo: outro sinal TTL, mas é um sinal de saída. Alimentado a 5V, este sinal poderá ficar acima dos 3,3V e queimar o PiPico. A solução é o tradicional divisor resistivo: o sinal vai para um resistor de 22K cuja outra ponta vai para o pino 3 do PiPico e um resistor de 33K com a outra ponta em terra.

No Arduino, costuma-se usar a função pulseIn para medir o pulso do sinal echo. Os exemplos que eu achei em MicroPython usam utime.ticks_us() e um loop. O resultado parece bom, mas (a) o processador fica preso no loop e (b) interrupções podem interferir na medida. Usando o PIO a medição roda independente do processador. Vamos aproveitar e usar o PIO também para gerar o pulso de trigger.

Minha primeira decisão foi qual clock a usar no PIO. Vamos querer medir um tempo entre 150us e 38ms, para um resolução razoável resolvi adotar um tempo de ciclo de meio microssegundo. Ou seja, um clock de 2MHz no PIO.

O programa em si não é complexo, mas existem algumas peculiaridades nas instruções do PIO:
  • Um registrador só pode ser iniciado por programa com um valor de 0 a 31. O jeito é receber o valor inicial do contador pela fila de transmissão e movê-lo para o registrador que fara a contagem (PULL, MOV X,OSR)
  • A instrução WAIT não tem timeout. Se não tiver um sensor ligado, o programa vai ficar parado no WAIT do início do pulso. O jeito seria fazer a espera "na unha" (como é feito na espera do fim do pulso). Fica como exercício para o leitor.
  • A instrução de desvio (JMP) só tem a opção de desviar se um pino estiver em nível alto. Como estamos esperando o pino retornar ao nível baixo, o código fica meio esquisito.
  • Para fazer um loop com contador a instrução JMP oferece somente o desvio quando o contador for diferente de zero, com pós decremento. Ou seja se o contador for zero segue em frente, senão decrementa e desvia. Meio estranho, mas se encaixa bem neste caso.
O resultado final é que o programa do PIO recebe um valor de timeout (usei 300000 ciclos, o que equivale a 150ms) e retorna quanto sobrou ao final do pulso. Aí é calcular quantos ciclos demorou para o sinal baixar, converter em microssegundos e determinar a distância (usando a velocidade de 340m/s e lembrando que o tempo é para ida e volta).

Segue o programa completo:
import utime
import rp2 
from rp2 import PIO, asm_pio
from machine import Pin

# Programa para o PIO
@asm_pio(set_init=rp2.PIO.OUT_LOW,autopush=False)
def ULTRA_PIO():
    # aguarda uma solicitação do programa
    pull()
    mov (x,osr)
    
    # gera um pulso de 10 us (20 ciclos)
    set(pins,1) [19]
    set(pins,0)
    
    # aguarda (indefinidamente) o inicio do pulso de eco
    wait(1,pin,0)
    
    # aguarda o fim do pulso de eco
    # decrementa X a cada 2 ciclos (1us)
    label('espera')
    jmp(pin,'continue')
    jmp('fim')    
    label('continue')
    jmp(x_dec,'espera')
    
    # retorna a duração do pulso de eco
    label('fim')
    mov(isr,x)
    push()
    
trigger = Pin(3, Pin.OUT)
echo = Pin(2, Pin.IN)
sm = rp2.StateMachine(0)
while True:
    sm.init(ULTRA_PIO,freq=2000000,set_base=trigger,in_base=echo,jmp_pin=echo)
    sm.active(1)
    sm.put(300000)
    val = sm.get()
    sm.active(0)
    tempo = 300000 - val
    distancia = (tempo * 0.0343) / 2
    print('Distancia = {0:.1f} cm'.format(distancia))
    utime.sleep_ms(1000)

Um comentário:

not yet disse...

Obrigado pelo conteudo!!
Fiz um teste do seu codigo, com duas maquinas de estado e dois sensores em um simulador online...
https://wokwi.com/projects/328389014755213906
Usei o clock da segunda SM 34000 qual o clock minimo na prática?
obs.. é o meu primeiro cod em python deve estar muito ruim...