4.07. Псевдопараллелизм.md


До сих пор мы сталкивались с одной фундаментальной проблемой - пока микроконтроллер выполняет одну задачу, особенно если в ней есть задержка delay(), весь остальной мир для него перестаёт существовать. Программа просто «зависает» на строчке delay(1000); и не может в это время ни считать данные с датчика, ни проверить нажатие кнопки. Это происходит потому, что микроконтроллер на плате Рудирон, как и большинство его собратьев, имеет одноядерный процессор. Он может выполнять только одну команду в один момент времени.

Но как же тогда старые компьютеры, тоже с одноядерными процессорами, умудрялись одновременно держать открытыми текстовый редактор, браузер и музыкальный плеер? Неужели они тоже были «однозадачными»? Ответ кроется в гениальном трюке под названием псевдопараллелизм или многозадачность с разделением времени.

Псевдопараллелизм

Идея псевдопараллелизма проста - вместо того чтобы выполнять одну задачу от начала и до конца, процессор очень быстро переключается между несколькими задачами. Он уделяет каждой задаче крошечный квант времени (несколько миллисекунд), а затем переходит к следующей. Для человека это переключение настолько быстрое, что создаётся полная иллюзия, будто все программы работают одновременно, или параллельно.

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

Чтобы реализовать этот подход, нам нужно отказаться от delay() и научиться управлять временем по-другому. Для этого в Рудироне есть две волшебные функции: millis() и micros().

Функции millis() и micros()

Функции millis() и micros() работают как внутренний секундомер (или, скорее, хронометр) микроконтроллера, который запускается в момент включения платы.

  • millis() возвращает количество миллисекунд (тысячных долей секунды), прошедших с момента старта программы.
  • micros() возвращает количество микросекунд (миллионных долей секунды), прошедших с момента старта.

Эти функции не останавливают программу. Они просто сообщают текущее «время». С их помощью мы можем построить логику, основанную не на ожидании, а на проверке - «Прошло ли уже достаточно времени, чтобы сделать вот это?».

Давайте посмотрим, как это работает на практике.

Реализация многозадачности с помощью millis()

Допустим, нам нужно решить две задачи «одновременно»: 1. Каждые 500 миллисекунд мигать светодиодом. 2. Каждые 2 секунды (2000 мс) отправлять сообщение в последовательный порт.

Если бы мы использовали delay(), наша программа выглядела бы ужасно и работала бы неправильно, потому что одна задержка блокировала бы другую. С millis() мы можем сделать это элегантно.

Пример (мигание светодиодом и отправка данных «одновременно»):

const int ledPin = LED_BUILTIN_1; // Используем встроенный светодиод L1

// Переменные для отслеживания времени для каждой задачи
unsigned long previousMillisLED = 0;
unsigned long previousMillisSerial = 0;

// Интервалы для каждой задачи
const long intervalLED = 500;    // Интервал для мигания светодиодом (500 мс)
const long intervalSerial = 2000; // Интервал для отправки данных (2000 мс)

// Переменная для хранения состояния светодиода
int ledState = LOW;

void setup() {
  Serial.begin(9600);
  pinMode(ledPin, OUTPUT);
}

void loop() {
  // Получаем текущее время
  unsigned long currentMillis = millis();

  // --- Задача 1. Мигание светодиодом ---
  // Проверяем, прошло ли 500 мс с последнего переключения светодиода
  if (currentMillis - previousMillisLED >= intervalLED) {
    // Сохраняем время последнего действия
    previousMillisLED = currentMillis;

    // Инвертируем состояние светодиода
    if (ledState == LOW) {
      ledState = HIGH;
    } else {
      ledState = LOW;
    }
    digitalWrite(ledPin, ledState);
  }

  // --- Задача 2. Отправка данных в Serial ---
  // Проверяем, прошло ли 2000 мс с последней отправки
  if (currentMillis - previousMillisSerial >= intervalSerial) {
    // Сохраняем время последнего действия
    previousMillisSerial = currentMillis;
    
    Serial.println("Прошло 2 секунды!");
  }

  // Сюда можно добавить и третью, и четвертую задачу!
}

Как это работает? Внутри loop() мы постоянно проверяем, не пришло ли время для выполнения одной из наших задач. Сравнивая currentMillis с previousMillis (временем последнего выполнения), мы определяем, истёк ли нужный интервал. Если да — выполняем действие и обновляем previousMillis. Цикл loop() при этом никогда не останавливается и выполняется тысячи раз в секунду, что позволяет ему очень быстро «переключаться» между проверками.

Зачем это нужно?

Отказ от delay() в пользу millis() — это, пожалуй, самый важный шаг на пути от новичка к опытному разработчику. Этот подход, называемый неблокирующим кодом, позволяет создавать сложные и отзывчивые устройства, которые могут:

  • Одновременно считывать данные с нескольких датчиков.
  • Управлять моторами, не прекращая следить за кнопками.
  • Обмениваться данными по сети и в то же время обновлять информацию на дисплее.
  • Реагировать на команды пользователя без малейшей задержки.

Теперь вы владеете техникой псевдопараллелизма и можете создавать по-настоящему многозадачные проекты на Рудироне. Если хотите закрепить эти знания, переходите к лабораторным работам. А если вы готовы начать соединять ваш Рудирон с другими умными устройствами или компьютером, то в следующем параграфе мы изучим один из самых базовых и полезных интерфейсов связи — UART.