Silnik krokowy

Artykuł opisuje sterowanie bipolarnym silnikiem krokowym. W materiale postaram się przedstawić budowę i zasadę działania tego typu silników oraz implementację sterowania z wykorzystaniem modułu DRV8834 oraz zestawu Nucelo-L476RG. W materiale opiszę zarówno sterowanie ciągle (stosowane np. do napędzania robotów), jak i obrót o zadany kąt (użyteczne w przypadku drukarek 3D, manipulatorów i maszyn CNC).

Budowa i zasada działania silnika krokowego

Silnik krokowy jest rodzajem silnika prądu stałego. Jego cechą charakterystyczną jest to, że gdy jest zasilany, to nie obraca się w sposób stały, tylko impulsowy – podanie zasilania na jego uzwojenie powoduje obrót wału o ustalony kąt. W odróżnieniu od popularnych szczotkowych silników prądu stałego, silnik krokowy nie wykorzystuje szczotek. Dodatkowo element ferromagnetyczny jest umieszczony w wirniku (części obracającej się), a zwoje w stojanie (obudowie). Do podstawowych różnic możemy zaliczyć także fakt, że silniki krokowe nie wymagają dodatkowych przekładni mechaniczny, jednak ich prędkość obrotowa jest znacznie mniejsza niż prędkość osiągana przez silniki szczotkowe.

Ze względu na rodzaj wirnika (budowę) wyróżnia się trzy odmiany silników krokowych:

  • silnik z magnesem trwałym – wirnik ma postać magnesu trwałego z dwoma biegunami. Zasilone uzwojenia wywołują ruch wirnika o zadany kąt.
  • silnik o zmiennej reluktancji – wirnik ma postać elementu z naciętymi rowkami (jak na rysunku powyżej). Obrót wywoływany jest dzięki wykorzystaniu zmiennej reluktancji (czyli rezystancji magnetycznej), która jest największa, gdy wypustki wirnika pokrywają się z uzwojeniami.
  • silnik hybrydowy – łączy zalety silnika z magnesem trwałym i reluktancyjnego. Wirnik ma postać magnesu trwałego z wyciętymi na obwodzie zębami.

Silniki krokowe możemy podzielić także ze względu na kierunek prądu płynącego przez uzwojenia. I tak wyróżniamy silniki krokowe unipolarne i bipolarne. W silniku unipolarnym prąd płynie tylko w jednym kierunku, a ruch obrotowy wirnika wywoływany jest przez podawanie napięcia na kolejne uzwojenia. Możemy go rozpoznać po 5 lub 6 wyprowadzeniach. W silniku bipolarnym w trakcie obrotu prąd płynie przez obie pary uzwojeń w przeciwnym kierunkach. Taki silnik ma zazwyczaj 4 wyprowadzenia i charakteryzuje się większym momentem obrotowym, niż silnik unipolarny.

Wśród zalet silników krokowych możemy wyróżnić przede wszystkim możliwość precyzyjnego sterowania w otwartej pętli sprzężenia zwrotnego (bez potrzeby montażu dodatkowych czujników obrotu) oraz duży moment obrotowy w stanie spoczynku. W silniku krokowym nie musimy stosować dodatkowych przekładni mechanicznych oraz mamy możliwość szybkiego rozbiegu, hamowania i zmiany kierunku ruchu. Brak szczotek zapewnia natomiast dużą niezawodność i żywotność.

Wśród wad możemy wymienić ograniczone możliwości osiągania dużych prędkości obrotowych oraz występowanie zjawiska „gubienia kroków” w przypadku, gdy wał silnika jest nadmiernie obciążony. Poza tym silniki krokowe pobierają dość dużo prądu i wydzielają dużo ciepła, dlatego wymagają dobrego chłodzenia. Należy podkreślić także to, że silniki krokowe można łatwo uszkodzić podając na nie zbyt duży prąd – kluczowe jest tutaj ograniczenie poboru prądu przez sterownik.

Ze względu na sposób działania i możliwość precyzyjnego sterowania, silniki krokowe stały się bardzo popularne w urządzeniach, gdzie wykonywanie dokładnego ruchu jest kluczowe m.in. w maszynach CNC, frezarkach, czy coraz bardziej popularnych drukarkach 3D. W robotyce silniki krokowe wykorzystywane są np. w manipulatorach.

Sterownik DRV8834

Dość skomplikowany sposób sterowania silnikami krokowymi powoduje, że niezbędnym elementem systemu wykorzystującego taki silnik jest odpowiedni sterownik. Przy doborze układu powinniśmy wziąć pod uwagę przede wszystkim rodzaj silnika (bipolarny lub unipolarny), jakim może sterować oraz maksymalny prąd i napięcie. Ja do dzisiejszego ćwiczenia użyję silnika bipolarnego o napięciu znamionowym 7,4 V oraz prądzie 0,28 A na cewkę. Odpowiednim do niego sterownikiem będzie np. DRV8834 o zakresie napięcia sterowania od 2,5 do 10,8 V i maksymalnym prądzie do 2 A na fazę, który wykorzystam w formie modułu Pololu. Jeżeli masz „pod ręką” inny silnik o wyższym napięciu znamionowym lub inny sterownik, z powodzeniem możesz go wykorzystać z kodem, który dzisiaj przedstawię. Większość popularnych sterownik silników krokowych wykorzystuje ten sam interfejs sterowania (wejścia DIR i STEP) – jeżeli Twój sterownik taki posiada, to program będzie w 100% kompatybilny.

Jak zbudowany jest taki sterownik? Najlepiej przedstawia to diagram z dokumentacji układu DRV8834 (str. 11). Podstawowym elementem sterownika jest obwód logiczny, który analizuje sygnały wejściowe oraz generuje sygnały PWM dla bloków sterujących uzwojeniami silnika. Za dostarczenie prądu o dostatecznie dużym natężeniu do cewek odpowiadają (podobnie jak w sterowniku silników DC DRV8835 przedstawionym w artykule „Sterowanie silnikiem DC„) mostki H. Silnik bipolarny, w odróżnieniu od szczotkowego silnika DC, wymaga dwóch mostków H, ponieważ każdy z nich steruje jedną parą wejść silnika.

Warto zauważyć, że taki sterownik może posłużyć również jako dwukanałowy sterownik silników szczotkowych prądu stałego (analogicznie układ DRV8835 może sterować silnikiem krokowym). Różnica w obu sterownikach polega na zastosowaniu dedykowanego do danego typu silnika interfejsu, dzięki czemu implementacja w kodzie programu jest łatwiejsza.

Podstawowy sposób podłączenia modułu Pololu DRV8834 oraz opis pinów przedstawia poniższy diagram oraz tabela.

PinOpis
ENABLEwłączenie modułu (zanegowane, domyślnie włączony)
M0, M1konfiguracja mikrokroków
(CONFIG)konfiguracja trybu pracy układu (silnik krokowy/szczotkowy silnik DC)
(VREF)wyjście prądu odniesienia
SLEEPwejście sterujące trybem uśpienia
STEPwejście sterujące ruchem silnika
DIRwejście sterujące kierunkiem obrotu
GNDmasa
FAULTwyjście błędu
A1, A2, B1, B2wyjścia sterujące silnikiem
VMOTzasilanie silnika (2,5 do 10,8 V)

Sterowanie modułem odbywa się zatem za pomocą dwóch pinów:

  • DIR – pin GPIO do sterowania kierunkiem obrotu
  • STEP – pin, na który podawany jest sygnał prostokątny (jeden impuls oznacza obrót o jeden krok)

Istotnym elementem jest właściwe ustawienie pinów M0 oraz M1 układu DRV8834. Możliwe konfiguracje przedstawione zostały w tabeli poniżej.

M0M1Rozdzielczość mikrokroków
NiskiNiskiPełen krok
WysokiNiskiPół kroku
Niepodłączony (Floating)Niski1/4 kroku
NiskiWysoki1/8 kroku
WysokiWysoki1/16 kroku
Niepodłączony (Floating)Wysoki1/32 kroku

Na koniec chciałbym zwrócić uwagę na jeszcze jeden element. Na płytce umieszczony został potencjometr do służący do ograniczenia maksymalnego prądu podawanego na silnik. Należy go odpowiednio ustawić, aby nie uszkodzić silnika. Są dwa sposoby na dobór odpowiedniej wartości natężenia prądu: podłączenie szeregowo multimetru pod jedną z faz i stopniowe zwiększanie prądu aż do osiągnięcia odpowiedniej wartości dla naszego silnika (prąd ograniczający powinien wynosić ok. 0,7 prądu maksymalnego silnika). Drugim sposobem jest pomiar napięcia na wyjściu VREF (domyślnie zworka może być niewlutowana, ale można napięcie zmierzyć na jednym z padów od zworki) i dostrojenie go za pomocą potencjometru tak, aby wartość napięcia wskazywana przez multimetr była dwa razy mniejsza od maksymalnego prądu (np. dla 0,5 A powinniśmy dobrać napięcie 0,25 V).

