Pamiętam gdy pierwszy raz chciałem użyć EEPROMU w STM32. Wszystko szło pięknie do momentu w którym chciałem faktycznie użyć tej pamięci. Okazało się, że… nie wszystkie STMki mają EEPROM! Pozbawione tego ficzera są zwłaszcza serie F. Co zobić w takiej sytuacji? Można użyć zewnętrznego układu na I2C/SPI lub… zaemulować EEPROM na wbudowanej pamięci FLASH. Chodź, pokażę Ci jak to zrobić.

Jak działa pamięć EEPROM?

Myślę, że w pierwszej kolejności należy wiedzieć czym jest pamięć EEPROM. Jest to pamięć nieulotna, czyli jej zawartość nie jest tracona po odłączeniu zasilania. Można z niej oczywiście czytać i nie ma to żadnego wpływu na dane zawarte w pamięci oraz ich żywotność. Możliwe jest też zapisywanie i to już od pojedynczego bajtu. Zaletą EEPROM’u jest również to, że można szybko zapisywać zarówno “zera” jak i “jedynki” co nie jest takie oczywiste w wypadku pamięci FLASH. Dostęp do każdego bajtu zarówno dla odczytu jak i zapisu jest swobodny.

W tym wpisie zajmuję się emulacją EEPROM na wewnętrznej pamięci FLASH mikrokontrolera. Jak działa pamięć FLASH? Działanie jest bardzo podobne do EEPROM’u z jedną “małą” różnicą. Kasowanie pamięci (czyli najczęściej zapis 0xFF w komórce) nie jest taki swobodny swobodny. Wykonuje się go większymi obszarami, które nazywane są sektorami lub stronami w zależności od literatury. Jest to tak zwane czyszczenie FLASH. Sektory te mogą mieć różne rozmiary jak np. 1 kB czy nawet 128 kB. Zapis we flash to tak naprawdę ustawienie samych “zer” pośród płotu “jedynek”.

Z tego zwględu, że pamięć FLASH wymaga specyficznego kasowania nie jest możliwe emulowanie zachowania EEPROM 1:1. Nie można przeznaczyć kilku bajtów pamięci FLASH na kilka zmiennych ponieważ nie jesteśmy w stanie “zapisać” jedynek. Przykład: napisanie 0xF0 wartością 0xFF w pamięci FLASH nie powiedzie się.

Jak wygląda emulacja EEPROM na pamięci FLASH?

Na szczęście ST dostarcza obszerną dokumentację jak emulacja EEPROM na pamięci FLASH powinna wyglądać. Dzisiaj omówię to na przykładzie STM32F103C8T6 znanego miedzy inymi z płytek BluePill oraz na STM32F401RE z zestawu Nucleo. 

Dla tych dwóch mikrokontrolerów ST przygotowało odpowiednie dokumentacje emulacji EEPROM:

Zasada jest taka sama dla obydwu rodzin. Omówię w skrócie działanie emulacji.

Reprezentacja danych

Dane we FLASH przechowywane są w 32-bitowych komórkach. Komórki te dzielone są na dwie części. Jedna to 16-bitowy wirtualny adres zmiennej w EEPROM. Druga część to wartość tej zmiennej. Dlaczego tak? Ponieważ każda aktualizacja zmiennej wpisuje jej nową wartość w kolejną, wolną komórkę FLASH. Nie nadpisujemy starej wartości.

Po co nam dwie strony pamięci?

Jeżeli każdą nową wartość zmiennej wpisujesz w nową komórkę, to kiedyś te komórki się skończą. Gdy do tego dojdzie, stara strona wyznaczona zostaje do transferu danych na nową stronę. Nie przenosimy wszystkich danych. Nie interesują nas dane historyczne zmiennych dlatego skanujemy od końca pełną stronę i przenosimy najnowsze wartości zmiennych do nowej strony. Po przeniesieniu się na nową stronę zostaje ona oznaczona jako aktywna, a stara będzie skasowana w całości (kasowanie całej strony FLASH).

Odczyt z emulowanego EEPROM

Aby odczytać zmienną o odpowiednim adresie wirtualnym trzeba przeglądnąć całą dotychczas zapisaną stronę. Za każdym razem, gdy spotkana zostanie zmienna o wymaganym przez nas adresie wirtualnym, zostaje zapamiętana jej wartość. Kolejne napotkanie tej samej zmiennej aktualizuje pamiętaną tymczasową wartość. Poszukiwania trwają dopóki nie natrafimy na skasowaną komórkę, czyli taką z adresem wirtualnym 0xFFFF. Stąd też zabronione jest użycie tego adresu wirtualnego. Ostatnia zapamiętana wartość jest zwracana jako ta, którą aktualnie przechowuje nasz EEPROM.

Zapis do emulowanego EEPROM

Zapis poszukuje pierwszej wolnej komórki w stronie pamięci. Jeżeli ją znajdzie to wpisuje podany adres wirtualny oraz wartość którą mu podamy. Jeżeli nie ma wolnych komórek na stronie, to rozpozczyna się procedura przeniesienia najnowszych kopii zmiennych z unikalnymi adresami wirtualnymi. Dopiero po tym wpisana zostanie nasza zmienna w pierwszą wolną komórkę już na nowej stronie. Stara będzie wykasowana. Strony wykorzystywane są w ten sposób naprzemiennie.

Przebieg aktualizacji danych

Zobrazowany cały proces wpisywania przykładowych danych oraz przenoszenia ich na nową stronę znajdziesz w dokumentacjach dla F1 (str. 8) i F4 (str. 11).

Biblioteka emulacji EEPROM

Czytanie dokumentacji i wymyślanie biblioteki dla poprawnego działania emulacji EEPROM może trochę zawrócić w głowie. Na szczęście ST przygotowało odpowiednią bibliotekę. Na dodatek działa ona od strzała z naszym ulubionym HAL’em. Cała biblioteka sprowadza się tylko do trzech funkcji co czyni ją niezamowicie prostą w użyciu. Pogimnastykować się natomiast można z jej konfiguracją w pliku eeprom.h. To tam znajduje się kompletna konfiguracja emulatora EEPROM. Pozwól, że najpierw omówię tę konfigurację. Definicje które nas interesują to:

  • #define PAGE_SIZE – definiuje rozmiar stron które zostaną użyte. Informacje tę znajdziesz w Reference Manual mokrokontrolera lub w bibliotekach HAL.
  • #define EEPROM_START_ADDRESS – od którego adresu w pamięci będzie znajdowała się emulacja EEPROM. Zalecam używać końcówki FLASH’a, aby nie kolidować z programem głównym. Adres ten będzie adresem pierwszej użytej strony FLASH.
  • #define PAGE0_BASE_ADDRESS 
    #define PAGE0_END_ADDRESS 
    #define PAGE0_ID – adresy początkowe i końcowe strony zerowej emulacji oraz ID pierwszej strony we FLASH. To samo jest dla PAGE1 EEPROM’u.
  • #define PAGE0
    #define PAGE1 – opis w nagłówku jest mylący i mówi o tym ile stron FLASH jest użyty dla stron EEPROM. Tak na prawdę ważna jest definicja PAGE1 która mówi o tym ile stron dalej znajduje się ona w stosunku do początku PAGE0. Chcąc użyć po 2 strony FLASH na jedną EEPROM, wpiszesz w PAGE1 liczbę 0x2.
  • #define NB_OF_VAR – liczba unikalnych zmiennych które chcemy przechowywać w emulowanej pamięci EEPROM. Jest to ważne z tego względu, że biblioeka potrzebuje tablicy adresów wirtualnych i trzeba ją wcześniej utworzyć w programie.

Funkcje biblioteczne

