
Пролог
У меня было множество вело фар и всегда меня раздражало то, что фара включается практически мгновенно. Глаза даже не успевают приспособиться и это доставляет существенный дискомфорт. При этом на работе я много лет как раз разрабатывал ECU-прошивки для управления кузовной электроникой. В связи с этим я принял решение разработать себе прототип вело фары с плавным включением яркости для вело-фары.
Постановка задачи
Разработать прототип вело-фары с плавным включением и плавным отключением. Яркость должна изменяться постепенно. Обеспечить возможность дистанционного включения лампы (например по IR пульту). Сделать возможность регулирования яркости свечения через инкрементный энкодер. Настройки интенсивности свечения сохранять в энергонезависимой памяти NVRAM. Попробовать два алгоритма регулирования яркости : PWM и PDM.
Аппаратная часть
В качестве отладочной платы для прототипа я по-прежнему выбрал с JZ-F407VET6.

Распиновка для сборки прототипа такая
GPIO | Pull | Wire Name | Dir | Comment |
PE0 | Up | Encoder A | in | Инкрементный энкодер |
PE1 | Up | Encoder B | in | Инкрементный энкодер |
PA6 | up | IR sensor | in | TSOP-36 |
PA9 | no | USART1_TX | out | CLI |
PA10 | up | USART1_RX | in | CLI |
PA0 | none | TIM5_CH1 | out | PWM |
PA3 | none | TIM9_CH2 | out | PWM |
PE4 | none | DeltaSigma | out | PDM |
В роли драйвера 12-вольтового LED я выбрал микросхему H-моста DRV8870. Вот так выглядит отладочная плата для этой микросхемы.

Нагрузкой является LED сборка со встроенными резисторами. Также нужен цоколь для LED лампы.

Весь прототип выглядит вот так

В чем трудность управления LED фонарями?
1) Главная особенность LED лампы (в отличие от ламп накаливания) в том, что она полностью без инерционная. То есть, если с нуля подать скважность 100%, то лампа в самом деле мгновенно и ослепительно включится на 100%. Выглядит как вспышка. Для меня это как раз нежелательный сценарий. Я хочу сделать искусственную инерционность LED лампы. Чтобы хрусталики глаз могли приспособиться к постепенному нарастанию яркости. Как же мне этого добиться?
2) Вторая особенность LED в том, что LEDы не так-то просто плавно включить. Уже при 0.7 % заполнения LED включается и это тоже визуально воспринимается как резкое включение. Хочется как-то сделать акцент именно на начальной фазе нарастания яркости и на фазе окончательного потухания. Добавить программную инерционность вокруг нулевого заполнения.
Программная часть
Прежде чем писать прошивку надо определиться с тем по какому алгоритму следует производить нарастание яркости. Простой линейный закон изменения яркости - это не так эффектно. Сначала я попробовал экспоненциальный закон нарастания яркости. С потуханием все хорошо, однако с включением плохо. На прототипе это выглядело как резкое включение.

В коде он выглядит так
Экспоненциальное нарастание и спад яркости LED-а
import matplotlib.pyplot as plt
import numpy as np
max_illum=100.0
scale = 0.5
time_s = np.arange(1, 7)
time_max=10
time_s = np.linspace(0, time_max, 100)
illumination_climax = max_illum*(1.0- np.exp(-scale*time_s))
illumination_decay = max_illum* np.exp(-scale*time_s)
plt.plot(time_s, illumination_climax)
plt.plot(time_s, illumination_decay)
plt.title('Illumination change')
plt.xlabel('Time,[s]')
plt.ylabel('Light level, [Lx]')
plt.grid()
plt.show()
Затем я попробовал реализовать нарастание и снижение яркости при помощи логистической кривой. Логистическая функция выражается формулой ниже, где k - коэффициент крутизны наклона кривой

Вот ее графики

Можно поэкспериментировать с различными параметрами переходного процесса отрисовывая графики: длительность переходного процесса и коэффициент в степени.
Код построения логистической кривой
import matplotlib.pyplot as plt
import numpy as np
max_illum=100.0
scale = 0.95
time_s = np.arange(1, 7)
time_max=10
time_s = np.linspace(0, time_max, 100)
time1_s=time_s-time_max/2
illumination_climax = max_illum*1.0/(1.0 + np.exp(-scale*time1_s))
illumination_decay = max_illum-illumination_climax
#illumination_decay1 = max_illum* np.exp(-scale*time_s)
plt.plot(time_s, illumination_climax)
plt.plot(time_s, illumination_decay)
plt.title('Illumination change')
plt.xlabel('Time,[s]')
plt.ylabel('Light level, [Lx]')
plt.grid()
plt.show()
Этот переход уже что надо, однако надо помнить, что у меня есть еще и инкрементный энкодер для ручного управления яркостью. Функциональное управление яркостью согласно логистической кривой без обратной связи перебивает алгоритм управления яркостью по квадратурному энкодеру. Как же разрешить это противоречие?
PID регулятор
Чтобы решить эту проблему я обратился к PID регулятору. PID-регулятор — это программа, которая читает показания датчика и управляет мощностью так, чтобы значение датчика стало тем, что вы задали. Что, если энкодером и кнопками задавать целевое значение для PID регулятора? При этом PID регулятор уже сам будет как-то делать желаемый переходной процесс. Главная роль тут отводится интегральной части регулятора. Ее вклад в финальное заполнение и будет вносить основной результат. Однако и тут облом. Дело в том, что сразу после установки нового целевого значения яркости образуется максимальная ошибка (погрешность). Это приводит к интенсивному нарастанию яркости на начальном этапе, которое как раз и портит весь визуальный эффект. Как же быть?
Чтобы сделать медленное изменение на начальном и конечном участке я слегка модернизировал PID регулятор. Добавил еще один параметр (смешение): отклонение от прошлой цели. Смещение противоположно ошибке. Смещение плюс погрешность дают расстояние, которое надо пройди регулятору от предыдущей цели к новой цели.
Чтобы появилась возможность вычислять смещение, надо при каждой установке нового целевого значения регулятора запоминать предыдущее значение цели last_target. Вот так.
bool pid_target_set(uint8_t num, float target) {
bool res = false;
PidHandle_t* Node = PidGetNode(num);
if(Node) {
res = is_float_equal_absolute(target, Node->target,0.0001);
if(!res) {
LOG_INFO(PID, "PID%u,Set,Target:%f->%f", num, Node->target, target);
Node->last_target = Node->target; // !!!!!!!
Node->target = target;
res = true;
}
} else {
LOG_ERROR(PID, "NodeErr,PID%u", num);
}
return res;
}
Затем на каждой итерации PID регулятора вычислять абсолютное значение смещения shift = |out - last_target|

Благодаря вычислению смещения можно на каждой итерации PID регулятора вычислять такое приращение к интегральной части, которое в результате и сделает нам S-образный переходной процесс (см зелёный график d ). Дифферециальная часть PID регулятора тут и вовсе не нужна. Коэффициент D можно обнулить. Однако, чтобы процесс начался нужно сделать ненулевой пропорциональный коэффициент. Код ядра PID регулятора получается такой.
static bool pid_proc_value_ll(PidHandle_t* const Node) {
bool res = false;
if(Node) {
Node->shift = Node->last_target - Node->out;
Node->d_sum = 0.0f;
if(0.0 < Node->error) {
Node->d_sum = MATH_MIN(fabsf(Node->shift),Node->error);
} else {
Node->d_sum = MATH_MAX(-fabsf(Node->shift),Node->error);
}
Node->error_sum += Node->d_sum;
Node->out = 0.0f;
Node->out += Node->p * Node->error;
Node->out += Node->i * Node->error_sum;
LOG_DEBUG(PID, "%s", PidNodeToStr(Node));
res = true;
}
return res;
}Вот так это работает в реальности. Перед Вами график заполнения PWM от времени прочитанный из реального UART лога. Как видно, это S-образный переходной процесс, который рассчитывается в потоковом режиме.

Варьируя интегральный коэффициент можно управлять скоростью переходного процесса.
Управление яркостью на низком уровне
Второй вопрос, что выбрать для непосредственного регулирования яркости: PWM или PDM?

Проблема PWM в том, что он не позволяет непрерывно включаться с нуля и включает ярость скачком уже на 0.7% заполнения. Поэтому я попробовал запрограммировать программный PDM.

Настроил прерывания по таймеру 1 c частотой 20kHz. В обработчике прерываний стал делать вычисление PDM семплов.
bool delta_sigma_isr_proc_one_ll(DeltaSigmaHandle_t* Node) {
bool res = false;
Node->error = Node->target - Node->dac_out;
Node->sum_error += Node->error;
Node->pdm = adc_1bit(Node->sum_error, Node->comparator_middle);
res = gpio_logic_level_set(Node->Pad, (GpioLogicLevel_t)Node->pdm);
Node->dac_out = dac_1bit(Node->pdm, Node->min, Node->max);
Node->sample_cnt++;
return res;
}Однако это дело не улучшило. PDM дает заметное мерцание на малой яркости. В результате я выбрал PWM, который по крайней мере не мерцает.
В результате, прошивка приняла такую структуру.

Возможные приложения прошивки драйвера LEDов
--Тестер автомобильных светодиодных лампочек
--Вело фара
--Торшеры для чтения
--Люстры
--Настольные светильники
--Плавное включение может пригодиться для управления подсветкой смартфонов при выходе из режима блокировки экрана. Это добавит эргономики.
--Можно включать LED лампы на малое свечение ночью, чтобы сбивать с толку комаров.
--S-образный переходной процесс может быть как нельзя кстати для управления шаговыми двигателями (ШД). Там как раз запрещено скачкообразное изменение скорости управляющего воздействия, чтобы не потерять отработку шагов на обмотках статора.
Итог
Удалось сделать прототип электроники и прошивки для велосипедной фары, который позволяет производить плавное управление яркостью LED при помощи инкрементного энкодера. Также заложен алгоритм плавного включения и отключения фары. Удалось сделать S-образный переходной процесс при помощи слегка модернизированного PI-регулятора.
Прошивка готова и ее можно скачать на github. Дальнейшим развитием проекта может быть проработка электропитания, разработка миниатюрной PCB, конструктива и крепления вело фары.
Ссылки
Вопросы:
1) Как при помощи PID регулятора сделать S-образный переходной процесс?
2) Как избавиться от мерцания при PDM управлении свечением?
3) Существуют ли в продаже миниатюрные отладочные платы с STM32, GPIO разъемами и драйвером hi-side ключей, чтобы управлять автомобильными LEDами.




