Konfiguracja mikrokontrolera

Sterowanie silnikiem krokowym zaimplementujemy dla mikrokontrolera STM32L476RG na zestawie Nucleo-L476RG w środowisku STM32CubeIDE.

Tworzymy zatem nowy projekt wybierając „File->New->STM32 Project”. Przechodzimy przez wstępną konfigurację projektu i zabieramy się za konfigurację wyjść mikrokontrolera. Ja wygenerowałem projekt z domyślną konfiguracją dla płytki Nucleo, dlatego część pinów mam już skonfigurowane. Do sterowania silnikiem będziemy potrzebowali jednego sygnału PWM oraz jednego wyjścia GPIO. Na pin STEP sterownika DRV8834 musimy podać impulsy – można to zrealizować za pomocą wyjścia cyfrowego i cyklicznego zmieniania jego stanu, ale znacznie sensowniej będzie tutaj wykorzystać sygnał PWM o wypełnieniu 50%. Dzięki temu, że użyjemy interfejsu sprzętowego, nie będziemy obciążać niepotrzebnie mikrokontrolera dodatkowymi operacjami. Jako wyjście PWM posłuży nam pin PB10 (TIMER 2, kanał 3). W oknie konfiguracji układów peryferyjnych po lewej stronie wybieramy zatem zakładkę „Timers->TIM2” i w polu „Channel 3” wybieramy „PWM Generation CH3„. Poza tym potrzebujemy jednego pinu do wyboru kierunku obrotu – wykorzystamy pin PA8. W przypadku wyjść GPIO, konfigurujemy je klikając lewym przyciskiem myszy na pin i wybierając opcję „GPIO_Output„. Możemy też przypisać etykietę DIR dla PA8 i STEP dla PB10 (klikając prawym przyciskiem na pin).

Teraz przejdziemy do konfiguracji samego układu Timer-a. Po wybraniu trybu pracy kanału 3 jako PWM, otworzy nam się okno Configuration, gdzie mamy dostępną konfiguracje rejestrów licznika w przedstawioną w przystępny sposób. W przypadku mikrokontrolera STM32L476RG (dla innych układów mogą nieznacznie się różnić) mamy możliwość skonfigurowania następujących ustawień:

  • Counter Settings – ogólnie ustawienia timer-a:
    • Prescaler – dzielnik zegara, wartość 16-bitowa, należy pamiętać, że wpisujemy tutaj wartość, jaką chcemy osiągnąć pomniejszoną o 1, czyli np. dla 100 wpisujemy 99 (wynika to z faktu, że w momencie kasowania zawartości rejestru przeskakuje nam dodatkowy takt zegara, bo liczmy od 0 – szczegóły w „Reference Manual” na stronie 1077)
    • Counter Mode – tryb zliczania: w górę (Up) lub w dół (Down)
    • Counter Period (AutoReload Register) – wartość, do jakiej (w tybie Up) lub od jakiej (w trybie Down) nasz licznik będzie liczył
    • Internal Clock Division – dodatkowy dzielnik zegara używany przez filtry
    • Auto-Reload Preload – bit decydujący, czy zawartość rejestru ARR, gdzie przechowywana jest wartość do której zlicza licznik, ma być buforowana (wpisana do rejestru na stałe, czy tylko w przypadku zdarzenia od zakończenia liczenia)
  • Trigger Output (TRGO) – konfiguracja wyjścia wyzwalającego dla innych liczników
  • Clear Input – konfiguracja resetowania wyjścia PWM na dodatkowe zdarzenia
  • PWM Generation Channel 3 – ustawienia kanału 3:
    • Mode – tryb PWM (tryb 1, tryb 2, tryb asymetryczny lub łączony). W trybie 1 stan wysoki na wyjściu jest utrzymywany do momentu, aż licznik osiągnie wartość wpisaną jako wypełnienie (Pulse) w rejestrze CCRx, potem następuje przełączenie na stan niski aż do osiągnięcia przez licznik wartości końcowej, czyli Counter Period. W trybie 2 licznik działa odwrotnie, najpierw wyjście przyjmuje stan niski, a po doliczeniu do wartości Pulse przełącza wyjście w stan wysoki.
    • Pulse – 32-bitowy licznik, wartość przy jakiej następuje zmiana stanu na wyjściu PWM, określa początkową wartość wypełnienia
    • Output Compare Preload – określa czy zmiana wypełnienia w trakcie liczenia jest możliwa (disable), czy nie (enable)
    • Fast Mode – trybki szybki, minimalizuje opóźnienie poprzez pominięcie porównywania licznika. Szczególnie przydatny w trybie One Pulse Mode
    • CH Polarity – polaryzacja wyjścia, działa podobnie jak tryb 1 i tryb 2, przy wartości High stan wysoki na wyjściu jest utrzymywany do momentu, aż licznik osiągnie wartość wpisaną jako wypełnienie (Pulse) w rejestrze CCRx, potem następuje przełączenie na stan niski aż do osiągnięcia przez licznik wartości końcowej, czyli Counter Period. W polaryzacji Low licznik działa odwrotnie, najpierw wyjście przyjmuje stan niski, a po doliczeniu do wartości Pulse przełącza wyjście w stan wysoki.

