STM8s partie 3, le protocole 1-wire
RetourIntro
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 :
- Extraire le bit de poids faible du
byte
avecbyte & 0x01
- Envoyer le bit sur le bus avec
onewire_write_bit()
. - Décaler l’octet d’un cran vers la droite (
>>= 1
) pour amener le bit suivant en position LSB. - 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 :
- l’octet
byte
est décalé vers la droite- libère les bits de poids fort pour recevoir les valeurs du bus
- décale les bits précédemment lus vers les positions de poids faibles
- lecture d’un bit sur le bus avec
if (onewire_read_bit()) byte |= 0x80;
- Si le bit vaut 1 alors
|= 0x80
force le bit de poids fort a 1 - Sinon rien ne change rien (le bit reste donc a 0)
- Si le bit vaut 1 alors
- 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
):
- Nous décalons
byte
et obtenons la valeur00000000
- Lecture sur le bus de
0
, rien à mettre. byte
=00000000
.
- Lecture sur le bus de
- Nous décalons
byte
et obtenons la valeur00000000
.- Lecture sur le bus de
1
et plaçons donc 1 en MSB. byte
=10000000
.
- Lecture sur le bus de
- Nous décalons
byte
et obtenons la valeur01000000
.- Lecture sur le bus de
1
et plaçons donc 1 en MSB. byte
=11000000
.
- Lecture sur le bus de
- Nous décalons
byte
et obtenons la valeur01100000
.- Lecture sur le bus de
0
, rien à mettre. byte
=01100000
.
- Lecture sur le bus de
- Nous décalons
byte
et obtenons la valeur00110000
.- Lecture sur le bus de
1
et plaçons donc 1 en MSB. byte
=10110000
.
- Lecture sur le bus de
- Nous décalons
byte
et obtenons la valeur01011000
.- Lecture sur le bus de
1
et plaçons donc 1 en MSB. byte
=11011000
.
- Lecture sur le bus de
- Nous décalons
byte
et obtenons la valeur01101100
.- Lecture sur le bus de
0
, rien à mettre. byte
=01101100
.
- Lecture sur le bus de
- Nous décalons
byte
et obtenons la valeur00110110
.- Lecture sur le bus de
1
et plaçons donc 1 en MSB. byte
=10110110
.
- Lecture sur le bus de
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.
- Les étapes de lecture d’un valeur de température débutent donc par l’émission sur le bus d’une demande de conversion :
- Envoie d’un reset sur le bus (obligatoire)
- 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)
- Envoie de la commande Convert T (0x44) pour lancer la mesure
- Puis une lecture de la valeur brute :
- Envoie d’un reset sur le bus (obligatoire)
- 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)
- 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 :
- On écrit le nombre en binaire.
- On inverse tous les bits.
- 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.
Retour