| name | esp32-s3-deep-learning |
| description | Deep Learning natif sur ESP32-S3 — ESP-DL, ESP-NN (optimisé pour Xtensa), accélération matérielle PIE/TIE, TFLite Micro avec custom kernels, traitement audio/vision temps réel, PSRAM, optimisation énergétique et déploiement. |
| version | 1.0.0 |
| author | EVA |
| license | Privée EVA St-Étienne |
| platforms | ["linux","macos","windows"] |
| metadata | {"EVA":{"tags":["esp32-s3","esp-dl","esp-nn","xtensa","tflite-micro","edge-ai","esp-idf","deep-learning","audio","computer-vision"],"related_skills":["tinyml-fundamentals","tinyml-mcu-inference","tensorflow-lite-deep-dive","model-optimization-edge"]}} |
Deep Learning sur ESP32-S3
Vue d'ensemble
L'ESP32-S3 d'Espressif est le SoC le plus performant de la gamme ESP32 pour l'IA embarquée. Il intègre un processeur Xtensa LX7 dual-core à 240 MHz, une extension vectorielle (PIE) optimisée pour les réseaux de neurones, jusqu'à 512 KB SRAM + 16 MB PSRAM, et un accélérateur cryptographique.
Spécifications clés
| Paramètre | Valeur |
|---|
| CPU | Xtensa LX7 dual-core @ 240 MHz |
| SRAM | 512 KB (TCM + cache) |
| PSRAM | Jusqu'à 16 MB (octal SPI) |
| Flash | Jusqu'à 16 MB |
| DSP | PIE (Parallel Instruction Engine) — SIMD vectoriel |
| Instructions NN | S3E (esp-nn optimisé Xtensa) |
| Connectivité | WiFi 802.11 b/g/n + BLE 5.0 |
| Périphériques | USB OTG, LCD (8080), Camera (DVP), I2S |
| Consommation inference | ~40 mW (ML), ~10 μW (deep sleep) |
Pipeline d'inférence ML sur ESP32-S3
┌──────────────────────────────────────────────────────┐
│ Entrée (Camera/Mic/IMU via I2S/SPI/DVP) │
├──────────────────────────────────────────────────────┤
│ Prétraitement (PIE vectorisé) │
├──────────────────────────────────────────────────────┤
│ Inférence ML │
│ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │
│ │ TFLite │ │ ESP-DL │ │ ESP-NN kernels │ │
│ │ Micro │ │ (C++ ML) │ │ (Xtensa LX7 opt.) │ │
│ └──────────┘ └──────────┘ └──────────────────┘ │
├──────────────────────────────────────────────────────┤
│ Post-traitement (IA, filtrage) │
└──────────────────────────────────────────────────────┘
1. ESP-NN : Bibliothèque de Kernels Neuronaux
1.1 Architecture ESP-NN
ESP-NN est la bibliothèque de kernels ML optimisés pour les processeurs Xtensa d'Espressif. Elle exploite l'extension PIE (Parallel Instruction Engine) qui permet des opérations SIMD vectorielles en une seule instruction.
1.2 Convolution 2D avec ESP-NN
#include "esp_nn.h"
void convolution_int8_example() {
const int input_h = 32, input_w = 32, input_c = 3;
const int output_h = 16, output_w = 16, output_c = 8;
const int kernel_h = 3, kernel_w = 3;
const int stride_h = 2, stride_w = 2;
const int pad_h = 1, pad_w = 1;
int8_t *input_data = (int8_t *)heap_caps_malloc(
input_h * input_w * input_c, MALLOC_CAP_DEFAULT);
int8_t *output_data = (int8_t *)heap_caps_malloc(
output_h * output_w * output_c, MALLOC_CAP_DEFAULT);
int8_t *weights = (int8_t *)heap_caps_malloc(
output_c * input_c * kernel_h * kernel_w, MALLOC_CAP_DEFAULT);
int32_t *bias = (int32_t *)heap_caps_malloc(
output_c * sizeof(int32_t), MALLOC_CAP_DEFAULT);
const int32_t input_offset = -128;
output_offset = ;
*bias_data = bias;
*output_shift = ([]){, , , , , , , };
*output_mult = ([]){
, , , ,
, , ,
};
activation_min = ;
activation_max = ;
esp_nn_conv_s8(
input_data,
input_w, input_h, input_c,
weights,
output_data,
output_w, output_h, output_c,
kernel_w, kernel_h,
stride_w, stride_h,
pad_w, pad_h,
input_offset, output_offset,
bias_data,
output_shift,
output_mult,
activation_min, activation_max,
);
();
heap_caps_free(input_data);
heap_caps_free(output_data);
heap_caps_free(weights);
heap_caps_free(bias);
}
1.3 Benchmark ESP-NN vs CMSIS-NN
void bench_esp_nn() {
const int iterations = 1000;
uint32_t start, elapsed;
float total = 0;
for (int i = 0; i < iterations; i++) {
start = esp_timer_get_time();
esp_nn_conv_s8(
input, 16, 16, 3,
weights,
output, 8, 8, 8,
3, 3, 2, 2, 1, 1,
input_offset, output_offset,
bias, shift, mult,
-128, 127, scratch
);
elapsed = esp_timer_get_time() - start;
total += elapsed;
}
printf("Temps moyen per conv (ESP-NN) : %.2f µs\n", total / iterations);
}
2. ESP-DL : Framework Deep Learning
2.1 Architecture ESP-DL
ESP-DL est le framework DL natif d'Espressif pour ESP32-S3/S2. Il permet de définir et exécuter des réseaux de neurones en C++ natif (sans TFLite).
#include "esp_dl.hpp"
using namespace dl;
class DetecteurVisage : public Model {
private:
Conv2D *conv1;
DepthwiseConv2D *dw2;
Conv2D *pw2;
Conv2D *conv3;
Conv2D *conv_out;
GlobalAveragePool2D *gap;
public:
DetecteurVisage() {
conv1 = new Conv2D(8, {3, 3}, {2, 2}, {1, 1}, "conv1");
conv1->set_activation(ReLU::get_instance());
dw2 = new DepthwiseConv2D({3, 3}, {1, 1}, {1, 1}, );
dw2->();
pw2 = (, {, }, {, }, {, }, );
pw2->(ReLU::());
conv3 = (, {, }, {, }, {, }, );
conv3->(ReLU::());
conv_out = (, {, }, {, }, {, }, );
conv_out->();
gap = ();
}
{
x = conv1->forward(input);
x_dw = dw2->forward(x);
x_pw = pw2->forward(x_dw);
x_add = x->(x_pw);
x;
x_dw;
x2 = conv3->forward(x_add);
x_add;
x3 = conv_out->forward(x2);
x2;
output = gap->forward(x3);
x3;
output;
}
};
2.2 Entraînement et conversion ESP-DL
def exporter_poids_espdl(poids_numpy: np.ndarray, nom: str, taille_coeff: int):
"""Exporte des poids numpy vers format ESP-DL (INT8 + coeff)."""
max_val = np.max(np.abs(poids_numpy))
scale = max_val / 127.0
poids_int8 = np.round(poids_numpy / scale).astype(np.int8)
with open(f"{nom}.hpp", "w") as f:
f.write("#pragma once\n\n")
f.write("#include <stdint.h>\n\n")
f.write(f"// Poids pour couche {nom}\n")
f.write(f"// Scale = {scale:.10f}\n\n")
flat = poids_int8.flatten()
f.write(f"const int8_t {nom}_weight[] = {{\n")
for i in range(0, len(flat), 16):
line = ", ".join(f"{v}" v flat[i:i+])
f.write()
f.write()
f.write()
coeff = (scale * ( << taille_coeff))
f.write()
f.write()
()
2.3 Détection de visages avec ESP-DL
#include "esp_dl.hpp"
#include "esp_camera.h"
#include "model_face_coeff.hpp"
#define CAMERA_FRAME_SIZE FRAMESIZE_QVGA
#define INPUT_SIZE 64
int8_t *input_buffer;
extern "C" void app_main() {
camera_config_t camera_config = {
.pin_pwdn = -1,
.pin_reset = -1,
.pin_xclk = GPIO_NUM_15,
.pin_sscb_sda = GPIO_NUM_4,
.pin_sscb_scl = GPIO_NUM_5,
.pin_d7 = GPIO_NUM_16,
.pin_d6 = GPIO_NUM_17,
.pin_d5 = GPIO_NUM_18,
.pin_d4 = GPIO_NUM_12,
.pin_d3 = GPIO_NUM_10,
.pin_d2 = GPIO_NUM_8,
.pin_d1 = GPIO_NUM_9,
.pin_d0 = GPIO_NUM_11,
.pin_vsync = GPIO_NUM_6,
.pin_href = GPIO_NUM_7,
.pin_pclk = GPIO_NUM_13,
.xclk_freq_hz = 20000000,
.ledc_timer = LEDC_TIMER_0,
.ledc_channel = LEDC_CHANNEL_0,
.pixel_format = PIXFORMAT_GRAYSCALE,
.frame_size = CAMERA_FRAME_SIZE,
.jpeg_quality = 12,
.fb_count = 1,
};
esp_camera_init(&camera_config);
DetecteurVisage detecteur;
input_buffer = (int8_t *)heap_caps_malloc(
64 * 64, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
Tensor<> input;
input.(input_buffer);
input.({, , });
input.();
() {
*fb = ();
(!fb) {
();
;
}
(fb->buf, fb->width, fb->height,
input_buffer, , );
Tensor<> *output = detecteur.forward(&input);
*scores = output->();
face_score = scores[] * ;
pas_face_score = scores[] * ;
(face_score > ) {
(, face_score * );
}
output;
(fb);
(());
}
}
3. TFLite Micro sur ESP32-S3
3.1 Intégration TFLite Micro + ESP-NN
#include "tensorflow/lite/micro/micro_interpreter.h"
#include "tensorflow/lite/micro/micro_mutable_op_resolver.h"
#include "tensorflow/lite/micro/kernels/esp_nn/esp_nn_conv.h"
void setup_tflite_esp32s3() {
tflite::MicroMutableOpResolver<15> resolver;
resolver.AddConv2D(
tflite::Register_CONV_2D_ESP_NN()
);
resolver.AddDepthwiseConv2D(
tflite::Register_DEPTHWISE_CONV_2D_ESP_NN()
);
resolver.AddAveragePool2D();
resolver.AddMaxPool2D();
resolver.AddFullyConnected(
tflite::Register_FULLY_CONNECTED_ESP_NN()
);
resolver.AddSoftmax();
resolver.AddRelu();
resolver.AddConcatenation();
resolver.AddReshape();
constexpr int kTensorArenaSize = 120 * 1024;
static tensor_arena[kTensorArenaSize];
;
}
3.2 Custom kernels pour TFLite Micro
#include "esp_dsp.h"
TfLiteStatus PretraitementCouleurEval(TfLiteContext* context, TfLiteNode* node) {
const TfLiteTensor* input = tflite::GetInput(context, node, 0);
TfLiteTensor* output = tflite::GetOutput(context, node, 0);
const float* in_data = tflite::GetTensorData<float>(input);
float* out_data = tflite::GetTensorData<float>(output);
int size = tflite::GetTensorShape(input).FlatSize();
for (int i = 0; i < size; i += 3) {
out_data[i/3] = 0.299f * in_data[i] +
0.587f * in_data[i + 1] +
0.114f * in_data[i + 2];
}
return kTfLiteOk;
}
3.3 Gestion de la PSRAM
#include "esp_heap_caps.h"
static uint8_t tensor_arena[120 * 1024]
__attribute__((section(".dram1")));
int8_t *model_weights;
size_t model_size;
void charger_modele_psram(const char *path) {
FILE *f = fopen(path, "rb");
if (!f) return;
fseek(f, 0, SEEK_END);
model_size = ftell(f);
fseek(f, 0, SEEK_SET);
model_weights = (int8_t *)heap_caps_malloc(
model_size, MALLOC_CAP_SPIRAM);
fread(model_weights, 1, model_size, f);
fclose(f);
printf("Modèle chargé en PSRAM : %zu bytes\n", model_size);
}
void check_memory() {
printf(,
(MALLOC_CAP_INTERNAL));
(,
(MALLOC_CAP_SPIRAM));
(,
(MALLOC_CAP_INTERNAL));
}
4. Traitement Audio (Keyword Spotting)
4.1 Pipeline audio complet
#include "driver/i2s.h"
#include "esp_dsp.h"
#include "esp_nn.h"
class KWS {
private:
static const int N_FFT = 256;
static const int N_MFCC = 10;
static const int N_FRAMES = 10;
static const int SAMPLE_RATE = 16000;
int16_t *audio_buffer;
float *fft_buffer;
float *mel_buffer;
float *mfcc_buffer;
tflite::MicroInterpreter *interpreter;
public:
KWS() {
audio_buffer = (int16_t *)heap_caps_malloc(
N_FFT * sizeof(int16_t), MALLOC_CAP_INTERNAL);
fft_buffer = (float *)heap_caps_malloc(
N_FFT * sizeof(float), MALLOC_CAP_INTERNAL);
mel_buffer = (float *)heap_caps_malloc(
20 * sizeof(float), MALLOC_CAP_INTERNAL);
mfcc_buffer = ( *)(
N_MFCC * (), MALLOC_CAP_INTERNAL);
i2s_config = {
.mode = ()(I2S_MODE_MASTER | I2S_MODE_RX),
.sample_rate = SAMPLE_RATE,
.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
.communication_format = I2S_COMM_FORMAT_STAND_I2S,
.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
.dma_buf_count = ,
.dma_buf_len = N_FFT,
};
(I2S_NUM_0, &i2s_config, , );
pin_config = {
.bck_io_num = GPIO_NUM_26,
.ws_io_num = GPIO_NUM_25,
.data_out_num = ,
.data_in_num = GPIO_NUM_27,
};
(I2S_NUM_0, &pin_config);
}
{
( i = N_FFT - ; i > ; i--) {
fft_buffer[i] = audio_buffer[i] - * audio_buffer[i - ];
}
fft_buffer[] = audio_buffer[];
( i = ; i < N_FFT; i++) {
fft_buffer[i] *= - * ( * M_PI * i / (N_FFT - ));
}
(fft_buffer, N_FFT);
power[N_FFT/];
( i = ; i < N_FFT/; i++) {
re = fft_buffer[*i];
im = fft_buffer[*i + ];
power[i] = re * re + im * im;
}
}
{
input_buffer[N_FRAMES * N_MFCC];
( f = ; f < N_FRAMES; f++) {
bytes_read;
(I2S_NUM_0, audio_buffer,
N_FFT * (), &bytes_read, portMAX_DELAY);
();
(&input_buffer[f * N_MFCC], mfcc_buffer,
N_MFCC * ());
}
input = interpreter->();
(input->data.int8, (input_buffer), input->bytes);
interpreter->();
output = interpreter->();
predicted = (output->data.int8[]);
(, predicted);
}
};
5. Optimisations Spécifiques ESP32-S3
5.1 Cache et préchargement
__attribute__((aligned(16))) int8_t weights_aligned[1024];
__attribute__((optimize("-O3")))
void prefetch_weights(int8_t *weights, size_t size) {
volatile int8_t sum = 0;
for (size_t i = 0; i < size; i += 16) {
sum += weights[i];
}
(void)sum;
}
5.2 Dual-core pour pipeline ML
void task_capture(void *arg) {
while (1) {
xSemaphoreGive(sem_inference);
vTaskDelay(pdMS_TO_TICKS(33));
}
}
void task_inference(void *arg) {
while (1) {
xSemaphoreTake(sem_inference, portMAX_DELAY);
interpreter->Invoke();
process_output();
}
}
void app_main() {
sem_inference = xSemaphoreCreateBinary();
xTaskCreatePinnedToCore(task_capture, "capture", 4096, NULL, 5, NULL, 0);
xTaskCreatePinnedToCore(task_inference, "inference", 8192, NULL, 5, NULL, 1);
}
5.3 Optimisation énergétique
void inference_periodique() {
const int INTERVAL_SEC = 2;
while (1) {
esp_light_sleep_start();
int8_t result = run_inference();
if (result > 0) {
wifi_send_alert(result);
}
esp_sleep_enable_timer_wakeup(INTERVAL_SEC * 1000000);
}
}
void adjust_cpu_freq() {
esp_clk_cpu_freq_t freq = ESP_CPU_FREQ_240M;
switch (current_mode) {
case MODE_INFERENCE:
freq = ESP_CPU_FREQ_240M;
break;
case MODE_IDLE:
freq = ESP_CPU_FREQ_40M;
break;
case MODE_DEEP_SLEEP:
break;
}
esp_clk_cpu_set(freq);
}
{
(enable) {
__asm__ ;
} {
__asm__ ( :: ());
}
}
6. Exemples Complets
6.1 Détection de mouvements (IMU)
void setup_imu_ml() {
i2c_config_t conf = {
.mode = I2C_MODE_MASTER,
.sda_io_num = GPIO_NUM_21,
.scl_io_num = GPIO_NUM_22,
.master.clk_speed = 400000,
};
i2c_param_config(I2C_NUM_0, &conf);
i2c_driver_install(I2C_NUM_0, I2C_MODE_MASTER, 0, 0, 0);
static int16_t imu_buffer[32 * 6];
while (1) {
for (int i = 0; i < 32; i++) {
read_mpu6050(&imu_buffer[i * 6]);
vTaskDelay(pdMS_TO_TICKS(10));
}
float float_input[32 * 6];
for (int i = 0; i < 32 * 6; i++) {
float_input[i] = imu_buffer[i] / 16384.0f;
}
run_tflite_inference(float_input);
}
}
6.2 Classification d'images (Camera)
. ~/esp/esp-idf/export.sh
idf.py create-project esp32_classification
idf.py menuconfig
cp ~/models/mobilenet_v1_0.25_128_quant.tflite \
components/main/model.tflite
xxd -i components/main/model.tflite > components/main/model_data.cc
idf.py build
idf.py -p /dev/ttyUSB0 flash monitor
Pièges Courants
-
PSRAM lente pour l'inférence : la PSRAM a une latence de ~12 cycles vs 0 pour SRAM. Ne JAMAIS placer la Tensor Arena ou les buffers de calcul en PSRAM — réserver la SRAM interne pour les données critiques.
-
Fréquence CPU insuffisante : à 240 MHz, l'ESP32-S3 est ~2× plus lent qu'un Cortex-M7 à 480 MHz. Compenser par les optimisations PIE et la quantification INT8.
-
ESP-NN pas activé par défaut : vérifier dans menuconfig que CONFIG_ESP_NN_ENABLE=y. Sans cela, les kernels génériques sont utilisés (3-5× plus lents).
-
WiFi + inférence simultanée : le WiFi utilise le même CPU et peut interrompre l'inférence. Réserver un core pour le WiFi et un pour l'inférence.
-
Débordement de pile (stack overflow) : l'inférence TFLite Micro consomme ~4-8 KB de stack. Augmenter la taille de pile des tâches : configMINIMAL_STACK_SIZE → 8192.
-
Modèle trop grand pour PSRAM : 16 MB max PSRAM, mais au-delà de 4 MB, l'adressage nécessite un mapping de page MMU. Réduire le modèle ou utiliser la quantification.
-
ALIGNEMENT des buffers : les opérations PIE nécessitent un alignement 16 bytes. Utiliser heap_caps_aligned_alloc(16, size, MALLOC_CAP_INTERNAL).
Références