Kursy • Poradniki • Inspirujące DIY • Forum
Mikrokontrolery z rodziny STM32 znane są z rozbudowanych możliwości timerów. Jest to niewątpliwie ich zaletą, jednak dla początkujących liczba dostępnych opcji może wydawać się nieco przytłaczająca. W ramach tego kursu nie będziemy omawiać wszystkich możliwości jakie dają układy licznikowe, skupimy się na tym co najważniejsze na początek - czyli głównie PWM.
Kiedy mówimy o timerach, najczęściej mamy na myśli odmierzanie czasu. Zanim więc zaczniemy na dobre poznawać liczniki musimy przypomnieć sobie podstaw oraz wykonać trochę obliczeń.
Liczniki w STM32 - podstawy teoretyczne
W przypadku sygnału okresowego s(t), czas T > 0 mierzony w sekundach jest okresem, jeśli dla każdego momentu t: s(t) = s(t + T), czyli co T sekund sygnał się powtarza. W elektronice często sekunda to bardzo długi czas, więc popularne jest używanie takich jednostek jak:
1 ms = 10-3 s
1 us = 10-6 s
1 ns = 10-9 s
Częstotliwość jest odwrotnością okresu: f = 1/T, jednostką jest Herz (Hz). Dla przypomnienia:
1 kHz = 103 Hz
1 MHz = 106 Hz
1 GHz = 109 Hz
Pracując z zegarami często będziemy przechodzić, albo zamiennie używać okresu i częstotliwości, warto zapamiętać następującą zależność:
Częstotliwość | Okres |
---|---|
1Hz | 1s |
1kHz | 1ms |
1MHz | 1us |
Dlaczego to takie ważne? Po prostu w pewnych momentach znana jest jedna z tych wartości, a wygodniej jest używać drugiej. Przykład: mikrokontroler na płytce Nucleo pracuje z częstotliwością 64MHz. Jaki należy zastosować dzielnik, aby uzyskać okres 1ms?
No właśnie - parametrem jest częstotliwość, a my chcemy uzyskać przerwanie co 1ms, jak to przeliczyć? Możemy okres, czyli 1ms zamienić na częstotliwość - 1kHz, a następnie podzielić. 64MHz przez 1kHz. Wynik to 64000 i taki dzielnik będzie nam potrzebny (w przykładach).
Gotowe zestawy do kursów Forbota
Komplet elementów Gwarancja pomocy Wysyłka w 24h
Zestaw ponad 120 elementów do przeprowadzenia wszystkich ćwiczeń z kursu można nabyć u naszych dystrybutorów! Dostępne są wersje z płytką Nucleo lub bez niej!
Zamów w Botland.com.pl »Podstawa czasu w STM32
Na początek upewnimy się, czy nasze obliczenia są poprawne. Ponieważ nie każdy ma pod ręką oscyloskop, spróbujemy skonfigurować licznik tak, aby dokładnie co 1s otrzymywać przerwanie. Za pomocą zegarka będziemy mogli się przekonać, czy nasz układ działa poprawnie.
Jak zwykle pierwszy krok to podłączenie zegara do naszego timera:
1 |
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); |
Układ STM32F103 posiada 4 moduły timerów, my wykorzystamy TIM2 jako przykład. Kolejny krok to jak zwykle zdefiniowanie zmiennej z konfiguracją. Na początek ustawiamy podstawę czasu:
1 2 3 4 5 |
TIM_TimeBaseStructInit(&tim); tim.TIM_CounterMode = TIM_CounterMode_Up; tim.TIM_Prescaler = 64000 - 1; tim.TIM_Period = 1000 - 1; TIM_TimeBaseInit(TIM2, &tim); |
Tryb pracy to zliczanie do góry, czyli wewnętrzny licznik będzie liczył od 0 do wartości TIM_Period. Licznik zlicza od 0, więc gdybyśmy ustawili ten parametr na 1000, okres wynosiłby 1001.
Dlatego odejmujemy 1 podczas konfiguracji.
Ponieważ zegar systemowy działa z częstotliwością 64 MHz, to do 1000 doliczy za szybko (otrzymamy częstotliwość 64 kHz, a chcieliśmy 1 Hz). Wykorzystamy więc preskaler, czyli dzielnik zegara wejściowego. Podzielimy 64 MHz przez 64000, dzięki czemu nasz timer będzie pracował z częstotliwością 1 kHz, więc do 999 doliczy dokładnie po 1s.
STM32 jest układem 32-bitowym, jednak rejestry układów zegarowych są 16-bitowe. Oznacza to, że maksymalna wartość TIM_Period wynosi 65535. Aby uzyskać dłuższy okres, musimy wykorzystać preskaler, który również jest 16-bitowy.
Jak sprawdzimy, czy mamy rację? Uruchomimy przerwanie wywoływane po przepełnieniu licznika:
1 |
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); |
Zdarzenie TIM_IT_Update jest generowane, gdy licznik modułu osiągnie wartość TIM_Period i zaczyna ponownie liczyć od zera. Nasz timer jeszcze nie pracuje, uruchomimy go komendą:
1 |
TIM_Cmd(TIM2, ENABLE); |
Potrzebujemy teraz skonfigurować moduł NVIC do obsługi przerwania:
1 2 3 4 5 |
nvic.NVIC_IRQChannel = TIM2_IRQn; nvic.NVIC_IRQChannelPreemptionPriority = 0; nvic.NVIC_IRQChannelSubPriority = 0; nvic.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&nvic); |
Wcześniej już używaliśmy przerwań, jedyna różnica to kanał przerwania – teraz jest to TIM2_IRQn. Czas napisać procedurę jego obsługi. Ważna jest nazwa TIM2_IRQHandler - jeśli podamy inną, program nie zadziała:
1 2 3 4 5 6 7 8 9 10 11 12 |
void TIM2_IRQHandler() { if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); if (GPIO_ReadOutputDataBit(GPIOA, GPIO_Pin_5)) GPIO_ResetBits(GPIOA, GPIO_Pin_5); else GPIO_SetBits(GPIOA, GPIO_Pin_5); } } |
W kodzie ważne jest sprawdzenie przyczyny przerwania za pomocą funkcji TIM_GetITStatus. W tej chwili interesuje nas tylko zdarzenie TIM_IT_Update, ale później dodamy kolejne.
Gdy obsłużymy przerwanie, powiadamiamy o tym układ zerując odpowiednią flagę za pomocą wywołania TIM_ClearITPendingBit. Dalsza część, to nasza funkcja - sprawdzamy, czy dioda świeci, jeśli tak to ją wyłączamy, a w przeciwnym przypadku włączamy.
Pełny kod programu:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
#include <stdbool.h> #include "stm32f10x.h" void TIM2_IRQHandler() { if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); if (GPIO_ReadOutputDataBit(GPIOA, GPIO_Pin_5)) GPIO_ResetBits(GPIOA, GPIO_Pin_5); else GPIO_SetBits(GPIOA, GPIO_Pin_5); } } int main(void) { GPIO_InitTypeDef gpio; TIM_TimeBaseInitTypeDef tim; NVIC_InitTypeDef nvic; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | RCC_APB2Periph_GPIOC | RCC_APB2Periph_GPIOD, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); GPIO_StructInit(&gpio); gpio.GPIO_Pin = GPIO_Pin_5; gpio.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_Init(GPIOA, &gpio); TIM_TimeBaseStructInit(&tim); tim.TIM_CounterMode = TIM_CounterMode_Up; tim.TIM_Prescaler = 64000 - 1; tim.TIM_Period = 1000 - 1; TIM_TimeBaseInit(TIM2, &tim); TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); TIM_Cmd(TIM2, ENABLE); nvic.NVIC_IRQChannel = TIM2_IRQn; nvic.NVIC_IRQChannelPreemptionPriority = 0; nvic.NVIC_IRQChannelSubPriority = 0; nvic.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&nvic); while (1) { } } |
Po uruchomieniu możemy sprawdzić, czy wszystko się zgadza. Dioda powinna świecić przez dokładnie jedną sekundę, po czym gasnąć na dokładnie taki sam czas. Jako ćwiczenie warto sprawdzić co stanie się, gdy zmienimy wartości parametrów TIM_Period i TIM_Prescaler.
Zadanie 7.1
Zmień program tak, aby dioda świeciła dokładnie 5 sekund, a następnie gasła na 5 sekund.
Zadanie 7.2
Wykonaj podobne ćwiczenie jak 7.1, ale ustaw czas na 100 ms.
Zadanie 7.3
Podczas sterowania silnikami, częstotliwość sterowania ma duże znaczenie. Zbyt wysoka powoduje straty mocy na mostku podczas przełączania. Zbyt mała może być bardzo uciążliwa dla użytkownika - jeśli częstotliwość przełączania jest w zakresie akustycznym, silnik może dość dokuczliwie piszczeć.
Dlatego najczęściej wykorzystuje się sterowanie z częstotliwością niesłyszalną dla człowieka. Przygotuj program, w którym przerwanie będzie wywoływane z częstotliwością 20 kHz. Sprawdź jaki jest zakres słyszalnych częstotliwości, czy to wystarczająca wartość? Jak sprawdzić, czy układ faktycznie pracuje z tą częstotliwością?
Liczniki w STM32 - kanały
Układy licznikowe STM32 posiadają jeszcze dodatkowe 4 rejestry kanałów. Gdy główny licznik zrówna się z wartością rejestru danego kanału, można np.: wygenerować przerwanie (można też zmienić stan wyjścia).
Dzięki temu możemy otrzymać przerwanie co okres zegara oraz cztery dodatkowe przerwania po zadanym czasie. W poprzednim przykładzie ustawiliśmy okres zegara na 1s. Teraz spróbujemy za pomocą kanałów wygenerować przerwania po 100ms, 200ms, 500ms i 900ms.
Do czego to nam się może przydać? Na początku okresu zapalimy 4 diody. Po nadejściu każdego z przerwań będziemy gasić jedną z nich. Powinniśmy otrzymać 4 synchronicznie pracujące diody, świecące. Jest to duży krok w stronę sterowania silnikami poprzez PWM. Jednak dzięki diodom i długiemu okresowi pracy, możemy zobaczyć jak układ działa.
Na początek podłączamy diody podobnie jak w części kursu omawiającej GPIO, czyli do pinów PC0, PC1, PC2, PC3.
Większość konfiguracji timera przebiega jak poprzednio, trzeba tylko ustawić wartość rejestrów kanałów:
1 2 3 4 |
TIM_SetCompare1(TIM2, 100); TIM_SetCompare2(TIM2, 200); TIM_SetCompare3(TIM2, 500); TIM_SetCompare4(TIM2, 900); |
Następnie musimy włączyć przerwanie od zdarzenia TIM_IT_Update (jak poprzednio) oraz od zdarzenia zrównania licznika z rejestrem kanału (Compare Channel) - TIM_IT_CCx:
1 |
TIM_ITConfig(TIM2, TIM_IT_Update|TIM_IT_CC1|TIM_IT_CC2|TIM_IT_CC3|TIM_IT_CC4, ENABLE); |
Najwięcej zmian musimy dokonać w procedurze obsługi przerwania. Sterowanie diodą na płytce Nucleo zostawimy jak poprzednio. Dodamy do tego sterowanie właśnie podłączonymi diodami:
Przy zdarzeniu Update zapalamy wszystkie 4 diody:
1 2 3 4 5 6 7 8 9 10 11 12 |
if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); if (GPIO_ReadOutputDataBit(GPIOA, GPIO_Pin_5)) GPIO_ResetBits(GPIOA, GPIO_Pin_5); else GPIO_SetBits(GPIOA, GPIO_Pin_5); // zapal wszystkie diody GPIO_SetBits(GPIOC, GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3); } |
Natomiast po wystąpieniu zdarzenia TIM_IT_CCx, gasimy odpowiednią diodę:
1 2 3 4 5 |
if (TIM_GetITStatus(TIM2, TIM_IT_CC1) == SET) { TIM_ClearITPendingBit(TIM2, TIM_IT_CC1); GPIO_ResetBits(GPIOC, GPIO_Pin_0); } |
Funkcja TIM_GetITStatus sprawdza, które zdarzenie jest przyczyną przerwania, następnie zerujemy flagę przerwania i gasimy diodę. To samo robimy dla pozostałych kanałów.
Kod całego programu prezentuje się tak, jak poniżej:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
#include "stm32f10x.h" void TIM2_IRQHandler() { if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); if (GPIO_ReadOutputDataBit(GPIOA, GPIO_Pin_5)) GPIO_ResetBits(GPIOA, GPIO_Pin_5); else GPIO_SetBits(GPIOA, GPIO_Pin_5); // zapal wszystkie diody GPIO_SetBits(GPIOC, GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3); } if (TIM_GetITStatus(TIM2, TIM_IT_CC1) == SET) { TIM_ClearITPendingBit(TIM2, TIM_IT_CC1); GPIO_ResetBits(GPIOC, GPIO_Pin_0); } if (TIM_GetITStatus(TIM2, TIM_IT_CC2) == SET) { TIM_ClearITPendingBit(TIM2, TIM_IT_CC2); GPIO_ResetBits(GPIOC, GPIO_Pin_1); } if (TIM_GetITStatus(TIM2, TIM_IT_CC3) == SET) { TIM_ClearITPendingBit(TIM2, TIM_IT_CC3); GPIO_ResetBits(GPIOC, GPIO_Pin_2); } if (TIM_GetITStatus(TIM2, TIM_IT_CC4) == SET) { TIM_ClearITPendingBit(TIM2, TIM_IT_CC4); GPIO_ResetBits(GPIOC, GPIO_Pin_3); } } int main(void) { GPIO_InitTypeDef gpio; TIM_TimeBaseInitTypeDef tim; NVIC_InitTypeDef nvic; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | RCC_APB2Periph_GPIOC | RCC_APB2Periph_GPIOD, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); GPIO_StructInit(&gpio); gpio.GPIO_Pin = GPIO_Pin_5; gpio.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_Init(GPIOA, &gpio); gpio.GPIO_Pin = GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3; GPIO_Init(GPIOC, &gpio); TIM_TimeBaseStructInit(&tim); tim.TIM_CounterMode = TIM_CounterMode_Up; tim.TIM_Prescaler = 64000 - 1; tim.TIM_Period = 1000 - 1; TIM_TimeBaseInit(TIM2, &tim); TIM_ITConfig(TIM2, TIM_IT_Update|TIM_IT_CC1|TIM_IT_CC2|TIM_IT_CC3|TIM_IT_CC4, ENABLE); TIM_Cmd(TIM2, ENABLE); TIM_SetCompare1(TIM2, 100); TIM_SetCompare2(TIM2, 200); TIM_SetCompare3(TIM2, 500); TIM_SetCompare4(TIM2, 900); nvic.NVIC_IRQChannel = TIM2_IRQn; nvic.NVIC_IRQChannelPreemptionPriority = 0; nvic.NVIC_IRQChannelSubPriority = 0; nvic.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&nvic); while (1) { } } |
Po uruchomieniu zobaczymy, że pierwsza dioda zapala się na bardzo krótko (100ms), kolejna nieco dłużej (200ms), itd.
Teraz spróbujmy zmienić preskaler, w tej chwili wynosi 64 000. Zmieńmy go przykładowo na 64:
1 |
TIM_TimeBaseStructure.TIM_Prescaler = 64 - 1; |
Przy częstotliwości 1kHz nie widzimy już migania diod. Za to świecą one z różną jasnością – pierwsza najsłabiej, ostatnia najjaśniej (jeśli różnica jest mało widoczna, najlepiej zmniejszyć współczynniki wypełnienia).
To co widzimy to sterowanie PWM. Gdybyśmy zamiast diod podłączyli odpowiedni sterownik, a do niego 4 silniki, ich prędkość obrotowa byłaby różna, podobnie jak jasność świecenia diod.
Poprawiamy program korzystający z kanałów licznika
Poprzedni program działał poprawnie, jednak w rzeczywistości trochę niechcący. Mianowicie konfiguracja kanałów naszego licznika była w dużym stopniu domyślna. Na nasze szczęście domyślne wartości w pełni nam odpowiadają, jednak dodajmy dla pewności do programu pełną konfigurację kanałów.
W tym celu potrzebujemy zmiennej typu TIM_OCInitTypeDef. Dotychczas poznaliśmy typ TIM_TimBaseInitTypeDef, który służył do konfiguracji podstawy czasu. Nowy typ pozwoli nam skonfigurować kanały. Funkcję musimy wywołać cztery razy (dla każdego kanału osobno):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
TIM_OCInitTypeDef channel; TIM_OCStructInit(&channel); channel.TIM_OCMode = TIM_OCMode_Timing; channel.TIM_Pulse = 100; TIM_OC1Init(TIM2, &channel); channel.TIM_Pulse = 200; TIM_OC2Init(TIM2, &channel); channel.TIM_Pulse = 500; TIM_OC3Init(TIM2, &channel); channel.TIM_Pulse = 900; TIM_OC4Init(TIM2, &channel); |
Jak zwykle najpierw wywołujemy konstruktor TIM_OCStructInit. Następnie ustawiamy tryb pracy kanału, na domyślną wartość TIM_OCMode_Timing. Pole TIM_Pulse, to wartość licznika naszego kanału – poprzednio jego wartość ustawialiśmy wywołaniem TIM_SetCompare, teraz ustawiamy ją podczas inicjalizacji.
Gdy uruchomimy program, będzie on działał identycznie jak poprzednio. Możemy jednak mieć wpływ na konfigurację kanałów. Co wykorzystamy w kolejnym przykładzie.
Kod całego programu:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
#include "stm32f10x.h" void TIM2_IRQHandler() { if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); if (GPIO_ReadOutputDataBit(GPIOA, GPIO_Pin_5)) GPIO_ResetBits(GPIOA, GPIO_Pin_5); else GPIO_SetBits(GPIOA, GPIO_Pin_5); GPIO_SetBits(GPIOC, GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3); } if (TIM_GetITStatus(TIM2, TIM_IT_CC1) == SET) { TIM_ClearITPendingBit(TIM2, TIM_IT_CC1); GPIO_ResetBits(GPIOC, GPIO_Pin_0); } if (TIM_GetITStatus(TIM2, TIM_IT_CC2) == SET) { TIM_ClearITPendingBit(TIM2, TIM_IT_CC2); GPIO_ResetBits(GPIOC, GPIO_Pin_1); } if (TIM_GetITStatus(TIM2, TIM_IT_CC3) == SET) { TIM_ClearITPendingBit(TIM2, TIM_IT_CC3); GPIO_ResetBits(GPIOC, GPIO_Pin_2); } if (TIM_GetITStatus(TIM2, TIM_IT_CC4) == SET) { TIM_ClearITPendingBit(TIM2, TIM_IT_CC4); GPIO_ResetBits(GPIOC, GPIO_Pin_3); } } int main(void) { GPIO_InitTypeDef gpio; TIM_TimeBaseInitTypeDef tim; TIM_OCInitTypeDef channel; NVIC_InitTypeDef nvic; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | RCC_APB2Periph_GPIOC | RCC_APB2Periph_GPIOD, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); GPIO_StructInit(&gpio); gpio.GPIO_Pin = GPIO_Pin_5; gpio.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_Init(GPIOA, &gpio); gpio.GPIO_Pin = GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3; GPIO_Init(GPIOC, &gpio); TIM_TimeBaseStructInit(&tim); tim.TIM_CounterMode = TIM_CounterMode_Up; tim.TIM_Prescaler = 64000 - 1; tim.TIM_Period = 1000 - 1; TIM_TimeBaseInit(TIM2, &tim); TIM_OCStructInit(&channel); channel.TIM_OCMode = TIM_OCMode_Timing; channel.TIM_Pulse = 100; TIM_OC1Init(TIM2, &channel); channel.TIM_Pulse = 200; TIM_OC2Init(TIM2, &channel); channel.TIM_Pulse = 500; TIM_OC3Init(TIM2, &channel); channel.TIM_Pulse = 900; TIM_OC4Init(TIM2, &channel); TIM_ITConfig(TIM2, TIM_IT_Update|TIM_IT_CC1|TIM_IT_CC2|TIM_IT_CC3|TIM_IT_CC4, ENABLE); TIM_Cmd(TIM2, ENABLE); nvic.NVIC_IRQChannel = TIM2_IRQn; nvic.NVIC_IRQChannelPreemptionPriority = 0; nvic.NVIC_IRQChannelSubPriority = 0; nvic.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&nvic); while (1) { } } |
Zadanie 7.4
Wykonanie procedury obsługi przerwania w obecnej postaci zajmuje mikrokontrolerowi ok. 7.7us (zmierzona wartość). Przyjmując, że częstotliwość sterowania wynosi 20 kHz, oblicz ile procent czasu procesora zajmuje obsługa przerwań (dla 1 i dla 4 kanałów).
Generowanie PWM na STM32
Poprzednio do sterowania wyjściami procesora wykorzystywaliśmy przerwania. Takie rozwiązanie jest dość nieekonomiczne – procesor ma pełne ręce roboty obsługując przerwania i w kółko zapalając i gasząc diody.
Znacznie lepiej jest sterować nimi sprzętowo, za pomocą PWM.
Na początek musimy wybrać wyprowadzenia, na których dostępny jest sprzętowy PWM. Pełny opis wyprowadzeń procesora jest dostępny w dokumentacji, czyli datasheet-cie. Okazuje się, że wyprowadzenia TIM2, który używaliśmy wcześniej kolidują z konwerterem UART-USB. Można użyć remapowania i innych wyprowadzeń, ale my wykorzystamy łatwiejszą opcję i zmienimy timer.
Okazuje się, że timer TIM4 udostępnia wyjścia PWM na pinach PB6, PB7, PB8 oraz PB8, które są bez problemu dostępne. Przełączamy układ zgodnie z rysunkiem:
Jak zawsze pierwszym etapem jest podłączenie zegara, tym razem do TIM4.
1 |
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE); |
Musimy również dodać konfigurację pinów PB6-9. Wykonamy to podobnie jak dotychczas, jednak użyjemy 2 nowych opcji. Ustawimy prędkość działania pinów na 50 MHz – domyślnie pracują z maksymalną prędkością 2MHz, jednak sprzętowy PWM może wymagać szybkich pinów.
Druga zmiana, to ustawienie trybu wyjścia jako GPIO_Mode_AF_PP, czyli funkcja alternatywna (PWM), w trybie push-pull. Kod wygląda następująco:
1 2 3 4 5 |
GPIO_StructInit(&gpio); gpio.GPIO_Pin = GPIO_Pin_6|GPIO_Pin_7|GPIO_Pin_8|GPIO_Pin_9; gpio.GPIO_Speed = GPIO_Speed_50MHz; gpio.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_Init(GPIOB, &gpio); |
Konfiguracja podstawy czasu pozostaje praktycznie bez zmian (poza innym timerem):
1 2 3 4 5 |
TIM_TimeBaseStructInit(&tim); tim.TIM_CounterMode = TIM_CounterMode_Up; tim.TIM_Prescaler = 64000 - 1; tim.TIM_Period = 1000 - 1; TIM_TimeBaseInit(TIM4, &tim); |
Natomiast, to co nowe, to ustawienia kanałów. Tym razem mają pracować w trybie PWM:
1 2 3 4 5 6 7 8 9 10 11 |
TIM_OCStructInit(&channel); channel.TIM_OCMode = TIM_OCMode_PWM1; channel.TIM_OutputState = TIM_OutputState_Enable; channel.TIM_Pulse = 100; TIM_OC1Init(TIM4, &channel); channel.TIM_Pulse = 200; TIM_OC2Init(TIM4, &channel); channel.TIM_Pulse = 500; TIM_OC3Init(TIM4, &channel); channel.TIM_Pulse = 900; TIM_OC4Init(TIM4, &channel); |
Początkowe wartości PWM to 100, 200 i 500. Ponieważ pełny okres PWM wynosi 1000, więc odpowiada to wypełnieniu 10%, 20% i 50%.
Pozostaje już tylko uruchomić timer:
1 |
TIM_Cmd(TIM4, ENABLE); |
Nasz program poza konfiguracją modułu już właściwie nic nie musi robić. Nie ma obsługi przerwań, program główny, to pusta pętla.
Moglibyśmy nawet przełączyć mikrokontroler w tryb uśpienia - wszystko odbywa się czysto sprzętowo.
Cały kod programu:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
#include <math.h> #include "stm32f10x.h" int main(void) { GPIO_InitTypeDef gpio; TIM_TimeBaseInitTypeDef tim; TIM_OCInitTypeDef channel; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | RCC_APB2Periph_GPIOC | RCC_APB2Periph_GPIOD, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE); GPIO_StructInit(&gpio); gpio.GPIO_Pin = GPIO_Pin_6|GPIO_Pin_7|GPIO_Pin_8|GPIO_Pin_9; gpio.GPIO_Speed = GPIO_Speed_50MHz; gpio.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_Init(GPIOB, &gpio); TIM_TimeBaseStructInit(&tim); tim.TIM_CounterMode = TIM_CounterMode_Up; tim.TIM_Prescaler = 64000 - 1; tim.TIM_Period = 1000 - 1; TIM_TimeBaseInit(TIM4, &tim); TIM_OCStructInit(&channel); channel.TIM_OCMode = TIM_OCMode_PWM1; channel.TIM_OutputState = TIM_OutputState_Enable; channel.TIM_Pulse = 100; TIM_OC1Init(TIM4, &channel); channel.TIM_Pulse = 200; TIM_OC2Init(TIM4, &channel); channel.TIM_Pulse = 500; TIM_OC3Init(TIM4, &channel); channel.TIM_Pulse = 900; TIM_OC4Init(TIM4, &channel); TIM_Cmd(TIM4, ENABLE); while (1) { } } |
Zadanie 7.5
Przygotuj program, który będzie płynnie zapalał i gasił kolejne diody.
Wykorzystanie PWM do sterowania diody RGB
Na zakończenie podłączymy diodę RGB i za pomocą PWM będziemy płynnie zmieniali kolor jej świecenia. Wykorzystamy TIM4, jak w poprzednim przykładzie, ale tym razem wystarczą nam 3 kanały. Podłączamy układ zgodnie ze schematem montażowym:
Dioda RGB dostarczana z zestawem ma wspólną anodę - więc nasz układ nie ma wspólnej masy, co jest typowym rozwiązaniem. Zamiast tego mamy wspólne zasilanie 3.3V, a sterowanie jest odwrócone.
Wystawienie logicznej 1 na odpowiedni pin wyłącza diodę, natomiast 0 włącza ją.
Moglibyśmy wykorzystać poprzedni program do sterowania, ale wtedy wypełnienie sygnały byłoby również odwrócone. Wykorzystamy więc tryb PWM2, który działa dokładnie tak jak potrzebujemy - na początku okresu wystawia logiczne 0, a po zrównaniu licznika z rejestrem kanału wystawia 1. Działa więc jak odwrócony tryb PWM1.
Ponieważ będziemy chcieli płynnie zmieniać kolory, przyda nam się funkcja opóźniająca delay_ms, którą napisaliśmy w części o GPIO. Dodamy również obsługę przerwania SysTick. Do ustawiania jasności świecenia każdej składowej wykorzystamy następujące funkcje:
TIM_SetCompare1, TIM_SetCompare2, TIM_SetCompare3.
Okazuje się, że pozornie proste zadanie, jak sterowanie diodą RGB zawiera kilka niespodzianek. Pierwsza, to sama budowa diody. Po podłączeniu wszystkich 3 składowych zamiast białej barwy uzyskamy widoczne trzy punkty świecące odpowiednio na czerwono, zielono i niebiesko.
Dioda w rzeczywistości zawiera trzy struktury, z których każda świeci innym kolorem. Niestety są one widoczne jako oddzielne źródła światła i nie mieszają się zgodnie z oczekiwaniami.
Aby poprawić efekt potrzebujemy nakryć diodę czymś matowym, ale jednocześnie przepuszczającym światło. Dzięki temu składowe światła będą mieszały się znacznie lepiej.
Najprościej poszukać odpowiedniego elementu z częściowo przezroczystego plastiku, można wydrukować coś ładnego w 3D, a jeśli nic nie znajdziemy, po prostu zasłonić diodę chusteczką.
Drugi problem, to znaczna nieliniowość jasności świecenia diody. Okazuje się, że zwiększając liniowo wypełnienie PWM, otrzymujemy bardzo nieliniową jasność świecenia diody. Poza bardzo małymi współczynnikami, dioda świeci właściwie z pełną jasnością.
Chcąc zlinearyzować jasność świecenia diody, musimy odpowiednio przeliczyć jasność na wypełnienie PWM. Możemy w tym celu wykorzystać funkcję:
Więcej na ten temat znaleźć można na Wikipedii.
Przyjmując oczekiwaną jasność wyrażoną w procentach (0-100%), możemy napisać następującą funkcję:
1 2 3 4 5 6 |
float calc_pwm(float val) { const float k = 0.1f; const float x0 = 60.0f; return 300.0f / (1.0f + exp(-k * (val - x0))); } |
Współczynniki 300, k oraz x0 są dobrane eksperymentalnie, zachęcam do ich zmiany i obserwacji wyników. Mając względnie liniową charakterystykę świecenia diody, możemy przegotować interesujący efekt wizualny. Z pomocą przychodzą nam funkcje trygonometryczne, np. sin().
Możemy napisać następujący program:
1 2 3 4 5 6 7 8 9 10 11 |
while (1) { float r = 50 * (1.0f + sin(counter / 100.0f)); float g = 50 * (1.0f + sin(1.5f * counter / 100.0f)); float b = 50 * (1.0f + sin(2.0f * counter / 100.0f)); TIM_SetCompare1(TIM4, calc_pwm(b)); TIM_SetCompare2(TIM4, calc_pwm(g)); TIM_SetCompare3(TIM4, calc_pwm(r)); delay_ms(20); counter++; } |
W efekcie dostaniemy całkiem ładnie wyglądającą lampkę RGB:
Kod całego programu:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
#include <math.h> #include "stm32f10x.h" volatile uint32_t timer_ms = 0; void SysTick_Handler() { if (timer_ms) timer_ms--; } void delay_ms(int time) { timer_ms = time; while (timer_ms); } float calc_pwm(float val) { const float k = 0.1f; const float x0 = 60.0f; return 300.0f / (1.0f + exp(-k * (val - x0))); } int main(void) { int counter = 0; GPIO_InitTypeDef gpio; TIM_TimeBaseInitTypeDef tim; TIM_OCInitTypeDef channel; NVIC_InitTypeDef nvic; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | RCC_APB2Periph_GPIOC | RCC_APB2Periph_GPIOD, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE); GPIO_StructInit(&gpio); gpio.GPIO_Pin = GPIO_Pin_5; gpio.GPIO_Speed = GPIO_Speed_2MHz; gpio.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_Init(GPIOA, &gpio); gpio.GPIO_Pin = GPIO_Pin_6|GPIO_Pin_7|GPIO_Pin_8; gpio.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_Init(GPIOB, &gpio); TIM_TimeBaseStructInit(&tim); tim.TIM_CounterMode = TIM_CounterMode_Up; tim.TIM_Prescaler = 64 - 1; tim.TIM_Period = 1000 - 1; TIM_TimeBaseInit(TIM4, &tim); TIM_OCStructInit(&channel); channel.TIM_OCMode = TIM_OCMode_PWM2; channel.TIM_OutputState = TIM_OutputState_Enable; TIM_OC1Init(TIM4, &channel); TIM_OC2Init(TIM4, &channel); TIM_OC3Init(TIM4, &channel); TIM_Cmd(TIM4, ENABLE); nvic.NVIC_IRQChannel = TIM4_IRQn; nvic.NVIC_IRQChannelPreemptionPriority = 0; nvic.NVIC_IRQChannelSubPriority = 0; nvic.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&nvic); SysTick_Config(SystemCoreClock / 1000); while (1) { float r = 50 * (1.0f + sin(counter / 100.0f)); float g = 50 * (1.0f + sin(1.5f * counter / 100.0f)); float b = 50 * (1.0f + sin(2.0f * counter / 100.0f)); TIM_SetCompare1(TIM4, calc_pwm(b)); TIM_SetCompare2(TIM4, calc_pwm(g)); TIM_SetCompare3(TIM4, calc_pwm(r)); delay_ms(20); counter++; } } |
Zadanie 7.6
Sprawdź, jak wygląda sterowanie jasnością świecenia diody bez funkcji calc_pwm. Najlepiej użyć jednego koloru i liniowo zmieniać wypełnienie - czy jasność świecenia diody też zmienia się liniowo? Wstaw w komentarzu film pokazujący rezultat.
Zadanie 7.7
Przygotuj inny sposób zmiany kolorów, wstaw w komentarzu filmik z rezultatem, który uzyskałeś.
Podsumowanie
Omówiliśmy tylko podstawowe możliwości układów licznikowych, w które wyposażony jest STM32F103RB. Warto chociaż wspomnieć o pozostałych funkcjach.
Korzystając ze sprzętowych liczników mamy możliwość pomiaru szerokości impulsu. Pozwala to przykładowy na dokładny pomiar odległości popularnym czujnikiem HC-SR04. Co ważne dla osób budujących roboty - wśród dostępnych opcji znajdziemy również sprzętowy interfejs do enkoderów. oraz do obsługi czujników hall-a.
Dodatkowo pierwszy licznik ma dodatkowe możliwości, m.in. generowania PWM z wyjściem komplementarnym i programowanym czasem martwym (deadtime). Daje to możliwość bezpośredniego sterowania zarówno górnymi jak i dolnymi tranzystorami mostka H.
Nawigacja kursu
Autor kursu: Piotr (Elvis) Bugalski
Redakcja: Damian (Treker) Szymański
Powiązane wpisy
kursSTM32, liczniki, programowanie, PWM, stm32, timer
Trwa ładowanie komentarzy...