Controllo motore Nema17

Nano-TMC2209-ST7735

 

Contenuti:

1

Come controllare il motore passo-passo con il driver DRV8825 e Arduino

2

 

3  
4  

 

Vorrei pilotare con arduino un motore Nema17 utilizzando il driver TMC2209, un monitor ST7735 per visualizzare i dati che avrà bisogno di un convertitore logico TXS0108E e 2 Moduli step down XL4015 per le alimentazioni, due condensatorei uno elettrolitico ed uno ceramico per stabilizzare l'alimentazione.

 

Sul  monitor ho le seguenti informazioni: in alto viene indicato il passo impostato da 1 a 1/256, sulla successiva riga viene indicato il  moltiplicatore un numero a 4 cifre che moltiplica il valore dell'encoder, poi abbiamo due frecce una con punta a sinistra ed una con punta a destra che indicano la direzione di rotazione, visibili alternativamente. In fine, nell'ultima riga viene indicato l'RPM (i giri per minuto).

Quando si accende il sistema la situazione di partenza è la seguente:

  • Passo impostato ad 1 (passo intero)
  • Moltiplicatore ad 1
  • Direzione oraria, freccia verso destra
  • RPM 0000

Procediamo con le impostazioni:

  • Con l'encoder a rotella rosso sulla destra andiamo ad impostare il passo, potremo lasciarlo ad 1 per ora
  • Con l'encoder di sinistra andiamo ad impostare il moltiplicatore, per default è impostato a 1, premendo il pulsante con un click si attiva la modalità modifica, il primo digit a sinistra inizia a lampeggiare, ruotando l'encoder possiamo impostare il valore da 0 a 9, con un'altro click si attiva il digit successivo sulla destra, il precedete si fissa, conservando il valore impostato, l'attuale inizia a lampeggiare, ruotando posso settare il valore numerico. Di click in click raggiunto il quarto digit, al click successivo si tornera al primo digit e così via. Per uscire dalla modalità modifica eseguire un long press, a questo punto avremo impostato il moltiplicatore ad es. a 10 il che significa che se facciamo uno scatto con l'encoder di destra in realtà è come se ne facessimo 10, questo rende l'encoder della velocità più reattivo
  • Con l'encoder di destra posso:
    • Controllare la velocità: ruotandolo, i valori impostabili rimangono all'interno di un ramge impostato nel codice ad es tra 0 e 3000, quando raggiunto decrementando lo zero ruotando ulteriormente l'encoder il valore rimane fisso a 0, quando raggiunto il valore 3000 un'ulteriore rotazione verso destra non avrà effetto il valore rimarrà fisso su 3000
    • Impostare la direzione: con un long press del pulsante, ad ogni pressione la direzione cambierà verso
    • Fermare il motore: eseguendo un semplice click la velocità viene impostata a zero il motore si ferma

I valori sullo schermo si aggiorneranno in tempo reale.

 

Materiale necessario:

  • Arduino Nano
  • 2 Moduli step down XL4015
  • Motore passo-passo Nema 17 (1,7 A, 0,59 Nm)
  • Drive TMC2209 vedi
  • Condensatori: un 100µF elettrolitico 50V, un ceramico da 100nF (0.1µF) 50V
  • Resistenza da 220 Ohm
  • Tre encoder rotativi
  • Monotor LCD ST7735
  • TXS0108E Logic level converter
  • Un alimentatore a 24V per alimentare il motore
  • 3 interruttori

Schema di collegamento:

 

TMC2209

Lato sinistro:

  • ENABLE non utilizzato, rimane libero
  • MS1 (Microstep mode 1): libero
  • MS2 (Microstep mode 2): libero
  • ---R8 duplicato del pin UART lasciare libero
  • UART collegare al pin TX di Arduino mediante resistenza da 220 Ohm
  • NC non utilizzato
  • STP al Pin 3 per gli step
  • DIR al Pin 2 per la direzione

Lato destro:

  • VM al positivo dell'alimentatore a 24 V tramite i due condensatori
  • GND al negativo dell'alimentatore a 24 V tramite i due condensatori
  • 2B filo blu motore
  • 2A filo rosso motore
  • 1A filo nero motore
  • 1B filo verde motore
  • VDD AI 5v Arduino
  • GND comune a tutti i GND

Collegamenti cavi motore al driver:

  • Avvolgimento 1: Nero – Verde
  • Avvolgimento 2: Rosso – Blu

 

Schema su breadboard:

 

Schema su millefori - solo alimientazione:

 

 

