ULP (ESP32) : un exemple simple

L’ULP est un coprocesseur à très faible consommation (Ultra Low Power) intrégré au MCU ESP32. Il s’agit donc d’un tout petit processeur qui fonctionne quasiment indépendemment des coeurs principaux et qui peuvent accéder aux GPIO, à certains périphériques et qui dispose aussi d’un contrôlleur I²C.

L’ULP est aussi capable de fonctionner alors que l’ESP32 est en mode deep-sleep. Dans ce mode, quasiment tous le MCU n’est plus alimenté, excepté le domaine RTC, dans lequel se trouve l’ULP. L’ULP est aussi capable de réveiller l’ESP32 (wake-up).

On pourrait envisager d’utiliser l’ULP pour effectuer une mesure de température et réveiller l’application si on dépasse un certain seuil, par exemple.

Programmer cet ULP semble donc très intéressant, mais malgré la documentation toujours très complète de Espressif, j’ai eu du mal à trouver un exemple simple et complet expliquant la programmation de ce coprocesseur.

Voici donc en détail un exemple ultra-simple : le traditionel « blink« , qui consiste à faire clignoter deux LEDs du kit WROVER-KIT V3!

L’exemple que je vais présenter ici consiste à faire clignoter deux LED (la bleue et la verte de mon ESP32-WROVER-KIT V3). La première sera pilotée par le coeur applicatif normal, via une boucle et un delay, le seconde sera pilotée par l’ULP.

Installation de la toochain ULP

L’installation de la toolchain est documentée dans la documentation officielle.

Le projet & CMake

Mhm au passage : l’ULP se programme via son language assembleur, il n’y a visiblement pas de compilateur C disponible 🙂

Nous allons donc commencer par créer un fichier ayant l’extension .S dans un sous-répertoire de notre projet. Je l’ai appelé ulp/main-ulp-blink.S.

Ensuite, nous allons modifier le fichier CMakeLists.txt du component main afin d’y référencer le code de l’ULP:

set(COMPONENT_SRCS "ulp_blink.c")
set(COMPONENT_ADD_INCLUDEDIRS "")
register_component()

# Ajouter les lignes suivantes pour compiler le code de l'ULP
set(ULP_APP_NAME ulp_${COMPONENT_NAME})
set(ULP_S_SOURCES ulp/main-ulp-blink.S)
set(ULP_EXP_DEP_SRCS "main-ulp-blink.c")
include(${IDF_PATH}/components/ulp/component_ulp_common.cmake)

Le code du programme « applicatif »

#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "esp_spi_flash.h"
#include "driver/gpio.h"
#include "esp32/ulp.h"
#include "ulp_main.h"
#include "driver/rtc_io.h"

extern const uint8_t bin_start[] asm("_binary_ulp_main_bin_start");
extern const uint8_t bin_end[]   asm("_binary_ulp_main_bin_end");

void app_main() {
  /* Init GPIO 4 (blue), which will be driven by the main core */
  gpio_config_t io_conf;
  io_conf.intr_type = GPIO_PIN_INTR_DISABLE;
  io_conf.mode = GPIO_MODE_OUTPUT;
  io_conf.pin_bit_mask = GPIO_SEL_4;
  io_conf.pull_down_en = 0;
  io_conf.pull_up_en = 0;
  gpio_config(&io_conf);

  /* Init GPIO2 (RTC_GPIO 12, green) which will be driven by ULP */
  rtc_gpio_init(2);
  rtc_gpio_set_direction(2, RTC_GPIO_MODE_OUTPUT_ONLY);
  rtc_gpio_set_level(2,0);

  vTaskDelay(1000/portTICK_PERIOD_MS);

  /* Start ULP program */
  ESP_ERROR_CHECK( ulp_load_binary(0, bin_start, (bin_end - bin_start) / sizeof(uint32_t)));
  ESP_ERROR_CHECK( ulp_run(&ulp_entry - RTC_SLOW_MEM) );

  bool s = false;
  for (;;) {
    s = !s;
    gpio_set_level(4, (s ? 1 : 0));
    vTaskDelay(1000/portTICK_PERIOD_MS);
  }
}

Ce programme, volontairement très simple, commence par initialiser les GPIO : GPIO 4 pour la LED bleue du WROVER-KIT V3, et GPIO 2 pour la led verte du kit. Remarquez que l’on passe par des méthodes rtc pour initialiser la LED verte, qui sera pilotée par l’ULP.

Il programme ensuite l’ULP et démarre ce programme, puis rentre dans une boucle infinie qui va faire clignoter la LED bleue toute les secondes, pendant que l’ULP s’occupera de la LED verte.

Le code du programme ULP

Pour info, voici la documentation du language assembleur. Ce document vous sera très utile pour comprendre et écrire vos propres programmes!

#include "soc/rtc_cntl_reg.h"
#include "soc/soc_ulp.h"
#include "soc/rtc_io_reg.h"

    .bss

    .text
	.global entry
entry:
    .global loop1

loop1:
    WRITE_RTC_REG(RTC_GPIO_OUT_REG,RTC_GPIO_OUT_DATA_S+12,1,1)
    WAIT 65535
    WAIT 65535
    WAIT 65535
    WAIT 65535
    WAIT 65535
    WAIT 65535
    WAIT 65535
    WAIT 65535
    WAIT 65535
    WAIT 65535
    WAIT 65535
    WAIT 65535
    WAIT 65535
    WAIT 65535
    WAIT 65535
    WAIT 65535
    WAIT 65535
    WAIT 65535
    WRITE_RTC_REG(RTC_GPIO_OUT_REG,RTC_GPIO_OUT_DATA_S+12,1,0)
    WAIT 65535
    WAIT 65535
    WAIT 65535
    WAIT 65535
    WAIT 65535
    WAIT 65535
    WAIT 65535
    WAIT 65535
    WAIT 65535
    WAIT 65535
    WAIT 65535
    WAIT 65535
    WAIT 65535
    WAIT 65535
    WAIT 65535
    WAIT 65535
    WAIT 65535
    WAIT 65535

    JUMP loop1

Wow, cela fait beaucoup de WAITs… je vais y venir 🙂

Le programme, en assembleur ULP, commence par déclarer un segment BSS vide (c’est là qu’on déclare des variables, mais ce programme est tellement simple qu’il n’en a pas!). Vient ensuite le segment TEXT qui contient le code.

Ce code consiste à écrire un 1 sur la GPIO2/RTC_GPIO12, attendre, attendre, attendre encore, puis écrire un 0 sur cette même GPIO, attendre encore et recomencer.

Pourquoi autant de WAITs? Le mnémonique WAIT prend en opérande une valeur sur 16bits (0..65535). Il s’agit du nombre de cycles (@8Mhz) que l’on souhaite attendre avant de passer à l’instruction suivante. Si on lui passe une valeur plus grande, l’assembleur ne va tenir compte que des 16 bits de poids faible, en ignorant les bits de poids fort, et donc attendre sans doute moins longtemps que prévu.

Dans un soucis d’extrême simplicité, j’ai décidé de ne pas écrire une vraie boucle et de simplement ajouter plusieurs WAITs pour attendre suffisement longtemps pour que cela soit visible à l’oeil nu.

Le code source de cet example est disponible sur mon GitHub.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *