ESP32 timer épisode 2 Edit : premiers résultats

Bonjour,

Ayant abandonné l’utilisation des registres timers j’ai décidé de rentrer dans le rang (dur dur) et j’essaye d’utiliser les fonctions fournies par le “package ESP32- Arduino”.
Code :

#include <Arduino.h>
#include <timer.h>

uint32_t boucle = 0;
uint64_t *compteur;

void setup() {
  Serial.begin(115200);
}

void loop() {
  boucle++; 
  Serial.printf("Num boucle=  %u \n", boucle);
  delay(10);
  timer_get_counter_value(0, 0, *compteur);
  Serial.printf("Compteur=  %u \n", compteur);  
}

Et j’obtiens cette erreur (avec platformIO qui est pas mal du tout) :

LDF Modes: Finder ~ chain, Compatibility ~ soft
Found 26 compatible libraries
Scanning dependencies…
No dependencies
Building in release mode
Compiling .pio/build/lolin32/src/main.cpp.o
src/main.cpp:2:19: fatal error: timer.h: No such file or directory



compilation terminated.
*** [.pio/build/lolin32/src/main.cpp.o] Error 1

Le fichier non trouvé par le “package ESP32-Arduino” se trouve ici :
.arduino15/packages/esp32/hardware/esp32/1.0.4/tools/sdk/include/driver/driver/timer.h

Je suis repassé dans l’IDE arduino pour voir : même erreur.

C’est moi qui ne comprend rien ou c’est Espressif qui n’a pas terminé son travail ?

Même chose ici :

sketch_feb26a:2:19: error: timer.h: No such file or directory

compilation terminated.

exit status 1
timer.h: No such file or directory

Mais peut-être que ceci peut aider.

/*
 Repeat timer example
 This example shows how to use hardware timer in ESP32. The timer calls onTimer
 function every second. The timer can be stopped with button attached to PIN 0
 (IO0).
 This example code is in the public domain.
 */

// Stop button is attached to PIN 0 (IO0)

...
#include <driver/timer.h>

Un élément de réponse :
En fouillant dans la doc ESP-IDF j'ai vu que dans un exemple sur les gpio l'auteur écrivait :
#include "driver/gpio.h"

En reportant dans mon essais et en écrivant :
#include "driver/timer.h"

je n'ai plus cette erreur mais j'ai les suivantes dont en ordre de priorité :

src/main.cpp:19:39: error: invalid conversion from 'int' to 'timer_group_t' [-fpermissive]
timer_get_counter_value(0, 0, compteur);
^

Je continue.

Nota :
Pendant que je rédigeais je n'ai pas vu la réponse d'hbacheti.

  timer_get_counter_value(TIMER_GROUP_0, TIMER_0, &compteur);

Je ne sais pas pourquoi mais je t’entends déjà râler contre la doc ESP-IDF.

Mais là je vais être d’accord avec toi :wink:
Pas très PRO.

EDIT

uint64_t compteur;

je pense qu'il faut travailler au niveau HAL (Hardware Abstraction Layer) qui se fait sans importer quoi que ce soit (c'est fait pour vous par l'IDE)

On doit prendre une structure représentant un hw_timer_t qu'on initialise en appelant timerBegin(). On sait qu'on a 2 groupes de 2 timers matériels de 64 bits (= 4 timers) et un prescaler (diviseur de la fréquence de base) sur 16 bits pour chacun.

timerBegin prend en paramètre le N° de timer que l'on veut utiliser (de 0 à 3), une valeur uint16_t pour le pre-scaler et enfin comme les timers savent incrémenter ou décrémenter, un booléen pour le sens de comptage (true pour incrémenter)

Pour mon ESP à 80Mhz, si on appelle

timer0 = timerBegin(0, 80, true);

on prend le premier timer, on le met à 1Mhz (80MHz / 80) et on incrémente le compteur du timer.

(à noter que millis() utilise l'esp_timer() et donc on n'a pas de conflit)

Une fois qu'on a notre timer, on peut lui accrocher une fonction d'interruption, lire la valeur du compteur etc

De part l'architecture de l'ESP32 (harvard architecture) et de son MMU, il y a un attribut spécifique [url=https://docs.espressif.com/projects/esp-idf/en/latest/api-guides/general-notes.html#iram-instruction-ram]IRAM_ATTR[/url] pour dire qu'on veut qu'un bout de code soit au accessible à une adresse donnée en RAM. (important pour les interruptions puisqu'on veut donner un pointeur vers l'ISR et on ne voudrait pas que la MMU 'mappe' cette mémoire à une autre adresse. notre fonction ISR qui va compter le nombre de déclenchements pourrait s'écrire

volatile uint32_t compteurISR=0;

void IRAM_ATTR monInterruption() {
  compteurISR++;
}

Vu l'architecture, pour protéger les sections critiques, il faut aussi utiliser des techniques de verrou de FreeRTOS avec un portMUX_TYPE (il y en a un dans la structure hw_timer_t)

donc l'ISR sera plutôt comme cela:

volatile uint32_t compteurISR=0;
portMUX_TYPE verrou = portMUX_INITIALIZER_UNLOCKED;
 
void IRAM_ATTR monInterruption() {
  portENTER_CRITICAL_ISR(&verrou);
  compteurISR++;
  portEXIT_CRITICAL_ISR(&verrou);
}

Pour accrocher une ISR, il y a la fonction timerAttachInterrupt()

void timerAttachInterrupt(hw_timer_t* timer, void (*fn)(void), bool edge)

qui prend le timer, un pointeur vers la fonction ISR et un booléen qui dit si l'interruption se fait sur le changement du front (FALLING ou RISING) versus état établi HIGH ou LOW.

Donc on peut faire un

 timerAttachInterrupt(timer0, &monInterruption, true); // sur front

ensuite il suffit de dire tous les quand on veut l'appel de l'ISR avec timerAlarmWrite()

void timerAlarmWrite(hw_timer_t* timer, uint64_t alarm_value, bool autoreload)

qui prend en paramètre notre timer, la valeur du compteur pour déclencher l'ISR et si on veut ré-armer automatiquement une fois l'ISR déclenchée. Donc si je veux un appel toutes les 5 secondes je vais faire un

 timerAlarmWrite(timer0, 5000000ULL, true);

on compte jusqu'à 5,000,000 parce on a pris un diviseur à 80 (donc lorsque le compteur vaut 1 million on a une seconde, lorsqu'il vaut 5 millions on a 5 secondes)

il ne reste plus qu'à activer l'alarme de ce timer avec un timerAlarmEnable()

Un code pour avoir une interruption toutes les 5 secondes est donc:

// compte de 5 secondes en 5 secondes

hw_timer_t* timer0;
volatile uint32_t compteurISR = 0;
portMUX_TYPE verrou = portMUX_INITIALIZER_UNLOCKED;

uint32_t tempsTotal;

void IRAM_ATTR monInterruption() {
  portENTER_CRITICAL_ISR(&verrou);
  compteurISR++;
  portEXIT_CRITICAL_ISR(&verrou);
}

void imprimerTemps()
{
  Serial.print("T=");
  Serial.print(tempsTotal);
  Serial.println("s");
}

void setup() {
  Serial.begin(115200);

  // on configure le timer avec un prescale de 80 et compte à 5 million
  timer0 = timerBegin(0, 80, true);
  timerAttachInterrupt(timer0, monInterruption, true);  // sur edge
  timerAlarmWrite(timer0, 5000000ULL, true);
  timerAlarmEnable(timer0);

  imprimerTemps();
}

void loop() {

  if (compteurISR > 0) { 
    portENTER_CRITICAL(&verrou);
    compteurISR--;
    portEXIT_CRITICAL(&verrou);
    tempsTotal += 5;
    imprimerTemps();
  }
}

—————

et si on veut voir la valeur du compteur d'un timer en cours de route, on peut appeler

timerRead(timer0);

qui nous donne un uint64_t

donc par exemple pour imprimer un point toutes les demi secondes avant d'afficher le compte par 5s, on pourrait faire

// compte de 5 secondes en 5 secondes

hw_timer_t* timer0;
volatile uint32_t compteurISR = 0;
portMUX_TYPE verrou = portMUX_INITIALIZER_UNLOCKED;

uint32_t tempsTotal;

void IRAM_ATTR monInterruption() {
  portENTER_CRITICAL_ISR(&verrou);
  compteurISR++;
  portEXIT_CRITICAL_ISR(&verrou);
}

void imprimerTemps()
{
  Serial.print("\nT=");
  Serial.print(tempsTotal);
  Serial.println("s");
}

void setup() {
  Serial.begin(115200);

  // on configure le timer avec un prescale de 80 et compte à 5 million
  timer0 = timerBegin(0, 80, true);
  timerAttachInterrupt(timer0, monInterruption, true);  // sur edge
  timerAlarmWrite(timer0, 5000000ULL, true);
  timerAlarmEnable(timer0);

  imprimerTemps();
}

void loop() {

  static uint64_t lastTick = 0;

  if (timerRead(timer0) - lastTick >= 500000ULL) { // toutes les demi secondes
    lastTick += 500000ULL;
    Serial.write('.'); // on met un . à l'écran
  }

  if (compteurISR > 0) {
    portENTER_CRITICAL(&verrou);
    compteurISR--;
    lastTick = 0;
    portEXIT_CRITICAL(&verrou);
    tempsTotal += 5;
    imprimerTemps();
  }

}

68tjs espérait travailler au ras des registres ... :wink:

Corrigeons d'abord ce petit problème de C :

  uint64_t *compteur;
  timer_get_counter_value(0, 0, *compteur);

Si tu passes un pointeur à une fonction tu doit soit déclarer un pointeur et le passer simplement.
Mais il vaut mieux lui affecter une valeur sinon c'est pointeur NULL

  uint64_t *compteur;
  // manque affectation
  timer_get_counter_value(TIMER_GROUP_0, TIMER_0, compteur);

Soit déclarer une variable et passer l'adresse :

  uint64_t compteur;
  timer_get_counter_value(TIMER_GROUP_0, TIMER_0, &compteur);

Ce qui me semble plus correct.

hbachetti:
68tjs espérait travailler au ras des registres ... :wink:

Il y a un "petit truc" qui m'était passé largement au dessus de la tête : freertos, ses tâches et le double cœur ! :confused:

Je pense avoir compris le pourquoi du app_main() à la place du main()

Pour le reste (freertos et double cœur) je n'ai pas compris grand chose mais j'en ai compris assez pour me rendre compte que la marche est un peu trop élevée, retour sur Terre .

hbachetti:
68tjs espérait travailler au ras des registres ... :wink:

oui mais c'est un peu plus compliqué avec cette architecture et un RTOS qui tourne...

68tjs:
Il y a un "petit truc" qui m'était passé largement au dessus de la tête : freertos, ses tâches et le double cœur ! :confused:

oui, c'est pour cela qu'on ne peut pas faire tout comme on veut, l'OS offre cette couche d'abstraction. Si vous vouliez jouer au niveau des registres, ça revient à écrire un driver sans doute et là leurs APIs sont quasiment pas documentées

Je me permets une petite opinion personnelle.

Un HAL est utile dans le cas où l'on cherche à masquer les aspects matériels de différentes plateformes en ayant au final une interface commune.

Chez ST-Microelectronics, le HAL STM32CubeFX en est un bel exemple.
En effet les HAL STM32CubeF4, STM32CubeF1, STM32CubeF4, STM32CubeL0, STM32CubeL4 sont très proches.
Exemple pas totalement parfait mais tout de même largement exploitable, étant donné la variété des plateformes, absolument énorme.
On peut citer également le logiciel STM32CUBEMX qui permet de pondre un code d'initialisation en choisissant de manière graphique les fonctions de chaque GPIO :

Chez Espressif par contre la notion de HAL me semble encore en pleine gestation.

Sur ESP8266 : HAL absent. Sur ESP32 : HAL présent.

Développer un HAL pour un ESP32 part d'une bonne intention, mais c'est valable uniquement si cette interface est respectée dans l'avenir.
Sur le prochain ESPXX : qu'en sera t-il ?

On a vraiment l'impression en voyant l'arborescence source SDK que deux équipes totalement indépendantes ont travaillé sur ces deux sujets.

Quand on ne sait pas faire, on cherche l'inspiration ailleurs, chez les gens qui ont l'expérience.

Pas très mature tout ça ...

oui c'est clairement pas mature, mais c'est la bonne approche pour que leur investissement code soit portables aussi.. c'est sûr qu'ils ont peu d'antériorité

Il y a un "petit truc" qui m'était passé largement au dessus de la tête : freertos, ses tâches et le double cœur ! :confused:

FreeRTOS est totalement indépendant du HAL. Je ne vois pas ce qui peut empêcher d'utiliser le HAL sans tenir compte du noyau RTOS. Le HAL s'occupe du hardware. Le RTOS est du logiciel pur.

Normalement on devrait être capable d'écrire un code totalement indépendant du noyau temps réel, et même sans l'utiliser.
Chez ST-Microelectronics (encore ...) c'est bien le cas.

Encore une fois, sur ESP32 il n'y a pas d'OS à proprement parler, et pas de drivers au sens habituel du terme.
On se rapproche plus de la notion de librairie de gestion du matériel, en tous cas il n'y a pas la barrière habituelle d'un OS :

  • espace kernel + drivers
  • espace applicatif

Des nouvelles de mon "work in progress"à moi :grin:

Ça y est je peux lire le contenu d'un compteur de timer, cela a été laborieux mais c'est bon.
Mon premier essais a consisté à "tenter" d'évaluer la fréquence d'horloge des timers parce que la doc Espressif sur ce sujet ben c'est une doc Espressif :confused: .

Ce que je peux dire c'est que la fréquence de l'horloge des timers ne dépends pas de la fréquence que j'appellerais "micro" (80 MHz, 160 MHz et 240 MHz)

Il se pourrait que le chiffre de 80 MHz pour le bus horloge des timers soit le bon, ce qui donnerait une fréquence de travail moitié, soit 40 MHz.
Cette valeur est bien fixe et est obtenue avec des boucles à verrouillage de phase.

Pour un delai de 1000 µs réalisé avec delayMicrosecond() j'obtient une différence compteur2 - compteur1 de :
F micro = 240 MHz diff = 39853 ==> Ftimer = 39,853 MHz
F micro = 160 MHz diff = 40135 ==> Ftimer = 40,135 MHz [1]
F micro = 80 MHz diff = 40302 ==> Ftimer = 40,302 MHz [1]

[1] Dans le micro ESP32 il n'est pas anormal de mesurer des petits écarts entre 240 Mhz et 80 MHz.
La fréquence du timer est fixe mais d'autres parties peuvent tourner à 160 ou 240 MHz.

Pour la programmation mes sources de documentation sont le fichier timer.h et l'exemple tiré de : esp-idf/examples/peripherals/timer_group at f91080637c054fa2b4107192719075d237ecc3ec · espressif/esp-idf · GitHub.

Pour configurer et lancer le timer j'utilise :

void timer_configuration()
{
  timer_config_t config;
    config.divider     = TIMER_DIVIDER  ;
    config.counter_dir = TIMER_COUNT_UP ;
    config.counter_en  = TIMER_PAUSE    ;
    config.alarm_en    = TIMER_ALARM_EN ;
    config.intr_type   = TIMER_INTR_LEVEL ;
    config.auto_reload = TEST_WITHOUT_RELOAD   ;

  timer_init(TIMER_GROUP_0,  TIMER_0, &config) ;
  timer_set_counter_value(TIMER_GROUP_0, TIMER_0, 0x00000000ULL) ;
  timer_start(TIMER_GROUP_0, TIMER_0) ;
}

Pour lire le compteur du timer j'utilise :

timer_get_counter_value(TIMER_GROUP_0, TIMER_0, &compteur1);
delayMicroseconds(1000);
timer_get_counter_value(TIMER_GROUP_0, TIMER_0, &compteur2);

Pour afficher des uint32_t ou uint64_t je n'ai pas réussi avec les indications de J-M-L mais j'ai trouver ceci :

Serial.printf("Boucle = %" PRIu32 " Compteur1 =  %" PRIu64 "  Compteur2 = %" PRIu64 " Diff = %" PRIu64"\n", boucle, compteur1, compteur2, compteur2-compteur1);

".............%" PRIu32 "\n"

Mon fichier platformIO.ini

[env:lolin32]
platform = espressif32
framework = arduino
board = lolin32
monitor_speed = 115200
; change MCU frequency
board_build.f_cpu = 80000000L
;board_build.f_cpu = 160000000L
;board_build.f_cpu = 240000000L

ça progresse !!