Добавить в корзинуПозвонить
Найти в Дзене

Полиномы и костыли

Друзья, заранее прошу прощения за криво вставленный в статью программный код. К сожалению, движок Дзена не позволяет это сделать корректным образом. Я всю неделю вовсе не бездельничал. Помимо текущих дел занимался исследованием разных способов аппроксимации кривых заряда-разряда литий-полимерных аккумуляторов на Ардуино. Задача корректного отображения заряда аккумулятора не так проста как кажется на первый взгляд, и как часто это бывает в инженерии, её реализация связана с кучей тонкостей, о которых я постараюсь рассказать. В статье будут графики с нудными пояснениями, так как я не сторонник выдачи готовых решений по типу «how to». Хотите быстро и качественно — берёте что-то типа MAX17043 и встраиваете в своё устройство. Я же, как товарищ Сухов, предпочел помучиться. Задача может быть решена разными способами. Но все эти способы так или иначе сведутся к преобразованию напряжения на нашем аккумуляторе в проценты по определенному математическому закону. Самый простой способ, который на

Друзья, заранее прошу прощения за криво вставленный в статью программный код. К сожалению, движок Дзена не позволяет это сделать корректным образом.

Я всю неделю вовсе не бездельничал. Помимо текущих дел занимался исследованием разных способов аппроксимации кривых заряда-разряда литий-полимерных аккумуляторов на Ардуино. Задача корректного отображения заряда аккумулятора не так проста как кажется на первый взгляд, и как часто это бывает в инженерии, её реализация связана с кучей тонкостей, о которых я постараюсь рассказать. В статье будут графики с нудными пояснениями, так как я не сторонник выдачи готовых решений по типу «how to». Хотите быстро и качественно — берёте что-то типа MAX17043 и встраиваете в своё устройство. Я же, как товарищ Сухов, предпочел помучиться.

Задача может быть решена разными способами. Но все эти способы так или иначе сведутся к преобразованию напряжения на нашем аккумуляторе в проценты по определенному математическому закону. Самый простой способ, который на Ардуино реализуется за 5 минут — линейная аппроксимация. График заряда-разряда нашего аккумулятора будет выглядеть следующим образом:

Как видите - просто линия соединяющая 2 точки: 2.9В и 0% с 4.2В и 100%. Далее мы считаем, всё что находится между двумя базовыми точками имеет линейную зависимость. Как делаем на Ардуино:

1. Задаём максимальное и минимальное значения напряжения на аккумуляторе.

int min_batt_volt = 300;

int max_batt_volt = 420;

И вот тут уже вы можете видеть один нюанс, о котором расскажу ниже.

2. Определяем напряжение на аккумуляторе (у меня это ножка(пин) А9):

float Ubatt = analogRead(A9)*(5.0 / 1023);

3. Переводим напряжение в процент заряда по линейному закону:

int batt_percent_line = map(Ubatt*100, min_batt_volt, max_batt_volt, 0, 100);

Можно захардкодить для понятности:

int batt_percent_line = map(Ubatt*100, 300, 420, 0, 100);

Функция map() в данном случае преобразует напряжение батареи умноженное на 100 из одного диапазона значений 300-420 в другой 0-100. Вот здесь и кроется тот самый нюанс о котором говорил выше. Все напряжения необходимо умножить хотя бы на 100. Дело в том, что функция map() целочисленная, а это значит, если мы скормим ей число с плавающей точкой, например 3.345В, то это число превратится в просто 3. В итоге у нас получится преобразование диапазона 3-4 в 0-100. Точность будет никакая. Поэтому все напряжения умножаем хотя бы на 100. Можно умножить на 1000, так будет еще точнее. Но я на тестовом стенде не заметил особой разницы.

4. Функция map() была мною замечена в хитрости — она может выдавать значения за пределами указанного ей диапазона. Поэтому, чтобы не получить заряд аккумулятора -10% или чего доброго в 146% надо сделать небольшую проверку.

if (batt_percent_line > 100) {

batt_percent_line = 100;

} else if (batt_percent_line < 0) {

batt_percent_line = 0;

}

Движок Дзена переформатирует код по своему, но я думаю, понятно: если процент больше 100, то принудительно присваиваем 100%, если меньше 0, то присваиваем 0%.

5. Для порядку следует куда-то вывести наше значение. У меня это экран, но тут пусть будет COM порт:

Serial.print(batt_percent_line);

Serial.print("%");

Serial.println();

На этом всё. Учитывать разницу между напряжением на батарее при зарядке и разрядке особо не требуется — у нас во всех случаях линейно от 0 до 100, а точность плюс-минус лапоть по карте. В общем: просто, неточно, но быстро. Я считаю, что данный метод имеет право на жизнь в тех случаях, когда нам надо что-то грубо и неточно показать.

Следующим методом, который я попробовал реализовать — полиномиальная аппроксимация.

В общем случае задача сводится к следующему:

- собрать данные заряда и разряда конкретного аккумулятора в конкретном устройстве;

- построить графики заряда и разряда;

- найти математическую функцию, наиболее точно описывающую наши графики;

- реализовать пересчет напряжения в процент с помощью функции в программном коде.

В статье «Ардуино. Полиномиальная аппроксимация заряда аккумулятора» я описывал как можно выполнить первые 3 пункта. Здесь остановимся на программной реализации и нюансах.

Уравнения у меня получились такие:

Заряд: y(x)=−486.8010x⁴+6961.1683x³−37045.1501x²+87023.3144x−76186.5712

Разряд: y(x)=17.8640x⁵−354.0801x⁴+2652.2137x³−9392.6518x²+15714.372x−9877.3302

При составлении уравнений надо принимать во внимание возможности вашего микроконтроллера. В моем случае это Arduino Mega 2560 — у него точность 6-7 знаков после запятой. Поэтому необходимо избегать коэффициентов вида 6,8E-13 (очень маленькое число). Также следует учитывать, сложность таких вычислений для микроконтроллера. Разрядное уравнение с полиномом 5-й степени на Arduino Mega 2560 считается 1,2мсек — это много. Подробней — смотрите на видео в конце статьи.

Можно подобрать уравнения, которые будут более точно описывать реальные кривые заряд-разряд. Но там получаться полиномиальные уравнения 14-й степени и выше. Какое точно время займет их вычисление на Arduino Mega 2560 я сказать не берусь. Возможно полсекунды. Имея ограничения на размер полинома мы имеем ограничение на точность описания наших зарядно-разрядных кривых. Соответствие расчетных кривых и реальных можно посмотреть вот в этой статье. Здесь мы посмотрим на чистую математику под микроскопом. :)

-2

Под микроскопом всё выглядит очень не радужно, в хорошем смысле этого слова.

Обратите внимание на верх и низ графика. Зарядная кривая у нас не доходит до 100%, а разрядная наоборот вылетает за 100%. Низ графика — это вообще «ужос». Если с разрядом всё более или менее — у нас растет % заряда батареи если напряжение ниже 3.1 вольта, то зарядная кривая уходит в отрицательную область. В общем, сингулярности чистой воды. Не может быть заряд аккумулятора ниже нуля. :) Поэтому программу придется писать с учетом всех вот этих нюансов. Также следует обратить внимание, что кривые разряда и заряда имеют расхождение. Например, при разряде аккумулятора до 3,6В — имеем еще 37% заряда, а вот при заряде аккумулятора это будет всего лишь 8%. Этот нюанс тоже надо учесть при программировании. Иначе у нас получится так, что пользователь подключил зарядку к устройству и у него процент заряда аккумулятора магическим образом уменьшился с 37% до 8%. Но так ведь не бывает!? Бывает. Но только в антивселенной. Хотя нет! Там тоже такого быть не может. Заряд аккумулятора это по сути энергия, а наличие отрицательной энергии пока не доказано.

В общем из всех этих умствований следует несколько особенностей, которые необходимо учесть при программировании:

- необходимо отслеживать подключение и отключение зарядного устройства, чтобы знать когда переключаться на ту или иную кривую;

- поскольку процент заряда существенно различается для разных кривых, то переход от разряда к заряду и наоборот надо обрабатывать таким образом, чтобы не пугать пользователя.

- нужно учесть что в определённых случаях мы будем иметь заряд больше 100% и меньше 0% и соответствующим образом эти случаи обработать.

Первым делом определим переменные, которые нам потребуются:

bool charger_state_changed = 1; //переменная для отслеживания переключения заряд-разряд

bool charger_plugged = 0; //переменная куда записываем подключено зарядное или нет

Обе переменные типа bool — правда / ложь или 0 / 1.

Далее определим подключено зарядное устройство или нет:

if (digitalRead(A8) == 1) { //считываем логический уровень на ноге А8.

charger_plugged = 1;

} else {

charger_plugged = 0;

}

У меня напряжение от зарядника заведено на ножку (пин) А8 микроконтроллера. В реальной программе лучше дополнительно считывать уровень при запуске микроконтроллера — для Ардуино это в void setup(), а далее периодически считывать подключен зарядник или нет в void loop(). Как это можно оформить — смотрите в прилагаемом скетче.

Теперь настало время работы с кривыми. Я решил начать с зарядной, но принципиальной разницы нет.

if (charger_plugged == 1) { // зарядник подключен

//зарядная кривая

if ((charger_state_changed == 1) && (round(-486.8010*pow(Ubatt,4)+6961.1683*pow(Ubatt,3)-37045.1501*pow(Ubatt,2)+87023.3144*Ubatt-76186.5712) <= batt_percent_poly)) { // в случае переключения ждем пока процент вырастит, а потом меняем

charger_state_changed = 1;

if (millis() < 5000) { //обрабатываем запуск при подключенной зарядке. Без этого условия при запуске с подкл. зарядником batt_percent_poly вычисляться не будет и процент зависнет.

charger_state_changed = 0;

}

} else {

charger_state_changed = 0;

batt_percent_poly = round(-486.8010*pow(Ubatt,4)+6961.1683*pow(Ubatt,3)-37045.1501*pow(Ubatt,2)+87023.3144*Ubatt-76186.5712);

if (Ubatt <= 3.54 ) {

batt_percent_poly = map(Ubatt * 100, 300, 354, 0, 4);

}

}

}

Что мы делаем если зарядное устройство подключено:

1. Отслеживаем переключение. Если ранее зарядник не был подключен, а теперь подключен, то вычисляем какой процент заряда должен быть по нашей кривой (зарядной), если процент меньше или равен существующему, то ничего не меняем. Таким образом, процент заряда на экране не будет изменяться, пока он не станет больше чем был. Всё это сделано для того, чтобы пользователь не пугался, когда у него заряд батареи резко падает с 37% до 8%. В нашем случае ничего никуда не упадет и не вырастит. На видео вы сможете увидеть как это происходит в реальности.

Условие отрабатывающие запуск при подключенной зарядке нужно чтобы временно вычислять и обновлять процент заряда. Если обратите внимание, то в верхней ветке условия процент вычисляется, но переменная не обновляется. В нижней ветке кода вычисляется процент и обновляется переменная. Сделал так, чтобы не вычислять процент дважды в одной ветке условия. Напомню, что вычисление процента заряда занимает от 0.8 до 1,2мс.

Далее, а точнее если все предыдущие условия не совпали (else), то вычисляем процент заряда и корректируем нашу кривую, которая заходит в минусА, с помощью кусочно-линейной аппроксимации, о которой чуть ниже расскажу подробней.

Вторая часть кода — разрядная кривая.