Jak już wspomniałem, do dyspozycji uzytkownika są 3 proste funkcje. Oto one:

  • uint16_t EE_Init(void) – inicjalizacja emulatora EEPROM. Sprawdza ona czy strony były wcześniej zapisane(po nagłówku) oraz czy trzeba je wykasować. Przywraca użyteczność po zaniku zasilania.
  • uint16_t EE_ReadVariable(uint16_t VirtAddress, uint16_t* Data) – Odczytanie zmiennej z EEPROM. Zmienna spod wirtualnego adresu VirtAddress trafi do pamięci spod wskaźnika Data.
  • uint16_t EE_WriteVariable(uint16_t VirtAddress, uint16_t Data) – Zapis do EEPROM. Zmienna pod wirtualnym adresem VirtAddress zostanie zapisana wartością Data.

Wszystkie te funkcje zwracają kody statusowe zgodne z HAL’em.

Prawda, że wyglądają na proste w użyciu? Czas podziałać z prawdziwymi układami.

Emulacja EEPROM na STM32F103C8T6

W pierwszej kolejności wziąłem popularnego BluePilla. Pozwól, że tym razem pominę konfigurację Cube’a i schemat. Nie są one istotne w tym ćwiczeniu. Skonfigurowałem sobie jedynie wbudowaną diodę oraz UART2 do przesyłania sobie zawartości emulowanego EEPROM na terminal.

Konfiguracja

Do poprawnej konfiguracji potrzebować będę Reference Manuala z mapą pamięci FLASH mikrokontrolera.

Interesuje nas Main memory. Mamy do dyspozycji 128 stron po 1 kB każda. Do testów mogę wykorzystać dwie ostatnie. Na ogół zalecałbym korzystanie właśnie z końcowych stron. 

W pliku nagłówkowym eeprom.h możesz zauważyć, że adresy każdej strony są już podefiniowane. Możnaby nawet nie patrzeć w dokumentacje, aby poprawnie skonfigurować lecz lepiej jest się upewnić, że nikt nie popełnił błędu w kodzie.

Rozmiar strony jest już zdefiniowany w bibliotece HAL. Można się jedynie upewnić czy ma ok 1 kB, czyli 0x400.

Chcemy wykorzystać dwie ostatnie strony, więc EEPROM_START_ADDRESS ustawiam na przedostatnią stronę, czyli ADDR_FLASH_PAGE_126.

Teraz adresy dwóch stron emulacji EEPROM oraz ilość wykorzystywanych stron. Można posłużyć się wcześniej podefiniowanymi adresami lub pobawić się ofsetami.

1
2
3
4
5
6
7
8
9
10
11
#define PAGE0_BASE_ADDRESS ((uint32_t)(EEPROM_START_ADDRESS + 0x0000))
#define PAGE0_END_ADDRESS ((uint32_t)(EEPROM_START_ADDRESS + (PAGE_SIZE - 1)))
#define PAGE0_ID ADDR_FLASH_PAGE_126

#define PAGE1_BASE_ADDRESS ((uint32_t)(EEPROM_START_ADDRESS + 0x400))
#define PAGE1_END_ADDRESS ((uint32_t)(EEPROM_START_ADDRESS + 0x400 + PAGE_SIZE - 1))
#define PAGE1_ID ADDR_FLASH_PAGE_127

/* Used Flash pages for EEPROM emulation */
#define PAGE0 ((uint16_t)0x0000)
#define PAGE1 ((uint16_t)0x0001)

Oraz ilość przechowywanych zmiennych.

1
#define NB_OF_VAR ((uint8_t)27)

Kod programu

Do działania biblioteki wymagane jest powołanie tablicy adresów wirtualnych.

1
uint16_t VirtAddVarTab[NB_OF_VAR];

Później tę tablicę wypełniam kolejnymi wartościami.

1
2
3
4
5
// Fill EEPROM variables addresses
for(VarIndex = 1; VarIndex <= NB_OF_VAR; VarIndex++)
{
VirtAddVarTab[VarIndex-1] = VarIndex;
}

