Sterowanie silnikiem DC

Pierwszy artykuł z serii sterowania napędami będzie opisywał najczęściej stosowany napęd w robotyce, czyli silniki prądu stałego. W materiale postaram się przedstawić budowę silnika prądu stałego, zasadę działania sterownika DRV8835 oraz implementację sterowania na zestawie Nucelo-L476RG.

Silnik prądu stałego

Silnik prądu stałego to napęd sterowany prądem o stałej wartości natężenia oraz kierunku przepływu. Oznacza to, że silnik będzie poruszał się, jeżeli do jego zacisków przyłożymy źródło prądu stałego czyli np. akumulator lub baterię.

Sam silnik charakteryzuje się dużą prędkością obrotową oraz małym momentem. Aby dostosować parametry do naszych potrzeb, na osi wyjściowej montowana jest przekładnia, czyli zestaw kół zębatych, które zwiększają moment obrotowy, a zmniejszają prędkość silnika. Dzięki temu silnik jest w stanie wywołać ruch np. robota mobilnego. Zestaw (silnik + przekładnia) pozwala nam na napędzanie obiektów o znacznej masie, a prędkość obrotu wału wyjściowego (za przekładnią) zależy od napięcia przyłożonego do styków silnika. Trzecim elementem, który stanowi ważną część napędu, jest enkoder (czujnik obrotu). Pozwala on na pomiar prędkości i kierunku obracającego się silnika, czyli daje informację zwrotną o tym, jak w rzeczywistości się porusza. Dzięki enkoderom możemy precyzyjnie sterować prędkością oraz reagować na wpływ otoczenia na działanie silnika np. zwiększony opór w trakcie obrotu.

Sterownik silnika DRV8835

Do sterowania silnikiem za pomocą mikrokontrolera potrzebujemy odpowiedniego układu. Dlaczego? Ponieważ silnik to urządzenie, które pobiera prąd o dość dużym natężeniu. Piny IO mikrokontrolera (zgodnie z dokumentacją na stronie 116) mają wydajność ok. 20 mA. Nasz silnik będzie pobierał natomiast nawet do 1,6 A, więc podłączenie go bezpośrednio do wyjść mikrokontrolera mogłoby spowodować uszkodzenie uC. To sterownik pozwala nam na kontrolowanie ruchu silnika za pomocą wyjść mikrokontrolera. Jak to się dzieje? Jeśli spojrzymy na dokumentację sterownika DRV8835, którego użyjemy w przykładzie, możemy zauważyć, że ma on wbudowane tranzystory MOSFET oraz dodatkowe elementy odpowiadające za logikę działania układu oraz zabezpieczenia. To dzięki tranzystorom i zbudowanemu z nich mostkowi H możemy sterować silnikiem. Jak pewnie zauważyliście, wybrany przeze mniej sterownik ma podwójny mostek mostek H, więc może kontrolować ruch dwóch silników DC.

Dla nas, jako programistów mikrokontrolerów, najważniejszy jest rozkład i funkcje wejść i wyjść układu. Sterownik DRV8835 jest dostępny w obudowie SMD, której nie możemy użyć bezpośrednio z płytką stykową, dlatego wykorzystamy gotową płytkę ze sterownikiem od firmy Pololu. Wyprowadzenia pinów na płytce przedstawione zostały poniżej.

PinOpis
VINzasilanie silników z zakresu od 0 V do 11 V z zabezpieczeniem przed odwrotnym podłączeniem
VCCzasilanie części logicznej układu z zakresu od 1,8 V do 7 V
VMzasilanie silników z pominięciem zabezpieczenia przed odwrotnym podłączeniem
GNDmasa
AOUT1wyjście 1 silnika A
AOUT2wyjście 2 silnika A
BOUT1wyjście 1 silnika B
BOUT2wyjście 2 silnika B
AIN1/APHASEwejście sterujące silnikiem A
AIN2/AENABLEwejście sterujące silnikiem A
BIN1/BPHASEwejście sterujące silnikiem B
BIN2/BENABLEwejście sterujące silnikiem B
MODEwybór trybu sterowania silnikiem ( stan niski to tryb IN/IN, stan wysoki to tryb PHASE/ENABLE)

