На пол пути к конечному автомату для Arduino. Однопроходные функции и фиксация событий программы с помощью флагов

image

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

Но так ли хорош этот метод для программирования микроконтроллеров, и есть ли какая-то простая и доступная альтернатива линейным алгоритмам? Я предлагаю вместе разобраться в этом вопросе.

Предисловие


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

Если вы только начинаете постигать программирование Arduino, этот материал именно для вас. А опытные программисты могут оставить свои рекомендации в комментариях, или сразу пройти к следующим не менее интересным статьям на хабре.

Эта статья предполагает продолжение. Я постараюсь с ним сильно не затягивать, но все будет зависеть от реакции читателя.

Текст статьи содержит основные идеи решения поставленных задач. Листинги и пояснения к ним ищите под спойлерами. Я спрятал их там, чтобы вы не терялись в объемном тексте. Желаю всем приятного и полезного чтения.

Хочешь программировать микроконтроллер, думай как микроконтроллер


Линейные программы пришли из такой математической дисциплины, как линейное программирование. И, простите за тавтологию, задумывалось это все для решения систем линейных уравнений. То есть программа представляет собой последовательность вычислительных операций и пересылки данных между ячейками памяти. Для построения графиков функций можно было рассчитать одно и тоже уравнение в некотором диапазоне значений с помощью циклов. А ветвления подходили для задач, подобных поиску дискриминанта.

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

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

image


Микроконтроллеру редко приходится решать задачи типа: «У Маши было два яблока, а у Вани одно…», хотя некоторые задачи бывают и не на много сложнее. Встречаются алгоритмы управления, которые можно выразить математической функцией. Это различного рода регуляторы, к примеру ПИД-регулятор. Но и они обычно являются составной частью какого-то алгоритма управления.

Пока компьютеры занимали целые машинные залы, для выполнения функций управления различными объектами строили специализированные электронные схемы. Естественно, что вопросу синтеза этих управляющих схем посвящали специальные теории и разрабатывали специальные методы. Именно тут и стоит вспомнить про теорию автоматического управления, в частности карты Карно, конъюнктивные и дизъюнктивные нормальные формы. Вот где все это было полезно.

image
Вот так выглядел блок управления двигателями в магнитофоне Электроника 003 в 80-х годах

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

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

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

Так какие же недостатки имеют линейные алгоритмы для микроконтроллеров? В следующем разделе попробуем решить несколько задач в виде привычного линейного алгоритма и проанализируем результаты.

Линейный алгоритм, все «за» и «против»


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

Пускай работа светофора определяется графиком переключений, показанным на рисунке.

image

Все эксперименты уже традиционно буду проводить в Proteus на плате Arduino Uno. Для визуализации работы устройства воспользуюсь виртуальной моделью светофора. На физическом макете можно использовать светодиоды с подходящими по номиналам резисторами.

image

Думаю, что составить алгоритм в привычном его представлении ни для кого не составит труда. Мой вариант вы можете увидеть на рисунке.

image

Текст программы напишу в Arduino IDE. Хотя программа совсем несложная, я все равно приведу ее здесь.

Алгоритмический обработчик светофора
//--------------------------------------------------
//Порты для управления светофором
#define PORT_RED     12
#define PORT_YELLOW  11
#define PORT_GREEN   10

//--------------------------------------------------
//Интервалы переключения сигналов светофора
#define TIME_RED     3000
#define TIME_YELLOW1 1000
#define TIME_GREEN   4000
#define TIME_PULS    500
#define NUM_PULS     6
#define TIME_YELLOW2 2000

//--------------------------------------------------
//настройка периферии микроконтроллера
void setup() {
  pinMode(PORT_RED, OUTPUT);
  pinMode(PORT_YELLOW, OUTPUT);
  pinMode(PORT_GREEN, OUTPUT);
}

//--------------------------------------------------
//супер цикл
void loop() {
  //красный сигнал
  digitalWrite(PORT_RED,HIGH);
  delay(TIME_RED);

  //красный + желтый сигналы
  digitalWrite(PORT_YELLOW,HIGH);
  delay(TIME_YELLOW1);

  //включен зеленый сигнал, остальные выключены
  digitalWrite(PORT_RED,LOW);
  digitalWrite(PORT_YELLOW,LOW);
  digitalWrite(PORT_GREEN,HIGH);
  delay(TIME_GREEN);

  //зеленый мигает
  for(uint8_t i = 0; i < NUM_PULS; ++i){
    digitalWrite(PORT_GREEN, !digitalRead(PORT_GREEN));
    delay(TIME_PULS);
  }

  //включен желтый, остальные выключены
  digitalWrite(PORT_GREEN,LOW);
  digitalWrite(PORT_YELLOW,HIGH);
  delay(TIME_YELLOW2);

  //выключить желтый
  digitalWrite(PORT_YELLOW,LOW);
}


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

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

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

