STM8s partie 1, environnement de dev et I/O standards
RetourIntro
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’est0000 0001
.<<
veut dire qu’on décale vers lagauche
les bits- Ici on décale donc ce
1
deN
bits vers la gauche
Remplaçons ce N
par une vraie valeur pour comprendre :
1 << 0
donne0000 0001
(bit 0 activé).1 << 3
donne0000 1000
(bit 3 activé).1 << 5
donne0010 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 modepush 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é a1
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 :
- Elle lit l’état actuel du bouton (sur la broche indiquée)
- 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
- 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.
Retour