Процедура tone() — что там внутри…

В статье про формирование звука с помощью 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() задействует один таймер и подменяет процедуру обработки прерываний от него. Сигнал может быть сформирован на любом выводе микроконтроллера.

Добавить комментарий

Ваш e-mail не будет опубликован.

Нажимая кнопку "Отправить" Вы даёте свое согласие на обработку введенной персональной информации в соответствии с Федеральным Законом №152-ФЗ от 27.07.2006 "О персональных данных".