P0pR0cK5's Blog

Pentest, Challenges, Tests and more ...

View on GitHub

STM8s partie 3, le protocole 1-wire

Retour

Intro

Dans cette partie, nous allons étudier et implémenter le protocole 1-wire pour lire une valeur de température sur un capteur Dallas DS18b20.

Le protocole 1-wire : briques de base

Le bus 1-Wire est un protocole de communication série développé par Dallas/Maxim. Son nom vient du fait qu’il utilise une seule ligne de données (plus une masse commune) pour dialoguer entre un maître (microcontrôleur) et un ou plusieurs esclaves (capteurs).

Le bus utilise un seul fil avec une masse commune avec la possibilité d’alimenter certains modules directement sur la ligne de données (Mode parasite power).

Le bus utilise une architecture mâitre/Esclave avec un seul maître qui contrôle le bus et plusieurs esclaves qui s’y connectent et sont identifié par un identifiant unique de 64 bit.

La communication se réalise avec des fronts montant et descendant aillant des durées spécifiques qui seront détaillés et intégré dans ce billet.

Le protocole implémente dans son datasheet diverses actions basiques de lecture et d’écriture de données que nous détaillerons ici afin de les implémenter avec du code.

[wiring]

Le bus est placée au niveau logique 1 a l’aide d’une résistance de 4,7 K ohm située entre VCC et DQ. Cela veut dire qu’une ligne de bus 1-wire est toujours placée au niveau logique quand elle est libre.

Ici je vais ignorer volontairement le cas d’usage avec plusieurs esclave sur le bus pour simplifier mon code et la compréhension du protocole

Macro pour manipuler le bus 1-wire

Pour contrôler le bus 1-wire, je code des macro rapide que je pourrais ensuite rappeler dans les fonctions afin de gagner du temps :

// === Macros bas-niveau pour manipuler la pin PD3 en mode 1-Wire ===
#define DS_LOW()    (PD_ODR &= ~(1 << DS_PIN))           // Force PD3 à 0V (sortie basse)
#define DS_HIGH()   (PD_ODR |=  (1 << DS_PIN))           // Force PD3 à 1 (sortie haute)
#define DS_INPUT()  (PD_DDR &= ~(1 << DS_PIN))           // Configure PD3 en entrée
#define DS_OUTPUT() (PD_DDR |=  (1 << DS_PIN))           // Configure PD3 en sortie
#define DS_READ()   (PD_IDR & (1 << DS_PIN))             // Lit l'état logique de PD3 (1 ou 0)

Elle rendent simplement le code plus lisible pour la suite et permettent de réaliser les actions d’écritures et de lecture de front montant/descendant sur le bus.

Timings et délais

Dans le but de gérer les délais requis au bon fonctionnement du bus, j’ai réalisé deux fonctions de délai. L’une en ms et l’autre en µs.

// === Temporisations ===
// Délai approximatif en microsecondes (fonction lente et approximative)
void delay_us(uint16_t us) {
    while(us--) {
        __asm__("nop"); __asm__("nop"); __asm__("nop");
        __asm__("nop"); __asm__("nop"); __asm__("nop");
    }
}

// Délai en millisecondes (approx. pour F_CPU = 16 MHz)
static inline void delay_ms(uint16_t ms) {
    uint32_t i;
    for (i = 0; i < ((F_CPU / 18000UL) * ms); i++)
        __asm__("nop");
}

Ce code n’est pas absolut mais reste dans les tolérances du protocole 1-wire.

Reset + Presence

Le reset + présence est une séquence spéciale du protocole 1-Wire qui sert à la synchronisation et la détection des périphériques avant tout échange de données.

Pour le reset, le maître force la ligne bas pendant un temps long d’au moins 480 µs (bien plus long qu’un slot de lecture/écriture de 60 µs).

Une fois la ligne relâchée par le maître, il attend 15 a 60 µs. Durant cette fenêtre, chaque périphérique connecté au bus 1-Wire tire la ligne a zéro pendant environ 60 a 240 µs. Cette impulsion basse est appelée pulse de présence.

Voici un chronogramme de cette séquence :

Temps [µs]   0                          480          495..540          555..720
Bus        ──────────────────────────────┐            ┌───────┐          ┌───────────
                                         │  Reset     │ Idle  │ Presence │    Idle
                                         └────────────┘       └──────────┘

Et voici le code qui l’implémente :

// Reset pulse + lecture présence
uint8_t onewire_reset(void) {
    uint8_t presence;

    DS_OUTPUT();
    DS_LOW();
    delay_us(480);       // tRSTL >= 480 µs
    DS_INPUT();          // relâcher la ligne
    delay_us(70);        // attendre fenêtre tPDHIGH (15–60 µs)
    presence = DS_READ() == 0; // esclave doit tirer bas (presence pulse)
    delay_us(410);       // attendre fin du cycle (~960 µs total)
    return presence;
}

Une fois cette action réalisée il deviens alors possible de discuter avec le capteur sur le bus 1-Wire

Écriture d’un bit (Write slots)

Le datasheet du capteur DS18b20 décris les deux chronogrammes suivants pour écrire la valeur 1 sur le bus de donnée :

Temps [µs]   0        6      15       60
Bus        ──┐        ┌────────────────────────────────
             │ 6 µs   │   fin de slot (ligne haute via pull-up)
             └────────┘

La ligne est tirée a 0 durant +/- 6 µs (tLOW1 étant comprise entre 1 et 15 µs) puis relâchée (remonte via pull-up) pour la fin du slot. La durée totale du slot est de +/- 60 µs (tSLOT).

Enfin, elle décris le chronogramme suivant pour écrire 0 sur le bus de donnée :

Temps [µs]   0               15           60
Bus        ──┐                          ┌──────────────
             │  ~60 µs bas (tLOW0)      │  fin de slot
             └──────────────────────────┘

La ligne est tirée a 0 durant +/- 60 µs (tLOW0 étant compris entre 60 et 120 µs) puis elle est relâchée juste avant la fin du slot.

Voici le code qui réalise cela :

void onewire_write_bit(uint8_t bit) {
    DS_OUTPUT();         // Placer le pin DS en sortie
    DS_LOW();			 // Placer le bus a LOW
    if (bit) {
        delay_us(6);     // bit = 1 -> impulsion courte
        DS_INPUT();      // relâcher
        delay_us(64);    // compléter slot ~70 µs
    } else {
        delay_us(60);    // bit = 0 -> impulsion longue
        DS_INPUT();      // relâcher
        delay_us(10);    // compléter slot ~70 µs
    }
}

Lecture d’un bit (Read slot)

Pour lire une valeur sur le bus il faut tirer la ligne a 0 durant 6 µs avant de la relâcher pour demander au capteur de nous envoyer des données. Une attente de 15 µs est ensuite réalisé avant de lire la valeur sur le bus.

L’idée est d’aller lire la valeur du bus au moment optimale pour déterminer la valeur du bit émis par le capteur :

Temps [µs]   0        6          15       60
Bus a 0       ──┐        ┌───────│────────────────────────
                │ 6 µs   │       │
                └────────┘       │
Bus  a 1      ──┐                │         ┌──────────────
                │  60 µs         │         │
                └────────────────│─────────┘
                                 │
                                 │
                              Lecture

Si le bus est a 0, le bit vaut 0 et si le bus est a 1 alors la valeur est a 1.

Le code est donc le suivant :

uint8_t onewire_read_bit(void) {
    uint8_t bit;

    DS_OUTPUT();           // Placer le pin DS en sortie
    DS_LOW();              // Placer le bus a LOW
    delay_us(6);           // Attendre TINIT (≥1 µs)
    DS_INPUT();            // Relâcher la ligne du bus
    delay_us(9);           // Attendre tRDV ~15 µs
    bit = DS_READ();       // lire la valeur
    delay_us(55);          // fin du slot ~70 µs total
    return bit;
}

Le protocole 1-wire : briques avancée

Maintenant que l’on est capable de réaliser des actions basiques sur le bus 1-wire il faut maintenant les assembler pour réaliser des actions concrétés.

Écrire un octet sur le bus 1-wire

Nous allons débuter par la lecture d’octets sur le bus en se basant sur les fonctions précédemment crée :

// Écrit un octet complet (8 bits)
void onewire_write_byte(uint8_t byte) {
    for (uint8_t i = 0; i < 8; i++) {
        onewire_write_bit(byte & 0x01); // Envoie le bit LSB
        byte >>= 1;
    }
}

Ici le bus travail en mode LSB first, prenons un exemple pour illustrer cela :

octet initial  :  b7 b6 b5 b4 b3 b2 b1 b0
ordre d'envoie :  b0 -> b1 -> b2 -> b3 -> b4 -> b5 -> b6 -> b7

Ici la boucle fonctionne ainsi :

  1. Extraire le bit de poids faible du byte avec byte & 0x01
  2. Envoyer le bit sur le bus avec onewire_write_bit().
  3. Décaler l’octet d’un cran vers la droite (>>= 1) pour amener le bit suivant en position LSB.
  4. Répéter ceci 8 fois.

Dans la partie du code byte & 0x01, l’opérateur & (ET logique) avec 0x01 permet d’isoler uniquement le bit de poids faible de notre octet.

Prenons exemple avec un octet 0b10110110 (0xB6) :

  • Itération 1 :
    • byte & 0x01 = 0b10110110 & 0b00000001 = 0.
    • Nous écrivons 0 sur le bus avec onewire_write_bit()
    • Puis byte >>= 1 (décalage) : 0b01011011.
  • Itération 2 :
    • 0b01011011 & 0x01 = 1.
    • Nous écrivons 1 sur le bus avec onewire_write_bit()
    • Puis décalage : 0b00101101.
  • Itération 3 :
    • 0b00101101 & 0x01 = 1.
    • Nous écrivons 1 sur le bus avec onewire_write_bit()
    • Décalage : 0b00010110.
  • Et l’on continue ainsi jusqu’à arriver a 8 bit

Lire un octet sur le bus 1-wire

Enfin, nous allons lire des octets sur le bus 1-wire et les écrire dans la variable vide byte avec le code suivant :

// Lit un octet depuis le bus
uint8_t onewire_read_byte(void) {
    uint8_t byte = 0;
    for (uint8_t i = 0; i < 8; i++) {
        byte >>= 1;
        if (onewire_read_bit()) byte |= 0x80; // Lit MSB en premier
    }
    return byte;
}

Cette fois-ci nous réalisons l’action inverse :

  1. l’octet byte est décalé vers la droite
    1. libère les bits de poids fort pour recevoir les valeurs du bus
    2. décale les bits précédemment lus vers les positions de poids faibles
  2. lecture d’un bit sur le bus avec if (onewire_read_bit()) byte |= 0x80;
    1. Si le bit vaut 1 alors |= 0x80 force le bit de poids fort a 1
    2. Sinon rien ne change rien (le bit reste donc a 0)
  3. On recommence 8 fois.

|= 0x80 force à 1 le bit de poids fort (bit 7) d’un octet, sans modifier les autres bits. C’est l’opposé logique de l’opération val & 0x01 (qui isole le bit de poids faible) On agit donc sur deux extrémités différentes du registre :

  • & 0x01 sert à lire ou tester le bit le plus bas.
  • |= 0x80 sert à activer le bit le plus haut.

Exemple :

uint8_t val = 0x12;   // binaire : 0001 0010
val |= 0x80;          // applique un OR avec 1000 0000

Détaillons le calcul qui est réalisé :

   0001 0010   (val = 0x12)
OR 1000 0000   (0x80)
   ---------
   1001 0010   (résultat = 0x92)

La logique de lecture est réalisée avec if (onewire_read_bit()) qui place le bit a 1 uniquement si un bit est lu sur le bus.

Prenons un exemple avec l’octet 0b10110110 (0xB6). Sur le bus 1-Wire, les bits arrivent en mode LSB first ce qui donne une séquence d’arrivée des bits de 0,1,1,0,1,1,0,1.

Réalisons une itération pour mieux comprendre avec byte initialisé a 0 (00000000):

  1. Nous décalons byte et obtenons la valeur 00000000
    1. Lecture sur le bus de 0, rien à mettre.
    2. byte= 00000000.
  2. Nous décalons byte et obtenons la valeur 00000000.
    1. Lecture sur le bus de 1 et plaçons donc 1 en MSB.
    2. byte=10000000.
  3. Nous décalons byte et obtenons la valeur 01000000.
    1. Lecture sur le bus de 1 et plaçons donc 1 en MSB.
    2. byte=11000000.
  4. Nous décalons byte et obtenons la valeur 01100000.
    1. Lecture sur le bus de 0, rien à mettre.
    2. byte= 01100000.
  5. Nous décalons byte et obtenons la valeur 00110000.
    1. Lecture sur le bus de 1 et plaçons donc 1 en MSB.
    2. byte= 10110000.
  6. Nous décalons byte et obtenons la valeur 01011000.
    1. Lecture sur le bus de 1 et plaçons donc 1 en MSB.
    2. byte= 11011000.
  7. Nous décalons byte et obtenons la valeur 01101100.
    1. Lecture sur le bus de 0, rien à mettre.
    2. byte= 01101100.
  8. Nous décalons byte et obtenons la valeur 00110110.
    1. Lecture sur le bus de 1 et plaçons donc 1 en MSB.
    2. byte= 10110110.

Résultat final : 0b10110110 (0xB6) ce qui est conforme à ce que l’esclave a envoyé.

Implémentation du protocole propre au DS18B20

Maintenant que l’on est en capacité de lire et écrire des données sur le bus, il faut implémenter les fonctions propres a notre capteur de température.

Le capteur réalise une lecture de température (Convert T) puis écris le résultat dans une zone mémoire (le scratchpad). Il faut ensuite lire cette valeur qu’il faut ensuite convertir dans le code pour obtenir des degrés.

Le scratchpad contient les informations suivantes :

Temp LSB
Temp MSB
Th register (high alarm)
Tl register (low alarm)
Configuration (résolution, etc.)
Reserved
Reserved
Reserved
CRC

Nous ne lirons ici que les deux premier octets.

  1. Les étapes de lecture d’un valeur de température débutent donc par l’émission sur le bus d’une demande de conversion :
    1. Envoie d’un reset sur le bus (obligatoire)
    2. Envoie de la commande Skip ROM (0xCC) pour lire la valeur directement sans avoir a gérer d’adresse (nous n’avons qu’un esclave sur le bus)
    3. Envoie de la commande Convert T (0x44) pour lancer la mesure
  2. Puis une lecture de la valeur brute :
    1. Envoie d’un reset sur le bus (obligatoire)
    2. Envoie de la commande Skip ROM (0xCC) pour lire la valeur directement sans avoir a gérer d’adresse (nous n’avons qu’un esclave sur le bus)
    3. Envoie de la commande Read Scratchpad (0xBE).

Implémentons ce code :

// Démarre une conversion de température (commandes 1-Wire)
void ds18b20_start_conversion(void) {
    onewire_reset();
    onewire_write_byte(0xCC); // Skip ROM (capteur unique sur le bus)
    onewire_write_byte(0x44); // Convert T (lance mesure)
}

// Lit la température brute (valeur sur 16 bits, unité = 0.0625 °C)
int16_t ds18b20_read_raw(void) {
    onewire_reset();
    onewire_write_byte(0xCC); // Skip ROM
    onewire_write_byte(0xBE); // Read Scratchpad

    uint8_t lsb = onewire_read_byte(); // LSB = partie fractionnaire
    uint8_t msb = onewire_read_byte(); // MSB = partie entière signée

    return ((int16_t)msb << 8) | lsb;  // Fusionne les 2 octets
}

Obtenir un température

Le DS18b20 retourne une valeur sur 16 bit (2 x 8 bit) sous la forme de complément a 2 avec un résolution de 1/16 de degrés.

Complément a 2

Le complément a 2 est une méthode de codage de valeur positive ou négative, pour passer d’un entier positif à sa valeur négative :

  1. On écrit le nombre en binaire.
  2. On inverse tous les bits.
  3. On ajoute 1 au résultat.

Exemple avec -5 sur 8 bits :

  • +5 = 0000 0101
  • inversion : 1111 1010
  • +1 = 1111 1011 : c’est -5 en complément à deux.

Cette méthode peremets de faire des calculs sur les valeurs sans manipulation spécifique :

0000 0101
+1111 1011
-----------
1 0000 0000

Si on ignore le dépassement a gauche, on obtiens bien 0.

Obtenir une température

Pour obtenir notre température, il suffit de prendre la valeur retournée par notre fonction ds18b20_read_raw qui fusionne les valeurs du MSB et du LSB et de la diviser par 16 pour obtenir une température :

float tempC = raw / 16.0f;

De par la limitation du STM8s, j’ai tendance a éviter d’utiliser des float et code plutôt les valeurs sous forme d’entiers de 16 bit :

int16_t temp_x100 = (raw * 625UL) / 100; // Résultat en °C * 100

Raw est la valeur brut de température sortie du scratchpad du ds18b20 qui est ensuite convertis en centième de degrés. Cela rends l’affichage sur la sortie série un peut plus complexe mais réalisable :

// Affiche formaté  2437 => 24.37 °C
printf("Température : %d.%02d °C\r\n", temp_x100 / 100, temp_x100 % 100);

Ici nous exploitons la division direct pour la partie entière de la température et le reste pour la partie décimale.

Pour le moment je n’ai pas eu l’occasion de tester la mesure en température négative.

Conclusion

Nous avons ici expliqué et implémenté de manière basique le protocole 1-wire sur STM8s. Bien que ce code ignore la gestion des addresses, il permets de réaliser simplement des petits projets autour du STM8s et du DS18b20.


Written on September 14, 2025 by


Retour