Музыкальная игрушка на STM32 из подручных средств


    Добрый день, уважаемые хабровчане.
    Как-то вечером мне стало скучно и я решил собрать небольшое электронное устройство из валяющихся дома компонентов, чисто для развлечения, безо всякой практической цели. Повторить его может любой желающий, не потребуется даже печатной платы — устройство собрано из минимума электронных компонентов «навесу», приклено при помощи эпоксидки к какой-то ненужной плате, исключительно как к элементу конструкции, спаяно при помощи проволочек и залито той же самой эпоксидкой для надежности.
    Итак, делаем электронную флейту!

    На самом деле, девайс весьма далек от флейты по спектру извлекаемого из него звука. Зато у него есть RGB-светодиод, изменяющий цвет в зависимости от сыгранных нот, а звуки производятся пьезопищалкой, благодаря чему потребление у «флейты» очень низкое.

    Давайте сразу посмотрим на результат:


    Выбор компонентов


    Собственно, с пищалок устройство и началось — я увидел у себя среди компонентов пару SMD-пьезоизлучателей от фирмы Murata, а именно PKLCS1212E4001 и аналогичного ему PKLCS1212e2000-r1. Это два обычных пьезоизлучателя весьма небольших габаритов (10 х 12 х 3 мм), один с пиком АЧХ в 4000 Гц, второй — в 2000 Гц. В отличие от динамиков, они почти не потребляют тока, пьезопластинка изгибается при приложении напряжения, поэтому прямоугольный сигнал на частоте 4КГц заставляет пластинку вибрировать с той же частотой, издавая громкий звук. При этом потребление составляет порядка 0.3 мА при 3.3В.
    Раз потребление такое низкое — почему бы не сделать маленькое электронное устройство, питающееся от батарейки? Ведь нам не нужны будут никакие усилители и мощные источники тока. Правда, расплачиваться за это придется очень кривой АЧХ пищалок — нет, они могут воспроизводить произвольный звуковой сигнал, для эксперимента я даже выводил на них WAV, но звучание оставляет желать лучшего, поэтому будем «питать» их обычным меандром. А меняя его скважность будем менять громкость звука.


    АЧХ пищалок

    Тут на руку играет наличие двух разных пьезоизлучателей — в сумме они дадут чуть более гладкую АЧХ и покроют больший диапазон частот.
    Итак, с излучателем звука мы определились. С источником питания тоже никаких вопросов — старая добрая CR2032, литиевая батарейка с очень низким уровнем саморазряда, напряжением от 3В (полностью заряжена), до 2V (полностью разряжена). Для нее у меня нашелся вот такой удобный SMD-держатель:
    image
    SMD-держатель для батареи

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

    image
    Электретный микрофон

    Выбирать ноту будем переменным резистором, опять-таки подойдет любой, но сопротивление лучше взять побольше, чтобы он не тянул много тока из батареи. Я взял 50 КОм.

    image
    Переменный резистор

    Можно заменить резистор несколькими кнопками, игра станет намного удобнее, но при этом увеличатся габариты устройства (и количество проводов, которые придется к этим кнопкам вести, а это, учитывая выбранный способ монтажа, весьма неприятное обстоятельство!)

    Чтобы девайс был поинтереснее, добавим RGB-светодиод. Конечно, это сильно скажется на потреблении, но вряд-ли кто-то будет играть на этой «флейте» достаточно долго, чтобы села батарейка, так что ничего страшного. Я выбрал SMD-светодиод KAA-3528EMBSGC.

    Остается контроллер — взял то, что было под рукой, STM32F100C4, в свое время они по какой-то акции стоили чуть ли не 20 рублей в Терре, и я, не удержавшись, купил их целый пакетик. Контроллер идет в не самом удобном корпусе для монтажа «на коленке» — LQFP48, c шагом между выводами 0.5 мм.

    Собственно, почти все детали представлены на фотографии ниже (и кусочек какой-то старой платы, к которой это все приклеивалось) — к ним добавилась вторая пищалка (на фото только одна, та, что на 4 КГц), пара SMD-кнопок и ручка для потенциометра.


    Детали устройства

    Что касается кнопок — изначально я планировал все ноты выбирать переменным резистором (а нот планировалось две полных октавы), но потом понял, что тогда устройство получится ну совсем неудобным и добавил две кнопки. Одна выбирает «диезы», то бишь смещает текущую выбранную ноту на полтона вверх, а вторая — смещает выбранную ноту на целую октаву. Таким образом, вместо 2 (октавы) * 12 (полутонов)= 24 позиций потенциометра нужно будет отслеживать всего 7, соответствующих семи нотам, а полутона и октавы по необходимости менять, зажимая кнопки.

    Схема устройства


    Схему всего этого дела я, разумеется, не рисовал, вместо этого воспользовавшись софтиной от STM для выбора конфигурации контроллера, MicroXplorer. Получилась вот такая красивая картинка:


    Итак, что и с чем мы будем соединять?
    1. Для начала, нам потребуется два ШИМ-канала для двух наших пищалок. В принципе, это вопрос спорный — можно обойтись одним и обе пищалки поставить в параллель, тогда они будут всегда питаться сигналом одинаковой частоты и скважности.
      Можно выделить по каналу на каждую пищалку, тогда частоты будут совпадать, а скважности (а значит, и форму огибающей!) можно будет задавать индивидуально. И, наконец, можно каждой пищалке назначить отдельный таймер, тогда можно будет задать разные частоты и разные скважности.
      Так как схема будет залита эпоксидкой, после чего поменять в ней что-либо будет невозможно, я решил вывести необходимые для реализации всех вариантов сигналы на маленький отладочный разъем, чтобы иметь возможность поменять решение потом.
      Поэтому берем таймер, скажем, TIM3, и выбираем пару его каналов для вывода ШИМа — это пины PA6 и PA7.
    2. Безусловно потребуются три ШИМ-канала для управления RGB-светодиодом. Учитывая напряжение батареи и ее внутреннее сопротивление, питать будем напрямую через пины контроллера, без резисторов — больше 10 мА по каждому пину мы все равно не отдадим, на синем диоде и вовсе падает столько, что его даже не удастся вывести на максимальную яркость при наших максимальных 3В питания.
      Четвертый канал выведем для упомянутой в п.1 ситуации — если что, будем им пищать.
      Значит, выбираем TIM1 и пины PA8, PA9, PA10, PA11 для вывода ШИМа.
    3. Однозначно придется снимать аналоговый сигнал с микрофона и потенциометра, для этого используем встроенный АЦП — пины PA2 и PA3.
      Раз уж заговорили об аналоговом сигнале, сразу подумаем, как снимать сигнал с микрофона. Традиционная схема включает в себя конденсатор, для отрезания постоянной составляющей, резистивный делитель, чтобы сдвинуть сигнал на половину питания, предусилитель, чтобы использовать весь динамический диапазон АЦП. Мы обойдемся безо всего этого. Постоянную составляющую отрежем программно, динамический диапазон нам не столь важен, поэтому подключим микрофон вот так:


      В результате на выходе получим порядка 2В в спокойном состоянии, и от ~1.5 до ~2.5 при громких звуках рядом с микрофоном.
      Потенциометр, понятное дело, включим как делитель между питанием и землей, выведя среднюю точку на соседний канал АЦП.
    4. Нам потребуется снимать сигнал с двух кнопок — для этого воспользуемся пока свободными пинами PA0 и PA1, подтяжку включим внутреннюю, поэтому просто подключаем пины через кнопку на землю.
    5. Нам безусловно потребуется отладочный интерфейс. В принципе, достаточно вывести SWDIO и SWCLK (а также землю и питание), но на деле очень, очень не помешает пин RESET — так как мы будем настраивать спящий режим, мы останемся без дебага как только контроллер заснет. И перешить его можно будет только STMовской утилиткой при наличии пина RESET, софтварный сброс не будет работать. Так что не будем играть с огнем, а просто вытащим на отладочный разъемчик землю, питание, пины PA13, PA14 и пин NRST
    6. Последний пункт необязательный, но очень облегчает отладку при работе с аналоговыми сигналами — выведем канал DAC на тот же разъем, на который вывели пины для прошивки — с его помощью мы сможем посмотреть любой промежуточный аналоговый сигнал в процессе обработки


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

    Пайка


    Тут описывать особенно нечего, прикладываю несколько фотографий процесса. Микросхема приклеивается «кверху ногами», к каждой требуемой ноге паяется тонкая зачищенная и облуженная проволочка и аккуратно отводится вбок. Когда все проволочки отведены, проверяется соединение и, если все хорошо, микросхема заливается каплей эпоксидки. После этого проволочки можно дергать безбоязненно. Та проволока, что идет кольцом — это земля, контроллер подключается к земле и к питанию с каждой стороны.


    Припаиваем землю. Старая плата пришлась кстати — я использовал ее земляной полигон для объединения земель.


    Припаиваем отладочный интерфейс и проверяем, что контроллер завелся


    Припаиваем все остальные пины


    Заливаем эпоксидкой


    Почти собранное устройство, осталось разместить вторую пищалку и долить эпоксидки сверху


    Все готово

    На этом аппаратная часть закончена, переходим к прошивке.

    Прошивка


    Прошивка довольно простая. Создаем пустой проект под наш STM32F100C и начинаем прописывать инициализацию:

    Инициализация GPIO
    #define GPIO_Red 		GPIO_Pin_9
    #define GPIO_Green 		GPIO_Pin_10
    #define GPIO_Blue		GPIO_Pin_11
    #define GPIO_SHARP		GPIO_Pin_0
    #define GPIO_OCT		GPIO_Pin_1
    #define GPIO_FreePWM	GPIO_Pin_8
    #define GPIO_BUZZER1	GPIO_Pin_6
    #define GPIO_BUZZER2	GPIO_Pin_7
    
    void InitGPIO()
    {
    	GPIO_InitTypeDef GPIO_InitStructure;
        RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE);
    
    	 	 GPIO_InitStructure.GPIO_Pin = GPIO_BUZZER1 | GPIO_BUZZER2
    	 			| GPIO_FreePWM |GPIO_Red|GPIO_Green|GPIO_Blue;
    	     GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    	     GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    	     GPIO_Init(GPIOA, &GPIO_InitStructure);
    
    	     GPIO_InitStructure.GPIO_Pin = GPIO_SHARP | GPIO_OCT;
    	     GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
    	     GPIO_InitStructure.GPIO_Speed = GPIO_Speed_10MHz;
    	     GPIO_Init(GPIOA, &GPIO_InitStructure);
    }
    


    Здесь мы настраиваем наши GPIO — все каналы ШИМа — это выход, управляемый периферией в режиме Push-Pull (GPIO_Mode_AF_PP), кнопки — подтянутые к питанию входные пины. Каналы АЦП и так по умолчанию настроены как аналоговые входы.

    Инициализация АЦП
    #define SIGNAL_OFFSET 	850
    
    void InitADC()
    {
    	ADC_InitTypeDef       ADC_InitStructure;
    	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
    	ADC_InitStructure.ADC_ScanConvMode = ENABLE;
    	ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;
    	ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
    	ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
    	ADC_Init(ADC1, &ADC_InitStructure);
    
    	ADC_InjectedSequencerLengthConfig(ADC1, 2);
    	ADC_InjectedChannelConfig(ADC1, ADC_Channel_2, 1, ADC_SampleTime_1Cycles5);
    	ADC_SetInjectedOffset(ADC1, ADC_InjectedChannel_1, SIGNAL_OFFSET);
    	ADC_InjectedChannelConfig(ADC1, ADC_Channel_3, 2, ADC_SampleTime_1Cycles5);
    	ADC_ExternalTrigInjectedConvConfig(ADC1, ADC_ExternalTrigInjecConv_None);
    	ADC_Cmd(ADC1, ENABLE);
    }
    


    Тут мы настраиваем два канала АЦП. Будем использовать его в режиме «инжектированных каналов», это значит, что у нас есть целых четыре регистра для данных, то есть, мы можем провести до четырех измерений с разных каналов и не заботиться о том, что одни данные перетрут другие.
    Говорим, что нам нужен режим SCAN — то есть, конверсия всех указанных каналов один за другим. Канал 2 отвечает за микрофон, поэтому говорим, что у нас есть оффсет в 850 единиц — это число автоматически будет вычтено из результата конверсии. Чтобы его вычислить, достаточно задать этот оффест нулем и посмотреть, какое значение снимается с АЦП в тишине.

    Инициализация таймеров
    void InitTimers()
    {
    	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
    	RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE);
    
    	TIM_TimeBaseInitTypeDef 	TIM_TimeBaseStructure;
    	TIM_OCInitTypeDef  			TIM_OCInitStructure;
    
    	TIM_TimeBaseStructure.TIM_Period = 0xFFF;
    	TIM_TimeBaseStructure.TIM_Prescaler = 0;
    	TIM_TimeBaseStructure.TIM_ClockDivision = 0;
    	TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
    
    	TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);
    
    	TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
    	TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
    	TIM_OCInitStructure.TIM_Pulse = 0x00;
    	TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
    
    	TIM_OC1Init(TIM3, &TIM_OCInitStructure);
    	TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Enable);
    
    	TIM_OC2Init(TIM3, &TIM_OCInitStructure);
    	TIM_OC2PreloadConfig(TIM3, TIM_OCPreload_Enable);
    
    	TIM_SetCompare1(TIM3, 0x00);
    	TIM_SetCompare2(TIM3, 0x00);
    	TIM_ARRPreloadConfig(TIM3, ENABLE);
    
    	TIM_Cmd(TIM3, ENABLE);
    	TIM_CCxCmd(TIM3, TIM_Channel_1, ENABLE);
    	TIM_CCxCmd(TIM3, TIM_Channel_2, ENABLE);
    
    
    	TIM_TimeBaseStructure.TIM_Period = 0xFFF;
    	TIM_TimeBaseStructure.TIM_Prescaler = 0;
    	TIM_TimeBaseStructure.TIM_ClockDivision = 0;
    	TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
    
    	TIM_TimeBaseInit(TIM1, &TIM_TimeBaseStructure);
    
    	TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
    	TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
    	TIM_OCInitStructure.TIM_Pulse = 0x000;
    
    	TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_Low;
    	TIM_OC1Init(TIM1, &TIM_OCInitStructure);
    	TIM_OC2Init(TIM1, &TIM_OCInitStructure);
    	TIM_OC3Init(TIM1, &TIM_OCInitStructure);
    	TIM_OC4Init(TIM1, &TIM_OCInitStructure);
    
    	TIM_CCxCmd(TIM1, TIM_Channel_1, DISABLE);
    	TIM_CCxCmd(TIM1, TIM_Channel_2, ENABLE); //R
    	TIM_CCxCmd(TIM1, TIM_Channel_3, ENABLE); //G
    	TIM_CCxCmd(TIM1, TIM_Channel_4, ENABLE); //B
    
    	TIM_Cmd(TIM1, ENABLE);
    	TIM_CCPreloadControl(TIM1, DISABLE);
    	TIM_CtrlPWMOutputs(TIM1, ENABLE);
    }
    


    Самая большая инициализирующая функция, настройка двух таймеров. Настраиваем оба на ШИМ, частота того, что управляет светодиодами, будет фиксированной, частота пищащего, разумеется будет меняться. Т.к. системная частота 8 МГц (чем меньше, тем лучше, меньше будет потреблять!), нам придется менять разрядность ШИМа, чтобы достичь требуемых частот на выходе (до 4 КГц+), но об этом позже.

    Теперь рассмотрим реализацию основной функции устройства — обработчика прерывания от системного таймера, тикающего с частотой 1 КГц.
    Изначально я предполагал использование вычисленной мощности захваченного сигнала для модуляции питающего пьезоизлучатели импульса, но оказалось, что на слух это звучит не очень, несмотря на фильтры. В итоге наиболее приятным на слух оказалось самое простое решение: мощность сигнала выступает в роли «спускового крючка», превышение порога (по абсолютному значению и по производной) запускает процесс воспроизведения.
    Огибающая же генерируется процессом, немного похожим на разрядку конденсатора: при превышении порога мы заносим в рабочую переменную Envelope некоторое начальное значение START_VAL — это мгновенная «зарядка» нашего конденсатора. Далее, на каждой обработке прерывания, новое значение сигнала получается из старого умножением на 0.987 — чисто эмпирически подобранное значение, которое, к тому же, зависит от того, с какой частотой идут интеррапты. Таким образом Envelope(t) = START_VAL*0.987^t. Чтобы не использовать софтварные флоаты воспользуемся фиксированной запятой, умножение на 0.987 равно умножению 64684 и делению на 65536 (сдвигу на 16 вправо). То есть,

    #define ENV_DECR		64684	//0,987
    Envelope = (Envelope*ENV_DECR)>>16;
    

    Ограничим выходное значение ClippedEnvelope некоторым числом, скажем, 4000, также подобранным эмпирически. Тогда выходное значение будет равно 4000, когда Envelope больше, чем 4000, либо самому значению Envelope, когда оно меньше. В результате получаем убывающую экспоненту с «полочкой» — небольшим промежутком времени, в течение которого выходной сигнал не зависит от времени и является максимальным.


    Значение ClippedEnvelope

    Этот сигнал можно напрямую задавать в качестве значения скважности, если бы не два НО:
    1. Чтобы изменить частоту звука, придется поменять период таймера, а с нашей системной частотой уже не выйдет 12-битного ШИМа на 4КГц.
    2. Для пьезопищалки ШИМ с максимальной скважностью не отличается от ШИМа с минимальной, поэтому максимальная громкость звука будет при меандре (скважность — 50%).

    Следовательно, период таймера TimerPeriod задается выбранной в данной момент нотой, а максимальное значение регистра сравнения будет равно половине периода (TimerPeriod/2), что будет означать прямоугольный сигнал со скважностью 50%, а значит — максимальную громкость звука.
    Тогда на данной частоте значение нашего сигнала ClippedEnvelope, равное 4000 должно задавать этот самый максимум в половину от периода, значит,

    u16 OutEnvelope = (TimerPeriod/2)*ClippedEnvelope/4000;
    

    Значения TimerPeriod будем выбирать по таблице, которую можно либо предпросчитать при старте по известной формуле image, либо вовсе задать константами, что я и сделал, чтобы сохранить точность. Формула задает отношения частот (и, соответственно, периодов) для равномерно-темперированного строя. Нам остается только выбрать базовую ноту, от которой мы будем отсчитывать все остальные — я взял ноту До третьей октавы на частоте 1046.5 Гц. Для текущего значения частоты контроллера (8 МГц), соответствующий период равен 7644 тикам таймера.
    При этом для воспроизведения нот следующей октавы нам достаточно поделить текущее значение периода на 2. А для воспроизведения «диезов» (на пол тона выше) — поделить период на image.
    Чтобы не заводить второй массив с предпросчитанными «диезами» снова воспользуемся фиксированной запятой — поделить на 1.059463 значит помножить на 61858 и разделить на 65536.
    Проверим наши вычисления: согласно таблице в википедии, частота ноты До-диез (C#), равна 1108.7 герц. Наш период для До равен 7644.
    c_sh = (7644*61858)>>16 = 7215
    Разделим частоту таймера (8 000 000) на полученный период, получаем 1108.8 Гц — весьма близко.

    Практически все вопросы рассмотрели, осталась иллюминация — давайте сначала реализуем вспомогательную функцию для вычисления цвета светодиода, а потом перейдем к коду обработчика прерывания.

    Вычисление RGB-компонентов цвета
    void Spectrum(u8 position, u32* r, u32* g, u32* b)
    {
    	if(position<85)
    	{
    		*r=85-position;
    		*g=position;
    		*b=0;
    	}
    	if(position>84&&position<170)
    	{
    		*r=0;
    		*g=170-position;
    		*b=position-85;
    	}
    	if(position>169)
    	{
    		*r=position-170;
    		*g=0;
    		*b=255-position;
    	}
    	*r*=3;
    	*g*=3;
    	*b*=3;
    }
    


    Для этого предлагаю вот такую реализацию. Байтовый параметр position указывает, в какой точке спектра мы находимся, а функция записывает байтовые же значения R, G и B в переданные ей указатели (указатели большей размерности для сопряжения с остальными частями кода).
    Реализация очень простая и прозрачная — если мы посмотрим на спектр, то увидим, что можно выделить 3 фрагмента равной длины. От нуля до 1/3 спектра интенсивность красного цвета падает с максимума до нуля, одновременно с этим интенсивность зеленого растет от нуля до максимума (покрывает красный-оранжевый-желтый-зеленый части). От одной третьей до двух третьих то же самое происходит с зеленым (падает до нуля) и синим (растет до максимума) цветами (покрывает зеленый-голубой-синий части), и наконец, последняя часть — красный снова набирает силу, а синий снижает интенсивность, покрывает синюю-фиолетовую части и снова закольцовывается в красную.

    В качестве входного параметра можно взять положение потенциометра, но еще лучше — взять, скажем, три последних положения — тогда даже сыграв три ноты подряд мы все равно увидим смену цветов. «Байтовость» входного значения нам поможет — не будем ничего изобретать, а просто сложим все три положения потенциометра, т.к. на выходе в любом случае получим число из диапазона 0-255.
    Заодно не забудем, что у нас есть еще вторая октава и кнопка выбора «диезов», так что учтем и их,

    ResistorValue = ((res*highOctave)+sharp)>>5;
    

    Здесь res — значение, считанное с канала АЦП, highOctave принимает значения 1 и 2 в зависимости от нажатой кнопки выбора октавы, а sharp добавляет небольшое смещение в случае, если нажата кнопка «диезов». Все это мы сдвигаем на 5, т.к. значение с АЦП у нас 12-битное — в итоге, выходное значение будет 8-битное, как того и требует функция вычисления цвета.

    Ниже приведена реализация обработчика прерывания:

    Обработчик прерывания системного таймера
    #define HALF_TONE		61858	//0.9438782
    #define ENV_DECR		64684	//0,987
    
    #define START_VAL 		7500
    #define CLIPPING_VAL	4000
    #define POWER_TH		400
    #define DPOWER_TH		500
    
    #define SLEEP_INTERVAL	10
    
    u16 Notes[7] = {7644, 6810, 6067, 5726, 5102, 4545, 4050};
    u16 ResLin[] = {10, 249, 480, 900, 2000, 3685, 4095};
    
    s32 Envelope=0;
    u32 TimerPeriod=0;
    s32 LastPower=0;
    u16 ResistorValue[3];
    u32 Sleeped = 0;
    u16 sharp=0, highOctave=0;
    
    void SysTick_Handler()
    {
    	volatile s16 mic;
    	volatile u16 res;
    
    	mic = (s16)ADC_GetInjectedConversionValue(ADC1, ADC_InjectedChannel_1);
    	res = ADC_GetInjectedConversionValue(ADC1, ADC_InjectedChannel_2);
    	ADC_SoftwareStartInjectedConvCmd(ADC1, ENABLE);
    
    	s32 power = mic;
    	power = (power*((s32)mic))>>9;
    	s32 dP = power-LastPower;
    	LastPower=power;
    
    	//linearizing resistor
    	u8 n;
    	for(n=0;n<6;n++)
    		if(res<ResLin[n])
    			break;
    
    	if(power>POWER_TH && dP> DPOWER_TH)
    	{
    		//got signal!
    		Sleeped=0;
    		TimerPeriod=Notes[n];
    		sharp=0;
    		highOctave=1;
    		if(!GPIO_ReadInputDataBit(GPIOA, GPIO_SHARP))
    		{
    			sharp = 0x700;
    			TimerPeriod=(TimerPeriod*HALF_TONE)>>16;
    		}
    
    		if(!GPIO_ReadInputDataBit(GPIOA, GPIO_OCT))
    		{
    			TimerPeriod/=2;
    			highOctave = 2;
    		}
    		if(Envelope<CLIPPING_VAL)
    		{
    			ResistorValue[0] = ResistorValue[1];
    			ResistorValue[1] = ResistorValue[2];
    			ResistorValue[2] = ((res*highOctave)+sharp)>>5;
    		}
    		TIM_SetAutoreload(TIM3, TimerPeriod);
    		Envelope=START_VAL;
    	}
    	Envelope = (Envelope*ENV_DECR)>>16;
    	if(Envelope<50)
    	{
    		Envelope=0;
    		Sleeped++;
    		StopPeripherals();
    		u32 interval=SLEEP_INTERVAL;
    		if(Sleeped>1000)
    			interval*=8;
    		Stop(interval);
    	}
    	u16 ClippedEnvelope=Envelope;
    	if(Envelope>CLIPPING_VAL)
    		ClippedEnvelope=CLIPPING_VAL;
    
    	u16 OutEnvelope = (TimerPeriod/2)*ClippedEnvelope/4000;
    
    	//Debug DAC
    	//DAC_SetChannel1Data(DAC_Align_12b_R, dP);
    
    	u32 r=0,g=0,b=0;
    	u8 sPos = 0;
    	for(u8 i=0;i<3;i++)
    		sPos+=ResistorValue[i];
    	Spectrum(sPos,&r,&g,&b);
    	r*=ClippedEnvelope;
    	g*=ClippedEnvelope;
    	b*=ClippedEnvelope;
    	r>>=11;
    	g>>=9;
    	b>>=8;
    	TIM_SetCompare1(TIM3, OutEnvelope);
    	TIM_SetCompare2(TIM3, OutEnvelope);
    	TIM_SetCompare2(TIM1, r);
    	TIM_SetCompare3(TIM1, g);
    	TIM_SetCompare4(TIM1, b);
    }
    


    Здесь мы почти все рассмотрели, за исключением линеаризации потенциометра — оказалось, что он жутко нелинейный ближе к краям диапазона. Так, например, нам необходимо поделить его максимальный угол поворота (порядка 275 градусов) на 7 секторов, но оказывается, что почти весь первый сектор значение, снимаемое с АЦП равно 0. Ближе к концу первого сектора оно начинает резко возрастать, дальше идет линейная часть, после чего, ближе к концу последнего сектора, снова резко возрастает до предельного значения. Спасает проградуированная шкала, по которой я крутил потенциометр примерно на требуемый угол и смотрел реальное значение АЦП, которое после занес в массив ResLin.
    Второй момент, который может броситься в глаза — сдвиговые операции после вычисления цвета. Во-первых, полученные значения цветовых компонентов необходимо умножить на огибающую, чтобы диод вспыхивал в такт музыке. Следовательно, мы уже получаем 12 бит + 8 бит = 20 бит. Необходимо сдвинуть результат на 8, чтобы не допустить переполнения.
    А разница в величинах сдвига объясняется тем, что на диодах разных цветов падает разное напряжение, из-за чего красный диод при том же значении ШИМа светит ярче, чем зеленый, и, тем более, чем синий. Выставив разный сдвиг мы немного компенсируем это, чтобы белый цвет выглядел действительно белым, а не желтоватым.

    Последний этап — это энергосбережение. В активном режиме устройство потребляет 5.5 мА (и до 10-15 при вспышке светодиода). Приемлемо для работающего устройства, но совершенно неприемлемо для ждущего сигнала. Будем уводить устройство в сон, как только значение огибающей достигнет 0 (точнее, когда достигнет 50, после мы приравниваем его нулю вручную, т.к. экспонента еще долго будет добираться до нуля, а пищалкам хватает даже скважности в 1/4096, чтобы издавать звук).
    Поэтому настраиваем RTC на частоту 1 КГц:

    Настройка RTC
    void InitRTC()
    {
    	RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);
    	PWR_DeInit();
    	PWR_BackupAccessCmd(ENABLE);
    	RCC_LSICmd(ENABLE);
    	while(RCC_GetFlagStatus(RCC_FLAG_LSIRDY) == RESET);
    	RCC_RTCCLKConfig(RCC_RTCCLKSource_LSI);
    	RCC_RTCCLKCmd(ENABLE);
    	RTC_WaitForSynchro();
    	RTC_WaitForLastTask();
    	RTC_SetPrescaler(40);
    	RTC_WaitForLastTask();
    	while(RTC_GetFlagStatus(RTC_FLAG_SEC|RTC_FLAG_ALR) == RESET);
    
    	EXTI_InitTypeDef EXTI_InitStructure;
    	EXTI_InitStructure.EXTI_Line = EXTI_Line17;
    	EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
    	EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising;
    	EXTI_InitStructure.EXTI_LineCmd = ENABLE;
    	EXTI_Init(&EXTI_InitStructure);
    
    	NVIC_InitTypeDef NVIC_InitStructure;
    
    	NVIC_InitStructure.NVIC_IRQChannel =  RTCAlarm_IRQn;
    	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
    	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
    	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    	NVIC_Init(&NVIC_InitStructure);
    }
    



    Определяем вспомогательные функции — заснуть на заданное количество миллисекунд, отключив периферию, включения периферии обратно и обработчик прерывания RTC Alarm:

    Функции энергосбережения
    void Stop(u32 delay)
    {
    	RTC_SetCounter(0);
    	RTC_WaitForLastTask();
    	RTC_SetAlarm(RTC_GetCounter()+delay);
    	RTC_WaitForLastTask();
    	PWR_EnterSTOPMode(PWR_Regulator_ON, PWR_STOPEntry_WFI);
    }
    
    void StopPeripherals()
    {
    	ADC_Cmd(ADC1, DISABLE);
    	TIM_Cmd(TIM1, DISABLE);
    	TIM_Cmd(TIM3, DISABLE);
    }
    
    void StartPeripherals()
    {
    	ADC_Cmd(ADC1, ENABLE);
    	TIM_Cmd(TIM1, ENABLE);
    	TIM_Cmd(TIM3, ENABLE);
    	ADC_SoftwareStartInjectedConvCmd(ADC1, ENABLE);
    	SysTick_Config(SystemCoreClock/1000);
    }
    
    void RTCAlarm_IRQHandler()
    {
    	EXTI_ClearITPendingBit(EXTI_Line17);
    	StartPeripherals();
    }
    



    Чтобы система адекватно себя вела во время игры, будем засыпать на SLEEP_INTERVAL = 10 мс, это совсем незаметно, однако снижает потребление до 1.1 мА. А чтобы не тратить лишнюю энергию, когда девайс отложили, будем отсчитывать наши интервалы в переменной Sleeped, и если насчитали больше 1000 (около 10 секунд) без входного сигнала — начинаем засыпать на SLEEP_INTERVAL*8, сокращая потребление до 0.7 мА.

    К сожалению, сильнее сократить потребление мне не удалось — не самый подходящий для низкого потребления контроллер, потенциометр с сопротивлением ниже, чем следовало бы (я планировал 50К, надо бы 100К, а впаял, я, похоже, по ошибке, вовсе на 24К), возможно — какие-то небольшие утечки через цепи питания микрофона и прочее. Впрочем, для игрушки сойдет, батарейка имеет емкость 150 мАч, так что ее должно хватить почти на 9 суток ожидания.

    На этом у меня все, удачных вам девайсов!
    Метки:
    Поделиться публикацией
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 45
    • –15
      Идея потрясающая. Зацепило.

      image
      • 0
        Давно хочу такой девайс, только c нормальным звуком. Что-нибудь типа карманной электрофлейты.
        • НЛО прилетело и опубликовало эту надпись здесь
          • 0
            Некую UHU Plus SCHNELLFEST, на хрупкость не проверял, но обещают 130 кг на отрыв. Склонен верить, т.к. видел склеенную ей металлическую стойку в магазине, где мне ее продали)
            • НЛО прилетело и опубликовало эту надпись здесь
              • НЛО прилетело и опубликовало эту надпись здесь
                • НЛО прилетело и опубликовало эту надпись здесь
                • 0
                  Если надо залить плату от воздействия атмосферы, может лучше силиконовый герметик? он точно не треснет…
                  • 0
                    Раньше, вроде как, эпоксидкой заливали устройства. До того, как печатные платы появились. Неремонтопригодно, зато на века.
                    • 0
                      Раньше силиконового герметика небыло, да и иногда все же нужна была жесткость конструкции и механическая защита. С герметиком проще работать, но он не обеспечивает жесткости и механической защиты. А если просто надо защитить плату от воздействия атмосферы, есть аэрозольные лаки с силиконом и без. Не обязательно всегда возится только с эбокситкой. Хотя если ставится задача надежно скрыть потроха конструкции от реверс-инженеринга то эбокситка — то что нужно.
                      • 0
                        А я термоклеем заливаю. Не треснет, при этом можно феном расковырять обратно.
              • 0
                Скажите, а почему формирование звука делается ШИМом а не встроеным ЦАП? я почему-то думал что так проще.
                Можете рассказать плюсы/минусы использования ЦАП для формирования звука?
                • 0
                  Ну так в данном-то случае у меня на выходе стоят пищалки, что мне на них ЦАПом выводить — синус или семплы не шибко с них звучат.
                  Я в первой версии девайса генерил звук гитарной струны известным алгоритмом и выводил его через ЦАП на пищалки, результат меня не порадовал (хотя если выводить на динамик, то звучит нормально).
                  Поэтому я остановился на прямоугольном сигнале — он богат гармониками, да и пищалки в таком режиме звучат намного громче.
                  Вот и все плюсы, они продиктованы исключительно устройством вывода звука.
                  • 0
                    Может быть, глупый вопрос, но нельзя ли программно компенсировать АЧХ?
                    • 0
                      Отчасти, может, и можно, ценой амплитуды сигнала — то есть, наверное, можно сделать фильтр, который сгладит ее пики.
                      Но что делать со впадинами?) Я имею в виду, что до 1 КГц и после 6 КГц она очень сильно спадает, никакие фильтры не заставят пищалку воспроизводить звук с нормальной громкостью на 500 Гц или на 8 КГц.
                • 0
                  Забавный звук получился.
                  Хотелось бы уточнить, вы действительно в эту флейту дуете или это просто для антуража?
                  • 0
                    Действительно, там же микрофон для этого — в статье описано, как сигнал с него управляет огибающей звука.
                    • 0
                      Действительно, не тем местом читал статью. Пардон.
                    • 0
                      И что сие должно символизировать?
                      • +1
                        Сие должно символизировать существование возможности сделать аккуратную плату, имея принтер, утюг, текстолит, хлорид железа (при необходимости заменяется на что-то более доставаемое) и старенький глянцевый «плейбой».

                        Ничего не имею против макеток, если устройство не сложное, но ровно до тех пор пока тут не появляются корпуса с шагом 0.5мм
                        • 0
                          Я же специально для вас оставил ремарку в статье
                          >Если вы не прочь потратить это время на возню с хлорным железом, то можно развести описанную схему на небольшой плате и пропустить следующие пункты.

                          Прошло то время, когда я делал платы ЛУТом и резистом, я зарекся брать хлорид в руки. Теперь — только заказные. Но для игрушки я заказывать плату не собирался и обошелся dead bug методом.
                          • 0
                            Хлорное железо и правда ядреное, но кроме него имеется масса других «подручных» протравливающих растворов — например смесь столовой соли с медным купоросом(удобрение) которое можно вылить после использования и не хранить.
                            • +1
                              Да не, я не столько из-за самого железа, сколько вообще из-за процесса. Ну вот вообще душа к этому не лежит, надоело.
                              Проектировать — отлично. Собирать — нормально. Но саму плату делать совсем не хочется.
                              • +1
                                Есть средство еще лучше — перекись водорода из аптеки (3%), лимонная кислота и повареная соль. Соотношение — 100мл/25гр/5гр, хватает на 100см2 меди. Не оставляет следов, как ХЖ, и скорость травления одна из самых высоких.
                                • 0
                                  Подтравы?
                                  • 0
                                    Не замечал. Последние 3 платы травил этим способом — все получились отлично (ЛУТ по методу DIHALTа, 2 стороны, дорожки 0.2мм, раствор подогревал горячей водой до 50-60 градусов).
                                    Про метод с лимонной кислотой и сравнение с другими доступными способами узнал тут.
                      • +1
                        Флейта-ксилофон получилась. Или духовой ксилофон.
                        • 0
                          Игрушка получилась очень забавная, возникла только пара комментов:

                          1. Чтобы снизить потребление, потенциометр можно питать от вывода порта в активном режиме, и переводить его в 0 или Z перед засыпанием, и обратно в 1 при просыпании по началу дутья. Пот можно кстати взять движковый, тогда получится тромбон ;). И естественно, с характеристикой A

                          2. Значенние постоянной составляющей лучше все-таки вычислять усреднением и вычитанием среднего — параметры микрофона могут плыть со временем, да и требование индивидуальной прошивки под каждый экземпляр — не комильфо.
                          • 0
                            Да, питать потенциометр точно нужно было от GPIO. В принципе это еще возможно пофиксить — провод питания к нему идет достаточно близко к поверхности эпоксидки, можно перерезать, а вместо него подключить один свободный с отладочного разъема.
                            Интересно, насколько упадет потребление.
                            О движковых думал, да, но что-то мне показалось, что будет еще менее удобно, чем с крутилкой — очень уж он короткий.

                            В данном случае это не важно, не уплывут они настолько, чтобы это было критично. Да и просто так вычислять среднее не получится, где гарантия, что в данный момент нет фонового шума?
                          • 0
                            Потребление упадет ровно на ток, протекающий через резистор в 24кОм под напряжением в 3В — то есть на 125 микроампер, и устройство будет способно жить месяцами от 2032

                            Сигнал с микрофона симметричный, поэтому среднее значение не изменится при наличии шума на входе. Усреднять можно при подаче питания в течение нескольких секунд, и запоминать дальше на все время работы, чтобы не тратить ресурсы на скользящее среднее
                            • 0
                              Спасибо, я еще помню закон Ома. Но я вроде как еще в статье сказал, что не уверен в значении резистора, который впаял, поэтому мне по-прежнему интересно, насколько же упадет потребление.
                              Каким образом устройство «будет жить месяцами», если сейчас оно потребляет порядка 750 мкА? Ну станет потреблять в лучшем случае 625. А если я взял тот резистор, который и собирался, то уменьшится не на 125, а всего на 60 мкА и разницы почти не будет.

                              Сигнал симметричный на определенном интервале времени, если взять интервал меньше, запросто можно получить «перевес».
                              • 0
                                600 мкА жрет скорее всего потому, что ждете не в Standby, а в Stop. Из Standby, правда, сложновато выходить — но можно попробовать завести watchdog на скажем раз в 200 мс, и просыпаясь по нему, быстро оценивать (в течение сотни миллисекунд) уровень сигнала микрофона — если на входе сигнал есть, то дальше подавлять WDT, и работать по Вашему алгоритму. Если нет — отправлять процессор опять в Standby и ждать следующего WDT wakeup. В Standby в ядре все остановлено, и потребление не превышает единиц микроампер.

                                Перевес не страшен — погрешность оценки DC за пару секунд при холодном старте будет ничтожна по сравнению с флуктуациями постоянной составляющей микрофона от времени или от экземпляра к экземпляру.
                                • 0
                                  Да нет, не поэтому. Вы, вероятно, путаете Stop и Sleep. Sleep в самом деле самый неглубокий режим сна, Стоп же почти не отличается от Standby — отличие только в том, что питается RAM, а вся остальная периферия, как и в Standby-режиме, отключена, включая системный клок (разница между режимами Standby и Stop в единицы мкА). В Standby РАМ отключается. RTCAlarm, как в моем девайсе, может выводить и из Стоп и из Standby, никакой разницы в реализации нет, Stop я использовал только для того, чтобы не терять содержимое памяти (нет желания возиться с Backup регистрами).

                                  Сейчас система работает точно так как вы и описали, посмотрите на код. Уходит в сон на 10 мс (в случае, если недавно был сигнал), либо на 80 мс (если сигнала не было более 10 сек). Просыпается — входит в прерывание системного таймера, где сразу же оценивается уровень мощности сигнала. И все повторяется.

                                  Тем не менее, потребление такое, как я сказал. Кроме того, даже если ввести в Standby (именно в Standby) сразу же после старта, до всяких настроек, потребление не упадет ниже 0.56 мкА.

                                  Если пару секунд, то перевес не страшен. Но ни от каких флуктуаций от времени не спасет, раз, как вы сказали,
                                  >запоминать дальше на все время работы
                                  Сейчас там и есть это среднее значение, просчитанное за несколько секунд заранее, поэтому разницы никакой, если не менять микрофон. А т.к. он залит эпоксидкой и не подлежит замене, то и вовсе никакой.
                                  • 0
                                    По даташиту потребление в Standby — единицы, а не сотни микроампер. Если у Вас SoC спит в нужном режиме, тогда забираю свое предположение обратно — потребление будет определяться скважностью фаз бодрствования и сна :) Куски кода, честно говоря, не смотрел

                                    Однако, кажется, есть еще один источник потребления — электретный микрофон с 10КОм в плюсовом плече. Если напряжение на нем порядка двух вольт, как Вы пишете, то свои 100 микроампер цепь его подпитки гарантированно забирает. Соответственно — запитать электрет можно также от выводов порта.
                                    • 0
                                      Ну так я это тоже упомянул в статье)

                                      >потенциометр с сопротивлением ниже, чем следовало бы (я планировал 50К, надо бы 100К, а впаял, я, похоже, по ошибке, вовсе на 24К), возможно — какие-то небольшие утечки через цепи питания микрофона

                                      Если информация из даташита в самом деле абсолютно верна, то, конечно, это указывает на то, что во сне потребляет, в основном, не контроллер — я специально уводил его в самый глубокий режим сна до всяких инициализаций, для проверки, потребление было порядка 500-600 мкА.

                                      Жаль, резистор питания микрофона залит толстенным слоем эпоксидки, уже не проверить, как упало бы потребление при питании потенциометра и микрофона от порта…
                                      • 0
                                        Все-таки перепилил дорожки, вместо питания отрубил от микрофона и потенциометра землю.
                                        Потребление в режиме Stop сразу после старта контроллера — 360-330 мкА, не снижается. Возможно, это погрешности измерения, хотя великоваты.
                                        • 0
                                          Значит, в ядре что-то продолжает крутиться, чудес не бывает — SoC с fully static architecture на кмоп-технологии в статике вообще ничего жрать не должны, это не 8080.
                                          По даташиту RC-генератор вотчдога берет всего несколько микроампер, остальное — это скорее всего, тактирование периферии, если само ядро спит.
                                          BTW, если будете специально тестировать low-power режимы, сообщите сюда плз — STM32 — прекрасная замена AVR в DIY-штуках, с Атмела пора уже слезать, но вот в автономном питании как раз и есть сомнения
                                          • 0
                                            Да не выполняется там тактирование периферии. Я склонен думать, что это-таки внешние утечки.
                                            Смотрите сами — я ввожу в режим Standby сразу — после старта. Весь другой код удалил. Более того, потом я еще и стал переводить все пины перед этим в аналоговый режим — это отключает триггер шмидта на входе.
                                            И потребление не падает ниже 300-330 мкА. Все тактирование однозначно отключено — это гарантировано, во-первых, режимом Standby, а во-вторых, если уж нет веры ему — тем, что никакая инициализация не проводилась и все тактирование и так выключено.

                                            Остаются только внешние утечки. Тем более, с таким-то монтажом.

                                            АВР, к сожалению, предлагает намного более низкое потребление. Они сместили свои приоритеты в ответ на выталкивание их с рынка «контроллеры для всех» со стороны СТМ. Тягаться с ними СТМки не могут — разве что, те что входят в серию низкопотребляющих, «L». И то, вроде бы, даже по даташиту, они потребляют слегка больше.
                                            • 0
                                              Это все очень странно — с одной стороны, 300 микроампер утечек — это слишком много, даже для эпоксидки итд. С другой — вот кусок даташита на насчет потребления в low-power режимах:
                                              image — судя по нему, в спящих режимах (даже в Stop с сохранением памяти) STM32 запросто может использоваться в системах с автономным питанием практически наравне с AVR. Другое дело, что в рабочем они тянут заметно больше — но тут очевидно сказывается гораздо более высокая тактовая частота * большее количество переключаемых вентилей

                                              PS — у меня есть F103 на eval board с питанием через USB и встроенным стабом 5 -> 3.3. Надо будет зацепиться после него и посмотреть, сколько он тянет. Плата там сделана нормально, утечек быть не должно
                                              • 0
                                                Ну AVR разные есть, новые тиньки могут работать от 1V и потребляют во сне 0.1 мкА.
                                                Впрочем, их, все-таки некорректно сравнивать с СТМкой. Думаю, СТМки из L серии вообще отлично подойдут на замену любых AVR.

                                                Да, 300 мкА действительно много — это сопротивление порядка 10К. В схеме только керамический конденсатор на 4.2 мкФ между питанием и землей батарейки. Контроллер все пины переводит в аналоговый инпут, значит земля микрофона и земля потенциометра висят в воздухе (на Z). Средний отвод от потенциометра тоже подключен к пину Z, и третий — к батарее.
                                                Питание микрофона по-прежнему подключено к батарее. Питание светодиода подключено к батарее, земли всех трех — Z.
                                                Кнопки — обычные, нормально-разомкнутые, между землей и пинами, которые в Z.
                                                Больше, вроде бы, ничего и нет. Остается только неотмытый флюс, эпоксидка, неудачные паянные соединения (может быть, где-то замкнуло через микроскопическую перемычку).
                              • 0
                                «Флейта» на STM32… переходник для геймпада из Raspberry Pi… Куда катится мир ?!
                                • 0
                                  Ну а почему бы и не STM32? У него есть объективные преимущества — встроенные ЦАП и АЦП с очень удобными фичами (возможность задать оффсет сигнала, инжектированные каналы, scan-режим — это из того, что используется в девайсе), большое количество удобных таймеров, включая системный, гибкий вочдог с ртц.
                                  При этом данный чип стоил 19-20 рублей.
                                  Какой бы предложили вы?
                                • 0
                                  Что-нибудь аналоговое (смайл).
                                  Я бы ничего не предложил — просто представьте в конце предыдущего комментария смайл.
                                  • НЛО прилетело и опубликовало эту надпись здесь

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