La technique de l’infrarouge est beaucoup utilisée dans les télécommandes d’appareils électroniques, notamment les téléviseurs où elle permet d’envoyer des ordres codés (volume, chaîne, etc.) par impulsions lumineuses invisibles.
Elle est également employée dans des systèmes de transfert de données entre appareils (comme les anciens téléphones ou imprimantes IRDA) ou encore des systèmes d’identification.
La communication par infrarouges est utilisée principalement par ce qu’elle est relativement peu sensible à l’environnement lumineux ambiant et aussi par ce qu’elle met en œuvre des techniques très économiques. D’un autre point de vue qui ne nous concerne pas dans notre hobby, c’est une technique d’échange d’informations très sûre car il n’est pas possible de traverser par exemple des cloisons. Cela reste donc restreint à des espaces réduits.
Les programmes :
Côté émetteur :
Téléchargement du programme pour Arduino (Uno, Nano...) et ATtiny (25, 45, 85) :
Cliquez sur l’icône pour lancer le téléchargement.
Voyons tout d’abord l’intégralité du code :
/*
idTrainIr38kHz_emetteur
Programme pour générer un identifiant en utilisant un émetteur et un récepteur infrarouge
Programme pour Arduino (Uno, Nano...) et ATtiny (25, 45, 85)
Christophe Bobille pour Locoduino mai 2025
version 1.1
*/
#include <Arduino.h>
// Configuration du timer pour PWM IR
# if defined(__AVR_ATmega328P__) || defined(__AVR_ATmega328PB__) || defined(__AVR_ATmega168__)
#define IR_USE_AVR_TIMER2 // Utilise Timer2 pour l'envoi (pin D3)
#define IR_SEND_PIN 3 // Broche utilisée
# elif defined(__AVR_ATtiny25__) || defined(__AVR_ATtiny45__) || defined(__AVR_ATtiny85__)
#define IR_SEND_PIN 4 // Broche utilisée
#define IR_USE_AVR_TIMER_TINY1 // send pin = (pin 4) ATtiny25 / 45 / 85
# endif
#define SEND_PWM_BY_TIMER // Active le mode PWM matériel
#include "IRTimer.hpp" // Part of Arduino-IRremote https://github.com/Arduino-IRremote/Arduino-IRremote.
constexpr uint16_t ID = 0xA5; // Identifiant à envoyer
constexpr uint8_t sizeofId = 8; // Taille de l'identifiant
constexpr uint16_t BIT_DURATION = 250; // Durée minimale d'un front en µs
// Envoie un octet codé via LED IR
void sendByte(uint16_t value) {
for (int i = sizeofId - 1; i >= 0; i--) { // MSB
bool bit = (value >> i) & 0x01;
if (bit) {
// 1
disableSendPWMByTimer();
delayMicroseconds(BIT_DURATION);
enableSendPWMByTimer();
delayMicroseconds(BIT_DURATION);
} else {
// 0
disableSendPWMByTimer();
delayMicroseconds(BIT_DURATION * 2);
enableSendPWMByTimer();
delayMicroseconds(BIT_DURATION * 2);
}
}
// Fin de trame
disableSendPWMByTimer();
delayMicroseconds(BIT_DURATION * 4);
enableSendPWMByTimer();
delayMicroseconds(BIT_DURATION * 4);
}
void setup() {
pinMode(IR_SEND_PIN, OUTPUT);
timerConfigForSend(38); // Fréquence IR : 38 kHz
disableSendPWMByTimer(); // Démarre éteint
}
void loop() {
sendByte(ID);
}
Comme j’ai déjà eu l’occasion de le préciser, le programme de l’émetteur est identique qu’il soit destiné à un Arduino Nano ou un ATtiny.
Cela est rendu possible par l’intégration du fichier IRTimer.hpp
de la bibliothèque IRremote qui gère la paramétrage (complexe) des broches et de PWM. C’est lui également qui expose les fonctions enableSendPWMByTimer()
et disableSendPWMByTimer()
qui permettent respectivement d’activer ou de désactiver l’envoi de la PWM.
Attention : Il n’est pas nécessaire de d’inclure la bibliothèque IRremote entière mais n’oubliez pas le fichier IRTimer.hpp qui est déjà dans le dossier du programme.
La seule information que l’utilisateur aura à saisir dans le code est l’identifiant à envoyer ligne 31 :
constexpr uint16_t ID = 0xA5; // A modifier par l'utilisateur en fonction de l'identifiant souhaité
constexpr uint8_t sizeofID = 8; // Taille en bits de l'identifiant
On renseigne ici la taille de l’identifiant (en nombre de bits). Ce n’est pas nécessairement un multiple de 8. Cela peut être 10, 13, 25...
Dans le setup()
, sont configurées la broche utilisée en sortie et la fréquence de la PWM. La broche de sortie est mise à l’état repos.
void setup() {
pinMode(IR_SEND_PIN, OUTPUT);
timerConfigForSend(38); // Fréquence IR : 38 kHz
disableSendPWMByTimer(); // Démarre éteint
}
Dans la fonction sendByte(uint16_t value)
, on active et on désactive la PWM pour une durée qui est un multiple de la constante BIT_DURATION fixée ici à 250µs.
constexpr uint16_t BIT_DURATION = 250; // Durée minimale d'un front en µs
Cette durée correspond à un bit 1
if (bit) {
// 1
disableSendPWMByTimer();
delayMicroseconds(BIT_DURATION);
enableSendPWMByTimer();
delayMicroseconds(BIT_DURATION);
Pour obtenir un bit 0, cette durée est multipliée par 2 :
} else {
// 0
disableSendPWMByTimer();
delayMicroseconds(BIT_DURATION * 2);
enableSendPWMByTimer();
delayMicroseconds(BIT_DURATION * 2) ;
Et pour le bit de synchronisation (séparation de octets), la durée est multipliée par 4 :
// Fin de trame
disableSendPWMByTimer();
delayMicroseconds(BIT_DURATION * 4);
enableSendPWMByTimer();
delayMicroseconds(BIT_DURATION * 4);
L’envoi des bits est réalisé dans le sens bit de poids fort en premier (MBS)
for (uint8_t i = sizeofID - 1; i >= 0; i--) { // MSB
bool bit = (value >> i) & 0x01;
Notez qu’il serait très facile d’envoyer un identifiant sur plus de 8 bits, 16 par exemple. Il suffirait de modifier la valeur de l’identifiant :
constexpr uint16_t ID = 0xA5E4; // Nouvel identifiant sur 16 bits
Côté récepteur :
Pour exécuter le code côté récepteur, j’ai souhaité utiliser un Raspberry Pi Pico dont vous nous entendez de plus en plus parler sur Locoduino. Et à juste titre au regard de ses performances et de son cout, de l’ordre de 5€ pour la version sans WiFi et 7€ pour la version avec WiFi.
Au sujet du Raspberry Pi Pico, je vous invite à lire l’article très intéressant de Jean-Luc ici.
Voici tout d’abord un code qui sert essentiellement aux tests et à la compréhension du mécanisme :
Téléchargement du programme pour Raspberry Pi Pico ou Raspberry Pi Pico W :
Cliquez sur l’icône pour lancer le téléchargement.
/*
IdTrainIr38KhzPicoRecepteur
For Raspberry Pi Pico and Raspberry Pi Pico W
Programme pour générer un identifiant en utilisant un émetteur et un récepteur infrarouge
Christophe Bobille pour Locoduino mai 2025
version 1.0
*/
#include <Arduino.h>
#include "pico/util/queue.h"
volatile uint32_t duration = 0;
constexpr byte pinIn = 15; // Broche du TSOP
bool receiving = false;
uint8_t currentByte = 0;
constexpr uint8_t sizeofID = 8; // Important, renseigner le nombre de bits attendus pour les identifiants : 8, 16, 24 ou +
int8_t bitIndex = 0;
// File pour les bits détectés
queue_t durationQueue;
enum DecodeState {
IDLE,
RECEIVING
};
DecodeState currentState = IDLE;
void handleIR() {
static uint32_t lastTime = 0;
uint32_t now = micros();
uint32_t duration = now - lastTime;
lastTime = now;
queue_try_add(&durationQueue, &duration);
}
void setup() {
Serial.begin(115200);
while (!Serial) {
; // wait for serial port to connect. Needed for native USB port only
}
pinMode(pinIn, INPUT);
// Initialisation de la queue pour 16 durées
queue_init(&durationQueue, sizeof(duration), 16);
attachInterrupt(digitalPinToInterrupt(pinIn), handleIR, RISING);
}
void loop() {
uint32_t duration = 0;
if (queue_try_remove(&durationQueue, &duration)) {
switch (currentState) {
case IDLE:
if (duration > 1600 && duration < 2400) {
// Début de trame détecté
currentByte = 0;
bitIndex = sizeofID - 1;
currentState = RECEIVING;
}
break;
case RECEIVING:
if (duration >= 400 && duration <= 700) {
// Bit 1
currentByte |= (1 << bitIndex);
bitIndex--;
} else if (duration >= 800 && duration <= 1200) {
// Bit 0
currentByte &= ~(1 << bitIndex);
bitIndex--;
} else {
// Durée invalide
currentState = IDLE;
break;
}
if (bitIndex < 0) {
Serial.printf("Octet reçu : 0x%02X\n", currentByte);
currentState = IDLE;
}
break;
}
}
}
Pour exécuter le code côté récepteur, j’ai souhaité utiliser un Raspberry Pi Pico dont vous nous entendez de plus en plus parler sur Locoduino et à juste titre concernant ses performances et son cout, de l’ordre de 5€ pour la version sans WiFi et 7€ pour la version avec WiFi.
Voici tout d’abord un code qui sert essentiellement aux tests et à la compréhension du mécanisme.
Globalement, ce programme est assez simple avec trois choses principales à repérer :
- Une routine d’interruption.
- Le traitement des données.
- Une file pour échanger les données entre la routine d’interruption et la fonction qui traite les données.
La routine d’interruption est appelée de manière classique :
attachInterrupt(digitalPinToInterrupt(pinIn), handleIR, RISING);
Notez que la routine n’est exécutée que sur les seuls fronts montants (RISING). Rappelez-vous les captures d’écrans d’oscilloscope qui présentaient la durée des trames mesurées d’un état HAUT à un autre état HAUT.
Comme il se doit, la routine d’interruption est simplifiée au maximum. En fait, elle se contente de mesurer la durée entre chaque front montant et de placer cette durée dans une file (durationQueue) en attente de traitement dans la fonction loop() ;
void handleIR() {
static uint32_t lastTime = 0;
uint32_t now = micros();
uint32_t duration = now - lastTime;
lastTime = now;
queue_try_add(&durationQueue, &duration);
}
Voyons justement la fonction loop() :
Après avoir relevé la première valeur disponible dans la file
if (queue_try_remove(&durationQueue, &duration)) {
la fonction va tourner en boucle jusqu’à rencontrer une durée comprise entre 1600µs et 2400 µs, durée caractéristique d’une trame de synchronisation qui indique que les bits suivants sont les bits de data. La variable d’état currentState est alors modifiée.
if (duration > 1600 && duration < 2400) {
// Début de trame détecté
currentByte = 0;
bitIndex = sizeofID - 1;
currentState = RECEIVING;
}
break;
Le programme va maintenant examiner les durées qui correspondent à des données :
- durée supérieure à 400 µs et inférieure à 700 µs, il s’agit d’un bit 1.
- durée supérieure à 800 µs et inférieure à 1200 µs, il s’agit d’un bit 0.
Dans le cas d’une durée contraire, il s’agit très probablement d’une erreur, on réinitialise sans plus attendre la machine à états à la recherche d’un nouveau bit de synchro.
case RECEIVING:
if (duration >= 400 && duration <= 700) {
// Bit 1
currentByte |= (1 << bitIndex);
bitIndex--;
} else if (duration >= 800 && duration <= 1200) {
// Bit 0
currentByte &= ~(1 << bitIndex);
bitIndex--;
} else {
// Durée invalide
currentState = IDLE;
break;
}
Si tout s’est bien déroulé, on affiche la valeur des données reçues et on initialise la machine à états.
if (bitIndex < 0) {
Serial.printf("Octet reçu : 0x%02X\n", currentByte);
currentState = IDLE;
}
Voilà l’essentiel de ce que l’on peut dire de ce programme, dont je rappelle qu’il ne sert qu’à l’affichage de l’identifiant reçu.
Je vous propose maintenant un programme qui pourra être utilisé tel quel sur votre réseau. Il permet avec un seul Raspberry Pi Pico de raccorder 16 capteurs IR et d’envoyer les informations à un gestionnaire de réseau. Ici, les résultats sont envoyés au logiciel Rocrail®.
Ce programme nécessite un Raspberry Pi Pico WiFi.
Téléchargement du programme pour Raspberry Pi Pico W :
Cliquez sur l’icône pour lancer le téléchargement.
Voici le programme dans son ensemble :
/*
Mettre plusieurs broches sous interruption en utilisant une seule fonction callback
Le RP2040 du Pico dispose de 30 broches GPIO, numérotées de 0 à 29. Parmi elles :
- Presque toutes peuvent être configurées pour déclencher une interruption sur front montant, descendant ou les deux.
- Il est donc possible d'attacher une routine à chaque GPIO, mais elles doivent partager la même fonction de callback
RP Pi Pico pinout : https://www.raspberrypi.com/documentation/microcontrollers/images/pico-2-r4-pinout.svg
version 0.5 du 29 mai 2025
*/
#include <Arduino.h>
#include <FreeRTOS.h>
#include <task.h>
#include <queue.h>
#include <WiFi.h>
#include <WiFiClient.h>
// Configuration Wi-Fi
const char* ssid = "YOUR_SSID";
const char* password = "YOUR_PASSWORD";
// Adresse de Rocrail
const char* serverIP = "192.168.1.15"; // IP du PC avec Rocrail
const uint16_t serverPort = 8051; // Port LAN configuré dans Rocrail
WiFiClient client; // Instance de Wifi
typedef struct { // Structure pour envoi des messages par TCP
char text[64];
} TcpMessage;
QueueHandle_t tcpMsgQueue; // sizeof(TcpMessage)
const uint8_t sizeOfID = 8; // Important, renseigner le nombre de bits attendus pour les identifiants
// Création d'un tableau pour les broches sous interruption
const byte nbInterPin = 16; // Choisir le nombre de broches
uint interPin[nbInterPin] = { 3, 4, 5, 6, 7, 8, 14, 15, 16, 17, 18, 19, 20, 21, 27, 28 }; // Identification des broches du Pico // Important, renseigner le nombre de bits attendus pour les identifiants : 8, 16, 24 ou +
const byte dernPin = interPin[nbInterPin - 1]; // Numéro de la dernière broche utilisée
// ID des capteurs dans Rocrail
const char* rrSensor[nbInterPin] = {
"sb1e", "sb1i",
"sb2e", "sb2i",
"sb3e", "sb3i",
"sb4e", "sb4i",
"sb5e", "sb5i",
"sb6e", "sb6i",
"sb7e", "sb7i",
"sb8e", "sb8i"
};
QueueHandle_t eventQueue; // Queue pour stockage des données reçues
constexpr byte eventQueueSize = 32; // Taille pour la file
typedef struct { // Une structure pour le stockage des informations suite à interruption
uint gpio; // De quelle pin s'agit - il ?
uint32_t duration; // Durée entre deux fronts montants
} GpioEvent;
typedef struct { // Une structure pour stockage des données après traitement
uint gpio;
int8_t bitIndex;
uint8_t currentState;
uint32_t currentByte;
uint32_t duration;
const char* rrSensor;
} Msg;
// Routine d'interruption
volatile uint32_t* lastTime = new uint32_t[dernPin - 1]; // On prévoit pour tous les GPIO (0–29)
void initLastTime() {
for (int i = 0; i < dernPin - 1; i++)
lastTime[i] = 0;
}
void handleIR(uint gpio, uint32_t events) {
uint32_t now = micros();
uint32_t duration = now - lastTime[gpio];
lastTime[gpio] = now;
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
GpioEvent evt = { gpio, duration };
xQueueSendFromISR(eventQueue, &evt, &xHigherPriorityTaskWoken);
// demande un changement de contexte si nécessaire
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
// Traitement des données
void taskTraitementData(void* pvParameters) {
enum { IDLE,
RECEIVING }; // Etats pendant le traitement
GpioEvent evt; // Instance de la structure GpioEvent
Msg** msg = new Msg*[dernPin]; // Tableau de pointeurs vers Msg
// Initialiser tout le pointeurs à NULL
for (byte i = 0; i <= dernPin; i++) {
msg[i] = nullptr;
}
// Pointeurs utilisés uniquement
for (byte i = 0; i < nbInterPin; i++) {
byte pin = interPin[i];
msg[pin] = new Msg;
msg[pin]->gpio = pin;
msg[pin]->bitIndex = -1;
msg[pin]->currentState = 0;
msg[pin]->currentByte = 0;
msg[pin]->duration = 0;
msg[pin]->rrSensor = rrSensor[i];
Serial.print("Initialisation msg[");
Serial.print(pin);
Serial.print("] -> sensor : ");
Serial.println(msg[pin]->rrSensor);
}
for (;;) {
while (xQueueReceive(eventQueue, &evt, portMAX_DELAY)) {
Serial.printf("GPIO %d déclenché, durée %d\n", evt.gpio, evt.duration);
const byte pin = evt.gpio;
switch (msg[pin]->currentState) {
case IDLE: // Recherche du bit de synchro
if (evt.duration > 1600 && evt.duration < 2400) // Début de trame détecté
{
msg[pin]->currentByte = 0;
msg[pin]->bitIndex = sizeOfID - 1;
msg[pin]->currentState = RECEIVING;
}
break;
case RECEIVING: // Réception des données
if (evt.duration >= 400 && evt.duration <= 700) // Bit 1
{
msg[pin]->currentByte |= (1 << msg[pin]->bitIndex);
msg[pin]->bitIndex--;
} else if (evt.duration >= 800 && evt.duration <= 1200) // Bit 0
{
msg[pin]->currentByte &= ~(1 << msg[pin]->bitIndex);
msg[pin]->bitIndex--;
} else {
// Durée invalide
msg[pin]->currentState = IDLE; // On arrete le processus en cours et on se met en attente d'un bit de synchro
break;
}
if (msg[pin]->bitIndex < 0) { // Tous les bits sont reçus
Serial.printf("Octet reçu capteur %s : 0x%02X\n", msg[pin]->rrSensor, msg[pin]->currentByte);
// On prépare le message et on le pplace dans la file d'attente pour envoi
TcpMessage rrMsg;
snprintf(rrMsg.text, sizeof(rrMsg.text),
"<fb id=\"%s\" identifier=\"%d\" state=\"true\" bididir=\"1\" actor=\"user\"/>", msg[pin]->rrSensor, msg[pin]->currentByte);
xQueueSend(tcpMsgQueue, &rrMsg, 0);
msg[pin]->currentState = IDLE;
}
break;
}
vTaskDelay(1);
}
}
}
void tcpSend(void* param) {
TcpMessage msg;
while (true) {
if (xQueueReceive(tcpMsgQueue, &msg, portMAX_DELAY)) {
if (client.connect(serverIP, serverPort)) {
Serial.println("Connexion TCP OK");
// Envoi du message reçu
client.print(msg.text);
delay(50);
client.stop();
Serial.println("Connexion fermée");
} else {
Serial.println("Connexion échouée");
}
}
}
}
/*---------------------------------------------------------------------------------------------
setup
----------------------------------------------------------------------------------------------*/
void setup() {
Serial.begin(115200);
while (!Serial) {
; // wait for serial port to connect. Needed for native USB port only
}
// === Connexion Wi-Fi ===
Serial.print("Connexion à : ");
Serial.println(ssid);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\nWi-Fi connecté !");
Serial.print("Adresse IP : ");
Serial.println(WiFi.localIP());
// === Connexion à Rocrail ===
Serial.print("Connexion à Rocrail : ");
Serial.print(serverIP);
Serial.print(":");
Serial.println(serverPort);
eventQueue = xQueueCreate(eventQueueSize, sizeof(GpioEvent)); // Une file d'attente entre l'interruption et le traitement
tcpMsgQueue = xQueueCreate(10, sizeof(char[64])); // Une file d'attente après traitement pour envoi à Rocrail
//init des broches des capteurs et création des interruptions
for (byte i = 0; i < nbInterPin; i++) {
// Initialisation des GPIO en entrée avec pull-up
gpio_init(interPin[i]);
gpio_set_dir(interPin[i], GPIO_IN);
gpio_pull_up(interPin[i]);
// création des interruptions
if (i == 0)
gpio_set_irq_enabled_with_callback(interPin[i], GPIO_IRQ_EDGE_RISE, true, &handleIR); // La foncion de callback n'est déclarée qu'une seule fois
else
gpio_set_irq_enabled(interPin[i], GPIO_IRQ_EDGE_RISE, true);
}
// Initialisation pour utilisation dans la routine d'interruption handleIR
initLastTime();
// Création de la tâches FreeRTOS pour le traitement des données
xTaskCreate(taskTraitementData, "Traitement data", 1024, nullptr, 1, nullptr);
// Création de la tâches FreeRTOS pour l'envoi à Rocrail
xTaskCreate(tcpSend, "tcpSend", 2048, NULL, 1, NULL);
}
void loop() {}
Nous avons également besoin d’une structure plus complète cette fois pour stocker les informations durant le traitement.
Nous stockons bien sûr le numéro de broche, mais aussi la position du bit dans le message (bitIndex), l’état de la machine à état, est-ce un bit de synchro ou un bit de données (currentState) et la durée du front pour dédire la nature du bit : 0 ou 1 ou encore bit de synchro.
// Traitement des données
void taskTraitementData(void* pvParameters) {
enum { IDLE,
RECEIVING };
GpioEvent evt;
Msg msg[nbInterPin];
for (byte i = 0; i < nbInterPin; i++) {
msg[i].gpio = interPin[i];
msg[i].currentState = IDLE;
}
Voici le début de la tâche de traitement des données.
Nous commençons par créer une petite énumération, simplement pour rendre le code plus lisible dans la machine à états qui sera utilisée plus loin : IDLE
et RECEIVING
étant plus explicite que 0 ou 1 !
Nous créons ensuite une nouvelle instance de GpioEvent
que nous appellerons evt et qui va recevoir les données en provenance de la routine d’interruption au travers de la file eventQueue
.
Puis autant d’instance de la structure Msg
que nous avons de pins sous interruption. Il s’agt en réalité d’un tableau de type Msg
de taille nbInterPin
.
Nous initialisons immédiatement certaines valeurs :
msg[i].gpio = interPin[i];
msg[i].currentState = IDLE;
msg[i].gpio
qui contient la valeur de la broche sous interruption et l’état initial qui doit être IDLE
.
Les autres valeurs des membres de la structure sont initialisées par défaut selon le principe du c/c++.
for (;;) {
while (xQueueReceive(eventQueue, &evt, portMAX_DELAY)) {
printf("GPIO %d déclenché, durée %d\n", evt.gpio, evt.duration);
La particularité d’une tâche FreeRTOS est qu’elle possède son propre loop()
qui est ici représenté par for (;;)
(boucle infinie) ou encore while (true)
, tant que la condition est vraie ce qui, écrit comme ceci est toujours le cas !
while (xQueueReceive(eventQueue, &evt, portMAX_DELAY)) {
Avec while
et portMAX_DELAY
, le programme attend qu’il y ait au moins une valeur dans la file eventQueue
. Quand une donnée se présente, elle est stockée dans la variable evt
.
Une fois reçu une nouvelle variable evt
, il faut tout d’abord chercher dans le tableau msg
a quel élément s’applique la durée reçue evt.duration
.
La méthode à laquelle on pense en premier, celle que j’avais utilisée initialement, est de parcourir le tableau msg
jusqu’à trouver l’élément qui correspond à la broche de l’interruption (evt.gpio).
for (byte i = 0; i < nbInterPin; i++) {
if (msg[i].gpio == evt.gpio) {
Mais cette façon de faire est très insatisfaisante car il nous faut potentiellement tester de nombreuses option invalides avant de trouver la bonne.
Comme il y a 16 broches sous interruption, cela veut dire que l’on peut potentiellement tester 15 possibilités avant de trouver la bonne. Multiplié par les 8 bits, cela correspond à 120 tests inutiles !!!
En cherchant, j’ai trouvé une solution qui me permet de trouver immédiatement l’élément du tableau qui correspond au données reçues.
Je cré un tableau de msg
dont l’index sera équivalent au numéro de broche reçu.
Il me faut donc pour cela créer un tableau dont la taille est au moins équivalent à la valeur de la dernière broche.
const byte dernPin = interPin[nbInterPin - 1]; // Numéro de la dernière pin utilisée
Msg** msg = new Msg*[dernPin];
C’est ce que je fais ici : const byte dernPin = interPin[nbInterPin - 1];
.
et que j’applique ici : Msg** msg = new Msg*[dernPin];
Mais alors, pourquoi utiliser des pointeurs (*) et même un pointeur de pointeur (**) ?
J’aurais pu créer directement un tableau Msg msg[dernPin];
mais celui-ci aurait alors une occupation de la mémoire égale à 28 x l’empreinte mémoire d’un élément msg
de l’ordre de 12 octets chaque. Ce n’est pas négligeable, il vaut mieux utiliser des pointeurs dont l’empreinte mémoire sur un Pico est 32 bits (4 octets).
for (byte i = 0; i <= dernPin; i++) {
msg[i] = nullptr;
}
Pour faire simple, j’initialise tous les pointeurs à nullptr
. L’empreinte mémoire est insignifiante.
// Pointeurs utilisés uniquement
for (byte i = 0; i < nbInterPin; i++) {
byte pin = interPin[i];
msg[pin] = new Msg;
msg[pin]->gpio = pin;
msg[pin]->bitIndex = -1;
msg[pin]->currentState = 0;
msg[pin]->currentByte = 0;
msg[pin]->duration = 0;
msg[pin]->rrSensor = rrSensor[i];
Serial.print("Initialisation msg[");
Serial.print(pin);
Serial.print("] -> sensor : ");
Serial.println(msg[pin]->rrSensor);
}
Puis je n’affecte de valeurs qu’aux seuls éléments du tableau que j’utilise. J’envoie le résultat sur la console de l’IDE pour m’assurer que le résultat est bien conforme à ce que je souhaitais.
Cette façon de faire est un peu plus longue, mais n’est réalisée qu’une seule fois au début du programme.
Voyons maintenant le traitement :
La condition switch...case réalise une petite machine à état qui a ici deux valeurs : IDLE, le bit de synchro pour un message n’a pas été trouvé (état initial) ou RECEIVING, l’analyse des données.
switch (msg[pin]->currentState)
Cette ligne a pour objectif de vérifier dans quel état est le message concerné.
case IDLE: // Recherche du bit de synchro
if (evt.duration > 1600 && evt.duration < 2400) // Début de trame détecté
{
msg[pin]->currentByte = 0;
msg[pin]->bitIndex = sizeOfID - 1;
msg[pin]->currentState = RECEIVING;
}
break;
Lorsque la durée mesurée dans la routine d’interruption et comprise entre 1600 µs et 2400µs, on considère qu’il s’agit d’un bit de synchro, on met les valeurs à zéro et on change le statut pour la machine à états.
case RECEIVING: // Réception des données
if (evt.duration >= 400 && evt.duration <= 700) // Bit 1
{
msg[pin]->currentByte |= (1 << msg[pin]->bitIndex);
msg[pin]->bitIndex--;
} else if (evt.duration >= 800 && evt.duration <= 1200) // Bit 0
{
msg[pin]->currentByte &= ~(1 << msg[pin]->bitIndex);
msg[pin]->bitIndex--;
} else {
// Durée invalide
msg[pin]->currentState = IDLE; // On arrête le processus en cours et on se met en attente d'un bit de synchro
break;
}
if (msg[pin]->bitIndex < 0) { // Tous les bits sont reçus
Serial.printf("Octet reçu capteur %s : 0x%02X\n", msg[pin]->rrSensor, msg[pin]->currentByte);
// On prépare le message et on le pplace dans la file d'attente pour envoi
TcpMessage rrMsg;
snprintf(rrMsg.text, sizeof(rrMsg.text),
"<fb id=\"%s\" identifier=\"%d\" state=\"true\" bididir=\"1\" actor=\"user\"/>", msg[pin]->rrSensor, msg[pin]->currentByte);
xQueueSend(tcpMsgQueue, &rrMsg, 0);
msg[pin]->currentState = IDLE;
}
break;
Maintenant que l’état est RECEIVING, le programme va faire le tri entre les durée comprises entre if (evt.duration >= 400 && evt.duration <= 700) // Bit 1
correspondant à un bit 1 et else if (evt.duration >= 800 && evt.duration <= 1200) // Bit 0
correspondant à un bit 0.
On considère que tous les autres cas sont des erreurs et qu’il nous faut abandonner et attendre un nouveau bit de synchro.
} else {
// Durée invalide
msg[pin]->currentState = IDLE; // On arrete le processus en cours et on se met en attente d'un bit de synchro
break;
}
if (msg[pin]->bitIndex < 0) { // Tous les bits sont reçus
Serial.printf("Octet reçu capteur %s : 0x%02X\n", msg[pin]->rrSensor, msg[pin]->currentByte);
// On prépare le message et on le place dans la file d'attente pour envoi
TcpMessage rrMsg;
snprintf(rrMsg.text, sizeof(rrMsg.text),
"<fb id=\"%s\" identifier=\"%d\" state=\"true\" bididir=\"1\" actor=\"user\"/>", msg[pin]->rrSensor, msg[pin]->currentByte);
xQueueSend(tcpMsgQueue, &rrMsg, 0);
msg[pin]->currentState = IDLE;
}
break;
Dans le cas où tous les bits attendus ont été reçu, on prépare le message à envoyer que l’on place dans une nouvelle file d’attente (tcpMsgQueue) et on n’oublie pas de changer l’état pour que le programme aille maintenant à la recherche d’un nouveau bit de synchro.
L’envoi du message est réalisé dans la méthode suivante qui je crois, ne nécessite pas de commentaires particuliers :
void tcpSend(void* param) {
TcpMessage msg;
while (true) {
if (xQueueReceive(tcpMsgQueue, &msg, portMAX_DELAY)) {
if (client.connect(serverIP, serverPort)) {
Serial.println("Connexion TCP OK");
// Envoi du message reçu
client.print(msg.text);
delay(50);
client.stop();
Serial.println("Connexion fermée");
} else {
Serial.println("Connexion échouée");
}
}
}
}
Voyons enfin la routine d’interruption, celle qui va mesurer la durée entre deux fronts montants pour nous permettre de déduire la valeur du bit envoyé.
A part quelques mécanismes de gestion des priorités propre à freeRTOS, cette fonction est très simple et surtout très rapidement exécutée.
volatile uint32_t* lastTime = new uint32_t[dernPin - 1]; // On prévoit pour tous les GPIO
void initLastTime() {
for (int i = 0; i < dernPin - 1; i++)
lastTime[i] = 0;
}
A l’extérieur de la routine, nous allons déclarer un pointeur de tableau intitulé lastTime. C’est dans ce tableau que seront stockées pour chacune des broches les valeurs nécessaires au calcul de la durée d’impulsion.
Dans une boucle for ous allons initialiser toutes les variables du tableau en une seule opération.
void handleIR(uint gpio, uint32_t events) {
uint32_t now = micros();
uint32_t duration = now - lastTime[gpio];
lastTime[gpio] = now;
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
GpioEvent evt = { gpio, duration };
xQueueSendFromISR(eventQueue, &evt, &xHigherPriorityTaskWoken);
// demande un changement de contexte si nécessaire
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
Comme j’ai déjà eu l’occasion de le dire, les opérations dans la routine sont assez simples :
- On déclare et on initialise une variable
now
à laquelle on affecte l’heure exacte courante avec la fonction micros()
. - On déclare et on initialise une variable
duration
et on lui affecte une valeur qui est la différence temporelle entre la dernière lecture lastTime[gpio]
et la lecture temporelle que nous venons de faire - On affecte à
lastTime[gpio]
la valeur temporelle actuelle now
dont nous allons avoir besoin lors de la prochaine interruption sur cette broche. - Avec
GpioEvent evt = { gpio, duration };
on déclare une variable de type GpioEvent
dans laquelle on place la valeur de la broche et la durée constatée - Puis on envoie la variable
evt
dans la file d’attente eventQueue
pour qu’elle soit traitée par la méthode vue plus avant.

- Figure 31 - Création d’un contrôleur dédié dans Rocrail
Dans le logiciel Rocrail®, (Figure 31) vous devrez créer un contrôleur spécifique qui va écouter les communication TCP sur le port 8051. Le même que celui déclaré dans le programme bien sûr.
Vous pouvez voir en arrière-plan en haut de l’image, mes autres contrôleurs dans Rocrail®, dccpp, celui qui me permet de piloter labox et decodeurs_CAN, celui qui reçoit les communications en CAN.

- Figure 32 - A l’activation du capteur sur le réseau, Rocrail® affiche la locomotive concernée.
Figure 32 – Affichage sur le TCO de Rocrail® du capteur sb1e
qui est actionné. Rocrail® associe alors l’identifiant à la locomotive concernée, ici ma 241A004.