ESP32 Lecture et écriture d'un registre connaissant son adresse

Bonjour,
C'est un peu la suite de mon post sur les tableaux de caractères mais le sujet a complètement dévié et je créé un nouveau fil.
Rappel du but : utilisation des registres de l'ESP32.

Document de référence ESP32Technical Reference Manual V4.1

Pour modifier un registre j'ai trouvé la macro REG_WRITE qui fonctionne bien et qui est dans le fichier soc.h du répertoire :
.arduino15/packages/esp32/hardware/esp32/1.0.4/tools/sdk/include/soc/soc
REG_WRITE(registre, bit)

Pour faire basculer la sortie des GPIOs j'ai trouvé les registres :
GPIO_OUT_W1TS_REG (W1TS = write one to set)
GPIO_OUT_WTC_REG (W1TC = write one to clear)

Cela fonctionne.
Je m'en suis servi pour voir les temps min de commutation d'un gpio.
Avec un analyseur logique clone de Saleae j'obtiens entre 41,6 ns et 83,3 ns ==> mesure imprécise car cet analyseur utilise une horloge de 24 MHz, avec un ESP32 il faudrait utiliser un analyseur beaucoup plus rapide.

Problème "logiciel" :
J'essaye de faire comme je le faisais avec les atmega : utiliser le compteur d'un timer .
Sauf que quand j'utilise le registre TIM0_T0CONFIG_REG le compilateur me dit que ce nom n'est pas défini.

Sur le document "ESP32 Technical Reference Manual V4.1" page 496 je trouve l'adresse de ces registres de configuration.
0x3FF5F00 pour le timer 0 du bloc 0
0x3ff60000 pour le timer 1 du bloc 0

0x3FF5F024 pour le timer 0 du bloc 1
0x3FF60024 pour le timer 1 du bloc 1

Une occasion d'utiliser les pointeurs et de voir ce que j'ai compris : en réalité pas grand chose :frowning: .
Je remercie d'avance ceux qui pourraient m'indiquer la bonne syntaxe pour manipuler le contenu des registres connaissant leur adresse.

Problème "matériel" :
Je n'arrive pas à trouver a fréquence de l'horloge qui pilote les timers. Un ESP32 est bien plus complexe qu'un atmega et l'horloge qui active les timers n'est pas l'horloge principale, mais qu'elle est sa valeur ?
J'ai bien vu dans le fichier soc.h :
#define TIMER_CLK_FREQ (80 000 000 >>4) //80MHz divided by 16
soit 5MHz mais je n'ose pas croire que c'est le vrai chiffre.

Merci pour vos réponses.

un pointeur vers une adresse mémoire précise sera sans doute de type

volatile uint32_t* TIMER0_BLOC0 = 0x3FF5F024;

TIMER0_BLOC0 va donc pointer vers cette case mémoire, ça s'utilise comme un pointeur

unsigned long x = *TIMER0_BLOC0; // lecture de ce qui est pointé
*TIMER0_BLOC0 = 0xDEADBEEF; // écriture des 4 octets à cette adresse

volatile est important pour que le compilateur ne fasse pas d'optimisations sauvages.


voici un code d'exemple qui pourra vous aider peut-être à visualiser mieux ce qu'il se passe en mémoire
(à utiliser sur un UNO ou MEGA)

