Introduction
Présentation
Cet article complète l'article Bare Metal - From zero to blink afin de rajouter le support du C++ sur STM32 et de proposer une implémentation en C++ moderne d'un pilote GPIO.
Le C++ est rarement un candidat de choix lorsque l'on parle MCU et empreinte mémoire de quelques kilo octets. En effet, le polymorphisme dynamique est synonyme de vtable
, déduction de type à l'exécution, etc. Une simple écriture sur le flux standard std::cout
a un empreinte mémoire d'une centaine de kilo octets.
Il est sujet ici de démystifier tout cela et de voir ensemble quelles sont les fonctionnalités du langage que l'on peut utiliser pour faire du baremetal et de mettre en œuvre des techniques de C++ moderne (C++17).
Prérequis
On suppose que les sujets traités dans l'article précédent sont maîtrisés ou du moins connus, sinon n'hésitez pas à vous y référer.
Environnement de développement
- Chaine de compilation croisée arm-none-eabi.
- CMake >= 3.16
- carte d'évaluation Nucleo32 (NUCLEO32-F303K8)
- éditeur de texte de votre choix
Par rapport à l'article précédent, le MCU est différent. De ce fait, les tailles de SRAM et FLASH ainsi que les vecteurs d'interruption et les périphériques (adresse et fonctionnalité) sont différents. Nous utiliserons aussi les bibliothèques standard C et C++, la libc étant newlib
et la libstdc++ celle de gcc
. Plutôt que de gérer manuellement un makefile
, le projet sera géré avec CMake
.
Construction avec CMake
Les sources du projet seront construites avec CMake
qui est un outil qui génère une solution de construction pour une utilisation en ligne de commande ou un environnement de développement intégré (make
, ninja
, Eclipse
).
Recette de base
L'arborescence du projet sera la suivante, nous détaillerons plus loin chaque fichier.
.
├── cmake
│ └── arm-none-eabi.cmake
├── CMakeLists.txt
├── include
├── lnk
│ └── gcc_cm4.ld
└── src
├── main.cpp
├── startup.c
└── syscalls.c
Le point d'entrée d'un projet est un fichier CMakeLists.txt
. Construisons donc ce fichier pour compiler notre firmware. Il faut définir un nom de projet, une version minimale de CMake
(le développement de CMake
étant très actif, ceci permet de s'assurer que l'utilisateur a une version ayant bien les fonctionnalités requises) et enfin la liste des sources pour construire l'exécutable.
cmake_minimum_required(VERSION 3.16)
project(BlinkModernCppStep1 C CXX)
add_executable(${PROJECT_NAME}
src/main.cpp
src/startup.c
src/syscalls.c
)
C'est bien, mais ce n'est pas suffisant, il faut fournir au compilateur les options de compilation nécessaires pour notre cible.
[...]
target_compile_options(${PROJECT_NAME} PRIVATE
-mcpu=cortex-m4
-mthumb
)
Les mots clés PRIVATE/PUBLIC/INTERFACE
spécifient la portée de ces options, PUBLIC/INTERFACE
n'ont de sens que pour des bibliothèques.
Enfin, nous ajoutons les options pour l'édition de lien en utilisant le script gcc_cm4.ld
de notre cible (nous ajoutons également la production d'un fichier .map
). Étant sur un micro-contrôleur relativement modeste, nous utilisons la version nano de la newlib.
[...]
target_link_libraries(${PROJECT_NAME}
--specs=nano.specs
-static
-T${PROJECT_SOURCE_DIR}/lnk/gcc_cm4.ld
-Wl,-Map=${PROJECT_NAME}.map
-Wl,--start-group c gcc -Wl,--end-group
)
Compilation croisée avec CMake
Pour effectuer de la compilation croisée, CMake
a besoin d'un fichier toolchain
que l'on transmet à la génération du projet comme ceci :
cmake -DCMAKE_TOOLCHAIN_FILE=<toolchain.cmake> <path/to/CMakeLists.txt>
Le fichier toolchain pour notre chaîne de compilation est le suivant :
# (1)
set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)
# (2)
set(CMAKE_SYSTEM_NAME Generic)
set(CMAKE_SYSTEM_PROCESSOR ARM)
# (3)
set(TOOLCHAIN_PREFIX /opt/arm-none-eabi/bin/arm-none-eabi)
# (4)
set(CMAKE_C_COMPILER ${TOOLCHAIN_PREFIX}-gcc)
set(CMAKE_CXX_COMPILER ${TOOLCHAIN_PREFIX}-g++)
set(CMAKE_ASM_COMPILER ${TOOLCHAIN_PREFIX}-gcc)
# (5)
set(CMAKE_FIND_ROOT_PATH /opt/arm-none-eabi)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
- (1)
CMake
vérifie que la chaîne de compilation est fonctionnelle en construisant un binaire simple, étant sur un système nécessitant plusieurs options pour l'édition de lien, queCMake
ne connaît pas, on lui indique de tester cette dernière en construisant une bibliothèque statique. - (2) Précise à
CMake
le nom du système ainsi que le processeur, pour du baremetal,CMake
s'attend à avoir le nomGeneric
- (3) Définition d'une variable pour renseigner le préfixe de la toolchain et permettre à
CMake
de trouver le binaire, ici, la chaîne de compilation estarm-none-eabi
et est installée dans/opt
. - (4) Définition des variables
CMake
précisant les compilateursASM/C/C++
- (5) Précise à
CMake
les chemins de recherche des fichiers d'en-tête et les bibliothèques et impose de ne chercher qu'à cet endroit (Ceci afin d'être certain d'utiliser la bonne libc s'il existe plusieurs installations de chaîne de compilation ARM sur la machine).
À ce point, nous pouvons générer la solution. Pour cela, nous nous plaçons dans un répertoire .build
pour faire une compilation out of tree
.
mkdir .build
cd .build
cmake .. -DCMAKE_TOOLCHAIN_FILE=../../cmake/arm-none-eabi.cmake
-- The C compiler identification is GNU 10.3.1
-- The CXX compiler identification is GNU 10.3.1
-- Check for working C compiler: /opt/arm-none-eabi/bin/arm-none-eabi-gcc
-- Check for working C compiler: /opt/arm-none-eabi/bin/arm-none-eabi-gcc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /opt/arm-none-eabi/bin/arm-none-eabi-g++
-- Check for working CXX compiler: /opt/arm-none-eabi/bin/arm-none-eabi-g++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: .build
Script d'édition de liens
Afin d'utiliser notre SoC et la newlib, il est nécessaire d'apporter quelques modifications au linker script
de l'article précédent, il se nomme dans notre projet lnk/gcc_cm4.ld.
Notre SoC dispose en effet de 64Ko de flash et 12Ko de SRAM, les adresses de bases sont identiques.
MEMORY
{
FLASH(rx):ORIGIN =0x08000000,LENGTH =64K
SRAM(rwx):ORIGIN =0x20000000,LENGTH =12K
}
Notre firmware faisant appel au code de démarrage de la newlib, c'est cette dernière qui se charge d'initialiser à zéro la section `.bss`, elle utilise pour cela les symboles __bss_start__
et __bss_end__
.
[...]
.bss :
{
. = ALIGN(4);
__bss_start__ = .;
*.(.bss)
. = ALIGN(4);
__bss_end__ = .;
}> SRAM
[...]
Code de démarrage
Le code de démarrage nécessite d'être mis à jour avec la table de vecteurs d'interruption correspondant à notre cible (non détaillé ici).
Le vecteur de reset
est quant à lui simplifié, il ne se charge plus que de copier la section .data
en RAM et passe la main à la routine de démarrage de la newlib _start
.
__attribute__((noreturn)) void Reset_Handler(void) {
extern void _start(void) __attribute__((noreturn));
uint32_t size = (uint32_t) &_edata - (uint32_t) &_sdata;
uint8_t *pSrc = (uint8_t *) &_etext; /* Flash to ... */
uint8_t *pDst = (uint8_t *) &_la_data; /* ... SRAM */
/* copy .data to sram*/
for (uint32_t i; i< size; i++) {
*pDst++ = *pSrc++;
}
/* libc entry point */
_start();
}
Première compilation
Nous pouvons essayer à ce point de compiler notre projet. Le fichier main.c
ne contient que la routine main
, qui est vide.
$ make
[ 25%] Building CXX object CMakeFiles/BlinkModernCpp.dir/src/main.cpp.obj
[ 50%] Building C object CMakeFiles/BlinkModernCpp.dir/src/startup.c.obj
[ 75%] Building C object CMakeFiles/BlinkModernCpp.dir/src/syscalls.c.obj
[100%] Linking CXX executable BlinkModernCpp.elf
/opt/arm-none-eabi/bin/../lib/gcc/arm-none-eabi/10.3.1/../../../../arm-none-eabi/bin/ld: /opt/arm-none-eabi/bin/../lib/gcc/arm-none-eabi/10.3.1/../../../../arm-none-eabi/lib/libc_nano.a(lib_a-exit.o): in function `exit':
exit.c:(.text.exit+0x34): undefined reference to `_exit'
collect2: error: ld returned 1 exit status
make[2]: *** [CMakeFiles/BlinkModernCppStep1.dir/build.make:114 : BlinkModernCpp.elf] Erreur 1
make[1]: *** [CMakeFiles/Makefile2:76 : CMakeFiles/BlinkModernCpp.dir/all] Erreur 2
make: *** [Makefile:84 : all] Erreur 2
L'édition de lien échoue car la newlib a besoin qu'on lui fournisse l'implémentation de l'appel système exit
. Dans le cas d'un firmware sur microcontrôleur, le main
ne se termine jamais, l'implémentation d'exit
peut donc être un bouchon.
On ajoute au fichier src/syscalls.c
la routine suivante :
void _exit(int status) {
(void)status;
for(;;);
}
Nous pouvons relancer la compilation.
$ make
Scanning dependencies of target BlinkModernCpp
[ 25%] Building C object CMakeFiles/BlinkModernCpp.dir/src/syscalls.c.obj
[ 50%] Linking CXX executable BlinkModernCpp.elf
[100%] Built target BlinkModernCpp
C++ moderne sur MCU
Commençons simplement par ajouter un handler
sur la méthode terminate
, cette dernière appelle abort
par défaut. Dans notre cas, nous n'avons pas d'implémentation pour abort
et on pourrait vouloir imprimer une backtrace
ou faire toute autre opération de débogage. Ceci dépasse largement le périmètre de l'article et nous allons donc nous limiter au même comportement que le Default_Handler
.
#include <exception>
[[noreturn]] void terminate_handler(void) { while(1); }
int main(void)
{
std::set_terminate(terminate_handler);
return 0;
}
L'attribut [[noreturn]]
est la version C++ de l'attribut gcc __attribute__((noreturn))
. Celle-ci fait partie intégrante du langage, le comité C++ ayant choisi la syntaxe de clang
pour le standard.
Ce simple ajout nécessite, par le biais des dépendances internes à la libc, de fournir trois nouvelles implémentations pour de nouveaux appels systèmes. Il faut donc fournir les implémentations de _getpid
, _kill
, _sbrk
(respectivement pour les appels système getpid/kill/sbrk
). Les deux premiers peuvent être bouchonnés, _sbrk
nécessite un peu plus de travail.
[...]
int _getpid(void)
{
return -1;
}
void _kill(int pid, int sig)
{
(void)pid;
(void)sig;
}
L'implémentation de _sbrk
sera vue un peu plus bas dans cet article.
Mise en place du tas
Dans notre cas, le tas (heap
) sera toute la SRAM non utilisée, i.e. entre la fin de la section .bss
et la fin de la pile. Pour rappel, le début de la pile est à la fin de la RAM et celle-ci est consommée vers les adresses décroissantes.
On définit donc un symbole __heap_start__
dans notre linker script après la section .bss
[...]
.bss :
{
[...]
}> SRAM
__heap_start__ = .;
Ensuite nous devons implémenter l'appel système sbrk
. Cette appel système est nécessaire à l'allocateur mémoire de la libc à chaque fois que la taille du tas doit être augmentée (c.f. page de manuel sbrk). Nous devons aussi au préalable fixer une taille pour la pile, nous prenons ici, arbitrairement, 1Ko.
[...]
#define STACK_START SRAM_END
#define STACK_SIZE (1024U)
#define HEAP_LIMIT (STACK_START - STACK_SIZE)
[...]
char* _sbrk(int incr)
{
extern char __heap_start__; // symbol in linker script
static char *current_heap_end = &__heap_start__;
char *previous_heap_end = current_heap_end;
if (current_heap_end + incr > (char*)HEAP_LIMIT) {
_impure_ptr->_errno = ENOMEM; // newlib's thread-specific errno
return (char *)-1;
}
current_heap_end += incr;
return (char *) previous_heap_end;
}
Pilote de GPIO
Comme annoncé dans l'introduction, nous développerons en C++ moderne et plus précisément en c++17. Nous devons dans un premier temps configurer CMake
pour compiler le C++ dans cette révision du langage.
target_compile_features(${PROJECT_NAME} PRIVATE cxx_std_17)
target_compile_features
vérifie que le compilateur supporte bien la fonctionnalité demandée et ajoute automatiquement les options et flags de compilation nécessaires pour les sources C++.
Notre pilote de GPIO ne va faire que quelques opérations simples :
- Configurer la direction (Entrée / Sortie)
- Positionner une pin dans son état actif (
set
) - Positionner une pin dans son état inactif (
clear
) - Inverser l'état d'une pin (
toggle
) - Lire l'état d'une pin
Certains SoC disposent simplement d'un registre qui est l'image des sorties et où l'on positionne directement les bits dans l'état voulu. Sur STM32, il y a d'autres registres qui permettent de set
ou clear
les bits que l'on positionne à 1, et les Atmel SAM4l ont en plus un registre pour toggle
les GPIO. Avec le C++ moderne, il est possible de détecter tout cela à la compilation et donc de choisir la bonne méthode à ce moment là. Ceci permet également une optimisation plus agressive par le compilateur.
Classe de base d'un port GPIO
Mettons en place pour commencer l'interface de notre classe gpio
. Nous avons donc une classe template d'un port de GPIO de base (basic_gpio_port
) qui sera spécialisée plus tard avec le pilote du périphérique matériel et une sous classe qui sera le point d'entrée pour utiliser une GPIO. Chaque pilote devra définir un type interne value_type
qui représente le type des données, par exemple sur STM32, le registre GPIO est un entier 32 bits, dans ce cas using value_type = uint32_t
, de la même manière un expander I²C avec un registre 8 bits définira using value_type = uint8_t
.
Le nombre de GPIO du port doit aussi être défini par le pilote.
template<typename gpio_port>
class basic_gpio_port {
using value_type = typename gpio_port::value_type;
static constexpr uint8_t nr_gpios = gpio_port::nr_gpios;
[...]
La classe gpio
est également un template prenant en paramètre un scalaire, ceci permet de spécifier à la compilation le numéro de la GPIO considerée. Notre classe va implémenter les fonctions de base décrites précédemment. On effectue également des vérifications de cohérence à la compilation avec static_assert
.
[...]
template<uint8_t num>
class gpio {
friend class basic_gpio_port;
static_assert(num < basic_gpio_port::nr_gpios, "invalid gpio number");
static constexpr uint8_t bit = num;
static constexpr basic_gpio_port::value_type mask = 1 << bit;
public:
static void set() { basic_gpio_port::set_pin(mask); }
static void clear() { basic_gpio_port::clear_pin(mask); }
static void toggle() { basic_gpio_port::toggle_pin(mask); }
static auto get() { return basic_gpio_port::get_pin(mask); }
static void set_direction(direction direction)
{
basic_gpio_port::set_pin_direction(bit, direction);
}
};
[...]
SFINAE et traits de type
L'implémentation du port de base se charge de la redirection vers la bonne implémentation dès la compilation. Pour ce faire, nous allons mettre en œuvre un concept du C++ qui est le SFINAE
(Substition Failure Is Not An Error). Ceci permet de détecter à la compilation les spécificités d'un type de donnée et d'agir en conséquence avec l'utilisation de condition évaluée à la compilation if constexpr
.
L'un des moyens de tester si une classe supporte une fonctionnalité est de définir un ou plusieurs alias du type standard true_type
(ce type est une structure ne contenant qu'un membre constant booléen étant vrai).
Par exemple, pour déclarer que notre pilote a la capacité de set ou clear une pin avec un registre spécifique. Nous définissons donc deux alias dans notre pilote.
using feature_set_bit = std::true_type;
using feature_clear_bit = std::true_type;
Afin de détecter si notre pilote contient cette fonctionnalité, nous devons utiliser le détecteur de type du c++17. Si ce type existe, on reçoit en retour une structure aliasant le type true_type
et le type voulu, en cas d'erreur de substitution, un alias de false_type est renvoyé (false_type
étant le retour par défaut).
L'astuce réside dans le fait que le type recherché n'est pas directement passé au trait mais on lui donne un template qui l'utilise, si la substitution réussit, le type existe, sinon il n'existe pas. En cas d'erreur de substitution, il existe tout de même un candidat à la surcharge, notre type par défaut.
Voici pour information le détail d'implémentation de GCC
pour une telle détection statique de type.
namespace detail {
template <class Default, class AlwaysVoid, template<class...> class Op, class... Args>
struct detector {
using value_t = std::false_type;
using type = Default;
};
template <class Default, template<class...> class Op, class... Args>
struct detector<Default, std::void_t<Op<Args...>>, Op, Args...> {
using value_t = std::true_type;
using type = Op<Args...>;
};
} // namespace detail
template <class Default, template<class...> class Op, class... Args>
using detected_or = detail::detector<Default, void, Op, Args...>;
Voyons donc comment l'utiliser dans le cadre de notre pilote. Pour rappel, nous souhaitons détecter si un pilote expose une fonctionnalité dès la compilation. Notre fonctionnalité étant définie comme un alias de true_type
, nous créons donc le trait de type suivant pour nos GPIO qui retourne vrai si la fonctionnalité existe:
template<template<class...> typename Feature, typename T>
inline constexpr auto has_feature_v = std::experimental::detected_or<std::false_type, Feature, T>::type::value;
Il ne nous reste plus qu'à définir les templates pour tester la présence de nos fonctionnalités.
namespace feature {
template<typename T>
using set_bit_t = typename T::feature_set_bit;
template<typename T>
using clear_bit_t = typename T::feature_clear_bit;
template<typename T>
using toggle_bit_t = typename T::feature_toggle_bit;
} // namespace feature
À l'utilisation, nous testons donc la présence de la fonctionnalité et la meilleure méthode est appelée, par exemple pour passer une GPIO à l'état actif, soit on set le bit dans le registre set
(ou seules les GPIO correspondant un bit à 1 sont impactées), soit on effectue une lecture du registre, on masque le registre pour ne modifier que la GPIO voulue et on réécrit la valeur.
static void set_pin(value_type mask)
{
using namespace gpio::traits;
if constexpr (has_feature_v<feature::set_bit_t, gpio_port>) {
gpio_port::set_bit(mask);
} else {
auto value = gpio_port::get_value();
value |= mask;
gpio_port::set_value(value);
}
}
Support du matériel
À ce point nous pouvons fournir un fichier complet pour notre classe de base de GPIO:
#pragma once
#include <cstdint>
#include <gpio/gpio_trait.hpp>
namespace gpio {
enum class direction : uint8_t {
Input,
Output,
};
template<typename gpio_port>
class basic_gpio_port {
using value_type = typename gpio_port::value_type;
static constexpr uint8_t nr_gpios = gpio_port::nr_gpios;
public:
template<uint8_t num>
class gpio {
friend class basic_gpio_port;
static_assert(num < basic_gpio_port::nr_gpios, "invalid gpio number");
static constexpr uint8_t bit = num;
static constexpr basic_gpio_port::value_type mask = 1 << bit;
public:
static void set() { basic_gpio_port::set_pin(mask); }
static void clear() { basic_gpio_port::clear_pin(mask); }
static void toggle() { basic_gpio_port::toggle_pin(mask); }
static auto get() { return basic_gpio_port::get_pin(mask); }
static void set_direction(direction direction) { basic_gpio_port::set_pin_direction(bit, direction); }
};
static void setup() { gpio_port::setup(); }
private:
static void set_pin(value_type mask)
{
using namespace gpio::traits;
if constexpr (has_feature_v<feature::set_bit_t, gpio_port>) {
gpio_port::set_pin(mask);
} else {
auto value = gpio_port::get_value();
value |= mask;
gpio_port::set_value(value);
}
}
static void clear_pin(value_type mask)
{
using namespace gpio::traits;
if constexpr (has_feature_v<feature::clear_bit_t, gpio_port>) {
gpio_port::clear_pin(mask);
} else {
auto value = gpio_port::get_value();
value &= ~mask;
gpio_port::set_value(value);
}
}
static void toggle_pin(value_type mask)
{
using namespace gpio::traits;
if constexpr (has_feature_v<feature::toggle_bit_t, gpio_port>) {
gpio_port::toggle_pin(mask);
} else {
auto value = gpio_port::get_value();
value ^= mask;
gpio_port::set_value(value);
}
}
static bool get_pin(value_type mask)
{
return gpio_port::get_value() & mask;
}
static void set_pin_direction(value_type mask, direction direction)
{
gpio_port::set_pin_direction(mask, direction);
}
};
} // namespace gpio
Concentrons nous maintenant sur l'écriture du pilote pour notre STM32, ce dernier dispose de registres pour lire/écrire le port de GPIO ainsi que deux registres permettant uniquement de set/clear
les bits positionnés dans ces registres. Notre pilote devra donc fournir les méthodes obligatoires setup
, set_value
, get_value
et set_pin_direction
ainsi que les méthodes optionnelles set_pin
et clear_pin
.
La méthode setup
active l'horloge du port, ceci sort un peu du périmètre de l'article, c'est donc bouchonné avec la valeur du port correspondant à l'exemple ci-dessous.
#pragma once
#include <cstddef>
#include <cstdint>
#include <gpio/gpio.hpp>
#define MODER *(volatile uint32_t*)(baseaddr)
#define IDR *(volatile uint32_t*)(baseaddr + 0x10)
#define ODR *(volatile uint32_t*)(baseaddr + 0x14)
#define BSRR *(volatile uint32_t*)(baseaddr + 0x18)
#define BRR *(volatile uint32_t*)(baseaddr + 0x28)
namespace gpio {
template<size_t baseaddr, uint8_t N>
class gpio_stm32 {
public:
using value_type = uint32_t;
static constexpr uint8_t nr_gpios = N;
using feature_set_bit = std::true_type;
using feature_clear_bit = std::true_type;
static void setup()
{
// XXX
// enable gpio port peripheral clock
// hard coded, out of article scope
#define RCC *(volatile uint32_t*)(0x40021014)
RCC |= (1 << 18);
}
static void set_pin(value_type mask) { BSRR = mask; }
static void clear_pin(value_type mask) { BRR = mask; }
static void set_value(value_type value) { ODR = value; }
static value_type get_value() { return IDR; }
static void set_pin_direction(uint8_t pin, direction dir)
{
uint32_t value = 0b00; // Input by default
uint32_t offset = pin << 1;
auto mask = 0x3 << offset;
int32_t tmp = MODER;
if (dir == direction::Output)
value = 0b01;
tmp &= ~mask;
tmp |= ((value << offset) & mask);
MODER = tmp;
}
};
} // namespace gpio
Let's blink
Nous pouvons maintenant écrire le code applicatif, notre fameuse LED clignotante. La carte d'évaluation utilisée est une NUCLEO32 avec un STM32F303K8. La LED3 de la carte est disponible pour l'utilisateur via la GPIO B3, le port B étant mappé à l'adresse 0x48000400
et dispose de 16 GPIO.
Commençons par spécialiser les templates correspondant.
#include <gpio/gpio.hpp>
#include <gpio/gpio_stm32.hpp>
using port_b = gpio::basic_gpio_port<gpio::gpio_stm32<0x48000400, 16>>;
using gpio_b3 = port_b::gpio<3>;
Dans notre fonction principale nous devons donc initialiser le port, configurer la GPIO en sortie et ensuite nous pourrons changer l'état. Pour illustrer l'utilisation de détection à la compilation de traits de type, nous allons faire trois opérations, allumer la LED, l'éteindre et la faire clignoter via la fonction membre toggle.
#include <gpio/gpio.hpp>
#include <gpio/gpio_stm32.hpp>
using port_b = gpio::basic_gpio_port<gpio::gpio_stm32<0x48000400, 16>>;
using gpio_b3 = port_b::gpio<3>;
int main(void)
{
port_b::setup();
gpio_b3::set_direction(gpio::direction::Output);
gpio_b3::set();
gpio_b3::clear();
while(1) {
gpio_b3::toggle();
}
return 0;
}
Analyse de l'assembleur généré
Au regard de l'implémentation C++, on peut supposer que l'on trouvera dans l'assembleur simplement un store
pour les fonctions set
et clear
ainsi qu'un load
, xor
, store
pour le toggle
qui utilise l'implémentation soft au lieu du registre spécifique (dont ne dispose pas le STM32). Le code assembleur de notre main
est le suivant :
08000568 <main>:
/* (1) */
8000568: 4a0d ldr r2, [pc, #52] ; (80005a0 <main+0x38>)
800056a: 6953 ldr r3, [r2, #20]
800056c: f443 2380 orr.w r3, r3, #262144 ; 0x40000
8000570: 6153 str r3, [r2, #20]
/* (2) */
8000572: f04f 4390 mov.w r3, #1207959552 ; 0x48000000
/* (3) */
8000576: f8d3 2400 ldr.w r2, [r3, #1024] ; 0x400
800057a: f022 02c0 bic.w r2, r2, #192 ; 0xc0
800057e: f042 0240 orr.w r2, r2, #64 ; 0x40
8000582: f8c3 2400 str.w r2, [r3, #1024] ; 0x400
/* (4) */
8000586: 2208 movs r2, #8
/* (5) */
8000588: f8c3 2418 str.w r2, [r3, #1048] ; 0x418
/* (6) */
800058c: f8c3 2428 str.w r2, [r3, #1064] ; 0x428
/* (7) */
8000590: f8d3 2410 ldr.w r2, [r3, #1040] ; 0x410
8000594: f082 0208 eor.w r2, r2, #8
8000598: f8c3 2414 str.w r2, [r3, #1044] ; 0x414
800059c: e7f8 b.n 8000590 <main+0x28>
800059e: bf00 nop
80005a0: 40021000 .word 0x40021000
- 1 : Configuration de la clock du périphérique
- 2 : Chargement de l'adresse de base pour utiliser les GPIO dans
r3
(attention ce n'est pas tout à fait l'adresse de base du port) - 3 : Configuration de la gpio_b3 en sortie
- 4 : Chargement de la valeur immédiate 8 (correspond au bit 3) dans
r2
- 5 : set de la GPIO en utilisant le registre
BSRR
- 6 : clear de la GPIO en utilisant le registre
BRR
- 7 : Clignotement de la LED en effectuant successivement lecture, xor et écriture
Nous avons ici un code assembleur attendu, ce dernier est très compact et correspond à ce à quoi on pourrait s'attendre si on avait écrit cela en accédant directement au registre, c'est-à-dire sans aucune abstraction.
Conclusion
Nous avons ici un code de haut niveau, sans détail matériel (dans un cas réel, la spécialisation des templates pourrait se trouver dans un fichier BSP avec un alias fonctionnel pour gpio_b3
). Cependant, ici, tout est résolu à la compilation, il n'y a donc pas de déréférencement de pointeur de fonction pour appeler la fonction adéquate, encore moins d'appel à des méthodes virtuelles comme en C++ plus conventionnel. En contre partie, les définitions sont statiques, il faut donc produire un firmware par configuration de carte, mais ce problème peut être adressé en générant du code (les spécialisations de template) à partir d'un langage de description du matériel (e.g. device tree, à l'image de ce que peut faire ZephyrOS).