максимальная частота ШИМ на основе прерываний при 500 Гц

Мне нужно управлять ультразвуковым преобразователем 24 кГц, и я решил использовать Arduino Uno, который у меня есть.

Использование tone() или аппаратной ШИМ Atmega не очень подходит, потому что мне нужны две прямоугольные волны 50/50 с разностью фаз pi (что равно двум выводам с XOR) для переключать полярность выхода h-моста синхронно. Поскольку UC больше нечего делать, я написал следующий скетч, в котором я просто жду прерывания и переключаю контакты 2 и 3:

bool do_task=false;
void timer_init () {
  TCCR0A |= (1 << WGM01);
  OCR0A = 83;
  TIMSK0 |= (1 << OCIE0A);
  TCCR0B |= (1 << CS01);
}
ISR (TIMER0_COMPA_vect) {
  do_task=true;
}

bool state=false;
void toggle() {
  state=!state;
  digitalWrite(2,state);
  digitalWrite(3,!state);

}

void setup() {
  pinMode(2,OUTPUT);
  pinMode(3,OUTPUT);
  timer_init();
}

void loop() {
  if(do_task) {
    do_task=false;
    toggle();
  }
}

Я ожидал, что на выходе будет частота ~24 кГц, но вместо этого она составляет 500 Гц.

Насколько я понимаю:

  • Частота ЦП 16 МГц
  • пределитель timer0 равен 8 (TCCR0B |= (1 << CS01))
  • выходное значение сравнения (OCR0A) равно 83

приведет к частоте 16 МГц / 8 / 83 = 24096,4 кГц - но, как я уже упоминал, это 500 Гц и, похоже, не зависит от прескалера.

, 👍1

Обсуждение

Вызовы digitalWrite() занимают слишком много времени. Это довольно большая функция. Используйте прямое портманипулирование, @chrisl

вы должны удалить предыдущие настройки таймера. Ядро Arduino инициализирует таймеры для PWM, и вы делаете |=, @Juraj

Почему бы вам не сделать переключение в ISR? Время, которое требуется функции цикла, чтобы найти, что флаг изменился, не будет тривиальным., @Nick Gammon

@chrisl, в конце концов, я решил не использовать ядро ардуино, все ведет себя так странно ... что именно делает простую функцию, такую как digitalWrite(), такой чертовски медленной??! Почему это не просто функция-оболочка для прямого манипулирования портом?, @Sim Son

@NickGammon Я начал сокращать код, и он просто остался там. Сколько циклов на самом деле занимает такое логическое сравнение? Изначально я планировал считывать частоту с потенциометра после каждого прерывания, разве это не было бы легко возможно за 8×83=664 цикла?, @Sim Son

@Юрай правильно! Теперь я решил не использовать ядро ардуино ..., @Sim Son


2 ответа


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

2

Что именно делает такую простую функцию, как digitalWrite(), такой чертовски медленной??! Почему это не просто функция-оболочка для прямого управления портом?

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

Если вы используете библиотеку DigitalWriteFast, это превращает вашу запись в прямую манипуляцию с портом, при условии, что вы используете константы.

Таким образом, вы можете использовать:

digitalWriteFast(2,HIGH); // или НИЗКИЙ

Но нет:

digitalWriteFast(myPort,someState);  // если они не константы

На самом деле вы можете отправлять переменные, но это превращается в те же медленные вызовы, которые использует digitalWrite.


Теперь я решил не использовать ядро arduino

С ядром все в порядке. Однако нужно понимать, что он делает. Чтобы быть вежливым с новичками, такие вещи, как digitalWrite, могут принимать переменные в качестве аргументов, поэтому возможны такие вещи:

for (int i = 0; i < 13; i++)
  digitalWrite (i, HIGH);

Как я намекнул в комментарии, все это можно сделать в ISR:

ISR (TIMER0_COMPA_vect) {
  static bool state;
  if (state)
    {
    digitalWriteFast(2,HIGH);
    digitalWriteFast(3,LOW);
    state = false; 
    }
  else
    {
    digitalWriteFast(2,LOW);
    digitalWriteFast(3,HIGH);
    state = true; 
    }
}  // конец TIMER0_COMPA_vect

Изначально я планировал считывать частоту с потенциометра после каждого прерывания, разве это не было бы легко возможно за 8×83=664 цикла?

Аналоговое чтение (чтение из АЦП) занимает около 104 мкс при использовании предварительного делителя АЦП по умолчанию. То есть 1664 такта. Так что нет, вы не можете читать в это время. Это аппаратное ограничение. Вы можете уменьшить предварительный масштаб без потери точности.

См. мою страницу о ADC. Установка предделителя 16 вместо 128 значительно увеличит скорость с минимальной потерей точности.

Чтобы сделать это на Uno:

ADCSRA &= ~(bit (ADPS0) | bit (ADPS1) | bit (ADPS2)); // очистить биты предделителя
ADCSRA |= bit (ADPS2);                               // 16
,

Я не новичок в программировании UC, я просто предпочитаю использовать программные регистры напрямую, а не в среде arduino. Я подумал, что эту маленькую программу можно было бы быстрее реализовать с использованием ядра arduino без необходимости искать что-то в таблице данных, но теперь я все равно это сделал. Что касается adc: если он находится в автономном режиме, аппаратное обеспечение выполняет преобразование параллельно. Поэтому, если я проверяю бит состояния только для завершенного преобразования, это не должно быть проблемой, и фактическое время, необходимое для обработки аналогового значения, значительно уменьшится. Разве это не правда?, @Sim Son

Мне нужно только аналоговое значение каждые 100 мс, и я не очень много обрабатываю его..., @Sim Son


3

Вы писали:

Использование [...] аппаратного pwm atmega не очень подходит, потому что я нужны две прямоугольные волны 50/50 с разностью фаз пи

Я считаю, что аппаратная ШИМ — лучшее решение вашей проблемы. ШИМ выходы могут быть установлены либо в «инвертирующий», либо в «неинвертирующий» режим. Установите два канала одного таймера на один и тот же рабочий цикл, с одним в неинвертирующем режиме, а другой в инвертирующем режиме, и у вас есть ваши дополнительные результаты.

Пример использования таймера 1:

const float SIGNAL_FREQUENCY = 24e3;
const uint16_t HALF_PERIOD = round(F_CPU / SIGNAL_FREQUENCY / 2);
const uint16_t PERIOD = 2 * HALF_PERIOD;

int main()
{
    /* Set the PWM pins as outputs. */
    DDRB |= _BV(PB1);  // цифровой 9 = PB1 = OC1A
    DDRB |= _BV(PB2);  // цифровой 10 = PB2 = OC1B

    /* Configure Timer 1. */
    ICR1   = PERIOD   - 1;
    OCR1A  = PERIOD/2 - 1;
    OCR1B  = PERIOD/2 - 1;
    TCCR1A = _BV(COM1A1)  // неинвертирующий ШИМ на OC1A
           | _BV(COM1B0)  // инвертирование ШИМ на OC1B
           | _BV(COM1B1)  // то же самое
           | _BV(WGM11);  // режим 14: быстрая ШИМ, TOP = ICR1
    TCCR1B = _BV(WGM12)  // то же самое
           | _BV(WGM13)  // то же самое
           | _BV(CS10);  // часы на F_CPU, запуск таймера
}
,

Спасибо за Ваш ответ! Да, я знаю режим инвертирования, но скоро мне нужно будет также изменить частоту в небольшом диапазоне. Сейчас я делаю прямые манипуляции с портами, и пока все работает нормально. Но вопрос: что это за _BV() вы используете?, @Sim Son

@SimSon: _BV(n) [это макрос для (1<<(n))](https://www.nongnu.org/avr-libc/user-manual/group__avr__sfr.html#ga11643f271076024c395a93800b3d9546) , из avr-libc. Я предполагаю, что это означает «значение байта». Мне нравится использовать его при написании AVR-идиоматического кода., @Edgar Bonet