Sterownik ma dwa tryby pracy, które możemy wybrać za pomocą stanu niskiego lub wysokiego na wejściu MODE.

  • Tryb PHASE/ENABLE to prostszy tryb sterowania – do uruchomienia silnika potrzebujemy jednego sygnału PWM oraz jednego sygnału cyfrowego GPIO. W trybie tym możemy sterować zarówno kierunkiem, jak i prędkością obrotu silnika oraz wywołać hamowanie.
  • Tryb IN/IN pozwala na bardziej zaawansowane sterowanie silnikiem. Do uruchomienia tego trybu wymagane są dwa sygnały PWM. Poza sterowaniem kierunkiem i prędkością oraz hamowaniem, mamy też możliwość wyłączenia wyjść sterownika (bieg jałowy).

Ze względu na prostszą implementację sterowania silnikiem wykorzystamy tryb PHASE/ENABLE. Opis sposobu sterowania wejściami PHASE i ENABLE przedstawiony został w tabeli poniżej.

PHASEENABLEOUT1OUT2Opis
0PWMPWMLruch w jednym kierunku z prędkością PWM
1PWMLPWMruch w drugim kierunku z prędkością PWM
X0LLhamowanie (wyjścia zwarte do GND)

U osób, które dopiero zaczynają swoją przygodę z robotyką i mikrokontrolerami, może pojawić się pytanie, czym jest sygnał PWM. PWM (Pulse Width Modulation) to sygnał prostokątny, w którym zmienna jest szerokość generowanego impulsu.

Najważniejszymi parametrami sygnału PWM jest:

  • częstotliwość – odwrotność czasu trwania, określa, ile razy w ciągu sekundy występuje impuls
  • szerokość impulsu (wypełnienia) – określa, przez jaki czas trwania całego impulsu aktywny jest stan wysoki

Taki sygnał na stykach silnika powoduje, że napięcie podawane jest tylko przez pewien czas trwania impulsu, a przerwy w podawaniu napięcia nie będą widoczne, jeżeli sygnał PWM będzie miał odpowiednio dużą częstotliwość. W przypadku silników prądu stałego sygnał powinien mieć częstotliwość co najmniej 20 kHz. Podyktowane jest to faktem, że częstotliwości słyszalne przez człowieka mieszczą się w zakresie od 20 do 20 kHz. Jeżeli nie chcemy zatem, aby silnik generował hałas, musimy zadbać o to, aby PWM miał odpowiednia częstotliwość.

Najważniejszą zasadą przy sterowaniu silnikiem za pomocą PWM jest to, że im większe wypełnienie, tym szybciej będzie poruszał się silnik, przy czym 0% oznacza, że silnik się nie obraca, a 100%, że obraca się z pełną prędkością.

Implementacja