Также хочу заметить, что при выполнении программы, написанной по данному алгоритму, наш микроконтроллер основное машинное время будет тратить на формирование интервалов времени между переключениями сигналов светофора. Это сильно ограничивает функциональные возможности нашей программы. Добавить еще какую-то параллельную задачу в наш код будет очень затруднительно.

Чтобы убедиться в истинности моих утверждений, предлагаю вам самостоятельно модифицировать программу для управления светофором на перекрестке по графику на рисунке ниже. Для этого дополним схему еще одной моделью светофора.

image

image

Сколько попыток вам понадобится, чтобы программа заработала без ошибок? Естественно, ошибки будут. Но они будут связаны не с тем, что алгоритм имеет какую-то невероятную сложность. И конечно же, ни с тем, что вы не знаете синтаксис. Ошибки будут обусловлены именно тем, что программный код сложно контролировать.

С вашего позволения я не буду рисовать алгоритм, а сразу приведу код программы. Алгоритм не будет принципиально отличаться от предыдущего ничем кроме дополнительного нагромождения блоков.

Алгоритмический обработчик светофора для перекрестка
//--------------------------------------------------
//Порты для управления светофором 1
#define PORT_RED_1     12
#define PORT_YELLOW_1  11
#define PORT_GREEN_1   10

//Порты для управления светофором 2
#define PORT_RED_2     9
#define PORT_YELLOW_2  8
#define PORT_GREEN_2   7

//--------------------------------------------------
//настройка периферии микроконтроллера
void setup() {
  pinMode(PORT_RED_1, OUTPUT);
  pinMode(PORT_YELLOW_1, OUTPUT);
  pinMode(PORT_GREEN_1, OUTPUT);

  pinMode(PORT_RED_2, OUTPUT);
  pinMode(PORT_YELLOW_2, OUTPUT);
  pinMode(PORT_GREEN_2, OUTPUT);
}

//--------------------------------------------------
//супер цикл
void loop() {
  //--------------------------------
  digitalWrite(PORT_RED_1,    HIGH);
  digitalWrite(PORT_RED_2,    HIGH);
  digitalWrite(PORT_YELLOW_2, HIGH);
  delay(1000);

  //--------------------------------
  digitalWrite(PORT_RED_2,    LOW);
  digitalWrite(PORT_YELLOW_2, LOW);
  digitalWrite(PORT_GREEN_2,  HIGH); 
  delay(2000);
  
  //--------------------------------
  for(uint8_t i = 0; i < 6; ++i){
    digitalWrite(PORT_GREEN_2, !digitalRead(PORT_GREEN_2));
    delay(500);
  } 

  //--------------------------------
  digitalWrite(PORT_GREEN_2,  LOW);
  digitalWrite(PORT_YELLOW_2, HIGH);
  delay(2000);
  //--------------------------------
  digitalWrite(PORT_YELLOW_2, LOW);
  digitalWrite(PORT_YELLOW_1, HIGH);
  digitalWrite(PORT_RED_2,    HIGH);
  delay(1000);

  //--------------------------------
  digitalWrite(PORT_RED_1,    LOW);
  digitalWrite(PORT_YELLOW_1, LOW);
  digitalWrite(PORT_GREEN_1,  HIGH);  
  delay(2000);
  
  //--------------------------------
  for(uint8_t i = 0; i < 6; ++i){
    digitalWrite(PORT_GREEN_1, !digitalRead(PORT_GREEN_1));
    delay(500);
  }

  //--------------------------------
  digitalWrite(PORT_GREEN_1,  LOW);
  digitalWrite(PORT_YELLOW_1, HIGH);
  delay(2000);

  //--------------------------------
  digitalWrite(PORT_YELLOW_1, LOW);
}


В этой версии программного кода я даже не стал пытаться дать осмысленные имена интервалам времени между переключениями сигналов светофоров. На мой взгляд это бы внесло еще больше путаницы. Такая простая задача, но так сложно было писать этот код. Не могу сказать, что это было долго, или алгоритм трудный, но контролировать последовательность переключений по графику было действительно сложно.

Давайте еще раз выделим особенности линейных алгоритмов для объемных задач:

1. Код получается очень неоднородным;
2. Необходимо постоянно контролировать весь объем кода;
3. Машинное время расходуется не рационально;
4. Практически невозможно организовать обработку параллельных процессов.