W przypadku silnika krokowego kwestia doboru częstotliwości sygnału PWM nie jest taka oczywista, jak przy silniku DC czy serwie. Od częstotliwości generowania sygnału będzie bowiem zależało to, jak często będzie generowany kolejny impuls przesuwający wał o jeden krok, a więc prędkość obrotu naszego silnika. Każdy silnik ma inną maksymalną częstotliwość, z jaką może przyjmować impulsy sterujące. Nasz sterownik może pracować z częstotliwością do 250 kHz, ale to silnik będzie tutaj ograniczeniem. Przekroczenie maksymalnej częstotliwości będzie powodowało charakterystyczne „piszczenie” silnika i objawi się tym, ze silnik nie będzie się poruszał. Jeżeli nie możemy znaleźć maksymalnej częstotliwości dla naszego silnika w dokumentacji, należy ją dobrać doświadczalnie. Parametry w oknie konfiguratora możemy zatem dobrać wstępnie, a dalsze sterowanie zrealizujemy programowo.

Na początku możemy zatem wykorzystywać sygnał PWM o częstotliwości 1 kHz. Aby skonfigurować nasz licznik, musimy odpowiednio ustawić wartości: Prescaler i Counter Period. Częstotliwość sygnału PWM na podstawie tych wartości oraz częstotliwości taktowania Timer-a oblicza się z poniższego wzoru:

PWM_Freq = Timer_Freq / (Prescaler * Counter Period)

Częstotliwość taktowania Timer-a 2, którego używamy w projekcie, możemy odczytać z zakładki Clock Configuration. Aby tego dokonać, potrzebujemy informacji o tym, do jakiej szyny podłączony jest Timer 2. Możemy to sprawdzić w dokumentacji (Datasheet) mikrokontrolera na stronie 17.

Jak możemy zauważyć, Timer 2 podłączony jest do szyny APB1. W zakładce Clock Configuration można odczytać, że jeżeli zegar główny (HCLK) ma ustawioną maksymalną częstotliwość dostępną dla tego mikrokontrolera, czyli 80 MHz, to szyna (a zatem również nasz licznik) jest taktowana z częstotliwością 80 MHz.

W polu Prescaler ustawimy wartość 80. Chcąc zatem, aby sygnał PWM miał częstotliwość 1 kHz, Counter Period powinien mieć wartość:

80 000 000/(1000 * 80) = 1000

Pozostały nam do skonfigurowania pozostałe istotne parametry licznika. Chcemy aby nasz licznik zliczał w górę (Counter Mode -> Up), wykorzystamy tryb 1 PWMbez trybu Fast oraz z polaryzacją High. Jak opisywałem wcześniej, wartość Pulse określa wypełnienie PWM. Chcemy aby generowany był sygnał prostokątny o wypełnieniu równym 50%. W polu Pulse ustawiamy zatem wartość 500. Pozostałe parametry nie są dla nas istotne i możemy je zostawić z wartością domyślną. Pełna konfiguracja Timer-a 2 oraz kanału 3 wygląda w następujący sposób.

