Czytamy karty katalogowe 2: SPI na STM32; PWM, timery i przerwania na STM8

Czytamy karty katalogowe 2: SPI na STM32; PWM, timery i przerwania na STM8

W pierwszej części Próbowałem wyjaśnić hobbystom elektroniki, którzy wyrośli z Arduino, jak i dlaczego warto czytać datasheety oraz inną dokumentację mikrokontrolerów. Tekst okazał się długi, dlatego obiecałem pokazać praktyczne przykłady w osobnym artykule. Cóż, nazwałem się grzybem…

Dziś pokażę, jak przy pomocy datasheetów rozwiązać dość proste, ale potrzebne w wielu projektach zadania na kontrolerach STM32 (Blue Pill) i STM8. Wszystkie projekty demonstracyjne poświęcone są moim ulubionym diodom LED, które będziemy zapalać w dużych ilościach, więc będziemy musieli wykorzystać różnorodną interesującą peryferię.

Tekst znów wyszedł ogromny, dlatego dla wygody robię spis treści:

STM32 Blue Pill: 16 diod LED z sterownikiem DM634
STM8: Ustawiamy sześć wyjść PWM
STM8: 8 RGB-diod LED na trzech pinach, przerwania

Zastrzeżenie: nie jestem inżynierem, nie roszczę sobie prawa do głębokiej wiedzy z zakresu elektroniki, artykuł jest przeznaczony dla takich jak ja amatorów. W rzeczywistości moją grupą docelową byłem ja sprzed dwóch lat. Gdyby ktoś wtedy powiedział mi, że czytanie datasheetów do nieznanego chipu nie jest straszne, nie straciłbym mnóstwa czasu na szukanie jakichś kawałków kodu w internecie i wynajdywanie kulawych rozwiązań z nożyczkami i plastrem.

W centrum tego artykułu znajdują się datasheety, a nie projekty, dlatego kod może nie być zbyt schludny i często jest kulawy. Same projekty są bardzo proste, choć nadają się na pierwsze zapoznanie się z nowym chipem.

Mam nadzieję, że mój artykuł pomoże komuś na podobnym etapie zanurzenia w hobby.

STM32

16 diod LED z DM634 i SPI

Mały projekt z wykorzystaniem Blue Pill (STM32F103C8T6) i sterownika LED DM634. Przy pomocy datasheetów zrozumiemy sterownik, porty IO STM i skonfigurujemy SPI.

DM634

Tajwański chip z 16 16-bitowymi wyjściami PWM, można łączyć w łańcuchy. Młodszy model 12-bitowy jest znany z krajowego projektu Lightpack. W swoim czasie, wybierając między DM63x a dobrze znanym TLC5940, zdecydowałem się na DM z kilku powodów: 1) TLC na AliExpress z pewnością jest podróbką, a ten nie; 2) DM ma autonomiczny PWM z własnym generatorem częstotliwości; 3) można go było tanio kupić w Moskwie, a nie czekać na paczkę z Ali. No i, oczywiście, było ciekawe samodzielnie nauczyć się zarządzać chipem, zamiast korzystać z gotowej biblioteki. Chipy obecnie są głównie dostępne w obudowie SSOP24, łatwo je przylutować do adaptera.

Ponieważ producent jest tajwański, karta danych w dokumentacji chipu napisane jest po chińsku angielsku, co oznacza, że będzie ciekawie. Najpierw spójrzmy na rozkład pinów (Pin Connection), aby zrozumieć, do którego pinu co podłączyć, oraz opis pinów (Pin Description). 16 wyprowadzeń:

Czytamy karty katalogowe 2: SPI na STM32; PWM, timery i przerwania na STM8
Źródła stałego prądu (otwarty dren)

Sink / Wyjście z otwartym drenem – dren; źródło wchodzącego prądu; wyjście, w aktywnym stanie podłączone do ziemi, – diody LED są podłączane katodami do sterownika. Elektrycznie to, oczywiście, nie jest żaden 'otwarty dren' (open drain), ale w arkuszach danych takie oznaczenie dla wyjść w trybie drenu występuje często.

Czytamy karty katalogowe 2: SPI na STM32; PWM, timery i przerwania na STM8
Zewnętrzne oporniki między REXT a GND służą do ustawienia wartości prądu wyjściowego

Między pinem REXT a ziemią instaluje się opornik referencyjny, który kontroluje wewnętrzną rezystancję wyjść, zob. wykres na str. 9 arkusza danych. W DM634 tym rezystorem można również zarządzać programowo, ustawiając ogólną jasność (global brightness); w tym artykule nie będę wchodził w szczegóły, po prostu umieszczę tutaj rezystor o wartości 2.2 – 3 kΩ.

Aby zrozumieć, jak zarządzać chipem, przyjrzyjmy się opisowi interfejsu urządzenia:

Czytamy karty katalogowe 2: SPI na STM32; PWM, timery i przerwania na STM8

Aha, oto on, chiński angielski w pełnej okazałości. Tłumaczenie tego jest problematyczne, zrozumieć można przy chęci, ale jest inna droga – spojrzeć, jak opisano podłączenie w arkuszu danych do funkcjonalnie zbliżonego TLC5940:

Czytamy karty katalogowe 2: SPI na STM32; PWM, timery i przerwania na STM8
… Do wprowadzenia danych do urządzenia wymagane są tylko trzy piny. Narastający front sygnału SCLK przesuwa dane z pinu SIN do wewnętrznego rejestru. Po załadowaniu wszystkich danych, krótki wysoki sygnał XLAT zapisuje kolejno przesłane dane w wewnętrznych rejestrach. Wewnętrzne rejestry – działają na poziomie sygnału XLAT zapadki. Wszystkie dane przesyłane są z najwyższym bitem na przód.

Latch – zapadka/zatrzask/fixator.
Narastający brzeg – narastający front impulsu
MSB first – przesunięcie bitem (najbardziej znaczącym) do przodu.
do zegarowania danych – przesyłanie danych sekwencyjnie (bitem po bicie).

Słowo latch czesto pojawia się w dokumentacji chipów i jest tłumaczone na różne sposoby, dlatego dla lepszego zrozumienia pozwolę sobie na

małe wprowadzenie do tematuSterownik LED to w zasadzie rejestr przesuwny. 'Przesunięcie' (shift) w nazwie oznacza bitowe przesunięcie danych wewnątrz urządzenia: każdy nowy bit wprowadzony do środka przesuwa całą łańcuch przed sobą do przodu. Ponieważ podczas przesunięcia nikt nie chce obserwować chaotycznego migotania diod LED, proces odbywa się w rejestrach buforowych, oddzielonych od roboczych zasuwą (latch) – to swoisty przedsionek, gdzie bity układają się we właściwej kolejności. Gdy wszystko jest gotowe, zasłona otwiera się i bity są wysyłane do pracy, zastępując poprzednią partię. Słowo latch w dokumentacji mikroukładów niemal zawsze oznacza taką zasłonę, bez względu na to, w jakich kombinacjach jest używane.

Tak więc przesył danych w DM634 odbywa się w ten sposób: ustawiamy wejście DAI na wartość najbardziej znaczącego bitu dalekiej diody LED, zaciągamy DCK w górę i w dół; ustawiamy wejście DAI na wartość następnego bitu, zaciągamy DCK; i tak dalej, aż wszystkie bity zostaną przesłane (wprowadzane zegarowo), po czym zaciągamy LAT. Można to zrobić ręcznie (bit-bang), ale lepiej skorzystać z specjalnie zaprojektowanego interfejsu SPI, który jest dostępny w naszym STM32 w dwóch egzemplarzach.

Niebieska płytka STM32F103

Uwagi wstępne: kontrolery STM32 są znacznie bardziej skomplikowane niż Atmega328, niż może się to wydawać. W związku z oszczędnością energii na początku wyłączona jest prawie cała peryferia, a częstotliwość taktowania wynosi 8 MHz z wewnętrznego źródła. Na szczęście programiści STM napisali kod, który zwiększa częstotliwość do 'obliczeniowych' 72 MHz, a twórcy wszystkich znanych mi IDE włączyli go w procedurę inicjalizacji, więc nie musimy się tym zajmować (ale możemy, jeśli naprawdę chcemy). Będzie trzeba jednak włączyć peryferię.

Dokumentacja: na Blue Pill zainstalowany jest popularny chip STM32F103C8T6, dla którego dostępne są dwa przydatne dokumenty:

W karcie danych mogą nas interesować:

  • Pinouty – przyporządkowania pinów chipów – na wypadek, gdybyśmy postanowili robić płytki sami;
  • Mapa pamięci – mapa pamięci dla konkretnego układu. W podręczniku referencyjnym znajduje się mapa dla całej linii, w której wymienione są rejestry, których nie ma w naszym.
  • Tabela definicji pinów – zestawienie podstawowych i alternatywnych funkcji pinów; dla „niebieskiej pigułki” w internecie można znaleźć bardziej przystępne obrazki z listą pinów i ich funkcjami. Dlatego natychmiast wyszukujemy pinout Blue Pill i trzymamy pod ręką taki obrazek:

Czytamy karty katalogowe 2: SPI na STM32; PWM, timery i przerwania na STM8
PS: na obrazku z internetu była błąd, zauważony w komentarzach, za co dziękuję. Obrazek został wymieniony, ale to jest lekcja – informacje nie pochodzące z kart katalogowych lepiej weryfikować.

Usuwamy kartę katalogową, otwieramy podręcznik referencyjny, od teraz korzystamy tylko z niego.
Procedura działań: zajmujemy się standardowym wejściem/wyjściem, konfigurujemy SPI, włączamy odpowiednią peryferię.

Wejście/wyjście

W Atmega328 wejście/wyjście zrealizowane jest w sposób maksymalnie prosty, przez co obfitość opcji STM32 może wprowadzać w zakłopotanie. Chociaż teraz potrzebujemy tylko wyjść, to mamy cztery warianty:

Czytamy karty katalogowe 2: SPI na STM32; PWM, timery i przerwania na STM8
wyjście z otwartym kolektorem, wyjście „ciągnij pchaj”, alternatywne „ciągnij pchaj”, alternatywne otwarty kolektor

„Ciągnij-pchaj” (push-pull) – standardowe wyjście z Arduino, pin może przyjmować wartość albo HIGH, albo LOW. A tutaj pojawia się „otwarty kolektor” zasady użycia, co stwarza pewne, chociaż w rzeczywistości wszystko jest proste:

Czytamy karty katalogowe 2: SPI na STM32; PWM, timery i przerwania na STM8
Czytamy karty katalogowe 2: SPI na STM32; PWM, timery i przerwania na STM8
Konfiguracja wyjścia / gdy port jest przypisany jako wyjście: / włączony bufor wyjścia: / - tryb otwartego kolektora: „0” w rejestrze wyjścia aktywuje N-MOS, „1” w rejestrze wyjścia zostawia port w trybie Hi-Z (P-MOS nie jest aktywowany) / - tryb „ciągnij-pchaj”: „0” w rejestrze wyjścia aktywuje N-MOS, „1” w rejestrze wyjścia aktywuje P-MOS.

Cała różnica między otwartym kolektorem (open drain) a „ciągnij-pchaj” (push-pull) polega na tym, że w pierwszym pin nie może przyjąć stanu HIGH: przy zapisie jedynki do rejestru wyjścia przechodzi w tryb wysokiej impedancji (high impedance, Hi-Z). Przy zapisie zera pin w obu trybach zachowuje się identycznie, zarówno logicznie, jak i elektrycznie.

W normalnym trybie wyjścia pin po prostu przekazuje zawartość rejestru wyjścia. W „alternatywnym” zarządza nim odpowiednia peryferia (zob. 9.1.4):

Czytamy karty katalogowe 2: SPI na STM32; PWM, timery i przerwania na STM8
Jeśli bit portu jest skonfigurowany jako wyjście funkcji alternatywnej, rejestr wyjścia jest wyłączony, a pin jest podłączony do sygnału wyjściowego peryferii.

Funkcjonalność alternatywna każdego pina jest opisana w Definicje pinów dane są na pobranym obrazku. Na pytanie, co robić, jeśli pin ma kilka alternatywnych funkcji, odpowiedź znajduje się w przypisach w dokumentacji:

Czytamy karty katalogowe 2: SPI na STM32; PWM, timery i przerwania na STM8
Jeśli kilka bloków peryferyjnych korzysta z tego samego pinu, aby uniknąć konfliktu pomiędzy alternatywnymi funkcjami, należy jednocześnie korzystać tylko z jednego bloku peryferyjnego, przełączając się za pomocą bitu aktywacji taktowania peryferiów (w odpowiednim rejestrze RCC).

W końcu piny w trybie wyjścia mają jeszcze prędkość taktowania. To kolejny element oszczędzania energii, w naszym przypadku po prostu ustawiamy na maksimum i zapominamy.

Zatem: używamy SPI, co oznacza, że dwa piny (z danymi i sygnałem taktującym) powinny być "alternatywną funkcją push-pull", a jeden (LAT) – "zwykłym push-pull". Ale zanim je przypiszemy, zrozummy SPI.

SPI

Jeszcze małe wprowadzenie

SPI, czyli Serial Peripheral Interface (szeregowy interfejs peryferyjny) – to prosty i bardzo efektywny interfejs do komunikacji między mikrokontrolerami oraz ze światem zewnętrznym. Zasada jego działania opisana jest powyżej, tam, gdzie mowa o chińskim sterowniku LED (w dokumentacji podręcznej w sekcji 25). SPI może działać w trybie master ("gospodarza") i slave ("niewolnika"). SPI ma cztery podstawowe kanały, z których nie wszystkie mogą być używane:

  • MOSI, Master Output / Slave Input: ten pin w trybie master przesyła, a w trybie slave odbiera dane;
  • MISO, Master Input / Slave Output: odwrotnie, w masterze odbiera, w slave – przesyła;
  • SCK, Serial Clock: ustala częstotliwość przesyłania danych w masterze lub odbiera sygnał taktujący w slave. W istocie oznacza to odtwarzanie bitów;
  • SS, Slave Select: za pomocą tego kanału slave dowiaduje się, że ktoś czegoś od niego chce. W STM32 nazywa się NSS, gdzie N = negatywny, tzn. kontroler staje się slave'em, jeśli na tym kanale jest masa. Dobrze współpracuje z trybem Open Drain Output, ale to inna historia.

Jak wszystko inne, SPI w STM32 jest bogate w funkcjonalność, co nieco komplikuje jego zrozumienie. Na przykład może działać nie tylko jako SPI, ale także jako interfejs I2S, a w dokumentacji ich opisy są wymieszane, trzeba na czas odseparować to, co zbędne. Nasze zadanie jest bardzo proste: trzeba tylko przesyłać dane, używając tylko MOSI i SCK. Przechodzimy do sekcji 25.3.4 (komunikacja półdupleksowa), gdzie znajdujemy 1 sygnał taktujący i 1 jednokierunkowy przepływ danych (1 clock and 1 unidirectional data wire):

Czytamy karty katalogowe 2: SPI na STM32; PWM, timery i przerwania na STM8
W tym trybie aplikacja korzysta z SPI, działając albo w trybie tylko przesyłania, albo tylko odbierania. / Tryb tylko przesyłania przypomina tryb duplexu: dane są przesyłane przez pin nadawczy (MOSI w trybie master lub MISO w trybie slave), a pin odbierający (MISO lub MOSI odpowiednio) można używać jako zwykły pin wejściowy-wyjściowy. W tym przypadku aplikacja może zignorować bufor Rx (jeśli go odczytamy, nie będzie w nim przesyłanych danych).

Świetnie, pin MISO się zwolnił, podłączmy do niego sygnał LAT. Rozwiążemy problem z Slave Select, który na STM32 można kontrolować programowo, co jest niezwykle wygodne. Czytamy odpowiedni akapit z sekcji 25.3.1 Opis ogólny SPI:

Czytamy karty katalogowe 2: SPI na STM32; PWM, timery i przerwania na STM8
Programowa kontrola NSS (SSM = 1) / Informacja o wyborze slave'a zawarta jest w bicie SSI rejestru SPI_CR1. Zewnętrzny pin NSS pozostaje wolny do innych potrzeb aplikacji.

Czas zacząć pisać do rejestrów. Postanowiłem użyć SPI2, szukam w dokumentacji jego adresu bazowego – w sekcji 3.3 Mapa pamięci:

Czytamy karty katalogowe 2: SPI na STM32; PWM, timery i przerwania na STM8

A więc zaczynamy:

#define _SPI2_(mem_offset) (*(volatile uint32_t *)(0x40003800 + (mem_offset)))

Otwieramy sekcję 25.3.3 o mówiącej nazwie „Konfiguracja SPI w trybie master”:

Czytamy karty katalogowe 2: SPI na STM32; PWM, timery i przerwania na STM8

1. Ustaw częstotliwość taktowania interfejsu szeregowego za pomocą bitów BR[2:0] w rejestrze SPI_CR1.

Rejestry są zgrupowane w tym samym rozdziale podręcznika referencyjnego. Przesunięcie adresu (Address offset) w CR1 wynosi 0x00, domyślnie wszystkie bity są zresetowane (Reset value 0x0000):

Czytamy karty katalogowe 2: SPI na STM32; PWM, timery i przerwania na STM8

Bity BR ustawiają dzielnik częstotliwości taktowania kontrolera, określając w ten sposób częstotliwość, z jaką będzie działać SPI. Częstotliwość STM32 wynosi 72 MHz, a sterownik LED, według jego dokumentacji, działa z częstotliwością do 25 MHz, dlatego trzeba podzielić przez cztery (BR[2:0] = 001).

#define _SPI_CR1 0x00

#define BR_0        0x0008
#define BR_1        0x0010
#define BR_2        0x0020

_SPI2_ (_SPI_CR1) |= BR_0;// pclk/4

2. Ustaw bity CPOL i CPHA, aby określić relacje między przesyłaniem danych a taktowaniem interfejsu szeregowego (patrz schemat na stronie 240)

Ponieważ czytamy dokumentację techniczną, a nie analizujemy schematy, lepiej zapoznajmy się z tekstowym opisem bitów CPOL i CPHA na stronie 704 (Opis ogólny SPI):

Czytamy karty katalogowe 2: SPI na STM32; PWM, timery i przerwania na STM8
Faza i polaryzacja sygnału taktującego
Za pomocą bitów CPOL i CPHA rejestru SPI_CR1 można programowo wybrać cztery warianty relacji czasowych. Bit CPOL (polaryzacja sygnału taktującego) kontroluje stan sygnału taktującego, gdy dane nie są przesyłane. Bit ten zarządza trybami master i slave. Jeśli CPOL jest zresetowany, pin SCK w trybie spoczynkowym znajduje się na poziomie niskim. Jeśli bit CPOL jest ustawiony, pin SCK w trybie spoczynkowym znajduje się na poziomie wysokim.
Jeśli ustawiony jest bit CPHA (faza sygnału zegarowego), to drugim zboczem sygnału SCK (opadającym, jeśli CPOL jest zerowany, lub wznoszącym, jeśli CPOL jest ustawiony) jest zbocze, które łapie najstarszy bit. Dane są rejestrowane przy drugim przełączeniu sygnału zegarowego. Jeśli bit CPHA jest zerowany, zbocze łapiące najstarszy bit to zbocze przednie sygnału SCK (opadające, jeśli CPOL jest ustawiony, lub wznoszące, jeśli CPOL jest zerowany). Dane są rejestrowane przy pierwszym przełączeniu sygnału zegarowego.

Analizując tę wiedzę, dochodzimy do wniosku, że oba bity muszą pozostać zerami, ponieważ sygnał SCK powinien pozostać niski, gdy nie jest używany, a dane powinny być przesyłane przy przednim zboczu impulsu (patrz Rising Edge w karcie danych DM634).

Przy okazji, tutaj po raz pierwszy spotykamy się z cechą leksykalną w kartach danych ST: w nich fraza „zresetować bit do zera” zapisywana jest to reset a bit, a nie to clear a bit, jak na przykład w przypadku Atmegi.

3. Ustaw bit DFF, aby określić 8-bitowy lub 16-bitowy format bloku danych.

Specjalnie wybrałem 16-bitowy DM634, aby nie komplikować przesyłania 12-bitowych danych PWM, jak w przypadku DM633. Ma sens ustawić DFF na jedynkę:

#define DFF         0x0800

_SPI2_ (_SPI_CR1) |= DFF; // 16-bit mode

4. Skonfiguruj bit LSBFIRST w rejestrze SPI_CR1, aby określić format bloku.

LSBFIRST, jak sama nazwa wskazuje, ustawia przesyłanie najstarszego bitu do przodu. Jednak DM634 chce otrzymywać dane, zaczynając od najstarszego bitu. Dlatego pozostawiamy go zresetowanym.

5. W trybie sprzętowym, jeśli wymagana jest wejście z pinu NSS, podaj na pin NSS wysoki sygnał podczas całej sekwencji przesyłania bajtów. W trybie programowym ustaw bity SSM i SSI w rejestrze SPI_CR1. Jeśli pin NSS ma działać jako wyjście, należy ustawić tylko bit SSOE.

Ustawiamy SSM i SSI, aby zapomnieć o trybie sprzętowym NSS:

#define SSI         0x0100
#define SSM         0x0200

_SPI2_ (_SPI_CR1) |= SSM | SSI; //enable software control of SS, SS high

6. Bity MSTR i SPE muszą być ustawione (pozostają ustawione tylko wtedy, gdy na NSS podawany jest wysoki sygnał).

W rzeczywistości tymi bitami przypisujemy nasz SPI jako mistrza i go włączamy:

#define MSTR        0x0004
#define SPE         0x0040

_SPI2_ (_SPI_CR1) |= MSTR; //SPI master
//когда все готово, включаем SPI
_SPI2_ (_SPI_CR1) |= SPE;

SPI jest skonfigurowany, napiszmy od razu funkcje, które będą przesyłać bajty do sterownika. Kontynuujemy czytanie 25.3.3 „Konfiguracja SPI w trybie master”:

Czytamy karty katalogowe 2: SPI na STM32; PWM, timery i przerwania na STM8
Kolejność przesyłania danych
Przesyłanie rozpoczyna się, gdy bajt jest zapisywany do bufora Tx.
Bajt danych ładowany jest do rejestru przesuwnego w trybie równoległym (z wewnętrznej magistrali) podczas przesyłania pierwszego bitu, po czym jest przesyłany do trybie (z wewnętrznej magistrali) podczas przesyłania pierwszego bitu, po czym jest przesyłany w sekwencyjnym trybie MOSI, z pierwszym lub ostatnim bitem przesuwanym w zależności od ustawienia bitu LSBFIRST w rejestrze CPI_CR1. Flaga TXE jest ustawiana po przesłaniu danych z bufora Tx do rejestru przesuwnego, a także generowane jest przerwanie, jeśli bit TXEIE jest ustawiony w rejestrze CPI_CR1.

W moim tłumaczeniu podkreśliłem kilka słów, aby zwrócić uwagę na jedną szczególną cechę implementacji SPI w kontrolerach STM. W Atmedze flaga TXE (Tx Empty, Tx jest pusty i gotowy do przyjęcia danych) jest ustawiana tylko po tym, jak cały bajt został wysłany na zewnątrz. A tutaj ta flaga jest ustawiana po tym, jak bajt trafił do wewnętrznego rejestru przesuwnego. Ponieważ jest tam wpychany wszystkimi bitami jednocześnie (równolegle), a następnie dane są przesyłane sekwencyjnie, TXE jest ustawiana przed pełnym wysłaniem bajtu. To ważne, ponieważ w przypadku naszego kontrolera LED musimy aktywować pin LAT po wysłaniu wszystkich danych, więc sama flaga TXE nie wystarczy.

A to oznacza, że potrzebna nam będzie jeszcze jakaś flaga. Zobaczmy w 25.3.7 – „Flagi stanu”:

Czytamy karty katalogowe 2: SPI na STM32; PWM, timery i przerwania na STM8
<…>
Czytamy karty katalogowe 2: SPI na STM32; PWM, timery i przerwania na STM8
Flaga BUSY
Flaga BSY jest ustawiana i resetowana sprzętowo (zapis w nią nie wpływa na nic). Flaga BSY pokazuje stan warstwy komunikacyjnej SPI.
Jest resetowana:
gdy przesyłanie jest zakończone (z wyjątkiem trybu master, jeśli przesyłanie jest ciągłe)
gdy SPI jest wyłączone
gdy występuje błąd trybu master (MODF=1)
Jeśli przesyłanie nie jest ciągłe, flaga BSY jest resetowana między każdą transmisją danych

Dobrze, przyda się. Dowiedzmy się, gdzie jest bufor Tx. W tym celu odczytujemy „Rejestr danych SPI”:

Czytamy karty katalogowe 2: SPI na STM32; PWM, timery i przerwania na STM8
Bity 15:0 DR[15:0] Rejestr danych
Otrzymane dane lub dane do przesłania.
Rejestr danych jest podzielony na dwa bufory – jeden do zapisu (bufor przesyłania) i drugi do odczytu (bufor odbierania). Zapis do rejestru danych pisze do bufora Tx, a odczyt z rejestru danych zwraca wartość zawartą w buforze Rx.

No i rejestr statusu, gdzie znajdują się flagi TXE i BSY:

Czytamy karty katalogowe 2: SPI na STM32; PWM, timery i przerwania na STM8

Piszę:

#define _SPI_DR  0x0C
#define _SPI_SR  0x08

#define BSY         0x0080
#define TXE         0x0002

void dm_shift16(uint16_t value)
{
    _SPI2_(_SPI_DR) = value; //send 2 bytes
    while (!(_SPI2_(_SPI_SR) & TXE)); //wait until they're sent
}

Ponieważ musimy przesłać 16 razy po dwa bajty, w liczbie wyjść kontrolera LED, wyglądałoby to mniej więcej tak:

void sendLEDdata()
{
    LAT_low();
    uint8_t k = 16;
    do
    {   k--;
        dm_shift16(leds[k]);
    } while (k);

    while (_SPI2_(_SPI_SR) & BSY); // zakończ transmisję

    LAT_pulse();
}

Ale nie umiemy jeszcze aktywować pinu LAT, więc wróćmy do I/O.

Przypisujemy piny

Rejestry w STM32F1 odpowiedzialne za stan pinów są dość nietypowe. Oczywiste jest, że jest ich więcej niż w Atmedze, ale różnią się również od innych chipów STM. Sekcja 9.1 Opis ogólny GPIO:

Czytamy karty katalogowe 2: SPI na STM32; PWM, timery i przerwania na STM8
Każdy z portów wejścia/wyjścia ogólnego przeznaczenia (GPIO) posiada dwa 32-bitowe rejestry konfiguracyjne (GPIOx_CRL i GPIOx_CRH), dwa 32-bitowe rejestry danych (GPIOx_IDR i GPIOx_ODR), 32-bitowy rejestr ustawienia/zerowania (GPIOx_BSRR), 16-bitowy rejestr zerowania (GPIOx_BRR) oraz 32-bitowy rejestr blokady (GPIOx_LCKR).

Są one nietypowe, a także dość niewygodne, ponieważ pierwsze dwa rejestry mają 16 pinów portu rozdzielonych w formacie „cztery bity na brat”. Tzn. piny od zerowego do siódmego znajdują się w CRL, a pozostałe – w CRH. Pozostałe rejestry pomyślnie pomieszczą bity wszystkich pinów portu – często pozostając w połowie „zarezerwowane”.

Dla uproszczenia zaczniemy od końca listy.

Rejestr blokady nie będzie nam potrzebny.

Rejestry ustawienia i zerowania są dość ciekawe, ponieważ częściowo się dublują: można pisać tylko w BSRR, gdzie wyższe 16 bitów zresetuje pin do zera, a niższe – ustawi na 1, lub używać również BRR, gdzie niższe 16 bitów tylko zerują pin. Osobiście wolę drugi wariant. Te rejestry są ważne, ponieważ zapewniają atomowy dostęp do pinów:

Czytamy karty katalogowe 2: SPI na STM32; PWM, timery i przerwania na STM8
Czytamy karty katalogowe 2: SPI na STM32; PWM, timery i przerwania na STM8
Atomowe ustawienie lub zerowanie
Nie trzeba wyłączać przerwań podczas programowania GPIOx_ODR na poziomie bitów: można zmieniać jeden lub kilka bitów jedną atomową operacją zapisu APB2. Osiąga się to zapisem „1” do rejestru ustawienia/zerowania (GPIOx_BSRR lub, tylko w przypadku zerowania, w GPIOx_BRR) bitu, który ma być zmieniony. Pozostałe bity pozostaną niezmienione.

Rejestry danych mają dość mówiące nazwy – IDR = Input Rejestr kierunku, rejestr wejścia; ODR = Output Rejestr kierunku, rejestr wyjścia. W bieżącym projekcie nie będą nam potrzebne.

No i wreszcie rejestry kontrolne. Ponieważ interesują nas piny drugiego SPI, a mianowicie PB13, PB14 i PB15, zaraz patrzymy na CRH:

Czytamy karty katalogowe 2: SPI na STM32; PWM, timery i przerwania na STM8

I widzimy, że trzeba będzie coś napisać w bitach od 20 do 31.

Już wcześniej omówiliśmy, czego oczekujemy od pinów, więc tym razem obejdę się bez zrzutu ekranu, po prostu powiem, że MODE ustawia kierunek (wejście, jeśli oba bity są ustawione na 0) oraz prędkość pinu (potrzebujemy 50MHz, czyli oba piny na „1”), a CNF ustawia tryb: zwykły „ciągnij-pchaj” – 00, „alternatywny” – 10. Domyślnie, jak widzimy powyżej, wszystkie piny mają ustawiony trzeci od dołu bit (CNF0), który ustawia je w tryb floating input.

Ponieważ planuję zrobić coś jeszcze z tym układem, dla uproszczenia zdefiniowałem wszystkie możliwe wartości MODE i CNF zarówno dla dolnych, jak i górnych rejestrów kontrolnych.

No więc coś takiego

#define CNF0_0 0x00000004
#define CNF0_1 0x00000008
#define CNF1_0 0x00000040
#define CNF1_1 0x00000080
#define CNF2_0 0x00000400
#define CNF2_1 0x00000800
#define CNF3_0 0x00004000
#define CNF3_1 0x00008000
#define CNF4_0 0x00040000
#define CNF4_1 0x00080000
#define CNF5_0 0x00400000
#define CNF5_1 0x00800000
#define CNF6_0 0x04000000
#define CNF6_1 0x08000000
#define CNF7_0 0x40000000
#define CNF7_1 0x80000000
#define CNF8_0 0x00000004
#define CNF8_1 0x00000008
#define CNF9_0 0x00000040
#define CNF9_1 0x00000080
#define CNF10_0 0x00000400
#define CNF10_1 0x00000800
#define CNF11_0 0x00004000
#define CNF11_1 0x00008000
#define CNF12_0 0x00040000
#define CNF12_1 0x00080000
#define CNF13_0 0x00400000
#define CNF13_1 0x00800000
#define CNF14_0 0x04000000
#define CNF14_1 0x08000000
#define CNF15_0 0x40000000
#define CNF15_1 0x80000000

#define MODE0_0 0x00000001
#define MODE0_1 0x00000002
#define MODE1_0 0x00000010
#define MODE1_1 0x00000020
#define MODE2_0 0x00000100
#define MODE2_1 0x00000200
#define MODE3_0 0x00001000
#define MODE3_1 0x00002000
#define MODE4_0 0x00010000
#define MODE4_1 0x00020000
#define MODE5_0 0x00100000
#define MODE5_1 0x00200000
#define MODE6_0 0x01000000
#define MODE6_1 0x02000000
#define MODE7_0 0x10000000
#define MODE7_1 0x20000000
#define MODE8_0 0x00000001
#define MODE8_1 0x00000002
#define MODE9_0 0x00000010
#define MODE9_1 0x00000020
#define MODE10_0 0x00000100
#define MODE10_1 0x00000200
#define MODE11_0 0x00001000
#define MODE11_1 0x00002000
#define MODE12_0 0x00010000
#define MODE12_1 0x00020000
#define MODE13_0 0x00100000
#define MODE13_1 0x00200000
#define MODE14_0 0x01000000
#define MODE14_1 0x02000000
#define MODE15_0 0x10000000
#define MODE15_1 0x20000000

Nasze piny znajdują się na porcie B (adres bazowy – 0x40010C00), kod:

#define _PORTB_(mem_offset) (*(volatile uint32_t *)(0x40010C00 + (mem_offset)))

#define _BRR  0x14
#define _BSRR 0x10
#define _CRL  0x00
#define _CRH  0x04

//используем стандартный SPI2: MOSI на B15, CLK на B13
//LAT пусть будет на неиспользуемом MISO – B14

//очищаем дефолтный бит, он нам точно не нужен
_PORTB_ (_CRH) &= ~(CNF15_0 | CNF14_0 | CNF13_0 | CNF12_0);

//альтернативные функции для MOSI и SCK
_PORTB_ (_CRH) |= CNF15_1 | CNF13_1;

//50 МГц, MODE = 11
_PORTB_ (_CRH) |= MODE15_1 | MODE15_0 | MODE14_1 | MODE14_0 | MODE13_1 | MODE13_0;

I odpowiednio można napisać definicje dla LAT, który będzie napędzał rejestry BRR i BSRR:

/*** LAT pulse – high, then low */
#define LAT_pulse() _PORTB_(_BSRR) = (1<<14); _PORTB_(_BRR) = (1<<14)

#define LAT_low() _PORTB_(_BRR) = (1<<14)

(LAT_low po prostu z przyzwyczajenia, zawsze tak było, niech już zostanie)

Teraz wszystko wygląda świetnie, tylko nie działa. Ponieważ to STM32, oszczędzają energię, a więc trzeba włączyć taktowanie potrzebnej peryferii.

Włączamy taktowanie

Za taktowanie odpowiadają zegary, czyli Clock. Już mogliśmy zauważyć skrót RCC. Szukamy go w dokumentacji: to Reset and Clock Control (Zarządzanie resetowaniem i taktem).

Jak wspomniano powyżej, na szczęście najtrudniejsze z tematu taktowania zrobili za nas ludzie z STM, za co ogromne dzięki (jeszcze raz dam link do strony Di Halt’a, aby było wiadomo, jak skomplikowane to jest). Potrzebujemy tylko rejestrów odpowiedzialnych za włączenie taktowania peryferii (Rejestry Włączenia Taktowania Peryferii). Na początek znajdźmy adres bazowy RCC, jest on na początku „Mapy pamięci”:

Czytamy karty katalogowe 2: SPI na STM32; PWM, timery i przerwania na STM8

#define _RCC_(mem_offset) (*(volatile uint32_t *)(0x40021000 + (mem_offset)))

A teraz można albo kliknąć w link, gdzie próbować coś znaleźć w tabelce, albo, co dużo lepsze, przeszukać opisy włączających rejestrów z sekcji o enable registers. Gdzie znajdziemy RCC_APB1ENR i RCC_APB2ENR:

Czytamy karty katalogowe 2: SPI na STM32; PWM, timery i przerwania na STM8
Czytamy karty katalogowe 2: SPI na STM32; PWM, timery i przerwania na STM8

I w nich, odpowiednio, bity włączające taktowanie SPI2, IOPB (I/O Port B) oraz funkcji alternatywnych (AFIO).

#define _APB2ENR 0x18
#define _APB1ENR 0x1C

#define IOPBEN 0x0008
#define SPI2EN 0x4000
#define AFIOEN 0x0001

//включаем тактирование порта B и альт. функций
_RCC_(_APB2ENR) |= IOPBEN | AFIOEN;

//включаем  тактирование SPI2
_RCC_(_APB1ENR) |= SPI2EN;

Końcowy kod można znaleźć tutaj.

Jeśli jest możliwość i chęć przetestować, to podłączamy DM634 tak: DAI do PB15, DCK do PB13, LAT do PB14. Zasilamy sterownik z 5 volt, nie zapominajmy o połączeniu mas.

Czytamy karty katalogowe 2: SPI na STM32; PWM, timery i przerwania na STM8

STM8 PWM

PWM na STM8

Kiedy tylko planowałem ten artykuł, postanowiłem spróbować opanować jakąś funkcję nieznanego mi chipu, używając tylko dokumentacji, aby nie być szewcem bez butów. STM8 idealnie się do tego nadawał: po pierwsze, miałem kilka chińskich płytek z STM8S103, a po drugie, nie jest on zbyt popularny, więc pokusa, by coś poczytać i znaleźć rozwiązanie w sieci, napotyka na brak tych rozwiązań.

Do chipu jest również karta danych i podręcznik referencyjny RM0016, w pierwszym przypisanie pinów i adresy rejestrów, w drugim – wszystko inne. STM8 programuje się w C w niezbyt ładnym środowisku IDE ST Visual Develop.

Takty i wejścia-wyjścia

Domyślnie STM8 działa z częstotliwością 2 MHz, co należy natychmiast poprawić.

Czytamy karty katalogowe 2: SPI na STM32; PWM, timery i przerwania na STM8
Sygnał taktujący HSI (wewnętrzny szybki)
Sygnał taktujący HSI pochodzi z wewnętrznego generatora RC o częstotliwości 16 MHz z programowalnym dzielnikiem (od 1 do 8). Ustala się go w rejestrze dzielnika sygnału taktującego (CLK_CKDIVR).
Uwaga: na starcie głównym źródłem sygnału taktującego wybierany jest generator HSI RC z dzielnikiem 8.

Znajdujemy adres rejestru w dokumentacji, opis w podręczniku referencyjnym i widzimy, że rejestr należy wyczyścić:

#define CLK_CKDIVR *(volatile uint8_t *)0x0050C6

CLK_CKDIVR &= ~(0x18);

Ponieważ zamierzamy uruchomić PWM i podłączyć diody LED, sprawdzamy przypisanie pinów:

Czytamy karty katalogowe 2: SPI na STM32; PWM, timery i przerwania na STM8

Chip jest mały, wiele funkcji jest związanych z tymi samymi pinami. To, co jest w kwadratowych nawiasach, to „funkcjonalność alternatywna”, którą można przełączać „opcjonalnymi bajtami” (option bytes) – coś jak bezpieczniki w Atmedze. Można zmieniać ich wartości programowo, ale nie trzeba, ponieważ nowa funkcjonalność aktywuje się dopiero po ponownym uruchomieniu. Lepiej użyć ST Visual Programmer (który można pobrać razem z Visual Develop), potrafiącego zmieniać te bajty. W rozkładzie pinów widać, że wyjścia CH1 i CH2 pierwszego timera są ukryte w kwadratowych nawiasach; trzeba w STVP przypisać bity AFR1 i AFR0, przy czym drugi również przeniesie wyjście CH1 drugiego timera z PD4 na PC5.

W ten sposób zarządzanie diodami LED będzie odbywać się za pomocą 6 pinów: PC6, PC7 i PC3 dla pierwszego timera, PC5, PD3 i PA3 dla drugiego.

Konfiguracja samych pinów wejścia-wyjścia na STM8 jest prostsza i bardziej logiczna niż na STM32:

  • znany z Atmegi rejestr kierunku danych DDR (Data Direction Register): 1 = wyjście;
  • pierwszy rejestr kontrolny CR1 podczas ustawienia na wyjście określa tryb „ciągnij-popychaj” (1) lub otwarty kolektor (0); ponieważ podłączam diody LED do chipu katodami, pozostawiam zera;
  • drugi rejestr kontrolny CR2 podczas ustawienia na wyjście określa szybkość taktowania: 1 = 10 MHz

#define PA_DDR     *(volatile uint8_t *)0x005002
#define PA_CR2     *(volatile uint8_t *)0x005004
#define PD_DDR     *(volatile uint8_t *)0x005011
#define PD_CR2     *(volatile uint8_t *)0x005013
#define PC_DDR     *(volatile uint8_t *)0x00500C
#define PC_CR2     *(volatile uint8_t *)0x00500E

PA_DDR = (1<<3); //output
PA_CR2 |= (1<<3); //fast
PD_DDR = (1<<3); //output
PD_CR2 |= (1<<3); //fast
PC_DDR = ((1<<3) | (1<<5) | (1<<6) | (1<<7)); //output
PC_CR2 |= ((1<<3) | (1<<5) | (1<<6) | (1<<7)); //fast

Ustawienie PWM

Na początek zdefiniujmy terminy:

  • Częstotliwość PWM – częstotliwość, z jaką tyka timer;
  • Auto-reload, AR – autoloaded value, do którego timer będzie odliczał (okres impulsu);
  • Update Event, UEV – zdarzenie, które następuje, gdy timer osiągnie AR;
  • Wypełnienie PWM – współczynnik wypełnienia PWM, często nazywany „duty cycle”;
  • Wartość Capture/Compare – wartość do porównania/złapania, do której timer coś zrobi (w przypadku PWM – odwróci sygnał wyjściowy);
  • Wartość Preload – wartość preload. Wartość do porównania nie może się zmieniać, dopóki timer tyka, w przeciwnym razie cykl PWM ulegnie awarii. Dlatego nowe przesyłane wartości trafiają do bufora i są pobierane stamtąd, gdy timer osiąga koniec odliczenia i jest resetowany;
  • Tryby wyrównania krawędziowego i Tryby wyrównania środkowego – wyrównanie do krawędzi i do środka, to samo co tryby atmeli; Szybki PWM i PWM ze skorygowanym fazowaniem.
  • OCiREF, Sygnał Referencyjny Wyjścia – referencyjny sygnał wyjściowy, właściwie to, co w trybie PWM znajduje się na odpowiednim pinie.

Jak już jasne z przypisania, PWM jest dostępne w dwóch timerach – pierwszym i drugim. Oba są 16-bitowe, pierwszy ma wiele dodatkowych funkcji (w szczególności potrafi liczyć w górę i w dół). Musimy, aby oba działały identycznie, więc postanowiłem zacząć od celowo bardziej ograniczonego drugiego, aby przypadkowo nie wykorzystać czegoś, czego nie ma. Pewnym problemem jest to, że opis funkcjonalności PWM wszystkich timerów w podręczniku referencyjnym znajduje się w rozdziale o pierwszym timerze (17.5.7 Tryb PWM), dlatego muszę ciągle skakać tam i z powrotem po dokumencie.

PWM w STM8 ma ważną przewagę nad PWM w Atmedze:

Czytamy karty katalogowe 2: SPI na STM32; PWM, timery i przerwania na STM8
PWM z wyrównaniem krawędziowym
Konfiguracja liczenia w górę
Liczenie w górę jest aktywne, jeśli bit DIR w rejestrze TIM_CR1 jest wyzerowany
Przykład
Przykład wykorzystuje pierwszy tryb PWM. Sygnał referencyjny PWM OCiREF jest utrzymywany na wysokim poziomie, podczas gdy TIM1_CNT < TIM1_CCRi. W przeciwnym razie przyjmuje niski poziom. Jeśli wartość porównawcza w rejestrze TIM1_CCRi jest większa niż wartość autoloaded (rejestr TIM1_ARR), sygnał OCiREF utrzymuje się w 1. Jeśli wartość porównawcza wynosi 0, OCiREF utrzymuje się na zerze.

Timer STM8 podczas zdarzenia aktualizacji najpierw sprawdza wartość porównawczą, a dopiero potem wydaje sygnał referencyjny. W Atmedze timer najpierw działa, a potem porównuje, przez co przy wartość porównawcza == 0 Na wyjściu otrzymujemy igłę, z którą trzeba jakoś walczyć (na przykład programowo inwertując logikę).

Zatem, co chcemy zrobić: 8-bitowe PWM (AR == 255), liczymy od dołu do góry, wyrównanie do granicy. Ponieważ diody są podłączone do układu katodami, PWM powinien wydawać 0 (LED świeci) do wartość porównawczą i 1 po.

Już przeczytaliśmy o pewnych trybach PWM, dlatego znajdujemy odpowiedni rejestr drugiego timera, przeszukując podręcznik referencyjny według tej frazy (18.6.8 – TIMx_CCMR1):

Czytamy karty katalogowe 2: SPI na STM32; PWM, timery i przerwania na STM8
110: Pierwszy tryb PWM – przy liczeniu od dołu do góry, pierwszy kanał jest aktywny, dopóki TIMx_CNT < TIMx_CCR1. W przeciwnym razie pierwszy kanał jest nieaktywny. [dalej w dokumencie błędne kopiowanie z timera 1]
111: Drugi tryb PWM – przy liczeniu od dołu do góry, pierwszy kanał jest nieaktywny, dopóki TIMx_CNT < TIMx_CCR1. W przeciwnym razie pierwszy kanał jest aktywny.

Ponieważ diody LED są podłączone do mikrokontrolera katodami, pasuje nam drugi tryb (pierwszy również, ale na razie o tym nie wiemy).

Czytamy karty katalogowe 2: SPI na STM32; PWM, timery i przerwania na STM8
Bit 3 OC1PE: Włączyć preładowanie wyjścia 1
0: Rejestr preładowania w TIMx_CCR1 jest wyłączony. Można zapisywać w TIMx_CCR1 w dowolnym czasie. Nowa wartość działa natychmiast.
1: Rejestr preładowania w TIMx_CCR1 jest włączony. Operacje odczytu/zapisu odnoszą się do rejestru preładowania. Wartość preładowana TIMx_CCR1 jest ładowana do rejestru cieniowego przy każdym zdarzeniu aktualizacji.
*Uwaga: aby tryb PWM działał prawidłowo, rejestry preładowania muszą być włączone. Nie jest to konieczne w trybie pojedynczego sygnału (w rejestrze TIMx_CR1 jest ustawiony bit OPM).

W porządku, włączamy wszystko, co potrzebne do trzech kanałów drugiego timera:

#define TIM2_CCMR1 *(volatile uint8_t *)0x005307
#define TIM2_CCMR2 *(volatile uint8_t *)0x005308
#define TIM2_CCMR3 *(volatile uint8_t *)0x005309

#define PWM_MODE2   0x70 //PWM mode 2, 0b01110000
#define OCxPE       0x08 //preload enable

TIM2_CCMR1 = (PWM_MODE2 | OCxPE);
TIM2_CCMR2 = (PWM_MODE2 | OCxPE);
TIM2_CCMR3 = (PWM_MODE2 | OCxPE);

AR składa się z dwóch ośmiobitowych rejestrów, tutaj jest wszystko proste:

#define TIM2_ARRH  *(volatile uint8_t *)0x00530F
#define TIM2_ARRL  *(volatile uint8_t *)0x005310

TIM2_ARRH = 0;
TIM2_ARRL = 255;

Drugi timer potrafi tylko liczyć od dołu do góry, wyrównanie do granicy, niczego zmieniać nie trzeba. Ustawimy dzielnik częstotliwości, na przykład, na 256. Dla drugiego timera dzielnik ustawia się w rejestrze TIM2_PSCR i jest potęgą liczby 2:

#define TIM2_PSCR  *(volatile uint8_t *)0x00530E

TIM2_PSCR = 8;

Pozostaje włączyć wyjścia i sam drugi timer. Pierwsze zadanie rozwiązuje rejestr Capture/Compare Włącz: jest ich dwa, trzy kanały są na nich rozłożone niesymetrycznie. Tutaj możemy również dowiedzieć się, że można zmieniać polaryzację sygnału, czyli w zasadzie można byłoby używać również PWM Mode 1. Pisziemy:

#define TIM2_CCER1 *(volatile uint8_t *)0x00530A
#define TIM2_CCER2 *(volatile uint8_t *)0x00530B

#define CC1E  (1<<0) // CCER1
#define CC2E  (1<<4) // CCER1
#define CC3E  (1<<0) // CCER2

TIM2_CCER1 = (CC1E | CC2E);
TIM2_CCER2 = CC3E;

No i w końcu uruchamiamy timer w rejestrze TIMx_CR1:

Czytamy karty katalogowe 2: SPI na STM32; PWM, timery i przerwania na STM8

#define TIM2_CR1   *(volatile uint8_t *)0x005300

TIM2_CR1 |= 1;

Napiszemy prosty analog AnalogWrite(), który będzie przekazywał timerowi właściwie wartości do porównania. Rejestry nazywają się przewidywalnie Capture/Compare registers, po dwa na każdy kanał: młodsze 8 bitów w TIM2_CCRxL i starsze w TIM2_CCRxH. Ponieważ uruchomiliśmy 8-bitowy PWM, wystarczy zapisywać tylko młodsze bity:

#define TIM2_CCR1L *(volatile uint8_t *)0x005312
#define TIM2_CCR2L *(volatile uint8_t *)0x005314
#define TIM2_CCR3L *(volatile uint8_t *)0x005316

void setRGBled(uint8_t r, uint8_t g, uint8_t b)
{
    TIM2_CCR1L = r;
    TIM2_CCR2L = g;
    TIM2_CCR3L = b;
}

Uważny czytelnik zauważy, że otrzymaliśmy lekko wadliwy PWM, który nie może osiągnąć 100% wypełnienia (przy maksymalnej wartości 255 sygnał odwraca się na jeden cykl timera). Dla diod LED to nie ma znaczenia, a uważny czytelnik sam domyśla się, jak to naprawić.

PWM na drugim timerze działa, przechodzimy do pierwszego.

Pierwszy timer ma dokładnie te same bity w takich samych rejestrach (po prostu te bity, które pozostały 'zarezerwowane' w drugim timerze, w pierwszym są aktywnie używane do różnych zaawansowanych funkcji). Dlatego wystarczy znaleźć adresy tych samych rejestrów w karcie katalogowej i skopiować kod. No i zmienić wartość dzielnika częstotliwości, ponieważ pierwszy timer chce otrzymać nie moc potęgi dwójki, ale precyzyjną wartość 16-bitową w dwóch rejestrach. Dzielnik wysokiej częstotliwości i Niski. Robimy wszystko i… pierwszy timer nie działa. O co chodzi?

Problem można rozwiązać tylko poprzez przestudiowanie całej sekcji dotyczącej rejestrów kontrolnych timera 1, gdzie szukamy tego, którego nie ma w drugim timerze. Znajdzie się. 17.7.30 Rejestr przerwania (TIM1_BKR), gdzie jest taki bit:

Czytamy karty katalogowe 2: SPI na STM32; PWM, timery i przerwania na STM8
Włącz główny wyjście

#define TIM1_BKR   *(volatile uint8_t *)0x00526D

TIM1_BKR = (1<<7);

Teraz na pewno wszystko, kod tamże.

Czytamy karty katalogowe 2: SPI na STM32; PWM, timery i przerwania na STM8

STM8 Mux

Mikroprzełączanie na STM8

Trzeci mini projekt polega na podłączeniu do drugiego timera w trybie PWM ośmiu diod RGB i sprawieniu, by pokazywały różne kolory. Opiera się na koncepcji mikroprzełączania LED, która polega na tym, że jeśli bardzo szybko włączać i wyłączać diody, wydaje nam się, że świecą one ciągle (trwałość wzroku, inercja percepcji wzrokowej). Kiedyś robiłem coś takiego na Arduino.

