feat(firmware): 48V pack class for 13s packs

Add a Kconfig voltage-class choice (BMU_PACK_CLASS: 24V default /
48V) plus an sdkconfig.defaults.48v overlay, implementing lot 4 of
the 48V design note.

48V class defaults: min 44600 mV (3.43 V/cell, same fleet policy
as 24V), max 55200 mV (charger CV 54.6 V + 600 mV margin),
imbalance 2000 mV (x13/7), VRM SOC window 44600-53500 mV.

Blocking fixes found while implementing:
- bmu_protection: plausibility guard rejected any reading above
  35 V as an I2C glitch -> class macro BMU_V_PLAUSIBLE_MAX_MV.
- bmu_ina237: BOVL/BUVL hardwired 30000/24000 -> programmed from
  runtime thresholds (NVS/Kconfig) at init; log real values.
- bmu_ble control + display steppers: hardcoded 20-30/25-35 V
  setting bounds rejected any 48V config -> class-derived macros
  BMU_VMIN/VMAX_SET_FLOOR/CEIL_MV in bmu_config.h.
- bmu_ui_detail: chart autoscale seeds 35000/15000 broke above
  35 V -> neutral 65535/0.
- bmu_ble_victron: SOC window hardcoded 24.0/28.8 -> reads the
  VRM Kconfig (class fallback when VRM disabled).
- legacy firmware/src/config.h: BATTERY_PACK_CLASS_48V ifdef.

Validated: idf.py build green for both classes (esp32s3, isolated
sdkconfig; 48V generates 44600/55200/2000), no new warnings in
touched files, legacy sim-host tests 13/13 pass. NVS erase needed
when reflashing a board that has 24V thresholds persisted.
This commit is contained in:
L'électron rare
2026-07-07 16:05:43 +02:00
parent 139fb718e0
commit 80cfa134a6
15 changed files with 154 additions and 45 deletions
+1 -1
View File
@@ -107,7 +107,7 @@ models/*.onnx
.failsafe/
firmware-idf/managed_components/
firmware-idf/build/
firmware-idf/build*/
firmware-idf/sdkconfig
firmware-idf/sdkconfig.old
firmware-idf/.cache/
@@ -69,24 +69,46 @@ identiques : la sous-carte 48 V est interchangeable avec la 30 V.
Le reste de la carte mère (INA, TCA, jumpers, RJ45, LEDs, bornes) est inchangé.
## 4. Firmware à modifier (variante Kconfig « 13S »)
## 4. Firmware à modifier (variante Kconfig « 13S ») — ✅ IMPLÉMENTÉ 2026-07-07
Trois points durs identifiés dans le code + deux calibrations :
Implémentation : choix Kconfig **`BMU_PACK_CLASS`** (24V défaut / 48V) dans
`bmu_config/Kconfig` + overlay **`sdkconfig.defaults.48v`**
(`idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.48v"`).
1. `bmu_config/Kconfig` — defaults : `BMU_MIN_VOLTAGE_MV` 24000 → 41000,
`BMU_MAX_VOLTAGE_MV` 30000 → 54600 (proposition à affiner).
2. `bmu_ble/bmu_ble_control_svc.cpp:233-234` — bornes de validation codées en
dur (2030 V / 2535 V) → à dériver du Kconfig (plage 13s : 3648 V /
4556 V) plutôt que de nouvelles constantes magiques.
3. `bmu_display/bmu_ui_config.cpp:188-189` — mêmes bornes dans les steppers
de l'écran → même traitement.
4. **Seuil de déséquilibre** : 1 V @7s ≈ 1,9 V @13s à iso-déséquilibre par
cellule (×13/7). Garder le paramètre en absolu mais adapter le défaut de
la variante (~2000 mV).
5. **SOC/VRM et SOH** : mapping SOC linéaire 24,028,8 V → profil 13s
(39,054,6 V) ; le modèle SOH FPNN est entraîné sur les packs 7s terrain
→ recalibration nécessaire pour la chimie INR 13s (advisory only, les
protections restent autoritaires).
1. `bmu_config/Kconfig` — defaults conditionnels : `BMU_MIN_VOLTAGE_MV`
24000 → **44600** (3,43 V/él., même politique parc que 24 V ; l'onduleur
~40,3 V et le BMS pack restent en filets), `BMU_MAX_VOLTAGE_MV` 30000 →
**55200** (4,246 V/él., CV 54,6 V + 600 mV — la proposition initiale
54600 déclenchait pile à la CV chargeur).
2. `bmu_ble/bmu_ble_control_svc.cpp` bornes dérivées des macros de
classe `BMU_VMIN_SET_FLOOR/CEIL_MV`, `BMU_VMAX_SET_FLOOR/CEIL_MV`
(13s : 3648 V / 4556 V) définies dans `bmu_config.h`.
3.`bmu_display/bmu_ui_config.cpp` — steppers écran sur les mêmes macros ;
défauts statiques 24000/30000 remplacés par les macros Kconfig.
4. **Seuil de déséquilibre** : défaut 2000 mV en classe 48 V (×13/7).
5.**SOC/VRM** : fenêtre SOC Kconfig `BMU_VRM_SOC_V_MIN/MAX` → défauts
13s 44600/53500 mV (×13/7 de 24,028,8 V) ; `bmu_ble_victron.cpp`
lit désormais le Kconfig (fallback par classe si VRM désactivé).
**SOH FPNN : recalibration chimie INR 13s toujours à faire** (advisory
only, les protections restent autoritaires).
Corrections trouvées en implémentant (bloquantes à 48 V) :
- `bmu_protection.cpp` : garde de plausibilité `v_mv > 35000` → toute
lecture 13s aurait été rejetée comme glitch I2C → macro de classe
`BMU_V_PLAUSIBLE_MAX_MV` (60 V en 48 V).
- `bmu_ina237.cpp` : alertes matérielles BOVL/BUVL codées en dur
30000/24000 → programmées depuis les seuils runtime (NVS/Kconfig).
NB : reprogrammées au boot uniquement (un changement BLE des seuils ne
met à jour le chip qu'au reboot ; la protection logicielle, elle, suit
immédiatement).
- `bmu_ui_detail.cpp` : seeds d'auto-échelle du graphe (35000/15000) →
65535/0, neutres toutes classes.
- Legacy `firmware/src/config.h` : bloc `#ifdef BATTERY_PACK_CLASS_48V`
(44600/55200), défaut 24 V inchangé — tests sim-host verts.
⚠ Passage d'une carte 24 V → 48 V déjà configurée : effacer la NVS
(seuils persistés) — cf. `firmware-idf/CLAUDE.md` « Erase NVS ».
## 5. Points système à instruire au banc
+8
View File
@@ -9,8 +9,16 @@ source ~/esp/esp-idf/export.sh
idf.py build # ~30-60s incremental
idf.py -p /dev/cu.usbmodem* flash monitor # flash + serial
idf.py menuconfig # configure sdkconfig
# Variante 48V (packs 13s, hardware BMU-v2-48v) — overlay Kconfig :
idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.48v" build
# (supprimer sdkconfig existant + erase NVS si seuils 24V déjà persistés)
```
**Classe de tension** : choix Kconfig `BMU_PACK_CLASS` (24V défaut / 48V).
Fixe les défauts des seuils (44600/55200 mV en 48V), les bornes de réglage
BLE/écran, le garde de plausibilité INA237 et la fenêtre SOC VRM.
**Erase NVS** (si config corrompue ou override sdkconfig) :
```bash
python3 -m esptool --chip esp32s3 -p /dev/cu.usbmodem* erase_region 0x9000 0x6000
@@ -229,9 +229,9 @@ static int control_chr_access_cb(uint16_t conn_handle, uint16_t attr_handle,
s_last_status.last_cmd = 2;
s_last_status.battery_idx = 0xFF;
/* Validation des plages */
if (min_mv < 20000 || min_mv > 30000 ||
max_mv < 25000 || max_mv > 35000 ||
/* Validation des plages — bornes selon la classe de tension */
if (min_mv < BMU_VMIN_SET_FLOOR_MV || min_mv > BMU_VMIN_SET_CEIL_MV ||
max_mv < BMU_VMAX_SET_FLOOR_MV || max_mv > BMU_VMAX_SET_CEIL_MV ||
max_ma < 1000 || max_ma > 50000 ||
diff_mv < 100 || diff_mv > 5000) {
ESP_LOGW(TAG, "Config: valeurs hors plage min=%u max=%u maxI=%u diff=%u",
@@ -89,8 +89,14 @@ static int build_battery_adv(uint8_t *buf, size_t buf_len)
float avg_v = avg_mv / 1000.0f;
/* SOC estimation */
float v_min = 24.0f; /* CONFIG_BMU_VRM_SOC_V_MIN / 1000 */
float v_max = 28.8f; /* CONFIG_BMU_VRM_SOC_V_MAX / 1000 */
#ifdef CONFIG_BMU_VRM_SOC_V_MIN
float v_min = (float)CONFIG_BMU_VRM_SOC_V_MIN / 1000.0f;
float v_max = (float)CONFIG_BMU_VRM_SOC_V_MAX / 1000.0f;
#elif CONFIG_BMU_PACK_CLASS_48V /* VRM desactive : fenetre par classe */
float v_min = 44.6f, v_max = 53.5f;
#else
float v_min = 24.0f, v_max = 28.8f;
#endif
float soc = (avg_v - v_min) / (v_max - v_min) * 100.0f;
if (soc < 0) soc = 0;
if (soc > 100) soc = 100;
@@ -1,12 +1,41 @@
menu "BMU Protection Config"
choice BMU_PACK_CLASS
prompt "Classe de tension du parc"
default BMU_PACK_CLASS_24V
help
Fixe les defauts usine des seuils tension (surchargeables via
NVS/BLE/ecran), les bornes de reglage et le garde de
plausibilite des lectures INA237.
24V = packs 7s Li-ion / 8s LFP (fenetre 20-30V).
48V = packs 13s Li-ion (fenetre 39.0-54.6V) — necessite le
hardware BMU-v2-48v (sous-carte IRF5210 + buck LM5164), cf.
docs/superpowers/specs/2026-07-05-bmu-48v-13s-variant-design.md
config BMU_PACK_CLASS_24V
bool "24V — packs 7s Li-ion / 8s LFP"
config BMU_PACK_CLASS_48V
bool "48V — packs 13s Li-ion"
endchoice
config BMU_MIN_VOLTAGE_MV
int "Seuil sous-tension (mV)"
default 44600 if BMU_PACK_CLASS_48V
default 24000
help
Deconnexion du pack sous ce seuil. Defauts : 24000 = 3.43 V/el.
sur 7s ; 44600 = meme politique sur 13s (l'onduleur ~40.3 V et
le BMS pack restent en filets de securite).
config BMU_MAX_VOLTAGE_MV
int "Seuil sur-tension (mV)"
default 55200 if BMU_PACK_CLASS_48V
default 30000
help
Deconnexion au-dessus de ce seuil. Defauts : 30000 = marge
600 mV sur la pleine charge 29.4 V (7s) ; 55200 = 4.246 V/el.
sur 13s, marge 600 mV sur la CV chargeur 54.6 V.
config BMU_MAX_CURRENT_MA
int "Seuil sur-courant (mA)"
@@ -14,7 +43,11 @@ menu "BMU Protection Config"
config BMU_VOLTAGE_DIFF_MV
int "Tolerance desequilibre (mV)"
default 2000 if BMU_PACK_CLASS_48V
default 1000
help
1000 mV @7s = ~143 mV/el. ; a iso-desequilibre par cellule le
13s tolere ~2000 mV (x13/7). Cf. note de design 48V, point 4.
config BMU_RECONNECT_DELAY_MS
int "Delai reconnexion (ms)"
@@ -22,6 +22,23 @@ extern "C" {
#define BMU_MAX_BATTERIES 32
#define BMU_MAX_TCA 8
/* ── Classe de tension du parc (Kconfig BMU_PACK_CLASS) ────────────────
* Bornes de reglage des seuils (BLE + ecran) et garde de plausibilite
* des lectures INA237. Les seuils actifs restent ceux de NVS/Kconfig. */
#if CONFIG_BMU_PACK_CLASS_48V
#define BMU_VMIN_SET_FLOOR_MV 36000 /* V_min reglable 36.0-48.0 V */
#define BMU_VMIN_SET_CEIL_MV 48000
#define BMU_VMAX_SET_FLOOR_MV 45000 /* V_max reglable 45.0-56.0 V */
#define BMU_VMAX_SET_CEIL_MV 56000
#define BMU_V_PLAUSIBLE_MAX_MV 60000 /* lecture > 60 V = glitch I2C */
#else /* 24 V (defaut) */
#define BMU_VMIN_SET_FLOOR_MV 20000
#define BMU_VMIN_SET_CEIL_MV 30000
#define BMU_VMAX_SET_FLOOR_MV 25000
#define BMU_VMAX_SET_CEIL_MV 35000
#define BMU_V_PLAUSIBLE_MAX_MV 35000
#endif
/* ── Limites buffers ───────────────────────────────────────────────── */
#define BMU_CONFIG_NAME_MAX 32
#define BMU_CONFIG_SSID_MAX 33 /* 802.11 max SSID = 32 + NUL */
@@ -14,11 +14,11 @@ static lv_obj_t *s_ssid_ta = NULL;
static lv_obj_t *s_pass_ta = NULL;
static lv_obj_t *s_mqtt_ta = NULL;
/* Valeurs seuils — modifiées par les steppers */
static uint16_t s_v_min = 24000;
static uint16_t s_v_max = 30000;
static uint16_t s_i_max = 10000;
static uint16_t s_v_diff = 1000;
/* Valeurs seuils — modifiées par les steppers (défauts = classe de tension) */
static uint16_t s_v_min = BMU_MIN_VOLTAGE_MV;
static uint16_t s_v_max = BMU_MAX_VOLTAGE_MV;
static uint16_t s_i_max = BMU_MAX_CURRENT_MA;
static uint16_t s_v_diff = BMU_VOLTAGE_DIFF_MV;
static lv_obj_t *s_vmin_lbl = NULL;
static lv_obj_t *s_vmax_lbl = NULL;
@@ -185,8 +185,8 @@ void bmu_ui_config_create(lv_obj_t *parent)
section_label(cont, "SEUILS PROTECTION");
bmu_config_get_thresholds(&s_v_min, &s_v_max, &s_i_max, &s_v_diff);
s_steppers[0] = {&s_v_min, 20000, 30000, 500, &s_vmin_lbl, "%u mV", true};
s_steppers[1] = {&s_v_max, 25000, 35000, 500, &s_vmax_lbl, "%u mV", true};
s_steppers[0] = {&s_v_min, BMU_VMIN_SET_FLOOR_MV, BMU_VMIN_SET_CEIL_MV, 500, &s_vmin_lbl, "%u mV", true};
s_steppers[1] = {&s_v_max, BMU_VMAX_SET_FLOOR_MV, BMU_VMAX_SET_CEIL_MV, 500, &s_vmax_lbl, "%u mV", true};
s_steppers[2] = {&s_i_max, 1000, 50000, 1000, &s_imax_lbl, "%u mA", false};
s_steppers[3] = {&s_v_diff, 100, 5000, 100, &s_vdiff_lbl, "%u mV", true};
@@ -77,7 +77,7 @@ static lv_color_t state_color(bmu_battery_state_t s)
static void update_chart_range(bmu_chart_history_t *hist)
{
if (hist->count == 0) return;
float v_min = 35000, v_max = 15000;
float v_min = 65535, v_max = 0; /* seeds neutres, valables toutes classes de tension */
for (int i = 0; i < hist->count; i++) {
int idx = (hist->head - hist->count + i + CONFIG_BMU_CHART_HISTORY_POINTS) % CONFIG_BMU_CHART_HISTORY_POINTS;
float v = hist->voltage_mv[idx];
@@ -1,6 +1,6 @@
idf_component_register(
SRCS "bmu_ina237.cpp"
INCLUDE_DIRS "include"
REQUIRES driver bmu_i2c_bitbang
REQUIRES driver bmu_i2c_bitbang bmu_config
PRIV_REQUIRES bmu_i2c
)
@@ -7,6 +7,7 @@
*/
#include "bmu_ina237.h"
#include "bmu_config.h"
#include "bmu_i2c.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
@@ -241,19 +242,24 @@ esp_err_t bmu_ina237_init(i2c_master_bus_handle_t bus, uint8_t addr,
goto fail_remove_device;
}
/* 7. Alertes bus tension : BOVL et BUVL
* Bus voltage LSB = 3.125 mV, registre = voltage_mV / 3.125 */
bovl = (uint16_t)(30000U * 1000U / 3125U);
buvl = (uint16_t)(24000U * 1000U / 3125U);
ret = ina237_write_reg16_retry(ctx->dev, INA237_REG_BOVL, bovl);
if (ret == ESP_OK) {
ret = ina237_write_reg16_retry(ctx->dev, INA237_REG_BUVL, buvl);
}
if (ret == ESP_OK) {
ESP_LOGI(TAG, "[0x%02X] Alertes bus: BOVL=30000 mV (reg=%u) BUVL=24000 mV (reg=%u)",
addr, bovl, buvl);
} else {
ESP_LOGW(TAG, "[0x%02X] Echec config alertes tension (non fatal)", addr);
/* 7. Alertes bus tension : BOVL et BUVL — seuils runtime (NVS/Kconfig,
* classe 24 V ou 48 V). Bus voltage LSB = 3.125 mV. */
{
uint16_t th_min_mv = BMU_MIN_VOLTAGE_MV, th_max_mv = BMU_MAX_VOLTAGE_MV;
uint16_t th_max_ma, th_diff_mv;
bmu_config_get_thresholds(&th_min_mv, &th_max_mv, &th_max_ma, &th_diff_mv);
bovl = (uint16_t)((uint32_t)th_max_mv * 1000U / 3125U);
buvl = (uint16_t)((uint32_t)th_min_mv * 1000U / 3125U);
ret = ina237_write_reg16_retry(ctx->dev, INA237_REG_BOVL, bovl);
if (ret == ESP_OK) {
ret = ina237_write_reg16_retry(ctx->dev, INA237_REG_BUVL, buvl);
}
if (ret == ESP_OK) {
ESP_LOGI(TAG, "[0x%02X] Alertes bus: BOVL=%u mV (reg=%u) BUVL=%u mV (reg=%u)",
addr, th_max_mv, bovl, th_min_mv, buvl);
} else {
ESP_LOGW(TAG, "[0x%02X] Echec config alertes tension (non fatal)", addr);
}
}
bmu_i2c_unlock();
@@ -1,5 +1,6 @@
#include "bmu_protection.h"
#include "bmu_balancer.h"
#include "bmu_config.h"
#include "bmu_i2c.h"
#include "bmu_rint.h"
#include "esp_log.h"
@@ -180,7 +181,7 @@ esp_err_t bmu_protection_check_battery_ex(bmu_protection_ctx_t *ctx, int idx, fl
* Symptomes observes : V/2 (14.4V au lieu de 28.8V), V*1.4 (40V).
* Filtre : si la batterie est CONNECTED/RECONNECTING et que la tension
* s'ecarte de >30% de la derniere valeur connue, c'est un artefact. */
if (v_mv > 35000 || fabs(i_a) > 50.0f) {
if (v_mv > BMU_V_PLAUSIBLE_MAX_MV || fabs(i_a) > 50.0f) {
ESP_LOGW(TAG, "BAT[%d] lecture aberrante V=%.0f I=%.1f — skip",
idx + 1, v_mv, i_a);
return ESP_ERR_INVALID_RESPONSE;
+2
View File
@@ -21,11 +21,13 @@ menu "BMU VRM (Victron Remote Monitoring)"
config BMU_VRM_SOC_V_MIN
int "SOC 0% voltage (mV)"
default 44600 if BMU_PACK_CLASS_48V
default 24000
depends on BMU_VRM_ENABLED
config BMU_VRM_SOC_V_MAX
int "SOC 100% voltage (mV)"
default 53500 if BMU_PACK_CLASS_48V
default 28800
depends on BMU_VRM_ENABLED
endmenu
+6
View File
@@ -0,0 +1,6 @@
# Overlay classe 48 V (packs 13s Li-ion, hardware BMU-v2-48v).
# Build :
# idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.48v" build
# (supprimer le sdkconfig existant ou effacer la NVS si des seuils 24 V
# y sont déjà persistés — cf. CLAUDE.md "Erase NVS")
CONFIG_BMU_PACK_CLASS_48V=y
+8
View File
@@ -13,8 +13,16 @@
#define I2C_Speed 50 // Vitesse I²C en kHz (50 = fiable sur câblage long)
// ── Protection tension ───────────────────────────────────────────────────────
// Classe de tension du parc : définir BATTERY_PACK_CLASS_48V (build_flags
// PlatformIO : -DBATTERY_PACK_CLASS_48V) pour un parc 13s Li-ion
// (fenêtre 39,054,6 V, hardware BMU-v2-48v requis). Défaut : 24 V (7s/8s).
#ifdef BATTERY_PACK_CLASS_48V
#define alert_bat_min_voltage 44600 // Seuil sous-tension en mV (3,43 V/él. sur 13s)
#define alert_bat_max_voltage 55200 // Seuil sur-tension en mV (4,246 V/él., CV 54,6 V + 600 mV)
#else
#define alert_bat_min_voltage 24000 // Seuil sous-tension en mV (24 V)
#define alert_bat_max_voltage 30000 // Seuil sur-tension en mV (30 V)
#endif
// ── Protection courant ───────────────────────────────────────────────────────
#define alert_bat_max_current 10 // Seuil sur-courant en A (valeur absolue)