Silnik krokowy jest znakomitym rozwiązaniem w przypadku, gdy potrzebujemy wykonać obrót o zadany kąt, dlatego koniecznie musimy zaimplementować taką funkcjonalność. Aby to zrealizować, będziemy potrzebowali zliczać impulsy generowane przez Timer 2 Channel 3 i zatrzymać sygnał PWM, gdy licznik osiągnie żądaną wartość. Chciałbym przedstawić dwa sposoby na wykonanie tego zadania: programową z wykorzystaniem przerwań od timera 2 po wygenerowaniu każdego impulsu oraz wykorzystując inny timer w trybie Slave. Dokładny opis przedstawię w dalszej części artykułu – teraz pokażę tylko, jak powinny być skonfigurowane peryferia, aby wykorzystać obie możliwości. Wybór sposobu należy do Was – zależy to od rodzaju projektu i dostępnych układów peryferyjnych w mikrokontrolerze.

W przypadku realizacji programowej, poza konfiguracją Timera 2 przedstawionej powyżej, potrzebujemy jeszcze włączyć przerwania, które będą generowane po wystąpieniu każdego impulsu. Wchodzimy do zakładki „NVIC Settings” i zaznaczamy „TIM2 global interrupt„. Priorytet tutaj nie będzie miał znaczenia, bo jest to nieskomplikowany projekt, ale jeśli używasz przerwań w większym projekcie, pamiętaj aby właściwie dobrać priorytety.

W przypadku realizacji sprzętowej z użyciem drugiego timera w trybie slave, musimy skonfigurować zarówno źródło, z którego będą zliczane impulsy (czyli nasz Timer 2), jak i dodatkowy timer, który będzie je zliczał. Do konfiguracji Timera 2 dodajemy zatem w części Trigger Output (TRGO) Parameters opcję Master/Slave Mode (MSM bit) jako Enabled oraz Trigger Event selection TRGO jako Output Compare (OC3REF), czyli generowanie sygnału po każdym impulsie na kanale 3.

Teraz przechodzimy do konfiguracji drugiego timera. Najpierw musimy wybrać układ, który jest wewnętrznie połączony z Timerem 2 i pozwoli nam na wykonanie zamierzonej operacji. Siatkę połączeń możemy sprawdzić w dokumentacji Reference Manual mikrokontrolera w dziale Peripherals interconnect matrix (dla STM32L476RG jest to str. 328). W tabeli wartością „1” oznaczone są dostępne połączenia między timerami. Jak widzimy TIM2 (gdy jest źródłem) połączony jest z TIM1, TIM8, TIM3, TIM4 oraz TIM5. W projekcie jako Timer w trybie slave wykorzystamy TIM1.

Przechodzimy zatem do zakładki TIM1 i ustawiamy Slave Mode jako External Clock Mode 1. Musimy jeszcze sprawdzić, jakie źródło przełączania wybrać. Przechodzimy do Reference Manual-a do rozdziału dotyczącego TIM1 (Advanced control timers TIM1/TIM8), a konkretnie do opisu rejestru TIMx slave mode control register. W tabeli na str. sprawdzamy, że dla TIM1 jako Slave, TIM2 będzie źródłem w przypadku konfiguracji ITR1. W naszym konfiguratorze w STM32CubeIDE wybieramy zatem ITR1 jako Trigger Source. W konfiguracji Timera w oknie na dole możemy zostawić domyślne wartości – przy wyborze Slave Mode->External Clock Mode wartość Slave Mode Controller zmieniła nam się automatycznie.

Na koniec zaznaczamy jeszcze, że będziemy korzystali z przerwań.

Przy tak skonfigurowanych peryferiach możemy wygenerować projekt („Project->Generate Code” lub „Alt+K„) i przejść do napisania kodu programu.

Implementacja sterowania

Na początku stworzymy dwa pliki do obsługi silnika krokowego: stepper.c (w folderze Core->Src) oraz stepper.h (w folderze Core->Inc), które będą stanowiły naszą bibliotekę.

W pliku stepper.h dodamy definicje stałych określających konfigurację naszego silnika krokowego oraz sterownika.

#define STEPPER_MOTOR_MAX_FREQ_HZ	(MICRO_STEP * 1000)
#define STEPPER_MOTOR_MIN_FREQ_HZ	1

#define STEPPER_MOTOR_MAX_SPEED		100

#define STEP_PER_REVOLUTION		200
#define MICRO_STEP			32

Prędkością silnika będziemy sterować zmieniając częstotliwość timera 2. Założyłem, że zakres częstotliwości będzie w przedziale od 1 Hz do 1 kHz (dobrałem tę wartość doświadczalnie przy pełnym kroku dla mojego silnika – przy wyższych wartościach piszczał i nie był już w stanie się ruszyć).