if (charger_plugged == 0) { //зарядник не подключен

// разрядная кривая

if ((charger_state_changed == 0) && (round(17.8640*pow(Ubatt,5)-354.0801*pow(Ubatt,4)+2652.2137*pow(Ubatt,3)-9392.6518*pow(Ubatt,2)+15714.372*Ubatt-9877.3302) >= batt_percent_poly)) { // в случае переключения ждем пока процент упадет, а потом меняем

charger_state_changed = 0;

} else {

batt_percent_poly = round(17.8640*pow(Ubatt,5)-354.0801*pow(Ubatt,4)+2652.2137*pow(Ubatt,3)-9392.6518*pow(Ubatt,2)+15714.372*Ubatt-9877.3302);

charger_state_changed = 1;

if (Ubatt <= 3.26) {

batt_percent_poly = map(Ubatt*100, 300, 326, 0, 3);//диапазон 3,0 вольта - 3,2В преобразуем в 0 - 3 процента.

}

}

}

Здесь всё по аналогии с зарядной кривой, но только наоборот. Там мы не изменяли переменную если вычисленное значение было меньше либо равно, то в случае разряда всё наоборот: не изменяем значение переменной пока вычисленное значение больше либо равно. Аккумулятор ведь разряжается, напряжение на нём падает. Также корректируем кривую с помощью кусочно линейной аппроксимации — функция map().

Ну и добавим ряд финальных проверок:

if (batt_percent_poly > 100) { //проверяем результат аппроксимации. В некоторых точках может быть <0 или >100

batt_percent_poly = 100;

} else if (batt_percent_poly < 0) {

batt_percent_poly = 0;

} else if (Ubatt > 4.15 && Ubatt < 4.17) { //здесь мы немного обманываем пользователя. По факту 100% считается 4.2В, но такое напряжение не бывает после отключения зарядника.

batt_percent_poly = 98;

} else if (Ubatt > 4.18 && Ubatt < 4.19) {

batt_percent_poly = 99;

} else if (Ubatt > 4.19) {

batt_percent_poly = 100;

}

Здесь, как мне кажется какие-либо комментарии не нужны. Всё понятно даже младенцу. Отсекаем не реальные случаи, и обрабатываем специфические. Например, когда батарея полностью заряжена, зарядка отключилась, устройство питается от внешнего источника питания, а напряжение на аккумуляторе падает до 4.15В. Такое происходит через несколько часов или суток, в зависимости от ёмкости и состояния аккумулятора.

Выводить в COM порт думаю не будем.

Теперь перейдем к способу, который я назвал кусочно-линейной аппроксимацией (по факту это именно кусочно-линейная аппроксимация).

Суть метода в том, чтобы разбить кривую на понятные, более или менее линейные участки, и каждый из этих участков обрабатывать отдельно. Поясню на примере разрядной кривой.

-3

Для примера я взял разрядную кривую, не реальную, в построенную с помощью уравнения. Красным на графике выделены фрагменты кусочно-линейной функции. Желтым — более точный вариант. Таким образом, если смотреть по красной функции, мы имеем 4 куска, более или менее описывающих нашу основную кривую: (2.9, 0 — 3.4, 15) (3.4, 15 — 3.88, 75) (3.88, 75 — 4.04, 92) (4.04, 92 — 4.2, 100). Далее надо всё это закодировать в программе для обеих кривых.

Вот так у меня это получилось:

Определяем переменную куда будем писать процент заряда:

int batt_percent_patch_line;

Что делаем если зарядник подключен:

if (charger_plugged == 1) { // зарядник подключен

if (Ubatt < 3.0) {

batt_percent_patch_line = 0;

} else if (Ubatt >= 2.9 && Ubatt <= 3.6) {

batt_percent_patch_line = map(Ubatt*100, 290, 360, 0, 3);

} else if (Ubatt > 3.6 && Ubatt <= 4.0) {

batt_percent_patch_line = map(Ubatt*100, 360, 400, 3, 82);

} else if (Ubatt > 4.0 && Ubatt <= 4.1) {

batt_percent_patch_line = map(Ubatt*100, 400, 410, 82, 90);

} else if (Ubatt > 4.1 && Ubatt <= 4.2) {

batt_percent_patch_line = map(Ubatt*100, 410, 420, 90, 100);

} else if (Ubatt > 4.2) {

batt_percent_patch_line = 100;

}

}

