Памятка. AVR. Buzic

в 12:55, , рубрики: avr, buzzer, Электроника для начинающих

Памятка. AVR. Buzic - 1

Суть

Я создал уже некоторое количество разных хоббийных электронных устройств, и у меня есть странная особенность: если на плате присутствует звуковой пьезоэлектрический излучатель(buzzer), я, по окончании основных работ над проектом, начинаю страдать ерундой и заставлять его проигрывать различные мелодии(насколько это возможно). Особенно бывает полезно включить мелодию, по окончании какого либо продолжительного процесса для привлечения внимания. Например, я это использовал, когда построил самодельную экспозиционную камеру для засветки фоторезиста и т.д.

Но когда я начал искать примеры генерации частот для AVR в сети, почему-то мне попадались монструозные, либо недостаточно лаконичные проекты, реализующие генерацию звуковых частот чисто программным образом. И тут я решил во всём разобраться сам…

Лирическое отступление

Моё хобби включает в себя создание различных устройств на микроконтроллерах, т.к это никак не пересекается с моей проф. деятельностью(разработка софта), я считаю себя абсолютным самоучкой, да и в электронике не слишком силен. На самом деле я предпочитаю PIC микроконтроллеры, но так уж случилось, что у меня накопилось некоторое количество AVR микроконтроллеров компании Atmel(сейчас уже Microchip). Сразу оговорюсь, что никогда не держал в руках AVR, т.е. это первый мой проект на MCU Atmel, а именно Atmega48pa. Сам проект выполняет некоторую полезную нагрузку, но здесь я опишу лишь его часть, относящуюся к генерации звуковых частот. Тест для генерации частот я назвал «buzic», как сокращение от buzzer's music. Да чуть не забыл: на Хабре есть пользователь c ником buzic, хотел сразу предупредить, что данная памятка никак не относится к нему и на всякий случай, сразу прошу прощения за использование буквосочетания «Buzic».

Итак, поехали

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

  1. подают высокий уровень на ногу микроконтроллера
  2. делают задержку
  3. подают низкий уровень на ногу микроконтроллера

Изменяя задержки и параметры таймера — подгоняют частоту.

Меня данный подход крайне не устраивал, т.к. у меня не было желания писать код для ручного управления ногой микроконтроллера. Я бы хотел, чтобы за меня звуковую частоту генерировал сам «камень», а я просто выставлял значения определенных регистров, тем самым меняя её(частоту).

При изучении даташита(далее ДШ), я таки нашел нужный мне режим таймера — и этим режимом, как Вы уже догадались, является режим CTC(Clear Timer on Compare Match). Так как функция проигрывания музыки является, мягко говоря, не основным функционалом я предпочел выделить для неё таймер 2(пункт 22 ДШ).

Все прекрасно знают, что практически у любого микроконтроллера режим генерации ШИМ сигнала реализован на таймерах и он вполне себе аппаратный. Но в данной задаче ШИМ не подходит т.к. аппаратно будет генерироваться только одна частота. Поэтому нам нужен ЧИМ(частотно импульсная модуляция). Некоторым подобием ЧИМ и является CTC режим таймера(пункт 22.7.2 ДШ).

CTC режим

Таймер 2 в микроконтроллере Atmega48pa 8ми битный, т.е он «тикает» от 0 до 255 и затем идет по кругу. К слову таймер может идти и в другом направлении, но не в нашем случае. Следующим обязательным компонентом является модуль сравнения(Compare Unit). Если говорить совсем грубо, то этот модуль и является инициатором любых событий связанных с таймером. События могут быть разными — такими как прерывания, изменения уровня определенных ног микроконтроллера и т.д.(Очевидно нам интересно второе). Как нетрудно догадаться, модуль сравнения, не просто так назван — он сравнивает определенное значение выбираемое разработчиком микропрограммы с текущим значением таймера. Если значение таймера достигло заданной нами величины, то происходит событие. Также события могут происходить при переполнении таймера либо при сбросе. Ок, мы пришли к тому, что нам удобно, чтобы в определенные моменты таймер вместе с модулем сравнения самостоятельно менял уровень на ноге микроконтроллера на противоположный — генерируя таким образом импульсы.