Następnie dodałem dwa typy wyliczeniowe z wartościami dla trybu pracy: bezczynność, tryb obrotu o kąt i tryb pracy ciągłej, oraz enum-a z kierunkiem obrotu.

typedef enum
{
	idle = 0,
	angle = 1,
	continous = 2
}stepper_mode;

typedef enum
{
	CCW = 0,
	CW = 1
}direction;

Do przechowywania informacji o pracy silnika krokowego stworzyłem strukturę stepper_s i podstrukturę htim_s. Przechowujemy w niej informację o używanym timerze i kanale, trybie pracy oraz licznik kroków i stałą przechowującą kroki do zliczenia.

struct htim_s
{
	TIM_HandleTypeDef *htim;
	uint32_t channel;
};

struct stepper_s
{
	struct htim_s timer;

	stepper_mode mode;

	volatile uint32_t step_counter;
	uint32_t steps_to_count;
};

W pliku stepper.c tworzymy funkcję inicjalizacyjną, która odpowiada za przypisanie wskaźnika do używanego timera i numer kanału.

void stepper_init(struct stepper_s *_stepper, TIM_HandleTypeDef *_htim, uint32_t _channel)
{
	_stepper->timer.htim = _htim;
	_stepper->timer.channel = _channel;
}

Implementujemy teraz trzy podstawowe funkcje używane we wszystkich trybach pracy: funkcję zatrzymującą pracę silnika, ustawienie kierunku i ustawienie prędkości.

void stepper_stop(struct stepper_s *_stepper)
{
	_stepper->mode = idle;

	__HAL_TIM_SET_COMPARE(_stepper->timer.htim, _stepper->timer.channel, 0);
	HAL_TIM_PWM_Stop(_stepper->timer.htim, _stepper->timer.channel);
}

void stepper_set_direction(struct stepper_s *_stepper, direction _dir)
{
	if(_dir == CCW)
		HAL_GPIO_WritePin(DIR_GPIO_Port, DIR_Pin, CCW);
	else if(_dir == CW)
		HAL_GPIO_WritePin(DIR_GPIO_Port, DIR_Pin, CW);
}

void stepper_set_speed(struct stepper_s *_stepper, uint32_t _speed)
{
	uint32_t counter, freq;

	if(_speed > 100)
	{
		_speed = 100;
	}
	else if(_speed == 0)
	{
		stepper_stop(_stepper);
		return;
	}

	freq = (_speed * (STEPPER_MOTOR_MAX_FREQ_HZ - STEPPER_MOTOR_MIN_FREQ_HZ))/STEPPER_MOTOR_MAX_SPEED;
	counter = HAL_RCC_GetPCLK1Freq() / (_stepper->timer.htim->Init.Prescaler * freq);

	__HAL_TIM_SET_COUNTER(_stepper->timer.htim, 0);
	__HAL_TIM_SET_AUTORELOAD(_stepper->timer.htim, counter - 1);
	__HAL_TIM_SET_COMPARE(_stepper->timer.htim, _stepper->timer.channel, (counter/2) - 1);
}

Funkcja stepper_stop() ustawia tryb pracy jako idle, wartość wypełnienia PWM na używanym wyjściu jako 0 (brak impulsów, cały czas stan niski) oraz zatrzymuje generowanie sygnału PWM na wskazanym kanale. Funkcja stepper_set_direction() ustawia odpowiedni stan na wyjściu DIR mikrokontrolera, sterując kierunkiem obrotu silnika. Natomiast funkcja stepper_set_speed() oblicza na podstawie zadanej prędkości częstotliwość dla naszego timera oraz wartość, jaką powinniśmy wpisać do rejestru ARR (Period). Obliczanie tej wartości starałem się napisać w miarę uniwersalnie, aby informacje o częstotliwości zegarów były pobierane automatycznie. Można tutaj oczywiście stworzyć stałe definiujące te wartości, jeżeli nie będziemy ich modyfikować w trakcie pracy nad projektem. Na koniec przypisujemy trzy wartości dla timera: licznik na 0, wartość counter do Period oraz Pulse jako połowa wartości Period. Pamiętajmy, że do rejestrów wpisujemy zawsze wartości pomniejszone o jeden, co wynika z liczenia przez układ timera od 0.

Tryb pracy ciągłej

Do implementacji trybu pracy ciągłej wykorzystamy stworzone powyżej funkcje. Nie potrzebujemy tutaj informacji o zliczonych krokach, a jedynie właściwie ustawiony sygnał sterujący PWM. Dodajemy zatem funkcję stepper_set_continous(), która jako argumenty przyjmuje wskaźnik do struktury, kierunek i zadaną prędkość. W funkcji wywołujemy poszczególne funkcje pomocnicze, a na koniec uruchamiamy generowanie sygnału PWM.

void stepper_set_continous(struct stepper_s *_stepper, direction _dir, uint32_t _speed)
{
	_stepper->mode = continous;

	stepper_set_direction(_stepper, _dir);
	stepper_set_speed(_stepper, _speed);

	HAL_TIM_PWM_Start(_stepper->timer.htim, _stepper->timer.channel);
}

Tryb obrotu o zadany kąt – zliczanie programowe

Aby zrealizować obrót o zadany kąt, wykorzystamy przerwanie od timera generującego sygnał PWM, które będzie wywoływane po każdym impulsie. W funkcji ustawiamy zadany kierunek oraz prędkość, analogicznie jak przy trybie pracy ciągłej, a następnie obliczamy potrzebne do wykonania kroki. Na koniec wywołujemy funkcję uruchamiającą generowanie PWM z sufiksem _IT, co oznacza, że aktywujemy przerwania.

void stepper_set_angle(struct stepper_s *_stepper, direction _dir, uint32_t _speed, uint32_t _angle)
{
	_stepper->mode = angle;

	stepper_set_direction(_stepper, _dir);
	stepper_set_speed(_stepper, _speed);

	_stepper->step_counter = 0;
	_stepper->steps_to_count = _angle * (STEP_PER_REVOLUTION * MICRO_STEP) / 360;

        if(0 == _stepper->steps_to_count)
	{
		stepper_stop(_stepper);
	}

	HAL_TIM_PWM_Start_IT(_stepper->timer.htim, _stepper->timer.channel);
}

Następnie w pliku main.c dodajemy obsługę callback-a od przerwania, w którym zliczamy impulsy i zatrzymujemy silnik, jeżeli zadana wartość zostanie osiągnięta.

void HAL_TIM_PWM_PulseFinishedCallback(TIM_HandleTypeDef *htim)
{
	if(htim->Instance == stepper.timer.htim->Instance)
	{
		stepper.step_counter++;

		if(stepper.step_counter >= stepper.steps_to_count)
		{
			stepper_stop(&stepper);
		}
	}
}

Tryb obrotu o zadany kąt – timer w trybie slave

Jak opisywałem w rozdziale o konfiguracji, do zliczania wygenerowanych przez wyjście PWM impulsów możemy zastosować także drugi timer ustawiony w trybie slave. Rozwiązanie to jest o tyle interesujące, że zmniejszamy ilość obliczeń wykonywanych przez mikrokontroler i używamy w ciekawy sposób części sprzętowej. Takie rozwiązanie może być jednak problematyczne, jeżeli chcemy sterować kilkoma silnikami i zwyczajnie może nam brakować peryferiów.

W celu realizacji tego sposobu obrotu, musimy wprowadzić kilka modyfikacji w dotychczasowym kodzie. W pliku stepper.h w strukturze stepper_s dodajemy informację o drugim timerze, który pracuje w trybie slave.

struct stepper_s
{
	struct htim_s timer;
	struct htim_s slave_timer;


	stepper_mode mode;
	volatile uint32_t step_counter;
	uint32_t steps_to_count;
};

Nasza funkcja inicjalizująca będzie teraz przyjmowała trzy argumenty.

void stepper_init(struct stepper_s *_stepper, TIM_HandleTypeDef *_htim, uint32_t _channel, TIM_HandleTypeDef *_slave_timer)
{
	_stepper->timer.htim = _htim;
	_stepper->timer.channel = _channel;
	_stepper->slave_timer.htim = _slave_timer;
}

Dodatkowo w funkcji stepper_set_angle() musimy skonfigurować timer w trybie slave w taki sposób, aby zliczał zadaną ilość impulsów i wygenerował przerwanie w odpowiednim momencie. W tym celu ilość kroków, jaką silnik musimy wykonać przypisujemy do rejestru ARR (Period) oraz startujemy pracę timera w trybie z przerwaniami.

void stepper_set_angle(struct stepper_s *_stepper, direction _dir, uint32_t _speed, uint32_t _angle)
{
	_stepper->mode = angle;

	stepper_set_direction(_stepper, _dir);
	stepper_set_speed(_stepper, _speed);

	_stepper->step_counter = 0;
	_stepper->steps_to_count = _angle * (STEP_PER_REVOLUTION * MICRO_STEP) / 360;

	if(0 == _stepper->steps_to_count)
	{
		stepper_stop(_stepper);
	}


	__HAL_TIM_SET_COUNTER(_stepper->slave_timer.htim, 0);
	__HAL_TIM_SET_AUTORELOAD(_stepper->slave_timer.htim, _stepper->steps_to_count - 1);
	HAL_TIM_Base_Start_IT(_stepper->slave_timer.htim);

	HAL_TIM_PWM_Start(_stepper->timer.htim, _stepper->timer.channel);

}

Na koniec w pliku main.c implementujemy obsługę callback-a, w którym zatrzymujemy silnik.

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
	if(htim->Instance == stepper.slave_timer.htim->Instance)
	{
		stepper_stop(&stepper);
	}
}

Powinniśmy również lekko zmodyfikować funkcję stepper_stop(), w której będziemy wyłączali przerwania od timera slave. O ile w przypadku ciągłego wykonywania obrotu o zadany kąt nie jest to potrzebne i przerwania mogą być aktywne cały czas, a tyle w przypadku wywołania obrotu w trybie ciągłym przerwania może wystąpić w momencie, w którym go nie oczekujemy i spowodować niepożądane zatrzymanie silnika.

void stepper_stop(struct stepper_s *_stepper)
{
	_stepper->mode = idle;

	__HAL_TIM_SET_COMPARE(_stepper->timer.htim, _stepper->timer.channel, 0);
	HAL_TIM_PWM_Stop(_stepper->timer.htim, _stepper->timer.channel);

	HAL_TIM_Base_Stop_IT(_stepper->slave_timer.htim);
}

Przetestowanie działania

Do przetestowania działania programu użyjemy wywołania funkcji do pracy ciągłej lub obrotu o zadany kąt z kolejno pobieranymi argumentami z tablicy.

  int32_t speed_table[SET_TABLE_SIZE] = {30, 20, -20, 10, 50};
  int32_t angle_table[SET_TABLE_SIZE] = {30, 90, 45, 15, -180};

  int i = 0;
  uint32_t time_tick = HAL_GetTick();
  uint32_t max_time = 2000;
  uint32_t angle = 0, speed = 0;
  direction dir = CW;

  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
	  if((HAL_GetTick() - time_tick) > max_time)
	  {
		  time_tick = HAL_GetTick();

                  //obrót o zadany kąt
		  if(angle_table[i] >= 0)
		  {
			  angle = angle_table[i];
			  dir = CW;
		  }
		  else
		  {
			  angle = -angle_table[i];
			  dir = CCW;
		  }

		  stepper_set_angle(&stepper, dir, 10, angle);

                  //praca ciągła
		  /*if(speed_table[i] >= 0)
		  {
			  speed = speed_table[i];
			  dir = CW;
		  }
		  else
		  {
			  speed = -speed_table[i];
			  dir = CCW;
		  }

		  stepper_set_continous(&stepper, dir, speed);*/

		  i++;

		  if(i >= SET_TABLE_SIZE)
		  {
			  i = 0;
		  }
	  }
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }

Prze funkcją main() należy jeszcze dodać definicję preprocesora.

#define SET_TABLE_SIZE	5

Pamiętajmy również o dodaniu pliku stepper.h w main.h.

/* USER CODE BEGIN Includes */
#include "stepper.h"
/* USER CODE END Includes */

Przed uruchomieniem projektu powinniśmy jeszcze połączyć sterownik z zestawem Nucleo i podłączyć silnik. Robimy to według poniższego schematu (mikrokroki ustawione na 1/32).

Teraz możemy skompilować kod (Project->Build Project) i go uruchomić (Run->Run). Efekt działania został przedstawiony na filmie poniżej.

Podsumowanie

W poradniku zapoznaliśmy się z budową i zasadą działania silnika krokowego. Precyzja sterowania tego typu silnikiem sprawia, że jest on bardzo często stosowany w projektach, gdzie jest to wymagane. Używany jest do sterowania w drukarkach 3D, maszynach CNC czy frezarkach, ale również w manipulatorach. Mam nadzieję, że artykuł pozwoli Ci poznać możliwości tego typu napędów i zastosować je we własnych projektach.

Do pobrania

Repozytorium GitHub

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *