STM32 i DataFlash w praktyce

Komunikacja pomiędzy układami scalonymi w systemach mikroprocesorowych/mikrokontrolerowych cieszą się wciąż niesłabnącym zainteresowaniem. W artykule przedstawiamy komunikację mikrokontrolera z rodziny STM32F107 z szeregową pamięcią DataFlash.

Współczesne mikrokontrolery mają pamięć Flash o coraz większej pojemności. Popularność zyskują układy z wbudowanym 1 lub nawet 2 MB pamięci nieulotnej. Wydawać by się mogło, że stosowanie zewnętrznych pamięci typu DataFlash nie ma już żadnego sensu. Jest to jednak z dwóch powodów złudny wniosek.

Po pierwsze, niekiedy jednak nawet pojemność rzędu megabajtów jest niewystarczająca. Nie chodzi tutaj o wielkość programu. W zwykłych aplikacjach, bez rozbudowanych systemów operacyjnych, dość trudno jest znaleźć takie, które potrzebowałyby aż tyle pamięci programu. Konieczność stosowania pamięci zewnętrznej może zaistnieć wtedy, kiedy wymagane jest zbieranie i zapisywanie dużej ilości informacji, jak choćby z czujników. Innym przykładem może być urządzenie wyposażone w rozbudowany interfejs użytkownika z wyświetlaczem graficznym. Dodatkowa pamięć nieulotna może w takim przypadku przechowywać bitmapy będące elementami interfejsu.
Po drugie – cena układów. Mikrokontrolery z dużą pamięcią Flash są zazwyczaj wyposażone w wiele układów peryferyjnych, a to bezpośrednio wpływa na ich cenę. Gdy projektowane urządzenie nie będzie musiało wykonywać zbyt skomplikowanych czynności, bardziej opłacalne może okazać się zastosowanie tańszego, a przez to słabiej wyposażonego, mikrokontrolera oraz pamięci zewnętrznej do przechowywania danych.
Nie wgłębiając się już bardziej w czynniki wpływające na decyzję konstruktora o stosowaniu pamięci zewnętrznej, przejdziemy do opisu stosownej aplikacji z wykorzystaniem mikrokontrolera STM32 i szeregowej pamięci DataFlash.

Interfejs komunikacyjny

Wybór pamięci zewnętrznej padł na układ AT45DB321D firmy Atmel. Jest to 32-megabitowa pamięć szeregowa, dostępna w 8-wyprowadzeniowej obudowie typu SOIC8. Wymiana danych z układem pamięci odbywa się przez interfejs SPI, który może pracować z częstotliwością zegarową do 66 MHz. Pamięć do mikrokontrolera STM32 należy dołączyć jak zwykłe urządzenia pracujące z interfejsem SPI. Przykładowy schemat zamieszczono na rysunku 1.

 

Rys. 1. Schemat dołączenia układu AT45DB321D do mikrokontrolera STM32

Rys. 1. Schemat dołączenia układu AT45DB321D do mikrokontrolera STM32

 

Układ AT45DB321D może komunikować się z MCU w trybach Mode 0 lub Mode 3. Szablony komunikacji w obydwu trybach przedstawiono na rysunku 2. W Mode 0 linia zegarowa SCK w stanie spoczynkowym znajduje się w na poziomie niskim, natomiast bity zatrzaskiwane są na zboczach narastających. W drugim trybie (Mode 3), gdy dane nie są przesyłane, SCK znajduje się na poziomie wysokim, poszczególne bity są jednak zapisywane również na zboczach narastających.

 

Rys. 2. Obsługiwane przez pamięć AT45DB321D tryby pracy magistrali SPI

Rys. 2. Obsługiwane przez pamięć AT45DB321D tryby pracy magistrali SPI

 

Sterownik magistrali SPI wbudowany w mikrokontrolery STM32 można ustawić do pracy w obydwu trybach. Przykładowa aplikacja będzie wykorzystywała Mode 0. Kod konfigurujący MCU do pracy w tym trybie przedstawiono na listingu 1 wraz z funkcją dataflashInit(), która inicjuje niezbędne peryferia.

 

List. 1. Funkcja inicjująca niezbędne do komunikacji z pamięcią peryferia

 

Przedstawiony kod jest wyraźnie uporządkowany w trzy bloki, ich zadaniem jest ustawienie parametrów odpowiednio: kontrolera przerwań NVIC, wykorzystywanych wyprowadzeń oraz samego kontrolera magistrali SPI. Użyto kontroler SPI1 oraz jego domyślne wyprowadzenia:

  • PA5 – SCK,
  • PA6 – MISO,
  • PA7 – MOSI.

Sygnał CS (Chip Select) jest dostarczany programowo poprzez wyprowadzenie PA4. Komunikacja będzie odbywała się w trybie full dupleks, a mikrokontroler będzie oczywiście układem nadrzędnym (Master). Zgodnie z akceptowanym przez układ pamięci formatem ramki danych, jej długość jest ustalana na 8 bitów, przy czym pierwszy na magistralę będzie wystawiany bit najbardziej znaczący (MSB).
Ustalanie trybu pracy SPI (czyli w przypadku układu AT45DB321D będzie to wybór pomiędzy Mode 0, a Mode 3) odbywa się przez wypełnienie pól SPI_CPOL i SPI_CPHA struktury inicjującej. Ostatnim parametrem jest wybranie prędkości przesyłania danych. Maksymalna szybkość komunikacji wynosi dla rodziny układów STM32F107 18 Mb/s. Kontroler SPI jest podłączony do wewnętrznej magistrali APB2, która może pracować z maksymalną częstotliwością 72 MHz. Jeśli właśnie taka jest częstotliwość sygnału taktującego APB2, to preskaler kontrolera SPI musi być ustawiony na 4 (72 MHz/4 = 18 MHz).

Właściwości pamięci

Producent deklaruje, że minimalna liczba cykli zapisu układu AT45DB321D wynosi co najmniej 100000, a dane przechowywane mogą być przez 20 lat. 100000 cykli zapisu może wydawać się dość dużą liczbą, ale jeśli aplikacja będzie zbyt często dokonywać zapisu, można się spodziewać, że dopuszczalny limit zostanie szybko wyczerpany i pamięć nie będzie się nadawać do pracy. Aby nie doprowadzić do takiej sytuacji należy unikać cyklicznych lub niekontrolowanych zapisów, np. wyzwalanych z funkcji obsługi jakiegoś przerwania.
Jak już wyżej wspomniano, rozmiar wykorzystanej pamięci to 32 Mb. Cała przestrzeń jest podzielona na strony i bloki. Rozmiar strony pamięci może wynosić 512 lub 528 bajtów (domyślnie), natomiast proces samego kasowania jest dość elastyczny, ponieważ pozwala na wymazanie całej strony, bloku (4 kB), sektora (64 kB) lub tez całego układu (32 Mb).

 

Rys. 3. Schemat blokowy pamięci DataFlash AT45DB321D

Rys. 3. Schemat blokowy pamięci DataFlash AT45DB321D

 

Dostęp do pamięci może odbywać się bezpośrednio, lub z użyciem jednego z dwóch buforów. Schemat blokowy ilustrujący relacje pomiędzy buforami, a interfejsem I/O przedstawiono na rysunku 3.
Układ AT45DB321D obsługuje wiele wariacji zapisu i odczytu. Cześć z nich, która wydała się najważniejsza lub najciekawsza została omówiona poniżej.

 

Rys. 4. Budowa rejestru statusu w układzie AT45DB321D

Rys. 4. Budowa rejestru statusu w układzie AT45DB321D

 

Odczyt rejestru statusu

Najłatwiejszym, ale zarazem często bardzo istotnym, etapem komunikacji z układem pamięci AT45DB321D jest odczytanie zawartości rejestru statusu. Na rysunku 4 zamieszczono budowę tego rejestru, natomiast funkcję odczytującą jego zawartość przestawiono na listingu 2. Zawartość rejestru statusu jest wystawiana na magistralę po wysłaniu do układu pamięci bajta o wartości 0xD7, według schematu z rysunku 5.

 

Rys. 5. Odczytywanie zawartości rejestru statusu układu pamięci

Rys. 5. Odczytywanie zawartości rejestru statusu układu pamięci

 

List. 2. Funkcja odczytująca rejestr statusu AT45DB321D

 

Ponieważ do wymiany danych zastosowano przerwania, to „sztywne” blokowanie wykonywania programu w oczekiwaniu na zakończenie komunikacji byłoby niemądre. Z tego powodu wykorzystano nieco bardziej skomplikowany mechanizm. Jak będzie można zauważyć dalej, każda funkcja obsługi pamięci DataFlash ma na początku pętlę while(), która będzie się wykonywać dopóki, dopóty zmienna SPI_InUse będzie miała wartość niezerową (prawdziwą w sensie logicznym). Dopiero po niespełnieniu tego warunku program przejdzie do kolejnych instrukcji. Zaraz poniżej wymienionej pętli zmienna SPI_InUse jest z powrotem ustawiana na wartość niezerową (czyli typu PRAWDA).

 

List. 3. Obsługa przerwania od kontrolera SPI

 

Wspomniana wyżej niezerowa wartość zmiennej SPI_InUse wynosząca szesnastkowo 0x11 może wydawać się zupełnie bez sensu. Uzasadnienie znajdujemy w funkcji obsługi przerwania od kontrolera SPI przedstawionej na listingu 3. W części nadawczej, gdy wysłany zostanie ostatni bajt nasza flaga SPI_InUse ma kasowany najmłodszy bit (stąd maska 0x01). W części odbiorczej kasowany jest natomiast bit czwarty (maska 0x10). Te wartości mogą być ustalane dowolnie. Celowo tutaj nie zastosowano definicji w miejsce „magicznych” liczb, ponieważ w surowej postaci łatwiej zrozumieć, co się właściwie dzieje. Przedstawiony wyżej mechanizm to nic innego, jak uproszczona wersja zastosowania muteksów.
Zatrzymajmy się na chwile przy muteksach, aby wyjaśnić, co to takiego jest i po co się je stosuje. Każdy zasób, czy to będzie konkretny fragment pamięci, czy też jakieś urządzenie peryferyjne, musi być w danej chwili wykorzystywany jednoznacznie. Wyobraźmy sobie sytuację, w której aplikacja wykorzystuje jakieś urządzenie peryferyjne, niech będzie to kontroler SPI. Jeśli nie chcemy blokować wykonywania programu na czas komunikacji, to może wystąpić sytuacja, w której mikrokontroler będzie próbował uzyskać dostęp do kontrolera SPI w czasie, kiedy poprzedni fragment wymiany danych nie został jeszcze zakończony. Najpewniej spowoduje to wystąpienie błędu lub nie wysłania danych.
Rozwiązanie takiego konfliktu jest łatwe. Narażonemu na hazardy zasobowi przyporządkowuje się obiekt (zmienną). Owa zmienna, którą można już nazwać muteksem, może przyjmować dwie wartości, dajmy na to zerową (fałsz) i niezerową (prawda). W założeniach projektowych przyjmujemy, że zasób jest wolny, czyli może być wykorzystany, tylko wtedy, kiedy wartość muteksa będzie fałszywa, czyli zerowa. Takie założenie jest oczywiście dowolne, można by przyjąć odwrotną logikę.
Programista, który będzie chciał skorzystać z chronionego zasobu musi najpierw sprawdzić jaka jest wartość muteksa. Jeśli jest ona różna od zera, to należy poczekać, aż muteks zostanie „oddany”, co będzie równoznaczne z przypisaniem mu wartości zerowej. Tylko w takiej sytuacji można użyć zasobu, pamiętając o tym, że zaraz na początku należy „zabrać” muteks, a po wykonaniu żądanych operacji „oddać” go.

 

List. 4. Funkcja main() programu odczytującego rejestr statusu układu pamięci

 

Wracając do obsługi pamięci – główną funkcję main() programu zamieszczono na listingu 4. Po wywołaniu funkcji odczytu rejestru statusu następuje oczekiwanie na zwolnienie muteksa. Gdyby program był bardziej rozbudowany, można by w tym miejscu wykonać inne operacje, a do skopiowania odebranej danej przystąpić później, już po zwolnieniu zasobu, w tym przypadku kontrolera SPI. Powód, dla którego odczytywany jest drugi bajt bufora odbiorczego RxBuf[] wynika wprost ze sposobu działania interfejsu SPI, co zobrazowano na rysunku 6. Rejestr przesuwny jest z jednej strony zapełniany bitami przychodzącymi po SPI, a drugiej strony poszczególne bity są wysyłane. Ponieważ przerwania od bufora nadawczego i odbiorczego są włączane w tym samym czasie (patrz funkcja readStatusRegister z listingu 2), to pierwsze przerwanie od części odbiorczej zostanie wygenerowane już po wysłaniu bajta komendy 0xD7. Ten pierwszy odebrany bajt jest nieistotny, dopiero drugie przerwanie odczytuje właściwą daną.

 

Rys. 6. Schemat działania interfejsu SPI

Rys. 6. Schemat działania interfejsu SPI

 

Kasowanie strony

Rozkaz kasowania całej wybranej strony ma wartość 0x81, a następujących po nim bajtach adresowych przesyłany jest adres strony według schematu z rysunku 7. Pierwszy bajt adresowy zawiera siedem bitów adresu, natomiast dugi sześć. Pozostałe pola, łącznie z całym trzecim bajtem adresowym są bez znaczenia. Proces kasowania strony rozpoczyna po przejściu linii CS w stan wysoki. Zgodnie z informacjami wynikającymi z noty katalogowej układu AT45DB321D maksymalny czas kasowania strony wynosi 35 ms, a typowo 15 ms. Zakończenie operacji kasowania jest również sygnalizowane na linii BUSY układu oraz przez zawartość rejestru statusu.

 

Rys. 7. Kasowanie wybranej strony w pamięci

Rys. 7. Kasowanie wybranej strony w pamięci

 

List. 5. Ciało funkcji kasującej zawartość strony

 

Na listingu 5 zamieszczono funkcję kasującą stronę w pamięci erasePage(). Określenie zajętości układu rozwiązano poprzez sprawdzanie zawartości rejestru statusu. Jednak aby nie wstrzymywać pracy całego systemu mikroprocesorowego na niekrótki przecież czas kilkudziesięciu milisekund, użyty został timer SysTick. Funkcja erasePage() tak konfiguruje timer, żeby ten po upływie 35 ms wygenerował przerwanie. Obsługa przerwania, zamieszczona na listingu 6, oddaje muteks oraz wyłącza przerwanie.

 

List. 6. Funkcja obsługi timera SysTick

 

Rys. 8. Ciągły odczyt pamięci bez użycia bufora

Rys. 8. Ciągły odczyt pamięci bez użycia bufora

 

Ciągły odczyt bez użycia bufora

Zasadę działania ciągłego odczytu pamięci bez użycia bufora przedstawiono na rysunku 8. Z rysunku wynika, należy podać adres początkowy bajta w stronie, a odczyt następuje już przez całą przestrzeń adresową. Po osiągnięciu ostatniego bajta w ostatniej stronie układ pamięci automatycznie rozpoczyna odczyt od początku pierwszej strony. Jak już wspomniano, domyślny rozmiar strony wynosi 528 bajtów.

 

Rys. 9. Adresowanie pierwszego do odczytu bajtu w stronie

Rys. 9. Adresowanie pierwszego do odczytu bajtu w stronie

 

Dla tego przypadku adresowanie jest następujące. W pierwszej kolejności należy wysłać do układu pamięci bajt komendy, dla ciągłego odczytu bez bufora będzie to wartość 0xE8, a zaraz po nim trzy bajty adresowe. Podział bajtów adresowych na adres strony oraz na adres bajta w stronie zamieszczono na rysunku 9, natomiast całą funkcję continousRead(), która inicjuje ciągły odczyt przedstawia listing 7. W argumentach do funkcji należy przekazać adres strony, adres bajta w stronie oraz liczbę bajtów, jakie mają zostać odczytane.

 

List. 7. Funkcja rozpoczynająca ciągły odczyt pamięci

 

Bufor odbiorczy RxBuf[] powinien być kopiowany dopiero od bajta o indeksie 8, a to dlatego, że pomiędzy ostatnim bajtem adresowym wysłanym do układu pamięci, a pierwszym odebranym bajtem z danymi muszą wystąpić cztery bajty nieznaczące. Zatem mamy „do przeskoczenia”: komendę (czyli pierwszy bajt), trzy bajty adresowe oraz cztery bajty nieznaczące.

