Как правильно согласовать код с основным циклом Loop(), и аппартными прерываниями, таймерами?

Здравия всем!

Люди с опытом, подскажите как правильно организовать код, что бы программа не “тупила”.
Суть в следующем :
На борту шаговый мотор, подключенный к драйверу, получающий управляющие импульсы от Timer4 с изменяемой частотой от потенциометра.
Оптический энкодер , с 50 прерываний на один оборот вала шагового двигателя (зубчатое колесо).
В панели управления несколько кнопок, LCD I2C 20x4 для задания параметров работы устройства, выбор пунктов меню осуществляется механическим энкодером ( прикрутил библиотеку Гайвера). Так же в дополнении есть 7Segment на TM1637, отображающий в реальном времени количество импульсов оптического энкодера. Период импульсов оптического энкодера, при максимальной скорости шагового двигателя , порядка 4ms.
Скажем так, аппаратная часть, т.е. прерывания, Таймер, работают четко. Но траблы начинаются с отображением информации на 7Segment ( 6 знаков), появляются пропуски с увеличением оборотов шагового мотора, а так же жестко начинает тупить механический энкодер, для меню LCD. И как “вишенка на торте”, при максимальных оборотах шаговика, когда в функции void SegmentDisp() подключен вывод на 7Segment - display.showNumberDec(OptoStepCounter); начинает срываться шаговый мотор. Как только отключаю вывод на 7Segment , шаговик работает без сбоев.
Я так понимаю, что когда обработчик прерывания оптического энкодера, начинает слишком часто вызывать функцию библиотеки 7Segment, то библиотека не успевает обрабатывать переменную из обработчика . А что касаемо механического энкодера, то его библиотека вызывается в основном цикле loop(), код которого выполняется в промежутках между прерываниями и прочими аппаратными ресурсами, которые выполняются намного чаще, не оставляя “воздуха” для кода в цикле.
Как решить такую проблему согласования?
Отказаться от библиотек 7Segmen ( TM1637TinyDisplay6.h) и EncButton.h от Гайвера, и прописать код для дисплея и энкодера самому? Или есть какой то другой метод реализации многозадачности, без “тормозов” ? ( RTOS не предлагать).

Приведу основные участки кода, где возникают траблы :

#include <Arduino.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <TM1637TinyDisplay6.h>
#include <EncButton.h>


const byte Encoder16Bit_OPTO_A = 3;          // Encoder wheel (for stacker sync microsteps) ++
const byte Encoder16Bit_OPTO_B = 4;          // Encoder wheel (for stacker sync microsteps) --

const uint16_t PUL1             = 7;           // Пин для задания скорости вращения Main Stepper
const byte DIR1                = 6;           // Пин для задания направления вращения Main Stepper
const byte ENA1                = 5;           // Пин для включения/отключения драйверва Main Stepper

const uint8_t potPin           = A0;          // Аналоговый пин для потенциометра скорости вращения Main Stepper


#define CLK 14
#define DIO 15

// Настройки для библиотеки Гайвера
#define EB_NO_FOR          // отключить поддержку pressFor/holdFor/stepFor и счётчик степов (экономит 2 байта оперативки)
#define EB_FAST_TIME 30    // таймаут быстрого поворота
#define EB_DEB_TIME 50     // дебаунс кнопки
#define EB_CLICK_TIME 500  // таймаут ожидания кликов (кнопка)
//End

#define A 52   // Пин для Энкодера A
#define B 53   // Пин для энкодера B
#define SW 51  // Пин для кнопки энкодера

#define LCD_ADDRESS 0x27
#define MENU_ROWS 4  // Количество строк для LCD
#define MENU_COLS 2  // Количество столбцов для LCD

//Disp1637_6 disp(DIO, CLK);
TM1637TinyDisplay6 display(CLK, DIO);       // 6-Digit Display Class

//TM1637 TM;
LiquidCrystal_I2C lcd(LCD_ADDRESS, 20, 4);  // Создание класса для LCD ( 20 x 4)

EncButton eb(A, B, SW, INPUT, INPUT);       // Создание экземпляра класса EncButton с указанными пинами энкодера и кнопки