Bardzo ważne jest, aby odblokować pamięć FLASH z poziomu programu zanim użyjemy funkcji inicjującej bibliotekę.

1
HAL_FLASH_Unlock();

Teraz można zainicjalizować EEPROM. 

1
2
3
4
if( EE_Init() != HAL_OK)
{
Error_Handler();
}

Od tej pory można używać EEPROM. W programie stworzyłem tablicę z przykładowymi danymi którą będę wpisywał do EEPROM. Zawiera ona napis “Mateusz Salamon msalamon.pl”. Składa się ona z 27 znaków, co uwględniłem wcześniej w konfiguracji.

W programie wpisuje i odczytuję te dane z EEPROM według schematu:

  1. Wpisanie danych do EEPROM. Każdy znak pod swój unikalny adres wirtualny.
  2. Odczyt i wypisanie na terminal
  3. Wpisane danych do EEPROM w odwrotnej kolejności.
  4. Odczyt i wypisanie na terminal. Napis powinien być w odwrotnej kolejności.
  5. Wpisanie danych do EEPROM. 
  6. Odczyt i wypisanie na terminal. Efekt taki sam jak w pkt. 2

Efekty z terminala widać na screenie.

Natomiast co się dzieje z pamięcią FLASH? Czego się spodziewam? Według dokumentacji emulacji EEPROM napis powinien trafić pod zmienne z adresami wirtualnymi 1-27 najpierw w normalnej kolejności, później wspak i na końcu znowu normalnie. Rezultat poniżej.

Dane oznaczone 1 są to dane z pierwszego zapisu. 2 to zapis odwrotny. 3 to ponowny zapis w normalnej kolejności. Przeanalizuj proszę te dane. Pierwsza komórka u góry z lewej to nagłówek strony. Jest on w stanie ERASED, więc można pisać do tej strony.

Dalej są dane. Pierwsze 16 bitów (4 cyfry) to wirtualny adres każdej ze zmiennych. Drugie 16 bitów do wartość, czyli znaki z napisu.

Emulacja EEPROM na STM32F401RE

Organizacja pamięci w mikroprocesorach może być różna. Z tego względu należy zawsze sprawdzać dokumentacje. W serii F4 możemy się lekko zaskoczyć gdyż pamięć FLASH wygląda tak:

Mamy tylko 8 sektorów z czego nie są one równe wielkościowo. Pamiętaj, że kasowanie FLASH odbywa się po całym sektorze. Smutno byłoby uzyć 256 kB FLASH’a dla kilku zmiennych EEPROM. Dlatego wykorzystam sektory 2 i 3. Konfiguracja:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* Define the size of the sectors to be used */
#define PAGE_SIZE (uint32_t)0x4000 /* Page size = 16KByte */

/* Device voltage range supposed to be [2.7V to 3.6V], the operation will
be done by word */

#define VOLTAGE_RANGE (uint8_t)VOLTAGE_RANGE_3

/* EEPROM start address in Flash */
#define EEPROM_START_ADDRESS ((uint32_t)0x08008000) /* EEPROM emulation start address:
from sector2 : after 16KByte of used
Flash memory */

/* Pages 0 and 1 base and end addresses */
#define PAGE0_BASE_ADDRESS ((uint32_t)(EEPROM_START_ADDRESS + 0x0000))
#define PAGE0_END_ADDRESS ((uint32_t)(EEPROM_START_ADDRESS + (PAGE_SIZE - 1)))
#define PAGE0_ID FLASH_SECTOR_2

#define PAGE1_BASE_ADDRESS ((uint32_t)(EEPROM_START_ADDRESS + 0x4000))
#define PAGE1_END_ADDRESS ((uint32_t)(EEPROM_START_ADDRESS + (2 * PAGE_SIZE - 1)))
#define PAGE1_ID FLASH_SECTOR_3

/* Used Flash pages for EEPROM emulation */
#define PAGE0 ((uint16_t)0x0000)
#define PAGE1 ((uint16_t)0x0001) /* Page nb between PAGE0_BASE_ADDRESS & PAGE1_BASE_ADDRESS*/

