ESP32-Bibliothek für Trinamic TMC2208-Schrittmotor-Treiber

Eigentlich war gar nicht vorgesehen, eine eigene Bibliothek für den TMC2208 zu schreiben. Der Plan war, den bisherigen Schrittmotor-Treiber durch einen TMC2208 Treiber zu ersetzen und dann im sogenannten Legacy Mode zu betreiben. Aber es kam dann doch ganz anders…

Der bisherige Schrittmotor-Treiber für das Zyklochron ist sehr einfach aufgebaut. Ein ULN2803A NPN Darlington-Transistor-Array steuert die Stränge des Schrittmotors direkt an, was jedoch bestenfalls einen Halbschrittbetrieb ermöglicht. Bei den 28BYJ-48 Schrittmotoren fällt das nicht weiter auf, da der Motor ein Getriebe mit einer Übersetzung von ca. 1:64 hat. Dennoch sind die Schrittmotoren recht laut, was gerade bei einer Uhr sehr störend sein kann. Bei der Recherche nach einer besseren Lösung bin dann auf den TMC2208 von Trinamic gestoßen. Diese Schrittmotor-Treiber gelten quasi als Referenz, was die Lautstärke betrifft. Neben der Lautstärke hat der Treiber noch einen zweiten großen Vorteil. Laut Datenblatt arbeitet er schon ab einer Spannung von 4,75 Volt. Das ist deswegen wichtig, weil die Uhr über einen USB Anschluss mit Strom versorgt wird.

Ein Vergleich des Aufbaus der beiden Treiber zeigt auf den ersten Blick, dass der TMC2208 in einer ganz anderen Liga spielt:

ULN2803

TMC2208

ULN2803

TMC2208

In einem ersten Testaufbau liefen die Schrittmotoren wunderbar leise. Vom Funktionieren der Schaltung überzeugt, habe ich gleich eine Platine bestellt und zwei Wochen später in das Gehäuse eingebaut. Doch dann kam das böse Erwachen. Die Uhr war alles andere als lautlos. Das Geräusch ist schwer zu beschreiben, eine Mischung aus Rauschen und Pfeifen. Und es trat nur dann auf, wenn beide Motoren liefen und war umso lauter, je langsamer sich die Schrittmotoren drehten. Interessanterweise schwankte die Lautstärke mit der Zeit. Mein Verdacht ist, dass hier irgendwelche Resonanzen auftreten, und da der Treiber permanent seine Parameter justiert, dieser Effekt unterschiedlich stark auftritt.

UART Schnittstelle

Neben dem Legacy Mode beherrscht der TMC2208 noch den OTP (one-time program) Mode, auf den ich hier nicht näher eingehe, und den UART Mode. Damit ist es möglich, viele Parameter des TMC2208 anzupassen.

Grundsätzlich beherrscht jedes TMC2208 Modul den UART Mode, es kann jedoch sein, dass zuerst eine Lötbrücke geschlossen werden muss, damit das Pin des Moduls mit der UART Schnittstelle des Chips verbunden ist. Das Einfachste ist es, gleich ein Modul zu kaufen, auf dem diese Verbindung schon vorhanden ist. Hier drei Beispiele, wovon bei dem ersten Modul von BIGTREETECH mit der Bezeichnung TMC2208 V3.0-UART die Lötbrücke schon geschlossen ist.

BIGTREETECH TMC2208 UART
BIGTREETECH TMC2208 UART
BIGTREETECH TMC2208
BIGTREETECH TMC2208
FYSETC TMC2208
FYSETC TMC2208

Über die UART Schnittstelle kann der TMC2208 von jedem Mikrocontroller gesteuert werden. Es handelt sich dabei um einen Eindraht Bus, das heißt die Sende- und Empfangsleitung teilen sich eine Ader. Um den TMC2208 mit dem ESP32 zu verbinden, wird das RX Pin direkt mit dem UART Pin des Treibers und das TX Pin über einem 1k Widerstand mit dem UART Pin verbunden. Die gesamte Schaltung schaut folgendermaßen aus:

ESP32 TMC2208 UART Connection

Die Enable Leitung könnte man sich sparen, da der TMC2208 über das UART Protokoll aktiviert/deaktiviert werden kann. Da der ESP32 aber genug IO Pins hat, habe ich auch diese Möglichkeit vorgesehen. Den VM Anschluss würde man normalerweise mit einer eigenen Spannungsversorgung für den Schrittmotor verbinden. Aber da ich die gesamte Elektronik ausschließlich über den USB Anschluss mit Strom versorgen will, habe ich den Anschluss direkt mit der restlichen Spannungsversorgung verbunden.

UART Kommunikation

Wie kommuniziert nun der ESP32 Mikrokontroller mit dem Schrittmotor Treiber? Dazu werden sogenannte Datagramme verwendet, mit denen die Register des TCM2208 gelesen bzw. geschrieben werden können, wobei einige Register nur gelesen oder geschrieben werden können. Die folgende Tabelle gibt einen Überblick über die verfügbaren Register:

Register Adresse Lesen/Schreiben Beschreibung
 GCONF  0x00 R/W Globale Konfigurations Flags 
 GSTAT  0x01 R/W (W löscht) Globale Status Flags
 IFCNT 0x02 R Zähler für Schreibzugriffe
 SLAVECONF  0x03 W Verzögerung bei Lesezugriff
 OTP_PROG 0x04 W Schreibzugriff programmiert OTP-Speicher 
 OTP_READ 0x05 R Zugriff auf den OTP-Speicher
 IOIN 0x06 R Liest den Zustand aller verfügbaren Eingangspins
 FACTORY_CONF  0x07 R/W FCLKTRIM und OTTRIM Werkseinstellungen
 IHOLD_IRUN  0x10 W Steuerung des Treiberstroms 
 TPOWERDOWN  0x11 W Zeit bis zum Abschalten des Motorstroms
 TSTEP 0x12 R Gemessene Zeit zwischen zwei Mikroschritten
 TPWMTHRS 0x13 W Obere Geschwindigkeit für den StealthChop 
 VACTUAL 0x22 W Bewegung des Motors durch UART-Steuerung
 MSCNT 0x6A R Mikroschrittzähler
 MSCURACT 0x6B R Aktueller Mikroschrittstrom
 CHOPCONF 0x6C R/W Chopper und Treiber Konfiguration
 DRV_STATUS  0x6F R Treiber Status Flags
 PWMCONF 0x70 R/W StealthChop PWM Chopper Konfiguration
 PWM_SCALE 0x71 R Ergebnisse des StealthChop Amplitudenreglers
 PWM_AUTO 0x72 R Generierte Werte für PWM_GRAD/PWM_OFS


Eine genaue Beschreibung aller Register und viele weitere Informationen findet man in dem Datenblatt des TMC2208

Die Struktur der Datagramme für Schreib- und Lesezugriffe ist ähnlich. Jedes Datagramm beginnt mit vier Bits zur Synchronisation. Dadurch kann sich der TMC2208 automatisch auf die verwendete Baudrate einstellen. Am Ende jedes Datagramms ist eine Prüfsumme, um die Richtigkeit der Daten zu gewährleisten.

Das Datagramm für den Schreibzugriff hat folgenden Aufbau:

UART Datagramm für Schreibzugriff
Sync + reserviert 8 Bit Adresse 7 Bit Register und Schreib-Bit 32 Bit Daten CRC
1 0 1 0  Egal, zählt aber für CRC Immer 0x00 Adresse 1 Daten Bytes
3, 2, 1, 0 
Prüfsumme
0 1 2 3 4 5 6 7 8 ... 15 16 ... 23 24 ... 55 56 ... 63


Wichtig ist, dass das Bit nach der Adresse des Registers auf Eins gesetzt ist. Bei jedem erfolgreichen Schreibzugriff wird ein Zähler erhöht. Dadurch kann überprüft werden, ob die Daten wirklich geschrieben wurden.

Der Lesezugriff benötigt zwei Datagramme. Zunächst wird dem TMC2208 durch einen Schreibzugriff mitgeteilt, welches Register man lesen möchte. Dieses Datagramm sieht folgendermaßen aus:

