Программный ШИМ на ATmega328P с максимальной частотой

Научная работа в ВУЗе. Получить максимальную частоту ШИМ на ардуино, в моем случае нано.

Конкретная проблема: когда указываю в регистр совпадения OCR1A число 10, частота на выходе примерно 500 кГц, когда указываю в тот-же самый регистр совпадения число 9 или 8 или 7 и т.д. частота не меняется никак, абсолютно никак, как была 500, так и осталась.

Код внутри обработчика прерывания способен выполняться со скоростью 4 МГц, проверял - знаю.

Ну не могу понять и принять, что скорость упала в 30 раз, так ещё и не реагирует на изменение регистра совпадения, хотя код не должен быть узким местом.

#include <avr/io.h>
#include <avr/interrupt.h>

#define r 2


void setup() {
  cli();  // отключить глобальные прерывания
  TCCR1A = 0;
  TCCR1B = 0;
  OCR1A = 5; // установка регистра совпадения
  TCCR1B |= (1 << WGM12);  // включение в CTC режим
  TCCR1B |= (1 << CS10);  // Установить CS10 бит так, чтобы таймер работал при тактовой частоте:
  TIMSK1 |= (1 << OCIE1A);  // включение прерываний по совпадению
  sei();

  pinMode(r, OUTPUT);
}


void loop() {

}


ISR(TIMER1_COMPA_vect) {
  PORTD |= (1 << 2);
  PORTD &= ~(1 << 2);
}

Цель кода дергать ногой D2, с максимальной частотой. На деле, между короткими импульсами длиной 150-200 нс, существует огромный интервал в 2 мкс.

Я понимаю, что период 200 нс + 200 нс я не получу, но хотя бы 200 нс + 800 нс должен.

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

UPD: При изменении регистра совпадения в меньшую сторону, ничего не меняется, но если его увеличивать, то и частота упадет. Вторая часть прошлого предложения может быть не всем интуитивно понятна из моего вопроса, так что решил добавить.


Ответы (3 шт):

Автор решения: maestro

С этим контроллером не работал, но вот что бросается в глаза.

Во-первых, вы странно устанавливаете предделитель. Вы пишете:

TCCR1B |= (1 << CS10);  // Установить CS10 бит так, чтобы таймер работал при тактовой частоте

Но вот что в документации:

введите сюда описание изображения

Вы не привели определение константы CS10, но есть здесь подразумевается, что в TCCR1B[2:0] запишется 0b010, то у вас получится делитель 8. Это явно не то, что вы хотели.

Во-вторых, вы пишете:

ISR(TIMER1_COMPA_vect) {
  PORTD |= (1 << 2);
  PORTD &= ~(1 << 2);
}

Записывать бит в порт ввода-вывода и сразу же его сбрасывать - плохая идея, так как ядро процессора и порты могут иметь различную тактовую частоту. Установка логического уровня на выводе микроконтроллера производится не в момент записи значения в регистр, а в момент фронта тактовой частоты. Если тактовая частота портов ввода-вывода меньше тактовой частоты ядра, или их тактовые сигналы рассинхронизированы, вы можете вообще не увидеть импульса. Причём это будет крайне сложно обнаружить, так как, предположим, 100 импульсов будут проходить нормально, а 101-й будет пропущен.

Вместо этого рекомендую в обработчике прерывания инвертировать состояние бита:

ISR(TIMER1_COMPA_vect) {
    PORTD ^= (1u << r);  // Вы же определили константу r, не так ли?
}

В третьих, вы действительно уверены, что вам нужен именно программный ШИМ? Таймеры микроконтроллеров умеют выводить ШИМ-сигнал на ножки сами, без участия ядра. Да, для этого придётся серьёзно разобраться в его работе, но вы значительно разгрузите ядро и упростите код. Вы можете регулировать период сигнала и коэффициент заполнения, и минимально возможный период будет соответствовать половине тактовой частоты. Настоятельно рекомендую использовать этот подход.

В четвёртых - здесь я сужу по другим контроллерам, но уверен, что AVR ничем не отличается - режим сравнения, который вы упомянули, уже предполагает, что таймер будет самостоятельно устанавливать логические уровни на портах ввода-вывода. Обычно бывает так: есть регистр сравнения и регистр переполнения. Когда значение регистра счётчика достигает значения регистра сравнения, таймер устанавливает высокий логический уровень. Счётчик же продолжает считать. Когда значение счётчика достигает значения регистра переполнения, таймер устанавливает на выводе низкий логический уровень и сбрасывает счётчик в 0. Процесс продолжается циклически.

Вы упомянули, что частота на выходе никак не меняется от регистра сравнения. Это может быть потому, что вы не установили значение регистра переполнения. Допустим, регистр сравнения равен 10, регистр переполнения равен 65535. Тогда через 10 тактов после запуска программы вы словите прерывание TIMER1_COMPA_vect, но следующее прерывание будет только через 65535 тактов!

Почитайте в документации принцип работы таймера, особое внимание уделите режиму сравнения. И настоятельно рекомендую перейти на аппаратную генерацию ШИМ.

→ Ссылка
Автор решения: MarianD

Когда единой целью является получить максимальную частоту, зачем усложнять задачу таймером/счетчиком и прерываниями, а не просто в бесконечном цикле переключать значение соответствующего бита:

#include <avr/io.h>

void setup() {
  TCCR1B = 0;
  TCCR1B |= (1 << CS10);  // Максимальная частота (прескалер == 1)

  DDRB   |= (1 << PORT2); // То же самое, как pinMode(PORT2, OUTPUT);
}

void loop() {
  PORTD ^=  (1 << PORT2); // Переключение бита (операция XOR)
}

Примечание:

Не надо придумывать имена для портов (#define r 2), они все уже определенны в файле portpins.h (который включен в <avr/io.h>), например в нем определения

/* Port Data Register (generic) */
#define    PORT7        7
#define    PORT6        6
#define    PORT5        5
#define    PORT4        4
#define    PORT3        3
#define    PORT2        2
#define    PORT1        1
#define    PORT0        0
→ Ссылка
Автор решения: Solt

Быстрее, чем переход без проверок вряд ли что-то придумаешь.

Второй переход goto _m2 сделан для выравнивания времени.

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

#include <avr/io.h>

void setup() {
  TCCR1B = 0;
  TCCR1B |= (1 << CS10);  // Максимальная частота (прескалер == 1)

  DDRB   |= (1 << PORT2); // То же самое, как pinMode(PORT2, OUTPUT);

  my_loop();
}

void my_loop() {
_m1:
  PORTD |=  (1 << PORT2);
  goto _m2;
_m2:
  PORTD &= ~(1 << PORT2);
  goto _m1;
}

Если сбой скважности 1 раз на 100 не критичен, можно разогнать так:

void my_loop() {
_m1:
  PORTD |=  (1 << PORT2);
  PORTD &= ~(1 << PORT2);

  ... 100 раз эти же строки

  PORTD |=  (1 << PORT2);
  //Одну строчку можно закомментировать
  //Тогда фазу не собьём за счет потери одного пика.
  //PORTD &= ~(1 << PORT2);

  goto _m1;
}
→ Ссылка