Controllare motore DC con encoder con Raspberry Pi

Di seguito illustro come realizzare una classe Python per il controllo di motori DC per il controllo di semplici motori DC per mezzo di un ponte H, sfruttando nel contempo il rilevamento della posizione offerto da un encoder magnetico.

Vantaggi di un motore DC + encoder

Sebbene un motore possa risultare meno efficiente sia uno stepper che un motore brushless, è anche decisamente più facile da controllare: basta infatti un semplice ed economico ponte H controllabile con soli due pin del microcontroller + qualche riga di codice.
Contrariamente ad uno stepper poi, l’encoder (che necessità anch’esso di due pin) garantisce di non perdere il conto dei passi effettivamente compiuti, eliminando il problema degli skipped steps in caso di stallo. Inoltre non è necessario dover azzeccare sequenze e timing per ottenere il massimo dal motore.

Hardware usato

Come motore ho usato questo a €9, più spedizione da AliExpress.
La ragione di questa scelta è che l’encoder è già incluso e montato, il gearing incrementa il momento del motore (a scapito della velocità) e la barra filettata M4 consente di impiegare il motore come un attuatore lineare relativamente potente e compatto. Ho optato per la versione a 12V, che consuma tra i 0.3A (senza carico) e i 1.5A (poco prima dello stallo).

Come ponte H ho usato un DRV8871. Adafruit ha sviluppato per questo chip una comoda breakout board, come sempre clonata in Cina e venduta su AliExpress e Ebay a prezzi irrisori.

Ho condotto i miei test con un PCB dedicato (realizzato da JLCPCB) in quanto parti di un mio altro progetto. Ma ciò non è assolutamente necessario e si può far funzionare tutto con una breadboard e i soliti jumpers.

Ho anche progettato delle parti in plastica complementari alle quali ho aggiunto due guide lineari da 5mm, due boccole LM5UU e un dado M4.

Wiring

Schema

Ho collegato direttamente al Raspberry i due input del DRV8871 e i due output dell’encoder. Quindi ho collegato al polo positivo e negativo del motore i due output del DRV8871. Infine ho collegato 12V al pin Vmot del DRV8871 e uno dei pin da 3.3V del raspberry all’alimentazione dell’encoder. Ovviamente almeno un pin GND del Raspberry, il polo negativo della sorgente a 12V, il GND dell’encoder e il GND del DRV8871 devono essere collegati fra loro.

Controllare ponte H

Come la maggior parte dei ponte H, il funzionamento del DRV8871 è molto semplice e necessita di due pin di input, che chiameremo mot_a e mot_b: se entrambi i pin di input sono LOW, al motore non viene fornita elettricità; se uno degli input è HIGH e l’altro è LOW, il motore si muove in una direzione; invertendo HIGH e LOW, il motore si muove nella direzione opposta. È anche possibile dosare la potenza del motore usando un segnale PWM anziché un HIGH costante.

Decodifica encoder

La decodifica dell’encoder può sembrare complicata, ma nella pratica è estremamente semplice e ci consente di tener traccia di quanti giri ha effettuato il motore in una direzione e nell’altra monitorando solo due pin.

Funzionamento encoder

Durante ogni giro dell’encoder, entrambi i pin leggeranno un segnale HIGH per metà giro e LOW per l’altra metà. Ciò che permette il funzionamento dell’encoder è che questi segnali sono sfasati. In particolare se l’encoder si muove in una direzione, noteremo che un pin (che chiameremo enc_a) diventerà HIGH leggermente prima dell’altro (enc_b). Se l’encoder si muove nella direzione opposta, sarà enc_b ad anticipare enc_a.

In sintesi ci basta eseguire un controllo nell’esatto momento in cui uno dei due pin, ad esempio enc_a, diventa HIGH. Se enc_b è ancora LOW, vuol dire che enc_a anticipa enc_b e ci stiamo muovendo in una certa direzione. Se invece enc_b è già HIGH, vuol dire che enc_b anticipa enc_a e ci stiamo muovendo nella direzione opposta.

Homing

Questo tipo di motore condivide uno degli svantaggi degli stepper motor: il posizionamento è solo relativo e non assoluto. Ossia, possiamo sapere di quanti giri si è mosso il motore, ma il conto viene resettato ogni volta che si riavvia il programma che lo controlla. Inoltre non è possibile stabilire la posizione all’avvio che sarà sempre 0, anche se magari si trova già a finecorsa.

Questo problema viene risolto nelle CNC e nelle stampanti 3D introducendo all’avvio una procedura di homing: il motore viene fatto girare in una direzione (generalmente in senso negativo) finché non si urta uno switch posto a finecorsa. Alla fine della procedura di homing la posizione del motore è nota (perché coincide con quella dello switch) e il contatore dei giri può essere posto a zero in sicurezza, assicurando la ripetibilità dei movimenti successivi.

Codice

Ho scritto una piccola classe in Python 3 senza troppre pretese per il controllo del motore.

import RPi.GPIO as GPIO
import time

class MotorEncoder:
    def __init__(self, mot_a, mot_b, enc_a, enc_b, end):
        self.mot_a = mot_a
        self.mot_b = mot_b
        self.enc_a = enc_a
        self.enc_b = enc_b
        self.end = end

        self.moving = False
        self.target = None
        self.steps = -1

        if GPIO.getmode() == None:
            GPIO.setmode(GPIO.BCM)

        GPIO.setup(self.mot_a, GPIO.OUT)
        GPIO.setup(self.mot_b, GPIO.OUT)
        GPIO.setup(self.enc_a, GPIO.IN, pull_up_down=GPIO.PUD_UP)
        GPIO.setup(self.enc_b, GPIO.IN, pull_up_down=GPIO.PUD_UP)
        GPIO.setup(self.end, GPIO.IN, pull_up_down=GPIO.PUD_UP)

    def count_steps(self, pid=0):

        if GPIO.input(self.enc_b):
            self.steps -= 1
        else:
            self.steps += 1

        if self.steps is not None and self.steps == self.target:
            self.target = None
            self.stop()

    def stop(self, pid=0):
        GPIO.output(self.mot_a,0)
        GPIO.output(self.mot_b,0)

        GPIO.remove_event_detect(self.enc_a)

        self.moving = False

    def up(self, pid=0):
        self.stop()
        GPIO.output(self.mot_a,1)
        GPIO.output(self.mot_b,0)

        GPIO.add_event_detect(self.enc_a, GPIO.RISING, callback=self.count_steps)

        self.moving = True

    def down(self, pid=0):
        self.stop()
        GPIO.output(self.mot_a,0)
        GPIO.output(self.mot_b,1)

        GPIO.add_event_detect(self.enc_a, GPIO.RISING, callback=self.count_steps)

        self.moving = True

    def home(self, pid=0):
        if GPIO.input(self.end):
            # homing
            # move down, stop on endstop
            GPIO.add_event_detect(self.end, GPIO.FALLING, callback=self.stop)
            self.down()
            self.wait()
            GPIO.remove_event_detect(self.end)

            time.sleep(.1)

            # bounce
            # move up until endstop is released
            GPIO.add_event_detect(self.end, GPIO.RISING, callback=self.stop)
            self.up()
            self.wait()
            GPIO.remove_event_detect(self.end)

            self.steps = 0

    def goto(self, step, wait=True):

        if step > self.steps:
            self.target = step
            self.up()
        if step < self.steps:
            self.target = step
            self.down()

        if wait:
            self.wait()

    def wait(self, delta=.1):
        while self.moving:
            time.sleep(delta)

Inizializzazione

Nella dichiarazione è necessario inserire i pin usati nel seguente ordine:

  • mot_a
  • mot_b
  • enc_a
  • enc_b
  • endstop

L’endstop, quando premuto, collega il pin a ground. In questo modo è possibile utilizzare il pullup interno del raspberry, ma occorre tener conto che la logica è invertita (LOW quando premuto, HIGH quando non premuto).

Ho usato inoltre il pullup interno anche sui pin dell’encoder, ma a seconda del tipo di encoder questo potrebbe non essere necessario.

Metodi

  • up(): muove il motore verso l’alto e imposta lo stato del motore a “in movimento”
  • down(): lo stesso, ma muove il motore verso il basso
  • stop(): interrompe i movimenti del motore e ne imposta lo stato a “non in movimento”
  • wait(): attende che il motore si fermi
  • goto(): porta il motore alla posizione desiderata
  • home(): esegue l’homing (verso il basso)

Esempio d’uso

Basta salvare il codice in un file MotorEncoder.py, quindi inserire tale file nella stessa directory che ospita il programma python che si vuole scrivere e importare la classe dal module con la direttiva import.

from MotorEncoder import MotorEncoder

    try:
        motor = MotorEncoder(26, 19, 13, 6, 5)
        motor.home()
        motor.goto(1000)
		motor.home()

    finally:
        GPIO.cleanup()

Commenti