Проблема с использованием Arduino Mega Timer2 с прерыванием PinChange

Я работал над программой для измерения ширины импульса , для этого я думал использовать таймер 2 в Arduino mega вместе с прерыванием смены контактов,

Программа, которую я написал, выглядит следующим образом

volatile float ovfCount = 0;


typedef struct{
  float curr_ovfCount,prev_ovfCount;
  uint8_t curr_tcnt2, prev_tcnt2;
  uint16_t width;
  bool stateHigh = true;
}Pulse;

Pulse ch1, ch2, ch3, ch4, ch5, ch6;


void setup() {
  Serial.begin(115200);

  /*
   * Pin Change Interupt for 
   * measuring the pulse width 
   * from the receiver
   * 
   */

  DDRK = 0; // A8:A15 -> direction as Input
  PORTK = B00111111; // A8:A15 -> pullupResistor

  PCICR = B00000100; // activating 2 nd PCINT
  PCIFR = 0; // resetting the flags
  PCMSK2 = B00111111; // activating from A8:21 :: 6

  /*
   * 8 bit timer setup for counting
   * timer 2 is used
   */
   TCCR2B=0x00;
   TCCR2A = 0x00; // wave generation is normal:: all zeros
   TCNT2 = 0x00;
   TIFR2 = 0x00; // resetting all flags
   TIMSK2 = 0x01; //timer overflow enable
   TCCR2B = 0x01; // no prescaling 
  
}

ISR(TIMER2_OVF_vect){
  ovfCount++;
}

ISR(PCINT2_vect){
  float ovf_count = ovfCount;
  uint8_t tcnt2 = TCNT2;

  if( (PINK & 1 << PINK0) & ch1.stateHigh ){
    //Low-High
    ch1.prev_ovfCount = ovf_count;
    ch1.prev_tcnt2 = tcnt2;
    ch1.stateHigh = false;
  } else if( ! ((PINK & 1 << PINK0) & ch1.stateHigh)){
//    High-Low
    ch1.curr_ovfCount = ovf_count;
    ch1.curr_tcnt2 = tcnt2;
    ch1.width = (256.0*(ch1.curr_ovfCount - ch1.prev_ovfCount) + (ch1.curr_tcnt2 - ch1.prev_tcnt2) ) * 1000 / 16e6;
    ch1.stateHigh = true;
  }

}


void loop() {
  Serial.println(ch1.width);
  pinMode(13, OUTPUT);
  digitalWrite(13,!digitalRead(13));
  delay(1000);

} 

и результат ,который я получаю,

16:22:05.667 -> 0
16:22:05.847 -> 927
16:22:06.826 -> 927
16:22:07.820 -> 1001
16:22:08.847 -> 1001
16:22:09.837 -> 1001
16:22:10.843 -> 1001
16:22:11.835 -> 1002
16:22:12.863 -> 1002
16:22:13.871 -> 1002
16:22:14.854 -> 1002
16:22:15.873 -> 1003
16:22:16.875 -> 1003
16:22:17.887 -> 1003
16:22:18.860 -> 1003
16:22:19.875 -> 1003
16:22:20.867 -> 1003
16:22:21.907 -> 1003
16:22:22.895 -> 1003
16:22:23.922 -> 1008
16:22:24.921 -> 1008
16:22:25.938 -> 1008
16:22:26.922 -> 1008
16:22:27.939 -> 1008
16:22:28.961 -> 1008
16:22:29.946 -> 1008
16:22:30.971 -> 1008
16:22:31.993 -> 1008
16:22:32.974 -> 1008
16:22:34.011 -> 1008
16:22:35.016 -> 1008
16:22:36.026 -> 1008
16:22:37.012 -> 1008
16:22:38.024 -> 1008
16:22:38.939 -> 1008
16:22:39.802 -> 906
16:22:40.638 -> 906
16:22:41.508 -> 844
16:22:42.345 -> 844
16:22:43.189 -> 842
16:22:44.059 -> 842
16:22:44.905 -> 843
16:22:45.750 -> 843
16:22:46.593 -> 842
16:22:47.428 -> 842
16:22:48.305 -> 842
16:22:49.139 -> 842
16:22:50.011 -> 845
16:22:50.839 -> 845
16:22:51.707 -> 842
16:22:52.539 -> 842
16:22:53.419 -> 842
16:22:54.260 -> 842
16:22:55.111 -> 842
16:22:55.975 -> 842
16:22:56.791 -> 842
16:22:57.642 -> 842
16:22:58.492 -> 842
16:22:59.372 -> 842
16:23:00.212 -> 842
16:23:01.046 -> 842
16:23:01.918 -> 842
16:23:02.775 -> 842
16:23:03.600 -> 849
16:23:04.473 -> 849
16:23:05.329 -> 842
16:23:06.159 -> 842
16:23:07.022 -> 842
16:23:07.862 -> 842
16:23:08.709 -> 842
16:23:09.574 -> 842
16:23:10.412 -> 843
16:23:11.292 -> 843
16:23:12.116 -> 848
16:23:12.996 -> 848
16:23:13.899 -> 848
16:23:14.779 -> 848

Выход который я ожидаю равен 1000 так как соединение с PCINT находится непосредственно на цифровом выводе 13 Arduino и он должен мигать в этот промежуток времени

Я хочу знать

  • Какую ошибку я совершаю здесь измеряя количество
  • Как я могу обобщить этот метод для измерения ширины 6 каналов одновременно?

= = = = = Редактировать после ввода Эдгара Бонета ===========

Я написал программу , не могли бы вы сказать мне, пожалуйста, является ли это хорошим способом выполнить мое требование

volatile uint32_t ovfCount = 0;

struct Pulse {
  uint32_t last_toggle;
  uint32_t width;
  bool stateHigh;
  bool input_is_high;
  uint32_t get_width() {
    noInterrupts();
    uint32_t width_copy = width;
    interrupts();
    return width_copy;
  }
};

Pulse ch1, ch2, ch3, ch4, ch5, ch6;

void setup() {
  Serial.begin(115200);

  /*
     Pin Change Interupt for
     measuring the pulse width
     from the receiver

  */

  DDRK = 0; // A8:A15 -> direction as Input
  PORTK = B00111111; // A8:A15 -> pullupResistor

  PCIFR = 0; // resetting the flags
  PCMSK2 = B00111111; // activating from A8:21 :: 6
  PCICR = B00000100; // activating 2 nd PCINT

  /*
     8 bit timer setup for counting
     timer 2 is used
  */
  TCCR2B = 0x00;
  TCCR2A = 0x00; // wave generation is normal:: all zeros
  TCNT2 = 0x00;
  TIFR2 = 0x00; // resetting all flags
  TIMSK2 = 0x01; //timer overflow enable
  TCCR2B = 0x01; // no prescaling

  DDRH = B00111000;
  fastPWM_init();

}

void fastPWM_init() {
  //    clearing
  TCCR4A = 0; TCCR4B = 0; TCCR4C = 0;
  //    Initializing
  TCCR4A = B10101010; // OC4A, OC4B, OC4C : ICR-WGM
  TCCR4B = B00011010; // ICR-WGM prescalar : 8
  //    Setting frequency
  ICR4 = 39999;
  //    Setting PWM
  OCR4A = 2000; OCR4B = 2000; OCR4C = 2000;
}

ISR(TIMER2_OVF_vect) {
  ovfCount++;
}


ISR(PCINT2_vect) {
  uint8_t tcnt2 = TCNT2;
  uint32_t ovf_count = ovfCount;

  if ( bit_is_set(TIFR2, TOV2) && tcnt2 < 128 ) {
    ovf_count ++;
  }

  uint32_t time = ovf_count << 8 | tcnt2;

  pinChangeFunction(&ch1, time, PK0);
  pinChangeFunction(&ch2, time, PK1);
  pinChangeFunction(&ch3, time, PK2);
  pinChangeFunction(&ch4, time, PK3);
  pinChangeFunction(&ch5, time, PK4);
  pinChangeFunction(&ch6, time, PK5);

}


void pinChangeFunction(Pulse *channel, uint32_t time, uint8_t pin){
  channel->input_is_high = bit_is_set(PINK, pin);
  if (channel->input_is_high && !channel->stateHigh) {
    channel->stateHigh = true;
  } else if (! channel->input_is_high && channel->stateHigh) {
    channel->width = time - channel->last_toggle;
    channel->stateHigh = false;
  }
  channel->last_toggle = time;
}

void loop() {

  if(Serial.available() > 0){
    OCR4A = Serial.parseInt();
  }
  
  Serial.print(ch1.get_width() / 16e3);Serial.print("\t");
  Serial.println(ch6.get_width() / 16e3);
  

}

ШИМ используется только для тестирования.

, 👍1

Обсуждение

Эта программа выполняет вычисления с плавающей запятой (которые очень медленны на AVR) в пределах ISR, который запускается каждые 16 мкс. Я удивлен, что это вообще работает., @Edgar Bonet

спасибо за предложение , так какой тип данных я должен использовать для подсчета переполнения .?, @Lawliet

Прерывания изменения контактов медленнее, чем обычные прерывания. Кроме того, для разрешения, с которым вы работаете, просто используйте "millis ()" или " micros ()", чтобы получить текущую метку времени, а не возиться с тайм-таймерами., @Majenko


1 ответ


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

1

Есть довольно много проблем с этой программой. Наиболее очевидным является использование вычислений с плавающей точкой в контексте прерывания. Прерывания должны подаваться как можно быстрее, а плавающая точка действительно медленная на AVR.

Некоторые другие вопросы:

  • Флаги прерываний должны быть очищены, написав логику 1 для них, как бы глупо это ни звучало.

  • Пара операторов if & в ISR(PCINT2_vect), вероятно, должна быть &&.

  • Логика обнаружения восходящих и падающих краев ошибочна.

  • Нет необходимости хранить так много данных в объектах Pulse.

  • ch1.width следует читать с отключенными прерываниями, чтобы избежать гонки данных.

  • Существует тонкое состояние гонки при чтении ovfCount и TCNT2: если таймер переполняется прямо при запуске ISR(PCINT2_vect), это переполнение не будет учитываться.

Вот версия программы, которая пытается исправить все эти моменты. Я также изменил его для работы на Uno (с pin 13 = PB5 = PCINT5), чтобы я мог проверить его:

/*
 * Ширина импульса через PCINT.
 *
 * https://arduino.stackexchange.com/questions/81688
 */

volatile uint32_t ovfCount = 0;

struct Pulse {
    uint32_t last_toggle;
    uint32_t width;
    bool stateHigh;
    uint32_t get_width() {
        noInterrupts();
        uint32_t width_copy = width;
        interrupts();
        return width_copy;
    }
};

Pulse ch1, ch2, ch3, ch4, ch5, ch6;

void setup() {
    Serial.begin(115200);
    pinMode(13, OUTPUT);

    /* Настройка прерывания смены контактов для обнаружения импульсов. */
    PCMSK0 = _BV(PCINT5);  // изменение смысла на выводе 13 = PB5 = PCINT5
    PCIFR  = _BV(PCIF0);   // сброс флага прерывания
    PCICR  = _BV(PCIE0);   // enable PCINT0_vect: pins PCINT[7:0]

    /* Настройка таймера 2 для подсчета. */
    TCCR2B = 0;           // стоп
    TCCR2A = 0;           // нормальный режим
    TCNT2  = 0;           // сброс таймера
    TIFR2  = _BV(TOV2);   // сброс флага прерывания
    TIMSK2 = _BV(TOIE2);  // включить прерывание переполнения таймера
    TCCR2B = _BV(CS20);   // count at F_CPU/1
}

ISR(TIMER2_OVF_vect) {
    ovfCount++;
}

ISR(PCINT0_vect) {
    uint8_t tcnt2 = TCNT2;
    uint32_t ovf_count = ovfCount;

    // Было ли переполнение, которое еще не было учтено?
    if (bit_is_set(TIFR2, TOV2) && tcnt2 < 128) {
        ovf_count++;
    }

    uint32_t time = ovf_count << 8 | tcnt2;

    bool input_is_high = bit_is_set(PINB, PB5);
    if (input_is_high && !ch1.stateHigh) {  // low -> high
        ch1.stateHigh = true;
    } else if (!input_is_high && ch1.stateHigh) {  // high -> low
        ch1.width = time - ch1.last_toggle;
        ch1.stateHigh = false;
    }
    ch1.last_toggle = time;
}

void loop() {
    digitalWrite(13, HIGH);
    delay(1000);
    digitalWrite(13, LOW);
    delay(200);
    Serial.println(ch1.get_width() / 16e3, 5);
}

И вот результат:

1000.02349
1000.01959
1000.01916
1000.02496
1000.01959
1000.02032
1000.01947
1000.02795
1000.02642
1000.01855
1000.01916
1000.02105
1000.02490
1000.01782
1000.03033
1000.01898
...

Печатные значения больше 1000 из-за времени, необходимого для выполнения digitalWrite().

Также обратите внимание, что 8-битный таймер, работающий на полной скорости процессора, переполняется каждые 16 мкс. Это довольно большая нагрузка на процессор, и многие операции займут больше времени, чем ожидалось, из-за этих прерываний. Если бы я хотел засечь что-то с разрешением в один цикл, я бы потянулся к 16-битному таймеру. И я бы предпочел использовать захват входного сигнала, хотя, к сожалению, на Мега доступны только два канала захвата входного сигнала (на контактах 48 и 49).


Правка: Отвечаю на вопросы в комментариях.

пожалуйста, взгляните на мои правки вопросов и, пожалуйста, дайте совет.

Я не тестировал его, но мне кажется, что этот код должен работать. Однако я бы предпочел иметь разные прерывания для каждого канала. В противном случае последовательные вызовы pinChangeFunction() могут сделать выполнение этого ISR довольно долгим.

Также обратите внимание, что Pulse::input_is_high не служит никакой полезной цели. Хранение этого значения в объекте замедляет ISR, так как он должен обращаться к памяти, в то время как локальная переменная будет храниться в (гораздо быстрее) внутреннем регистре процессора.

что мы будем делать, если у нас есть 8 каналов, как в радиоприемнике?

Вы можете использовать 6 внешних прерываний и 2 прерывания смены контактов.

Почему вы взяли tcnt2 < 128, когда tcnt2 max равен 256?

Это хитрое дело. Я постараюсь объяснить всю логику теста.

Рассмотрим следующую наивную версию ISR:

ISR(PCINT0_vect) {
    // ← A
    uint8_t tcnt2 = TCNT2;
    uint32_t ovf_count = ovfCount;
    uint32_t time = ovf_count << 8 | tcnt2;
    // ...
}

Есть небольшой шанс, что Таймер 2 переполняется в точке А, т. е. в то время как этот ISR выполняет свой пролог и еще не прочитал TCNT2. Если это произойдет, то TIMER2_OVF ISR не будет запущен немедленно, потому что мы уже выполняем ISR, а ISR не гнездятся. Затем ovfCount не обновляется. Если это происходит, мы затем считываем значение TCNT2 , на которое влияет переполнение, и значение ovfCount, которое не учитывает это переполнение. Затем мы объединяем эти несогласованные значения в значение времени, которое пропускает 256 тиков таймера.

Мы можем обнаружить эту ситуацию, прочитав флаг TOV2 в TIFR2. Этот флаг устанавливается только при наличии ожидающего запроса прерывания для TIMER2_OVF, то есть если счетчик переполнен и это переполнение еще не было учтено в ovfCount. Таким образом, “фиксированный” ISR выглядит следующим образом:

ISR(PCINT0_vect) {
    // ← A
    uint8_t tcnt2 = TCNT2;
    // ← B
    uint32_t ovf_count = ovfCount;
    // ← B
    if (bit_is_set(TIFR2, TOV2)) {
        ovf_count++;
    }
    uint32_t time = ovf_count << 8 | tcnt2;
    // ...
}

При этом, если переполнение происходит в точке А, нам удается вычислить нужное время. Но теперь подумайте, что произойдет, если таймер переполнится в любой из точек, отмеченных B, то есть после чтения TCNT2, но до чтения TOV2. Если это происходит, мы считываем значения предварительного прерывания как TCNT2, так и ovfCount. Таким образом, для вычисления последовательного времени не требуется никакой коррекции . Тем не менее, TOV2 повышается, когда мы тестируем его: затем мы применяем ненужную коррекцию, и в итоге получаем время, которое слишком велико на 256 тиков.

Чтобы устранить эту проблему, мы должны убедиться, что коррекция применяется, если переполнение происходит в точке А, но не в точке В. Как мы можем различить эти две ситуации? Ответ таков: взглянув на значение, записанное в tcnt2. В случае А счетчик считывался сразу после переполнения, поэтому при копировании в tcnt2 он имел очень малое значение (близкое к 0). В случае B счетчик считывался, когда он был близок к переполнению, поэтому он имел очень большое значение (близкое к 255). Если TOV2 установлен, когда мы его читаем, это означает, что переполнение произошло в самом начале текущего ISR, очень близко к точке, где мы читаем TCNT2. Таким образом, значение, записанное в tcnt2 должен быть либо очень маленьким, либо очень большим. Он не может быть посередине (близко к 128).

Поэтому мы должны решить, является ли tcnt2 очень маленьким или очень большим. Существует множество возможных значений, которые можно использовать в качестве порога. Наиболее естественным является средний диапазон: 128. Он также является наиболее эффективным для вычисления, так как компилятор может оптимизировать сравнение в простую проверку наиболее значимого бита. Таким образом, окончательный вариант теста:

    if (bit_is_set(TIFR2, TOV2) && tcnt2 < 128) {
        ovf_count++;
    }
,

Как изменить то же самое для использования с 6 каналами? извините, но я начинаю путаться .., @Lawliet

@Lawliet: У вас есть только 3 прерывания смены контактов. Чтение 6 каналов сделало бы ISR более сложным, так как им нужно было бы выяснить, какие каналы вызвали прерывание. Я бы предпочел использовать регулярные внешние прерывания, так как есть 6 из них, доступных на Mega (INT0–INT5). Вы можете использовать шаблон, чтобы аннулировать повторяющийся код., @Edgar Bonet

пожалуйста, взгляните на мои правки вопросов и, пожалуйста, дайте совет., @Lawliet

Кроме того, что мы будем делать, если у нас будет 8 каналов, как в радиоприемнике ?, @Lawliet

Сомнение 1: Почему вы взяли `tcnt2 < 128 when tcnt2 max is 256` . ?, @Lawliet

@Lawliet: См. Измененный ответ., @Edgar Bonet

с вашими советами я попытался написать программу с 16-битным таймером и получаю какую-то ошибку в последовательности измерения такта часов . пожалуйста, взгляните . ссылка, @Lawliet

Причина , по которой я использую PCINT , заключается в том, что я должен использовать один IMU - который является I2C :: для этого я должен получить доступ к выводам scl и sda, затем у меня есть 4 в одной настройке и 8 сервоприводов в другой настройке, которые также должны быть использованы, и, наконец, я должен измерять обороты 4/8 двигателей также . Так что для этого вы предложите всю комбинацию выводов. (1.) Приемник :: 6/8 каналов - ширина, которая будет рассчитана (2.) I2C :: (3.) 4/8 :: servos (4.) 4/8 :: датчики измерения скорости, @Lawliet

Пожалуйста, помогите мне с этим вопросом @edgar-bonet . QuestionLink, @Lawliet