uint16_t x = 0xAABB;
uint16_t* ptrX = &x;

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

  Serial.print("x vaut 0x");
  Serial.println(x, HEX);

  Serial.print("x est rangé en mémoire a partir de l'adresse 0x");
  Serial.println((uint16_t) ptrX, HEX); // on afficher l'adresse mémoire sous forme de nombre

  // on peut voir qu'on a une architecture Little Endian
  uint8_t* pointeurOctet = (uint8_t*) ptrX; // pointeurOctet pointe sur des octets, si on veut lire la mémoire octet par octet

  Serial.print("L'octet a l'adresse 0x");
  Serial.print((uint16_t) pointeurOctet, HEX);
  Serial.print(" est 0x");
  Serial.println(*pointeurOctet, HEX); // on afficher l'adresse mémoire sous forme de nombre

  // on affiche la case mémoire suivante
  Serial.print("L'octet a l'adresse 0x");
  Serial.print((uint16_t) (pointeurOctet + 1), HEX);
  Serial.print(" est 0x");
  Serial.println(*(pointeurOctet + 1), HEX); // on afficher l'adresse mémoire sous forme de nombre


  uint16_t y = *ptrX; // on va lire le contenu de cette addresse
  Serial.print("y vaut 0x");
  Serial.println(y, HEX); // et on voit bien que c'est comme x

  *ptrX = 0xEEFF; // on va modifier les 2 octets pointés
  Serial.print("x vaut 0x"); // et donc ça modifie x qui vaut maintenant EEFF
  Serial.println(x, HEX);
}

void loop() {}

la console vous dira

[color=purple]x vaut 0xAABB
x est rangé en mémoire a partir de l'adresse 0x200
L'octet a l'adresse 0x200 est 0xBB      [color=green]<<== on voit architecture little endian[/color]
L'octet a l'adresse 0x201 est 0xAA      [color=green]     avec l'octet de poids faible en premier[/color]
y vaut 0xAABB
x vaut 0xEEFF
[/color]

je m'inscris ici car le sujet m'intéresse
je suis toujours à la recherche du moyen d'activer les buffers analogiques de sortie des DACs, buffers non documentés, mais qui existent selon les gens de chez python
mon "idée" est de positionner des bits au pif dans les registres pour voir ce qui se passe ...

bonjour,
Je reprends.

Avant de poster j'avais fait des essais d'utilisation de pointeurs :
Avec Code::Blocks

#include <iostream>
using namespace std;

uint32_t valeur;
uint32_t *ptr1 ;

int main(){
   valeur = 48 ;
   printf("valeur initiale = %d \n",valeur);
   ptr1 = &valeur ;
   printf("adresse valeur  = %p \n",&valeur);
   printf("adresse ptr     = %p \n",&ptr1);

   *ptr1 = 56;
   printf("valeur finale pt1  = %d \n",valeur);
    return 0;
}

Ce qui me donnait en sortie console :

valeur initiale = 48
adresse valeur = 0x5637d64ef048
adresse ptr = 0x5637d64ef050
valeur finale pt1 = 56

La même chose avec l'ESP32

uint32_t valeur ;
uint32_t *ptr ;

void setup()
{
  Serial.begin(115200);
  valeur = 48 ;
  Serial.printf("valeur initiale = %d \n", valeur);
  ptr = &valeur ;
  Serial.printf("adresse valeur  = %p \n", &valeur);
  Serial.printf("adresse ptr     = %p \n", &ptr);

  *ptr = 56;
  Serial.printf("valeur finale valeur  = %d \n", valeur);
}

void loop(){
}

Avec en sortie :

valeur initiale = 48
adresse valeur = 0x3ffc00b8
adresse ptr = 0x3ffc00b4
valeur finale valeur = 56

J'étais content puisque je changeais bien le contenu de "valeur" en me servant d'un pointeur.
Il y a quand même quelque chose que je ne comprend pas :
Si ptr pointe sur l'adresse de la variable "valeur" il devrait avoir la même adresse .
Or je remarque que sur le PC l'adresse du pointeur est décallé de +2 et sur l'ESP32 elle est décalée de -4.

Si je déclare un autre pointeur qui pointe lui aussi sur la variable "valeur" les deux pointeurs ont la même adresse.
Si je fait pointer ptr1sur l'adresse de "valeur" et ptr2 sur l'adresse de ptr1 alors ptr1 et ptr2 ont toujours la même adresse.

Quand j'ai tenté d'appliquer aux registres de l'ESP j'obtenais un message d'erreur que j’obtiens toujours avec les indications de J-M-L.

volatile uint32_t *TIM0_BLOC0 =  0x3FF5F000;
void setup() {
}

void loop() {                   
}

J'obtiens le message d'erreur :

registres_timer_forum1:5:34: error: invalid conversion from 'int' to 'volatile uint32_t* {aka volatile unsigned int*}' [-fpermissive]
volatile uint32_t TIM0_BLOC0 = 0x3FF5F000;
^
exit status 1
invalid conversion from 'int' to 'volatile uint32_t
{aka volatile unsigned int*}' [-fpermissive]

J'obtiens quand même l'accès en lecture en utilisant un #define TIM0_B0 0x3FF5F000 comme j'en ai vu l'emploi dans le fichier soc.h.

#define TIM0_B0 0x3FF5F000
void setup() {
  Serial.begin(115200);  
  Serial.printf("Valeur de TIM0_B0 = %d \n",TIM0_B0);
}

void loop() {                      
}

Par contre dès que je veux modifier le contenu de ce registre (fonction configT0B0) j'obtiens le message d'erreur :

#define TIM0_B0 0x3FF5F000

void setup() {
  Serial.begin(115200); 
  Serial.printf("Valeur de TIM0_B0 = %d \n",TIM0_B0);
  configT0B0(); 
}

void loop() {                      
}

void configT0B0(){
    uint32_t modif= 0b1100000000000000;
    TIM0_B0 |= modif;
}

/home/b/10_prog/00_ESP/20_ESP32/Programmes/registres_timer/registres_timer.ino: In function 'void configT0B0()':
registres_timer:25:13: error: lvalue required as left operand of assignment
** TIM0_B0 |= modif;**
** ^**
exit status 1
lvalue required as left operand of assignment

fais-je une erreur de syntaxe ?
Je suis passé par une variable intermédiaire déclarée en uint32_t cela n'a rien changé.

Actuellement je peux lire un registre connaissant son adresse mais je ne peux pas le modifier.

Il y a quand même quelque chose que je ne comprend pas :
Si ptr pointe sur l'adresse de la variable "valeur" il devrait avoir la même adresse .

Vous faites

 ptr = &valeur ;
  Serial.printf("adresse valeur  = %p \n", &valeur);
  Serial.printf("adresse ptr     = %p \n", &ptr);

Quand vous regardez valeur, c'est une variable et vous avez alloué 4 octets (uint32_t) pour stocker le nombre (48 ici). Ces 4 octets sont rangés quelque part en mémoire, la première case mémoire est ce qu'on appelle l'adresse de la variable (et l'ordre des octets dans ces cases dépend de l'architecture little ou big endian).

Il en va de même pour ptr. C'est une variable, donc comme toute variable il a besoin d'espace mémoire pour ranger son contenu qui ici sera l'adresse mémoire de la variable valeur. Ce contenu se trouve à une certaine adresse

donc dans les octets réservés pour la variable ptr1, vous avez l'adresse de valeur et ces octets sont stockés forcément ailleurs. c'est pour cela que ce n'est pas le même nombre que vous imprimez.

si vous aviez imprimé le contenu du pointeur et non pas son adresse

   printf("adresse valeur  = %p \n",&valeur);
   printf("adresse ptr     = %p \n",ptr1);

là vous devriez voir la même adresse mémoire.

Or je remarque que sur le PC l'adresse du pointeur est décallé de +2 et sur l'ESP32 elle est décalée de -4.

Non pas +2 :slight_smile:
Vous avez oublié que vous êtes en hexadécimal donc après 9 il y a A,B,C,D,E,F avant le 0

donc ce que vous voyez

valeur initiale = 48 
adresse valeur  = 0x5637d64ef048 
adresse ptr     = 0x5637d64ef050 
valeur finale pt1  = 56

qui veut dire que c'est rangé comme cela en mémoire.

Sur l'ESP32 vous voyez ceci

valeur initiale = 48 
adresse valeur  = 0x3ffc00b8 
adresse ptr     = 0x3ffc00b4 
valeur finale valeur  = 56

qui veut dire que c'est rangé comme cela en mémoire.

Le nombre d'octet pour représenter une adresse dépend de la plateforme: sur un UNO c'est 2 octets, sur un ESP32 c'est 4 octets et sur votre PC vous êtes en architecture 64 bits et c'est 8 octets.

De plus à la compilation l'endroit où on range les données peut se faire de bas en haut ou de haut en bas dans l'ordre où on les rencontre et elles ne sont pas forcément contigües le compilateur aimant bien que les adresses tombent sur des multiples de la taille de l'architecture donc sur le PC de 8 en 8 et sur l'ESP de 4 en 4. c'est pour cela que le pointeur et la variable valeur sont côte à côte en mémoire sur l'ESP alors qu'il y a un "trou" (mémoire non utilisée) sur le PC entre les 2.


Pour ce point

Quand j'ai tenté d'appliquer aux registres de l'ESP j'obtenais un message d'erreur que j'obtiens toujours avec les indications de J-M-L.

volatile uint32_t *TIM0_BLOC0 =  0x3FF5F000;

void setup() {}
void loop() {}



J'obtiens le message d'erreur :
registres_timer_forum1:5:34: error: invalid conversion from 'int' to 'volatile uint32_t* {aka volatile unsigned int*}' [-fpermissive]
volatile uint32_t *TIM0_BLOC0 = 0x3FF5F000;
^
exit status 1
invalid conversion from 'int' to 'volatile uint32_t* {aka volatile unsigned int*}' [-fpermissive]

c'est effectivement que le compilateur est un peu plus sensible et qu'il faut sans doute faire un cast de la valeur numérique en adresse mémoire lors de l'affectation. essayez

volatile uint32_t* TIM0_BLOC0 =  (uint32_t*) 0x3FF5F000;
void setup() {}
void loop() {}

concernant

J'obtiens quand même l'accès en lecture en utilisant un #define TIM0_B0 0x3FF5F000 comme j'en ai vu l'emploi dans le fichier soc.h.
....
Par contre dès que je veux modifier le contenu de ce registre (fonction configT0B0) j'obtiens le message d'erreur :

c'est normal, souvenez vous qu'un #define est remplacé syntaxiquement avant la compilation donc votre

    uint32_t modif= 0b1100000000000000;
    TIM0_B0 |= modif;

va devenir

    uint32_t modif= 0b1100000000000000;
    0x3FF5F000 |= modif;

--> 0x3FF5F000 est vu comme un entier et un entier ne peut pas se trouver à gauche d'une affectation. avec

volatile uint32_t* TIM0_BLOC0 =  (uint32_t*) 0x3FF5F000;

vous devriez pouvoir effectuer un

  const uint32_t modif= 0b1100000000000000;
  *TIM0_BLOC0 |= modif;

PS/ notez que pour imprimer un uint32_t avec printf on utilise pas %d qui serait bon pour un entier signé (même si ça va marcher dans votre cas car les octets seront au bon endroit et vous n'avez pas une grande valeur) mais on doit utiliser %ul

hello
instructif, :slight_smile: MERCI MONSIEUR :slight_smile:

De rien :wink:

Bon je n'ai plus d'erreur, merci J-M-L de ta patience.

Par contre impossible de modifier quoi que ce soit.
Avec un 328p et la doc Atmel aucun souci, pas une interrogation : composant de professionnel.

avec plaisir :slight_smile:

oui la doc des ESP n'est pas un modèle du genre...

qu'est-ce que vous essayez de faire exactement?

J'essayai de mettre en route le timer pour mesurer le temps d'écriture dans une sortie en utilisant les indications du registre compteur du timer. L'unité étant les cycles horloge.

Cela n'a rien d'indispensable, j'ai lu un message dans lequel quelqu'un se posait la question et j'ai essayé d'y répondre en utilisant la même méthode que j'avais utilisé sur Atmel.

Je suis arrivé à la conclusion que ce n'est pas possible parce que l'ESP32 est un microcontôleur particulier :
C'est une base de microcontroleur (Tensilica Xtensa) qui est "à terminer" par les clients comme Espressif.

La notion de "à terminer" est vaste. Si c'est juste des masques métalliques c'est peu onéreux, s'il faut utiliser des "briques de base" (building block) dans le silicium ou de l'IP il faut les acheter et parfois très cher.

J'ai pu voir que contrairement aux microcontroleurs Atmel, STM32,etc... l'UART, l'I2C, le SPI ne sont pas matériels mais logiciels. Espressif pour tirer les prix n'a pas acheté les buiding blocks matériels correspondants.
Mesuré à l'analyseur logique le temps pour faire changer d'état une sortie pour l'ESP8266 avec digitalWrite est catastrophique, ramené à la fréquence horloge un atmega328p est plus rapide !

C'est aussi le cas pour les registres qui probablement ne sont pas exactement des emplacements fixes dans une partie spéciale de la mémoire comme dans les microcontrôleurs habituels.
Je pense que pour le faire il aurait fallu modifier beaucoup de masques, les "registres Espressif" sont probablement des mémoires tout juste un peu spéciales . Le problème est de quantifier le "tout juste un peu".

La documentation Xtensa que j'avais trouvé est une documentation "développeur" et absolument pas une doc utilisateur, je l'ai benné je n'y comprenais rien.

Conclusion :
Pour le moment je mets en sommeil et je me recentre sur ce qui est prévu "dans le moule Arduino".

Vous avez jeté un oeil à leur Hardware Abstraction Layer

En gros ils disent de ne l’utiliser tel quel et de dépendre de fonctions de plus haut niveau

The HAL layer is designed to be used by the drivers. We don't guarantee the stability and back-compatibility among versions. The HAL layer may update very frequently with the driver. Please don't use them in the applications or treat them as stable APIs.

Je suppose que ça veut dire qu’il faut passer par timer_get_counter_value()

 /**
 * @brief Read the counter value of hardware timer.
 *
 * @param group_num Timer group, 0 for TIMERG0 or 1 for TIMERG1
 * @param timer_num Timer index, 0 for hw_timer[0] & 1 for hw_timer[1]
 * @param timer_val Pointer to accept timer counter value.
 *
 * @return
 *     - ESP_OK Success
 *     - ESP_ERR_INVALID_ARG Parameter error
 */
esp_err_t timer_get_counter_value(timer_group_t group_num, timer_idx_t timer_num, uint64_t* timer_val);

Et ce qui est décrit dans la documentation

Alors pourquoi il existe ce document qui décrit les registres :
https://www.espressif.com/sites/default/files/documentation/esp32_technical_reference_manual_en.pdf
s'il ne faut pas les utiliser ?
La dernière édition date du 21 novembre 2019.
Il manque un responsable de la documentation chez Espressif.

Oui ou peut-être pour les développeurs de drivers...

Bonjour

l'accès concurrent des deux coeurs au même espace mémoire, l'intervention de Rtos.... ça complique ceratineemnt pas mal les choses. Espressif n'a pas documenté la manière d'accéder directement ('bare metal') aux registres.
Outils ? méthodes ?

les retours d'un moteur de recherche avec 'ESP32 bare metal' sont maigres ....

en voici deux :

https://vivonomicon.com/2019/03/30/getting-started-with-bare-metal-esp32-programming/

Un soc à deux coeurs géré sous Rtos n'est pas comparable avec un microncontrolleur géré 'en direct'. On est peut là face à un usage qui, du point de vue d'Espressif, relève du contact avec un ingénieur d'application, situation fréquente avec tous les fabricants de micros, dès qu'on sort de ce qu'il considère comme usage 'normal')

Quand on développe un logiciel avec ARDUINO la loopTask() est une tâche FreeRTOS.
La création de la loopTask est planquée dans la lib sdk.

Y a t-il d'autres tâches ? vTaskList() permettrait de le dire.

FreeRTOS est un noyau temps réel, rien de plus, au même titre que NUCLEUS ou d'autres.

Mais de là à imaginer que l'accès aux registres soit interdit à cause de l'existence de modes d'exécution SUPERVISEUR et USER, il y a un pas que je ne franchirai pas.

FreeRTOS tourne aussi sur Cortex Cortex-A8 Cortex-A9, mais dans un mode purement SUPERVISEUR. Il n'exploite absolument pas sur ces processeurs la gestion de deux espaces SUPERVISEUR et USER, comme Linux le ferait.

A mon sens si l'accès aux registres était protégé sur un ESP32 un accès sauvage provoquerait une exception.

Un petit test simple :

#include <FreeRTOS.h>

// dans la loop()
  vTaskList(ptrTaskList);  // permet d'afficher la liste des tâches
  Serial.print(ptrTaskList);

Compilation :

/tmp/arduino_modified_sketch_504944/HelloServer.ino:102: undefined reference to `vTaskList'

Compilation OK, lien KO. Cette fonction existe pourtant dans FreeRTOS.

Dans esp-idf/components/freertos/tasks.c

#if ( ( configUSE_TRACE_FACILITY == 1 ) && ( configUSE_STATS_FORMATTING_FUNCTIONS > 0 ) )

 void vTaskList( char * pcWriteBuffer )
 {

Pas étonnant. La compilation de vTaskList est conditionnelle.
Il ont peur de quoi ?
Pouvoir lister les tâches dans un système temps réel est-il si extraordinaire ?

Autre chose :

esp-idf/components/esp_http_server/src/httpd_main.c :

Dans la fonction httpd_create à aucun moment la variable config.core_id n'est initialisée.
Cela veut dire core_id reste à zéro -> CŒUR ZÉRO

Vous croyez que votre serveur WEB ESP32 fait travailler deux cœurs ?
Certainement pas !
Si vous voulez faire bosser le deuxième cœur : retroussez vos manches, xTaskCreatePinnedToCore() est à votre disposition pour vos propres tâches.

Enfin bref pour moi : SDK ESP32 = PIPEAU

hbachetti:
Enfin bref pour moi : SDK ESP32 = PIPEAU

allez soyez généreux, on va dire "work in progress" :slight_smile:

work in progress ...

50% c'est généreux :slight_smile:

Vous croyez que votre serveur WEB ESP32 fait travailler deux cœurs ?
Certainement pas !

Heu ... pas tout à fait.

esp-idf/components/esp_http_server/src/httpd_main.c :

Dans la fonction httpd_create à aucun moment la variable config.core_id n'est initialisée.
Cela veut dire core_id reste à zéro -> CŒUR ZÉRO

En fait il semblerait que ce http_server du SDK ne soit pas utilisé par les librairies ESP32, seulement dans un exemple : ESP32/examples/Camera/CameraWebServer

Par contre la tâche WIFI est lancée comme ceci :

WiFi/src/WiFiGeneric.cpp:

#if CONFIG_FREERTOS_UNICORE
#define ARDUINO_RUNNING_CORE 0
#else
#define ARDUINO_RUNNING_CORE 1
#endif
// ...
        xTaskCreatePinnedToCore(_network_event_task, "network_event", 4096, NULL,
ESP_TASKD_EVENT_PRIO - 1, &_network_event_task_handle, ARDUINO_RUNNING_CORE);

Comme CONFIG_FREERTOS_UNICORE n'est défini nulle part le coeur utilisé pour le WIFI est le N°1.

Par contre la tâche ARDUINO est elle aussi créée sur ce même ARDUINO_RUNNING_CORE.

Il faut aller chercher dans le SDK pour trouver des créations de tâches sur le cœur N°0.
Par exemple la tâche timers FreeRTOS et le BT.

On peut donc affirmer que les deux cœurs sont utilisés.

On peut dire aussi que la faible utilisation du cœur N°0 permet de lui donner éventuellement un peu plus de travail.

Le débroussaillage est long et demanderait un peu plus d'outillage, et des manipulations.