Второй задачей является задание промежутков между этими импульсами — т.е. управление частотой генерации. Вся уникальность режима CTC заключается в том, что в этом режиме таймер не идет до конца(255), а сбрасывается при достижении заданного значения. Соответственно, изменяя это значение, мы можем фактически управлять частотой. Например если значение модуля сравнения мы задаем в 10, то изменение уровня на ноге микроконтроллера будет происходить в 20 раз чаще, чем если бы мы задали его(значение модуля сравнения) в 200. Теперь мы можем управлять частотой!

Памятка. AVR. Buzic - 2

Железо

Памятка. AVR. Buzic - 3
По распиновке микроконтроллера видно, что нам нужно подключить наш buzzer либо к ноге PB3(OC2A) либо к ноге PD3(OC2B), т.к. OC2A и OC2B означает именно то, что на этих ногах таймер 2 может генерировать сигналы.

Схема, которой я обычно пользуюсь для подключения buzzer'а:
Памятка. AVR. Buzic - 4

И вот мы собрали устройство.

Регистры

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

Настройку нашего таймера 2 мы будем производить меняя 3 регистра:

  1. TCCR2A — настройки режима и выбор поведения
  2. TCCR2B — настройки режима и делитель частоты таймера(ещё FOC биты — мы их не используем)
  3. OCR2A (OCR2B для случая с ногой PD3) — значение модуля сравнения

Рассмотрим для начала регистры TCCR2A и TCCR2B
Памятка. AVR. Buzic - 5
Как видно мы имеем 3 группы битов значащих для нас — это биты серии COM2xx, WGM2x и CS2x
Первое, что мы должны менять — это WGM2x — это главное, чтобы выбрать режим генерации — именно эти биты служат для выбора нашего CTC режима.

Памятка. AVR. Buzic - 6
примечание: очевидно в ДШ опечатка в «Update of OCR0x at» должно быть OCR2x

Т.е. код будет такой:

TCCR2A = _BV(WGM21) ;

Как видно TCCR2B мы пока не используем т.к. WGM22 должен быть равен нулю, но он и так равен нулю.

Следующим шагом необходимо настроить биты COM2xx, точнее COM2Ax — т.к. мы работает с ногой PB3(для PD3 аналогично используются COM2Bx). От них зависит то, что будет происходить с нашей ногой PB3.

Биты COM2xx зависят от режима, который мы выбрали битами WGM2x, поэтому нам придется найти соответствующий раздел в ДШ. Т.к. у нас режим CTC, т.е. не ШИМ, то ищем табличку «Compare Output Mode, non-PWM», вот она:
Памятка. AVR. Buzic - 7
Здесь необходимо выбрать «Toggle» — чтобы уровень на ноге менялся на противоположный при достижении таймером заданного значения. Постоянное изменение уровня и реализует генерацию необходимой нам частоты.

Т.к. биты COM2xx также лежат в регистре TCCR2A — то меняется только он:

TCCR2A = _BV(COM2A0) | _BV(WGM21) ;

Естественно, необходимо также выбрать делитель частоты битами CS2x, ну и конечно настроить ножку PB3 на выход… но мы этого делать пока не будем, чтобы при включении МК мы не получили пронзительный визг на непонятной частоте, а вот когда мы будем производить все остальные настройки и включать ножку на выход — будет рассказано ниже.

Итак давайте приведем нашу инициализацию к завершенному виду:


#include <avr/io.h>

//set bit - using bitwise OR operator
#define sbi(x,y) x |= _BV(y)
//clear bit - using bitwise AND operator
#define cbi(x,y) x &= ~(_BV(y))

#define BUZ_PIN PB3

