Якщо ви добре розбираєтеся в програмуванні і не відчуваєте потреби заглиблюватися у роздуми про особливості реалізації багатозадачності - просто скачайте архів з кодом і правте його на свій розсуд. Весь наступний текст просто пояснює звідки цей код взявся і чому він є саме таким.
Насправді Arduino використовує одноядерний процесор, тому багатозадачність є ілюзією. В будь-який момент часу Arduino може опрацьовувати лише одну "думку" (задачу). Ефекту багатозадачності вдається досягти, якщо опрацьовувати кожну задачу короткими порціями, швидко перемикаючись між ними по черзі.
Є бібліотеки, які дозволяють реалізувати досить потужну систему багатозадачності, співмірну з такими, що використовуються на великих комп'ютерах. Наприклад, у системі FreeRTOS працює повноцінна багатозадачність із системою пріорітизації і витіснення (peemptive multitasking).
Чудовий опис основних ідей побудови багатозадачності можна знайти в статті Multi-tasking the Arduino (англ, частина 1, частина 2) з сайту Adafruit. Інші статті, які нам попадалися на очі - це зазвичай переклади або перекази саме Адафрутівської (напр. Багатозадачність на Arduino (рос.)).
Цікаві унікальні ідеї хіба є ще тут: Витісняюча багатозадачність для Arduino (рос, частина 1, частина 2).
На організацію "правильної" багатозадачності витрачається пам'ять і процесорний час. Та й не для кожного випадку потрібні такі складнощі.
Ми вирішили створити спрощену систему багатозадачності, з мінімальною надлишковістю. В основу поклали чотири основні принципи:
- Кооперативність. Всі задачі повинні писатися з розрахунку спільної роботи і не займати процесор надовго. Також задачі повинні добровільно і без затримок відпускати процесор іншим задачам.
- Не використовувати delay() в коді, бо на час вичікування робот повністю втрачає розум. І в гіршому випадку може навіть влетіти в стіну, чи звалитися зі сходів. Тому - забудьте про delay і не використовуйте його ніколи. Ну хіба на дуже короткі проміжки часу (10-20 мілісекунд).
- Початок або завершення дії визначається постійним перевірянням чи наступив відповідний момент за допомогою функції millis().
- Виняткові чи катастрофічні ситуації треба ловити за допомогою переривань (interrupts). Наприклад, якщо робот помічає прірву в себе під гусеницями.
Простий багатозадачний код, побудований на таких принципах може виглядати так:
loop() {
<якщо пора ввімкнути двигун> {
<ввімкнути двигун>
<запам'ятати, коли двигун треба вимкнути>
}
<якщо пора перевірити відстань до перешкоди> {
<перевірити відстань до перешкоди>
<подумати що робити з тою відстанню> {
<може поставити в чергу команду зупинки двигунів?>
<може поставити в чергу команду виконати поворот?>
}
}
<якщо пора блимнути діодом> {
<блимнути>
}
…
}
Зрозуміло, що для процесора такий текст програми буде дуже зручним. Але дуже швидко він перестане поміщатися у пам'ять програміста. Розбиратися в блоці коду, довшому ніж два екрани - річ дуже непевна. Уникайте цього будь-якими способами. Як мінімум можна виділити кожен блок в окрему функцію:
loop() {В кожній окремій функції вже можна додати логіку, яка буде займатися одною конкретною задачею. Мозку програміста стане працювати легше - код стане якіснішим, мети ви досягнете швидше. Але і це не є межею досконалості. Набагато легше працювати з кодом програми, який погрупований в автономні блоки, які повністю незалежні один від одного і взаємодіють тільки через наперед задані і обмежені канали зв'язку (інтерфейси). Такі автономні блоки можна побудувати за допомогою Об'єктно-Орієнтованого Підходу (ООП).
опрацюватиЗадачіДвигунів();
опрацюватиЗадачіСенсораВідстані();
опрацюватиЗадачіГенератораЗвуків():
опрацюватиЗадачіШтучногоІнтелекту():
}
Так наприклад можна описати всі наші задачі як нащадки класу такого вигляду:
class TaskInterface { public: virtual void processTask() = 0; };Такий запис означає, що до об'єкту такого класу можна звернутися лише через виклик метода processTask(). Присвоєння нуля цьому методу змушує всі класи-нащадки описувати цей метод по своєму.
Насправді - це все, що треба знати головній програмі, щоб по черзі викликати код кожної задачі. Головна програма може виглядати зовсім просто:
#include "Arduino.h" … // створюємо об'єкти відповідних задач // класи цих задач описуються в окремих файлах // всі вони повинні наслідуватися від класу TaskInterface static RobotMotors motors(); // двигуни static RobotVoice robotVoice(); // звукові ефекти static RobotAI robotAI(); // штучний інтелект // створюємо список задач, які будемо опрацьовувати у вічному циклі по черзі // таких задач у прикладі є 3 штуки static TaskInterface* (robotTasks[3]); void setup() { // зберігаємо посилання у список задач robotTasks[0] = &motors; robotTasks[1] = &robotVoice; robotTasks[2] = &robotAI; } void loop() { // викликаємо кожну задачу в циклі через метод processTask() for(uint8_t i=0; i<3; i++) { robotTasks[i]->processTask(); } }
Залишається дрібниця - для кожної задачі описати її поведінку в окремому класі, який буде зберігатися в окремих файлах.
Для прикладу розглянемо як міг би виглядати опис класу, відповідального за виведення звуку на динамік робота
/* * Файл RobotVoice.h */ #ifndef ROBOTVOICE_H_ #define ROBOTVOICE_H_ #include "TaskInterface.h" // це просто собі зручний спосіб кодувати тип звуку, // який ми збираємося відтворювати enum VoiceSounds {sndNoSound, sndHello, sndOK, sndQuestion, sndScared}; // опис самого класу відтворювача звуку // він наслідується від TaskInterface, // тому може бути доданий у список задач в головній програмі class RobotVoice: public TaskInterface { private: // зовнішні програми не мають доступу до цих змінних і методів // тут буде запам'ятовуватися код звуку, // який ми хочемо відтворити наступним VoiceSounds nextSound; // дві функції з бібліотеки, яку ми розглядали минулого разу - пам'ятаєте? void playTone(unsigned int toneFrequency, byte beats); void playFreqGlissando( float freqFrom, float freqTo, float duty, float duration); public: // конструктор, який ініціалізує всі поля об'єкта RobotVoice(); // деструктор - мав би як мінімум гарантовано вимикати всі звуки ~RobotVoice(); // метод, який викликається в циклі і перевіряє чи не пора зіграти якийсь звук // якщо пора - грає virtual void processTask(); // метод, який дозволяє ставити в чергу якийсь звук // саме цей метод буде викликатися з модуля штучного інтелекту, // коли йому буде потрібно пропищати страх або питання // тут головне якомога швидше записати звук у чергу // і відпустити процесор // сам звук буде відтворено під час чергового виклику метода processTask() void queueSound(VoiceSounds soundCode); }; #endif /* ROBOTVOICE_H_ */
Повний текст програми зі всіма модулями можна побачити на GitHub.
В порівнянні із прикладами наведеними тут - у фінальному коді є такі ускладнення:
- Створення об'єктів супроводжується передаванням їм номерів виходів Arduino, якими вони повинні користуватися для керування своїми модулями. Таким чином всі номери виходів задаються в одному місці (robot.ino) і їх не треба виловлювати по всій програмі.
- Посилання на об'єкти передаються один одному, якщо один об'єкт повинен використовувати іншого. Наприклад об'єкт штучного інтелекту подає команди об'єктам двигунів, звуку і т.д.
- В опис класу TaskInterface включено два типові методи, які дозволяють додавати події в чергу і легко перевіряти чи не наступив вже час діяти. Завдяки використанню ООП нема потреби копіювати цей код у всі описи класів.
Немає коментарів:
Дописати коментар