Embedded HandbookПриложения
Все разделы › XX. Элементы и типовые схемы вокруг МК
STM32F072, STM32G0B1, STM32L452 · уровень продвинутый · 16 мин чтения
Открыть в интерактивном справочнике

173в. Открытая BMS на STM32: разбор Libre Solar — измерение ячеек, балансировка, защита и SOC

Раздел 173б берёг одну ячейку: зарядная микросхема, защита на ячейке и счёт кулонов в прошивке. Батарея из нескольких ячеек подряд требует большего: следить за каждой ячейкой, разряжать самые полные, выключать ключи заряда и разряда по отдельности и рассказывать о себе по шине. Это и есть BMS — система управления батареей.

Здесь разобрана открытая BMS проекта Libre Solar: что делает микросхема измерения, что — STM32 под Zephyr, какие пороги стоят по умолчанию, как устроены выравнивание и оценка заряда. Логика проекта собрана на ПК вместе с его драйвером bq769x0 и прогнана по сценарию на модели батареи 4S LFP. Прогон нашёл в драйвере несколько ошибок; они разобраны в 173в.7 — на них хорошо видно, где в BMS тонкие места.

Опоры: исходники Libre Solar BMS — github.com/LibreSolar/bms-firmware (коммит ee3f15a), лицензия Apache-2.0 — ниже только короткие фрагменты с путями. Список плат и микросхем — boards/ и docs/src/supported_hardware.rst. Сборка — gcc в образе stm-sys:1 (логика и драйвер без изменений плюс заглушки Zephyr и модель микросхемы), размеры кода — arm-none-eabi-gcc в stm-arm:1; каталог _verify/bms173v. Приложение целиком не собрано: Zephyr взят версии v4.4.2, как и требует west.yml (ветка v4.4), но библиотеки ThingSet (thingset-zephyr-sdk) в сборку не входили. Даташиты bq769x0, BQ76952 и ISL94202 не использовались: всё о микросхемах — из драйверов проекта. Лицензия: Apache-2.0.

Как это называется в документации

Оригинал Перевод
BMS (battery management system) система управления батареей
AFE (analog front end), BMS IC микросхема измерения и защиты батареи
cell balancing, bleeding выравнивание ячеек, разряд самых полных через резистор
OV / UV (cell over-/undervoltage) перенапряжение / переразряд ячейки
OCD / SCD (overcurrent / short circuit in discharge) перегрузка / короткое замыкание при разряде
CHG / DSG FET ключ заряда / ключ разряда
reset threshold, hysteresis порог возврата, гистерезис
state of charge (SOC), OCV table заряд в процентах, таблица напряжения холостого хода
coulomb counter (CC) счётчик кулонов, интегратор тока
ThingSet открытый протокол данных устройства: чтение, запись, публикация

173в.1 Проект: платы, микросхемы и кто что делает

Libre Solar BMS — прошивка на Zephyr для пяти плат. В каждой батарею меряет и защищает отдельная микросхема, а микроконтроллер читает её по I²C, решает, какие ключи включить, считает заряд и отвечает по ThingSet.

Плата (boards/libresolar/) МК Микросхема Шунт, мкОм Ток платы, А
bms_5s50_sc STM32F072 bq76920 1000 50
bms_15s80_sc STM32F072 bq76930/40 1500 80
bms_16s100_sc STM32G0B1 BQ76952 300 100
bms_8s50_ic STM32L452 ISL94202 1000 (v0.1), 500 (v0.2) 50
bms_c1 ESP32-C3 BQ76952 300 100

Микросхемы по supported_hardware.rst: bq76920 — 3–5 ячеек, bq76930 — 6–10, bq76940 — 9–15, BQ76952 — 3–16, ISL94202 — 3–8. Для каждой в drivers/bms_ic/ свой драйвер за общим интерфейсом bms_ic.h: configure (записать пороги), read_data (прочитать ячейки, ток, температуры, флаги), set_switches (ключи), balance, set_mode. Прикладной код (app/src/) почти не знает, какая микросхема стоит — исключение в 173в.4.

Libre Solar BMS: батарея 4S, микросхема bq76920 меряет ячейки и ток и сама защищает по OV, UV, OCD и SCD; STM32F072 с Zephyr раз в 500 мс читает данные, считает заряд и ведёт автомат ключей OFF, CHG, DIS, NORMAL; наружу — ThingSet