Однопроходный алгоритм


Если в вашей жизни не достает хардкора, попробуйте добавить в наш светофор на перекрестке дополнительную секцию для поворота или еще пешеходный светофор! Вот где точно оголятся выявленные проблемы.

В этом разделе предлагаю рассмотреть еще один способ решения задачи для светофора в виде однопроходного алгоритма. Сперва реализуем одиночный светофор по первому графику.

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

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

Нам могут быть интересны следующие свойства однопроходных алгоритмов:

1. Алгоритм выполняется от начала и до самого конца для каждого нового входящего элемента данных;
2. Алгоритм может функционировать только дискретно. То есть после каждого завершения работы алгоритма, его необходимо вызывать повторно.

Рассмотрим возможную схему обработки светофора в виде однопроходного алгоритма.
Для удобства понимания того, как это все будет работать, можно представить будущую программу в виде электрической схемы, а точнее в виде ее функциональной диаграммы. Как-будто мы будем проектировать светофор на цифровых микросхемах.

image

Задающий генератор формирует базовые интервалы времени, на основе которых должен формироваться график переключения сигналов светофора. Для этого счетчик ведет подсчет количества тактовых сигналов и передает накопленное значение логике формирования выходных сигналов.

Блок логики формирования выходных сигналов также синхронизируется от задающего генератора, чтобы гарантировать точность моментов переключения сигналов светофора относительно счетчика.

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

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

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

image

Полный текст программы я разместил под спойлером.

Однопроходный обработчик светофора
//--------------------------------------------------
//Порты для управления светофором
#define PORT_RED     12
#define PORT_YELLOW  11
#define PORT_GREEN   10

//--------------------------------------------------
//Интервалы переключения сигналов светофора
#define TIME_RED     3000
#define TIME_YELLOW1 1000
#define TIME_GREEN   4000
#define TIME_PULS    500
#define TIME_YELLOW2 2000

//Базовый интервал времени
#define TICK  500

//--------------------------------------------------
//настройка периферии микроконтроллера
void setup() {
  pinMode(PORT_RED, OUTPUT);
  pinMode(PORT_YELLOW, OUTPUT);
  pinMode(PORT_GREEN, OUTPUT);
}

//--------------------------------------------------
//супер цикл
void loop() {
  for(  uint16_t counter = 0; 
        counter < TIME_RED + TIME_GREEN + 6*TIME_PULS + TIME_YELLOW2; 
        counter += TICK){
    //Включен красный 
    if(counter == 0){
      digitalWrite(PORT_RED,    HIGH);
      digitalWrite(PORT_YELLOW, LOW);
      digitalWrite(PORT_GREEN,  LOW);
    }
    else
    //включен красный и желтый
    if(counter == TIME_RED - TIME_YELLOW1){
      digitalWrite(PORT_RED,    HIGH);
      digitalWrite(PORT_YELLOW, HIGH);
      digitalWrite(PORT_GREEN,  LOW);
    }
    else
    //включен зеленый
    if( (counter == TIME_RED) ||
        (counter == TIME_RED + TIME_GREEN + TIME_PULS) ||
        (counter == TIME_RED + TIME_GREEN + 3*TIME_PULS) ||
        (counter == TIME_RED + TIME_GREEN + 5*TIME_PULS) ){
      digitalWrite(PORT_RED,    LOW);
      digitalWrite(PORT_YELLOW, LOW);
      digitalWrite(PORT_GREEN,  HIGH);
    }
    else
    //все выключено
    if( (counter == TIME_RED + TIME_GREEN) ||
        (counter == TIME_RED + TIME_GREEN + 2*TIME_PULS) ||
        (counter == TIME_RED + TIME_GREEN + 4*TIME_PULS) ){
      digitalWrite(PORT_RED,    LOW);
      digitalWrite(PORT_YELLOW, LOW);
      digitalWrite(PORT_GREEN,  LOW);
    }
    else
    //включен только желтый
    if(counter == TIME_RED + TIME_GREEN + 6*TIME_PULS){
      digitalWrite(PORT_RED,    LOW);
      digitalWrite(PORT_YELLOW, HIGH);
      digitalWrite(PORT_GREEN,  LOW);
    }

  delay(TICK);
  }  
}


Давайте проанализируем полученный код. Функции задающего генератора в программе выполняет задержка времени. По ней производится синхронизация счета времени и запуска обработки светофора. Базовый интервал выбран равным 500 мс, это значение кратно абсолютно всем интервалам переключений на графике.

Счетчик реализован как переменная »counter», значение которой увеличивается в конце каждого цикла на значение базового интервала.

