Оглавление
Идея
Светящийся шар с выбором цвета вращением (поворотом шара). Всё остальное крутится вокруг этой идеи. Ещё шар реагирует на касание, хотя правильнее будет сказать: на накрытие, но об этом позже. Название: Лайти (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.: Кому было интересно, подписывайтесь на мой телеграм канал. Шутка, нет у меня канала.
