В статье про формирование звука с помощью Arduino было отмечено удобство использования функции tone() для генерации сигнала. Интересно было покопаться в библиотеке Brett Hagman — каким образом реализована эта функция?
tone()
Генерирует на порту вход/выхода сигнал — прямоугольную «волну», заданной частоты и с 50% рабочим циклом.
Несложно найти файл с описанием исследуемой процедуры:
C:\Program Files\Arduino\hardware\arduino\cores\arduino\tone.cpp
Размер текста довольно внушителен, но понять его можно. Когда очистим текст от многочисленных проверок наличия тех или иных таймеров и регистров, то, упрощенно (тот уникальный случай, когда функция toneBegin(_pin) вернула нам значение 2 — доступный Таймер 2), процедуру можно изобразить так:
// frequency (in hertz) and duration (in milliseconds). void tone(uint8_t _pin, unsigned int frequency, unsigned long duration) { uint8_t prescalarbits = 0b001; long toggle_count = 0; uint32_t ocr = 0; int8_t _timer; _timer = toneBegin(_pin); // Set the pinMode as OUTPUT pinMode(_pin, OUTPUT); // if we are using an 8 bit timer, scan through prescalars to find the best fit ocr = F_CPU / frequency / 2 - 1; prescalarbits = 0b001; // ck/1: same for both timers if (ocr > 255) { ocr = F_CPU / frequency / 2 / 8 - 1; prescalarbits = 0b010; // ck/8: same for both timers if (_timer == 2 && ocr > 255) { ocr = F_CPU / frequency / 2 / 32 - 1; prescalarbits = 0b011; } if (ocr > 255) { ocr = F_CPU / frequency / 2 / 64 - 1; prescalarbits = _timer == 0 ? 0b011 : 0b100; } } TCCR2B = prescalarbits; // Calculate the toggle count if (duration > 0) toggle_count = 2 * frequency * duration / 1000; else toggle_count = -1; // Set the OCR for the given timer, // set the toggle count, // then turn on the interrupts OCR2A = ocr; timer2_toggle_count = toggle_count; bitWrite(TIMSK2, OCIE2A, 1); }
В переменной prescalarbits формируется коэффициент «предделения» для таймера микроконтроллера. Делитель выбирается минимально возможный для достижения требуемой частоты сигнала (аргумент функции frequency) — для этого служат многочисленные проверки ocr и 255.
Используемый для формирования сигнала таймер определяется в функции toneBegin(_pin) — она приведена в конце статьи, в-общем, она использует определения pin из pins_arduino.h.
Далее для выбранного таймера загружается сформированный предделитель. Найденный коэффициент деления для установленного в плате кварца и заданной частоты формируемого сигнала (переменная ocr) записывается в регистр сравнения Таймера (в нашем «узком» случае — OCR2A).
Длительность формируемого сигнала определяется подсчетом числа периодов сигнала — для этого формируется переменная toggle_count (и далее ее конкретный для выбранного таймера аналог — в нашем случае timer2_toggle_count).
В завершении процедуры, разрешаются прерывания для выбранного Таймера.
Сам сигнал на выводе микроконтроллера формируется в обработке прерывания от Таймера (также упростим):
ISR(TIMER2_COMPA_vect)
{
if (timer2_toggle_count != 0)
{ // toggle the pin
*timer2_pin_port ^= timer2_pin_mask;
if (timer2_toggle_count > 0)
timer2_toggle_count--;
}
}
В каждом прерывании сигнал на определенном все в той же функции toneBegin(_pin) выводе микроконтроллера инвертируется (таким образом формируется сигнал с 50% «рабочим циклом» 🙂 ). Одновременно уменьшается значение переменной timer2_toggle_count (в нашем примере). Как только переменная обнулится, инверсия сигнала на выводе прекращается (формирование сигнала заканчивается).
Посмотрим как выглядит toneBegin(_pin), возвращающая номер «свободного для использования в tone()» Таймера микроконтроллера:
static int8_t toneBegin(uint8_t _pin) { int8_t _timer = -1; // if we're already using the pin, the timer should be configured. for (int i = 0; i < AVAILABLE_TONE_PINS; i++) { if (tone_pins[i] == _pin) { return pgm_read_byte(tone_pin_to_timer_PGM + i); } } // search for an unused timer. for (int i = 0; i < AVAILABLE_TONE_PINS; i++) { if (tone_pins[i] == 255) { tone_pins[i] = _pin; _timer = pgm_read_byte(tone_pin_to_timer_PGM + i); break; } } if (_timer != -1) { // Set timer specific stuff // All timers in CTC mode // 8 bit timers will require changing prescalar values, // whereas 16 bit timers are set to either ck/1 or ck/64 prescalar switch (_timer) { case 0: // 8 bit timer TCCR0A = 0; TCCR0B = 0; bitWrite(TCCR0A, WGM01, 1); bitWrite(TCCR0B, CS00, 1); timer0_pin_port = portOutputRegister(digitalPinToPort(_pin)); timer0_pin_mask = digitalPinToBitMask(_pin); break; case 1: // 16 bit timer TCCR1A = 0; TCCR1B = 0; bitWrite(TCCR1B, WGM12, 1); bitWrite(TCCR1B, CS10, 1); timer1_pin_port = portOutputRegister(digitalPinToPort(_pin)); timer1_pin_mask = digitalPinToBitMask(_pin); break; case 2: // 8 bit timer TCCR2A = 0; TCCR2B = 0; bitWrite(TCCR2A, WGM21, 1); bitWrite(TCCR2B, CS20, 1); timer2_pin_port = portOutputRegister(digitalPinToPort(_pin)); timer2_pin_mask = digitalPinToBitMask(_pin); break; } } return _timer; }
Сначала определяется «а не используется ли уже этот пин в tone(), запущенном ранее?» Если да, то с тем таймером и работаем и пользуемся уже заданными значениями переменных. Если нет, смотрим доступный таймер и работаем с ним. Параметры таймера и сопутствующих переменных выставляются в соответствующем switch-case.
Приведенное описание tone() очень сильно упрощено для облегчения понимания общего принципа функционирования процедуры.
Выводы можно сделать такие: применение tone() задействует один таймер и подменяет процедуру обработки прерываний от него. Сигнал может быть сформирован на любом выводе микроконтроллера.