Проблема с использованием 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);
}
ШИМ используется только для тестирования.
@Lawliet, 👍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
- 4-битный счетчик вверх и вниз
- Проблема прерывания библиотеки MPU6050 Arduino Jeff Rowberg
- Точность синхронизации Arduino nano
- Считать данные датчика повторно через указанное время?
- DS3231 с Arduino Nano для точной синхронизации
- Обнаружение входящего импульса 7,875 кГц для использования в качестве триггера
- Arduino mega PinChangeInterrupt с 16 битным таймером
- Не удается изменить указатель на порт в главном цикле
Эта программа выполняет вычисления с плавающей запятой (которые очень медленны на AVR) в пределах ISR, который запускается каждые 16 мкс. Я удивлен, что это вообще работает., @Edgar Bonet
спасибо за предложение , так какой тип данных я должен использовать для подсчета переполнения .?, @Lawliet
Прерывания изменения контактов медленнее, чем обычные прерывания. Кроме того, для разрешения, с которым вы работаете, просто используйте "millis ()" или " micros ()", чтобы получить текущую метку времени, а не возиться с тайм-таймерами., @Majenko