UART Datagramm für Anforderung Lesezugriff
Sync + reserviert 8 Bit Adresse 7 Bit Register und Lese-Bit CRC
1 0 1 0  Egal, zählt aber für CRC Immer 0x00 Adresse 0 Prüfsumme
0 1 2 3 4 5 6 7 8 ... 15 16 ... 23 24 ... 31


Wie oben beschrieben gibt es nur eine Signalleitung für Lesen und Schreiben. Daher sendet der TMC2208 die Antwort nicht sofort, sondern wartet eine bestimmte Zeit, die durch das SLAVECONF Register konfiguriert werden kann. Standardwert ist die Zeit, die 8 Bits bei der aktuellen Baudrate benötigen. Das Datagramm mit der Antwort ist fast identisch mit dem Datagramm für den Schreibzugriff:

UART Datagramm für Antwort Lesezugriff
Sync + reserviert 8 Bit Adresse 7 Bit Register und Lese-Bit 32 Bit Daten CRC
1 0 1 0  Egal, zählt aber für CRC Immer 0xFF Adresse 0 Daten Bytes
3, 2, 1, 0 
Prüfsumme
0 1 2 3 4 5 6 7 8 ... 15 16 ... 23 24 ... 55 56 ... 63


Der Aufbau der Datagramme und die Kommunikation mit dem Schrittmotortreiber ist für alle Mitglieder der TMC22xx Familie mit UART Schnittstelle gleich. Sie unterscheiden sich lediglich durch ihre Funktionen und damit Anzahl und Inhalt der Register.

ESP32 Komponente

Der ESP32 verfügt über drei UART-Controller, die zur Ansteuerung des TMC2208 verwendet werden können. Damit können maximal drei Schrittmotoren unabhängig voneinander konfiguriert werden. Die Bibliothek ist mit dem  ESP-IDF Framework von Espressif in C geschrieben. 
Den Kern der Komponente bilden die Funktionen, die Datagramme senden und empfangen. Die Struktur aller Register ist in einem Header File definiert. Das GCONF Register ist beispielsweise folgendermaßen definiert:

typedef union {
    uint32_t value;
    struct {
        uint32_t
        I_scale_analog   :1,
        internal_Rsense  :1,
        en_spreadcycle   :1,
        shaft            :1,
        index_otpw       :1,
        index_step       :1,
        pdn_disable      :1,
        mstep_reg_select :1,
        multistep_filt   :1,
        test_mode        :1,
        reserved         :22;
    };
} tmc2208_gconf_reg_t;


Die Funktion zum Schreiben eines Registers ist recht kurz und besteht hauptsächlich aus dem Setzen der Datagramm-Felder und dem Schreiben auf die UART Schnittstelle:

 static void write_register(stepper_driver_tmc2208_t *tmc2208, tmc2208_datagram_t *reg)
 {
    ESP_LOGD(TAG, "Write register 0x%02x", reg->addr.idx);

    tmc2208_write_datagram_t datagram;

    datagram.msg.sync = 0x05;
    datagram.msg.slave = 0x00;
    datagram.msg.addr.idx = reg->addr.idx;
    datagram.msg.addr.write = 1;
    datagram.msg.payload.value = reg->payload.value;

    byteswap(datagram.msg.payload.data);

    calcCRC(datagram.data, sizeof(datagram.data));

    uart_write_bytes(tmc2208->driver_config.uart_port, &datagram, sizeof(datagram));
    uart_wait_tx_done(tmc2208->driver_config.uart_port, UART_MAX_DELAY);
    uart_flush(tmc2208->driver_config.uart_port);
}


Folgende Punkte müssen beachtet werden:

  • Die 32-Bit-Datenwörter werden mit dem höchsten Byte zuerst übertragen. Daher müssen mit der byteswap Funktion die 4 Bytes vertauscht werden
  • Da die serielle Schnittstelle nur eine Leitung verwendet, landen alle gesendete Daten automatisch im RX Buffer. Daher muss der RX Buffer nach dem Senden gelöscht werden

Das Lesen eines Registers sieht ähnlich aus, nur dass erst ein Datagramm gesendet werden muss und anschließend die Antwort empfangen wird:

static esp_err_t read_register(stepper_driver_tmc2208_t *tmc2208, tmc2208_datagram_t *reg)
{
    esp_err_t ret = ESP_OK;

    ESP_LOGD(TAG, "Read register 0x%02x", reg->addr.idx);

    tmc2208_read_request_datagram_t request_datagram;
    tmc2208_read_reply_datagram_t reply_datagram;

    request_datagram.msg.sync = 0x05;
    request_datagram.msg.slave = 0x00;
    request_datagram.msg.addr.idx = reg->addr.idx;
    request_datagram.msg.addr.write = 0;

    calcCRC(request_datagram.data, sizeof(request_datagram.data));

    uart_write_bytes(tmc2208->driver_config.uart_port, &request_datagram, sizeof(request_datagram));
    uart_wait_tx_done(tmc2208->driver_config.uart_port, UART_MAX_DELAY);
    uart_flush(tmc2208->driver_config.uart_port);

    uart_read_bytes(tmc2208->driver_config.uart_port, &reply_datagram, sizeof(reply_datagram), UART_MAX_DELAY);

    if(reply_datagram.msg.slave == 0xff && reply_datagram.msg.addr.idx == request_datagram.msg.addr.idx) {
        uint8_t reply_crc = reply_datagram.msg.crc;
        calcCRC(reply_datagram.data, sizeof(reply_datagram.data));
        if((reply_crc == reply_datagram.msg.crc)) {
            reg->payload.value = reply_datagram.msg.payload.value;
            byteswap(reg->payload.data);
        }
        else {
            ESP_LOGE(TAG, "CRC failed: %d %d", reply_crc, reply_datagram.msg.crc);
            ret = ESP_FAIL;
        }
    }
    else {
        ESP_LOGE(TAG, "Reply datagram corrupt: slave %d addr %d", reply_datagram.msg.slave, reply_datagram.msg.addr.value);
        ret = ESP_FAIL;
    }

    return ret;
}


Anhand des Wertes der Slave-Adresse und der CRC Prüfsumme kann überprüft werden, ob die Daten korrekt übertragen worden sind. Da diese Überprüfung beim Schreiben eines Registers nicht möglich ist, gibt es noch eine Funktion, die Anhand des IFCNT Registers feststellt, ob das Register korrekt geschrieben worden ist:

static esp_err_t write_register_safe(stepper_driver_tmc2208_t *tmc2208, tmc2208_datagram_t *reg)
{
    uint8_t ifcnt_pre;
    uint8_t ifcnt_after;

    read_register(tmc2208, (tmc2208_datagram_t *)&tmc2208->ifcnt);
    ifcnt_pre = tmc2208->ifcnt.reg.count;

    write_register(tmc2208, reg);

    read_register(tmc2208, (tmc2208_datagram_t *)&tmc2208->ifcnt);
    ifcnt_after = tmc2208->ifcnt.reg.count;

    bool ok = ifcnt_after - ifcnt_pre == 1;

    if (ok) {
        return ESP_OK;
    }
    else {
        ESP_LOGE(TAG, "Write failed: %d %d", ifcnt_pre, ifcnt_after);
        return ESP_FAIL;
    }
}


Mit der RS232 Dekodierfunktion eines Oszilloskops kann man überprüfen, ob die Daten richtig über die serielle Schnittstelle geschickt werden. Setzt man zum Beispiel bestimmte Werte im GCONF Register mittels

// Set default values
tmc2208->gconf.reg.pdn_disable = 1; // 0: PDN_UART controls standstill current reduction, 1: PDN_UART input function disabled
tmc2208->gconf.reg.I_scale_analog = 0; // 0: Use internal reference derived from 5VOUT,  1: Use voltage supplied to VREF as current reference
tmc2208->gconf.reg.mstep_reg_select = 1;  // 0: Microstep resolution selected by pins MS1, MS2, 1: Microstep resolution selected by MSTEP register 
tmc2208->gconf.reg.en_spreadcycle = 0;  // 0: StealthChop PWM mode enabled, 1: SpreadCycle mode enabled 
tmc2208->gconf.reg.multistep_filt = 1;  // 0: No filtering of STEP pulses, 1: Software pulse generator optimization enabled 
    
write_register(tmc2208, (tmc2208_datagram_t *)&tmc2208->gconf);


werden diese Datensignale erzeugt:

TMC2208 UART datagram write

Die angezeigten Werte entsprechend genau dem, was für ein Schreib-Datagramm erwartet wird. Man sieht auch sehr schön, dass alle auf der TX-Leitung gesendetes Bytes auch auf der RX-Leitung erscheinen und der RX Buffer daher nach dem Senden wieder gelöscht werden muss.

Das Lesen des GCONF Registers erzeugt diese Signale:

TMC2208 UART datagram read

Hier sieht man sehr schön die Pause nach dem Senden des Request Datagramms, die exakt der Zeit für das Übertragen eines Bytes entspricht.

Das Bewegen des Schrittmotors kann auf zwei verschiedene Weisen erfolgen. Für eine konstante Drehung reicht es aus, einen Wert ungleich 0 in das VACTUAL Register zu schreiben. Dieser Wert gibt die Motordrehzahl in Mikroschritten pro Zeiteinheit an. Die Motorrichtung wird durch das Vorzeichen von VACTUAL gesteuert. Um den Motor um eine genaue Anzahl von Schritten zu drehen, verwende ich das RMT (Remote Control) Modul. Mit diesem Modul ist es möglich, eine genaue Abfolge von Impulsen zu erzeugen, die über die STEP Leitung des TMC2208 den Motor um einzelne Schritte drehen. Der Vorteil dieser Methode ist, dass die CPU während dem Senden der Signale nicht blockiert wird.

Dazu muss das RMT Modul zunächst initialisiert werden:

rmt_driver_install(tmc2208->driver_config.channel, 0, 0);
rmt_config_t config = RMT_DEFAULT_CONFIG_TX(tmc2208->driver_config.step_pin, tmc2208->driver_config.channel);
rmt_config(&config);

Die Funktion zum Erzeugen und Senden der Signale sieht folgendermaßen aus:

// Allocate memory for the RMT items
rmt_item32_t* items = (rmt_item32_t*) pvPortMalloc(sizeof(rmt_item32_t) * steps);
if (items == NULL) {
	ESP_LOGE("RMT", "Failed to allocate memory for RMT items");
	return ESP_FAIL ;
}

// Configure the RMT items
for (int i = 0; i < steps; i++) {
	items[i].level0 = 1;
	items[i].duration0 = signal_duration;
	items[i].level1 = 0;
	items[i].duration1 = signal_duration;
}

ret = rmt_write_items(tmc2208->driver_config.channel, items, steps, true);

// Free the memory for the RMT items
vPortFree(items);

return ret;

Der Wert von signal_duration gibt die Anzahl der Ticks an, die das Signal im High bzw. Low Zustand ist. Bei einem Peripherie-Bus (APB) Takt von 80 MHz und einem Teiler von 80 (Default Konfiguration) entspricht ein Tick genau 1 µs. Das folgende Bild zeigt das Signal bei einer signal_duration von 100 Ticks:

ESP32 RMT signals

Wie erwartet beträgt die Pulsbreite genau 100 µs. Über die Frequenz der Impulse wird die Geschwindigkeit gesteuert, mit der sich der Motor dreht.

Die folgende Liste gibt einen Überblick über die Funktionen des Schrittmotor APIs. Im Source Code gibt es ein wenig Dokumentation, aber das ist sicher noch ausbaufähig :-)

esp_err_t (*init)(stepper_driver_t *handle);
esp_err_t (*clear_gstat)(stepper_driver_t *handle);

esp_err_t (*enable)(stepper_driver_t *handle);
esp_err_t (*disable)(stepper_driver_t *handle);
esp_err_t (*direction)(stepper_driver_t *handle, uint8_t direction);
esp_err_t (*steps)(stepper_driver_t *handle, uint32_t steps, uint32_t signal_duration);

esp_err_t (*set_vactual)(stepper_driver_t *handle, int32_t speed);
esp_err_t (*set_tpowerdown)(stepper_driver_t *handle, uint8_t tpowerdown);
esp_err_t (*set_stealthchop_thrs)(stepper_driver_t *handle, uint32_t tpwmthrs);
esp_err_t (*set_current)(stepper_driver_t *handle, uint16_t milliampere_run, uint8_t percent_hold);

esp_err_t (*set_microsteps_per_step)(stepper_driver_t *handle, stepper_driver_microsteps_t microssteps);
esp_err_t (*set_tbl)(stepper_driver_t *handle, uint8_t tbl);  
esp_err_t (*set_toff)(stepper_driver_t *handle, uint8_t toff);
esp_err_t (*set_hysteresis)(stepper_driver_t *handle, uint8_t hstrt, uint8_t hend); 

esp_err_t (*set_pwm_lim)(stepper_driver_t *handle, uint8_t pwm_lim);
esp_err_t (*set_pwm_reg)(stepper_driver_t *handle, uint8_t pwm_reg);
esp_err_t (*set_pwm_freq)(stepper_driver_t *handle, stepper_driver_pwm_freq_t pwm_freq);
esp_err_t (*set_pwm_grad)(stepper_driver_t *handle, uint8_t grad);
esp_err_t (*set_pwm_offset)(stepper_driver_t *handle, uint8_t offset);
esp_err_t (*disable_pwm_autograd)(stepper_driver_t *handle);
esp_err_t (*enable_pwm_autograd)(stepper_driver_t *handle); 
esp_err_t (*disable_pwm_autoscale)(stepper_driver_t *handle);
esp_err_t (*enable_pwm_autoscale)(stepper_driver_t *handle);
    
esp_err_t (*read_register_gstat)(stepper_driver_t *handle);
esp_err_t (*read_register_tstep)(stepper_driver_t *handle);
esp_err_t (*read_register_drv_status)(stepper_driver_t *handle);
esp_err_t (*read_register_ioin)(stepper_driver_t *handle);
esp_err_t (*read_register_otp_read)(stepper_driver_t *handle);
esp_err_t (*read_register_mscntd)(stepper_driver_t *handle);
esp_err_t (*read_register_mscuract)(stepper_driver_t *handle);

esp_err_t (*dump_register_tstep)(stepper_driver_t *handle);
esp_err_t (*dump_register_drv_status)(stepper_driver_t *handle);
esp_err_t (*dump_register_ioin)(stepper_driver_t *handle);
esp_err_t (*dump_register_gconf)(stepper_driver_t *handle);
esp_err_t (*dump_register_otp_read)(stepper_driver_t *handle);
esp_err_t (*dump_register_chopconf)(stepper_driver_t *handle);
esp_err_t (*dump_register_pwmconf)(stepper_driver_t *handle);
esp_err_t (*dump_register_factory_conf)(stepper_driver_t *handle);
esp_err_t (*dump_register_mscuract)(stepper_driver_t *handle);
esp_err_t (*dump_register_pwm_scale)(stepper_driver_t *handle);
esp_err_t (*dump_register_pwm_auto)(stepper_driver_t *handle);


Abschließend noch ein kurzes Beispiel, wie die Komponente verwendet wird:

// Create new driver
stepper_driver_t *motor = stepper_driver_new_tmc2208(&config->stepper_driver_conf);
// Init driver
stepper_driver_init(motor);
// Clear status
stepper_driver_clear_gstat(motor);

stepper_driver_set_stealthchop_thrs(motor, 0);
stepper_driver_set_current(motor, 150, 100);
stepper_driver_set_microsteps_per_step(motor, MICROSTEPS_1);

stepper_driver_enable_pwm_autograd(motor);
stepper_driver_enable_pwm_autoscale(motor);
stepper_driver_set_pwm_reg(motor, 1);
stepper_driver_set_pwm_freq(motor, FREQ_2_512);
stepper_driver_enable(motor);

stepper_driver_steps(motor, 30000, 200);


Den kompletten Source Code der ESP32 Komponente gibt es auf GitHub. Über Feedback oder Verbesserungsvorschläge würde ich mich sehr freuen.

Konversation wird geladen