Разделение работы — как в 173б, только крупнее. Быстрая защита (порог ячейки, перегрузка, короткое замыкание) — в микросхеме: она выключает ключ сама, даже если программа зависла. Медленное — в программе: температура, ток заряда у bq769x0, выравнивание, заряд в процентах и решение, когда включить ключи обратно. Основной цикл main.c — каждые CONFIG_BMS_IC_POLLING_INTERVAL_MS = 500 мс: прочитать всё, обновить заряд, шаг автомата.


173в.2 Измерение: ячейки, ток, температура

Разберём на самом простом драйвере — bq769x0 (плата bms_5s50_sc).

Ячейки. АЦП микросхемы 14-битный: драйвер берёт adc_raw &= 0x3FFF. Усиление и смещение у каждой микросхемы свои, записаны на заводе; драйвер читает их при пуске из ADCGAIN1, ADCGAIN2, ADCOFFSET и пересчитывает код в вольты: код × усиление (мкВ) + смещение (мВ). Каналы ниже 0,5 В драйвер не считает подключёнными: в среднее и минимум они не входят. Так в модели 4 ячейки стоят на пятиканальной микросхеме — пятый канал читается как 0 В.

Ток — интегратор на шунте, знак «+» — заряд (bms_ic.h). Пересчёт в драйвере:

int32_t current_mA = (int16_t)adc_raw * 8.44F / (dev_config->shunt_resistor_uohm / 1000.0F);

При шунте 1 мОм одна единица — 8,44 мА; всё, что меньше 10 мА по модулю, драйвер обнуляет.

Температура — термистор 10 кОм на выводе TS1, пересчёт по бета-формуле, бета по умолчанию 3435 (привязка ti,bq769x0.yaml). В прогоне термистор модели при 50 °C читается как 50,04 °C, при −3 °C — как −2,98 °C.

Обмен с микросхемой. У bq769x0 бывают варианты с CRC на I²C. Драйвер пробует писать и читать с CRC, если не вышло — без. В прогоне модель ответила с CRC: 2917 записей, ни одной ошибки CRC.

Модель в прогоне: 4 ячейки LFP по 50 А·ч, внутреннее сопротивление 2 мОм, напряжение холостого хода — по таблице проекта (173в.6), за её краями — 0,2 В на процент; усиление АЦП 377 мкВ, смещение 0 мВ. Это допущения, а не данные микросхемы.


173в.3 Защита: пороги по умолчанию и что с ними делает драйвер

Пороги задаёт bms_init_config() в app/src/bms_common.c по типу ячеек (Kconfig: LFP по умолчанию, ещё NMC и LTO) и ёмкости (CONFIG_BAT_CAPACITY_AH, по умолчанию 50 А·ч):

Порог ячейки, В LFP NMC LTO
перенапряжение cell_ov_limit 3,80 4,25 2,85
заряд до cell_chg_voltage_limit 3,55 4,20 2,80
возврат после OV cell_ov_reset 3,40 4,05 2,70
выравнивать выше bal_cell_voltage_min 3,30 3,80 2,50
возврат после UV cell_uv_reset 3,10 3,50 2,10
разряд до cell_dis_voltage_limit 2,80 3,20 2,00
переразряд cell_uv_limit 2,50 3,00 1,90

Общие для всех химий: задержки OV и UV — 2000 мс; ток разряда и заряда — 1C, то есть 50 А при 50 А·ч, задержка 320 мс; короткое замыкание — 2C, 100 А, 200 мкс; температура разряда от −20 до 45 °C, заряда — от 0 до 45 °C, гистерезис 5 °C. Все они меняются по ThingSet.

Микросхема умеет не любые числа, и драйвер округляет: порог OV — код OV_TRIP, задержки и токи — по таблицам в bq769x0_registers.h (задержки OV 1, 2, 4, 8 с; UV 1, 4, 8, 16 с; порог короткого 44…200 мВ на шунте). В прогоне с усилением 377 мкВ:

Что Задано В микросхеме
OV 3,80 В OV_TRIP = 0x75 → 3794 мВ
UV 2,50 В UV_TRIP = 0x9F → 2503 мВ
перегрузка разряда 50 А, 320 мс 50 мВ на 1 мОм → 50 А, 320 мс
короткое 100 А, 200 мкс 89 мВ → 89 А, 200 мкс
задержка OV 2000 мс 8 с
задержка UV 2000 мс 16 с

Две последние строки — ошибка единиц (173в.7): таблицы задержек в секундах, а сравнивает драйвер с миллисекундами.

Что остаётся программе. У bq769x0 в железе только OV, UV, перегрузка и короткое при разряде. Ток заряда и все четыре температурных порога драйвер проверяет сам при каждом чтении флагов, температуру — с гистерезисом: флаг, раз поднявшись, снимается только на 5 °C дальше порога. Прогон:

Температура, °C Флаги Автомат
50 0x0280: перегрев заряда и разряда NORMAL → CHG → OFF
42 0x0280 — гистерезис держит OFF
39 0 OFF → DIS → NORMAL
−3 0x0100: холодно для заряда NORMAL → DIS
3 0x0100 — гистерезис держит DIS
6 0 DIS → NORMAL

173в.4 Автомат ключей

bms_state_machine() — четыре рабочих состояния: OFF (оба ключа выключены), CHG (только заряд), DIS (только разряд), NORMAL (оба). Шаг решает по двум вопросам: можно ли заряжать и можно ли разряжать.

return !bms_chg_error(bms->ic_data.error_flags & ~BMS_ERR_CHG_OFF) && !bms->full
       && bms->chg_enable;

bms_chg_error() смотрит OV, ток заряда, температуры заряда, обрыв провода ячейки, перегрев микросхемы и отказ ячейки; bms_dis_error() — UV, короткое, ток разряда, температуры разряда и те же общие. chg_enable и dis_enable — ручные выключатели, по ThingSet это wChgEnable и wDisEnable. Флаги full и empty в приложении нигде не выставляются — только в тестах: окончание заряда на 3,55 В проект оставляет зарядному устройству.

Ещё одна деталь для bq769x0 и ISL94202: в состоянии CHG ключ разряда включается, если через батарею течёт разрядный ток больше 0,5 А, и выключается ниже 0,1 А (в DIS так же с ключом заряда). Это «идеальный диод»: ток идёт через открытый канал, а не через встроенный диод транзистора.

Прогон — заряд 25 А зарядником, который не остановился на 3,55 В, затем разряд (исправленный драйвер, 173в.7):

Событие Время, с
пуск: OFF → DIS, затем DIS → NORMAL 0,5 и 1,0
самая полная ячейка выше 3794 мВ 496,73
микросхема: OV, ключ заряда выключен (через 7,99 с) 504,72
автомат: NORMAL → DIS 505,0
нагрузка 10 А: ячейка ниже 3,40 В 828,61
OV снят, DIS → NORMAL 829,00
разряд 40 А: самая слабая ячейка ниже 2503 мВ 5077,96
микросхема: UV (через 15,99 с), NORMAL → CHG 5093,95 и 5094,0
заряд 10 А: самая слабая выше 3,10 В, CHG → NORMAL 6188,00

Из CHG батарея заряжается, но не разряжается — ровно то, что нужно после переразряда.


173в.5 Выравнивание

У bq769x0 выравнивание ведёт драйвер: работа balancing_work раз в секунду. Условия из bq769x0.c: батарея в покое дольше bal_idle_delay (ток меньше bal_idle_current = 0,1 А), самая полная ячейка выше bal_cell_voltage_min (3,30 В у LFP), разброс больше bal_cell_voltage_diff = 10 мВ. Тогда кандидаты — все ячейки выше минимальной больше чем на 10 мВ, по убыванию напряжения; соседние ячейки в одной пятёрке одновременно разряжать нельзя, и из пары остаётся более полная. Номер ячейки — номер бита в CELLBAL1.

Прогон после заряда (покой):

Ячейка Напряжение, В Выравнивается
1 3,3432 нет — соседка ячейки 2
2 3,3745 да
3 3,3138 нет — самая низкая
4 3,7662 да