Schema su millefori - completo:

 

Condensatori

Stabilizzano la tensione: i motori stepper assorbono corrente in modo impulsivo, e un condensatore aiuta a ridurre i cali di tensione.
Riducono i disturbi elettrici: evita picchi di tensione che potrebbero danneggiare il DRV8825 o altri componenti.
Proteggono da accensioni/spegnimenti improvvisi: aiuta a gestire la corrente di spunto quando il motore parte.

  • 100 µF - 470 µF, 50V (elettrolitico)
  • 100nF (0,1 µF) ceramico → filtra meglio i disturbi ad alta frequenza

Alimentazione Arduino:

  • VIN: 7.5V (positivo dell'alimentatore)
  • GND:7.5V (negativo dell'alimentatore)

Nema17full.ino

#include <Adafruit_GFX.h>
#include <Adafruit_ST7735.h>
#include <EncButton.h>
#include "Blink4.h" 
#include "VelDir.h" 
#include "Passo.h" 
#include "Etich.h" 
#include "StepperMotorController.h"

StepperMotorController stepper(4, 3, 7, 6, 5);
Adafruit_ST7735 tft(10, 9, 8);
EncButton velo(A0,A1,A2);
EncButton molt(A5, A4, A3);
EncButton pass(12,2,-1);

Blink4* dspDg;
VelDir* velDr;
Passo* setPs;
Etich* etic;

int stepMode = 0;
bool direz = true;

void setup() {
  stepper.begin();
   
  tft.initR(INITR_BLACKTAB);
  tft.setRotation(1);
  tft.fillScreen(ST77XX_BLACK);

  dspDg = new Blink4(90, 40, &tft, &molt);  
  dspDg->drawAll(true);
  velDr = new VelDir(8, 126, &tft, &velo);
  setPs = new Passo(&tft,&pass);
  etic = new Etich(&tft);
  etic->drawAll();
}

void loop() {
  dspDg->update();
  setPs->update();
  velDr->update();

  float passo = setPs->getValue();              // es. 0.25
  int idPas =  setPs->getValForCase();          // es. 0 - 5   
  int moltiplicatore = dspDg->getValue();       // es. 100
  int velocita = velDr->getValue();             // es. 2
  bool dir = velDr->getDir();                   // true = destra
  velDr->setMaxPassiSec(5000,dspDg->getValue());
  stepper.setMicrostepping(idPas);
  stepper.setParameters(passo, moltiplicatore, velocita, dir);
  stepper.update();

  velDr->drawRpm(stepper.getRpm());
}

 

Blink4.h

#pragma once
#include <Adafruit_GFX.h>
#include <Adafruit_ST7735.h>
#include <EncButton.h>
#include "DS_Digit18pt7b.h"

class Blink4 {
  private:
    Adafruit_ST7735* tft;
    EncButton* enc;
    const GFXfont* font;
    int x, y, spz;
    int cifre[4];
    int xPos[4];
    const int blinkInterval = 400;
    unsigned long lastBlink = 0;
    bool visible = true;
    bool inEdit = false;
    int pos = 0;
    uint16_t color;
    uint16_t holdSec;
    
  public:   
    Blink4(int x, int y, Adafruit_ST7735* display, EncButton* encod )
    : x(x), y(y), tft(display), enc(encod), spz(16), font(&DS_Digit18pt7b),
     color(0xffff00), holdSec(400)
    { 
      enc->setEncReverse(true);
      setLpTm(holdSec);    
      calcCfr();
      calcPos();
    }

    void setSpaz(int spazio) {
      spz = spazio;
      calcPos();
    }

    void setFont(const GFXfont* fnt) {
      font = fnt;
    }
    
    void setColor(uint32_t hexColor) {
        color = hexToColor565(hexColor);
    }
    unsigned int getValue() {
      return cifre[0] * 1000 + cifre[1] * 100 + cifre[2] * 10 + cifre[3];
    }

    void setLpTm(uint16_t msRit){
      enc->setHoldTimeout(msRit);
    }
   
    void drawAll(bool allVisible) {
        tft->setFont(font);
        for (int i = 0; i < 4; i++) {
          bool vis = allVisible || (inEdit && i == pos && visible);
          drawDigit(i, vis);
        }
      }
    void update() {
      enc->tick();

      if (enc->click()) {
        if (!inEdit) {
          inEdit = true;
          pos = 0;
          visible = true;
          lastBlink = millis();
        } else {
          drawDigit(pos, true);
          pos = (pos + 1) % 4;
          visible = true;
          lastBlink = millis();
        }
      }

      if (enc->hold()) {
        inEdit = false;
        visible = true;
        drawAll(true);
      }

      if (inEdit) {
        bool changed = false;

        if (enc->right()) {
          drawDigit(pos, false);
          cifre[pos] = (cifre[pos] + 1) % 10;
          changed = true;
        } else if (enc->left()) {
          drawDigit(pos, false);
          cifre[pos] = (cifre[pos] + 9) % 10;
          changed = true;
        }

        if (changed || millis() - lastBlink >= blinkInterval) {
          visible = !visible;
          drawDigit(pos, visible);
          lastBlink = millis();
        }
      }
    }

private:
    void calcPos() {
        xPos[0] = x;
        for (int i = 1; i < 4; i++) {
            xPos[i] = xPos[i - 1] + spz;
        }
    }
    void calcCfr(){
      for (int i = 0; i < 4; i++) cifre[i] = 0;
    }

 uint16_t hexToColor565(uint32_t hexColor) {
    uint8_t r = (hexColor >> 16) & 0xFF;
    uint8_t g = (hexColor >> 8) & 0xFF;
    uint8_t b = hexColor & 0xFF;
    return tft->color565(r, g, b);  // o tft.color565(...) se non è un puntatore
  }

    void drawDigit(int p, bool vis) {
      tft->setFont(font);
      tft->setCursor(xPos[p], y);
      tft->setTextColor(vis ? color : ST77XX_BLACK, ST77XX_BLACK);
      tft->print(cifre[p]);
    }

    
};

 

VelDir.h

#pragma once
#include <Adafruit_GFX.h>
#include <Adafruit_ST7735.h>
#include <EncButton.h>
#include "DS_Digit36pt7b.h"

class VelDir {
private:
    Adafruit_ST7735* tft;
    EncButton* enc;
    const GFXfont* font;
    int x, y;
    uint16_t color;
    int value = 0;
    int maxVal = 1000;
    bool dir = true; // true = destra, false = sinistra
    uint16_t arrowRightColor = ST77XX_GREEN;
    uint16_t arrowLeftColor = ST77XX_RED;
    uint16_t bgColor = ST77XX_BLACK;
    uint16_t txtColor = ST77XX_YELLOW;
    uint32_t holdSec;
    bool changed = true;
    int valuePrecedente = -9999;
    int prevValue = -1;
    bool prevDir = !dir;

public:
    VelDir(int x, int y, Adafruit_ST7735* display, EncButton* encod)
        : x(x), y(y), tft(display), enc(encod), font(&DS_Digit36pt7b),
          color(0xffff00), holdSec(400) {
        enc->setHoldTimeout(holdSec);
        enc->setEncReverse(true);
    }

    void setFont(const GFXfont* f) { font = f; }
    void setColor(uint16_t c) { txtColor = c; }
    void setHoldTm(uint32_t ms) { holdSec = ms; enc->setHoldTimeout(holdSec); }

    void setMaxPassiSec(int maxPassiSec, int moltiplicatore) {
        if (moltiplicatore <= 0) return;
        maxVal = maxPassiSec / moltiplicatore;
    }

    int getValue() const { return value; }
    bool getDir() const { return dir; }

    void drawRpm(uint16_t rpm) {
        static int lastDigits[4] = {-1, -1, -1, -1};
        int digits[4] = {0, 0, 0, 0};
        int temp = rpm;

        for (int i = 3; i >= 0; i--) {
            digits[i] = temp % 10;
            temp /= 10;
        }

        // Calcola l'indice del primo digit significativo
        int firstDigit = 0;
        while (firstDigit < 3 && digits[firstDigit] == 0) {
            firstDigit++;
        }

        tft->setFont(font);

        for (int i = 0; i < 4; i++) {
            int xPos = 10 + i * 36;

            // Se la cifra è non significativa e non è l'ultima (cioè, tutto zero)
            if (i < firstDigit) {
                if (lastDigits[i] != -1) {
                    tft->setTextColor(bgColor, bgColor);
                    tft->setCursor(xPos, y);
                    tft->print(lastDigits[i]); // cancella
                    lastDigits[i] = -1;
                }
                continue; // salta stampa
            }

            if (digits[i] != lastDigits[i]) {
                // Cancella cifra precedente
                tft->setTextColor(bgColor, bgColor);
                tft->setCursor(xPos, y);
                if (lastDigits[i] >= 0) tft->print(lastDigits[i]);

                // Scrive nuova cifra
                tft->setTextColor(txtColor, bgColor);
                tft->setCursor(xPos, y);
                tft->print(digits[i]);

                lastDigits[i] = digits[i];
            }
        }
    }

    void update() {
        enc->tick();

        if (enc->right() && value < maxVal) value++;
        if (enc->left() && value > 0) value--;
        if (enc->click()) value = 0;
        if (enc->hold()) dir = !dir;

        if (dir != prevDir) {
            drawArrow();
            prevDir = dir;
        }
    }

private: 
    uint16_t hexToColor565(uint32_t hexColor) {
        uint8_t r = (hexColor >> 16) & 0xFF;
        uint8_t g = (hexColor >> 8) & 0xFF;
        uint8_t b = hexColor & 0xFF;
        return tft->color565(r, g, b);
    }

    void drawArrow() {
        int cy = 60;
        int size = 14;

        // Cancella entrambe le aree prima di ridisegnare
        tft->fillRect(8, cy - size - 2, 30, size * 2 + 4, bgColor);     // area sinistra
        tft->fillRect(122, cy - size - 2, 30, size * 2 + 4, bgColor);   // area destra

        if (dir) {
            // Freccia a DESTRA (verde)
            int cx = 136;
            tft->fillTriangle(
                cx - size, cy - size,
                cx - size, cy + size,
                cx + size, cy,
                arrowRightColor
            );
        } else {
            // Freccia a SINISTRA (rossa)
            int cx = 22;
            tft->fillTriangle(
                cx + size, cy - size,
                cx + size, cy + size,
                cx - size, cy,
                arrowLeftColor
            );
        }

    }
};

 

Passo.h

#pragma once
#include <Adafruit_GFX.h>
#include <Adafruit_ST7735.h>
#include <EncButton.h>
#include "ARLRDBD10pt7b.h"

class Passo {
private:
    Adafruit_ST7735* tft;
    EncButton* enc;
    int x, y;
    const GFXfont* font;
    uint16_t textColor;
    int value = 0;
 
public:
    Passo(Adafruit_ST7735* display, EncButton* encod): tft(display), enc(encod), font(&ARLRDBD10pt7b){    
        tft->setFont(font);
        enc->setEncReverse(true);
    }

    void setFont(const GFXfont* newFont) {
        if (newFont) {
            font = newFont;
            tft->setFont(font);
        }
    }

    void setColor(uint32_t hexColor) {
        textColor = hexToColor565(hexColor);
        tft->setTextColor(textColor);
    }

    void setCursor(int newX, int newY) {
        x = newX;
        y = newY;
        tft->setCursor(x, y);
    }

    float getValue(){
        static const float stepTable[] = {1, 0.5, 0.25, 0.125, 0.0625, 0.03125};
        return stepTable[value];        
    }
    
    int getValForCase() {
        return value;
    }

    void draw(const String& text) {
        tft->setCursor(x, y);
        tft->print(text);
    }

    void update() {
    //static int value = 0;  // mantiene il valore tra 0 e 5
    static int lastValue = -1;
    tft->setFont(font);
    enc->tick();  // aggiorna lo stato dell'encoder

    if (enc->right() && value < 5) {
        value++;
    } else if (enc->left() && value > 0) {
        value--;
    }

    if (value != lastValue) {
        static const char* values[] = { "1", "1/2", "1/4", "1/8", "1/16", "1/32" };

        // Cancella il valore precedente scrivendolo in nero
        tft->setTextColor(0x0000, 0x0000);  // nero su nero
        tft->setCursor(98, 14);
        tft->print(values[lastValue >= 0 ? lastValue : 0]);

        // Scrive il nuovo valore in bianco su sfondo nero
        tft->setTextColor(0xFFFF, 0x0000);  // giallo su nero
        tft->setCursor(98, 14);
        tft->print(values[value]);

        lastValue = value;
    }
}

private:
 uint16_t hexToColor565(uint32_t hexColor) {
    uint8_t r = (hexColor >> 16) & 0xFF;
    uint8_t g = (hexColor >> 8) & 0xFF;
    uint8_t b = hexColor & 0xFF;
    return tft->color565(r, g, b);  // o tft.color565(...) se non è un puntatore
    }
};

 

Etich.h

#pragma once
#include <Adafruit_GFX.h>
#include <Adafruit_ST7735.h>
#include "ARLRDBD8pt7b.h"
#include "ARLRDBD14pt7b.h"  // Font di default

class Etich {
private:
    Adafruit_ST7735* tft;
    int x, y;
    const GFXfont* font;
    uint16_t textColor;
 
public:
    Etich(Adafruit_ST7735* display): tft(display){    
        tft->setFont(&ARLRDBD8pt7b);
        setColor(0x00ff00);
    }

    void setFont(const GFXfont* newFont) {
        if (newFont) {
            font = newFont;
            tft->setFont(font);
        }
    }

    void setColor(uint32_t hexColor) {
        textColor = hexToColor565(hexColor);
        tft->setTextColor(textColor);
    }

    void setCursor(int newX, int newY) {
        x = newX;
        y = newY;
        tft->setCursor(x, y);
    }

    void setCenter(int newY, String str) {
    y = newY;
    // Usa getTextBounds per ottenere le dimensioni del testo
    int16_t x1, y1;
    uint16_t textWidth = 0, textHeight = 0;
    tft->getTextBounds(str.c_str(), 0, 0, &x1, &y1, &textWidth, &textHeight);
    
    // Calcola la posizione orizzontale centrata
    int centerX = (tft->width() - textWidth) / 2;
    setCursor(centerX, y);
    tft->print(str);
  }

    void draw(const String& text) {
        tft->setCursor(x, y);
        tft->print(text);
    }
    void drawAll() {
        setCursor(7, 33);
        draw("Multiplier:");
        setCursor(40, 12);
        draw("Passo: ");
        setFont(&ARLRDBD14pt7b);
        setCursor(50, 68);
        draw("RPM");
    }
private:
 uint16_t hexToColor565(uint32_t hexColor) {
    uint8_t r = (hexColor >> 16) & 0xFF;
    uint8_t g = (hexColor >> 8) & 0xFF;
    uint8_t b = hexColor & 0xFF;
    return tft->color565(r, g, b);  // o tft.color565(...) se non è un puntatore
    }
};

 

StepperMotorController.h

#pragma once

#include <Arduino.h>
#include <TMCStepper.h>

#define R_SENSE 0.11f // Valore standard se non conosci quello esatto, altrimenti sostituiscilo

class StepperMotorController {
private:
    uint8_t stepPin, dirPin;
    TMC2209Stepper driver;

    bool direction = true;
    float passo = 1.0;

    unsigned long lastStepTime = 0;
    unsigned long delayPerStep = 1000; // in microsecondi
    int rpm = 0;
    int passiAlSecondo = 0;

public:
    StepperMotorController(uint8_t step, uint8_t dir)
        : stepPin(step), dirPin(dir), driver(&Serial, R_SENSE) {}

    void begin() {
        pinMode(stepPin, OUTPUT);
        pinMode(dirPin, OUTPUT);

        Serial.begin(115200);  // UART per TMC2209 via pin TX (D1)
        delay(500);            // Tempo per inizializzazione driver

        driver.begin();                     // Inizializza il driver
        driver.rms_current(1200);           // Corrente RMS prudenziale (mA)
        driver.microsteps(16);              // Imposta il microstepping
        driver.en_spreadCycle(false);       // Usa stealthChop per funzionamento silenzioso
        driver.pdn_disable(true);           // Disabilita pin PDN (default HIGH)
        driver.I_scale_analog(false);       // Disabilita controllo analogico corrente
    }

    void setMicrostepping(uint16_t microstepValue) {
        driver.microsteps(microstepValue);  // Es: 1, 2, 4, 8, 16, 32, 64, 128, 256
    }

    void setCurrent(uint16_t mA) {
        driver.rms_current(mA);             // Imposta corrente RMS in mA
    }

    void setParameters(float passoVal, int moltiplicatore, int velocita, bool dir) {
        passo = passoVal;
        direction = dir;

        float passiPerGiro = 200.0 / passo;
        passiAlSecondo = moltiplicatore * velocita;

        delayPerStep = (passiAlSecondo > 0) ? (1000000UL / passiAlSecondo) : 1000000UL;
        if (delayPerStep < 200) delayPerStep = 200;

        rpm = (uint16_t)((float)passiAlSecondo * 60.0 / passiPerGiro);

        digitalWrite(dirPin, direction);
    }

    void update() {
        unsigned long now = micros();

        if (passiAlSecondo <= 0) {
            digitalWrite(stepPin, LOW);
            return;
        }

        if (now - lastStepTime >= delayPerStep) {
            digitalWrite(stepPin, HIGH);
            delayMicroseconds(delayPerStep / 2);
            digitalWrite(stepPin, LOW);
            delayMicroseconds(delayPerStep / 2);
            lastStepTime = micros();
        }
    }

    int getRpm() const {
        return rpm;
    }
};