Eh bien voilà, très (trop) longtemps après le premier épisode, me revoilà pour reprendre avec vous la série sur l'écriture d'un système d'exploitation pour un microcontrôleur STM32F103. Pour rappel, mon système d'exploitation MOS est écrit dans un but d'apprentissage. Il vise à être simple à appréhender et à permettre à chacun de découvrir les entrailles d'un OS. Cela implique deux conséquences :
- je réinvente la roue puisque je réécris tout de zéro
- il y aura sûrement des bugs, n'essayez pas de le mettre en production (!)
Voilà, tout ceci étant dit, on va pouvoir attaquer les choses sérieuses. Dans cet épisode, nous allons organiser notre projet et configurer nos outils. Bonne lecture !
Sommaire
Organisation du projet
Pour commencer, on va organiser notre projet. L'arborescence que j'ai choisi d'adopter est la suivante :
.
├── COPYING
├── include
│ ├── cpu
│ ├── kernel
│ └── soc
├── linker.lds
├── Makefile
├── README
└── src
├── cpu
├── kernel
└── soc
Les en-têtes (dans include/
) et les sources (dans src/
) sont organisées de manière symétrique, et séparées en plusieurs dossiers : cpu/
pour ce qui est spécifique au Cortex-M3, soc/
pour ce qui relève des particularités du STM32F103, et enfin kernel/
pour tout ce qui est du ressort générique du noyau et non spécifique au matériel (gestion des tâches, de la mémoire, des événements, bibliothèque standard…).
Notez également la présence d'un fichier COPYING
qui contient la licence (BSD en l'occurrence) et d'un README
qui contient une brève description du projet. Le linker script et le Makefile seront décrits plus en détail dans les paragraphes suivants.
Configuration d'OpenOCD
Nous avons installé OpenOCD, mais il nous reste à le configurer. Rassurez-vous, ça se résume à pas grand chose… une fois que l'on a trouvé quoi mettre dans les fichiers de configuration ! Je vous épargne donc les recherches sur le site d'OpenOCD et sur les diverses mailing lists, et je vous offre mon fichier de configuration, que j'ai stocké dans ~/.config/openocd/mos.cfg
:
source [find interface/olimex-arm-usb-tiny-h.cfg]
source [find target/stm32f1x.cfg]
Dans la première ligne, on explique à OpenOCD comment s'interfacer avec la sonde JTAG, et dans la seconde, on lui indique quel microcontrôleur se trouve à l'autre bout. Si tout marche bien, on doit pouvoir le lancer comme cela openocd -f ~/.config/openocd/mos.cfg
.
Note : pour éviter les problèmes, vérifiez bien que vous avez le droit d'accéder au périphérique correspondant à votre sonde JTAG. Chez moi par exemple, le paquet OpenOCD installe des règles udev dans /lib/udev/rules.d/40-openocd.rules
qui attribuent le périphérique de ma sonde au groupe plugdev
lors du branchement.
Configuration de GDB
Notre ami GDB va nous servir à debugger le code tournant sur notre cible. Habituellement, lorsqu'on fait du cross-debugging, on exécute un gdbserver
sur la cible afin que GDB sur l'hôte puisse savoir ce qui se passe sur la cible. Dans notre cas ce n'est évidemment pas possible, puisque nous n'avons pas d'OS sur la cible qui pourrait nous permettre d'exécuter gdbserver
. D'où l'importance d'OpenOCD, qui va se faire passer pour un gdbserver
et permettre, via le JTAG, d'aller étudier tout ce qui se passe sur la cible.
Pour mieux comprendre comment tout cela s'articule, voici un résumé de la chaîne de debug :gdb <---(socket)---> openocd <---(USB)---> sonde JTAG <------> STM32
On va donc se faire un fichier .gdbinit
aux petits oignons pour faciliter notre debug. En ce qui me concerne, je l'ai placé à la racine de mon projet puisque c'est de là que je lance mon GDB. Voici à quoi il ressemble :
target remote :3333
monitor reset halt
define mos_flash
monitor flash probe 0
monitor stm32f1x mass_erase 0
monitor flash write_bank 0 mos.bin 0
monitor reset halt
dont-repeat
end
Les deux premières lignes seront exécutées systématiquement au lancement de GDB. La première indique à GDB que l'on souhaite contacter un gdbserver
sur le port 3333 de localhost
. C'est le port par défaut sur lequel OpenOCD attend une connexion de GDB. La seconde ligne est une commande monitor
. Dans le langage de GDB, une commande monitor
est une commande qui n'est pas interprétée par GDB lui-même, mais qui est envoyée telle quelle au gdbserver
(c'est à dire à OpenOCD, vous suivez ?). Donc toutes les commandes monitor
que vous verrez ici sont en fait écrites dans la syntaxe d'OpenOCD, et ne sauraient être comprises par GDB. Revenons à la signification de cette seconde ligne : on demande donc à OpenOCD de faire un reset de la cible, puis d'arrêter le CPU juste après le reset, soit avant même l'exécution de la toute première instruction.
Les lignes suivantes définissent une commande GDB du nom de mos_flash
. Cette commande est simplement définie ici, elle n'est pas exécutée automatiquement au lancement de GDB. Son but est de nous permettre d'écrire le binaire de MOS dans la flash du microcontrôleur facilement. Et vous vous apercevez que toutes les commandes qui la composent sont des commandes monitor
, ce qui est logique puisque GDB ne sait pas comment flasher un STM32… C'est bien OpenOCD qui « connaît » notre puce. Pour résumer ce que font ces commandes, dans l'ordre, on détecte la flash, puis on l'efface, on écrit le binaire mos.bin
et enfin on effectue un reset du CPU. Un mot sur la directive dont-repeat
: par défaut, GDB relance la dernière commande exécutée lorsque l'on appuie sur <Entrée>
sans avoir rien saisi au prompt GDB. Cette directive inhibe ce comportement pour la commande mos_flash
. On ne souhaite pas en effet écrire plusieurs fois de suite le même binaire dans la flash, car en plus d'être inutile, cela risque d'user la flash pour rien.
Vous aurez peut-être noté que j'ai mis le chemin du fichier à écrire en flash mos.bin
en relatif. Cela signifie que pour que la commande fonctionne correctement, OpenOCD doit être exécuté depuis le dossier où se trouve le binaire à écrire. J'aurais certainement pu faire quelque chose de plus intelligent, mais je suis une feignasse :-) Donc j'ai simplement pris l'habitude de lancer OpenOCD et GDB depuis le répertoire racine de mon projet.
Makefile
À ce stade, j'en vois au fond de la salle qui se disent que c'est bien beau d'avoir configuré tout ce bazar, mais qu'on a pas de code à debugger. Et ils ont raison. Dans cette partie, on va donc se concocter un beau Makefile pour compiler notre OS et pouvoir le charger sur notre cible. Sans plus attendre, sous vos yeux ébahis, le Makefile :
################################################################################
# Customizable variables
################################################################################
# Directories that need to be built
MODULES := src/cpu src/kernel src/soc
# Name of the output binary
OUT := mos
# Build options
CFLAGS := -Iinclude -Wall -Wextra -Werror -mcpu=cortex-m3 -nostdinc -mthumb \
-ggdb -fomit-frame-pointer -DDEBUG
LDFLAGS := -nostdlib
CROSS := arm-none-eabi-
################################################################################
# Build instructions, nothing should be customized under this line
################################################################################
CC := $(CROSS)gcc
LD := $(CROSS)ld
AS := $(CROSS)as
GDB := $(CROSS)gdb
OBJCOPY := $(CROSS)objcopy
SIZE := $(CROSS)size
OBJ :=
# Include all subdirectries makefiles, they will add their object files to the
# OBJ variable
include $(foreach module, $(MODULES), $(wildcard $(module)/*.mk))
# Default target (build the OS !)
all: $(OUT).bin
# Flat binary output
$(OUT).bin: $(OUT).elf
$(OBJCOPY) -O binary $< $@
$(SIZE) $<
# ELF binary output
$(OUT).elf: $(OBJ)
$(LD) $(LDFLAGS) -T linker.lds -o $@ $^
# Cleanup target, remove generated object files and binaries
clean:
rm -f $(OBJ) $(OUT).bin $(OUT).elf
debug: $(OUT).elf $(OUT).bin
$(GDB) $<
Je ne vais pas vous faire ici un cours sur Make, d'autant plus que j'ai mis des commentaires, mais on va discuter de quelques points.
D'abord l'usage de la variable CROSS
. C'est une méthode courante pour faire de la cross-compilation, qui permet d'appeler les outils de génération de code adaptés pour l'architecture de la cible.
Ensuite, les options de compilation pour GCC, notamment une que vous n'avez pas forcément l'habitude de voir : -notstdinc
indique à GCC que l'on ne veut pas utiliser les chemins d'en-têtes par défaut. Et ce pour la simple et bonne raison que nous n'avons pas de bibliothèque C standard sur notre cible. Cela signifie que si mon code contient un #include <stdio.h>
, je ne veux pas du fichier d'en-tête standard fourni par mon compilateur, et je devrai fournir le fichier stdio.h
dans les répertoires d'en-têtes de mon projet. Bienvenue dans le monde du développement bare-metal.
Pour le linker, même punition : -nostdlib
demande de ne surtout pas lier les bibliothèques standards au binaire généré. Cela signifie que si mon code appelle une fonction standard, par exemple strcpy()
, c'est à moi de fournir une implémentation de cette fonction dans les sources de mon projet.
Enfin, après la génération de l'exécutable mos.elf
, vous noterez que j'utilise objcopy
pour le convertir en binaire brut. En effet, un fichier ELF est un fichier structuré, avec des en-têtes, il est destiné à être parsé avant d'être executé. Ce qui est (entre autres) le travail d'un OS. Mais notre exécutable ne sera pas exécuté par un OS. Il faut donc en faire un binaire brut qui sera flashé dans la puce. Le fichier ELF par contre reste utile pour GDB, car il contient tous les symboles de debug.
Le reste est du confort : la cible debug
permet de compiler le binaire et de lancer GDB dans la foulée, tandis que l'appel à size
après la compilation permet d'afficher dans le terminal les tailles de code et de données utilisées par notre noyau, à titre d'information.
Conclusion
Nous avons maintenant un environnement tout prêt pour commencer à coder et exécuter du code sur notre cible. Tous les outils sont là, il n'y a plus qu'à… Enfin presque. Les observateurs attentifs auront remarqué que je n'ai pas abordé le sujet du linker script (le fichier linker.lds
). On en parlera dans le prochain épisode, car cela nécessite d'abord un point sur les différentes sections d'un exécutable, ainsi que sur le processus de démarrage du STM32F103. Et c'est promis, dans le prochain épisode, on fera tourner du code sur notre cible ! Promis également, le prochain épisode ne sera pas dans un an :-)
Aller plus loin
- Code source du projet MOS (452 clics)
# ça me dit quelque chose
Posté par cévhé . Évalué à 9. Dernière modification le 10 décembre 2015 à 22:23.
ha oui :
(just a hobby, won’t be big and professional like gnu)
Je n'y comprend rien, mais je te souhaite néanmoins une belle réussite.
# SOS
Posté par Victor STINNER (site web personnel) . Évalué à 5.
Moi ça me rappelle le projet SOS de Thomas Petazzoni ;-) http://sos.enix.org/fr/PagePrincipale
[^] # Re: SOS
Posté par maxb . Évalué à 4.
Oui, clairement c'est dans la même veine. D'ailleurs SOS est un des projets qui m'a donné envie de m'y mettre, et j'admire beaucoup le travail de Thomas Petazzoni. Par contre MOS est beaucoup moins ambitieux que MOS, pour diverses raisons :
[^] # Re: SOS
Posté par maxb . Évalué à 3.
Il faut évidemment lire "MOS est beaucoup moins ambitieux que SOS"…
# nostdinc
Posté par pulkomandy (site web personnel, Mastodon) . Évalué à 1.
Est-ce que nostdinc est suffisant pour se débarasser de la librairie standard? J'aurais plutôt utilisé l'option -ffreestanding.
[^] # Re: nostdinc
Posté par maxb . Évalué à 5.
Il y a toute une flopée d'options pour calmer l'enthousiasme de GCC. Ce que tu vois dans le Makefile est le résultat de mon interprétation du manuel, mais il est possible que je n'aie pas opté pour la méthode la plus directe. Ce qui est sûr c'est que pour l'instant ça a suffi pour que GCC n'inclue dans mon binaire que mon code.
Juste pour préciser toutefois :
nostdinc
permet de se débarrasser des includes par défaut, il n'agit que sur le préprocesseur je pense. C'est lenostdlib
passé au linker qui me permet de me débarrasser de la librairie standard en tant que telle. Il permet aussi de ne pas linker avec le startup code par défaut.[^] # Re: nostdinc
Posté par Maxime (site web personnel) . Évalué à 2.
Perso je compile avec -ffreestanding -nostdlib -nostdinc -fno-builtin sur mon OS maison. Mais je ne me souviens plus exactement de ce qui est utile et ne l'est pas :). Attention aussi à certaines optimisations du compilo. Par exemple, vu que j'utilise aussi une libC maison, gcc se permet de remplacer du code par des appels de fonctions, c'est comme ça qu'il a jugé pertinent de remplacer le coeur de ma fonction memset par un appel (récursif du coup) à memset…
# Typo
Posté par djano . Évalué à 2.
…par…
[^] # Re: Typo
Posté par Benoît Sibaud (site web personnel) . Évalué à 3.
Corrigé, merci.
# Yup
Posté par Michael Vergoz . Évalué à 1.
Salut à tous
Pas mal les articles, c'est sympa :)
Je dev un projet d'OS (miniPhi) assez important sous licence GPLv3, je fais un peu de pub ici si il y a du monde chaud pour m'aider un peu !
Actuellement l'OS fonctionne sur les MCU texas instrument MSP430 mais j'ai commencé le portage pour STM32 et du coup c'est du taff :)
En fait je rend disponible les drivers que je développe pour mes besoins. Allez faire un tour dans le répertoire drivers/. Du coup ils sont portables pour n'importe quelle archi.
J'ai fait au mieux pour la doc donc un ptit coup de doxygen dans le répertoire root et on voit pas mal de truc
https://github.com/mykiimike/miniPhi
+++
mykii
Suivre le flux des commentaires
Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.