// Глобальные перменнные для пунктов меню 
//.....
//End

// Счетчики оборотов - флаг
volatile bool dataUpdated = false;


// Определяем переменные для хранения состояний пинов
uint32_t OptoStepCounter = 0;


// Переменные для потенциометра
volatile uint8_t potValue = 0;  // Переменная для хранения текущего значения потенциометра

// Настройка Timer2 на прерывание при переполнении для оптического энкодера

void setupTimer2() {
// Настраиваем Timer2 на прерывание при переполнении
  TCCR2A = 0;             // Сбрасываем регистр управления таймером
  TCCR2B = 0;             // Сбрасываем регистр управления таймером
  TCNT2 = 0;              // Сбрасываем счетчик таймера
  OCR2A = 250;            // Устанавливаем значение сравнения, чтобы вызывать прерывание каждые 1 мс
  TIMSK2 |= (1 << TOIE2); // Включаем прерывание по переполнению таймера
  TCCR2B |= (1 << CS22);  // Устанавливаем делитель частоты 64, чтобы таймер 2 работал на частоте 1 МГц
}


// Функция настроек таймера шагового двигателя

void setupTimer4() {
  cli();  // Отключение прерываний

  TCCR4A = 0;
  TCCR4B = 0;
  TCNT4 = 0;
  OCR4A = 0;
  OCR4B = 0;
  TCCR4A = (1 << COM4B1) | (1 << COM4B0) | (1 << WGM41) | (1 << WGM40);
  TCCR4B = (1 << WGM43) | (1 << WGM42) | (1 << CS40);

  sei();  // Включение прерываний
}

// Функция для настройки ADC

void setupADC() {

  ADCSRA = 0;                                            // Сбрасываем регистр ADCSRA
  ADCSRB = 0;                                            // Сбрасываем регистр ADCSRB
  ADMUX = (1 << REFS0) | (0 & 0x07) | (1 << ADLAR);      // Настройка регистра ADMUX для выбора опорного напряжения и мультиплексирования A0, разрешение 8 бит
  ADCSRA |= (1 << ADEN);                                 // Включаем АЦП
  ADCSRA |= (1 << ADSC);                                 // Запускаем преобразование
  ADCSRA |= (1 << ADATE);                                // Непрерывный режим работы АЦП
  ADCSRA |= (1 << ADPS2) | (1 << ADPS1) | (1 << ADPS0);  // Предделитель ADC на 128
  ADCSRA |= (1 << ADIE);                                 // Разрешение прерывания ADC
}


/***************************************************************************************************************/


void setup() {
  Serial.begin(115200);
  Wire.begin();            // Инициализируем шину I2C
  Wire.setClock(400000L);  // Установка скорости передачи данных на шине I2C в 400 кГц
  setupTimer2();
  setupTimer4();  
  setupADC();
  cli();                   // Запрещаем все прерывания.
  // Порт D,E пины 18 и 3
  DDRD &= ~(1 << DDD3);    // PD3 как вход
  DDRE &= ~(1 << DDE5);    // PE5 как вход

  PORTD |= (1 << PORTD3);  // PD2 с уровнем 1
  PORTE |= (1 << PORTE5);  // PE5 с уровнем 1

// Порт H,G пины 17 и 4
  DDRH &= ~(1 << DDH0);    // PH0 как вход
  DDRG &= ~(1 << DDG5);    // PG5 как вход 

  PORTH |= (1 << PORTH0);  // PH0 с уровнем 1
  PORTG |= (1 << PORTG5);  // PG5 с уровнем 1


  // Настройка пинов PH4, PH3, PE3, PL4, PL2, PL3 как выходы, с начальным уровнем 0 и без подтяжки к питанию

  // Порт H (PH4, PH3)
  DDRH |= (1 << DDH4) | (1 << DDH3);         // Настройка пинов PH4 и PH3 как выходы
  PORTH &= ~((1 << PORTH4) | (1 << PORTH3)); // Установка начального уровня 0 на пинах PH4 и PH3

  // Порт E (PE3)
  DDRE |= (1 << DDE3);                       // Настройка пина PE3 как выход
  PORTE &= ~(1 << PORTE3);                   // Установка начального уровня 0 на пине PE3

  // Порт L (PL4, PL2, PL3)
  DDRL |= (1 << DDL4) | (1 << DDL2) | (1 << DDL3);           // Настройка пинов PL4, PL2 и PL3 как выходы
  PORTL &= ~((1 << PORTL4) | (1 << PORTL2) | (1 << PORTL3)); // Установка начального уровня 0 на пинах PL4, PL2 и PL3

 

  // Настройка прерываний для пина 19 (INT2)
  EICRA &= ~(1 << ISC30); // Прерывание по изменению состояния
  EICRA |= (1 << ISC31);

  // Настройка прерываний для пина 3 (INT5) 
  EICRB &= ~(1 << ISC50);
  EICRB |= (1 << ISC51);

  // Включаем прерывания
  EIMSK |= (1 << INT3) | (1 << INT5); // Включаем прерывания INT4 

  sei();  // Разрешаем все прерывания.


  eb.attach(callback_ENC);

  lcd.clear();  //Очистка дисплея ( обновление)
  printMenu();  // Вызов функции основного меню

  // Initialize 7-segment display
  display.begin();
  display.clear();
  display.setBrightness(7);
  display.showNumberDec(OptoStepCounter);

}

/*******Блок Функций для вывода МЕНЮ на LCD*******/
void printMenu(){}
void myClick(){}
void myTurn(){}
//.....
/*******Конец Блока вывода МЕНЮ на LCD*******/

//Обработчик вызовов состояния энкодера
void callback_ENC() {
  switch (eb.action()) {  // Вызываем метод действия для пинов Энкодера
    case EB_CLICK:        // Если действие является кликом (нажатием кнопки)
      myClick();          // Выполняем функцию обработки клика
      break;
    case EB_TURN:  // Если действие является поворотом (вращением кнопки)
      myTurn();    // Выполняем функцию обработки поворота
      break;
  }
}

// Вывод инфы на 7Segment
void SegmentDisp() {
  if (dataUpdated) {
    display.showNumberDec(OptoStepCounter);
    dataUpdated = false;
  }
}

//Обработчик преывания от Timer2 (Частота вызовов 1МГц, согласно настройкам таймера)
ISR(TIMER2_OVF_vect) {
  SegmentDisp();           // Вызываем SegmentDsp() по прерыванию от Timer2
}

void loop() {
    eb.tick();            // Выполняем опрос событий Энкодера
  // Остальной код...
}

// Обработчик прерывания ADC
ISR(ADC_vect) {
  potValue = ADCH;                            // Сохранение текущего значения потенциометра (для 8 бит разрешение)
  OCR4A = map(potValue, 0, 255, 24000, 800);  // Преобразование среднего значения и установка регистра сравнения
  OCR4B = OCR4A - 48;                         // (48 - 3мкс)
}

ISR(INT5_vect) {
  // Сбросить состояние прерывания INT2 в начале
  EIFR |= (1 << INTF5); 

  bool OptoStep_B = (PING & (1 << PING5));

  if (OptoStep_B) {
    OptoStepCounter -= 1;
  } else {
    OptoStepCounter += 1;
  }
  dataUpdated = true;
}

проблема в строке 207

2 лайка

Особой проблемы не вижу, пока. В настоящий момент там весь код закомментирован. И в целом представляет собой опрос состояния 5 кнопок, с любой, удобной для loop() скоростью.
Гайверовская библа для энкодера, подразумевает вызов eb.tick(); как можно чаще. Но если вызов функции для 7Segmeynt (display.showNumberDec(OptoStepCounter); происходит со скоростью срабатывания прерывания оптического энкодера, то loop(), уже не до eb.tick(); и всему остальному , внутри тела этого цикла.
Я подозреваю, что библиотека TM1637TinyDisplay6.h слишком тяжелая, что бы так часто вызываться из прерывания.
Но так не хочется писать свой код для 7Segment, поскольку сомневаюсь, что он получится легче библиотеки, написанной профессионалами.
Поэтому и интересуюсь методами , как согласовывают или синхронизируют работу loop(), с аппаратной периферией.
Внятных решений не нашел, кроме как делать обработчики прерываний как можно короче, выставления задержек, типа mills(), micros(), или применение вспомогательной RTOS, что не актуально для Arduino Mega 1280.

В дополнении, если убрать опрос оптического энкодера Timer2 , и поместить вызов функции display.showNumberDec(OptoStepCounter); в loop(), то ситуация с пропусками еще хуже. Стоит крутануть механический энкодер, как на 7Segment “проваливаются” целые блоки значений.

А какое поведение ожидается при кручении механического энкодера?

В целом, ожидается что бы шаговик крутил без сбоев на любой скорости, оптический энкодер не делал пропусков и корректно отображался на 7Segment, при этом обычный механический энкодер, мог без “фризов” передвигаться по пунктам меню на I2C LCD.
Очень хочется, что бы каждая функция, будь то энкодеры, нажатие кнопок, отображение и изменение информации на дисплеях работали независимо и не оказывали влияния друг на друга.
Может я слишком многого хочу от Arduino Mega 1280?
Но прежде, чем в 3-й раз менять железо, все же хочу разобраться , где мои кривые руки. Ну должен же 8-ми разрядный контроллер справляться с такой простой задачей.
Не лезть же на STM32F4…

МК-то справится, сдюжит ли программист…

Мне, вот, например, подзабылось - что там в ADCH, какие значения попадают в potValue?

Точность ADC не важна, поэтому задействован только 8-ми битный режим. Значения попадают от 0 до 255.
Далее это передается на регистр OCR4A (Timer4), который изменяет частоту следования импульсов для драйвера шагового мотора. Длительность импульса составляет 3 мкс ( OCR4B = OCR4A - 48; )

Стараюсь. Потому и открыл тему.

Што-то ине представляется, что с ADCH только два верхних бита читаются.

Впрочем, если сбивается хардварная генерация на таймере, то искать стоит причину постоянного переконфигурирования таймера. Так мне кажется.

9КГц на моторе - правильно я по картинке понял?

Зачем, когда есть rp2040, например. Любите же вы все некрофилией страдать.

Это , что то около средней скорости. Потенциометр выставлен в среднее положение. На максимально скорости шаговик получает импульсы с частотой около 20кГц, и крутит со скоростью 6.25 об/сек. ( микрошаг = 16. 3200 микрошагов на один оборот).

Ну, обычные nema17 с ширпотреб-драйвером пропердывать уже на 8-9КГц начинают.

Да можно любое, более менее скоростное и современное железо поставить. Но как то хотелось реализовать на старом 8 - разрядном. Сначала вообще залепил на UNO, и с костылями PCF8575 ( расширитель портов). Но там вообще слезы.
Переобулся в Mega 1280. Вроде и таймеров, и прерываний хватает. Но с кодом опять траблы.
Опять другое железо ставить, так получится, что “плохому танцору яйца мешают”.
Не верю, что на Mega нельзя реализовать нормально работающий код, под относительно простую задачу.

Драйвер DM556S, шаговик Nema23CS30C-500 (5A), купленные на Ali.
Работают нормально. 300 об/мин, шаговик выдает без напряга и без срывов ( если в коде ничего не путается). Это с выставленным на дравере микрошагом = 16. Если больше микрошагов, для плавности хода выставить, то ощутимо теряем крутящий момент. Поэтому 4-16 микрошагов - оптимальное значение.
Можно вообще поставить 2 пульса на шаг. Получим максимальную скорость. Но двигло в “космос” сорвется быстро ), на 20кГц частоте пульсов.

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

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

Согласен. И причина скорее всего в вызове тяжелой функции display.showNumberDec(OptoStepCounter); из библы TM1637TinyDisplay6.h, при каждом прерывании таймера.
Вот и ищу способ это исправить.
Или самому писать код для 7Segment, что весьма муторно для меня, и не факт , что код будет работать быстрее. Или искать другое решение для согласования аппаратной части с библиотеками и loop().

А зачем связывать вызов void SegmentDisp() с оборотами двигателя?
Всё равно глаз не заметит быстрых изменений. Достаточно 1 раз в 20мс (и даже реже)вызвать SegmentDisp() , а обновлять можно отдельную переменную, значение которой и выводить потом на 7 сегм.