Далее, значение переменной »counter» передается на проверку конструкции множественного выбора »if-else». При совпадении счетчика с метками времени на графике производится формирование сигналов управления светофором.

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

Главным преимуществом полученного однопроходного кода является однократный вызов функции задержки времени. Причем задержка каждый раз производится на одно и тоже значение. Следовательно, ее достаточно просто заменить на какую-то другую полезную работу при условии, что эта работа будет выполняться ровно 500 мс.

Для примера в этой же парадигме реализуем обработчик светофора для перекрестка по второму графику. В качестве допущения примем, что время. затраченное на выполнение конструкции множественного выбора для формирования сигналов управления светофором »if-else» ничтожно мало, и им можно пренебречь на фоне базового интервала в 500 мс.

Функциональная диаграмма светофора на перекрестке может выглядеть следующим образом.

image

Обратите внимание, что управление вторым светофором я вывел в отдельный функциональный блок. Это возможно потому, что первый функциональный блок не блокирует выполнение программы, а заканчивает свою работу в каждом повторении цикла. Следовательно, второй блок мы можем также выполнить в виде однопроходного кода, и разместить его следом за уже готовым блоком первого светофора. К тому же это дает еще один неоспоримый плюс: мы можем использовать уже готовую часть программу, не нужно будет тратить дополнительное время для ее написания и проверки.

Замечу, что оба блока управления первым и вторым светофором взаимодействуют с одним и тем же счетчиком и источником тактового сигнала. Это возможно потому, что на графике переключений оба светофора связанны друг с другом единой логикой работы.

Для удобства составления алгоритма программы, конструкцию множественного выбора, которая управляет светофором, мы можем рассматривать как некий »черный ящик» (точнее как »белый ящик», т.к. его содержимое нам известно, но термин »черный ящик» звучит загадочнее).

image

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

image

Алгоритм работы второго »черного ящика» будет совершенно аналогичен первому, это становится очевидным, если продлить сигналы светофоров на графике, как показано на рисунке ниже. Также на рисунке видно, что оба светофора имеют одинаковые интервалы времени. Различие в работе первого и второго светофоров сводится к начальной фазе (показано как φ на графике).

image

Выходит, что оба »черных ящика» совершенно идентичны, их различия сводятся только к номерам портов, которые управляют светофорами. Но значение счетчика для второго »черного ящика» следует передавать со смещением.

image

Если модифицировать предыдущий код «в лоб», получается очень массивно. Я разместил этот вариант для тех, кто еще не очень владеет синтаксисом.

Однопроходный обработчик светофора для перекрестка. Вариант 1
//--------------------------------------------------
//Порты для управления светофором 1
#define PORT_RED     12
#define PORT_YELLOW  11
#define PORT_GREEN   10

//--------------------------------------------------
//Порты для управления светофором 2
#define PORT_RED_2     9
#define PORT_YELLOW_2  8
#define PORT_GREEN_2   7

//--------------------------------------------------
//Интервалы переключения сигналов светофора
#define TIME_RED     9000
#define TIME_YELLOW1 1000
#define TIME_GREEN   2000
#define TIME_PULS    500
#define TIME_YELLOW2 2000

//Базовый интервал времени
#define TICK  500

//--------------------------------------------------
//настройка периферии микроконтроллера
void setup() {
  pinMode(PORT_RED, OUTPUT);
  pinMode(PORT_YELLOW, OUTPUT);
  pinMode(PORT_GREEN, OUTPUT);

  pinMode(PORT_RED_2, OUTPUT);
  pinMode(PORT_YELLOW_2, OUTPUT);
  pinMode(PORT_GREEN_2, OUTPUT);
}

