External Loader – generowanie pliku .stldr

Stosując zewnętrzną pamięć do przechowywania danych o grafikach czy dźwiękach zaczynamy od obsługi komunikacji z układem. Przychodzi jednak moment, kiedy chcielibyśmy zaprogramować pamięć za pomocą danych, które mamy przygotowane w pliku na komputerze. Możemy kupić uniwersalny programator pamięci i zastosować adapter do naszej płytki. Jeżeli jednak pamięć jest podłączona do mikrokontrolera STM32, możemy go zaprogramować za pomocą STM32CubeProgrammer-a i specjalnego programu ładującego – External Loader-a.
Wpis jest kontynuacją materiału przedstawionego w poprzednim artykule: „Obsługa pamięci SPI Flash„. Pokazana w nim obsługa pamięci SPI Flash posłuży nam teraz do stworzenia projektu External Loader-a i wygenerowania pliku .stldr odpowiadającego za programowanie zewnętrznej pamięci.
Czym jest External Loader
External Loader to specjalny wsad pozwalający nam na programowanie zewnętrznej pamięci podłączonej do mikrokontrolera. Słowo „specjalny” właściwie odnosi się nie tyle do samego programu, a do koncepcji jego wykorzystania. External Loader to tak naprawdę standardowy program napisany dla wybranego przez nas mikrokontrolera (obojętnie w jakim IDE, nie ważne czy używa bibliotek HAL-a, LL, czy jest napisany bezpośrednio za pomocą rejestrów). Musi jednak zawierać określone funkcjonalności i w odpowiedni sposób zostać skompilowany.
Odpowiednio przygotowany wsad (program) może następnie być wykorzystany w połączeniu z aplikacją STM32CubeProgrammer, który zaprogramuje naszą pamięć zewnętrzną przy pomocy ST-Link-a danymi, które umieścimy w pliku binarnym (z rozszerzeniem .bin). Procedura samego programowania jest zatem bardzo podobna, jak programowanie mikrokontrolera – w końcu to właściwie to samo.
Warto zauważyć, że External Loader jest wykorzystywany m.in. przy tworzeniu projektów z wykorzystaniem TouchGFX do wgrania na zewnętrzne pamięci Flash grafik do wyświetlacza.
Budowa projektu External Loader-a
Jak już wspomniałem, External Loader to w zasadzie „zwykły” program na STM32, który został w odpowiedni sposób przygotowany i skompilowany. Tworzymy zatem normalny projekt (w moim przypadku dla Nucleo-L476RG). Może to być ten sam projekt, który tworzyliśmy w poprzedniej części przy obsłudze pamięci SPI Flash. Następnie konfigurujemy interfejs SPI i pin GPIO do sterowania linią CS. Pełna konfiguracja SPI przedstawiona został poniżej.
Konfiguracja pinów będzie wyglądała jak na poniższej grafice. Warto zauważyć, że dodałem etykietę dla pinu do sterowania linią CS, jako Chip_Select.
Szczegóły tego etapu zostały przedstawione w poprzednim artykule: „Obsługa pamięci SPI Flash„. Mając tak skonfigurowany mikrokontroler, możemy wygenerować projekt (skrót Alt+K).
Do projektu dodajemy pliki „W25Q64_Flash.h” oraz „W25Q64_Flash.c” do obsługi pamięci Flash SPI W25Q64FV firmy Wibond. Następnie tworzymy trzy pliki potrzebne do prawidłowej pracy External Loadera. Przykłady tych plików (dla pamięci QSPI) możemy znaleźć w repozytorium GitHub firmy STMicroelectronics.
- Dev_Inf.h w folderze Core->Inc
- Dev_Inf.c w folderze Core->Src
- Loader_Src.c w folderze Core->Src
W pliku „Dev_Inf.h” dodajemy definicje stałych dla poszczególnych typów pamięci oraz strukturę zawierającą informację o pamięci, dla której External Loader jest przygotowany.
#define MCU_FLASH 1
#define NAND_FLASH 2
#define NOR_FLASH 3
#define SRAM 4
#define PSRAM 5
#define PC_CARD 6
#define SPI_FLASH 7
#define I2C_FLASH 8
#define SDRAM 9
#define I2C_EEPROM 10
#define SECTOR_NUM 10 // Max Number of Sector types
struct DeviceSectors {
unsigned long SectorNum; // Number of Sectors
unsigned long SectorSize; // Sector Size in Bytes
};
struct StorageInfo {
char DeviceName[100]; // Device Name and Description
unsigned short DeviceType; // Device Type: ONCHIP, EXT8BIT, EXT16BIT, ...
unsigned long DeviceStartAddress; // Default Device Start Address
unsigned long DeviceSize; // Total Size of Device
unsigned long PageSize; // Programming Page Size
unsigned char EraseValue; // Content of Erased Memory
struct DeviceSectors sectors[SECTOR_NUM];
};
W pliku „Dev_Inf.c” tworzymy zmienną typu struct StorageInfo i uzupełniamy ją wartościami odpowiadającymi parametrom pamięci. Co oznaczają poszczególny elementy, opisane zostało w komentarzu. W przypadku pamięci QSPI, która jest mapowana jako część pamięci mikrokontrolera, wartość adresu początkowego będzie inna niż 0 i będzie znajdowała się za pamięcią Flash uC. W przypadku interfejsu SPI przyjmujemy wartość 0. Warto dodać, że pierwsze pole struktury jest tablicą znaków i jest umieszczono typowo informacyjnie – to co wpiszemy w tym miejscu będzie się nam wyświetlało jako opis wsadu w STM32CubeProgrammer.
#if defined (__ICCARM__)
__root struct StorageInfo const StorageInfo = {
#else
struct StorageInfo const StorageInfo = {
#endif
"W25Q64FV_STM32L476RG_SPI", // Device Name + version number
SPI_FLASH, // Device Type
0x00000000, // Device Start Address
0x00800000, // Device Size in Bytes
0x00000100, // Programming Page Size
0xFF, // Initial Content of Erased Memory
// Specify Size and Address of Sectors
{ {
0x800, // Number of Sectors
0x1000 // Sector Size in Bytes
}, //Sector Size
{ 0x00000000, 0x00000000 }
}
};
Teraz czas na najważniejszą cześć programu, czyli plik „Loader_Src.c”. Przygotowałem wersję „minimum”, tzn. zaimplementowałem tylko niezbędne do prawidłowej pracy funkcje: Init, Write i SectorErase. Do uzyskania pełnej funkcjonalności można jeszcze dodać funkcje Read, Verify, MassErase oraz CheckSum. Szczegóły odnośnie ich działania można znaleźć w dokumentacji „UM2237” w rozdziale „Developing customized loaders for external memory” na stronie 28.
Na początku pliku „Loader_Src.c” umieszczamy definicje stałych oraz deklarację funkcji jako extern, abyśmy mogli ją wywołać ją z poziomu tego pliku. Poza tym dodajemy include dla plików, z których będziemy korzystali.
#include "spi.h"
#include "main.h"
#include "gpio.h"
#include "W25Q64_Flash.h"
#define LOADER_OK 0x1
#define LOADER_FAIL 0x0
extern void SystemClock_Config(void);
Następnie implementujmy niezbędne funkcje. Każda z nich będzie zwracała nam status powodzenia operacji: LOADER_OK lub błędu LOADER_FAIL.
Jak sama nazwa wskazuje, funkcja Init odpowiada za inicjalizację mikrokontrolera. Będzie ona uruchomiona jako pierwsza po wciśnięciu przycisku programowania w aplikacji STM32CubeProgrammer. Ze względu na inaczej zorganizowany proces kompilacji (o czym za chwilę), nie wywoła nam się funkcja main(), dlatego konfigurację zegarów, GPIO i SPI musimy umieścić tutaj. Ponadto musimy ustawić adres offset-u wektora przerwań, ponieważ nie będzie się on znajdował w standardowym miejscu w pamięci Flash, ale w pamięci RAM.
int Init(void) {
HAL_StatusTypeDef status;
SystemInit();
SCB->VTOR = 0x20000000 | 0x200;
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_SPI2_Init();
status = W25Q64_Init();
if(HAL_OK == status)
return LOADER_OK;
else
return LOADER_FAIL;
}
Funkcja Write odpowiada za zapisywanie danych na pamięci. Jako argumenty przyjmuje adres początkowy do zapisu, rozmiar oraz dane. W naszej bibliotece dla W25Q64 zaimplementowaliśmy funkcję, która programuje jedną stronę, czyli 256 bajtów. Tutaj możemy otrzymać znacznie więcej danych za jednym razem, dlatego musimy to obsłużyć. Obliczamy zatem ilość stron do programowania (uwzględniając za pomocą operacji modulo, że nie muszą to być pełne strony) i wywołujemy funkcję W25Q64_PageProgram określoną ilość razy, przekazując jej kolejne dane.
int Write(uint32_t Address, uint32_t Size, uint8_t* buffer) {
uint32_t i, number_of_pages, page_address;
uint8_t *page_buffer;
uint8_t modulo_flag;
HAL_StatusTypeDef status;
if((Size % PAGE_SIZE) == 0)
{
number_of_pages = Size/PAGE_SIZE;
modulo_flag = 0;
}
else
{
number_of_pages = Size/PAGE_SIZE + 1;
modulo_flag = 1;
}
page_address = Address;
page_buffer = buffer;
for(i=0; i<number_of_pages; i++)
{
status = W25Q64_PageProgram(page_address, page_buffer, PAGE_SIZE);
page_address += PAGE_SIZE;
page_buffer += PAGE_SIZE;
if(HAL_OK != status)
return LOADER_FAIL;
}
if(modulo_flag == 1)
{
status = W25Q64_PageProgram(page_address, page_buffer, Size % PAGE_SIZE);
if(HAL_OK != status)
return LOADER_FAIL;
}
return LOADER_OK;
}
Na koniec dodajemy funkcję SectorErase do czyszczenia sektorów w pamięci. Tutaj jako argumenty otrzymujemy adres pierwszego oraz ostatniego sektora, który ma być wykasowany. Oczywiście kasujemy też wszystkie sektory pomiędzy.
int SectorErase(uint32_t EraseStartAddress, uint32_t EraseEndAddress) {
HAL_StatusTypeDef status;
int i;
uint32_t erase_start_sector_nbr, erase_end_sector_nbr;
erase_start_sector_nbr = EraseStartAddress/SECTOR_SIZE;
erase_end_sector_nbr = EraseEndAddress/SECTOR_SIZE;
for(i = erase_start_sector_nbr; i <= erase_end_sector_nbr; i++)
{
status = W25Q64_SectorErase(i);
if(HAL_OK != status)
return LOADER_FAIL;
}
return LOADER_OK;
}
W zależności od zastosowanej pamięci, czasy kasowania pojedynczych sektorów przy ich większej liczbie może znacznie przekraczać czas potrzeby na wykasowanie całej pamięci za pomocą instrukcji MassErase. Jeżeli nie zależy nam na danych, które są w pozostałej części pamięci (wgrywamy całe potrzebne dane za jednym razem), możemy tutaj umieścić funkcję MassErase, która wyczyści nam całą pamięć. Robimy to taką metodą, ponieważ domyślnie przy operacji programowania zewnętrznej pamięci External Loader korzysta z funkcji SectorErase i to ona będzie wywoływana przez przesyłaniem danych.
int SectorErase(uint32_t EraseStartAddress, uint32_t EraseEndAddress) {
HAL_StatusTypeDef status;
status = W25Q64_ChipErase();
if(HAL_OK == status)
return LOADER_OK;
else
return LOADER_FAIL;
}
W ten sposób dodaliśmy niezbędne elementy programu. Teraz pora na skompilowanie projektu.
Generowanie pliku .stldr
Nie wspomniałem jeszcze o jednej ważnej kwestii. Plik potrzebny do zaprogramowania pamięci za pomocą STM32CubeProgrammer-a powinien mieć rozszerzenie „.stldr”. Jak taki plik wygenerować? Okazuje się, że to nie jest skomplikowane, a wręcz bardzo proste. Plik „.stldr” to tak na prawdę plik „.elf”. Inne rozszerzenie pozwala jedynie na odpowiednie wczytanie plików przez STM32CubeProgrammer.
W pierwszej kolejności powinniśmy dodać plik linkera, który odpowiednio zbuduje projekt. W zasadzie różnica w odniesieniu do standardowego linkera generowanego przez IDE polega na tym, że cały program umieszczony jest w pamięci RAM, dzięki czemu STM32CubeProgrammer będzie mógł wywołać funkcję za pomocą ST-Link-a. Gotowy plik dedykowany do External Loadera znajdziemy tutaj.
W pliku linkder.ld warto zmienić rozmiar pamięci RAM na taką, jaką mamy dostępną w naszym mikrokontrolerze (w przypadku STM32L476RG jest to 96 kB).
MEMORY
{
RAM (xrw) : ORIGIN = 0x20000004, LENGTH = 96K
}
Następnie powinniśmy zmienić w konfiguracji projektu nazwę linkera, z którego chcemy skorzystać przy budowaniu projektu. Zaznaczamy projekt, a następnie wybieramy zakładkę „Project->Properties”. Przechodzimy do „C/C++ Build->Settings”. W zakładce „Tool Settings” przechodzimy do „MCU GCC Linker -> General” i w okienku „Linker Script” zmieniamy nazwę pliku linkera na „linker.ld”. Dodatkowo odznaczamy pole przy opcji „Discard unused sections”.
Klikamy „Apply and Close” i kompilujemy projekt. Teraz przechodzimy do folderu z projektem, następnie do Debug i odnajdujemy plik z „.elf”. Zmieniamy rozszerzenie pliku na „.stldr”. Mamy gotowy plik External Loader-a.
Obsługa za pomocą STM32CubeProgrammer
Aby użyć plik „.stldr” za pomocą STM32CubeProgrammer-a, musimy go przenieść do odpowiedniego folderu. Kopiujemy zatem plik i przenosimy go do „C:\Program Files\STMicroelectronics\STM32Cube\STM32CubeProgrammer\bin\ExternalLoader”. Jest to domyślna ścieżka. Jeżeli zainstalowałeś STM32CubeProgrammer w innej lokalizacji, przejdź do niej i odnajdź właściwy folder. W folderze „\bin\ExternalLoader” jest już trochę plików z Loaderami dla zestawów ewaluacyjnych i różnych pamięci.
Jeżeli mamy skopiowany plik, uruchamiamy aplikację STM32CubeProgrammer. Przechodzimy do zakładki External Loader. Jeżeli poprawnie skonfigurowaliśmy strukturę „StorageInfo”, pojawią się informację o typie pamięci, pojemności oraz używanej płytce/układzie.
Zaznaczamy nasz plik. Jeżeli jeszcze nie podłączyłeś płytki Nucleo do komputera, teraz jest na to najwyższa pora.
Następnie klikamy „Connect” i przechodzimy do zakładki „Erasing & Programming”. Wybieramy plik z wsadem na pamięć Flash (ja wrzuciłem testowy plik z repozytorium STMicroelectronics). Następnie zmieniamy Adres startowy na 0x00000000 i rozpoczynamy procedurę programowania przyciskiem „Start Programming”.
Jeżeli procedura przebiegła pomyślnie, w konsoli powinna pojawić się informacja o kasowaniu danych z sektorów, na które będą wgrywane dane, oraz postępie programowania. Przy pliku 1 MB procedura może zająć ok 30 s. Teraz na naszej pamięci SPI Flash mamy już dane z pliku.
Warto wspomnieć, że jeżeli zaimplementowaliśmy funkcję „MassErase”, możemy wykasować całą pamięć korzystając z okna „Erase external memory” po prawej stronie. Możemy w nim także skasować dane z wybranych sektorów.
Podsumowanie
W artykule przedstawiłem, jak przygotować projekt External Loadera i poprawnie wygenerować plik „.stldr”. Za pomocą STM32CubeProgrammera możemy w łatwy sposób wgrać dane na pamięć i w każdej chwili je zmodyfikować, unikając uciążliwego wyjmowania pamięci lub jej wylutowywania i umieszczania w uniwersalnym programatorze. W podobny sposób możemy stworzyć plik „.stldr” dla dowolnego mikrokontrolera STM32 i dowolnej pamięci zewnętrznej. Od teraz brak miejsca w pamięci to nie problem.
Bardzo fajny artykuł, nie wiem tylko czemu nie chcecie zrobić poprawki funkcji zapisu, którą wcześniej wam wysłałem.
Tak jak pisałem w skasowanym komentarzu Stm32CubeProgrammer potrafi wywołać funkcję Write nie w granicy 256bajtów, wobec czego powyższa funkcja nie zadziała.
Właśnie zdarzyło mi się spotkać z takim problem.
Write zostało wywołane z adresem 0x152A4. W tym przypadku trzeba w pierwszym kroku zapisać 0xFF – 0xA6 danych i dopiero zapisywać pełne strony.
Dzięki za uwagi. Komentarz nie był skasowany, a czekał na zatwierdzenia. Ze względu na zdarzający się spam, którego nie wyłapuje filtr zatwierdzam komentarze ręcznie 🙂
Prawidłowy kod:
int Write(uint32_t Address, uint32_t Size, uint8_t* buffer)
{
#define PAGE_SIZE 256
uint32_t byteToWrite = 0;
while(Size)
{
if((Address % PAGE_SIZE) == 0)
{
if(Size > PAGE_SIZE)
byteToWrite = PAGE_SIZE;
else
byteToWrite = Size;
}
else
{
if(Size > PAGE_SIZE)
byteToWrite = PAGE_SIZE – (Address % PAGE_SIZE);
else
byteToWrite = Size;
}
pageProgram(Address, byteToWrite, buffer);
Address += byteToWrite;
buffer += byteToWrite;
Size -= byteToWrite;
}
return 1;
}
Witam,
W kodzie zapisu jest błąd.
number_of_pages = Size/PAGE_SIZE + 1;
ilość number_of_pages ma być zawsze Size/PAGE_SIZE.
Dodatkową stronę zapisuje warunek:
if(modulo_flag == 1)
{
pageProgram(page_address, Size % PAGE_SIZE, page_buffer);
}
Dodatkowo stm32Cubeprogrammer potrafi wywołać funkcję z adresem nie w granicy PAGE_SIZE np:
13:22:41:607 : run ap 0
13:22:41:607 : w ap 0 @0x20002360 : 0x000002A4 bytes, Data 0x00000000…
13:22:41:607 : W B1 in RAM @0x20002360 size 0x000002A4 : 0005ms
13:22:41:696 : halt ap 0
13:22:41:697 : r ap 0 reg 0 R0 0x00000001
13:22:41:697 : Wait W B2 in flash @0x00013B00 size 0x00001500: 0094ms
13:22:41:698 : w ap 0 reg 0 R0 0x00015000
13:22:41:698 : w ap 0 reg 1 R1 0x000002A4
13:22:41:699 : w ap 0 reg 2 R2 0x20002360
13:22:41:699 : w ap 0 reg 3 R3 0x00000001
13:22:41:699 : w ap 0 reg 4 R4 0x00000000
13:22:41:701 : w ap 0 reg 5 R5 0x00000000
13:22:41:702 : w ap 0 reg 6 R6 0x00000000
13:22:41:704 : w ap 0 reg 7 R7 0x00000000
13:22:41:704 : w ap 0 reg 8 R8 0x00000000
13:22:41:704 : w ap 0 reg 9 R9 0x00000000
13:22:41:704 : w ap 0 reg 10 R10 0x00000000
13:22:41:704 : w ap 0 reg 11 R11 0x00000000
13:22:41:705 : w ap 0 reg 12 R12 0x00000000
13:22:41:705 : w ap 0 reg 13 SP 0x00000000
13:22:41:712 : w ap 0 reg 14 LR 0x20000001
13:22:41:713 : w ap 0 reg 15 PC 0x20001C2D
13:22:41:713 : w ap 0 reg 16 xPSR 0x01000000
13:22:41:713 : w ap 0 reg 17 MSP 0x20002324
13:22:41:713 : w ap 0 reg 18 PSP 0x00000000
13:22:41:713 : run ap 0
13:22:41:713 : w ap 0 @0x20002604 : 0x000002A0 bytes, Data 0x00000000…
13:22:41:713 : W B2 in RAM @0x20003860 size 0x000002A0: 0015ms
13:22:41:731 : r ap 0 reg 0 R0 0x00000001
13:22:41:731 : Wait W B1 in Flash @0x00015000 size 0x000002A4: 0019ms
13:22:41:732 : w ap 0 reg 0 R0 0x000152A4
13:22:41:732 : w ap 0 reg 1 R1 0x000002A0
13:22:41:734 : w ap 0 reg 2 R2 0x20002604
13:22:41:736 : w ap 0 reg 3 R3 0x00000001
13:22:41:745 : w ap 0 reg 4 R4 0x00000000
13:22:41:745 : w ap 0 reg 5 R5 0x00000000
13:22:41:745 : w ap 0 reg 6 R6 0x00000000
13:22:41:745 : w ap 0 reg 7 R7 0x00000000
13:22:41:745 : w ap 0 reg 8 R8 0x00000000
13:22:41:745 : w ap 0 reg 9 R9 0x00000000
13:22:41:745 : w ap 0 reg 10 R10 0x00000000
13:22:41:745 : w ap 0 reg 11 R11 0x00000000
13:22:41:745 : w ap 0 reg 12 R12 0x00000000
13:22:41:745 : w ap 0 reg 13 SP 0x00000000
13:22:41:746 : w ap 0 reg 14 LR 0x20000001
13:22:41:746 : w ap 0 reg 15 PC 0x20001C2D
13:22:41:746 : w ap 0 reg 16 xPSR 0x01000000
13:22:41:746 : w ap 0 reg 17 MSP 0x20002324
13:22:41:746 : w ap 0 reg 18 PSP 0x00000000
Funkcję trzeba zmodyfikować żeby to obsłużyła.
Świetny artykuł. Czy planujesz w przyszłości zmienić SPI na QSPI? Nucleo L476 posiada taki interfejs. Od jakiegoś czasu z tym walcze jednak bezskutecznie.
Dzięki. Nie planowałem w najbliższym czasie artykułów z QSPI, ale pomyślę, może coś jednak przygotuję 🙂 W poradnikach ST są przykłady z QSPI, jeśli nie zaglądałeś to warto: https://www.youtube.com/watch?v=YFIvJVsvIsE.
znam ten poradnik, samo qspi dziala prawidlowo jednak external loader ma problem z odczytem pamieci.