Teleinfo 2.0 (Partie 2)

Rédigé par guiguid Aucun commentaire

Décodage Teleinfo partie logicielle

Nous avons vu dans la partie 1, comment réaliser le hardware de façon simplifiée et efficace.

Maintenant attaquons-nous à la partie démodulation et traduction en JSON.

 

Couche Logicielle Arduino

But

Nous allons utiliser la plateforme Arduino basée sur du 328p le plus commun, mais ceci est adaptable sur la grande majorité des autres microcontrôleurs.

Le rôle du logiciel est un peu plus complexe.
Tout d'abord, il doit s'affranchir de la porteuse à 50kHz, puis détecter la vitesse de transmission (1200 bauds pour le protocole historique, ou 9600 bauds pour le nouveau standard,  détecter les bits, les convertir en octets, traduire le protocole reçu, et afficher sur le port USB les informations décodées.
Les informations décodées changent en fonction de l'abonnement de l'usager, du nombre de phases, du mode de communication (historique ou standard Linky). Le but n'étant pas de limiter les usages en définissant arbitrairement les informations que nous enverrons sur le port USB, j'ai fait le choix d'envoyer toutes les informations, et ce sera a l'application réceptrice d'en faire ce que vous lui direz (soit rien, soit un truc génial ! ).
Le format de sortie sera de la forme : CLE:VALEUR en json (ideal pour HomeAssistant).

Démodulation de la porteuse de 50kHz

On gardera un principe KISS non bloquant suivant :

  • On crée une variable qui représentera notre signal démodulé.
  • Un TIMER à une fréquence 1.5 fois inférieure à la porteuse veut mettre cette variable à 0
  • En même temps une Interruption pilotée par le signal Téleinfo remet cette variable à 1.

Ce qui donne :

const byte TELEINFOPIN = 2;
volatile boolean flag;
void setup() {
// initialize digital pin LED_BUILTIN as an output.
pinMode(TELEINFOPIN, INPUT);
pinMode(LED_BUILTIN, OUTPUT);
// set up Timer 1
TCCR1A = 0;          // normal operation
TCCR1B = bit(WGM12) | bit(CS10);   // CTC, no pre-scaling
OCR1A =  600;       // compare A register value  * clock speed) ( 16MHz/ (50kHz/2) )
TIMSK1 = bit (OCIE1A);             // interrupt on Compare A Match
attachInterrupt (digitalPinToInterrupt (TELEINFOPIN), teleinfoISR, RISING);  // attach interrupt handler 
}
// the loop nothing to do for now..
void loop() {
}
// Interrupt Service Routine (ISR)
void teleinfoISR ()
{
TCNT1 = 0x00;            // reset Timer 1
flag = true;
digitalWrite(LED_BUILTIN, HIGH);  
}  // end ISR
ISR(TIMER1_COMPA_vect)
{
flag = false;
digitalWrite(LED_BUILTIN, LOW);    // turn the LED off by making the voltage LOW
}  // end ISR

Bon, ça fonctionne mais cela pourrait être un vraiment plus optimisé, le 50000 fois par seconde le CPU passe du temps à faire des choses non nécessaires. Nous devons optimiser tout cela.

Changement d'état de GPIO en un cycle : DigitalWriteFast (x60)

The regular digitalWrite() in Arduino Uno core (16MHz) takes about 6280nS while digitalWriteFast() port manipulation takes 125nS.

Utilisation des Interruptions sans filet et par les vecteurs d'interruption en direct : ISR_NAKED (x20)

Minimizing the interrupt latency on the Arduino :  to get from 99 down to 5 cycles !

Du coup on comprend mieux pourquoi on a besoin de CPU à 4GHz pour les applis non optimisées ;-) ...

Pour l'ISR_NAKED, cela a vraiement fait l'objet de progrès à tel point que le code géneré peut rester tel quel.
Du coups, je m'en suis passé.
#include "digitalWriteFast.h"
const byte TELEINFOPIN = 2;
volatile boolean flag;
void setup() {
noInterrupts();
// initialize digital pin LED_BUILTIN as an output.
pinMode(TELEINFOPIN, INPUT);
pinMode(LED_BUILTIN, OUTPUT);
// set up Timer 1
TCCR1A = 0;          // normal operation
TCCR1B = bit(WGM12) | bit(CS10);   // CTC, no pre-scaling
OCR1A =  600;       // compare A register value  * clock speed) 16MHz (50kHz * 16MHz)
TIMSK1 = bit (OCIE1A);             // interrupt on Compare A Match
// Setup rising edge on INT0 (D2) pin. 
EICRA = bit(ISC01) | bit(ISC00);  // sense any change on the INT0 pin
EIMSK = bit(INT0);   // enable INT0 interrupt
interrupts();
}
// the loop nothing to do for now.. 
void loop() {
}
// Interrupt Service Routine (ISR)
ISR(INT0_vect) // INT0 is pin D2 on Nano
{
TCNT1 = 0x00;            // reset Timer 1
flag = true;
digitalWriteFast(LED_BUILTIN, HIGH); // Optimized version
}  // end ISR
ISR(TIMER1_COMPA_vect) 
{
flag = false;
digitalWriteFast(LED_BUILTIN, LOW); // Optimized version 1 asm instruction !
}  // end ISR

Le code ASM généré est plutôt propre.

On notera que le flag est superflu vu que l'on a la GPIO qui en fait office ;-) C'est d'ailleurs une très bonne optimisation sur les ISR "nacked", car cela remplace un "volatile boolean" en instruction 1 cycle ;-)

 

ISR(TIMER1_COMPA_vect) 
{
160:	1f 92       	push	r1
162:	0f 92       	push	r0
164:	0f b6       	in	r0, 0x3f	; 63
166:	0f 92       	push	r0
168:	11 24       	eor	r1, r1
flag = false;
16a:	10 92 00 01 	sts	0x0100, r1
digitalWriteFast(LED_BUILTIN, LOW); // Optimized version 1 asm instruction !
16e:	2d 98       	cbi	0x05, 5	; 5
}  // end ISR
170:	0f 90       	pop	r0
172:	0f be       	out	0x3f, r0	; 63
174:	0f 90       	pop	r0
176:	1f 90       	pop	r1
178:	18 95       	reti

La variable est aussi représentée par l'état de la LED de l'Arduino qui clignote au rythme de celle du montage téléinfo ci-dessus, qui n'est plus modulée.

Si on superpose les signaux cela donne :

et

Pas mal !

Ceux qui ont un système qui sait nativement parlé le teleinfo @1200 bauds, vous pouvez vous arrêter ici, et relier D13 (la led) a votre entrée RX de votre Raspberry PI ou ESP ou tout port série sur USB en 3.3V ou 5V. 

C'est directement compatible avec domoticz par exemple.

Mais pour HomeAssistant, il va falloir aller plus loin !
 

Décodage des bits

Bon, là ça va se compliquer un peut...

Les signaux importants pour la détection des bits et du baud-rate sont les suivants selon §5.3 [1]

  • temps des bits égaux pour les 1 ou les 0
  • logique inverse : la porteuse est présente alors le bit vaut «0» et inversement.
  • débit 1200 ou 9600 bauds
  • Chaque caractère est émis sur 10 bits
    • un bit de start correspondant à un "0" logique, (il faut comprendre qu'il y aura forcément absence de la porteuse avant ce bit (soit volontaire, soit le bit de stop de la donnée précédente)
    • 7 bits pour représenter le caractère en ASCII,
    • 1 bit de parité, parité paire,
    • un bit de stop correspondant à un "1" logique.
    • le Least Significant bit(L.S.B.) en premier, le Most Significant bit(M.S.B.) en dernier.
  • Délais entre la fin et le début de la trame suivante, de 16,7 à 33,4ms,
  • Délai entre 2 groupes d'information successifs d'une même trame < 33,4ms.
  • Une trame est constituée de trois parties:
    • le caractère "Start TeXt" STX (0x02) indique le début de la trame,
    • le corps de la trame est composé de plusieurs groupes d'informations,
    • le caractère "End TeXt" ETX (0x03) indique la fin de la trame

Nous allons utiliser un timer pour décoder tout cela, mais sur quelle fréquence ?
On en déduire les valeurs suivantes :

  • détection de fin de trame > 16.7ms soit 267200 tics d'horloge @16MHz
  • durée d'un bit @9600 bauds = 104us soit 1664 tics d'horloge @ 16MHz
  • durée d'un bit @1200 bauds = 833us soit 13328 tics d'horloge @ 16MHz
  • porteuse @50kHz durée de 2 alternances = 20us soit 320 tics d'horloge @ 16MHz
  • durée d'une trame de 10bits @9600 bauds = 1040us
  • durée d'une trame de 10bits @1200 bauds = 8330us

Nous n'avons qu'un compteur 16 bits soit 65536 tics. Heureusement il existe de diviseurs de fréquences.
on doit pouvoir compter au moins jusqu'à 16.7ms, donc 267200/65536 = 4.07... On retiendra donc le diviseur juste > à 4, soit 8 selon [6]

Ce qui nous donne avec un prescaller de 8:

  • détection de fin de trame > 16.7ms soit 33400x8 tics d'horloge @16MHz
  • durée d'un bit @9600 bauds = 104us soit 208x8 tics d'horloge @ 16MHz
  • durée d'un bit @1200 bauds = 833us soit 1666x8 tics d'horloge @ 16MHz
  • porteuse @50kHz durée de 2 alternances = 20us soit 40x8 tics d'horloge @ 16MHz

On devrait pouvoir continuer à utiliser le Timer1 à 16bits.

Détection du baudrate.

On va partir du principe que l'on est en 1200 bauds, et si on détecte un bit d'une durée supérieure à 1/2 bit @9600 et inférieure à  1.5 bit @9600 on bascule définitivement en 9600 bauds.

Plus généralement, on utilisera des valeurs plus fines (minimales) pour être sûr de détecter tous les bits lors de la transmission.

On va détecter l'inter-trame avec un dépassement du TIMER1
Puis attendre un front montant.
Pour chaque font montant,

  • on met la variable "teleinfo_bit" à 0 (oui c'est inversé)
  • on sauvegarde la durée à "1" dans une variable partagée (oui, comme on a la porteuse on passe à 0, donc la durée du timer représente la durée pendant laquelle on a eut "0". ouff)
  • on réinitialise le TIMER1
  • on arme la détection de la fin de la porteuse à dans 20us soit 40 tics

Pour chaque fin de détection de porteuse

  • on met la variable "teleinfo_bit" à 1 (oui c'est toujours inversé)
  • on sauvegarde la durée à "0" dans une variable
  • on réinitialise le TIMER1
  • on arrête de détecter la porteuse.
  • on rempli un buffer avec le nombre de bit à 1
  • on rempli un buffer avec le nombre de bit à 0
  • on détecte si on a le motif "bit de stop, parité, bit 6 à 0, bit de start.
  • si on a le motif, on le met les 7 bits qui forme le caractère teleinfo dans le buffer de la boucle principale.

Décodage du protocole

Alors là, on va faire du KISS + tronçonneuse :

le but est d'avoir une sortie du type CLE:VALEUR au format json.

Donc on attend un code 0x02 on écrit "{" , si on a un espace ou TAB, on écrit ":" au second espace on arrête jusqu'au prochain (*) "0x0a/0x0d", si on a 0x03 on écrit "}". Voila du JSON à pas cher... 

(*) en mode tronçonneuse, je ne récupère pas le 3eme arguments des trames "nouveau standard" avec horodatage.

{"ADCO" : "0xxxxxyyzzzz", "OPTARIF" : "BASE", "ISOUSC" : "50", "BASE" : "117173383", "PTEC" : "TH..", "IINST1" : "002", "IINST2" : "013", "IINST3" : "000", "IMAX1" : "054", "IMAX2" : "048", "IMAX3" : "040", "PMAX" : "19230", "PAPP" : "03497", "MOTDETAT" : "000000", "PPOT" : "00", }

C'est bon à la virgule près.

Le code source est ICI

Sortie série "générique"

Notre sortie est maintenant directement compatible avec tout système qui sait lire du JSON @115200 bauds !

Soit à minima :

# Sensors
sensor:
  - platform: serial
    serial_port: /dev/ttyUSB0
    baudrate: 115200

- platform: template
    sensors:
      edf_base:
        value_template: "{{ state_attr('sensor.serial_sensor', 'BASE')/1000|int }}"
        unit_of_measurement: 'kWh'
        icon_template: mdi:flash

  - platform: template
    sensors:
      edf_iphase1:
        value_template: "{{ state_attr('sensor.serial_sensor', 'IINST1')|int }}"
        unit_of_measurement: 'A'
        icon_template: mdi:flash
  - platform: template
    sensors:
      edf_iphase2:
        value_template: "{{ state_attr('sensor.serial_sensor', 'IINST2')|int }}"
        unit_of_measurement: 'A'
        icon_template: mdi:flash
  - platform: template
    sensors:
      edf_iphase3:
        value_template: "{{ state_attr('sensor.serial_sensor', 'IINST3')|int }}"
        unit_of_measurement: 'A'
        icon_template: mdi:flash

 


[1] : https://www.enedis.fr/sites/default/files/Enedis-NOI-CPT_54E.pdf

[2] : https://www.arduino.cc/reference/en/language/functions/external-interrupts/attachinterrupt/

[3] : http://gammon.com.au/interrupts

[4] : https://www.robotshop.com/community/forum/t/arduino-101-timers-and-interrupts/13072

[5] : ISR_NAKED

[6] : ATMEGA328p Datasheet

Merci PluXML

Rédigé par guiguid Aucun commentaire

Après avoir essayé quelques CMS, pour ce blog, j'ai fini par adopter PluXML.
J'ai essayé , Grav, otext et PluXML.
Ce site est hebergé sur Deamhost en mutualisé.
Sans rentrer dans les détails, je vous laisse découvrir la notation PageSpeed de google.

Lire la suite de Merci PluXML

Upgrade d'un ESP8266

Rédigé par guiguid Aucun commentaire

Les cartes Wemos D1 min à base d'ESP8266 sont souvent utilisées pour les montages domotiques, de part leur faible encombrement, des IO facilement accessibles, et la présence d'un port micro-USB.
Hors il peut arriver en cours de développement que l'on soit vite à l'étroit sur ce type carte.
Heureusement, il existe le TTGO T7 qui sera capable de réaliser tous vos souhaits sans remise en compte du hardware et avec des modifications minimes du software.

Lire la suite de Upgrade d'un ESP8266

Fil RSS des articles