Pull to refresh

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

Reading time 16 min
Views 60K

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

На самом деле, девайс весьма далек от флейты по спектру извлекаемого из него звука. Зато у него есть 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 суток ожидания.

На этом у меня все, удачных вам девайсов!
Tags:
Hubs:
+48
Comments 45
Comments Comments 45

Articles