В портативном устройстве, работающем от аккумулятора, почти обязательным «удобством» является индикатор уровня его заряда. Казалось бы, если оно собрано на основе любого современного микроконтроллера и имеет графический дисплей, ничего сложного в этом нет: нужно лишь регулярно измерять напряжение батарейки с помощью встроенного АЦП и выводить его в виде традиционной батарейки????, степень заполнения которой зеленой краской зависит от напряжения.  Но если так сделать в лоб, есть риск, что индикатор будет вести себя, как в известном перле «она металась, как стрелка осциллографа». В лучшем случае, он будет время от времени раздражающе подергиваться туда-сюда на один-два пикселя.

В статье описывается простая реализация индикатора разряда, лишенная этого недостатка.

Проблема «дергающейся батарейки»

Типичные разрядные характеристики литий-ионного аккумулятора при различных токах
Типичные разрядные характеристики литий-ионного аккумулятора при различных токах

Причин такой нестабильности показаний индикатора несколько. Для начала, нужно отметить, что напряжение почти полностью заряженного литий-ионного аккумулятора – 4,0 В, а почти полностью разряженного  -- 3,4-3,5 В. Соответственно, перепад от 0 до 100% соответствует всего 0,5-0,6 В, то есть индикация заряда с шагом 10% требует точности измерения напряжения не хуже 1%. При этом метрологические характеристики «вольтметра», встроенного в устройство, чаще всего достаточно скверные, потому что всерьез к проектированию этого узла относятся достаточно редко. Да и само напряжение, поступающее на устройство, потребление тока которым постоянно меняется в интервале от нескольких до 150-200 миллиампер, с учетом его подключения через невысокого качества китайский разъем типа JST – тоже непостоянно. При непостоянном токе потребления, зависимость разрядной характеристики аккумулятора от тока разряда – самое главное препятствие для точного определения заряженности по напряжению. Поэтому в смартфонах и ноутбуках для этого чаще применяют другой подход – специализированный контроллер подсчитывает кулоны, пошедшие на зарядку батареи и затраченные затем при разряде, а напряжение при этом играет вспомогательную роль.

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

Решение

Черная линия – измеренное напряжение, красная – то, что мы будем использовать для определения уровня заряда.
Черная линия – измеренное напряжение, красная – то, что мы будем использовать для определения уровня заряда.

Предлагаемая идея состоит в том, что раз потребление тока устройством меняется и наибольшая просадка напряжения происходит в моменты наибольшего потребления, нужно фиксировать напряжение именно в такие моменты. Это логично, так разряженный аккумулятор еще может долгое время «тянуть» устройство, пока оно находится в малопотребляющем режиме, но быстро просядет ниже минимально допустимого напряжения, когда потребление подскочит, например, при включении дисплея. При этом очевидно, что когда аккумулятор разряжается, степень его заряженности может только снижаться, но никак не увеличиваться. И наоборот, когда аккумулятор заряжается – степень его заряженности только возрастает, несмотря на то, что измеренное значение напряжения может в какие-то моменты падать из-за помех и т.п. Поэтому давайте будем во время разряда игнорировать поступающие данные об изменениях напряжения, если оно растет, считать этот рост артефактом. Делается это элементарно – путем сравнения каждого следующего значения измеренного напряжения с ранее зафиксированным минимальным, которое обновляется каждый раз, когда измеренное значение окажется ниже него. Во время заряда мы поступим аналогично, но фиксировать будем не минимумы, а максимумы.

Разумеется, нам здесь понадобится некий сигнал от зарядного устройства, информирующий о том, в каком состоянии (заряд или разряд) находится аккумулятор. Обычно контроллеры заряда литиевых аккумуляторов имеют выход на светодиод или пару светодиодов, который несложно завести на GPIO контроллера.

Тут нужно учесть еще и то, что кривые разряда и заряда существенно различаются. Поэтому по смене статуса зарядного контроллера нам нужно сменить не только направление работы индикатора, но и формулу расчета процентов заряженности от напряжения. А также то, что на протяжении этапа CV, на который приходится примерно 25-30% емкости батареи и половина времени заряда, индикатор будет показывать 100%, если мы будем принимать во внимание только напряжение. Можно так и оставить (сделав внятную индикацию, что зарядка еще не окончена), а можно заморочиться и вычислять на этом этапе проценты заряженности, как линейную (или более сложную) функцию от времени.

Код

Нижеприведенный код на Си реализует самый простой вариант описанного алгоритма. Здесь мы считаем, что полностью разряженная батарейка при разряде дает 3,4 В. Чем это обусловлено? Во-первых, тем, что примерно с этого напряжения начинается быстрый спад напряжения, и дальнейший разряд не дает существенно большего времени работы. Во-вторых, если питать МК от аккумулятора через LDO на 3,3 В, при снижении напряжения ниже этого значения начинает падать и напряжение питания МК. В некоторых случаях это не очень желательно, и в частности, в данной задаче пришлось бы задействовать встроенный источник опорного напряжения, чтобы измерить напряжение батареи в 3,3В и ниже. Та же полностью разряженная батарея при включении заряда сразу увеличивает напряжение до 3,65 В, я же взял 3,6 В, так как тогда при том же коэффициенте наклона автоматически выходит нужное напряжение на 100% заряженном аккумуляторе 4,2 В.

// Глобальные переменные и типы данных: 
// Состояние зарядного устройства 
typedef enum  {
                  NOCHG,
                  CHG,
                  CHGEND
              } tChgState;  

tChgState oldChargeStatus = NOCHG // Переменная для хранения предыдущего состояния
  																// зарядного устройства между вызовами функции 
uint8_t minBatPercent = 100;      // Минимальное и максимальное значения 
uint8_t maxBatPercent = 0;        // уровня заряда батареи 

// Код следующих двух функций я не привожу, так как он привязан
// к реализации конкретного устройства в железе.

tChgState getChargeState(void) 
{       
		// Здесь мы определяем состояние зарядного устройства
  	.
    .
    . 
}  

uint16_t getBatVoltage() 
{         
		// А здесь запрашиваем АЦП и вычисляем значение напряжения на батарее в милливольтах
  	.
    .
    . 
}

uint8_t batPercent(uint16_t voltage) 
{         
		tChgState chargeStatus = getChargeState();
  	uint16_t emptyBatVoltage = 3400;// Напряжение, соответствующее полностью 
																		// разряженной батарее
    uint8_t slope = 6;							// 6 мВ/%
  	if(chargeStatus == CHG)					// При заряде напряжение возрастает, учитываем это
    		emptyBatVoltage = 3600;
  	int8_t result = (voltage - emptyBatVoltage) / slope;
    if(result < 0) result = 0;     // Уровень заряда не может оказаться меньше нуля
  	if(result > 100) result = 100; // и больше 100%.
  	
    // Ищем минимум и максимум и сохраняем их в глобальных переменных для 
    // использования при следующем вызове
  	if(minBatPercent > result)	minBatPercent = result;
  	if(maxBatPercent < result) 	maxBatPercent = result;
  
  	if(chargeStatus != oldChargeStatus) // При изменении состояния зарядного устройства
    {                               		// начинаем заново с чистого листа.
      	minBatPercent = result;
      	maxBatPercent = result;
    }
		if(maxBatPercent - minBatPercent > 20) // Защита от особо сильных помех
  	{
    		minBatPercent = result;
      	maxBatPercent = result;
    }
  	oldChargeStatus = chargeStatus; // Перед окончанием сохраняем текущее состояние ЗУ
  	// И, наконец, возвращаем максимальное значение, если идет заряд
  	// или минимальное -- если идет разряд.
  	if(chargeStatus == CHG)
    {
      	return maxBatPercent; 
    }
  	else
    {
      	return minBatPercent;
    }
}

Далее мы в удобном месте вызываем функцию batPercent, скажем, раз в секунду, и то, что она вернула, передаем в код, рисующий батарейку.

* * *

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