CELLBAL1 = 0x0A — ячейки 2 и 4. Выравнивание началось через 2,29 с после того, как ток прекратился, хотя по умолчанию задано 1800 с (30 минут) — это та же ошибка единиц, что и с задержками OV и UV.

У BQ76952 и ISL94202 выравнивает сама микросхема: драйверы переводят те же пороги в её регистры. Ток выравнивания в модели не учтён: при резисторах в десятки миллиампер и ячейках в 50 А·ч разница уходит часами, а в прогоне важно, какие ячейки и когда.


173в.6 Заряд в процентах и связь

Таблица. Напряжение холостого хода в bms_common.c — 21 точка через 5 %. У LFP (из прогона):

% 100 95 90 80 70 60 50 40 30 20 10 5 0
В 3,392 3,314 3,309 3,304 3,283 3,271 3,265 3,262 3,240 3,213 3,177 3,132 2,833

У NMC те же 21 точка от 4,198 до 2,800 В, у LTO — от 2,800 до 2,000 В (источник таблицы LTO указан в коде: DOI 10.3390/batteries5010031). Таблицу и ёмкость можно заменить по ThingSet: sOcvPoints_V, sSocPoints_pct, sNominalCapacity_Ah.

Как проект считает заряд. При пуске — один раз по таблице: bms_soc_reset(&bms, -1) берёт среднее напряжение ячеек. В прогоне 3,3105 В дали 91,53 % при истинных 91,25 %. Дальше — только счёт кулонов, bms_soc_update() после каждого чтения тока: заряд копится в мА·с и переносится в проценты, когда набегает больше 0,1 % — при 50 А·ч это 180000 мА·с. Больше таблица не используется: нет поправки ни по покою, ни по полному заряду (full не выставляется). Документация проекта так и пишет: оценка заряда основана на счёте кулонов.

Почему по напряжению у LFP нельзя: таблица почти плоская.

Напряжение, В 3,260 3,270 3,280 3,290 3,300
Заряд по таблице, % 39,00 58,33 68,12 72,69 77,50

10 мВ между 3,26 и 3,27 В — это 19,33 % заряда. Для сравнения: у ячейки из 173б 10 мВ давали 2 %. В прогоне модель без ошибки датчика тока, и счёт держался: к переразряду 2,39 % против истинных 2,00 %. Смещение датчика и старение копятся так, как в разделе 121.

Связь. Все данные — объекты ThingSet в app/src/data_objects.c, в четырёх группах: Device (производитель, версии, xShutdown, xReset), Conf (все пороги из 173в.3, таблица, ёмкость, предустановки xPresetLFP, xPresetNMC, xPresetLTO), Meas (rCellVoltages_V, rPackCurrent_A, rSOC_pct, rErrorFlags, rBmsState, rBalancingStatus и др.), Input (wChgEnable, wDisEnable). По флагам в коде видна роль первой буквы: r — только чтение, s — чтение и запись с сохранением в энергонезависимую память, w — запись без сохранения, x — вызов функции. Транспорт: последовательный порт (по документации 115200 бод, живые данные раз в секунду); CAN и ISO-TP включены в .conf плат bms_8s50_ic, bms_16s100_sc и bms_c1 (500 кбит/с в devicetree; по документации адрес источника — младший байт идентификатора кадра); Bluetooth — на bms_c1. На bms_5s50_sc CAN выключен: по .conf платы F072 не хватает ОЗУ. После записи в Conf вызывается bms_ic_configure() — пороги уходят в микросхему сразу. Формат кадров ThingSet — в библиотеке thingset-node-c, здесь он не разобран. Про CAN как шину для прикладных протоколов — раздел 19б.

Размер логики на Cortex-M0 (F072) и M0+ (G0B1), arm-none-eabi-gcc -Os, без Zephyr и ThingSet: bms_common.c — 1536 байт кода, bms_soc.c — 272, helper.c — 252, драйвер bq769x0 — 4562; всего 6622 байта. В ОЗУ логики — 168 байт: две рабочие таблицы по 21 числу float.


173в.7 Грабли

Первые восемь — в драйвере bq769x0 этой версии, прогон их показывает. Исправлять или нет — дело проекта; здесь они как типовые места, где BMS ошибается.

