Точность таймера Всегда отключено на 1 мкс

Я использую ATMega32u4 для генерации импульса частотой 1 кГц. Я использую Таймер 3, который является 16-битным таймером и использует предварительный делитель 1, поэтому у меня есть разрешение 0,0625 мкс. Хорошая новость в том, что это работает, я могу контролировать частоту и длину импульса прямоугольной волны. Проблема в том, что я заметил, что пульс всегда кажется отключенным на 1 мкс.

Если установить период 1000 мкс и импульс 500 мкс, осциллограф покажет период 1001 мкс и импульс 501 мкс.

Если установить период 100 мкс и импульс 10 мкс, осциллограф покажет период 101 мкс и импульс 11 мкс.

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

const int pin1 = 4;

void setup() {
  //Установить пин для вывода
  pinMode(pin1, OUTPUT);

  // Сброс регистра управления таймера 3 в 0
  TCCR3A = 0;

  // Установить прескалер в 1
  TCCR3B &= ~(1 << CS32); //0
  TCCR3B &= ~(1 << CS31); //0
  TCCR3B |= (1 << CS30);  //1

  //Сброс таймера 3 и установка значения для сравнения
  TCNT3 = 0;

  // Включение таймера 3 для сравнения прерываний A и B
  TIMSK3 |= (1 << OCIE3B);
  TIMSK3 |= (1 << OCIE3A);

  //Включить глобальные прерывания
  sei();

  //Установить частоту обновления
  OCR3A = 1000;
  OCR3B = 100 / 0.0625;
}

void loop() {
  delay(100);
}

ISR(TIMER3_COMPA_vect) {
  PORTD &= ~_BV(pin1); //НИЗКИЙ
  OCR3A = 160;
}

ISR(TIMER3_COMPB_vect) {
  TCNT3 = 0;
  PORTD |= _BV(pin1); //ВЫСОКИЙ
}

, 👍3

Обсуждение

Зачем делать деление с плавающей запятой в «100/0,0625», а не просто «1600»?, @hcheung

@hcheung Я просто делаю это, чтобы его было легко изменить. Я скажу «1600», когда заблокирую частоту, которую хочу использовать. Сейчас только тестирую, пока работает., @M.Schindler

Дело в том, что вычисления с плавающей запятой требуют времени, ваш вопрос касается времени... Конечно, компилятор может оптимизировать его для вас во время компиляции, но это предположение., @hcheung

@hcheung, операция с плавающей запятой есть только в настройке, поэтому она должна обрабатывать это число только один раз, а затем помещать его в регистр. Если я изменю его на «1600», время не изменится., @M.Schindler

о да, вы правы., @hcheung

Мало того, компилятор, вероятно, оптимизирует его до постоянного значения, так что вообще не будет хрустящих чисел (по крайней мере, не для ATMega)., @StarCat


1 ответ


Лучший ответ:

4

Проблема здесь:

ISR(TIMER3_COMPB_vect) {
  TCNT3 = 0;
  ...

Если вы хотите получить что-то, что смутно похоже на точное время, вы не должны никогда сбрасывать таймер. MCU требуется время для обработки прерывание: он должен сохранить счетчик программ в стек, загрузить адрес вектора прерывания, выполнить его (обычно это переход), сохранить все нужные ему регистры в стек, и только после этого он может стартовать выполнение кода, который вы написали в ISR. Судя по вашему эксперименту вроде весь этот процесс занимает около 1 мкс. Но не принимайте эту задержку само собой разумеющееся: время от времени прерывание будет срабатывать, пока MCU обработки другого прерывания, и это увеличит задержку.

У вас есть два варианта:

  1. Обновите регистры сравнения, чтобы запрограммировать следующие прерывания некоторое время в будущем. В этом случае COMPB ISR будет делать OCR3A = OCR3B + 160; OCR3B += 1600;. Не беспокойтесь о переполнения: они поступают правильно.

  2. Позвольте таймеру сбросить себя до нуля: тогда он сделает это без потеря одного цикла. См. режимы CTC и Fast PWM таймера.

Для вашего конкретного случая использования ШИМ кажется очевидным выбором. Не только будет ли таймер сам обрабатывать сброс, он также может генерировать выходной сигнал, не требуя от вас записи ISR. Остерегайтесь, что в в этом случае период сигнала - это то, что вы загружаете в соответствующий сравнить регистр плюс один, а длительность импульса другая сравнить регистр плюс один.

В качестве примечания:

  // Reset Timer 3 Control Register to 0
  TCCR3A = 0;

Я бы также сбросил TCCR3B. Ядро Arduino настраивает оба регистра для своих целей, поэтому было бы безопаснее очистить их обоих.

,