P0pR0cK5's Blog

Pentest, Challenges, Tests and more ...

View on GitHub

STM8s partie 1, environnement de dev et I/O standards

Retour

Intro

En parcourant les sites de composants électroniques chinois, je suis tombé sur une petite carte à base de STM8 proposée à seulement 45 centimes. Habitué aux cartes plus répandues comme les Arduino ou les cartes a base de STM32 (toutes compatible avec l’environnement Arduino), je me suis dit que cette curiosité méritait un détour. Derrière son apparente simplicité, elle offre un terrain d’expérimentation idéal pour revenir aux bases du développement bas niveau.

Le STM8 est une architecture 8 bits minimaliste, bien loin des microcontrôleurs puissants et riches en périphériques auxquels on s’habitue rapidement. Ses nombreuses limitations comme sa mémoire réduite, sa puissance de calcul ou sont écosystème logiciel restreint semblent décourageantes au premier abord. Mais c’est précisément ce cadre contraint qui en fait un excellent outil pédagogique : il force à comprendre ce qui se passe réellement dans la machine, à optimiser chaque ligne de code, et à manipuler directement les registres plutôt que de s’abriter derrière des bibliothèques haut-niveau.

Dans ce blog, j’explorerai cette carte STM8 « blue pill » à travers une suite logicielle entièrement libre : SDCC comme compilateur, VSCode comme environnement de travail, et bien sûr Linux comme système hôte. L’objectif n’est pas seulement de faire tourner quelques exemples, mais surtout d’apprendre à raisonner à un niveau plus proche du matériel, là où chaque instruction compte.

Matos requis

Il vous faut :

  • Une carte STM8s “blue pill”
  • Un Stlink V2
  • Un PC sous Ubuntu
  • Un Doliprane (au cas ou)

L’environnement

Je débute par l’installation de SDCC :

## telecharger la derniere version de SDCC
$ wget https://sourceforge.net/projects/sdcc/files/snapshot_builds/amd64-unknown-linux2.5/sdcc-snapshot-amd64-unknown-linux2.5-20210621-12488.tar.bz2

$ tar -xjf ./sdcc-snapshot-amd64-unknown-linux2.5-20210621-12488.tar.bz2
$ sudo mv sdcc /opt
$ echo "export PATH=\$PATH:/opt/sdcc/bin" >> ~/.bashrc
$ source ~/.bashrc

## test de SDCC
$ sdcc -v

SDCC : mcs51/z80/z180/r2k/r2ka/r3ka/sm83/tlcs90/ez80_z80/z80n/ds390/TININative/ds400/hc08/s08/stm8/pdk13/pdk14/pdk15/mos6502 4.2.0 #13081 (Linux)
published under GNU General Public License (GPL)

Puis j’installe stm8flash

$ git clone https://github.com/vdudouyt/stm8flash.git
$ cd stm8flash
$ make
$ sudo make install

Pour Vscode, je vous laisse vous débrouiller selon votre distribution.

Testons rapidement avec un code qui fais clignoter une led :

#define PB_ODR *(volatile char*)0x5005
#define PB_DDR *(volatile char*)0x5007
#define PB_CR1 *(volatile char*)0x5008

void main() {

  PB_CR1= (1 << 5);
  PB_DDR = (1 << 5);

  while(1) {
    PB_ODR ^= (1 << 5);
    for(int i = 0; i < 30000; i++){;}
  }

}

Note : nous expliquerons ce code plus tard.

Puis je compile le code :

sdcc -mstm8 --out-fmt-ihx --std-sdcc11 main.c 

Et je l’upload sur la carte :

stm8flash -c stlinkv2 -p stm8s003f3 -w main.ihx

A ce stade la led de test situé sur la carte devrait clignoter.

Comprendre les registres

La puce STM8, comme de nombreuses puces, utilise des registres appelés port pour configurer et utiliser ses GPIO.

Un port (comme PORTA, PORTB, etc.) sur un microcontrôleur, c’est un groupe de petites “interrupteurs” que tu peux contrôler individuellement depuis ton programme. Chaque porte correspond à une broche physique (un pin) de la puce.

Pour savoir si une porte doit être ouverte (mettre du courant = niveau haut, logique 1) ou fermée (pas de courant = niveau bas, logique 0), on utilise une sorte de tableau interne qu’on appelle un registre.

Un registre, c’est juste une case mémoire spéciale de 8 cases (on est sur une architecture 8 bits), où chaque case (chaque bit) correspond à une broche du port :

  • Si le bit vaut 1, la broche envoie du courant (sortie à l’état haut).
  • Si le bit vaut 0, la broche reste éteinte (sortie à l’état bas).

Prenons un exemple :

Le registre du PORTA contrôle toutes les broches du port A matérialisé par PA1, PA2 etc.. Si tu écris 0000 1000 dans ce registre, seule la broche numéro 3 (en partant de 0) passera à l’état haut (PA3).

Maintenant il faut aussi comprendre qu’il y a souvent plusieurs registres associés à un port :

  • ODR (Output Data Register) : dit quelle valeur (0 ou 1) tu veux envoyer.
  • IDR (Input Data Register) : permet de lire l’état de la broche si elle est configurée en entrée.
  • DDR (Data Direction Register) : dit si la broche doit être une sortie (écrire) ou une entrée (lire).

Donc pour bosser avec une GPIO :

  • Il faut choisir si la broche est une entrée ou une sortie (DDR).
  • Il faut lire ou écrire dans le registre correspondant (IDR ou ODR).
  • Chaque bit contrôle une broche du microcontrôleur.

C’est comme un tableau de 8 switchs que tu les règles avec ton programme.

Manipuler les registres

Une fois que l’on comprends que tout est une question de registre, il faut comprendre comment les manipuler. Chaque registre est situé a une adresse mémoire qui est fixé par le datasheet de la puce. Dans notre exemple nous allons pour le moment ignorer cet aspect que nous détaillerons plus tard.

La majorité des opération sur les registres se réalise avec des décalage de bits. Prenons un premier exemple :

Exemples de base

REG |= (1 << N);

Pour 1 << N :

  • 1 en binaire, c’est 0000 0001.
  • << veut dire qu’on décale vers la gauche les bits
  • Ici on décale donc ce 1 de N bits vers la gauche

Remplaçons ce N par une vraie valeur pour comprendre :

  • 1 << 0 donne 0000 0001 (bit 0 activé).
  • 1 << 3 donne 0000 1000 (bit 3 activé).
  • 1 << 5 donne 0010 0000 (bit 5 activé).

Ici, on réalise un masque avec un seul bit qui est celui que l’on souhaite modifier.

Pour REG |= :

  • |= réalise un OR sur la valeur du registre et la réécris dans ce dernier.
  • L’opération logique OR (OU) garde les 1 déjà présents, et ajoute le nouveau 1.
    • 0 | 0 = 0
    • 0 | 1 = 1
    • 1 | 0 = 1
    • 1 | 1 = 1

Le but est de passer un bit du registre a 1 sans toucher aux autres.

Exemples d’autres manipulations plus complexes

uint8_t REG = 0b10101100;  // Exemple de registre

    // 1. Lire un bit N (exemple N=5)
    if (REG & (1 << 5)) {
        // le bit 5 est a 1
    } else {
        // le bit 5 est a 0
    }

    // 2. Mettre un bit N à 1 (exemple N=0)
    REG |= (1 << 0);  // Met bit 0 à 1

    // 3. Mettre un bit N à 0 (exemple N=3)
    REG &= ~(1 << 3);  // Met bit 3 à 0

    // 4. Inverser un bit N (exemple N=2)
    REG ^= (1 << 2);   // Toggle bit 2

    // 5. Mettre plusieurs bits (ex: bits 2 et 5) à 1
    REG |= (1 << 2) | (1 << 5);

    // 6. Mettre plusieurs bits (ex: bits 2 et 5) à 0
    REG &= ~((1 << 2) | (1 << 5));

    // 7. Lire plusieurs bits (exemple bits 3 et 4)
    uint8_t mask = (1 << 3) | (1 << 4);
    uint8_t bits_val = (REG & mask) >> 3;  // Décalage à droite pour s'aligner sur le bit 0
    // bits_val contient la valeur des bits 4 et 3 dans les bits 1 et 0

    // 8. Écrire plusieurs bits (ex: bits 2 et 3) avec une valeur donnée (val=0b10)
    mask = (1 << 2) | (1 << 3);
    uint8_t val = 0b10;
    REG = (REG & ~mask) | ((val << 2) & mask);

Faire clignoter une LED

Dans le datasheet de notre puce se trouve un tableau qui donne les adresses mémoire de tous les registres liées aux GPIO :

Dans notre exemple nous allons connecter une LED sur le port D3 de notre carte :

Il faut ensuite configurer les registres du port D en suivant les informations du datasheet :

Détaillons ce que font chacun d’entre eux :

  • DDR : Data direction register définis si le pin GPIO seras une sortie ou une entrée
  • ODR : Output Data register sert a définir l’état du pin GPIO
  • IDR : Input Data Register sert a lire l’état d’un GPIO
  • CR1 et CR2 servent a configurer le comportement du GPIO

Je définis donc dans mon programme les adresses mémoire des registres que je vais modifier :

//Definition des 3 adresses des registres du port D conformément au datasheet
#define PD_ODR *(volatile char*)0x500F
#define PD_DDR *(volatile char*)0x5011
#define PD_CR1 *(volatile char*)0x5012

Puis je configure le port D :

  // Configuration du pin 3 du port D (PD3)
  PD_CR1= (1 << 3); // en mode push-pull (valeur 1)
  PD_DDR = (1 << 3); // en mode output (valeur 1)

En fait, ici je réécris tout le registre car je ne compte pas garder les valeurs qui y sont déjà stockés.

  • CR1 est placé en mode push pull afin d’avoir deux transistor qui “pousse” la sortie a la tension de la sortie a VCC ou “tire” la tension de la sortie a GND.
    • Cela permets d’avoir un “vrai” 1 ou 0 sur la sortie sans avoir a utiliser de résistance de pull-up externe pour obtenir un sortie a VCC.
  • DDR est placé a 1 pour mettre le pin en mode “sortie”

Pour finir on crée une boucle infinie qui fais clignoter la led :

  while(1) { // boucle infini
    PD_ODR ^= (1 << 3); 
    for(int i = 0; i < 30000; i++){;}
  }
  • ODR est placé simultanément a 1 et 0 avec un “toggle”
    • Si la sortie est a 1, elle passe a 0 et si elle est a 0, elle passe a 1
  • On réalise ensuite une boucle for qui ne fais rien pour créer un délai imprécis mais efficace

Voici le code complet :

//Definition des 3 adresses des registres du port D conformément au datasheet
#define PD_ODR *(volatile char*)0x500F
#define PD_DDR *(volatile char*)0x5011
#define PD_CR1 *(volatile char*)0x5012

void main() { // Fonction principale

  // Configuration du pin 3 du port D (PD3)
  PD_CR1= (1 << 3); // en mode push-pull (valeur 1)
  PD_DDR = (1 << 3); // en mode output (valeur 1)

  while(1) { // boucle infini
    PD_ODR ^= (1 << 3); // toggle de la valeur du registre 1->0, 0->1
    for(int i = 0; i < 30000; i++){;} // mimick de delay
  }
}

Note : Pour réaliser un blink plus précis il faudrait exploiter un timer

Pour les prochains exemple nous utiliserons le fichier d’entête suivant : stm8s.h

Un delay moins basique

Dans le code précédent nous avons utilisé un délais un peu basique est imprécis pour tester notre blink. Voici une variante moins abrupte et un peut plus élégante :

static inline void delay_ms(uint16_t ms) {
    uint32_t i;
    for (i = 0; i < ((F_CPU / 18000UL) * ms); i++)
        __asm__("nop");         // Instruction vide pour consommer du temps
}

Au lieu de faire une boucle for qui ne fait rien, on réalise des nop en assembleur avec une correlation sur la vitesse d’horloge CPU.

Note : ne pas oublier de définir cette horloge au début du code et du main.

Voici un blink plus propre :

//Definition des 3 adresses des registres du port D conformément au datasheet
#define PD_ODR *(volatile char*)0x500F
#define PD_DDR *(volatile char*)0x5011
#define PD_CR1 *(volatile char*)0x5012

#define F_CPU 16000000UL        // Définition de la fréquence CPU à 16 MHz

// fonction de délai 
static inline void delay_ms(uint16_t ms) {
    uint32_t i;
    for (i = 0; i < ((F_CPU / 18000UL) * ms); i++)
        __asm__("nop");         // Instruction vide pour consommer du temps
}

void main() { // Fonction principale

  CLK_CKDIVR = 0x00; // forcer la fréquence CPU
  // Configuration du pin 3 du port D (PD3)
  PD_CR1= (1 << 3); // en mode push-pull (valeur 1)
  PD_DDR = (1 << 3); // en mode output (valeur 1)

  while(1) { // boucle infini
    PD_ODR ^= (1 << 3); // toggle de la valeur du registre 1->0, 0->1
    delay_ms(1000); // délai plus propre
  }
}

Entrée binaire

Pour l’exemple suivant nous allons câbler un bouton sur la carte STM8s :

[3.3V]
   |
  [R] 10k pull-up
   |
  PA3 ---- Bouton ---- GND

Nous allons configurer nos ports pour utiliser PA3 en entrée :

PA_DDR &= ~(1 << 3);  // PA3 en entrée
PA_CR1 |= (1 << 3);   // Pull-up interne activée
PA_CR2 &= ~(1 << 3);  // Interruption désactivée (pour l’instant)

Note : l’entrée est effectivement configurée en mode pull-up mais je préfère tout de même en rajouter une externe

Il est ensuite possible de récupérer l’état du bouton avec ce code :

if (!(PA_IDR & (1 << 3))) {
    PB_ODR ^= (1 << 5);  // Inverse LED sur PB5
    delay_ms(200);       // Anti-rebond simple
}

Note : un bouton est un élément physique imparfait dont le contact a tendance a rebondir. Se faisant des information parasitent l’entrée et redent le bouton inefficace avec des comportement imprédictibles. Le faible délai rajouté dans le code sert a partiellement supprimer ce problème mais reste non fiable.

Voici un code permettant de mieux gérer les rebonds :

int8_t button_pressed(volatile uint8_t* idr, uint8_t pin) {
    static uint8_t last_state[8] = {0xFF};  // Mémorise les derniers états (1 = repos, car pull-up active)
    uint8_t current_state = *idr & (1 << pin);  // Lecture du bit correspondant au bouton

    // Si changement détecté (rebond ou appui réel)
    if (current_state != (last_state[pin] & (1 << pin))) {
        delay_ms(5);  // Pause pour laisser passer les rebonds
        current_state = *idr & (1 << pin);  // Relire après stabilisation

        // Si l’état est toujours différent après le délai
        if (current_state != (last_state[pin] & (1 << pin))) {
            last_state[pin] = *idr;         // Mémoriser le nouvel état
            if (!(current_state)) {         // Si le niveau est bas (appui)
                return 1;                   // Retourner 1 : bouton pressé
            }
        }
    }

    return 0;  // Aucun appui détecté
}

Cette fonction fait trois choses :

  1. Elle lit l’état actuel du bouton (sur la broche indiquée)
  2. Si l’état a changé par rapport au précédent :
    • Elle attend 20 ms
    • Elle vérifie si le changement est stable
    • Si c’est un appui (niveau bas), elle retourne 1
  3. Sinon, elle retourne 0 (pas de nouvel appui)

Voici le code final :

#include "../stm8s.h"           // Inclusion au STM8S
#define F_CPU 16000000UL        // Définition de la fréquence CPU à 16 MHz

// Fonction de délai (bloquante) pour simuler delay_ms
static inline void delay_ms(uint16_t ms) {
    uint32_t i;
    for (i = 0; i < ((F_CPU / 18000UL) * ms); i++)
        __asm__("nop");         // Instruction vide pour consommer du temps
}

// Fonction de détection d'appui sur un bouton avec anti-rebond
int8_t button_pressed(volatile uint8_t* idr, uint8_t pin) {
    static uint8_t last_state[8] = {0x00};  // Mémorise les derniers états (1 = repos, car pull-up active)
    uint8_t current_state = *idr & (1 << pin);  // Lecture du bit correspondant au bouton

    // Si changement détecté (rebond ou appui réel)
    if (current_state != (last_state[pin] & (1 << pin))) {
        delay_ms(5);  // Pause pour laisser passer les rebonds
        current_state = *idr & (1 << pin);  // Relire après stabilisation

        // Si l’état est toujours différent après le délai
        if (current_state != (last_state[pin] & (1 << pin))) {
            last_state[pin] = *idr;         // Mémoriser le nouvel état
            if (!(current_state)) {         // Si le niveau est bas (appui)
                return 1;                   // Retourner 1 : bouton pressé
            }
        }
    }

    return 0;  // Aucun appui détecté
}

void main() {
    // --- Configuration de la LED intégrée sur PB5 ---
    PB_DDR |= (1 << 5);      // Direction : sortie
    PB_CR1 |= (1 << 5);      // Sortie push-pull

    // --- Configuration du bouton sur PA3 ---
    PA_DDR &= ~(1 << 3);    // PA3 en entrée
    PA_CR1 |= (1 << 3);     // Pull-up interne activée

    // --- Boucle principale ---
    while (1) {
        if (button_pressed(&PA_IDR, 3)) {   // Si le bouton est pressé (PA3 à 0)
            PB_ODR ^= (1 << 5);             // Inverser l’état de la LED sur PB5
        }
    }
}

Conclusions

Nous avons expliqué et testé des usages basiques des GPIO du STM8 en exploitant uniquement les registres et sans fonctions haut niveau. Dans le prochain épisode nous mettrons en place les sorties série afin de pouvoir discuter avec notre STM8 et réaliser des actions basiques de debug.


Written on August 20, 2025 by


Retour