//--------------------------------------------------
//супер цикл
void loop() {
  for(  uint16_t counter = 0; 
        counter < TIME_RED + TIME_GREEN + 6*TIME_PULS + TIME_YELLOW2; 
        counter += TICK){
    
    //--------------------------------------------------
    //Светофор 1
    
    //Включен красный 
    if(counter == 0){
      digitalWrite(PORT_RED,    HIGH);
      digitalWrite(PORT_YELLOW, LOW);
      digitalWrite(PORT_GREEN,  LOW);
    }
    else
    //включен красный и желтый
    if(counter == TIME_RED - TIME_YELLOW1){
      digitalWrite(PORT_RED,    HIGH);
      digitalWrite(PORT_YELLOW, HIGH);
      digitalWrite(PORT_GREEN,  LOW);
    }
    else
    //включен зеленый
    if( (counter == TIME_RED) ||
        (counter == TIME_RED + TIME_GREEN + TIME_PULS) ||
        (counter == TIME_RED + TIME_GREEN + 3*TIME_PULS) ||
        (counter == TIME_RED + TIME_GREEN + 5*TIME_PULS) ){
      digitalWrite(PORT_RED,    LOW);
      digitalWrite(PORT_YELLOW, LOW);
      digitalWrite(PORT_GREEN,  HIGH);
    }
    else
    //все выключено
    if( (counter == TIME_RED + TIME_GREEN) ||
        (counter == TIME_RED + TIME_GREEN + 2*TIME_PULS) ||
        (counter == TIME_RED + TIME_GREEN + 4*TIME_PULS) ){
      digitalWrite(PORT_RED,    LOW);
      digitalWrite(PORT_YELLOW, LOW);
      digitalWrite(PORT_GREEN,  LOW);
    }
    else
    //включен только желтый
    if(counter == TIME_RED + TIME_GREEN + 6*TIME_PULS){
      digitalWrite(PORT_RED,    LOW);
      digitalWrite(PORT_YELLOW, HIGH);
      digitalWrite(PORT_GREEN,  LOW);
    }

  //--------------------------------------------------
    //Светофор 2
    
    //смещаем значение счетчика 
    uint16_t temp_counter = (counter + TIME_RED - TIME_YELLOW1);
    //циклический перенос счетчика
    temp_counter %= (TIME_RED + TIME_GREEN + 6*TIME_PULS + TIME_YELLOW2);

    //Включен красный 
    if(temp_counter == 0){
      digitalWrite(PORT_RED_2,    HIGH);
      digitalWrite(PORT_YELLOW_2, LOW);
      digitalWrite(PORT_GREEN_2,  LOW);
    }
    else
    //включен красный и желтый
    if(temp_counter == TIME_RED - TIME_YELLOW1){
      digitalWrite(PORT_RED_2,    HIGH);
      digitalWrite(PORT_YELLOW_2, HIGH);
      digitalWrite(PORT_GREEN_2,  LOW);
    }
    else
    //включен зеленый
    if( (temp_counter == TIME_RED) ||
        (temp_counter == TIME_RED + TIME_GREEN + TIME_PULS) ||
        (temp_counter == TIME_RED + TIME_GREEN + 3*TIME_PULS) ||
        (temp_counter == TIME_RED + TIME_GREEN + 5*TIME_PULS) ){
      digitalWrite(PORT_RED_2,    LOW);
      digitalWrite(PORT_YELLOW_2, LOW);
      digitalWrite(PORT_GREEN_2,  HIGH);
    }
    else
    //все выключено
    if( (temp_counter == TIME_RED + TIME_GREEN) ||
        (temp_counter == TIME_RED + TIME_GREEN + 2*TIME_PULS) ||
        (temp_counter == TIME_RED + TIME_GREEN + 4*TIME_PULS) ){
      digitalWrite(PORT_RED_2,    LOW);
      digitalWrite(PORT_YELLOW_2, LOW);
      digitalWrite(PORT_GREEN_2,  LOW);
    }
    else
    //включен только желтый
    if(temp_counter == TIME_RED + TIME_GREEN + 6*TIME_PULS){
      digitalWrite(PORT_RED_2,    LOW);
      digitalWrite(PORT_YELLOW_2, HIGH);
      digitalWrite(PORT_GREEN_2,  LOW);
    }

  delay(TICK);
  }  
}


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

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

image

Однопроходный обработчик светофора для перекрестка. Вариант 2
//--------------------------------------------------
//Порты для управления светофором 1
#define PORT_RED     12
#define PORT_YELLOW  11
#define PORT_GREEN   10

//--------------------------------------------------
//Порты для управления светофором 2
#define PORT_RED_2     9
#define PORT_YELLOW_2  8
#define PORT_GREEN_2   7

//--------------------------------------------------
//Интервалы переключения сигналов светофора
#define TIME_RED     9000
#define TIME_YELLOW1 1000
#define TIME_GREEN   2000
#define TIME_PULS    500
#define TIME_YELLOW2 2000

//Базовый интервал времени
#define TICK  500

//--------------------------------------------------
//структура для хранения параметров светофора
typedef struct TrafficLight{
  //порты для управления сигналами светофора
  uint8_t portRed;
  uint8_t portYellow;
  uint8_t portGreen;

  //интервалы времени 
  uint16_t timeRed;
  uint16_t timeYellow1;
  uint16_t timeGreen;
  uint16_t timePuls;
  uint16_t timeYellow2;
} TrafficLight_t;

