Wstęp
Poradniki pomagające w tworzeniu oprogramowania na mikrokontrolery skupiają się głównie na wykorzystaniu peryferiów samego mikrokontrolera, co jest zrozumiałe, ale niestety rzadko skncentrują się na części programu, która wykonuje się przed funkcją main.
Poniższy poradnik ma na celu przybliżenie tej części programu na podstawie GNU ARM toolchain oraz mikrokontrolera stm32F334 z rdzeniem cortex-m4.
Używane narzędzia:
- GNU Arm Embedded Toolchain 7-2018-q2-update
https://developer.arm.com/open-source/gnu-toolchain/gnu-rm/downloads
- OpenOCD
- cygwin (z make)
- puTTy
- sterowniki wymagane przez STLINK
Zainstalować powyższe programy i dodać do zmiennej środowiskowej Path ścieżkę do folderu bin dla toolchain’a (dla naszej wygody, inaczej za każdym razem trzeba podawać relatywną ścieżkę do narzędzi).
1. Startup
Od czego zacząć pisanie kodu? Od poszukiwania informacji na temat jak producent rdzenia (w tym wypadku ARM) przewidział startup procesora:
https://www.keil.com/pack/doc/CMSIS/Core/html/startup_s_pg.html
Nasz kod zaczyna się od adresu 0x00000000 gdzie znajduje się wartość stack pointera, a następnie program zaczyna się pod adresem 0x00000004 gdzie znajduje się początek vector table, której pierwszym elementem jest pointer na funkcję „Reset_Handler”.
Jest możliwość zmiany adresu pamięci z jakiego nasz program się uruchamia. Jest to możliwe za pomocą fizycznej ingerencji w piny oznaczone jako BOOT0 i nBOOT1. Co ciekawe, możemy wystartować program z „System memory „ gdzie znajduje się „Embedded boot loader „ który jest wgrywany podczas produkcji układu i nie jest możliwy do modyfikacji.
Więcej informacji o Embedded boot loader’a można znaleźć pod linkiem:
https://www.st.com/content/ccc/resource/technical/document/application_note/b9/9b/16/3a/12/1e/40/0c/CD00167594.pdf/files/CD00167594.pdf/jcr:content/translations/en.CD00167594.pdf
Możemy teraz przystąpić do pisania własnego kodu startup.S. Czym jest ten plik? Jest to plik napisany w języku asembly zwanym u nas assembleerem. Język assembly składa się z dwóch zestawów instrukcji: ARM i thumb (thumb-2). Więcej na ten temat można poczytać w tym wątku:
https://stackoverflow.com/questions/28669905/what-is-the-difference-between-the-arm-thumb-and-thumb-2-instruction-encodings
W naszym przypadku używamy zestawu instrukcji thumb-2:
http://infocenter.arm.com/help/topic/com.arm.doc.qrc0001m/QRC0001_UAL.pdf
1. dyrektywa .global powoduje że symbol _start jest widoczna dla linkera (ld)
2.dyrektywa .thumb_func mówi assemblerowi (as) że następny symbol wskazuje na instrukcje thumb
3. adres stack point’a, którego wartość znajdzie się pod adresem 0x08000000 flash. Aktualnie zostanie ustawiony na koniec pamięci SRAM, który możemy odczytać z dokumentacji mikrokontrolera.
4. vector reset, który znajdzie się pod adresem 0x08000004 flash. Zawartość tego adresu zawiera adres na funkcję reset:
5. Branch – przeskoczenie do funkcji o etykiecie (adresie) bez wpisywania adresu do lr (link register).
6. Kod nigdy nie powinien się tutaj znaleźć.
7. Funkcja z nieskończoną pętlą.
2. Skrypt Linkera
Jeśli mamy już nasz startup to możemy zacząć pisać nasz skrypt linkera. Poniższy skrypt opisuje jaki kod (i w jakiej kolejności) będzie trafiał do poszczególnych sekcji oraz dostarczy nam informacje o początkach i końcach poszczególnych sekcji.
1. MEMORY przydziela nazwy do przestrzeni pamięci.
2. Początek pamięci flash oraz długość. Informacja o adresie w dokumentacji
3. Początek pamięci oraz długość Ram. Ponownie informacja o adresie w dokumentacji
4. sekcja .text w tej sekcji znajduje się kod wykonywalny.
5. adres początku sekcji. Możemy również ustawić adres sekcji ręcznie poprzez . = 0x80000;
6. Wrzucenie wszystkich text z obiektów do tej sekcji
7 i 8. Wrzucenie konkretnych text z obiektów do tej przestrzeni. Trzeba mieć na uwadze, że zamiana kolejności obiektów spowoduje, że kod nie będzie działać poprawnie!
9. Adres końca sekcji
10. Wskazanie na jaki adres będzie leciała dana sekcja.
11. Sekcja .rodata zawiera wszystkie zmienne stałe.
12.Sekcja .bss (block started by symbol) zawiera wszystkie zmienne statyczne nie zainicjalizowane.
13. Sekcja .data zawiera wszystkie zmienne statyczne zainicjalizowane. Zapis „AT (__rodata_end__)” Oznacza tyle, że sekcja chociaż znajdować się będzie w SRAM to na początku fizycznie będzie znajdować się w flash i trzeba ją następnie przekopiować w cstartup do ramu.
3. cstartup
Jeśli mamy już nasz skrypt linkera to możemy zacząć pisać nasz własny crt0. Jest to kawałek kodu wykonywany przed funkcją main, która zapewnia skopiowanie wszelakich statycznych danych z pamięci nieulotnej do ulotnej (w przypadku statycznych niezainicjalizowane zmiennych, wyzerowanie ich).
Tak więc tak wygląda nasz _cstartup:
Jest to kod napisany w C, jednak bez żadnych przeszkód możemy go napisać w asm.
Zmienne globalne zaczynające się od extern są zmiennymi zawierające informację o adresach początku i końca sekcji, które będą szczegółowiej opisane w części poświęconej linkerowi. Powyższy kod zeruje przestrzeń .bss gdzie znajdują się niezainicjalizowane zmienne statyczne, gdy do sekcji .data zostają przeniesione z pamięci nieulotnej wszystkie zainicjalizowane zmienne statyczne.
Ważne wspomnieć, że jeśli chcemy by inne sekcje również znajdowały się w RAM (np. sekcja z kodem, który ma się wykonywać z RAM’u), również tutaj muszą zostać przekopiowane.
4. Main
Gdy już mamy napisane wszystkie elementy związane z startup'em możemy przystąpić do pisania naszej funkcji main:
Adres RCCBASE oraz GPIOBASE możemy odczytać z dokumentacji z opisem za co poszczególne rejestry odpowiadają. Komentarze w kodzie są wystarczając i ten kawałek kodu nie wymaga dłuższego omówienia. Możemy teraz przejść do omówienia sposobu budowania projektu.
5. Makefile
To co powyżej widzimy to plik makefile, który zawiera opis budowania naszego projektu. Sposób budowania jest specjalnie uproszczony by w dość przejrzysty sposób wytłumaczyć na czym polega proces budowania binarki.
Zaczynamy od kompilatora gcc. Jest to program kombajn, który wiele może zrobić za naszymi plecami, ale jego głównym założeniem jest kompilacja plików napisanych w C do plików w języku asm. W tym wypadku zachodzi niejawne użycie programu as (assembler), który tłumaczy assembly na język maszynowy.
Opis poszczególnych flag użytych podczas wywołania kompilatora i as (niejawnie):
-Wall Wyświetla wszystkie warningi, które zostały wykryte w kodzie.
-g zamieszcza w outpucie symbole debugowe.
-c kompilowanie i asemblowanie plików źródłowych bez ich linkowania.
-o file nazwa pliku wyjściowego
-march Nazwa architektury pod którą budujemy projektu
Gdy już uzyskamy pliki .o możemy przystąpić do linkowania za pomocą ld z flagą -T która powoduje, że linker używa naszego skryptu.
Kolejnymi dwoma narzędziami są objdump oraz nm, które mogą nam pomóc w lepszym zrozumieniu naszego kodu oraz są wielce pomocne podczas poszukiwania pewnych rodzajów błędów w kodzie.
Na samym końcu używamy objcopy do translacji pliku .elf na plik binarny.
Opis wszystkich opcji gcc:
https://gcc.gnu.org/onlinedocs/gcc/Option-Summary.html
Gdy posiadamy już nasz plik binarny oraz używamy ST Link’a, który zgłasza się jako mass storage device po podpięciu pod USB, możemy przerzucić bezpośrednio wygenerowaną binarkę do ST Link’a a flashowanie zacznie się automatycznie. Nasz układ zaczyna mrugać LED!
Efekt końcowy:
6. Debugowanie
Ważne! Kompilator musi budować z flagą -g !
flashujemu nasz układ poprzez openocd:
$ openocd -f ../OpenOCD/0.10.0-9-20181016-1725/scripts/board/stm32f334discovery.cfg -c init -c "reset halt" -c "flash write_image erase blink.elf" -c "verify_image blink.elf" -c reset -c shutdown
następnie odpalamy openocd z plikiem konfiguracyjnym dla naszego boarda:
openocd -f ../OpenOCD/0.10.0-9-20181016-1725/scripts/board/st_nucleo_f3.cfg
następnie odpalamy puTTy i łaczymy pod adres 127.0.0.1 na porcie 4444 poprzez telnet
jeśli coś nie działa musimy odblokować clienta telnet:
https://www.lifewire.com/what-is-telnet-2626026
Gdy już odpaliliśmy puTTy to wpisujemy:
reset halt
odpalamy nowy terminal i przechodzimy tam gdzie zbudowaliśmy projekt. Odpalamy teraz gdb z argumentem do pliku .elf:
arm-none-eabi-gdb blink.elf
W terminalu wklepujemy taką o to sekwencję:
target remote localhost:3333
monitor reset halt
load
Gratuluje, możemy debugować. Wszelakie komendy potrzebne do debuggowania z terminalu można znaleźć w sieci. Niestety nie ma możliwości używania gdbtui pod windowsem
Powyższe przykład służy jedynie do uświadomienia jak wiele dzieje się jeszcze przed uruchomieniem funkcji „main”. Nie jest to rozwiązanie, które można stosować w poważnych projektach, ale mam nadzieję że posłuży jako pewien punkt podparcia w poszukiwaniu dalszej wiedzy na temat rozpoczynania projektów „from scratch”.
Dodatkowe materiały:
https://www.embedded.com/design/mcus-processors-and-socs/4007119/Building-Bare-Metal-ARM-Systems-with-GNU-Part-1--Getting-Started
https://community.arm.com/processors/b/blog/posts/useful-assembler-directives-and-macros-for-the-gnu-assembler
http://www.bravegnu.org/gnu-eprog/lds.html
http://pandafruits.com/stm32_primer/stm32_primer_minimal.php
Poradniki pomagające w tworzeniu oprogramowania na mikrokontrolery skupiają się głównie na wykorzystaniu peryferiów samego mikrokontrolera, co jest zrozumiałe, ale niestety rzadko skncentrują się na części programu, która wykonuje się przed funkcją main.
Poniższy poradnik ma na celu przybliżenie tej części programu na podstawie GNU ARM toolchain oraz mikrokontrolera stm32F334 z rdzeniem cortex-m4.
Używane narzędzia:
- GNU Arm Embedded Toolchain 7-2018-q2-update
https://developer.arm.com/open-source/gnu-toolchain/gnu-rm/downloads
- OpenOCD
- cygwin (z make)
- puTTy
- sterowniki wymagane przez STLINK
Zainstalować powyższe programy i dodać do zmiennej środowiskowej Path ścieżkę do folderu bin dla toolchain’a (dla naszej wygody, inaczej za każdym razem trzeba podawać relatywną ścieżkę do narzędzi).
1. Startup
Od czego zacząć pisanie kodu? Od poszukiwania informacji na temat jak producent rdzenia (w tym wypadku ARM) przewidział startup procesora:
https://www.keil.com/pack/doc/CMSIS/Core/html/startup_s_pg.html
Nasz kod zaczyna się od adresu 0x00000000 gdzie znajduje się wartość stack pointera, a następnie program zaczyna się pod adresem 0x00000004 gdzie znajduje się początek vector table, której pierwszym elementem jest pointer na funkcję „Reset_Handler”.
Jest możliwość zmiany adresu pamięci z jakiego nasz program się uruchamia. Jest to możliwe za pomocą fizycznej ingerencji w piny oznaczone jako BOOT0 i nBOOT1. Co ciekawe, możemy wystartować program z „System memory „ gdzie znajduje się „Embedded boot loader „ który jest wgrywany podczas produkcji układu i nie jest możliwy do modyfikacji.
Więcej informacji o Embedded boot loader’a można znaleźć pod linkiem:
https://www.st.com/content/ccc/resource/technical/document/application_note/b9/9b/16/3a/12/1e/40/0c/CD00167594.pdf/files/CD00167594.pdf/jcr:content/translations/en.CD00167594.pdf
Możemy teraz przystąpić do pisania własnego kodu startup.S. Czym jest ten plik? Jest to plik napisany w języku asembly zwanym u nas assembleerem. Język assembly składa się z dwóch zestawów instrukcji: ARM i thumb (thumb-2). Więcej na ten temat można poczytać w tym wątku:
https://stackoverflow.com/questions/28669905/what-is-the-difference-between-the-arm-thumb-and-thumb-2-instruction-encodings
W naszym przypadku używamy zestawu instrukcji thumb-2:
http://infocenter.arm.com/help/topic/com.arm.doc.qrc0001m/QRC0001_UAL.pdf
Code: text
1. dyrektywa .global powoduje że symbol _start jest widoczna dla linkera (ld)
2.dyrektywa .thumb_func mówi assemblerowi (as) że następny symbol wskazuje na instrukcje thumb
3. adres stack point’a, którego wartość znajdzie się pod adresem 0x08000000 flash. Aktualnie zostanie ustawiony na koniec pamięci SRAM, który możemy odczytać z dokumentacji mikrokontrolera.
4. vector reset, który znajdzie się pod adresem 0x08000004 flash. Zawartość tego adresu zawiera adres na funkcję reset:
5. Branch – przeskoczenie do funkcji o etykiecie (adresie) bez wpisywania adresu do lr (link register).
6. Kod nigdy nie powinien się tutaj znaleźć.
7. Funkcja z nieskończoną pętlą.
2. Skrypt Linkera
Jeśli mamy już nasz startup to możemy zacząć pisać nasz skrypt linkera. Poniższy skrypt opisuje jaki kod (i w jakiej kolejności) będzie trafiał do poszczególnych sekcji oraz dostarczy nam informacje o początkach i końcach poszczególnych sekcji.
Code: text
1. MEMORY przydziela nazwy do przestrzeni pamięci.
2. Początek pamięci flash oraz długość. Informacja o adresie w dokumentacji
3. Początek pamięci oraz długość Ram. Ponownie informacja o adresie w dokumentacji
4. sekcja .text w tej sekcji znajduje się kod wykonywalny.
5. adres początku sekcji. Możemy również ustawić adres sekcji ręcznie poprzez . = 0x80000;
6. Wrzucenie wszystkich text z obiektów do tej sekcji
7 i 8. Wrzucenie konkretnych text z obiektów do tej przestrzeni. Trzeba mieć na uwadze, że zamiana kolejności obiektów spowoduje, że kod nie będzie działać poprawnie!
9. Adres końca sekcji
10. Wskazanie na jaki adres będzie leciała dana sekcja.
11. Sekcja .rodata zawiera wszystkie zmienne stałe.
12.Sekcja .bss (block started by symbol) zawiera wszystkie zmienne statyczne nie zainicjalizowane.
13. Sekcja .data zawiera wszystkie zmienne statyczne zainicjalizowane. Zapis „AT (__rodata_end__)” Oznacza tyle, że sekcja chociaż znajdować się będzie w SRAM to na początku fizycznie będzie znajdować się w flash i trzeba ją następnie przekopiować w cstartup do ramu.
3. cstartup
Jeśli mamy już nasz skrypt linkera to możemy zacząć pisać nasz własny crt0. Jest to kawałek kodu wykonywany przed funkcją main, która zapewnia skopiowanie wszelakich statycznych danych z pamięci nieulotnej do ulotnej (w przypadku statycznych niezainicjalizowane zmiennych, wyzerowanie ich).
Tak więc tak wygląda nasz _cstartup:
Code: text
Jest to kod napisany w C, jednak bez żadnych przeszkód możemy go napisać w asm.
Zmienne globalne zaczynające się od extern są zmiennymi zawierające informację o adresach początku i końca sekcji, które będą szczegółowiej opisane w części poświęconej linkerowi. Powyższy kod zeruje przestrzeń .bss gdzie znajdują się niezainicjalizowane zmienne statyczne, gdy do sekcji .data zostają przeniesione z pamięci nieulotnej wszystkie zainicjalizowane zmienne statyczne.
Ważne wspomnieć, że jeśli chcemy by inne sekcje również znajdowały się w RAM (np. sekcja z kodem, który ma się wykonywać z RAM’u), również tutaj muszą zostać przekopiowane.
4. Main
Gdy już mamy napisane wszystkie elementy związane z startup'em możemy przystąpić do pisania naszej funkcji main:
Code: c
Adres RCCBASE oraz GPIOBASE możemy odczytać z dokumentacji z opisem za co poszczególne rejestry odpowiadają. Komentarze w kodzie są wystarczając i ten kawałek kodu nie wymaga dłuższego omówienia. Możemy teraz przejść do omówienia sposobu budowania projektu.
5. Makefile
Code: makefile
To co powyżej widzimy to plik makefile, który zawiera opis budowania naszego projektu. Sposób budowania jest specjalnie uproszczony by w dość przejrzysty sposób wytłumaczyć na czym polega proces budowania binarki.
Zaczynamy od kompilatora gcc. Jest to program kombajn, który wiele może zrobić za naszymi plecami, ale jego głównym założeniem jest kompilacja plików napisanych w C do plików w języku asm. W tym wypadku zachodzi niejawne użycie programu as (assembler), który tłumaczy assembly na język maszynowy.
Opis poszczególnych flag użytych podczas wywołania kompilatora i as (niejawnie):
-Wall Wyświetla wszystkie warningi, które zostały wykryte w kodzie.
-g zamieszcza w outpucie symbole debugowe.
-c kompilowanie i asemblowanie plików źródłowych bez ich linkowania.
-o file nazwa pliku wyjściowego
-march Nazwa architektury pod którą budujemy projektu
Gdy już uzyskamy pliki .o możemy przystąpić do linkowania za pomocą ld z flagą -T która powoduje, że linker używa naszego skryptu.
Kolejnymi dwoma narzędziami są objdump oraz nm, które mogą nam pomóc w lepszym zrozumieniu naszego kodu oraz są wielce pomocne podczas poszukiwania pewnych rodzajów błędów w kodzie.
Na samym końcu używamy objcopy do translacji pliku .elf na plik binarny.
Opis wszystkich opcji gcc:
https://gcc.gnu.org/onlinedocs/gcc/Option-Summary.html
Gdy posiadamy już nasz plik binarny oraz używamy ST Link’a, który zgłasza się jako mass storage device po podpięciu pod USB, możemy przerzucić bezpośrednio wygenerowaną binarkę do ST Link’a a flashowanie zacznie się automatycznie. Nasz układ zaczyna mrugać LED!
Efekt końcowy:
6. Debugowanie
Ważne! Kompilator musi budować z flagą -g !
flashujemu nasz układ poprzez openocd:
$ openocd -f ../OpenOCD/0.10.0-9-20181016-1725/scripts/board/stm32f334discovery.cfg -c init -c "reset halt" -c "flash write_image erase blink.elf" -c "verify_image blink.elf" -c reset -c shutdown

następnie odpalamy openocd z plikiem konfiguracyjnym dla naszego boarda:
openocd -f ../OpenOCD/0.10.0-9-20181016-1725/scripts/board/st_nucleo_f3.cfg

następnie odpalamy puTTy i łaczymy pod adres 127.0.0.1 na porcie 4444 poprzez telnet
jeśli coś nie działa musimy odblokować clienta telnet:
https://www.lifewire.com/what-is-telnet-2626026

Gdy już odpaliliśmy puTTy to wpisujemy:
reset halt

odpalamy nowy terminal i przechodzimy tam gdzie zbudowaliśmy projekt. Odpalamy teraz gdb z argumentem do pliku .elf:
arm-none-eabi-gdb blink.elf
W terminalu wklepujemy taką o to sekwencję:
target remote localhost:3333
monitor reset halt
load

Gratuluje, możemy debugować. Wszelakie komendy potrzebne do debuggowania z terminalu można znaleźć w sieci. Niestety nie ma możliwości używania gdbtui pod windowsem


Powyższe przykład służy jedynie do uświadomienia jak wiele dzieje się jeszcze przed uruchomieniem funkcji „main”. Nie jest to rozwiązanie, które można stosować w poważnych projektach, ale mam nadzieję że posłuży jako pewien punkt podparcia w poszukiwaniu dalszej wiedzy na temat rozpoczynania projektów „from scratch”.
Dodatkowe materiały:
https://www.embedded.com/design/mcus-processors-and-socs/4007119/Building-Bare-Metal-ARM-Systems-with-GNU-Part-1--Getting-Started
https://community.arm.com/processors/b/blog/posts/useful-assembler-directives-and-macros-for-the-gnu-assembler
http://www.bravegnu.org/gnu-eprog/lds.html
http://pandafruits.com/stm32_primer/stm32_primer_minimal.php
Cool? Ranking DIY