Ниже описан мой первый опыт общения с программируемым микроконтроллером в лице STM32VLDiscovery, результатом которого явилась машинка из LEGO, управляемая с телефона, и ещё кое-что. Я постарался изложить свой путь в виде пошагового руководства к действию, но, предупреждаю сразу, не руководства «как делать правильно». Первые два раздела являются предисловием и не относятся непосредственно к данному микроконтроллеру.
Содержание:
- Первый контакт с микроэлектроникой.
- Строительство машинки на непрограммируемых логических элементах.
- Начало работы с STM32. Среда разработки. Подключение микроконтроллера к машинке.
- Определяем угол поворота передних колёс — обратная связь через АЦП.
- Использование ЦАП для воспроизведения звуков.
1. Первый контакт с микроэлектроникой
Неважно почему, но возникла мысль: сделать устройство, воспринимающее команды с телефона. Как это сделать? Можно через Bluetooth, WiFi, USB и т.д. Но проще и универсальней мне показалось распознавание звуков, которые можно брать с выхода на гарнитуру мобильного телефона (здесь и далее для решения задач будут выбираться самые простые в реализации, результат-ориентированные методы).
Звуки можно синтезировать специальной программой для смартфона, но есть вариант интересней — DTMF-сигналы. Они стандартны и используются в любом мобильном и в большинстве стационарных телефонов (при наборе номера или в голосовом меню), без проблем передаются в голосовом канале.
Задача 1: Распознавание DTMF-сигналов
Из Интернетов распечатываются три варианта реализации DTMF-дешифрации (моя идея не нова), с ними — в известный магазин. Покупается:
- Дешифратор MT 8870DE — из входного аналогового сигнала получает двоичный код (4 бита) последней распознанной DTMF команды и 1 бит — наличие/отсутствие DTMF команды сию секунду (назовём его «пятый вывод»).
- Кварцевый резонатор, конденсаторы и резисторы, необходимые для работы дешифратора.
- 7-сегментный индикатор — для отображения нажатой на телефоне цифры.
- КР 514ИД-1 дешифратор для 7-сегментного индикатора — из двоичного кода получает 7 выходов для каждого из сегментов.
- Макетная плата для монтажа без пайки.
- Проводки/перемычки для макетной платы.
Собираем, проверяем, исправляем ошибки… работает. Шок.
Теперь всё это нужно заставить управлять машинкой. Причём, чтобы действия машинки логично соответствовали нажатой цифре: 2 — вперёд, 4 — влево, 6 — вправо, 8 — назад.
Задача 2: 4 определённые комбинации сигналов с DTMF-декодера должны активировать одну из 4-х функций
Простейший вариант — десятичный дешифратор. Был куплен К155ИД10 с открытым коллектором — 4 входа для двоичного кода, 10 выходов символизируют числа от 0 до 9, на нужном выходе будет «0», на остальных — «1». Данный дешифратор применяется с нагрузками, рабочий ток в которых может достигать 80 мА (лампочки накаливания, реле), что удобно. Хотя транзисторы всё равно пришлось использовать.
Задача 3: Смена полярности для моторов, Н-мост
Всё детство я собирал машинки из LEGO, которые ездили вперёд-назад ручным переключением полярности батареек (наборов LEGO с моторами и пультами у меня никогда не было и нет). Теперь пришло время играть в игрушки по-взрослому.
Нормальные люди делают Н-мост на транзисторах. Продвинутые — покупают готовое решение на этих самых транзисторах. Я сделал на переключающихся электромагнитных реле. Позже я купил мощные (на коллекторный ток до 3А) NPN и PNP транзисторы TIP31 C ST и TIP32 C ST, собрал из них этот самый Н-мост, но он почему-то работал только со встречно включёнными светодиодами, а мотор крутить был не в силах. Не знаю, почему.
Переключающихся реле требуется по два на каждый мотор (транзисторов или замыкающихся реле требуется по 4). По умолчанию, оба контакта моторчика (обычного коллекторного, как в игрушках) соединены с минусом. Если на одно из реле подать управляющий сигнал, контакт моторчика переключится на плюс, мотор заработает. Огромное преимущество такого подключения — ни при каких условиях невозможно даже кратковременное короткое замыкание по вине реле. Недостаток — при снятии управляющего напряжения с реле мотор закорачивается и становится тормозом. Решение — ещё одно реле, замыкающее. Теперь для вращения мотора нужно замыкать и его тоже, для свободного хода — размыкать, для «торможения» — снова замыкать. Это реле будет добавлено позже.
Задача 4: Собрать машинку
Моторов в машинке два: один через LEGOвский дифференциал крутит задние колёса, второй — поворачивает передние. Пластмассовые шестерни в редукторах не добавляют системе КПД, но всё это работает.
Ставим на плату 4 реле. Мне говорили, что макетная плата не рассчитана на большие токи (каждый мотор потребляет по 1-1,6 А), но если я не получу результат в короткие сроки, проект рискует остаться очередным воздушным замком. Кстати, включение любого реле вызывало броски напряжения, что приводило к зацикливанию и сбоям в работе. Наверняка это решается специальными схемами, но я просто запитал моторы от отдельного блока с батарейками.
Электромагнитные реле, два источника питания — не многовато ли хардкора? Дальше больше!
Осталось подключить всё это к десятичному дешифратору. Нужные выводы к эмиттерам 4-х NPN-транзиторов BC547, к базе которых сигнал с DTMF-декодера о том, что в данный момент нажата клавиша («пятый вывод»), коллекторы — к каждому из реле. Криво, странно, но работает.
2. Строительство машинки на непрограммируемых логических элементах
Предположим, машинка должна уметь одновременно ехать и поворачивать. Логично для этого использовать цифры 1, 3, 7, 9, при нажатии на которые будут срабатывать по два реле. Изначально я планировал реализовать это подключением диодов от эмиттеров транзисторов к десятичному дешифратору, но это всё не работало. Сейчас до меня доходит, что если десятичный дешифратор выдаёт «0», то нужен PNP транзистор, который будет этим «нулём» открываться. Но у меня были только NPN.
Задача 5: Машинка должна ехать и поворачивать при нажатии 1, 3, 7 или 9
В целях развлечения/освоения/использования куплены логические элементы 4-И, 4-ИЛИ, НЕ, И-НЕ, ИЛИ-НЕ, И по одной штуке. Первая же мысль — использовать выводы с десятичного дешифратора и «пятый вывод» с DTMF-декодера в простой логической схеме, которая в итоге будет открывать нужные транзисторы (а через них — реле). Но! Десятичный дешифратор К155ИД10 в качестве «единицы» выдаёт 2 с чем-то Вольта. Ни один из имевшихся в распоряжении логических элементов такой сигнал за «единицу» не считал. Кроме 4-И (КР1533). Если использовать «голые» 4 бита двоичного кода с DTMF-а, то собрать требующуюся логику из имеющихся элементов не представлялось возможным. Нормальный же десятичный дешифратор облегчил бы задачу во много раз. А так получилось такое маленькое издевательство — сделать тяжело, но ведь можно же! (Идея лечь спать и утром съездить за деталями была отвергнута).
Помня, что десятичный дешифратор выдаёт свою «недоединицу» на все выводы, кроме вывода с дешифрованным числом, делаем следующее.
На первый… м… квартет входов элемента 4-И подаём числа 1, 2, 3 и «единицу»; на второй — 7, 8, 9 и «единицу». С выхода первого получим «ноль», если нажата была 1, 2 или 3 (машинка едет вперёд), с выхода второго — если 7, 8, 9 (назад). Иначе оба выхода элемента 4-И — «единицы».
Теперь повороты. Используем 5 оставшихся логических элементов и собираем хитрую схему. От цифр 4 и 6 было решено отказаться (зачем на месте поворачивать колёса?!), но даже составление схемы для поворотов одними только 1 и 7 (налево), 3 и 9 (направо) заставило изрядно напрячься.
В итоге, на плате (пришлось купить ещё одну, побольше) DTMF-декодер, индикатор, дешифратор для него, десятичный дешифратор, 4-И, НЕ, 4-ИЛИ, ИЛИ-НЕ, И-НЕ, 4 транзистора, 4 реле.
Машинка исполняет 6 разных команд, но возвращать передние колёса на середину не умеет. КПД LEGOвских деталей и малая мощность моторчика не позволяют сделать возвратную пружину или резинку.
Реле иногда цокают по несколько раз, причины не ясны (помехи?).
Логика работы с логикой освоена.
3. Начало работы с STM32
Среда разработки
Пришло время перейти от программирования проводками к программированию кодом. О программируемых микроконтроллерах/чипах я имел только смутные представления, что Raspberry Pi — слишком мощно, Arduino — дорого и не комильфо. Тут-то продавец Михаил и выписал мне STM32VLDiscovery за 579 рублей. О нём здесь и не только здесь уже писали, что очень помогало мне в его изучении.
Контроллер можно воткнуть в макетную плату, но не в любую. 6 ножек при этом останутся висеть — без них можно обойтись, благо остаётся ещё несколько десятков. Проверить работоспособность залитой в память программы можно даже не отключая плату от mini-USB шнура, через который программа попала в устройство. И питаться можно от него же.
Оптимальным выбором для начала работы показалась среда CoIDE от CooCox, потребовавшая всего одного танца с бубнами — ручного скачивания определённых файлов в определённую папку. Правда, заливать из неё прошивку в устройство у меня так и не получилось — приходится пользоваться ST-LINK Utility от производителя платы. Говорят, можно компилировать прямо из удобного Sublime Text 2, но длина инструкции поселила в меня слишком много скепсиса, чтобы попробовать.
Учитывая, что на Си я никогда не писал, даже простейшие мигания светодиодами вызывали у меня существенные затруднения. Вдобавок, непонятные регистры, 16-ричная система, разные режимы работы выходов и входов для человека, плохо понимающего назначение подтягивающего резистора… Но метод копипаста чьих-то готовых примеров делал своё дело. В свою очередь, цена устройства позволяет не слишком заморачиваться с правильным подключением аппаратной части. Например, я так и не понял, надо ли подключать каждую ножку GND к земле и не вредно ли питать устройство от 4-х пальчиковых батареек — пока всё живо и работает.
Ближе к коду. Приводить и описывать здесь чужие примеры не буду, но предлагаю на ваш суд несколько своих наработок. Они появлялись в тот момент, когда в Сети не находилось приемлемое для меня решение определённой проблемы.
Задача 6: Задержка
Например, нам хочется включить светодиод, а через некоторое время его погасить. Зачастую задержка реализована пустым циклом, выполняющимся много раз. Способ безусловно прост, но применим только там, где кроме мигания светодиода от платы ничего не требуется. Мы можем прервать цикл только, простите, прерыванием (о них чуть позже) или ставить дополнительные условия выхода из цикла. Иначе плата не делает ничего и ни на что не реагирует, пока длится этот цикл.
Что предлагаю я: используем наш главный бесконечный цикл сразу для всех необходимых в программе задержек, каждый проход увеличивая значение определённой переменной (назовём её «x»). Как только переменная достигает определённых величин, в теле цикла срабатывают определённые условия (их может быть сколько угодно). Недостаток — мы заранее не знаем, сколько точно будет длиться такая задержка. Если это и неважно — подбираем эмпирически нужные значения и пользуемся.
int main(void)
{
unsigned int x=0;
char something;
while(1)
{
if (x>5000)
{
// код
};
if (x==10000)
{
// Другой код. Сработает только один раз, пока не обнулим x.
};
if (something==1)// Некоторое событие
{
x=0; // На следующей итерации цикла всё начнётся сначала
}
if (x<15000)
{
x++;
}; // Чтобы x не прошёл рано или поздно через 0, прекращаем инкрементацию x после 15000.
};
};
Задача 7: Прерывания с заданным интервалом
Предположим, мы не меняли кварцевый резонатор, и частота работы процессора осталась 24 МГц. Создав прерывание при достижении 2400-го отсчёта процессора через специальные инструкции, мы получим выполнение функции TIM6_DAC_IRQHandler (это обработчик прерывания) с частотой 100 Гц — каждые 10 миллисекунд. Приведённый ниже код изначально где-то взят.
unsigned int ti=0;
int main(void)
{
//Включаем порт С и таймер 6
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM6,ENABLE);
TIM6->PSC = 24000 - 1; // Таймер тикает 1000 раз в секунду
TIM6->ARR = 10 - 1; // Прерывание срабатывает на каждой 10-й миллисекунде
TIM6->DIER |= TIM_DIER_UIE; // Разрешаем прерывание от таймера
TIM6->CR1 |= TIM_CR1_CEN; // Начать отсчёт!
NVIC_EnableIRQ(TIM6_DAC_IRQn); // Разрешение TIM6_DAC_IRQn прерывания
};
// Обработчик прерывания TIM6_DAC
void TIM6_DAC_IRQHandler(void)
{
ti++;
TIM6->SR &= ~TIM_SR_UIF; //Сбрасываем флаг после обработки прерывания
}
Главная цель данного прерывания — использовать переменную ti, которая увеличивается точно каждые 10 миллисекунд. Вскоре я выяснил, что прерывания не просто «вклиниваются» в основной код программы, но также могут исказить операцию сравнения или присвоения переменной ti, т. к. в обработчике прерывания она каждый раз изменяется. Может возникнуть ситуация, когда вместо присвоения другой переменной значения перемнной ti, присвоится часть старого значения и часть нового. Среди найденных в Сети решений проблемы, я выделил примерно следующие:
- использование атомарных операций (они не прерываются) — присвоение типа int таковой не является, а использовать нужно именно «длинный» тип;
- использование дополнительных переменных или регистров для выяснения, срабатывало ли прерывание за время выполнения конкретных инструкций — немного громоздко и не слишком понятно (для меня);
- другие варианты...
Я решил эту проблему так. Присваиваем локальной переменной sti изменяющуюся переменную ti до тех пор, пока она не «присвоилась». Можно добавить небольшой «люфт», если точность непринципиальна.
do
{
sti=ti;
} while (ti>1+sti);
Сама по себе локальная переменная sti используется только лишь для того, чтобы не вставлять подобные циклы везде, где нам нужно значение переменной ti.
Далее в программе можно безбоязненно обращаться к переменной sti и даже обнулять её. Я использовал всё это для фиксации времени, в течение которого машинке подаётся та или иная команда, чтобы потом машинка могла повторить маршрут. Естественно, обнуление должно не пропасть даром, поэтому в конце основного цикла программы добавлен нехитрый код:
if (sti==0)
{
do
{
ti=0;
} while (ti>1);
}
Если в момент присвоения или в момент проверки условия выхода переменная ti изменится и будет «представлена» в некорректном виде, цикл повторится. Чем выше частота прерываний, тем чаще условие выхода из цикла будет срабатывать со второго раза. Естественно, если частота прерываний слишком высока, зацикливания не заставят себя ждать. Для контроля количества неудавшихся попыток присвоения я использовал светодиоды.
Задача 8: Светодиоды в качестве дисплея
Иногда для корректировки программы хочется получать значения определённых переменных в процессе работы устройства, а дисплея или индикатора под рукой нет или их не хочется подключать. Тогда можно просто заставить светодиоды помигать нужное количество раз, сделать паузу, помигать ещё раз и т. д. Для этих целей я использую в теле главного цикла следующий код:
// Счёто-мигалка
if ((todisp>0) && (sti%10==0))
{
if ((sti%(todisp*20+100)<todisp*20) && (sti%20<10))
{
GPIOC->BSRR=GPIO_BSRR_BS8;
}
else
{
GPIOC->BRR=GPIO_BRR_BR8;
};
};
В любом месте программы мы присваиваем переменной todisp, например, 3, и светодиод №8 на плате будет мигать по три раза, делая секундную паузу между сериями. Переменная sti инкрементируется каждые 10 миллисекунд (через переменную ti), вместо неё можно использовать переменную x, инкрементирующуюся с каждым проходом главного цикла. В этом случае коэффициенты надо будет немного увеличить.
while (1)
{
do
{
sti=ti;
todisp++;
} while (ti>1+sti);
todisp--;
if ((todisp>0) && (sti%10==0))
{
if ((sti%(todisp*20+100)<todisp*20) && (sti%20<10))
{
GPIOC->BSRR=GPIO_BSRR_BS8;
}
else
{
GPIOC->BRR=GPIO_BRR_BR8;
};
};
}
В данном примере каждый раз, когда из-за прерывания операция присвоения sti=ti прошла некорректно и, следовательно, операция сравнения ti>1+sti возвращает true (что может также произойти, если прерывание случилось в момент сравнения), светодиод будет мигать на один раз больше. В принципе, можно даже замерить, сколько раз мигает светодиод через n минут работы платы и посчитать среднее время работы цикла (исходя из частоты прерываний), а также убедиться, что все эти меры предосторожности при использовании прерываний отнюдь не лишние.
Задача 9: Подключение микроконтроллера к машинке
Проведя достаточное количество экспериментов на светодиодах, можно постепенно менять их на колёса. Берём от старой схемы на базовой логике только самое нужное: DTMF-декодер, индикатор с дешифратором для красоты, реле с транзисторами. О десятичном дешифраторе можно наконец-то забыть!
Выходы с DTMF-декодера соединяем с входами на контроллере, которые мы выбрали для этих целей. Выходы с контроллера (опять-таки, выбранные и инициализированные в программе) через резистор (например, 3 кОм) на базу обычного NPN транзистора (я использовал BC547). Эмиттеры — к «плюсу», коллекторы — на катушку соответствующего реле. Добавляем пятое реле, предотвращающее закорачивание (режим «тормоза») основного мотора, поворотники/габариты по вкусу. «Плюс» для моторов подводится к реле от отдельного блока батареек. Для питания как схемы, так и моторов, я использую пластиковые корпуса (2 штуки) для 4-х пальчиковых батареек/аккумуляторов каждый. Они стоят всего по 40 рублей, а я в детстве вечно мучался со скотчем и фольгой…
У меня получилось так, что провода от DTMF-декодера проходят аккурат над реле, отвечающими за движение вперёд/назад. Возможно, это добавляет помех к уже имеющимся скачкам напряжения при включении реле. В результате, во время презентации перед однокурсниками реле начинали переодически трещать, а назад машинка вовсе отказывалась ехать — реле безостановочно переключалось, издавая равномерный треск. Диоды подключенные параллельно катушкам реле в сторону «плюса», никак не влияли на эту ситуацию. Дорабатываем программу — через каждые 10 миллисекунд проверяем значения входов и реагируем на изменения, только если 6 раз подряд комбинация пяти входов одинакова. Задержка на глаз почти незаметна, проблема с помехами решена. Вот они, главные преимущества программируемых микроконтроллеров!
4. Определяем угол поворота передних колёс — обратная связь через АЦП (аналогово-цифровой преобразователь)
Во второй части я упомянул, что механически выравнять передние колёса на середину не получается. Пришло время решить проблему, т. к. машинка должна уметь ездить по прямой!
Задача 10: Подключение реостата
Решение напрашивается само собой — если не пружина, то пусть сам мотор крутится в обратную сторону, пока не выровняет колёса. Цифровых энкодеров под рукой нет, но есть замечательный реостат на 10 кОм, в плате есть 12-разрядный АЦП (даже 2), в Интернетах есть схема подключения и код. Концы реостата к «плюсу» и «минусу», ползунок — через резистор 1 кОм на вход. Кстати, для целей АЦП можно использовать не любой вход. Я использовал тот, который был указан в примере. Пример кода можно найти прямо в CoIDE для каждой из библиотек, в том числе ADC (не забываем в CoIDE подключить библиотеку ADC). Код копипастом оказывается в моей программе и, что главное, работает.
Теперь устанавливаем реостат в машинку, вал реостата соединяем соосно с выходным валом «редуктора» поворотного механизма — он третий от мотора. Процесс сопряжения неLEGOвских деталей с LEGOвскими всегда доставляет, но мотор-таки умудряется и реостат вращать, и колёса поворачивать. С трудом, надо сказать. От реостата к плате тянется три провода, ещё четыре тянутся от моторов.
Задача 11: Использование данных с реостата
Эмпирическим методом подбираются численные значения от 0 до 4095, соответствующие приемлемой левой и правой границам угла поворота передних колёс при их установке прямо. Вроде, этот диапазон называется мёртвой зоной — в пределах него колёса считаются выровненными на середину. Также я подобрал значения для отключения мотора при достижении максимального угла поворота.
js= 2500; // Примерное значение центра
j — значение входного напряжения от 0 до 4095 (АЦП)
gist=150; // Диапазон мёртвой зоны
full=500; // Граница достижения максимального угла поворота
while (1)
{
//...
if ((x>=max*7) && (inpok)) // inpok равен 0, если ни одна кнопка в данный момент не нажата. Нестрогое равенство приводит к проверке следующих условий на каждой последующей итерации главного цикла.
if ((num==1) || (num==4) || (num==7)) //Влево
{
GPIOC->BRR=GPIO_BRR_BR7;
if (j>0+full)
{
GPIOC->BSRR=GPIO_BSRR_BS6;
}
else
{
if (j<0+full-gist)
{
GPIOC->BRR=GPIO_BRR_BR6;
}
}
}
else
{
GPIOC->BRR=GPIO_BRR_BR1;
}
if ((num==3) || (num==6) || (num==9)) //Вправо
{
GPIOC->BRR=GPIO_BRR_BR6;
if (j<4250-full)
{
GPIOC->BSRR=GPIO_BSRR_BS7; // Включаем реле «мотор вправо»
}
else
{
if (j>4250-full+gist)
{
GPIOC->BRR=GPIO_BRR_BR7; // Выключаем реле «мотор вправо»
}
}
}
else
{
GPIOC->BRR=GPIO_BRR_BR3;
}
// Выравниваем колёса на середину
if ((num==2) || (num==5) || (num==8))
{
if (j<js-gist*3) // Если колёса смещены от центра влево
{
GPIOC->BRR=GPIO_BRR_BR6; // Выключаем реле «мотор влево» (если вдруг реле ещё включено)
GPIOC->BSRR=GPIO_BSRR_BS7; // Включаем реле «мотор вправо»
}
else
{
if (j>js-gist*2)
{
GPIOC->BRR=GPIO_BRR_BR7; Выключаем реле «мотор вправо»
}
};
if (j>js+gist*3)
{
GPIOC->BRR=GPIO_BRR_BR7;
GPIOC->BSRR=GPIO_BSRR_BS6;
}
else
{
if (j<js+gist*2)
{
GPIOC->BRR=GPIO_BRR_BR6;
}
};
};
};
//..
}
Если машинка эксплуатируется в экстремальных режимах, и под воздействием чудовищных перегрузок колёса вдруг изменят угол поворота без соответствующей команды, этот же код вернёт их в требуемое положение. Если же мы нажимаем, например, 4, а колёса уже находятся в крайнем левом положении, реле включаться не будет.
Напомню, некоторый код брался из чьих-то примеров.
/**
*****************************************************************************
* @title ADC_simple.c
* @author Claude
* @date 2010 Dec 29
* @brief ADC Example, Blink a LED according to ADC value
*******************************************************************************
*/
#include<stm32f10x_rcc.h>
#include<stm32f10x_gpio.h>
#include<stm32f10x_adc.h>
#include "stm32f10x.h"
#include "stm32f10x_conf.h"
GPIO_InitTypeDef GPIO_InitStructure;
ADC_InitTypeDef ADC_InitStructure;
unsigned int i,j,js,gist,full,ti=0;
/* Blink a LED, blink speed is set by ADC value */
int main(void)
{
unsigned int x,max;
unsigned int move[10];
unsigned int oldsti=0,sti=0,time[10],tit=0;
unsigned short recheck=5;
unsigned char todisp=0,todisp2=0;
x=0;
unsigned char ji,ju,inp0,inp1,inp2,inp3,inpok,ink,in0,in1,in2,in3,inok,num,oldnum,back;
max=250;
inp0=0;
inp1=0;
inp2=0;
inp3=0;
inpok=0;
ink=0;
back=0;
num=0;
void backreset(void)
{
back=0;
for (ju=0;ju<10;ju++)
{
time[ju]=0;
move[ju]=0;
}
GPIOC->BRR=GPIO_BRR_BR9;
}
GPIO_InitTypeDef PORT,GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);
PORT.GPIO_Pin = (GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_6 | GPIO_Pin_7 | GPIO_Pin_10 | GPIO_Pin_11 | GPIO_Pin_12| GPIO_Pin_8 | GPIO_Pin_9);
PORT.GPIO_Mode = GPIO_Mode_Out_PP;
PORT.GPIO_Speed = GPIO_Speed_2MHz;
GPIO_Init( GPIOC , &PORT);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //включаем тактирование порта A
GPIO_InitStructure.GPIO_Pin = (GPIO_Pin_9 | GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7); //задаем номер вывода
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_Init(GPIOB, &GPIO_InitStructure);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA , ENABLE);
PORT.GPIO_Pin = (GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_5);
PORT.GPIO_Mode = GPIO_Mode_Out_PP;
PORT.GPIO_Speed = GPIO_Speed_2MHz;
GPIO_Init( GPIOA , &PORT);
// input of ADC (it doesn't seem to be needed, as default GPIO state is floating input)
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1 ; // that's ADC1 (PA1 on STM32)
GPIO_Init(GPIOA, &GPIO_InitStructure);
//clock for ADC (max 14MHz --> 72/6=12MHz)
RCC_ADCCLKConfig (RCC_PCLK2_Div6);
// enable ADC system clock
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
// define ADC config
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
ADC_InitStructure.ADC_ScanConvMode = DISABLE;
ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; // we work in continuous sampling mode
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
ADC_InitStructure.ADC_NbrOfChannel = 1;
ADC_RegularChannelConfig(ADC1,ADC_Channel_1, 1,ADC_SampleTime_28Cycles5); // define regular conversion config
ADC_Init ( ADC1, &ADC_InitStructure); //set config of ADC1
// enable ADC
ADC_Cmd (ADC1,ENABLE); //enable ADC1
// ADC calibration (optional, but recommended at power on)
ADC_ResetCalibration(ADC1); // Reset previous calibration
while(ADC_GetResetCalibrationStatus(ADC1));
ADC_StartCalibration(ADC1); // Start new calibration (ADC must be off at that time)
while(ADC_GetCalibrationStatus(ADC1));
// start conversion
ADC_Cmd (ADC1,ENABLE); //enable ADC1
ADC_SoftwareStartConvCmd(ADC1, ENABLE); // start conversion (will be endless as we are in continuous mode)
// debug information
RCC_ClocksTypeDef forTestOnly;
RCC_GetClocksFreq(&forTestOnly); //this could be used with debug to check to real speed of ADC clock
//Включаем порт С и таймер 6
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM6,ENABLE);
TIM6->PSC = 24000 - 1; // Настраиваем делитель что таймер тикал 1000 раз в секунду
// TIM6->ARR = 1000 ; // Чтоб прерывание случалось раз в секунду
TIM6->ARR =4;
TIM6->DIER |= TIM_DIER_UIE; //разрешаем прерывание от таймера
TIM6->CR1 |= TIM_CR1_CEN; // Начать отсчёт!
NVIC_EnableIRQ(TIM6_DAC_IRQn); //Разрешение TIM6_DAC_IRQn прерывания
j= 2500;
js=j;
gist=150;
full=500;
if (back==0)
{
for (ju=0;ju<10;ju++)
{
time[ju]=0;
move[ju]=0;
}
}
while (1)
{
// adc is in free run, and we get the value asynchronously, this is not a really nice way of doing, but it work!
j = ADC_GetConversionValue(ADC1) ; // value from 0 to 4095
/* possible change :
* ADC_ContinuousConvMode = DISABLE
* then on the infinite loop, something like :
*
* ADC_SoftwareStartConvCmd(ADC1, ENABLE); // start ONE conversion
* while(ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET); // wait end of conversion
* j = ADC_GetConversionValue(ADC1) * 500; // get value
*
*/
do {
sti=ti;
} while (ti>1+sti);
//if ((back!=2) || GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_9))
if (oldsti!=sti)
{
oldsti=sti;
if (ink<recheck)
{
in0+=GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_5);
in1+=GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_6);
in2+=GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_7);
in3+=GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_8);
inok+=GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_9);
ink++;
if (ink==recheck)
{
ink=0;
if ((in0==0 || in0==recheck) && (in1==0 || in1==recheck) && (in2==0 || in2==recheck) && (in3==0 || in3==recheck) && (inok==0 || inok==recheck))
{
if (in0==recheck)
{
in0=1;
}
if (in1==recheck)
{
in1=1;
}
if (in2==recheck)
{
in2=1;
}
if (in3==recheck)
{
in3=1;
}
if (inok==recheck)
{
inok=1;
}
if ((back==2) && (inok==1))
{
backreset();
}
if ( (back!=2) && (back!=4) && ((inp0!=in0) || (inp1!=in1) || (inp2!=in2) || (inp3!=in3) || (inpok!=inok)) )
{
x=0;
inp0=in0;
inp1=in1;
inp2=in2;
inp3=in3;
inpok=inok;
oldnum=num;
if (inp0) // Двоично-десятичный дешифратор
{
if (inp1)
{
if (inp2)
{
if (inp3)
{
num=15;
}
else
{
num=7;
};
}
else
{
if (inp3)
{
num=11;
}
else
{
num=3;
};
};
}
else
{
if (inp2)
{
if (inp3)
{
num=13;
}
else
{
num=5;
};
}
else
{
if (inp3)
{
num=9;
}
else
{
num=1;
};
};
};
}
else
{
if (inp1)
{
if (inp2)
{
if (inp3)
{
num=14;
}
else
{
num=6;
};
}
else
{
if (inp3)
{
num=10;
}
else
{
num=2;
};
};
}
else
{
if (inp2)
{
if (inp3)
{
num=12;
}
else
{
num=4;
};
}
else
{
if (inp3)
{
num=8;
}
else
{
num=0;
};
};
};
};
};
}
in0=0;
in1=0;
in2=0;
in3=0;
inok=0;
}
}
};
// Счётчики
if ((todisp>0) && (sti%10==0))
{
if ((sti%(todisp*20+100)<todisp*20) && (sti%20<10))
{
GPIOC->BSRR=GPIO_BSRR_BS8;
}
else
{
GPIOC->BRR=GPIO_BRR_BR8;
};
};
if ((todisp2>0) && (sti%10==2))
{
if ((sti%(todisp2*20+100)<todisp2*20) && (sti%20<10))
{
GPIOC->BSRR=GPIO_BSRR_BS9;
}
else
{
GPIOC->BRR=GPIO_BRR_BR9;
};
};
// Повторение маршрута
if ((num==12))
{
switch (back)
{
case 0: if (inpok)
{
back=3;
};
break;
case 3:
if (!inpok)
{
back=4;ji=0;inpok=0;x=0;
sti=0;
};
break;
default: ;
}
};
if (back==4)
{
if (inpok==0)
{
while ((move[ji]<1) && (ji<10))
{
ji++;
};
if ((ji<10))
{
if ((sti>100))
{
x=0;
sti=0;
num=move[ji];
tit=time[ji];
inpok=1;
GPIOC->BRR=GPIO_BRR_BR8;
GPIOC->BSRR=GPIO_BSRR_BS9;
}
}
else
{
backreset();
}
}
else
{
if (sti>tit)
{
inpok=0;
sti=0;
x=0;
ji++;
GPIOC->BSRR=GPIO_BSRR_BS8;
}
}
};
// Возвращение назад
if ((num==10))
{
switch (back)
{
case 0: if (inpok)
{
back=1;
};
break;
case 1:
if (!inpok)
{
back=2;ji=9;inpok=0;x=0;
sti=0;
};
break;
default: ;
}
};
if (back==2)
{
if (inpok==0)
{
if ((ji>0) && (move[ji]>0))
{
if ((sti>100))
{
x=0;
sti=0;
num=move[ji];
if (num<4)
{
num+=6;
}
else
{
if (num>6)
{
num-=6;
}
}
tit=time[ji];
inpok=1;
GPIOC->BRR=GPIO_BRR_BR8;
GPIOC->BSRR=GPIO_BSRR_BS9;
}
}
else
{
backreset();
}
}
else
{
if (sti>tit)
{
inpok=0;
x=0;
sti=0;
ji--;
GPIOC->BSRR=GPIO_BSRR_BS8;
}
}
};
// Стираем историю движений при нажатии "*"
if (num==11)
{
backreset();
}
if ((x==3*max))
{
GPIOC->BRR=GPIO_BRR_BR12;
if ((!inpok) || (num<1) || (num>9))
{
GPIOC->BRR=GPIO_BRR_BR6;
GPIOC->BRR=GPIO_BRR_BR7;
GPIOC->BRR=GPIO_BRR_BR10;
GPIOC->BRR=GPIO_BRR_BR11;
}
if ((back!=2)&&(back!=4))
{
in0=inp0;
in1=inp1;
in2=inp2;
in3=inp3;
inok=inpok;
}
if ((move[9]>0)&&(move[9]<10)&&(!inpok)&&(back!=2)&&(back!=4))
{
for (ju=0;ju<9;ju++)
{
time[ju]=time[ju+1];
}
time[9]=sti;
};
};
if ((x==5*max) && (inpok))
{
if ((1<=num) && (num<=9)&&(back!=2)&&(back!=4))
{
for (ju=0;ju<9;ju++) //Сдвиг записей предыдущих движений
{
move[ju]=move[ju+1];
}
move[9]=num;
sti=0; //Засекаем время, в течение которого будет нажата кнопка
};
if ((num==1) || (num==2) || (num==3))
{
GPIOC->BRR=GPIO_BRR_BR10;
GPIOC->BSRR=GPIO_BSRR_BS11;
GPIOC->BSRR=GPIO_BSRR_BS2;
}
else
{
GPIOC->BRR=GPIO_BRR_BR11;
};
if ((num==7) || (num==8) || (num==9)) //Едем назад
{
GPIOC->BRR=GPIO_BRR_BR11;
GPIOC->BSRR=GPIO_BSRR_BS10;
GPIOC->BSRR=GPIO_BSRR_BS2;
}
else
{
GPIOC->BRR=GPIO_BRR_BR10;
};
};
if ((x>=max*7) && (inpok)) // inpok равен 0, если ни одна кнопка в данный момент не нажата. Нестрогое равенство приводит к проверке следующих условий на каждой последующей итерации главного цикла.
{
if ((num==1) || (num==4) || (num==7)) //Влево
{
GPIOC->BRR=GPIO_BRR_BR7;
if (j>0+full)
{
GPIOC->BSRR=GPIO_BSRR_BS6;
}
else
{
if (j<0+full-gist)
{
GPIOC->BRR=GPIO_BRR_BR6;
}
}
}
else
{
GPIOC->BRR=GPIO_BRR_BR1;
}
if ((num==3) || (num==6) || (num==9)) //Вправо
{
GPIOC->BRR=GPIO_BRR_BR6;
if (j<4250-full)
{
GPIOC->BSRR=GPIO_BSRR_BS7;
}
else
{
if (j>4250-full+gist)
{
GPIOC->BRR=GPIO_BRR_BR7;
}
}
}
else
{
GPIOC->BRR=GPIO_BRR_BR3;
}
// Выравниваем колёса на середину
if ((num==2) || (num==5) || (num==8))
{
if (j<js-gist*3) // Если колёса смещены от центра влево
{
GPIOC->BRR=GPIO_BRR_BR6; // Выключаем реле «мотор влево» (если вдруг реле ещё включено)
GPIOC->BSRR=GPIO_BSRR_BS7; // Включаем реле «мотор вправо»
}
else
{
if (j>js-gist*2)
{
GPIOC->BRR=GPIO_BRR_BR7; Выключаем реле «мотор вправо»
}
};
if (j>js+gist*3)
{
GPIOC->BRR=GPIO_BRR_BR7;
GPIOC->BSRR=GPIO_BSRR_BS6;
}
else
{
if (j<js+gist*2)
{
GPIOC->BRR=GPIO_BRR_BR6;
}
};
};
};
if ((x==max*15))
{
if (((num==1) || (num==2) || (num==3) || (num==5) || (num==7) || (num==8) || (num==9))&&(inpok)) //Замыкаем реле основного мотора
{
GPIOC->BSRR=GPIO_BSRR_BS12;
}
else
{
GPIOC->BRR=GPIO_BRR_BR12;
}
x++;
}
else
{
x++;
if (x>max*20+300000)
{
if (!inpok)
{
GPIOC->BRR=GPIO_BRR_BR2;// Гасим габариты
}
x=max*20+1;
}
}
// Мигание поворотников
if (x%80000<40000)
{
if ((num%3==1) && (inpok) && (num<10))
{
GPIOC->BSRR=GPIO_BSRR_BS1;
}
else
{
GPIOC->BRR=GPIO_BRR_BR1;
}
if ((num%3==0) && (inpok) && (num<10))
{
GPIOC->BSRR=GPIO_BSRR_BS3;
}
else
{
GPIOC->BRR=GPIO_BRR_BR3;
}
}
else
{
GPIOC->BRR=GPIO_BRR_BR1;
GPIOC->BRR=GPIO_BRR_BR3;
}
if (sti==0)
{
do {
ti=0;
} while (ti>1);
}
};
return 0;
};
// Обработчик прерывания TIM6_DAC
void TIM6_DAC_IRQHandler(void)
{
ti++;
TIM6->SR &= ~TIM_SR_UIF; //Сбрасываем флаг UIF
}
Работает это примерно так:
5. Использование ЦАП (цифро-аналогового преобразователя) для воспроизведения звуков
Раз уж был освоен АЦП, то и ЦАП не должен остаться без внимания. Наиболее очевидным способом его использования в учебно-развлекательных целях мне представляется генерация звуков. Порядок освоения классический: находим в Сети готовый пример, запускаем, «вау!», модифицируем код.
Динамик я подключил к плюсу и коллектору NPN транзистора, на базу транзистора через резистор приходит сигнал с ноги, на которую выведен ЦАП. По идее, динамик питается электрическим сигналом звуковой частоты с изменяющейся полярностью, я же подаю ток на динамик только в одном направлении. Пробовал разные схемы о двух транзисторах, но услышать существенной разницы в звучании не получилось. Возможно, просто не нашёл подходящей схемы.
Первые звуки были получены с помощью этого примера.
#include "stm32f10x.h"
#include "stm32f10x_rcc.h"
#include "stm32f10x_gpio.h"
/* Массив, элементы которого нужно быстро запихивать в DAC чтоб получить синус */
const uint16_t sin[32] = {
2047, 2447, 2831, 3185, 3498, 3750, 3939, 4056, 4095, 4056,
3939, 3750, 3495, 3185, 2831, 2447, 2047, 1647, 1263, 909,
599, 344, 155, 38, 0, 38, 155, 344, 599, 909, 1263, 1647};
unsigned char i=0;
int main(void) {
/* Включаем порт А */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
/* Включаем ЦАП */
RCC_APB1PeriphClockCmd(RCC_APB1Periph_DAC, ENABLE);
/* Включаем таймер 6 */
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM6,ENABLE);
/* Настраиваем ногу ЦАПа */
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
GPIO_Init(GPIOA, &GPIO_InitStructure);
/* Настраиваем таймер так чтоб он тикал почаще */
TIM6->PSC = 0;
TIM6->ARR = 500;
TIM6->DIER |= TIM_DIER_UIE; //разрешаем прерывание от таймера
TIM6->CR1 |= TIM_CR1_CEN; // Начать отсчёт!
NVIC_EnableIRQ(TIM6_DAC_IRQn); //Разрешение TIM6_DAC_IRQn прерывания
/* Включить DAC1 */
DAC->CR |= DAC_CR_EN1;
/* Бесконечный цикл */
while (1)
{
}
}
/*Обработчик прерывания от таймера 6 */
void TIM6_DAC_IRQHandler(void) {
TIM6->SR &= ~TIM_SR_UIF; //Сбрасываем флаг UIF
DAC->DHR12R1=sin[i++]; //Запихиваем в ЦАП очередной элемент массива
if (i==32) i=0; //Если вывели в ЦАП все 32 значения то начинаем заново
}
Неплохо бы посчитать, какой частоты должно получиться пищание, исходя из кода программы, и сравнить результат с показаниями тюнера. Частота процессора: 24 000 000 Гц. Таймер TIM6->PSC сбрасывается при достижении нуля, т.е. каждый такт. Таймер TIM6->ARR считает до 500, т.е. прерывание срабатывает на каждый 501-й сброс TIM6->PSC — в нашем случае это (24 000 000 Гц / 1) / 501 ≈ 47 904 Гц. Это частота дискретизации будущего звукого сигнала. Напомню, что в подавляющем большинстве лицензионных компакт-дисков и пиратских mp3 файлов используется частота дискретизации 44 100 Гц. Но, признаться, разрядность в них повыше — 16 бит вместо наших 12. Уверен, вы это знали.
Звуковой сигнал — синусоида, один период которой описан 32 значениями в массиве sin. Делим частоту дискретизации на количество элементов этого массива и получаем частоту звукового сигнала: 47 904 Гц / 32 ≈ 1 497 Гц. Скачиваем тюнер на смартфон (ещё лет 5 назад я бы очень удивился такой фразе), запускаем программу на контроллере и… очередное «вау!». Тюнер показывает близость к F#6, что означает Фа диез третьей октавы, а маленькие циферки уточняют частоту: 1 497,1 Гц.
Задача 12: Обучим контроллер нотной грамоте
Т.к. я несколько лет учился в музыкальной школе, просто пищание на какой-то частоте меня мало устраивает. Во-первых, выберем круглую частоту дискретизации. Учитывая, что частоты более 5 кГц я использовать не планирую, достаточно частоты дискретизации в 10 кГц. Вносим настройки в таймеры:
TIM6->PSC = 23; // Это 1 MГц
TIM6->ARR = 99; // При PSC 23 и ARR 99 прерывание каждые 100 мкс (10 кГц)
Теперь берём частоту ноты Ля, например, Субконтроктавы — 27,5 Гц. Ля выбрана потому, что частота этой ноты является целым числом и считается эталоном (440 Гц Первой октавы, обычно камертоны вибрируют с этой частотой), от неё по формуле считаются остальные 11 нот (с учётом диезов). Частоты нот соседних октав отличаются друг от друга ровно в два раза (подставляем в формулу 12 вместо n и убеждаемся в этом).
Дальше мы делаем выбор: либо нагружаем процессор контроллера расчётом синусоиды, либо загружаем его память массивами заранее вычисленных в Excel значений. Первый вариант я не стал даже пробовать, т. к. посчитал, что процессор не справится с расчётом одновременно нескольких нот (аккордов), их сложением и пр. за 2 400 тактов. Что касается расчёта значений с помощью Excel, думаю, для читателя Хабра это не составит труда:
- Несколько октав, в каждой по 12 нот (вместе с диезами).
- Для каждой ноты расчитывается частота по формуле исходя из базовой (Ля — 27,5 Гц) и количества ступеней до неё (Ля# — это +1 ступень, Си — +2 ступени, Ля следующей октавы — +12 ступеней).
- Получаем длины периодов в секундах путём деления единицы на частоту, результат делим на 10 000 Гц, округляем и получаем количество элементов в массиве для данной ноты при частоте 10 000 Гц.
- Высчитываем массив значений синуса конкретной ноты с учётом периода этой ноты.
- Нормализуем в диапазон от 0 до 4095, округляем.
Первым элементом массива я сделал количество элементов в нём же. Результат:
//Нота До
const uint16_t noteC[]={
76,2216,2383,2547,2709,2865,3017,3162,3299,3428,3547,3657,3755,3842,3917,
3979,4028,4064,4086,4095,4090,4070,4038,3991,3932,3860,3775,3679,3572,
3455,3328,3193,3050,2900,2744,2584,2419,2253,2085,1917,1749,1584,1422,
1264,1111,965,826,695,574,462,361,271,194,129,76,38,12,1,3,19,49,92,149,
218,300,393,498,613,738,871,1013,1161,1316,1475,1639,1805,1973
};
//Нота До диез
const uint16_t noteCd[]={
72,2226,2402,2576,2746,2911,3069,3220,3361,3493,3614,3722,3818,3901,3970,
4024,4063,4087,4095,4088,4065,4027,3974,3907,3825,3730,3622,3503,3372,
3231,3081,2924,2759,2590,2416,2239,2061,1883,1706,1532,1362,1196,1038,
887,744,612,490,381,284,200,130,75,35,10,0,6,27,64,116,182,263,357,464,
583,713,853,1002,1159,1323,1492,1666,1842,2020
};
const uint16_t noteD[]={
68,2236,2423,2607,2786,2959,3124,3280,3425,3559,3680,3787,3879,3955,4016,
4059,4086,4095,4087,4061,4018,3959,3883,3792,3686,3566,3432,3288,3132,
2968,2795,2617,2433,2246,2057,1869,1682,1498,1318,1145,980,823,677,543,
421,314,221,143,82,38,10,0,7,32,74,133,207,298,403,523,655,799,954,1118,
1291,1469,1652,1839,2028
};
const uint16_t noteDd[]={
63,2247,2445,2639,2828,3009,3181,3342,3490,3625,3745,3849,3935,4003,
4053,4084,4095,4087,4059,4012,3946,3862,3761,3643,3510,3363,3204,
3034,2854,2666,2473,2275,2076,1876,1678,1483,1293,1111,938,775,
625,488,366,260,171,100,48,14,0,6,31,76,139,220,319,434,565,710,
868,1037,1215,1402,1594,1792,1991
};
//Нота Ми
const uint16_t noteE[]={
60,2259,2469,2673,2872,3061,3239,3405,3556,3691,3809,3907,3986,4044,4080,
4095,4087,4058,4008,3936,3844,3733,3604,3458,3297,3122,2937,2741,2538,
2330,2119,1907,1697,1490,1289,1097,914,744,588,447,324,219,133,69,25,
3,3,24,68,133,218,323,446,586,742,912,1095,1287,1488,1694,1905
};
const uint16_t noteF[]={
57,2272,2493,2709,2917,3115,3300,3470,3623,3756,3870,3961,4029,4074,
4094,4089,4060,4007,3930,3831,3710,3569,3410,3234,3044,2842,2631,
2413,2190,1965,1742,1522,1309,1104,911,731,567,421,295,189,106,47,
11,0,14,51,113,199,306,434,582,748,929,1123,1329,1543,1763,1987
};
const uint16_t noteFd[]={
54,2285,2519,2747,2966,3172,3362,3536,3689,3820,3927,4008,
4063,4091,4092,4065,4010,3929,3823,3693,3540,3367,3177,
2971,2753,2526,2291,2054,1817,1582,1354,1135,929,738,564,
410,279,171,89,33,4,3,29,83,163,269,399,550,723,912,1118,1336,1563,1797,2034
};
const uint16_t noteG[]={
51,2299,2547,2787,3016,3230,3426,3602,3754,3880,3978,4047,4086,
4094,4071,4017,3934,3822,3683,3519,3333,3127,2905,2670,2426,2176,
1924,1674,1430,1194,972,766,580,416,276,163,79,25,1,8,47,115,213,
338,490,665,861,1075,1303,1543,1791,2042
};
const uint16_t noteGd[]={
48,2314,2576,2829,3068,3290,3492,3668,3817,3936,4023,4076,
4095,4079,4028,3944,3828,3681,3506,3307,3086,2848,2596,
2335,2069,1802,1540,1286,1045,821,618,440,288,167,78,22,
0,14,61,143,257,401,574,771,991,1228,1479,1739,2005
};
const uint16_t noteA[]={
45,2330,2606,2872,3123,3353,3558,3734,3878,3987,4059,4092,
4087,4043,3961,3842,3689,3504,3292,3056,2801,2532,2253,
1970,1689,1415,1153,907,684,486,319,184,85,23,0,16,71,
163,292,454,646,865,1107,1366,1639,1919
};
const uint16_t noteAd[]={
42,2346,2639,2918,3179,3416,3624,3798,3934,4030,4083,4093,
4059,3982,3864,3707,3514,3290,3039,2767,2480,2183,1883,
1587,1301,1031,782,561,371,218,103,30,1,15,72,172,313,
490,700,940,1203,1484,1777
};
const uint16_t noteH[]={
40,2364,2673,2967,3238,3481,3690,3859,3985,4064,4095,4076,
4009,3894,3736,3536,3301,3036,2747,2441,2126,1809,1498,1199,
922,671,453,274,137,46,3,10,65,168,316,506,732,991,1274,1577,1890
};
const uint16_t noteP[]={ // Музыкальная пауза
1,2048
};
Теперь функция, возращающая очередное значение передаваемой ей ноты с поправкой на октаву (указывать надо 1, 2, 4, 8 и т.д.):
uint16_t mnote(const uint16_t *pa,char octave) // Возвращаем значение амплитуды ноты для данного момента времени
{
int ii=0;
do {ii=i;} while (ii!=i);
ii=(octave*ii)%pa[0];
ii=*(pa+ii);
return ii;
}
Переменная i увеличивается каждые 100 микросекунд с помощью прерывания:
/*Обработчик прерывания от таймера 6 */
void TIM6_DAC_IRQHandler(void) {
i++;
TIM6->SR &= ~TIM_SR_UIF; //Сбрасываем флаг UIF
}
В общем, самое время написать мелодию. Почти как в старые добрые времена, в синтезаторе мелодий на Nokia:
uint16_t *melody[]={
noteE,noteP,noteG,noteP,
noteA,noteA,noteP,noteE,
noteP,noteG,noteP,noteAd,
noteA,noteA,noteP,noteP,
noteE,noteP,noteG,noteP,
noteA,noteA,noteP,noteG,
noteP,noteE,noteP,noteP,
noteP,noteP,noteP,noteP
};
Интересно, как быстро в комментариях появится название культовой песни?..
Чтобы образовать трезвучие (аккорд), добавим ещё две мелодии, с другими нотами:
uint16_t *melody2[]={
noteH,noteP,noteD,noteP,
noteE,noteE,noteP,noteH,
noteP,noteD,noteP,noteF,
noteE,noteE,noteP,noteP,
noteH,noteP,noteD,noteP,
noteE,noteE,noteP,noteD,
noteP,noteH,noteP,noteP,
noteP,noteP,noteP,noteP
};
uint16_t *melody3[]={
noteGd,noteP,noteH,noteP,
noteCd,noteCd,noteP,noteGd,
noteP,noteH,noteP,noteD,
noteCd,noteCd,noteP,noteP,
noteGd,noteP,noteH,noteP,
noteCd,noteCd,noteP,noteH,
noteP,noteGd,noteP,noteP,
noteP,noteP,noteP,noteP
};
Для одновременного воспроизведения сразу нескольких нот просто складываем значения их текущих амлитуд (из массивов). Но! ЦАП принимает значения от 0 до 4095, и элементы массивов нормализованы в этот же диапазон. Если нам не нужен эффект овердрайва, просто делим получившуюся сумму на количество звучащих нот, результат скармливаем ЦАПу. И так каждые 100 микросекунд…
Пора всё это воспроизвести.
#include "stm32f10x.h"
#include "stm32f10x_rcc.h"
#include "stm32f10x_gpio.h"
int i=0;
uint16_t current=0;
int loccurrent=0;
uint16_t mnote(const uint16_t *pa,char octave) // Возвращаем значение амплитуды ноты для данного момента времени
{
int ii=0;
do {ii=i;} while (ii!=i);
ii=(octave*ii)%pa[0];
ii=*(pa+ii);
return ii;
}
//Значения синуса для нот через каждые 100 микросекунд, от 0 до 4095
//Нота До
const uint16_t noteC[]={
76,2216,2383,2547,2709,2865,3017,3162,3299,3428,3547,3657,3755,3842,3917,3979,4028,4064,4086,4095,4090,4070,4038,3991,3932,3860,3775,3679,3572,3455,3328,3193,3050,2900,2744,2584,2419,2253,2085,1917,1749,1584,1422,1264,1111,965,826,695,574,462,361,271,194,129,76,38,12,1,3,19,49,92,149,218,300,393,498,613,738,871,1013,1161,1316,1475,1639,1805,1973
};
//Нота До диез
const uint16_t noteCd[]={
72,2226,2402,2576,2746,2911,3069,3220,3361,3493,3614,3722,3818,3901,3970,4024,4063,4087,4095,4088,4065,4027,3974,3907,3825,3730,3622,3503,3372,3231,3081,2924,2759,2590,2416,2239,2061,1883,1706,1532,1362,1196,1038,887,744,612,490,381,284,200,130,75,35,10,0,6,27,64,116,182,263,357,464,583,713,853,1002,1159,1323,1492,1666,1842,2020
};
const uint16_t noteD[]={
68,2236,2423,2607,2786,2959,3124,3280,3425,3559,3680,3787,3879,3955,4016,4059,4086,4095,4087,4061,4018,3959,3883,3792,3686,3566,3432,3288,3132,2968,2795,2617,2433,2246,2057,1869,1682,1498,1318,1145,980,823,677,543,421,314,221,143,82,38,10,0,7,32,74,133,207,298,403,523,655,799,954,1118,1291,1469,1652,1839,2028
};
const uint16_t noteDd[]={
63,2247,2445,2639,2828,3009,3181,3342,3490,3625,3745,3849,3935,4003,4053,4084,4095,4087,4059,4012,3946,3862,3761,3643,3510,3363,3204,3034,2854,2666,2473,2275,2076,1876,1678,1483,1293,1111,938,775,625,488,366,260,171,100,48,14,0,6,31,76,139,220,319,434,565,710,868,1037,1215,1402,1594,1792,1991
};
//Нота Ми
const uint16_t noteE[]={
60,2259,2469,2673,2872,3061,3239,3405,3556,3691,3809,3907,3986,4044,4080,4095,4087,4058,4008,3936,3844,3733,3604,3458,3297,3122,2937,2741,2538,2330,2119,1907,1697,1490,1289,1097,914,744,588,447,324,219,133,69,25,3,3,24,68,133,218,323,446,586,742,912,1095,1287,1488,1694,1905
};
const uint16_t noteF[]={
57,2272,2493,2709,2917,3115,3300,3470,3623,3756,3870,3961,4029,4074,4094,4089,4060,4007,3930,3831,3710,3569,3410,3234,3044,2842,2631,2413,2190,1965,1742,1522,1309,1104,911,731,567,421,295,189,106,47,11,0,14,51,113,199,306,434,582,748,929,1123,1329,1543,1763,1987
};
const uint16_t noteFd[]={
54,2285,2519,2747,2966,3172,3362,3536,3689,3820,3927,4008,4063,4091,4092,4065,4010,3929,3823,3693,3540,3367,3177,2971,2753,2526,2291,2054,1817,1582,1354,1135,929,738,564,410,279,171,89,33,4,3,29,83,163,269,399,550,723,912,1118,1336,1563,1797,2034
};
const uint16_t noteG[]={
51,2299,2547,2787,3016,3230,3426,3602,3754,3880,3978,4047,4086,4094,4071,4017,3934,3822,3683,3519,3333,3127,2905,2670,2426,2176,1924,1674,1430,1194,972,766,580,416,276,163,79,25,1,8,47,115,213,338,490,665,861,1075,1303,1543,1791,2042
};
const uint16_t noteGd[]={
48,2314,2576,2829,3068,3290,3492,3668,3817,3936,4023,4076,4095,4079,4028,3944,3828,3681,3506,3307,3086,2848,2596,2335,2069,1802,1540,1286,1045,821,618,440,288,167,78,22,0,14,61,143,257,401,574,771,991,1228,1479,1739,2005
};
const uint16_t noteA[]={
45,2330,2606,2872,3123,3353,3558,3734,3878,3987,4059,4092,4087,4043,3961,3842,3689,3504,3292,3056,2801,2532,2253,1970,1689,1415,1153,907,684,486,319,184,85,23,0,16,71,163,292,454,646,865,1107,1366,1639,1919
};
const uint16_t noteAd[]={
42,2346,2639,2918,3179,3416,3624,3798,3934,4030,4083,4093,4059,3982,3864,3707,3514,3290,3039,2767,2480,2183,1883,1587,1301,1031,782,561,371,218,103,30,1,15,72,172,313,490,700,940,1203,1484,1777
};
const uint16_t noteH[]={
40,2364,2673,2967,3238,3481,3690,3859,3985,4064,4095,4076,4009,3894,3736,3536,3301,3036,2747,2441,2126,1809,1498,1199,922,671,453,274,137,46,3,10,65,168,316,506,732,991,1274,1577,1890
};
const uint16_t noteP[]={
1,2048
};
//Мелодия, основной тон
uint16_t *melody[]={
noteE,noteP,noteG,noteP,
noteA,noteA,noteP,noteE,
noteP,noteG,noteP,noteAd,
noteA,noteA,noteP,noteP,
noteE,noteP,noteG,noteP,
noteA,noteA,noteP,noteG,
noteP,noteE,noteP,noteP,
noteP,noteP,noteP,noteP
};
//Чистая квинта
uint16_t *melody2[]={
noteH,noteP,noteD,noteP,
noteE,noteE,noteP,noteH,
noteP,noteD,noteP,noteF,
noteE,noteE,noteP,noteP,
noteH,noteP,noteD,noteP,
noteE,noteE,noteP,noteD,
noteP,noteH,noteP,noteP,
noteP,noteP,noteP,noteP
};
//Большая терция
uint16_t *melody3[]={
noteGd,noteP,noteH,noteP,
noteCd,noteCd,noteP,noteGd,
noteP,noteH,noteP,noteD,
noteCd,noteCd,noteP,noteP,
noteGd,noteP,noteH,noteP,
noteCd,noteCd,noteP,noteH,
noteP,noteGd,noteP,noteP,
noteP,noteP,noteP,noteP
};
int main(void) {
GPIO_InitTypeDef PORT;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC , ENABLE);
PORT.GPIO_Pin = (GPIO_Pin_9 | GPIO_Pin_8);
PORT.GPIO_Mode = GPIO_Mode_Out_PP;
PORT.GPIO_Speed = GPIO_Speed_2MHz;
GPIO_Init(GPIOC, &PORT);
/* Включаем порт А */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
/* Включаем ЦАП */
RCC_APB1PeriphClockCmd(RCC_APB1Periph_DAC, ENABLE);
/* Включаем таймер 6 */
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM6,ENABLE);
/* Настраиваем ногу ЦАПа */
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
GPIO_Init(GPIOA, &GPIO_InitStructure);
TIM6->PSC = 23; //При 24000-1 будет 1000Гц
TIM6->ARR = 99; //При PSC 23 и ARR 99 прерывание каждые 100 мкс
TIM6->DIER |= TIM_DIER_UIE; //разрешаем прерывание от таймера
TIM6->CR1 |= TIM_CR1_CEN; // Начать отсчёт!
NVIC_EnableIRQ(TIM6_DAC_IRQn); //Разрешение TIM6_DAC_IRQn прерывания
/* Включить DAC1 */
DAC->CR |= DAC_CR_EN1;
/* Бесконечный цикл */
while (1)
{
GPIOC->BRR=GPIO_BRR_BR8;
GPIOC->BSRR=GPIO_BSRR_BS9;
do
{
loccurrent=i;
} while (i!=loccurrent); //Безопасное получение переменной, используемой в прерываниях
GPIOC->BRR=GPIO_BRR_BR9;
GPIOC->BSRR=GPIO_BSRR_BS8;
current=(loccurrent/(2000))%32; //Текущая нота из мелодии, от 0 до 31.
DAC->DHR12R1=(mnote(melody[current],1)+mnote(melody2[current],1)+mnote(melody[current],2)+mnote(melody2[current],2)+mnote(melody[current],4)+mnote(melody3[current],2))/6;
}
}
/*Обработчик прерывания от таймера 6 */
void TIM6_DAC_IRQHandler(void) {
i++;
TIM6->SR &= ~TIM_SR_UIF; //Сбрасываем флаг UIF
}
Возникает логичный вопрос, можно ли вместо одного периода синусоиды записать в массив какой-нибудь замечательный звук? Да.
Задача 13: Воспроизведение звуков из файла
Сразу говорю, есть другие, более разумные варианты озвучки STM32VLDiscovery, но мне не захотелось их осваивать. Мой алгоритм:
1. Ищем на Youtube звук запуска двигателя Aston Martin.
2. Записываем (или скачиваем) этот звук.
3. Открываем файл в аудиоредакторе (я использовал бесплатный Audacity).
4. Выбираем минимально приемлемую частоту дискретизации. Я выбрал 24 кГц. Учитывая, что каждый сэмпл занимает в памяти контроллера 2 байта, мы можем записать всего 2-3 секунды моно-звука. В самый раз.
5. Сохраняем коротенький звуковой WAV-файл.
6. Преобразуем с помощью специальной программы (вроде WAV 2 TEXT) — да-да, звук в текст. Вернее, в таблицу значений амлитуд.
7. Нормализуем в значения от 0 до 4095 с помощью Excel. Все 60 000 значений копируем в Notepad++ или Sublime Text, массово меняем табуляции на запятые. Также в Excel я вставлял каждые 30 строк букву, чтобы потом все их заменить на перенос строки — CoIDE не любит длинные строки…
8. Сохраняем значения в массив.
Пожалуй, over 9000 чисел здесь приводить не буду. Предлагаю видео:
В том случае, если вы просто перемотали топик до конца и не видели, как, собственно, машинка ездит, повторяю видео:
Как-то так.
Автор: Pontific