Files
L'électron rare f55093d6fe
ESP-IDF CI / Host Tests (Unity) (push) Successful in 1m8s
CI / firmware-native (push) Successful in 2m57s
Rust Protection Tests / Cargo test (host) (push) Failing after 3m21s
ESP-IDF CI / ESP-IDF Build (v5.4) (push) Failing after 6m55s
ESP-IDF CI / Memory Budget Gate (push) Has been skipped
qa-cicd-environments / qa-kxkm-s3-build (push) Successful in 8m53s
qa-cicd-environments / qa-sim-host (push) Successful in 2m2s
qa-cicd-environments / qa-kxkm-s3-memory-budget (push) Successful in 11m17s
chore: import KXKM Batterie Parallelator
Context: the project archive (KXKM_Batterie_Parallelator-main) had
no git history locally; a fresh repository is needed to host it on
git.saillant.cc (electron/KXKM_Batterie_Parallelator).

Approach: initialize a new repo on branch main, stage the archive
content, and harden .gitignore before the first commit.

Changes:
- Import the full project tree: firmware/, firmware-idf/,
  firmware-rs/, iosApp/, kxkm-bmu-app/, kxkm-api/, hardware/,
  docs/, specs/, scripts/, models/, tests/
- Keep project dotfiles tracked despite the trailing '.*' ignore
  rule: .github/, .claude/, .superpowers/, .gitattributes,
  .markdownlint.json
- Extend .gitignore: firmware/src/credentials.h (local secrets,
  template kept), kxkm-bmu-app/**/build/ (66 MB compiled iOS
  framework), .remember/ (session data)

Impact: the project can now be maintained on the self-hosted Gitea
forge with a clean, secret-free initial history.
2026-07-04 12:32:28 +02:00

9.1 KiB
Raw Permalink Blame History

Plan d'Optimisation Firmware BMU — ESP-IDF

État actuel

Ressource Utilisé Disponible Utilisation
Flash (OTA) 1.71 MB 2.0 MB 82%
DRAM 249 KB 342 KB 73%
IRAM 16.4 KB 16.4 KB 100%
PSRAM (Octal) ~150 KB 8 MB 2%
Tâches FreeRTOS 7 tâches 28.5 KB stacks

Top consommateurs

Composant Flash (code) DRAM (data)
LVGL 271 KB (16%) 66 KB (27%)
bmu_display 31 KB 8 KB
bmu_ble (all) 15 KB 3.4 KB
bmu_influx 7 KB 4.6 KB
bmu_web 7 KB 0.4 KB
bmu_protection 5 KB 0
chart_hist (PSRAM) 150 KB

Phase 1 : Optimisation RAM (priorité haute)

1.1 Déplacer les buffers lourds en PSRAM

Cible : -40 KB DRAM

Buffer Taille Actuellement Déplacer vers
LVGL draw buffers 2 × 320×240×2 = 30 KB DRAM PSRAM (heap_caps_malloc(MALLOC_CAP_SPIRAM))
BLE GATT chr defs s_bat_chr_defs 3 KB BSS (DRAM) PSRAM ou allocation dynamique au runtime
InfluxDB buffer s_buffer[4096] 4 KB BSS PSRAM
R_int cache s_cache[32] 2.5 KB BSS PSRAM

Action : Dans bmu_display.cpp, vérifier que les draw buffers LVGL sont alloués en PSRAM (le BSP BOX-3 devrait le faire). Si non, forcer avec LV_MEM_CUSTOM=1 + heap_caps_malloc.

1.2 Réduire les tableaux statiques à nb_ina réel

Cible : -8 KB DRAM

Actuellement TOUS les tableaux sont dimensionnés à BMU_MAX_BATTERIES=32, même si seulement 8 batteries sont présentes.

Tableau Taille actuelle Taille réelle (8 bat) Économie
battery_voltages[32] 128 B 32 B 96 B
nb_switch[32] 128 B 32 B 96 B
reconnect_time_ms[32] 256 B 64 B 192 B
battery_state[32] 128 B 32 B 96 B
imbalance_count[32] 32 B 8 B 24 B
s_rint_cache[32] 2560 B 640 B 1920 B
10 × UI pointers [32] 2560 B 640 B 1920 B
BLE handles [32] 576 B 144 B 432 B

Approche : Garder les tableaux statiques à 32 (évite les réallocs pour hotplug), mais déplacer les plus gros en PSRAM via EXT_RAM_BSS_ATTR ou allocation dynamique.

1.3 Optimiser les stacks de tâches

Cible : -6 KB DRAM

Tâche Stack actuel Stack optimisé Justification
cloud 4096 6144 (PSRAM) snprintf + MQTT — trop petit, déplacer en PSRAM
Ah_all 4096 2048 Juste des lectures I2C + calculs float
vrm 4096 2048 Similaire à cloud mais plus léger
ws_push 4096 2048 Juste un push WebSocket
rint_periodic 4096 3072 Mesures I2C + calculs
hotplug 3072 2048 Probes I2C légers
vedirect 4096 2048 Parse UART texte

Méthode : Utiliser uxTaskGetStackHighWaterMark() en debug pour mesurer l'usage réel, puis ajuster. Les tâches en PSRAM utilisent CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY=y (déjà activé).


Phase 2 : Optimisation CPU/Cycles (priorité haute)

2.1 Réduire la fréquence de la boucle protection

Actuellement : 500ms × 8 batteries = 16 lectures I2C / seconde + 16 TCA switches potentiels.

Proposition :

  • Protection V/I : garder 500ms (sécurité)
  • Imbalance check : calculer fleet_max une seule fois par cycle au lieu de N fois
  • Balancer tick : passer de 500ms à 2000ms (le balancing n'a pas besoin de réagir vite)
  • Display update : passer de chaque cycle à toutes les 2s

2.2 Pré-calculer fleet_max une fois par cycle

Gain : 7 appels à find_fleet_max_mv() économisés par cycle

// AVANT (dans check_battery, appelé 8 fois) :
is_imbalance_ok(v_mv, find_fleet_max_mv(ctx))  // itère les 8 batteries à chaque appel

// APRÈS (dans main loop, calculé une fois) :
float fleet_max = compute_fleet_max(&prot);
for (int i = 0; i < nb_ina; i++) {
    bmu_protection_check_battery_with_fleet(&prot, i, fleet_max);
}

2.3 Éviter les switch_battery(OFF) redondants

Déjà fait dans le fix précédent — ne switch OFF que sur transition d'état. Économise ~50ms I2C par batterie déjà OFF.

2.4 Réduire le spam I2C du hotplug

Actuellement : Le hotplug probe toutes les adresses inconnues (16 INA + 8 TCA = 24 probes potentiels) toutes les 10s, avec double-probe (50ms × 24 = 1.2s de bus occupé).

Proposition :

  • Ne scanner que les adresses qui étaient présentes au boot + les adresses adjacentes
  • Augmenter l'intervalle à 30s si la topologie est stable depuis 5 minutes

Phase 3 : Optimisation Flash (priorité moyenne)

3.1 Optimiser LVGL (271 KB = 16% du flash)

Option Économie estimée Impact
Désactiver les widgets LVGL non utilisés -30 à -50 KB Aucun si bien ciblé
LV_USE_CHART=0 et implémenter un chart minimal custom -15 KB Perte de la lib chart mais gain perf
LV_USE_FLEX=0 si pas utilisé -5 KB
LV_USE_GRID=0 si pas utilisé -5 KB
LV_FONT_MONTSERRAT_14 seul (désactiver les autres) -10 KB
LV_USE_THEME_DEFAULT=0 + theme minimal -8 KB

Méthode : Auditer lv_conf.h ou sdkconfig pour les LV_USE_* activés mais non utilisés.

3.2 Compiler avec -Os au lieu de -Og

Gain : ~10-15% de flash en passant de -Og (debug) à -Os (size). Configurable via menuconfigCompiler options → Optimization Level.

Gain : ~5-10% flash supplémentaire. CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_SILENT=y + CONFIG_COMPILER_OPTIMIZATION_SIZE=y.


Phase 4 : Optimisation C++ / Architecture (priorité moyenne)

4.1 Éliminer les vTaskDelay dans la state machine

Les switch_battery() fait un vTaskDelay(100ms) pour le dead-time MOSFET et 50ms pour le switch-off. Pendant ce temps, la tâche main est bloquée et ne traite pas les autres batteries.

Proposition : Architecture non-bloquante avec timer :

// Au lieu de :
switch_battery(ON);  // bloque 100ms
// Utiliser :
set_switch_gpio(ON);
schedule_callback(100ms, verify_switch_and_set_led);

Alternative plus simple : Réduire les dead-times :

  • MOSFET IRF4905 : temps de commutation ~50ns. Le 100ms est excessif → 10ms suffit
  • Switch OFF : 50ms → 5ms

4.2 Snapshot atomique de l'état au début de chaque cycle

Résout C1, H4 de l'audit :

// Au début de chaque cycle main loop :
typedef struct {
    float fleet_max;
    uint8_t nb_ina;
    uint8_t nb_tca;
} cycle_snapshot_t;

cycle_snapshot_t snap;
xSemaphoreTake(prot.state_mutex, ...);
snap.nb_ina = prot.nb_ina;
snap.nb_tca = prot.nb_tca;
snap.fleet_max = compute_fleet_max_locked(&prot);
xSemaphoreGive(prot.state_mutex);

for (int i = 0; i < snap.nb_ina; i++) {
    bmu_protection_check_battery(&prot, i, &snap);
}

4.3 Event-driven au lieu de polling pour le display

Actuellement : Le display met à jour toutes les batteries toutes les 500ms via la tâche LVGL.

Proposition : Notification par event group quand une tension change significativement (>100mV) :

xEventGroupSetBits(display_events, BAT_UPDATED_BIT);
// La tâche display attend l'event au lieu de poller

Phase 5 : Robustesse (priorité haute, en parallèle)

5.1 Corriger les data races de l'audit (C1, C3, H2, H3, H4)

ID Fix Effort
C1 fleet_max pré-calculé sous mutex (Phase 4.2) 1h
C3 __atomic_load_n pour nb_ina dans cloud task 15min
H2 _Atomic int pour s_consecutive_failures 15min
H3 Suspendre protection pendant compaction hotplug 1h
H4 Snapshot nb_ina en local au début du cycle 15min

5.2 Corriger le web switch (C2 de l'audit)

esp_err_t bmu_protection_web_switch(ctx, idx, on) {
    if (state == LOCKED || state == ERROR) return ESP_ERR_NOT_ALLOWED;
    if (on) {
        // validate voltage + current + imbalance
        switch_battery(ON);
        ctx->nb_switch[idx]++;
        ctx->battery_state[idx] = BMU_STATE_RECONNECTING;
    }
}

5.3 Supprimer le bit-bang recovery (H1 de l'audit)

Le gpio_config() dans bmu_i2c_bus_recover() détache les pins I2C du périphérique. Comme i2c_master_bus_reset() fait déjà le recovery SCL, le fallback bit-bang est dangereux → le supprimer.


Ordre d'exécution recommandé

Sprint Phase Gain Risque
1 5.1-5.3 Robustesse Faible
2 2.1-2.3 CPU -30% Faible
3 1.1-1.3 DRAM -40 KB Moyen (PSRAM latence)
4 4.1-4.2 CPU -20%, propreté Moyen
5 3.1-3.3 Flash -50 KB Faible
6 4.3 CPU display -50% Moyen

Métriques de succès

Métrique Avant Cible
DRAM utilisé 73% (249 KB) <60% (205 KB)
Flash OTA 82% (1.71 MB) <75% (1.5 MB)
Boucle protection ~800ms (8 bat × 100ms) <200ms
I2C transactions/s ~32 <20
Uptime stable ~2 min → LOCKED >24h sans disconnect
Watchdog LVGL Occasionnel Jamais