Чтение квадратурного энкодера в реальном времени с полным разрешением только с одним прерыванием на ATmega328

Я хочу прочитать квадратурные энкодеры с полным разрешением и одним прерыванием на Arduino Nano (ATmega328). Итак, я обнаружил, что мы можем использовать XOR для достижения полного разрешения:


Где контакт 3 является прерываемым, а 4 и 5 — нет.

Наши требования:

  1. Полное разрешение: например, решение, предложенное в этом ответе, работает только с половинным разрешением, не обнаруживая все края.
  2. В режиме реального времени: быть максимально быстрым до уровня, который позволяет это оборудование, и безопасным с точки зрения отсутствия пропуска ни одного из фронтов сигнала.
  3. используя только одно прерывание. Нам нужен другой для других целей.

На данный момент я разработал два разных кода, один из которых направлен на обеспечение безопасности, а другой — на производительность:

Преамбула:

#include "digitalWriteFast.h"

#define pinT 3 // триггерный вывод, исходящий от XOR

#define pinA 4 // канал A
#define pinB 5 // канал B

volatile long counter = 0;
long counter_ = counter;

где библиотеку digitalWriteFast можно загрузить здесь.

Исполнитель:

volatile short int increment = 0;
volatile bool tmpA;
volatile bool tmpB;
volatile bool lastA;

ISR(trigger) {
  tmpA = digitalReadFast(pinA);
  tmpB = digitalReadFast(pinB);
  counter += (1 - 2 * tmpA) * (1 - 2 * tmpB) * (1 - 2 * (lastA != tmpA));
  lastA = tmpA;
}

void setup() {
  pinMode(pinA, INPUT_PULLUP);
  pinMode(pinB, INPUT_PULLUP);
  pinMode(pinT, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(pinT), trigger, CHANGE);
  lastA = digitalReadFast(pinA);
  Serial.begin(9600);
}

void loop() {
  if (counter != counter_) {
    Serial.println(counter);
    counter_ = counter;
  }
}

Безопасно:

volatile unsigned long error = 0;
unsigned long error_ = 0;

volatile bool inputA;
volatile bool inputB;

volatile short state; // 100 110 101 111
volatile short state_;

void detectState() {
  inputA = digitalReadFast(pinA);
  inputB = digitalReadFast(pinB);

  if (inputA) {
    if (inputB) {
      state = 111;
    } else {
      state = 110;
    }
  } else {
    if (inputB) {
      state = 101;
    } else {
      state = 100;
    }
  }
}

ISR(trigger) {

  detectState();

  if (state_ != state) {
    switch (state_) {
      case 100:
        if (state == 110) {
          counter++;
        } else if (state == 101) {
          counter--;
        } else {
          error++;
        }
        break;
      case 110:
        if (state == 100) {
          counter--;
        } else if (state == 111) {
          counter++;
        } else {
          error++;
        }
        break;
      case 101:
        if (state == 100) {
          counter++;
        } else if (state == 111) {
          counter--;
        } else {
          error++;
        }
        break;
      case 111:
        if (state == 110) {
          counter--;
        } else if (state == 101) {
          counter++;
        } else {
          error++;
        }
        break;
      default:
        error++;
        break;
    }

    state_ = state;
  } else {
    error++;
  }
}

void setup() {
  pinMode(pinT, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(pinT), trigger, CHANGE);

  pinMode(pinA, INPUT_PULLUP);
  pinMode(pinB, INPUT_PULLUP);

  detectState();
  state_ = state;

  Serial.begin(9600);
}

void loop() {
  if (counter != counter_) {
    Serial.print("counter: ");
    Serial.println(counter);
    counter_ = counter;
  }

  if (error != error_) {
    Serial.print(error);
    Serial.println(" errors");
    error_ = error;
  }
}

Теперь мои вопросы:

  1. Есть ли способ повысить скорость/безопасность производительного/безопасного кода? Приветствуется любой способ улучшить этот код.
  2. Возможно ли сочетание максимальной скорости и безопасности или компромисс между ними?
  3. Какую максимальную частоту мы можем получить при такой конфигурации?

Заранее спасибо за помощь.

, 👍0

Обсуждение

Обратите внимание, что вместо вентиля XOR вы можете использовать одно _прерывание смены контакта_ на обоих каналах., @Edgar Bonet

@EdgarBonet Вы имеете в виду подключение двух каналов к двум прерываниям? что мы не можем сделать. Arduino Uno/Nano имеет только два прерывания, и нам нужно одно для других целей. Это то, что вы имели в виду?, @Foad

Нет, я имею в виду подключение двух каналов к одному прерыванию смены вывода_. Arduino имеет три прерывания по изменению контакта в дополнение к двум внешним прерываниям., @Edgar Bonet

Я не уверен, что такое «прерывание смены контакта», но если вы подключаете два канала к одному прерываемому контакту (т. е. 3 или 4), то это работает как «И»., @Foad

Нет, это не так. Выполните поиск в Интернете по запросу «_Прерывание смены контакта Arduino_» или «_Прерывание смены контакта AVR_»., @Edgar Bonet

@ ЭдгарБонет Хорошо. Читая [эту страницу](https://github.com/NicoHood/PinChangeInterrupt), я не думаю, что использование прерываний смены контакта/порта является лучшим решением. Если только я не понял вашу точку зрения, и вы можете уточнить., @Foad

MCU имеет прерывания по смене контактов, поэтому вам не нужно добавлять логический элемент XOR и использовать один из внешних контактов прерывания. Вам не нужна библиотека для их использования, и вам, вероятно, в любом случае не нужны накладные расходы на библиотеку. Прочитайте [Прерывания смены контакта Arduino](https://thewanderingengineer.com/2014/08/11/arduino-pin-change-interrupts/)., @Edgar Bonet

@EdgarBonet, пожалуйста, учтите, что остальные контакты не бесплатны. есть чтение и запись других вещей. Итак, если я использую 3 доступных блока PinChangeInterrupts (0-7, 8-13, A0-A5). Я получаю много ненужных прерываний. XOR помогает считывать оба канала в «реальном времени» с помощью всего одного вывода прерывания и одного обычного. См. [это изображение](https://trello-attachments.s3.amazonaws.com/5d15eec00aa2638683ed41ed/600x173/f34f85ad8bd4d7c17a69661855122f79/eiczu.png)., @Foad

Нет, вы не получите ненужных прерываний, если не активируете PCINT на других выводах., @Edgar Bonet

@EdgarBonet, каков правильный синтаксис для чтения контактов 4 и 5 (которые не прерываются) с использованием прерывания смены контакта / порта? какие двоичные/шестнадцатеричные значения должны быть присвоены PCICR и PCMSK0/PCMSK1/PCMSK2?, @Foad


1 ответ


1

Это всего лишь частичный, поспешный ответ.

Вы сможете получить значительное ускорение, если определите свой собственный ISR для обработки прерывания, например:

ISR(INT1_vect) { ... }

Вместо этого Arduino использует прерывания

void trigger() { ...}

void setup() {
    attachInterrupt(int_number, trigger, CHANGE);
    ...
}

но это медленно, так как использует ISR, обеспечиваемый ядром Arduino, который должен найти и вызвать ваш обработчик прерывания. Как ты написал обработчик неверен: либо вы определяете обычную функцию без ISR и пойти по пути Arduino, или вы идете по простому пути AVR и самостоятельно определите ISR, который должен называться INTERRUPT_NAME_vect, в в этом случае вы не используете attachInterrupt(). То, как вы это сделали, вероятно, самый медленный, который вы можете получить, так как компилятор без необходимости добавит Пролог типа ISR и эпилог вашего обработчика.

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


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

Во-первых, нужно посмотреть на распиновку Arduino Uno и увидеть, что:

  • цифровой 4 = PD4 = PCINT20
  • цифровой 5 = PD5 = PCINT21

Оба вывода связаны с запросом прерывания 2 на смену вывода (PCINT2_vect). Это прерывание можно настроить в setup() следующим образом:

PCMSK2 = _BV(PCINT20)   // разрешить прерывание на выводе PCINT20 = PD4
       | _BV(PCINT21);  // разрешить прерывание на выводе PCINT21 = PD5
PCIFR  = _BV(PCIF2);    // очистить флаг прерывания
PCICR  = _BV(PCIE2);    // разрешить запрос прерывания смены контакта 2

Теперь прерывание будет срабатывать при каждом изменении логических уровней входы 4 и 5.

Затем в ISR весь порт D может быть прочитан за один цикл с помощью просто оценивая соответствующий входной регистр вывода, т.е. PIND. С некоторое смещение и маскирование, можно получить фазу между 0 и 3 (счетчик в коде Грея: 0, 1, 3, 2, 0...). Из предыдущего и текущей фазе, можно вычислить изменение, которое необходимо применить к счетчик:

// Обработка запроса прерывания смены контакта 2.
ISR(PCINT2_vect) {
    static uint8_t previous_phase;
    uint8_t phase = (PIND >> 4) & 0x03;  // чтение входных контактов
    counter += encoder_delta(previous_phase, phase);
    previous_phase = phase;
}

Реализация encoder_delta() оставлена в качестве упражнения для читатель. ;-)

,

Спасибо. Я просмотрю новые правки и буду беспокоить вас, если у меня возникнут дополнительные вопросы., @Foad