Algorytm działania wygląda tak:

  • podłączyliśmy anodę pierwszej diody RGB;
  • włączyliśmy ją, podając odpowiednie sygnały na katody;
  • czekaliśmy na zakończenie cyklu PWM;
  • podłączyliśmy anodę drugiej diody RGB;
  • włączyliśmy ją…

No i tak dalej. Oczywiście, dla ładnej pracy wymaga się, aby podłączenie anody i 'włączenie' diody LED działo się jednocześnie. Cóż, prawie. W każdym razie musimy napisać kod, który w trzech kanałach drugiego timera wyda wartości, a przy osiągnięciu UEV zmieni je i jednocześnie zmieni aktywną w danym momencie diodę RGB.

Ponieważ przełączanie LED odbywa się automatycznie, musimy utworzyć „pamięć wideo”, z której obsługa przerwania będzie pobierać dane. To prosta tablica:

uint8_t colors[8][3];

Aby zmienić kolor konkretnej diody LED, wystarczy zapisać odpowiednie wartości w tej tablicy. A za numer aktywnej diody LED będzie odpowiadała zmienna

uint8_t cnt;

Demuks

Do prawidłowego multipleksowania potrzebny będzie, jak dziwnie by to nie brzmiało, demultiplekser CD74HC238. Demultiplekser to układ scalony, który realizuje operator <<. Przez trzy wejściowe piny (bity 0, 1 i 2) podajemy mu trzybitową liczbę X, a on w odpowiedzi aktywuje wyjście numer (1<<X). Pozostałe wejścia układu są używane do skalowania całej konstrukcji. Ten układ potrzebny jest nam nie tylko do zmniejszenia liczby zajętych pinów mikrokontrolera, ale także dla bezpieczeństwa – aby przypadkowo nie włączyć więcej diod LED, niż to możliwe, i nie spalić mikrokontrolera. Chip kosztuje grosze, warto zawsze mieć go w domowej apteczce.

CD74HC238 będzie odpowiedzialny za podawanie napięcia do anodu odpowiedniej diody LED. W pełnym multipleksie podawałby napięcie na kolumnę przez P-MOSFET, ale w tym demie można to zrobić bezpośrednio, ponieważ ciągnie 20 mA, zgodnie absolute maximum ratings z kartą katalogową. Z karty katalogowej CD74HC238 potrzebujemy przypisania pinów i tej ściągawki:

Czytamy karty katalogowe 2: SPI na STM32; PWM, timery i przerwania na STM8
H = wysoki poziom napięcia, L = niski poziom napięcia, X – obojętne

Podłączamy E2 i E1 do masy, E3, A0, A1 i A3 do pinów PD5, PC3, PC4 i PC5 STM8. Ponieważ powyższa tabela zawiera zarówno niskie, jak i wysokie poziomy, konfigurujemy te piny jako wyjścia push-pull.

PWM

PWM na drugim timerze konfiguruje się tak jak w poprzedniej historii, z dwiema różnicami:

Po pierwsze, musimy włączyć przerwanie dla Update Event (UEV), które wywoła funkcję przełączającą aktywny LED. Robi się to przez zmianę bitu Update Interrupt Enable w rejestrze o wymownej nazwie

Czytamy karty katalogowe 2: SPI na STM32; PWM, timery i przerwania na STM8
Rejestr włączenia przerwań

#define TIM2_IER   *(volatile uint8_t *)0x005303

//enable interrupt
TIM2_IER = 1;

Drugą różnicą jest zjawisko multipleksowania zwane ghosting – zjawisko niepożądane świecenia diod. W naszym przypadku może to się zdarzyć, ponieważ timer, który wywołuje przerwanie na UEV, kontynuuje odliczanie, a obsługa przerwania nie zdąży przełączyć LED, zanim timer zacznie coś zapisywać na wyjściach. W celu zwalczania tego problemu trzeba odwrócić logikę (0 = maksymalna jasność, 255 = nic nie świeci) i nie dopuszczać do ekstremalnych wartości wypełnienia. Tzn. osiągnąć to, aby po UEV diody LED całkowicie gały na jeden takt PWM.

Zmieniamy polaryzację:

//set polarity 
    TIM2_CCER1 |= (CC1P | CC2P);
    TIM2_CCER2 |= CC3P;

Unikamy ustawiania r, g i b na 255 i nie zapominamy je odwrócić podczas używania.

Przerwania

Istotą przerwania jest to, że w określonych okolicznościach chip przerywa wykonywanie głównego programu i wywołuje jakąś funkcję zewnętrzną. Przerwania powstają na skutek zewnętrznych lub wewnętrznych bodźców, w tym od timera.

Kiedy po raz pierwszy utworzyliśmy projekt w ST Visual Develop, otrzymaliśmy oprócz main.c okno z tajemniczym plikiem stm8_interrupt_vector.c, automatycznie włączonym do projektu. W tym pliku do każdego przerwania przypisana jest funkcja NonHandledInterrupt. Musimy przypisać naszą funkcję do odpowiedniego przerwania.

W nocie katalogowej znajduje się tabela wektorów przerwań, gdzie znajdujemy potrzebne:

Czytamy karty katalogowe 2: SPI na STM32; PWM, timery i przerwania na STM8
13 TIM2 aktualizacja/przepełnienie
14 TIM2 uchwyt/porównanie

Musimy zmieniać LED przy UEV, więc potrzebne jest przerwanie nr 13.

Odpowiednio, po pierwsze w pliku stm8_interrupt_vector.c zmieniamy nazwę funkcji odpowiadającej za przerwanie nr 13 (IRQ13) z domyślnej na swoją:

{0x82, TIM2_Overflow}, /* irq13 */

Po drugie, będziemy musieli utworzyć plik main.h o takiej zawartości:

#ifndef __MAIN_H
#define __MAIN_H

@far @interrupt void TIM2_Overflow (void);
#endif

No i na koniec wpisujemy tę funkcję w swoim main.c:

@far @interrupt void TIM2_Overflow (void)
{
    PD_ODR &= ~(1<<5); // wyłączamy demultiplekser
    PC_ODR = (cnt<<3); // zapisujemy nową wartość w demultiplekserze
    PD_ODR |= (1<<5); // włączamy demultiplekser

    TIM2_SR1 = 0; // resetujemy flaga Update Interrupt Pending

    cnt++; 
    cnt &= 7; // przesuwamy licznik LED

    TIM2_CCR1L = ~colors[cnt][0]; // przesyłamy do bufora odwrócone wartości
    TIM2_CCR2L = ~colors[cnt][1]; // dla następnej pętli PWM
    TIM2_CCR3L = ~colors[cnt][2]; // 

    return;
}

Pozostaje włączyć przerwania. Robi się to poleceniem assemblerowym rim – trzeba ją znaleźć w Podręcznik programowania:

//enable interrupts
_asm("rim");

Inne polecenie assemblerowe – sim – wyłącza przerwania. Należy je wyłączać na czas zapisywania nowych wartości w «pamięci wideo», aby przerwanie wywołane w nieodpowiednim momencie nie zniszczyło tablicy.

Cały kod – na GitHubie.

Czytamy karty katalogowe 2: SPI na STM32; PWM, timery i przerwania na STM8

Jeśli ta artykuł okaże się przydatny dla kogoś, to znaczy, że nie pisałem go na darmo. Będę wdzięczny za komentarze i uwagi, postaram się odpowiedzieć na wszystkie.

Źródło: habr.com

Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS | ProHoster