Оглавление
Идея
Светящийся шар с выбором цвета вращением (поворотом шара). Всё остальное крутится вокруг этой идеи. Ещё шар реагирует на касание, хотя правильнее будет сказать: на накрытие, но об этом позже. Название: Лайти (Lighty). Одно видео лучше тысячи символов.
Режимы
Есть 4 режима: выбор цвета, пульт управления, цветомузыка и плавная смена цветов. Переключение режимов тоже вращением, но в перевёрнутом состоянии. Для каждого режима свой цветной сектор. После перехода на другой сектор, начинается плавное затухание, в процессе которого можно передумать и выбрать другой режим.
Выбор цвета, основной режим работы
Крутим шар - меняется цвет свечения. Кладём ладонь — управляем яркостью: если просто прикоснуться ладонью, то яркость увеличивается, если накрыть — уменьшается. Максимальная яркость обозначается белой вспышкой. Представлен на первом видео статьи.
Пульт управления по ИК каналу
Например телевизором, но в моём случае компом (на видео телевизор подключён к компьютеру). Можно регулировать громкость: крутим против часовой стрелки — уменьшается, по часовой — увеличивается. Можно делать play / pause: касаемся и немного задерживаем ладонь на шаре до белой вспышки.
Цветомузыка
Реагирует на звуки, мигает под музыку. На видео другой корпус, с ним этот и следующий режимы красивее (смотреть со звуком).
Плавная смена цветов
C переходом одних в другие. Скорость перетекания цветов задаётся углом поворота.
Железо
На маленькой (⌀ 43 мм) круглой платке:
3 RGB светодиода + драйвер для них (MAX6967)
магнитометр и акселерометр LSM303DLHC для определения направления, угла наклона и т. д.
оптический датчик приближения VCNL4000 используется для управления яркостью
микрофон и усилитель MAX9814 для цветомузыки
micro USB для связи с PC и зарядки
LiPo аккумулятор (приклеивается к плате с обратной стороны) и контроллер заряда LM3658SD
микроконтроллер STM32F103T8
ну и по мелочи: линейный стабилизатор с низким падением напряжения LP2981AIM5-3.3, диоды, маленький кварц, USBLC6-2SC6 для защиты USB
Схема
Плата приклеена двусторонним скотчем на основание из оргстекла, которое вставляется в матовый стеклянный шар (плафон от светильника).
Важное замечание
Устройство (плата и ПО) было разработано и сделано 11 лет назад. Одиннадцать, Карл! (мем тех лет). Все основные функции были реализованы, но не всё доведено до ума: после пары месяцев неспешной работы над прошивкой в свободное время энтузиазм иссяк, да и появились другие проекты.
И вот, недавно, попалась статья про сезон DIY и я решил, если не расскажу про Лайти сейчас, то не расскажу уже никогда. Нашёл всё железо, разобрался в исходниках, наснимал фото / видео.
Обращаю внимание, что вся компонентная база уже устарела и сейчас я бы многое сделал по другому (цифровой MEMS микрофон на I2S, адресные светодиоды). И так как всё это делалось для фана, я не задавался вопросами экономической целесообразности применения той или иной микросхемы, использовал то, с чем хотелось поиграться.
История разработки
Прототип
Я всегда питал слабость ко всяким светящимся штукам. В детстве пробовал делать разную цветомузыку и ещё тогда понял, что главное в ней это экран (рассеиватель). Когда мне на глаза попался стеклянный матовый плафон от настенного светильника, я понял — оно!
Первые же опыты показали, что такой плафон красиво рассеивает свет, а ещё он очень приятный тактильно, его приятно трогать. Есть и минус: всё таки это стекло, поэтому нужно обращаться крайне аккуратно. Прототип был сделан под этот плафон и так как хотелось чтобы снизу ничего не выпирало, чтобы шар оставался шаром насколько это возможно, я решил сделать круглую плату которая поместится в отверстие плафона.
Поставил только один сверх яркий 3W RGB светодиод без драйвера, просто на транзисторах с мощными резисторами (большая часть энергии уходила на их нагрев, а не на свечение). ATmega8 под ним, а магнитометр с обратной стороны платы. Микрофон и LM358 для цветомузыки. Не было ни аккумулятора, ни USB, а питалось всё от внешнего БП. Плата крепилась к основанию из оргстекла на винтах, а само основание держалось в плафоне на распорках.
Lighty 1.0
Прототип подтвердил жизнеспособность идеи, но стало очевидно что провод питания сильно мешает и нужен аккумулятор. Ещё, я понял что такой яркий светодиод ни к чему, и в итоге разработал новую плату, в которой сделал всё по другому. Так получился Lighty 1.0, улучшенный и расширенный. Использовал тот же плафон и основание, что и для прототипа.
Появилось много нового, например VCNL4000. Это ИК датчик приближения, может измерять расстояние до 20 см и общую освещенность. В случае со стеклянным плафоном, расстояние до стекла уже на грани максимума измерений. К тому же, стекло шара ослабляет ИК излучение, поэтому измерять расстояние, например от руки до шара, получается плохо. Но я заметил, что значение приближенности меняется в зависимости от того накрыт ли шар рукой или нет. Меняется оно не сильно, но вполне достаточно для грубого измерения степени накрытия в процентах. Это я и применил для регулировки яркости: касаемся (немного накрываем) — яркость увеличивается, кладём всю ладонь (накрываем сильнее) — яркость уменьшается.
Так как мне хотелось цветомузыку получше (я конечно понимал что это бестолковая фигня, которая надоедает через 2 минуты, но бывает что хочется странного), то я поставил MAX9814 в качестве усилителя. Благодаря AGC (АРУ), усиление автоматически подстраивается под уровень звука.
Аккумулятор привнёс автономность и удобство. Старался разместить его так чтобы не сильно увеличивать габариты. Это позволяет встраивать платку в разные места и корпуса.
Ошибки
Самая глупая — забыл добавить резисторный делитель для контроля напряжения аккумулятора (это в автономном то устройстве!). Отказался от контроля LM3658SD через линию LM3658_ENABLE и использовал её для измерения напряжения аккумулятора (добавил 2 выводных резистора навесным монтажом).
Ещё забыл сделать линию управления MAX9814: на плате пин !SHDN подключён к питанию. Оказалось, что MAX9814 потребляет довольно много тока, что негативно сказывается на времени работы от аккумулятора, особенно это заметно в спящем режиме МК (читать даташит на усилитель надо было внимательнее). Пришлось порезать дорожки, припаяться к via (переходному отверстию) и кинуть проводок на заботливо разведённый неиспользуемый выход микроконтроллера. Теперь, когда не нужен звук с микрофона, можно выключать усилитель.
Пульт управления
После исправления ошибок добавил новую функцию: управление по ИК каналу. На тот момент у меня уже довольно давно применялся USB-HID IR приёмник (тоже самодельный) и какой-то ненужный пульт.
Приёмник эмулирует клавиатуру и при получении сигнала с кодом кнопки PAUSE отправляет по USB виртуальное нажатие клавиши пробел. Ещё есть управление громкостью. Этот пульт я и заменил: Лайти может слать коды трёх кнопок пульта (PAUSE, VOLUME+, VOLUME-). Добавил ещё 2 выводных резистора, транзистор, ИК светодиод и нашел ещё один свободный и легкодоступный пин STM32F103 с поддержкой выхода аппаратного таймера с PWM.
В режиме ИК пульта важна энергоэффективность. Шар стоит на столе целыми днями и должен быть всегда готовым к управлению, без необходимости предварительного включения. Поэтому в этом режиме МК большую часть времени спит, включается от акселерометра когда шар крутят, передаёт ИК команды и засыпает снова. В таком режиме аккумулятора хватает на неделю. В целом, управление работает довольно надёжно: даже через матовое стекло ИК светодиод добивает несколько метров до приёмника, так как в импульсе через него фигачит неслабый ток.
Другой плафон
Стеклянный плафон слишком хорошо рассеивает и смешивает цвета от светодиодов: кажется что он там один, а не три. Поэтому в режимах цветомузыки и плавной смены цвета всё это превращается в цветную кашу.
Нашел другой корпус, пластиковый, с матовым рассеивателем. В нём свет от отдельных светодиодов не так сильно сливается и получаются классные световые пятна, что как раз и нужно для цветомузыки и плавного перетекания цвета. Под этот корпус был применён второй Лайти (собирал две платы одновременно). Кстати в нем датчик приближения работает как положено: можно измерять расстояние от руки до пластика.
ПО
Всё написано на C, без ОС (зря, было бы удобнее). В качестве IDE использовал широко популярный (в узких кругах) на тот момент CoIDE.
Направление
Одной из основных задач была работа с LSM303DLHC. Он не даёт готовое направление (heading), его нужно высчитывать из значений напряжённости магнитного поля по осям XYZ. Данные по I2C читаются с помощью драйвера LSM303DLHC от ST. На просторах сети был найден алгоритм получения направления, который помимо магнитных данных, использует ещё и показания акселерометра: выполняет пересечение и нормализацию векторов ускорения и магнитного поля, рассчитывает направление в градусах.
код
static vector a; // accelerometer readings
static vector m; // magnetometer readings
vector m_max; // maximum magnetometer values, used for calibration
vector m_min; // minimum magnetometer values, used for calibration
static vector from = {.x = 0, .y = -1, .z =0};
void vector_cross(const vector *a, const vector *b, vector *out)
{
out->x = a->y*b->z - a->z*b->y;
out->y = a->z*b->x - a->x*b->z;
out->z = a->x*b->y - a->y*b->x;
}
float vector_dot(const vector *a, const vector *b)
{
return a->x*b->x+a->y*b->y+a->z*b->z;
}
void vector_normalize(vector *a)
{
float mag = sqrt(vector_dot(a,a));
a->x /= mag;
a->y /= mag;
a->z /= mag;
}
// Returns the number of degrees from the From vector projected into
// the horizontal plane is away from north.
//
// Description of heading algorithm:
// Shift and scale the magnetic reading based on calibration data to
// to find the North vector. Use the acceleration readings to
// determine the Down vector. The cross product of North and Down
// vectors is East. The vectors East and North form a basis for the
// horizontal plane. The From vector is projected into the horizontal
// plane and the angle between the projected vector and north is
// returned.
uint16_t sensorsCompassGetHeading(void)
{
// shift and scale
m.x = (m.x - m_min.x) / (m_max.x - m_min.x) * 2 - 1.0;
m.y = (m.y - m_min.y) / (m_max.y - m_min.y) * 2 - 1.0;
m.z = (m.z - m_min.z) / (m_max.z - m_min.z) * 2 - 1.0;
vector temp_a = a;
// normalize
vector_normalize(&temp_a);
// compute E and N
vector E;
vector N;
vector_cross(&m, &temp_a, &E);
vector_normalize(&E);
vector_cross(&temp_a, &E, &N);
// compute heading
int heading = 0;
heading = round(atan2(vector_dot(&E, &from), vector_dot(&N, &from)) * 180 / M_PI);
if (heading < 0) heading += 360;
return heading;
}
Алгоритм предполагает наличие данных минимумов и максимумов значений напряженности магнитного полей по осям (m_min, m_max). Для этого есть калибровка с сохранением результатов во флеш память. Если при старте, сохранённых данных не найдено, то запускается процедура калибровки: нужно вращать устройство по всем осям пока не прекратятся вспышки (если найдено новое минимальное или максимальное значение светодиоды вспыхивают). Калибровку обязательно делать с той же конфигурацией металлических материалов (например крепёжные винты) которая будет присутствовать в дальнейшем. Если же калибровка не удалась или нужно её повторить, то всегда можно запустить её снова, включив Лайти в перевёрнутом состоянии.
Для плавности изменения цвета, полученные значения направлений нужно усреднять. Но просто так усреднять углы не получиться, ведь у нас замкнутая шкала (0..359) и в районе перехода через ноль будут проблемы. Нашел следующее решение: каждый угол (направление) перевести в координаты x, y, потом отдельно усреднить x и y компоненты векторов и восстановить угол обратно.
код
float y_part = 0.f, x_part = 0.f;
for (i = 0; i < HEADING_MEAN_SIZE; i++)
{
x_part += cos(HeadingMeanArray[i] * (M_PI / 180.f));
y_part += sin(HeadingMeanArray[i] * (M_PI / 180.f));
}
Heading = (uint32_t)((atan2(y_part / HEADING_MEAN_SIZE, x_part / HEADING_MEAN_SIZE) * (180.f / M_PI))+180.f);
Помимо направления, из LSM303DLHC считываются данные акселерометра, чтобы получить отклонение от вертикальной оси и понять примерную ориентацию в пространстве: например перевёрнут ли шар или нет.
Наконец, линии прерывания (INT1 / INT2) LSM303DLHC задействованы для пробуждения в случае движения (вибрации) — это для режима пульта управления. Акселерометр настраивается по I2C таким образом, что в случае превышения порогового значения он дёргает линию INT1 и выводит МК из сна по EXTI прерыванию.
Управление светодиодами
Полученное значение направления (вращение по вертикальной оси) выступает в качестве Hue компоненты HSV цветового пространства, которое используется повсеместно в прошивке. Накрытие ладонью определяет яркость — Value. Насыщенность цвета (Saturation) регулируется отклонением от вертикали: в основном режиме берём шар в руку и наклоняем его. Это не очень нужно, так как по умолчанию она и так максимальна, а её уменьшение делает цвет менее насыщенным, ближе к белому, что не так красиво.
Так как для светодиодов нужны значения в RGB пространстве, нужно преобразование.
HSV в RGB
void toolsHSVtoRGB(ColorHSV_t *hsv, ColorRGB_t *rgb)
{
uint32_t f=0, H=0, S=0, V=0;
uint32_t w=0, q=0, t=0, i=0;
uint32_t r=0, g=0, b=0;
H = hsv->h;
S = hsv->s;
V = hsv->v;
if (S==0) { r = g = b = V; }
else
{
i = H / 60;
f= (((H*100)/60)-(i*100));
w = V * (100 - S) / 100;
q = V * (100 * 100 - (S * f)) / 10000;
t = V * (100 * 100 - (S * (100 - f))) / 10000;
switch (i)
{
case 0:
case 6:
r = V;
g = t;
b = w;
break;
case 1:
r = q;
g = V;
b = w;
break;
case 2:
r = w;
g = V;
b = t;
break;
case 3:
r = w;
g = q;
b = V;
break;
case 4:
r = t;
g = w;
b = V;
break;
case 5:
r = V;
g = w;
b = q;
break;
}
}
rgb->r = (r * 255)/100;
rgb->g = (g * 255)/100;
rgb->b = (b * 255)/100;
}
Светодиодами управляет 10 канальный MAX6967, на каждый из трёх светодиодов нужно 3 канала, один остался свободным. Разрешение каждого канала 8 бит (255 значений яркости). Для передачи данных используется SPI.
Датчик расстояния
Используя данные приближенности (proximity) от VCNL4000, с помощью простейшего масштабирования рассчитывается степень накрытия шара. Для этого, нужно знать значение приближенности соответствующее 0% (когда рука не накрывает шар) и значение при накрытии рукой (это будет 100%).
код
uint32_t sensorsProximityGetCover(void)
{
int32_t counts = 0;
uint32_t percents = 0;
counts = SysSensorsData.Proximity;
counts = counts - ProximityOffset;
if (counts < 0) counts=0;
percents = (uint32_t)(((float)counts) / ProximityMaxCoverCoeff);
if (percents >= 100) percents = 100;
return percents;
}
Для получения этих значений нужна калибровка. Она выполняется сразу после калибровки LSM303DLHC: сначала определяем 0% (ProximityOffset), потом ждём накрытия ладонью и сохраняем максимальное значение (в коде вместо максимального значения сохраняется уже рассчитанный коэффициент для пересчета - ProximityMaxCoverCoeff).
VCNL4000 позволяет получить текущий уровень освещенности, но на данный момент это не используется: можно сделать автоматическую регулировку яркости.
Цветомузыка
Сначала усиленный сигнал микрофона оцифровывается АЦП микроконтроллера, результаты читаются без использования DMA (а надо бы). Затем выполняется FFT (БПФ) на 64 точки. Использовал STM32F10x_DSP_Lib от ST - пригодилась функции cr4_fft_64_stm32(), для которой нужно соблюдать специальный формат комплексных чисел (вещественная и мнимая части в одной переменной, мнимая часть равна нулю).
Hidden text
static int32_t RawADC, CmpxBufIn[FFT_SIZE], CmpxBufOut[FFT_SIZE], MagBuf[FFT_SIZE];
static uint32_t ReadySpectrum[FFT_SIZE / 2];
for (i=0; i< FFT_SIZE; i++)
{
RawADC = sensorsGetAudioSample();
/* to complex number with real and imaginary part (imaginary zero) */
CmpxBufIn[i] = (((int16_t)RawADC) - 2048) << 4 ;
}
//Radix-4 complex FFT for STM32
cr4_fft_64_stm32(CmpxBufOut, CmpxBufIn, FFT_SIZE);
int32_t lX,lY;
for (i=0; i < FFT_SIZE; i++)
{
lX= (CmpxBufOut[i]<<16)>>16; // sine_cosine --> cos
lY= (CmpxBufOut[i] >> 16); //sine_cosine --> sin
MagBuf[i] = sqrt(lX*lX + lY*lY);
}
Полученный спектр на 32 частоты (нам нужна только первая половина массива готовых амплитуд) усредняется и распределяется по трём светодиодам (9 каналов) в соответствии с шаблоном, полученным опытным путём, с помощью программы для ПК. Шаблон это 9 переменных uint32_t (по числу используемых каналов MAX6967), каждый бит в переменной указывает надо ли этому каналу реагировать на ту или иную частоту.
Программа для ПК
Да да, для отладки, была написана программа на C# и реализован обмен данными по USB: STM32 реализует интерфейс HID устройства с одним endpoint (буфер на 64 байта).
В программе можно посмотреть показания всех датчиков, задать любой цвет для любого светодиода и посмотреть визуализацию аудио спектра.
Использование и неожиданные применения
Лайти - это всего лишь игрушка сделанная по фану и как это часто бывает с игрушками, она надоедает. Тем не менее, несколько месяцев он красиво стоял на столе, светил и использовался для регулировки громкости при просмотре всякого.
Потом, прошло много лет, оба сделанных Лайти затерялись в дальних углах. Но несколько лет назад было найдено новое применение обоим: первый стал ёлкой а второй котиком → детским ночником.
Ёлка
Микро ёлка из икеи, навесил на неё гирлянды с микро лампочками, Лайти с более ёмким аккумулятором разместился в горшке. Светодиоды были выпаяны, а вместо них подключил гирлянды.
Добавил новые функции: циклическое переключение режимов по двойному тапу, например двойном ударе по столу на котором стоит горшок: не переворачивать же ёлку с шариками чтобы переключить режим. Вместо ИК пульта, новый режим в котором можно выбрать только одну гирлянду. И последняя модификация, специально для дочки: включение при касании волшебной палочкой звезды на макушке (магнит вклеенный в палочку, геркон в звезде, чуть кода и получается магия).
Котик
Самый обычный детский ночник из магазина с ярким разноцветным светодиодом. Теперь там Лайти с очень простым функционалом в прошивке: включение по кнопке (опять спящий режим МК) и самое главное: таймер автоматического отключения через полчаса с постепенным угасанием приятного теплого света.
Нереализованные идеи
В основной прошивке активны 4 режима, хотя при выборе их больше: 2 слота так и остались пустыми. Мне казалось что 6 режимов я точно сделаю, но руки так и не дошли. Ниже несколько идей из списка 10 летней давности.
Магический шар (спроси у вселенной) - трясешь и получаешь ответ красным / зеленым цветом.
Режим работы от компьютера по USB: индикация уведомлений, событий и т.д.
Режим "Свеча" — имитация огня и задувание посредством микрофона.
Выводы
Хоть я и не сделал следующую версию платы с исправлениями и доработками, я получил удовольствие от придумывания разных идей и разработки как железа, так и ПО. Испытываешь прекрасное чувство удовлетворённости когда получается воплотить идеи в что‑то материальное и работоспособное.
Получил много опыта, который реально применял позже: при разработке других, «серьёзных» и «полезных» устройств с цифровым компасом и аудио усилителем. Надеюсь что мой опыт, идеи и решения покажутся читателям любопытными, а может даже полезными.
И ещё: рассказывайте о своих проектах! Ведь я ещё тогда, 11 лет назад, хотел рассказать о проекте на хабре, но не делал этого, так как стремился сначала довести всё до идеала. Сейчас понимаю, что если бы я уже тогда написал статью, то возможно, именно это и помогло бы сделать следующую версию. Так что всё должно быть в своё время и в стремлении к идеалу надо знать меру.
P.S.: Кому было интересно, подписывайтесь на мой телеграм канал. Шутка, нет у меня канала.