Мастер-класс по многопоточности Arduino с протопотоками: пошаговое руководство
Компоненты и материалы
| Компонент | Количество |
|---|---|
| Arduino UNO | 1 |
| Sunfounder Blue 16x2 LCD-дисплей | 1 |
| Платы для прототипирования (breadboard) и провода | 1 |
| Поворотный потенциометр (общего назначения, ~1 кОм) | 1 |
О проекте
Это руководство демонстрирует, как заставить однокерновой Arduino выполнять три задачи одновременно:
- Пульсация подсветки LCD с постоянной частотой без прерываний
- Инкремент счетчика каждую секунду и вывод на дисплей
- Поочередный вывод нескольких текстовых сообщений каждые несколько секунд
Что такое протопотоки (protothreading)?
Протопотоки — это способ реализации псевдо-многозадачности на Arduino. Поскольку Arduino — однокерновой микроконтроллер с процедурным кодом, настоящее многопоточность невозможна. Протопотоки позволяют имитировать одновременное выполнение нескольких задач на одном ядре.
«Настоящая» многопоточность vs протопотоки
Для понимания разницы рассмотрим исторический пример: технология Hyperthreading от Intel. Она заставляет одно физическое ядро процессора вести себя как два логических, распределяя такты процессора между потоками. Arduino Uno (ATmega328P) работает на 16 МГц — 16 млн тактов в секунду. Но у нас только одно ядро, поэтому классическая многопоточность недоступна.
Протопотоки работают иначе: вместо разделения тактов процессора мы разбиваем работу на уровне циклов loop(). Основной цикл выполняет самую частую задачу (менеджер), а периодически проверяет, не пора ли выполнить другие задачи. Если задача «блокирующая» (долгая операция без прерываний, например чтение SD-карты), она всё равно заблокирует остальные протопотоки. Но для быстрых операций (изменение переменных, вывод на дисплей) метод работает отлично.
Принцип работы
- Определяем задачи и ранжируем их от наименее блокирующих к наиболее блокирующим.
- Самая частая и быстрая задача становится основным потоком (менеджером). В примере — пульсация подсветки.
- В основном цикле (и во всех вложенных циклах) вызываем
.check()для остальных задач. - Задачи срабатывают по заданным интервалам через библиотеку
TimedAction.
В коде ниже основной поток — плавное затухание/растворение подсветки (циклы for с analogWrite). Внутри этих циклов вызываются numberThread.check() и textThread.check(), чтобы счетчик и текст обновлялись вовремя.
Код (multithread.ino v1.1)
Использует библиотеку TimedAction.h для трёх независимых таймеров. Важно: в новых версиях Arduino IDE в файле библиотеки TimedAction.h нужно заменить WProgram.h на Arduino.h.
/* Arduino Protothreading Example v1.1 by Drew Alden (@ReanimationXP)
* Обновление v1.1 - 18.08.2017: мелкие правки для Arduino 1.6.6+
* (функции объявлены перед использованием, убран foreach).
* Примечание: TimedAction устарел. Читайте примечания об ошибках WProgram.h / Arduino.h.
*/
// КОМПОНЕНТЫ
// Код написан для синего LCD из набора Sunfounder Arduino Starter Kit (доступен на Amazon).
// СТОРОННИЕ БИБЛИОТЕКИ (устанавливаются вручную)
// TimedAction — действия по отдельным таймерам
// https://playground.arduino.cc/Code/TimedAction
// https://wiring.uniandes.edu.co/source/trunk/wiring/firmware/libraries/TimedAction
#include <TimedAction.h>
// ПОСЛЕ УСТАНОВКИ БИБЛИОТЕКИ: отредактируйте TimedAction.h,
// замените WProgram.h на Arduino.h
// СТАНДАРТНЫЕ БИБЛИОТЕКИ
#include <LiquidCrystal.h>
/* LiquidCrystal Library - Hello World
* Пример подключения 16x2 LCD:
* LCD RS -> pin 12
* LCD EN -> pin 11
* LCD D4 -> pin 5
* LCD D5 -> pin 4
* LCD D6 -> pin 3
* LCD D7 -> pin 2
* LCD R/W -> GND
* LCD VSS -> GND
* LCD VDD -> 5V
* 10кОм потенциометр: концы -> 5V и GND, средний -> LCD VO (pin 3)
* Подсветка: LCD K -> GND, LCD A -> 220Ом резистор -> pin 9 (PWM)
*/
// ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ
int backlightPin = 9; // пин подсветки (PWM)
int timerCounter = 0; // счетчик (переполнится со временем)
int stringNo = 0; // индекс текущей строки
// Макс 16 символов
char* stringArray[] = {
"Check it out... ",
"I have 3 threads",
"going at once...",
"Cool, huh?! :D "
};
// ИНИЦИАЛИЗАЦИЯ
// Лучше делать в setup(), но здесь глобально для простоты
LiquidCrystal lcd(12, 11, 5, 4, 3, 2);
// ФУНКЦИИ ЗАДАЧ
// Задача 1: вывод инкрементируемого числа на вторую строку LCD
void incrementNumber() {
lcd.setCursor(0, 1); // столбец 0, строка 1 (вторая строка)
timerCounter = timerCounter + 1;
lcd.print(timerCounter);
}
// Задача 2: ротация текстовых строк на первой строке LCD
void changeText() {
lcd.setCursor(0, 0);
lcd.print(stringArray[stringNo]);
// Проверка границ массива
if (stringNo >= sizeof(stringArray) / sizeof(char *)) {
stringNo = 0;
changeText(); // рекурсивный вызов для первого элемента
} else {
stringNo = stringNo + 1;
}
}
// ТАЙМЕРЫ (создаются ПОСЛЕ объявления функций!)
TimedAction numberThread = TimedAction(700, incrementNumber); // каждые 700 мс
TimedAction textThread = TimedAction(3000, changeText); // каждые 3000 мс
// Третья задача — основной loop() (пульсация подсветки).
// Она выполняется чаще всего, остальные «прерывают» её через .check()
void setup() {
lcd.begin(16, 2);
changeText(); // начальную строку сразу
}
void loop() {
// Проверка фоновых задач
numberThread.check();
textThread.check();
// ЗАДАЧА 3: пульсация подсветки (вход/выход)
digitalWrite(13, HIGH); // индикатор на плате (опционально)
// Нарастание яркости 0 → 255 шагом 10
for (int fadeValue = 0; fadeValue <= 255; fadeValue += 10) {
numberThread.check(); // критично: проверка внутри цикла!
textThread.check();
analogWrite(backlightPin, fadeValue);
delay(20); // короткая задержка, не блокируем надолго
}
// Затухание 255 → 0
digitalWrite(13, LOW);
for (int fadeValue = 255; fadeValue >= 0; fadeValue -= 10) {
numberThread.check();
textThread.check();
analogWrite(backlightPin, fadeValue);
delay(20);
}
/* Закомментировано: пример прокрутки текста для будущего
lcd.setCursor(15,0);
for (int positionCounter1 = 0; positionCounter1 < 26; positionCounter1++) {
lcd.scrollDisplayLeft();
lcd.print(array1[positionCounter1]);
delay(tim);
}
lcd.clear();
lcd.setCursor(15,1);
for (int positionCounter = 0; positionCounter < 26; positionCounter++) {
lcd.scrollDisplayLeft();
lcd.print(array2[positionCounter]);
delay(tim);
}
lcd.clear();
*/
}
Заключение
Протопотоки — простой и эффективный паттерн для одновременного выполнения нескольких периодических задач на Arduino. Главное правило: в любом длительном цикле вызывайте .check() для фоновых задач, иначе они сработают с задержкой. Для сложных проектов с блокирующими операциями рассмотрите FreeRTOS или другие RTOS.
Производственный процесс
- Измерение качества воздуха с OpenSensors: руководство по Arduino и Python
- Создание надежного RFID-замка на Arduino — пошаговое руководство
- Создание универсального ИК-пульта на Arduino: пошаговое руководство
- Цифровой стадиометр на Arduino: отслеживание роста лазерным датчиком VL53L0X
- Использование термистора: пошаговое руководство для Arduino
- Генерация музыкальных тонов на Arduino: полный спектр 88 клавиш пианино
- Подключение Arduino к NMEA-0183: пошаговое руководство
- Проект датчика отпечатков пальцев на Arduino Nano – Пошаговое руководство
- Мастерство Modbus на Arduino: пошаговое руководство
- Урок Arduino для начинающих 01: Начало работы с первым проектом