С помощью условий и функции map() описываем наши куски. В примере они отличаются от графика, но сути это не меняет. Она довольно проста: берем диапазон напряжений, например 2,9 — 3,6 вольта, умножаем всё на 100, и преобразуем диапазон напряжений в диапазон нужных нам процентов.

else if (Ubatt >= 2.9 && Ubatt <= 3.6) — диапазон напряжений

batt_percent_patch_line = map(Ubatt*100, 290, 360, 0, 3); - преобразование диапазона 2,9 — 3,6 вольта в диапазон 0-3 процента.

Разряд делаем по аналогии. Разумеется для другой кривой и других диапазонов.

if (charger_plugged == 0) { //зарядник не подключен

if (Ubatt > 4.195) {

batt_percent_patch_line = 100;

} else if (Ubatt < 4.195 && Ubatt >= 4.09) {

batt_percent_patch_line = 99;

} else if (Ubatt < 4.09 && Ubatt >= 3.32) {

batt_percent_patch_line = map(Ubatt*100, 332, 409, 5, 99);

} else if (Ubatt < 3.32 && Ubatt >= 3.0) {

batt_percent_patch_line = map(Ubatt*100, 300, 332, 0, 5);

} else if (Ubatt < 3.0) {

batt_percent_patch_line = 0;

}

}

На видео показано как это работает в реальности.

В видео вместо аккумулятора я использовал лабораторный блок питания. Точность установки напряжения в районе 0,05В. Поэтому не стоит пугаться расхождения показаний на блоке питания и тестовом стенде. Видео прошу смотреть очень внимательно, все моменты описанные в тексте в нём отражены.

А здесь тот самый баг, когда юзер быстро включил и выключил зарядник:

Следует сказать, что блокировку изменения процента для кусочно-линейной аппроксимации я не делал. Т.е если вы просто повторите код из скетча, то скачки процентов у вас будут. Я что-то так и не смог придумать толкового решения, кроме как делать проверку в каждом из условий. На мой взгляд это сильно осложнит понимание и запутает код. Если у кого-то будут толковые идеи, высказывайте их в комментариях. Также в реализации полиномиальной аппроксимации есть небольшой баг. Если быстро включить и выключить зарядник, то можно увидеть скачок процентов. Не стал заморачиваться с устранением бага. В реальном устройстве такого нет из-за того, что напряжение и процент довольно сильно усредняются, поэтому краткосрочные изменения не влияют на общую картину.

Что касается трех протестированных методов, то кратко могу сказать, что все они имеют право на жизнь и каждый можно применять.

Линейная аппроксимация — простота и скорость как реализации, так и работы. Но очень низкая точность.

Полиномиальная аппроксимация — высокая точность, но увы, не для Ардуино. Хотим получить более или менее высокую точность, придется ставить костыли. Есть перспектива развития. Например, можно строить модели с учетом температуры аккумулятора, старения аккумулятора, тока разряда и т.п. В принципе всё это можно реализовать на Ардуино.

Кусочно-линейная аппроксимация — кроме высокой скорости работы, есть неплохой задел по увеличению точности. Можно разбивать исходную функцию не на 5 кусков, а на 10, тем самым повышая точность. Если учесть переход с одной кривой на другую, который я не сделал, то количество кода будет сопоставимо с полиномиальной аппроксимацией. Скорость работы всё равно будет выше. Если критична скорость, то лучше применять кусочно-линейный метод. Возможно, опять же при помощи различных костылей, можно учесть температуру аккумулятора, старение, ток разряда и заряда. Но я не думал как это сделать.

Я остановился на полиномиальной аппроксимации. Не по той причине, что она лучшая. Просто в своём устройстве я хотел и реализовал её раньше всех других. А переписывать код, да еще с учетом, что не придумал хороших решений обработки перехода с одной кривой на другую, не охота.

Готовый скетч можно скачать тут.

Строит графики по уравнениям или точкам можно тут.

По традиции: больше математики и кривых вам в ленту, при полном отсутствии костылей. :)