void timer2_buzzer_init()
{
   //обнуляем PB3
   cbi(PORTB, BUZ_PIN);
   //делаем PB3 пока входом, а не выходом
   cbi(DDRB, BUZ_PIN);

   //настройка режимов
   TCCR2A = _BV(COM2A0) | _BV(WGM21) ;

   //обнуляем значение модуля сравнения(хотя я думаю можно и не обнулять)
   OCR2A = 0;
}

Я использовал макросы cbi и sbi(подсмотрел где-то в сети) для установки отдельных битов, да так и оставил. Эти макросы разумеется у меня вынесены в заголовочный файл, но для наглядности я поместил их сюда.

Расчет частоты и длительности нот

Теперь мы подходим к самой сути вопроса. Некоторое время назад, знакомые музыканты пытались вбить в мой мозг программиста некоторое количество информации по поводу нотного стана, мозг у меня чуть не вскипел, но всё же полезную крупицу я вынес из этих бесед.
Сразу предупреждаю — возможны огромные неточности.

  1. каждый такт, состоит из 4 четвертей
  2. У каждой мелодии есть темп — т.е. число таких четвертей в минуту
  3. Каждая нота может проигрываться как весь целый такт, так и его часть 1/2, 1/3, 1/4 и т.д.
  4. У каждой ноты, само собой, есть определенная частота

Мы рассмотрели самый распространенный случай, на самом деле там всё сложнее по крайней мере для меня, поэтому я не буду муссировать данную тему в рамках этого повествования.

Ну да ладно, будем работать с тем что есть. Самое главное для нас это в конечном итоге получить частоту ноты(на самом деле значение регистра OCR2A) и её длительность, например в миллисекундах. Соответственно необходимо сделать некоторые расчеты.

Т.к. мы находимся в рамках языка программирования, мелодии проще всего хранить в массиве. Логичней всего задавать каждый элемент массива в формате — нота + длительность. Необходимо посчитать размер элемента в байтах, ведь мы пишем под микроконтроллер и с ресурсами тут туго — значит размер элемента в байтах должен быть адекватным.

Частота

Начнем с частоты. Т.к. таймер 2 у нас 8-битный, регистр сравнения OCR2A также 8-битный. То есть наш элемент массива мелодии будет уже, как минимум, 2 байта, потому что нужно же ещё длительность сохранять. На самом деле 2 байта — это предел для подобного рода поделок. Хорошего звучания мы всё равно, мягко говоря, не получим, а тратить больше байт неразумно. Итак, мы остановились на 2х байтах.

При подсчете частоты, на самом деле, вылезает ещё одна большая проблема.
Если посмотреть частоты нот, то мы увидим, что они делятся на октавы.
Памятка. AVR. Buzic - 8
Для большинства несложных мелодий, достаточно 3х октав, я же решил извернуться и реализовать 6ть: большую, малую и следующие 4е.

Теперь отвлечемся от музыки и окунемся обратно в мир программирования микроконтроллеров.
Любой таймер в AVR(и подавляющим большинстве других МК) привязан к частоте самого МК. Частота кварца в моей схеме 16Mhz. Эта же частота определена «дефайном» F_CPU равным в моём случае 16000000. В регистре TCCR2B мы можем выбрать делители частоты, чтобы наш таймер 2 «тикал» не с бешеной скоростью 16000000 раз в секунду, а чуть медленнее. Делитель частоты выбирается битами CS2x, как говорилось выше.

Памятка. AVR. Buzic - 9
примечание: очевидно в ДШ опечатка вместо «CA2x» должно быть CS2x

Возникает вопрос — как настроить делитель?

Для этого необходимо понять как вычислять значения для регистра OCR2A. А вычислять его достаточно просто:
OCR2A = F_CPU / (делитель частоты кварца * 2) / частота ноты
Например, берем ноту ДО первой октавы и делитель 256(CS22 = 1, CS21 = 1, CS20 = 0):
OCR2A = 16000000 / (256 * 2) / 261 = 119

Сразу поясню — откуда взялось умножение на 2. Дело в том, что мы выбрали режим «Toggle» регистрами COM2Ax, а это значит, что смена уровней на ноге с низкого на высокий(или наоборот) и обратно будет происходить за 2 прохода таймера: сначала таймер дойдет до значения OCR2A и поменяет ногу микроконтроллера, допустим, с 1 на 0, сбросится и лишь на втором круге поменяет 0 обратно на 1. Поэтому на каждую полную волну уходит 2 круга таймера, соответственно делитель нужно умножить на 2, иначе мы получим лишь половину частоты нашей ноты.

Отсюда и появляется вышеупомянутая беда…

Если мы возьмем ноту ДО большой октавы и оставим делитель 256:
OCR2A = 16000000 / (256 * 2) / 65 = 480!!!
480 — это число явно больше чем 255 и физически не влезет в 8-битный регистр OCR2A.

Что же делать? Очевидно менять делитель, но если мы поставим делитель 1024, то с большой октавой будет всё хорошо. Проблемы начнутся с верхними октавами:
ЛЯ 4й октавы — OCR2A = 16000000 / (1024 * 2) / 3520 = 4
ЛЯ диез 4й октавы — OCR2A = 16000000 / (1024 * 2) / 3729 = 4
Значения OCR2A перестали отличаться, а значит и звук также перестанет отличаться.

Выход только один: для частоты нот нужно хранить не только значения регистра OCR2A, но и биты делителя частоты кварца. Т.к. для разных октав будет разное значение делителя частоты кварца, которое мы должны будем установить в регистре TCCR2B!

Теперь всё встаёт на свои места — и я наконец то объяснил, почему мы сразу не могли заполнить значение делителя в функции timer2_buzzer_init().

К сожалению, делитель частоты — это ещё 3 бита. И их придется занять во втором байте элемента массива мелодии.

Да здравствуют макросы

#define DIV_MASK (_BV(CS20) | _BV(CS21) |  _BV(CS22))

#define DIV_1024 (_BV(CS20) | _BV(CS21) |  _BV(CS22))
#define DIV_256 (_BV(CS21) |  _BV(CS22))
#define DIV_128 (_BV(CS20) |  _BV(CS22))
#define DIV_64 _BV(CS22)
#define DIV_32 (_BV(CS20) | _BV(CS21))

#define NOTE_1024( x ) ((F_CPU / (1024 * 2) / x) | (DIV_1024 << 8))
#define NOTE_256( x ) ((F_CPU / (256 * 2) / x) | (DIV_256 << 8))
#define NOTE_128( x ) ((F_CPU / (128 * 2) / x) | (DIV_128 << 8))
#define NOTE_64( x ) ((F_CPU / (64 * 2) / x) | (DIV_64 << 8))
#define NOTE_32( x ) ((F_CPU / (32 * 2) / x) | (DIV_32 << 8))

//большая октава
#define DOB NOTE_1024( 65 )
#define DO_B NOTE_1024( 69 )
#define REB NOTE_1024 ( 73 )
#define RE_B NOTE_1024 ( 78 )
#define MIB NOTE_1024 ( 82 )
#define FAB NOTE_1024 ( 87 )
#define FA_B NOTE_1024 ( 93 )
#define SOLB NOTE_1024 ( 98 )
#define SOL_B NOTE_1024 ( 104 )
#define LAB NOTE_1024 ( 110 )
#define LA_B NOTE_1024 ( 116 )
#define SIB NOTE_1024 ( 123 )

//малая октава
#define DOS NOTE_256( 131 )
#define DO_S NOTE_256( 138 )
#define RES NOTE_256 ( 146 )
#define RE_S NOTE_256 ( 155 )
#define MIS NOTE_256 ( 164 )
#define FAS NOTE_256 ( 174 )
#define FA_S NOTE_256 ( 185 )
#define SOLS NOTE_256 ( 196 )
#define SOL_S NOTE_256 ( 207 )
#define LAS NOTE_256 ( 219 )
#define LA_S NOTE_256 ( 233 )
#define SIS NOTE_256 ( 246 )