Zapis do bufora

Pamięć AT45DB321D ma wbudowane dwa bufory SRAM o rozmiarze 528/512 bajtów każdy. Poniżej przedstawiono sposób zapisu danych do jednego z nich. Zapis rozpoczyna się od wysłania do układu pamięci odpowiedniego rozkazu, dla pierwszego bufora będzie to kod 0x84, a dla drugiego 0x87. Po kodzie rozkazu mikrokontroler powinien wysłać trzy bajty adresowe, które, jeśli zinterpretować je jako jeden ciąg bitów, zawierają na początku 14 bitów nieznaczących, a następnie 10 bitów identyfikujących bajt, od którego będzie następował zapis bufora. Po bajtach adresowych można już przesyłać dane przeznaczone do zapisu do bufora. Dane będą zapisywane do czasu, aż na linii chip select (CS) nie pojawi się zbocze narastające. Warto również zaznaczyć, że po osiągnięciu końca bufora, tj, po zapisie jego ostatniego bajta, proces rozpoczyna się od początku bufora. Nieco więcej światła na przedstawioną wyżej komunikację z pewnością rzuci rysunek 10.

 

Rys. 10. Zapis do bufora SRAM pamięci AT45DB321D

Rys. 10. Zapis do bufora SRAM pamięci AT45DB321D

 

List. 8. Ciało funkcji zapisującej bufor SRAM pamięci zewnętrznej

 

Funkcję realizującą zapis do bufora przedstawiono na listingu 8. W argumentach do funkcji należy przesłać rozkaz, do którego bufora zapis ma być przeprowadzony (0x84 lub 0x87) oraz liczbę zapisywanych bajtów. Globalny bufor nadawczy musi być wypełniony przez wywołaniem funkcji wysyłające dane dla bufora. Podobnie, jak było poprzednio, również funkcja bufferWrite(), zabiera muteks dla kontrolera SPI, blokując w ten sposób dostęp do kontrolera SPI. Jeśli dalej w programie zaistnieje potrzeba skorzystania z SPI, to w pierwszej kolejności należy sprawdzić, czy muteks został oddany, co będzie jednoznaczne z tym, że kontroler SPI jest gotowy do ponownego użycia.

Zapis bufora do pamięci

Gdy żądane operacje zapisu do bufora zostaną już przeprowadzone, należałoby jego zawartość zapisać do pamięci nieulotnej. Możliwe są dwa sposoby zapisu. Pierwszy polega na zapisie zawartości bufora bez kasowania docelowej strony w pamięci, natomiast drugi najpierw wymaże zawartość strony, a dopiero po tym wykonany będzie zapis danych z bufora. Przedstawimy ten drugi sposób.
Komenda zapisu bufora do pamięci to odpowiednio na bufora 1 i bufora 2: 0x83 oraz 0x86. Po kodzie komendy muszą zostać wysłane trzy bajty adresowe, określające, która strona w pamięci będzie zapisywana. Trzynaście bitów adresowych powinno być rozmieszczone w wysyłanych bajtach analogicznie do adresowania strony przeznaczonej do skasowania (patrz rysunek 7).

 

List. 9. Funkcja wyzwalająca zapis bufora do pamięci nieulotnej

 

Kasowanie, a następnie zapisywanie strony rozpoczyna się po wystąpieniu narastającego zbocza na linii CS. Podobnie, jak miało to miejsce w przypadku kasowania strony, czas wymagany, na wykonie całej operacji nie jest krótki i może wynosić od 17, do 40 ms. Problem oczekiwania na zakończenie operacji kasowania i programowania strony rozwiązano w takim sam sposób, jak przy poleceniu kasowania strony. Do odmierzania czasu użyty został systemowy timer SysTick. Ciało funkcji zapisującej bufor do pamięci przedstawiono na listingu 9. Jest to kod analogiczny, do funkcji kasowania strony, różnica, to oczywiście wartość pierwszego bajta (czyli komendy rozkazu), pozostały schemat komunikacji jest taki sam. Jedynie timer SysTick jest konfigurowany do wygenerowania przerwania po upływie dłuższego czasu – po 40 ms.
Krzysztof Paprocki

Autor: