Buck-boost преобразователь с цифровым управлением на STM32F334 в режиме CC/CV

Наиболее популярные топологии dc/dc преобразователей buck и boost имеют существенное ограничение: топология buck может лишь понижать входное напряжение, а топология boost только повышает его. Однако бывают задачи, когда диапазон входного напряжения требует одновременно работы и на повышение и на понижение, например, мы имеем вход 3…15В, а на выходе необходимо получить стабилизированные 12В. Знакомая ситуация?

Тут возможны 2 решения:


  • С помощью преобразователя boost повысить входное напряжение из 3…15В до стабильных 15В на выходе, а затем уже с помощью топологии buck понизить напряжение до требуемых 12В;
  • Применить топологию buck-boost, которая позволяет оптимально решить данную задачу.

Очевидным минусом первого способа является необходимость применять 2 дросселя, увеличенное количество конденсаторов и не самый оптимальный режим работы, а значит более низкий КПД. Buck-boost топология лишена данных недостатков, поэтому сегодня рассказ пойдет о ней. Чтобы было интересно, я решил не брать какой-то готовый контроллер и реализовал dc/dc преобразователь с цифровым управлением на базе STM32F334C8T6.

Фото преобразователя


Результат работы для тех, кто не хочет читать стену текста

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


1. Кратко о работе топологии buck-boost

Для реализации данной топологии понадобится: 2 транзистора и 2 диода, но т.к. я буду рассказывать о синхронной версии топологии, то потребуется 2 полумоста (4 транзистора). Так же потребуется один силовой дроссель, что выгодно отличает топологию. В итоге структурная схема топологии выглядит следующим образом:

Структура buck-boost

Тут же стоит сказать, что для реализации полноценного преобразователя с различными защитами и алгоритмами от микроконтроллера понадобится: 2 независимых канала ШИМ сигнала (или 2 комплементарные пары, зависит от драйвера), 4 канала АЦП для измерения входного и выходного напряжения, а так же входного и выходного токов. В идеале еще необходим 1 канал АЦП для измерения температуры преобразователя и 2 компаратора для защиты по току (OCP) на входе и выходе. Почему 2 компаратора? Защита от КЗ по входному шунту призвана защитить сам преобразователь, а защита от КЗ по выходному шунту уже защищает нагрузку, подключенную к преобразователю.

Стоит отметить, что в данной задаче выгодно использовать специализированные микроконтроллеры, например, STM32F334, STM32G474, XMC4108, TMS320F28027 и подобные, т.к. внутри них уже содержится все необходимое: HRPWM, АЦП, ЦАП, компараторы и операционные усилители. Этого достаточно, чтобы реализовать с помощью МК не только цифровое управление, но и весь аналоговый фронтенд, что значительно уменьшает стоимость устройства и его габариты. Да и подобные контроллеры позволяют создать очень гибкое решение, где все параметры, например, порог защиты по току (OCP) или усиление ОУ, можно изменить со стороны софта, а не перепаиванием резисторов.

Теперь вернемся к структурной схеме… Давайте попытаемся мысленно разбить buck-boost топологию на две отдельные составляющие и изобразим эквивалентную схему:

Эквивалентная схема buck-boost

На схеме нарисовал отдельно buck (слева) и boost (справа), у каждого свой дроссель и вот тут самое интересное — конденсатор С3 является выходной емкостью buck-а, а L1C3 образуют LC-фильтр. При этом С3 так же является входной емкостью boost-а, то есть питается с него. Из этого следует, что выходное напряжение buck-а, является входным напряжение для boost-а. Это позволит легким движением руки вывести формулу по которой собственно будет осуществляться управление для стабилизации выходного напряжения.

Вспомним формулу для топологии buck:

$V_{out} = V_{in} * D$

Теперь формулу для топологии boost:

$V_{out} = \frac{V_{in}} {(1 - D)}$

Теперь исходя из того, что выходное напряжения buck-преобразователя является входных для boost подставим первую формулу во вторую и получим:

$V_{out-boost} = \frac{V_{in-buck} * D_{buck}} {1 - D_{boost}}$

Как видим напряжение на выходе buck-boost преобразователя зависит от 3-х составляющих: входного напряжения и коэффициентов заполнения на двух полумостах (на структурной схеме выше это PWM-BUCK и PWM-BOOST). Входное напряжение нам не подвластно, да и вообще задача преобразователя поддерживать на выходе заданное напряжение при изменение входного напряжения.

Необходимо проверить формулу и убедиться, что она работает. Проведем небольшой эксперимент, на вход преобразователя подадим 8В, значение Dboost выставим 70%, а значение для Dbuck пусть будет 50%. В итоге на выходе мы должны получить напряжение равное:

$V_{out-boost} = \frac{V_{in-buck} * D_{buck}} {1 - D_{boost}} = \frac{8V * 0,5} {1 - 0,7} = 13,33V$

Теперь подключим преобразователь к лабораторному блоку питания, выставим 8В и в коде зададим соответствующие коэффициенты заполнения. Включаем, ставим щупы на вход, выход, на 2 ШИМ-сигнала и проверяем на соответствие с теорией:

Диаграмма с осциллографа

Как видно практика соответствует теории и теперь можно двигаться дальше. Исходя из формулы для управления выходным напряжением нам необходимо изменять заполнение (duty) 2-х ШИМ сигналов. При стабилизации выходного напряжения необходимо будет вычислять значения duty, чтобы реагировать на новое значение входного напряжения.


2. Реализация управления. Общая концепция

Для начала давайте рассмотрим 2 простых режима работы преобразователя, а именно: стабилизация тока и стабилизация напряжения. Оба алгоритма реализую независимо друг от друга, чтобы понять принцип работы, а затем уже объединим их в полноценный CC/CV алгоритм и протестируем на реальной нагрузке. Схема управления для обоих режимов будет выглядеть следующим образом:

Структурная схема управления

И для CC и для CV принцип управления одинаковый, единственное мы будет поддерживать разные параметры: в случае контроля напряжения на REF устанавливаем значение, которое нужно удерживать на выходе, а АЦП измеряет выходное напряжение; в случае, когда нужно контролировать ток, референсным является значение тока, который источник должен удерживать на выходе и измеряем соответственно уже не напряжение, а ток на выходе.

Я не хочу углубляться в описание работы ПИД-регулятора, благо информации о его работе много и на хабре и на других ресурсах, лишь кратко «на пальцах» объясню затею. У нас есть «образцовый» сигнал (REF), который задает значение параметра, который нужно получить на выходе системы, пусть это будет 10В. У нас есть инвертор, напряжение на выходе которого для buck-boost измеряется по формуле, описанной ранее и зависящее от заполнения (duty). Изменяя заполнение ШИМ-сигнала на силовых транзисторах мы влияем на систему. Так же необходимо ввести обратную связь, измеряя напряжение на выходе с помощью АЦП.

Зная величину текущего напряжения на выходе мы можем вычислить ошибку:

$error = V_{reference} - V_{out}$


Что это нам дает? А мы теперь знаем насколько сильно отклонилось реальное значение напряжения на выходе от заданной величины (REF). Это позволяет нам понять необходимо ли «дать команду» на увеличение или на уменьшение напряжения. Например, в топологии buck для увеличения напряжения нужно увеличить коэффициент заполнения (duty) для верхнего транзистора, а для уменьшения напряжения соответственно уменьшить заполнение. Для топологии boost наоборот, а для buck-boost имеется 2 сигнала и тут уже сложнее — нужно балансировать, но думаю в среднем задумка понятна, для наглядности приведу псевдо-код для управления buck:

// Текущее значение коэффициента заполнения для ШИМ-сигнала
uint16_t dutyPWM = 0;

// Задаем значение, которое нужно поддерживать на выходе
const float referenceOutputVoltage = 10.0f;

// Настраиваем прерывание по таймеру, например, на 1 кГц
void sTim3::handler (void) {
    float outputVoltage = GetOutputVoltage();
    if (outputVoltage > referenceOutputVoltage) {
        dutyPWM--;
    } else {
        dutyPWM++;
    }
}

Как видно простейшее управление сводится к измерению напряжения на выходе и сравнению с заданным значением, далее на основе результата заполнение ШИМа или увеличивается или уменьшается. По своей сути это реализация логики простого П-регулятора (это ПИД только с пропорциональной составляющей), так давайте же теперь сделаем тоже самое, но по-человечески, то есть напишем простой П-регулятор:

// Текущее значение коэффициента заполнения для ШИМ-сигнала
uint16_t dutyPWM = 0;

// Задаем значение, которое нужно поддерживать на выходе
const float referenceOutputVoltage = 10.0f;

// Устанавливаем значение пропорционального коэффициента
const float Kp = 1.0f;

// Настраиваем прерывание по таймеру, например, на 1 кГц
void sTim3::handler (void) {
    float outputVoltage = GetOutputVoltage();
    float error = referenceOutputVoltage - outputVoltage;
    dutyPWM += Kp * error;
}

Собственно что тут происходит… Мы вычисляем ошибку и если реальное измеренное напряжение на выходе будет больше заданного, то ошибка примет отрицательное значение, а дальше мы ее суммируем к текущему значению заполнения ШИМ (dutyPWM) и в случае топологии buck это приведет к уменьшению заполнения и, следовательно, к уменьшению напряжения на выходе. Если же измеренное напряжение окажется меньше, чем заданное (reference), то ошибка примет положительное значение и при суммировании с dutyPWM это приведет к увеличению заполнения ШИМ сигнала на затворе реального транзистора и тем самым увеличит выходное напряжение.

Теперь обратим внимание на то, что с частотой 1 кГц у нас генерируется прерывание по таймеру и вызывается обработчик прерывания, где и происходят все вычисления. Собственно каждую миллисекунду будет происходить вычисление нового значения error и будет записываться новое значение заполнения ШИМ-а. Обратите внимание, что dutyPWM по сути является аккумулятором и мы прибавляем новое значение к старому.

Для лучшего осознания представим себе buck dc/dc преобразователь, которому на вход подают 20В. Теперь мы включаем устройство и в начальный момент времени dutyPWM равно 0, а его максимальное значение 1000, что по формуле для buck дает Vout = Vin x dutyPWM = 20V x 0 = 0V, то есть в начальный момент времени у нас на выходе 0В. При следующем вызове прерывания (шаг №1) мы получим error = 10 — 0 = 10 и значение ШИМа на выходе микроконтроллера станет dutyPWM = 10, а напряжение соответственно Vout = Vin x dutyPWM = 20V x (10/1000) = 0.2V. Теперь через 1 мс у нас прерывание вызовет еще раз обработчик (шаг №2) и произойдет вычисление нового значения error = 10 — 0.2V = 9.8V, соответственно dutyPWM = 19.8, это приведет опять к увеличению напряжения на выходе и его реальное значение приблизится к заданному (reference). Так с каждым шагом мы будем приближаться к заданному значению, пока напряжение на выходе не станет равным reference или 10В (на самом деле оно лишь приблизится к нему).

Теперь необходимо сказать собственно о коэффициенте Kp, дело в том, что настройка системы управления в основном сводится к подбору коэффициентов регулирования. В своем примере я специально задал его равным 1, чтобы он ни на что не влиял. Как видно из расчетов выше, самый большой шаг регулирования будет равен 0.2В и затем на каждой последующей итерации он будет уменьшаться. В данном случае система будет довольно долго достигать (приближаться) заданного значения, но нам то нужно, чтобы система работало максимально быстро и реагировала на изменения. Давайте попробуем увеличит значение Kp до 10 и тогда после первой итерации получим: dutyPWM = Kp x error = 10 x (10 — 0) = 100, то есть за один шаг получим прирост не 0.2В, а Vout = Vin x dutyPWM = 20V x (100/1000) = 2V, то есть «цена шага» стала в 10 раз больше. Увеличение Kp позволит системе гораздо быстрее довести напряжение на выходе до заданного. Однако у этого есть и побочный эффект, если коэффициенту задать слишком большое значение, то система начнет колебаться и о стабильности стоит забыть, но подбор коэффициентов это отдельная крайне обширная и сложная тема, про нее читайте дополнительно.

И так… стало понятно? Вроде написал простыми словами, чтобы каждый смог разобраться в общей концепции управления.


3. Реализация управления CV mode

Теперь перейдем к практической реализации CV mode, то есть к стабилизации выходного напряжения. Тут применим полноценный регулятор (настраивать буду только пропорциональную часть), его реализацию можно посмотреть, например, в моем проекте: Реализация ПИД-регулятора на С++. Данная реализация взята из матлаба, имеет защиту от насыщения. Реализация автора не является истиной в последней инстанции, возможно вы захотите реализовать свой вариант или применить стороннее решение.

В своем проекте используется микроконтроллер STM32F334C8T6, из периферии понадобится АЦП, HRPWM и таймер для генерации прерывания. Тут могут сильно возразить, что вычисление в прерывании вести не надо, плохая практика и в таком духе. Однако у меня всего одно прерывание и мешать ему никто не будет, хотя если у вас будет больше (2–3–4) прерываний, то все тоже будет работать, главное не забывайте установить максимальный приоритет для прерывания с системой управления.

Собственно сама реализация CV режима выглядит вот так:

/***********************************************************
*   Частота работы преобразователя около 200 кГц,
*   период HRPWM для данной частоты - 30 000.
*   Напряжение на входе: 3...15В
*   Номинальная мощность: 20 Вт
*
*   Заданное значение на выходе: 12В
***********************************************************/

void sTim3::handler (void) {
    TIM3->SR &= ~TIM_SR_UIF;

    float inputVoltage = Feedback::GetInputVoltage();

    // Режим работы boost, когда Vin < 90% * Vref
    if (inputVoltage <= (Application::referenceOutputVoltage * 0.9)) {
        Hrpwm::SetDuty(Hrpwm::Channel::buck, 29000);
        float outputVoltage = Feedback::GetOutputVoltage();

        pidVoltageMode
            .SetReference(Application::referenceOutputVoltage)
            .SetSaturation(-29800, 29800)
            .SetFeedback(outputVoltage, 0.001)
            .SetCoefficient(10,0,0,0,0)
            .Compute();

        Application::dutyBoost += pidVoltageMode.Get()
        Hrpwm::SetDuty(Hrpwm::Channel::boost, Application::dutyBoost);
    }

    // Режим работы buck, когда Vin > Vref * 110%
    if (inputVoltage >= (Application::referenceOutputVoltage * 1.1)) {
        Hrpwm::SetDuty(Hrpwm::Channel::boost, 1000);
        float outputVoltage = Feedback::GetOutputVoltage();

        pidVoltageMode
            .SetReference(Application::referenceOutputVoltage)
            .SetSaturation(-29800, 29800)
            .SetFeedback(outputVoltage, 0.001)
            .SetCoefficient(10,0,0,0,0)
            .Compute();

        Application::dutyBuck += pidVoltageMode.Get()
        Hrpwm::SetDuty(Hrpwm::Channel::buck, Application::dutyBuck);
    }

    // Режим работы buck-boost, когда (90% * Vref) < Vin < (110% * Vref)
    if ((inputVoltage > (Application::referenceOutputVoltage * 0.9)) && (inputVoltage < (Application::referenceOutputVoltage * 1.1))) {
        Hrpwm::SetDuty(Hrpwm::Channel::boost, 6000);
        float outputVoltage = Feedback::GetOutputVoltage();

        pidVoltageMode
            .SetReference(Application::referenceOutputVoltage)
            .SetSaturation(-29800, 29800)
            .SetFeedback(outputVoltage, 0.001)
            .SetCoefficient(10,0,0,0,0)
            .Compute();

        Application::dutyBuck += pidVoltageMode.Get()
        Hrpwm::SetDuty(Hrpwm::Channel::buck, Application::dutyBuck);
    }    
}

Теперь по порядку… используется плата где аппаратно реализован преобразователь buck-boost, то есть задача усложняется и нужно управлять не одним коэффициентом заполнения, а сразу 2-мя для buck и для boost частей. В принципе решить задачу можно 2-мя способами: оптимальным и простым. Простой, более понятный спрячу в спойлер, желающие смогут ознакомиться, а рассказ пойдет об оптимальном варианте.


Простой алгоритм управления
/***********************************************************
*   Частота работы преобразователя около 200 кГц,
*   период HRPWM для данной частоты - 30 000.
*   Напряжение на входе: 3...15В
*   Номинальная мощность: 20 Вт
*
*   Заданное значение на выходе: 12В
***********************************************************/

void sTim3::handler (void) {
    TIM3->SR &= ~TIM_SR_UIF;

    // Вычисляем входное напряжение и выставляем оптимальное заполнение для части boost
    float inputVoltage = Feedback::GetInputVoltage();
    if (inputVoltage < 6.0f) { Application::dutyBoost = 25000; }
    if ((inputVoltage >= 6.0f) && (inputVoltage < 12.0f)) { Application::dutyBoost = 18000; }
    if (inputVoltage >= 12.0f) { Application::dutyBoost = 6000; }
    Hrpwm::SetDuty(Hrpwm::Channel::boost, Application::dutyBoost);

    // Регулируем выходное напряжение за счет управления заполнением на buck части
    float outputVoltage = Feedback::GetOutputVoltage();

    pidVoltageMode
        .SetReference(Application::referenceOutputVoltage)
        .SetSaturation(-29800, 29800)
        .SetFeedback(outputVoltage, 0.001)
        .SetCoefficient(10,0,0,0,0)
        .Compute();

    Application::dutyBuck += pidVoltageMode.Get();
    Hrpwm::SetDuty(Hrpwm::Channel::buck, Application::dutyBuck);    
}

В простом варианте я решил разбить весь диапазон входного напряжения на 3 части: 3…6В, 6…12В и 12…15В и для каждого диапазона задать свой фиксированный коэффициент для boost полумоста, а регулировать только buck. Такое решение позволило упростить понимание и алгоритм управления, но плата за это — не всегда ток в дросселе оптимален, а значит и КПД преобразователя ниже. Когда мощность небольшая и такой компромисс может устроить, если же речь идет о преобразователях большой мощности (сотни Ватт и далее), то уже придется все таки реализовывать полноценное управление.

В моем случае достаточным оказалось 3 диапазона, чтобы получить требуемый КПД, но чем диапазонов будет больше, тем оптимальнее будет режим работы преобразователя, однако увлекаться однозначно не стоит. Все три значения dutyBoost считаются одинаково: я ранее писал, что выход buck полумоста является входом для boost-а, максимальное заполнение пусть будет 90% (больше можно). Суть моего преобразователя такова, что входное напряжение имеет диапазон 3…15В и выходное тоже с таким же диапазоном. Для вычисления dutyBoost будем рассматривать граничные значения — вход 3В и выход 15В, это самый суровый режим работы преобразователя, т.к. в данном случае будет максимальный ток протекать через дроссель. При максимальном заполнение dutyBuck 90% и входе 3В на «вход» boost-а попадет лишь 3В x 0,9 = 2,7В, то есть наш boost должен обеспечивать 15В при входе 2.7В! Для этого ему потребуется установить заполнение dutyBoost на 1 — (Vout / Vin) = 1 — 2,7 / 15 = 82%, учитывая, что период таймера ШИМ-а равен 30000, то коэффициент должен быть равен 30 000×82% = 24 600, округлим в сторону запаса и получим 25000.

Если напряжение станет не 3В, а вырастет до 5В, то система управления уменьшит его с помощью buck полумоста обратно расчетных 3В. Конечно, оптимальнее было бы при росте напряжения плавно уменьшить dutyBoost, оставив buck на 90% пока мы не приблизились к заданному значению в 12В. Кстати, в процессе тестов я реализовал оба варианта управления и за счет неоптимального управления проиграл всего лишь ~3,2% КПД. Много это или мало? В большинстве случаев не критично, у меня же питается преобразователь от аккумулятора и эта пара процентов для меня важна, поэтому пришлось реализовывать «оптимальный» вариант.

Исходя из расчетов для первого интервала получим значение для второго на 6…12В равное 60% или 18000, а так же для третьего на 12…15В равное 20% или 6000. Как-то так…

Как видно из кода контроллер может работать в 3-х режимах в зависимости от входного напряжения. Если напряжение на входе ниже, чем заданное напряжение стабилизации, то выставляем на buck полумосте заполнение максимально близкое к 100% и регулируем boost-полумост, получая тем самым классический boost dc/dc преобразователь. Если же напряжение на входе выше, чем заданное напряжение стабилизации, то наоборот — выставляет заполнение на boost-полумосту близкое к 0% и регулируем выход с помощью buckDuty, получая классический buck dc/dc преобразователь. Ну и самый интересный вариант — когда напряжение на входе примерно равно напряжению заданному для стабилизации выхода, то нормально ни buck, ни boost работать не могут и вот тут включается режим buck-boost…

В данном режиме необходимо для boost полумоста задать такое заполнение, чтобы он мог «вывозить» подъем напряжения от 90% заданного напряжения, то есть от Vref x 90% = 12В x 0,9 = 10,8В. Для этого нужно задать постоянное заполнение dutyBoost = 1 — (Vref x 90%) / (Vref x 110%) = 1 — 0,9 / 1,1 = 19% = 5700, округлим до 6000 для запаса. Регулирование же выхода внутри данного диапазона происходит за счет управления buck-полумостом через buckDuty. В отличии от «простого» способа регулирования режим buck-boost активирован не постоянно и работает лишь в пограничной зоне двух базовых режимов, при чем константное заполнение в столь небольшом диапазоне является предельно оптимальным. В итоге получаем стабилизацию напряжения во всем диапазоне входных напряжений, что можно увидеть в следующем небольшом видео:



4. Реализация управления CC mode

Теперь давайте реализуем не стабилизацию напряжения, а стабилизацию тока на выходе. Это пригодится нам, например, для заряда аккумуляторов, питания LED, какой-нибудь гальваники и прочих нагрузок, требующих источник тока. Стоит понимать, что в dc/dc преобразователе мы можем изменять только напряжение, т.к. оно напрямую зависит от заполнению (duty), как же перейти к управлению током?

Как обычно все очень просто — нам потребуется закон Ома в виде: U = I x R. Представим, что у нас идеальная активная нагрузка, например, резистор сопротивлением 10 Ом. Подадим на него напряжение с нашего dc/dc равное 10В и получим, что ток через него будет протекать I = U / R = 1А, при чем в процессе работы ток потребления практически не будет изменяться, т.к. напряжение константа и сопротивление тоже. Теперь изменим нагрузку на Li-ion аккумулятор, в таком случае сопротивление в нашей формуле перестанет быть постоянным и мы получим ситуацию, когда в разряженном состоянии АКБ при зарядке можно потреблять 15А, где-то по середине процесса 5А, а по мере приближения к полному заряду ток вообще будет стремиться к нулю. Слишком большой ток в момент начала заряда выведет АКБ из строя, поэтому нам нужно его ограничить и стабилизировать, получаем что I = U / R = const.

Мы знаем, что сопротивление изменяется и мы на него повлиять не можем, однако в формуле есть напряжение, которое мы можем регулировать в нашем преобразователе. То есть, задача регулирования будет сводиться к тому, чтобы при изменение сопротивления нагрузки реагировать на это и изменять напряжение так, чтобы ток оставался постоянным и соблюдалась пропорция I = U / R = const.

Например, заряжать АКБ будем током 1А. В момент начала заряда сопротивление АКБ равно 1 Ом и чтобы обеспечить заданный ток нужно задать на выход напряжение 1В: I = 1А = const = U / R = 1В / 1 Ом. Через какое время АКБ немного зарядился и сопротивление стало 5 Ом, чтобы обеспечить постоянный ток нужно повысить напряжение до 5В: I = 1А = const = U / R = 5В / 5 Ом. Если мы напряжение не увеличим до 5В, то при предыдущей уставке в 1В ток был бы всего 0.2А и пропорция бы не соблюдалась.

Из всего выше написанного следует, что нам необходимо измерять ток с помощью АЦП, затем вычислять ошибку (error) и в зависимости от ее знака и значения реагировать на систему, изменять значение заполнения ШИМ сигнала (dutyPWM). Давайте реализуем такое управление:


/***********************************************************
*   Частота работы преобразователя около 200 кГц,
*   период HRPWM для данной частоты - 30 000.
*   Напряжение на входе: 3...15В
*   Номинальная мощность: 20 Вт
*
*   Заданное значение на выходе: 1А
***********************************************************/

void sTim3::handler (void) {
    TIM3->SR &= ~TIM_SR_UIF;

    float inputVoltage = Feedback::GetInputVoltage();

    // Режим работы boost, когда Vin < 90% * Vref
    if (inputVoltage <= (Application::referenceOutputVoltage * 0.9)) {
        Hrpwm::SetDuty(Hrpwm::Channel::buck, 29000);
        float outputCurrent = Feedback::GetOutputCurrent();

        pidCurrentMode
            .SetReference(Application::referenceOutputCurrent)
            .SetSaturation(-29800, 29800)
            .SetFeedback(outputCurrent, 0.001)
            .SetCoefficient(10,0,0,0,0)
            .Compute();

        Application::dutyBoost += pidCurrentMode.Get()
        Hrpwm::SetDuty(Hrpwm::Channel::boost, Application::dutyBoost);
    }

    // Режим работы buck, когда Vin > Vref * 110%
    if (inputVoltage >= (Application::referenceOutputVoltage * 1.1)) {
        Hrpwm::SetDuty(Hrpwm::Channel::boost, 1000);
        float outputCurrent = Feedback::GetOutputCurrent();

        pidCurrentMode
            .SetReference(Application::referenceOutputCurrent)
            .SetSaturation(-29800, 29800)
            .SetFeedback(outputCurrent, 0.001)
            .SetCoefficient(10,0,0,0,0)
            .Compute();

        Application::dutyBuck += pidCurrentMode.Get()
        Hrpwm::SetDuty(Hrpwm::Channel::buck, Application::dutyBuck);
    }

    // Режим работы buck-boost, когда (90% * Vref) < Vin < (110% * Vref)
    if ((inputVoltage > (Application::referenceOutputVoltage * 0.9)) && (inputVoltage < (Application::referenceOutputVoltage * 1.1))) {
        Hrpwm::SetDuty(Hrpwm::Channel::boost, 6000);
        float outputCurrent = Feedback::GetOutputCurrent();

         pidCurrentMode
            .SetReference(Application::referenceOutputCurrent)
            .SetSaturation(-29800, 29800)
            .SetFeedback(outputCurrent, 0.001)
            .SetCoefficient(10,0,0,0,0)
            .Compute();

        Application::dutyBuck += pidCurrentMode.Get()
        Hrpwm::SetDuty(Hrpwm::Channel::buck, Application::dutyBuck);
    }    
}

Как видно из кода нам по прежнему нужно знать входное напряжение, чтобы определить режим работы buck-boost преобразователя. Однако теперь для стабилизации тока не нужно знать напряжение на выходе, поэтому измеряем выходной ток (outputCurrent) и внутрь ПИД-регулятора в качестве заданного значения (referenceOutputCurrent) теперь подаем значение тока, который хотим получить на выходе, а на сигнал фидбека подаем значение выходного тока. В итоге вычисление ошибки для данного случая будет выглядеть так:

$error = I_{reference} - I_{out}$

Далее логика работы абсолютно такая же, как в разделе про CV mode. По сути П-регулятор вообще не знает что именно он регулирует, ему нужно лишь знать 2 значения: заданное и реально измеренное, то есть в ваших задачах это может быть и частота, и давление и что угодно вообще, но при этом логика работы не изменится.

Для демонстрации работы я взял две LED матрицы на 10 Вт каждая и с номинальным током 1А, результат работы можно посмотреть на небольшом видео:



5. Реализация управления CC/CV mode

Мы научились стабилизировать напряжение и ток по отдельности, теперь же нужно победить следующую проблему… Представим два варианта развития событий:


  • Преобразователь работает в режиме стабилизации напряжения 10В и к нему подключается разряженный Li-ion аккумулятор, при таком напряжение АКБ может сожрать десятки ампер, ему очень быстро по плохеет и он скорее всего загорится. Вообще в таком случае, согласно закону Ома, ток будет ограничен лишь сопротивлением нагрузки (или еще мощностью источника энергии от которого питается преобразователь) и в идеале будет стремиться к бесконечности, пока не сработает защиты по току или преобразователь не сгорит.
  • Преобразователь работает в режиме стабилизации тока с порогом 1А, к нему УЖЕ подключили разряженный Li-ion аккумулятор и зарядка спокойно идет, АКБ очень рад зарядке маленьким стабильным током. Дождались пока АКБ зарядится и отключаем его от преобразователя… Что с ним будет? Преобразователь, в попытка удержать ток, будет увеличивать напряжение на своем выходе, а так как сопротивление после отключения АКБ стало примерно бесконечным большим, то и преобразователь будет стремиться увеличить напряжение на выходе до бесконечности, но это у него не получится — конденсаторы на выходе по взрываются сильно раньше.

Оба варианта как-то не очень, не так ли? Чтобы их избежать применяют алгоритм CC/CV, то есть одновременно стабилизируя и ток и напряжение. Объясню логику на том же АКБ: есть у нас 1 элемент Li-ion на 4.2В в разряженном состоянии и есть преобразователь, который умеет работать в режиме CC/CV с выходом 3…15В и током 1А. Мы измеряем на выходе и ток и напряжение, в момент 1 у нас АКБ не подключен, нагрузка стремится к бесконечности и вот в обычном CC режиме у нас напряжение улетало бы сильно вверх, но мы же еще и напряжение измеряем в это время. Поэтому, если в попытках удержать ток преобразователь достиг предела 15В, то в этот момент он сам переходит в режим CV, то есть удерживает заданное напряжение на уровне 15В (или любое другое).

Наступает момент 2 и мы подключаем АКБ к преобразователю, т.к. АКБ разряжен, то ток начинает расти и при 15В он был бы огромный, но в этот момент измеряется ток, как только достигнуто значение 1А — преобразователь начинает уменьшать напряжение и уже стабилизирует ток, то есть переходит в режим СС. Затем наступает момент 3, АКБ заряжен, мы его отключаем, сопротивление нагрузки опять стремится к бесконечности и напряжение начинает расти и преобразователь переходит в CV режим, то есть стабилизирует напряжение. Надеюсь мое описание вам понятно и теперь рассмотрим код, который обеспечивает данный алгоритм:


/***********************************************************
*   Частота работы преобразователя около 200 кГц,
*   период HRPWM для данной частоты - 30 000.
*   Напряжение на входе: 3...15В
*   Номинальная мощность: 20 Вт
*
*   Заданное значение на выходе: 10В 1А
***********************************************************/

void sTim3::handler (void) {
    TIM3->SR &= ~TIM_SR_UIF;

    float resultPID = 0.0f;

    float inputVoltage = Feedback::GetInputVoltage();

    // Режим работы boost, когда Vin < 90% * Vref
    if (inputVoltage <= (Application::referenceOutputVoltage * 0.9)) {
        Hrpwm::SetDuty(Hrpwm::Channel::buck, 29000);

        float outputVoltage = Feedback::GetOutputVoltage();
        float outputCurrent = Feedback::GetOutputCurrent();

        // Работаем в CC mode, когда Vout < Vref
        if (outputVoltage < (Application::referenceOutputVoltage - 0.2f)) {
            pidCurrentMode
                .SetReference(Application::referenceOutputCurrent)
                .SetSaturation(-29800, 29800)
                .SetFeedback(outputCurrent, 0.001)
                .SetCoefficient(20,0,0,0,0)
                .Compute();
            resultPID = pidCurrentMode.Get();
        }

        // Работаем в CV mode, когда (Iout -> 0) ИЛИ (Vout => Vref)
        if ((outputCurrent < 0.05f) || (outputVoltage >= (Application::referenceOutputVoltage - 0.2f))) {
            pidVoltageMode
                .SetReference(Application::referenceOutputVoltage)
                .SetSaturation(-29800, 29800)
                .SetFeedback(outputVoltage, 0.001)
                .SetCoefficient(50,0,0,0,0)
                .Compute();
            resultPID = pidVoltageMode.Get();
        }

        Application::dutyBoost += resultPID;
        Hrpwm::SetDuty(Hrpwm::Channel::boost, Application::dutyBoost);
    }

    // Режим работы buck, когда Vin > Vref * 110%
    if (inputVoltage >= (Application::referenceOutputVoltage * 1.1)) {
        Hrpwm::SetDuty(Hrpwm::Channel::boost, 1000);

        float outputVoltage = Feedback::GetOutputVoltage();
        float outputCurrent = Feedback::GetOutputCurrent();

        // Работаем в CC mode, когда Vout < Vref
        if (outputVoltage < (Application::referenceOutputVoltage - 0.2f)) {
            pidCurrentMode
                .SetReference(Application::referenceOutputCurrent)
                .SetSaturation(-29800, 29800)
                .SetFeedback(outputCurrent, 0.001)
                .SetCoefficient(20,0,0,0,0)
                .Compute();
            resultPID = pidCurrentMode.Get();
        }

        // Работаем в CV mode, когда (Iout -> 0) ИЛИ (Vout => Vref)
        if ((outputCurrent < 0.05f) || (outputVoltage >= (Application::referenceOutputVoltage - 0.2f))) {
            pidVoltageMode
                .SetReference(Application::referenceOutputVoltage)
                .SetSaturation(-29800, 29800)
                .SetFeedback(outputVoltage, 0.001)
                .SetCoefficient(50,0,0,0,0)
                .Compute();
            resultPID = pidVoltageMode.Get();
        }

        Application::dutyBuck += resultPID;
        Hrpwm::SetDuty(Hrpwm::Channel::buck, Application::dutyBuck);
    }

    // Режим работы buck-boost, когда (90% * Vref) < Vin < (110% * Vref)
    if ((inputVoltage > (Application::referenceOutputVoltage * 0.9)) && (inputVoltage < (Application::referenceOutputVoltage * 1.1))) {
        Hrpwm::SetDuty(Hrpwm::Channel::boost, 6000);

        float outputVoltage = Feedback::GetOutputVoltage();
        float outputCurrent = Feedback::GetOutputCurrent();

        // Работаем в CC mode, когда Vout < Vref
        if (outputVoltage < (Application::referenceOutputVoltage - 0.2f)) {
            pidCurrentMode
                .SetReference(Application::referenceOutputCurrent)
                .SetSaturation(-29800, 29800)
                .SetFeedback(outputCurrent, 0.001)
                .SetCoefficient(20,0,0,0,0)
                .Compute();
            resultPID = pidCurrentMode.Get();
        }

        // Работаем в CV mode, когда (Iout -> 0) ИЛИ (Vout => Vref)
        if ((outputCurrent < 0.05f) || (outputVoltage >= (Application::referenceOutputVoltage - 0.2f))) {
            pidVoltageMode
                .SetReference(Application::referenceOutputVoltage)
                .SetSaturation(-29800, 29800)
                .SetFeedback(outputVoltage, 0.001)
                .SetCoefficient(50,0,0,0,0)
                .Compute();
            resultPID = pidVoltageMode.Get();
        }

        Application::dutyBuck += resultPID;
        Hrpwm::SetDuty(Hrpwm::Channel::buck, Application::dutyBuck);
    }    
}


Небольшое замечание по коду

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

Как видите глобально идея не изменилась, мы используем 2 ПИД-регулятора, которые существуют независимо, но воздействуют на один параметр — заполнение ШИМ-сигнала. Разумеется нужно добавить условие перехода между CC mode и CV mode. Тут все просто: если напряжение на выходе приблизилось к заданному (Application: referenceOutputVoltage) или ток потребления стал близким к нулю, то преобразователь переходит в режим CV mode, то есть ограничивает напряжение на выходе на уровне 15В. Если же напряжение на выходе не приблизилось к заданному, то преобразователь работает в режиме CC mode и на его выходе стабилизированный ток 1А.

Тут стоит добавить, что для корректной работы частота вызова алгоритма/прерывания должна быть достаточно высокой, чтобы при переходе между режимами не было длительных переходных процессов. Например, в данной задаче на конкретно моем железе при работе с LED или с АКБ достаточной частотой было значение 2 кГц. Если нагрузка менее инертная и носит ярко выраженный реактивный характер, то частоту вызова нужно увеличить, обычно 10 кГц хватает на все и STM32F334 с запасом вывозит расчеты и остается время на другие задачи.

И собственно финальная демонстрация работоспособности buck-boost dc/dc преобразователя в режиме CC/CV:



Послесловие…

Хотелось бы выразить огромную благодарность местным жителям lamerok и veydlin, а так же Игорю Спиридонову (увы, на хабре его нет, но читать будет), которые помогли своими советами и конструктивной критикой при работе над софтом для данного проекта и МРРТ контроллера!

Так же приглашаю к нам в телеграмм канал тех, кто увлекается разработкой электроники и хочет общаться с адекватными единомышленниками: https://t.me/proHardware. У нас нет ардуины, политоты, оскорблений, а все токсики давно забанены, кроме админа, разумеется :))

Спасибо тем, кто дочитал статью до конца. Хотя данный материал и является лишь видением автора по решению задачи управления buck-boost-ом, и тем более не является истиной, я все равно надеюсь, что материал принесет вам пользу и вы либо найдете для себя что-то новое, либо не совершите моих ошибок.

© Habrahabr.ru