//первая октава
#define DO1 NOTE_256( 261 )
#define DO_1 NOTE_256( 277 )
#define RE1 NOTE_256 ( 293 )
#define RE_1 NOTE_256 ( 310 )
#define MI1 NOTE_256 ( 329 )
#define FA1 NOTE_256 ( 348 )
#define FA_1 NOTE_256 ( 369 )
#define SOL1 NOTE_256 ( 391 )
#define SOL_1 NOTE_256 ( 414 )
#define LA1 NOTE_256 ( 439 )
#define LA_1 NOTE_256 ( 465 )
#define SI1 NOTE_256 ( 493 )

//вторая октава
#define DO2 NOTE_128( 522 )
#define DO_2 NOTE_128( 553 )
#define RE2 NOTE_128 ( 586 )
#define RE_2 NOTE_128 ( 621 )
#define MI2 NOTE_128 ( 658 )
#define FA2 NOTE_128 ( 697 )
#define FA_2 NOTE_128 ( 738 )
#define SOL2 NOTE_128 ( 782 )
#define SOL_2 NOTE_128 ( 829 )
#define LA2 NOTE_128 ( 878 )
#define LA_2 NOTE_128 ( 930 )
#define SI2 NOTE_128 ( 985 )

//третья октава
#define DO3 NOTE_64( 1047 )
#define DO_3 NOTE_64( 1109 )
#define RE3 NOTE_64 ( 1175 )
#define RE_3 NOTE_64 ( 1245 )
#define MI3 NOTE_64 ( 1319 )
#define FA3 NOTE_64 ( 1397 )
#define FA_3 NOTE_64 ( 1480 )
#define SOL3 NOTE_64 ( 1568 )
#define SOL_3 NOTE_64 ( 1661 )
#define LA3 NOTE_64 ( 1760 )
#define LA_3 NOTE_64 ( 1865 )
#define SI3 NOTE_64 ( 1976 )

//четвертая октава
#define DO4 NOTE_32( 2093 )
#define DO_4 NOTE_32( 2217 )
#define RE4 NOTE_32 ( 2349 )
#define RE_4 NOTE_32 ( 2489 )
#define MI4 NOTE_32 ( 2637 )
#define FA4 NOTE_32 ( 2794 )
#define FA_4 NOTE_32 ( 2960 )
#define SOL4 NOTE_32 ( 3136 )
#define SOL_4 NOTE_32 ( 3322 )
#define LA4 NOTE_32 ( 3520 )
#define LA_4 NOTE_32 ( 3729 )
#define SI4 NOTE_32 ( 3951 )

А на длительность ноты у нас осталось всего 5 бит, так давайте же посчитаем длительность.

Длительность

Для начала необходимо перевести значение темпа во временные единицы(например в миллисекунды) — я сделал это так:
Длительность музыкального такта в мс = (60000 мс * 4 четверти) / значение темпа.

Соответственно, если мы говорим о долях такта, то данное значение необходимо делить и по началу я думал, что обычного сдвига влево для делителей будет достаточно. Т.е. код был таков:

uint16_t calc_note_delay(uint16_t precalced_tempo, uint16_t note)
{
   return (precalced_tempo / _BV((note >> 11) & 0b00111));
} 

Т.е. я использовал 3 бита(из оставшихся 5ти) и получил части музыкального такта из степеней 2ки аж до 1/128. Но когда я отдал товарищу с просьбой написать мне, какой нибудь рингтон под мою железяку возникли вопросы почему нет 1/3 или 1/6й и я начал думать…

В конце концов, я сделал хитрую систему, чтобы получить такие длительности. Один бит из оставшихся 2х — я потратил на признак умножения на 3 для делителя такта, получившегося после сдвига. А последний бит — для индикации того, нужно ли вычитать 1. Это сложно описать, проще посмотреть код:

uint16_t calc_note_delay(uint16_t precalced_tempo, uint16_t note)
{
   note >>= 11;
   uint8_t divider = _BV(note & 0b00111);
   note >>= 3;
   divider *= ((note & 0b01) ? 3 : 1);
   divider -= (note >> 1);
   return (precalced_tempo / divider);
}

Затем я «задефайнил» все возможные(кроме тех, которые меньше 1/128) длительности нот.

Вот они

#define DEL_MINUS_1 0b10000
#define DEL_MUL_3 0b01000

#define DEL_1 0
#define DEL_1N2 1
#define DEL_1N3 (2 | DEL_MINUS_1)
#define DEL_1N4 2
#define DEL_1N5 (1 | DEL_MINUS_1 | DEL_MUL_3)
#define DEL_1N6 (1 | DEL_MUL_3)
#define DEL_1N7 (3 | DEL_MINUS_1)
#define DEL_1N8 3

#define DEL_1N11 (2 | DEL_MUL_3 | DEL_MINUS_1)
#define DEL_1N12 (2 | DEL_MUL_3)

#define DEL_1N15 (4 | DEL_MINUS_1)
#define DEL_1N16 4

#define DEL_1N23 (3 | DEL_MUL_3 | DEL_MINUS_1)
#define DEL_1N24 (3 | DEL_MUL_3)

#define DEL_1N31 (5 | DEL_MINUS_1)
#define DEL_1N32 5

#define DEL_1N47 (4 | DEL_MUL_3 | DEL_MINUS_1)
#define DEL_1N48 (4 | DEL_MUL_3)

#define DEL_1N63 (6 | DEL_MINUS_1)
#define DEL_1N64 6

#define DEL_1N95 (5 | DEL_MUL_3 | DEL_MINUS_1)
#define DEL_1N96 (5 | DEL_MUL_3)

#define DEL_1N127 (7 | DEL_MINUS_1)
#define DEL_1N128 7

Собираем всё вместе

Итого, мы имеем следующий формат элемента массива нашего рингтона.

  • 1bit: delay divider — 1
  • 1bit: delay divider * 3
  • 3bit: delay divider shift
  • 3bit: cpu clock divider
  • 8bit: OCR2A value

Всего 16 бит.

Уважаемый читатель при желании может пофантазировать над форматом сам, может быть родится что-то более ёмкое, чем у меня.

Мы забыли ещё добавить пустую ноту, т.е. тишину. И вот наконец-то я объяснил почему мы в самом начале в функции timer2_buzzer_init() специально установили ногу PB3 на вход а не на выход. Меняя регистр DDRB, мы и будем включать и выключать проигрывание «тишины» или композиции в целом. Т.к. ноты со значением 0 у нас быть не может — она и будет являться «пустой» нотой.

Определим недостающие макросы и функцию включения генерации звука:

#define EMPTY_NOTE 0

#define NOTE(delay, note) (uint16_t)((delay << 11) | note)
........
........
........
void play_music_note(uint16_t note)
{
   if (note)
   {
      TCCR2B = (note >> 8) & DIV_MASK;
      OCR2A = note & 0xff;
      sbi(DDRB, BUZ_PIN);
   }
   else
      cbi(DDRB, BUZ_PIN);
}

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

