Microscópio motorizado OpenMV com precisão de 0,0001 milímetro
Tutorial em vídeo OpenMV da SingTown - Eu construí um microscópio motorizado OpenMV com precisão de 0,0001 milímetro!
O microscópio OpenFlexure é um microscópio de código aberto, baixo custo e modular, projetado para ser fácil de montar, operar e modificar. O projeto visa oferecer uma alternativa flexível e acessível aos microscópios de laboratório tradicionais para educação, pesquisa e ambientes com poucos recursos. A SingTown adicionou suporte deste microscópio ao OpenMV.
O microscópio OpenFlexure tem as seguintes vantagens:
(1) A característica mais marcante é a estrutura da plataforma, que aproveita a flexibilidade do material PLA, não sendo afetada por atrito ou vibração, e é extremamente simples de montar.
(2) A precisão é muito alta: um passo do motor de passo é menor que cem nanômetros, ou seja, 1/100 de uma hemácia.
(3) Outra vantagem é o custo relativamente baixo da impressão 3D.
O mais interessante da estrutura do microscópio é que ele aproveita a flexibilidade do material PLA, e o corpo principal é impresso de uma só peça. O movimento da plataforma não tem atrito, e a precisão é muito boa, alcançando 100 nanômetros de precisão. Como os microscópios prontos têm limitações significativas, não usamos um microscópio pronto — em vez disso, nos baseamos no design do microscópio OpenFlexure e o modificamos, projetando todo o caminho óptico do zero.
Fizemos as seguintes modificações no microscópio OpenFlexure para adaptá-lo ao OpenMV, sendo as quatro principais:
(1) Primeiro, modificamos o tubo da lente para adaptá-lo ao sensor de imagem da câmera OpenMV. Vale destacar que há uma lente de tubo aqui, que tem duas funções: encurtar a distância focal da objetiva e ajustar a abertura NA, para tornar a imagem mais clara.
(2) A segunda modificação foi aprimorar o design da gaveta inferior, além de adicionar botões e uma placa de acionamento de motor de passo, permitindo o controle preciso do movimento e do foco do microscópio.
(3) O terceiro ponto foi projetar uma nova placa de acionamento de motor de passo para o OpenMV, que pode controlar quatro motores de passo via I2C.
(4) Por fim, adicionamos a tela LCD do OpenMV e projetamos uma alimentação independente para todo o conjunto.
Estrutura geral
A principal modificação na estrutura geral foi substituir o esquema original baseado em Raspberry Pi pelo esquema OpenMV, adicionar botões e uma tela LCD, e usar uma bateria para alimentação independente, permitindo operação autônoma.
O microscópio como um todo é fabricado com tecnologia de impressão 3D. Graças à alta flexibilidade das peças impressas em 3D com baixo preenchimento e paredes finas, o microscópio não precisou de uma estrutura mecânica complexa, implementando o movimento da plataforma e da lente de forma extremamente engenhosa — detalhado mais adiante neste texto.

Movimento da plataforma e do tubo da lente
Nesta parte, o design do OpenFlexure aproveita a grande flexibilidade das peças impressas em 3D com um determinado preenchimento e espessura, criando uma estrutura muito engenhosa que realiza a translação da plataforma.

A estrutura central que realiza o movimento da plataforma é semelhante a uma escada, como mostrado na figura abaixo. A instabilidade do quadrilátero interno dessa estrutura é o cerne do movimento da plataforma. Ao imprimir essa estrutura em uma impressora 3D, é preciso garantir que a impressora consiga imprimir as duas travessas intermediárias — essas travessas são a chave para fornecer flexibilidade suficiente à estrutura, portanto há certas exigências quanto ao desempenho da impressora 3D.

Tomando o eixo X como exemplo para o movimento da plataforma: depois de garantir que o corpo principal do microscópio já foi impresso, o movimento da plataforma no eixo X requer apenas um conjunto de parafusos e um elástico. O parafuso passa por uma engrenagem grande e se conecta ao suporte da "escada"; a engrenagem grande será posteriormente acionada pelo motor de passo. O elástico também se conecta ao suporte da escada, fornecendo uma força oposta à do parafuso. A ilustração da instalação do parafuso e do elástico é mostrada nas duas figuras a seguir:

O parafuso e o elástico fornecem, respectivamente, as duas forças que deslocam a plataforma na direção do eixo X. Quando o motor de passo aciona a engrenagem grande em sentido horário, o parafuso se tensiona, o suporte da escada é levantado, a escada se desloca para a esquerda, realizando o movimento da plataforma na direção positiva do eixo X; quando o motor de passo aciona a engrenagem grande em sentido anti-horário, o parafuso relaxa, o suporte da escada é puxado para baixo pela força do elástico, a escada se desloca para a direita, e a plataforma se move na direção negativa do eixo X:
Parafuso tensionado

Estado inicial

Parafuso relaxado

O movimento na direção do eixo Y funciona da mesma maneira.
O movimento vertical do tubo da lente usa o mesmo método: quando o parafuso é tensionado, o suporte é levantado e a lente se move para baixo; quando o parafuso é relaxado, o suporte é puxado para baixo pelo elástico e a lente se move para cima.
Composição do tubo da lente
No tubo da lente, foram feitas algumas modificações para adaptá-lo ao sensor de imagem da câmera OpenMV. O projeto original do microscópio OpenFlexure usa a câmera arducam_b0196 do Raspberry Pi, cujo tamanho não é compatível com a lente do OpenMV. Modificamos o código OpenSCAD original para adicionar suporte à lente do OpenMV:
Lente original:

Lente OpenMV:

O tubo da lente aloja a lente convexa, o sensor de imagem e a objetiva:
Lente convexa:

Sensor de imagem OpenMV:

Objetiva:

Controle do motor de passo
Considerando que seria necessário adicionar botões e uma tela LCD posteriormente, e que o motor de passo 28BYJ-48 pode ser controlado sem um driver dedicado de motor de passo, adotamos aqui o chip PCA9555BS para expansão de IO, usando comunicação I2C. Usamos o transistor Darlington ULN2803LVS para acionar o motor de passo. O OpenMV envia via I2C os pulsos de acionamento para o PCA9555BS, que gera nas suas saídas de IO os níveis alto e baixo correspondentes para acionar o motor de passo por meio do Darlington:
O OpenMV é configurado com I2C por software, com P4 como SCL e P5 como SDA:
i2c = SoftI2C(scl=Pin("P4"), sda=Pin("P5"))
O endereço do PCA9555BS é 0x20, e todas as IOs são inicializadas em modo de saída:
pca = PCA9555(i2c, address = 0x20)
for i in range(16):
pca.outputPins(i)
Enviando pulsos da esquerda para a direita o motor de passo gira no sentido direto; enviando da direita para a esquerda, o motor gira em sentido reverso:
[(1,0,0,0),(1,1,0,0),(0,1,0,0),(0,1,1,0),(0,0,1,0),(0,0,1,1),(0,0,0,1),(1,0,0,1),]
Placa de expansão do motor de passo:

Esquema da placa de expansão do motor de passo:

Varredura dos botões
Os botões usam outra placa de expansão de IO PCA9555BS para leitura, também usando o I2C por software do OpenMV, com P4 como SCL e P5 como SDA. Mas o endereço do PCA9555BS dos botões é 0x21, e as IOs são configuradas em modo de entrada:
pca2 = PCA9555(i2c, address = 0x21)
for i in range(16):
pca2.inputPins(i)
Placa de expansão de botões:

Esquema da placa de expansão de botões:

Bandeja de PCB
No projeto original do microscópio, a bandeja era feita para o Raspberry Pi e não tinha interruptor, tela nem botões — o microscópio não podia funcionar de forma independente. Redesenhamos a bandeja para adaptá-la ao OpenMV, adicionando os furos para os botões, a porta de depuração USB do OpenMV e o slot do interruptor.
Bandeja original:

Bandeja OpenMV:

A PCB é montada sobre a bandeja, conectada à tela LCD por meio de cabos jumper, a bateria é instalada, e então tudo é colocado na base:
Vista frontal:

Vista superior:

Vista lateral direita:

Tela LCD
Usamos a tela LCD que acompanha o OpenMV, conectada ao OpenMV por meio de cabos jumper, e fixada na parte externa da base do microscópio.
Base:

Tela LCD:

Para garantir que a tela LCD exiba a imagem completa, o código a seguir gira e redimensiona a imagem original para que ela corresponda à resolução da tela:
lcd.write(img.copy(x_scale=0.25, y_scale=0.267, hint=image.ROTATE_90))
Demonstração dos resultados do microscópio
Arranjo de pixels RGB da tela de um celular:

Lâmina de nematódeo:

Esfregaço de sangue de rã:

Lâmina de paramécio:

Código completo do OpenMV
main.py
from machine import SoftI2C, Pin
from pca9555 import PCA9555
import csi, image, time
import display
csi0 = csi.CSI()
csi0.reset()
csi0.pixformat(csi.RGB565)
csi0.framesize(csi.VGA)
csi0.snapshot(time = 2000)
clock = time.clock()
i2c=SoftI2C(scl=Pin("P4"),sda=Pin("P5"))
#print(i2c.scan())
pca=PCA9555(i2c, address = 0x20)
pca2=PCA9555(i2c, address = 0x21)
for i in range(16):
pca.outputPins(i)
for i in range(16):
pca2.inputPins(i)
thresholds = [
(0, 34, -128, 127, -128, 127),
]#hemácias
thresholds2 = [
(0, 45, -128, 127, -128, 127),
]#paramécios
thresholds3 = [
(0, 48, -128, 127, -128, 127),
]#euglenas
class Motor():
def __init__(self, motor_id):
self.step_list = [(1,0,0,0),(1,1,0,0),(0,1,0,0),(0,1,1,0),(0,0,1,0),(0,0,1,1),(0,0,0,1),(1,0,0,1)]
self.state_index = 0
if motor_id == 1:
self.pins = [0,1,2,3]
elif motor_id == 2:
self.pins = [4,5,6,7]
elif motor_id == 3:
self.pins = [8,9,10,11]
elif motor_id == 4:
self.pins = [12,13,14,15]
def step(self, inverse=False):
if inverse:
self.state_index -= 1
if self.state_index == -1:
self.state_index = 7
else:
self.state_index += 1
if self.state_index == 8:
self.state_index = 0
for i in range(4):
pca.writePin(self.pins[i], self.step_list[self.state_index][i])
def steps(self, n, delay=10):
inverse = False
if n > 0:
inverse = True
for i in range(abs(n)):
self.step(inverse)
time.sleep_ms(1)
mz = Motor(1)
#m2 = Motor(2)
mx = Motor(3)
my = Motor(4)
def key_read():
if pca2.readPin(0)==0:
my.steps(50)
# time.sleep_ms(10)
elif pca2.readPin(1)==0:
mx.steps(50)
# time.sleep_ms(10)
elif pca2.readPin(2)==0:
my.steps(-50)
# time.sleep_ms(10)
elif pca2.readPin(3)==0:
mx.steps(-50)
# time.sleep_ms(10)
elif pca2.readPin(4)==0:
mz.steps(50)
# time.sleep_ms(10)
elif pca2.readPin(5)==0:
mz.steps(-50)
cell_num = 0
photo_num = 0
step_num = 0
sum_num = 0
avg_num = 0
lcd = display.SPIDisplay()
while(True):
key_read()
clock.tick()
img = csi0.snapshot()
# strs = str(photo_num) +".jpg"
# img.save(strs)
# photo_num=photo_num+1
# for blob in img.find_blobs(thresholds, pixels_threshold=20, area_threshold=0,merge=0): #hemácias
# if blob.elongation > 0:
# img.draw_cross((blob.cx, blob.cy),size=9)
# cell_num=cell_num+1
# for blob in img.find_blobs(thresholds2, pixels_threshold=200, area_threshold=200,merge=0): #paramécios
# if blob.elongation > 0:
# img.draw_cross((blob.cx, blob.cy))
# cell_num=cell_num+1
# for blob in img.find_blobs(thresholds3, pixels_threshold=70, area_threshold=70,merge=0): #euglenas
# if blob.elongation > 0:
# img.draw_cross((blob.cx, blob.cy))
# cell_num=cell_num+1
sum_num=sum_num+cell_num
cell_num=0
step_num=step_num+1
if step_num == 20:
avg_num=sum_num/step_num
step_num=0
sum_num=0
if round(avg_num)!=0:
print(round(avg_num))
lcd.write(img.copy(x_scale=0.25,y_scale =0.267,hint=image.ROTATE_90)) # Take a picture and display the image.
# print(clock.fps())
microscope.py
from machine import SoftI2C, Pin
from pca9555 import PCA9555
import time
i2c=SoftI2C(scl=Pin("P4"),sda=Pin("P5"))
print(i2c.scan())
pca=PCA9555(i2c, address = 0x20)
for i in range(16):
pca.outputPins(i)
class Motor():
def __init__(self, motor_id):
self.step_list = [(1,0,0,0),(1,1,0,0),(0,1,0,0),(0,1,1,0),(0,0,1,0),(0,0,1,1),(0,0,0,1),(1,0,0,1)]
self.state_index = 0
if motor_id == 1:
self.pins = [0,1,2,3]
elif motor_id == 2:
self.pins = [4,5,6,7]
elif motor_id == 3:
self.pins = [8,9,10,11]
elif motor_id == 4:
self.pins = [12,13,14,15]
def step(self, inverse=False):
if inverse:
self.state_index -= 1
if self.state_index == -1:
self.state_index = 7
else:
self.state_index += 1
if self.state_index == 8:
self.state_index = 0
for i in range(4):
pca.writePin(self.pins[i], self.step_list[self.state_index][i])
def steps(self, n, delay=10):
inverse = False
if n > 0:
inverse = True
for i in range(abs(n)):
self.step(inverse)
time.sleep_ms(10)
m1 = Motor(1)
while True:
m1.steps(10)
m1.steps(-10)
pca9555.py
from machine import SoftI2C, Pin
import time
InputPort0 = 0x00
InputPort1 = 0x01
OutputPort0 = 0x02
OutputPort1 = 0x03
PolInversionPort0 = 0x04
PolInversionPort1 = 0x05
ConfigPort0 = 0x06
ConfigPort1 = 0x07
#############################################################################
class PCA9555:
"""PCA955 Driver
16Bit IO extender
i2c communication
"""
def __init__(self, i2cBus, address=0x20):
"""
Args:
i2cBus: SoftI2C(Pin(*SCLpin*),Pin(*SDApin*))
address: i2c address in hex (0x20 by default)
"""
self.i2c = i2cBus
self.address = address
self.pinStats=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
self.pinValues=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
def inputPins(self,inputPin):
"""
Args:
inputPin(int): 0-7 = IO0_0 - IO0_7 ; 8-15 = IO1_0-IO1_7
"""
self.pinStats[inputPin]=1
stats=0
if inputPin <= 7:
for i in range(8):
stats+=self.pinStats[i]<<i
stats=int(hex(stats),0)
self.i2c.writeto_mem(self.address,ConfigPort0,bytes([stats]))
else:
for i in range(8):
stats+=self.pinStats[i+8]<<i
stats=int(hex(stats),0)
self.i2c.writeto_mem(self.address,ConfigPort1,bytes([stats]))
def outputPins(self,outputPin):
"""
Args:
outputPin(int): 0-7 = IO0_0 - IO0_7 ; 8-15 = IO1_0-IO1_7
"""
self.pinStats[outputPin]=0
stats=0
if outputPin <= 7:
for i in range(8):
stats+=self.pinStats[i]<<i
stats=int(hex(stats),0)
self.i2c.writeto_mem(self.address,ConfigPort0,bytes([stats]))
else:
for i in range(8):
stats+=self.pinStats[i+8]<<i
stats=int(hex(stats),0)
self.i2c.writeto_mem(self.address,ConfigPort1,bytes([stats]))
def writePin(self,pin, value):
"""
Args:
Pin(int): 0-7 = IO0_0 - IO0_7 ; 8-15 = IO1_0-IO1_7
value(int): 0 = off ; 1 = on
"""
self.pinValues[pin]=value
vals=0
if self.pinStats[pin]:
print('ATTENTION Pin '+str(pin)+' at i2c address '+str(self.address)+' is configed as INPUT')
return
elif pin <= 7:
for i in range(8):
vals+=self.pinValues[i]<<i
vals=int(hex(vals),0)
self.i2c.writeto_mem(self.address,OutputPort0,bytes([vals]))
else:
for i in range(8):
vals+=self.pinValues[i+8]<<i
vals=int(hex(vals),0)
self.i2c.writeto_mem(self.address,OutputPort1,bytes([vals]))
# print(vals)
def readPin(self, pin):
"""Issue a measurement.
Args:
writeAddress (int): address to write to
:return:
"""
comeback = bytearray(1)
if not self.pinStats[pin]:
print('ATTENTION Pin '+str(pin)+' at i2c address '+str(self.address)+' is configed as OUTPUT')
return
elif pin <=7:
comeback =self.i2c.readfrom_mem(self.address,InputPort0,1)
else:
self.i2c.readfrom_mem(self.address,InputPort1,2)
raw = (comeback[0] >> (pin % 8)) & 1
return raw
Download dos arquivos de impressão 3D
Arquivos de impressão 3D do microscópio motorizado OpenMV da SingTown
Lista de materiais (BOM) e etapas de montagem do microscópio