Комментарии (21)


  1. AntonSor
    28.11.2021 20:31

    Спасибо, очень интересно! Сам в подобной ситуации усреднял десяток-другой измерений (секунд на 30). А окончание заряда отслеживал с выхода микросхемы зарядника tp4056


    1. jar_ohty Автор
      28.11.2021 21:53
      +2

      Тоже имеющий право на существование вариант. Но он хорошо отсекает дрожание индикатора от шумов, попадающих в АЦП, но не устраняет влияния включения дисплея, после которого такой индикатор сразу начинает бежать вниз. А сильно увеличивать время усреднения — это потерять оперативность. Мой вариант хорош именно тем, что на падение напряжения он реагирует моментально, при этом не шевелясь, когда не надо.


  1. VT100
    28.11.2021 22:49
    +4

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

    Строго говоря — может увеличиваться. После длительной разрядки высоким током (истощение приповерхностного слоя электролита) и перехода к длительному малопотреблению (диффузионное выравнивание концентраций) — возможно некоторое увеличение остаточной ёмкости (см. разрядные кривые для разных токов).


    1. jar_ohty Автор
      28.11.2021 23:08

      Это да. Но все-таки, это эффект кажущийся, реально новой энергии в аккумуляторе не вырабатывается. Так что еще строже говоря - не может)


      1. gleb_l
        28.11.2021 23:41
        +2

        Принесли прибор с мороза домой. Аккумулятор прогрелся, его внутреннее сопротивление упало, ЭДС возросла, фактическая доступная ёмкость выросла. Как это обрабатывать?


        1. jar_ohty Автор
          28.11.2021 23:44
          +1

          Для этого там есть один if, который сработает, если напряжение вырастет существенно. А если не сильно вырастет, то и Махадев с ним.


      1. VT100
        28.11.2021 23:48
        +1

        Да, энергия не вырабатывается. Но ёмкость вырасти — может.
        Просто при большой токовой нагрузке — емкость падает. Условно: при токе С20 емкость 1 отн. ед.; при С10 — 0,95 отн… ед.; при С5 — 0,75 отн. ед. и т.д. Переход между кривыми — приносит/отнимает некоторое количество ёмкости.


  1. kox
    28.11.2021 22:59
    +1

    Я использовал фильтр Калмана.


    1. jar_ohty Автор
      28.11.2021 23:07

      Ну, Калмана тут самое милое дело применять, только может быть, что-нибудь попроще?


      1. fk01
        30.11.2021 01:52

        Скользящее экспоненциальное среднее?

        Y[n] = (1UL * X[n] * N + Y[n-1] * ((1UL<<16) - N)) >> 16.

        Где N подобрать по вкусу. Проще некуда. Хотя АЧХ такого фильтра спадает не быстро и возможны казусы:

        EMA filter frequency response (picture)


  1. Fenex
    29.11.2021 00:58
    +1

    Не хватает обработки случая, когда устройство продолжительное время на зарядке потребляет в единицу времени энергии больше, чем получает. Такое ведь в теории может быть. Допустим, устройство питается от солнечных панелей, и возникает необходимость в течении часа что-то усердно считать. Но вот незадача — погода испортилась и тучки набежали :( Я конечно понимаю, что мк и «считать» это странная связка, но…

    Смартфоны кстати умеют о таком режиме «зарядки» предупреждать: у меня показывает такую уведомляшку если включить GPS, интернет с картами и дисплей на 100% яркости (зарядное устройство в авто плохое). Но тут понятно, как говорилось в статье — обработка намного сложнее, да ещё и отдельным контроллером.


    1. jar_ohty Автор
      29.11.2021 09:53

      Тут опять же, все аппаратурно-зависимо. Например, если делать на LTC4054, TP4056 и тому подобных — нужно делать еще и схему, которая при зарядке переключает питание устройства на внешний блок питания, а аккумулятор при этом только заряжается. Нельзя просто подключить схему заряда к аккумулятору с одной стороны, а питаемую схему с другой — так как в таком случае заряд не прекратится никогда. И в таком случае вообще этой проблемы не возникает. Если напряжение источника просаживается ниже допустимого — 4054 просто выставляет NOCHG и прекращает заряд. Более продвинутые контроллеры батареи умеют это делать сами, они выставляют более широкие данные о своей работе, иногда используя для этого какую-нибудь i2c, и тут уже дело техники считать этот статус и вывести его при необходимости на экран.


  1. ionicman
    29.11.2021 09:49

    Мне кажется, что достаточно ввести коэффициент колебания, чтобы устранить все недостатки.

    Как-то так:
    если ( снятыеПоказания — предыдущиеПоказания > коэффициента ) обновитьПоказанияНаЭкране, предыдущиеПоказания = снятые показания

    Все — т.е. если будет сильное падение — он его но покажет, тоже самое и с сильным увеличением и при этом дрожать не будет.

    Если коэффициент взять как 1% от ( максимальногоНапряжения — минимальное ), мне кажется это будет достаточно и для отсутствия дерганья и минимизации погрешности.


  1. fk01
    29.11.2021 09:54
    +1

    Вы сделали измеритель внутреннего сопротивления батареи в итоге. Т.к. напряжение условно идеального химического элемента от "заряда" практически не зависит (что литий-ионная батарея и демонстрирует). Как внутреннее сопротивление зависит от степени заряда батареи ещё тот вопрос, может быть как-то отдалённо-пропорционально, но оно так же зависит и от температуры, и от износа батареи, и ток потребляемый вашим прибором далеко не константа. В итоге получился -- показометр. Что-то показывает, но на его показания положиться нельзя. В лучшем случае такой показометр может показывать один, два, три или четыре кубика заряда батареи. Но не сколько-нибудь точный процент.

    Так никто не делает. Обычно используют специальный контроллер батареи, который имеет т.н. coulomb meter. Идея в том, что он измеряет и интегрирует ток протекающий через шунт. Фактически "считает кулоны". И знает, сколько кулонов способна отдать полностью заряженная батарея, и таким образом расчитывает процент для показа на экране, оставшееся время работы прибора и т.п.

    Можно обойтись и без специального контроллера, но суть та же. Нужно усилить напряжение падения на шунте и проинтегрировать. Можно это всё сделать на контроллере и дискретных компонентах, в частности нужен операционный усилитель (т.к. на шунте будут падать милливольты, а их нужно привести к диапазону АЦП в ~3 вольта). Шунт удобней включать в разрыв "земли", иначе нужен rail to rail ОУ. Есть кстати специализированные измерители тока в виде микросхем (high/low side current sense amplifier). Можно самостоятельно на токовом зеркале сделать (но нужны специальные/подобранные транзисторные пары, в виде микросхемы, а не дискретные транзисторы). Проблемой будет вписаться с очень широким динамическим диапазоном потребляемых прибором токов в узкий диапазон 12-битного АЦП. Почему лучше аналоговый интегратор или попросту специальный контроллер батареи (может оказаться проще и дешевле).


    1. jar_ohty Автор
      29.11.2021 10:04
      +2

      Вы впадаете в очень распространенное заблуждение, будто бы при разрядке гальванического элемента или аккумулятора ЭДС постоянна, а растет внутреннее сопротивление. Я не знаю, кто первым это сказал, но в той или иной форме эта мысль активно циркулировала по советской литературе. На самом деле, только в очень редких случаях это верно. Примером такого редкого случая является нормальный элемент Вестона, с некоторой натяжкой — ртутно-цинковый элемент и литий-металлический. В литий-ионном аккумуляторе в процессе заряда и разряда меняются концентрации лития в активной массе электродов, и в соответствии с уравнением Нернста меняется и ЭДС. Так что внутреннее сопротивление тут ни при чем, если мы измерим напряжение на разомкнутом литий-ионном аккумуляторе с помощью электрометрического вольтметра, оно точно так же будет зависеть от степени разряженности.
      Я как раз и писал в самом начале статьи, что «ваш» путь идеальный. Но там же я и написал, почему отказываюсь от него. Потому что либо монитор батареи будет едва ли не сложнее основного устройства, или придется добывать этот самый специальный контроллер, покупать под него программатор и программу для его зашивки и так далее.
      А с аналоговым интегратором — это как? Вы сделаете на доступных компонентах аналоговый интегратор, который сможет интегрировать на протяжении хотя бы недели? Что-то сомневаюсь…
      По поводу точности: а зачем она такая нужна в сущности? Мы не делаем измерительный прибор, нам нужно примерно знать, в каком состоянии находится аккумулятор. Описанный мной способ позволяет это делать, и делает это немного лучше, чем просто измерение напряжения в произвольные моменты времени с усреднением.


      1. VT100
        29.11.2021 13:18
        +1

        Я не знаю, кто первым это сказал, но в той или иной форме эта мысль активно циркулировала по советской литературе.… Так что внутреннее сопротивление тут ни при чем .....

        Вполне себе в современной циркулирует. Не вчитывался, но вот "Theory and Implementation of Impedance Track™ Battery Fuel-Gauging Algorithm in bq2750x Family":


        Summary of the Algorithm Operation
        The gas gauge algorithm uses three types of information to calculate remaining capacity (DataRAM.Remaining Capacity( )) and full-charge capacity (DataRAM.Full Charge Capacity( )).
        Chemical: depth of discharge (DOD) and total chemical capacity Qmax
        Electrical: internal battery resistance dependence on DOD
        External: load and temperature
        DataRAM.Full Charge Capacity( ) is defined as the amount of charge passed from a fully charged state until the voltage defined in DF.Terminate Voltage flash constant is reached at a given rate of discharge, after subtracting the reserve capacity (DF.Reserve Capacity).
        Note that DataRAM.Full Charge Capacity( ) depends on the rate of discharge and is lower at higher rates and low temperatures because the cell I*R drop causes the Terminate Voltage threshold to be reached earlier.

        А с аналоговым интегратором — это как? Вы сделаете на доступных компонентах аналоговый интегратор, который сможет интегрировать на протяжении хотя бы недели? Что-то сомневаюсь…

        Периодический сброс с переносом накопленного в цифру в помощь. Хотя, конечно, может быть проще и по другому.


        По поводу точности: а зачем она такая нужна в сущности? .....

        Пожалуй — да.


        1. jar_ohty Автор
          29.11.2021 13:32

          Я не отрицаю существования зависимости внутреннего сопротивления от уровня заряда. Она есть. Но изменение напряжения по мере разряда отнюдь не связано только с ней, более того - при токах на уровне 0,1C и ниже влияние внутреннего сопротивления ничтожно мало на всех этапах разряда.


        1. armature_current
          29.11.2021 13:44

          По поводу точности: а зачем она такая нужна в сущности? .....

          Правильный вопрос, с него и надо было начинать и заканчивать статью. Какую точность дают известные алгоритмы в зависимости от температуры, циклов заряда/разряда, технологических отклонений, типов полимеров, частотных свойств входного сопротивления нагрузки. На рынке есть много готовых решений в виде миниатюрных микросхем BMS, которые могут еще и посчитать уровень деградации (State of health).
          А если надо просто приблизительно знать с погрешностью +-50% - ну ок, авторский вариант наверно и подойдет


      1. fk01
        30.11.2021 01:33

        Цитирую: "будто бы при разрядке гальванического элемента или аккумулятора ЭДС постоянна, а растет внутреннее сопротивление" -- ну так в этом не сложно убедиться глядя на график разряда любой литиевой батарейки или аккумулятора. Почти плоская прямая до самого разряда, а дальше напряжение резко падает. Там ловить нечего. Там скорей можно поймать температурные зависимости, когда например ток растет, батарея разогревается и начинает работать лучше...

        В том же уравнении Нернста, кстати, ЭДС прямо пропорционально температуре. Что однозначно намекает, что ЭДС в чистом виде оно не дает, и связь там какая-то не однозначная (потому, что при более-менее любой температуре на выходе заряженной литиевой батарейки будет ~порядка четырех вольт).

        "Монитор батареи будет едва ли не сложнее основного устройства, или придется добывать этот самый специальный контроллер, покупать под него программатор и программу для его зашивки и так далее..." -- не так. Контроллер батареи программировать специально не нужно, он управляется через I2C, "прошивки" не требует. Единственное что может останавливать -- цена. Для совсем бюджетных приборов. Но, боюсь, альтернативные решения сразу становятся хуже и едва ли дешевле. Там ОУ прецизионный нужен, который сходу не дешевле (иначе он там свои же ошибки так наинтегрирует, что и смысла уже никакого).

        "А с аналоговым интегратором — это как? Вы сделаете на доступных компонентах аналоговый интегратор, который сможет интегрировать на протяжении хотя бы недели?" -- зачем недели? Достаточно даже может быть долей секунд. Основная идея в том, чтоб периодически считываь сигнал с интегратора и сбрасывать его. Причём даже это слишком сложно, можно пойти другим путём: пусть интегратор сам сбрасывается по достижении фиксированного порога и выдаёт импульс на микроконтроллер. А тот только импульсы считает (аппаратным счётчиком...) Условно, 1 импульс -- сколько-то долей кулона. Вот и всё. А схема, грубо, примерно такая:

        <img src="http://www.seekic.com/uploadfile/ic-circuit/200971625042424.gif" />

        Ссылка.

        Конечно именно так делать -- нельзя. Должен быть источник опорного напряжения, должен быть компаратор (или их можно заменить на какой-то аналог TL431, правда он сам по себе потребляет многовато). Но суть именно такая.

        "По поводу точности: а зачем она такая нужна в сущности?" -- в случае с интегратором очень критично смещение (offset) операционника, т.к. оно домножается на период интегрирования и получается большая ошибка. И так же температурные колебания других параметров. Конечно МК может калиброваться (по известной ему нагрузке) и даже иметь какую-то калибровочную кривую связанную с температурой, для исключения ошибки добавляемой ОУ... Также критична ёмкость конденсатора (период прямо пропорционален ёмкости). У керамических конденсаторов ёмкость начинает зависеть от напряжения -- не угадаешь. Нужно скорей использовать пленочные прецизионные конденсаторы. Резисторы тоже нужны прецизионные. Достаточно муторно всё становится, и заменяется одной микросхемой.

        "Мы не делаем измерительный прибор, нам нужно примерно знать, в каком состоянии находится аккумулятор." -- вот я и говорю, метод хорош для "показометра" из кубиков. Если нужно что-то серьёзней, то нужно считать кулоны. А показометр однозначно будет плохо работать на морозе, например. И по нему сложно сказать аккумулятор деградировал и его пора менять, или что-то ещё.

        Кстати о заблуждениях. Есть известное заблуждение, мол "на морозе ёмкость падает". А куда спрашивается девается энергия закачанная в аккумулятор? Разумеется ничего никуда не падает, а растёт внутреннее сопротивление аккумулятора. А количество энергии в нём остаётся то же самое. И его даже можно примерно оценить подключая известную нагрузку и измеряя падение напряжения. Но в случае измерения кулонов цепь -- последовательная, и все свои кулоны аккумулятор должен выдать, хотя наверное только если с куда меньшими токами...


  1. vsemenov141
    09.12.2021 13:45

    Измерять Кулоны - это круто... А надо ли ? У нас на работе десятки Серверов как раз с "интеллектуальными" UPS, считающими Кулоны. И это довольно нудно,- перед запуском в эксплуатацию каждый UPS надо калибровать, т.е. вставить новые аккумуляторы, полностью зарядить их, потом полностью разрядить, затем опять полностью зарядить батарею. На это уходит часа 3, а в это время контроллер UPS считает Кулоны. После замены аккумуляторов - опять калибровка. А Сервер простаивает, а должен работать 24/7. Играться с Кулонами некогда. Поэтому, гораздо практичнее иметь простую индикацию заряда батареи. Тем более, что в процессе эксплуатации емкость падает и НЕ контролируется. А программы отключения по снижению питания срабатывают по Напряжению, а не по оставшейся Емкости. Такая же история с любыми батареями,- от автомобилей до смартфонов. Измерять Кулоны - это круто. Можно похвалиться перед пацанами. А оно Вам надо ? (Как говорят в Одессе).


    1. JerleShannara
      09.12.2021 17:12

      А чем это отличается от калибровки обычного свинцового ИБП после замены батарей? Тоже самое выходит — выкинул старые, установил новые, дал сутки зарядиться, запустил калибровку. Оборудование при этом спокойно висит на этом самом ИБП и не жужжыт, хотя если не повезёт и тётя света закончится сразу после калибровки, то будет плохо.