Sterowanie silnikiem prądu stałego za pomocą sterownika DRV8835 zaimplementujemy dla mikrokontrolera STM32L476RG na zestawie Nucleo-L476RG w środowisku STM32CubeIDE. Jeżeli nie używałeś jeszcze tego środowiska, zachęcam do zapoznania się z moim poradnikiem „STM32CubeIDE – pierwszy projekt„.

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 DC w trybie PHASE/ENABLE będziemy potrzebowali trzech wyjść: dwóch wyjść cyfrowych (do sterowanie pinem MODE oraz pinem PHASE) oraz wyjścia PWM. Mikrokontroler STM32L476RG ma dostępnych wiele wyjść PWM, a tym bardziej GPIO, które możemy wykorzystać w projekcie, dlatego wybrane przeze mnie wyjścia nie są jedynym słusznym wyborem. Dzisiaj przykładowo wykorzystamy piny PA8 i PA9 jako wyjścia GPIO oraz pin PB10 jako wyjście PWM (TIMER 2, kanał 3). W przypadku wyjść GPIO, konfigurujemy je klikając lewym przyciskiem myszy na piny PA8 i PA9 mikrokontrolera i wybieramy opcję „GPIO_Output„. Analogicznie konfigurujemy pin PB10 wybierając opcję „TIM2_CH3”. Następnie w oknie konfiguracji układów peryferyjnych po lewej stronie wybieramy zakładkę „Timers->TIM2” i w polu „Channel 3” wybieramy „PWM Generation CH3„. Dla większej przejrzystości naszego projektu i kodu, który będziemy pisali później, warto zdefiniować własne nazwy pinów, których używamy, co ułatwi ich używanie w programie. Klikając prawym przyciskiem myszy na pin mikrokontrolera, wybieramy „Enter User Label” i przypisujemy naszym pinom odpowiednio nazwy „APHASE” dla PA9, „MODE” dla PA8 oraz „AENABLE” dla PB10.

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.

Jak wspominałem wcześniej, do sterowania silników prądu stałego powinniśmy wykorzystywać sygnał PWM o częstotliwości powyżej 20 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że odczytać, że jeżeli zegar główny (HCLK) ustawiony ma maksymalną częstotliwość dostępna dla tego mikrokontrolera, czyli 80 MHz, to szyna (a zatem również nasz licznik) jest taktowana również z częstotliwością 80 MHz.

Wartość Counter Period określa naszą rozdzielczość sterowania prędkością silnika, czyli ilość poziomów prędkości, jakie będziemy mogli rozróżnić. W naszym przypadku chcemy sterować prędkością z rozdzielczością 100. Chcąc zatem, aby sygnał PWM miał częstotliwość 20 kHz, Prescaler powinien mieć wartość:

80 000 000/(20 000 * 100) = 40

Pozostały nam do skonfigurowania pozostałe istotne parametry licznika. Chcemy aby nasz licznik zliczał w górę (Counter Mode -> Up), wykorzystamy tryb 1 PWM, bez trybu Fast oraz z polaryzacją High. Jak opisywałem wcześniej, wartość Pulse określa wypełnienie PWM. Tutaj konfigurujemy tak naprawdę wartość początkową, ponieważ wypełnienie będziemy modyfikować za każdym razem, gdy będziemy chcieli zmienić prędkość silnika. Możemy ustawić ją zatem jako „0”. 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.

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

Celem moich poradników jest nie tylko pokazanie fragmentu kodu, ale także wyrobienie nawyku pisania w miarę przejrzystego kodu. Sam się cały czas tego uczę, ale warto od początku nabierać nawyków, które później zaprocentują. Aby nasz program był jasny do zrozumienia i możliwie łatwy do zastosowania w różnych projektach, stworzymy dwa pliki do obsługi sterownika: drv8835.c (w folderze Core->Src) oraz drv8835.h (w folderze Core->Inc), które będą stanowiły naszą bibliotekę.

W pierwszej kolejności powinniśmy skonfigurować tryb pracy sterownika. W tym celu w pliku drv8835.h stworzymy prosty typ wyliczeniowy enum z nazwami trybów:

typedef enum
{
	In_In_Mode = 0,
	Phase_Enable_Mode = 1
}DRV8835_Mode;

Następnie w pliku drv8835.c dodamy funkcję, która będzie obsługiwała zmianę trybu działania sterownika. Jak pamiętamy z opisu działania ukłądu DRV8835, tryb pracy wybieramy poprzez podanie właściwego stanu na pin MODE. W naszym przypadku jest to pin PA8, a dzięki zastosowanej w konfiguracji projektu etykiecie możemy posługiwać się nazwami: MODE_GPIO_Port oraz MODE_Pin). Funkcja o nazwie drv8835_mode_control będzie więc wyglądała jak poniżej.

void drv8835_mode_control(DRV8835_Mode mode)
{
	if(mode == Phase_Enable_Mode)
		HAL_GPIO_WritePin(MODE_GPIO_Port, MODE_Pin, SET);
	else if(mode == In_In_Mode)
		HAL_GPIO_WritePin(MODE_GPIO_Port, MODE_Pin, RESET);
}

Co daje nam zastosowanie typu wyliczeniowego enum? Teraz wywołanie funkcji konfigurującej będzie wyglądało tak:

drv8835_mode_control(Phase_Enable_Mode);

Wydaje mi się, że jest czytelne i zrozumiałe dla każdego, kto pierwszy raz styka się z kodem, a przecież w programowaniu o to nam chodzi.

Kolejnym etapem jest ustawienie kierunku obrotu silnika. Tutaj analogicznie dodamy w pliku drv8835.h typ wyliczeniowy, gdzie CW (clockwise) oznacza obrót zgodnie z ruchem wskazówek zegara, a CCW (counter clockwise) – przeciwnie do ruchu wskazówek zegara.

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

Ustalenie kierunku obrotu w DRV8835 odbywa się poprzez podanie odpowiedniego stanu na pin APHASE, czyli w naszym przypadku PA9, do którego przypisaliśmy również etykietę. Funkcja obsługująca zmianę kierunku będzie wyglądała następująco.

void drv8835_set_motorA_direction(DRV8835_Direction dir)
{
	if(dir == CW)
		HAL_GPIO_WritePin(APHASE_GPIO_Port, APHASE_Pin, SET);
	else if(dir == CCW)
		HAL_GPIO_WritePin(APHASE_GPIO_Port, APHASE_Pin, RESET);
}

Teraz pozostało nam napisać funkcję zadającą prędkość, czyli wypełnienie PWM. W konfiguracji ustaliliśmy, że będziemy prędkością sterować wartościami od 0 do 99 (czyli wartości Period – rejestr ARR). Dlatego na wstępie wpisałem proste zabezpieczeni w przypadku, gdyby ktoś podał większą wartość. Cała konfiguracja Timera 2 i kanału 3 jest wykonywana na wstępie programu w funkcji MX_TIM2_Init() w pliku main.c i została przygotowana przez generator środowiska. My musimy jedynie zmienić wartość Pulse, czyli wypełnienia PWM, która domyślnie była ustawiona na 0. Możemy to zrealizować przez proste makro __HAL_TIM_SetCompare(). Cała funkcja będzie wyglądała jak poniżej.

void drv8835_set_motorA_speed(uint8_t speed)
{
	if(speed >= htim2.Instance->ARR)
		speed = htim2.Instance->ARR;

	__HAL_TIM_SetCompare(&htim2, TIM_CHANNEL_3, speed);
}

Należy tutaj pamiętać jeszcze o jednej rzeczy. Samo wpisanie wartości do rejestru nie sprawi, że sygnał PWM będzie wygenerowany na wyjściu pinu PB10. Musimy jeszcze wystartować działanie naszego timera z trybem PWM poprzez wywołanie funkcji z biblioteki HAL-a o nazwie HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_3). Wywołać ją musimy tylko raz na początku programu, dlatego dodamy jeszcze funkcję inicjalizującą sterownik, którą użyjemy przed pętlą główną.

void drv8835_init()
{
	drv8835_mode_control(Phase_Enable_Mode);
	drv8835_set_motorA_direction(CCW);
	drv8835_set_motorA_speed(0);

	HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_3);
}

Jak widać, dodałem tutaj od razu konfigurację naszego trybu oraz wyzerowanie kierunku i prędkości.

Aby nasze funkcje były widoczne w pliku main.c, musimy dodać jeszcze deklaracje funkcji w pliku drv8835.h. Cały plik będzie wyglądał tak:

#ifndef INC_DRV8835_H_
#define INC_DRV8835_H_

typedef enum
{
	In_In_Mode = 0,
	Phase_Enable_Mode = 1
}DRV8835_Mode;

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

void drv8835_init();
void drv8835_mode_control(DRV8835_Mode);
void drv8835_set_motorA_direction(DRV8835_Direction);
void drv8835_set_motorA_speed(uint8_t);

#endif /* INC_DRV8835_H_ */

Teraz pozostało nam napisać program testowy w funkcji main(), który pokaże, czy silnik pracuje poprawnie. Aby nie blokować programu przy pomocy funkcji opóźniającej HAL_Delay(), użyjemy do opóźnienia funkcji HAL_GetTick(), która zwraca ilość tyknięć SysTick Timera. Do przetestowania napiszemy prosty program, który będzie zadawał kolejne wartości wypełnienia od 0 do 99 i od 99 do 0 w kierunku CCW, a potem w kierunku CW. W ten sposób silnik powinien rozpędzać się stopniowo i zwalniać w jednym kierunku, a potem w drugim. Przed pętlą główną inicjalizujemy sterownik i zmienną całkowitą pwm (wypełnienie), dir (kierunek obrotu) i inc_dec (flaga określająca, czy zwiększamy, czy zmniejszamy licznik). Do zmiennej time_tick przypisujemy liczbę tyknięć SysTick-a i czekamy, aż różnica będzie większa od 20 (każde tick odpowiada 1 ms). Po każdym wejściu w instrukcję warunkową if przypisujemy nową wartość do time_tick. Przy dojściu do 100 (nasza maksymalna wartość wypełnienia PWM) oraz 0 (wartość minimalna), zmieniamy kierunek zwiększania/zmniejszania i kierunek obrotu.

 /* USER CODE BEGIN 2 */
  drv8835_init();

  int pwm = 0, dir = CCW, inc_dec = 1;
  uint32_t time_tick = HAL_GetTick();

  drv8835_set_motorA_direction(dir);
  drv8835_set_motorA_speed(pwm);
  /* USER CODE END 2 */

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

		  pwm += inc_dec;
		  drv8835_set_motorA_speed(pwm);

		  if(pwm >= 100)
		  {
			  inc_dec = -1;
		  }
		  else if(pwm <= 0)
		  {
			  inc_dec = 1;

			  if(dir == CCW)
				  dir = CW;
			  else if(dir == CW)
				  dir = CCW;

			  drv8835_set_motorA_direction(dir);
		  }
	  }
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */

Na koniec dodamy jeszcze nagłówek „drv8835.h” w pliku main.h, aby nasza biblioteka była widoczna w pliku main.c.

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "drv8835.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.

Teraz możemy skompilować kod (Project->Build Project) i go uruchomić (Run->Run). Jeżeli masz jeszcze problem z obsługą tych funkcji środowiska, zachęcam do zapoznania się z moim poradnikiem „STM32CubeIDE – pierwszy projekt„. Efekt działania został przedstawiony na filmie poniżej.

Podsumowanie

W tym poradniku zapoznaliśmy się z budową i zasadą działania silnika prądu stałego oraz sterownika DRV8835. Każdy sterownik działa trochę inaczej, ale sens działania jest podobny – może różnić się nazwą wejść i wyjść lub dodatkową funkcjonalnością. Silniki DC to napędy najpowszechniej stosowane w robotyce do napędzania pojazdów ze względu na prosty sposób sterowania i dobra właściwości. Mam nadzieję, że dzięki artykułowi uda Ci się uruchomić swojego pierwszego robota.

Do pobrania

Repozytorium GitHub

Komentarz

  1. Hello,

    Thanks for all your tutorial very helpful.

    I wanted to know the way to use the drv8835 to drive a stepper motor ?

    Best regards,

    Antoine

Dodaj komentarz

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