После перенапряжения батарея больше не заряжается. Обработчик ошибок снимает OV, когда самая полная ячейка ниже dev_data->ic_conf.cell_ov_reset, а UV — когда самая слабая выше cell_uv_reset. Но в копию настроек драйвера эти два порога не копируются ни в одной функции — там нули. OV не снимается никогда: в прогоне ячейка опустилась ниже 3,40 В на 828,61 с, а на 1417,01 с флаг всё ещё стоял. UV снимается сразу: под той же нагрузкой 40 А за 60 с микросхема выключала ключ разряда 4 раза, ключ был открыт 31,70 с, и ячейка ушла до 2,3710 В в покое. С двумя строками исправления (кэш получает оба порога, fix173v.py) — одно срабатывание, 0,00 с, 2,5119 В.

Задержки в 4 раза и 8 раз длиннее. Таблицы задержек OV и UV в секундах, а cell_ov_delay_ms = 2000 сравнивается с ними как есть: выбирается наибольшая — 8 с и 16 с — и обратно в поле _ms пишется 8 и 16. В прогоне OV сработал через 7,99 с, UV — через 15,99 с.

Выравнивание через 2 с вместо 30 минут. if (k_uptime_get() - dev_data->active_timestamp >= dev_data->ic_conf.bal_idle_delay — слева миллисекунды, справа 1800 секунд. Выравнивание начинает разряжать ячейки почти сразу после окончания тока, пока напряжение ещё не успокоилось.

Пороги в вольтах превращаются в милливольты. После записи в микросхему драйвер возвращает «фактический» порог: ic_conf->cell_ov_limit = 3794, а не 3,794. По ThingSet sCellOvervoltage_V покажет 3794. Хуже другое: любая запись в Conf снова вызывает bms_ic_configure(), и из «3794 В» получается OV_TRIP = 0xF2 — порог 4548 мВ для ячейки LFP, UV — 2955 мВ. Это на ПК (long 64 бита); на Cortex-M long 32 бита, произведение переполняется, и число будет другим — не проверено.

Смещение АЦП со знаком читается без знака. dev_data->adc_offset = (signed int)adcoffset; — приведение uint8_t к int знак не восстанавливает. Код в самом драйвере назван «2's complement»; при ADCOFFSET = 0xFE (−2 мВ) драйвер берёт 254 мВ, и ячейка 3,2646 В читается как 3,5207 В. Проверять на плате с отрицательным смещением.

После перегрузки автомат в NORMAL, а ключ разряда выключен. Первый проход обработчика снимает флаг короткого или перегрузки сразу (счётчик секунд 0, а 0 делится на 60). В прогоне короткое 150 А на 20 мс сработало и было снято в тот же шаг 10 мс, основной цикл флага не увидел, автомат остался в NORMAL и ключ не включал; нагрузка 10 А после этого — ток 0,00 А. Помогло только выключить и включить wDisEnable. Так же с перегрузкой 60 А: срабатывание через 0,32 с, и снова ключ выключен при NORMAL. Здесь есть допущение модели: микросхема при UV, перегрузке и коротком сама выключает ключ разряда, при OV — ключ заряда (по даташиту не сверено). У ISL94202 драйвер для такого случая выставляет флаги BMS_ERR_DIS_OFF и BMS_ERR_CHG_OFF, у bq769x0 — нет.

Защита по току заряда дребезжит. Она в программе и без задержки (/* ToDo: consider ic_conf->chg_oc_delay */): ток 55 А → флаг → ключ заряда выключен → ток 0 → флаг снят → ключ включён. В прогоне — 20 смен состояния за 10 с, каждые 0,5 с.

Состояние выравнивания показывает ноль. Работа выравнивания пишет маску в ic_data->balancing_status, а каждое чтение данных затирает её значением, которое меняет только ручное bms_ic_balance(). В прогоне из 1000 шагов, когда CELLBAL1 = 0x0A, rBalancingStatus показывал 0 в 510.

Заряд в процентах никто не поправляет. Таблица используется только при пуске. Если прибор перезапустился под нагрузкой — ошибка стартовой точки останется до следующего пуска: на плоской части LFP 10 мВ стоят почти 20 %.


173в.8 Итоги

Дальше: раздел 174 «Кварцы, генераторы и тактовые цепи».

Открыть в интерактивном справочнике