//светофор 1
TrafficLight_t trafficLight1 = {
  .portRed    = PORT_RED,
  .portYellow = PORT_YELLOW,
  .portGreen  = PORT_GREEN,

  .timeRed      = TIME_RED,
  .timeYellow1  = TIME_YELLOW1,
  .timeGreen    = TIME_GREEN,
  .timePuls     = TIME_PULS,
  .timeYellow2  = TIME_YELLOW2
};

//светофор 2
TrafficLight_t trafficLight2 = {
  .portRed    = PORT_RED_2,
  .portYellow = PORT_YELLOW_2,
  .portGreen  = PORT_GREEN_2,

  .timeRed      = TIME_RED,
  .timeYellow1  = TIME_YELLOW1,
  .timeGreen    = TIME_GREEN,
  .timePuls     = TIME_PULS,
  .timeYellow2  = TIME_YELLOW2
};

//--------------------------------------------------
//обработчик светофора
void trafficLight_action(TrafficLight_t * tf, uint16_t counter);

//--------------------------------------------------
//настройка периферии микроконтроллера
void setup() {
  pinMode(trafficLight1.portRed, OUTPUT);
  pinMode(trafficLight1.portGreen, OUTPUT);
  pinMode(trafficLight1.portYellow, OUTPUT);

  pinMode(trafficLight2.portRed, OUTPUT);
  pinMode(trafficLight2.portGreen, OUTPUT);
  pinMode(trafficLight2.portYellow, OUTPUT);
}

//--------------------------------------------------
//супер цикл
void loop() {
  for(  uint16_t counter = 0; 
        counter < TIME_RED + TIME_GREEN + 6*TIME_PULS + TIME_YELLOW2; 
        counter += TICK){
    
    //--------------------------------------------------
    //Светофор 1
    trafficLight_action(&trafficLight1, counter);

  //--------------------------------------------------
    //Светофор 2                        смещаем значение счетчика 
    trafficLight_action(&trafficLight2, counter + TIME_RED - TIME_YELLOW1);

  delay(TICK);
  }  
}

//--------------------------------------------------
//обработчик светофора
void trafficLight_action(TrafficLight_t * tf, uint16_t counter){
  //циклический перенос счетчика
  counter %= (tf->timeRed + tf->timeGreen + 6*tf->timePuls + tf->timeYellow2);

  //Включен красный 
  if(counter == 0){
    digitalWrite(tf->portRed,    HIGH);
    digitalWrite(tf->portYellow, LOW);
    digitalWrite(tf->portGreen,  LOW);
  }
  else
  //включен красный и желтый
  if(counter == tf->timeRed - tf->timeYellow1){
    digitalWrite(tf->portRed,    HIGH);
    digitalWrite(tf->portYellow, HIGH);
    digitalWrite(tf->portGreen,  LOW);
  }
  else
  //включен зеленый
  if( (counter == tf->timeRed) ||
      (counter == tf->timeRed + tf->timeGreen + tf->timePuls) ||
      (counter == tf->timeRed + tf->timeGreen + 3*tf->timePuls) ||
      (counter == tf->timeRed + tf->timeGreen + 5*tf->timePuls) ){
    digitalWrite(tf->portRed,    LOW);
    digitalWrite(tf->portYellow, LOW);
    digitalWrite(tf->portGreen,  HIGH);
  }
  else
  //все выключено
  if( (counter == tf->timeRed + tf->timeGreen) ||
      (counter == tf->timeRed + tf->timeGreen + 2*tf->timePuls) ||
      (counter == tf->timeRed + tf->timeGreen + 4*tf->timePuls) ){
    digitalWrite(tf->portRed,    LOW);
    digitalWrite(tf->portYellow, LOW);
    digitalWrite(tf->portGreen,  LOW);
  }
  else
  //включен только желтый
  if(counter == tf->timeRed + tf->timeGreen + 6*tf->timePuls){
    digitalWrite(tf->portRed,    LOW);
    digitalWrite(tf->portYellow, HIGH);
    digitalWrite(tf->portGreen,  LOW);
  }
}
Если вы еще не разобрались как следует со структурами в языке С, под спойлером для вас я оставил разбор этой программы.
Язык С относится к неструктурированным языкам. Для примера сравните его с Паскалем, где четко определен порядок и назначение блоков кода. Эта особенность позволяет программисту формировать свою структуру программы, что делает код более выразительным.

Моя программа начинается с блока макроопределений. Они фактически формируют интерфейс программиста, который позволяет быстро менять основные параметры программы. Команды разбиты на блоки, которые определяют имена портов для управления сигналами светофора и интервалы времени переключения сигналов светофора. Интервалы времени будут общие для обоих светофоров.

//--------------------------------------------------
//Порты для управления светофором 1
#define PORT_RED     12
#define PORT_YELLOW  11
#define PORT_GREEN   10

//--------------------------------------------------
//Порты для управления светофором 2
#define PORT_RED_2     9
#define PORT_YELLOW_2  8
#define PORT_GREEN_2   7

//--------------------------------------------------
//Интервалы переключения сигналов светофора
#define TIME_RED     9000
#define TIME_YELLOW1 1000
#define TIME_GREEN   2000
#define TIME_PULS    500
#define TIME_YELLOW2 2000

//Базовый интервал времени
#define TICK  500

Имя »TICK» для базового интервала времени выбрано как дань традиции. Такое имя часто использовалось в различных операционных системах для определения времени, выделяемого для обработки задач.

Для хранения параметров светофора в программе объявлена структура »TrafficLight». Для удобства программирования и получения более коротких записей эта структура переопределена как тип данных »TrafficLight_t». Благодаря этому в дальнейшем будет меньше мороки при передаче экземпляров структуры в качестве входных параметров функций.

//--------------------------------------------------
//структура для хранения параметров светофора
typedef struct TrafficLight{
  //порты для управления сигналами светофора
  uint8_t portRed;
  uint8_t portYellow;
  uint8_t portGreen;

  //интервалы времени 
  uint16_t timeRed;
  uint16_t timeYellow1;
  uint16_t timeGreen;
  uint16_t timePuls;
  uint16_t timeYellow2;
} TrafficLight_t;

В конструкции:»typedef struct TrafficLight{…} TrafficLight_t; » — имя структуры »TrafficLight» можно было бы опустить. Объявление выглядело бы следующим образом:»typedef struct {…} TrafficLight_t; ». Эта запись сообщает компилятора о намерениях программиста размещать в памяти какие-то данные в формате, который указан между фигурными скобками. Выделение памяти при этом не происходит.

Далее в программе определяются экземпляры структуры »TrafficLight_t». Имена »trafficLight1» и »trafficLight2» могут рассматриваться как переменные, т.е. для них уже будет выделено место в памяти микроконтроллера.

//светофор 1
TrafficLight_t trafficLight1 = {
  .portRed    = PORT_RED,
  .portYellow = PORT_YELLOW,
  .portGreen  = PORT_GREEN,

  .timeRed      = TIME_RED,
  .timeYellow1  = TIME_YELLOW1,
  .timeGreen    = TIME_GREEN,
  .timePuls     = TIME_PULS,
  .timeYellow2  = TIME_YELLOW2
};

//светофор 2
TrafficLight_t trafficLight2 = {
  .portRed    = PORT_RED_2,
  .portYellow = PORT_YELLOW_2,
  .portGreen  = PORT_GREEN_2,

  .timeRed      = TIME_RED,
  .timeYellow1  = TIME_YELLOW1,
  .timeGreen    = TIME_GREEN,
  .timePuls     = TIME_PULS,
  .timeYellow2  = TIME_YELLOW2
};

Стоит обратить внимание на форму инициализации полей структуры, которую я использовал. При помощи точки (оператор обращения к полю структуры) я явно указываю имена полей, в которые записываются данные. Эта форма более наглядна и снижает вероятность перепутать, какие данные для какого поля предназначены. Но, к сожалению, Arduino IDE не поддерживает эту фишку целиком. Порядок инициализаторов должен совпадать с тем, как они определены при объявлении шаблона структуры. Не допускается пропускать поля при инициализации. Спасибо и на этом, т.к. некоторые компиляторы для микроконтроллеров и такого не поддерживают.

Далее в программе производится определение функции »trafficLight_action()». Мы заявляем компилятору о своих намерениях использовать это имя как функцию, и определяем формат ее параметров. Само объявление функции будет в конце программы. Это позволяет расширить область имени функции и не загромождать код перед описанием основной логики программы.

//--------------------------------------------------
//обработчик светофора
void trafficLight_action(TrafficLight_t * tf, uint16_t counter);

Функция принимает два параметра. Первый  — это указатель на структуру, которая хранит параметры светофора. Благодаря этому возможно обрабатывать два светофора с разными портами и интервалами времени с помощью одной и той же функции. Второй параметр — это значение времени относительно начала на графике работы светофора. Это позволит обрабатывать два светофора со смещением начальной фазы относительно друг друга.

Обратите внимание, как в функции »setup()» выполнена настройка портов микроконтроллера. Вместо прямого указания номера вывода на плате Arduino UNO, я передаю поле структуры »trafficLight1», которое содержит соответствующее значение. Это не очень хорошо влияет на размер генерируемого кода, но положительно сказывается на универсальности текста программы. Здесь более уместно было бы использовать макросы из начала программы. Но я написал так для демонстрации синтаксиса.