Ilość zmiennych tak samo jak dla F1 ustawiam na

1
2
3
/* Variables' number */

#define NB_OF_VAR ((uint8_t)27)

Kod w pliku main.c jest taki sam. Rezultat na terminalu jest identyczny jak dla F1, a w pamięci FLASH wygląda to podobnie z tym, że pod innym adresem.

Pokusiłem się o sprawdzenie ile czasu zajmuje zapisanie i odczytanie mojej tablicy znaków.

173 ms dla zapisu i 3,24 ms dla odczytu. Co ciekawe wartości te nie zmieniają się znacząco wraz ze wzrostem zapełnienia FLASH. Jest jeden moment, kiedy czas ten będzie bardzo duży. Jest to przeniesienie danych na nową stronę EEPROM oraz kasowanie sektorów FLASH.

Emulacja EEPROM przed programem głównym

Jak pewnie zauważyłeś zdefiniowałem emulację EEPROM w połowie FLASHA F401RE. Niechętnie chciałbym wykorzystywać połowę dostępnej pamięci na EEPROM gdybym definiował ją na dwóch ostatnich sektorach.

Z tego względu wpadłem na pomysł, aby EEPROM zrobić na pierwszych dwóch sektorach, a program główny przesunąć na sektor nr 2. Niestety wykonanie tego zabiegu powoduje jakiś błąd w działaniu programu i mikrokontroler się wywala… Pogrzebałem trochę w kodzie i znalazłem pewien problem przy funkcji weryfikującej czy strona jest wykasowana. Użyta jest ona w inicjalizacji na której wywala się program. Jest nawet założony wątek na forum ST odnośnie tego błędu.

Niestety moje próby naprawienia tego fragmentu nie pomogły. Być może problem jeszcze jest gdzieś indziej lub po prostu nie da się wykonać zamiany EEPROM z programem głównym.

Jedyne co mi przychodzi do głowy podczas pisania tego artykułu to rozdział projektu na bootloader i program główny. Bootloader byłby uruchamiany z adresu początkowego i wykonywałby skok do aplikacji głównej, która mieści się za dwoma kolejnymi, pustymi sektorami. Wtedy kawałek FLASH między botloaderem a programem głównym mógły być wykorzystany na emulację EEPROM 🙂

Podsumowanie

Czy warto uzywać emulacji EEPROM? Sądząc po tym, że kod od wielu lat ma łatwe do znalezienia błędy w kodzie to obstawiam, że mało kto go wykorzystuje. Działanie emulacji EEPROMU też nie jest idealne. Zabiera co najmniej dwa sektory (strony) pamięci FLASH.

Jak widziałeś w mikrokontrolerach serii F4 emulacja EEPROM może być bardzo uciążliwa ze względu na różne rozmiary sektorów pamięci.

Jest jeszcze jedna kwestia której nie poruszyłem. Żywotność tego rozwiązania. Pamieć FLASH nie jest demonem żywotności. Jedna komórka pamięci może wytrzymać tylko ok 10 tys. cykli zapisu. Nie jest to zbyt duża ilość zwłaszcza porównując ją z żywotnością EEPROM na poziomie około miliona cykli (pamięci AT24C32 od Atmel).

Powstaje więc pytanie – czy warto emulować EEPROM? Czasem warto, czasem nie 🙂 Na to należy sobie odpowiedzieć indywidualnie. Zależy to od projektu. Można skorzystać z takiego STM32, który ma już EEPROM w swojej strukturze. Można uzyć zewnętrznego układu. Rozwiązań jest co najmniej kilka. 

Warto jednak wiedzieć, że w sytuacjach ekstremalnych można skorzystać z pamięci FLASH jako emulowanego EEPROM. Działa to poprawnie więc dla rzadko zmienianych danych może być to całkiem niezły sposób.

Kody testowe wraz z bibliotekami znajdziesz na moim GitHUB’ie: F103C8, F401RE

Uważasz, że popełniłem gdzieś błąd? Masz jakiś ciekawy pomysł co można ulepszyć? Podziel się tym w komentarzu! Pamiętaj, że dyskusja ma być kulturalna i zgodna z zasadami języka polskiego.

kurs stm32

Wyniki konkursu

Ostatnio ogłosiłem mały konkurs. Sposród zgłoszeń wybrałem troje zwycięzców. Są nimi:

  1. Leoneq ;3  z komentarzem 
    “Hmm, ja bym najchętniej chciał zobaczyć artykuł o generowaniu obrazu VGA. Na arduino jest jakaś biblioteka, ale 120×60 pikseli to słabo, nie mówiąc o dwóch zajętych timerach. Na stm generowanie obrazu nie powinno być tak zajmujące, a dałoby baaardzo dużo możliwości dla nowych projektów 😉 Btw, obserwuję Twojego bloga od dłuższego czasu, i muszę powiedzieć że utrzymuje poziom. Ładnie napisane, konkretnie i ciekawie. Pozdrawiam 😉
  2. Mruczek  z komentarzem
    “Komunikacja! Ethernet – ten na dużych Nucleo, wykorzystanie ESP8266 jako WiFi, NRF24L01+, który jest fajny do zastosowań energooszczędnych z komunikacją bezprzewodową.”
  3. Mateusz z komentarzem
    “Witam 🙂 jestem uszczęśliwiony, że na blogu są już tematy, które mnie interesowały m.in. właśnie o wyświetlaczach czy alcelerometrze. Ale sam byłbym zainteresowany bardziej wykorzystaniem zarówno FPU jak i opcji ADC w „bardziej zaawansowanym” przetwarzaniu sygnałów. W temacie można by zawrzeć także proste procedury przetwarzania takie jak chociażby FFT do zmiany dziedziny czasu w dziedzinę częstotliwości, czy tez proste/bardziej złożone układy kondycjonowani sygnałów na takie przyjazne dla procesora, pomiar względem masy pozornej itp. Myśle, ze pomimo wszechobecnej cyfryzacji taki temat mógłby się przydać 🙂

Gratuluję zwycięzcom! Odezwijcie się do mnie na maila: mateusz@msalamon.pl w celu finalizacji dostarczenia nagród 🙂


4 Komentarze

dambo · 17/08/2019 o 15:05

Wow – 173ms na zapis? Myślałem, że to była zamiana kolejności w poprzednich wartościach – 173ms to kaaaaawał czasu. Dopisane do listy, żeby się tym pobawić jakoś dokładniej – jakbym coś ciekawego się dowiedział to dam znać.

    Mateusz Salamon · 17/08/2019 o 15:19

    Wychodzi ok 6,4 ms na jedną zmienną. Fakt, kawał czasu. Według dokumentacji ST dla F4 czas zapisu jednej zmiennej nie powinien przekroczyć 255 us przy zegarze systemowym 168 MHz. Pewnie jeszcze coś pokombinuję z tym i podzielę się wynikami w aktualizacji 🙂

dambo · 17/08/2019 o 08:52

Odczyt trwa ~5x dłużej niż zapis?

    Mateusz Salamon · 17/08/2019 o 09:23

    O kur! Zerknałemjeszcze raz w kod i miałem oznaczyć “doliny”, a nie “górki” na analizatorze. Już poprawiam 😀 dzięki za czujność! Faktycznie coś mi śmierdziało jak opisywałem wyniki 😛

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *

Serwis wykorzystuje pliki cookies. Korzystając ze strony wyrażasz zgodę na wykorzystywanie plików cookies. Więcej informacji znajdziesz na stronie Polityka Prywatności

The cookie settings on this website are set to "allow cookies" to give you the best browsing experience possible. If you continue to use this website without changing your cookie settings or you click "Accept" below then you are consenting to this.

Close