Générer des salves d'impulsions

Bonjour,

la question existentielle du jour : y a-t-il une méthode préférable pour générer ndes séries de d'impulsions sur des GPIO d'un ESP32.
Par "salves" j'entends un signal carré de fréquence ~2 à 3Hz avec entre 1 et 5 impulsions puis ça s'arrête.

Méthode 1 :

void clicClicGPIO(int pin, int n) {
    for (int i = 0; i < n; i++) {
        digitalWrite(pin, HIGH); 
        delay(200);            
        digitalWrite(pin, LOW); 
        delay(200);  
    }
}

Simple mais c'est bloquant si je veux envoyer la commande sur plusieurs pin différents

Méthode 2 : utiliser millis() dans le loop avec test et compteur.
Ça devrait marcher mais comme je le fait déjà par ailleurs pour des actions toutes les minutes, toutes les heures et d'autres, ça devient lourdingue.

Méthode 3 en mode science fiction (aucune idée de la faisabilité) : peut-on détourner les fonctions de PWM ?

Il y a surement d'autres idées que j'écouterai avec plaisir.

(oui, c'est toujours les commandes de volets : pour mimer les "clic clic" sur les boutons et piloter des relais)

Bonjour ProfesseurMephisto

Mais c'est quand même la plus élégante :wink:

Bonne journée
jpbbricole

Non, mais ça ne simplifie pas la chose, regardes ici

Bonjour @ProfesseurMephisto

Les ESP32 ont un module RMT (Remote Control Transceiver) conçu pour générer/recevoir des salves d'impulsions (modulées ou pas.)
https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/peripherals/rmt.html

Par contre je ne suis pas certain que le core ESP32 pour Arduino permet de gérer simplement ce module RMT

Oui d'après DepSeek qui propose le code suivant pour faire produire par le module hardware RMT 5 impulsions identiques dont les états hauts et bas durent 250ms
Non testé, sI OK une fois configuré le module RMT et défini le 'profil' de la salve à émettre , un simple appel de fonction (non bloquante) permet d'envoyer une salve prédéfinie

#include <driver/rmt.h>

#define RMT_TX_CHANNEL RMT_CHANNEL_0  // Canal RMT à utiliser
#define RMT_TX_GPIO_NUM 4             // GPIO à utiliser pour la sortie

void setup() {
  // Configuration du canal RMT
  rmt_config_t config;
  config.rmt_mode = RMT_MODE_TX;
  config.channel = RMT_TX_CHANNEL;
  config.gpio_num = (gpio_num_t)RMT_TX_GPIO_NUM;
  config.mem_block_num = 1;
  config.tx_config.loop_en = false;
  config.tx_config.carrier_en = false;
  config.tx_config.idle_output_en = true;
  config.tx_config.idle_level = RMT_IDLE_LEVEL_LOW;
  config.clk_div = 80;  // Diviseur d'horloge (80 MHz / 80 = 1 MHz → 1 tick = 1 µs)

  rmt_config(&config);
  rmt_driver_install(config.channel, 0, 0);
  
  // Création des impulsions
  const int pulse_count = 10;  // 5 impulsions = 10 transitions (haut+bas)
  rmt_item32_t items[pulse_count];
  
  // Durées en µs (250 ms = 250000 µs)
  const uint32_t high_duration = 250000;
  const uint32_t low_duration = 250000;  // Même durée pour l'état bas dans cet exemple
  
  // Remplir le tableau d'items
  for (int i = 0; i < pulse_count; i++) {
    items[i].duration0 = (i % 2 == 0) ? high_duration : low_duration;
    items[i].level0 = (i % 2 == 0) ? 1 : 0;
    items[i].duration1 = 0;
    items[i].level1 = 0;
  }
  
  // Envoyer les impulsions
  rmt_write_items(RMT_TX_CHANNEL, items, pulse_count, true);
}

void loop() {
  // Ne rien faire ici
}
`...

Comme on le dit assez souvent, "c'est un projet qui se prêterait bien à l'utilisation de machines à états".
Je suggérerais l'utilisation de cette librairie qui est intéressante. J'ai déjà joué avec et je pense qu'elle devrait permettre de faire ce que tu veux.

Hi,
Avec le pwm c'est fastoche , tu créés une interruption sur le(s) front descendant , qui stoppe le pwm quand est atteint le nombre d'impulsions souhaitées.
Rappel , grâce à la matrice , tu peux avoir ce signal sur (presque) n'importe quelle broche.
Le RMT c'est le + élégant et intéressant , mais je crains la raideur de la courbe ... d'apprentissage

Avec le tuto qui va bien... Je l'ai déjà survolé il y a longtemps mais sans plus, n'ayant pas de besoin particulier. Je vais le reprendre, merci !

As tu pensé à faire un objet d'envois d'un train d'impulsion basé sur une petite machine à état?
Tu aurais alors qu'a instancier un objet par Pin utilisé.

En quoi millis est lourding? la répétition ?

Je ne me suis pas encore attelé à cette partie...

Oui, des tests les uns après les autres : toutes les minutes pour voir s'il y a une action à réaliser, il faudra mettre à l'heure par NTP, je veux faire clignoter une LED en cas d'erreur, etc.

J'ai l'impression que certaines tâches ralentissent trop le loop() : si la synchro NTP perturbe le rythme des clics clics pour les volets, la commande ne sera pas interprétée. Je peux m'arranger pour la faire de nuit ou alors il faut ajouter un test pour savoir si une action aura lieu la minute suivante.

Je ne suis pas sûr de bien comprendre.
Tu as fait une(des) machine(s) à états ?

Pareil la synchro NTP est fait assez rarement executé, donc qu'elle perturbe, c'est effectivement compréhensible, mais pourquoi aurais tu une synchro pendant l'envois d'une salve ?
Tu avais parlé de RTC ou pas, je ne m'en rappel pas bien, mais il me semble?

J'ai surement dû lire trop vite et louper un truc.
Mais tu envois tes salves dans quel cas de figure ?
En gros, ma question est ne peux tu pas, décaler une des deux actions, si une action CPUphage est en cours d'exécution ?
De même si crois que tu es sur ESP32, n'est-il pas possible, de séparer les actions sur des cœurs différents?

Aller je vais jouer le relou de service :slight_smile:, tu as fait un organigramme de ton application ou des machines à états ?

la structure pourrait être un truc comme cela (tapé ici)

class GenerateurImpulsions {

  private:
    byte broche;
    unsigned long dureeImpulsion;
    unsigned long dureePause;
    unsigned long dernierChangement;
    byte nbComptage;
    byte nbImpulsions;
    enum Etat : byte {ATTENTE_CYLE, IMPULSION_PAUSE, IMPULSION_HAUT, IMPULSION_BAS} etat;
    GenerateurImpulsions* suivant;
    static GenerateurImpulsions* debut;

  public:
    GenerateurImpulsions(byte b, unsigned long periodeMs, unsigned long pauseMs, byte n)
      : broche(b), dureeImpulsion(periodeMs / 2), dureePause(pauseMs), dernierChangement(-dureeImpulsion),
        nbComptage(0), nbImpulsions(n), etat(IMPULSION_PAUSE) {
      suivant = debut;
      debut = this;
    }

    void commencerBroche() {
      pinMode(broche, OUTPUT);
      digitalWrite(broche, LOW);
    }

    void actualiserBroche() {
      unsigned long maintenant = millis();

      switch (etat) {
        case ATTENTE_CYLE:
          break;

        case IMPULSION_PAUSE:
          break;

        case IMPULSION_HAUT:
          break;

        case IMPULSION_BAS:
          break;
      }
    }

    static void commencer() {
      for (GenerateurImpulsions* g = debut; g; g = g->suivant) {
        g->commencerBroche();
      }
    }

    static void actualiser() {
      for (GenerateurImpulsions* g = debut; g; g = g->suivant) {
        g->actualiserBroche();
      }
    }
};

GenerateurImpulsions* GenerateurImpulsions::debut = nullptr;


// ---------------------------

// exemple sur 3 pins
GenerateurImpulsions pulse1(2,  250ul, 2000ul, 3);
GenerateurImpulsions pulse2(3,  500ul, 3000ul, 4);
GenerateurImpulsions pulse3(4, 1000ul, 4000ul, 1);

void setup() {
  GenerateurImpulsions::commencer();
}

void loop() {
  GenerateurImpulsions::actualiser();
}

Je vous laisse faire la machine à états

L'usage d'une liste chaînée avec une variable de classe permet de simplifier le setup() et la loop(), on n'appelle juste commencer() et actualiser() et c'est la classe qui se charge de maintenir toutes les instances ➜ comme ça pas de boucle for dans le setup et la loop et besoin d'un tableau des instances, ça fait plus "clean".

1 Like

Bonjour à tous!

J'y vais vais de ma version, c'est basé sur les millis()

Dans cette version, les trains d'impulsions se commandent depuis la console, ainsi en tapant:
020222000 = 2ème port 22 impulsions avec une période de 2000 millisecondes.
03010500 = 3ème port 10 impulsions avec une période de 500 millisecondes.

La syntaxe est:
PPNNNMMMMMMM
Le port PP (1 à pulsesPortsNbr)
NNN nombre d'impulsions (1-999)
MMMMMM période en millisecondes

Plusieurs trains d'impulsions peuvent fonctionner "en parallèle".
Il y a des indications quant au fonctionnement du programme dans le moniteur:

Commande recue: 03010500
	Début 3
	Fin 3
Commande recue: 040051000
	Début 4
	Fin 4

Le programme:

/*
    Name:       AF_GenerateurSalvesImpulsions.ino
    Created:	14.05.2025
    Author:     jpbbricole
	Remarque:	Générer des salves d'impulsions à une fréquence
				https://forum.arduino.cc/t/generer-des-salves-dimpulsions/1379201
*/

const int pulsesPorts[] = {4, 5, 6, 7}; // Ports des impulsions
const int pulsesPortsNbr = sizeof(pulsesPorts) / sizeof(pulsesPorts[0]); // Nombre de ports
const int pulseEtatOn = HIGH; // Ports des impulsions

struct pulseArryayDef // Structure des générateurs
{
	int nombre; // Nombre d'impulsions
	unsigned long pulseTempo; // Période de l'impulsion
	unsigned long pulseMillis; // Période de l'impulsion, chrono
};
pulseArryayDef pulsesArray[pulsesPortsNbr];

void setup()
{
	Serial.begin(115200);
	
	for (int p = 0; p < pulsesPortsNbr; p ++)
	{
		pinMode(pulsesPorts[p], OUTPUT);
		digitalWrite(pulsesPorts[p], !pulseEtatOn); // Impulsion OFF
	}

}

void loop()
{
	for (int p = 0; p < pulsesPortsNbr; p ++)
	{
		if (pulsesArray[p].nombre > 0) // Si des impulsione en cours pour ce port
		{
			if (millis() - pulsesArray[p].pulseMillis >= pulsesArray[p].pulseTempo)
			{
				digitalWrite(pulsesPorts[p], !digitalRead(pulsesPorts[p])); // Inverser le port
				pulsesArray[p].pulseMillis = millis();
				
				pulsesArray[p].nombre --;
				if (pulsesArray[p].nombre < 1) // Si train d'impulsions terminé
				{
					digitalWrite(pulsesPorts[p], !pulseEtatOn);
					Serial.println("\tFin " + String(p +1));
				}
			}
		}
		
	}

	//--------------------------------- Commandes moniteur
	if (Serial.available()) // Si commande reçue
	{
		cmdExecute(Serial.readStringUntil('\n')); // Lire jusqu'à nouvelle ligne
	}
}

/*
Commandes depuis le moniteur, les commandes ne sont pas sensibles à la casse

Commandes reconnues:
PPNNNMMMMMMM  Le port PP (1 à pulsesPortsNbr) NNN nombre d'impulsions (1-999) MMMMM période en millisecondes
010104000
020222000
03010500
0410020
*/

void cmdExecute(String cmdRx)
{
	cmdRx.trim(); // Nettoyage
	cmdRx.replace(" ", ""); // Sans espaces

	Serial.println("Commande recue: " + cmdRx);

	int portNum = cmdRx.substring(0, 2).toInt() -1;
	int pulsesNbr = cmdRx.substring(2, 5).toInt();
	unsigned long pulsesTempo = cmdRx.substring(5).toInt();
	
	pulsesArray[portNum].nombre = pulsesNbr *2;
	pulsesArray[portNum].pulseTempo = pulsesTempo/2;
	pulsesArray[portNum].pulseMillis = millis();

	Serial.println("\tDébut " + String(portNum +1));
}

Bonne journée
jpbbricole

Pas stricto-sensu, simplement parce que je n'ai pas encore assimilé et compris correctement le tuto de @J-M-L
Mais je me soigne ! (et j'ai des copies à corriger :frowning: )

Parce que principe de Murphy oblige, si ça peut arriver ça va arriver.
En fait je vais la faire de nuit et je n'ouvre pas les volets de nuit, donc, OSEF.

Oui et c'est indispensable : il est déjà arrivé d'avoir des (courtes) coupures de jus pendant les vacances. Pas grave avec le système d'horloge électromécanique actuel, le système prend juste quelques minutes de retard. Un système à microcontroleur perd complètement l'heure.

Quand c'est l'heure de manipuler les volets...

Oui, ou le contraire, les tâches CPUphages n'étant pas nécessairement prioritaires

Sûrement mais je ne sais pas faire...

Nan... parce que pour le moment, je n'ai que des morceaux de briques du système dans la tête, pas un ensemble cohérent. Bien sûr, l'organigramme m'aidera à mettre de la cohérence donc il faut que je fasse de l'ordre dans mes idées pour le faire et mettre de l'ordre dans mes idées !

Merci je vais regarder ça... dès que possible

[Mod humour on]: il ne faut pas rejeter que le professeur(toi :slight_smile: ) soit mauvais :rofl:
[Mod humour off]:

Si je peux formuler un conseil, il faut partir sur un cas simple et le complexifier petit à petit.
Bizarrement ce n'est pas toujours intuitif de raisonner en état finis(enfin pour moi en tout cas).

Non!, :slight_smile: cela dépend surtout de ce que tu as prévus, ce que je voulais dire en filigrane, c'est il y a t-il une raison pour que tu laisse ton programme activer tes volets pendant que tu fais une synchro ?
Donc que tu change l'état et déclenche l'action associée ?
Double donc, tu peux très bien faire le changement d'état une fois la synchro terminé.

Même si c'est un cas qui ne se pose pas réellement, ce n'est qu'une condition comme une autre pour changer d'état.

Oui, mais si la tâche est en cours, c'est plus compliqué de la décaler que temporiser celle qui est avenir :slight_smile:

Effet si tu sais, mais tu ne sais pas que tu le sais :slight_smile:
@philippe86220 a poster du code qui montre la mise en oeuvre dans un de ses posts.

Pour l'organigramme, je sais que tu n'en ai pas forcément là, c'était vraiment pour faire le relou.
Mais moi pour les machines à états cela m'aide beaucoup, car j'ai tendance à pensé en action et pas état.

Bonsoir, je ne sais pas si cela peut s'appliquer ici mais il faut penser que le dac peut être utilisé pour générer des signaux.

Bon, c'est clairement du code un peu trop compliqué pour mon niveau mais j'essaye :

C'est là que se fait la commutation des niveaux de sortie (digitalWrite(broche, HIGH); ou LOW) ? Mais comment se défini la durée de l'état haut ou bas ? Pas avec un delay ?

La liste chainée c'est bien le mécanisme qui utilise debut et suivant ? mais ça sert à générer une salve sur une GPIO. Si à 18h12 je veux envoyer 3 clics sur le GPIO17 et 4 clics sur le GIO18, il faut deux « instances » (?) du GenerateurImpulsions ?

Je pense que mes questions sont débiles tellement je ne comprends pas la structure de ce code :frowning:

Surtout pas !

Dans mon tuto il y a l’usage de millis() et si vous mémorisez le moment de la dernière transition d’état vous pouvez calculer la durée depuis que vous êtes dans le nouvel état . En comparant cette durée avec un seuil approprié (qui dépend de l’état - durée du HIGH, du LOW, du cycle..) vous déclenchez un événement « temps écoulé » qui permet de changer d’état.

(cf mon tuto pour plus de détails )

Ça sert à mémoriser tous les générateurs de salves que vous voulez avoir dans le code afin que le développeur n’ait pas à se soucier de maintenir tout cela dans un tableau et écrire une boucle pour parcourir le tableau. Mais je n’ai peut être pas compris l’usage exact que vous vouliez faire des générateurs - Sont ils ponctuels et ensuite inutiles ou alors est-ce qu’ils tournent constamment pendant toute la vie du programme ?

Je n'ai surtout peut-être pas été clair alors je prends un exemple simplifié. Un seul volet, une seule programmation.

Le module yokis attend des salves d'impulsions pour exécuter les ordres :
3 impulsions, le volet s'ouvre, 4 impulsions, le volet se ferme (il y a d'autres commandes possible)
Une seule impulsion, c'est le fonctionnement manuel du bouton poussoir.
Les impulsions doivent être rapides et assez régulières.

Exemple de programmation ultra-simplifiée : l'ESP envoie 3 impulsion à 8h12min et 4 impulsions à 21h42.

C'est ce que je fais actuellement avec une horloge électromécanique et un module yokis propriétaire supplémentaire mais pour tous les volets d'un coup.

L'idée est de faire mieux, plus fin au niveau des réglages :
Avoir une interface web pour faire la programmation et les manipulations (éventuellement à distance, hors du domicile et du réseau local)
Calculer les horaires du soleil (et/ou mettre un capteur de lumière)
Pour obtenir quelque chose du genre :

  • Du lundi au vendredi, ouvrir à 7h15 dans les chambres, 7h30 dans la cuisine, etc. (j'ai prévu 5 groupes de volets)
  • Fermer tous les soirs au coucher du soleil.
  • Le week-end ouvrir les volets à 9h00
  • Fermer partiellement les volets au sud en période de canicule
    etc.

La plupart du temps, d'un jour à l'autre, la programmation ne changera pas et l'ESP ne fera rien d'autre qu'attendre le moment de faire ses impulsions, servir la page web, et de temps à autre resynchroniser l'horloge RTC par NTP.

La partie chi*te est le démarrage (après coupure de courant et coupure de la box) pour s'assurer que tout va reprendre sans intervention humaine...

D'où mes dernières questions sur les salves d'impulsions et l'horloge RTC :wink:

Pour le code des impulsions, j'ai beaucoup de mal à comprendre le votre... aucune critique, c'est juste que mon niveau n'est pas suffisant et mon esprit probablement encore trop formaté à la logique plus procédurale et pas du tout objet de mon apprentissage du pascal il y a plus de 30 ans.

Je suis quand même arrivé à faire un truc qui semble fonctionner, non bloquant, avec des tests et boucles et millis() dans le loop() . Je le posterai si j'arrive à le consolider ce soir pour avis

En tout cas merci de l'aide apportée !

OK. les 3 ou 4 impulsions sont elles proches ou pas ? Si elles sont suffisamment proches (quelques ms) vous pouvez ne pas vous embêter avec une machine asynchrone pour envoyer la salve, faites cela avec delay() et même s'il y a 20 volets à commander ils recevront leur salve avec un peu de décalage mais ça ne se verra pas.

Si par contre un train de pulsation prend longtemps disons une demi seconde alors ce n'est pas jouable car si vous avez 20 volets, il va se passer 10 secondes entre la commande du premier et du dernier si vous demandez de tout fermer / ouvrir.

➜ combien doit durer une salve ?