LXF160:Arduino взволнован
|
|
|
Электроника. Аппаратные проекты с открытым кодом, расширяющие ваш кругозор.
Содержание |
Arduino: Гоним волну таймером
Хитроумно манипулируя с таймерами, Ник Вейч превращает Arduino в генератор колебаний.
В прошлый раз мы говорили о... времени, а именно – в подробностях рассмотрели работу со счетчиком Timer1 и узнали, как с помощью кое-каких низкоуровневых манипуляций сделать то, что нам нужно: запустить несколько надежных процедур прерывания типа реле времени. Но, как это ни было прекрасно, возможности таймера гораздо шире, и сегодня мы рассмотрим еще несколько примеров его использования. Можете не тревожить свой ящик с деталями, хотя динамик, пожалуй, не повредит.
- Метамодернизм в позднем творчестве В.Г. Сорокина
- ЛитРПГ - последняя отрыжка постмодерна
- "Ричард III и семиотика"
- 3D-визуализация обложки Ridero создаем обложку книги при работе над самиздатом.
- Архитектура метамодерна - говоря о современном искусстве, невозможно не поговорить об архитектуре. В данной статье будет отмечено несколько интересных принципов, характерных для построек "новой волны", столь притягательных и скандальных.
- Литература
- Метамодерн
- Рокер-Прометей против изначального зла в «Песне про советскую милицию» Вени Дркина, Автор: Нина Ищенко, к.ф.н, член Союза Писателей ЛНР - перепубликация из журнала "Топос".
- Как избавиться от комаров? Лучшие типы ловушек.
- Что делать если роблокс вылетает на windows
- Что делать, если ребенок смотрит порно?
- Почему собака прыгает на людей при встрече?
- Какое масло лить в Задний дифференциал (мост) Visco diff 38434AA050
- О чем может рассказать хвост вашей кошки?
- Верветки
- Отчетность бюджетных учреждений при закупках по Закону № 223-ФЗ
- Срок исковой давности как правильно рассчитать
- Дмитрий Патрушев минсельхоз будет ли преемником Путина
- Кто такой Владислав Поздняков? Что такое "Мужское Государство" и почему его признали экстремистским в России?
- Как правильно выбрать машинное масло в Димитровграде?
- Как стать богатым и знаменитым в России?
- Почему фильм "Пипец" (Kick-Ass) стал популярен по всему миру?
- Как стать мудрецом?
- Как правильно установить FreeBSD
- Как стать таким как Путин?
- Где лучше жить - в Димитровграде или в Ульяновске?
- Почему город Димитровград так называется?
- Что такое метамодерн?
- ВАЖНО! Временное ограничение движения автотранспортных средств в Димитровграде
- Тарифы на электроэнергию для майнеров предложено повысить
Впрочем, перед тем как перейти к самим таймерам, выясним, как они используются в самом Arduino. Вы когда-нибудь интересовались тем, как чисто цифровое устройство формирует аналоговый выход? Этот специальный режим выходного сигнала можно включить на выводах 3, 5, 6, 9, 10 и 11 Arduino на базе Atmega168 и Atmega368. При записи, например, значения 128 в один из этих выводов значение выходного сигнала оказывается равным (в данном случае) около 2,5 В.
Происходит следующее: на короткий интервал времени вывод включается и затем снова выключается. Он генерирует прямоугольное колебание, которое в среднем образует сигнал напряжением 2,5 В. В реальности это лишь очень быстрое переключение между 5 В и 0 В. Значение, которое вы записываете, определяет соотношение времени в состоянии ВКЛ ко времени в состоянии ВЫКЛ (коэффициент заполнения, часто выражаемый в процентах). Как и при ловкости рук, скорость здесь – важный фактор. Хотя среднее напряжение, независимо от того, включать и выключать его дважды или двести раз в секунду (при том же соотношении продолжительности ВКЛ/ВЫКЛ), будет одинаково, в первом случае вы получите мигающий свет, а во втором свет окажется тусклым (на самом деле он просто мигает очень быстро). Обычно частота прямоугольных колебаний составляет около 500 Гц при вызове функции digitalWrite(), но можно изменить сами часы, чтобы получить другие значения.
Может иметь значение основная частота широтно-импульсной модуляции (ШИМ). Настоящие электронные компоненты существуют в реальном мире, и хотя время нарастания сигнала (требуемое для изменения выходного сигнала с нуля [LOW] до единицы [HIGH]) в Arduino измеряется в наносекундах, время, необходимое для включения или выключения электромагнита, легко может быть в 1000 раз больше. Поэтому иногда основную частоту ШИМ нужно изменить. Для выводов 11 и 3, использующих встроенный Timer2, это не повлияет на остальную часть вашей программы. А вот для выводов 5 и 6 изменение частоты (т. е. изменение значения предварительного делителя частоты) имеет побочный эффект. С помощью импульсов этого таймера работают функции Arduino millis() и delay(), поэтому уменьшение частоты может увеличить задержки.
Основы ШИМ
Таймер управляет режимом ШИМ вполне очевидным образом. Таймер подсчитывает периоды сигнала ВКЛ до своего предела в 255 (для Timer2) и затем переполняется, снова начиная отсчет с нуля. Можно настроить регистр сравнения на любое из значений от 0 до 255, и когда счетчик достигнет выбранного значения, в зависимости от заданного режима могут быть выполнены различные действия, обычно включающие автоматическую установку значения определенного вывода.
Как всегда на микросхемах Atmega, есть несколько различных вариантов возможных действий. В самом простом случае регистр сравнения переключает выход в «единицу» и сбрасывается снова при перезапуске счетчика. Тогда высокое значение регистра сравнения означает больше времени «выключено» и меньше «включено». Также можно использовать один из регистров сравнения в качестве верхнего предела счетчика, таким образом более точно отрегулировав его частоту (а также вычисления коэффициента заполнения).
Мы будем пользоваться ШИМ с коррекцией фазы, полагая, что сигнал достигает высшей точки ровно посреди временного интервала. Это означает и то, что сгенерированный сигнал имеет самый низкий уровень в начале и в конце этого временного интервала. Для генерации сигналов правильной формы, например, для управления двигателем или даже воспроизведения звука, это жизненно важно.
В режиме коррекции фазы счетчик ШИМ ведет себя иначе – сначала он досчитывает до 255, а потом вместо сброса начинает считать обратно до нуля. Если регистр сравнения настроен на изменение выходного сигнала в обоих направлениях, то в каждом направлении на состояния ВКЛ и ВЫКЛ приходятся одинаковые интервалы времени, и коэффициент заполнения сохраняется. Явный недостаток этого режима в том, что эффективная частота уменьшается вдвое. Впрочем, это не должно стать проблемой для нас, так как при частоте процессора 16 МГц полоса остается приемлемой для аудио- и других ценных сигналов.
Больше шума
Мы можем воспользоваться возможностями ШИМ Timer2 для генерации звука. В предыдущих экспериментах со звуком мы полагались только на изрыгающую биты встроенную функцию tone(), которая формирует приемлемые, разве что звучащие сердито, прямоугольные колебания на заданном выводе. Но с возможностями ШИМ и небольшим хитрым кодом мы можем создавать звук любой формы и размера. Конечно, хотя мы считаем этот сигнал звуковым, фактически мы создаем сигналы, пригодные в самых разных ситуациях – как для тестирования другого оборудования, так и для непосредственного управления двигателями.
Для генерации колебаний мы можем имитировать аналоговые значения, используя ШИМ на высокой частоте для включения и выключения вывода. Но нужно задать это значение надежно. Тут нам поможет тот же самый таймер. Как и у Timer1, которым мы пользовались в предыдущей статье, у Timer2 есть вектор прерываний. В режиме ШИМ с коррекцией фазы его можно заставить срабатывать по завершению полного цикла счетчика (т. е. когда счетчик досчитал до 255 и вернулся обратно к 0). Связав с этим процедуру обработки прерывания, при необходимости можно менять значение вывода ШИМ на каждом цикле вывода.
Полный цикл таймера составляет 510 тактов (конечные значения считаются только один раз), и это дает нам 16 МГц/510 = 31,372 кГц – более чем достаточно для создания аудиосигнала. Как и в прошлый раз, для задания корректных значений таймера необходимо немного поработать со специальными регистрами микросхемы Atmega. Вот код для установки таймера:
TCCR2A = _BV(COM2B1) | _BV(WGM20);
TCCR2B = _BV(CS20);
TIMSK2 = _BV(TOIE2);
pinMode(3,OUTPUT);
Если вы не читали прошлой статьи, то вряд ли что-то поняли. Мы не будем возвращаться к побитовой арифметике (то, что делает символ |), но немного пояснить происходящее все же стоит. TCCR2A и TCCR2B – два 8-битовых регистра (т. е. две специальные области памяти), которые содержат значения, управляющие работой таймера. Среди прочего они определяют, какой режим ШИМ используется и как быстро он работает. Каждый бит или набор битов задает определенный аспект работы таймера. Установленный бит COM2B1 говорит компаратору B, что в режиме ШИМ с коррекцией фазы нужно сбросить выход в ноль при достижении верхнего предела и установить его в значение верхнего предела при достижении нуля (обратный режим). Бит WGM20 переводит таймер в режим ШИМ с коррекцией фазы. Три менее важных бита в TCCR2B устанавливают значение делителя частоты таймера (сколько тиков системных часов нужно ждать для обновления значения таймера). Если установить только бит CS20, таймер будет работать с той же частотой, что и системные часы (а сброс всех битов в ноль отключает таймер). TIMSK2 – другой регистр, который на сей раз содержит маску прерываний. При возникновении определенного события (например, достижения таймером конца цикла) процессор считывает значение этого регистра, чтобы посмотреть, назначено ли прерывание для этого события. Если да, он обращается к вектору прерываний, другой специальной области памяти, в которой хранится адрес процедуры, которую нужно запустить.
Наконец, мы назначаем выходной сигнал на цифровой вывод 3. Почему на него? А он напрямую управляется регистром сравнения B таймера Timer2. Желая воспользоваться выводом 11, можно взять вместо него регистр сравнения A таймера Timer2. Только два этих вывода управляются напрямую с таймера.
Прерывание
Чтобы корректно задать прерывание, воспользуемся специальным макросом ISR(). Дело в том, что адрес прерывания нужно загружать в вектор прерывания, но пока мы не скомпилировали программу, этот адрес неизвестен. Этот волшебный макрос позаботится обо всем; нужно лишь объявить следующую процедуру:
ISR(TIMER2_OVF_vect){
}
Указанный бит говорит макросу, с каким вектором мы хотим связать эту процедуру. Теперь нужно только заполнить ее. Раз уж мы возжелали иметь несколько различных типов колебаний, есть смысл воспользоваться конструкцией switch ... case. В языке C она позволяет выполнять различные блоки кода в зависимости от значения одной переменной. Она работает так:
switch(variable) {
case 1:
// что-нибудь делаем
break;
case 2:
// что-нибудь делаем
break;
…
default:
// остальное не подошло, сделаем это
}
Если значение переменной равно 1, выполняется первый блок кода, и т. д. Если это значение не соответствует ни одному из блоков, выполняется блок default, но он не обязателен. Каждый блок следует завершать оператором break;, по которому происходит выход из всей структуры, в противном случае переменная продолжит сравниваться с каждым блоком (и в итоге выполнится код по умолчанию). Поэтому все, что нам нужно – переменная, которая скажет нам, какой сигнал мы генерируем. Переменная должна быть глобальной и задаваться вне этой конструкции, чтобы можно было установить ее в основном коде. Назовем ее wave.waveform.
Да, и нужно сделать кое-что еще. Хотя код вызовется точно в нужный момент времени по прерыванию, на его выполнение может потребоваться разное время, в зависимости от того, какой блок кода будет выполнен. Чтобы избежать дрожаний выходного сигнала, мы должны подать значение регистра сравнения (т. е. нашу величину от 0 до 255) на выход с самого начала. Затем мы можем воспользоваться оставшейся частью прерывания для генерации выходного значения для следующего раза. У компаратора есть специальный адрес, но, к счастью, есть и условное значение, в которое его можно установить. Выходное значение будет еще одной из этих нудных глобальных переменных, поэтому назовем его wave.output. Начало нашей функции теперь выглядит так:
ISR(TIMER2_OVF_vect){
OCR2B = wave.output;
switch(wave.waveform) {
case 1: // прямоугольная волна
Да, мы начинаем с прямоугольных колебаний! Ну, да, да, мы могли сделать это и раньше, но не на тех частотах, которые теперь у нас в распоряжении! Прямоугольные колебания довольно просты: полпериода волна проводит вверху, другие полпериода – внизу. Чтобы определить, когда ее включать и выключать, нужно знать частоту воспроизводимой ноты(wave.frequency) и количество маленьких порций времени, которые уже прошли (м-мм, wave.f_counter?). После этого генерация волны сводится к проверке, в какой временной доле мы находимся, определении, не во второй ли мы половине полного цикла, и включении или выключении сигнала. Как мы знаем, каждая временная доля составляет 1/31372,5 секунды, и дело лишь за небольшой арифметикой:
case 1: // прямоугольная волна
wave.f_counter++;
if (wave.f_counter<(15686/wave.frequency)){
// первая половина цикла – выход максимален
wave.output=wave.volume;
}
else{
// вторая половина цикла – выход = 0
wave.output=0;
}
if (wave.f_counter>(31372.5/wave.frequency)){
// цикл завершен, сборос счетчика
wave.f_counter=0;
}
break;
Да, мы вбросили туда и wave.volume – почему бы нет? Тогда мы сможем управлять еще и амплитудой прямоугольных колебаний. Может быть, сейчас вас беспокоит, откуда будут браться эти значения и как мы будем ими пользоваться. Не волнуйтесь, именно так и стоит писать код. Теперь у нас есть все необходимые параметры, и можно создать специальную переменную, где все они будут храниться. В предыдущих руководствах мы рассматривали структуры, и они довольно просты. Это лишь способ задать составной тип данных – нечто вроде класса, но без методов.
struct generator{
bool active;
uint8_t waveform;
uint16_t frequency;
uint16_t f_counter;
uint16_t duration;
uint16_t table_value;
double table_inc;
uint8_t output;
uint8_t volume;
} wave;
Можно порезвиться с некоторыми переменными и типами, но в целом это очень похоже на готовое управление звуком (к табличным данным перейдем через минуту). Как видите, в конец объявления структуры мы поместили имя wave. Это значит, что будет создан один экземпляр структуры с именем wave, и для доступа к ее элементам мы можем использовать конструкции wave.waveform, wave.frequency... весьма удобно, потому что мы уже написали код, который делает именно это.
Можно работать с частями этой переменной по отдельности, но лучше создать вспомогательную функцию для задания выходного значения. Она может принимать один аргумент (частоту) и устанавливать все необходимые значения следующим образом:
void playSquare (uint16_t f){
wave.waveform=1;
wave.frequency=f;
wave.f_counter=0;
wave.active=true;
wave.volume=255;
};
Здесь мы присваиваем булевское значение переменной wave.active. Ее можно использовать в качестве ключа в начале процедуры прерывания – если волна выключена, то никаких действий предпринимать не нужно. Это поможет сберечь несколько тактов процессора для других действий.
Arduino 1.0!
Да, после нескольких лет пребывания в версии «ноль точка», команда Arduino, наконец, сочла ПО достойным присвоения ему номера 1.0. Если вы еще не обновились до этой версии, поспешите. Одна из мелких, но важных реформ – смена расширения по умолчанию для проектов с исходным кодом с .pde на .ino, чтобы исключить возможные конфликты расширений. Формат файла не изменился, но вы обнаружите, что старые версии программ не распознают новое расширение, и чтобы открыть файл в новой версии программы, понадобится копирование и вставка. В новой версии можно сделать так, чтобы тип файла автоматически обновлялся для более старых проектов при их повторном сохранении, и это, пожалуй, будет удобно.
> В новой версии Arduino есть немного косметических изменений, а также несколько долгожданных реформ в библиотеках.
Гляньте-ка в таблицу!
До появления калькуляторов у каждого школьника (и, наверное, школьницы, хотя я не ходил в такую школу) была книжка с таблицей логарифмов. Каждый раз, когда вам нужен был логарифм числа, вы заглядывали в эту таблицу с мелки цифрами, ища максимально близкое.
Ныне нам не нужны даже калькуляторы – у нас есть компьютеры, и большинство людей и не упомнят, что такое логарифм. Но по иронии судьбы поиск чисел в таблице, невероятно скучный для нас, для компьютера часто гораздо быстрее (в зависимости от требуемых вычислений). Расчет логарифмических и тригонометрических значений затратен, и, что иногда важно, для его выполнения может требоваться разное время. Взять значение в таблице надежно, и время процессора на это всегда требуется одинаковое.
Создание таблицы с результатами и поиск в ней очень эффективен, особенно когда дело доходит до микроконтроллеров, где нет сопроцессора для работы с плавающей точкой. Единственный недостаток – нужно сгенерировать и хранить таблицу с данными, и если вам нужна очень высокая точность или у вас нет запасной оперативной памяти, возможно, стоит выполнять вычисления на лету. Во всех остальных случаев без таблиц соответствия не обойтись.
Объяснения еще нужны? Хорошо. Таблица, по сути, массив чисел, а индекс – значение, для которого вы ищете результат. Конечно, роль играют несколько факторов – число значений в массиве, или, если угодно, размер шага, зависит от требуемой точности данных (нечто вроде битрейта в музыке). Точность можно слегка увеличить, делая интерполяцию между двумя значениями из таблицы, но для этого нужны добавочные вычислительные ресурсы.
Единственное ограничение для таблицы соответствия – в том, что индекс должен быть целым числом: ведь нельзя же получить значение 3,82-го элемента в списке. Но это может быть и благом! Хотя точность при этом снижается, хранение чисел без плавающей точки делает их (обычно) меньше, и работать с ними становится определенно быстрее. Если вам хватает двух знаков после запятой, зачем вам четыре или пять, которые могут дать числа с плавающей точкой? Умножайте все на 100 и пользуйтесь целыми числами! Или, для более эффективного использования пространства, используйте числа до 255, которые аккуратно умещаются в один байт.
Синусит времен
Конечно, есть и более натуральные волны – хотя бы треугольные и синусоидальные волны. Синусоидальная волна лучше всего генерируется с помощью таблицы значений (см. врезку). Математические расчеты для вычисления значений на лету слишком затратны по времени. Помните, наши временные доли равны 1/31372,5 секунды – а на выполнение процедуры отводится не более данного интервала, и нужно экономить! Определить положение в таблице, найти значение и инкрементировать несколько счетчиков – это всего несколько тактов процессора (из возможных 510, которые у нас есть), так что это простой подход.
Как определить наше положение в таблице? Нам известен размер таблицы и частота воспроизводимой ноты – достаточно выполнить умножение, деление и посмотреть на остаток. Нам не нужно считать временные доли – все сложные вычисления можно выполнить заранее при задании параметров волны; у нас должны быть только счетчик индекса и переменная, содержащая количество битов индекса, которые нужно пропускать с каждой долей. Мы делим значение счетчика на 256 и используем остаток для получения следующего значения. Если необходима большая точность, можно сделать эти значения действительными (с плавающей точкой) и работать с долями значений индекса (получить два и интерполировать), но размер таблицы в любом случае должен быть невелик – для хранения большей таблицы элементарно не хватит памяти. Поэтому теперь сложные расчеты выполняются во вспомогательной функции:
void playSine (uint16_t f){
wave.waveform=3;
wave.frequency=f;
wave.f_counter=0;
wave.table_value=0;
wave.table_inc=(255.0/(31372.54/wave.frequency));
wave.active=true;
}
Но реализовать ее в прерывании просто:
case 3: // синусоидальная волна
wave.output=sinLUT[wave.table_value%256];
wave.table_value += wave.table_inc;
break;
Ничего не забыли? Ах да, сама таблица. Ну, это просто стопка чисел – ее можно найти на DVD. Треугольная волна также реализована в таблице соответствия – лениво, да? Но хотя значения для треугольной волны достаточно просто рассчитать, работать с таблицей все равно быстрее! Если вы хотите послушать некоторые звуковые частоты, подключите динамик между выводом 3 и землей, желательно с конденсатором. |