const uint16_t king[] PROGMEM =
{
   NOTE(DEL_1N4, MI3),
   NOTE(DEL_1N4, FA_3),
   NOTE(DEL_1N4, SOL3),
   NOTE(DEL_1N4, LA3),

   NOTE(DEL_1N4, SI3),
   NOTE(DEL_1N4, SOL3),
   NOTE(DEL_1N2, SI3),

   NOTE(DEL_1N4, LA_3),
   NOTE(DEL_1N4, FA_3),
   NOTE(DEL_1N4, LA_3),
   NOTE(DEL_1N4, EMPTY_NOTE),

   NOTE(DEL_1N4, LA3),
   NOTE(DEL_1N4, FA3),
   NOTE(DEL_1N2, LA3),

   NOTE(DEL_1N4, MI3),
   NOTE(DEL_1N4, FA_3),
   NOTE(DEL_1N4, SOL3),
   NOTE(DEL_1N4, LA3),

   NOTE(DEL_1N4, SI3),
   NOTE(DEL_1N4, SOL3),
   NOTE(DEL_1N4, SI3),
   NOTE(DEL_1N4, MI4),

   NOTE(DEL_1N4, RE4),
   NOTE(DEL_1N4, SI3),
   NOTE(DEL_1N4, SOL3),
   NOTE(DEL_1N4, SI3),

   NOTE(DEL_1N2, RE4),
   NOTE(DEL_1N2, EMPTY_NOTE),
};

Проигрывание рингтона

У нас осталась одна задача — проигрывание мелодии. Для этого нам нужно «бежать» по массиву рингтона, выдерживая соответствующие паузы и переключая частоты нот. Очевидно, нам нужен ещё один таймер, который, к слову, можно использовать и для других общих задач, как обычно делаю я. Причем переключаться между элементами массива можно либо в прерывании этого таймера, либо в основном цикле, а таймер использовать для вычисления времени. В этом примере я использовал 2й вариант.

Как известно тело любой программы для МК включает в себя бесконечный цикл:

int main(void)
{
    for(;;)
    {
      //здесь будет проигрыватель
    }
    return 0;
}

В нем мы и будем «бежать» по нашему массиву. Но нам нужна функция, подобная GetTickCount из WinApi, возвращающая количество миллисекунд в операционных системах семейства Windows. Но естественно в мире МК нет никаких таких функций «из коробки», поэтому мы должны написать её сами.

Таймер 1

Для подсчета временных промежутков(я намеренно не пишу миллисекунд, позднее Вы поймете почему) я использовал таймер 1 совместно с, уже известным нам, CTC режимом. Таймер 1 является 16-битным таймером, а это значит, что значение модуля сравнения для него указывается уже 2мя 8-битными регистрами OCR1AH и OCR1AL — для старшего и младшего байта соответственно. Я не хочу подробно описывать работу с таймером 1, так как это не относится к основной теме данной памятки. Поэтому расскажу всего лишь в 2х словах.

Нам, фактически нужны 3 функции:

  • Инициализация таймера
  • Обработчик прерывания таймера
  • функция, возвращающая количество временных промежутков.

Код С файла

#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/atomic.h>
#include "timer1_ticks.h"

volatile unsigned long timer1_ticks;

//обработчик прерывания
ISR (TIMER1_COMPA_vect)
{
    timer1_ticks++;
}

void timer1_ticks_init()
{
    // настройка таймера
    // CTC режим, делим частоту кварца на 8
    TCCR1B |= (1 << WGM12) | (1 << CS11);
    
    //устанавливаем значение для модуля сравнения
    OCR1AH = (uint8_t)(CTC_MATCH_OVERFLOW >> 8);
    OCR1AL = (uint8_t) CTC_MATCH_OVERFLOW;

    // Разрешить прерывание таймера
    TIMSK1 |= (1 << OCIE1A);
}

unsigned long ticks()
{
    unsigned long ticks_return;

    //должно быть атомарно, чтобы значение ticks_return было корректным 
    //во время внезапного возникновения прерывания 
    ATOMIC_BLOCK(ATOMIC_FORCEON)
    {
        ticks_return = timer1_ticks;
    }

    return ticks_return;
}

Прежде чем я покажу заголовочный файл с определенной константой CTC_MATCH_OVERFLOW, нам нужно немного вернуться во времени к разделу «Длительность» и определить самый главный для мелодии макрос, который вычисляет темп мелодии. Я долго ждал для того, чтобы его определить, так как он непосредственно связан с проигрывателем, а значит с таймером 1.
В первом приближении он выглядел так(см. вычисления в разделе «Длительность»):

#define TEMPO( x ) (60000 * 4 / x)

Значение которое мы получаем на выходе мы должны в последствии подставить первым аргументом в функцию calc_note_delay. Теперь внимательно посмотрим на функцию calc_note_delay, а именно на строчку:

return (precalced_tempo / divider);

Мы видим, что значение, полученное в результате вычисления макроса TEMPO, делится на некий делитель. Вспомним, что максимальный делитель который у нас определен — это DEL_1N128, т.е. делитель будет 128.

Теперь возьмем распространенное значение темпа равное 240 и проведем нехитрые вычисления:
60000 * 4 / 240 = 1000
О ужас! У нас получилось всего 1000, в виду того, что это значение ещё будет делиться на 128, мы рискуем скатиться в 0, при высоких значениях темпа. Это вторая проблема длительности.

Как же её решить? Очевидно, чтобы расширить диапазон значений темпа, нам каким то образом надо увеличить число, получающиеся в результате вычисления макроса TEMPO. Это можно сделать только одним способом — уйти от миллисекунд и считать время в неких временных промежутках. Теперь Вы поняли, почему я всё это время избегал упоминания «миллисекунд» в рассказе. Давайте определим ещё один макрос:

 #define MS_DIVIDER 4

Пусть он будет нашим делителем миллисекунды — разделим миллисекунду, допустим, на 4(250 мкс).
Тогда необходимо поменять макрос TEMPO:

#define TEMPO( x ) (60000 * MS_DIVIDER * 4 / x)

Теперь я с чистой совестью приведу заголовочный файл для работы с таймером 1:

#ifndef TIMER1_TICKS_H_INCLUDED
#define TIMER1_TICKS_H_INCLUDED

#define MS_DIVIDER 4
#define CTC_MATCH_OVERFLOW ((F_CPU / 1000) / (8 * MS_DIVIDER))

void timer1_ticks_init();
unsigned long ticks();

#endif // TIMER1_TICKS_H_INCLUDED

Теперь Мы можем, меняя MS_DIVIDER, подгонять диапазон под наши задачи — у меня в коде стоит 4 — для моих задач этого хватило. Внимание: если у Вас будут ещё какие либо задачи «завязанные» на таймер 1, не забывайте контрольные значения времени для них умножать/делить на MS_DIVIDER.

Проигрыватель

Теперь напишем наш проигрыватель. Я думаю из кода и комментариев будет всё понятно.

int main(void)
{
    timer1_ticks_init();
    // разрешаем прерывания
    sei();
    timer2_buzzer_init();

    //в миллисекундах деленных на MS_DIVIDER
    long time_since = ticks();

    //текущая длительность ноты в миллисекундах деленных на MS_DIVIDER
    uint16_t note_delay = 0;
    //текущая позиция в массиве мелодии
    uint16_t note_pos = 0;
    //длина мелодии
    uint16_t length = sizeof(king) / sizeof(king[0]);
	//устанавливаем значение темпа под рингтон
    uint16_t tempo = TEMPO(240);

    for(;;)
    {
            unsigned long time_current = ticks();
            if (time_current - time_since > note_delay)
            {
                //читаем элемент массива
                uint16_t note = pgm_read_word(&king[note_pos]);
                //установить частоту ноты
                play_music_note(note);
                //вычислить длительность проигрывания ноты
                note_delay = calc_note_delay(tempo, note);
                //зациклить мелодию
                if (++note_pos >= length)
                    note_pos = 0;
                time_since = time_current;
            }
    }
    return 0;
}

Заключение

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

Ну и традиционно видео и исходный код(разрабатывал я это в среде Code Blocks, так что не пугайтесь непонятных файлов):

Исходники

Автор: fsmoke

Источник

Поделиться

* - обязательные к заполнению поля