Pull to refresh

Как я сделал светящийся шар, который может всякое

Level of difficultyMedium
Reading time12 min
Views15K

Оглавление

Идея

Светящийся шар с выбором цвета вращением (поворотом шара). Всё остальное крутится вокруг этой идеи. Ещё шар реагирует на касание, хотя правильнее будет сказать: на накрытие, но об этом позже. Название: Лайти (Lighty). Одно видео лучше тысячи символов.

Режимы

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

Выбор цвета, основной режим работы

Крутим шар - меняется цвет свечения. Кладём ладонь — управляем яркостью: если просто прикоснуться ладонью, то яркость увеличивается, если накрыть — уменьшается. Максимальная яркость обозначается белой вспышкой. Представлен на первом видео статьи.

Пульт управления по ИК каналу

Например телевизором, но в моём случае компом (на видео телевизор подключён к компьютеру). Можно регулировать громкость: крутим против часовой стрелки — уменьшается, по часовой — увеличивается. Можно делать play / pause: касаемся и немного задерживаем ладонь на шаре до белой вспышки.

Цветомузыка

Реагирует на звуки, мигает под музыку. На видео другой корпус, с ним этот и следующий режимы красивее (смотреть со звуком).

Плавная смена цветов

C переходом одних в другие. Скорость перетекания цветов задаётся углом поворота.

Железо

Плата с разных сторон, с аккумулятором и без
Плата с разных сторон, с аккумулятором и без

На маленькой ( 43 мм) круглой платке:

  • 3 RGB светодиода + драйвер для них (MAX6967)

  • магнитометр и акселерометр LSM303DLHC для определения направления, угла наклона и т. д.

  • оптический датчик приближения VCNL4000 используется для управления яркостью

  • микрофон и усилитель MAX9814 для цветомузыки

  • micro USB для связи с PC и зарядки

  • LiPo аккумулятор (приклеивается к плате с обратной стороны) и контроллер заряда LM3658SD

  • микроконтроллер STM32F103T8

  • ну и по мелочи: линейный стабилизатор с низким падением напряжения LP2981AIM5-3.3, диоды, маленький кварц, USBLC6-2SC6 для защиты USB

Все SMD компоненты на верхней стороне платы (кроме USB разъёма)
Все SMD компоненты на верхней стороне платы (кроме USB разъёма)
Схема
Микроконтроллер и светодиоды с драйвером
Микроконтроллер и светодиоды с драйвером
Датчики
Датчики
USB и питание
USB и питание

Плата приклеена двусторонним скотчем на основание из оргстекла, которое вставляется в матовый стеклянный шар (плафон от светильника).

В основание вкручены стойки для винтов, на которых одеты резинки - держится нормально
В основание вкручены стойки для винтов, на которых одеты резинки - держится нормально

Важное замечание

Устройство (плата и ПО) было разработано и сделано 11 лет назад. Одиннадцать, Карл! (мем тех лет). Все основные функции были реализованы, но не всё доведено до ума: после пары месяцев неспешной работы над прошивкой в свободное время энтузиазм иссяк, да и появились другие проекты.

И вот, недавно, попалась статья про сезон DIY и я решил, если не расскажу про Лайти сейчас, то не расскажу уже никогда. Нашёл всё железо, разобрался в исходниках, наснимал фото / видео.

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


История разработки

Прототип

Я всегда питал слабость ко всяким светящимся штукам. В детстве пробовал делать разную цветомузыку и ещё тогда понял, что главное в ней это экран (рассеиватель). Когда мне на глаза попался стеклянный матовый плафон от настенного светильника, я понял — оно!

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

Прототип (он прошёл через многое)
Прототип (он прошёл через многое)

Поставил только один сверх яркий 3W RGB светодиод без драйвера, просто на транзисторах с мощными резисторами (большая часть энергии уходила на их нагрев, а не на свечение). ATmega8 под ним, а магнитометр с обратной стороны платы. Микрофон и LM358 для цветомузыки. Не было ни аккумулятора, ни USB, а питалось всё от внешнего БП. Плата крепилась к основанию из оргстекла на винтах, а само основание держалось в плафоне на распорках.

Lighty 1.0

Прототип подтвердил жизнеспособность идеи, но стало очевидно что провод питания сильно мешает и нужен аккумулятор. Ещё, я понял что такой яркий светодиод ни к чему, и в итоге разработал новую плату, в которой сделал всё по другому. Так получился Lighty 1.0, улучшенный и расширенный. Использовал тот же плафон и основание, что и для прототипа.

 Заказывал 4 платы, две остались до сих пор. Кстати на фото виден брак (нижняя дорожка на левой плате)
Заказывал 4 платы, две остались до сих пор. Кстати на фото виден брак (нижняя дорожка на левой плате)

Появилось много нового, например VCNL4000. Это ИК датчик приближения, может измерять расстояние до 20 см и общую освещенность. В случае со стеклянным плафоном, расстояние до стекла уже на грани максимума измерений. К тому же, стекло шара ослабляет ИК излучение, поэтому измерять расстояние, например от руки до шара, получается плохо. Но я заметил, что значение приближенности меняется в зависимости от того накрыт ли шар рукой или нет. Меняется оно не сильно, но вполне достаточно для грубого измерения степени накрытия в процентах. Это я и применил для регулировки яркости: касаемся (немного накрываем) — яркость увеличивается, кладём всю ладонь (накрываем сильнее) — яркость уменьшается.