//--------------------------------------------------
//настройка периферии микроконтроллера
void setup() {
  pinMode(trafficLight1.portRed, OUTPUT);
  pinMode(trafficLight1.portGreen, OUTPUT);
  pinMode(trafficLight1.portYellow, OUTPUT);

  pinMode(trafficLight2.portRed, OUTPUT);
  pinMode(trafficLight2.portGreen, OUTPUT);
  pinMode(trafficLight2.portYellow, OUTPUT);
}

В начале функции »loop()» мы снова видим цикл »for», который выполняет счет времени работы светофора. Для обеспечения функционирования программного интерфейса, о котором мы говорили в начале описания листинга, предел счетчика ограничен записью »TIME_RED + TIME_GREEN + 6*TIME_PULS + TIME_YELLOW2», ее вычисление компилятор выполнит на стадии сборки программы, и это не повлияет на производительность. Можно было бы заранее определить подходящий макрос, который заменил бы такую длинную запись и сделал бы ее более осмысленной, но это значение используется в программе однократно, и я решил оставить так.
  for(  uint16_t counter = 0; 
        counter < TIME_RED + TIME_GREEN + 6*TIME_PULS + TIME_YELLOW2; 
        counter += TICK){

Хотя с точки зрения дальнейшего апгрейда функциональности, лучше бы сделать отдельную переменную, в которую суммировались бы поля структуры с соответствующими параметрами светофора. К примеру, можно было бы изменять интервалы времени через USART. Но, парой, сложно предусмотреть все сразу.

Замечу, что увеличение счетчика так, как это сделано в программе »counter += TICK», не самое оптимальное решение для микроконтроллера. Счетчик объявлен как «uint16_t», и занимает 2 байта в памяти микроконтроллера. Оперативная память у нашей платы 8-ми битная. Обработка счетчика будет занимать значительно больше тактов, чем если бы он был 8-ми битным. Время на графике можно было бы считать в количестве тактов, а не в мили секундах, тогда бы и 8-ми битной переменной хватило. Но читаемость кода получилась бы менее наглядной. Задача, которую мы решаем, занимает далеко не весь вычислительный потенциал Arduino UNO, поэтому я и не стал заморачиваться.

Посмотрим, как выполнена обработка светофоров. Вызов функции »trafficLight_action()» производится два раза подряд. Но в каждом вызове она получает параметры разных светофоров. С помощью оператора получения адреса »&» мы передаем ссылку на структуру. При этом в стек локальных переменных функции попадает только два байта адреса. Если бы мы передавали не ссылку на структуру, а структуру целиком, то при каждом вызове функции в стек переписывались бы все ее поля. Это не лучшим образом отразилось бы на производительности программы, да и сожрало бы лишнюю оперативку.

//--------------------------------------------------
    //Светофор 1
    trafficLight_action(&trafficLight1, counter);

    //--------------------------------------------------
    //Светофор 2                        смещаем значение счетчика 
    trafficLight_action(&trafficLight2, counter + TIME_RED - TIME_YELLOW1);

Обращение к структуре через указатель таит за собой некоторую опасность. Поля структуры могут быть безвозвратно модифицированы внутри функции. В некоторых случаях это будет наоборот полезно. А в некоторых недопустимо.

При обработке второго светофора значение счетчика смещается на время, пока на первом светофоре горит красный:»counter + TIME_RED — TIME_YELLOW1». Результат такого сложения может вывести счетчик за пределы графика переключений светофора. Защита от таких ситуаций должна быть предусмотрена в самой функции »trafficLight_action()». Это хороший прием особенно для тех случаев, когда значение какого-то параметра программа получает из вне, и вы заранее не можете быть уверены, что значение будет введено в корректном диапазоне.

//--------------------------------------------------
//обработчик светофора
void trafficLight_action(TrafficLight_t * tf, uint16_t counter){
  //циклический перенос счетчика
  counter %= (tf->timeRed + tf->timeGreen + 6*tf->timePuls + tf->timeYellow2);

Обращаю внимание, что символ »%» — это не получение процентов, а получение остатка от целочисленного деления (5%2 == 1, два помещается в пятерку целиком два раза и единичка остается в остатке). А запись »%=» это сокращение с операцией присвоения результата (а %= b эквивалентно a = a % b).

Остальная часть функции обработки светофора последовательно проверяет значение счетчика. Если он совпадает с 

© Habrahabr.ru