Так как мне хотелось цветомузыку получше (я конечно понимал что это бестолковая фигня, которая надоедает через 2 минуты, но бывает что хочется странного), то я поставил MAX9814 в качестве усилителя. Благодаря AGC (АРУ), усиление автоматически подстраивается под уровень звука.

Аккумулятор привнёс автономность и удобство. Старался разместить его так чтобы не сильно увеличивать габариты. Это позволяет встраивать платку в разные места и корпуса.

Ошибки

Самая глупая — забыл добавить резисторный делитель для контроля напряжения аккумулятора (это в автономном то устройстве!). Отказался от контроля LM3658SD через линию LM3658_ENABLE и использовал её для измерения напряжения аккумулятора (добавил 2 выводных резистора навесным монтажом).

Ещё забыл сделать линию управления MAX9814: на плате пин !SHDN подключён к питанию. Оказалось, что MAX9814 потребляет довольно много тока, что негативно сказывается на времени работы от аккумулятора, особенно это заметно в спящем режиме МК (читать даташит на усилитель надо было внимательнее). Пришлось порезать дорожки, припаяться к via (переходному отверстию) и кинуть проводок на заботливо разведённый неиспользуемый выход микроконтроллера. Теперь, когда не нужен звук с микрофона, можно выключать усилитель.

Пульт управления

После исправления ошибок добавил новую функцию: управление по ИК каналу. На тот момент у меня уже довольно давно применялся USB-HID IR приёмник (тоже самодельный) и какой-то ненужный пульт.

Пульт от ТВ тюнера и USB приёмник на ATmega8 (софтовый USB!)
Пульт от ТВ тюнера и USB приёмник на ATmega8 (софтовый USB!)

Приёмник эмулирует клавиатуру и при получении сигнала с кодом кнопки PAUSE отправляет по USB виртуальное нажатие клавиши пробел. Ещё есть управление громкостью. Этот пульт я и заменил: Лайти может слать коды трёх кнопок пульта (PAUSE, VOLUME+, VOLUME-). Добавил ещё 2 выводных резистора, транзистор, ИК светодиод и нашел ещё один свободный и легкодоступный пин STM32F103 с поддержкой выхода аппаратного таймера с PWM.

Выглядит страшно, но работает
Выглядит страшно, но работает

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

Другой плафон

Стеклянный плафон слишком хорошо рассеивает и смешивает цвета от светодиодов: кажется что он там один, а не три. Поэтому в режимах цветомузыки и плавной смены цвета всё это превращается в цветную кашу.

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

Корпус для второго Лайти
Корпус для второго Лайти

ПО

Проект на github

Всё написано на 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 байта).

HSV круг
HSV круг

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


Использование и неожиданные применения

Лайти - это всего лишь игрушка сделанная по фану и как это часто бывает с игрушками, она надоедает. Тем не менее, несколько месяцев он красиво стоял на столе, светил и использовался для регулировки громкости при просмотре всякого.

Потом, прошло много лет, оба сделанных Лайти затерялись в дальних углах. Но несколько лет назад было найдено новое применение обоим: первый стал ёлкой а второй котиком → детским ночником.

Ёлка

Во всей красе
Во всей красе

Микро ёлка из икеи, навесил на неё гирлянды с микро лампочками, Лайти с более ёмким аккумулятором разместился в горшке. Светодиоды были выпаяны, а вместо них подключил гирлянды.

Справа сверху виден родной чёрный горшок ёлки, он вставляется в белый, купленный отдельно
Справа сверху виден родной чёрный горшок ёлки, он вставляется в белый, купленный отдельно

Добавил новые функции: циклическое переключение режимов по двойному тапу, например двойном ударе по столу на котором стоит горшок: не переворачивать же ёлку с шариками чтобы переключить режим. Вместо ИК пульта, новый режим в котором можно выбрать только одну гирлянду. И последняя модификация, специально для дочки: включение при касании волшебной палочкой звезды на макушке (магнит вклеенный в палочку, геркон в звезде, чуть кода и получается магия).

Котик

Скотч - сила!
Скотч - сила!

Самый обычный детский ночник из магазина с ярким разноцветным светодиодом. Теперь там Лайти с очень простым функционалом в прошивке: включение по кнопке (опять спящий режим МК) и самое главное: таймер автоматического отключения через полчаса с постепенным угасанием приятного теплого света.

Теплота и ламповость зашкаливают
Теплота и ламповость зашкаливают

Нереализованные идеи

В основной прошивке активны 4 режима, хотя при выборе их больше: 2 слота так и остались пустыми. Мне казалось что 6 режимов я точно сделаю, но руки так и не дошли. Ниже несколько идей из списка 10 летней давности.

  • Магический шар (спроси у вселенной) - трясешь и получаешь ответ красным / зеленым цветом.

  • Режим работы от компьютера по USB: индикация уведомлений, событий и т.д.

  • Режим "Свеча" — имитация огня и задувание посредством микрофона.

Выводы

Хоть я и не сделал следующую версию платы с исправлениями и доработками, я получил удовольствие от придумывания разных идей и разработки как железа, так и ПО. Испытываешь прекрасное чувство удовлетворённости когда получается воплотить идеи в что‑то материальное и работоспособное.

Получил много опыта, который реально применял позже: при разработке других, «серьёзных» и «полезных» устройств с цифровым компасом и аудио усилителем. Надеюсь что мой опыт, идеи и решения покажутся читателям любопытными, а может даже полезными.

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

P.S.: Кому было интересно, подписывайтесь на мой телеграм канал. Шутка, нет у меня канала.

Tags:
Hubs:
Total votes 60: ↑60 and ↓0+60
Comments19

Articles